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/material.dart';
import 'package:flutter_redux_starter/ui/stub/stub_item.dart';
import 'package:flutter_redux_starter/ui/stub/stub_list_vm.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/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 {
final StubListVM viewModel;
StubList({
const StubList({
Key key,
@required this.viewModel,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (! viewModel.isLoaded) {
return Center(child: CircularProgressIndicator());
if (!viewModel.isLoaded) {
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);
}
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) {
return RefreshIndicator(
onRefresh: () => viewModel.onRefreshed(context),
child: ListView.builder(
shrinkWrap: true,
itemCount: viewModel.stubList.length,
itemBuilder: (BuildContext context, index) {
var stubId = viewModel.stubList[index];
var stub = viewModel.stubMap[stubId];
final stubId = viewModel.stubList[index];
final stub = viewModel.stubMap[stubId];
return Column(children: <Widget>[
StubItem(
StubListItem(
user: viewModel.user,
filter: viewModel.filter,
stub: stub,
onDismissed: (DismissDirection direction) =>
viewModel.onDismissed(context, stub, direction),
onTap: () => viewModel.onStubTap(context, stub),
onLongPress: () => _showMenu(context, stub),
),
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 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 'package:redux/redux.dart';
import 'package:built_collection/built_collection.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:flutter_redux_starter/redux/stub/stub_selectors.dart';
import 'package:flutter_redux_starter/ui/app/icon_message.dart';
import 'package:flutter_redux_starter/data/models/stub_model.dart';
import 'package:flutter_redux_starter/ui/stub/stub_list.dart';
import 'package:flutter_redux_starter/redux/app/app_state.dart';
import 'package:flutter_redux_starter/redux/stub/stub_actions.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/stub/stub_selectors.dart';
import 'package:invoiceninja_flutter/data/models/models.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 {
static final String route = '/stubs/edit';
StubListBuilder({Key key}) : super(key: key);
const StubListBuilder({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, StubListVM>(
converter: StubListVM.fromStore,
builder: (context, vm) {
builder: (context, viewModel) {
return StubList(
viewModel: vm,
viewModel: viewModel,
);
},
);
@ -30,59 +30,106 @@ class StubListBuilder extends StatelessWidget {
}
class StubListVM {
final UserEntity user;
final List<int> stubList;
final BuiltMap<int, StubEntity> stubMap;
final String filter;
final bool isLoading;
final bool isLoaded;
final Function(BuildContext, StubEntity) onStubTap;
final Function(BuildContext, StubEntity, DismissDirection) onDismissed;
final Function(BuildContext) onRefreshed;
final Function(BuildContext, StubEntity, EntityAction) onEntityAction;
StubListVM({
@required this.user,
@required this.stubList,
@required this.stubMap,
@required this.filter,
@required this.isLoading,
@required this.isLoaded,
@required this.onStubTap,
@required this.onDismissed,
@required this.onRefreshed,
@required this.onEntityAction,
});
static StubListVM fromStore(Store<AppState> store) {
Future<Null> _handleRefresh(BuildContext context) {
final Completer<Null> completer = new Completer<Null>();
store.dispatch(LoadStubs(completer, true));
return completer.future.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: IconMessage(
message: 'Refresh complete',
),
duration: Duration(seconds: 3)));
});
if (store.state.isLoading) {
return Future<Null>(null);
}
final completer = snackBarCompleter(
context, AppLocalization.of(context).refreshComplete);
store.dispatch(LoadStubs(completer: completer, force: true));
return completer.future;
}
final state = store.state;
return StubListVM(
stubList: memoizedStubList(store.state.stubState.map,
store.state.stubState.list, store.state.stubListState),
stubMap: store.state.stubState.map,
isLoading: store.state.isLoading,
isLoaded: store.state.stubState.isLoaded,
user: state.user,
stubList: memoizedFilteredStubList(state.stubState.map,
state.stubState.list, state.stubListState),
stubMap: state.stubState.map,
isLoading: state.isLoading,
isLoaded: state.stubState.isLoaded,
filter: state.stubUIState.listUIState.filter,
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),
onDismissed: (BuildContext context, StubEntity stub,
DismissDirection direction) {
final Completer<Null> completer = new Completer<Null>();
store.dispatch(DeleteStubRequest(completer, stub.id));
var message = 'Successfully Deleted Stub';
return completer.future.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: IconMessage(
message: message,
),
duration: Duration(seconds: 3)));
});
final localization = AppLocalization.of(context);
if (direction == DismissDirection.endToStart) {
if (stub.isDeleted || stub.isArchived) {
store.dispatch(RestoreStubRequest(
snackBarCompleter(context, localization.restoredStub),
stub.id));
} else {
store.dispatch(ArchiveStubRequest(
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,58 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_starter/ui/app/app_search.dart';
import 'package:flutter_redux_starter/ui/app/app_search_button.dart';
import 'package:flutter_redux_starter/redux/app/app_state.dart';
import 'package:flutter_redux_starter/data/models/models.dart';
import 'package:flutter_redux_starter/data/models/stub_model.dart';
import 'package:flutter_redux_starter/ui/stub/stub_list_vm.dart';
import 'package:flutter_redux_starter/redux/stub/stub_actions.dart';
import 'package:flutter_redux_starter/ui/app/app_drawer_vm.dart';
import 'package:flutter_redux_starter/ui/app/app_bottom_bar.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/stub/stub_list_vm.dart';
import 'package:invoiceninja_flutter/redux/stub/stub_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 StubScreen extends StatelessWidget {
static final String route = '/stub';
static const String route = '/stub';
@override
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(
appBar: AppBar(
title: AppSearch(
entityType: EntityType.stub,
onSearchChanged: (value) {
store.dispatch(SearchStubs(value));
},
),
actions: [
AppSearchButton(
return WillPopScope(
onWillPop: () async => false,
child: Scaffold(
appBar: AppBar(
title: ListFilter(
entityType: EntityType.stub,
onSearchPressed: (value) {
store.dispatch(SearchStubs(value));
onFilterChanged: (value) {
store.dispatch(FilterStubs(value));
},
),
],
),
drawer: AppDrawerBuilder(),
body: StubListBuilder(),
bottomNavigationBar: AppBottomBar(
entityType: EntityType.stub,
onSelectedSortField: (value) {
store.dispatch(SortStubs(value));
},
sortFields: [
// STARTER: sort - do not remove comment
],
),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
store.dispatch(EditStub(stub: StubEntity(), context: context));
},
child: Icon(Icons.add,color: Colors.white,),
tooltip: 'New Stub',
actions: [
ListFilterButton(
entityType: EntityType.stub,
onFilterPressed: (String value) {
store.dispatch(FilterStubs(value));
},
),
],
),
drawer: AppDrawerBuilder(),
body: StubListBuilder(),
bottomNavigationBar: AppBottomBar(
entityType: EntityType.stub,
onSelectedSortField: (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: [
StubFields.stubKey,
StubFields.cost,
StubFields.updatedAt,
],
onSelectedState: (EntityState state, value) {
store.dispatch(FilterStubsByState(state));
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: user.canCreate(EntityType.stub)
? FloatingActionButton(
key: Key(StubKeys.stubScreenFABKeyString),
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
store.dispatch(
EditStub(stub: StubEntity(), context: context));
},
child: Icon(
Icons.add,
color: Colors.white,
),
tooltip: localization.newStub,
)
: null,
),
);
}