diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2917adc9eee..fa16644ac16 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -380,7 +380,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.0.80; + MARKETING_VERSION = 5.0.81; PRODUCT_BUNDLE_IDENTIFIER = com.invoiceninja.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -510,7 +510,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.0.80; + MARKETING_VERSION = 5.0.81; PRODUCT_BUNDLE_IDENTIFIER = com.invoiceninja.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -534,7 +534,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.0.80; + MARKETING_VERSION = 5.0.81; PRODUCT_BUNDLE_IDENTIFIER = com.invoiceninja.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/lib/constants.dart b/lib/constants.dart index cc45c6223eb..e60bdfa1821 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.80'; +const String kClientVersion = '5.0.81'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/lib/data/web_client.dart b/lib/data/web_client.dart index 649f769eec2..e450192fb5d 100644 --- a/lib/data/web_client.dart +++ b/lib/data/web_client.dart @@ -36,6 +36,8 @@ class WebClient { url += '&per_page=999999'; } + url += '&t=${DateTime.now().millisecondsSinceEpoch}'; + print('GET: $url'); final client = http.Client(); diff --git a/lib/ui/app/entity_dropdown.dart b/lib/ui/app/entity_dropdown.dart index 185c45d8fe4..dcb0aada3de 100644 --- a/lib/ui/app/entity_dropdown.dart +++ b/lib/ui/app/entity_dropdown.dart @@ -12,6 +12,7 @@ import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/.env.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/app_border.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; @@ -38,6 +39,7 @@ class EntityDropdown extends StatefulWidget { this.onFieldSubmitted, this.overrideSuggestedAmount, this.overrideSuggestedLabel, + this.onCreateNew, }); final EntityType entityType; @@ -54,6 +56,7 @@ class EntityDropdown extends StatefulWidget { final Function(Completer completer) onAddPressed; final Function(BaseEntity) overrideSuggestedAmount; final Function(BaseEntity) overrideSuggestedLabel; + final Function(Completer completer, String) onCreateNew; @override _EntityDropdownState createState() => _EntityDropdownState(); @@ -104,6 +107,8 @@ class _EntityDropdownState extends State { super.didUpdateWidget(oldWidget); if (widget.entityId != oldWidget.entityId) { + final state = StoreProvider.of(context).state; + _entityMap = widget.entityMap ?? state.getEntityMap(widget.entityType); _textController.text = _getEntityLabel(_entityMap[widget.entityId]); } } @@ -252,6 +257,12 @@ class _EntityDropdownState extends State { return []; } + if (widget.onCreateNew != null && + options.isEmpty && + textEditingValue.text.isNotEmpty) { + options.add(_AutocompleteEntity(name: textEditingValue.text)); + } + return options; }, displayStringForOption: (entity) => entity.listDisplayName, @@ -266,14 +277,32 @@ class _EntityDropdownState extends State { return; } - widget.onSelected(entity); + void _wrapUp(SelectableEntity entity) { + widget.onSelected(entity); - _focusNode.requestFocus(); + _focusNode.requestFocus(); - WidgetsBinding.instance.addPostFrameCallback((duration) { - _textController.selection = TextSelection.fromPosition( - TextPosition(offset: _textController.text.length)); - }); + WidgetsBinding.instance.addPostFrameCallback((duration) { + _textController.selection = TextSelection.fromPosition( + TextPosition(offset: _textController.text.length)); + }); + } + + if (entity?.id == _AutocompleteEntity.KEY) { + final name = (entity as _AutocompleteEntity).name; + _textController.text = name; + _focusNode.removeListener(_onFocusChanged); + final completer = Completer(); + completer.future.then((value) { + _wrapUp(value); + _focusNode.addListener(_onFocusChanged); + }).catchError((dynamic error) { + _focusNode.addListener(_onFocusChanged); + }); + widget.onCreateNew(completer, name); + } else { + _wrapUp(entity); + } }, fieldViewBuilder: (BuildContext context, TextEditingController textEditingController, @@ -551,3 +580,29 @@ class EntityAutocompleteListTile extends StatelessWidget { ); } } + +class _AutocompleteEntity extends Object with SelectableEntity { + _AutocompleteEntity({this.name}); + + static const KEY = '__new__'; + + final String name; + + @override + String get id => KEY; + + @override + bool matchesFilter(String filter) => true; + + @override + String matchesFilterValue(String filter) => null; + + @override + String get listDisplayName { + final localization = AppLocalization.of(navigatorKey.currentContext); + return '${localization.create}: $name'; + } + + @override + double get listDisplayAmount => null; +} diff --git a/lib/ui/app/forms/notification_settings.dart b/lib/ui/app/forms/notification_settings.dart index 396b6b58264..679acc0bbce 100644 --- a/lib/ui/app/forms/notification_settings.dart +++ b/lib/ui/app/forms/notification_settings.dart @@ -116,7 +116,10 @@ class NotificationSettings extends StatelessWidget { value = NOTIFY_NONE; } return DataRow(cells: [ - DataCell(Text(localization.lookup(eventType))), + // workaround for mistake in translations + DataCell(Text(eventType == kNotificationsInvoiceSent + ? localization.notificationInvoiceSent + : localization.lookup(eventType))), DataCell(isAllEnabled ? value == NOTIFY_ALL ? IconText( diff --git a/lib/ui/expense/edit/expense_edit_details.dart b/lib/ui/expense/edit/expense_edit_details.dart index aac0200b2e4..1c5b90481b3 100644 --- a/lib/ui/expense/edit/expense_edit_details.dart +++ b/lib/ui/expense/edit/expense_edit_details.dart @@ -161,6 +161,13 @@ class ExpenseEditDetailsState extends State { onAddPressed: (completer) { viewModel.onAddVendorPressed(context, completer); }, + /* + onCreateNew: (completer, name) { + store.dispatch(SaveVendorRequest( + vendor: VendorEntity().rebuild((b) => b..name = name), + completer: completer)); + }, + */ ), if (!expense.isInvoiced) ...[ EntityDropdown( diff --git a/lib/ui/reports/reports_screen_vm.dart b/lib/ui/reports/reports_screen_vm.dart index 3c2e4c466f0..4fee61f3b55 100644 --- a/lib/ui/reports/reports_screen_vm.dart +++ b/lib/ui/reports/reports_screen_vm.dart @@ -404,10 +404,8 @@ class ReportsScreenVM { final value = row[i] .renderText(context, column) .trim() - .replaceAll('"', '\\"'); - csvData += value.contains(' ') || value.contains('"') - ? '"$value",' - : '$value,'; + .replaceAll('"', '""'); + csvData += '"$value",'; } csvData = csvData.substring(0, csvData.length - 1); }); @@ -432,14 +430,12 @@ class ReportsScreenVM { groupTotals.rows.forEach((group) { final row = groupTotals.totals[group]; csvData += - '"${group.trim().replaceAll('"', '\\"')}",${row['count'].toInt()}'; + '"${group.trim().replaceAll('"', '""')}",${row['count'].toInt()}'; columns.forEach((column) { final value = - row[column].toString().trim().replaceAll('"', '\\"'); - csvData += value.contains(' ') || value.contains('"') - ? ',"$value"' - : ',$value'; + row[column].toString().trim().replaceAll('"', '""'); + csvData += ',"$value"'; }); csvData += '\n'; diff --git a/lib/ui/settings/client_portal.dart b/lib/ui/settings/client_portal.dart index ed7b50806ed..78e029a3458 100644 --- a/lib/ui/settings/client_portal.dart +++ b/lib/ui/settings/client_portal.dart @@ -392,13 +392,6 @@ class _ClientPortalState extends State .rebuild((b) => b..enablePortalDashboard = value)), ), */ - BoolDropdownButton( - label: localization.tasks, - value: settings.enablePortalTasks, - iconData: getEntityIcon(EntityType.task), - onChanged: (value) => viewModel.onSettingsChanged( - settings.rebuild((b) => b..enablePortalTasks = value)), - ), BoolDropdownButton( label: localization.documentUpload, helpLabel: localization.documentUploadHelp, diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index fc9162b089c..60d584975f2 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -408,6 +408,7 @@ class SettingsSearch extends StatelessWidget { 'task_settings', 'auto_start_tasks', 'show_tasks_table', + 'client_portal', ], ], kSettingsTaskStatuses: [ @@ -515,7 +516,6 @@ class SettingsSearch extends StatelessWidget { [ 'client_portal', 'dashboard', - 'tasks', 'portal_mode', 'subdomain', 'domain', diff --git a/lib/ui/settings/task_settings.dart b/lib/ui/settings/task_settings.dart index 583ca88f6ad..90e02cb5026 100644 --- a/lib/ui/settings/task_settings.dart +++ b/lib/ui/settings/task_settings.dart @@ -1,5 +1,6 @@ // Flutter imports: import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/constants.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/settings_model.dart'; @@ -8,9 +9,11 @@ import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_form.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/bool_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/settings/task_settings_vm.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/icons.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class TaskSettings extends StatefulWidget { @@ -160,29 +163,37 @@ class _TaskSettingsState extends State { ], ], ), - if ((settings.enablePortalTasks ?? false) != false) - FormCard( - children: [ - AppDropdownButton( - labelText: localization.tasksShownInPortal, - value: settings.clientPortalTasks, - onChanged: (dynamic value) { - viewModel.onSettingsChanged( - settings.rebuild((b) => b..clientPortalTasks = value)); - }, - items: [ - SettingsEntity.PORTAL_TASKS_INVOICED, - SettingsEntity.PORTAL_TASKS_UNINVOICED, - SettingsEntity.PORTAL_TASKS_ALL, - ] - .map((value) => DropdownMenuItem( - child: Text(localization.lookup(value)), - value: value, - )) - .toList(), - ), - ], - ), + FormCard( + children: [ + BoolDropdownButton( + label: localization.clientPortal, + value: settings.enablePortalTasks, + iconData: getSettingIcon(kSettingsClientPortal), + onChanged: (value) => viewModel.onSettingsChanged( + settings.rebuild((b) => b..enablePortalTasks = value)), + ), + AppDropdownButton( + labelText: localization.tasksShownInPortal, + value: settings.clientPortalTasks, + onChanged: ((settings.enablePortalTasks ?? false) != false) + ? (dynamic value) { + viewModel.onSettingsChanged(settings + .rebuild((b) => b..clientPortalTasks = value)); + } + : null, + items: [ + SettingsEntity.PORTAL_TASKS_INVOICED, + SettingsEntity.PORTAL_TASKS_UNINVOICED, + SettingsEntity.PORTAL_TASKS_ALL, + ] + .map((value) => DropdownMenuItem( + child: Text(localization.lookup(value)), + value: value, + )) + .toList(), + ), + ], + ), if (!viewModel.state.settingsUIState.isFiltered) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index d298ea32870..b56829e5ade 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -16,6 +16,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'notification_invoice_sent': 'Invoice Sent', 'auto_archive_paid_invoices': 'Auto Archive Paid', 'auto_archive_paid_invoices_help': 'Automatically archive invoices when they are paid.', @@ -75031,6 +75032,10 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]['auto_archive_cancelled_invoices_help'] ?? _localizedValues[localeCode]['auto_archive_cancelled_invoices_help']; + String get notificationInvoiceSent => + _localizedValues[localeCode]['notification_invoice_sent'] ?? + _localizedValues[localeCode]['notification_invoice_sent']; + // STARTER: lang field - do not remove comment String lookup(String key) { diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index adc8eaa2b44..9fbf6ee350e 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -426,7 +426,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 5.0.80; + MARKETING_VERSION = 5.0.81; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -554,7 +554,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 5.0.80; + MARKETING_VERSION = 5.0.81; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -576,7 +576,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 5.0.80; + MARKETING_VERSION = 5.0.81; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 4cba09be19a..ad41424f310 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.80+80 +version: 5.0.81+81 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none @@ -94,7 +94,7 @@ msix_config: display_name: Invoice Ninja publisher_display_name: Invoice Ninja identity_name: InvoiceNinja.InvoiceNinja - msix_version: 5.0.80.0 + msix_version: 5.0.81.0 publisher: CN=2B7AA393-06A0-46F5-AF85-1917142440C3 architecture: x64 capabilities: 'internetClient' diff --git a/pubspec.next.yaml b/pubspec.next.yaml index b47d600ffd9..d669339714a 100644 --- a/pubspec.next.yaml +++ b/pubspec.next.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.80+80 +version: 5.0.81+81 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none @@ -94,7 +94,7 @@ msix_config: display_name: Invoice Ninja publisher_display_name: Invoice Ninja identity_name: InvoiceNinja.InvoiceNinja - msix_version: 5.0.80.0 + msix_version: 5.0.81.0 publisher: CN=2B7AA393-06A0-46F5-AF85-1917142440C3 architecture: x64 capabilities: 'internetClient' diff --git a/pubspec.yaml b/pubspec.yaml index 96ed4c800f3..6968180172a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.80+80 +version: 5.0.81+81 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none @@ -100,7 +100,7 @@ msix_config: display_name: Invoice Ninja publisher_display_name: Invoice Ninja identity_name: InvoiceNinja.InvoiceNinja - msix_version: 5.0.80.0 + msix_version: 5.0.81.0 publisher: CN=2B7AA393-06A0-46F5-AF85-1917142440C3 architecture: x64 capabilities: 'internetClient' diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 616e0f01960..4d543c2e398 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.80' +version: '5.0.81' summary: Create invoices, accept payments, track expenses & time-tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead