Commit fe4a9101 authored by LAVENIER's avatar LAVENIER
Browse files

Merge branch 'release/1.6.4'

parents 863f8ea7 bdab89af
<?xml version='1.0' encoding='utf-8'?>
<widget android-versionCode="10603" id="net.sumaris.app" version="1.6.3" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<widget android-versionCode="10604" id="net.sumaris.app" version="1.6.4" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>SUMARiS</name>
<description>Halieutic data capture</description>
<author email="contact@e-is.pro" href="http://www.e-is.pro">Environmental Information Systems</author>
......
......@@ -15,7 +15,7 @@ if [[ "_$INSTALL_DIR" == "_" ]]; then
fi
latest_version() {
echo "1.6.3" #lastest
echo "1.6.4" #lastest
}
api_release_url() {
......
{
"name": "sumaris-app",
"description": "SUMARiS app",
"version": "1.6.3",
"version": "1.6.4",
"author": "contact@e-is.pro",
"license": "AGPL-3.0",
"readmeFilename": "README.md",
......@@ -96,9 +96,9 @@
"ng2-charts": "^2.4.2",
"ng2-charts-schematics": "^0.1.7",
"ngx-color-picker": "^10.1.0",
"ngx-markdown": "^11.0.0",
"ngx-markdown": "^11.0.1",
"ngx-material-timepicker": "5.5.3",
"ngx-quicklink": "^0.2.4",
"ngx-quicklink": "^0.2.6",
"roboto-fontface": "^0.10.0",
"rxjs": "^6.6.3",
"scrypt-async": "^2.0.1",
......
......@@ -2,6 +2,19 @@
[hasValidate]="form.dirty && !saving"
(onValidate)="save($event)"
[canGoBack]="false">
<ion-buttons slot="end">
<!-- loader -->
<ion-spinner *ngIf="loading || saving; else endButtons"></ion-spinner>
<ng-template #endButtons>
<!-- refresh button -->
<ion-button [matTooltip]="'COMMON.BTN_REFRESH'|translate"
(click)="refresh($event)">
<mat-icon slot="icon-only">refresh</mat-icon>
</ion-button>
</ng-template>
</ion-buttons>
</app-toolbar>
<ion-content>
......
......@@ -25,6 +25,7 @@ import {StatusIds} from "../services/model/model.enum";
})
export class AccountPage extends AppForm<Account> implements OnDestroy {
loading = true;
isLogin: boolean;
changesSubscription: Subscription;
account: Account;
......@@ -74,6 +75,9 @@ export class AccountPage extends AppForm<Account> implements OnDestroy {
if (accountService.isLogin()) {
this.onLogin(this.accountService.account);
}
else {
this.loading = false;
}
}));
}
......@@ -95,6 +99,8 @@ export class AccountPage extends AppForm<Account> implements OnDestroy {
this.markAsPristine();
this.startListenChanges();
this.loading = false;
}
onLogout() {
......@@ -109,6 +115,14 @@ export class AccountPage extends AppForm<Account> implements OnDestroy {
this.stopListenChanges();
}
async refresh(event?: UIEvent) {
this.disable();
this.loading = true;
return this.accountService.refresh();
}
startListenChanges() {
if (this.changesSubscription) return; // already started
this.changesSubscription = this.accountService.listenChanges();
......@@ -145,6 +159,10 @@ export class AccountPage extends AppForm<Account> implements OnDestroy {
}
async save(event: MouseEvent) {
if (this.saving) return;
await AppFormUtils.waitWhilePending(this.form);
if (this.form.invalid) {
AppFormUtils.logFormErrors(this.form);
return;
......
import {ModuleWithProviders, NgModule} from '@angular/core';
import {ModuleWithProviders, NgModule, Optional} from '@angular/core';
import {RouterModule} from '@angular/router';
import {AccountValidatorService} from './services/validator/account.validator';
import {UserSettingsValidatorService} from './services/validator/user-settings.validator';
......@@ -175,11 +175,10 @@ export {
export class CoreModule {
static forRoot(): ModuleWithProviders<CoreModule> {
console.info("[core] Creating module (root)");
return {
ngModule: CoreModule,
providers: [ PlatformService ]
}
};
}
}
import {ChangeDetectorRef, Directive, EventEmitter, Injector, OnDestroy, OnInit, Optional} from '@angular/core';
import {
AfterViewInit,
ChangeDetectorRef,
Directive,
EventEmitter,
Injector,
OnDestroy,
OnInit,
Optional
} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {AlertController, ToastController} from "@ionic/angular";
......@@ -27,6 +36,7 @@ import {ErrorCodes, ServerErrorCodes} from "../services/errors";
export class AppEditorOptions extends AppTabFormOptions {
autoLoad?: boolean;
autoLoadDelay?: number;
pathIdAttribute?: string;
enableListenChanges?: boolean;
......@@ -48,12 +58,13 @@ export abstract class AppEntityEditor<
S extends EntityService<T> = EntityService<T>
>
extends AppTabEditor<T, EntityServiceLoadOptions>
implements OnInit, OnDestroy {
implements OnInit, OnDestroy, AfterViewInit {
private _usageMode: UsageMode;
private readonly _enableListenChanges: boolean;
private readonly _pathIdAttribute: string;
private readonly _autoLoad: boolean;
private readonly _autoLoadDelay: number;
private readonly _autoUpdateRoute: boolean;
private _autoOpenNextTab: boolean;
......@@ -108,6 +119,7 @@ export abstract class AppEntityEditor<
enableListenChanges: (environment.listenRemoteChanges === true),
pathIdAttribute: 'id',
autoLoad: true,
autoLoadDelay: 0,
autoUpdateRoute: true,
// Following options are override inside ngOnInit()
......@@ -124,6 +136,7 @@ export abstract class AppEntityEditor<
this._enableListenChanges = options.enableListenChanges;
this._pathIdAttribute = options.pathIdAttribute;
this._autoLoad = options.autoLoad;
this._autoLoadDelay = options.autoLoadDelay;
this._autoUpdateRoute = options.autoUpdateRoute;
this._autoOpenNextTab = options.autoOpenNextTab;
......@@ -143,10 +156,12 @@ export abstract class AppEntityEditor<
// Disable page, during load
this.disable();
}
async ngAfterViewInit() {
// Load data
if (this._autoLoad) {
this.loadFromRoute();
setTimeout(() => this.loadFromRoute(), this._autoLoadDelay);
}
}
......
......@@ -12,7 +12,7 @@
</ion-item>
<ion-card-content>
<ion-label class="ion-text-wrap">
<ion-label class="ion-text-wrap status">
<!-- creation date -->
<ion-text *ngIf="value.creationDate">
<ion-icon name="calendar"></ion-icon>&nbsp;
......
......@@ -10,8 +10,8 @@ ion-card {
ion-card-content {
background-color: var(--ion-color-light-tint);
ion-label,
* ion-label {
ion-label.status,
* ion-label.status {
font-size: 12px !important;
color: var(--ion-color-step-850);
}
......
......@@ -139,7 +139,9 @@ export abstract class AppTabEditor<T = any, O = any> implements IAppForm, OnInit
this.selectedTabIndex = this.queryParams[this.queryTabIndexParamName];
}
}
this.tabGroup.realignInkBar();
// Realign tab, after a delay because the tab can be disabled when component is created
setTimeout(() => this.tabGroup.realignInkBar(), 500);
}));
}
......@@ -217,6 +219,20 @@ export abstract class AppTabEditor<T = any, O = any> implements IAppForm, OnInit
if (!this.loading && (!opts || opts.emitEvent !== false)) this.markForCheck();
}
markAsLoading(opts?: { emitEvent?: boolean; }){
if (!this.loading) {
this.loading = true;
if (!opts || opts.emitEvent !== false) this.markForCheck();
}
}
markAsLoaded(opts?: { emitEvent?: boolean; }){
if (this.loading) {
this.loading = false;
if (!opts || opts.emitEvent !== false) this.markForCheck();
}
}
onTabChange(event: MatTabChangeEvent, queryTabIndexParamName?: string): boolean {
queryTabIndexParamName = queryTabIndexParamName || this.queryTabIndexParamName;
......
......@@ -12,13 +12,10 @@
<ion-card-content>
<div>
<h4 *ngIf="loading">
<span translate>REGISTER.CONFIRMED.LOADING</span>
</h4>
<p *ngIf="loading">
<ion-spinner></ion-spinner>
</p>
<ng-container *ngIf="loading">
<h4 translate>REGISTER.CONFIRMED.LOADING</h4>
<p><ion-spinner></ion-spinner></p>
</ng-container>
<!-- error -->
<ion-item *ngIf="!loading && error">
......
......@@ -474,7 +474,7 @@ export class AccountService extends BaseEntityService {
// Load account data
try {
await this.loadData({offline});
await this.loadData({offline, fetchPolicy: 'network-only'});
}
catch (err) {
// If account not found, check if email is valid
......@@ -514,7 +514,7 @@ export class AccountService extends BaseEntityService {
return this.data.account;
}
public async refresh(): Promise<Account> {
async refresh(): Promise<Account> {
if (!this.data.pubkey) throw new Error("User not logged");
if (this.network.offline) throw new Error("Cannot check account in offline mode");
......@@ -668,13 +668,14 @@ export class AccountService extends BaseEntityService {
responseType: 'dataUrl'
})
.then(dataUrl => {
console.debug("[account] Image fetched: " + dataUrl);
if (dataUrl && this._debug) console.debug("[account] Image fetched: ", dataUrl.substring(0, 50));
// TODO: make sure to display Base64 image in the menu top header
//jsonAccount.avatar = dataUrl;
})
.catch(err => {
console.error(`[account] Error while fetching image: ${jsonAccount.avatar}: ${err}`);
});
}
await Promise.all([
......@@ -933,7 +934,7 @@ export class AccountService extends BaseEntityService {
const self = this;
console.debug('[account] [WS] Listening changes on {/subscriptions/websocket}...');
console.debug('[account] [WS] Listening account changes');
const subscription = this.graphql.subscribe<{updateAccount: any}>({
query: UpdateSubscription,
......@@ -946,13 +947,12 @@ export class AccountService extends BaseEntityService {
message: 'ERROR.ACCOUNT.SUBSCRIBE_ACCOUNT_ERROR'
}
}).subscribe({
async next(data) {
if (data && data.updateAccount) {
const existingUpdateDate = self.data.account && toDateISOString(self.data.account.updateDate);
if (existingUpdateDate !== data.updateAccount.updateDate) {
console.debug("[account] [WS] Detected update on {" + data.updateAccount.updateDate + "}");
await self.refresh();
}
async next({updateAccount}) {
if (!updateAccount) return;
const existingUpdateDate = self.data.account && toDateISOString(self.data.account.updateDate);
if (existingUpdateDate !== updateAccount.updateDate) {
console.debug("[account] [WS] Detected update on {" + updateAccount.updateDate + "}");
await self.refresh();
}
},
async error(err) {
......@@ -974,7 +974,7 @@ export class AccountService extends BaseEntityService {
});
// Add log when closing WS
subscription.add(() => console.debug('[account] [WS] Stop to listen changes'));
subscription.add(() => console.debug('[account] [WS] Stop listening account changes'));
return subscription;
}
......
......@@ -58,7 +58,7 @@ export abstract class Entity<T extends IEntity<any, O, ID>, O extends EntityAsOb
asObject(opts?: O): StoreObject {
const target: any = Object.assign({}, this); //= {...this};
if (!opts || opts.keepTypename !== true) delete target.__typename;
if ((!opts || opts.keepLocalId === false) && target.id < 0) delete target.id;
if (opts && opts.keepLocalId === false && target.id < 0) delete target.id;
target.updateDate = toDateISOString(this.updateDate);
return target;
}
......
......@@ -3,11 +3,12 @@ import {EventEmitter, Inject, Injectable, InjectionToken, Optional} from "@angul
import {Storage} from "@ionic/storage";
import {Platform} from "@ionic/angular";
import {environment} from "../../../../environments/environment";
import {catchError, switchMap, throttleTime} from "rxjs/operators";
import {catchError, first, switchMap, throttleTime} from "rxjs/operators";
import {Entity} from "../model/entity.model";
import {isEmptyArray, isNilOrBlank} from "../../../shared/functions";
import {LoadResult} from "../../../shared/services/entity-service.class";
import {ENTITIES_STORAGE_KEY_PREFIX, EntityStorageLoadOptions, EntityStore, EntityStoreTypePolicy} from "./entity-store.class";
import {ProgressBarService} from "../../../shared/services/progress-bar.service";
export interface EntitiesStorageTypePolicies {
......@@ -17,9 +18,7 @@ export interface EntitiesStorageTypePolicies {
export const APP_LOCAL_STORAGE_TYPE_POLICIES = new InjectionToken<EntitiesStorageTypePolicies>('localStorageTypePolicies');
@Injectable({providedIn: 'root'})
export class EntitiesStorage
// TODO: implements EntityService<T, EntitiesStorageLoadOption>
{
export class EntitiesStorage {
public static TRASH_PREFIX = "Trash#";
......@@ -35,7 +34,6 @@ export class EntitiesStorage
private _dirty = false;
private _saving = false;
onStart = new Subject<void>();
get dirty(): boolean {
......@@ -44,6 +42,7 @@ export class EntitiesStorage
public constructor(
private platform: Platform,
private progressBarService: ProgressBarService,
private storage: Storage,
@Optional() @Inject(APP_LOCAL_STORAGE_TYPE_POLICIES) typePolicies: EntitiesStorageTypePolicies
) {
......@@ -51,7 +50,7 @@ export class EntitiesStorage
// For DEV only
this._debug = !environment.production;
if (this._debug) console.debug('[entity-storage] Creating entity storage');
if (this._debug) console.debug('[entities-storage] Creating service');
}
watchAll<T extends Entity<T>>(entityName: string,
......@@ -70,9 +69,14 @@ export class EntitiesStorage
.pipe(switchMap(() => this.watchAll<T>(entityName, variables, opts))); // Loop
}
this.progressBarService.increase();
const storeName = variables && variables.trash ? (EntitiesStorage.TRASH_PREFIX + entityName) : entityName;
return this.getEntityStore<T>(storeName, {create: true})
const result = this.getEntityStore<T>(storeName, {create: true})
.watchAll(variables, opts);
result.pipe(first()).subscribe(() => this.progressBarService.decrease());
return result;
}
async loadAll<T extends Entity<T>>(entityName: string,
......@@ -88,19 +92,33 @@ export class EntitiesStorage
// Make sure store is ready
if (!this._started) await this.ready();
if (this._debug) console.debug(`[entity-storage] Loading ${entityName}...`);
try {
this.progressBarService.increase();
if (this._debug) console.debug(`[entities-storage] Loading ${entityName}...`);
const entityStore = this.getEntityStore<T>(entityName, {create: false});
if (!entityStore) return {data: [], total: 0}; // No store for this entity name
const entityStore = this.getEntityStore<T>(entityName, {create: false});
if (!entityStore) return {data: [], total: 0}; // No store for this entity name
return entityStore.loadAll(variables, opts);
return entityStore.loadAll(variables, opts);
}
finally {
this.progressBarService.decrease();
}
}
async load<T extends Entity<T>>(id: number, entityName: string, opts?: EntityStorageLoadOptions): Promise<T> {
await this.ready();
const entityStore = this.getEntityStore<T>(entityName, {create: false});
if (!entityStore) return undefined;
return entityStore.load(id, opts);
try {
this.progressBarService.increase();
const entityStore = this.getEntityStore<T>(entityName, {create: false});
if (!entityStore) return undefined;
return entityStore.load(id, opts);
}
finally {
this.progressBarService.decrease();
}
}
async nextValue(entityOrName: string | any): Promise<number> {
......@@ -118,7 +136,7 @@ export class EntitiesStorage
for (let i = 0; i < entityCount - 1; i++) {
store.nextValue();
}
if (this._debug) console.debug(`[entity-storage] Reserving range [${firstValue},${store.currentValue()}] for ${entityName}'s sequence`);
if (this._debug) console.debug(`[entities-storage] Reserving range [${firstValue},${store.currentValue()}] for ${entityName}'s sequence`);
return firstValue;
}
......@@ -135,15 +153,22 @@ export class EntitiesStorage
await this.ready();
this._dirty = true;
let storeName = opts && opts.entityName || this.detectEntityName(entity);
this.getEntityStore<T>(storeName)
.save(entity, opts);
try {
this.progressBarService.increase();
// Ask to save
this._$save.emit();
this._dirty = true;
const storeName = opts && opts.entityName || this.detectEntityName(entity);
this.getEntityStore<T>(storeName)
.save(entity, opts);
return entity;
// Ask to save
this._$save.emit();
return entity;
}
finally {
this.progressBarService.decrease();
}
}
async saveAll<T extends Entity<T>>(entities: T[], opts?: {
......@@ -155,9 +180,16 @@ export class EntitiesStorage
await this.ready();
this._dirty = true;
const entityName = opts && opts.entityName || this.detectEntityName(entities[0]);
return this.getEntityStore<T>(entityName).saveAll(entities, opts);
try {
this.progressBarService.increase();
this._dirty = true;
const entityName = opts && opts.entityName || this.detectEntityName(entities[0]);
return this.getEntityStore<T>(entityName).saveAll(entities, opts);
}
finally {
this.progressBarService.decrease();
}
}
async delete<T extends Entity<T>>(entity: T, opts?: {
......@@ -182,22 +214,28 @@ export class EntitiesStorage
if (!opts || isNilOrBlank(opts.entityName)) throw new Error("Missing argument 'opts' or 'entityName'");
//if (id >= 0) throw new Error('Invalid id a local entity (not a negative number): ' + id);
const entityStore = this.getEntityStore<T>(opts.entityName, {create: false});
if (!entityStore) return undefined;
try {
this.progressBarService.increase();
const entityStore = this.getEntityStore<T>(opts.entityName, {create: false});
if (!entityStore) return undefined;
const deletedEntity = entityStore.delete(id, opts);
const deletedEntity = entityStore.delete(id, opts);
// If something deleted
if (deletedEntity) {
// If something deleted
if (deletedEntity) {
// Mark as dirty
this._dirty = true;
// Mark as dirty
this._dirty = true;
// Ask to save
this._$save.emit();
}
// Ask to save
this._$save.emit();
}
return deletedEntity;
return deletedEntity;
}
finally {
this.progressBarService.decrease();
}
}
async deleteMany<T extends Entity<T>>(ids: number[], opts: {
......@@ -208,24 +246,29 @@ export class EntitiesStorage
if (!opts || isNilOrBlank(opts.entityName)) throw new Error("Missing argument 'opts' or 'opts.entityName'");
const entityStore = this.getEntityStore<T>(opts.entityName, {create: false});
if (!entityStore) return undefined;
try {
this.progressBarService.increase();
const entityStore = this.getEntityStore<T>(opts.entityName, {create: false});
if (!entityStore) return undefined;
// Do deletion
const deletedEntities = entityStore.deleteMany(ids, opts);
// Do deletion
const deletedEntities = entityStore.deleteMany(ids, opts);
// If something deleted
if (deletedEntities.length > 0) {
// If something deleted
if (deletedEntities.length > 0) {
// Mark as dirty
this._dirty = true;
// Mark as dirty
this._dirty = true;
// Mark as save need
this._$save.emit();
// Mark as save need
this._$save.emit();
}
}
return deletedEntities;
return deletedEntities;
} finally {
this.progressBarService.decrease();
}
}
async deleteFromTrash<T extends Entity<T>>(entity: T, opts?: {
......@@ -362,7 +405,7 @@ export class EntitiesStorage
if (this._started) return Promise.resolve();
const now = Date.now();
console.info(`[entity-storage] Starting entity storage...`);