@data-fair/lib-vue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export declare function useConceptFilters(reactiveSearchParams: Record<string, string>, datasetId?: string): Record<string, string>;
2
+ export default useConceptFilters;
@@ -0,0 +1,24 @@
1
+ // filter reactiveSearchParams to conceptFilters (params prefixed by _c_)
2
+ import { reactive, watch } from 'vue';
3
+ export function useConceptFilters(reactiveSearchParams, datasetId) {
4
+ const conceptFilters = reactive({});
5
+ const datasetFiltersPrefix = datasetId && `_d_${datasetId}_`;
6
+ watch(reactiveSearchParams, () => {
7
+ for (const key of Object.keys(reactiveSearchParams)) {
8
+ if (key.startsWith('_c_'))
9
+ conceptFilters[key] = reactiveSearchParams[key];
10
+ if (datasetFiltersPrefix && key.startsWith(datasetFiltersPrefix)) {
11
+ conceptFilters[key.replace(datasetFiltersPrefix, '')] = reactiveSearchParams[key];
12
+ }
13
+ }
14
+ for (const key of Object.keys(conceptFilters)) {
15
+ if (key.startsWith('_c_') && reactiveSearchParams[key] === undefined)
16
+ delete conceptFilters[key];
17
+ if (datasetFiltersPrefix && !key.startsWith('_c_') && reactiveSearchParams[datasetFiltersPrefix + key] === undefined) {
18
+ delete conceptFilters[key];
19
+ }
20
+ }
21
+ }, { immediate: true });
22
+ return conceptFilters;
23
+ }
24
+ export default useConceptFilters;
@@ -0,0 +1,2 @@
1
+ export declare function formatBytes(bytes: number | string, locale?: string): string;
2
+ export default formatBytes;
@@ -0,0 +1,16 @@
1
+ const locales = {
2
+ fr: [[0, 'octet'], [1, 'octets'], [1000, 'ko'], [1000 * 1000, 'Mo'], [1000 * 1000 * 1000, 'Go'], [1000 * 1000 * 1000 * 1000, 'To'], [1000 * 1000 * 1000 * 1000 * 1000, 'Po']],
3
+ en: [[0, 'byte'], [1, 'bytes'], [1000, 'kb'], [1000 * 1000, 'Mb'], [1000 * 1000 * 1000, 'Gb'], [1000 * 1000 * 1000 * 1000, 'Tb'], [1000 * 1000 * 1000 * 1000 * 1000, 'Pb']]
4
+ };
5
+ export function formatBytes(bytes, locale = 'fr') {
6
+ const bytesInt = Math.abs(typeof bytes === 'string' ? parseInt(bytes, 10) : bytes);
7
+ const def = locales[locale] ?? locales.en;
8
+ for (let i = 0; i < def.length; i++) {
9
+ const step = def[i][0];
10
+ if (bytesInt < step || i === def.length - 1) {
11
+ return (bytesInt / (def[i - 1][0] || 1)).toLocaleString(locale, { maximumFractionDigits: 0 }) + ' ' + def[i - 1][1];
12
+ }
13
+ }
14
+ return ''; // this is only for strict typing, but the code cannot go there, the return in the loop is always called
15
+ }
16
+ export default formatBytes;
@@ -0,0 +1,3 @@
1
+ import type { Field } from '@data-fair/lib-common-types/application/index.js';
2
+ export declare function formatField(item: Record<string, string | null | undefined | number | boolean>, field: Field): string;
3
+ export declare function getFieldLabel(field: Field): string;
@@ -0,0 +1,28 @@
1
+ // TODO: use locale-dayjs for localization of date formats
2
+ import dayjs from 'dayjs';
3
+ export function formatField(item, field) {
4
+ const value = item[field.key];
5
+ if (value === undefined || value === null || value === '')
6
+ return '';
7
+ if (field['x-labels']?.['' + item[field.key]])
8
+ return field['x-labels']['' + item[field.key]];
9
+ if (field.type === 'number' || field.type === 'integer')
10
+ return value.toLocaleString('fr');
11
+ if (typeof value === 'boolean')
12
+ return value ? 'Oui' : 'Non';
13
+ if (typeof value === 'string') {
14
+ if (field['x-refersTo'] === 'http://schema.org/Date' ||
15
+ field['x-refersTo'] === 'https://schema.org/startDate' ||
16
+ field['x-refersTo'] === 'https://schema.org/endDate' ||
17
+ field['x-refersTo'] === 'http://schema.org/dateCreated') {
18
+ if (field.format === 'date-time')
19
+ return dayjs(value).format('DD/MM/YYYY, HH[h]mm');
20
+ else
21
+ return dayjs(value).format('DD/MM/YYYY');
22
+ }
23
+ }
24
+ return '' + value;
25
+ }
26
+ export function getFieldLabel(field) {
27
+ return field.title || field['x-originalName'] || field.key;
28
+ }
@@ -0,0 +1,6 @@
1
+ export declare const locale: import("vue").Ref<string, string>, dayjs: (date?: string | number | import("dayjs").Dayjs | Date | null | undefined) => import("dayjs").Dayjs & {
2
+ fromNow(withoutSuffix?: boolean): string;
3
+ from(compared: import("dayjs").ConfigType, withoutSuffix?: boolean): string;
4
+ toNow(withoutSuffix?: boolean): string;
5
+ to(compared: import("dayjs").ConfigType, withoutSuffix?: boolean): string;
6
+ };
@@ -0,0 +1,7 @@
1
+ // same as locale-dayjs but in a module level singleton for convenience when not using SSR
2
+ import { getLocaleDayjs } from './locale-dayjs.js';
3
+ // @ts-ignore
4
+ if (import.meta.env?.SSR) {
5
+ throw new Error('this module uses a module level singleton, it cannot be used in SSR mode');
6
+ }
7
+ export const { locale, dayjs } = getLocaleDayjs();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { Dayjs, ConfigType } from 'dayjs';
2
+ import type { Ref, App } from 'vue';
3
+ import dayjs from 'dayjs';
4
+ import 'dayjs/locale/fr';
5
+ import 'dayjs/locale/en';
6
+ type RelativeDayjs = Dayjs & {
7
+ fromNow(withoutSuffix?: boolean): string;
8
+ from(compared: ConfigType, withoutSuffix?: boolean): string;
9
+ toNow(withoutSuffix?: boolean): string;
10
+ to(compared: ConfigType, withoutSuffix?: boolean): string;
11
+ };
12
+ export declare function getLocaleDayjs(_locale?: Ref<string>): {
13
+ locale: Ref<string, string>;
14
+ dayjs: (date?: string | number | dayjs.Dayjs | Date | null | undefined) => RelativeDayjs;
15
+ };
16
+ export declare const localeDayjsKey: unique symbol;
17
+ export declare function createLocaleDayjs(locale: Ref<string>): {
18
+ install(app: App): void;
19
+ };
20
+ export declare function useLocaleDayjs(): ReturnType<typeof getLocaleDayjs>;
21
+ export default useLocaleDayjs;
@@ -0,0 +1,32 @@
1
+ import { inject, ref } from 'vue';
2
+ import dayjs from 'dayjs';
3
+ import 'dayjs/locale/fr';
4
+ import 'dayjs/locale/en';
5
+ import localizedFormat from 'dayjs/plugin/localizedFormat.js';
6
+ import relativeTime from 'dayjs/plugin/relativeTime.js';
7
+ dayjs.extend(localizedFormat);
8
+ dayjs.extend(relativeTime);
9
+ // main functionality, use through the createLocaleDayjs plugin and useLocaleDayjs composable
10
+ // or as a global singleton through ./locale-dayjs-global.js
11
+ export function getLocaleDayjs(_locale) {
12
+ const locale = _locale ?? ref('fr');
13
+ return {
14
+ locale,
15
+ dayjs: (date) => {
16
+ return dayjs(date).locale(locale.value);
17
+ }
18
+ };
19
+ }
20
+ // uses pattern for SSR friendly plugin/composable, cf https://antfu.me/posts/composable-vue-vueday-2021#shared-state-ssr-friendly
21
+ export const localeDayjsKey = Symbol('localeDayjs');
22
+ export function createLocaleDayjs(locale) {
23
+ const localeDayjs = getLocaleDayjs(locale);
24
+ return { install(app) { app.provide(localeDayjsKey, localeDayjs); } };
25
+ }
26
+ export function useLocaleDayjs() {
27
+ const localeDayjs = inject(localeDayjsKey);
28
+ if (!localeDayjs)
29
+ throw new Error('useLocaleDayjs requires using the plugin createLocaleDayjs');
30
+ return localeDayjs;
31
+ }
32
+ export default useLocaleDayjs;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@data-fair/lib-vue",
3
+ "version": "0.1.0",
4
+ "description": "Composables and other utilities for Vue applications in the data-fair stack.",
5
+ "main": "index.js",
6
+ "files": [
7
+ "**/*.js",
8
+ "**/*.d.ts"
9
+ ],
10
+ "author": "",
11
+ "license": "MIT",
12
+ "peerDependencies": {
13
+ "dayjs": "^1.11.13",
14
+ "ofetch": "^1.4.0",
15
+ "reconnecting-websocket": "^4.4.0",
16
+ "vue": "^3.5.11"
17
+ },
18
+ "peerDependenciesMeta": {
19
+ "reconnecting-websocket": {
20
+ "optional": true
21
+ },
22
+ "dayjs": {
23
+ "optional": true
24
+ }
25
+ },
26
+ "dependencies": {
27
+ "@data-fair/lib-common-types": "^0.1.0",
28
+ "@data-fair/lib-utils": "^0.2.0",
29
+ "jwt-decode": "^4.0.0",
30
+ "universal-cookie": "^7.2.0"
31
+ },
32
+ "devDependencies": {
33
+ "vue-router": "^4.4.5"
34
+ },
35
+ "type": "module"
36
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: Record<string, string>;
2
+ export default _default;
@@ -0,0 +1,7 @@
1
+ // same as use-reactive-search-params.js but in a module level singleton for convenience when not using SSR
2
+ import { getReactiveSearchParams } from './reactive-search-params.js';
3
+ // @ts-ignore
4
+ if (import.meta.env?.SSR) {
5
+ throw new Error('this module uses a module level singleton, it cannot be used in SSR mode');
6
+ }
7
+ export default getReactiveSearchParams();
@@ -0,0 +1,21 @@
1
+ import type { App } from 'vue';
2
+ import type { Router } from 'vue-router';
3
+ export declare function getReactiveSearchParams(router?: Router): Record<string, string>;
4
+ export declare const reactiveSearchParamsKey: unique symbol;
5
+ export declare function createReactiveSearchParams(router?: Router): {
6
+ install(app: App): void;
7
+ value: Record<string, string>;
8
+ };
9
+ export declare function useReactiveSearchParams(): ReturnType<typeof getReactiveSearchParams>;
10
+ export declare const useStringSearchParam: (key: string, options?: string | {
11
+ default?: string;
12
+ }) => import("vue").WritableComputedRef<string, string>;
13
+ export declare const useBooleanSearchParam: (key: string, options?: boolean | {
14
+ default?: boolean;
15
+ strings?: [string, string];
16
+ }) => import("vue").WritableComputedRef<boolean, boolean>;
17
+ export declare const useNumberSearchParam: (key: string) => import("vue").WritableComputedRef<number | null, number | null>;
18
+ export declare const useStringsArraySearchParam: (key: string, options?: "csv" | {
19
+ style?: "csv";
20
+ }) => import("vue").WritableComputedRef<string[], string[]>;
21
+ export default useReactiveSearchParams;
@@ -0,0 +1,155 @@
1
+ // inspired by useUrlSearchParams (https://github.com/vueuse/vueuse/blob/main/packages/core/useUrlSearchParams/index.ts)
2
+ // but even simpler, without array values, always in history mode, and shared in a app plugin
3
+ import { reactive, watch, inject, computed } from 'vue';
4
+ import Debug from 'debug';
5
+ const debug = Debug('reactive-search-params');
6
+ debug.log = console.log.bind(console);
7
+ const applySearchParams = (state, queryParams) => {
8
+ const unusedKeys = new Set(Object.keys(state));
9
+ for (const key of Object.keys(queryParams)) {
10
+ const value = queryParams[key];
11
+ if (typeof value === 'string') {
12
+ state[key] = value;
13
+ unusedKeys.delete(key);
14
+ }
15
+ if (Array.isArray(value)) {
16
+ const lastValue = value[value.length - 1];
17
+ if (typeof lastValue === 'string') {
18
+ state[key] = lastValue;
19
+ unusedKeys.delete(key);
20
+ }
21
+ }
22
+ }
23
+ for (const unusedKey of unusedKeys) {
24
+ delete state[unusedKey];
25
+ }
26
+ };
27
+ export function getReactiveSearchParams(router) {
28
+ // @ts-ignore
29
+ if (!import.meta.env?.SSR && !router) {
30
+ try {
31
+ // nuxt 3 way of reading router
32
+ // @ts-ignore
33
+ router = __unctx__.get('nuxt-app').use().$router;
34
+ debug('using nuxt 3 router implicitly');
35
+ }
36
+ catch (e) {
37
+ // nothing to do
38
+ }
39
+ }
40
+ const state = reactive({});
41
+ // 2 modes, 1 based on vue router, 1 based on window.location.search
42
+ if (router) {
43
+ debug('initialize reactive search params based on vue router');
44
+ watch(router.currentRoute, (route) => {
45
+ debug('route.query changed', route.query);
46
+ applySearchParams(state, route.query);
47
+ }, { immediate: true });
48
+ watch(state, () => {
49
+ debug('state changed', state);
50
+ router?.replace({ query: state });
51
+ });
52
+ }
53
+ else {
54
+ debug('initialize reactive search params based on window.location.search');
55
+ window.addEventListener('popstate', () => {
56
+ debug('update state based on window.location.search', window.location.search);
57
+ applySearchParams(state, Object.fromEntries(new URLSearchParams(window.location.search)));
58
+ });
59
+ applySearchParams(state, Object.fromEntries(new URLSearchParams(window.location.search)));
60
+ const updateUrl = () => {
61
+ debug('update url based on state', state);
62
+ const params = new URLSearchParams('');
63
+ for (const key of Object.keys(state)) {
64
+ const value = state[key];
65
+ if (value !== null && value !== undefined && value !== '') {
66
+ params.set(key, value);
67
+ }
68
+ }
69
+ const newQuery = params.toString();
70
+ const newSearch = newQuery.length > 0 ? '?' + newQuery : '';
71
+ if (newSearch !== window.location.search) {
72
+ window.history.replaceState(window.history.state, window.document.title, window.location.pathname + newSearch);
73
+ }
74
+ };
75
+ watch(state, () => {
76
+ updateUrl();
77
+ });
78
+ }
79
+ return state;
80
+ }
81
+ // uses pattern for SSR friendly plugin/composable, cf https://antfu.me/posts/composable-vue-vueday-2021#shared-state-ssr-friendly
82
+ export const reactiveSearchParamsKey = Symbol('reactiveSearchParams');
83
+ export function createReactiveSearchParams(router) {
84
+ const reactiveSearchParams = getReactiveSearchParams(router);
85
+ return {
86
+ install(app) { app.provide(reactiveSearchParamsKey, reactiveSearchParams); },
87
+ value: reactiveSearchParams
88
+ };
89
+ }
90
+ export function useReactiveSearchParams() {
91
+ const reactiveSearchParams = inject(reactiveSearchParamsKey);
92
+ if (!reactiveSearchParams)
93
+ throw new Error('useReactiveSearchParams requires using the plugin createReactiveSearchParams');
94
+ return reactiveSearchParams;
95
+ }
96
+ export const useStringSearchParam = (key, options = {}) => {
97
+ const reactiveSearchParams = useReactiveSearchParams();
98
+ const defaultValue = typeof options === 'string' ? options : (options.default ?? '');
99
+ return computed({
100
+ get: () => reactiveSearchParams[key] ?? defaultValue,
101
+ set: (value) => {
102
+ if (value === defaultValue)
103
+ delete reactiveSearchParams[key];
104
+ else
105
+ reactiveSearchParams[key] = value;
106
+ }
107
+ });
108
+ };
109
+ export const useBooleanSearchParam = (key, options = {}) => {
110
+ const reactiveSearchParams = useReactiveSearchParams();
111
+ const defaultValue = typeof options === 'boolean' ? options : (options.default ?? false);
112
+ const strings = (typeof options !== 'boolean' && options.strings) || ['1', '0'];
113
+ return computed({
114
+ get: () => key in reactiveSearchParams ? reactiveSearchParams[key] === strings[0] : defaultValue,
115
+ set: (value) => {
116
+ if (value === defaultValue)
117
+ delete reactiveSearchParams[key];
118
+ else
119
+ reactiveSearchParams[key] = value ? strings[0] : strings[1];
120
+ }
121
+ });
122
+ };
123
+ export const useNumberSearchParam = (key) => {
124
+ const reactiveSearchParams = useReactiveSearchParams();
125
+ return computed({
126
+ get: () => {
127
+ if (key in reactiveSearchParams) {
128
+ const value = Number(reactiveSearchParams[key]);
129
+ if (!isNaN(value))
130
+ return value;
131
+ }
132
+ return null;
133
+ },
134
+ set: (value) => {
135
+ if (value === null)
136
+ delete reactiveSearchParams[key];
137
+ else
138
+ reactiveSearchParams[key] = '' + value;
139
+ }
140
+ });
141
+ };
142
+ export const useStringsArraySearchParam = (key, options = {}) => {
143
+ const reactiveSearchParams = useReactiveSearchParams();
144
+ // const style = typeof options === 'string' ? options : (options.style ?? 'csv')
145
+ return computed({
146
+ get: () => reactiveSearchParams[key] ? reactiveSearchParams[key]?.split(',') : [],
147
+ set: (value) => {
148
+ if (value.length === 0)
149
+ delete reactiveSearchParams[key];
150
+ else
151
+ reactiveSearchParams[key] = value.join(',');
152
+ }
153
+ });
154
+ };
155
+ export default useReactiveSearchParams;
package/session.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { type IncomingMessage } from 'node:http';
2
+ import { type Ref, App } from 'vue';
3
+ import { type RouteLocation } from 'vue-router';
4
+ import { type fetch } from 'ofetch';
5
+ import { type SessionState, type SessionStateAuthenticated } from '@data-fair/lib-common-types/session/index.js';
6
+ interface GenericCookies {
7
+ get: (key: string) => string | undefined;
8
+ set: (key: string, value: string, options?: Record<string, any>) => void;
9
+ remove: (key: string) => void;
10
+ }
11
+ export interface SessionOptions {
12
+ route?: RouteLocation;
13
+ sitePath?: string;
14
+ directoryUrl?: string;
15
+ logoutRedirectUrl?: string;
16
+ req?: IncomingMessage;
17
+ cookies?: GenericCookies;
18
+ customFetch?: typeof fetch;
19
+ }
20
+ export interface Session {
21
+ state: SessionState;
22
+ loginUrl: (redirect?: string, extraParams?: Record<string, string>, immediateRedirect?: true) => string;
23
+ login: (redirect?: string, extraParams?: Record<string, string>, immediateRedirect?: true) => void;
24
+ logout: (redirect?: string) => Promise<void>;
25
+ switchOrganization: (org: string | null, dep?: string) => void;
26
+ setAdminMode: (adminMode: boolean, redirect?: string) => Promise<void>;
27
+ asAdmin: (user: any | null) => Promise<void>;
28
+ cancelDeletion: () => Promise<void>;
29
+ keepalive: () => Promise<void>;
30
+ switchDark: (value: boolean) => void;
31
+ switchLang: (value: string) => void;
32
+ topLocation: Ref<Location | undefined>;
33
+ options: SessionOptions;
34
+ }
35
+ export type SessionAuthenticated = Session & {
36
+ state: SessionStateAuthenticated;
37
+ };
38
+ export declare function getSession(initOptions: SessionOptions): Promise<Session>;
39
+ export declare const sessionKey: unique symbol;
40
+ export declare function createSession(initOptions: SessionOptions): Promise<{
41
+ install(app: App): void;
42
+ value: Session;
43
+ }>;
44
+ export declare function useSession(): Session;
45
+ export declare function useSessionAuthenticated(errorBuilder?: () => any): SessionAuthenticated;
46
+ export default useSession;
package/session.js ADDED
@@ -0,0 +1,289 @@
1
+ import { reactive, computed, watch, inject } from 'vue';
2
+ import { ofetch } from 'ofetch';
3
+ import { jwtDecode } from 'jwt-decode';
4
+ import cookiesModule from 'universal-cookie';
5
+ import Debug from 'debug';
6
+ const Cookies = cookiesModule;
7
+ const debug = Debug('session');
8
+ debug.log = console.log.bind(console);
9
+ function jwtDecodeAlive(jwt) {
10
+ if (!jwt)
11
+ return;
12
+ const decoded = jwtDecode(jwt);
13
+ if (!decoded)
14
+ return;
15
+ const now = Math.ceil(Date.now().valueOf() / 1000);
16
+ if (typeof decoded.exp !== 'undefined' && decoded.exp < now) {
17
+ console.error(`token expired: ${decoded.exp}<${now}, ${JSON.stringify(decoded)}`);
18
+ return;
19
+ }
20
+ if (typeof decoded.nbf !== 'undefined' && decoded.nbf > now) {
21
+ console.warn(`token not yet valid: ${decoded.nbf}>${now}, ${JSON.stringify(decoded)}`);
22
+ // do not return null here, this is probably a false flag due to a slightly mismatched clock
23
+ // return null
24
+ }
25
+ return decoded;
26
+ }
27
+ const getTopLocation = () => {
28
+ try {
29
+ return window.top ? window.top.location : window.location;
30
+ }
31
+ catch (err) {
32
+ return window.location;
33
+ }
34
+ };
35
+ const goTo = (url) => {
36
+ const topLocation = getTopLocation();
37
+ if (topLocation == null) {
38
+ throw new TypeError('session.goTo was called without access to the window object or its location');
39
+ }
40
+ if (url)
41
+ topLocation.href = url;
42
+ else
43
+ topLocation.reload();
44
+ };
45
+ const defaultOptions = { directoryUrl: '/simple-directory', sitePath: '' };
46
+ export async function getSession(initOptions) {
47
+ const options = { ...defaultOptions, ...initOptions };
48
+ const cookiesPath = options.sitePath + '/';
49
+ debug(`init directoryUrl=${options.directoryUrl}, cookiesPath=${cookiesPath}`);
50
+ const ssr = !!options.req;
51
+ if (ssr)
52
+ debug('run in SSR context');
53
+ const customFetch = initOptions?.customFetch ?? ofetch;
54
+ // use vue-router to detect page change and maintain a reference to the current page location
55
+ // top page if we are in iframe context
56
+ const topLocation = computed(() => {
57
+ if (ssr)
58
+ return undefined;
59
+ if (options.route?.fullPath) { /* empty */ } // adds reactivity
60
+ const location = getTopLocation();
61
+ debug('update location based on route change', location);
62
+ return location;
63
+ });
64
+ // the core state of the session that is filled by reading cookies
65
+ const state = reactive({});
66
+ // cookies are the source of truth and this information is transformed into the state reactive object
67
+ const cookies = initOptions?.cookies ?? new Cookies(options.req?.headers.cookie);
68
+ const readCookies = () => {
69
+ const darkCookie = cookies.get('theme_dark');
70
+ state.dark = darkCookie === '1' || darkCookie === 'true';
71
+ const langCookie = cookies.get('i18n_lang');
72
+ if (langCookie)
73
+ state.lang = langCookie;
74
+ else
75
+ delete state.lang;
76
+ const idToken = cookies.get('id_token');
77
+ const user = jwtDecodeAlive(idToken);
78
+ if (!user) {
79
+ delete state.user;
80
+ delete state.organization;
81
+ delete state.account;
82
+ delete state.accountRole;
83
+ return;
84
+ }
85
+ // this is to prevent null values that are put by SD versions that do not strictly respect their schema
86
+ for (const org of user.organizations) {
87
+ if (!org.department) {
88
+ delete org.department;
89
+ delete org.departmentName;
90
+ }
91
+ }
92
+ state.user = user;
93
+ const organizationId = cookies.get('id_token_org');
94
+ const departmentId = cookies.get('id_token_dep');
95
+ if (organizationId) {
96
+ if (departmentId) {
97
+ state.organization = state.user.organizations.find(o => o.id === organizationId && o.department === departmentId);
98
+ }
99
+ else {
100
+ state.organization = state.user.organizations.find(o => o.id === organizationId);
101
+ }
102
+ }
103
+ else {
104
+ delete state.organization;
105
+ }
106
+ if (state.organization) {
107
+ state.account = {
108
+ type: 'organization',
109
+ id: state.organization.id,
110
+ name: state.organization.name,
111
+ department: state.organization.department,
112
+ departmentName: state.organization.departmentName
113
+ };
114
+ state.accountRole = state.organization.role;
115
+ }
116
+ else {
117
+ state.account = {
118
+ type: 'user',
119
+ id: state.user.id,
120
+ name: state.user.name
121
+ };
122
+ state.accountRole = 'admin';
123
+ }
124
+ };
125
+ readCookies();
126
+ debug('initial state', state);
127
+ if (!ssr) {
128
+ // sessionData is also stored in localStorage as a way to access it in simpler pages that do not require use-session
129
+ // and in order to listen to storage event from other contexts and sync session info accross windows and tabs
130
+ const storageListener = (event) => {
131
+ if (event.key === 'sd-session' + options.sitePath)
132
+ readCookies();
133
+ };
134
+ window.addEventListener('storage', storageListener);
135
+ // we cannot use onUnmounted here or we get warnings "onUnmounted is called when there is no active component instance to be associated with. "
136
+ // TODO: should we have another cleanup mechanism ?
137
+ // onUnmounted(() => { window.removeEventListener('storage', storageListener) })
138
+ // trigger some full page refresh when some key session elements are changed
139
+ // the danger of simply using reactivity is too high, data must be re-fetched, etc.
140
+ watch(() => state.account, (account, oldAccount) => {
141
+ if (account?.type !== oldAccount?.type || account?.id !== oldAccount?.id || account?.department !== oldAccount?.department) {
142
+ goTo(null);
143
+ }
144
+ });
145
+ watch(() => state.lang, () => {
146
+ goTo(null);
147
+ });
148
+ watch(() => state.dark, () => {
149
+ goTo(null);
150
+ });
151
+ watch(state, (state) => {
152
+ if (!ssr) {
153
+ window.localStorage.setItem('sd-session' + options.sitePath, JSON.stringify(state));
154
+ }
155
+ debug('state changed', state);
156
+ });
157
+ }
158
+ // login can be performed as a simple link (please use target=top) or as a function
159
+ function loginUrl(redirect, extraParams = {}, immediateRedirect = true) {
160
+ // login can also be used to redirect user immediately if he is already logged
161
+ if (redirect && state.user && immediateRedirect)
162
+ return redirect;
163
+ if (!redirect && topLocation.value)
164
+ redirect = topLocation.value.href;
165
+ let url = `${options.directoryUrl}/login?redirect=${encodeURIComponent(redirect ?? '')}`;
166
+ Object.keys(extraParams).filter(key => ![null, undefined, ''].includes(extraParams[key])).forEach((key) => {
167
+ url += `&${key}=${encodeURIComponent(extraParams[key])}`;
168
+ });
169
+ return url;
170
+ }
171
+ const login = (redirect, extraParams = {}, immediateRedirect = true) => {
172
+ goTo(loginUrl(redirect, extraParams, immediateRedirect));
173
+ };
174
+ const logout = async (redirect) => {
175
+ await customFetch(`${options.directoryUrl}/api/auth`, { method: 'DELETE' });
176
+ // sometimes server side cookie deletion is not applied immediately in browser local js context
177
+ // so we do it here to
178
+ cookies.remove('id_token');
179
+ cookies.remove('id_token_org');
180
+ cookies.remove('id_token_dep');
181
+ goTo(redirect ?? options.logoutRedirectUrl ?? null);
182
+ };
183
+ const switchOrganization = (org, dep) => {
184
+ if (org)
185
+ cookies.set('id_token_org', org, { path: cookiesPath });
186
+ else
187
+ cookies.remove('id_token_org');
188
+ if (dep)
189
+ cookies.set('id_token_dep', dep, { path: cookiesPath });
190
+ else
191
+ cookies.remove('id_token_dep');
192
+ readCookies();
193
+ };
194
+ const setAdminMode = async (adminMode, redirect) => {
195
+ if (adminMode) {
196
+ const params = { adminMode: 'true' };
197
+ if (state.user != null)
198
+ params.email = state.user.email;
199
+ const url = loginUrl(redirect, params, true);
200
+ goTo(url);
201
+ }
202
+ else {
203
+ await customFetch(`${options.directoryUrl}/api/auth/adminmode`, { method: 'DELETE' });
204
+ goTo(redirect ?? null);
205
+ }
206
+ };
207
+ const asAdmin = async (user) => {
208
+ if (user) {
209
+ await customFetch(`${options.directoryUrl}/api/auth/asadmin`, { method: 'POST', body: user });
210
+ }
211
+ else {
212
+ await customFetch(`${options.directoryUrl}/api/auth/asadmin`, { method: 'DELETE' });
213
+ }
214
+ readCookies();
215
+ };
216
+ const cancelDeletion = async () => {
217
+ if (state.user == null)
218
+ return;
219
+ await customFetch(`${options.directoryUrl}/api/users/${state.user.id}`, { method: 'PATCH', body: ({ plannedDeletion: null }) });
220
+ readCookies();
221
+ };
222
+ const switchDark = (value) => {
223
+ const maxAge = 60 * 60 * 24 * 365; // 1 year
224
+ cookies.set('theme_dark', `${value}`, { maxAge, path: cookiesPath });
225
+ readCookies();
226
+ };
227
+ const switchLang = (value) => {
228
+ const maxAge = 60 * 60 * 24 * 365; // 1 year
229
+ cookies.set('i18n_lang', value, { maxAge, path: cookiesPath });
230
+ readCookies();
231
+ };
232
+ const keepalive = async () => {
233
+ if (state.user == null)
234
+ return;
235
+ window.localStorage.setItem('sd-keepalive' + options.sitePath, `${new Date().getTime()}`);
236
+ await customFetch(`${options.directoryUrl}/api/auth/keepalive`, { method: 'POST' });
237
+ readCookies();
238
+ };
239
+ // immediately performs a keepalive, but only on top windows (not iframes or popups)
240
+ // and only if it was not done very recently (maybe from a refreshed page next to this one)
241
+ if (!ssr && window.top === window.self) {
242
+ const lastKeepalive = window.localStorage.getItem('sd-keepalive' + options.sitePath);
243
+ if (!lastKeepalive || (new Date().getTime() - Number(lastKeepalive)) > 10000) {
244
+ await keepalive();
245
+ }
246
+ }
247
+ const session = {
248
+ state,
249
+ loginUrl,
250
+ login,
251
+ logout,
252
+ switchOrganization,
253
+ setAdminMode,
254
+ asAdmin,
255
+ cancelDeletion,
256
+ keepalive,
257
+ switchDark,
258
+ switchLang,
259
+ topLocation,
260
+ options
261
+ };
262
+ return session;
263
+ }
264
+ // uses pattern for SSR friendly plugin/composable, cf https://antfu.me/posts/composable-vue-vueday-2021#shared-state-ssr-friendly
265
+ export const sessionKey = Symbol('session');
266
+ export async function createSession(initOptions) {
267
+ const session = await getSession(initOptions);
268
+ return {
269
+ install(app) { app.provide(sessionKey, session); },
270
+ value: session
271
+ };
272
+ }
273
+ export function useSession() {
274
+ const session = inject(sessionKey);
275
+ if (!session)
276
+ throw new Error('useSession requires using the plugin createSession');
277
+ return session;
278
+ }
279
+ export function useSessionAuthenticated(errorBuilder) {
280
+ const session = useSession();
281
+ if (!session.state.user) {
282
+ if (errorBuilder)
283
+ throw errorBuilder();
284
+ else
285
+ throw new Error('useSessionAuthenticated requires a logged in user');
286
+ }
287
+ return session;
288
+ }
289
+ export default useSession;
@@ -0,0 +1,21 @@
1
+ declare const _default: {
2
+ current: import("vue").Ref<{
3
+ type?: ("default" | "info" | "success" | "warning") | undefined;
4
+ msg: string;
5
+ } | {
6
+ type: "error";
7
+ msg: string;
8
+ error: any;
9
+ errorMsg: string;
10
+ } | null, import("./ui-notif.js").UiNotif | {
11
+ type?: ("default" | "info" | "success" | "warning") | undefined;
12
+ msg: string;
13
+ } | {
14
+ type: "error";
15
+ msg: string;
16
+ error: any;
17
+ errorMsg: string;
18
+ } | null>;
19
+ send: (partialNotif: import("./ui-notif.js").PartialUiNotif) => void;
20
+ };
21
+ export default _default;
@@ -0,0 +1,7 @@
1
+ // same as use-ui-notif.js but in a module level singleton for convenience when not using SSR
2
+ import { getUiNotif } from './ui-notif.js';
3
+ // @ts-ignore
4
+ if (import.meta.env?.SSR) {
5
+ throw new Error('this module uses a module level singleton, it cannot be used in SSR mode');
6
+ }
7
+ export default getUiNotif();
package/ui-notif.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { Ref, App } from 'vue';
2
+ export type UiNotif = UiNotifBase | UiNotifError;
3
+ export interface PartialUiNotif {
4
+ type?: 'default' | 'info' | 'success' | 'warning' | 'error';
5
+ msg: string;
6
+ error?: any;
7
+ errorMsg?: string;
8
+ }
9
+ interface UiNotifBase {
10
+ type?: 'default' | 'info' | 'success' | 'warning';
11
+ msg: string;
12
+ }
13
+ interface UiNotifError {
14
+ type: 'error';
15
+ msg: string;
16
+ error: any;
17
+ errorMsg: string;
18
+ }
19
+ export declare const getUiNotif: () => {
20
+ current: Ref<{
21
+ type?: ("default" | "info" | "success" | "warning") | undefined;
22
+ msg: string;
23
+ } | {
24
+ type: "error";
25
+ msg: string;
26
+ error: any;
27
+ errorMsg: string;
28
+ } | null, UiNotif | {
29
+ type?: ("default" | "info" | "success" | "warning") | undefined;
30
+ msg: string;
31
+ } | {
32
+ type: "error";
33
+ msg: string;
34
+ error: any;
35
+ errorMsg: string;
36
+ } | null>;
37
+ send: (partialNotif: PartialUiNotif) => void;
38
+ };
39
+ export declare const uiNotifKey: unique symbol;
40
+ export declare function createUiNotif(): {
41
+ install(app: App): void;
42
+ };
43
+ export declare function useUiNotif(): ReturnType<typeof getUiNotif>;
44
+ export default useUiNotif;
package/ui-notif.js ADDED
@@ -0,0 +1,38 @@
1
+ import { ref, inject } from 'vue';
2
+ import inIframe from '@data-fair/lib-utils/in-iframe.js';
3
+ function getFullNotif(notif) {
4
+ if (typeof notif === 'string')
5
+ return { msg: notif, type: 'default' };
6
+ if (notif.error) {
7
+ notif.type = 'error';
8
+ notif.errorMsg = (notif.error.response && (notif.error.response.data || notif.error.response.status)) || notif.error.message || notif.error;
9
+ }
10
+ notif.type = notif.type || 'default';
11
+ if (inIframe) {
12
+ window.top?.postMessage({ vIframe: true, uiNotification: notif }, '*');
13
+ }
14
+ else {
15
+ console.log('notification', notif);
16
+ }
17
+ throw new Error('invalid UI notification');
18
+ }
19
+ export const getUiNotif = () => {
20
+ const current = ref(null);
21
+ const send = (partialNotif) => {
22
+ current.value = getFullNotif(partialNotif);
23
+ };
24
+ return { current, send };
25
+ };
26
+ // uses pattern for SSR friendly plugin/composable, cf https://antfu.me/posts/composable-vue-vueday-2021#shared-state-ssr-friendly
27
+ export const uiNotifKey = Symbol('uiNotif');
28
+ export function createUiNotif() {
29
+ const uiNotif = getUiNotif();
30
+ return { install(app) { app.provide(uiNotifKey, uiNotif); } };
31
+ }
32
+ export function useUiNotif() {
33
+ const session = inject(uiNotifKey);
34
+ if (!session)
35
+ throw new Error('useUiNotif requires using the plugin createUiNotif');
36
+ return session;
37
+ }
38
+ export default useUiNotif;
package/ws.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import reconnectingWebSocketModule from 'reconnecting-websocket';
2
+ export declare function useWS(path: string): {
3
+ opened: import("vue").Ref<boolean, boolean>;
4
+ ws: reconnectingWebSocketModule.default;
5
+ subscribe: <T>(channel: string, listener: (message: T) => void) => void;
6
+ unsubscribe: (channel: string, listener: (message: any) => void) => void;
7
+ } | undefined;
8
+ export default useWS;
package/ws.js ADDED
@@ -0,0 +1,66 @@
1
+ import reconnectingWebSocketModule from 'reconnecting-websocket';
2
+ import { ref, reactive, onScopeDispose } from 'vue';
3
+ const ReconnectingWebSocket = reconnectingWebSocketModule;
4
+ const getWS = (path) => {
5
+ if (!window.WebSocket)
6
+ return;
7
+ // @ts-ignore
8
+ if (import.meta.env?.SSR)
9
+ return;
10
+ const url = (window.location.origin + path).replace('http:', 'ws:').replace('https:', 'wss:');
11
+ const ws = new ReconnectingWebSocket(url);
12
+ const subscriptions = reactive({});
13
+ const opened = ref(false);
14
+ ws.addEventListener('open', () => {
15
+ opened.value = true;
16
+ for (const channel of Object.keys(subscriptions)) {
17
+ if (subscriptions[channel].length) {
18
+ ws.send(JSON.stringify({ type: 'subscribe', channel }));
19
+ }
20
+ else {
21
+ ws.send(JSON.stringify({ type: 'unsubscribe', channel }));
22
+ delete subscriptions[channel];
23
+ }
24
+ }
25
+ });
26
+ ws.addEventListener('close', () => {
27
+ opened.value = false;
28
+ });
29
+ ws.onmessage = (event) => {
30
+ const body = JSON.parse(event.data);
31
+ if (body.type === 'message') {
32
+ if (subscriptions[body.channel]?.length) {
33
+ for (const listener of subscriptions[body.channel]) {
34
+ listener(body.data);
35
+ }
36
+ }
37
+ }
38
+ };
39
+ function subscribe(channel, listener) {
40
+ if (!subscriptions[channel]) {
41
+ subscriptions[channel] = [];
42
+ if (opened.value)
43
+ ws.send(JSON.stringify({ type: 'subscribe', channel }));
44
+ }
45
+ subscriptions[channel].push(listener);
46
+ onScopeDispose(() => {
47
+ unsubscribe(channel, listener);
48
+ });
49
+ }
50
+ const unsubscribe = (channel, listener) => {
51
+ if (subscriptions[channel]) {
52
+ subscriptions[channel] = subscriptions[channel].filter(l => l !== listener);
53
+ if (subscriptions[channel].length === 0 && opened.value) {
54
+ ws.send(JSON.stringify({ type: 'unsubscribe', channel }));
55
+ delete subscriptions[channel];
56
+ }
57
+ }
58
+ };
59
+ return { opened, ws, subscribe, unsubscribe };
60
+ };
61
+ const sockets = {};
62
+ export function useWS(path) {
63
+ sockets[path] = sockets[path] ?? getWS(path);
64
+ return sockets[path];
65
+ }
66
+ export default useWS;