Commit 8be03ec5 authored by LAVENIER's avatar LAVENIER
Browse files

Merge branch 'release/1.5.4'

parents f3745362 d4edcbb4
<?xml version='1.0' encoding='utf-8'?>
<widget android-versionCode="10503" id="net.sumaris.app" version="1.5.3" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<widget android-versionCode="10504" id="net.sumaris.app" version="1.5.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.5.3" #lastest
echo "1.5.4" #lastest
}
api_release_url() {
......
{
"name": "sumaris-app",
"description": "SUMARiS app",
"version": "1.5.3",
"version": "1.5.4",
"author": "contact@e-is.pro",
"license": "AGPL-3.0",
"readmeFilename": "README.md",
......
......@@ -22,12 +22,12 @@ release_description=$4
if [[ ! $task =~ ^(pre|rel)$ || ! $version =~ ^[0-9]+.[0-9]+.[0-9]+(-(alpha|beta|rc)[0-9]+)?$ || ! $androidVersion =~ ^[0-9]+$ ]]; then
echo "Wrong version format"
echo "Usage:"
echo " > ./release-gitflow.sh [pre|rel] <version> <android-version> <release_description>"
echo " > $0 pre|rel <version> <android-version> <release_description>"
echo "with:"
echo " - pre: use for pre-release"
echo " - rel: for full release"
echo " - version: x.y.z"
echo " - android-version: nnn"
echo " - android-version: xxyyzz"
echo " - release_description: a comment on release"
exit 1
fi
......
import {Component, EventEmitter, OnInit, Output} from "@angular/core";
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output} from "@angular/core";
import {FormBuilder, Validators} from "@angular/forms";
import {ModalController} from "@ionic/angular";
import {RegisterModal} from '../../register/modal/modal-register';
import {AuthData} from "../../services/account.service";
......@@ -10,7 +10,7 @@ import {slideUpDownAnimation} from "../../../shared/material/material.animations
import {PlatformService} from "../../services/platform.service";
import {DateAdapter} from "@angular/material/core";
import {Moment} from "moment";
import {debounceTime, throttleTime} from "rxjs/operators";
import {debounceTime} from "rxjs/operators";
import {AppForm} from "../../form/form.class";
......@@ -18,7 +18,8 @@ import {AppForm} from "../../form/form.class";
selector: 'app-form-auth',
templateUrl: 'form-auth.html',
styleUrls: ['./form-auth.scss'],
animations: [slideUpDownAnimation]
animations: [slideUpDownAnimation],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AuthForm extends AppForm<AuthData> implements OnInit {
......@@ -44,7 +45,8 @@ export class AuthForm extends AppForm<AuthData> implements OnInit {
formBuilder: FormBuilder,
settings: LocalSettingsService,
private modalCtrl: ModalController,
public network: NetworkService
public network: NetworkService,
private cd: ChangeDetectorRef
) {
super(dateAdapter,
formBuilder.group({
......@@ -104,9 +106,16 @@ export class AuthForm extends AppForm<AuthData> implements OnInit {
this.onCancel.emit();
setTimeout(async () => {
const modal = await this.modalCtrl.create({
component: RegisterModal
component: RegisterModal,
backdropDismiss: false
});
return modal.present();
}, 200);
}
/* -- protected functions -- */
protected markForCheck() {
this.cd.markForCheck();
}
}
......@@ -45,8 +45,7 @@ import {
Entity,
EntityAsObjectOptions,
entityToString,
EntityUtils,
PropertiesMap
EntityUtils
} from './services/model/entity.model';
// import ngx-translate and the http loader
import {HttpClientModule} from '@angular/common/http';
......@@ -59,7 +58,7 @@ import {AppListForm} from "./form/list.form";
import {PlatformService} from "./services/platform.service";
import {IsNotOnFieldModePipe, IsOnFieldModePipe} from "./services/pipes/usage-mode.pipes";
import {PersonToStringPipe} from "./services/pipes/person-to-string.pipe";
import {NetworkStatusCard} from "./peer/network-status-card.component";
import {AppInstallUpgradeCard} from "./install/install-upgrade-card.component";
import {AccountToStringPipe, IsLoginAccountPipe} from "./services/pipes/account.pipes";
export {
......@@ -99,8 +98,7 @@ export {
joinPropertiesPath,
FormArrayHelper,
AppTableUtils,
EntityAsObjectOptions,
PropertiesMap
EntityAsObjectOptions
};
......@@ -138,7 +136,7 @@ export {
// Network
SelectPeerModal,
NetworkStatusCard,
AppInstallUpgradeCard,
// Other components
TableSelectColumnsComponent,
......@@ -171,7 +169,7 @@ export {
AboutModal,
AppPropertiesForm,
AppListForm,
NetworkStatusCard,
AppInstallUpgradeCard,
]
})
export class CoreModule {
......
......@@ -15,7 +15,7 @@ import {
toBoolean
} from '../../shared/shared.module';
import {Moment} from "moment";
import {LocalSettingsService} from "../services/local-settings.service";
import {AddToPageHistoryOptions, LocalSettingsService} from "../services/local-settings.service";
import {filter} from "rxjs/operators";
import {Entity} from "../services/model/entity.model";
import {HistoryPageReference, UsageMode} from "../services/model/settings.model";
......@@ -352,6 +352,7 @@ export abstract class AppEntityEditor<
}
async saveAndClose(event: Event, options?: any): Promise<boolean> {
const saved = await this.save(event);
if (saved) {
await this.close(event);
......@@ -360,6 +361,11 @@ export abstract class AppEntityEditor<
}
async close(event: Event) {
if (event) {
if (event.defaultPrevented) return;
event.preventDefault();
event.stopPropagation();
}
if (this.appToolbar && this.appToolbar.canGoBack) {
await this.appToolbar.goBack();
}
......@@ -490,6 +496,9 @@ export abstract class AppEntityEditor<
this.onEntityDeleted(data);
// Remove page history
this.removePageHistory();
} catch (err) {
this.submitted = true;
this.setError(err);
......@@ -509,6 +518,8 @@ export abstract class AppEntityEditor<
return this.router.navigateByUrl('/');
}
}, 500);
}
/* -- protected methods to override -- */
......@@ -593,24 +604,26 @@ export abstract class AppEntityEditor<
// If NOT data, then add to page history
if (!this.isNewData) {
this.addToPageHistory({
return this.addToPageHistory({
title,
path: this.router.url
});
}
}
protected addToPageHistory(page: HistoryPageReference, opts?: {
removePathQueryParams?: boolean;
removeTitleSmallTag?: boolean;
}) {
this.settings.addToPageHistory(page, {
protected async addToPageHistory(page: HistoryPageReference, opts?: AddToPageHistoryOptions) {
return this.settings.addToPageHistory(page, {
removePathQueryParams: true,
removeTitleSmallTag: true,
emitEvent: false,
...opts
});
}
protected async removePageHistory(opts?: { emitEvent?: boolean; }) {
return this.settings.removePageHistory(this.router.url, opts);
}
/**
* Open the first tab that is invalid
*/
......
......@@ -8,18 +8,18 @@ import {
ValidatorFn
} from "@angular/forms";
import {
sleep,
filterNumberInput,
isNil,
nullIfUndefined,
selectInputContent,
sleep,
toBoolean,
toDateISOString
} from "../../shared/shared.module";
import {isMoment} from "moment";
import {Entity, ObjectMap} from "../services/model/entity.model";
import {Entity} from "../services/model/entity.model";
import {timer} from "rxjs";
import {filter, first, tap} from "rxjs/operators";
import {filter, first} from "rxjs/operators";
import {SharedFormArrayValidators} from "../../shared/validator/validators";
import {round} from "../../shared/functions";
......@@ -493,31 +493,39 @@ export function markAsUntouched(form: FormGroup, opts?: {onlySelf?: boolean; })
*/
export function waitWhilePending<T extends {pending: boolean; }>(form: T, opts?: {
checkPeriod?: number;
timeout?: number;
}): Promise<any> {
const period = opts && opts.checkPeriod || 300;
if (!form.pending) return;
let stop = false;
if (opts && opts.timeout) {
setTimeout(() => {
console.warn(`Waiting async validator: timeout reached (after ${opts.timeout}ms)`);
stop = true;
}, opts.timeout);
}
return timer(period, period)
.pipe(
// For DEBUG :
tap(() => console.debug("Waiting async validator...", form)),
filter(() => !form.pending),
//tap(() => console.debug("Waiting async validator...", form)),
filter(() => stop || !form.pending),
first()
).toPromise();
}
export function isControlHasInput(controls: ObjectMap<AbstractControl>, controlName: string): boolean {
export function isControlHasInput(controls: { [key:string]: AbstractControl}, controlName: string): boolean {
// true if the control has a value and its 'calculated' control has the value 'false'
return controls[controlName].value && !toBoolean(controls[controlName + AppFormUtils.calculatedSuffix].value, false);
}
export function setCalculatedValue(controls: ObjectMap<AbstractControl>, controlName: string, value: number | undefined) {
export function setCalculatedValue(controls: { [key:string]: AbstractControl}, controlName: string, value: number | undefined) {
// set value to control
controls[controlName].setValue(round(value));
// set 'calculated' control to 'true'
controls[controlName + AppFormUtils.calculatedSuffix].setValue(true);
}
export function resetCalculatedValue(controls: ObjectMap<AbstractControl>, controlName: string) {
export function resetCalculatedValue(controls: { [key:string]: AbstractControl}, controlName: string) {
if (!AppFormUtils.isControlHasInput(controls, controlName)) {
// set undefined only if control already calculated
AppFormUtils.setCalculatedValue(controls, controlName, undefined);
......
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Injector, Input, OnInit, Optional} from "@angular/core";
import {FormArray, FormBuilder, FormGroup, FormGroupDirective, Validators} from "@angular/forms";
import {EntityUtils} from "../services/model/entity.model";
import {FormFieldDefinition, FormFieldDefinitionMap, FormFieldValue} from "../../shared/form/field.model";
import {FormFieldDefinition, FormFieldDefinitionMap} from "../../shared/form/field.model";
import {isEmptyArray, isNil} from "../../shared/functions";
import {DateAdapter} from "@angular/material/core";
import {Moment} from "moment";
import {LocalSettingsService} from "../services/local-settings.service";
import {AppForm} from "./form.class";
import {FormArrayHelper, FormArrayHelperOptions} from "./form.utils";
import {Property} from "../../shared/types";
@Component({
selector: 'app-properties-form',
templateUrl: 'properties.form.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppPropertiesForm<T = FormFieldValue> extends AppForm<T[]> implements OnInit {
export class AppPropertiesForm<T = Property> extends AppForm<T[]> implements OnInit {
loading = true;
definitionsMapByKey: FormFieldDefinitionMap = {};
definitionsByIndex: { [index: number]: FormFieldDefinition } = {};
helper: FormArrayHelper<FormFieldValue>;
helper: FormArrayHelper<Property>;
@Input() formArrayName: string;
......@@ -54,9 +55,6 @@ export class AppPropertiesForm<T = FormFieldValue> extends AppForm<T[]> implemen
null,
settings);
//this.debug = !environment.production;
}
......@@ -83,7 +81,7 @@ export class AppPropertiesForm<T = FormFieldValue> extends AppForm<T[]> implemen
this.form.addControl(this.formArrayName, this.formArray);
}
this.helper = new FormArrayHelper<FormFieldValue>(
this.helper = new FormArrayHelper<Property>(
this.formArray,
(value) => this.getPropertyFormGroup(value),
(v1, v2) => (!v1 && !v2) || v1.key === v2.key,
......@@ -127,7 +125,7 @@ export class AppPropertiesForm<T = FormFieldValue> extends AppForm<T[]> implemen
if (!data) return; // Skip
// Transform properties map into array
const values = EntityUtils.getObjectAsArray(data);
const values = EntityUtils.getMapAsArray<T>(data);
this.helper.resize(values.length);
this.helper.formArray.patchValue(values, {emitEvent: false});
......
......@@ -245,7 +245,6 @@ export abstract class AppTabEditor<T = any, O = any> implements IAppForm, OnInit
*/
onSwipeTab(event: HammerSwipeEvent) {
// DEBUG
console.debug("[tab-editor] TODO onSwipeTab()");
// if (this.debug) console.debug("[tab-page] onSwipeTab()");
// Skip, if not a valid swipe event
......
......@@ -33,7 +33,7 @@ import loggerLink from 'apollo-link-logger';
import {Platform} from "@ionic/angular";
import {EntityUtils, IEntity} from "../services/model/entity.model";
import {DataProxy} from 'apollo-cache';
import {isNotNil, toNumber} from "../../shared/functions";
import {isNil, isNotNil, toNumber} from "../../shared/functions";
import {Resolvers} from "@apollo/client/core/types";
import {HttpHeaders} from "@angular/common/http";
import {EmptyObject} from "apollo-angular/types";
......@@ -299,37 +299,40 @@ export class GraphqlService {
opts.arrayFieldName = opts.arrayFieldName || 'data';
try {
const res = cache.readQuery<any, V>(opts);
let data = cache.readQuery<any, V>(opts);
if (data && data[opts.arrayFieldName]) {
// Copy because immutable
data = { ...data };
if (res && res[opts.arrayFieldName]) {
// Append to result array
res[opts.arrayFieldName].push(opts.data);
data[opts.arrayFieldName] = [ ...data[opts.arrayFieldName], opts.data];
// Resort, if need
if (opts.sortFn) {
res[opts.arrayFieldName].sort(opts.sortFn);
data[opts.arrayFieldName].sort(opts.sortFn);
}
// Exclude if exceed max size
const size = toNumber(opts.variables && opts.variables['size'], -1);
if (size > 0 && res[opts.arrayFieldName].length > size) {
res[opts.arrayFieldName].splice(size, res[opts.arrayFieldName].length - size);
if (size > 0 && data[opts.arrayFieldName].length > size) {
data[opts.arrayFieldName].splice(size, data[opts.arrayFieldName].length - size);
}
// Increment total
if (isNotNil(opts.totalFieldName)) {
if (res[opts.totalFieldName]) {
res[opts.totalFieldName] += 1;
if (isNotNil(data[opts.totalFieldName])) {
data[opts.totalFieldName] += 1;
}
else {
console.warn('[graphql] Unable to update cached query. Unknown result part: ' + opts.totalFieldName);
}
}
cache.writeQuery<T[], V>({
cache.writeQuery({
query: opts.query,
variables: opts.variables,
data: res
data
});
}
else {
......@@ -357,33 +360,36 @@ export class GraphqlService {
opts.arrayFieldName = opts.arrayFieldName || 'data';
try {
const res = cache.readQuery(opts);
let data:any = cache.readQuery(opts);
if (data && data[opts.arrayFieldName]) {
// Copy because immutable
data = { ...data };
if (res && res[opts.arrayFieldName]) {
// Keep only not existing res
const equalsFn = opts.equalsFn || ((d1, d2) => d1['id'] === d2['id'] && d1['entityName'] === d2['entityName']);
let newItems = opts.data.filter(inputValue => res[opts.arrayFieldName].findIndex(existingValue => equalsFn(inputValue, existingValue)) === -1);
const newItems = opts.data.filter(inputValue => data[opts.arrayFieldName].findIndex(existingValue => equalsFn(inputValue, existingValue)) === -1);
if (!newItems.length) return; // No new value
// Append to result array
res[opts.arrayFieldName] = res[opts.arrayFieldName].concat(newItems);
// Append to array
data[opts.arrayFieldName] = [ ...data[opts.arrayFieldName], ...newItems]
// Resort, if need
if (opts.sortFn) {
res[opts.arrayFieldName].sort(opts.sortFn);
data[opts.arrayFieldName].sort(opts.sortFn);
}
// Exclude if exceed max size
const size = toNumber(opts.variables && opts.variables['size'], -1);
if (size > 0 && res[opts.arrayFieldName].length > size) {
res[opts.arrayFieldName].splice(size, res[opts.arrayFieldName].length - size);
if (size > 0 && data[opts.arrayFieldName].length > size) {
data[opts.arrayFieldName].splice(size, data[opts.arrayFieldName].length - size);
}
// Increment the total
if (isNotNil(opts.totalFieldName)) {
if (res[opts.totalFieldName]) {
res[opts.totalFieldName] += newItems.length;
if (isNotNil(data[opts.totalFieldName])) {
data[opts.arrayFieldName] += newItems.length;
}
else {
console.warn('[graphql] Unable to update cached query. Unknown result part: ' + opts.totalFieldName);
......@@ -394,7 +400,7 @@ export class GraphqlService {
cache.writeQuery({
query: opts.query,
variables: opts.variables,
data: res
data
});
}
else {
......@@ -418,20 +424,25 @@ export class GraphqlService {
opts.arrayFieldName = opts.arrayFieldName || 'data';
try {
const res = cache.readQuery({...opts, id: opts.id.toString()});
let data:any = cache.readQuery({...opts, id: opts.id.toString()});
if (res && res[opts.arrayFieldName]) {
if (data && data[opts.arrayFieldName]) {
// Copy because immutable
data = { ...data };
const index = res[opts.arrayFieldName].findIndex(item => item['id'] === opts.id);
const index = data[opts.arrayFieldName].findIndex(item => item['id'] === opts.id);
if (index === -1) return; // Skip (nothing removed)
// Remove the item
res[opts.arrayFieldName].splice(index, 1);
// Copy, then remove deleted item
data[opts.arrayFieldName] = data[opts.arrayFieldName].slice();
const deletedItem = data[opts.arrayFieldName].splice(index, 1)[0];
cache.evict({id: dataIdFromObject(deletedItem)});
// Increment the total
// Decrement the total
if (isNotNil(opts.totalFieldName)) {
if (res[opts.totalFieldName]) {
res[opts.totalFieldName] -= 1;
if (isNotNil(data[opts.totalFieldName])) {
data[opts.totalFieldName] -= 1;
}
else {
console.warn('[graphql] Unable to update cached query. Unknown result part: ' + opts.totalFieldName);
......@@ -442,7 +453,7 @@ export class GraphqlService {
cache.writeQuery({
query: opts.query,
variables: opts.variables,
data: res
data
});
}
else {
......@@ -466,38 +477,55 @@ export class GraphqlService {
opts.arrayFieldName = opts.arrayFieldName || 'data';
try {
const res = cache.readQuery(opts);
let data:any = cache.readQuery(opts);
if (res && res[opts.arrayFieldName]) {
if (data && data[opts.arrayFieldName]) {
// Copy because immutable
data = { ...data };
const newArray = res[opts.arrayFieldName].reduce((result: any[], item: any) => {
return opts.ids.includes(item['id']) ?
// Remove it
result :
// Or keep it
result.concat(item);
const deletedIndexes = data[opts.arrayFieldName].reduce((res, item, index) => {
return opts.ids.includes(item['id']) ? res.concat(index) : res;
}, []);
const deleteCount = res[opts.arrayFieldName].length - newArray.length;
if (deleteCount <= 0) return; // Skip (nothing removed)
if (deletedIndexes.length <= 0) return; // Skip (nothing removed)
res[opts.arrayFieldName] = newArray;
// Query has NO total
if (isNil(opts.totalFieldName)) {
// Increment the total
if (isNotNil(opts.totalFieldName)) {
if (res[opts.totalFieldName]) {
res[opts.totalFieldName] -= deleteCount; // Remove deletion count
// Evict each object
deletedIndexes
.map(index => data[opts.arrayFieldName][index])
.map(dataIdFromObject)
.forEach(id => cache.evict({id}));
}
// Query has a total
else {
// Copy the array
data[opts.arrayFieldName] = data[opts.arrayFieldName].slice();
// remove from array, then evict
deletedIndexes
// Reverse: to keep valid index
.reverse()
// Remove from the array
.map(index => data[opts.arrayFieldName].splice(index, 1)[0])
// Evict from cache
.map(dataIdFromObject)
.forEach(id => cache.evict({id}));
if (isNotNil(data[opts.totalFieldName])) {
data[opts.totalFieldName] -= deletedIndexes.length; // Remove deletion count
}
else {
console.warn('[graphql] Unable to update cached query. Unknown result part: ' + opts.totalFieldName);
}
cache.writeQuery({
query: opts.query,
variables: opts.variables,
data
});
}
cache.writeQuery({
query: opts.query,
variables: opts.variables,
data: res
});
}
else {
console.warn('[graphql] Unable to update cached query. Unknown result part: ' + opts.arrayFieldName);
......@@ -513,39 +541,44 @@ export class GraphqlService {
opts:DataProxy.Query<V> & {
arrayFieldName: string;
totalFieldName?: string;
data: any,
data: T,