This commit is contained in:
Hillel Coren 2018-08-20 23:28:06 -07:00
parent f0c323b3c3
commit 63592a2994
5 changed files with 310 additions and 155 deletions

View File

@ -1,66 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux_starter/data/models/stub_model.dart';
//import 'package:flutter_redux_starter/ui/app/dismissible_entity.dart';
class StubItem extends StatelessWidget {
final DismissDirectionCallback onDismissed;
final GestureTapCallback onTap;
final StubEntity stub;
static final stubItemKey = (int id) => Key('__stub_item_${id}__');
StubItem({
@required this.onDismissed,
@required this.onTap,
@required this.stub,
});
@override
Widget build(BuildContext context) {
/*
return DismissibleEntity(
entity: stub,
onDismissed: onDismissed,
onTap: onTap,
child: ListTile(
onTap: onTap,
title: Container(
width: MediaQuery.of(context).size.width,
child: Row(
children: <Widget>[
Expanded(
child: Text(
stub.displayName,
style: Theme.of(context).textTheme.title,
),
),
],
),
),
// STARTER: subtitle - do not remove comment
),
);
}
*/
return ListTile(
onTap: onTap,
title: Container(
width: MediaQuery.of(context).size.width,
child: Row(
children: <Widget>[
Expanded(
child: Text(
stub.displayName,
style: Theme.of(context).textTheme.title,
),
),
],
),
),
// STARTER: subtitle - do not remove comment
);
}
}

View File

