@tekkare/romulus 0.1.242 → 0.1.244
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.
- package/dist/module.json +1 -1
- package/layer/app/components/AppUpdateToast.vue +157 -0
- package/layer/app/components/OipShell.vue +2 -0
- package/layer/app/components/auth/SplitLayout.vue +261 -0
- package/layer/app/components/layout/alertBrandSync.vue +143 -0
- package/layer/app/components/layout/firstVisitVideo.vue +165 -0
- package/layer/app/components/layout/tool/adminTemplates.vue +261 -0
- package/layer/app/components/layout/tool/alerts.vue +969 -0
- package/layer/app/components/layout/tool/export.vue +590 -0
- package/layer/app/components/layout/tool/views.vue +933 -0
- package/layer/app/components/layout/viewEmptyState.vue +278 -0
- package/layer/app/components/layout/viewPill.vue +550 -0
- package/layer/app/components/panels/OipPanelProjects.vue +94 -0
- package/layer/app/composables/useAdminAccess.ts +86 -0
- package/layer/app/composables/useAlertBrandSync.ts +85 -0
- package/layer/app/composables/useAlertSource.ts +210 -0
- package/layer/app/composables/useAlerts.ts +345 -0
- package/layer/app/composables/useExportSource.ts +236 -0
- package/layer/app/composables/useHubrEmbedded.ts +13 -0
- package/layer/app/composables/usePermalink.ts +108 -0
- package/layer/app/composables/useSavedExports.ts +286 -0
- package/layer/app/composables/useViewCardActions.ts +500 -0
- package/layer/app/composables/useViewEditing.ts +11 -0
- package/layer/app/composables/useViewTemplates.ts +369 -0
- package/layer/app/composables/useViews.ts +574 -0
- package/layer/app/oip-defaults/alerts.ts +34 -0
- package/layer/app/oip-defaults/api.ts +18 -0
- package/layer/app/oip-defaults/export.ts +23 -0
- package/layer/app/oip-defaults/ui.ts +21 -0
- package/layer/app/oip-defaults/views.ts +59 -0
- package/layer/app/utils/alertFieldLabels.ts +191 -0
- package/layer/app/utils/countries.ts +30 -0
- package/layer/app/utils/exportPage.ts +418 -0
- package/layer/app/utils/headerTabs.ts +15 -0
- package/layer/app/utils/hybridRows.ts +22 -0
- package/layer/app/utils/oipScopeStore.ts +112 -0
- package/layer/app/utils/oipScopes.ts +376 -0
- package/layer/app/utils/postLoginRedirect.ts +58 -0
- package/layer/app/utils/valueTable.ts +126 -0
- package/layer/app/utils/viewPages.ts +30 -0
- package/layer/app/utils/viewTemplateCriteria.ts +215 -0
- package/layer/app/utils/viewTemplates.ts +116 -0
- package/layer/nuxt.config.ts +9 -0
- package/layer/server/api/oip/notes.get.ts +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useOipApi } from "#oip-app/api";
|
|
2
|
+
/**
|
|
3
|
+
* useAdminAccess - a-t-on le droit de publier un template, et pour qui.
|
|
4
|
+
*
|
|
5
|
+
* Deux choses distinctes, souvent confondues :
|
|
6
|
+
* - `isPlatformAdmin`, porte par `/me/summary`, vaut pour toute adresse
|
|
7
|
+
* @tekkare.com : il ouvre les produits, pas l'administration ;
|
|
8
|
+
* - `isSuperAdmin`, rendu par `/superadmin/me`, est attribue a la main. C'est
|
|
9
|
+
* lui qui garde la console d'OIP 7, donc aussi `/superadmin/workspaces`.
|
|
10
|
+
*
|
|
11
|
+
* Publier un template met une vue devant des espaces clients : c'est le
|
|
12
|
+
* second portillon, le meme que la console. Rien n'est demande au chargement
|
|
13
|
+
* de la page : `ensureLoaded()` n'est appele qu'a l'ouverture du dialogue de
|
|
14
|
+
* partage, sans quoi tout le monde paierait un aller-retour pour une option
|
|
15
|
+
* que presque personne ne voit.
|
|
16
|
+
*
|
|
17
|
+
* Meme forme que le composable homonyme d'OIP 7 : un `useState` partage, une
|
|
18
|
+
* seule requete par session.
|
|
19
|
+
*/
|
|
20
|
+
export interface AdminWorkspace {
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
name: string;
|
|
23
|
+
tier: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const useAdminAccess = () => {
|
|
27
|
+
const isSuperAdmin = useState<boolean>("admin:isSuperAdmin", () => false);
|
|
28
|
+
const loaded = useState<boolean>("admin:loaded", () => false);
|
|
29
|
+
const workspaces = useState<AdminWorkspace[]>("admin:workspaces", () => []);
|
|
30
|
+
const workspacesLoaded = useState<boolean>("admin:workspacesLoaded", () => false);
|
|
31
|
+
const elysiaApi = useOipApi();
|
|
32
|
+
const { status } = useAuth();
|
|
33
|
+
|
|
34
|
+
async function ensureLoaded(): Promise<boolean> {
|
|
35
|
+
if (loaded.value) return isSuperAdmin.value;
|
|
36
|
+
if (status.value !== "authenticated") return false;
|
|
37
|
+
try {
|
|
38
|
+
const me = await elysiaApi.admin.me();
|
|
39
|
+
isSuperAdmin.value = !!me?.isSuperAdmin;
|
|
40
|
+
} catch {
|
|
41
|
+
isSuperAdmin.value = false;
|
|
42
|
+
} finally {
|
|
43
|
+
loaded.value = true;
|
|
44
|
+
}
|
|
45
|
+
return isSuperAdmin.value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tous les espaces, pour le selecteur de cibles d'un template.
|
|
50
|
+
*
|
|
51
|
+
* La route est paginee et rend bien plus que des noms (licences, membres,
|
|
52
|
+
* comptes) : on la deroule une fois, on ne garde que le nom, et on ecarte
|
|
53
|
+
* les espaces personnels — offrir un template a l'espace prive de
|
|
54
|
+
* quelqu'un n'a pas de sens. La recherche se fait ensuite dans le
|
|
55
|
+
* selecteur, sur la liste en memoire, comme pour les membres.
|
|
56
|
+
*/
|
|
57
|
+
async function ensureWorkspaces(): Promise<AdminWorkspace[]> {
|
|
58
|
+
if (workspacesLoaded.value) return workspaces.value;
|
|
59
|
+
if (!(await ensureLoaded())) return [];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const first = await elysiaApi.admin.workspaces({ page: 1, limit: 100 });
|
|
63
|
+
const pages = first?.pagination?.totalPages ?? 1;
|
|
64
|
+
const rest = await Promise.all(
|
|
65
|
+
Array.from({ length: Math.max(0, pages - 1) }, (_, i) =>
|
|
66
|
+
elysiaApi.admin.workspaces({ page: i + 2, limit: 100 }),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
workspaces.value = [first, ...rest]
|
|
71
|
+
.flatMap((res) => res?.workspaces ?? [])
|
|
72
|
+
.filter((ws) => !ws.isPersonal)
|
|
73
|
+
.map((ws) => ({ workspaceId: ws.workspaceId, name: ws.name, tier: ws.tier }))
|
|
74
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error("[useAdminAccess] Impossible de lister les espaces:", err);
|
|
77
|
+
workspaces.value = [];
|
|
78
|
+
} finally {
|
|
79
|
+
workspacesLoaded.value = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return workspaces.value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { isSuperAdmin, workspaces, ensureLoaded, ensureWorkspaces };
|
|
86
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAlertBrandSync - proposer a une alerte de marque de suivre sa vue.
|
|
3
|
+
*
|
|
4
|
+
* Une alerte de vue est repointee toute seule quand la vue bouge : l'API
|
|
5
|
+
* compare les criteres et la page lui remet sa requete (cf.
|
|
6
|
+
* `alertRefreshPayload`). Une alerte de MARQUE, elle, ne peut pas l'etre :
|
|
7
|
+
* elle ne lit ni les filtres de la vue ni son index, elle porte sa propre
|
|
8
|
+
* liste de marques, figee a la creation.
|
|
9
|
+
*
|
|
10
|
+
* Sur les pages de ventes c'est le seul genre d'alerte possible, et la vue y
|
|
11
|
+
* est justement une selection de produits : elargir la vue sans toucher a
|
|
12
|
+
* l'alerte laisse une veille qui ne couvre plus ce que l'ecran montre, sans
|
|
13
|
+
* que rien ne le dise.
|
|
14
|
+
*
|
|
15
|
+
* D'ou cette demande, posee au moment ou la vue est enregistree. Demander, et
|
|
16
|
+
* non suivre en silence : une alerte de marque qui suivrait sa vue d'
|
|
17
|
+
* elle-meme deviendrait une alerte de vue deguisee, alors que les deux ne se
|
|
18
|
+
* remettent pas a jour de la meme facon et ne survivent pas aux memes
|
|
19
|
+
* changements.
|
|
20
|
+
*/
|
|
21
|
+
import type { Alert } from "#oip-app/api";
|
|
22
|
+
|
|
23
|
+
export interface AlertBrandSyncRequest {
|
|
24
|
+
alert: Alert;
|
|
25
|
+
/** Marques que porte desormais la vue. */
|
|
26
|
+
brands: string[];
|
|
27
|
+
/** Celles que l'alerte suit encore. */
|
|
28
|
+
current: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pending = shallowRef<AlertBrandSyncRequest | null>(null);
|
|
32
|
+
const syncing = ref(false);
|
|
33
|
+
|
|
34
|
+
export function useAlertBrandSync() {
|
|
35
|
+
const alerts = useAlerts();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Regarde si la vue enregistree porte une alerte de marque a decaler.
|
|
39
|
+
*
|
|
40
|
+
* Rien n'est demande quand les listes sont deja identiques, ni quand la vue
|
|
41
|
+
* n'a plus de marques du tout : une vue videe ne veut pas dire « ne
|
|
42
|
+
* surveille plus rien », et l'API refuserait une alerte sans marque.
|
|
43
|
+
*
|
|
44
|
+
* Une seule alerte a la fois : deux demandes empilees feraient deux modales
|
|
45
|
+
* l'une derriere l'autre, et le cas ne s'est jamais presente.
|
|
46
|
+
*/
|
|
47
|
+
function ask(pulseId: string, brands: string[]) {
|
|
48
|
+
if (brands.length === 0) return;
|
|
49
|
+
|
|
50
|
+
const alert = alerts.alerts.value.find(
|
|
51
|
+
(entry) => entry.kind === "product" && entry.pulseId === pulseId,
|
|
52
|
+
);
|
|
53
|
+
if (!alert) return;
|
|
54
|
+
|
|
55
|
+
const current = alert.brands ?? [];
|
|
56
|
+
const same =
|
|
57
|
+
current.length === brands.length &&
|
|
58
|
+
[...current].sort().join("|") === [...brands].sort().join("|");
|
|
59
|
+
if (same) return;
|
|
60
|
+
|
|
61
|
+
pending.value = { alert, brands, current };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function dismiss() {
|
|
65
|
+
pending.value = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Reporte les marques de la vue sur l'alerte. L'API la reseme. */
|
|
69
|
+
async function confirm() {
|
|
70
|
+
const request = pending.value;
|
|
71
|
+
if (!request) return;
|
|
72
|
+
syncing.value = true;
|
|
73
|
+
try {
|
|
74
|
+
await alerts.update(request.alert.id, { brands: request.brands });
|
|
75
|
+
// L'alerte repart en `seeding` cote worker : la carte doit le dire.
|
|
76
|
+
await alerts.load(true);
|
|
77
|
+
alerts.followSeeding();
|
|
78
|
+
pending.value = null;
|
|
79
|
+
} finally {
|
|
80
|
+
syncing.value = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { pending: computed(() => pending.value), syncing, ask, dismiss, confirm };
|
|
85
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAlertSource - ce qu'une page donne a surveiller.
|
|
3
|
+
*
|
|
4
|
+
* Une alerte rejoue la requete d'une vue sauvegardee et rapporte ce qui y
|
|
5
|
+
* apparait, y change ou en sort. L'API ne sait pas construire cette requete :
|
|
6
|
+
* la DSL Elasticsearch est assemblee dans le navigateur, clause par clause,
|
|
7
|
+
* par les composants de filtre de la page (`elasticQueryStore`). La page la
|
|
8
|
+
* remet donc telle quelle au moment ou l'alerte est creee, et l'API la garde
|
|
9
|
+
* figee.
|
|
10
|
+
*
|
|
11
|
+
* Consequence assumee : une vue modifiee doit repointer ses alertes. Quand
|
|
12
|
+
* l'edition part de la page, le client renvoie la nouvelle requete avec les
|
|
13
|
+
* criteres et l'API repart d'une nouvelle reference. Par tout autre chemin,
|
|
14
|
+
* l'alerte passe en `stale` et attend d'etre rearmee ici.
|
|
15
|
+
*
|
|
16
|
+
* Meme forme que `useExportSource` et `useViews` : un `shallowRef` de module,
|
|
17
|
+
* pose au montage, retire au demontage, donc rien a nettoyer a la main et rien
|
|
18
|
+
* qui traverse le rendu serveur (les descripteurs portent des fonctions).
|
|
19
|
+
*/
|
|
20
|
+
import type { AlertResourceType as HubrResourceType } from "#oip-app/alerts";
|
|
21
|
+
import { VIEW_BRAND_FILTERS } from "#oip-app/views";
|
|
22
|
+
|
|
23
|
+
/** Un champ que la page propose de surveiller. */
|
|
24
|
+
export interface AlertField {
|
|
25
|
+
/** Chemin du champ dans le `_source`, points admis. */
|
|
26
|
+
value: string;
|
|
27
|
+
/** Libelle affiche, deja traduit. */
|
|
28
|
+
label: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AlertSource {
|
|
32
|
+
/** Index Elasticsearch interroge par la page. */
|
|
33
|
+
index: string;
|
|
34
|
+
/** Famille du document, cf. `useHubrResourceKinds`. */
|
|
35
|
+
resourceType: HubrResourceType;
|
|
36
|
+
/**
|
|
37
|
+
* Requete de l'ecran, relue au moment ou l'alerte est creee.
|
|
38
|
+
*
|
|
39
|
+
* La clause `query` seule : ni tri, ni agregation, ni pagination. Le tri ne
|
|
40
|
+
* decrit rien pour une alerte (le diff porte sur un ensemble, pas sur un
|
|
41
|
+
* classement) et les agregations couteraient cher pour rien.
|
|
42
|
+
*/
|
|
43
|
+
query: () => any;
|
|
44
|
+
/**
|
|
45
|
+
* Champs relus pour nommer un element dans le journal : titre, puis
|
|
46
|
+
* sous-titre. Ils sont aussi gardes dans la reference, seul endroit ou le
|
|
47
|
+
* nom d'un element sorti de la vue existe encore.
|
|
48
|
+
*/
|
|
49
|
+
labelFields: [string] | [string, string];
|
|
50
|
+
/** Champs surveillables, tels que la page les propose a l'ecran. */
|
|
51
|
+
fields: AlertField[];
|
|
52
|
+
/** Titre suggere a la creation. Defaut : le nom de la vue. */
|
|
53
|
+
defaultTitle?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const registry = shallowRef<(() => AlertSource | null) | null>(null);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Marques que la page affichee met en avant, quand elle en met une.
|
|
60
|
+
*
|
|
61
|
+
* Deuxieme registre plutot qu'un champ de `AlertSource` : une fiche produit et
|
|
62
|
+
* l'ecran de recherche ne livrent aucune requete surveillable (l'une est un
|
|
63
|
+
* document, l'autre un agregat de dix index), ils n'ont donc pas de source du
|
|
64
|
+
* tout. Ils ont pourtant la seule chose dont une alerte de MARQUE a besoin :
|
|
65
|
+
* le nom du produit qu'on regarde.
|
|
66
|
+
*
|
|
67
|
+
* Ce sont des candidats, pas des marques : le panneau les confronte au
|
|
68
|
+
* repertoire avant de cocher quoi que ce soit, parce qu'un terme de recherche
|
|
69
|
+
* peut etre une molecule (« pembrolizumab ») ou n'exister nulle part.
|
|
70
|
+
*/
|
|
71
|
+
const brandRegistry = shallowRef<(() => string[]) | null>(null);
|
|
72
|
+
|
|
73
|
+
export function useAlertSource() {
|
|
74
|
+
/**
|
|
75
|
+
* Declare la source de la page. A appeler dans le `setup`, une seule fois :
|
|
76
|
+
* le descripteur est relu a chaque acces, donc il peut lire des refs.
|
|
77
|
+
*/
|
|
78
|
+
const declare = (build: () => AlertSource | null) => {
|
|
79
|
+
// Page du tiroir de projet : les alertes restent celles de l'ecran dessous.
|
|
80
|
+
if (useHubrEmbedded()) return;
|
|
81
|
+
// Le moteur d'alertes interroge Elasticsearch lui-meme, par NOM : l'id
|
|
82
|
+
// Directus du catalogue est traduit ici, une fois pour tous les appels.
|
|
83
|
+
const { nameOf } = useOipEsIndices();
|
|
84
|
+
const named = () => {
|
|
85
|
+
const source = build();
|
|
86
|
+
return source ? { ...source, index: nameOf(source.index) } : null;
|
|
87
|
+
};
|
|
88
|
+
registry.value = named;
|
|
89
|
+
onScopeDispose(() => {
|
|
90
|
+
if (registry.value === named) registry.value = null;
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** La source de la page affichee, ou `null` si elle n'en declare pas. */
|
|
95
|
+
const current = computed(() => registry.value?.() ?? null);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Declare les marques de la page. Meme forme que `declare`, et independant :
|
|
99
|
+
* une page peut n'avoir que ca.
|
|
100
|
+
*/
|
|
101
|
+
const declareBrands = (build: () => string[]) => {
|
|
102
|
+
if (useHubrEmbedded()) return;
|
|
103
|
+
brandRegistry.value = build;
|
|
104
|
+
onScopeDispose(() => {
|
|
105
|
+
if (brandRegistry.value === build) brandRegistry.value = null;
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Les marques candidates de la page affichee, vides si elle n'en propose pas. */
|
|
110
|
+
const brands = computed(() =>
|
|
111
|
+
(brandRegistry.value?.() ?? []).map((brand) => String(brand).trim()).filter(Boolean),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return { declare, current, declareBrands, brands };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Noms sous lesquels une vue range sa selection de produits.
|
|
119
|
+
*
|
|
120
|
+
* Le portail ecrit `brands` (cf. useDrugsViews), les delivrances
|
|
121
|
+
* hospitalieres `brandSelection` : c'est le nom qu'elles donnent en base
|
|
122
|
+
* depuis l'origine, garde tel quel pour ne pas perdre les vues deja
|
|
123
|
+
* enregistrees.
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Les marques que porte une vue, s'il y en a.
|
|
128
|
+
*
|
|
129
|
+
* Sur les pages de ventes, une vue EST une selection de produits : faute de
|
|
130
|
+
* requete de documents a livrer, « surveiller cette vue » ne peut vouloir
|
|
131
|
+
* dire que « surveiller ces marques ». C'est ce qui pre-remplit le formulaire
|
|
132
|
+
* d'alerte, et ce qu'on propose de reporter sur une alerte existante quand la
|
|
133
|
+
* vue change.
|
|
134
|
+
*/
|
|
135
|
+
export function viewBrandsFromCriteria(criteria: any[]): string[] {
|
|
136
|
+
const entry = (Array.isArray(criteria) ? criteria : []).find((item: any) =>
|
|
137
|
+
VIEW_BRAND_FILTERS.includes(item?.filter),
|
|
138
|
+
);
|
|
139
|
+
return Array.isArray(entry?.values) ? entry.values.map(String) : [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Deux chemins qui designent la meme page, `trailingSlash` etant actif. */
|
|
143
|
+
const samePage = (a: unknown, b: unknown): boolean =>
|
|
144
|
+
String(a ?? "").replace(/\/$/, "") === String(b ?? "").replace(/\/$/, "");
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Ce qu'une mise a jour de vue doit emporter pour que ses alertes survivent.
|
|
148
|
+
*
|
|
149
|
+
* L'API repointe toute seule les alertes d'une vue dont les criteres ont
|
|
150
|
+
* bouge, mais elle a besoin de la requete : sa DSL est assemblee dans le
|
|
151
|
+
* navigateur, elle ne sait pas la reconstruire. Sans elle, les alertes
|
|
152
|
+
* passent en `stale` et attendent un rearmement a la main depuis la page.
|
|
153
|
+
*
|
|
154
|
+
* Rien n'est envoye si la vue mise a jour n'est pas celle de la page
|
|
155
|
+
* affichee : la requete de l'ecran decrirait alors une autre page que la vue
|
|
156
|
+
* qu'on enregistre.
|
|
157
|
+
*/
|
|
158
|
+
export function alertRefreshPayload(
|
|
159
|
+
page: unknown,
|
|
160
|
+
): { alertQuery?: any; alertIndex?: string } {
|
|
161
|
+
const declared = registry.value?.() ?? null;
|
|
162
|
+
if (!declared) return {};
|
|
163
|
+
if (!samePage(page, useViews().current.value?.page)) return {};
|
|
164
|
+
return { alertQuery: declared.query(), alertIndex: declared.index };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Ce que la snackbar ajoute quand des alertes ont suivi, ou pas.
|
|
169
|
+
*
|
|
170
|
+
* Sans ce mot, l'API rendait deja ces deux nombres et personne ne les lisait :
|
|
171
|
+
* une vue elargie endormait ses alertes en silence, et la panne ne se
|
|
172
|
+
* decouvrait qu'en ouvrant le panneau, parfois des jours plus tard.
|
|
173
|
+
*/
|
|
174
|
+
export function alertRefreshNotice(result: unknown): string {
|
|
175
|
+
const refreshed = Number((result as any)?.alertsRefreshed ?? 0);
|
|
176
|
+
const stale = Number((result as any)?.alertsStale ?? 0);
|
|
177
|
+
// Appelee depuis les magasins, hors setup : la langue vient de l'app, pas de useI18n.
|
|
178
|
+
const fr = String(tryUseNuxtApp()?.$i18n?.locale?.value ?? "fr") !== "en";
|
|
179
|
+
const plural = (n: number) => (n > 1 ? "s" : "");
|
|
180
|
+
if (stale > 0) {
|
|
181
|
+
return fr
|
|
182
|
+
? ` (${stale} alerte${plural(stale)} à réarmer)`
|
|
183
|
+
: ` (${stale} alert${plural(stale)} to re-arm)`;
|
|
184
|
+
}
|
|
185
|
+
if (refreshed > 0) {
|
|
186
|
+
return fr
|
|
187
|
+
? ` (${refreshed} alerte${plural(refreshed)} mise${plural(refreshed)} à jour)`
|
|
188
|
+
: ` (${refreshed} alert${plural(refreshed)} updated)`;
|
|
189
|
+
}
|
|
190
|
+
return "";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Requete d'alerte a partir des filtres d'une page heart.
|
|
195
|
+
*
|
|
196
|
+
* Les pages du hub tiennent leurs clauses dans `elasticQueryStore` sous la
|
|
197
|
+
* forme `{ filter, page, values, keyName }`, et `useHeartElastic` les assemble
|
|
198
|
+
* en `bool.must` au moment d'interroger le cluster. On reprend exactement le
|
|
199
|
+
* meme assemblage : une alerte qui ne poserait pas la meme question que
|
|
200
|
+
* l'ecran ne vaut rien.
|
|
201
|
+
*/
|
|
202
|
+
export function alertQueryFromFilters(filters: any[]): any {
|
|
203
|
+
return {
|
|
204
|
+
bool: {
|
|
205
|
+
must: (filters ?? [])
|
|
206
|
+
.map((entry: any) => entry?.values)
|
|
207
|
+
.filter((clause: unknown) => clause !== undefined && clause !== null),
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|