@tekkare/romulus 0.1.241 → 0.1.243

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.
Files changed (64) hide show
  1. package/dist/module.json +1 -1
  2. package/layer/app/app.config.ts +90 -0
  3. package/layer/app/assets/tekkare_logo.svg +20 -0
  4. package/layer/app/components/AppUpdateToast.vue +157 -0
  5. package/layer/app/components/OipDirectusSources.vue +192 -0
  6. package/layer/app/components/OipDocumentPreview.vue +158 -0
  7. package/layer/app/components/OipMetadataEditDialog.vue +350 -0
  8. package/layer/app/components/OipNoteEditDialog.vue +382 -0
  9. package/layer/app/components/OipShell.vue +713 -0
  10. package/layer/app/components/OipSourceEditDialog.vue +419 -0
  11. package/layer/app/components/OipSourceGroups.vue +107 -0
  12. package/layer/app/components/auth/SplitLayout.vue +261 -0
  13. package/layer/app/components/layout/firstVisitVideo.vue +165 -0
  14. package/layer/app/components/panels/OipPanelDocuments.vue +220 -0
  15. package/layer/app/components/panels/OipPanelMetadata.vue +388 -0
  16. package/layer/app/components/panels/OipPanelNotes.vue +99 -0
  17. package/layer/app/components/panels/OipPanelProjects.vue +94 -0
  18. package/layer/app/components/panels/OipPanelSourceTables.vue +8 -0
  19. package/layer/app/components/panels/OipPanelSources.vue +361 -0
  20. package/layer/app/components/panels/OipPanelTopics.vue +339 -0
  21. package/layer/app/components/panels/OipPanelTutorials.vue +122 -0
  22. package/layer/app/composables/useOipCrumbLabel.ts +11 -0
  23. package/layer/app/composables/useOipEsIndices.ts +57 -0
  24. package/layer/app/composables/useOipFavorites.ts +69 -0
  25. package/layer/app/composables/useOipNotes.ts +90 -0
  26. package/layer/app/composables/useOipNotify.ts +21 -0
  27. package/layer/app/composables/useOipSourceTableGroups.ts +54 -0
  28. package/layer/app/composables/useViewEditing.ts +11 -0
  29. package/layer/app/layouts/anonymous.vue +6 -0
  30. package/layer/app/layouts/error.vue +38 -0
  31. package/layer/app/notes/categories.ts +46 -0
  32. package/layer/app/notes/types.ts +100 -0
  33. package/layer/app/plugins/oip-es-indices.client.ts +18 -0
  34. package/layer/app/types/metadata.ts +38 -0
  35. package/layer/app/types/oip.ts +60 -0
  36. package/layer/app/types/tableGroups.ts +42 -0
  37. package/layer/app/utils/countries.ts +30 -0
  38. package/layer/app/utils/exportPage.ts +418 -0
  39. package/layer/app/utils/headerTabs.ts +15 -0
  40. package/layer/app/utils/hybridRows.ts +22 -0
  41. package/layer/app/utils/oipRail.ts +82 -0
  42. package/layer/app/utils/postLoginRedirect.ts +58 -0
  43. package/layer/app/utils/valueTable.ts +126 -0
  44. package/layer/nuxt.config.ts +32 -0
  45. package/layer/server/api/oip/asset/[id].get.ts +89 -0
  46. package/layer/server/api/oip/documents.get.ts +92 -0
  47. package/layer/server/api/oip/es-indices.get.ts +21 -0
  48. package/layer/server/api/oip/favorites.get.ts +107 -0
  49. package/layer/server/api/oip/metadata.get.ts +109 -0
  50. package/layer/server/api/oip/metadata.patch.ts +163 -0
  51. package/layer/server/api/oip/note-options.get.ts +89 -0
  52. package/layer/server/api/oip/note.get.ts +95 -0
  53. package/layer/server/api/oip/notes.get.ts +173 -0
  54. package/layer/server/api/oip/notes.patch.ts +198 -0
  55. package/layer/server/api/oip/sources.patch.ts +145 -0
  56. package/layer/server/api/oip/sources.post.ts +88 -0
  57. package/layer/server/api/oip/table-groups.get.ts +145 -0
  58. package/layer/server/api/oip/tutorial-video.get.ts +44 -0
  59. package/layer/server/api/romulus/version.get.ts +4 -0
  60. package/layer/server/utils/oipAuth.ts +69 -0
  61. package/layer/server/utils/oipEsIndices.ts +99 -0
  62. package/layer/server/utils/oipGuards.ts +90 -0
  63. package/layer/server/utils/oipNoteMarkup.ts +197 -0
  64. package/package.json +6 -3
