@tekkare/romulus 0.1.247 → 0.1.248
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/OipShell.vue +12 -4
- package/layer/app/components/panels/OipPanelNotes.vue +20 -4
- package/layer/app/components/panels/OipPanelSources.vue +39 -1
- package/layer/app/composables/useOipNotes.ts +30 -3
- package/layer/app/notes/types.ts +7 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -62,8 +62,12 @@ const props = withDefaults(
|
|
|
62
62
|
sections: OipNavSection[];
|
|
63
63
|
/** Partie du hub ouverte, parmi app.config.oip.parts. */
|
|
64
64
|
appKey?: string;
|
|
65
|
-
/** Maillon de partie pose a la main, pour une app sans `parts`. */
|
|
65
|
+
/** Maillon de partie pose a la main (lien deja localise), pour une app sans `parts`. */
|
|
66
66
|
part?: { label: string; to: string } | null;
|
|
67
|
+
/** Maillon sous la partie (ex. la profession ouverte d'un annuaire). */
|
|
68
|
+
subPart?: { label: string; to: string } | null;
|
|
69
|
+
/** Sections de la barre gauche depliees par defaut. */
|
|
70
|
+
navDefaultOpen?: boolean;
|
|
67
71
|
/** Remplace app.config.oip.home quand le maillon pays est dynamique. */
|
|
68
72
|
home?: OipExternalCrumb | null;
|
|
69
73
|
/**
|
|
@@ -88,7 +92,7 @@ const props = withDefaults(
|
|
|
88
92
|
*/
|
|
89
93
|
panelProps?: Partial<Record<OipRailTileKey, Record<string, unknown>>>;
|
|
90
94
|
}>(),
|
|
91
|
-
{ flag: "fr", appKey: "", part: null, home: undefined, tiles: () => ({}), superAdmin: false, userName: "", hideTabs: false, panelProps: () => ({}) },
|
|
95
|
+
{ flag: "fr", appKey: "", part: null, subPart: null, navDefaultOpen: false, home: undefined, tiles: () => ({}), superAdmin: false, userName: "", hideTabs: false, panelProps: () => ({}) },
|
|
92
96
|
);
|
|
93
97
|
|
|
94
98
|
/** Tuile ouverte, ou null quand la toolbar est fermee. */
|
|
@@ -167,7 +171,9 @@ const partLabel = (part: { label: string; labelEn?: string }) =>
|
|
|
167
171
|
const currentPart = computed(() => config.parts.find((p) => p.key === props.appKey) ?? null);
|
|
168
172
|
|
|
169
173
|
/** Route d'accueil de la partie courante, sinon de l'app. */
|
|
170
|
-
const appTo = computed(() =>
|
|
174
|
+
const appTo = computed(() =>
|
|
175
|
+
currentPart.value ? localePath(currentPart.value.to) : props.part?.to ?? localePath("/"),
|
|
176
|
+
);
|
|
171
177
|
|
|
172
178
|
const crumbLabel = useOipCrumbLabel();
|
|
173
179
|
|
|
@@ -251,8 +257,9 @@ const crumbs = computed(() => {
|
|
|
251
257
|
...(currentPart.value
|
|
252
258
|
? [{ label: partLabel(currentPart.value), to: appTo.value, menu: partMenu.value }]
|
|
253
259
|
: props.part
|
|
254
|
-
? [{ label: props.part.label, to:
|
|
260
|
+
? [{ label: props.part.label, to: props.part.to }]
|
|
255
261
|
: []),
|
|
262
|
+
...(props.subPart ? [{ label: props.subPart.label, to: props.subPart.to }] : []),
|
|
256
263
|
// Sur une fiche, la page d'origine reste dans le fil avant elle.
|
|
257
264
|
...(activeItem.value || !originPage.value || !props.sections.length
|
|
258
265
|
? []
|
|
@@ -501,6 +508,7 @@ const tekkareLogo = tekkareLogoRaw.replaceAll('fill="#2100AD"', 'fill="currentCo
|
|
|
501
508
|
:title="title"
|
|
502
509
|
:flag="flag"
|
|
503
510
|
:title-to="appTo"
|
|
511
|
+
:default-open="navDefaultOpen"
|
|
504
512
|
:sections="navSections"
|
|
505
513
|
:active-path="currentPath"
|
|
506
514
|
:collapsed="collapsed"
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
</template>
|
|
44
44
|
|
|
45
45
|
<script setup lang="ts">
|
|
46
|
+
import type { Note } from "../../notes/types";
|
|
47
|
+
|
|
46
48
|
const props = withDefaults(
|
|
47
49
|
defineProps<{
|
|
48
50
|
appKey?: string;
|
|
@@ -56,19 +58,21 @@ const props = withDefaults(
|
|
|
56
58
|
* l'ouvrir plus largement (toute adresse @tekkare.com).
|
|
57
59
|
*/
|
|
58
60
|
showOrigin?: boolean;
|
|
61
|
+
/** Notes encore ecrites dans le code de l'app (cf. useOipNotes). */
|
|
62
|
+
localNotes?: Note[];
|
|
59
63
|
}>(),
|
|
60
|
-
{ sourceUuids: () => [], superAdmin: false, showOrigin: undefined },
|
|
64
|
+
{ sourceUuids: () => [], superAdmin: false, showOrigin: undefined, localNotes: () => [] },
|
|
61
65
|
);
|
|
62
66
|
|
|
63
67
|
const { locale } = useI18n();
|
|
64
68
|
const router = useRouter();
|
|
65
|
-
const { notes, categories, refresh } = useOipNotes(() => props.sourceUuids);
|
|
69
|
+
const { notes, categories, refresh } = useOipNotes(() => props.sourceUuids, () => props.localNotes);
|
|
66
70
|
|
|
67
71
|
const showOrigin = computed(() => props.showOrigin ?? props.superAdmin);
|
|
68
72
|
|
|
69
73
|
const ORIGIN = {
|
|
70
|
-
fr: { subject: "sujet", source: "source" },
|
|
71
|
-
en: { subject: "subject", source: "source" },
|
|
74
|
+
fr: { subject: "sujet", source: "source", local: "code" },
|
|
75
|
+
en: { subject: "subject", source: "source", local: "code" },
|
|
72
76
|
} as const;
|
|
73
77
|
|
|
74
78
|
const shownNotes = computed(() => {
|
|
@@ -80,7 +84,19 @@ const shownNotes = computed(() => {
|
|
|
80
84
|
const isEditorOpen = ref(false);
|
|
81
85
|
const editingId = ref<string | null>(null);
|
|
82
86
|
|
|
87
|
+
const notify = useOipNotify();
|
|
88
|
+
|
|
83
89
|
function openEditor(id: string) {
|
|
90
|
+
// Une note encore portee par un module TS n'a pas d'identifiant Directus.
|
|
91
|
+
if (notes.value.find((note) => note.id === id)?.match === "local") {
|
|
92
|
+
notify(
|
|
93
|
+
locale.value === "en"
|
|
94
|
+
? "This note still lives in the app code: enter it in Directus first."
|
|
95
|
+
: "Cette note vit encore dans le code de l'app : saisissez-la dans Directus avant de la corriger ici.",
|
|
96
|
+
"info",
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
84
100
|
editingId.value = id;
|
|
85
101
|
isEditorOpen.value = true;
|
|
86
102
|
}
|
|
@@ -14,7 +14,21 @@
|
|
|
14
14
|
<div class="src_tabs">
|
|
15
15
|
<div v-if="!showTables" class="src_sources">
|
|
16
16
|
<p class="src_heading">Sources</p>
|
|
17
|
+
<!-- Sources rangees par groupe (essais, publications...) quand l'app
|
|
18
|
+
les classe ; sinon une seule liste. -->
|
|
19
|
+
<template v-if="sourceGroups?.length">
|
|
20
|
+
<section v-for="group in sourceGroups" :key="group.name" class="src_group_block">
|
|
21
|
+
<h2 class="src_group_heading">{{ group.title ?? groupTitle(group.name) }}</h2>
|
|
22
|
+
<OipSourceGroups
|
|
23
|
+
:uuids="group.ids ?? []"
|
|
24
|
+
:primary-uuid="primaryUuid && group.ids?.includes(primaryUuid) ? primaryUuid : undefined"
|
|
25
|
+
:lang="locale === 'en' ? 'en' : 'fr'"
|
|
26
|
+
:editable="superAdmin"
|
|
27
|
+
/>
|
|
28
|
+
</section>
|
|
29
|
+
</template>
|
|
17
30
|
<OipSourceGroups
|
|
31
|
+
v-else
|
|
18
32
|
:uuids="uuids"
|
|
19
33
|
:primary-uuid="primaryUuid"
|
|
20
34
|
:lang="locale === 'en' ? 'en' : 'fr'"
|
|
@@ -150,8 +164,13 @@ const props = withDefaults(
|
|
|
150
164
|
view?: "sources" | "tables";
|
|
151
165
|
/** Droit a l'onglet Tables (licence Databank, admins), connu de l'app. */
|
|
152
166
|
canSeeTables?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Sources rangees par groupe : une section par groupe, titree par
|
|
169
|
+
* `title` ou par le nom technique mis en forme. Remplace `uuids`.
|
|
170
|
+
*/
|
|
171
|
+
sourceGroups?: { name: string; title?: string; ids?: string[] }[];
|
|
153
172
|
}>(),
|
|
154
|
-
{ superAdmin: false, uuids: () => [], primaryUuid: undefined, view: "sources", canSeeTables: false },
|
|
173
|
+
{ superAdmin: false, uuids: () => [], primaryUuid: undefined, view: "sources", canSeeTables: false, sourceGroups: () => [] },
|
|
155
174
|
);
|
|
156
175
|
|
|
157
176
|
const { locale } = useI18n();
|
|
@@ -160,6 +179,10 @@ const isFr = computed(() => locale.value !== "en");
|
|
|
160
179
|
const { groups, pending, readable } = useOipSourceTableGroups(() => props.canSeeTables);
|
|
161
180
|
const showTables = computed(() => props.view === "tables");
|
|
162
181
|
|
|
182
|
+
// "clinicalTrials" -> "Clinical Trials" : le nom technique du groupe sert de titre.
|
|
183
|
+
const groupTitle = (name: string) =>
|
|
184
|
+
name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
|
|
185
|
+
|
|
163
186
|
// Les tables se lisent d'abord par leur nom : le detail se deplie a la demande.
|
|
164
187
|
const opened = ref(new Set<string>());
|
|
165
188
|
const isOpen = (key: string) => opened.value.has(key);
|
|
@@ -202,6 +225,21 @@ function copy(value: string) {
|
|
|
202
225
|
width: 100%;
|
|
203
226
|
}
|
|
204
227
|
|
|
228
|
+
.src_group_block {
|
|
229
|
+
display: flex;
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
gap: 8px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.src_group_heading {
|
|
235
|
+
margin: 0;
|
|
236
|
+
color: var(--text-secondary);
|
|
237
|
+
font-size: var(--font-12);
|
|
238
|
+
font-weight: var(--weight-semibold);
|
|
239
|
+
letter-spacing: 0.04em;
|
|
240
|
+
text-transform: uppercase;
|
|
241
|
+
}
|
|
242
|
+
|
|
205
243
|
.src_heading {
|
|
206
244
|
margin: 0;
|
|
207
245
|
color: var(--text-primary);
|
|
@@ -26,7 +26,7 @@ import { noteCategories } from "../notes/categories";
|
|
|
26
26
|
import type { ApiNote, Note } from "../notes/types";
|
|
27
27
|
|
|
28
28
|
/** Une note de l'ecran, avec ce qui l'y a amenee. */
|
|
29
|
-
export type OipShownNote = Note & { match: "subject" | "source" };
|
|
29
|
+
export type OipShownNote = Note & { match: "subject" | "source" | "local" };
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* `/en/hubr/heart`, `/hubr/heart` et `/hubr/heart/` designent le meme ecran.
|
|
@@ -44,7 +44,25 @@ function normalize(url: string) {
|
|
|
44
44
|
* les connait (elle les montre deja dans la tuile « Sources ») et les passe
|
|
45
45
|
* ici ; une note qui les cite suit ces jeux d'un ecran a l'autre.
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
/** Un chemin de note locale vers son motif (`*` : un segment, `$` : exact). */
|
|
48
|
+
function matcher(pattern: string) {
|
|
49
|
+
const exact = pattern.endsWith("$");
|
|
50
|
+
const body = (exact ? pattern.slice(0, -1) : pattern)
|
|
51
|
+
.split("*")
|
|
52
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
53
|
+
.join("[^/]+");
|
|
54
|
+
return new RegExp(`^${body}${exact ? "$" : "(/|$)"}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `localNotes` : notes encore ecrites dans le code de l'app (modules TS), pas
|
|
59
|
+
* dans Directus. Une note de page sort sur les chemins qu'elle cite, une note
|
|
60
|
+
* d'app partout ; un titre deja servi par Directus ne sort pas deux fois.
|
|
61
|
+
*/
|
|
62
|
+
export function useOipNotes(
|
|
63
|
+
sourceUuids: MaybeRefOrGetter<string[] | undefined>,
|
|
64
|
+
localNotes: MaybeRefOrGetter<Note[] | undefined> = [],
|
|
65
|
+
) {
|
|
48
66
|
const { locale } = useI18n();
|
|
49
67
|
const route = useRoute();
|
|
50
68
|
|
|
@@ -81,7 +99,16 @@ export function useOipNotes(sourceUuids: MaybeRefOrGetter<string[] | undefined>)
|
|
|
81
99
|
}
|
|
82
100
|
}
|
|
83
101
|
|
|
84
|
-
|
|
102
|
+
const served = new Set([...page, ...fromSource].map((note) => note.title.trim().toLowerCase()));
|
|
103
|
+
const all = toValue(localNotes) ?? [];
|
|
104
|
+
const local: OipShownNote[] = [
|
|
105
|
+
...all.filter((note) => note.scope !== "app" && (note.paths ?? []).some((p) => matcher(p).test(path))),
|
|
106
|
+
...all.filter((note) => note.scope === "app"),
|
|
107
|
+
]
|
|
108
|
+
.filter((note) => !served.has(note.title.trim().toLowerCase()))
|
|
109
|
+
.map((note) => ({ ...note, match: "local" as const }));
|
|
110
|
+
|
|
111
|
+
return [...local, ...page, ...fromSource];
|
|
85
112
|
});
|
|
86
113
|
|
|
87
114
|
const categories = computed(() => noteCategories(lang.value));
|
package/layer/app/notes/types.ts
CHANGED
|
@@ -38,6 +38,13 @@ export interface Note {
|
|
|
38
38
|
* categorie. Absent vaut `page`, le cas courant.
|
|
39
39
|
*/
|
|
40
40
|
scope?: "page" | "app";
|
|
41
|
+
/**
|
|
42
|
+
* Notes locales seulement (modules TS d'une app, cf. useOipNotes) : ecrans
|
|
43
|
+
* concernes, par prefixe de chemin sans locale. `*` vaut un segment (un
|
|
44
|
+
* identifiant de fiche), un `$` final restreint au chemin exact. Ignore en
|
|
45
|
+
* `app`.
|
|
46
|
+
*/
|
|
47
|
+
paths?: string[];
|
|
41
48
|
/** Date ISO de derniere revision editoriale du texte. */
|
|
42
49
|
updated: string;
|
|
43
50
|
title: string;
|