@ -1,40 +1,105 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux_starter/ui/stub/stub_item.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:flutter_redux_starter/ui/stub/stub_list_vm.dart'; import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart';
import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart';
import 'package:invoiceninja_flutter/ui/stub/stub_list_item.dart';
import 'package:invoiceninja_flutter/ui/stub/stub_list_vm.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
class StubList extends StatelessWidget { class StubList extends StatelessWidget {
final StubListVM viewModel; final StubListVM viewModel;
StubList({ const StubList({
Key key, Key key,
@required this.viewModel, @required this.viewModel,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (! viewModel.isLoaded) { if (!viewModel.isLoaded) {
return Center(child: CircularProgressIndicator()); return LoadingIndicator();
} else if (viewModel.stubList.isEmpty) {
return Opacity(
opacity: 0.5,
child: Center(
child: Text(
AppLocalization.of(context).noRecordsFound,
style: TextStyle(
fontSize: 18.0,
),
),
),
);
} }
return _buildListView(context); return _buildListView(context);
} }
void _showMenu(BuildContext context, StubEntity stub) async {
final user = viewModel.user;
final message = await showDialog<String>(
context: context,
builder: (BuildContext context) => SimpleDialog(children: <Widget>[
user.canCreate(EntityType.stub)
? ListTile(
leading: Icon(Icons.control_point_duplicate),
title: Text(AppLocalization.of(context).clone),
onTap: () => viewModel.onEntityAction(
context, stub, EntityAction.clone),
)
: Container(),
Divider(),
user.canEditEntity(stub) && !stub.isActive
? ListTile(
leading: Icon(Icons.restore),
title: Text(AppLocalization.of(context).restore),
onTap: () => viewModel.onEntityAction(
context, stub, EntityAction.restore),
)
: Container(),
user.canEditEntity(stub) && stub.isActive
? ListTile(
leading: Icon(Icons.archive),
title: Text(AppLocalization.of(context).archive),
onTap: () => viewModel.onEntityAction(
context, stub, EntityAction.archive),
)
: Container(),
user.canEditEntity(stub) && !stub.isDeleted
? ListTile(
leading: Icon(Icons.delete),
title: Text(AppLocalization.of(context).delete),
onTap: () => viewModel.onEntityAction(
context, stub, EntityAction.delete),
)
: Container(),
]));
if (message != null) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: message,
)));
}
}
Widget _buildListView(BuildContext context) { Widget _buildListView(BuildContext context) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => viewModel.onRefreshed(context), onRefresh: () => viewModel.onRefreshed(context),
child: ListView.builder( child: ListView.builder(
shrinkWrap: true,
itemCount: viewModel.stubList.length, itemCount: viewModel.stubList.length,
itemBuilder: (BuildContext context, index) { itemBuilder: (BuildContext context, index) {
var stubId = viewModel.stubList[index]; final stubId = viewModel.stubList[index];
var stub = viewModel.stubMap[stubId]; final stub = viewModel.stubMap[stubId];
return Column(children: <Widget>[ return Column(children: <Widget>[
StubItem( StubListItem(
user: viewModel.user,
filter: viewModel.filter,
stub: stub, stub: stub,
onDismissed: (DismissDirection direction) => onDismissed: (DismissDirection direction) =>
viewModel.onDismissed(context, stub, direction), viewModel.onDismissed(context, stub, direction),
onTap: () => viewModel.onStubTap(context, stub), onTap: () => viewModel.onStubTap(context, stub),
onLongPress: () => _showMenu(context, stub),
), ),
Divider( Divider(
height: 1.0, 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 StubListItem extends StatelessWidget {
final UserEntity user;
final DismissDirectionCallback onDismissed;
final GestureTapCallback onTap;
final GestureTapCallback onLongPress;
//final ValueChanged<bool> onCheckboxChanged;
final StubEntity stub;
final String filter;
static final stubItemKey = (int id) => Key('__stub_item_${id}__');
const StubListItem({
@required this.user,
@required this.onDismissed,
@required this.onTap,
@required this.onLongPress,
//@required this.onCheckboxChanged,
@required this.stub,
@required this.filter,
});
@override
Widget build(BuildContext context) {
final filterMatch = filter != null && filter.isNotEmpty
? stub.matchesFilterValue(filter)
: null;
final subtitle = filterMatch ?? stub.notes;
return DismissibleEntity(
user: user,
entity: stub,
onDismissed: onDismissed,
child: ListTile(
onTap: onTap,
onLongPress: onLongPress,
/*
leading: Checkbox(
//key: NinjaKeys.stubItemCheckbox(stub.id),
value: true,
//onChanged: onCheckboxChanged,
onChanged: (value) {
return true;
},
),
*/
title: Container(
width: MediaQuery.of(context).size.width,
child: Row(
children: <Widget>[
Expanded(
child: Text(
stub.stubKey,
//key: NinjaKeys.clientItemClientKey(client.id),
style: Theme.of(context).textTheme.title,
),
),
Text(formatNumber(stub.cost, 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(stub),
],
),
),
);
}
}

View File

@ -1,28 +1,28 @@
import 'dart:async'; import 'dart:async';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'package:built_collection/built_collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_starter/redux/stub/stub_selectors.dart'; import 'package:built_collection/built_collection.dart';
import 'package:flutter_redux_starter/ui/app/icon_message.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:flutter_redux_starter/data/models/stub_model.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:flutter_redux_starter/ui/stub/stub_list.dart'; import 'package:invoiceninja_flutter/redux/stub/stub_selectors.dart';
import 'package:flutter_redux_starter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; import 'package:invoiceninja_flutter/ui/stub/stub_list.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/stub/stub_actions.dart';
class StubListBuilder extends StatelessWidget { class StubListBuilder extends StatelessWidget {
static final String route = '/stubs/edit'; const StubListBuilder({Key key}) : super(key: key);
StubListBuilder({Key key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StoreConnector<AppState, StubListVM>( return StoreConnector<AppState, StubListVM>(
converter: StubListVM.fromStore, converter: StubListVM.fromStore,
builder: (context, vm) { builder: (context, viewModel) {
return StubList( return StubList(
viewModel: vm, viewModel: viewModel,
); );
}, },
); );
@ -30,59 +30,106 @@ class StubListBuilder extends StatelessWidget {
} }
class StubListVM { class StubListVM {
final UserEntity user;
final List<int> stubList; final List<int> stubList;
final BuiltMap<int, StubEntity> stubMap; final BuiltMap<int, StubEntity> stubMap;
final String filter;
final bool isLoading; final bool isLoading;
final bool isLoaded; final bool isLoaded;
final Function(BuildContext, StubEntity) onStubTap; final Function(BuildContext, StubEntity) onStubTap;
final Function(BuildContext, StubEntity, DismissDirection) onDismissed; final Function(BuildContext, StubEntity, DismissDirection) onDismissed;
final Function(BuildContext) onRefreshed; final Function(BuildContext) onRefreshed;
final Function(BuildContext, StubEntity, EntityAction) onEntityAction;
StubListVM({ StubListVM({
@required this.user,
@required this.stubList, @required this.stubList,
@required this.stubMap, @required this.stubMap,
@required this.filter,
@required this.isLoading, @required this.isLoading,
@required this.isLoaded, @required this.isLoaded,
@required this.onStubTap, @required this.onStubTap,
@required this.onDismissed, @required this.onDismissed,
@required this.onRefreshed, @required this.onRefreshed,
@required this.onEntityAction,
}); });
static StubListVM fromStore(Store<AppState> store) { static StubListVM fromStore(Store<AppState> store) {
Future<Null> _handleRefresh(BuildContext context) { Future<Null> _handleRefresh(BuildContext context) {
final Completer<Null> completer = new Completer<Null>(); if (store.state.isLoading) {
store.dispatch(LoadStubs(completer, true)); return Future<Null>(null);
return completer.future.then((_) { }
Scaffold.of(context).showSnackBar(SnackBar( final completer = snackBarCompleter(
content: IconMessage( context, AppLocalization.of(context).refreshComplete);
message: 'Refresh complete', store.dispatch(LoadStubs(completer: completer, force: true));
), return completer.future;
duration: Duration(seconds: 3)));
});
} }
final state = store.state;
return StubListVM( return StubListVM(
stubList: memoizedStubList(store.state.stubState.map, user: state.user,
store.state.stubState.list, store.state.stubListState), stubList: memoizedFilteredStubList(state.stubState.map,
stubMap: store.state.stubState.map, state.stubState.list, state.stubListState),
isLoading: store.state.isLoading, stubMap: state.stubState.map,
isLoaded: store.state.stubState.isLoaded, isLoading: state.isLoading,
isLoaded: state.stubState.isLoaded,
filter: state.stubUIState.listUIState.filter,
onStubTap: (context, stub) { onStubTap: (context, stub) {
store.dispatch(ViewStub(stub: stub, context: context)); store.dispatch(EditStub(stub: stub, context: context));
},
onEntityAction: (context, stub, action) {
switch (action) {
case EntityAction.clone:
Navigator.of(context).pop();
store.dispatch(
EditStub(context: context, stub: stub.clone));
break;
case EntityAction.restore:
store.dispatch(RestoreStubRequest(
popCompleter(
context, AppLocalization.of(context).restoredStub),
stub.id));
break;
case EntityAction.archive:
store.dispatch(ArchiveStubRequest(
popCompleter(
context, AppLocalization.of(context).archivedStub),
stub.id));
break;
case EntityAction.delete:
store.dispatch(DeleteStubRequest(
popCompleter(
context, AppLocalization.of(context).deletedStub),
stub.id));
break;
}
}, },
onRefreshed: (context) => _handleRefresh(context), onRefreshed: (context) => _handleRefresh(context),
onDismissed: (BuildContext context, StubEntity stub, onDismissed: (BuildContext context, StubEntity stub,
DismissDirection direction) { DismissDirection direction) {
final Completer<Null> completer = new Completer<Null>(); final localization = AppLocalization.of(context);
store.dispatch(DeleteStubRequest(completer, stub.id)); if (direction == DismissDirection.endToStart) {
var message = 'Successfully Deleted Stub'; if (stub.isDeleted || stub.isArchived) {
return completer.future.then((_) { store.dispatch(RestoreStubRequest(
Scaffold.of(context).showSnackBar(SnackBar( snackBarCompleter(context, localization.restoredStub),
content: IconMessage( stub.id));
message: message, } else {
), store.dispatch(ArchiveStubRequest(
duration: Duration(seconds: 3))); snackBarCompleter(context, localization.archivedStub),
}); stub.id));
}
} else if (direction == DismissDirection.startToEnd) {
if (stub.isDeleted) {
store.dispatch(RestoreStubRequest(
snackBarCompleter(context, localization.restoredStub),
stub.id));
} else {
store.dispatch(DeleteStubRequest(
snackBarCompleter(context, localization.deletedStub),
stub.id));
}
}
}); });
} }
} }

View File

@ -1,35 +1,41 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_starter/ui/app/app_search.dart'; import 'package:invoiceninja_flutter/ui/app/list_filter.dart';
import 'package:flutter_redux_starter/ui/app/app_search_button.dart'; import 'package:invoiceninja_flutter/ui/app/list_filter_button.dart';
import 'package:flutter_redux_starter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:flutter_redux_starter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:flutter_redux_starter/data/models/stub_model.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:flutter_redux_starter/ui/stub/stub_list_vm.dart'; import 'package:invoiceninja_flutter/ui/stub/stub_list_vm.dart';
import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; import 'package:invoiceninja_flutter/redux/stub/stub_actions.dart';
import 'package:flutter_redux_starter/ui/app/app_drawer_vm.dart'; import 'package:invoiceninja_flutter/ui/app/app_drawer_vm.dart';
import 'package:flutter_redux_starter/ui/app/app_bottom_bar.dart'; import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart';
import 'package:invoiceninja_flutter/utils/keys.dart';
class StubScreen extends StatelessWidget { class StubScreen extends StatelessWidget {
static final String route = '/stub'; static const String route = '/stub';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var store = StoreProvider.of<AppState>(context); final store = StoreProvider.of<AppState>(context);
final company = store.state.selectedCompany;
final user = company.user;
final localization = AppLocalization.of(context);
return Scaffold( return WillPopScope(
onWillPop: () async => false,
child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: AppSearch( title: ListFilter(
entityType: EntityType.stub, entityType: EntityType.stub,
onSearchChanged: (value) { onFilterChanged: (value) {
store.dispatch(SearchStubs(value)); store.dispatch(FilterStubs(value));
}, },
), ),
actions: [ actions: [
AppSearchButton( ListFilterButton(
entityType: EntityType.stub, entityType: EntityType.stub,
onSearchPressed: (value) { onFilterPressed: (String value) {
store.dispatch(SearchStubs(value)); store.dispatch(FilterStubs(value));
}, },
), ),
], ],
@ -38,21 +44,40 @@ class StubScreen extends StatelessWidget {
body: StubListBuilder(), body: StubListBuilder(),
bottomNavigationBar: AppBottomBar( bottomNavigationBar: AppBottomBar(
entityType: EntityType.stub, entityType: EntityType.stub,
onSelectedSortField: (value) { onSelectedSortField: (value) => store.dispatch(SortStubs(value)),
store.dispatch(SortStubs(value)); customValues1: company.getCustomFieldValues(CustomFieldType.stub1,
}, excludeBlank: true),
customValues2: company.getCustomFieldValues(CustomFieldType.stub2,
excludeBlank: true),
onSelectedCustom1: (value) =>
store.dispatch(FilterStubsByCustom1(value)),
onSelectedCustom2: (value) =>
store.dispatch(FilterStubsByCustom2(value)),
sortFields: [ sortFields: [
// STARTER: sort - do not remove comment StubFields.stubKey,
StubFields.cost,
StubFields.updatedAt,
], ],
onSelectedState: (EntityState state, value) {
store.dispatch(FilterStubsByState(state));
},
), ),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: FloatingActionButton( floatingActionButton: user.canCreate(EntityType.stub)
? FloatingActionButton(
key: Key(StubKeys.stubScreenFABKeyString),
backgroundColor: Theme.of(context).primaryColorDark, backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () { onPressed: () {
store.dispatch(EditStub(stub: StubEntity(), context: context)); store.dispatch(
EditStub(stub: StubEntity(), context: context));
}, },
child: Icon(Icons.add,color: Colors.white,), child: Icon(
tooltip: 'New Stub', Icons.add,
color: Colors.white,
),
tooltip: localization.newStub,
)
: null,
), ),
); );
} }