Commit 6cbeca34 authored by LAVENIER's avatar LAVENIER
Browse files

Merge branch 'release/1.3.4'

parents e7253725 ab295046
<?xml version='1.0' encoding='utf-8'?>
<widget android-versionCode="10303" id="net.sumaris.app" version="1.3.3" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<widget android-versionCode="10304" id="net.sumaris.app" version="1.3.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 "v1.3.3" #lastest
echo "v1.3.4" #lastest
}
api_release_url() {
......
{
"name": "sumaris-app",
"description": "SUMARiS app",
"version": "1.3.3",
"version": "1.3.4",
"author": "contact@e-is.pro",
"license": "AGPL-3.0",
"readmeFilename": "README.md",
......
......@@ -13,7 +13,6 @@ import {AuctionControlPage} from "./trip/auctioncontrol/auction-control.page";
import {IonicRouteStrategy} from "@ionic/angular";
import {AuthGuardService} from "./core/services/auth-guard.service";
import {LandedTripPage} from "./trip/landedtrip/landed-trip.page";
import {environment} from "../environments/environment";
const routeOptions: ExtraOptions = {
enableTracing: false,
......@@ -204,40 +203,37 @@ const routes: Routes = [
path: 'extraction',
canActivate: [AuthGuardService],
loadChildren: () => import('./trip/extraction/extraction.module').then(m => m.ExtractionModule)
}
];
},
// Add test pages (DEV only)
if (!environment.production) {
routes.push(
{
path: 'testing',
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'shared',
},
// Shared module
{
path: 'shared',
loadChildren: () => import('./shared/shared.testing.module').then(m => m.SharedTestingModule)
},
// Trip module
{
path: 'trip',
loadChildren: () => import('./trip/trip.testing.module').then(m => m.TripTestingModule)
}
]
});
// Test module (disable in menu, by default - can be enable by the Pod configuration page)
{
path: 'testing',
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'shared',
},
// Shared module
{
path: 'shared',
loadChildren: () => import('./shared/shared.testing.module').then(m => m.SharedTestingModule)
},
// Trip module
{
path: 'trip',
loadChildren: () => import('./trip/trip.testing.module').then(m => m.TripTestingModule)
}
]
},
}
// Other route redirection (should at the end of the array)
{
path: "**",
redirectTo: '/'
}
];
// Other route redirection (should at the end of the array)
routes.push({
path: "**",
redirectTo: '/'
});
@Injectable()
export class CustomReuseStrategy extends IonicRouteStrategy {
......
......@@ -128,7 +128,7 @@ import {APP_TESTING_PAGES, TestingPage} from "./shared/material/testing/material
// Settings
{title: '' /*empty divider*/, cssClass: 'flex-spacer'},
{title: 'MENU.TESTING', path: '/testing', icon: 'code', color: 'danger', ifProperty: 'sumaris.testing.enable',},
{title: 'MENU.TESTING', path: '/testing', icon: 'code', color: 'danger', ifProperty: 'sumaris.testing.enable', profile: 'ADMIN'},
{title: 'MENU.LOCAL_SETTINGS', path: '/settings', icon: 'settings', color: 'medium'},
{title: 'MENU.ABOUT', action: 'about', matIcon: 'help_outline', color: 'medium', cssClass: 'visible-mobile'},
......
......@@ -129,21 +129,22 @@ export abstract class AppTabEditor<T = any, O = any> implements IAppForm, OnInit
this.queryTabIndexParamName = this.queryTabIndexParamName || 'tab';
// Read the selected tab index, from path query params
this.registerSubscription(this.route.queryParams
.subscribe(queryParams => {
this.queryParams = {...queryParams};
// Parse tab param
if (this.tabCount > 1 && this.queryTabIndexParamName) {
const tabIndex = queryParams[this.queryTabIndexParamName];
this.queryParams[this.queryTabIndexParamName] = tabIndex && parseInt(tabIndex) || undefined;
if (isNotNil(this.queryParams[this.queryTabIndexParamName])) {
this.selectedTabIndex = this.queryParams[this.queryTabIndexParamName];
if (this.tabGroup) {
this.registerSubscription(this.route.queryParams
.subscribe(queryParams => {
this.queryParams = {...queryParams};
// Parse tab param
if (this.tabCount > 1 && this.queryTabIndexParamName) {
const tabIndex = queryParams[this.queryTabIndexParamName];
this.queryParams[this.queryTabIndexParamName] = tabIndex && parseInt(tabIndex) || undefined;
if (isNotNil(this.queryParams[this.queryTabIndexParamName])) {
this.selectedTabIndex = this.queryParams[this.queryTabIndexParamName];
}
}
}
this.tabGroup.realignInkBar();
}));
this.tabGroup.realignInkBar();
}));
}
// Catch back click events
if (this.appToolbar) {
......
......@@ -282,7 +282,8 @@ export class HomePage implements OnDestroy {
const filteredButtons = (this.buttons || [])
.filter((item) => MenuItems.checkIfVisible(item, this.accountService, this._config, {
isLogin: this.isLogin,
debug: this._debug
debug: this._debug,
logPrefix: "[home]"
}))
.map(item => {
// Replace title using properties
......
......@@ -2,7 +2,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef, HostListener,
HostListener,
Inject,
InjectionToken,
Input,
......@@ -22,12 +22,11 @@ import {AboutModal} from '../about/modal-about';
import {environment} from '../../../environments/environment';
import {fadeInAnimation} from '../../shared/material/material.animations';
import {TranslateService} from "@ngx-translate/core";
import {isNil, isNotNilOrBlank} from "../../shared/functions";
import {isNotNilOrBlank} from "../../shared/functions";
import {BehaviorSubject, merge, Subscription} from "rxjs";
import {ConfigService} from "../services/config.service";
import {mergeMap, tap, throttleTime} from "rxjs/operators";
import {mergeMap, tap} from "rxjs/operators";
import {HammerSwipeEvent} from "../../shared/gesture/hammer.utils";
import {DOCUMENT} from "@angular/common";
export interface MenuItem {
title: string;
......@@ -51,14 +50,15 @@ export class MenuItems {
accountService: AccountService,
config: Configuration,
opts?: {
isLogin?: boolean;
debug?: boolean
isLogin?: boolean;
debug?: boolean;
logPrefix?: string;
}): boolean {
opts = opts || {};
if (item.profile) {
const hasProfile = accountService.isLogin() && accountService.hasMinProfile(item.profile);
if (!hasProfile) {
if (opts.debug) console.debug("[menu] User does not have minimal profile '" + item.profile + "' for ", item.path);
if (opts.debug) console.debug(`${opts && opts.logPrefix || '[menu]'} Hide item '${item.title}': need the min profile '${item.profile}' to access path '${item.path}'`);
return false;
}
}
......@@ -73,6 +73,7 @@ export class MenuItems {
// If enable by config
if (item.ifProperty) {
console.log("TODO if property " + item.ifProperty, config && config.properties);
const isEnableByConfig = config && config.properties[item.ifProperty] === 'true';
if (!isEnableByConfig) {
if (opts.debug) console.debug("[menu] Config property '" + item.ifProperty + "' not 'true' for ", item.path);
......@@ -148,8 +149,10 @@ export class MenuComponent implements OnInit {
this.splitPane.when = SPLIT_PANE_SHOW_WHEN;
this.splitPaneOpened = true;
// Update component when refresh is need (=login events or config changed)
// Wait account first start
await this.accountService.ready();
// Update component when refresh is need (=login events or config changed)
this._subscription.add(
merge(
this.accountService.onLogin,
......@@ -160,8 +163,9 @@ export class MenuComponent implements OnInit {
)
)
.pipe(
// Wait account service ready (can be restarted)
mergeMap(() => this.accountService.ready()),
throttleTime(200)
//debounceTime(200)
)
.subscribe(account => {
if (this.accountService.isLogin()) {
......
......@@ -32,15 +32,15 @@ export const ConfigOptions: FormFieldDefinitionMap = Object.freeze({
values: [
{
key: 'DDMMSS',
value: 'COMMON.DDMMSS_PLACEHOLDER'
value: 'COMMON.LAT_LONG.DDMMSS_PLACEHOLDER'
},
{
key: 'DDMM',
value: 'COMMON.DDMM_PLACEHOLDER'
value: 'COMMON.LAT_LONG.DDMM_PLACEHOLDER'
},
{
key: 'DD',
value: 'COMMON.DD_PLACEHOLDER'
value: 'COMMON.LAT_LONG.DD_PLACEHOLDER'
}
]
},
......
......@@ -334,7 +334,7 @@ export class LocalSettingsService {
}
}
async removeHistory(path: string) {
async removeHistory(path: string, opts?: {emitEvent?: boolean;}) {
const index = this.data.pageHistory.findIndex(p => p.path === path);
if (index === -1) return; // skip if not found
......@@ -342,6 +342,11 @@ export class LocalSettingsService {
// Save locally
this.persistLocally();
// Emit event
if (!opts || opts.emitEvent !== false) {
this.onChange.next(this.data);
}
}
async clearPageHistory() {
......
......@@ -197,6 +197,9 @@ export class SettingsPage extends AppForm<LocalSettings> implements OnInit, OnDe
// Check peer alive, before saving
const peerChanged = this.form.get('peerUrl').dirty;
// Clean page history, when peer changed
if (peerChanged) data.pageHistory = [];
try {
this.disable();
......
......@@ -5,7 +5,7 @@
<!-- back button -->
<ion-button class="back-button"
[class.can-go-back]="canGoBack"
*ngIf="canGoBack"
(click)="doBackClick($event)"
routerDirection="back" >
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
......
// Default behavior: hide back button
ion-button.back-button:not(.can-go-back) {
display: none;
}
/*
ion-button.back-button.can-go-back {
display: inline-block !important;
}*/
......@@ -94,30 +94,30 @@ export abstract class ExtractionAbstractPage<T extends ExtractionType | Aggregat
.pipe(
// Convert query params into a valid type
mergeMap(async ({category, label, sheet}) => {
const paramType = this.fromObject({category, label});
// Read type
const types = await firstNotNilPromise(this.$types);
let selectedType;
// If not type found in params, redirect to first one
if (isNil(paramType.category) || isNil(paramType.label)) {
selectedType = types && types[0];
}
// Select the exact type object in the filter form
else {
selectedType = types.find(t => this.isEquals(t, paramType)) || paramType;
}
const selectedSheetName = sheet || (selectedType && selectedType.sheetNames && selectedType.sheetNames.length && selectedType.sheetNames[0]);
if (selectedSheetName && selectedType && !selectedType.sheetNames) {
selectedType.sheetNames = [selectedSheetName];
}
return {selectedType, selectedSheetName};
}))
const paramType = this.fromObject({category, label});
// Read type
const types = await firstNotNilPromise(this.$types);
let selectedType;
// If not type found in params, redirect to first one
if (isNil(paramType.category) || isNil(paramType.label)) {
selectedType = types && types[0];
}
// Select the exact type object in the filter form
else {
selectedType = types.find(t => this.isEquals(t, paramType)) || paramType;
}
const selectedSheetName = sheet || (selectedType && selectedType.sheetNames && selectedType.sheetNames.length && selectedType.sheetNames[0]);
if (selectedSheetName && selectedType && !selectedType.sheetNames) {
selectedType.sheetNames = [selectedSheetName];
}
return {selectedType, selectedSheetName};
})
)
.subscribe(async ({selectedType, selectedSheetName}) => {
// Set the type
await this.setType(selectedType, {
......@@ -127,7 +127,7 @@ export abstract class ExtractionAbstractPage<T extends ExtractionType | Aggregat
});
// Execute the first load
await this.load();
await this.loadData();
}));
}
......
......@@ -100,7 +100,7 @@ export class ExtractionDataPage extends ExtractionAbstractPage<ExtractionType> i
)
.subscribe(() => {
if (this.loading || isNil(this.type)) return; // avoid multiple load
return this.load();
return this.loadData();
});
}
......
......@@ -215,7 +215,7 @@ export class ExtractionMapPage extends ExtractionAbstractPage<AggregationType> i
switchMap(() => {
if (!this.ready || this.loading || isNil(this.type)) return; // avoid multiple load
console.debug('[extraction-map] Refreshing...');
return this.load();
return this.loadData();
})
).subscribe(() => this.markAsPristine())
);
......@@ -349,7 +349,7 @@ export class ExtractionMapPage extends ExtractionAbstractPage<AggregationType> i
strata
});
await this.load();
await this.loadData();
hasData = this.hasData;
}
......
......@@ -113,6 +113,7 @@
[placeholder]="'TRIP.TABLE.FILTER.RECORDER_PERSON'|translate"
formControlName="recorderPerson"
[config]="autocompleteFields.person"
[i18nPrefix]="'USER.'"
[clearable]="true">
<ion-icon matPrefix name="person"></ion-icon>
</mat-autocomplete-field>
......
......@@ -201,6 +201,7 @@
[placeholder]="'TRIP.TABLE.FILTER.RECORDER_PERSON'|translate"
formControlName="recorderPerson"
[config]="autocompleteFields.person"
[i18nPrefix]="'USER.'"
[clearable]="true">
<ion-icon matPrefix name="person"></ion-icon>
</mat-autocomplete-field>
......
......@@ -393,6 +393,12 @@ export class TripTable extends AppTable<Trip, TripFilter> implements OnInit, OnD
this.showToast({
message: 'INFO.SYNCHRONIZATION_SUCCEED'
});
// Clean history
// FIXME: find a way o clean only synchronized trips ?
this.settings.clearPageHistory();
} catch (error) {
this.userEventService.showToastErrorWithContext({
error,
......
......@@ -27,6 +27,7 @@
"BTN_REFRESH": "Refresh",
"BTN_SEARCH": "Search",
"BTN_NEXT": "Next",
"BTN_NEXT_SHORT": "Next",
"BTN_BACK": "Back",
"BTN_SEND": "Send",
"BTN_OPTIONS": "Options",
......@@ -38,10 +39,11 @@
"BTN_SHOW_MENU": "Show menu",
"BTN_SHOW_MORE": "Show more",
"BTN_LOGOUT": "Log out",
"BTN_INIT_OFFLINE_MODE": "Enable offline mode",
"CREATED_ON": "Created on ",
"COMMENTS": "Comments",
"BTN_DECIMAL_SEPARATOR": ".",
"EMPTY_OPTION": "(Empty)",
"All_OPTION": "(All)",
"DATE_PATTERN": "MM/DD/YYYY",
"DATE_TIME_PATTERN": "MM/DD/YYYY HH:mm",
......@@ -106,6 +108,7 @@
"CONFIGURATION": {
"TAB_GENERAL": "Details",
"TAB_CACHE": "Server cache",
"TAB_NOTIFICATIONS": "Notifications",
"APP_NAME": "Application Name",
"DESCRIPTION": "Description (home page)",
"STATUS": "Status",
......@@ -117,7 +120,7 @@
"PROPERTY_VALUE": "Value",
"BTN_ADD_PROPERTY": "Add an option",
"BTN_CLEAR_CACHE": "Clear server cache",
"CACHE_DIVIDER": "Server cache statistics",
"CACHE_TITLE": "Server cache statistics",
"NEW": {
"TITLE": "New configuration"
},
......@@ -130,6 +133,7 @@
"FAVICON": "favicon",
"DEFAULT_LOCALE": "Default Language",
"DEFAULT_LATLONG_FORMAT": "Default lat/lon format",
"NOT_SELF_DATA_ACCESS_MIN_ROLE": "User profile (min) to access other people data",
"HOME": {
"LOGO_LARGE": "Home > Logo (max width 400px)",
"PARTNER_DEPARTMENTS": "Home > Partners logos",
......@@ -181,7 +185,7 @@
"MAP": "Maps",
"FOOTER_VERSION_ABOUT": "v{{version}} | About",
"LOCAL_SETTINGS": "Settings",
"SERVER_SETTINGS": "Server settings",
"SERVER": "Server management",
"LOGOUT": "Logout",
"ABOUT": "About"
},
......@@ -324,10 +328,12 @@
"STATUS": "Status",
"PUBKEY": "Public key",
"PROFILE": "Profile",
"PROFILE_ADMIN": "Administrator",
"PROFILE_SUPERVISOR": "Manager",
"PROFILE_USER": "Observer",
"PROFILE_GUEST": "Invité",
"PROFILE_ENUM": {
"ADMIN": "Administrator",
"SUPERVISOR": "Manager",
"USER": "Observer",
"GUEST": "Invité"
},
"LIST": {
"TITLE": "Users",
"FILTER": {
......@@ -391,11 +397,11 @@
"LOCATION": "Port (departure or return)",
"VESSEL": "Vessel",
"SYNCHRONIZATION_STATUS": "Status",
"RECORDER_DEPARTMENT": "Recorder depatrment",
"RECORDER_DEPARTMENT": "Recorder department",
"RECORDER_PERSON": "Recorder user"
},
"SYNCHRONIZATION_STATUS": {
"DIRTY_HELP": "Unsynchronized trips",
"DIRTY_HELP": "Not-synchronized trips",
"READY_TO_SYNC_HELP": "Ready to sync trips",
"SYNC_HELP": "Online trips"
}
......@@ -441,11 +447,17 @@
"TITLE": "Existing gears"
},
"ERROR": {
"LOAD_PHYSICAL_GEARS_ERROR": "Erreur de chargement des engins."
"LOAD_PHYSICAL_GEARS_ERROR": "Error while loading gears."
}
},
"OPERATION": {
"BTN_SHOW_ALL": "Show all",
"BTN_HISTORY": "Log",
"BTN_SHOW_MAP": "Positions map",
"COMMENTS": "Comments",
"LAST_OPERATIONS": "Last operations",
"NO_LAST_OPERATION": "No previous operation",
"OPE_PREFIX": "OPE",
"TITLE_PREFIX": "<small>{{vessel}} - Trip {{departureDateTime}}<br></small>",
"NEW": {
"TITLE": "New operation"
......@@ -456,6 +468,7 @@
"TAB_GENERAL": "Details",
"TAB_PHYSICAL_GEAR": "Use of gear",
"TAB_CATCH": "Catch",
"TAB_CATCH_TOTAL": "Total catch",
"TAB_SURVIVAL_TEST": "Survival tests",
"TAB_INDIVIDUAL_MONITORING": "Individual monitoring",
"TAB_INDIVIDUAL_RELEASES": "Releases",
......@@ -469,6 +482,11 @@
"OTHER_FEATURES": "Other features:",
"NO_TARGET_SPECIES_FOUND": "No species for this gear"
},
"MAP": {
"TITLE": "Positions map",
"TRIP_LAYER": "Trip",
"OPERATIONS_LAYER": "Operations"
},
"LIST": {
"PHYSICAL_GEAR": "Used gear",
"METIER": "Metier",
......@@ -528,7 +546,7 @@
"TITLE": "Batch {{label}}",
"TAXON_GROUP": "Commercial species (FAO)",
"TAXON_NAME": "Scientific species",
"PARENT": "Species batch",
"PARENT_GROUP": "Species batch",
"BTN_AUTO_INCREMENT": "Auto increment",
"BTN_INDIVIDUAL_MEASURE": "Fill measurements",
"ESTIMATED_WEIGHT": "Estimated weight",
......@@ -567,7 +585,7 @@
"SAMPLING_RATIO": "Sampling fraction",
"SAMPLING_INDIVIDUAL_COUNT": "Sampling nb indiv.",
"SAMPLING_WEIGHT": "Sampling weight",
"PARENT": "Species batch",
"PARENT_GROUP": "Species batch",
"INDIVIDUAL_COUNT": "Nb indiv.",
"INDIVIDUAL_UNIT": "indiv.",
"WEIGHT": "Weight (kg)",
......@@ -590,7 +608,8 @@
},
"EDIT": {
"TITLE": "Batch {{label}}",
"TAXON_GROUP": "Commercial species (FAO)"
"TAXON_GROUP": "Commercial species (FAO)",
"TAXON_NAME": "Scientific species (FAO)"
},
"TABLE": {
"LABEL": "Batch",
......@@ -922,8 +941,8 @@
"NAME": "Scientific species name"
},
"GEAR": {
"LABEL": "Code engin",
"NAME": "Libellé engin"
"LABEL": "Gear code",
"NAME": "Gear label"
},
"PARAMETER": {
"TAB_GENERAL": "Details",
......@@ -993,10 +1012,10 @@
"PMFM": "Collected parameters",
"MATRIX": "Matrices",
"FRACTION": "Fractions",
"QUALITATIVE_VALUE": "Qualitatives values",
"QUALITATIVE_VALUE": "Qualitative values",
"TAXON_GROUP_TYPE": "Species group types",
"TAXON_GROUP": "Species group",
"TAXON_NAME": "Scientifique species",
"TAXON_NAME": "Scientific species",
"TAXONOMIC_LEVEL": "Taxonomic levels",
"REFERENCE_TAXON": "Scientific species identifier",
"PROGRAM": "Data collection programmes",
......@@ -1024,7 +1043,10 @@