Merge remote-tracking branch 'origin/master' into cire16

This commit is contained in:
2022-12-22 10:22:58 +01:00
801 changed files with 39243 additions and 6591 deletions

View File

@@ -80,7 +80,9 @@ header {
margin: 0;
padding: 0;
border-radius: 0;
z-index: 1500;
a.dropdown-item {
padding: 0.5rem 1rem;
width: 120%;
border: 0;
border-bottom: 1px solid $gray-200;
@@ -517,3 +519,16 @@ div.popover {
div.v-toast {
z-index: 10000!important;
}
// export index page
div.exports-list {
div.flex-bloc .item-bloc {
flex-basis: 33%;
@include media-breakpoint-down(lg) { flex-basis: 50%; }
@include media-breakpoint-down(sm) { flex-basis: 100%; }
div:last-child,
p:last-child {
margin-top: auto;
}
}
}

View File

@@ -24,7 +24,7 @@ require('./chillmain.scss');
import { chill } from './js/chill.js';
global.chill = chill;
require('./js/date.js');
require('./js/date');
require('./js/counter.js');
/// Load fonts

View File

@@ -12,7 +12,7 @@
* Do not take time into account
*
*/
const dateToISO = (date) => {
export const dateToISO = (date: Date|null): string|null => {
if (null === date) {
return null;
}
@@ -29,7 +29,7 @@ const dateToISO = (date) => {
*
* **Experimental**
*/
const ISOToDate = (str) => {
export const ISOToDate = (str: string|null): Date|null => {
if (null === str) {
return null;
}
@@ -38,25 +38,25 @@ const ISOToDate = (str) => {
}
let
[year, month, day] = str.split('-');
[year, month, day] = str.split('-').map(p => parseInt(p));
return new Date(year, month-1, day);
return new Date(year, month-1, day, 0, 0, 0, 0);
}
/**
* Return a date object from iso string formatted as YYYY-mm-dd:HH:MM:ss+01:00
*
*/
const ISOToDatetime = (str) => {
export const ISOToDatetime = (str: string|null): Date|null => {
if (null === str) {
return null;
}
let
[cal, times] = str.split('T'),
[year, month, date] = cal.split('-'),
[year, month, date] = cal.split('-').map(s => parseInt(s)),
[time, timezone] = times.split(times.charAt(8)),
[hours, minutes, seconds] = time.split(':')
[hours, minutes, seconds] = time.split(':').map(s => parseInt(s));
;
return new Date(year, month-1, date, hours, minutes, seconds);
@@ -66,7 +66,7 @@ const ISOToDatetime = (str) => {
* Convert a date to ISO8601, valid for usage in api
*
*/
const datetimeToISO = (date) => {
export const datetimeToISO = (date: Date): string => {
let cal, time, offset;
cal = [
date.getFullYear(),
@@ -92,7 +92,7 @@ const datetimeToISO = (date) => {
return x;
};
const intervalDaysToISO = (days) => {
export const intervalDaysToISO = (days: number|string|null): string => {
if (null === days) {
return 'P0D';
}
@@ -100,7 +100,7 @@ const intervalDaysToISO = (days) => {
return `P${days}D`;
}
const intervalISOToDays = (str) => {
export const intervalISOToDays = (str: string|null): number|null => {
if (null === str) {
return null
}
@@ -154,12 +154,3 @@ const intervalISOToDays = (str) => {
return days;
}
export {
dateToISO,
ISOToDate,
ISOToDatetime,
datetimeToISO,
intervalISOToDays,
intervalDaysToISO,
};

View File

@@ -5,6 +5,10 @@ ul.record_actions {
justify-content: flex-end;
padding: 0.5em 0;
&.inline {
display: inline-block;
}
&.column {
flex-direction: column;
}
@@ -18,6 +22,13 @@ ul.record_actions {
padding-right: 1em;
}
&.small {
.btn {
padding: .25rem .5rem;
font-size: .75rem;
}
}
li {
display: inline-block;
list-style-type: none;

View File

@@ -0,0 +1,4 @@
export function fetchResults<T>(uri: string, params: {item_per_page?: number}): Promise<T[]>;
export function makeFetch<T, B>(method: "GET"|"POST"|"PATCH"|"DELETE", url: string, body: B, options: {[key: string]: string}): Promise<T>;

View File

@@ -1,110 +0,0 @@
/**
* Generic api method that can be adapted to any fetch request
*/
const makeFetch = (method, url, body, options) => {
let opts = {
method: method,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: (body !== null) ? JSON.stringify(body) : null
};
if (typeof options !== 'undefined') {
opts = Object.assign(opts, options);
}
return fetch(url, opts)
.then(response => {
if (response.ok) {
return response.json();
}
if (response.status === 422) {
return response.json().then(response => {
throw ValidationException(response)
});
}
if (response.status === 403) {
throw AccessException(response);
}
throw {
name: 'Exception',
sta: response.status,
txt: response.statusText,
err: new Error(),
violations: response.body
};
});
}
/**
* Fetch results with certain parameters
*/
const _fetchAction = (page, uri, params) => {
const item_per_page = 50;
if (params === undefined) {
params = {};
}
let url = uri + '?' + new URLSearchParams({ item_per_page, page, ...params });
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
}).then(response => {
if (response.ok) { return response.json(); }
throw Error({ m: response.statusText });
});
};
const fetchResults = async (uri, params) => {
let promises = [],
page = 1;
let firstData = await _fetchAction(page, uri, params);
promises.push(Promise.resolve(firstData.results));
if (firstData.pagination.more) {
do {
page = ++page;
promises.push(_fetchAction(page, uri, params).then(r => Promise.resolve(r.results)));
} while (page * firstData.pagination.items_per_page < firstData.count)
}
return Promise.all(promises).then(values => values.flat());
};
const fetchScopes = () => {
return fetchResults('/api/1.0/main/scope.json');
};
/**
* Error objects to be thrown
*/
const ValidationException = (response) => {
const error = {};
error.name = 'ValidationException';
error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
error.titles = response.violations.map((violation) => violation.title);
error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
return error;
}
const AccessException = (response) => {
const error = {};
error.name = 'AccessException';
error.violations = ['You are not allowed to perform this action'];
return error;
}
export {
makeFetch,
fetchResults,
fetchScopes
}

View File

@@ -0,0 +1,223 @@
import {Scope} from '../../types';
export type body = {[key: string]: boolean|string|number|null};
export type fetchOption = {[key: string]: boolean|string|number|null};
export interface Params {
[key: string]: number|string
}
export interface PaginationResponse<T> {
pagination: {
more: boolean;
items_per_page: number;
};
results: T[];
count: number;
}
export interface FetchParams {
[K: string]: string|number|null;
};
export interface TransportExceptionInterface {
name: string;
}
export interface ValidationExceptionInterface extends TransportExceptionInterface {
name: 'ValidationException';
error: object;
violations: string[];
titles: string[];
propertyPaths: string[];
}
export interface ValidationErrorResponse extends TransportExceptionInterface {
violations: {
title: string;
propertyPath: string;
}[];
}
export interface AccessExceptionInterface extends TransportExceptionInterface {
name: 'AccessException';
violations: string[];
}
export interface NotFoundExceptionInterface extends TransportExceptionInterface {
name: 'NotFoundException';
}
export interface ServerExceptionInterface extends TransportExceptionInterface {
name: 'ServerException';
message: string;
code: number;
body: string;
}
/**
* Generic api method that can be adapted to any fetch request
*/
export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | Input | null, options?: FetchParams): Promise<Output> => {
let opts = {
method: method,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
};
if (body !== null || typeof body !== 'undefined') {
Object.assign(opts, {body: JSON.stringify(body)})
}
if (typeof options !== 'undefined') {
opts = Object.assign(opts, options);
}
return fetch(url, opts)
.then(response => {
if (response.ok) {
return response.json();
}
if (response.status === 422) {
return response.json().then(response => {
throw ValidationException(response)
});
}
if (response.status === 403) {
throw AccessException(response);
}
throw {
name: 'Exception',
sta: response.status,
txt: response.statusText,
err: new Error(),
violations: response.body
};
});
}
/**
* Fetch results with certain parameters
*/
function _fetchAction<T>(page: number, uri: string, params?: FetchParams): Promise<PaginationResponse<T>> {
const item_per_page: number = 50;
let searchParams = new URLSearchParams();
searchParams.append('item_per_page', item_per_page.toString());
searchParams.append('page', page.toString());
if (params !== undefined) {
Object.keys(params).forEach(key => {
let v = params[key];
if (typeof v === 'string') {
searchParams.append(key, v);
} else if (typeof v === 'number') {
searchParams.append(key, v.toString());
} else if (v === null) {
searchParams.append(key, '');
}
});
}
let url = uri + '?' + searchParams.toString();
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
}).then((response) => {
if (response.ok) { return response.json(); }
if (response.status === 404) {
throw NotFoundException(response);
}
if (response.status === 422) {
return response.json().then(response => {
throw ValidationException(response)
});
}
if (response.status === 403) {
throw AccessException(response);
}
if (response.status >= 500) {
return response.text().then(body => {
throw ServerException(response.status, body);
});
}
throw new Error("other network error");
}).catch((reason: any) => {
console.error(reason);
throw new Error(reason);
});
};
export const fetchResults = async<T> (uri: string, params?: FetchParams): Promise<T[]> => {
let promises: Promise<T[]>[] = [],
page = 1;
let firstData: PaginationResponse<T> = await _fetchAction(page, uri, params) as PaginationResponse<T>;
promises.push(Promise.resolve(firstData.results));
if (firstData.pagination.more) {
do {
page = ++page;
promises.push(
_fetchAction<T>(page, uri, params)
.then(r => Promise.resolve(r.results))
);
} while (page * firstData.pagination.items_per_page < firstData.count)
}
return Promise.all(promises).then((values) => values.flat());
};
export const fetchScopes = (): Promise<Scope[]> => {
return fetchResults('/api/1.0/main/scope.json');
};
/**
* Error objects to be thrown
*/
const ValidationException = (response: ValidationErrorResponse): ValidationExceptionInterface => {
const error = {} as ValidationExceptionInterface;
error.name = 'ValidationException';
error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
error.titles = response.violations.map((violation) => violation.title);
error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
return error;
}
const AccessException = (response: Response): AccessExceptionInterface => {
const error = {} as AccessExceptionInterface;
error.name = 'AccessException';
error.violations = ['You are not allowed to perform this action'];
return error;
}
const NotFoundException = (response: Response): NotFoundExceptionInterface => {
const error = {} as NotFoundExceptionInterface;
error.name = 'NotFoundException';
return error;
}
const ServerException = (code: number, body: string): ServerExceptionInterface => {
const error = {} as ServerExceptionInterface;
error.name = 'ServerException';
error.code = code;
error.body = body;
return error;
}

View File

@@ -0,0 +1,6 @@
import {fetchResults} from "./apiMethods";
import {Location, LocationType} from "../../types";
export const getLocations = (): Promise<Location[]> => fetchResults('/api/1.0/main/location.json');
export const getLocationTypes = (): Promise<LocationType[]> => fetchResults('/api/1.0/main/location-type.json');

View File

@@ -0,0 +1,25 @@
import {User} from "../../types";
import {makeFetch} from "./apiMethods";
export const whoami = (): Promise<User> => {
const url = `/api/1.0/main/whoami.json`;
return fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw {
msg: 'Error while getting whoami.',
sta: response.status,
txt: response.statusText,
err: new Error(),
body: response.body
};
});
};
export const whereami = (): Promise<Location | null> => {
const url = `/api/1.0/main/user-current-location.json`;
return makeFetch<null, Location|null>("GET", url);
}

View File

@@ -1,4 +1,4 @@
/*
/*
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
@@ -15,12 +15,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var mime = require('mime-types')
var mime = require('mime')
var download_report = (url, container) => {
var download_text = container.dataset.downloadText,
alias = container.dataset.alias;
window.fetch(url, { credentials: 'same-origin' })
.then(response => {
if (!response.ok) {
@@ -29,21 +29,21 @@ var download_report = (url, container) => {
return response.blob();
}).then(blob => {
var content = URL.createObjectURL(blob),
link = document.createElement("a"),
type = blob.type,
hasForcedType = 'mimeType' in container.dataset,
extension;
if (hasForcedType) {
// force a type
type = container.dataset.mimeType;
blob = new Blob([ blob ], { 'type': type });
content = URL.createObjectURL(blob);
}
extension = mime.extension(type);
extension = mime.getExtension(type);
link.appendChild(document.createTextNode(download_text));
link.classList.add("btn", "btn-action");
@@ -56,7 +56,7 @@ var download_report = (url, container) => {
container.appendChild(link);
}).catch(function(error) {
console.log(error);
var problem_text =
var problem_text =
document.createTextNode("Problem during download");
container
@@ -64,4 +64,4 @@ var download_report = (url, container) => {
});
};
module.exports = download_report;
module.exports = download_report;

View File

@@ -14,9 +14,11 @@
// 4. Include any default map overrides here
@import "custom/_maps";
@import "bootstrap/scss/maps";
// 5. Include remainder of required parts
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
@import "bootstrap/scss/root";

View File

@@ -6,6 +6,7 @@ import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
const i18n = _createI18n(appMessages);
let appsOnPage = new Map();
let appsPerInput = new Map();
function loadDynamicPicker(element) {
@@ -78,13 +79,14 @@ function loadDynamicPicker(element) {
.mount(el);
appsOnPage.set(uniqId, app);
appsPerInput.set(input.name, app);
});
}
document.addEventListener('show-hide-show', function(e) {
loadDynamicPicker(e.detail.container)
})
});
document.addEventListener('show-hide-hide', function(e) {
console.log('hiding event caught')
@@ -95,13 +97,25 @@ document.addEventListener('show-hide-hide', function(e) {
appsOnPage.delete(uniqId);
}
})
})
});
document.addEventListener('pick-entity-type-action', function (e) {
console.log('pick entity event', e);
if (!appsPerInput.has(e.detail.name)) {
console.error('no app with this name');
return;
}
const app = appsPerInput.get(e.detail.name);
if (e.detail.action === 'add') {
app.addNewEntity(e.detail.entity);
} else if (e.detail.action === 'remove') {
app.removeEntity(e.detail.entity);
} else {
console.error('action not supported: '+e.detail.action);
}
});
document.addEventListener('DOMContentLoaded', function(e) {
loadDynamicPicker(document)
})

View File

@@ -0,0 +1,28 @@
import {ShowHide} from 'ChillMainAssets/lib/show_hide/index';
document.addEventListener('DOMContentLoaded', function(_e) {
console.log('pick-rolling-date');
document.querySelectorAll('div[data-rolling-date]').forEach( (picker) => {
const
roll_wrapper = picker.querySelector('div.roll-wrapper'),
fixed_wrapper = picker.querySelector('div.fixed-wrapper');
new ShowHide({
froms: [roll_wrapper],
container: [fixed_wrapper],
test: function (elems) {
console.log('testing');
console.log('elems', elems);
for (let el of elems) {
for (let select_roll of el.querySelectorAll('select[data-roll-picker]')) {
console.log('select_roll', select_roll);
console.log('value', select_roll.value);
return select_roll.value === 'fixed_date';
}
}
return false;
}
})
});
});

View File

@@ -0,0 +1,139 @@
export interface DateTime {
datetime: string;
datetime8601: string
}
export interface Civility {
id: number;
// TODO
}
export interface Job {
id: number;
type: "user_job";
label: {
"fr": string; // could have other key. How to do that in ts ?
}
}
export interface Center {
id: number;
type: "center";
name: string;
}
export interface Scope {
id: number;
type: "scope";
name: {
"fr": string
}
}
export interface User {
type: "user";
id: number;
username: string;
text: string;
email: string;
user_job: Job;
label: string;
// todo: mainCenter; mainJob; etc..
}
export interface UserAssociatedInterface {
type: "user";
id: number;
};
export type TranslatableString = {
fr?: string;
nl?: string;
}
export interface Postcode {
id: number;
name: string;
code: string;
center: Point;
}
export type Point = {
type: "Point";
coordinates: [lat: number, lon: number];
}
export interface Country {
id: number;
name: TranslatableString;
code: string;
}
export interface Address {
type: "address";
address_id: number;
text: string;
street: string;
streetNumber: string;
postcode: Postcode;
country: Country;
floor: string | null;
corridor: string | null;
steps: string | null;
flat: string | null;
buildingName: string | null;
distribution: string | null;
extra: string | null;
confidential: boolean;
lines: string[];
addressReference: AddressReference | null;
validFrom: DateTime;
validTo: DateTime | null;
}
export interface AddressReference {
id: number;
createdAt: DateTime | null;
deletedAt: DateTime | null;
municipalityCode: string;
point: Point;
postcode: Postcode;
refId: string;
source: string;
street: string;
streetNumber: string;
updatedAt: DateTime | null;
}
export interface Location {
type: "location";
id: number;
active: boolean;
address: Address | null;
availableForUsers: boolean;
createdAt: DateTime | null;
createdBy: User | null;
updatedAt: DateTime | null;
updatedBy: User | null;
email: string | null
name: string;
phonenumber1: string | null;
phonenumber2: string | null;
locationType: LocationType;
}
export interface LocationAssociated {
type: "location";
id: number;
}
export interface LocationType {
type: "location-type";
id: number;
active: boolean;
addressRequired: "optional" | "required";
availableForUsers: boolean;
editableByUsers: boolean;
contactData: "optional" | "required";
title: TranslatableString;
}

View File

@@ -54,7 +54,7 @@
</template>
<script>
import { dateToISO, ISOToDate } from 'ChillMainAssets/chill/js/date.js';
import { dateToISO, ISOToDate } from 'ChillMainAssets/chill/js/date';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import ActionButtons from './ActionButtons.vue';

View File

@@ -146,6 +146,9 @@ export default {
}
},
titleCreate() {
if (typeof this.allowedTypes === 'undefined') {
return 'onthefly.create.title.default';
}
return this.allowedTypes.every(t => t === 'person')
? 'onthefly.create.title.person'
: this.allowedTypes.every(t => t === 'thirdparty')

View File

@@ -1,5 +1,5 @@
<template>
<ul class="list-suggest remove-items" v-if="picked.length">
<ul :class="listClasses" v-if="picked.length && displayPicked">
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type+p.id">
<span class="chill_denomination">{{ p.text }}</span>
</li>
@@ -40,6 +40,15 @@ export default {
uniqid: {
type: String,
required: true,
},
removableIfSet: {
type: Boolean,
default: true,
},
displayPicked: {
// display picked entities.
type: Boolean,
default: true,
}
},
emits: ['addNewEntity', 'removeEntity'],
@@ -78,7 +87,13 @@ export default {
} else {
return appMessages.fr.pick_entity.modal_title_one + trans.join(', ');
}
}
},
listClasses() {
return {
'list-suggest': true,
'remove-items': this.$props.removableIfSet,
};
},
},
methods: {
addNewEntity({ selected, modal }) {
@@ -90,6 +105,9 @@ export default {
modal.showModal = false;
},
removeEntity(entity) {
if (!this.$props.removableIfSet) {
return;
}
this.$emit('removeEntity', entity);
}
},

View File

@@ -20,7 +20,7 @@
</template>
<script>
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.js';
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.ts';
export default {
name: "EntityWorkflowVueSubscriber",

View File

@@ -2,57 +2,65 @@
<transition name="modal">
<div class="modal-mask">
<!-- :: styles bootstrap :: -->
<div class="modal-dialog" :class="modalDialogClass">
<div class="modal fade show" style="display: block" aria-modal="true" role="dialog">
<div class="modal-dialog" :class="modalDialogClass">
<div class="modal-content">
<div class="modal-header">
<slot name="header"></slot>
<button class="close btn" @click="$emit('close')">
<i class="fa fa-times" aria-hidden="true"></i></button>
</div>
<div class="body-head">
<div class="modal-header">
<slot name="header"></slot>
<button class="close btn" @click="$emit('close')">
<i class="fa fa-times" aria-hidden="true"></i></button>
</div>
<div class="modal-body">
<div class="body-head">
<slot name="body-head"></slot>
</div>
<div class="modal-body">
<slot name="body"></slot>
</div>
<div class="modal-footer" v-if="!hideFooter">
<button class="btn btn-cancel" @click="$emit('close')">{{ $t('action.close') }}</button>
<slot name="footer"></slot>
</div>
</div>
<slot name="body"></slot>
</div>
<div class="modal-footer" v-if="!hideFooter">
<button class="btn btn-cancel" @click="$emit('close')">{{ $t('action.close') }}</button>
<slot name="footer"></slot>
</div>
</div>
</div>
</div>
</div>
<!-- :: end styles bootstrap :: -->
</div>
</transition>
</template>
<script>
<script lang="ts">
import {defineComponent} from "vue";
/*
* This Modal component is a mix between Vue3 modal implementation
* [+] with 'v-if:showModal' directive:parameter, html scope is added/removed not just shown/hidden
* [+] with slot we can pass content from parent component
* [+] some classes are passed from parent component
* and Bootstrap 4.6 _modal.scss module
* and Bootstrap 5 _modal.scss module
* [+] using bootstrap css classes, the modal have a responsive behaviour,
* [+] modal design can be configured using css classes (size, scroll)
*/
export default {
export default defineComponent({
name: 'Modal',
props: {
modalDialogClass: {
type: String,
required: false
type: Object,
required: false,
default: {},
},
hideFooter: {
type: Boolean,
required: false
required: false,
default: false
}
},
emits: ['close']
}
});
</script>
<style lang="scss">
/**
* This is a mask behind the modal.
*/
.modal-mask {
position: fixed;
z-index: 9998;

View File

@@ -41,7 +41,7 @@
</template>
<script>
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.js';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.ts';
export default {
name: "NotificationReadToggle",

View File

@@ -1,27 +1,6 @@
import { createI18n } from 'vue-i18n'
import { createI18n } from 'vue-i18n';
import datetimeFormats from '../i18n/datetimeFormats';
const datetimeFormats = {
fr: {
short: {
year: "numeric",
month: "numeric",
day: "numeric"
},
text: {
year: "numeric",
month: "long",
day: "numeric",
},
long: {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: false
}
}
};
const messages = {
fr: {
action: {
@@ -76,11 +55,13 @@ const messages = {
}
};
const _createI18n = (appMessages) => {
const _createI18n = (appMessages: any, legacy?: boolean) => {
Object.assign(messages.fr, appMessages.fr);
return createI18n({
legacy: typeof legacy === undefined ? true : legacy,
locale: 'fr',
fallbackLocale: 'fr',
// @ts-ignore
datetimeFormats,
messages,
})

View File

@@ -0,0 +1,27 @@
export default {
fr: {
short: {
year: "numeric",
month: "numeric",
day: "numeric"
},
text: {
year: "numeric",
month: "long",
day: "numeric",
},
long: {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: false
},
hoursOnly: {
hour: "numeric",
minute: "numeric",
hour12: false,
}
}
};