@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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.246",
7
+ "version": "0.1.247",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -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(() => localePath(currentPart.value?.to ?? props.part?.to ?? "/"));
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: localePath(props.part.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
- export function useOipNotes(sourceUuids: MaybeRefOrGetter<string[] | undefined>) {
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
- return [...page, ...fromSource];
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));
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.247",
3
+ "version": "0.1.248",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",