This commit is contained in:
Hillel Coren 2018-12-13 13:21:56 +02:00
parent 53ed6494d4
commit ee470f6eff
33 changed files with 2162 additions and 39 deletions

View File

@ -22,6 +22,9 @@ import 'package:invoiceninja_flutter/redux/client/client_state.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_state.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_state.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/data/models/project_model.dart';
import 'package:invoiceninja_flutter/redux/project/project_state.dart';
import 'package:invoiceninja_flutter/redux/payment/payment_state.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
@ -75,6 +78,8 @@ part 'serializers.g.dart';
TimezoneItemResponse,
TimezoneListResponse,
// STARTER: serializers - do not remove comment
ProjectEntity,
PaymentEntity,

View File

@ -99,6 +99,8 @@ Serializers _$serializers = (new Serializers().toBuilder()
..add(ProjectEntity.serializer)
..add(ProjectItemResponse.serializer)
..add(ProjectListResponse.serializer)
..add(ProjectState.serializer)
..add(ProjectUIState.serializer)
..add(QuoteState.serializer)
..add(QuoteUIState.serializer)
..add(SizeEntity.serializer)
@ -349,5 +351,7 @@ Serializers _$serializers = (new Serializers().toBuilder()
..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(PaymentEntity)]), () => new MapBuilder<int, PaymentEntity>())
..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder<int>())
..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(ProductEntity)]), () => new MapBuilder<int, ProductEntity>())
..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder<int>())
..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(ProjectEntity)]), () => new MapBuilder<int, ProjectEntity>())
..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder<int>()))
.build();

View File

@ -0,0 +1,69 @@
import 'dart:async';
import 'dart:convert';
import 'dart:core';
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/auth/auth_state.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/data/web_client.dart';
class ProjectRepository {
final WebClient webClient;
const ProjectRepository({
this.webClient = const WebClient(),
});
Future<ProjectEntity> loadItem(
CompanyEntity company, AuthState auth, int entityId) async {
final dynamic response = await webClient.get(
'${auth.url}/projects/$entityId', company.token);
final ProjectItemResponse projectResponse =
serializers.deserializeWith(ProjectItemResponse.serializer, response);
return projectResponse.data;
}
Future<BuiltList<ProjectEntity>> loadList(
CompanyEntity company, AuthState auth, int updatedAt) async {
String url = auth.url + '/projects?';
if (updatedAt > 0) {
url += '&updated_at=${updatedAt - kUpdatedAtBufferSeconds}';
}
final dynamic response = await webClient.get(url, company.token);
final ProjectListResponse projectResponse =
serializers.deserializeWith(ProjectListResponse.serializer, response);
return projectResponse.data;
}
Future<ProjectEntity> saveData(
CompanyEntity company, AuthState auth, ProjectEntity project,
[EntityAction action]) async {
final data = serializers.serializeWith(ProjectEntity.serializer, project);
dynamic response;
if (project.isNew) {
response = await webClient.post(
auth.url + '/projects',
company.token,
json.encode(data));
} else {
var url = auth.url + '/projects/' + project.id.toString();
if (action != null) {
url += '?action=' + action.toString();
}
response = await webClient.put(url, company.token, json.encode(data));
}
final ProjectItemResponse projectResponse =
serializers.deserializeWith(ProjectItemResponse.serializer, response);
return projectResponse.data;
}
}

View File

