Working on dashboard
This commit is contained in:
parent
15775f1421
commit
314171504e
|
|
@ -3,18 +3,31 @@ import 'package:json_annotation/json_annotation.dart';
|
||||||
part 'entities.g.dart';
|
part 'entities.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class BaseResponse extends Object with _$BaseResponseSerializerMixin {
|
class BaseListResponse extends Object with _$BaseListResponseSerializerMixin {
|
||||||
//final String message;
|
//final String message;
|
||||||
|
|
||||||
@JsonKey(name: "data")
|
@JsonKey(name: "data")
|
||||||
final List<dynamic> data;
|
final List<dynamic> data;
|
||||||
|
|
||||||
BaseResponse(
|
BaseListResponse(
|
||||||
//this.message,
|
|
||||||
this.data,
|
this.data,
|
||||||
);
|
);
|
||||||
|
|
||||||
factory BaseResponse.fromJson(Map<String, dynamic> json) => _$BaseResponseFromJson(json);
|
factory BaseListResponse.fromJson(Map<String, dynamic> json) => _$BaseListResponseFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class BaseItemResponse extends Object with _$BaseItemResponseSerializerMixin {
|
||||||
|
//final String message;
|
||||||
|
|
||||||
|
@JsonKey(name: "data")
|
||||||
|
final dynamic data;
|
||||||
|
|
||||||
|
BaseItemResponse(
|
||||||
|
this.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory BaseItemResponse.fromJson(Map<String, dynamic> json) => _$BaseItemResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,22 @@ part of 'entities.dart';
|
||||||
// Generator: JsonSerializableGenerator
|
// Generator: JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
BaseResponse _$BaseResponseFromJson(Map<String, dynamic> json) =>
|
BaseListResponse _$BaseListResponseFromJson(Map<String, dynamic> json) =>
|
||||||
new BaseResponse(json['data'] as List);
|
new BaseListResponse(json['data'] as List);
|
||||||
|
|
||||||
abstract class _$BaseResponseSerializerMixin {
|
abstract class _$BaseListResponseSerializerMixin {
|
||||||
List<dynamic> get data;
|
List<dynamic> get data;
|
||||||
Map<String, dynamic> toJson() => <String, dynamic>{'data': data};
|
Map<String, dynamic> toJson() => <String, dynamic>{'data': data};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BaseItemResponse _$BaseItemResponseFromJson(Map<String, dynamic> json) =>
|
||||||
|
new BaseItemResponse(json['data']);
|
||||||
|
|
||||||
|
abstract class _$BaseItemResponseSerializerMixin {
|
||||||
|
dynamic get data;
|
||||||
|
Map<String, dynamic> toJson() => <String, dynamic>{'data': data};
|
||||||
|
}
|
||||||
|
|
||||||
ProductEntity _$ProductEntityFromJson(Map<String, dynamic> json) =>
|
ProductEntity _$ProductEntityFromJson(Map<String, dynamic> json) =>
|
||||||
new ProductEntity(json['id'] as int)
|
new ProductEntity(json['id'] as int)
|
||||||
..productKey = json['product_key'] as String
|
..productKey = json['product_key'] as String
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:core';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import 'package:invoiceninja/redux/auth/auth_state.dart';
|
||||||
|
import 'package:invoiceninja/data/models/entities.dart';
|
||||||
|
import 'package:invoiceninja/data/repositories/repositories.dart';
|
||||||
|
import 'package:invoiceninja/data/file_storage.dart';
|
||||||
|
import 'package:invoiceninja/data/web_client.dart';
|
||||||
|
|
||||||
|
/// A class that glues together our local file storage and web client. It has a
|
||||||
|
/// clear responsibility: Load Dashboard and Persist products.
|
||||||
|
class DashboardRepositoryFlutter implements BaseRepository {
|
||||||
|
final FileStorage fileStorage;
|
||||||
|
final WebClient webClient;
|
||||||
|
|
||||||
|
const DashboardRepositoryFlutter({
|
||||||
|
@required this.fileStorage,
|
||||||
|
this.webClient = const WebClient(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Loads products first from File storage. If they don't exist or encounter an
|
||||||
|
/// error, it attempts to load the Dashboard from a Web Client.
|
||||||
|
@override
|
||||||
|
Future<dynamic> loadItem(AuthState auth) async {
|
||||||
|
print('ProductRepo: loadDashboard...');
|
||||||
|
|
||||||
|
final data = await webClient.fetchItem(
|
||||||
|
auth.url + '/dashboard', auth.token);
|
||||||
|
|
||||||
|
//fileStorage.saveDashboard(products);
|
||||||
|
print('== LOAD DASHBOARD ==');
|
||||||
|
print(data);
|
||||||
|
return data.map((data) => ProductEntity.fromJson(data)).toList();
|
||||||
|
|
||||||
|
/*
|
||||||
|
try {
|
||||||
|
return await fileStorage.loadData();
|
||||||
|
} catch (exception) {
|
||||||
|
final products = await webClient.fetchData(
|
||||||
|
auth.url + '/products', auth.token);
|
||||||
|
|
||||||
|
//fileStorage.saveDashboard(products);
|
||||||
|
|
||||||
|
return products;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<dynamic>> loadItems(dynamic product) {
|
||||||
|
return Future.wait<dynamic>([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persists products to local disk and the web
|
||||||
|
@override
|
||||||
|
Future saveData(List<dynamic> products) {
|
||||||
|
return Future.wait<dynamic>([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,10 +22,10 @@ class ProductsRepositoryFlutter implements BaseRepository {
|
||||||
/// Loads products first from File storage. If they don't exist or encounter an
|
/// Loads products first from File storage. If they don't exist or encounter an
|
||||||
/// error, it attempts to load the Products from a Web Client.
|
/// error, it attempts to load the Products from a Web Client.
|
||||||
@override
|
@override
|
||||||
Future<List<dynamic>> loadData(AuthState auth) async {
|
Future<List<dynamic>> loadItems(AuthState auth) async {
|
||||||
print('ProductRepo: loadProducts...');
|
print('ProductRepo: loadProducts...');
|
||||||
|
|
||||||
final products = await webClient.fetchData(
|
final products = await webClient.fetchList(
|
||||||
auth.url + '/products', auth.token);
|
auth.url + '/products', auth.token);
|
||||||
|
|
||||||
//fileStorage.saveProducts(products);
|
//fileStorage.saveProducts(products);
|
||||||
|
|
@ -46,7 +46,11 @@ class ProductsRepositoryFlutter implements BaseRepository {
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persists products to local disk and the web
|
@override
|
||||||
|
Future loadItem(dynamic product) {
|
||||||
|
return Future.wait<dynamic>([]);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future saveData(List<dynamic> products) {
|
Future saveData(List<dynamic> products) {
|
||||||
return Future.wait<dynamic>([
|
return Future.wait<dynamic>([
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@ import 'package:invoiceninja/data/models/entities.dart';
|
||||||
/// inject the correct implementation depending on the environment, such as
|
/// inject the correct implementation depending on the environment, such as
|
||||||
/// web or Flutter.
|
/// web or Flutter.
|
||||||
abstract class BaseRepository {
|
abstract class BaseRepository {
|
||||||
/// Loads products first from File storage. If they don't exist or encounter an
|
|
||||||
/// error, it attempts to load the Todos from a Web Client.
|
|
||||||
Future<List<dynamic>> loadData(AuthState auth);
|
|
||||||
|
|
||||||
// Persists products to local disk and the web
|
Future<List<dynamic>> loadItems(AuthState auth);
|
||||||
|
|
||||||
|
Future<dynamic> loadItem(AuthState auth);
|
||||||
|
|
||||||
Future saveData(List<dynamic> data);
|
Future saveData(List<dynamic> data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,23 @@ class WebClient {
|
||||||
|
|
||||||
const WebClient();
|
const WebClient();
|
||||||
|
|
||||||
/// Mock that "fetches" some Products from a "web service" after a short delay
|
Future<http.Response> sendRequest(String url, String token) {
|
||||||
Future<List<dynamic>> fetchData(String url, String token) async {
|
return http.Client().get(
|
||||||
|
|
||||||
final response = await http.Client().get(
|
|
||||||
url,
|
url,
|
||||||
headers: {'X-Ninja-Token': token},
|
headers: {'X-Ninja-Token': token},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return BaseResponse
|
Future<dynamic> fetchItem(String url, String token) async {
|
||||||
|
final http.Response response = await sendRequest(url, token);
|
||||||
|
return BaseItemResponse
|
||||||
|
.fromJson(json.decode(response.body))
|
||||||
|
.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<dynamic>> fetchList(String url, String token) async {
|
||||||
|
final http.Response response = await sendRequest(url, token);
|
||||||
|
return BaseListResponse
|
||||||
.fromJson(json.decode(response.body))
|
.fromJson(json.decode(response.body))
|
||||||
.data
|
.data
|
||||||
.toList();
|
.toList();
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
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:redux/redux.dart';
|
import 'package:redux/redux.dart';
|
||||||
import 'package:invoiceninja/redux/app/app_state.dart';
|
import 'package:redux_logging/redux_logging.dart';
|
||||||
|
import 'package:invoiceninja/routes.dart';
|
||||||
|
import 'package:invoiceninja/ui/auth/login.dart';
|
||||||
import 'package:invoiceninja/ui/dashboard/dashboard.dart';
|
import 'package:invoiceninja/ui/dashboard/dashboard.dart';
|
||||||
import 'package:invoiceninja/ui/client/clients.dart';
|
import 'package:invoiceninja/ui/client/clients.dart';
|
||||||
import 'package:invoiceninja/ui/product/products.dart';
|
import 'package:invoiceninja/ui/product/products.dart';
|
||||||
import 'package:invoiceninja/ui/auth/login.dart';
|
import 'package:invoiceninja/redux/app/app_reducer.dart';
|
||||||
import 'package:invoiceninja/routes.dart';
|
import 'package:invoiceninja/redux/app/app_state.dart';
|
||||||
|
import 'package:invoiceninja/redux/auth/auth_middleware.dart';
|
||||||
|
import 'package:invoiceninja/redux/dashboard/dashboard_actions.dart';
|
||||||
|
import 'package:invoiceninja/redux/dashboard/dashboard_middleware.dart';
|
||||||
import 'package:invoiceninja/redux/product/product_actions.dart';
|
import 'package:invoiceninja/redux/product/product_actions.dart';
|
||||||
import 'package:invoiceninja/redux/product/product_middleware.dart';
|
import 'package:invoiceninja/redux/product/product_middleware.dart';
|
||||||
import 'package:invoiceninja/redux/auth/auth_middleware.dart';
|
|
||||||
import 'package:invoiceninja/redux/app/app_reducer.dart';
|
|
||||||
import 'package:redux_logging/redux_logging.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
||||||
|
|
@ -29,6 +32,7 @@ class InvoiceNinjaApp extends StatelessWidget {
|
||||||
initialState: AppState.loading(),
|
initialState: AppState.loading(),
|
||||||
middleware: []
|
middleware: []
|
||||||
..addAll(createStoreAuthMiddleware())
|
..addAll(createStoreAuthMiddleware())
|
||||||
|
..addAll(createStoreDashboardMiddleware())
|
||||||
..addAll(createStoreProductsMiddleware())
|
..addAll(createStoreProductsMiddleware())
|
||||||
..addAll([
|
..addAll([
|
||||||
LoggingMiddleware.printer(),
|
LoggingMiddleware.printer(),
|
||||||
|
|
@ -59,6 +63,7 @@ class InvoiceNinjaApp extends StatelessWidget {
|
||||||
},
|
},
|
||||||
AppRoutes.dashboard: (context) {
|
AppRoutes.dashboard: (context) {
|
||||||
return StoreBuilder<AppState>(
|
return StoreBuilder<AppState>(
|
||||||
|
onInit: (store) => store.dispatch(LoadDashboardAction()),
|
||||||
builder: (context, store) {
|
builder: (context, store) {
|
||||||
return Dashboard();
|
return Dashboard();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:invoiceninja/data/models/models.dart';
|
||||||
|
|
||||||
|
class LoadDashboardAction {}
|
||||||
|
|
||||||
|
class DashboardNotLoadedAction {
|
||||||
|
final dynamic error;
|
||||||
|
|
||||||
|
DashboardNotLoadedAction(this.error);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DashboardNotLoadedAction{products: $error}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DashboardLoadedAction {
|
||||||
|
final List<ProductEntity> products;
|
||||||
|
|
||||||
|
DashboardLoadedAction(this.products);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DashboardLoadedAction{products: $products}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:redux/redux.dart';
|
||||||
|
import 'package:invoiceninja/redux/dashboard/dashboard_actions.dart';
|
||||||
|
import 'package:invoiceninja/redux/app/app_state.dart';
|
||||||
|
import 'package:invoiceninja/data/repositories/repositories.dart';
|
||||||
|
import 'package:invoiceninja/data/repositories/dashboard_repository.dart';
|
||||||
|
import 'package:invoiceninja/data/file_storage.dart';
|
||||||
|
import 'package:invoiceninja/redux/product/product_selectors.dart';
|
||||||
|
import 'package:invoiceninja/data/models/entities.dart';
|
||||||
|
|
||||||
|
List<Middleware<AppState>> createStoreDashboardMiddleware([
|
||||||
|
BaseRepository repository = const DashboardRepositoryFlutter(
|
||||||
|
fileStorage: const FileStorage(
|
||||||
|
'__invoiceninja__',
|
||||||
|
getApplicationDocumentsDirectory,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]) {
|
||||||
|
final loadDashboard = _createLoadDashboard(repository);
|
||||||
|
|
||||||
|
return [
|
||||||
|
TypedMiddleware<AppState, LoadDashboardAction>(loadDashboard),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Middleware<AppState> _createSaveDashboard(BaseRepository repository) {
|
||||||
|
return (Store<AppState> store, action, NextDispatcher next) {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
/*
|
||||||
|
repository.saveDashboard(
|
||||||
|
productsSelector(store.state).map((product) => product.toEntity()).toList(),
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Middleware<AppState> _createLoadDashboard(BaseRepository repository) {
|
||||||
|
return (Store<AppState> store, action, NextDispatcher next) {
|
||||||
|
|
||||||
|
repository.loadItems(store.state.auth).then(
|
||||||
|
(data) {
|
||||||
|
store.dispatch(
|
||||||
|
DashboardLoadedAction(
|
||||||
|
//products.map(ProductEntity.fromEntity).toList(),
|
||||||
|
data,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).catchError((error) => store.dispatch(DashboardNotLoadedAction(error)));
|
||||||
|
|
||||||
|
next(action);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -40,9 +40,7 @@ Middleware<AppState> _createSaveProducts(BaseRepository repository) {
|
||||||
Middleware<AppState> _createLoadProducts(BaseRepository repository) {
|
Middleware<AppState> _createLoadProducts(BaseRepository repository) {
|
||||||
return (Store<AppState> store, action, NextDispatcher next) {
|
return (Store<AppState> store, action, NextDispatcher next) {
|
||||||
|
|
||||||
print('Product Middleware: createLoadProducts...');
|
repository.loadItems(store.state.auth).then(
|
||||||
|
|
||||||
repository.loadData(store.state.auth).then(
|
|
||||||
(products) {
|
(products) {
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
ProductsLoadedAction(
|
ProductsLoadedAction(
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,27 @@ class Dashboard extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new Scaffold(
|
return Scaffold(
|
||||||
appBar: new AppBar(
|
appBar: AppBar(
|
||||||
title: new Text('Dashboard'),
|
title: Text('Dashboard'),
|
||||||
),
|
),
|
||||||
drawer: new Sidebar(),
|
drawer: Sidebar(),
|
||||||
body: new Center(
|
body: Column(
|
||||||
child: new Column(
|
children: <Widget>[
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Card(
|
||||||
children: [
|
child: Column(
|
||||||
new Text(
|
mainAxisSize: MainAxisSize.min,
|
||||||
'DASHBOARD',
|
children: <Widget>[
|
||||||
|
const ListTile(
|
||||||
|
leading: const Icon(Icons.album),
|
||||||
|
title: const Text('The Enchanted Nightingale'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Music by Julie Gable. Lyrics by Sidney Stein.'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue