diff --git a/lib/data/models/design_model.dart b/lib/data/models/design_model.dart new file mode 100644 index 000000000..4aa9a9cdf --- /dev/null +++ b/lib/data/models/design_model.dart @@ -0,0 +1,117 @@ +import 'package:built_value/built_value.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; + +part 'design_model.g.dart'; + +abstract class DesignListResponse + implements Built { + factory DesignListResponse([void updates(DesignListResponseBuilder b)]) = + _$DesignListResponse; + + DesignListResponse._(); + + BuiltList get data; + + static Serializer get serializer => + _$designListResponseSerializer; +} + +abstract class DesignItemResponse + implements Built { + factory DesignItemResponse([void updates(DesignItemResponseBuilder b)]) = + _$DesignItemResponse; + + DesignItemResponse._(); + + DesignEntity get data; + + static Serializer get serializer => + _$designItemResponseSerializer; +} + +class DesignFields { + static const String name = 'name'; +} + +abstract class DesignEntity extends Object + with BaseEntity + implements Built { + + factory DesignEntity() { + return _$DesignEntity._( + id: BaseEntity.nextId, + isChanged: false, + // STARTER: constructor - do not remove comment + ); + } + + DesignEntity._(); + + String get displayName { + // STARTER: display name - do not remove comment + } + + int compareTo(DesignEntity design, String sortField, bool sortAscending) { + int response = 0; + DesignEntity designA = sortAscending ? this : design; + DesignEntity designB = sortAscending ? design : this; + + switch (sortField) { + // STARTER: sort switch - do not remove comment + } + + if (response == 0) { + // STARTER: sort default - do not remove comment + } else { + return response; + } + } + + bool matchesSearch(String filter) { + if (filter == null || filter.isEmpty) { + return true; + } + + filter = filter.toLowerCase(); + + // STARTER: filter - do not remove comment + + return false; + } + + @override + bool matchesFilter(String filter) { + if (filter == null || filter.isEmpty) { + return true; + } + + filter = filter.toLowerCase(); + + return false; + } + + @override + String matchesFilterValue(String filter) { + if (filter == null || filter.isEmpty) { + return null; + } + + filter = filter.toLowerCase(); + + return null; + } + + @override + String get listDisplayName => null; + + @override + double get listDisplayAmount => null; + + @override + FormatNumberType get listDisplayAmountType => null; + + static Serializer get serializer => _$designEntitySerializer; +} diff --git a/lib/data/models/design_model.g.dart b/lib/data/models/design_model.g.dart new file mode 100644 index 000000000..31b4538c6 --- /dev/null +++ b/lib/data/models/design_model.g.dart @@ -0,0 +1,598 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'design_model.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$designListResponseSerializer = + new _$DesignListResponseSerializer(); +Serializer _$designItemResponseSerializer = + new _$DesignItemResponseSerializer(); +Serializer _$designEntitySerializer = + new _$DesignEntitySerializer(); + +class _$DesignListResponseSerializer + implements StructuredSerializer { + @override + final Iterable types = const [DesignListResponse, _$DesignListResponse]; + @override + final String wireName = 'DesignListResponse'; + + @override + Iterable serialize(Serializers serializers, DesignListResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'data', + serializers.serialize(object.data, + specifiedType: + const FullType(BuiltList, const [const FullType(DesignEntity)])), + ]; + + return result; + } + + @override + DesignListResponse deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new DesignListResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'data': + result.data.replace(serializers.deserialize(value, + specifiedType: const FullType( + BuiltList, const [const FullType(DesignEntity)])) + as BuiltList); + break; + } + } + + return result.build(); + } +} + +class _$DesignItemResponseSerializer + implements StructuredSerializer { + @override + final Iterable types = const [DesignItemResponse, _$DesignItemResponse]; + @override + final String wireName = 'DesignItemResponse'; + + @override + Iterable serialize(Serializers serializers, DesignItemResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'data', + serializers.serialize(object.data, + specifiedType: const FullType(DesignEntity)), + ]; + + return result; + } + + @override + DesignItemResponse deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new DesignItemResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'data': + result.data.replace(serializers.deserialize(value, + specifiedType: const FullType(DesignEntity)) as DesignEntity); + break; + } + } + + return result.build(); + } +} + +class _$DesignEntitySerializer implements StructuredSerializer { + @override + final Iterable types = const [DesignEntity, _$DesignEntity]; + @override + final String wireName = 'DesignEntity'; + + @override + Iterable serialize(Serializers serializers, DesignEntity object, + {FullType specifiedType = FullType.unspecified}) { + final result = []; + if (object.isChanged != null) { + result + ..add('isChanged') + ..add(serializers.serialize(object.isChanged, + specifiedType: const FullType(bool))); + } + if (object.createdAt != null) { + result + ..add('created_at') + ..add(serializers.serialize(object.createdAt, + specifiedType: const FullType(int))); + } + if (object.updatedAt != null) { + result + ..add('updated_at') + ..add(serializers.serialize(object.updatedAt, + specifiedType: const FullType(int))); + } + if (object.archivedAt != null) { + result + ..add('archived_at') + ..add(serializers.serialize(object.archivedAt, + specifiedType: const FullType(int))); + } + if (object.isDeleted != null) { + result + ..add('is_deleted') + ..add(serializers.serialize(object.isDeleted, + specifiedType: const FullType(bool))); + } + if (object.createdUserId != null) { + result + ..add('user_id') + ..add(serializers.serialize(object.createdUserId, + specifiedType: const FullType(String))); + } + if (object.assignedUserId != null) { + result + ..add('assigned_user_id') + ..add(serializers.serialize(object.assignedUserId, + specifiedType: const FullType(String))); + } + if (object.subEntityType != null) { + result + ..add('entity_type') + ..add(serializers.serialize(object.subEntityType, + specifiedType: const FullType(EntityType))); + } + if (object.id != null) { + result + ..add('id') + ..add(serializers.serialize(object.id, + specifiedType: const FullType(String))); + } + return result; + } + + @override + DesignEntity deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new DesignEntityBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'isChanged': + result.isChanged = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'created_at': + result.createdAt = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'updated_at': + result.updatedAt = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'archived_at': + result.archivedAt = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'is_deleted': + result.isDeleted = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'user_id': + result.createdUserId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'assigned_user_id': + result.assignedUserId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'entity_type': + result.subEntityType = serializers.deserialize(value, + specifiedType: const FullType(EntityType)) as EntityType; + break; + case 'id': + result.id = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + } + } + + return result.build(); + } +} + +class _$DesignListResponse extends DesignListResponse { + @override + final BuiltList data; + + factory _$DesignListResponse( + [void Function(DesignListResponseBuilder) updates]) => + (new DesignListResponseBuilder()..update(updates)).build(); + + _$DesignListResponse._({this.data}) : super._() { + if (data == null) { + throw new BuiltValueNullFieldError('DesignListResponse', 'data'); + } + } + + @override + DesignListResponse rebuild( + void Function(DesignListResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DesignListResponseBuilder toBuilder() => + new DesignListResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DesignListResponse && data == other.data; + } + + @override + int get hashCode { + return $jf($jc(0, data.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('DesignListResponse') + ..add('data', data)) + .toString(); + } +} + +class DesignListResponseBuilder + implements Builder { + _$DesignListResponse _$v; + + ListBuilder _data; + ListBuilder get data => + _$this._data ??= new ListBuilder(); + set data(ListBuilder data) => _$this._data = data; + + DesignListResponseBuilder(); + + DesignListResponseBuilder get _$this { + if (_$v != null) { + _data = _$v.data?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(DesignListResponse other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$DesignListResponse; + } + + @override + void update(void Function(DesignListResponseBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$DesignListResponse build() { + _$DesignListResponse _$result; + try { + _$result = _$v ?? new _$DesignListResponse._(data: data.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'data'; + data.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'DesignListResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$DesignItemResponse extends DesignItemResponse { + @override + final DesignEntity data; + + factory _$DesignItemResponse( + [void Function(DesignItemResponseBuilder) updates]) => + (new DesignItemResponseBuilder()..update(updates)).build(); + + _$DesignItemResponse._({this.data}) : super._() { + if (data == null) { + throw new BuiltValueNullFieldError('DesignItemResponse', 'data'); + } + } + + @override + DesignItemResponse rebuild( + void Function(DesignItemResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DesignItemResponseBuilder toBuilder() => + new DesignItemResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DesignItemResponse && data == other.data; + } + + @override + int get hashCode { + return $jf($jc(0, data.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('DesignItemResponse') + ..add('data', data)) + .toString(); + } +} + +class DesignItemResponseBuilder + implements Builder { + _$DesignItemResponse _$v; + + DesignEntityBuilder _data; + DesignEntityBuilder get data => _$this._data ??= new DesignEntityBuilder(); + set data(DesignEntityBuilder data) => _$this._data = data; + + DesignItemResponseBuilder(); + + DesignItemResponseBuilder get _$this { + if (_$v != null) { + _data = _$v.data?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(DesignItemResponse other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$DesignItemResponse; + } + + @override + void update(void Function(DesignItemResponseBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$DesignItemResponse build() { + _$DesignItemResponse _$result; + try { + _$result = _$v ?? new _$DesignItemResponse._(data: data.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'data'; + data.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'DesignItemResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$DesignEntity extends DesignEntity { + @override + final bool isChanged; + @override + final int createdAt; + @override + final int updatedAt; + @override + final int archivedAt; + @override + final bool isDeleted; + @override + final String createdUserId; + @override + final String assignedUserId; + @override + final EntityType subEntityType; + @override + final String id; + + factory _$DesignEntity([void Function(DesignEntityBuilder) updates]) => + (new DesignEntityBuilder()..update(updates)).build(); + + _$DesignEntity._( + {this.isChanged, + this.createdAt, + this.updatedAt, + this.archivedAt, + this.isDeleted, + this.createdUserId, + this.assignedUserId, + this.subEntityType, + this.id}) + : super._(); + + @override + DesignEntity rebuild(void Function(DesignEntityBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DesignEntityBuilder toBuilder() => new DesignEntityBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DesignEntity && + isChanged == other.isChanged && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + archivedAt == other.archivedAt && + isDeleted == other.isDeleted && + createdUserId == other.createdUserId && + assignedUserId == other.assignedUserId && + subEntityType == other.subEntityType && + id == other.id; + } + + @override + int get hashCode { + return $jf($jc( + $jc( + $jc( + $jc( + $jc( + $jc( + $jc( + $jc($jc(0, isChanged.hashCode), + createdAt.hashCode), + updatedAt.hashCode), + archivedAt.hashCode), + isDeleted.hashCode), + createdUserId.hashCode), + assignedUserId.hashCode), + subEntityType.hashCode), + id.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('DesignEntity') + ..add('isChanged', isChanged) + ..add('createdAt', createdAt) + ..add('updatedAt', updatedAt) + ..add('archivedAt', archivedAt) + ..add('isDeleted', isDeleted) + ..add('createdUserId', createdUserId) + ..add('assignedUserId', assignedUserId) + ..add('subEntityType', subEntityType) + ..add('id', id)) + .toString(); + } +} + +class DesignEntityBuilder + implements Builder { + _$DesignEntity _$v; + + bool _isChanged; + bool get isChanged => _$this._isChanged; + set isChanged(bool isChanged) => _$this._isChanged = isChanged; + + int _createdAt; + int get createdAt => _$this._createdAt; + set createdAt(int createdAt) => _$this._createdAt = createdAt; + + int _updatedAt; + int get updatedAt => _$this._updatedAt; + set updatedAt(int updatedAt) => _$this._updatedAt = updatedAt; + + int _archivedAt; + int get archivedAt => _$this._archivedAt; + set archivedAt(int archivedAt) => _$this._archivedAt = archivedAt; + + bool _isDeleted; + bool get isDeleted => _$this._isDeleted; + set isDeleted(bool isDeleted) => _$this._isDeleted = isDeleted; + + String _createdUserId; + String get createdUserId => _$this._createdUserId; + set createdUserId(String createdUserId) => + _$this._createdUserId = createdUserId; + + String _assignedUserId; + String get assignedUserId => _$this._assignedUserId; + set assignedUserId(String assignedUserId) => + _$this._assignedUserId = assignedUserId; + + EntityType _subEntityType; + EntityType get subEntityType => _$this._subEntityType; + set subEntityType(EntityType subEntityType) => + _$this._subEntityType = subEntityType; + + String _id; + String get id => _$this._id; + set id(String id) => _$this._id = id; + + DesignEntityBuilder(); + + DesignEntityBuilder get _$this { + if (_$v != null) { + _isChanged = _$v.isChanged; + _createdAt = _$v.createdAt; + _updatedAt = _$v.updatedAt; + _archivedAt = _$v.archivedAt; + _isDeleted = _$v.isDeleted; + _createdUserId = _$v.createdUserId; + _assignedUserId = _$v.assignedUserId; + _subEntityType = _$v.subEntityType; + _id = _$v.id; + _$v = null; + } + return this; + } + + @override + void replace(DesignEntity other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$DesignEntity; + } + + @override + void update(void Function(DesignEntityBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$DesignEntity build() { + final _$result = _$v ?? + new _$DesignEntity._( + isChanged: isChanged, + createdAt: createdAt, + updatedAt: updatedAt, + archivedAt: archivedAt, + isDeleted: isDeleted, + createdUserId: createdUserId, + assignedUserId: assignedUserId, + subEntityType: subEntityType, + id: id); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new diff --git a/lib/data/models/entities.dart b/lib/data/models/entities.dart index e732003ca..603791a11 100644 --- a/lib/data/models/entities.dart +++ b/lib/data/models/entities.dart @@ -33,11 +33,12 @@ class EntityType extends EnumClass { static const EntityType payment = _$payment; static const EntityType group = _$group; static const EntityType user = _$user; - static const EntityType company = _$company; static const EntityType gateway = _$gateway; static const EntityType gatewayToken = _$gatewayToken; static const EntityType invoiceItem = _$invoiceItem; + static const EntityType design = _$design; + // STARTER: entity type - do not remove comment static const EntityType quoteItem = _$quoteItem; static const EntityType contact = _$contact; static const EntityType vendorContact = _$vendorContact; diff --git a/lib/data/models/entities.g.dart b/lib/data/models/entities.g.dart index 3e0b79b4f..5d65ba4b6 100644 --- a/lib/data/models/entities.g.dart +++ b/lib/data/models/entities.g.dart @@ -29,6 +29,7 @@ const EntityType _$company = const EntityType._('company'); const EntityType _$gateway = const EntityType._('gateway'); const EntityType _$gatewayToken = const EntityType._('gatewayToken'); const EntityType _$invoiceItem = const EntityType._('invoiceItem'); +const EntityType _$design = const EntityType._('design'); const EntityType _$quoteItem = const EntityType._('quoteItem'); const EntityType _$contact = const EntityType._('contact'); const EntityType _$vendorContact = const EntityType._('vendorContact'); @@ -92,6 +93,8 @@ EntityType _$typeValueOf(String name) { return _$gatewayToken; case 'invoiceItem': return _$invoiceItem; + case 'design': + return _$design; case 'quoteItem': return _$quoteItem; case 'contact': @@ -150,6 +153,7 @@ final BuiltSet _$typeValues = _$gateway, _$gatewayToken, _$invoiceItem, + _$design, _$quoteItem, _$contact, _$vendorContact, diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index 1a45a1c55..a52873afa 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -2,7 +2,6 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; import 'package:invoiceninja_flutter/utils/strings.dart'; - export 'package:invoiceninja_flutter/data/models/client_model.dart'; export 'package:invoiceninja_flutter/data/models/company_model.dart'; export 'package:invoiceninja_flutter/data/models/credit_model.dart'; @@ -13,7 +12,9 @@ export 'package:invoiceninja_flutter/data/models/expense_model.dart'; export 'package:invoiceninja_flutter/data/models/invoice_model.dart'; export 'package:invoiceninja_flutter/data/models/payment_model.dart'; export 'package:invoiceninja_flutter/data/models/product_model.dart'; +export 'package:invoiceninja_flutter/data/models/design_model.dart'; export 'package:invoiceninja_flutter/data/models/project_model.dart'; +// export 'package:invoiceninja_flutter/data/models/static/country_model.dart'; export 'package:invoiceninja_flutter/data/models/static/currency_model.dart'; export 'package:invoiceninja_flutter/data/models/static/date_format_model.dart'; diff --git a/lib/data/models/quote_model.dart b/lib/data/models/quote_model.dart index 29f3394cf..d57330369 100644 --- a/lib/data/models/quote_model.dart +++ b/lib/data/models/quote_model.dart @@ -63,4 +63,3 @@ class QuoteFields { static const String archivedAt = 'archivedAt'; static const String isDeleted = 'isDeleted'; } - diff --git a/lib/data/models/serializers.dart b/lib/data/models/serializers.dart index e5003d58f..2ab53a0b6 100644 --- a/lib/data/models/serializers.dart +++ b/lib/data/models/serializers.dart @@ -36,6 +36,9 @@ import 'package:invoiceninja_flutter/redux/payment/payment_state.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_state.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/data/models/design_model.dart'; +import 'package:invoiceninja_flutter/redux/design/design_state.dart'; + import 'package:invoiceninja_flutter/data/models/credit_model.dart'; import 'package:invoiceninja_flutter/redux/credit/credit_state.dart'; @@ -112,6 +115,8 @@ part 'serializers.g.dart'; TaxRateItemResponse, TaxRateListResponse, // STARTER: serializers - do not remove comment + DesignEntity, + InvoiceEntity, PaymentableEntity, diff --git a/lib/data/models/serializers.g.dart b/lib/data/models/serializers.g.dart index 767e672fa..1e301118e 100644 --- a/lib/data/models/serializers.g.dart +++ b/lib/data/models/serializers.g.dart @@ -45,6 +45,9 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(DatetimeFormatEntity.serializer) ..add(DatetimeFormatItemResponse.serializer) ..add(DatetimeFormatListResponse.serializer) + ..add(DesignEntity.serializer) + ..add(DesignState.serializer) + ..add(DesignUIState.serializer) ..add(DocumentEntity.serializer) ..add(DocumentItemResponse.serializer) ..add(DocumentListResponse.serializer) @@ -383,6 +386,8 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(PaymentTypeEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(CountryEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(InvoiceStatusEntity)]), () => new MapBuilder()) + ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(DesignEntity)]), () => new MapBuilder()) + ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(DocumentEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(ExpenseEntity)]), () => new MapBuilder()) diff --git a/lib/data/repositories/credit_repository.dart b/lib/data/repositories/credit_repository.dart index 325d29c2f..8d0b266fe 100644 --- a/lib/data/repositories/credit_repository.dart +++ b/lib/data/repositories/credit_repository.dart @@ -21,7 +21,7 @@ class CreditRepository { '${credentials.url}/credits/$entityId?', credentials.token); final InvoiceItemResponse creditResponse = - serializers.deserializeWith(InvoiceItemResponse.serializer, response); + serializers.deserializeWith(InvoiceItemResponse.serializer, response); return creditResponse.data; } @@ -37,7 +37,7 @@ class CreditRepository { final dynamic response = await webClient.get(url, credentials.token); final InvoiceListResponse creditResponse = - serializers.deserializeWith(InvoiceListResponse.serializer, response); + serializers.deserializeWith(InvoiceListResponse.serializer, response); return creditResponse.data; } @@ -52,7 +52,7 @@ class CreditRepository { data: json.encode({'ids': ids})); final InvoiceListResponse invoiceResponse = - serializers.deserializeWith(InvoiceListResponse.serializer, response); + serializers.deserializeWith(InvoiceListResponse.serializer, response); return invoiceResponse.data.toList(); } @@ -72,11 +72,11 @@ class CreditRepository { url += '?action=' + action.toString(); } response = - await webClient.put(url, credentials.token, data: json.encode(data)); + await webClient.put(url, credentials.token, data: json.encode(data)); } final InvoiceItemResponse creditResponse = - serializers.deserializeWith(InvoiceItemResponse.serializer, response); + serializers.deserializeWith(InvoiceItemResponse.serializer, response); return creditResponse.data; } diff --git a/lib/data/repositories/design_repository.dart b/lib/data/repositories/design_repository.dart new file mode 100644 index 000000000..6a9587cd3 --- /dev/null +++ b/lib/data/repositories/design_repository.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:core'; +import 'package:invoiceninja_flutter/.env.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/serializers.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/data/web_client.dart'; + +class DesignRepository { + const DesignRepository({ + this.webClient = const WebClient(), + }); + + final WebClient webClient; + + Future loadItem( + Credentials credentials, String entityId) async { + final dynamic response = await webClient.get( + '${credentials.url}/designs/$entityId', credentials.token); + + final DesignItemResponse designResponse = + serializers.deserializeWith(DesignItemResponse.serializer, response); + + return designResponse.data; + } + + Future> loadList( + Credentials credentials, int updatedAt) async { + String url = credentials.url + '/designs?'; + + if (updatedAt > 0) { + url += '&updated_at=${updatedAt - kUpdatedAtBufferSeconds}'; + } + + final dynamic response = await webClient.get(url, credentials.token); + + final DesignListResponse designResponse = + serializers.deserializeWith(DesignListResponse.serializer, response); + + return designResponse.data; + } + + Future> bulkAction( + Credentials credentials, List ids, EntityAction action) async { + var url = credentials.url + '/designs/bulk?'; + if (action != null) { + url += '&action=' + action.toString(); + } + final dynamic response = await webClient.post(url, credentials.token, + data: json.encode({'ids': ids})); + + final DesignListResponse designResponse = + serializers.deserializeWith(DesignListResponse.serializer, response); + + return designResponse.data.toList(); + } + + Future saveData(Credentials credentials, DesignEntity design, + [EntityAction action]) async { + final data = serializers.serializeWith(DesignEntity.serializer, design); + dynamic response; + + if (design.isNew) { + response = await webClient.post( + credentials.url + '/designs', credentials.token, + data: json.encode(data)); + } else { + var url = credentials.url + '/designs/' + design.id.toString(); + if (action != null) { + url += '?action=' + action.toString(); + } + response = + await webClient.put(url, credentials.token, data: json.encode(data)); + } + + final DesignItemResponse designResponse = + serializers.deserializeWith(DesignItemResponse.serializer, response); + + return designResponse.data; + } +} diff --git a/lib/main.dart b/lib/main.dart index 202b2e28b..12ba269d1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -51,6 +51,12 @@ import 'package:sentry/sentry.dart'; import 'package:shared_preferences/shared_preferences.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/ui/design/design_screen.dart'; +import 'package:invoiceninja_flutter/ui/design/edit/design_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/design/view/design_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; +import 'package:invoiceninja_flutter/redux/design/design_middleware.dart'; + import 'package:invoiceninja_flutter/ui/credit/credit_screen.dart'; import 'package:invoiceninja_flutter/ui/credit/edit/credit_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/credit/view/credit_view_vm.dart'; @@ -100,6 +106,7 @@ void main({bool isTesting = false}) async { ..addAll(createStoreSettingsMiddleware()) ..addAll(createStoreReportsMiddleware()) // STARTER: middleware - do not remove comment + ..addAll(createStoreDesignsMiddleware()) ..addAll(createStoreCreditsMiddleware()) ..addAll(createStoreUsersMiddleware()) ..addAll(createStoreTaxRatesMiddleware()) @@ -351,6 +358,9 @@ class InvoiceNinjaAppState extends State { QuoteEditScreen.route: (context) => QuoteEditScreen(), QuoteEmailScreen.route: (context) => QuoteEmailScreen(), // STARTER: routes - do not remove comment + DesignScreen.route: (context) => DesignScreenBuilder(), + DesignViewScreen.route: (context) => DesignViewScreen(), + DesignEditScreen.route: (context) => DesignEditScreen(), CreditScreen.route: (context) => CreditScreenBuilder(), CreditViewScreen.route: (context) => CreditViewScreen(), CreditEditScreen.route: (context) => CreditEditScreen(), diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index e19967324..63f3d0aab 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_actions.dart @@ -27,6 +27,8 @@ import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; + import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; class PersistUI {} @@ -232,6 +234,13 @@ void filterEntitiesByType({ )); break; // STARTER: filter - do not remove comment + case EntityType.design: + store.dispatch(FilterDesignsByEntity( + entityId: filterEntity.id, + entityType: filterEntity.entityType, + )); + break; + case EntityType.credit: store.dispatch(FilterCreditsByEntity( entityId: filterEntity.id, @@ -307,6 +316,10 @@ void viewEntitiesByType({ store.dispatch(ViewGroupList(navigator: navigator)); break; // STARTER: view list - do not remove comment + case EntityType.design: + store.dispatch(ViewDesignList(navigator: navigator)); + break; + case EntityType.credit: store.dispatch(ViewCreditList(navigator: navigator)); break; @@ -434,6 +447,14 @@ void viewEntityById({ )); break; // STARTER: view - do not remove comment + case EntityType.design: + store.dispatch(ViewDesign( + designId: entityId, + navigator: navigator, + force: force, + )); + break; + case EntityType.credit: store.dispatch(ViewCredit( creditId: entityId, @@ -555,6 +576,14 @@ void createEntityByType( )); break; // STARTER: create type - do not remove comment + case EntityType.design: + store.dispatch(EditDesign( + navigator: navigator, + force: force, + design: DesignEntity(state: state), + )); + break; + case EntityType.credit: store.dispatch(EditCredit( navigator: navigator, @@ -696,6 +725,15 @@ void createEntity({ )); break; // STARTER: create - do not remove comment + case EntityType.design: + store.dispatch(EditDesign( + navigator: navigator, + design: entity, + force: force, + completer: completer, + )); + break; + case EntityType.credit: store.dispatch(EditCredit( navigator: navigator, @@ -886,6 +924,19 @@ void editEntityById( )); break; // STARTER: edit - do not remove comment + case EntityType.design: + store.dispatch(EditDesign( + design: map[entityId], + navigator: navigator, + completer: completer ?? + snackBarCompleter( + context, + entity.isNew + ? localization.createdDesign + : localization.updatedDesign), + )); + break; + case EntityType.credit: store.dispatch(EditCredit( credit: map[entityId], @@ -968,6 +1019,10 @@ void handleEntitiesActions( handleDocumentAction(context, entities, action); break; // STARTER: actions - do not remove comment + case EntityType.design: + handleDesignAction(context, entities, action); + break; + case EntityType.credit: handleCreditAction(context, entities, action); break; diff --git a/lib/redux/app/app_reducer.dart b/lib/redux/app/app_reducer.dart index 52d411890..7959fb853 100644 --- a/lib/redux/app/app_reducer.dart +++ b/lib/redux/app/app_reducer.dart @@ -18,6 +18,8 @@ import 'package:invoiceninja_flutter/redux/auth/auth_reducer.dart'; import 'package:invoiceninja_flutter/redux/company/company_reducer.dart'; import 'package:invoiceninja_flutter/redux/static/static_reducer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; + import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; // We create the State reducer by combining many smaller reducers into one! @@ -85,6 +87,10 @@ final lastErrorReducer = combineReducers([ return '${action.error}'; }), // STARTER: errors - do not remove comment + TypedReducer((state, action) { + return '${action.error}'; + }), + TypedReducer((state, action) { return '${action.error}'; }), diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index 7e3a42824..c399e70d5 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -164,6 +164,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceState.map; // STARTER: states switch map - do not remove comment + case EntityType.design: + return designState.map; + case EntityType.credit: return creditState.map; @@ -224,6 +227,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceState.list; // STARTER: states switch list - do not remove comment + case EntityType.design: + return designState.list; + case EntityType.credit: return creditState.list; @@ -263,6 +269,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceUIState; // STARTER: states switch - do not remove comment + case EntityType.design: + return designUIState; + case EntityType.credit: return creditUIState; @@ -316,6 +325,10 @@ abstract class AppState implements Built { ListUIState get invoiceListState => uiState.invoiceUIState.listUIState; // STARTER: state getters - do not remove comment + DesignState get designState => userCompanyState.designState; + ListUIState get designListState => uiState.designUIState.listUIState; + DesignUIState get designUIState => uiState.designUIState; + CreditState get creditState => userCompanyState.creditState; ListUIState get creditListState => uiState.creditUIState.listUIState; @@ -423,6 +436,8 @@ abstract class AppState implements Built { case CreditEditScreen.route: return hasCreditChanges(creditUIState.editing, creditState.map); // STARTER: has changes - do not remove comment + case DesignEditScreen.route: + return hasDesignChanges(designUIState.editing, designState.map); } if (uiState.currentRoute.startsWith('/settings')) { diff --git a/lib/redux/company/company_reducer.dart b/lib/redux/company/company_reducer.dart index 7f5579551..2268b3a6d 100644 --- a/lib/redux/company/company_reducer.dart +++ b/lib/redux/company/company_reducer.dart @@ -18,6 +18,8 @@ import 'package:invoiceninja_flutter/redux/payment/payment_reducer.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_reducer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/design/design_reducer.dart'; + import 'package:invoiceninja_flutter/redux/credit/credit_reducer.dart'; import 'package:invoiceninja_flutter/redux/user/user_reducer.dart'; @@ -40,6 +42,7 @@ UserCompanyState companyReducer(UserCompanyState state, dynamic action) { ..vendorState.replace(vendorsReducer(state.vendorState, action)) ..taskState.replace(tasksReducer(state.taskState, action)) // STARTER: reducer - do not remove comment + ..designState.replace(designsReducer(state.designState, action)) ..creditState.replace(creditsReducer(state.creditState, action)) ..userState.replace(usersReducer(state.userState, action)) ..taxRateState.replace(taxRatesReducer(state.taxRateState, action)) diff --git a/lib/redux/company/company_state.dart b/lib/redux/company/company_state.dart index 9963894fb..8765635d1 100644 --- a/lib/redux/company/company_state.dart +++ b/lib/redux/company/company_state.dart @@ -7,6 +7,8 @@ import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/design/design_state.dart'; + import 'package:invoiceninja_flutter/redux/credit/credit_state.dart'; import 'package:invoiceninja_flutter/redux/user/user_state.dart'; @@ -39,6 +41,8 @@ abstract class UserCompanyState paymentState: PaymentState(), quoteState: QuoteState(), // STARTER: constructor - do not remove comment + designState: DesignState(), + creditState: CreditState(), userState: UserState(), @@ -74,6 +78,8 @@ abstract class UserCompanyState QuoteState get quoteState; // STARTER: fields - do not remove comment + DesignState get designState; + CreditState get creditState; UserState get userState; diff --git a/lib/redux/company/company_state.g.dart b/lib/redux/company/company_state.g.dart index 9fa3595d0..253324394 100644 --- a/lib/redux/company/company_state.g.dart +++ b/lib/redux/company/company_state.g.dart @@ -52,6 +52,9 @@ class _$UserCompanyStateSerializer 'quoteState', serializers.serialize(object.quoteState, specifiedType: const FullType(QuoteState)), + 'designState', + serializers.serialize(object.designState, + specifiedType: const FullType(DesignState)), 'creditState', serializers.serialize(object.creditState, specifiedType: const FullType(CreditState)), @@ -134,6 +137,10 @@ class _$UserCompanyStateSerializer result.quoteState.replace(serializers.deserialize(value, specifiedType: const FullType(QuoteState)) as QuoteState); break; + case 'designState': + result.designState.replace(serializers.deserialize(value, + specifiedType: const FullType(DesignState)) as DesignState); + break; case 'creditState': result.creditState.replace(serializers.deserialize(value, specifiedType: const FullType(CreditState)) as CreditState); @@ -321,6 +328,8 @@ class _$UserCompanyState extends UserCompanyState { @override final QuoteState quoteState; @override + final DesignState designState; + @override final CreditState creditState; @override final UserState userState; @@ -347,6 +356,7 @@ class _$UserCompanyState extends UserCompanyState { this.projectState, this.paymentState, this.quoteState, + this.designState, this.creditState, this.userState, this.taxRateState, @@ -383,6 +393,9 @@ class _$UserCompanyState extends UserCompanyState { if (quoteState == null) { throw new BuiltValueNullFieldError('UserCompanyState', 'quoteState'); } + if (designState == null) { + throw new BuiltValueNullFieldError('UserCompanyState', 'designState'); + } if (creditState == null) { throw new BuiltValueNullFieldError('UserCompanyState', 'creditState'); } @@ -424,6 +437,7 @@ class _$UserCompanyState extends UserCompanyState { projectState == other.projectState && paymentState == other.paymentState && quoteState == other.quoteState && + designState == other.designState && creditState == other.creditState && userState == other.userState && taxRateState == other.taxRateState && @@ -449,21 +463,24 @@ class _$UserCompanyState extends UserCompanyState { $jc( $jc( $jc( - 0, - userCompany + $jc( + 0, + userCompany + .hashCode), + documentState .hashCode), - documentState + productState .hashCode), - productState + clientState .hashCode), - clientState.hashCode), - invoiceState.hashCode), - expenseState.hashCode), - vendorState.hashCode), - taskState.hashCode), - projectState.hashCode), - paymentState.hashCode), - quoteState.hashCode), + invoiceState.hashCode), + expenseState.hashCode), + vendorState.hashCode), + taskState.hashCode), + projectState.hashCode), + paymentState.hashCode), + quoteState.hashCode), + designState.hashCode), creditState.hashCode), userState.hashCode), taxRateState.hashCode), @@ -485,6 +502,7 @@ class _$UserCompanyState extends UserCompanyState { ..add('projectState', projectState) ..add('paymentState', paymentState) ..add('quoteState', quoteState) + ..add('designState', designState) ..add('creditState', creditState) ..add('userState', userState) ..add('taxRateState', taxRateState) @@ -563,6 +581,12 @@ class UserCompanyStateBuilder set quoteState(QuoteStateBuilder quoteState) => _$this._quoteState = quoteState; + DesignStateBuilder _designState; + DesignStateBuilder get designState => + _$this._designState ??= new DesignStateBuilder(); + set designState(DesignStateBuilder designState) => + _$this._designState = designState; + CreditStateBuilder _creditState; CreditStateBuilder get creditState => _$this._creditState ??= new CreditStateBuilder(); @@ -607,6 +631,7 @@ class UserCompanyStateBuilder _projectState = _$v.projectState?.toBuilder(); _paymentState = _$v.paymentState?.toBuilder(); _quoteState = _$v.quoteState?.toBuilder(); + _designState = _$v.designState?.toBuilder(); _creditState = _$v.creditState?.toBuilder(); _userState = _$v.userState?.toBuilder(); _taxRateState = _$v.taxRateState?.toBuilder(); @@ -647,6 +672,7 @@ class UserCompanyStateBuilder projectState: projectState.build(), paymentState: paymentState.build(), quoteState: quoteState.build(), + designState: designState.build(), creditState: creditState.build(), userState: userState.build(), taxRateState: taxRateState.build(), @@ -677,6 +703,8 @@ class UserCompanyStateBuilder paymentState.build(); _$failedField = 'quoteState'; quoteState.build(); + _$failedField = 'designState'; + designState.build(); _$failedField = 'creditState'; creditState.build(); _$failedField = 'userState'; diff --git a/lib/redux/credit/credit_actions.dart b/lib/redux/credit/credit_actions.dart index 0f452e603..198fea670 100644 --- a/lib/redux/credit/credit_actions.dart +++ b/lib/redux/credit/credit_actions.dart @@ -35,10 +35,10 @@ class EditCredit extends AbstractNavigatorAction implements PersistUI, PersistPrefs { EditCredit( {this.credit, - @required NavigatorState navigator, - this.creditItemIndex, - this.completer, - this.force = false}) + @required NavigatorState navigator, + this.creditItemIndex, + this.completer, + this.force = false}) : super(navigator: navigator); final InvoiceEntity credit; @@ -360,14 +360,14 @@ class FilterCreditsByCustom4 implements PersistUI { Future handleCreditAction( BuildContext context, List credits, EntityAction action) async { assert( - [ - EntityAction.restore, - EntityAction.archive, - EntityAction.delete, - EntityAction.toggleMultiselect - ].contains(action) || - credits.length == 1, - 'Cannot perform this action on more than one credit'); + [ + EntityAction.restore, + EntityAction.archive, + EntityAction.delete, + EntityAction.toggleMultiselect + ].contains(action) || + credits.length == 1, + 'Cannot perform this action on more than one credit'); final store = StoreProvider.of(context); final localization = AppLocalization.of(context); @@ -402,7 +402,7 @@ Future handleCreditAction( case EntityAction.sendEmail: store.dispatch(ShowEmailCredit( completer: - snackBarCompleter(context, localization.emailedCredit), + snackBarCompleter(context, localization.emailedCredit), credit: credit, context: context)); break; diff --git a/lib/redux/credit/credit_middleware.dart b/lib/redux/credit/credit_middleware.dart index 049a4e616..e913eacdf 100644 --- a/lib/redux/credit/credit_middleware.dart +++ b/lib/redux/credit/credit_middleware.dart @@ -120,7 +120,7 @@ Middleware _showEmailCredit() { if (isMobile(action.context)) { final emailWasSent = - await Navigator.of(action.context).pushNamed(CreditEmailScreen.route); + await Navigator.of(action.context).pushNamed(CreditEmailScreen.route); if (action.completer != null && emailWasSent != null && emailWasSent) { action.completer.complete(null); @@ -133,10 +133,10 @@ Middleware _archiveCredit(CreditRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { final action = dynamicAction as ArchiveCreditsRequest; final prevCredits = - action.creditIds.map((id) => store.state.creditState.map[id]).toList(); + action.creditIds.map((id) => store.state.creditState.map[id]).toList(); repository .bulkAction( - store.state.credentials, action.creditIds, EntityAction.archive) + store.state.credentials, action.creditIds, EntityAction.archive) .then((List credits) { store.dispatch(ArchiveCreditsSuccess(credits)); if (action.completer != null) { @@ -158,11 +158,11 @@ Middleware _deleteCredit(CreditRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { final action = dynamicAction as DeleteCreditsRequest; final prevCredits = - action.creditIds.map((id) => store.state.creditState.map[id]).toList(); + action.creditIds.map((id) => store.state.creditState.map[id]).toList(); repository .bulkAction( - store.state.credentials, action.creditIds, EntityAction.delete) + store.state.credentials, action.creditIds, EntityAction.delete) .then((List credits) { store.dispatch(DeleteCreditsSuccess(credits)); if (action.completer != null) { @@ -184,11 +184,11 @@ Middleware _restoreCredit(CreditRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { final action = dynamicAction as RestoreCreditsRequest; final prevCredits = - action.creditIds.map((id) => store.state.creditState.map[id]).toList(); + action.creditIds.map((id) => store.state.creditState.map[id]).toList(); repository .bulkAction( - store.state.credentials, action.creditIds, EntityAction.restore) + store.state.credentials, action.creditIds, EntityAction.restore) .then((List credits) { store.dispatch(RestoreCreditsSuccess(credits)); if (action.completer != null) { @@ -235,7 +235,7 @@ Middleware _emailCredit(CreditRepository repository) { final origCredit = store.state.creditState.map[action.creditId]; repository .emailCredit(store.state.credentials, origCredit, action.template, - action.subject, action.body) + action.subject, action.body) .then((void _) { store.dispatch(EmailCreditSuccess()); if (action.completer != null) { @@ -292,7 +292,9 @@ Middleware _loadCredit(CreditRepository repository) { } store.dispatch(LoadCreditRequest()); - repository.loadItem(store.state.credentials, action.creditId).then((credit) { + repository + .loadItem(store.state.credentials, action.creditId) + .then((credit) { store.dispatch(LoadCreditSuccess(credit)); if (action.completer != null) { diff --git a/lib/redux/credit/credit_reducer.dart b/lib/redux/credit/credit_reducer.dart index df77b3978..603f6d64f 100644 --- a/lib/redux/credit/credit_reducer.dart +++ b/lib/redux/credit/credit_reducer.dart @@ -33,8 +33,9 @@ String filtercreditDropdownReducer( Reducer selectedIdReducer = combineReducers([ TypedReducer((selectedId, action) => action.creditId), TypedReducer( - (selectedId, action) => action.credit.id), - TypedReducer((selectedId, action) => action.credit.id), + (selectedId, action) => action.credit.id), + TypedReducer( + (selectedId, action) => action.credit.id), TypedReducer((selectedId, action) => ''), ]); @@ -97,7 +98,7 @@ InvoiceEntity _updateEditing(InvoiceEntity credit, dynamic action) { InvoiceEntity _addCreditItem(InvoiceEntity credit, AddCreditItem action) { return credit.rebuild( - (b) => b..lineItems.add(action.creditItem ?? InvoiceItemEntity())); + (b) => b..lineItems.add(action.creditItem ?? InvoiceItemEntity())); } InvoiceEntity _addCreditItems(InvoiceEntity credit, AddCreditItems action) { @@ -227,7 +228,8 @@ ListUIState _addToListMultiselect( ListUIState _removeFromListMultiselect( ListUIState creditListState, RemoveFromCreditMultiselect action) { - return creditListState.rebuild((b) => b..selectedIds.remove(action.entity.id)); + return creditListState + .rebuild((b) => b..selectedIds.remove(action.entity.id)); } ListUIState _clearListMultiselect( @@ -368,5 +370,6 @@ CreditState _updateCredit(CreditState creditState, dynamic action) { return creditState.rebuild((b) => b..map[action.credit.id] = action.credit); } -CreditState _setLoadedCredits(CreditState creditState, LoadCreditsSuccess action) => +CreditState _setLoadedCredits( + CreditState creditState, LoadCreditsSuccess action) => creditState.loadCredits(action.credits); diff --git a/lib/redux/credit/credit_selectors.dart b/lib/redux/credit/credit_selectors.dart index 2f6081fc8..cefd3d527 100644 --- a/lib/redux/credit/credit_selectors.dart +++ b/lib/redux/credit/credit_selectors.dart @@ -8,10 +8,11 @@ ClientEntity creditClientSelector( return clientMap[credit.clientId]; } -var memoizedFilteredCreditList = memo4((BuiltMap creditMap, - BuiltList creditList, - BuiltMap clientMap, - ListUIState creditListState) => +var memoizedFilteredCreditList = memo4((BuiltMap + creditMap, + BuiltList creditList, + BuiltMap clientMap, + ListUIState creditListState) => filteredCreditsSelector(creditMap, creditList, clientMap, creditListState)); List filteredCreditsSelector( @@ -64,7 +65,7 @@ List filteredCreditsSelector( } var memoizedCreditStatsForClient = memo2( - (String clientId, BuiltMap creditMap) => + (String clientId, BuiltMap creditMap) => creditStatsForClient(clientId, creditMap)); EntityStats creditStatsForClient( @@ -85,13 +86,13 @@ EntityStats creditStatsForClient( } var memoizedCreditStatsForUser = memo2( - (String userId, BuiltMap creditMap) => + (String userId, BuiltMap creditMap) => creditStatsForUser(userId, creditMap)); EntityStats creditStatsForUser( - String userId, - BuiltMap creditMap, - ) { + String userId, + BuiltMap creditMap, +) { int countActive = 0; int countArchived = 0; creditMap.forEach((creditId, credit) { @@ -108,5 +109,5 @@ EntityStats creditStatsForUser( } bool hasCreditChanges( - InvoiceEntity credit, BuiltMap creditMap) => + InvoiceEntity credit, BuiltMap creditMap) => credit.isNew ? credit.isChanged : credit != creditMap[credit.id]; diff --git a/lib/redux/design/design_actions.dart b/lib/redux/design/design_actions.dart new file mode 100644 index 000000000..cd7dc3abf --- /dev/null +++ b/lib/redux/design/design_actions.dart @@ -0,0 +1,335 @@ +import 'dart:async'; +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; + +class ViewDesignList extends AbstractNavigatorAction implements PersistUI { + ViewDesignList({ + @required NavigatorState navigator, + this.force = false, + }) : super(navigator: navigator); + + final bool force; +} + +class ViewDesign extends AbstractNavigatorAction + implements PersistUI, PersistPrefs { + ViewDesign({ + @required NavigatorState navigator, + @required this.designId, + this.force = false, + }) : super(navigator: navigator); + + final String designId; + final bool force; +} + +class EditDesign extends AbstractNavigatorAction + implements PersistUI, PersistPrefs { + EditDesign( + {@required this.design, + @required NavigatorState navigator, + this.completer, + this.cancelCompleter, + this.force = false}) + : super(navigator: navigator); + + final DesignEntity design; + final Completer completer; + final Completer cancelCompleter; + final bool force; +} + +class UpdateDesign implements PersistUI { + UpdateDesign(this.design); + + final DesignEntity design; +} + +class LoadDesign { + LoadDesign({this.completer, this.designId}); + + final Completer completer; + final String designId; +} + +class LoadDesignActivity { + LoadDesignActivity({this.completer, this.designId}); + + final Completer completer; + final String designId; +} + +class LoadDesigns { + LoadDesigns({this.completer, this.force = false}); + + final Completer completer; + final bool force; +} + +class LoadDesignRequest implements StartLoading {} + +class LoadDesignFailure implements StopLoading { + LoadDesignFailure(this.error); + + final dynamic error; + + @override + String toString() { + return 'LoadDesignFailure{error: $error}'; + } +} + +class LoadDesignSuccess implements StopLoading, PersistData { + LoadDesignSuccess(this.design); + + final DesignEntity design; + + @override + String toString() { + return 'LoadDesignSuccess{design: $design}'; + } +} + +class LoadDesignsRequest implements StartLoading {} + +class LoadDesignsFailure implements StopLoading { + LoadDesignsFailure(this.error); + + final dynamic error; + + @override + String toString() { + return 'LoadDesignsFailure{error: $error}'; + } +} + +class LoadDesignsSuccess implements StopLoading, PersistData { + LoadDesignsSuccess(this.designs); + + final BuiltList designs; + + @override + String toString() { + return 'LoadDesignsSuccess{designs: $designs}'; + } +} + +class SaveDesignRequest implements StartSaving { + SaveDesignRequest({this.completer, this.design}); + + final Completer completer; + final DesignEntity design; +} + +class SaveDesignSuccess implements StopSaving, PersistData, PersistUI { + SaveDesignSuccess(this.design); + + final DesignEntity design; +} + +class AddDesignSuccess implements StopSaving, PersistData, PersistUI { + AddDesignSuccess(this.design); + + final DesignEntity design; +} + +class SaveDesignFailure implements StopSaving { + SaveDesignFailure(this.error); + + final Object error; +} + +class ArchiveDesignsRequest implements StartSaving { + ArchiveDesignsRequest(this.completer, this.designIds); + + final Completer completer; + final List designIds; +} + +class ArchiveDesignsSuccess implements StopSaving, PersistData { + ArchiveDesignsSuccess(this.designs); + + final List designs; +} + +class ArchiveDesignsFailure implements StopSaving { + ArchiveDesignsFailure(this.designs); + + final List designs; +} + +class DeleteDesignsRequest implements StartSaving { + DeleteDesignsRequest(this.completer, this.designIds); + + final Completer completer; + final List designIds; +} + +class DeleteDesignsSuccess implements StopSaving, PersistData { + DeleteDesignsSuccess(this.designs); + + final List designs; +} + +class DeleteDesignsFailure implements StopSaving { + DeleteDesignsFailure(this.designs); + + final List designs; +} + +class RestoreDesignsRequest implements StartSaving { + RestoreDesignsRequest(this.completer, this.designIds); + + final Completer completer; + final List designIds; +} + +class RestoreDesignsSuccess implements StopSaving, PersistData { + RestoreDesignsSuccess(this.design); + + final List designs; +} + +class RestoreDesignsFailure implements StopSaving { + RestoreDesignsFailure(this.design); + + final List designs; +} + +class FilterDesigns implements PersistUI { + FilterDesigns(this.filter); + + final String filter; +} + +class SortDesigns implements PersistUI { + SortDesigns(this.field); + + final String field; +} + +class FilterDesignsByState implements PersistUI { + FilterDesignsByState(this.state); + + final EntityState state; +} + +class FilterDesignsByCustom1 implements PersistUI { + FilterDesignsByCustom1(this.value); + + final String value; +} + +class FilterDesignsByCustom2 implements PersistUI { + FilterDesignsByCustom2(this.value); + + final String value; +} + +class FilterDesignsByCustom3 implements PersistUI { + FilterDesignsByCustom3(this.value); + + final String value; +} + +class FilterDesignsByCustom4 implements PersistUI { + FilterDesignsByCustom4(this.value); + + final String value; +} + +class FilterDesignsByEntity implements PersistUI { + FilterDesignsByEntity({this.entityId, this.entityType}); + + final String entityId; + final EntityType entityType; +} + +void handleDesignAction( + BuildContext context, List designs, EntityAction action) { + if (designs.isEmpty) { + return; + } + + final store = StoreProvider.of(context); + final state = store.state; + final CompanyEntity company = state.company; + final localization = AppLocalization.of(context); + final design = designs.first as DesignEntity; + final designIds = designs.map((design) => design.id).toList(); + + switch (action) { + case EntityAction.edit: + editEntity(context: context, entity: design); + break; + case EntityAction.restore: + store.dispatch(RestoreDesignsRequest( + snackBarCompleter(context, localization.restoredDesign), + designIds)); + break; + case EntityAction.archive: + store.dispatch(ArchiveDesignsRequest( + snackBarCompleter(context, localization.archivedDesign), + designIds)); + break; + case EntityAction.delete: + store.dispatch(DeleteDesignsRequest( + snackBarCompleter(context, localization.deletedDesign), + designIds)); + break; + case EntityAction.toggleMultiselect: + if (!store.state.designListState.isInMultiselect()) { + store.dispatch(StartDesignMultiselect(context: context)); + } + + if (designs.isEmpty) { + break; + } + + for (final design in designs) { + if (!store.state.designListState.isSelected(design.id)) { + store.dispatch( + AddToDesignMultiselect(context: context, entity: design)); + } else { + store.dispatch( + RemoveFromDesignMultiselect(context: context, entity: design)); + } + } + break; + } +} + +class StartDesignMultiselect { + StartDesignMultiselect({@required this.context}); + + final BuildContext context; +} + +class AddToDesignMultiselect { + AddToDesignMultiselect({@required this.context, @required this.entity}); + + final BuildContext context; + final BaseEntity entity; +} + +class RemoveFromDesignMultiselect { + RemoveFromDesignMultiselect({@required this.context, @required this.entity}); + + final BuildContext context; + final BaseEntity entity; +} + +class ClearDesignMultiselect { + ClearDesignMultiselect({@required this.context}); + + final BuildContext context; +} diff --git a/lib/redux/design/design_middleware.dart b/lib/redux/design/design_middleware.dart new file mode 100644 index 000000000..360a3df13 --- /dev/null +++ b/lib/redux/design/design_middleware.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:invoiceninja_flutter/redux/app/app_middleware.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/design/design_screen.dart'; +import 'package:invoiceninja_flutter/ui/design/edit/design_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/design/view/design_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/repositories/design_repository.dart'; + +List> createStoreDesignsMiddleware([ + DesignRepository repository = const DesignRepository(), +]) { + final viewDesignList = _viewDesignList(); + final viewDesign = _viewDesign(); + final editDesign = _editDesign(); + final loadDesigns = _loadDesigns(repository); + final loadDesign = _loadDesign(repository); + final saveDesign = _saveDesign(repository); + final archiveDesign = _archiveDesign(repository); + final deleteDesign = _deleteDesign(repository); + final restoreDesign = _restoreDesign(repository); + + return [ + TypedMiddleware(viewDesignList), + TypedMiddleware(viewDesign), + TypedMiddleware(editDesign), + TypedMiddleware(loadDesigns), + TypedMiddleware(loadDesign), + TypedMiddleware(saveDesign), + TypedMiddleware(archiveDesign), + TypedMiddleware(deleteDesign), + TypedMiddleware(restoreDesign), + ]; +} + +Middleware _editDesign() { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as EditDesign; + + if (!action.force && + hasChanges(store: store, context: action.context, action: action)) { + return; + } + + next(action); + + store.dispatch(UpdateCurrentRoute(DesignEditScreen.route)); + + if (isMobile(action.context)) { + action.navigator.pushNamed(DesignEditScreen.route); + } + }; +} + +Middleware _viewDesign() { + return (Store store, dynamic dynamicAction, + NextDispatcher next) async { + final action = dynamicAction as ViewDesign; + + if (!action.force && + hasChanges(store: store, context: action.context, action: action)) { + return; + } + + next(action); + + store.dispatch(UpdateCurrentRoute(DesignViewScreen.route)); + + if (isMobile(action.context)) { + Navigator.of(action.context).pushNamed(DesignViewScreen.route); + } + }; +} + +Middleware _viewDesignList() { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as ViewDesignList; + + if (!action.force && + hasChanges(store: store, context: action.context, action: action)) { + return; + } + + next(action); + + if (store.state.designState.isStale) { + store.dispatch(LoadDesigns()); + } + + store.dispatch(UpdateCurrentRoute(DesignScreen.route)); + + if (isMobile(action.context)) { + Navigator.of(action.context).pushNamedAndRemoveUntil( + DesignScreen.route, (Route route) => false); + } + }; +} + +Middleware _archiveDesign(DesignRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as ArchiveDesignsRequest; + final prevDesigns = + action.designIds.map((id) => store.state.designState.map[id]).toList(); + repository + .bulkAction( + store.state.credentials, action.designIds, EntityAction.archive) + .then((List designs) { + store.dispatch(ArchiveDesignsSuccess(designs)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(ArchiveDesignsFailure(prevDesigns)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _deleteDesign(DesignRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as DeleteDesignsRequest; + final prevDesigns = + action.designIds.map((id) => store.state.designState.map[id]).toList(); + repository + .bulkAction( + store.state.credentials, action.designIds, EntityAction.delete) + .then((List designs) { + store.dispatch(DeleteDesignsSuccess(designs)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(DeleteDesignsFailure(prevDesigns)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _restoreDesign(DesignRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as RestoreDesignsRequest; + final prevDesigns = + action.designIds.map((id) => store.state.designState.map[id]).toList(); + repository + .bulkAction( + store.state.credentials, action.designIds, EntityAction.restore) + .then((List designs) { + store.dispatch(RestoreDesignSuccess(designs)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(RestoreDesignFailure(prevDesigns)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _saveDesign(DesignRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as SaveDesignRequest; + repository + .saveData(store.state.credentials, action.design) + .then((DesignEntity design) { + if (action.design.isNew) { + store.dispatch(AddDesignSuccess(design)); + } else { + store.dispatch(SaveDesignSuccess(design)); + } + + action.completer.complete(design); + + final designUIState = store.state.designUIState; + if (designUIState.saveCompleter != null) { + designUIState.saveCompleter.complete(design); + } + }).catchError((Object error) { + print(error); + store.dispatch(SaveDesignFailure(error)); + action.completer.completeError(error); + }); + + next(action); + }; +} + +Middleware _loadDesign(DesignRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as LoadDesign; + final AppState state = store.state; + + if (state.isLoading) { + next(action); + return; + } + + store.dispatch(LoadDesignRequest()); + repository.loadItem(state.credentials, action.designId).then((design) { + store.dispatch(LoadDesignSuccess(design)); + + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(LoadDesignFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _loadDesigns(DesignRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as LoadDesigns; + final AppState state = store.state; + + if (!state.designState.isStale && !action.force) { + next(action); + return; + } + + if (state.isLoading) { + next(action); + return; + } + + final int updatedAt = (state.designState.lastUpdated / 1000).round(); + + store.dispatch(LoadDesignsRequest()); + repository.loadList(state.credentials, updatedAt).then((data) { + store.dispatch(LoadDesignsSuccess(data)); + + if (action.completer != null) { + action.completer.complete(null); + } + /* + if (state.productState.isStale) { + store.dispatch(LoadProducts()); + } + */ + }).catchError((Object error) { + print(error); + store.dispatch(LoadDesignsFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} diff --git a/lib/redux/design/design_reducer.dart b/lib/redux/design/design_reducer.dart new file mode 100644 index 000000000..b8b1419b3 --- /dev/null +++ b/lib/redux/design/design_reducer.dart @@ -0,0 +1,275 @@ +import 'package:redux/redux.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/company/company_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; +import 'package:invoiceninja_flutter/redux/design/design_state.dart'; +import 'package:invoiceninja_flutter/data/models/entities.dart'; + +EntityUIState designUIReducer(DesignUIState state, dynamic action) { + return state.rebuild((b) => b + ..listUIState.replace(designListReducer(state.listUIState, action)) + ..editing.replace(editingReducer(state.editing, action)) + ..selectedId = selectedIdReducer(state.selectedId, action)); +} + +Reducer selectedIdReducer = combineReducers([ + TypedReducer( + (String selectedId, dynamic action) => action.designId), + TypedReducer( + (String selectedId, dynamic action) => action.design.id), + TypedReducer((selectedId, action) => ''), +]); + +final editingReducer = combineReducers([ + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer((designs, action) { + return action.designs[0]; + }), + TypedReducer((designs, action) { + return action.designs[0]; + }), + TypedReducer((designs, action) { + return action.designs[0]; + }), + TypedReducer(_updateEditing), + TypedReducer((design, action) { + return action.design.rebuild((b) => b..isChanged = true); + }), + TypedReducer(_clearEditing), + TypedReducer(_clearEditing), +]); + +DesignEntity _clearEditing(DesignEntity design, dynamic action) { + return DesignEntity(); +} + +DesignEntity _updateEditing(DesignEntity design, dynamic action) { + return action.design; +} + +final designListReducer = combineReducers([ + TypedReducer(_sortDesigns), + TypedReducer(_filterDesignsByState), + TypedReducer(_filterDesigns), + TypedReducer(_filterDesignsByCustom1), + TypedReducer(_filterDesignsByCustom2), + TypedReducer(_filterDesignsByClient), + TypedReducer(_startListMultiselect), + TypedReducer(_addToListMultiselect), + TypedReducer( + _removeFromListMultiselect), + TypedReducer(_clearListMultiselect), +]); + +ListUIState _filterDesignsByClient( + ListUIState designListState, FilterDesignsByEntity action) { + return designListState.rebuild((b) => b + ..filterEntityId = action.entityId + ..filterEntityType = action.entityType); +} + +ListUIState _filterDesignsByCustom1( + ListUIState designListState, FilterDesignsByCustom1 action) { + if (designListState.custom1Filters.contains(action.value)) { + return designListState + .rebuild((b) => b..custom1Filters.remove(action.value)); + } else { + return designListState.rebuild((b) => b..custom1Filters.add(action.value)); + } +} + +ListUIState _filterDesignsByCustom2( + ListUIState designListState, FilterDesignsByCustom2 action) { + if (designListState.custom2Filters.contains(action.value)) { + return designListState + .rebuild((b) => b..custom2Filters.remove(action.value)); + } else { + return designListState.rebuild((b) => b..custom2Filters.add(action.value)); + } +} + +ListUIState _filterDesignsByState( + ListUIState designListState, FilterDesignsByState action) { + if (designListState.stateFilters.contains(action.state)) { + return designListState.rebuild((b) => b..stateFilters.remove(action.state)); + } else { + return designListState.rebuild((b) => b..stateFilters.add(action.state)); + } +} + +ListUIState _filterDesigns(ListUIState designListState, FilterDesigns action) { + return designListState.rebuild((b) => b + ..filter = action.filter + ..filterClearedAt = action.filter == null + ? DateTime.now().millisecondsSinceEpoch + : designListState.filterClearedAt); +} + +ListUIState _sortDesigns(ListUIState designListState, SortDesigns action) { + return designListState.rebuild((b) => b + ..sortAscending = b.sortField != action.field || !b.sortAscending + ..sortField = action.field); +} + +ListUIState _startListMultiselect( + ListUIState productListState, StartDesignMultiselect action) { + return productListState.rebuild((b) => b..selectedIds = ListBuilder()); +} + +ListUIState _addToListMultiselect( + ListUIState productListState, AddToDesignMultiselect action) { + return productListState.rebuild((b) => b..selectedIds.add(action.entity.id)); +} + +ListUIState _removeFromListMultiselect( + ListUIState productListState, RemoveFromDesignMultiselect action) { + return productListState + .rebuild((b) => b..selectedIds.remove(action.entity.id)); +} + +ListUIState _clearListMultiselect( + ListUIState productListState, ClearDesignMultiselect action) { + return productListState.rebuild((b) => b..selectedIds = null); +} + +final designsReducer = combineReducers([ + TypedReducer(_updateDesign), + TypedReducer(_addDesign), + TypedReducer(_setLoadedDesigns), + TypedReducer(_setLoadedDesign), + TypedReducer(_archiveDesignRequest), + TypedReducer(_archiveDesignSuccess), + TypedReducer(_archiveDesignFailure), + TypedReducer(_deleteDesignRequest), + TypedReducer(_deleteDesignSuccess), + TypedReducer(_deleteDesignFailure), + TypedReducer(_restoreDesignRequest), + TypedReducer(_restoreDesignSuccess), + TypedReducer(_restoreDesignFailure), +]); + +DesignState _archiveDesignRequest( + DesignState designState, ArchiveDesignsRequest action) { + final designs = action.designIds.map((id) => designState.map[id]).toList(); + + for (int i = 0; i < designs.length; i++) { + designs[i] = designs[i] + .rebuild((b) => b..archivedAt = DateTime.now().millisecondsSinceEpoch); + } + return designState.rebuild((b) { + for (final design in designs) { + b.map[design.id] = design; + } + }); +} + +DesignState _archiveDesignSuccess( + DesignState designState, ArchiveDesignsSuccess action) { + return designState.rebuild((b) { + for (final design in action.designs) { + b.map[design.id] = design; + } + }); +} + +DesignState _archiveDesignFailure( + DesignState designState, ArchiveDesignsFailure action) { + return designState.rebuild((b) { + for (final design in action.designs) { + b.map[design.id] = design; + } + }); +} + +DesignState _deleteDesignRequest( + DesignState designState, DeleteDesignsRequest action) { + final designs = action.designIds.map((id) => designState.map[id]).toList(); + + for (int i = 0; i < designs.length; i++) { + designs[i] = designs[i].rebuild((b) => b + ..archivedAt = DateTime.now().millisecondsSinceEpoch + ..isDeleted = true); + } + return designState.rebuild((b) { + for (final design in designs) { + b.map[design.id] = design; + } + }); +} + +DesignState _deleteDesignSuccess( + DesignState designState, DeleteDesignsSuccess action) { + return designState.rebuild((b) { + for (final design in action.designs) { + b.map[design.id] = design; + } + }); +} + +DesignState _deleteDesignFailure( + DesignState designState, DeleteDesignsFailure action) { + return designState.rebuild((b) { + for (final design in action.designs) { + b.map[design.id] = design; + } + }); +} + +DesignState _restoreDesignRequest( + DesignState designState, RestoreDesignsRequest action) { + final designs = action.designIds.map((id) => designState.map[id]).toList(); + + for (int i = 0; i < designs.length; i++) { + designs[i] = designs[i].rebuild((b) => b + ..archivedAt = null + ..isDeleted = false); + } + return designState.rebuild((b) { + for (final design in designs) { + b.map[design.id] = design; + } + }); +} + +DesignState _restoreDesignSuccess( + DesignState designState, RestoreDesignsSuccess action) { + return designState.rebuild((b) { + for (final design in action.designs) { + b.map[design.id] = design; + } + }); +} + +DesignState _restoreDesignFailure( + DesignState designState, RestoreDesignsFailure action) { + return designState.rebuild((b) { + for (final design in action.designs) { + b.map[design.id] = design; + } + }); +} + +DesignState _addDesign(DesignState designState, AddDesignSuccess action) { + return designState.rebuild((b) => b + ..map[action.design.id] = action.design + ..list.add(action.design.id)); +} + +DesignState _updateDesign(DesignState designState, SaveDesignSuccess action) { + return designState.rebuild((b) => b..map[action.design.id] = action.design); +} + +DesignState _setLoadedDesign( + DesignState designState, LoadDesignSuccess action) { + return designState.rebuild((b) => b..map[action.design.id] = action.design); +} + +DesignState _setLoadedDesigns( + DesignState designState, LoadDesignsSuccess action) => + designState.loadDesigns(action.designs); diff --git a/lib/redux/design/design_selectors.dart b/lib/redux/design/design_selectors.dart new file mode 100644 index 000000000..6ce255a1c --- /dev/null +++ b/lib/redux/design/design_selectors.dart @@ -0,0 +1,65 @@ +import 'package:invoiceninja_flutter/data/models/design_model.dart'; +import 'package:memoize/memoize.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; + +var memoizedDropdownDesignList = memo3( + (BuiltMap designMap, BuiltList designList, + String clientId) => + dropdownDesignsSelector(designMap, designList, clientId)); + +List dropdownDesignsSelector(BuiltMap designMap, + BuiltList designList, String clientId) { + final list = designList.where((designId) { + final design = designMap[designId]; + /* + if (clientId != null && clientId > 0 && design.clientId != clientId) { + return false; + } + */ + return design.isActive; + }).toList(); + + list.sort((designAId, designBId) { + final designA = designMap[designAId]; + final designB = designMap[designBId]; + return designA.compareTo(designB, DesignFields.name, true); + }); + + return list; +} + +var memoizedFilteredDesignList = memo3( + (BuiltMap designMap, BuiltList designList, + ListUIState designListState) => + filteredDesignsSelector(designMap, designList, designListState)); + +List filteredDesignsSelector(BuiltMap designMap, + BuiltList designList, ListUIState designListState) { + final list = designList.where((designId) { + final design = designMap[designId]; + if (designListState.filterEntityId != null && + design.entityId != designListState.filterEntityId) { + return false; + } else {} + + if (!design.matchesStates(designListState.stateFilters)) { + return false; + } + return design.matchesFilter(designListState.filter); + }).toList(); + + list.sort((designAId, designBId) { + final designA = designMap[designAId]; + final designB = designMap[designBId]; + return designA.compareTo( + designB, designListState.sortField, designListState.sortAscending); + }); + + return list; +} + +bool hasDesignChanges( + DesignEntity design, BuiltMap designMap) => + design.isNew ? design.isChanged : design != designMap[design.id]; diff --git a/lib/redux/design/design_state.dart b/lib/redux/design/design_state.dart new file mode 100644 index 000000000..f2fdde8dc --- /dev/null +++ b/lib/redux/design/design_state.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/design_model.dart'; +import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; + +part 'design_state.g.dart'; + +abstract class DesignState implements Built { + factory DesignState() { + return _$DesignState._( + lastUpdated: 0, + map: BuiltMap(), + list: BuiltList(), + ); + } + DesignState._(); + + @nullable + int get lastUpdated; + + BuiltMap get map; + BuiltList get list; + + bool get isStale { + if (!isLoaded) { + return true; + } + + return DateTime.now().millisecondsSinceEpoch - lastUpdated > + kMillisecondsToRefreshData; + } + + bool get isLoaded => lastUpdated != null && lastUpdated > 0; + + DesignState loadDesigns(BuiltList clients) { + final map = Map.fromIterable( + clients, + key: (dynamic item) => item.id, + value: (dynamic item) => item, + ); + + return rebuild((b) => b + ..lastUpdated = DateTime.now().millisecondsSinceEpoch + ..map.addAll(map) + ..list.replace(map.keys)); + } + + static Serializer get serializer => _$designStateSerializer; +} + +abstract class DesignUIState extends Object + with EntityUIState + implements Built { + factory DesignUIState() { + return _$DesignUIState._( + listUIState: ListUIState(DesignFields.name), + editing: DesignEntity(), + selectedId: '', + ); + } + DesignUIState._(); + + @nullable + DesignEntity get editing; + + @override + bool get isCreatingNew => editing.isNew; + + static Serializer get serializer => _$designUIStateSerializer; +} diff --git a/lib/redux/design/design_state.g.dart b/lib/redux/design/design_state.g.dart new file mode 100644 index 000000000..e84fd1262 --- /dev/null +++ b/lib/redux/design/design_state.g.dart @@ -0,0 +1,406 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'design_state.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$designStateSerializer = new _$DesignStateSerializer(); +Serializer _$designUIStateSerializer = + new _$DesignUIStateSerializer(); + +class _$DesignStateSerializer implements StructuredSerializer { + @override + final Iterable types = const [DesignState, _$DesignState]; + @override + final String wireName = 'DesignState'; + + @override + Iterable serialize(Serializers serializers, DesignState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'map', + serializers.serialize(object.map, + specifiedType: const FullType(BuiltMap, + const [const FullType(String), const FullType(DesignEntity)])), + 'list', + serializers.serialize(object.list, + specifiedType: + const FullType(BuiltList, const [const FullType(String)])), + ]; + if (object.lastUpdated != null) { + result + ..add('lastUpdated') + ..add(serializers.serialize(object.lastUpdated, + specifiedType: const FullType(int))); + } + return result; + } + + @override + DesignState deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new DesignStateBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'lastUpdated': + result.lastUpdated = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'map': + result.map.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltMap, const [ + const FullType(String), + const FullType(DesignEntity) + ]))); + break; + case 'list': + result.list.replace(serializers.deserialize(value, + specifiedType: + const FullType(BuiltList, const [const FullType(String)])) + as BuiltList); + break; + } + } + + return result.build(); + } +} + +class _$DesignUIStateSerializer implements StructuredSerializer { + @override + final Iterable types = const [DesignUIState, _$DesignUIState]; + @override + final String wireName = 'DesignUIState'; + + @override + Iterable serialize(Serializers serializers, DesignUIState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'listUIState', + serializers.serialize(object.listUIState, + specifiedType: const FullType(ListUIState)), + ]; + if (object.editing != null) { + result + ..add('editing') + ..add(serializers.serialize(object.editing, + specifiedType: const FullType(DesignEntity))); + } + if (object.selectedId != null) { + result + ..add('selectedId') + ..add(serializers.serialize(object.selectedId, + specifiedType: const FullType(String))); + } + return result; + } + + @override + DesignUIState deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new DesignUIStateBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'editing': + result.editing.replace(serializers.deserialize(value, + specifiedType: const FullType(DesignEntity)) as DesignEntity); + break; + case 'listUIState': + result.listUIState.replace(serializers.deserialize(value, + specifiedType: const FullType(ListUIState)) as ListUIState); + break; + case 'selectedId': + result.selectedId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + } + } + + return result.build(); + } +} + +class _$DesignState extends DesignState { + @override + final int lastUpdated; + @override + final BuiltMap map; + @override + final BuiltList list; + + factory _$DesignState([void Function(DesignStateBuilder) updates]) => + (new DesignStateBuilder()..update(updates)).build(); + + _$DesignState._({this.lastUpdated, this.map, this.list}) : super._() { + if (map == null) { + throw new BuiltValueNullFieldError('DesignState', 'map'); + } + if (list == null) { + throw new BuiltValueNullFieldError('DesignState', 'list'); + } + } + + @override + DesignState rebuild(void Function(DesignStateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DesignStateBuilder toBuilder() => new DesignStateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DesignState && + lastUpdated == other.lastUpdated && + map == other.map && + list == other.list; + } + + @override + int get hashCode { + return $jf( + $jc($jc($jc(0, lastUpdated.hashCode), map.hashCode), list.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('DesignState') + ..add('lastUpdated', lastUpdated) + ..add('map', map) + ..add('list', list)) + .toString(); + } +} + +class DesignStateBuilder implements Builder { + _$DesignState _$v; + + int _lastUpdated; + int get lastUpdated => _$this._lastUpdated; + set lastUpdated(int lastUpdated) => _$this._lastUpdated = lastUpdated; + + MapBuilder _map; + MapBuilder get map => + _$this._map ??= new MapBuilder(); + set map(MapBuilder map) => _$this._map = map; + + ListBuilder _list; + ListBuilder get list => _$this._list ??= new ListBuilder(); + set list(ListBuilder list) => _$this._list = list; + + DesignStateBuilder(); + + DesignStateBuilder get _$this { + if (_$v != null) { + _lastUpdated = _$v.lastUpdated; + _map = _$v.map?.toBuilder(); + _list = _$v.list?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(DesignState other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$DesignState; + } + + @override + void update(void Function(DesignStateBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$DesignState build() { + _$DesignState _$result; + try { + _$result = _$v ?? + new _$DesignState._( + lastUpdated: lastUpdated, map: map.build(), list: list.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'map'; + map.build(); + _$failedField = 'list'; + list.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'DesignState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$DesignUIState extends DesignUIState { + @override + final DesignEntity editing; + @override + final ListUIState listUIState; + @override + final String selectedId; + @override + final Completer saveCompleter; + @override + final Completer cancelCompleter; + + factory _$DesignUIState([void Function(DesignUIStateBuilder) updates]) => + (new DesignUIStateBuilder()..update(updates)).build(); + + _$DesignUIState._( + {this.editing, + this.listUIState, + this.selectedId, + this.saveCompleter, + this.cancelCompleter}) + : super._() { + if (listUIState == null) { + throw new BuiltValueNullFieldError('DesignUIState', 'listUIState'); + } + } + + @override + DesignUIState rebuild(void Function(DesignUIStateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DesignUIStateBuilder toBuilder() => new DesignUIStateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DesignUIState && + editing == other.editing && + listUIState == other.listUIState && + selectedId == other.selectedId && + saveCompleter == other.saveCompleter && + cancelCompleter == other.cancelCompleter; + } + + @override + int get hashCode { + return $jf($jc( + $jc( + $jc($jc($jc(0, editing.hashCode), listUIState.hashCode), + selectedId.hashCode), + saveCompleter.hashCode), + cancelCompleter.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('DesignUIState') + ..add('editing', editing) + ..add('listUIState', listUIState) + ..add('selectedId', selectedId) + ..add('saveCompleter', saveCompleter) + ..add('cancelCompleter', cancelCompleter)) + .toString(); + } +} + +class DesignUIStateBuilder + implements Builder { + _$DesignUIState _$v; + + DesignEntityBuilder _editing; + DesignEntityBuilder get editing => + _$this._editing ??= new DesignEntityBuilder(); + set editing(DesignEntityBuilder editing) => _$this._editing = editing; + + ListUIStateBuilder _listUIState; + ListUIStateBuilder get listUIState => + _$this._listUIState ??= new ListUIStateBuilder(); + set listUIState(ListUIStateBuilder listUIState) => + _$this._listUIState = listUIState; + + String _selectedId; + String get selectedId => _$this._selectedId; + set selectedId(String selectedId) => _$this._selectedId = selectedId; + + Completer _saveCompleter; + Completer get saveCompleter => _$this._saveCompleter; + set saveCompleter(Completer saveCompleter) => + _$this._saveCompleter = saveCompleter; + + Completer _cancelCompleter; + Completer get cancelCompleter => _$this._cancelCompleter; + set cancelCompleter(Completer cancelCompleter) => + _$this._cancelCompleter = cancelCompleter; + + DesignUIStateBuilder(); + + DesignUIStateBuilder get _$this { + if (_$v != null) { + _editing = _$v.editing?.toBuilder(); + _listUIState = _$v.listUIState?.toBuilder(); + _selectedId = _$v.selectedId; + _saveCompleter = _$v.saveCompleter; + _cancelCompleter = _$v.cancelCompleter; + _$v = null; + } + return this; + } + + @override + void replace(DesignUIState other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$DesignUIState; + } + + @override + void update(void Function(DesignUIStateBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$DesignUIState build() { + _$DesignUIState _$result; + try { + _$result = _$v ?? + new _$DesignUIState._( + editing: _editing?.build(), + listUIState: listUIState.build(), + selectedId: selectedId, + saveCompleter: saveCompleter, + cancelCompleter: cancelCompleter); + } catch (_) { + String _$failedField; + try { + _$failedField = 'editing'; + _editing?.build(); + _$failedField = 'listUIState'; + listUIState.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'DesignUIState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new diff --git a/lib/redux/ui/pref_reducer.dart b/lib/redux/ui/pref_reducer.dart index 992c25844..ed6ef1d27 100644 --- a/lib/redux/ui/pref_reducer.dart +++ b/lib/redux/ui/pref_reducer.dart @@ -23,6 +23,8 @@ import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; import 'package:invoiceninja_flutter/redux/user/user_actions.dart'; import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; + import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; import 'package:redux/redux.dart'; @@ -397,6 +399,13 @@ Reducer> historyReducer = combineReducers([ _addToHistory(historyList, HistoryRecord(id: action.group.id, entityType: EntityType.group))), // STARTER: history - do not remove comment + TypedReducer, ViewDesign>((historyList, action) => + _addToHistory(historyList, + HistoryRecord(id: action.designId, entityType: EntityType.design))), + TypedReducer, EditDesign>((historyList, action) => + _addToHistory(historyList, + HistoryRecord(id: action.design.id, entityType: EntityType.design))), + TypedReducer, ViewCredit>((historyList, action) => _addToHistory(historyList, HistoryRecord(id: action.creditId, entityType: EntityType.credit))), diff --git a/lib/redux/ui/ui_reducer.dart b/lib/redux/ui/ui_reducer.dart index c6630d0fd..e431b2d73 100644 --- a/lib/redux/ui/ui_reducer.dart +++ b/lib/redux/ui/ui_reducer.dart @@ -22,6 +22,8 @@ import 'package:invoiceninja_flutter/redux/quote/quote_reducer.dart'; import 'package:invoiceninja_flutter/redux/task/task_reducer.dart'; import 'package:invoiceninja_flutter/redux/vendor/vendor_reducer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/design/design_reducer.dart'; + import 'package:invoiceninja_flutter/redux/credit/credit_reducer.dart'; import 'package:invoiceninja_flutter/redux/user/user_reducer.dart'; @@ -49,6 +51,7 @@ UIState uiReducer(UIState state, dynamic action) { .replace(dashboardUIReducer(state.dashboardUIState, action)) ..reportsUIState.replace(reportsUIReducer(state.reportsUIState, action)) // STARTER: reducer - do not remove comment + ..designUIState.replace(designUIReducer(state.designUIState, action)) ..creditUIState.replace(creditUIReducer(state.creditUIState, action)) ..userUIState.replace(userUIReducer(state.userUIState, action)) ..taxRateUIState.replace(taxRateUIReducer(state.taxRateUIState, action)) diff --git a/lib/redux/ui/ui_state.dart b/lib/redux/ui/ui_state.dart index cc290ee69..5f16f6d46 100644 --- a/lib/redux/ui/ui_state.dart +++ b/lib/redux/ui/ui_state.dart @@ -16,6 +16,8 @@ import 'package:invoiceninja_flutter/redux/task/task_state.dart'; import 'package:invoiceninja_flutter/redux/vendor/vendor_state.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/design/design_state.dart'; + import 'package:invoiceninja_flutter/redux/credit/credit_state.dart'; import 'package:invoiceninja_flutter/redux/user/user_state.dart'; @@ -37,6 +39,8 @@ abstract class UIState implements Built { clientUIState: ClientUIState(), invoiceUIState: InvoiceUIState(), // STARTER: constructor - do not remove comment + designUIState: DesignUIState(), + creditUIState: CreditUIState(), userUIState: UserUIState(), @@ -77,6 +81,8 @@ abstract class UIState implements Built { InvoiceUIState get invoiceUIState; // STARTER: properties - do not remove comment + DesignUIState get designUIState; + CreditUIState get creditUIState; UserUIState get userUIState; diff --git a/lib/redux/ui/ui_state.g.dart b/lib/redux/ui/ui_state.g.dart index a510d0d4c..dd2eafdf1 100644 --- a/lib/redux/ui/ui_state.g.dart +++ b/lib/redux/ui/ui_state.g.dart @@ -42,6 +42,9 @@ class _$UIStateSerializer implements StructuredSerializer { 'invoiceUIState', serializers.serialize(object.invoiceUIState, specifiedType: const FullType(InvoiceUIState)), + 'designUIState', + serializers.serialize(object.designUIState, + specifiedType: const FullType(DesignUIState)), 'creditUIState', serializers.serialize(object.creditUIState, specifiedType: const FullType(CreditUIState)), @@ -142,6 +145,10 @@ class _$UIStateSerializer implements StructuredSerializer { result.invoiceUIState.replace(serializers.deserialize(value, specifiedType: const FullType(InvoiceUIState)) as InvoiceUIState); break; + case 'designUIState': + result.designUIState.replace(serializers.deserialize(value, + specifiedType: const FullType(DesignUIState)) as DesignUIState); + break; case 'creditUIState': result.creditUIState.replace(serializers.deserialize(value, specifiedType: const FullType(CreditUIState)) as CreditUIState); @@ -228,6 +235,8 @@ class _$UIState extends UIState { @override final InvoiceUIState invoiceUIState; @override + final DesignUIState designUIState; + @override final CreditUIState creditUIState; @override final UserUIState userUIState; @@ -269,6 +278,7 @@ class _$UIState extends UIState { this.productUIState, this.clientUIState, this.invoiceUIState, + this.designUIState, this.creditUIState, this.userUIState, this.taxRateUIState, @@ -308,6 +318,9 @@ class _$UIState extends UIState { if (invoiceUIState == null) { throw new BuiltValueNullFieldError('UIState', 'invoiceUIState'); } + if (designUIState == null) { + throw new BuiltValueNullFieldError('UIState', 'designUIState'); + } if (creditUIState == null) { throw new BuiltValueNullFieldError('UIState', 'creditUIState'); } @@ -372,6 +385,7 @@ class _$UIState extends UIState { productUIState == other.productUIState && clientUIState == other.clientUIState && invoiceUIState == other.invoiceUIState && + designUIState == other.designUIState && creditUIState == other.creditUIState && userUIState == other.userUIState && taxRateUIState == other.taxRateUIState && @@ -408,12 +422,12 @@ class _$UIState extends UIState { $jc( $jc( $jc( - $jc($jc($jc($jc($jc(0, selectedCompanyIndex.hashCode), currentRoute.hashCode), previousRoute.hashCode), filter.hashCode), - filterClearedAt.hashCode), - dashboardUIState.hashCode), - productUIState.hashCode), - clientUIState.hashCode), - invoiceUIState.hashCode), + $jc($jc($jc($jc($jc($jc(0, selectedCompanyIndex.hashCode), currentRoute.hashCode), previousRoute.hashCode), filter.hashCode), filterClearedAt.hashCode), + dashboardUIState.hashCode), + productUIState.hashCode), + clientUIState.hashCode), + invoiceUIState.hashCode), + designUIState.hashCode), creditUIState.hashCode), userUIState.hashCode), taxRateUIState.hashCode), @@ -442,6 +456,7 @@ class _$UIState extends UIState { ..add('productUIState', productUIState) ..add('clientUIState', clientUIState) ..add('invoiceUIState', invoiceUIState) + ..add('designUIState', designUIState) ..add('creditUIState', creditUIState) ..add('userUIState', userUIState) ..add('taxRateUIState', taxRateUIState) @@ -510,6 +525,12 @@ class UIStateBuilder implements Builder { set invoiceUIState(InvoiceUIStateBuilder invoiceUIState) => _$this._invoiceUIState = invoiceUIState; + DesignUIStateBuilder _designUIState; + DesignUIStateBuilder get designUIState => + _$this._designUIState ??= new DesignUIStateBuilder(); + set designUIState(DesignUIStateBuilder designUIState) => + _$this._designUIState = designUIState; + CreditUIStateBuilder _creditUIState; CreditUIStateBuilder get creditUIState => _$this._creditUIState ??= new CreditUIStateBuilder(); @@ -608,6 +629,7 @@ class UIStateBuilder implements Builder { _productUIState = _$v.productUIState?.toBuilder(); _clientUIState = _$v.clientUIState?.toBuilder(); _invoiceUIState = _$v.invoiceUIState?.toBuilder(); + _designUIState = _$v.designUIState?.toBuilder(); _creditUIState = _$v.creditUIState?.toBuilder(); _userUIState = _$v.userUIState?.toBuilder(); _taxRateUIState = _$v.taxRateUIState?.toBuilder(); @@ -655,6 +677,7 @@ class UIStateBuilder implements Builder { productUIState: productUIState.build(), clientUIState: clientUIState.build(), invoiceUIState: invoiceUIState.build(), + designUIState: designUIState.build(), creditUIState: creditUIState.build(), userUIState: userUIState.build(), taxRateUIState: taxRateUIState.build(), @@ -680,6 +703,8 @@ class UIStateBuilder implements Builder { clientUIState.build(); _$failedField = 'invoiceUIState'; invoiceUIState.build(); + _$failedField = 'designUIState'; + designUIState.build(); _$failedField = 'creditUIState'; creditUIState.build(); _$failedField = 'userUIState'; diff --git a/lib/ui/app/menu_drawer.dart b/lib/ui/app/menu_drawer.dart index 3a19002a2..66af0b5f8 100644 --- a/lib/ui/app/menu_drawer.dart +++ b/lib/ui/app/menu_drawer.dart @@ -28,6 +28,8 @@ import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:url_launcher/url_launcher.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; + class MenuDrawer extends StatelessWidget { const MenuDrawer({ @@ -256,6 +258,14 @@ class MenuDrawer extends StatelessWidget { title: localization.expenses, ), // STARTER: menu - do not remove comment +DrawerTile( +company: company, +entityType: EntityType.design, +icon: getEntityIcon(EntityType.design), +title: localization.designs, +}, +), + DrawerTile( company: company, icon: getEntityIcon(EntityType.reports), diff --git a/lib/ui/credit/credit_list_vm.dart b/lib/ui/credit/credit_list_vm.dart index b3a965f99..9ac28aa0f 100644 --- a/lib/ui/credit/credit_list_vm.dart +++ b/lib/ui/credit/credit_list_vm.dart @@ -54,22 +54,22 @@ class CreditListVM extends EntityListVM { List tableColumns, EntityType entityType, }) : super( - state: state, - user: user, - listState: listState, - invoiceList: invoiceList, - invoiceMap: invoiceMap, - clientMap: clientMap, - filter: filter, - isLoading: isLoading, - isLoaded: isLoaded, - onInvoiceTap: onInvoiceTap, - onRefreshed: onRefreshed, - onClearEntityFilterPressed: onClearEntityFilterPressed, - onViewEntityFilterPressed: onViewEntityFilterPressed, - tableColumns: tableColumns, - entityType: entityType, - ); + state: state, + user: user, + listState: listState, + invoiceList: invoiceList, + invoiceMap: invoiceMap, + clientMap: clientMap, + filter: filter, + isLoading: isLoading, + isLoaded: isLoaded, + onInvoiceTap: onInvoiceTap, + onRefreshed: onRefreshed, + onClearEntityFilterPressed: onClearEntityFilterPressed, + onViewEntityFilterPressed: onViewEntityFilterPressed, + tableColumns: tableColumns, + entityType: entityType, + ); static CreditListVM fromStore(Store store) { Future _handleRefresh(BuildContext context) { @@ -105,7 +105,7 @@ class CreditListVM extends EntityListVM { entityId: state.creditListState.filterEntityId, entityType: state.creditListState.filterEntityType), onEntityAction: (BuildContext context, List credits, - EntityAction action) => + EntityAction action) => handleCreditAction(context, credits, action), tableColumns: CreditPresenter.getTableFields(state.userCompany), entityType: EntityType.credit, diff --git a/lib/ui/credit/credit_presenter.dart b/lib/ui/credit/credit_presenter.dart index 1acaa3056..131e40791 100644 --- a/lib/ui/credit/credit_presenter.dart +++ b/lib/ui/credit/credit_presenter.dart @@ -29,7 +29,7 @@ class CreditPresenter extends EntityPresenter { return Text(credit.number); case CreditFields.client: return Text((state.clientState.map[credit.clientId] ?? - ClientEntity(id: credit.clientId)) + ClientEntity(id: credit.clientId)) .listDisplayName); case CreditFields.date: return Text(formatDate(credit.date, context)); diff --git a/lib/ui/credit/edit/credit_edit.dart b/lib/ui/credit/edit/credit_edit.dart index 4a5819701..4a2933352 100644 --- a/lib/ui/credit/edit/credit_edit.dart +++ b/lib/ui/credit/edit/credit_edit.dart @@ -26,7 +26,7 @@ class _CreditEditState extends State TabController _controller; static final GlobalKey _formKey = - GlobalKey(debugLabel: '_creditEdit'); + GlobalKey(debugLabel: '_creditEdit'); static const kDetailsScreen = 0; static const kItemScreen = 1; @@ -39,7 +39,7 @@ class _CreditEditState extends State final viewModel = widget.viewModel; final index = - viewModel.invoiceItemIndex != null ? kItemScreen : kDetailsScreen; + viewModel.invoiceItemIndex != null ? kItemScreen : kDetailsScreen; _controller = TabController(vsync: this, length: 3, initialIndex: index); } @@ -79,33 +79,33 @@ class _CreditEditState extends State appBarBottom: state.prefState.isDesktop ? null : TabBar( - controller: _controller, - //isScrollable: true, - tabs: [ - Tab( - text: localization.details, - ), - Tab( - text: localization.items, - ), - Tab( - text: localization.notes, - ), - ], - ), + controller: _controller, + //isScrollable: true, + tabs: [ + Tab( + text: localization.details, + ), + Tab( + text: localization.items, + ), + Tab( + text: localization.notes, + ), + ], + ), body: Form( key: _formKey, child: state.prefState.isDesktop ? CreditEditDetailsScreen() : TabBarView( - key: ValueKey('__credit_${viewModel.invoice.id}__'), - controller: _controller, - children: [ - CreditEditDetailsScreen(), - CreditEditItemsScreen(), - CreditEditNotesScreen(), - ], - ), + key: ValueKey('__credit_${viewModel.invoice.id}__'), + controller: _controller, + children: [ + CreditEditDetailsScreen(), + CreditEditItemsScreen(), + CreditEditNotesScreen(), + ], + ), ), bottomNavigationBar: BottomAppBar( color: Theme.of(context).primaryColor, @@ -133,8 +133,8 @@ class _CreditEditState extends State excluded: invoice.lineItems .where((item) => item.isTask || item.isExpense) .map((item) => item.isTask - ? viewModel.state.taskState.map[item.taskId] - : viewModel.state.expenseState.map[item.expenseId]) + ? viewModel.state.taskState.map[item.taskId] + : viewModel.state.expenseState.map[item.expenseId]) .toList(), clientId: invoice.clientId, onItemsSelected: (items, [clientId]) { diff --git a/lib/ui/credit/edit/credit_edit_items_vm.dart b/lib/ui/credit/edit/credit_edit_items_vm.dart index 466a8274f..967ec969e 100644 --- a/lib/ui/credit/edit/credit_edit_items_vm.dart +++ b/lib/ui/credit/edit/credit_edit_items_vm.dart @@ -74,7 +74,8 @@ class CreditEditItemsVM extends EntityEditItemsVM { if (index == credit.lineItems.length) { store.dispatch(AddCreditItem(creditItem: creditItem)); } else { - store.dispatch(UpdateCreditItem(creditItem: creditItem, index: index)); + store.dispatch( + UpdateCreditItem(creditItem: creditItem, index: index)); } }); } diff --git a/lib/ui/credit/edit/credit_edit_vm.dart b/lib/ui/credit/edit/credit_edit_vm.dart index 474f0682a..3f1ac8951 100644 --- a/lib/ui/credit/edit/credit_edit_vm.dart +++ b/lib/ui/credit/edit/credit_edit_vm.dart @@ -46,16 +46,16 @@ class CreditEditVM extends EntityEditVM { bool isSaving, Function(BuildContext) onCancelPressed, }) : super( - state: state, - company: company, - invoice: invoice, - invoiceItemIndex: invoiceItemIndex, - origInvoice: origInvoice, - onSavePressed: onSavePressed, - onItemsAdded: onItemsAdded, - isSaving: isSaving, - onCancelPressed: onCancelPressed, - ); + state: state, + company: company, + invoice: invoice, + invoiceItemIndex: invoiceItemIndex, + origInvoice: origInvoice, + onSavePressed: onSavePressed, + onItemsAdded: onItemsAdded, + isSaving: isSaving, + onCancelPressed: onCancelPressed, + ); factory CreditEditVM.fromStore(Store store) { final AppState state = store.state; @@ -75,7 +75,8 @@ class CreditEditVM extends EntityEditVM { if (isMobile(context)) { store.dispatch(UpdateCurrentRoute(CreditViewScreen.route)); if (credit.isNew) { - Navigator.of(context).pushReplacementNamed(CreditViewScreen.route); + Navigator.of(context) + .pushReplacementNamed(CreditViewScreen.route); } else { Navigator.of(context).pop(savedCredit); } diff --git a/lib/ui/credit/view/credit_view_vm.dart b/lib/ui/credit/view/credit_view_vm.dart index 6243322c4..e1e96d08b 100644 --- a/lib/ui/credit/view/credit_view_vm.dart +++ b/lib/ui/credit/view/credit_view_vm.dart @@ -56,22 +56,22 @@ class CreditViewVM extends EntityViewVM { Function(BuildContext, DocumentEntity) onDeleteDocument, Function(BuildContext, DocumentEntity) onViewExpense, }) : super( - state: state, - company: company, - invoice: invoice, - client: client, - isSaving: isSaving, - isDirty: isDirty, - onActionSelected: onEntityAction, - onEditPressed: onEditPressed, - onClientPressed: onClientPressed, - onPaymentsPressed: onPaymentsPressed, - onPaymentPressed: onPaymentPressed, - onRefreshed: onRefreshed, - onUploadDocument: onUploadDocument, - onDeleteDocument: onDeleteDocument, - onViewExpense: onViewExpense, - ); + state: state, + company: company, + invoice: invoice, + client: client, + isSaving: isSaving, + isDirty: isDirty, + onActionSelected: onEntityAction, + onEditPressed: onEditPressed, + onClientPressed: onClientPressed, + onPaymentsPressed: onPaymentsPressed, + onPaymentPressed: onPaymentPressed, + onRefreshed: onRefreshed, + onUploadDocument: onUploadDocument, + onDeleteDocument: onDeleteDocument, + onViewExpense: onViewExpense, + ); factory CreditViewVM.fromStore(Store store) { final state = store.state; @@ -125,8 +125,8 @@ class CreditViewVM extends EntityViewVM { completer.future.then((client) { Scaffold.of(context).showSnackBar(SnackBar( content: SnackBarRow( - message: AppLocalization.of(context).uploadedDocument, - ))); + message: AppLocalization.of(context).uploadedDocument, + ))); }).catchError((Object error) { showDialog( context: context, diff --git a/lib/ui/design/design_list.dart b/lib/ui/design/design_list.dart new file mode 100644 index 000000000..211257835 --- /dev/null +++ b/lib/ui/design/design_list.dart @@ -0,0 +1,167 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/pref_state.dart'; +import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; +import 'package:invoiceninja_flutter/ui/app/help_text.dart'; +import 'package:invoiceninja_flutter/ui/app/lists/list_divider.dart'; +import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart'; +import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; +import 'package:invoiceninja_flutter/ui/design/design_presenter.dart'; +import 'package:invoiceninja_flutter/ui/app/tables/entity_datatable.dart'; +import 'package:invoiceninja_flutter/ui/design/design_list_item.dart'; +import 'package:invoiceninja_flutter/ui/design/design_list_vm.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class DesignList extends StatefulWidget { + const DesignList({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final DesignListVM viewModel; + + @override + _DesignListState createState() => _DesignListState(); +} + +class _DesignListState extends State { + EntityDataTableSource dataTableSource; + + @override + void initState() { + super.initState(); + + final viewModel = widget.viewModel; + + dataTableSource = EntityDataTableSource( + context: context, + entityType: EntityType.design, + editingId: viewModel.state.designUIState.editing.id, + tableColumns: viewModel.tableColumns, + entityList: viewModel.designList, + entityMap: viewModel.designMap, + entityPresenter: DesignPresenter(), + onTap: (BaseEntity design) => viewModel.onDesignTap(context, design)); + } + + @override + void didUpdateWidget(DesignList oldWidget) { + super.didUpdateWidget(oldWidget); + + final viewModel = widget.viewModel; + dataTableSource.editingId = viewModel.state.designUIState.editing.id; + dataTableSource.entityList = viewModel.designList; + dataTableSource.entityMap = viewModel.designMap; + + // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member + dataTableSource.notifyListeners(); + } + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final viewModel = widget.viewModel; + final state = viewModel.state; + final listUIState = state.uiState.designUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); + final isList = state.prefState.moduleLayout == ModuleLayout.list; + final designList = viewModel.designList; + + if (!viewModel.isLoaded) { + return viewModel.isLoading ? LoadingIndicator() : SizedBox(); + } else if (viewModel.designMap.isEmpty) { + return HelpText(AppLocalization.of(context).noRecordsFound); + } + + if (state.shouldSelectEntity( + entityType: EntityType.design, hasRecords: designList.isNotEmpty)) { + viewEntityById( + context: context, + entityType: EntityType.design, + entityId: designList.isEmpty ? null : designList.first, + ); + } + + final listOrTable = () { + if (isList) { + return ListView.separated( + separatorBuilder: (context, index) => ListDivider(), + itemCount: viewModel.designList.length, + itemBuilder: (BuildContext context, index) { + final designId = viewModel.designList[index]; + final design = viewModel.designMap[designId]; + + return DesignListItem( + user: viewModel.state.user, + filter: viewModel.filter, + design: design, + onEntityAction: (EntityAction action) { + if (action == EntityAction.more) { + showEntityActionsDialog( + entities: [design], + context: context, + ); + } else { + handleDesignAction(context, [design], action); + } + }, + onTap: () => viewModel.onDesignTap(context, design), + onLongPress: () async { + final longPressIsSelection = + state.prefState.longPressSelectionIsDefault ?? true; + if (longPressIsSelection && !isInMultiselect) { + handleDesignAction( + context, [design], EntityAction.toggleMultiselect); + } else { + showEntityActionsDialog( + entities: [design], + context: context, + ); + } + }, + isChecked: isInMultiselect && listUIState.isSelected(design.id), + ); + }); + } else { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(12), + child: PaginatedDataTable( + onSelectAll: (value) { + final designs = viewModel.designList + .map( + (designId) => viewModel.designMap[designId]) + .where((design) => value != listUIState.isSelected(design.id)) + .toList(); + handleDesignAction( + context, designs, EntityAction.toggleMultiselect); + }, + columns: [ + if (!listUIState.isInMultiselect()) DataColumn(label: SizedBox()), + ...viewModel.tableColumns.map((field) => DataColumn( + label: Text(AppLocalization.of(context).lookup(field)), + numeric: EntityPresenter.isFieldNumeric(field), + onSort: (int columnIndex, bool ascending) => + store.dispatch(SortDesigns(field)))), + ], + source: dataTableSource, + header: DatatableHeader( + entityType: EntityType.design, + onClearPressed: viewModel.onClearEntityFilterPressed, + ), + ), + )); + } + }; + + return RefreshIndicator( + onRefresh: () => viewModel.onRefreshed(context), + child: listOrTable(), + ); + } +} diff --git a/lib/ui/design/design_list_item.dart b/lib/ui/design/design_list_item.dart new file mode 100644 index 000000000..5d179faab --- /dev/null +++ b/lib/ui/design/design_list_item.dart @@ -0,0 +1,104 @@ +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/design_model.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/ui/app/entity_state_label.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/dismissible_entity.dart'; + +class DesignListItem extends StatelessWidget { + const DesignListItem({ + @required this.user, + @required this.onEntityAction, + @required this.onTap, + @required this.onLongPress, + @required this.design, + @required this.filter, + this.onCheckboxChanged, + this.isChecked = false, + }); + + final UserEntity user; + final Function(EntityAction) onEntityAction; + final GestureTapCallback onTap; + final GestureTapCallback onLongPress; + final DesignEntity design; + final String filter; + final Function(bool) onCheckboxChanged; + final bool isChecked; + + static final designItemKey = (int id) => Key('__design_item_${id}__'); + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final state = store.state; + final uiState = state.uiState; + final designUIState = uiState.designUIState; + final listUIState = designUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); + final showCheckbox = onCheckboxChanged != null || isInMultiselect; + + final filterMatch = filter != null && filter.isNotEmpty + ? design.matchesFilterValue(filter) + : null; + final subtitle = filterMatch; + + return DismissibleEntity( + userCompany: state.userCompany, + entity: design, + isSelected: design.id == + (uiState.isEditing + ? designUIState.editing.id + : designUIState.selectedId), + onEntityAction: onEntityAction, + child: ListTile( + onTap: isInMultiselect + ? () => onEntityAction(EntityAction.toggleMultiselect) + : onTap, + onLongPress: onLongPress, + leading: showCheckbox + ? IgnorePointer( + ignoring: listUIState.isInMultiselect(), + child: Checkbox( + value: isChecked, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onChanged: (value) => onCheckboxChanged(value), + activeColor: Theme.of(context).accentColor, + ), + ) + : null, + title: Container( + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + Expanded( + child: Text( + design.name, + style: Theme.of(context).textTheme.headline6, + ), + ), + Text(formatNumber(design.listDisplayAmount, context), + style: Theme.of(context).textTheme.headline6), + ], + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + subtitle != null && subtitle.isNotEmpty + ? Text( + subtitle, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ) + : Container(), + EntityStateLabel(design), + ], + ), + ), + ); + } +} diff --git a/lib/ui/design/design_list_vm.dart b/lib/ui/design/design_list_vm.dart new file mode 100644 index 000000000..c06dd8f96 --- /dev/null +++ b/lib/ui/design/design_list_vm.dart @@ -0,0 +1,110 @@ +import 'dart:async'; +import 'package:invoiceninja_flutter/data/models/design_model.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/redux/client/client_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/redux/design/design_selectors.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/design/design_list.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; + +class DesignListBuilder extends StatelessWidget { + const DesignListBuilder({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: DesignListVM.fromStore, + builder: (context, viewModel) { + return DesignList( + viewModel: viewModel, + ); + }, + ); + } +} + +class DesignListVM { + DesignListVM({ + @required this.state, + @required this.userCompany, + @required this.designList, + @required this.designMap, + @required this.filter, + @required this.isLoading, + @required this.isLoaded, + @required this.onDesignTap, + @required this.listState, + @required this.onRefreshed, + @required this.onEntityAction, + @required this.tableColumns, + @required this.onClearEntityFilterPressed, + @required this.onViewEntityFilterPressed, + }); + + static DesignListVM fromStore(Store store) { + Future _handleRefresh(BuildContext context) { + if (store.state.isLoading) { + return Future(null); + } + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(LoadDesigns(completer: completer, force: true)); + return completer.future; + } + + final state = store.state; + + return DesignListVM( + state: state, + userCompany: state.userCompany, + listState: state.designListState, + designList: memoizedFilteredDesignList( + state.designState.map, state.designState.list, state.designListState), + designMap: state.designState.map, + isLoading: state.isLoading, + isLoaded: state.designState.isLoaded, + filter: state.designUIState.listUIState.filter, + onClearEntityFilterPressed: () => store.dispatch(FilterDesignsByEntity()), + onViewEntityFilterPressed: (BuildContext context) => viewEntityById( + context: context, + entityId: state.designListState.filterEntityId, + entityType: state.designListState.filterEntityType), + onDesignTap: (context, design) { + if (store.state.designListState.isInMultiselect()) { + handleDesignAction(context, [design], EntityAction.toggleMultiselect); + } else { + viewEntity(context: context, entity: design); + } + }, + onEntityAction: (BuildContext context, List designs, + EntityAction action) => + handleDesignAction(context, designs, action), + onRefreshed: (context) => _handleRefresh(context), + ); + } + + final AppState state; + final UserCompanyEntity userCompany; + final List designList; + final BuiltMap designMap; + final ListUIState listState; + final String filter; + final bool isLoading; + final bool isLoaded; + final Function(BuildContext, DesignEntity) onDesignTap; + final Function(BuildContext) onRefreshed; + final Function(BuildContext, List, EntityAction) onEntityAction; + final Function onClearEntityFilterPressed; + final Function(BuildContext) onViewEntityFilterPressed; + final List tableColumns; +} diff --git a/lib/ui/design/design_presenter.dart b/lib/ui/design/design_presenter.dart new file mode 100644 index 000000000..4ae1d4577 --- /dev/null +++ b/lib/ui/design/design_presenter.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/data/models/design_model.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; + +class DesignPresenter extends EntityPresenter { + static List getTableFields(UserCompanyEntity userCompany) { + return []; + } + + @override + Widget getField({String field, BuildContext context}) { + final state = StoreProvider.of(context).state; + final design = entity as InvoiceEntity; + + switch (field) { + } + + return super.getField(field: field, context: context); + } +} diff --git a/lib/ui/design/design_screen.dart b/lib/ui/design/design_screen.dart new file mode 100644 index 000000000..131e72147 --- /dev/null +++ b/lib/ui/design/design_screen.dart @@ -0,0 +1,148 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/save_cancel_buttons.dart'; +import 'package:invoiceninja_flutter/ui/app/list_scaffold.dart'; +import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; +import 'package:invoiceninja_flutter/ui/app/list_filter.dart'; +import 'package:invoiceninja_flutter/ui/app/list_filter_button.dart'; +import 'package:invoiceninja_flutter/ui/design/design_list_vm.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +import 'design_screen_vm.dart'; + +class DesignScreen extends StatelessWidget { + const DesignScreen({ + Key key, + @required this.viewModel, + }) : super(key: key); + + static const String route = '/design'; + + final DesignScreenVM viewModel; + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final state = store.state; + final company = state.company; + final userCompany = state.userCompany; + final localization = AppLocalization.of(context); + final listUIState = state.uiState.designUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); + + return ListScaffold( + isChecked: isInMultiselect && + listUIState.selectedIds.length == viewModel.designList.length, + showCheckbox: isInMultiselect, + onHamburgerLongPress: () => store.dispatch(StartDesignMultiselect()), + onCheckboxChanged: (value) { + final designs = viewModel.designList + .map((designId) => viewModel.designMap[designId]) + .where((design) => value != listUIState.isSelected(design.id)) + .toList(); + + handleDesignAction(context, designs, EntityAction.toggleMultiselect); + }, + appBarTitle: ListFilter( + title: localization.designs, + key: ValueKey(state.designListState.filterClearedAt), + filter: state.designListState.filter, + onFilterChanged: (value) { + store.dispatch(FilterDesigns(value)); + }, + ), + appBarActions: [ + if (!viewModel.isInMultiselect) + ListFilterButton( + filter: state.designListState.filter, + onFilterPressed: (String value) { + store.dispatch(FilterDesigns(value)); + }, + ), + if (viewModel.isInMultiselect) + SaveCancelButtons( + saveLabel: localization.done, + onSavePressed: listUIState.selectedIds.isEmpty + ? null + : (context) async { + final designs = listUIState.selectedIds + .map( + (designId) => viewModel.designMap[designId]) + .toList(); + + await showEntityActionsDialog( + entities: designs, + context: context, + multiselect: true, + completer: Completer() + ..future.then( + (_) => store.dispatch(ClearDesignMultiselect())), + ); + }, + onCancelPressed: (context) => + store.dispatch(ClearDesignMultiselect()), + ), + ], + body: DesignListBuilder(), + bottomNavigationBar: AppBottomBar( + entityType: EntityType.design, + onSelectedSortField: (value) { + store.dispatch(SortDesigns(value)); + }, + sortFields: [ + DesignFields.name, + DesignFields.balance, + DesignFields.updatedAt, + ], + onSelectedState: (EntityState state, value) { + store.dispatch(FilterDesignsByState(state)); + }, + onCheckboxPressed: () { + if (store.state.designListState.isInMultiselect()) { + store.dispatch(ClearDesignMultiselect()); + } else { + store.dispatch(StartDesignMultiselect()); + } + }, + customValues1: company.getCustomFieldValues(CustomFieldType.design1, + excludeBlank: true), + customValues2: company.getCustomFieldValues(CustomFieldType.design2, + excludeBlank: true), + customValues3: company.getCustomFieldValues(CustomFieldType.design3, + excludeBlank: true), + customValues4: company.getCustomFieldValues(CustomFieldType.design4, + excludeBlank: true), + onSelectedCustom1: (value) => + store.dispatch(FilterDesignsByCustom1(value)), + onSelectedCustom2: (value) => + store.dispatch(FilterDesignsByCustom2(value)), + onSelectedCustom3: (value) => + store.dispatch(FilterDesignsByCustom3(value)), + onSelectedCustom4: (value) => + store.dispatch(FilterDesignsByCustom4(value)), + ), + floatingActionButton: userCompany.canCreate(EntityType.design) + ? FloatingActionButton( + heroTag: 'design_fab', + backgroundColor: Theme.of(context).primaryColorDark, + onPressed: () { + createEntityByType( + context: context, entityType: EntityType.design); + }, + child: Icon( + Icons.add, + color: Colors.white, + ), + tooltip: localization.newDesign, + ) + : null, + ); + } +} diff --git a/lib/ui/design/design_screen_vm.dart b/lib/ui/design/design_screen_vm.dart new file mode 100644 index 000000000..0514ab3f9 --- /dev/null +++ b/lib/ui/design/design_screen_vm.dart @@ -0,0 +1,59 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; +import 'package:invoiceninja_flutter/redux/design/design_selectors.dart'; +import 'package:redux/redux.dart'; + +import 'design_screen.dart'; + +class DesignScreenBuilder extends StatelessWidget { + const DesignScreenBuilder({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: DesignScreenVM.fromStore, + builder: (context, vm) { + return DesignScreen( + viewModel: vm, + ); + }, + ); + } +} + +class DesignScreenVM { + DesignScreenVM({ + @required this.isInMultiselect, + @required this.designList, + @required this.userCompany, + @required this.onEntityAction, + @required this.designMap, + }); + + final bool isInMultiselect; + final UserCompanyEntity userCompany; + final List designList; + final Function(BuildContext, List, EntityAction) onEntityAction; + final BuiltMap designMap; + + static DesignScreenVM fromStore(Store store) { + final state = store.state; + + return DesignScreenVM( + designMap: state.designState.map, + designList: memoizedFilteredDesignList( + state.designState.map, state.designState.list, state.designListState), + userCompany: state.userCompany, + isInMultiselect: state.designListState.isInMultiselect(), + onEntityAction: (BuildContext context, List designs, + EntityAction action) => + handleDesignAction(context, designs, action), + ); + } +} diff --git a/lib/ui/design/edit/design_edit.dart b/lib/ui/design/edit/design_edit.dart new file mode 100644 index 000000000..28a857726 --- /dev/null +++ b/lib/ui/design/edit/design_edit.dart @@ -0,0 +1,102 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/design/edit/design_edit_vm.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; + +class DesignEdit extends StatefulWidget { + const DesignEdit({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final DesignEditVM viewModel; + + @override + _DesignEditState createState() => _DesignEditState(); +} + +class _DesignEditState extends State { + static final GlobalKey _formKey = + GlobalKey(debugLabel: '_designEdit'); + final _debouncer = Debouncer(); + + // STARTER: controllers - do not remove comment + + List _controllers = []; + + @override + void didChangeDependencies() { + _controllers = [ + // STARTER: array - do not remove comment + ]; + + _controllers.forEach((controller) => controller.removeListener(_onChanged)); + + final design = widget.viewModel.design; + // STARTER: read value - do not remove comment + + _controllers.forEach((controller) => controller.addListener(_onChanged)); + + super.didChangeDependencies(); + } + + @override + void dispose() { + _controllers.forEach((controller) { + controller.removeListener(_onChanged); + controller.dispose(); + }); + + super.dispose(); + } + + void _onChanged() { + _debouncer.run(() { + final design = widget.viewModel.design.rebuild((b) => b + // STARTER: set value - do not remove comment + ); + if (design != widget.viewModel.design) { + widget.viewModel.onChanged(design); + } + }); + } + + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final localization = AppLocalization.of(context); + final design = viewModel.design; + + return EditScaffold( + onCancelPressed: (context) => viewModel.onCancelPressed(context), + onSavePressed: (context) { + final bool isValid = _formKey.currentState.validate(); + + setState(() { + _autoValidate = !isValid; + }); + + if (!isValid) { + return; + } + + viewModel.onSavePressed(context); + }, + body: Form( + key: _formKey, + child: Builder(builder: (BuildContext context) { + return ListView( + children: [ + FormCard( + children: [ + // STARTER: widgets - do not remove comment + ], + ), + ], + ); + })), + ); + } +} diff --git a/lib/ui/design/edit/design_edit_vm.dart b/lib/ui/design/edit/design_edit_vm.dart new file mode 100644 index 000000000..a518b48b7 --- /dev/null +++ b/lib/ui/design/edit/design_edit_vm.dart @@ -0,0 +1,105 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/design/design_screen.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; +import 'package:invoiceninja_flutter/ui/design/view/design_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; +import 'package:invoiceninja_flutter/data/models/design_model.dart'; +import 'package:invoiceninja_flutter/ui/design/edit/design_edit.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class DesignEditScreen extends StatelessWidget { + const DesignEditScreen({Key key}) : super(key: key); + static const String route = '/design/edit'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return DesignEditVM.fromStore(store); + }, + builder: (context, viewModel) { + return DesignEdit( + viewModel: viewModel, + key: ValueKey(viewModel.design.id), + ); + }, + ); + } +} + +class DesignEditVM { + DesignEditVM({ + @required this.state, + @required this.design, + @required this.company, + @required this.onChanged, + @required this.isSaving, + @required this.origDesign, + @required this.onSavePressed, + @required this.onCancelPressed, + @required this.isLoading, + }); + + factory DesignEditVM.fromStore(Store store) { + final state = store.state; + final design = state.designUIState.editing; + + return DesignEditVM( + state: state, + isLoading: state.isLoading, + isSaving: state.isSaving, + origDesign: state.designState.map[design.id], + design: design, + company: state.selectedCompany, + onChanged: (DesignEntity design) { + store.dispatch(UpdateDesign(design)); + }, + onCancelPressed: (BuildContext context) { + store.dispatch( + EditDesign(design: DesignEntity(), context: context, force: true)); + store.dispatch(UpdateCurrentRoute(state.uiState.previousRoute)); + }, + onSavePressed: (BuildContext context) { + final Completer completer = new Completer(); + store.dispatch(SaveDesignRequest(completer: completer, design: design)); + return completer.future.then((savedDesign) { + if (isMobile(context)) { + store.dispatch(UpdateCurrentRoute(DesignViewScreen.route)); + if (design.isNew) { + Navigator.of(context) + .pushReplacementNamed(DesignViewScreen.route); + } else { + Navigator.of(context).pop(savedDesign); + } + } else { + store.dispatch(ViewDesign( + context: context, designId: savedDesign.id, force: true)); + } + }).catchError((Object error) { + showDialog( + context: context, + builder: (BuildContext context) { + return ErrorDialog(error); + }); + }); + }, + ); + } + + final DesignEntity design; + final CompanyEntity company; + final Function(DesignEntity) onChanged; + final Function(BuildContext) onSavePressed; + final Function(BuildContext) onCancelPressed; + final bool isLoading; + final bool isSaving; + final DesignEntity origDesign; + final AppState state; +} diff --git a/lib/ui/design/view/design_view.dart b/lib/ui/design/view/design_view.dart new file mode 100644 index 000000000..c5280f88b --- /dev/null +++ b/lib/ui/design/view/design_view.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/ui/app/buttons/edit_icon_button.dart'; +import 'package:invoiceninja_flutter/ui/app/actions_menu_button.dart'; +import 'package:invoiceninja_flutter/ui/design/view/design_view_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/app/entities/entity_state_title.dart'; + +class DesignView extends StatefulWidget { + const DesignView({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final DesignViewVM viewModel; + + @override + _DesignViewState createState() => new _DesignViewState(); +} + +class _DesignViewState extends State { + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final userCompany = viewModel.state.userCompany; + final design = viewModel.design; + + return Scaffold( + appBar: AppBar( + title: EntityStateTitle(entity: design), + actions: [ + userCompany.canEditEntity(design) + ? EditIconButton( + isVisible: !design.isDeleted, + onPressed: () => viewModel.onEditPressed(context), + ) + : Container(), + ActionMenuButton( + entityActions: design.getActions(userCompany: userCompany), + isSaving: viewModel.isSaving, + entity: design, + onSelected: viewModel.onEntityAction, + ) + ], + ), + body: FormCard(children: [ + // STARTER: widgets - do not remove comment + ]), + ); + } +} diff --git a/lib/ui/design/view/design_view_vm.dart b/lib/ui/design/view/design_view_vm.dart new file mode 100644 index 000000000..0fcaa0c3b --- /dev/null +++ b/lib/ui/design/view/design_view_vm.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/ui/design/design_screen.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:redux/redux.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; +import 'package:invoiceninja_flutter/data/models/design_model.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/design/view/design_view.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class DesignViewScreen extends StatelessWidget { + const DesignViewScreen({Key key}) : super(key: key); + static const String route = '/design/view'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return DesignViewVM.fromStore(store); + }, + builder: (context, vm) { + return DesignView( + viewModel: vm, + ); + }, + ); + } +} + +class DesignViewVM { + DesignViewVM({ + @required this.state, + @required this.design, + @required this.company, + @required this.onEntityAction, + @required this.onRefreshed, + @required this.isSaving, + @required this.isLoading, + @required this.isDirty, + }); + + factory DesignViewVM.fromStore(Store store) { + final state = store.state; + final design = state.designState.map[state.designUIState.selectedId] ?? + DesignEntity(id: state.designUIState.selectedId); + + Future _handleRefresh(BuildContext context) { + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(LoadDesign(completer: completer, designId: design.id)); + return completer.future; + } + + return DesignViewVM( + state: state, + company: state.selectedCompany, + isSaving: state.isSaving, + isLoading: state.isLoading, + isDirty: design.isNew, + design: design, + onRefreshed: (context) => _handleRefresh(context), + onEntityAction: (BuildContext context, EntityAction action) => + handleDesignAction(context, design, action), + ); + } + + final AppState state; + final DesignEntity design; + final CompanyEntity company; + final Function(BuildContext, EntityAction) onEntityAction; + final Function(BuildContext) onRefreshed; + final bool isSaving; + final bool isLoading; + final bool isDirty; +} diff --git a/lib/ui/reports/reports_screen_vm.dart b/lib/ui/reports/reports_screen_vm.dart index 93072f452..904724691 100644 --- a/lib/ui/reports/reports_screen_vm.dart +++ b/lib/ui/reports/reports_screen_vm.dart @@ -28,7 +28,7 @@ import 'package:memoize/memoize.dart'; import 'package:path_provider/path_provider.dart'; import 'package:redux/redux.dart'; import 'package:invoiceninja_flutter/utils/web_stub.dart' -if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; + if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; class ReportsScreenBuilder extends StatelessWidget { const ReportsScreenBuilder({Key key}) : super(key: key); diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 860b26ce3..ba1adc974 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -15,6 +15,17 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'design': 'Design', + 'designs': 'Designs', + 'new_design': 'New Design', + 'edit_design': 'Edit Design', + 'created_design': 'Successfully created design', + 'updated_design': 'Successfully updated design', + 'archived_design': 'Successfully archived design', + 'deleted_design': 'Successfully deleted design', + 'removed_design': 'Successfully removed design', + 'restored_design': 'Successfully restored design', + 'proposals': 'Proposals', 'tickets': 'Tickets', 'recurring_invoices': 'Recurring Invoices', @@ -32780,6 +32791,16 @@ mixin LocalizationsProvider on LocaleCodeAware { String get appUpdated => _localizedValues[localeCode]['app_updated']; // STARTER: lang field - do not remove comment + String get design => _localizedValues[localeCode][' design']; + String get designs => _localizedValues[localeCode]['designs']; + String get newDesign => _localizedValues[localeCode]['new_design']; + String get createdDesign => _localizedValues[localeCode]['created_design']; + String get updatedDesign => _localizedValues[localeCode]['updated_design']; + String get archivedDesign => _localizedValues[localeCode]['archived_design']; + String get deletedDesign => _localizedValues[localeCode]['deleted_design']; + String get restoredDesign => _localizedValues[localeCode]['restored_design']; + String get editDesign => _localizedValues[localeCode]['edit_design']; + String get newCredit => _localizedValues[localeCode]['new_credit']; String get createdCredit => _localizedValues[localeCode]['created_credit']; diff --git a/starter.sh b/starter.sh index 1e845b0ec..d2ebac16a 100644 --- a/starter.sh +++ b/starter.sh @@ -438,6 +438,10 @@ else code="String get ${module_camel} => _localizedValues[localeCode][' ${module_snake}']; String get ${module_camel}s => _localizedValues[localeCode]['${module_snake}s']; String get new${Module} => _localizedValues[localeCode]['new_${module_snake}']; String get created${Module} => _localizedValues[localeCode]['created_${module_snake}']; String get updated${Module} => _localizedValues[localeCode]['updated_${module_snake}']; String get archived${Module} => _localizedValues[localeCode]['archived_${module_snake}']; String get deleted${Module} => _localizedValues[localeCode]['deleted_${module_snake}']; String get restored${Module} => _localizedValues[localeCode]['restored_${module_snake}']; String get edit${Module} => _localizedValues[localeCode]['edit_${module_snake}'];${lineBreak}" sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/utils/i18n.dart + comment="STARTER: entity type - do not remove comment" + code="static const EntityType ${module_camel} = _\$${module_camel}${lineBreak}" + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/data/models/entities.dart + echo "Generating built files.." flutter packages pub run build_runner clean flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/stubs/data/models/stub_model b/stubs/data/models/stub_model index 9968118f2..64a74c767 100644 --- a/stubs/data/models/stub_model +++ b/stubs/data/models/stub_model @@ -2,10 +2,10 @@ import 'package:built_value/built_value.dart'; import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; part 'stub_model.g.dart'; - abstract class StubListResponse implements Built { factory StubListResponse([void updates(StubListResponseBuilder b)]) = @@ -48,6 +48,8 @@ abstract class StubEntity extends Object with BaseEntity implements Built null; + + @override + double get listDisplayAmount => null; + + @override + FormatNumberType get listDisplayAmountType => null; + static Serializer get serializer => _$stubEntitySerializer; }