Commit 31064236 authored by LAVENIER's avatar LAVENIER
Browse files

Merge branch 'release/1.6.5'

parents fe4a9101 f07093fd
<?xml version='1.0' encoding='utf-8'?>
<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">
<widget android-versionCode="10605" id="net.sumaris.app" version="1.6.5" 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.4" #lastest
echo "1.6.5" #lastest
}
api_release_url() {
......
{
"name": "sumaris-app",
"description": "SUMARiS app",
"version": "1.6.4",
"version": "1.6.5",
"author": "contact@e-is.pro",
"license": "AGPL-3.0",
"readmeFilename": "README.md",
......
......@@ -37,11 +37,15 @@ import {TranslateLoader, TranslateModule} from "@ngx-translate/core";
import {SharedModule} from "./shared/shared.module";
import {HttpTranslateLoaderFactory} from "./shared/translate/http-translate-loader-factory";
import {MarkdownModule, MarkedOptions} from "ngx-markdown";
import {APP_LOCAL_STORAGE_TYPE_POLICIES} from "./core/services/storage/entities-storage.service";
import {
APP_LOCAL_STORAGE_TYPE_POLICIES,
EntitiesStorageTypePolicies
} from "./core/services/storage/entities-storage.service";
import {AppGestureConfig} from "./shared/gesture/gesture-config";
import {TypePolicies} from "@apollo/client/core";
import {APP_GRAPHQL_TYPE_POLICIES} from "./core/graphql/graphql.service";
import {SocialModule} from "./social/social.module";
import {TRIP_TESTING_PAGES} from "./trip/trip.testing.module";
@NgModule({
......@@ -161,7 +165,7 @@ import {SocialModule} from "./social/social.module";
// Settings
{title: '' /*empty divider*/, cssClass: 'flex-spacer'},
{title: 'MENU.TESTING', path: '/testing', icon: 'code', color: 'danger', ifProperty: 'sumaris.testing.enable', profile: 'ADMIN'},
{title: 'MENU.TESTING', path: '/testing', icon: 'code', color: 'danger', ifProperty: 'sumaris.testing.enable', profile: 'SUPERVISOR'},
{title: 'MENU.LOCAL_SETTINGS', path: '/settings', icon: 'settings', color: 'medium'},
{title: 'MENU.ABOUT', action: 'about', matIcon: 'help_outline', color: 'medium', cssClass: 'visible-mobile'},
......@@ -227,12 +231,14 @@ import {SocialModule} from "./social/social.module";
}
},
// Entities options
{ provide: APP_LOCAL_STORAGE_TYPE_POLICIES, useValue: { ...TRIP_STORAGE_TYPE_POLICIES}},
// Entities storage options
{ provide: APP_LOCAL_STORAGE_TYPE_POLICIES, useValue: <EntitiesStorageTypePolicies>{
...TRIP_STORAGE_TYPE_POLICIES
}},
// Test pages link
// Testing pages
{ provide: APP_TESTING_PAGES, useValue: <TestingPage[]>[
{label: 'Batch tree', page: '/testing/trip/batchTree'}
...TRIP_TESTING_PAGES
]},
],
bootstrap: [AppComponent],
......
......@@ -8,8 +8,8 @@ ion-menu {
--ion-item-text-color: var(--ion-color-primary-tint);
--ion-item-background-selected: var(--ion-color-secondary100);
--ion-item-text-color-selected: var(--ion-color-secondary100-contrast);
--ion-item-icon-color-selected: var(--ion-color-secondary100-contrast);
--ion-item-text-color-selected: var(--ion-color-primary);
--ion-item-icon-color-selected: var(--ion-color-primary);
--ion-item-text-color-disable: var(--ion-color-medium);
--ion-item-icon-color-disable: var(--ion-color-medium);
......
......@@ -228,7 +228,7 @@ export class NetworkService {
this._startPromise = undefined;
// Stop timer if cannot refresh anymore
if (this._timerRefreshCondition() == false) {
if (this._timerRefreshCondition() === false) {
this.stopRefreshTimer();
}
......@@ -660,8 +660,17 @@ export class NetworkService {
// Change to offline
if (this._deviceConnectionType === 'none') {
// Alert the user
this.showToast({message: 'NETWORK.INFO.OFFLINE'});
if (this._mobile) {
// Force offline mode
this._forceOffline = true;
// Alert the user
this.showToast({message: 'NETWORK.INFO.OFFLINE_HELP'});
}
else {
// Alert the user
this.showToast({message: 'NETWORK.INFO.OFFLINE'});
}
// Stop the network service
this.stop();
......
......@@ -184,10 +184,9 @@ export class SettingsPage extends AppForm<LocalSettings> implements OnInit, OnDe
// Remove all empty controls
this.propertiesFormHelper.removeAllEmpty();
/*if (this.form.invalid) {
if (this.form.invalid && this.debug) {
AppFormUtils.logFormErrors(this.form);
return;
}*/
}
console.debug("[settings] Saving local settings...");
......
......@@ -43,7 +43,7 @@
<ion-row>
<ion-col></ion-col>
<ion-col size="auto">
<ion-button fill="clear" (click)="cancel()">
<ion-button fill="clear" color="dark" (click)="cancel()">
<ion-label translate>COMMON.BTN_CANCEL</ion-label>
</ion-button>
......
......@@ -248,12 +248,12 @@
<span translate>REFERENTIAL.UPDATE_DATE</span>
</mat-header-cell>
<mat-cell *matCellDef="let row" class="mat-form-field-disabled">
<ion-text class="ion-text-end" color="dark" *ngIf="row.id!==-1">
<small [matTooltip]="'REFERENTIAL.CREATION_DATE'|translate">
<ion-icon name="calendar"></ion-icon> {{row.validator.controls.creationDate.value | dateFormat: {time: true} }}
<ion-text class="ion-text-end" color="medium" *ngIf="row.id!==-1">
<small [matTooltip]="'REFERENTIAL.CREATION_DATE'|translate" *ngIf="row.currentData.updateDate; let creationDate">
<ion-icon name="calendar"></ion-icon> {{ creationDate | dateFormat: {time: true} }}
</small><br/>
<small [matTooltip]="'REFERENTIAL.UPDATE_DATE'|translate">
<ion-icon name="time-outline"></ion-icon> {{row.validator.controls.updateDate.value | dateFormat: {time: true} }}
<small [matTooltip]="'REFERENTIAL.UPDATE_DATE'|translate" *ngIf="row.currentData.updateDate; let updateDate">
<ion-icon name="time-outline"></ion-icon> {{ updateDate | dateFormat: {time: true} }}
</small>
</ion-text>
</mat-cell>
......
......@@ -371,18 +371,18 @@ export class ReferentialRefService extends BaseEntityService
throw new Error('Not implemented yet');
}
async lastUpdateDate(): Promise<Moment> {
async lastUpdateDate(opts?: {fetchPolicy?: FetchPolicy}): Promise<Moment> {
try {
const res = await this.graphql.query<{lastUpdateDate: string}>({
const {lastUpdateDate} = await this.graphql.query<{lastUpdateDate: string}>({
query: LastUpdateDate,
variables: {},
fetchPolicy: "network-only"
fetchPolicy: opts && opts.fetchPolicy || 'network-only'
});
return res && fromDateISOString(res.lastUpdateDate);
return fromDateISOString(lastUpdateDate);
}
catch (err) {
console.error('[referential-ref] Cannot get pod lastUpdateDate: ' + (err && err.message || err), err);
console.error('[referential-ref] Cannot get remote lastUpdateDate: ' + (err && err.message || err), err);
return undefined;
}
}
......
......@@ -47,7 +47,7 @@
</ion-button>
<ion-button *ngIf="editing"
fill="clear" expand="block"
fill="clear" expand="block" color="dark"
class="visible-mobile visible-xs visible-sm"
(click)="cancel()">
<ion-label translate>COMMON.BTN_CANCEL</ion-label>
......
......@@ -111,7 +111,7 @@
[required]="required"
[tabindex]="tabindex"
(keyup.enter)="onKeyupEnter.emit($event)">
<mat-option *ngFor="let item of definition.values" [value]="item.key">{{ item.value | translate }}</mat-option>
<mat-option *ngFor="let item of definition.values" [value]="item.key || item">{{ (item.value || item) | translate }}</mat-option>
</mat-select>
<mat-error *ngIf="formControl.hasError('required')" translate>ERROR.FIELD_REQUIRED</mat-error>
......
......@@ -3,10 +3,13 @@ import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
forwardRef,
Input, OnDestroy,
Input,
OnDestroy,
OnInit,
Optional,
Output,
QueryList,
ViewChild,
ViewChildren
......@@ -35,8 +38,7 @@ import {InputElement, setTabIndex} from "../../inputs";
import {isFocusableElement} from "../../focusable";
import {BehaviorSubject, Subscription} from "rxjs";
import {MatDatepicker, MatDatepickerInputEvent} from "@angular/material/datepicker";
import {sleep, isNil, isNilOrBlank, toBoolean, toDateISOString} from "../../functions";
import {firstNotNilPromise} from "../../observables";
import {isNil, isNilOrBlank, isNotNil, sleep, toBoolean, toDateISOString} from "../../functions";
export const DEFAULT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
......@@ -77,15 +79,15 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
protected writing = true;
protected disabling = false;
protected _tabindex: number;
protected keyboardHideDelay: number;
protected waitHideKeyboardDelay: number;
form: FormGroup;
displayPattern: string;
dayPattern: string;
_value: Moment;
locale: string;
dayMask = DAY_MASK;
hourMask = HOUR_MASK;
readonly dayMask = DAY_MASK;
readonly hourMask = HOUR_MASK;
@Input() mobile: boolean;
......@@ -124,6 +126,10 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
@Input() clearable = false;
// For DEBUG ---
@Input() debug = false;
@Output() onLogDebug = new EventEmitter<string>();
@ViewChild('datePicker1') datePicker1: MatDatepicker<Moment>;
@ViewChild('datePicker2') datePicker2: MatDatepicker<Moment>;
@ViewChild('timePicker') timePicker: NgxTimePicker;
......@@ -135,7 +141,7 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
}
constructor(
platform: Platform,
private platform: Platform,
private dateAdapter: DateAdapter<Moment>,
private translate: TranslateService,
private formBuilder: FormBuilder,
......@@ -143,15 +149,14 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
private cd: ChangeDetectorRef,
@Optional() private formGroupDir: FormGroupDirective,
) {
// Workaround because ion-datetime has issue (do not returned a ISO date)
this.mobile = platform.is('mobile');
this.keyboardHideDelay = this.mobile && KEYBOARD_HIDE_DELAY_MS || 0;
this.locale = (translate.currentLang || translate.defaultLang).substr(0, 2);
}
ngOnInit() {
this.mobile = isNil(this.mobile) ? this.platform.is('mobile') : this.mobile;
this.waitHideKeyboardDelay = this.mobile && KEYBOARD_HIDE_DELAY_MS || 0;
this.formControl = this.formControl || this.formControlName && this.formGroupDir && this.formGroupDir.form.get(this.formControlName) as FormControl;
if (!this.formControl) throw new Error("Missing mandatory attribute 'formControl' or 'formControlName' in <mat-date-time-field>.");
......@@ -160,15 +165,13 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
const dayValidator: ValidatorFn = (_) => $error.getValue();
this.required = toBoolean(this.required, this.formControl.validator === Validators.required);
this.form = this.formBuilder.group({});
this.form.addControl('day', this.formBuilder.control(null, dayValidator));
if (this.displayTime) {
this.form = this.formBuilder.group({
day: [dayValidator],
hour: ['', this.required ? Validators.compose([Validators.required, Validators.pattern(HOUR_TIME_PATTERN)]) : Validators.pattern(HOUR_TIME_PATTERN)]
});
} else {
this.form = this.formBuilder.group({
day: [dayValidator]
});
const hourValidator = this.required ?
(this.mobile ? Validators.required : Validators.compose([Validators.required, Validators.pattern(HOUR_TIME_PATTERN)])) :
(this.mobile ? null : Validators.pattern(HOUR_TIME_PATTERN));
this.form.addControl('hour', this.formBuilder.control(null, hourValidator));
}
// Add custom 'validDate' validator
......@@ -210,7 +213,8 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
writeValue(obj: any): void {
if (this.writing) return;
if (this.writing) return; // Skip
if (this.debug) this.logDebug("writeValue(obj) with obj:", obj);
if (isNilOrBlank(obj)) {
this.writing = true;
......@@ -229,8 +233,11 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
return;
}
if (this.debug) this.logDebug("writeValue(obj) parsing...");
this._value = this.dateAdapter.parse(obj, DATE_ISO_PATTERN);
if (!this._value) { // invalid date
if (this.debug) this.logDebug("writeValue(obj) parse result: invalid !");
return;
}
......@@ -273,7 +280,7 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
}
setDisabledState(isDisabled: boolean): void {
if (this.disabling) return;
if (this.disabling) return; // Skip
this.disabling = true;
this.disabled = isDisabled;
......@@ -354,6 +361,8 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
}
onDatePickerChange(event: MatDatepickerInputEvent<Moment>): void {
if (this.debug) this.logDebug("onDatePickerChange");
if (this.writing || !(event && event.value)) return; // Skip if call by self
this.writing = true;
......@@ -403,7 +412,7 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
this.preventEvent(event);
// Make sure the keyboard is closed
await this.waitKeyboardHide(false);
await this.hideKeyboard(false);
// Open the picker
this.openDatePicker(null, datePicker);
......@@ -424,30 +433,44 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
async openTimePickerIfMobile(event: UIEvent) {
if (!this.mobile || event.defaultPrevented) return;
this.preventEvent(event);
//this.preventEvent(event);
// Make sure the keyboard is closed
await this.waitKeyboardHide(true);
await this.hideKeyboard(true);
// Open the picker
this.openTimePicker(null);
}
openTimePicker(event: UIEvent) {
if (!this.timePicker) return; // Skip
if (this.timePicker) {
if (this.debug) this.logDebug("openTimePicker() event:", event);
if (event) this.preventEvent(event);
this.preventEvent(event);
this.timePicker.open();
}
this.timePicker.open();
}
onTimePickerChange(value: string) {
if (this.form.controls['hour'].value !== value) {
this.form.controls['hour'].patchValue(value, {emitEvent: false});
if (this.form.controls.hour.value !== value) {
this.form.controls.hour.patchValue(value, {emitEvent: false});
if (this.debug) {
this.logDebug("onTimePickerChange() new value:", value);
if (this.form.controls.hour.invalid) {
this.logDebug("hour errors:", this.form.controls.hour.errors);
}
}
this.markForCheck();
}
else {
if (this.debug) {
this.logDebug("onTimePickerChange(): no changes", this.form.controls.hour.value);
if (this.form.controls.hour.invalid) {
this.logDebug("hour errors:", this.form.controls.hour.errors);
}
}
}
}
onTimePickerKeyup(event: KeyboardEvent) {
......@@ -493,7 +516,7 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
/* -- protected method -- */
protected async waitKeyboardHide(waitKeyboardDelay: boolean) {
protected async hideKeyboard(waitIsHidden: boolean) {
if (!this.keyboard.isVisible) return; // ok, already hidden
......@@ -504,8 +527,8 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
await this.keyboard.onKeyboardHide().pipe(first()).toPromise();
// Wait an additional delay if need (depending on the OS)
if (this.keyboardHideDelay > 0 && waitKeyboardDelay) {
await sleep(this.keyboardHideDelay);
if (this.waitHideKeyboardDelay > 0 && waitIsHidden) {
await sleep(this.waitHideKeyboardDelay);
}
}
......@@ -524,5 +547,12 @@ export class MatDateTime implements OnInit, OnDestroy, ControlValueAccessor, Inp
protected markForCheck() {
this.cd.markForCheck();
}
protected logDebug(message: string, obj?: any) {
if (!this.debug) return; // Silent
console.debug("[mat-date-time-field] " + message, obj);
this.onLogDebug.emit("[mat-date-time-field] " + message + (obj !== undefined ? (' ' + JSON.stringify(obj)) : ''));
}
}
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Date/Time field test page</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<form class="form-container" [formGroup]="form" (ngSubmit)="doSubmit($event)">
<ion-grid>
<!-- Mobile mode -->
<ion-row><ion-col><ion-text><h4>Mobile mode</h4></ion-text></ion-col></ion-row>
<ion-row>
<ion-col>
<!-- Empty value -->
<ion-card>
<ion-card-header>
<ion-card-title>
<ion-label color="primary">
Empty value
</ion-label>
</ion-card-title>
<ion-card-subtitle>
<ion-text color="medium">
<small><pre>{{stringify(form.controls.empty.value)}}</pre></small>
</ion-text>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<mat-date-time-field formControlName="empty"
placeholder="Date/Time"
[required]="true"
[mobile]="true"
[clearable]="true"
[debug]="true"
(onLogDebug)="logDebug($event)">
<ion-icon matPrefix name="calendar-outline"></ion-icon>
</mat-date-time-field>
</ion-card-content>
</ion-card>
</ion-col>
<!-- Enable -->
<ion-col>
<ion-card>
<ion-card-header>
<ion-card-title>
<ion-label color="primary">
With value
</ion-label>
</ion-card-title>
<ion-card-subtitle>
<ion-text color="medium">
<small><pre>{{stringify(form.controls.enable.value)}}</pre></small>
</ion-text>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<mat-date-time-field formControlName="enable"
placeholder="Date/Time"
[required]="true"
[mobile]="true"
[clearable]="true"
(onLogDebug)="logDebug($event)">
<ion-icon matPrefix name="calendar-outline"></ion-icon>
</mat-date-time-field>
</ion-card-content>
</ion-card>
</ion-col>
<!-- Disable -->
<ion-col>
<ion-card>
<ion-card-header>
<ion-card-title>
<ion-label color="primary">
Disable
</ion-label>
</ion-card-title>
<ion-card-subtitle>
<ion-text color="medium">
<small><pre>{{stringify(form.controls.disable.value)}}</pre></small>
</ion-text>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<mat-date-time-field formControlName="disable"
placeholder="Date/Time"
[required]="true"
[mobile]="true">
<ion-icon matPrefix name="calendar-outline"></ion-icon>
</mat-date-time-field>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
<!-- debug console -->
<ion-row>
<ion-col>
<ion-text color="primary">Debug console:<br/></ion-text>
<div class="ion-padding-start">
<ion-text color="medium">
<small [innerHTML]="debugPanelContent"></small>
</ion-text>
</div>
</ion-col>
</ion-row>
<!-- Desktop mode -->
<ion-row><ion-col><ion-text><h4>Desktop mode</h4></ion-text></ion-col></ion-row>
<ion-row>
<ion-col>
<!-- Empty value -->
<ion-card>
<ion-card-header>
<ion-card-title>
<ion-label color="primary">
Empty value
</ion-label>
</ion-card-title>
<ion-card-subtitle>
<ion-text color="medium">
<small><pre>{{stringify(form.controls.empty.value)}}</pre></small>
</ion-text>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<mat-date-time-field formControlName="empty"
placeholder="Date/Time"
[required]="true"
[mobile]="false"
[clearable]="true">
<ion-icon matPrefix name="calendar-outline"></ion-icon>
</mat-date-time-field>
</ion-card-content>
</ion-card>
</ion-col>
<!-- Enable -->
<ion-col>
<ion-card>
<ion-card-header>
<ion-card-title>
<ion-label color="primary">
With value
</ion-label>
</ion-card-title>