@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.
- package/dist/module.json +1 -1
- package/layer/app/app.config.ts +90 -0
- package/layer/app/assets/tekkare_logo.svg +20 -0
- package/layer/app/components/AppUpdateToast.vue +157 -0
- package/layer/app/components/OipDirectusSources.vue +192 -0
- package/layer/app/components/OipDocumentPreview.vue +158 -0
- package/layer/app/components/OipMetadataEditDialog.vue +350 -0
- package/layer/app/components/OipNoteEditDialog.vue +382 -0
- package/layer/app/components/OipShell.vue +713 -0
- package/layer/app/components/OipSourceEditDialog.vue +419 -0
- package/layer/app/components/OipSourceGroups.vue +107 -0
- package/layer/app/components/auth/SplitLayout.vue +261 -0
- package/layer/app/components/layout/firstVisitVideo.vue +165 -0
- package/layer/app/components/panels/OipPanelDocuments.vue +220 -0
- package/layer/app/components/panels/OipPanelMetadata.vue +388 -0
- package/layer/app/components/panels/OipPanelNotes.vue +99 -0
- package/layer/app/components/panels/OipPanelProjects.vue +94 -0
- package/layer/app/components/panels/OipPanelSourceTables.vue +8 -0
- package/layer/app/components/panels/OipPanelSources.vue +361 -0
- package/layer/app/components/panels/OipPanelTopics.vue +339 -0
- package/layer/app/components/panels/OipPanelTutorials.vue +122 -0
- package/layer/app/composables/useOipCrumbLabel.ts +11 -0
- package/layer/app/composables/useOipEsIndices.ts +57 -0
- package/layer/app/composables/useOipFavorites.ts +69 -0
- package/layer/app/composables/useOipNotes.ts +90 -0
- package/layer/app/composables/useOipNotify.ts +21 -0
- package/layer/app/composables/useOipSourceTableGroups.ts +54 -0
- package/layer/app/composables/useViewEditing.ts +11 -0
- package/layer/app/layouts/anonymous.vue +6 -0
- package/layer/app/layouts/error.vue +38 -0
- package/layer/app/notes/categories.ts +46 -0
- package/layer/app/notes/types.ts +100 -0
- package/layer/app/plugins/oip-es-indices.client.ts +18 -0
- package/layer/app/types/metadata.ts +38 -0
- package/layer/app/types/oip.ts +60 -0
- package/layer/app/types/tableGroups.ts +42 -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/oipRail.ts +82 -0
- package/layer/app/utils/postLoginRedirect.ts +58 -0
- package/layer/app/utils/valueTable.ts +126 -0
- package/layer/nuxt.config.ts +32 -0
- package/layer/server/api/oip/asset/[id].get.ts +89 -0
- package/layer/server/api/oip/documents.get.ts +92 -0
- package/layer/server/api/oip/es-indices.get.ts +21 -0
- package/layer/server/api/oip/favorites.get.ts +107 -0
- package/layer/server/api/oip/metadata.get.ts +109 -0
- package/layer/server/api/oip/metadata.patch.ts +163 -0
- package/layer/server/api/oip/note-options.get.ts +89 -0
- package/layer/server/api/oip/note.get.ts +95 -0
- package/layer/server/api/oip/notes.get.ts +173 -0
- package/layer/server/api/oip/notes.patch.ts +198 -0
- package/layer/server/api/oip/sources.patch.ts +145 -0
- package/layer/server/api/oip/sources.post.ts +88 -0
- package/layer/server/api/oip/table-groups.get.ts +145 -0
- package/layer/server/api/oip/tutorial-video.get.ts +44 -0
- package/layer/server/api/romulus/version.get.ts +4 -0
- package/layer/server/utils/oipAuth.ts +69 -0
- package/layer/server/utils/oipEsIndices.ts +99 -0
- package/layer/server/utils/oipGuards.ts +90 -0
- package/layer/server/utils/oipNoteMarkup.ts +197 -0
- 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>
|