@ -40,6 +40,12 @@ import 'package:local_auth/local_auth.dart';
//import 'package:quick_actions/quick_actions.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/ui/project/project_screen.dart';
import 'package:invoiceninja_flutter/ui/project/edit/project_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/project/view/project_view_vm.dart';
import 'package:invoiceninja_flutter/redux/project/project_actions.dart';
import 'package:invoiceninja_flutter/redux/project/project_middleware.dart';
import 'package:invoiceninja_flutter/ui/payment/payment_screen.dart';
import 'package:invoiceninja_flutter/ui/payment/edit/payment_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/payment/view/payment_view_vm.dart';
@ -69,6 +75,8 @@ void main() async {
..addAll(createStoreInvoicesMiddleware())
..addAll(createStorePersistenceMiddleware())
// STARTER: middleware - do not remove comment
..addAll(createStoreProjectsMiddleware())
..addAll(createStorePaymentsMiddleware())
..addAll(createStoreQuotesMiddleware())
..addAll([
@ -240,6 +248,13 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
InvoiceEditScreen.route: (context) => InvoiceEditScreen(),
InvoiceEmailScreen.route: (context) => InvoiceEmailScreen(),
// STARTER: routes - do not remove comment
ProjectScreen.route: (context) {
widget.store.dispatch(LoadProjects());
return ProjectScreen();
},
ProjectViewScreen.route: (context) => ProjectViewScreen(),
ProjectEditScreen.route: (context) => ProjectEditScreen(),
PaymentScreen.route: (context) {
if (widget.store.state.paymentState.isStale) {
widget.store.dispatch(LoadPayments());

View File

@ -13,6 +13,8 @@ import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/project/project_state.dart';
import 'package:invoiceninja_flutter/redux/payment/payment_state.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
@ -103,9 +105,10 @@ abstract class AppState implements Built<AppState, AppStateBuilder> {
case EntityType.invoice:
return invoiceUIState;
// STARTER: states switch - do not remove comment
case EntityType.project:
return projectUIState;
case EntityType.payment:
return paymentUIState;
case EntityType.quote:
return quoteUIState;
@ -137,6 +140,12 @@ abstract class AppState implements Built<AppState, AppStateBuilder> {
ListUIState get invoiceListState => uiState.invoiceUIState.listUIState;
// STARTER: state getters - do not remove comment
ProjectState get projectState => selectedCompanyState.projectState;
ListUIState get projectListState => uiState.projectUIState.listUIState;
ProjectUIState get projectUIState => uiState.projectUIState;
PaymentState get paymentState => selectedCompanyState.paymentState;
ListUIState get paymentListState => uiState.paymentUIState.listUIState;

View File

@ -9,6 +9,8 @@ import 'package:invoiceninja_flutter/redux/dashboard/dashboard_reducer.dart';
import 'package:invoiceninja_flutter/redux/company/company_actions.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/project/project_reducer.dart';
import 'package:invoiceninja_flutter/redux/payment/payment_reducer.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_reducer.dart';
@ -25,6 +27,8 @@ CompanyState companyReducer(CompanyState state, dynamic action) {
..productState.replace(productsReducer(state.productState, action))
..invoiceState.replace(invoicesReducer(state.invoiceState, action))
// STARTER: reducer - do not remove comment
..projectState.replace(projectsReducer(state.projectState, action))
..paymentState.replace(paymentsReducer(state.paymentState, action))
..quoteState.replace(quotesReducer(state.quoteState, action)));

View File

@ -7,6 +7,8 @@ import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/project/project_state.dart';
import 'package:invoiceninja_flutter/redux/payment/payment_state.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
@ -23,6 +25,8 @@ abstract class CompanyState
clientState: ClientState(),
invoiceState: InvoiceState(),
// STARTER: constructor - do not remove comment
projectState: ProjectState(),
paymentState: PaymentState(),
quoteState: QuoteState(),
);
@ -42,6 +46,8 @@ abstract class CompanyState
InvoiceState get invoiceState;
// STARTER: fields - do not remove comment
ProjectState get projectState;
PaymentState get paymentState;
QuoteState get quoteState;

View File

@ -44,6 +44,9 @@ class _$CompanyStateSerializer implements StructuredSerializer<CompanyState> {
'invoiceState',
serializers.serialize(object.invoiceState,
specifiedType: const FullType(InvoiceState)),
'projectState',
serializers.serialize(object.projectState,
specifiedType: const FullType(ProjectState)),
'paymentState',
serializers.serialize(object.paymentState,
specifiedType: const FullType(PaymentState)),
@ -92,6 +95,10 @@ class _$CompanyStateSerializer implements StructuredSerializer<CompanyState> {
result.invoiceState.replace(serializers.deserialize(value,
specifiedType: const FullType(InvoiceState)) as InvoiceState);
break;
case 'projectState':
result.projectState.replace(serializers.deserialize(value,
specifiedType: const FullType(ProjectState)) as ProjectState);
break;
case 'paymentState':
result.paymentState.replace(serializers.deserialize(value,
specifiedType: const FullType(PaymentState)) as PaymentState);
@ -119,6 +126,8 @@ class _$CompanyState extends CompanyState {
@override
final InvoiceState invoiceState;
@override
final ProjectState projectState;
@override
final PaymentState paymentState;
@override
final QuoteState quoteState;
@ -132,6 +141,7 @@ class _$CompanyState extends CompanyState {
this.productState,
this.clientState,
this.invoiceState,
this.projectState,
this.paymentState,
this.quoteState})
: super._() {
@ -147,6 +157,9 @@ class _$CompanyState extends CompanyState {
if (invoiceState == null) {
throw new BuiltValueNullFieldError('CompanyState', 'invoiceState');
}
if (projectState == null) {
throw new BuiltValueNullFieldError('CompanyState', 'projectState');
}
if (paymentState == null) {
throw new BuiltValueNullFieldError('CompanyState', 'paymentState');
}
@ -171,6 +184,7 @@ class _$CompanyState extends CompanyState {
productState == other.productState &&
clientState == other.clientState &&
invoiceState == other.invoiceState &&
projectState == other.projectState &&
paymentState == other.paymentState &&
quoteState == other.quoteState;
}
@ -181,10 +195,14 @@ class _$CompanyState extends CompanyState {
$jc(
$jc(
$jc(
$jc($jc($jc(0, company.hashCode), dashboardState.hashCode),
$jc(
$jc(
$jc($jc(0, company.hashCode),
dashboardState.hashCode),
productState.hashCode),
clientState.hashCode),
invoiceState.hashCode),
projectState.hashCode),
paymentState.hashCode),
quoteState.hashCode));
}
@ -197,6 +215,7 @@ class _$CompanyState extends CompanyState {
..add('productState', productState)
..add('clientState', clientState)
..add('invoiceState', invoiceState)
..add('projectState', projectState)
..add('paymentState', paymentState)
..add('quoteState', quoteState))
.toString();
@ -236,6 +255,12 @@ class CompanyStateBuilder
set invoiceState(InvoiceStateBuilder invoiceState) =>
_$this._invoiceState = invoiceState;
ProjectStateBuilder _projectState;
ProjectStateBuilder get projectState =>
_$this._projectState ??= new ProjectStateBuilder();
set projectState(ProjectStateBuilder projectState) =>
_$this._projectState = projectState;
PaymentStateBuilder _paymentState;
PaymentStateBuilder get paymentState =>
_$this._paymentState ??= new PaymentStateBuilder();
@ -257,6 +282,7 @@ class CompanyStateBuilder
_productState = _$v.productState?.toBuilder();
_clientState = _$v.clientState?.toBuilder();
_invoiceState = _$v.invoiceState?.toBuilder();
_projectState = _$v.projectState?.toBuilder();
_paymentState = _$v.paymentState?.toBuilder();
_quoteState = _$v.quoteState?.toBuilder();
_$v = null;
@ -288,6 +314,7 @@ class CompanyStateBuilder
productState: productState.build(),
clientState: clientState.build(),
invoiceState: invoiceState.build(),
projectState: projectState.build(),
paymentState: paymentState.build(),
quoteState: quoteState.build());
} catch (_) {
@ -303,6 +330,8 @@ class CompanyStateBuilder
clientState.build();
_$failedField = 'invoiceState';
invoiceState.build();
_$failedField = 'projectState';
projectState.build();
_$failedField = 'paymentState';
paymentState.build();
_$failedField = 'quoteState';

View File

@ -0,0 +1,226 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:built_collection/built_collection.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/app/app_actions.dart';
class ViewProjectList implements PersistUI {
ViewProjectList(this.context);
final BuildContext context;
}
class ViewProject implements PersistUI {
ViewProject({this.projectId, this.context});
final int projectId;
final BuildContext context;
}
class EditProject implements PersistUI {
EditProject({this.project, this.context, this.completer, this.trackRoute = true});
final ProjectEntity project;
final BuildContext context;
final Completer completer;
final bool trackRoute;
}
class UpdateProject implements PersistUI {
UpdateProject(this.project);
final ProjectEntity project;
}
class LoadProject {
LoadProject({this.completer, this.projectId, this.loadActivities = false});
final Completer completer;
final int projectId;
final bool loadActivities;
}
class LoadProjectActivity {
LoadProjectActivity({this.completer, this.projectId});
final Completer completer;
final int projectId;
}
class LoadProjects {
LoadProjects({this.completer, this.force = false});
final Completer completer;
final bool force;
}
class LoadProjectRequest implements StartLoading {}
class LoadProjectFailure implements StopLoading {
LoadProjectFailure(this.error);
final dynamic error;
@override
String toString() {
return 'LoadProjectFailure{error: $error}';
}
}
class LoadProjectSuccess implements StopLoading, PersistData {
LoadProjectSuccess(this.project);
final ProjectEntity project;
@override
String toString() {
return 'LoadProjectSuccess{project: $project}';
}
}
class LoadProjectsRequest implements StartLoading {}
class LoadProjectsFailure implements StopLoading {
LoadProjectsFailure(this.error);
final dynamic error;
@override
String toString() {
return 'LoadProjectsFailure{error: $error}';
}
}
class LoadProjectsSuccess implements StopLoading, PersistData {
LoadProjectsSuccess(this.projects);
final BuiltList<ProjectEntity> projects;
@override
String toString() {
return 'LoadProjectsSuccess{projects: $projects}';
}
}
class SaveProjectRequest implements StartSaving {
SaveProjectRequest({this.completer, this.project});
final Completer completer;
final ProjectEntity project;
}
class SaveProjectSuccess implements StopSaving, PersistData, PersistUI {
SaveProjectSuccess(this.project);
final ProjectEntity project;
}
class AddProjectSuccess implements StopSaving, PersistData, PersistUI {
AddProjectSuccess(this.project);
final ProjectEntity project;
}
class SaveProjectFailure implements StopSaving {
SaveProjectFailure (this.error);
final Object error;
}
class ArchiveProjectRequest implements StartSaving {
ArchiveProjectRequest(this.completer, this.projectId);
final Completer completer;
final int projectId;
}
class ArchiveProjectSuccess implements StopSaving, PersistData {
ArchiveProjectSuccess(this.project);
final ProjectEntity project;
}
class ArchiveProjectFailure implements StopSaving {
ArchiveProjectFailure(this.project);
final ProjectEntity project;
}
class DeleteProjectRequest implements StartSaving {
DeleteProjectRequest(this.completer, this.projectId);
final Completer completer;
final int projectId;
}
class DeleteProjectSuccess implements StopSaving, PersistData {
DeleteProjectSuccess(this.project);
final ProjectEntity project;
}
class DeleteProjectFailure implements StopSaving {
DeleteProjectFailure(this.project);
final ProjectEntity project;
}
class RestoreProjectRequest implements StartSaving {
RestoreProjectRequest(this.completer, this.projectId);
final Completer completer;
final int projectId;
}
class RestoreProjectSuccess implements StopSaving, PersistData {
RestoreProjectSuccess(this.project);
final ProjectEntity project;
}
class RestoreProjectFailure implements StopSaving {
RestoreProjectFailure(this.project);
final ProjectEntity project;
}
class FilterProjects {
FilterProjects(this.filter);
final String filter;
}
class SortProjects implements PersistUI {
SortProjects(this.field);
final String field;
}
class FilterProjectsByState implements PersistUI {
FilterProjectsByState(this.state);
final EntityState state;
}
class FilterProjectsByCustom1 implements PersistUI {
FilterProjectsByCustom1(this.value);
final String value;
}
class FilterProjectsByCustom2 implements PersistUI {
FilterProjectsByCustom2(this.value);
final String value;
}
class FilterProjectsByEntity implements PersistUI {
FilterProjectsByEntity({this.entityId, this.entityType});
final int entityId;
final EntityType entityType;
}

View File

@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/product/product_actions.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/project/project_screen.dart';
import 'package:invoiceninja_flutter/ui/project/edit/project_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/project/view/project_view_vm.dart';
import 'package:invoiceninja_flutter/redux/project/project_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/data/repositories/project_repository.dart';
List<Middleware<AppState>> createStoreProjectsMiddleware([
ProjectRepository repository = const ProjectRepository(),
]) {
final viewProjectList = _viewProjectList();
final viewProject = _viewProject();
final editProject = _editProject();
final loadProjects = _loadProjects(repository);
final loadProject = _loadProject(repository);
final saveProject = _saveProject(repository);
final archiveProject = _archiveProject(repository);
final deleteProject = _deleteProject(repository);
final restoreProject = _restoreProject(repository);
return [
TypedMiddleware<AppState, ViewProjectList>(viewProjectList),
TypedMiddleware<AppState, ViewProject>(viewProject),
TypedMiddleware<AppState, EditProject>(editProject),
TypedMiddleware<AppState, LoadProjects>(loadProjects),
TypedMiddleware<AppState, LoadProject>(loadProject),
TypedMiddleware<AppState, SaveProjectRequest>(saveProject),
TypedMiddleware<AppState, ArchiveProjectRequest>(archiveProject),
TypedMiddleware<AppState, DeleteProjectRequest>(deleteProject),
TypedMiddleware<AppState, RestoreProjectRequest>(restoreProject),
];
}
Middleware<AppState> _editProject() {
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
next(action);
final project =
await Navigator.of(action.context).pushNamed(ProjectEditScreen.route);
if (action.completer != null && project != null) {
action.completer.complete(project);
}
};
}
Middleware<AppState> _viewProject() {
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
next(action);
store.dispatch(UpdateCurrentRoute(ProjectViewScreen.route));
Navigator.of(action.context).pushNamed(ProjectViewScreen.route);
};
}
Middleware<AppState> _viewProjectList() {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
next(action);
store.dispatch(UpdateCurrentRoute(ProjectScreen.route));
Navigator.of(action.context).pushNamedAndRemoveUntil(ProjectScreen.route, (Route<dynamic> route) => false);
};
}
Middleware<AppState> _archiveProject(ProjectRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final origProject = store.state.projectState.map[action.projectId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origProject, EntityAction.archive)
.then((ProjectEntity project) {
store.dispatch(ArchiveProjectSuccess(project));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(ArchiveProjectFailure(origProject));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}
Middleware<AppState> _deleteProject(ProjectRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final origProject = store.state.projectState.map[action.projectId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origProject, EntityAction.delete)
.then((ProjectEntity project) {
store.dispatch(DeleteProjectSuccess(project));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(DeleteProjectFailure(origProject));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}
Middleware<AppState> _restoreProject(ProjectRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final origProject = store.state.projectState.map[action.projectId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origProject, EntityAction.restore)
.then((ProjectEntity project) {
store.dispatch(RestoreProjectSuccess(project));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(RestoreProjectFailure(origProject));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}
Middleware<AppState> _saveProject(ProjectRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
repository
.saveData(
store.state.selectedCompany, store.state.authState, action.project)
.then((ProjectEntity project) {
if (action.project.isNew) {
store.dispatch(AddProjectSuccess(project));
} else {
store.dispatch(SaveProjectSuccess(project));
}
action.completer.complete(project);
}).catchError((Object error) {
print(error);
store.dispatch(SaveProjectFailure(error));
action.completer.completeError(error);
});
next(action);
};
}
Middleware<AppState> _loadProject(ProjectRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final AppState state = store.state;
if (state.isLoading) {
next(action);
return;
}
store.dispatch(LoadProjectRequest());
repository
.loadItem(state.selectedCompany, state.authState, action.projectId)
.then((project) {
store.dispatch(LoadProjectSuccess(project));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(LoadProjectFailure(error));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}
Middleware<AppState> _loadProjects(ProjectRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final AppState state = store.state;
if (!state.projectState.isStale && !action.force) {
next(action);
return;
}
if (state.isLoading) {
next(action);
return;
}
final int updatedAt = (state.projectState.lastUpdated / 1000).round();
store.dispatch(LoadProjectsRequest());
repository
.loadList(state.selectedCompany, state.authState, updatedAt)
.then((data) {
store.dispatch(LoadProjectsSuccess(data));
if (action.completer != null) {
action.completer.complete(null);
}
if (state.productState.isStale) {
store.dispatch(LoadProducts());
}
}).catchError((Object error) {
print(error);
store.dispatch(LoadProjectsFailure(error));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}

View File

@ -0,0 +1,197 @@
import 'package:redux/redux.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/ui/list_ui_state.dart';
import 'package:invoiceninja_flutter/redux/project/project_actions.dart';
import 'package:invoiceninja_flutter/redux/project/project_state.dart';
EntityUIState projectUIReducer(ProjectUIState state, dynamic action) {
return state.rebuild((b) => b
..listUIState.replace(projectListReducer(state.listUIState, action))
..editing.replace(editingReducer(state.editing, action))
..selectedId = selectedIdReducer(state.selectedId, action));
}
Reducer<int> selectedIdReducer = combineReducers([
TypedReducer<int, ViewProject>(
(int selectedId, dynamic action) => action.projectId),
TypedReducer<int, AddProjectSuccess>(
(int selectedId, dynamic action) => action.project.id),
]);
final editingReducer = combineReducers<ProjectEntity>([
TypedReducer<ProjectEntity, SaveProjectSuccess>(_updateEditing),
TypedReducer<ProjectEntity, AddProjectSuccess>(_updateEditing),
TypedReducer<ProjectEntity, RestoreProjectSuccess>(_updateEditing),
TypedReducer<ProjectEntity, ArchiveProjectSuccess>(_updateEditing),
TypedReducer<ProjectEntity, DeleteProjectSuccess>(_updateEditing),
TypedReducer<ProjectEntity, EditProject>(_updateEditing),
TypedReducer<ProjectEntity, UpdateProject>(_updateEditing),
TypedReducer<ProjectEntity, SelectCompany>(_clearEditing),
]);
ProjectEntity _clearEditing(ProjectEntity project, dynamic action) {
return ProjectEntity();
}
ProjectEntity _updateEditing(ProjectEntity project, dynamic action) {
return action.project;
}
final projectListReducer = combineReducers<ListUIState>([
TypedReducer<ListUIState, SortProjects>(_sortProjects),
TypedReducer<ListUIState, FilterProjectsByState>(_filterProjectsByState),
TypedReducer<ListUIState, FilterProjects>(_filterProjects),
TypedReducer<ListUIState, FilterProjectsByCustom1>(_filterProjectsByCustom1),
TypedReducer<ListUIState, FilterProjectsByCustom2>(_filterProjectsByCustom2),
TypedReducer<ListUIState, FilterProjectsByEntity>(_filterProjectsByClient),
]);
ListUIState _filterProjectsByClient(
ListUIState projectListState, FilterProjectsByEntity action) {
return projectListState.rebuild((b) => b
..filterEntityId = action.entityId
..filterEntityType = action.entityType);
}
ListUIState _filterProjectsByCustom1(
ListUIState projectListState, FilterProjectsByCustom1 action) {
if (projectListState.custom1Filters.contains(action.value)) {
return projectListState
.rebuild((b) => b..custom1Filters.remove(action.value));
} else {
return projectListState.rebuild((b) => b..custom1Filters.add(action.value));
}
}
ListUIState _filterProjectsByCustom2(
ListUIState projectListState, FilterProjectsByCustom2 action) {
if (projectListState.custom2Filters.contains(action.value)) {
return projectListState
.rebuild((b) => b..custom2Filters.remove(action.value));
} else {
return projectListState.rebuild((b) => b..custom2Filters.add(action.value));
}
}
ListUIState _filterProjectsByState(
ListUIState projectListState, FilterProjectsByState action) {
if (projectListState.stateFilters.contains(action.state)) {
return projectListState.rebuild((b) => b..stateFilters.remove(action.state));
} else {
return projectListState.rebuild((b) => b..stateFilters.add(action.state));
}
}
ListUIState _filterProjects(ListUIState projectListState, FilterProjects action) {
return projectListState.rebuild((b) => b..filter = action.filter);
}
ListUIState _sortProjects(ListUIState projectListState, SortProjects action) {
return projectListState.rebuild((b) => b
..sortAscending = b.sortField != action.field || !b.sortAscending
..sortField = action.field);
}
final projectsReducer = combineReducers<ProjectState>([
TypedReducer<ProjectState, SaveProjectSuccess>(_updateProject),
TypedReducer<ProjectState, AddProjectSuccess>(_addProject),
TypedReducer<ProjectState, LoadProjectsSuccess>(_setLoadedProjects),
TypedReducer<ProjectState, LoadProjectSuccess>(_setLoadedProject),
TypedReducer<ProjectState, ArchiveProjectRequest>(_archiveProjectRequest),
TypedReducer<ProjectState, ArchiveProjectSuccess>(_archiveProjectSuccess),
TypedReducer<ProjectState, ArchiveProjectFailure>(_archiveProjectFailure),
TypedReducer<ProjectState, DeleteProjectRequest>(_deleteProjectRequest),
TypedReducer<ProjectState, DeleteProjectSuccess>(_deleteProjectSuccess),
TypedReducer<ProjectState, DeleteProjectFailure>(_deleteProjectFailure),
TypedReducer<ProjectState, RestoreProjectRequest>(_restoreProjectRequest),
TypedReducer<ProjectState, RestoreProjectSuccess>(_restoreProjectSuccess),
TypedReducer<ProjectState, RestoreProjectFailure>(_restoreProjectFailure),
]);
ProjectState _archiveProjectRequest(
ProjectState projectState, ArchiveProjectRequest action) {
final project = projectState.map[action.projectId]
.rebuild((b) => b..archivedAt = DateTime.now().millisecondsSinceEpoch);
return projectState.rebuild((b) => b..map[action.projectId] = project);
}
ProjectState _archiveProjectSuccess(
ProjectState projectState, ArchiveProjectSuccess action) {
return projectState.rebuild((b) => b..map[action.project.id] = action.project);
}
ProjectState _archiveProjectFailure(
ProjectState projectState, ArchiveProjectFailure action) {
return projectState.rebuild((b) => b..map[action.project.id] = action.project);
}
ProjectState _deleteProjectRequest(
ProjectState projectState, DeleteProjectRequest action) {
final project = projectState.map[action.projectId].rebuild((b) => b
..archivedAt = DateTime.now().millisecondsSinceEpoch
..isDeleted = true);
return projectState.rebuild((b) => b..map[action.projectId] = project);
}
ProjectState _deleteProjectSuccess(
ProjectState projectState, DeleteProjectSuccess action) {
return projectState.rebuild((b) => b..map[action.project.id] = action.project);
}
ProjectState _deleteProjectFailure(
ProjectState projectState, DeleteProjectFailure action) {
return projectState.rebuild((b) => b..map[action.project.id] = action.project);
}
ProjectState _restoreProjectRequest(
ProjectState projectState, RestoreProjectRequest action) {
final project = projectState.map[action.projectId].rebuild((b) => b
..archivedAt = null
..isDeleted = false);
return projectState.rebuild((b) => b..map[action.projectId] = project);
}
ProjectState _restoreProjectSuccess(
ProjectState projectState, RestoreProjectSuccess action) {
return projectState.rebuild((b) => b..map[action.project.id] = action.project);
}
ProjectState _restoreProjectFailure(
ProjectState projectState, RestoreProjectFailure action) {
return projectState.rebuild((b) => b..map[action.project.id] = action.project);
}
ProjectState _addProject(ProjectState projectState, AddProjectSuccess action) {
return projectState.rebuild((b) => b
..map[action.project.id] = action.project
..list.add(action.project.id));
}
ProjectState _updateProject(ProjectState projectState, SaveProjectSuccess action) {
return projectState.rebuild((b) => b
..map[action.project.id] = action.project);
}
ProjectState _setLoadedProject(
ProjectState projectState, LoadProjectSuccess action) {
return projectState.rebuild((b) => b
..map[action.project.id] = action.project);
}
ProjectState _setLoadedProjects(
ProjectState projectState, LoadProjectsSuccess action) {
final state = projectState.rebuild((b) => b
..lastUpdated = DateTime.now().millisecondsSinceEpoch
..map.addAll(Map.fromIterable(
action.projects,
key: (dynamic item) => item.id,
value: (dynamic item) => item,
)));
return state.rebuild((b) => b..list.replace(state.map.keys));
}

View File

@ -0,0 +1,60 @@
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 memoizedDropdownProjectList = memo2(
(BuiltMap<int, ProjectEntity> projectMap, BuiltList<int> projectList) =>
dropdownProjectsSelector(projectMap, projectList));
List<int> dropdownProjectsSelector(
BuiltMap<int, ProjectEntity> projectMap, BuiltList<int> projectList) {
final list =
projectList.where((projectId) => projectMap[projectId].isActive).toList();
list.sort((projectAId, projectBId) {
final projectA = projectMap[projectAId];
final projectB = projectMap[projectBId];
return projectA.compareTo(projectB, ProjectFields.name, true);
});
return list;
}
var memoizedFilteredProjectList = memo3((BuiltMap<int, ProjectEntity> projectMap,
BuiltList<int> projectList, ListUIState projectListState) =>
filteredProjectsSelector(projectMap, projectList, projectListState));
List<int> filteredProjectsSelector(BuiltMap<int, ProjectEntity> projectMap,
BuiltList<int> projectList, ListUIState projectListState) {
final list = projectList.where((projectId) {
final project = projectMap[projectId];
if (!project.matchesStates(projectListState.stateFilters)) {
return false;
}
if (projectListState.custom1Filters.isNotEmpty &&
!projectListState.custom1Filters.contains(project.customValue1)) {
return false;
}
if (projectListState.custom2Filters.isNotEmpty &&
!projectListState.custom2Filters.contains(project.customValue2)) {
return false;
}
/*
if (projectListState.filterEntityId != null &&
project.entityId != projectListState.filterEntityId) {
return false;
}
*/
return project.matchesFilter(projectListState.filter);
}).toList();
list.sort((projectAId, projectBId) {
final projectA = projectMap[projectAId];
final projectB = projectMap[projectBId];
return projectA.compareTo(
projectB, projectListState.sortField, projectListState.sortAscending);
});
return list;
}

View File

@ -0,0 +1,59 @@
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/project_model.dart';
import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.dart';
import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart';
part 'project_state.g.dart';
abstract class ProjectState implements Built<ProjectState, ProjectStateBuilder> {
factory ProjectState() {
return _$ProjectState._(
lastUpdated: 0,
map: BuiltMap<int, ProjectEntity>(),
list: BuiltList<int>(),
);
}
ProjectState._();
@nullable
int get lastUpdated;
BuiltMap<int, ProjectEntity> get map;
BuiltList<int> get list;
bool get isStale {
if (! isLoaded) {
return true;
}
return DateTime.now().millisecondsSinceEpoch - lastUpdated > kMillisecondsToRefreshData;
}
bool get isLoaded => lastUpdated != null && lastUpdated > 0;
static Serializer<ProjectState> get serializer => _$projectStateSerializer;
}
abstract class ProjectUIState extends Object with EntityUIState implements Built<ProjectUIState, ProjectUIStateBuilder> {
factory ProjectUIState() {
return _$ProjectUIState._(
listUIState: ListUIState(ProjectFields.name),
editing: ProjectEntity(),
selectedId: 0,
);
}
ProjectUIState._();
@nullable
ProjectEntity get editing;
@override
bool get isCreatingNew => editing.isNew;
static Serializer<ProjectUIState> get serializer => _$projectUIStateSerializer;
}

View File

@ -0,0 +1,392 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'project_state.dart';
// **************************************************************************
// BuiltValueGenerator
// **************************************************************************
// ignore_for_file: always_put_control_body_on_new_line
// ignore_for_file: annotate_overrides
// ignore_for_file: avoid_annotating_with_dynamic
// ignore_for_file: avoid_catches_without_on_clauses
// ignore_for_file: avoid_returning_this
// ignore_for_file: lines_longer_than_80_chars
// ignore_for_file: omit_local_variable_types
// ignore_for_file: prefer_expression_function_bodies
// ignore_for_file: sort_constructors_first
// ignore_for_file: unnecessary_const
// ignore_for_file: unnecessary_new
// ignore_for_file: test_types_in_equals
Serializer<ProjectState> _$projectStateSerializer =
new _$ProjectStateSerializer();
Serializer<ProjectUIState> _$projectUIStateSerializer =
new _$ProjectUIStateSerializer();
class _$ProjectStateSerializer implements StructuredSerializer<ProjectState> {
@override
final Iterable<Type> types = const [ProjectState, _$ProjectState];
@override
final String wireName = 'ProjectState';
@override
Iterable serialize(Serializers serializers, ProjectState object,
{FullType specifiedType = FullType.unspecified}) {
final result = <Object>[
'map',
serializers.serialize(object.map,
specifiedType: const FullType(BuiltMap,
const [const FullType(int), const FullType(ProjectEntity)])),
'list',
serializers.serialize(object.list,
specifiedType:
const FullType(BuiltList, const [const FullType(int)])),
];
if (object.lastUpdated != null) {
result
..add('lastUpdated')
..add(serializers.serialize(object.lastUpdated,
specifiedType: const FullType(int)));
}
return result;
}
@override
ProjectState deserialize(Serializers serializers, Iterable serialized,
{FullType specifiedType = FullType.unspecified}) {
final result = new ProjectStateBuilder();
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(int),
const FullType(ProjectEntity)
])) as BuiltMap);
break;
case 'list':
result.list.replace(serializers.deserialize(value,
specifiedType:
const FullType(BuiltList, const [const FullType(int)]))
as BuiltList);
break;
}
}
return result.build();
}
}
class _$ProjectUIStateSerializer
implements StructuredSerializer<ProjectUIState> {
@override
final Iterable<Type> types = const [ProjectUIState, _$ProjectUIState];
@override
final String wireName = 'ProjectUIState';
@override
Iterable serialize(Serializers serializers, ProjectUIState object,
{FullType specifiedType = FullType.unspecified}) {
final result = <Object>[
'selectedId',
serializers.serialize(object.selectedId,
specifiedType: const FullType(int)),
'listUIState',
serializers.serialize(object.listUIState,
specifiedType: const FullType(ListUIState)),
];
if (object.editing != null) {
result
..add('editing')
..add(serializers.serialize(object.editing,
specifiedType: const FullType(ProjectEntity)));
}
return result;
}
@override
ProjectUIState deserialize(Serializers serializers, Iterable serialized,
{FullType specifiedType = FullType.unspecified}) {
final result = new ProjectUIStateBuilder();
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(ProjectEntity)) as ProjectEntity);
break;
case 'selectedId':
result.selectedId = serializers.deserialize(value,
specifiedType: const FullType(int)) as int;
break;
case 'listUIState':
result.listUIState.replace(serializers.deserialize(value,
specifiedType: const FullType(ListUIState)) as ListUIState);
break;
}
}
return result.build();
}
}
class _$ProjectState extends ProjectState {
@override
final int lastUpdated;
@override
final BuiltMap<int, ProjectEntity> map;
@override
final BuiltList<int> list;
factory _$ProjectState([void updates(ProjectStateBuilder b)]) =>
(new ProjectStateBuilder()..update(updates)).build();
_$ProjectState._({this.lastUpdated, this.map, this.list}) : super._() {
if (map == null) {
throw new BuiltValueNullFieldError('ProjectState', 'map');
}
if (list == null) {
throw new BuiltValueNullFieldError('ProjectState', 'list');
}
}
@override
ProjectState rebuild(void updates(ProjectStateBuilder b)) =>
(toBuilder()..update(updates)).build();
@override
ProjectStateBuilder toBuilder() => new ProjectStateBuilder()..replace(this);
@override
bool operator ==(Object other) {
if (identical(other, this)) return true;
return other is ProjectState &&
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('ProjectState')
..add('lastUpdated', lastUpdated)
..add('map', map)
..add('list', list))
.toString();
}
}
class ProjectStateBuilder
implements Builder<ProjectState, ProjectStateBuilder> {
_$ProjectState _$v;
int _lastUpdated;
int get lastUpdated => _$this._lastUpdated;
set lastUpdated(int lastUpdated) => _$this._lastUpdated = lastUpdated;
MapBuilder<int, ProjectEntity> _map;
MapBuilder<int, ProjectEntity> get map =>
_$this._map ??= new MapBuilder<int, ProjectEntity>();
set map(MapBuilder<int, ProjectEntity> map) => _$this._map = map;
ListBuilder<int> _list;
ListBuilder<int> get list => _$this._list ??= new ListBuilder<int>();
set list(ListBuilder<int> list) => _$this._list = list;
ProjectStateBuilder();
ProjectStateBuilder get _$this {
if (_$v != null) {
_lastUpdated = _$v.lastUpdated;
_map = _$v.map?.toBuilder();
_list = _$v.list?.toBuilder();
_$v = null;
}
return this;
}
@override
void replace(ProjectState other) {
if (other == null) {
throw new ArgumentError.notNull('other');
}
_$v = other as _$ProjectState;
}
@override
void update(void updates(ProjectStateBuilder b)) {
if (updates != null) updates(this);
}
@override
_$ProjectState build() {
_$ProjectState _$result;
try {
_$result = _$v ??
new _$ProjectState._(
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(
'ProjectState', _$failedField, e.toString());
}
rethrow;
}
replace(_$result);
return _$result;
}
}
class _$ProjectUIState extends ProjectUIState {
@override
final ProjectEntity editing;
@override
final int selectedId;
@override
final ListUIState listUIState;
factory _$ProjectUIState([void updates(ProjectUIStateBuilder b)]) =>
(new ProjectUIStateBuilder()..update(updates)).build();
_$ProjectUIState._({this.editing, this.selectedId, this.listUIState})
: super._() {
if (selectedId == null) {
throw new BuiltValueNullFieldError('ProjectUIState', 'selectedId');
}
if (listUIState == null) {
throw new BuiltValueNullFieldError('ProjectUIState', 'listUIState');
}
}
@override
ProjectUIState rebuild(void updates(ProjectUIStateBuilder b)) =>
(toBuilder()..update(updates)).build();
@override
ProjectUIStateBuilder toBuilder() =>
new ProjectUIStateBuilder()..replace(this);
@override
bool operator ==(Object other) {
if (identical(other, this)) return true;
return other is ProjectUIState &&
editing == other.editing &&
selectedId == other.selectedId &&
listUIState == other.listUIState;
}
@override
int get hashCode {
return $jf($jc($jc($jc(0, editing.hashCode), selectedId.hashCode),
listUIState.hashCode));
}
@override
String toString() {
return (newBuiltValueToStringHelper('ProjectUIState')
..add('editing', editing)
..add('selectedId', selectedId)
..add('listUIState', listUIState))
.toString();
}
}
class ProjectUIStateBuilder
implements Builder<ProjectUIState, ProjectUIStateBuilder> {
_$ProjectUIState _$v;
ProjectEntityBuilder _editing;
ProjectEntityBuilder get editing =>
_$this._editing ??= new ProjectEntityBuilder();
set editing(ProjectEntityBuilder editing) => _$this._editing = editing;
int _selectedId;
int get selectedId => _$this._selectedId;
set selectedId(int selectedId) => _$this._selectedId = selectedId;
ListUIStateBuilder _listUIState;
ListUIStateBuilder get listUIState =>
_$this._listUIState ??= new ListUIStateBuilder();
set listUIState(ListUIStateBuilder listUIState) =>
_$this._listUIState = listUIState;
ProjectUIStateBuilder();
ProjectUIStateBuilder get _$this {
if (_$v != null) {
_editing = _$v.editing?.toBuilder();
_selectedId = _$v.selectedId;
_listUIState = _$v.listUIState?.toBuilder();
_$v = null;
}
return this;
}
@override
void replace(ProjectUIState other) {
if (other == null) {
throw new ArgumentError.notNull('other');
}
_$v = other as _$ProjectUIState;
}
@override
void update(void updates(ProjectUIStateBuilder b)) {
if (updates != null) updates(this);
}
@override
_$ProjectUIState build() {
_$ProjectUIState _$result;
try {
_$result = _$v ??
new _$ProjectUIState._(
editing: _editing?.build(),
selectedId: selectedId,
listUIState: listUIState.build());
} catch (_) {
String _$failedField;
try {
_$failedField = 'editing';
_editing?.build();
_$failedField = 'listUIState';
listUIState.build();
} catch (e) {
throw new BuiltValueNestedFieldError(
'ProjectUIState', _$failedField, e.toString());
}
rethrow;
}
replace(_$result);
return _$result;
}
}

View File

@ -9,6 +9,8 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_reducer.dart';
import 'package:redux/redux.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/project/project_reducer.dart';
import 'package:invoiceninja_flutter/redux/payment/payment_reducer.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_reducer.dart';
@ -28,6 +30,8 @@ UIState uiReducer(UIState state, dynamic action) {
..invoiceUIState.replace(invoiceUIReducer(state.invoiceUIState, action))
..dashboardUIState.replace(dashboardUIReducer(state.dashboardUIState, action))
// STARTER: reducer - do not remove comment
..projectUIState.replace(projectUIReducer(state.projectUIState, action))
..paymentUIState.replace(paymentUIReducer(state.paymentUIState, action))
..quoteUIState.replace(quoteUIReducer(state.quoteUIState, action)));
}

View File

@ -8,6 +8,8 @@ import 'package:invoiceninja_flutter/redux/product/product_state.dart';
import 'package:invoiceninja_flutter/ui/auth/login_vm.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/project/project_state.dart';
import 'package:invoiceninja_flutter/redux/payment/payment_state.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
@ -28,6 +30,8 @@ abstract class UIState implements Built<UIState, UIStateBuilder> {
clientUIState: ClientUIState(),
invoiceUIState: InvoiceUIState(),
// STARTER: constructor - do not remove comment
projectUIState: ProjectUIState(),
paymentUIState: PaymentUIState(company),
quoteUIState: QuoteUIState(),
);
@ -57,6 +61,8 @@ abstract class UIState implements Built<UIState, UIStateBuilder> {
String get filter;
// STARTER: properties - do not remove comment
ProjectUIState get projectUIState;
PaymentUIState get paymentUIState;
QuoteUIState get quoteUIState;

View File

@ -58,6 +58,9 @@ class _$UIStateSerializer implements StructuredSerializer<UIState> {
'invoiceUIState',
serializers.serialize(object.invoiceUIState,
specifiedType: const FullType(InvoiceUIState)),
'projectUIState',
serializers.serialize(object.projectUIState,
specifiedType: const FullType(ProjectUIState)),
'paymentUIState',
serializers.serialize(object.paymentUIState,
specifiedType: const FullType(PaymentUIState)),
@ -127,6 +130,10 @@ class _$UIStateSerializer implements StructuredSerializer<UIState> {
result.filter = serializers.deserialize(value,
specifiedType: const FullType(String)) as String;
break;
case 'projectUIState':
result.projectUIState.replace(serializers.deserialize(value,
specifiedType: const FullType(ProjectUIState)) as ProjectUIState);
break;
case 'paymentUIState':
result.paymentUIState.replace(serializers.deserialize(value,
specifiedType: const FullType(PaymentUIState)) as PaymentUIState);
@ -164,6 +171,8 @@ class _$UIState extends UIState {
@override
final String filter;
@override
final ProjectUIState projectUIState;
@override
final PaymentUIState paymentUIState;
@override
final QuoteUIState quoteUIState;
@ -182,6 +191,7 @@ class _$UIState extends UIState {
this.clientUIState,
this.invoiceUIState,
this.filter,
this.projectUIState,
this.paymentUIState,
this.quoteUIState})
: super._() {
@ -212,6 +222,9 @@ class _$UIState extends UIState {
if (invoiceUIState == null) {
throw new BuiltValueNullFieldError('UIState', 'invoiceUIState');
}
if (projectUIState == null) {
throw new BuiltValueNullFieldError('UIState', 'projectUIState');
}
if (paymentUIState == null) {
throw new BuiltValueNullFieldError('UIState', 'paymentUIState');
}
@ -241,6 +254,7 @@ class _$UIState extends UIState {
clientUIState == other.clientUIState &&
invoiceUIState == other.invoiceUIState &&
filter == other.filter &&
projectUIState == other.projectUIState &&
paymentUIState == other.paymentUIState &&
quoteUIState == other.quoteUIState;
}
@ -257,6 +271,7 @@ class _$UIState extends UIState {
$jc(
$jc(
$jc(
$jc(
$jc(
$jc(
0,
@ -271,6 +286,7 @@ class _$UIState extends UIState {
clientUIState.hashCode),
invoiceUIState.hashCode),
filter.hashCode),
projectUIState.hashCode),
paymentUIState.hashCode),
quoteUIState.hashCode));
}
@ -288,6 +304,7 @@ class _$UIState extends UIState {
..add('clientUIState', clientUIState)
..add('invoiceUIState', invoiceUIState)
..add('filter', filter)
..add('projectUIState', projectUIState)
..add('paymentUIState', paymentUIState)
..add('quoteUIState', quoteUIState))
.toString();
@ -348,6 +365,12 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
String get filter => _$this._filter;
set filter(String filter) => _$this._filter = filter;
ProjectUIStateBuilder _projectUIState;
ProjectUIStateBuilder get projectUIState =>
_$this._projectUIState ??= new ProjectUIStateBuilder();
set projectUIState(ProjectUIStateBuilder projectUIState) =>
_$this._projectUIState = projectUIState;
PaymentUIStateBuilder _paymentUIState;
PaymentUIStateBuilder get paymentUIState =>
_$this._paymentUIState ??= new PaymentUIStateBuilder();
@ -374,6 +397,7 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
_clientUIState = _$v.clientUIState?.toBuilder();
_invoiceUIState = _$v.invoiceUIState?.toBuilder();
_filter = _$v.filter;
_projectUIState = _$v.projectUIState?.toBuilder();
_paymentUIState = _$v.paymentUIState?.toBuilder();
_quoteUIState = _$v.quoteUIState?.toBuilder();
_$v = null;
@ -410,6 +434,7 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
clientUIState: clientUIState.build(),
invoiceUIState: invoiceUIState.build(),
filter: filter,
projectUIState: projectUIState.build(),
paymentUIState: paymentUIState.build(),
quoteUIState: quoteUIState.build());
} catch (_) {
@ -424,6 +449,8 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
_$failedField = 'invoiceUIState';
invoiceUIState.build();
_$failedField = 'projectUIState';
projectUIState.build();
_$failedField = 'paymentUIState';
paymentUIState.build();
_$failedField = 'quoteUIState';

View File

@ -17,6 +17,8 @@ import 'package:redux/redux.dart';
import 'package:cached_network_image/cached_network_image.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/project/project_actions.dart';
import 'package:invoiceninja_flutter/redux/payment/payment_actions.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:url_launcher/url_launcher.dart';
@ -186,6 +188,12 @@ class AppDrawer extends StatelessWidget {
},
),
// STARTER: menu - do not remove comment
ListTile(
leading: Icon(Icons.widgets),
title: Text('Projects'),
onTap: () => store.dispatch(ViewProjectList(context)),
),
DrawerTile(
company: company,
entityType: EntityType.payment,

View File

@ -0,0 +1,112 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/project/edit/project_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/refresh_icon_button.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
class ProjectEdit extends StatefulWidget {
final ProjectEditVM viewModel;
const ProjectEdit({
Key key,
@required this.viewModel,
}) : super(key: key);
@override
_ProjectEditState createState() => _ProjectEditState();
}
class _ProjectEditState extends State<ProjectEdit> {
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// STARTER: controllers - do not remove comment
List<TextEditingController> _controllers = [];
@override
void didChangeDependencies() {
_controllers = [
// STARTER: array - do not remove comment
];
_controllers.forEach((controller) => controller.removeListener(_onChanged));
final project = widget.viewModel.project;
// 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() {
final project = widget.viewModel.project.rebuild((b) => b
// STARTER: set value - do not remove comment
);
if (project != widget.viewModel.project) {
widget.viewModel.onChanged(project);
}
}
@override
Widget build(BuildContext context) {
final viewModel = widget.viewModel;
final localization = AppLocalization.of(context);
final project = viewModel.project;
return WillPopScope(
onWillPop: () async {
viewModel.onBackPressed();
return true;
},
child: Scaffold(
appBar: AppBar(
title: Text(viewModel.project.isNew
? localization.newProject
: localization.editProject),
actions: <Widget>[
Builder(builder: (BuildContext context) {
RefreshIconButton(
icon: Icons.cloud_upload,
tooltip: localization.save,
isVisible: project.isDeleted,
isDirty: project.isNew || project != viewModel.origProject,
isSaving: viewModel.isSaving,
onPressed: () {
if (! _formKey.currentState.validate()) {
return;
}
viewModel.onSavePressed(context);
},
);
}),
],
),
body: Form(
key: _formKey,
child: ListView(
children: <Widget>[
FormCard(
children: <Widget>[
// STARTER: widgets - do not remove comment
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,84 @@
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/project/project_screen.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/project/project_actions.dart';
import 'package:invoiceninja_flutter/data/models/project_model.dart';
import 'package:invoiceninja_flutter/ui/project/edit/project_edit.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/icon_message.dart';
class ProjectEditScreen extends StatelessWidget {
static const String route = '/project/edit';
ProjectEditScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, ProjectEditVM>(
converter: (Store<AppState> store) {
return ProjectEditVM.fromStore(store);
},
builder: (context, viewModel) {
return ProjectEdit(
viewModel: viewModel,
);
},
);
}
}
class ProjectEditVM {
final ProjectEntity project;
final Function(ProjectEntity) onChanged;
final Function(BuildContext) onSavePressed;
final bool isSaving;
final ProjectEntity origProject;
final Function onBackPressed;
final bool isLoading;
ProjectEditVM({
@required this.project,
@required this.onChanged,
@required this.isSaving,
@required this.origProject,
@required this.onSavePressed,
@required this.onBackPressed,
@required this.isLoading,
});
factory ProjectEditVM.fromStore(Store<AppState> store) {
final project = store.state.projectUIState.editing;
final state = store.state;
return ProjectEditVM(
isLoading: state.isLoading,
isSaving: state.isSaving,
project: project,
origProject: state.projectState.map[project.id],
onChanged: (ProjectEntity project) {
store.dispatch(UpdateProject(project));
},
onBackPressed: () {
store.dispatch(UpdateCurrentRoute(ProjectScreen.route));
},
onSavePressed: (BuildContext context) {
final Completer<Null> completer = new Completer<Null>();
store.dispatch(SaveProjectRequest(completer: completer, project: project));
return completer.future.then((_) {
/*
Scaffold.of(context).showSnackBar(SnackBar(
content: IconMessage(
message: project.isNew
? 'Successfully Created Project'
: 'Successfully Updated Project',
),
duration: Duration(seconds: 3)));
*/
});
},
);
}
}

View File

@ -0,0 +1,119 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart';
import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart';
import 'package:invoiceninja_flutter/ui/project/project_list_item.dart';
import 'package:invoiceninja_flutter/ui/project/project_list_vm.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
class ProjectList extends StatelessWidget {
final ProjectListVM viewModel;
const ProjectList({
Key key,
@required this.viewModel,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (!viewModel.isLoaded) {
return LoadingIndicator();
} else if (viewModel.projectList.isEmpty) {
return Opacity(
opacity: 0.5,
child: Center(
child: Text(
AppLocalization.of(context).noRecordsFound,
style: TextStyle(
fontSize: 18.0,
),
),
),
);
}
return _buildListView(context);
}
void _showMenu(BuildContext context, ProjectEntity project) async {
if (project == null) {
return;
}
final user = viewModel.user;
final message = await showDialog<String>(
context: context,
builder: (BuildContext context) => SimpleDialog(children: <Widget>[
user.canCreate(EntityType.project)
? ListTile(
leading: Icon(Icons.control_point_duplicate),
title: Text(AppLocalization.of(context).clone),
onTap: () => viewModel.onEntityAction(
context, project, EntityAction.clone),
)
: Container(),
Divider(),
user.canEditEntity(project) && !project.isActive
? ListTile(
leading: Icon(Icons.restore),
title: Text(AppLocalization.of(context).restore),
onTap: () => viewModel.onEntityAction(
context, project, EntityAction.restore),
)
: Container(),
user.canEditEntity(project) && project.isActive
? ListTile(
leading: Icon(Icons.archive),
title: Text(AppLocalization.of(context).archive),
onTap: () => viewModel.onEntityAction(
context, project, EntityAction.archive),
)
: Container(),
user.canEditEntity(project) && !project.isDeleted
? ListTile(
leading: Icon(Icons.delete),
title: Text(AppLocalization.of(context).delete),
onTap: () => viewModel.onEntityAction(
context, project, EntityAction.delete),
)
: Container(),
]));
if (message != null) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: message,
)));
}
}
Widget _buildListView(BuildContext context) {
return RefreshIndicator(
onRefresh: () => viewModel.onRefreshed(context),
child: ListView.builder(
itemCount: viewModel.projectList.length,
itemBuilder: (BuildContext context, index) {
final projectId = viewModel.projectList[index];
final project = viewModel.projectMap[projectId];
return Column(children: <Widget>[
ProjectListItem(
user: viewModel.user,
filter: viewModel.filter,
project: project,
onEntityAction: (EntityAction action) {
if (action == EntityAction.more) {
_showMenu(context, project);
} else {
viewModel.onEntityAction(context, project, action);
}
},
onTap: () => viewModel.onProjectTap(context, project),
onLongPress: () => _showMenu(context, project),
),
Divider(
height: 1.0,
),
]);
}),
);
}
}

View File

@ -0,0 +1,84 @@
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 ProjectListItem extends StatelessWidget {
final UserEntity user;
final Function(EntityAction) onEntityAction;
final GestureTapCallback onTap;
final GestureTapCallback onLongPress;
//final ValueChanged<bool> onCheckboxChanged;
final ProjectEntity project;
final String filter;
static final projectItemKey = (int id) => Key('__project_item_${id}__');
const ProjectListItem({
@required this.user,
@required this.onEntityAction,
@required this.onTap,
@required this.onLongPress,
//@required this.onCheckboxChanged,
@required this.project,
@required this.filter,
});
@override
Widget build(BuildContext context) {
final filterMatch = filter != null && filter.isNotEmpty
? project.matchesFilterValue(filter)
: null;
final subtitle = filterMatch ?? project.privateNotes;
return DismissibleEntity(
user: user,
entity: project,
onEntityAction: onEntityAction,
child: ListTile(
onTap: onTap,
onLongPress: onLongPress,
/*
leading: Checkbox(
//key: NinjaKeys.projectItemCheckbox(project.id),
value: true,
//onChanged: onCheckboxChanged,
onChanged: (value) {
return true;
},
),
*/
title: Container(
width: MediaQuery.of(context).size.width,
child: Row(
children: <Widget>[
Expanded(
child: Text(
project.name,
//key: NinjaKeys.clientItemClientKey(client.id),
style: Theme.of(context).textTheme.title,
),
),
Text(formatNumber(project.listDisplayAmount, context),
style: Theme.of(context).textTheme.title),
],
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
subtitle != null && subtitle.isNotEmpty ?
Text(
subtitle,
maxLines: 3,
overflow: TextOverflow.ellipsis,
) : Container(),
EntityStateLabel(project),
],
),
),
);
}
}

View File

@ -0,0 +1,135 @@
import 'dart:async';
import 'package:redux/redux.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/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/redux/project/project_selectors.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/project/project_list.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/project/project_actions.dart';
class ProjectListBuilder extends StatelessWidget {
const ProjectListBuilder({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, ProjectListVM>(
converter: ProjectListVM.fromStore,
builder: (context, viewModel) {
return ProjectList(
viewModel: viewModel,
);
},
);
}
}
class ProjectListVM {
final UserEntity user;
final List<int> projectList;
final BuiltMap<int, ProjectEntity> projectMap;
final String filter;
final bool isLoading;
final bool isLoaded;
final Function(BuildContext, ProjectEntity) onProjectTap;
final Function(BuildContext, ProjectEntity, DismissDirection) onDismissed;
final Function(BuildContext) onRefreshed;
final Function(BuildContext, ProjectEntity, EntityAction) onEntityAction;
ProjectListVM({
@required this.user,
@required this.projectList,
@required this.projectMap,
@required this.filter,
@required this.isLoading,
@required this.isLoaded,
@required this.onProjectTap,
@required this.onDismissed,
@required this.onRefreshed,
@required this.onEntityAction,
});
static ProjectListVM fromStore(Store<AppState> store) {
Future<Null> _handleRefresh(BuildContext context) {
if (store.state.isLoading) {
return Future<Null>(null);
}
final completer = snackBarCompleter(
context, AppLocalization.of(context).refreshComplete);
store.dispatch(LoadProjects(completer: completer, force: true));
return completer.future;
}
final state = store.state;
return ProjectListVM(
user: state.user,
projectList: memoizedFilteredProjectList(state.projectState.map,
state.projectState.list, state.projectListState),
projectMap: state.projectState.map,
isLoading: state.isLoading,
isLoaded: state.projectState.isLoaded,
filter: state.projectUIState.listUIState.filter,
onProjectTap: (context, project) {
store.dispatch(EditProject(project: project, context: context));
},
onEntityAction: (context, project, action) {
switch (action) {
case EntityAction.clone:
Navigator.of(context).pop();
store.dispatch(
EditProject(context: context, project: project.clone));
break;
case EntityAction.restore:
store.dispatch(RestoreProjectRequest(
popCompleter(
context, AppLocalization.of(context).restoredProject),
project.id));
break;
case EntityAction.archive:
store.dispatch(ArchiveProjectRequest(
popCompleter(
context, AppLocalization.of(context).archivedProject),
project.id));
break;
case EntityAction.delete:
store.dispatch(DeleteProjectRequest(
popCompleter(
context, AppLocalization.of(context).deletedProject),
project.id));
break;
}
},
onRefreshed: (context) => _handleRefresh(context),
onDismissed: (BuildContext context, ProjectEntity project,
DismissDirection direction) {
final localization = AppLocalization.of(context);
if (direction == DismissDirection.endToStart) {
if (project.isDeleted || project.isArchived) {
store.dispatch(RestoreProjectRequest(
snackBarCompleter(context, localization.restoredProject),
project.id));
} else {
store.dispatch(ArchiveProjectRequest(
snackBarCompleter(context, localization.archivedProject),
project.id));
}
} else if (direction == DismissDirection.startToEnd) {
if (project.isDeleted) {
store.dispatch(RestoreProjectRequest(
snackBarCompleter(context, localization.restoredProject),
project.id));
} else {
store.dispatch(DeleteProjectRequest(
snackBarCompleter(context, localization.deletedProject),
project.id));
}
}
});
}
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart';
import 'package:invoiceninja_flutter/ui/app/list_filter.dart';
import 'package:invoiceninja_flutter/ui/app/list_filter_button.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/project/project_list_vm.dart';
import 'package:invoiceninja_flutter/redux/project/project_actions.dart';
import 'package:invoiceninja_flutter/ui/app/app_drawer_vm.dart';
import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart';
import 'package:invoiceninja_flutter/utils/keys.dart';
class ProjectScreen extends StatelessWidget {
static const String route = '/project';
@override
Widget build(BuildContext context) {
final store = StoreProvider.of<AppState>(context);
final company = store.state.selectedCompany;
final user = company.user;
final localization = AppLocalization.of(context);
return WillPopScope(
onWillPop: () async {
store.dispatch(ViewDashboard(context));
return false;
},
child: Scaffold(
appBar: AppBar(
title: ListFilter(
entityType: EntityType.project,
onFilterChanged: (value) {
store.dispatch(FilterProjects(value));
},
),
actions: [
ListFilterButton(
entityType: EntityType.project,
onFilterPressed: (String value) {
store.dispatch(FilterProjects(value));
},
),
],
),
drawer: AppDrawerBuilder(),
body: ProjectListBuilder(),
bottomNavigationBar: AppBottomBar(
entityType: EntityType.project,
onSelectedSortField: (value) => store.dispatch(SortProjects(value)),
customValues1: company.getCustomFieldValues(CustomFieldType.project1,
excludeBlank: true),
customValues2: company.getCustomFieldValues(CustomFieldType.project2,
excludeBlank: true),
onSelectedCustom1: (value) =>
store.dispatch(FilterProjectsByCustom1(value)),
onSelectedCustom2: (value) =>
store.dispatch(FilterProjectsByCustom2(value)),
sortFields: [
ProjectFields.projectKey,
ProjectFields.cost,
ProjectFields.updatedAt,
],
onSelectedState: (EntityState state, value) {
store.dispatch(FilterProjectsByState(state));
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: user.canCreate(EntityType.project)
? FloatingActionButton(
key: Key(ProjectKeys.projectScreenFABKeyString),
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
store.dispatch(
EditProject(project: ProjectEntity(), context: context));
},
child: Icon(
Icons.add,
color: Colors.white,
),
tooltip: localization.newProject,
)
: null,
),
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/ui/app/actions_menu_button.dart';
import 'package:invoiceninja_flutter/ui/project/view/project_view_vm.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
class ProjectView extends StatefulWidget {
final ProjectViewVM viewModel;
const ProjectView({
Key key,
@required this.viewModel,
}) : super(key: key);
@override
_ProjectViewState createState() => new _ProjectViewState();
}
class _ProjectViewState extends State<ProjectView> {
@override
Widget build(BuildContext context) {
final viewModel = widget.viewModel;
final project = viewModel.project;
return Scaffold(
appBar: AppBar(
title: Text(project.displayName),
actions: project.isNew
? []
: [
IconButton(
icon: Icon(Icons.edit),
onPressed: () {
viewModel.onEditPressed(context);
},
),
ActionMenuButton(
user: viewModel.company.user,
isSaving: viewModel.isSaving,
entity: project,
onSelected: viewModel.onActionSelected,
),
],
),
body: FormCard(
children: [
// STARTER: widgets - do not remove comment
]
),
);
}
}

View File

@ -0,0 +1,82 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/project/project_actions.dart';
import 'package:invoiceninja_flutter/data/models/project_model.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/project/view/project_view.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/icon_message.dart';
class ProjectViewScreen extends StatelessWidget {
static const String route = '/project/view';
const ProjectViewScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, ProjectViewVM>(
converter: (Store<AppState> store) {
return ProjectViewVM.fromStore(store);
},
builder: (context, vm) {
return ProjectView(
viewModel: vm,
);
},
);
}
}
class ProjectViewVM {
final ProjectEntity project;
final CompanyEntity company;
final Function(BuildContext, EntityAction) onActionSelected;
final Function(BuildContext) onEditPressed;
final bool isSaving;
final bool isLoading;
final bool isDirty;
ProjectViewVM({
@required this.project,
@required this.company,
@required this.onActionSelected,
@required this.onEditPressed,
@required this.isSaving,
@required this.isLoading,
@required this.isDirty,
});
factory ProjectViewVM.fromStore(Store<AppState> store) {
final state = store.state;
final payment = state.projectState.map[state.projectUIState.selectedId];
return ProjectViewVM(
isLoading: store.state.isLoading,
project: project,
onEditPressed: (BuildContext context) {
store.dispatch(EditProject(project: project, context: context));
},
onActionSelected: (BuildContext context, EntityAction action) {
final localization = AppLocalization.of(context);
switch (action) {
case EntityAction.archive:
store.dispatch(ArchiveProjectRequest(
popCompleter(context, localization.archivedProject),
project.id));
break;
case EntityAction.delete:
store.dispatch(DeleteProjectRequest(
popCompleter(context, localization.deletedProject),
project.id));
break;
case EntityAction.restore:
store.dispatch(RestoreProjectRequest(
snackBarCompleter(context, localization.restoredProject),
project.id));
break;
}
});
}
}

View File

@ -20,6 +20,7 @@ class AppLocalization {
static final Map<String, Map<String, String>> _localizedValues = {
'en': {
'new_project': 'New Project',
'thank_you_for_using_our_app': 'Thank you for using our app!',
'if_you_like_it': 'If you like it please',
'click_here': 'click here',
@ -9850,6 +9851,8 @@ class AppLocalization {
},
};
String get newProject => _localizedValues[locale.languageCode]['new_project'];
String get thankYouForUsingOurApp =>
_localizedValues[locale.languageCode]['thank_you_for_using_our_app'];

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:core';
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/auth/auth_state.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';

View File

@ -46,11 +46,11 @@ final stubListReducer = combineReducers<ListUIState>([
TypedReducer<ListUIState, FilterStubs>(_filterStubs),
TypedReducer<ListUIState, FilterStubsByCustom1>(_filterStubsByCustom1),
TypedReducer<ListUIState, FilterStubsByCustom2>(_filterStubsByCustom2),
TypedReducer<ListUIState, FilterStubsByClient>(_filterStubsByClient),
TypedReducer<ListUIState, FilterStubsByEntity>(_filterStubsByClient),
]);
ListUIState _filterStubsByClient(
ListUIState stubListState, FilterStubsByClient action) {
ListUIState stubListState, FilterStubsByEntity action) {
return stubListState.rebuild((b) => b
..filterEntityId = action.entityId
..filterEntityType = action.entityType);

View File

@ -51,7 +51,7 @@ class _StubEditState extends State<StubEdit> {
super.dispose();
}
_onChanged() {
void _onChanged() {
final stub = widget.viewModel.stub.rebuild((b) => b
// STARTER: set value - do not remove comment
);
@ -64,6 +64,7 @@ class _StubEditState extends State<StubEdit> {
Widget build(BuildContext context) {
final viewModel = widget.viewModel;
final localization = AppLocalization.of(context);
final stub = viewModel.stub;
return WillPopScope(
onWillPop: () async {
@ -76,18 +77,19 @@ class _StubEditState extends State<StubEdit> {
? localization.newStub
: localization.editStub),
actions: <Widget>[
Builder(builder: (BuildContext context) {
return SaveIconButton(
isLoading: viewModel.isLoading,
RefreshIconButton(
icon: Icons.cloud_upload,
tooltip: localization.save,
isVisible: !stub.isDeleted,
isDirty: stub.isNew || stub != viewModel.origStub,
isSaving: viewModel.isSaving,
onPressed: () {
if (! _formKey.currentState.validate()) {
return;
}
viewModel.onSavePressed(context);
},
);
}),
)
],
),
body: Form(

View File

@ -40,6 +40,8 @@ class StubEditVM {
StubEditVM({
@required this.stub,
@required this.onChanged,
@required this.isSaving,
@required this.origStub,
@required this.onSavePressed,
@required this.onBackPressed,
@required this.isLoading,
@ -47,9 +49,12 @@ class StubEditVM {
factory StubEditVM.fromStore(Store<AppState> store) {
final stub = store.state.stubUIState.editing;
final state = store.state;
return StubEditVM(
isLoading: store.state.isLoading,
isLoading: state.isLoading,
isSaving: state.isSaving,
origStub: state.stubState.map[stub.id],
stub: stub,
onChanged: (StubEntity stub) {
store.dispatch(UpdateStub(stub));

View File

@ -99,8 +99,13 @@ class StubList extends StatelessWidget {
user: viewModel.user,
filter: viewModel.filter,
stub: stub,
onDismissed: (DismissDirection direction) =>
viewModel.onDismissed(context, stub, direction),
onEntityAction: (EntityAction action) {
if (action == EntityAction.more) {
_showMenu(context, stub);
} else {
viewModel.onEntityAction(context, stub, action);
}
},
onTap: () => viewModel.onStubTap(context, stub),
onLongPress: () => _showMenu(context, stub),
),

View File

@ -7,7 +7,7 @@ import 'package:invoiceninja_flutter/ui/app/dismissible_entity.dart';
class StubListItem extends StatelessWidget {
final UserEntity user;
final DismissDirectionCallback onDismissed;
final Function(EntityAction) onEntityAction;
final GestureTapCallback onTap;
final GestureTapCallback onLongPress;
//final ValueChanged<bool> onCheckboxChanged;
@ -18,7 +18,7 @@ class StubListItem extends StatelessWidget {
const StubListItem({
@required this.user,
@required this.onDismissed,
@required this.onEntityAction,
@required this.onTap,
@required this.onLongPress,
//@required this.onCheckboxChanged,
@ -56,12 +56,12 @@ class StubListItem extends StatelessWidget {
children: <Widget>[
Expanded(
child: Text(
stub.stubKey,
stub.name,
//key: NinjaKeys.clientItemClientKey(client.id),
style: Theme.of(context).textTheme.title,
),
),
Text(formatNumber(stub.cost, context),
Text(formatNumber(stub.listDisplayAmount, context),
style: Theme.of(context).textTheme.title),
],
),