@@ -0,0 +1,388 @@
1
+ <!--
2
+ Le volet « Métadonnées » du rail : ce que Directus sait de cette app.
3
+
4
+ La fiche `OipApplication` (nom, description, statut, URLs, catégories) et
5
+ ses `OipSubject`, un par écran du portail. C'est le référentiel que lisent
6
+ les notes, les favoris et le portail OIP 7 : le relire depuis l'app évite
7
+ d'ouvrir le CMS pour vérifier qu'un écran y est bien déclaré.
8
+
9
+ Réservé aux superadmins, comme la correction : la tuile n'est posée par le
10
+ cadre que pour eux, et la route serveur revalide le droit.
11
+
12
+ Seul le sujet de l'écran ouvert est affiché : on ouvre le volet pour
13
+ vérifier la page qu'on a sous les yeux.
14
+ -->
15
+ <template>
16
+ <div class="toolbar_tab_container meta_panel">
17
+ <div v-if="pending" class="meta_loading">
18
+ <RmSkeleton v-for="i in 3" :key="i" width="100%" :height="72" radius="8px" />
19
+ </div>
20
+
21
+ <p v-else-if="error" class="meta_error">
22
+ {{ isFr ? "Métadonnées illisibles : " : "Could not read metadata: " }}{{ errorMessage }}
23
+ </p>
24
+
25
+ <template v-else-if="data">
26
+ <section class="meta_section">
27
+ <p class="meta_section_title">Application</p>
28
+
29
+ <article v-if="data.application" class="meta_card meta_card--app">
30
+ <div class="meta_card_head">
31
+ <div class="meta_card_titles">
32
+ <h3 class="meta_card_title">{{ text(data.application).name || data.application.prod_url }}</h3>
33
+ <RmBadge :variant="statusVariant(data.application.status)">
34
+ {{ statusLabel(data.application.status) }}
35
+ </RmBadge>
36
+ </div>
37
+ <button
38
+ type="button"
39
+ class="meta_edit_btn"
40
+ :aria-label="isFr ? 'Modifier' : 'Edit'"
41
+ :title="isFr ? 'Modifier' : 'Edit'"
42
+ @click="edit('application', data.application)"
43
+ >
44
+ <RmIcon name="PencilSimple" :size="14" />
45
+ </button>
46
+ </div>
47
+
48
+ <div
49
+ v-if="text(data.application).description"
50
+ class="meta_card_description"
51
+ v-html="text(data.application).description"
52
+ />
53
+
54
+ <dl class="meta_facts">
55
+ <dt>Prod</dt>
56
+ <dd><a :href="data.application.prod_url" target="_blank" rel="noopener">{{ host(data.application.prod_url) }}</a></dd>
57
+ <dt>Staging</dt>
58
+ <dd>
59
+ <a v-if="data.application.staging_url" :href="data.application.staging_url" target="_blank" rel="noopener">{{ host(data.application.staging_url) }}</a>
60
+ <span v-else class="meta_empty">{{ isFr ? "non renseigné" : "not set" }}</span>
61
+ </dd>
62
+ <dt>GitHub</dt>
63
+ <dd>
64
+ <a v-if="data.application.github_front_url" :href="data.application.github_front_url" target="_blank" rel="noopener">{{ repo(data.application.github_front_url) }}</a>
65
+ <span v-else class="meta_empty">{{ isFr ? "non renseigné" : "not set" }}</span>
66
+ </dd>
67
+ <dt>{{ isFr ? "Catégories" : "Categories" }}</dt>
68
+ <dd>
69
+ <span v-if="data.application.categories.length" class="meta_tags">
70
+ <RmTag v-for="c in data.application.categories" :key="c">{{ c }}</RmTag>
71
+ </span>
72
+ <span v-else class="meta_empty">{{ isFr ? "aucune" : "none" }}</span>
73
+ </dd>
74
+ <dt>{{ isFr ? "Langues" : "Languages" }}</dt>
75
+ <dd>{{ langs(data.application) }}</dd>
76
+ <dt>{{ isFr ? "Mis à jour" : "Updated" }}</dt>
77
+ <dd>{{ day(data.application.date_updated) }}</dd>
78
+ </dl>
79
+ </article>
80
+
81
+ <p v-else class="meta_empty">
82
+ {{ isFr ? "Aucune fiche OipApplication ne porte l'URL de cette app." : "No OipApplication record carries this app's URL." }}
83
+ </p>
84
+ </section>
85
+
86
+ <section class="meta_section">
87
+ <p class="meta_section_title">{{ isFr ? "Écran ouvert" : "Current screen" }}</p>
88
+
89
+ <p v-if="!currentSubject" class="meta_empty">
90
+ {{ isFr ? "Aucun sujet déclaré pour cet écran." : "No subject declared for this screen." }}
91
+ </p>
92
+
93
+ <article v-for="s in currentSubject ? [currentSubject] : []" :key="s.id" class="meta_card">
94
+ <div class="meta_card_head">
95
+ <div class="meta_card_titles">
96
+ <h3 class="meta_card_title">{{ text(s).name || s.relative_path }}</h3>
97
+ <code class="meta_path">{{ s.relative_path }}</code>
98
+ <div class="meta_card_badges">
99
+ <RmBadge :variant="statusVariant(s.status)">{{ statusLabel(s.status) }}</RmBadge>
100
+ <RmBadge v-if="s.is_specialized_report" variant="info">
101
+ {{ isFr ? "Rapport spécialisé" : "Specialized report" }}
102
+ </RmBadge>
103
+ <span class="meta_langs">{{ langs(s) }}</span>
104
+ </div>
105
+ </div>
106
+ <button
107
+ type="button"
108
+ class="meta_edit_btn"
109
+ :aria-label="isFr ? 'Modifier' : 'Edit'"
110
+ :title="isFr ? 'Modifier' : 'Edit'"
111
+ @click="edit('subject', s)"
112
+ >
113
+ <RmIcon name="PencilSimple" :size="14" />
114
+ </button>
115
+ </div>
116
+
117
+ <RmCollapse v-if="text(s).description || s.keywords" :title="isFr ? 'Détails' : 'Details'">
118
+ <div v-if="text(s).description" class="meta_card_description" v-html="text(s).description" />
119
+ <p v-if="s.keywords" class="meta_keywords">
120
+ <span class="meta_keywords_label">{{ isFr ? "Mots-clés : " : "Keywords: " }}</span>{{ s.keywords }}
121
+ </p>
122
+ </RmCollapse>
123
+ </article>
124
+ </section>
125
+
126
+ </template>
127
+
128
+ <OipMetadataEditDialog
129
+ v-if="editing"
130
+ v-model="isEditorOpen"
131
+ :kind="editing.kind"
132
+ :item="editing.item"
133
+ :lang="isFr ? 'fr' : 'en'"
134
+ @saved="refresh()"
135
+ />
136
+ </div>
137
+ </template>
138
+
139
+ <script setup lang="ts">
140
+ import type { Metadata, MetadataApplication, MetadataSubject } from "../../types/metadata";
141
+
142
+ // Passee par OipShell a tous les panneaux ; la fiche est celle de l'app.
143
+ defineProps<{ appKey?: string; superAdmin?: boolean }>();
144
+
145
+ const { locale } = useI18n();
146
+ const route = useRoute();
147
+ const isFr = computed(() => locale.value !== "en");
148
+
149
+ const { data, pending, error, refresh } = useFetch<Metadata>(useAppConfig().oip.panels.Metadata.endpoint, {
150
+ key: "oip-rail-metadata",
151
+ });
152
+
153
+ const errorMessage = computed(
154
+ () => (error.value as any)?.data?.message || error.value?.message || ""
155
+ );
156
+
157
+ /** Le texte dans la langue du rail, le francais ou l'anglais a defaut. */
158
+ function text(item: MetadataApplication | MetadataSubject) {
159
+ const wanted = isFr.value ? "fr-FR" : "en-US";
160
+ const rows = item.translations;
161
+ return (
162
+ rows.find((t) => t.languages_code === wanted) ??
163
+ rows.find((t) => t.languages_code === "fr-FR") ??
164
+ rows[0] ?? { name: "", description: "" }
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Le sujet de l'ecran ouvert : le chemin le plus long qui prefixe celui de
170
+ * la page, meme peigne que les notes (`/fr/hubr/x/` et `/hubr/x` se valent).
171
+ */
172
+ function normalize(path: string) {
173
+ return (path.split("?")[0] || "").replace(/^\/(fr|en)(?=\/|$)/, "").replace(/\/+$/, "") || "/";
174
+ }
175
+
176
+ const currentSubject = computed(() => {
177
+ const path = normalize(route.path);
178
+ let best: MetadataSubject | null = null;
179
+ for (const s of data.value?.subjects ?? []) {
180
+ const target = normalize(s.relative_path);
181
+ if (target === "/") continue;
182
+ if (path === target || path.startsWith(target + "/")) {
183
+ if (!best || target.length > normalize(best.relative_path).length) best = s;
184
+ }
185
+ }
186
+ return best;
187
+ });
188
+
189
+ const STATUS: Record<string, { fr: string; en: string; variant: "active" | "pending" | "error" | "info" | "grey" }> = {
190
+ published: { fr: "Publié", en: "Published", variant: "active" },
191
+ draft: { fr: "Brouillon", en: "Draft", variant: "pending" },
192
+ archived: { fr: "Archivé", en: "Archived", variant: "error" },
193
+ ai_generated: { fr: "Généré par IA", en: "AI generated", variant: "info" },
194
+ };
195
+ const statusLabel = (s: string) => (STATUS[s] ? (isFr.value ? STATUS[s].fr : STATUS[s].en) : s || "?");
196
+ const statusVariant = (s: string) => STATUS[s]?.variant ?? "grey";
197
+
198
+ const langs = (item: MetadataApplication | MetadataSubject) =>
199
+ item.translations.map((t) => t.languages_code.slice(0, 2)).join(", ") || (isFr.value ? "aucune" : "none");
200
+
201
+ const host = (url: string) => url.replace(/^https?:\/\//, "").replace(/\/$/, "");
202
+ const repo = (url: string) => url.replace(/^https?:\/\/github\.com\//, "").replace(/\/$/, "");
203
+ const day = (iso: string | null) => (iso ? iso.slice(0, 10) : isFr.value ? "jamais" : "never");
204
+
205
+ const isEditorOpen = ref(false);
206
+ const editing = ref<
207
+ | { kind: "application"; item: MetadataApplication }
208
+ | { kind: "subject"; item: MetadataSubject }
209
+ | null
210
+ >(null);
211
+
212
+ function edit(kind: "application", item: MetadataApplication): void;
213
+ function edit(kind: "subject", item: MetadataSubject): void;
214
+ function edit(kind: "application" | "subject", item: any) {
215
+ editing.value = { kind, item };
216
+ isEditorOpen.value = true;
217
+ }
218
+ </script>
219
+
220
+ <style scoped>
221
+ .meta_panel {
222
+ align-self: stretch;
223
+ width: 100%;
224
+ }
225
+
226
+ .meta_loading {
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 12px;
230
+ width: 100%;
231
+ }
232
+
233
+ .meta_section {
234
+ display: flex;
235
+ flex-direction: column;
236
+ gap: 10px;
237
+ width: 100%;
238
+ }
239
+
240
+ .meta_section_title {
241
+ align-items: center;
242
+ color: var(--text-secondary);
243
+ display: flex;
244
+ font-size: 12px;
245
+ font-weight: 600;
246
+ gap: 8px;
247
+ letter-spacing: 0.04em;
248
+ margin: 0;
249
+ text-transform: uppercase;
250
+ }
251
+
252
+ .meta_card {
253
+ background: var(--bg-surface);
254
+ border: 1px solid var(--border-default);
255
+ border-radius: var(--radius-xl, 12px);
256
+ display: flex;
257
+ flex-direction: column;
258
+ gap: 10px;
259
+ padding: 12px 14px;
260
+ }
261
+
262
+ .meta_card_head {
263
+ align-items: flex-start;
264
+ display: flex;
265
+ gap: 8px;
266
+ justify-content: space-between;
267
+ }
268
+
269
+ .meta_card_titles {
270
+ display: flex;
271
+ flex-direction: column;
272
+ gap: 4px;
273
+ min-width: 0;
274
+ }
275
+
276
+ .meta_card_title {
277
+ color: var(--text-primary);
278
+ font-size: 14px;
279
+ font-weight: 600;
280
+ line-height: 1.3;
281
+ margin: 0;
282
+ }
283
+
284
+ .meta_path {
285
+ color: var(--text-secondary);
286
+ font-size: 12px;
287
+ word-break: break-all;
288
+ }
289
+
290
+ .meta_card_badges {
291
+ align-items: center;
292
+ display: flex;
293
+ flex-wrap: wrap;
294
+ gap: 6px;
295
+ margin-top: 2px;
296
+ }
297
+
298
+ .meta_langs {
299
+ color: var(--text-secondary);
300
+ font-size: 12px;
301
+ }
302
+
303
+ .meta_edit_btn {
304
+ align-items: center;
305
+ background: transparent;
306
+ border: 1px solid var(--border-default);
307
+ border-radius: 8px;
308
+ color: var(--text-secondary);
309
+ cursor: pointer;
310
+ display: inline-flex;
311
+ flex: none;
312
+ height: 28px;
313
+ justify-content: center;
314
+ width: 28px;
315
+ }
316
+
317
+ .meta_edit_btn:hover {
318
+ border-color: var(--color-primary);
319
+ color: var(--color-primary);
320
+ }
321
+
322
+ .meta_card_description {
323
+ color: var(--text-primary);
324
+ font-size: 13px;
325
+ line-height: 1.45;
326
+ }
327
+
328
+ .meta_card_description :deep(p) {
329
+ margin: 0 0 6px;
330
+ }
331
+
332
+ .meta_facts {
333
+ display: grid;
334
+ font-size: 13px;
335
+ gap: 4px 10px;
336
+ grid-template-columns: max-content 1fr;
337
+ margin: 0;
338
+ }
339
+
340
+ .meta_facts dt {
341
+ color: var(--text-secondary);
342
+ }
343
+
344
+ .meta_facts dd {
345
+ margin: 0;
346
+ min-width: 0;
347
+ overflow-wrap: anywhere;
348
+ }
349
+
350
+ .meta_facts a {
351
+ color: var(--color-primary);
352
+ text-decoration: none;
353
+ }
354
+
355
+ .meta_facts a:hover {
356
+ text-decoration: underline;
357
+ }
358
+
359
+ .meta_tags {
360
+ display: flex;
361
+ flex-wrap: wrap;
362
+ gap: 4px;
363
+ }
364
+
365
+ .meta_empty {
366
+ color: var(--text-secondary);
367
+ font-size: 13px;
368
+ font-style: italic;
369
+ margin: 0;
370
+ }
371
+
372
+ .meta_error {
373
+ color: var(--color-danger, #d64545);
374
+ font-size: 13px;
375
+ margin: 0;
376
+ }
377
+
378
+ .meta_keywords {
379
+ color: var(--text-secondary);
380
+ font-size: 12px;
381
+ line-height: 1.45;
382
+ margin: 6px 0 0;
383
+ }
384
+
385
+ .meta_keywords_label {
386
+ font-weight: 600;
387
+ }
388
+ </style>
@@ -0,0 +1,99 @@
1
+ <!--
2
+ Servi par le layer OIP (tuile Notes).
3
+
4
+ Le volet « Notes » du rail : la documentation de l'ecran ouvert.
5
+
6
+ Il remplace les anciens volets Infos, Metrics, Tips et Methodo, qui
7
+ decoupaient la meme documentation en quatre tuiles selon son genre. Le genre
8
+ est devenu une categorie qu'on filtre dans la liste.
9
+
10
+ Le titre reste vide : le cadre du panneau (RmRailPanel) porte deja
11
+ « Notes » dans son en-tete, l'afficher deux fois ferait doublon.
12
+
13
+ Une note sort de Directus par son sujet (le chemin de l'ecran) ou par une
14
+ source citee par la page. L'equipe Tekkare voit lequel des deux, en
15
+ etiquette : c'est le seul moyen de relire un rattachement sans ouvrir le
16
+ CMS. Un lecteur d'un espace client ne voit rien de tout ca.
17
+ -->
18
+ <template>
19
+ <RmNotes
20
+ class="full_width"
21
+ :notes="shownNotes"
22
+ :categories="categories"
23
+ title=""
24
+ :description="
25
+ locale === 'en'
26
+ ? 'Official documentation written by Tekkare: calculation methodologies, source coverage limits, reading conventions. Updated regularly.'
27
+ : `Documentation officielle rédigée par Tekkare : méthodologies de calcul, limites de couverture des sources, conventions d'interprétation. Mise à jour régulièrement.`
28
+ "
29
+ badge="Tekkare"
30
+ :lang="locale === 'en' ? 'en' : 'fr'"
31
+ :editable="superAdmin"
32
+ @navigate="go"
33
+ @edit="openEditor"
34
+ />
35
+
36
+ <OipNoteEditDialog
37
+ v-if="editingId"
38
+ v-model="isEditorOpen"
39
+ :note-id="editingId"
40
+ :lang="locale === 'en' ? 'en' : 'fr'"
41
+ @saved="refresh()"
42
+ />
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ const props = withDefaults(
47
+ defineProps<{
48
+ appKey?: string;
49
+ /** `source_uuid` des jeux affiches par l'ecran, fournis par l'app. */
50
+ sourceUuids?: string[];
51
+ /** Superadmin : crayon de correction. Passe par OipShell. */
52
+ superAdmin?: boolean;
53
+ /**
54
+ * Montre d'ou vient chaque note (sujet ou source) : un outil de relecture
55
+ * pour l'equipe. Par defaut, les superadmins seulement ; l'app peut
56
+ * l'ouvrir plus largement (toute adresse @tekkare.com).
57
+ */
58
+ showOrigin?: boolean;
59
+ }>(),
60
+ { sourceUuids: () => [], superAdmin: false, showOrigin: undefined },
61
+ );
62
+
63
+ const { locale } = useI18n();
64
+ const router = useRouter();
65
+ const { notes, categories, refresh } = useOipNotes(() => props.sourceUuids);
66
+
67
+ const showOrigin = computed(() => props.showOrigin ?? props.superAdmin);
68
+
69
+ const ORIGIN = {
70
+ fr: { subject: "sujet", source: "source" },
71
+ en: { subject: "subject", source: "source" },
72
+ } as const;
73
+
74
+ const shownNotes = computed(() => {
75
+ if (!showOrigin.value) return notes.value;
76
+ const words = ORIGIN[locale.value === "en" ? "en" : "fr"];
77
+ return notes.value.map((note) => ({ ...note, tag: words[note.match] }));
78
+ });
79
+
80
+ const isEditorOpen = ref(false);
81
+ const editingId = ref<string | null>(null);
82
+
83
+ function openEditor(id: string) {
84
+ editingId.value = id;
85
+ isEditorOpen.value = true;
86
+ }
87
+
88
+ /** Les renvois d'une note pointent vers une page de l'app : navigation interne. */
89
+ function go(to: string) {
90
+ router.push(to);
91
+ }
92
+ </script>
93
+
94
+ <style scoped>
95
+ .full_width {
96
+ align-self: stretch;
97
+ width: 100%;
98
+ }
99
+ </style>
@@ -0,0 +1,94 @@
1
+ <!--
2
+ Panneau « Projets » du rail, servi par le layer OIP (tuile Projects) :
3
+ RmProjectPanel.
4
+
5
+ Ou partent mes ajouts : le projet actif s'y designe, et un projet cree ici
6
+ devient l'actif sans quitter la page. Renommer et supprimer restent sur la
7
+ page du projet.
8
+
9
+ Les projets vivent dans la Virtual Lab API, que chaque app interroge avec
10
+ son propre client : l'app passe la liste et les gestes par
11
+ `panelProps.Projects` (cf. les props ci-dessous), et son bouton de creation
12
+ assistee par `newButton` (un composant qui emet `free` pour ouvrir le
13
+ formulaire simple).
14
+ -->
15
+ <template>
16
+ <RmProjectPanel
17
+ :projects="projects"
18
+ :active-id="activeId"
19
+ :active-count="activeCount"
20
+ :loading="loading"
21
+ :failed="failed"
22
+ :creating="creating"
23
+ :all-to="allTo"
24
+ :lang="locale === 'en' ? 'en' : 'fr'"
25
+ @activate="emit('activate', $event)"
26
+ @open="emit('open', $event)"
27
+ @pin="emit('pin', $event)"
28
+ @create="onCreate"
29
+ >
30
+ <template v-if="newButton" #new="{ openForm }">
31
+ <component :is="newButton" size="sm" @free="openForm" />
32
+ </template>
33
+ </RmProjectPanel>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import type { Component } from "vue";
38
+ import type { RmProject } from "@tekkare/romulus/utils/projects";
39
+
40
+ const props = withDefaults(
41
+ defineProps<{
42
+ appKey?: string;
43
+ superAdmin?: boolean;
44
+ projects?: RmProject[];
45
+ activeId?: string | null;
46
+ /** Elements du projet actif. */
47
+ activeCount?: number;
48
+ loading?: boolean;
49
+ failed?: boolean;
50
+ /** Lien « Tous mes projets ». Vide : pas de lien. */
51
+ allTo?: string;
52
+ /**
53
+ * Cree un projet a partir d'un nom ; le panneau attend la fin pour
54
+ * rendre la main. L'app decide si le projet cree devient l'actif.
55
+ */
56
+ create?: (title: string) => Promise<unknown>;
57
+ /** Bouton de creation de l'app (assistant), qui emet `free`. */
58
+ newButton?: Component | null;
59
+ }>(),
60
+ {
61
+ projects: () => [],
62
+ activeId: null,
63
+ activeCount: 0,
64
+ loading: false,
65
+ failed: false,
66
+ allTo: "",
67
+ create: undefined,
68
+ newButton: null,
69
+ },
70
+ );
71
+
72
+ const emit = defineEmits<{
73
+ activate: [projectId: string];
74
+ open: [projectId: string];
75
+ pin: [projectId: string];
76
+ /** Au montage : l'app restaure le projet actif et charge la liste. */
77
+ mounted: [];
78
+ }>();
79
+
80
+ const { locale } = useI18n();
81
+ const creating = ref(false);
82
+
83
+ async function onCreate(title: string) {
84
+ if (!props.create) return;
85
+ creating.value = true;
86
+ try {
87
+ await props.create(title);
88
+ } finally {
89
+ creating.value = false;
90
+ }
91
+ }
92
+
93
+ onMounted(() => emit("mounted"));
94
+ </script>
@@ -0,0 +1,8 @@
1
+ <!-- Tuile « Tables » : le panneau Sources sur sa vue tablegroups. -->
2
+ <template>
3
+ <OipPanelSources v-bind="$attrs" view="tables" />
4
+ </template>
5
+
6
+ <script setup lang="ts">
7
+ import OipPanelSources from "./OipPanelSources.vue";
8
+ </script>