diff --git a/android/build.gradle.foss b/android/build.gradle.foss new file mode 100644 index 00000000000..619dce9d138 --- /dev/null +++ b/android/build.gradle.foss @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.72' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.6.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/lib/constants.dart b/lib/constants.dart index f446957fccd..7ea8332c9d8 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -125,7 +125,7 @@ const double kLighterOpacity = .6; const int kMaxNumberOfCompanies = 10; const int kMaxNumberOfHistory = 50; const int kMaxRecordsPerApiPage = 5000; -const int kMaxPostSeconds = 120; +const int kMaxPostSeconds = 30; const int kMillisecondsToRefreshData = 1000 * 60 * 15; // 15 minutes const int kUpdatedAtBufferSeconds = 600; const int kMillisecondsToRefreshActivities = 1000 * 60 * 60 * 24; // 1 day diff --git a/lib/data/models/import_model.dart b/lib/data/models/import_model.dart index 4dbbcdafde6..fbe554df434 100644 --- a/lib/data/models/import_model.dart +++ b/lib/data/models/import_model.dart @@ -2,6 +2,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; import 'package:flutter/foundation.dart'; +import 'package:invoiceninja_flutter/data/models/entities.dart'; part 'import_model.g.dart'; @@ -19,9 +20,26 @@ abstract class PreImportResponse String get hash; - BuiltList> get headers; + BuiltMap get mappings; + + static Serializer get serializer => + _$preImportResponseSerializer; +} + +abstract class PreImportResponseEntityDetails + implements Built { + factory PreImportResponseEntityDetails() { + return _$PreImportResponseEntityDetails._(); + } + + PreImportResponseEntityDetails._(); + + @override + @memoized + int get hashCode; BuiltList get available; + BuiltList> get headers; BuiltList get fields1 => headers.isEmpty ? BuiltList() : headers[0]; @@ -29,21 +47,21 @@ abstract class PreImportResponse BuiltList get fields2 => headers.length < 2 ? BuiltList() : headers[1]; - static Serializer get serializer => - _$preImportResponseSerializer; + static Serializer get serializer => + _$preImportResponseEntityDetailsSerializer; } abstract class ImportRequest implements Built { factory ImportRequest({ @required String hash, - @required String entityType, + @required String importType, @required bool skipHeader, - @required BuiltMap columnMap, + @required BuiltMap> columnMap, }) { return _$ImportRequest._( hash: hash, - entityType: entityType, + importType: importType, skipHeader: skipHeader, columnMap: columnMap, ); @@ -57,14 +75,58 @@ abstract class ImportRequest String get hash; - @BuiltValueField(wireName: 'entity_type') - String get entityType; + @BuiltValueField(wireName: 'import_type') + String get importType; @BuiltValueField(wireName: 'skip_header') bool get skipHeader; @BuiltValueField(wireName: 'column_map') - BuiltMap get columnMap; + BuiltMap> get columnMap; + + // This needed so the builder factory for BuiltMap is auto-created. + @nullable + @BuiltValueField(wireName: 'dummy_field') + BuiltMap get dummy; static Serializer get serializer => _$importRequestSerializer; } + +class ImportType extends EnumClass { + const ImportType._(String name) : super(name); + + static Serializer get serializer => _$importTypeSerializer; + + static const ImportType csv = _$csv; + static const ImportType freshbooks = _$freshbooks; + static const ImportType invoice2go = _$invoice2go; + + static const ImportType invoicely = _$invoicely; + static const ImportType waveaccounting = _$waveaccounting; + static const ImportType zoho = _$zoho; + + static BuiltSet get values => _$typeValues; + + List get uploadParts { + switch (this) { + case ImportType.csv: + return [ + EntityType.client.toString(), + EntityType.invoice.toString(), + EntityType.payment.toString(), + EntityType.product.toString(), + EntityType.vendor.toString(), + EntityType.expense.toString(), + ]; + case ImportType.freshbooks: + return [ + EntityType.client.toString(), + EntityType.payment.toString(), + ]; + default: + return []; + } + } + + static ImportType valueOf(String name) => _$typeValueOf(name); +} diff --git a/lib/data/models/import_model.g.dart b/lib/data/models/import_model.g.dart index f79b0a17c87..57174bfffc6 100644 --- a/lib/data/models/import_model.g.dart +++ b/lib/data/models/import_model.g.dart @@ -6,10 +6,50 @@ part of 'import_model.dart'; // BuiltValueGenerator // ************************************************************************** +const ImportType _$csv = const ImportType._('csv'); +const ImportType _$freshbooks = const ImportType._('freshbooks'); +const ImportType _$invoice2go = const ImportType._('invoice2go'); +const ImportType _$invoicely = const ImportType._('invoicely'); +const ImportType _$waveaccounting = const ImportType._('waveaccounting'); +const ImportType _$zoho = const ImportType._('zoho'); + +ImportType _$typeValueOf(String name) { + switch (name) { + case 'csv': + return _$csv; + case 'freshbooks': + return _$freshbooks; + case 'invoice2go': + return _$invoice2go; + case 'invoicely': + return _$invoicely; + case 'waveaccounting': + return _$waveaccounting; + case 'zoho': + return _$zoho; + default: + throw new ArgumentError(name); + } +} + +final BuiltSet _$typeValues = + new BuiltSet(const [ + _$csv, + _$freshbooks, + _$invoice2go, + _$invoicely, + _$waveaccounting, + _$zoho, +]); + Serializer _$preImportResponseSerializer = new _$PreImportResponseSerializer(); +Serializer + _$preImportResponseEntityDetailsSerializer = + new _$PreImportResponseEntityDetailsSerializer(); Serializer _$importRequestSerializer = new _$ImportRequestSerializer(); +Serializer _$importTypeSerializer = new _$ImportTypeSerializer(); class _$PreImportResponseSerializer implements StructuredSerializer { @@ -24,15 +64,12 @@ class _$PreImportResponseSerializer final result = [ 'hash', serializers.serialize(object.hash, specifiedType: const FullType(String)), - 'headers', - serializers.serialize(object.headers, - specifiedType: const FullType(BuiltList, const [ - const FullType(BuiltList, const [const FullType(String)]) + 'mappings', + serializers.serialize(object.mappings, + specifiedType: const FullType(BuiltMap, const [ + const FullType(String), + const FullType(PreImportResponseEntityDetails) ])), - 'available', - serializers.serialize(object.available, - specifiedType: - const FullType(BuiltList, const [const FullType(String)])), ]; return result; @@ -54,18 +91,73 @@ class _$PreImportResponseSerializer result.hash = serializers.deserialize(value, specifiedType: const FullType(String)) as String; break; - case 'headers': - result.headers.replace(serializers.deserialize(value, - specifiedType: const FullType(BuiltList, const [ - const FullType(BuiltList, const [const FullType(String)]) - ])) as BuiltList); + case 'mappings': + result.mappings.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltMap, const [ + const FullType(String), + const FullType(PreImportResponseEntityDetails) + ]))); break; + } + } + + return result.build(); + } +} + +class _$PreImportResponseEntityDetailsSerializer + implements StructuredSerializer { + @override + final Iterable types = const [ + PreImportResponseEntityDetails, + _$PreImportResponseEntityDetails + ]; + @override + final String wireName = 'PreImportResponseEntityDetails'; + + @override + Iterable serialize( + Serializers serializers, PreImportResponseEntityDetails object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'available', + serializers.serialize(object.available, + specifiedType: + const FullType(BuiltList, const [const FullType(String)])), + 'headers', + serializers.serialize(object.headers, + specifiedType: const FullType(BuiltList, const [ + const FullType(BuiltList, const [const FullType(String)]) + ])), + ]; + + return result; + } + + @override + PreImportResponseEntityDetails deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new PreImportResponseEntityDetailsBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { case 'available': result.available.replace(serializers.deserialize(value, specifiedType: const FullType(BuiltList, const [const FullType(String)])) as BuiltList); break; + case 'headers': + result.headers.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltList, const [ + const FullType(BuiltList, const [const FullType(String)]) + ])) as BuiltList); + break; } } @@ -85,18 +177,27 @@ class _$ImportRequestSerializer implements StructuredSerializer { final result = [ 'hash', serializers.serialize(object.hash, specifiedType: const FullType(String)), - 'entity_type', - serializers.serialize(object.entityType, + 'import_type', + serializers.serialize(object.importType, specifiedType: const FullType(String)), 'skip_header', serializers.serialize(object.skipHeader, specifiedType: const FullType(bool)), 'column_map', serializers.serialize(object.columnMap, - specifiedType: const FullType( - BuiltMap, const [const FullType(int), const FullType(String)])), + specifiedType: const FullType(BuiltMap, const [ + const FullType(String), + const FullType( + BuiltMap, const [const FullType(int), const FullType(String)]) + ])), ]; - + if (object.dummy != null) { + result + ..add('dummy_field') + ..add(serializers.serialize(object.dummy, + specifiedType: const FullType(BuiltMap, + const [const FullType(int), const FullType(String)]))); + } return result; } @@ -116,8 +217,8 @@ class _$ImportRequestSerializer implements StructuredSerializer { result.hash = serializers.deserialize(value, specifiedType: const FullType(String)) as String; break; - case 'entity_type': - result.entityType = serializers.deserialize(value, + case 'import_type': + result.importType = serializers.deserialize(value, specifiedType: const FullType(String)) as String; break; case 'skip_header': @@ -126,6 +227,14 @@ class _$ImportRequestSerializer implements StructuredSerializer { break; case 'column_map': result.columnMap.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltMap, const [ + const FullType(String), + const FullType(BuiltMap, + const [const FullType(int), const FullType(String)]) + ]))); + break; + case 'dummy_field': + result.dummy.replace(serializers.deserialize(value, specifiedType: const FullType(BuiltMap, const [const FullType(int), const FullType(String)]))); break; @@ -136,27 +245,39 @@ class _$ImportRequestSerializer implements StructuredSerializer { } } +class _$ImportTypeSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [ImportType]; + @override + final String wireName = 'ImportType'; + + @override + Object serialize(Serializers serializers, ImportType object, + {FullType specifiedType = FullType.unspecified}) => + object.name; + + @override + ImportType deserialize(Serializers serializers, Object serialized, + {FullType specifiedType = FullType.unspecified}) => + ImportType.valueOf(serialized as String); +} + class _$PreImportResponse extends PreImportResponse { @override final String hash; @override - final BuiltList> headers; - @override - final BuiltList available; + final BuiltMap mappings; factory _$PreImportResponse( [void Function(PreImportResponseBuilder) updates]) => (new PreImportResponseBuilder()..update(updates)).build(); - _$PreImportResponse._({this.hash, this.headers, this.available}) : super._() { + _$PreImportResponse._({this.hash, this.mappings}) : super._() { if (hash == null) { throw new BuiltValueNullFieldError('PreImportResponse', 'hash'); } - if (headers == null) { - throw new BuiltValueNullFieldError('PreImportResponse', 'headers'); - } - if (available == null) { - throw new BuiltValueNullFieldError('PreImportResponse', 'available'); + if (mappings == null) { + throw new BuiltValueNullFieldError('PreImportResponse', 'mappings'); } } @@ -173,23 +294,20 @@ class _$PreImportResponse extends PreImportResponse { if (identical(other, this)) return true; return other is PreImportResponse && hash == other.hash && - headers == other.headers && - available == other.available; + mappings == other.mappings; } int __hashCode; @override int get hashCode { - return __hashCode ??= $jf( - $jc($jc($jc(0, hash.hashCode), headers.hashCode), available.hashCode)); + return __hashCode ??= $jf($jc($jc(0, hash.hashCode), mappings.hashCode)); } @override String toString() { return (newBuiltValueToStringHelper('PreImportResponse') ..add('hash', hash) - ..add('headers', headers) - ..add('available', available)) + ..add('mappings', mappings)) .toString(); } } @@ -202,24 +320,19 @@ class PreImportResponseBuilder String get hash => _$this._hash; set hash(String hash) => _$this._hash = hash; - ListBuilder> _headers; - ListBuilder> get headers => - _$this._headers ??= new ListBuilder>(); - set headers(ListBuilder> headers) => - _$this._headers = headers; - - ListBuilder _available; - ListBuilder get available => - _$this._available ??= new ListBuilder(); - set available(ListBuilder available) => _$this._available = available; + MapBuilder _mappings; + MapBuilder get mappings => + _$this._mappings ??= + new MapBuilder(); + set mappings(MapBuilder mappings) => + _$this._mappings = mappings; PreImportResponseBuilder(); PreImportResponseBuilder get _$this { if (_$v != null) { _hash = _$v.hash; - _headers = _$v.headers?.toBuilder(); - _available = _$v.available?.toBuilder(); + _mappings = _$v.mappings?.toBuilder(); _$v = null; } return this; @@ -243,20 +356,136 @@ class PreImportResponseBuilder _$PreImportResponse _$result; try { _$result = _$v ?? - new _$PreImportResponse._( - hash: hash, - headers: headers.build(), - available: available.build()); + new _$PreImportResponse._(hash: hash, mappings: mappings.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'mappings'; + mappings.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'PreImportResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$PreImportResponseEntityDetails extends PreImportResponseEntityDetails { + @override + final BuiltList available; + @override + final BuiltList> headers; + + factory _$PreImportResponseEntityDetails( + [void Function(PreImportResponseEntityDetailsBuilder) updates]) => + (new PreImportResponseEntityDetailsBuilder()..update(updates)).build(); + + _$PreImportResponseEntityDetails._({this.available, this.headers}) + : super._() { + if (available == null) { + throw new BuiltValueNullFieldError( + 'PreImportResponseEntityDetails', 'available'); + } + if (headers == null) { + throw new BuiltValueNullFieldError( + 'PreImportResponseEntityDetails', 'headers'); + } + } + + @override + PreImportResponseEntityDetails rebuild( + void Function(PreImportResponseEntityDetailsBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + PreImportResponseEntityDetailsBuilder toBuilder() => + new PreImportResponseEntityDetailsBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is PreImportResponseEntityDetails && + available == other.available && + headers == other.headers; + } + + int __hashCode; + @override + int get hashCode { + return __hashCode ??= + $jf($jc($jc(0, available.hashCode), headers.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('PreImportResponseEntityDetails') + ..add('available', available) + ..add('headers', headers)) + .toString(); + } +} + +class PreImportResponseEntityDetailsBuilder + implements + Builder { + _$PreImportResponseEntityDetails _$v; + + ListBuilder _available; + ListBuilder get available => + _$this._available ??= new ListBuilder(); + set available(ListBuilder available) => _$this._available = available; + + ListBuilder> _headers; + ListBuilder> get headers => + _$this._headers ??= new ListBuilder>(); + set headers(ListBuilder> headers) => + _$this._headers = headers; + + PreImportResponseEntityDetailsBuilder(); + + PreImportResponseEntityDetailsBuilder get _$this { + if (_$v != null) { + _available = _$v.available?.toBuilder(); + _headers = _$v.headers?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(PreImportResponseEntityDetails other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$PreImportResponseEntityDetails; + } + + @override + void update(void Function(PreImportResponseEntityDetailsBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$PreImportResponseEntityDetails build() { + _$PreImportResponseEntityDetails _$result; + try { + _$result = _$v ?? + new _$PreImportResponseEntityDetails._( + available: available.build(), headers: headers.build()); } catch (_) { String _$failedField; try { - _$failedField = 'headers'; - headers.build(); _$failedField = 'available'; available.build(); + _$failedField = 'headers'; + headers.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'PreImportResponse', _$failedField, e.toString()); + 'PreImportResponseEntityDetails', _$failedField, e.toString()); } rethrow; } @@ -269,23 +498,25 @@ class _$ImportRequest extends ImportRequest { @override final String hash; @override - final String entityType; + final String importType; @override final bool skipHeader; @override - final BuiltMap columnMap; + final BuiltMap> columnMap; + @override + final BuiltMap dummy; factory _$ImportRequest([void Function(ImportRequestBuilder) updates]) => (new ImportRequestBuilder()..update(updates)).build(); _$ImportRequest._( - {this.hash, this.entityType, this.skipHeader, this.columnMap}) + {this.hash, this.importType, this.skipHeader, this.columnMap, this.dummy}) : super._() { if (hash == null) { throw new BuiltValueNullFieldError('ImportRequest', 'hash'); } - if (entityType == null) { - throw new BuiltValueNullFieldError('ImportRequest', 'entityType'); + if (importType == null) { + throw new BuiltValueNullFieldError('ImportRequest', 'importType'); } if (skipHeader == null) { throw new BuiltValueNullFieldError('ImportRequest', 'skipHeader'); @@ -307,27 +538,31 @@ class _$ImportRequest extends ImportRequest { if (identical(other, this)) return true; return other is ImportRequest && hash == other.hash && - entityType == other.entityType && + importType == other.importType && skipHeader == other.skipHeader && - columnMap == other.columnMap; + columnMap == other.columnMap && + dummy == other.dummy; } int __hashCode; @override int get hashCode { return __hashCode ??= $jf($jc( - $jc($jc($jc(0, hash.hashCode), entityType.hashCode), - skipHeader.hashCode), - columnMap.hashCode)); + $jc( + $jc($jc($jc(0, hash.hashCode), importType.hashCode), + skipHeader.hashCode), + columnMap.hashCode), + dummy.hashCode)); } @override String toString() { return (newBuiltValueToStringHelper('ImportRequest') ..add('hash', hash) - ..add('entityType', entityType) + ..add('importType', importType) ..add('skipHeader', skipHeader) - ..add('columnMap', columnMap)) + ..add('columnMap', columnMap) + ..add('dummy', dummy)) .toString(); } } @@ -340,28 +575,34 @@ class ImportRequestBuilder String get hash => _$this._hash; set hash(String hash) => _$this._hash = hash; - String _entityType; - String get entityType => _$this._entityType; - set entityType(String entityType) => _$this._entityType = entityType; + String _importType; + String get importType => _$this._importType; + set importType(String importType) => _$this._importType = importType; bool _skipHeader; bool get skipHeader => _$this._skipHeader; set skipHeader(bool skipHeader) => _$this._skipHeader = skipHeader; - MapBuilder _columnMap; - MapBuilder get columnMap => - _$this._columnMap ??= new MapBuilder(); - set columnMap(MapBuilder columnMap) => + MapBuilder> _columnMap; + MapBuilder> get columnMap => + _$this._columnMap ??= new MapBuilder>(); + set columnMap(MapBuilder> columnMap) => _$this._columnMap = columnMap; + MapBuilder _dummy; + MapBuilder get dummy => + _$this._dummy ??= new MapBuilder(); + set dummy(MapBuilder dummy) => _$this._dummy = dummy; + ImportRequestBuilder(); ImportRequestBuilder get _$this { if (_$v != null) { _hash = _$v.hash; - _entityType = _$v.entityType; + _importType = _$v.importType; _skipHeader = _$v.skipHeader; _columnMap = _$v.columnMap?.toBuilder(); + _dummy = _$v.dummy?.toBuilder(); _$v = null; } return this; @@ -387,14 +628,17 @@ class ImportRequestBuilder _$result = _$v ?? new _$ImportRequest._( hash: hash, - entityType: entityType, + importType: importType, skipHeader: skipHeader, - columnMap: columnMap.build()); + columnMap: columnMap.build(), + dummy: _dummy?.build()); } catch (_) { String _$failedField; try { _$failedField = 'columnMap'; columnMap.build(); + _$failedField = 'dummy'; + _dummy?.build(); } catch (e) { throw new BuiltValueNestedFieldError( 'ImportRequest', _$failedField, e.toString()); diff --git a/lib/data/models/serializers.g.dart b/lib/data/models/serializers.g.dart index 48005167d73..b7098465ddb 100644 --- a/lib/data/models/serializers.g.dart +++ b/lib/data/models/serializers.g.dart @@ -123,6 +123,7 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(PaymentUIState.serializer) ..add(PaymentableEntity.serializer) ..add(PreImportResponse.serializer) + ..add(PreImportResponseEntityDetails.serializer) ..add(PrefState.serializer) ..add(ProductEntity.serializer) ..add(ProductItemResponse.serializer) @@ -194,14 +195,6 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(WebhookListResponse.serializer) ..add(WebhookState.serializer) ..add(WebhookUIState.serializer) - ..addBuilderFactory( - const FullType(BuiltList, const [ - const FullType(BuiltList, const [const FullType(String)]) - ]), - () => new ListBuilder>()) - ..addBuilderFactory( - const FullType(BuiltList, const [const FullType(String)]), - () => new ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(ClientEntity)]), () => new ListBuilder()) @@ -456,6 +449,12 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory(const FullType(BuiltList, const [const FullType(SizeEntity)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) + ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) + ..addBuilderFactory( + const FullType(BuiltList, const [ + const FullType(BuiltList, const [const FullType(String)]) + ]), + () => new ListBuilder>()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(TaskEntity)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(TaskStatusEntity)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(TaxRateEntity)]), () => new ListBuilder()) @@ -489,6 +488,14 @@ Serializers _$serializers = (new Serializers().toBuilder() ]), () => new MapBuilder>()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(ReportSettingsEntity)]), () => new MapBuilder()) + ..addBuilderFactory( + const FullType(BuiltMap, const [ + const FullType(String), + const FullType( + BuiltMap, const [const FullType(int), const FullType(String)]) + ]), + () => new MapBuilder>()) + ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(String)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(ClientEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(CompanyGatewayEntity)]), () => new MapBuilder()) @@ -528,6 +535,7 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(PaymentTermEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) + ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(PreImportResponseEntityDetails)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(ProductEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(ProjectEntity)]), () => new MapBuilder()) @@ -554,8 +562,7 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(VendorEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(WebhookEntity)]), () => new MapBuilder()) - ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) - ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(String)]), () => new MapBuilder())) + ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder())) .build(); // 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/web_client.dart b/lib/data/web_client.dart index 0aee6780cd7..566c7341137 100644 --- a/lib/data/web_client.dart +++ b/lib/data/web_client.dart @@ -55,6 +55,7 @@ class WebClient { String token, { dynamic data, MultipartFile multipartFile, + List multipartFiles, String secret, String password, bool rawResponse = false, @@ -74,7 +75,15 @@ class WebClient { http.Response response; if (multipartFile != null) { - response = await _uploadFile(url, token, multipartFile, data: data); + if (multipartFiles == null) { + multipartFiles = [multipartFile]; + } else { + multipartFiles.add(multipartFile); + } + } + + if (multipartFiles != null) { + response = await _uploadFiles(url, token, multipartFiles, data: data); } else { response = await http.Client() .post(url, @@ -116,7 +125,7 @@ class WebClient { http.Response response; if (multipartFile != null) { - response = await _uploadFile(url, token, multipartFile, + response = await _uploadFiles(url, token, [multipartFile], fileIndex: fileIndex, data: data, method: 'PUT'); } else { response = await http.Client().put( @@ -236,13 +245,13 @@ String _parseError(int code, String response) { return '$code: $message'; } -Future _uploadFile( - String url, String token, MultipartFile multipartFile, +Future _uploadFiles( + String url, String token, List multipartFiles, {String method = 'POST', String fileIndex = 'file', dynamic data}) async { final request = http.MultipartRequest(method, Uri.parse(url)) ..fields.addAll(data ?? {}) ..headers.addAll(_getHeaders(url, token)) - ..files.add(multipartFile); + ..files.addAll(multipartFiles); return await http.Response.fromStream(await request.send()) .timeout(const Duration(minutes: 10)); diff --git a/lib/ui/app/menu_drawer_vm.dart b/lib/ui/app/menu_drawer_vm.dart index 93bb94b210c..7f7817af48c 100644 --- a/lib/ui/app/menu_drawer_vm.dart +++ b/lib/ui/app/menu_drawer_vm.dart @@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/auth/auth_actions.dart'; @@ -56,7 +55,7 @@ class MenuDrawerVM { final bool isLoading; static MenuDrawerVM fromStore(Store store) { - final GoogleSignIn _googleSignIn = GoogleSignIn(); + //final GoogleSignIn _googleSignIn = GoogleSignIn(); final AppState state = store.state; diff --git a/lib/ui/app/upgrade_dialog.dart.foss b/lib/ui/app/upgrade_dialog.dart.foss new file mode 100644 index 00000000000..cd2e124148e --- /dev/null +++ b/lib/ui/app/upgrade_dialog.dart.foss @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class UpgradeDialog extends StatefulWidget { + @override + _UpgradeDialogState createState() => _UpgradeDialogState(); +} + +class _UpgradeDialogState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/ui/auth/login_vm.dart.foss b/lib/ui/auth/login_vm.dart.foss new file mode 100644 index 00000000000..62704cddad8 --- /dev/null +++ b/lib/ui/auth/login_vm.dart.foss @@ -0,0 +1,185 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/pref_state.dart'; +import 'package:invoiceninja_flutter/ui/app/app_builder.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/auth/auth_actions.dart'; +import 'package:invoiceninja_flutter/ui/auth/login_view.dart'; +import 'package:invoiceninja_flutter/redux/auth/auth_state.dart'; + +class LoginScreen extends StatelessWidget { + const LoginScreen({Key key}) : super(key: key); + + static const String route = '/login'; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: StoreConnector( + converter: LoginVM.fromStore, + builder: (context, viewModel) { + return LoginView( + viewModel: viewModel, + ); + }, + ), + ); + } +} + +class LoginVM { + LoginVM({ + @required this.state, + @required this.isLoading, + @required this.authState, + @required this.onLoginPressed, + @required this.onRecoverPressed, + @required this.onSignUpPressed, + @required this.onGoogleLoginPressed, + @required this.onGoogleSignUpPressed, + }); + + AppState state; + bool isLoading; + AuthState authState; + + final Function( + BuildContext, + Completer completer, { + @required String email, + @required String password, + @required String url, + @required String secret, + @required String oneTimePassword, + }) onLoginPressed; + + final Function( + BuildContext, + Completer completer, { + @required String email, + @required String url, + @required String secret, + }) onRecoverPressed; + + final Function( + BuildContext, + Completer completer, { + @required String email, + @required String password, + }) onSignUpPressed; + + final Function(BuildContext, Completer completer, + {String url, String secret, String oneTimePassword}) onGoogleLoginPressed; + final Function(BuildContext, Completer completer) onGoogleSignUpPressed; + + static LoginVM fromStore(Store store) { + void _handleLogin({BuildContext context, bool isSignUp = false}) { + final layout = calculateLayout(context); + + store.dispatch(UpdateUserPreferences(appLayout: layout)); + AppBuilder.of(context).rebuild(); + + WidgetsBinding.instance.addPostFrameCallback((duration) { + if (layout == AppLayout.mobile) { + if (isSignUp) { + store.dispatch( + UpdateUserPreferences(moduleLayout: ModuleLayout.list)); + } + store.dispatch(ViewDashboard(navigator: Navigator.of(context))); + } else { + store.dispatch(ViewMainScreen(navigator: Navigator.of(context))); + } + }); + } + + return LoginVM( + state: store.state, + isLoading: store.state.isLoading, + authState: store.state.authState, + onGoogleLoginPressed: ( + BuildContext context, + Completer completer, { + @required String url, + @required String secret, + @required String oneTimePassword, + }) async {}, + onGoogleSignUpPressed: + (BuildContext context, Completer completer) async {}, + onSignUpPressed: ( + BuildContext context, + Completer completer, { + @required String email, + @required String password, + }) async { + if (store.state.isLoading) { + return; + } + + store.dispatch(UserSignUpRequest( + completer: completer, + email: email.trim(), + password: password.trim(), + )); + completer.future + .then((_) => _handleLogin(context: context, isSignUp: true)); + }, + onRecoverPressed: ( + BuildContext context, + Completer completer, { + @required String email, + @required String url, + @required String secret, + }) async { + if (store.state.isLoading) { + return; + } + + if (url.isNotEmpty && !url.startsWith('http')) { + url = 'https://' + url; + } + + store.dispatch(RecoverPasswordRequest( + completer: completer, + email: email.trim(), + url: formatApiUrl(url.trim()), + secret: secret.trim(), + )); + }, + onLoginPressed: ( + BuildContext context, + Completer completer, { + @required String email, + @required String password, + @required String url, + @required String secret, + @required String oneTimePassword, + }) async { + if (store.state.isLoading) { + return; + } + + if (url.isNotEmpty && !url.startsWith('http')) { + url = 'https://' + url; + } + + store.dispatch(UserLoginRequest( + completer: completer, + email: email.trim(), + password: password.trim(), + url: formatApiUrl(url.trim()), + secret: secret.trim(), + platform: getPlatform(context), + oneTimePassword: oneTimePassword.trim(), + )); + completer.future.then((_) => _handleLogin(context: context)); + }); + } +} diff --git a/lib/ui/settings/import_export.dart b/lib/ui/settings/import_export.dart index fab9da9501a..eceddc58b63 100644 --- a/lib/ui/settings/import_export.dart +++ b/lib/ui/settings/import_export.dart @@ -8,9 +8,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:http/http.dart'; import 'package:invoiceninja_flutter/constants.dart'; -import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/data/models/import_model.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; +import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/data/web_client.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -42,7 +42,7 @@ class _ImportExportState extends State { FocusScopeNode _focusNode; bool autoValidate = false; PreImportResponse _response; - var _entityType = EntityType.client; + var _importType = ImportType.csv; @override void initState() { @@ -74,15 +74,24 @@ class _ImportExportState extends State { children: [ if (_response == null) _FileImport( - entityType: _entityType, - onUploaded: (response) => setState(() => _response = response), - onEntityTypeChanged: (entityType) => - setState(() => _entityType = entityType), + importType: _importType, + onUploaded: (response) => { + if (_importType == ImportType.csv) + { + setState(() => _response = response), + } + else + { + showToast(localization.startedImport), + } + }, + onImportTypeChanged: (importType) => + setState(() => _importType = importType), ) else _FileMapper( key: ValueKey(_response.hash), - entityType: _entityType, + importType: _importType, formKey: _formKey, response: _response, onCancelPressed: () => setState(() => _response = null), @@ -96,13 +105,13 @@ class _ImportExportState extends State { class _FileImport extends StatefulWidget { const _FileImport({ - @required this.entityType, - @required this.onEntityTypeChanged, + @required this.importType, + @required this.onImportTypeChanged, @required this.onUploaded, }); - final EntityType entityType; - final Function(EntityType) onEntityTypeChanged; + final ImportType importType; + final Function(ImportType) onImportTypeChanged; final Function(PreImportResponse) onUploaded; @override @@ -110,133 +119,156 @@ class _FileImport extends StatefulWidget { } class _FileImportState extends State<_FileImport> { - MultipartFile _multipartFile; + final Map _multipartFiles = {}; bool _isLoading = false; void uploadFile() { - if (true) { - final webClient = WebClient(); - final state = StoreProvider.of(context).state; - final credentials = state.credentials; - final url = '${credentials.url}/preimport'; - - setState(() => _isLoading = true); - - webClient.post( - url, - credentials.token, - multipartFile: _multipartFile, - data: { - 'entity_type': widget.entityType.snakeCase, - }, - ).then((dynamic result) { - setState(() => _isLoading = false); + final localization = AppLocalization.of(context); + + if (widget.importType != ImportType.csv) { + /* + for (MapEntry uploadPart in widget.importType.uploadParts.entries) { + if (!_multipartFiles.containsKey(uploadPart.key)) { + showErrorDialog( + context: context, message: localization.requiredFilesMissing); + return; + } + } + */ + } + + final webClient = WebClient(); + final state = StoreProvider.of(context).state; + final credentials = state.credentials; + final url = widget.importType == ImportType.csv + ? '${credentials.url}/preimport' + : '${credentials.url}/import'; + + setState(() => _isLoading = true); + + webClient.post( + url, + credentials.token, + multipartFiles: _multipartFiles.values.toList(), + data: { + 'import_type': widget.importType.toString(), + }, + ).then((dynamic result) { + setState(() => {_isLoading = false, _multipartFiles.clear()}); + + if (widget.importType != ImportType.csv) { + showToast(localization.startedImport); + } else { final response = serializers.deserializeWith(PreImportResponse.serializer, result); - widget.onUploaded(response); - }).catchError((dynamic error) { - setState(() => _isLoading = false); - showErrorDialog(context: context, message: '$error'); - }); - } else { - //const dataStr = '{"hash":"GdfMUa4ULdW6fTP4IXIB4LBQlxHZVH64","headers":[["Client","Email","User","Invoice Number","Amount","Paid","PO Number","Status","Invoice Date","Due Date","Discount","Partial\/Deposit","Partial Due Date","Public Notes","Private Notes","surcharge Label","tax tax","crv","ody","Item Product","Item Notes","prod1","prod2","Item Cost","Item Quantity","Item Tax Name","Item Tax Rate","Item Tax Name","Item Tax Rate"],["Test","g@gmail.com","David Bomba","0001","\$10.00","\$10.00","","Archived","2016-02-01","","","\$0.00","","","","0","0","","","10","Green Men","","","10","1","","0","","0"]]}'; - const dataStr = - '{"hash":"GdfMUa4ULdW6fTP4IXIB4LBQlxHZVH64","available":["invoice.client_id","invoice.invoice_number","invoice.user","payment.date","invoice.custom1","invoice.custom2","invoice.custom3","invoice.custom4"],"headers":[["Client","Email","User","Invoice Number","Amount","Paid","PO Number","Status","Invoice Date","Due Date","Discount","Partial\/Deposit","Partial Due Date"],["Test","g@gmail.com","David Bomba","0001","\$10.00","\$10.00","","Archived","2016-02-01","","","\$0.00","","","","0","0","","","10","Green Men","","","10","1","","0","","0"]]}'; - - final response = serializers.deserializeWith( - PreImportResponse.serializer, json.decode(dataStr)); - - widget.onUploaded(response); - } + } + }).catchError((dynamic error) { + setState(() => _isLoading = false); + showErrorDialog(context: context, message: '$error'); + }); } @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); - return FormCard( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - InputDecorator( - decoration: InputDecoration( - labelText: localization.importType, - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isDense: true, - value: widget.entityType, - onChanged: (dynamic value) => widget.onEntityTypeChanged(value), - items: [ - EntityType.client, - EntityType.product, - EntityType.invoice, - ] - .map((entityType) => DropdownMenuItem( - value: entityType, - child: Text(localization.lookup('$entityType')))) - .toList()), - ), + List children = [ + InputDecorator( + decoration: InputDecoration( + labelText: localization.importType, ), - DecoratedFormField( - key: ValueKey(_multipartFile?.filename), - enabled: false, - label: localization.csvFile, - initialValue: _multipartFile == null - ? localization.noFileSelected - : '${_multipartFile.filename} • ${formatSize(_multipartFile.length)}'), - SizedBox(height: 20), - if (_isLoading) - LinearProgressIndicator() - else - Row( - children: [ - Expanded( - child: OutlineButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - child: Text(localization.selectFile), - onPressed: () async { - final multipartFile = await pickFile( - fileType: FileType.custom, - allowedExtensions: ['csv'], - ); - - if (multipartFile != null) { - setState(() { - _multipartFile = multipartFile; - }); - } - }, - ), - ), - SizedBox(width: kTableColumnGap), - Expanded( - child: OutlineButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - child: Text(localization.uploadFile), - onPressed: _multipartFile == null ? null : () => uploadFile(), - //onPressed: () => uploadFile(), - ), - ), - ], - ), - ], - ); + child: DropdownButtonHideUnderline( + child: DropdownButton( + isDense: true, + value: widget.importType, + onChanged: (dynamic value) => widget.onImportTypeChanged(value), + items: [ + ImportType.csv, + ImportType.freshbooks, + ImportType.invoice2go, + ImportType.invoicely, + ImportType.waveaccounting, + ImportType.zoho, + ] + .map((importType) => DropdownMenuItem( + value: importType, + child: Text(localization.lookup('$importType')))) + .toList()), + ), + ) + ]; + + /* + for (MapEntry uploadPart in widget.importType.uploadParts.entries) { + final multipartFile = _multipartFiles.containsKey(uploadPart.key) + ? _multipartFiles[uploadPart.key] + : null; + + final field = DecoratedFormField( + enabled: false, + key: ValueKey(uploadPart.key + + (multipartFile != null ? multipartFile.filename : '')), + label: localization.lookup(uploadPart.value), + initialValue: !_multipartFiles.containsKey(uploadPart.key) + ? localization.noFileSelected + : '${_multipartFiles[uploadPart.key].filename} • ${formatSize( + _multipartFiles[uploadPart.key].length)}'); + + children.add(Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded(child: field), + SizedBox(width: kTableColumnGap), + OutlineButton( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + child: Text(localization.selectFile), + onPressed: () async { + final multipartFile = await pickFile( + fileIndex: 'files[' + uploadPart.key + ']', + fileType: FileType.custom, + allowedExtensions: ['csv'], + ); + + if (multipartFile != null) { + setState(() { + _multipartFiles[uploadPart.key] = multipartFile; + }); + } + }, + ), + ])); + } + + */ + + children.add(SizedBox(height: 20)); + + if (_isLoading) + children.add(LinearProgressIndicator()); + else + children.add(OutlineButton( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + child: Text(localization.uploadFile), + onPressed: _multipartFiles == null ? null : () => uploadFile(), + //onPressed: () => uploadFile(), + )); + + return FormCard( + crossAxisAlignment: CrossAxisAlignment.stretch, children: children); } } class _FileMapper extends StatefulWidget { const _FileMapper({ Key key, - @required this.entityType, + @required this.importType, @required this.response, @required this.onCancelPressed, @required this.formKey, }) : super(key: key); - final EntityType entityType; + final ImportType importType; final PreImportResponse response; final Function onCancelPressed; final GlobalKey formKey; @@ -247,7 +279,7 @@ class _FileMapper extends StatefulWidget { class __FileMapperState extends State<_FileMapper> { bool _useFirstRowAsHeaders = true; - final _mapping = {}; + final _mapping = >{}; bool _isLoading = false; @override @@ -256,17 +288,24 @@ class __FileMapperState extends State<_FileMapper> { final localization = AppLocalization.of(context); final response = widget.response; - final fields = response.fields1; - for (var i = 0; i < fields.length; i++) { - final field = fields[i]; - for (var availableField in response.available) { - final possible = availableField.split('.').last; - final spaceCase = possible.replaceAll('_', ' '); - final translated = localization.lookup(possible); + for (MapEntry entry + in response.mappings.entries) { + final fields = entry.value.fields1; + if (!_mapping.containsKey(entry.key)) { + _mapping[entry.key] = {}; + } + + for (var i = 0; i < fields.length; i++) { + final field = fields[i]; + for (var availableField in entry.value.available) { + final possible = availableField.split('.').last; + final spaceCase = possible.replaceAll('_', ' '); + final translated = localization.lookup(possible); - if ([possible, spaceCase, translated].contains(field.toLowerCase())) { - _mapping[i] = availableField; + if ([possible, spaceCase, translated].contains(field.toLowerCase())) { + _mapping[entry.key][i] = availableField; + } } } } @@ -277,116 +316,142 @@ class __FileMapperState extends State<_FileMapper> { final localization = AppLocalization.of(context); final response = widget.response; - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only(bottom: 20), - child: FormCard( - crossAxisAlignment: CrossAxisAlignment.start, + final List children = [ + SwitchListTile( + activeColor: Theme.of(context).accentColor, + title: Text(AppLocalization.of(context).firstRowAsColumnNames), + value: _useFirstRowAsHeaders, + onChanged: (value) => setState(() => _useFirstRowAsHeaders = value), + ), + ]; + + for (MapEntry entry + in response.mappings.entries) { + children.addAll([ + SizedBox(height: 25), + Text( + localization.lookup(entry.key), + style: Theme.of(context).textTheme.subtitle1, + overflow: TextOverflow.clip, + maxLines: 1, + ), + SizedBox(height: 12), + Row( children: [ - SwitchListTile( - activeColor: Theme.of(context).accentColor, - title: Text(AppLocalization.of(context).firstRowAsColumnNames), - value: _useFirstRowAsHeaders, - onChanged: (value) => - setState(() => _useFirstRowAsHeaders = value), + Expanded( + child: Text(_useFirstRowAsHeaders + ? localization.column + : localization.sample), + ), + Expanded( + child: Text(localization.sample), ), - SizedBox(height: 25), - Row( - children: [ - Expanded( - child: Text(_useFirstRowAsHeaders - ? localization.column - : localization.sample), - ), - Expanded( - child: Text(localization.sample), - ), - Expanded( - child: Text(localization.mapTo), - ), - ], + Expanded( + child: Text(localization.mapTo), ), - SizedBox(height: 12), - for (var i = 0; i < response.fields1.length; i++) - _FieldMapper( - field1: response.fields1[i], - field2: - response.fields2.length > i ? response.fields2[i] : null, - available: response.available, - mappedTo: _mapping[i] ?? '', - mapping: _mapping, - onMappedToChanged: (String value) { - setState(() { - _mapping[i] = value; - widget.formKey.currentState.validate(); + ], + ), + SizedBox(height: 12), + for (var i = 0; i < entry.value.fields1.length; i++) + _FieldMapper( + field1: entry.value.fields1[i], + field2: + entry.value.fields2.length > i ? entry.value.fields2[i] : null, + available: entry.value.available, + mappedTo: _mapping[entry.key][i] ?? '', + mapping: _mapping[entry.key], + onMappedToChanged: (String value) { + setState(() { + if (!_mapping.containsKey(entry.key)) { + _mapping[entry.key] = {}; + } + + _mapping[entry.key][i] = value; + widget.formKey.currentState.validate(); + }); + }, + ), + ]); + } + + children.addAll([ + SizedBox(height: 25), + if (_isLoading) + LinearProgressIndicator() + else + Row( + children: [ + Expanded( + child: OutlineButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + child: Text(localization.cancel), + onPressed: () => widget.onCancelPressed(), + ), + ), + SizedBox(width: kTableColumnGap), + Expanded( + child: OutlineButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + child: Text(localization.import), + onPressed: () { + final bool isValid = widget.formKey.currentState.validate(); + + if (!isValid) { + return; + } + + final webClient = WebClient(); + final state = StoreProvider.of(context).state; + final credentials = state.credentials; + final url = '${credentials.url}/import'; + final convertedMapping = >{}; + + for (MapEntry> e + in _mapping.entries) { + convertedMapping[e.key] = BuiltMap(e.value); + } + + setState(() => _isLoading = true); + + final importRequest = ImportRequest( + hash: widget.response.hash, + skipHeader: _useFirstRowAsHeaders, + columnMap: BuiltMap(convertedMapping), + importType: widget.importType.name, + ); + + final data = serializers.serializeWith( + ImportRequest.serializer, importRequest); + + webClient + .post( + url, + credentials.token, + data: json.encode(data), + ) + .then((dynamic result) { + setState(() => _isLoading = false); + widget.onCancelPressed(); + showToast(localization.startedImport); + }).catchError((dynamic error) { + setState(() => _isLoading = false); + showErrorDialog(context: context, message: '$error'); }); }, ), - SizedBox(height: 25), - if (_isLoading) - LinearProgressIndicator() - else - Row( - children: [ - Expanded( - child: OutlineButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - child: Text(localization.cancel), - onPressed: () => widget.onCancelPressed(), - ), - ), - SizedBox(width: kTableColumnGap), - Expanded( - child: OutlineButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - child: Text(localization.import), - onPressed: () { - final bool isValid = - widget.formKey.currentState.validate(); - - if (!isValid) { - return; - } - - final webClient = WebClient(); - final state = StoreProvider.of(context).state; - final credentials = state.credentials; - final url = '${credentials.url}/import'; - - setState(() => _isLoading = true); - - final importRequest = ImportRequest( - hash: widget.response.hash, - skipHeader: _useFirstRowAsHeaders, - columnMap: BuiltMap(_mapping), - entityType: widget.entityType.snakeCase, - ); - - final data = serializers.serializeWith( - ImportRequest.serializer, importRequest); - - webClient - .post( - url, - credentials.token, - data: json.encode(data), - ) - .then((dynamic result) { - setState(() => _isLoading = false); - widget.onCancelPressed(); - showToast(localization.startedImport); - }).catchError((dynamic error) { - setState(() => _isLoading = false); - showErrorDialog(context: context, message: '$error'); - }); - }, - ), - ), - ], - ), + ), ], + ) + ]); + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: FormCard( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, ), ), ); diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 4a960f5bceb..18f7325f140 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -78,6 +78,14 @@ mixin LocalizationsProvider on LocaleCodeAware { 'select_file': 'Select File', 'no_file_selected': 'No File Selected', 'csv_file': 'CSV File', + 'csv': 'CSV', + 'freshbooks': 'FreshBooks', + 'invoice2go': 'Invoice2go', + 'invoicely': 'Invoicely', + 'waveaccounting': 'Wave Accounting', + 'zoho': 'Zoho', + 'accounting': 'Accounting', + 'required_files_missing': 'Please provide all CSVs.', 'import_type': 'Import Type', 'draft_mode': 'Draft Mode', 'draft_mode_help': 'Preview updates faster but is less accurate', @@ -51587,6 +51595,8 @@ mixin LocalizationsProvider on LocaleCodeAware { String get uploadFile => _localizedValues[localeCode]['upload_file'] ?? ''; + String get requiredFilesMissing => _localizedValues[localeCode]['required_files_missing'] ?? ''; + String get download => _localizedValues[localeCode]['download'] ?? ''; String get noRecordSelected => diff --git a/pubspec.lock b/pubspec.lock index 18303e4ed98..6b486e9cdc9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -63,7 +63,7 @@ packages: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "2.1.7" + version: "2.1.8" build_resolvers: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d6c1afcbfe..4e79cefca2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,6 @@ dependencies: flutter_redux: ^0.7.0 redux_logging: ^0.4.0 http: ^0.12.0+4 - in_app_purchase: ^0.3.1+1 path_provider: ^1.6.1 shared_preferences: ^0.5.12 material_design_icons_flutter: ^4.0.5345 @@ -36,19 +35,6 @@ dependencies: intl: ^0.16.1 flutter_slidable: ^0.5.4 charts_flutter: ^0.9.0 - firebase_auth: 0.15.2 #https://github.com/FirebaseExtended/flutterfire/issues/2433#issuecomment-622438185 - #firebase_auth: ^0.18.4+1 - #google_sign_in: ^4.5.1 - google_sign_in: - git: - url: https://github.com/invoiceninja/plugins.git - path: packages/google_sign_in/google_sign_in - ref: master - google_sign_in_web: - git: - url: https://github.com/invoiceninja/plugins.git - path: packages/google_sign_in/google_sign_in_web - ref: master local_auth: ^0.6.1+3 sentry_flutter: ^4.0.4 image_picker: ^0.6.3+4 @@ -75,6 +61,18 @@ dependencies: extended_image: 1.3.1-dev #TODO remove file_picker: ^2.1.1 draggable_scrollbar: ^0.0.4 + in_app_purchase: ^0.3.1+1 + firebase_auth: 0.15.2 #https://github.com/FirebaseExtended/flutterfire/issues/2433#issuecomment-622438185 + google_sign_in: + git: + url: https://github.com/invoiceninja/plugins.git + path: packages/google_sign_in/google_sign_in + ref: master + google_sign_in_web: + git: + url: https://github.com/invoiceninja/plugins.git + path: packages/google_sign_in/google_sign_in_web + ref: master dependency_overrides: # https://github.com/flutter/flutter/issues/70433#issuecomment-727154345 diff --git a/pubspec.yaml.foss b/pubspec.yaml.foss new file mode 100644 index 00000000000..79e72ee8854 --- /dev/null +++ b/pubspec.yaml.foss @@ -0,0 +1,91 @@ +name: invoiceninja_flutter +description: Client for Invoice Ninja +version: 5.0.42+42 +author: Hillel Coren +homepage: https://invoiceninja.com +documentation: http://docs.invoiceninja.com +publish_to: none + +environment: + sdk: ">=2.9.0 <3.0.0" + +flutter_icons: + android: "launcher_icon" + ios: false + image_path: "assets/images/logo.png" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + flutter_redux: ^0.7.0 + redux_logging: ^0.4.0 + http: ^0.12.0+4 + path_provider: ^1.6.1 + shared_preferences: ^0.5.12 + material_design_icons_flutter: ^4.0.5345 + built_value: ^7.0.9 + built_collection: ^4.3.2 + memoize: ^2.0.0 + #cached_network_image: 2.3.0-beta.1 #https://github.com/Baseflow/flutter_cached_network_image/issues/395#issuecomment-635413976 + cached_network_image: ^2.5.0 + url_launcher: ^5.4.2 + share: ^0.6.3+6 + intl: ^0.16.1 + flutter_slidable: ^0.5.4 + charts_flutter: ^0.9.0 + local_auth: ^0.6.1+3 + sentry_flutter: ^4.0.4 + image_picker: ^0.6.3+4 + flutter_colorpicker: ^0.3.2 + flutter_json_widget: ^1.0.2 + webview_flutter: ^0.3.19+8 + timeago: ^2.0.26 + native_pdf_view: ^3.9.0 + #flutter_typeahead: 1.8.0 + flutter_typeahead: + git: + url: git://github.com/hillelcoren/flutter_typeahead.git + flutter_share: ^1.0.2+1 + package_info: ^0.4.0+16 + rounded_loading_button: ^1.0.0 + # quick_actions: ^0.2.1 + version: ^1.0.0 + # idb_shim: ^1.11.1+1 + flutter_launcher_icons: ^0.8.1 + overflow_view: ^0.2.1 + flutter_styled_toast: ^1.5.1+1 + permission_handler: ^5.0.1+1 + contacts_service: ^0.4.6 + extended_image: 1.3.1-dev #TODO remove + file_picker: ^2.1.1 + draggable_scrollbar: ^0.0.4 + +dependency_overrides: + # https://github.com/flutter/flutter/issues/70433#issuecomment-727154345 + intl: ^0.17.0-nullsafety.2 + # https://github.com/flutter/flutter/issues/57712#issuecomment-703382420 + google_sign_in_platform_interface: + git: + url: https://github.com/invoiceninja/plugins.git + path: packages/google_sign_in/google_sign_in_platform_interface + +dev_dependencies: + #flutter_driver: # TODO Re-enable + # sdk: flutter + test: ^1.6.3 + #flutter_test: + # sdk: flutter + build_runner: ^1.7.4 + built_value_generator: ^7.1.0 + faker: ^1.1.1 + +flutter: + + uses-material-design: true + + assets: + - assets/images/logo.png + - assets/images/google-icon.png + - assets/images/payment_types/