@tekkare/romulus 0.1.240 → 0.1.242
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/dist/runtime/assets/styles/tokens.css +1 -1
- package/layer/app/app.config.ts +90 -0
- package/layer/app/assets/tekkare_logo.svg +20 -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 +711 -0
- package/layer/app/components/OipSourceEditDialog.vue +419 -0
- package/layer/app/components/OipSourceGroups.vue +107 -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/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/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/oipRail.ts +82 -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 +5 -3
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<RmModal v-model="open" size="lg" mode="free" :label="title">
|
|
3
|
+
<template #header>
|
|
4
|
+
<h2 class="meta_edit_title">{{ title }}</h2>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<template #tabs>
|
|
8
|
+
<div class="meta_edit_tabs">
|
|
9
|
+
<RmTabBar
|
|
10
|
+
v-model="tabLang"
|
|
11
|
+
:tabs="LANGUAGES"
|
|
12
|
+
:aria-label="isFr ? 'Langue de la fiche' : 'Card language'"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<div class="meta_edit_body">
|
|
18
|
+
<section class="meta_edit_section">
|
|
19
|
+
<p class="meta_edit_eyebrow">{{ isFr ? "Texte" : "Text" }}</p>
|
|
20
|
+
<p v-if="!activeTranslation?.id" class="meta_edit_hint">
|
|
21
|
+
{{ isFr
|
|
22
|
+
? "Rien dans cette langue. Elle sera créée à l'enregistrement."
|
|
23
|
+
: "Nothing in this language. It will be created on save." }}
|
|
24
|
+
</p>
|
|
25
|
+
<RmInput v-model="form.name" :label="isFr ? 'Nom' : 'Name'" />
|
|
26
|
+
<RmRichText
|
|
27
|
+
v-model="form.description"
|
|
28
|
+
:label="isFr ? 'Description' : 'Description'"
|
|
29
|
+
:rows="6"
|
|
30
|
+
:lang="isFr ? 'fr' : 'en'"
|
|
31
|
+
/>
|
|
32
|
+
</section>
|
|
33
|
+
|
|
34
|
+
<section class="meta_edit_section">
|
|
35
|
+
<p class="meta_edit_eyebrow">{{ isFr ? "Fiche" : "Record" }}</p>
|
|
36
|
+
<RmDropdownSelect
|
|
37
|
+
v-model="statusLabel"
|
|
38
|
+
:label="isFr ? 'Statut' : 'Status'"
|
|
39
|
+
:options="statusLabels"
|
|
40
|
+
:search="false"
|
|
41
|
+
:lang="isFr ? 'fr' : 'en'"
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<template v-if="kind === 'application'">
|
|
45
|
+
<RmInput v-model="app.prod_url" label="URL de production" placeholder="https://" />
|
|
46
|
+
<RmInput v-model="app.staging_url" label="URL de staging" placeholder="https://" />
|
|
47
|
+
<RmInput v-model="app.github_front_url" label="GitHub (front)" placeholder="https://" />
|
|
48
|
+
<RmComplexSelect
|
|
49
|
+
v-model="app.categories"
|
|
50
|
+
:label="isFr ? 'Catégories' : 'Categories'"
|
|
51
|
+
:options="CATEGORY_OPTIONS"
|
|
52
|
+
:lang="isFr ? 'fr' : 'en'"
|
|
53
|
+
/>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<template v-else>
|
|
57
|
+
<RmInput
|
|
58
|
+
v-model="subject.relative_path"
|
|
59
|
+
:label="isFr ? 'Chemin de l\'écran' : 'Screen path'"
|
|
60
|
+
placeholder="/hubr/..."
|
|
61
|
+
/>
|
|
62
|
+
<RmTextarea
|
|
63
|
+
:model-value="subject.keywords"
|
|
64
|
+
:label="isFr ? 'Mots-clés (séparés par des virgules)' : 'Keywords (comma separated)'"
|
|
65
|
+
:rows="4"
|
|
66
|
+
@update:model-value="subject.keywords = $event"
|
|
67
|
+
/>
|
|
68
|
+
<RmSwitch
|
|
69
|
+
v-model="subject.is_specialized_report"
|
|
70
|
+
:label="isFr ? 'Rapport spécialisé' : 'Specialized report'"
|
|
71
|
+
:active-text="isFr ? 'Oui' : 'Yes'"
|
|
72
|
+
:inactive-text="isFr ? 'Non' : 'No'"
|
|
73
|
+
/>
|
|
74
|
+
</template>
|
|
75
|
+
</section>
|
|
76
|
+
|
|
77
|
+
<p v-if="error" class="meta_edit_error">{{ error }}</p>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<template #footer>
|
|
81
|
+
<RmButton variant="tertiary" @click="open = false">
|
|
82
|
+
{{ isFr ? "Annuler" : "Cancel" }}
|
|
83
|
+
</RmButton>
|
|
84
|
+
<RmButton variant="primary" :disabled="saving" @click="save">
|
|
85
|
+
{{ saving ? (isFr ? "Enregistrement..." : "Saving...") : (isFr ? "Enregistrer" : "Save") }}
|
|
86
|
+
</RmButton>
|
|
87
|
+
</template>
|
|
88
|
+
</RmModal>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
/**
|
|
93
|
+
* Correction d'une metadonnee de l'app, reservee aux superadmins : la fiche
|
|
94
|
+
* `OipApplication` ou l'un de ses `OipSubject`.
|
|
95
|
+
*
|
|
96
|
+
* Meme geste que pour une source ou une note : un crayon sur la fiche, un
|
|
97
|
+
* formulaire, une route serveur qui revalide le droit. Les deux fiches
|
|
98
|
+
* partagent leur texte traduit (nom, description, une langue par onglet) et
|
|
99
|
+
* leur statut ; le reste depend de ce qu'on corrige.
|
|
100
|
+
*
|
|
101
|
+
* Les traductions se gardent par langue en brouillon et ne partent que si
|
|
102
|
+
* elles ont change : une langue ouverte puis laissee telle quelle ne
|
|
103
|
+
* reecrit rien dans le CMS.
|
|
104
|
+
*/
|
|
105
|
+
import type {
|
|
106
|
+
MetadataApplication,
|
|
107
|
+
MetadataSubject,
|
|
108
|
+
MetadataTranslation,
|
|
109
|
+
} from "../types/metadata";
|
|
110
|
+
|
|
111
|
+
const props = defineProps<{
|
|
112
|
+
kind: "application" | "subject";
|
|
113
|
+
item: MetadataApplication | MetadataSubject;
|
|
114
|
+
lang: string;
|
|
115
|
+
}>();
|
|
116
|
+
const emit = defineEmits<{ saved: [] }>();
|
|
117
|
+
const open = defineModel<boolean>({ default: false });
|
|
118
|
+
|
|
119
|
+
const isFr = computed(() => props.lang === "fr");
|
|
120
|
+
const kind = computed(() => props.kind);
|
|
121
|
+
|
|
122
|
+
const title = computed(() =>
|
|
123
|
+
props.kind === "application"
|
|
124
|
+
? isFr.value ? "Modifier l'application" : "Edit application"
|
|
125
|
+
: isFr.value ? "Modifier le sujet" : "Edit subject"
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const LANGUAGES = [
|
|
129
|
+
{ value: "fr-FR", name: "Français" },
|
|
130
|
+
{ value: "en-US", name: "English" },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const STATUSES = [
|
|
134
|
+
{ value: "published", fr: "Publié", en: "Published" },
|
|
135
|
+
{ value: "draft", fr: "Brouillon", en: "Draft" },
|
|
136
|
+
{ value: "archived", fr: "Archivé", en: "Archived" },
|
|
137
|
+
{ value: "ai_generated", fr: "Généré par IA", en: "AI generated" },
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
/** Les choix du champ `categories` de Directus, tels que le CMS les liste. */
|
|
141
|
+
const CATEGORY_OPTIONS = [
|
|
142
|
+
"Census",
|
|
143
|
+
"Clinical Trials",
|
|
144
|
+
"Drugs Reimbursement",
|
|
145
|
+
"Epidemio",
|
|
146
|
+
"Global Customer Knowledge",
|
|
147
|
+
"Hospital Activity",
|
|
148
|
+
"Market Authorization",
|
|
149
|
+
"Medical Device",
|
|
150
|
+
"Oncology",
|
|
151
|
+
"Price Tracker",
|
|
152
|
+
"Rare Disease Knowledge",
|
|
153
|
+
"Sunshine Act",
|
|
154
|
+
"Vaccination",
|
|
155
|
+
].map((value) => ({ label: value, value }));
|
|
156
|
+
|
|
157
|
+
const activeLang = ref("fr-FR");
|
|
158
|
+
/**
|
|
159
|
+
* L'onglet de langue : changer d'onglet garde le brouillon de la langue
|
|
160
|
+
* qu'on quitte et charge celui de la langue qu'on ouvre. `reset` pose la
|
|
161
|
+
* langue directement, sans passer par ici : au moment ou il la pose, le
|
|
162
|
+
* formulaire porte encore la fiche precedente et ne doit rien garder.
|
|
163
|
+
*/
|
|
164
|
+
const tabLang = computed({
|
|
165
|
+
get: () => activeLang.value,
|
|
166
|
+
set: (next: string) => {
|
|
167
|
+
if (next === activeLang.value) return;
|
|
168
|
+
keepDraft(activeLang.value);
|
|
169
|
+
activeLang.value = next;
|
|
170
|
+
loadDraft(next);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const form = reactive({ name: "", description: "" });
|
|
174
|
+
const drafts = reactive<Record<string, { name: string; description: string }>>({});
|
|
175
|
+
const loaded = reactive<Record<string, string>>({});
|
|
176
|
+
const statusLabel = ref<string | undefined>(undefined);
|
|
177
|
+
const app = reactive({ prod_url: "", staging_url: "", github_front_url: "", categories: [] as string[] });
|
|
178
|
+
const subject = reactive({ relative_path: "", keywords: "", is_specialized_report: false });
|
|
179
|
+
|
|
180
|
+
const saving = ref(false);
|
|
181
|
+
const error = ref("");
|
|
182
|
+
|
|
183
|
+
const statusLabels = computed(() => STATUSES.map((s) => (isFr.value ? s.fr : s.en)));
|
|
184
|
+
|
|
185
|
+
const activeTranslation = computed<MetadataTranslation | undefined>(() =>
|
|
186
|
+
props.item.translations.find((t) => t.languages_code === activeLang.value)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const fingerprint = (draft: { name: string; description: string }) =>
|
|
190
|
+
JSON.stringify([draft.name, draft.description]);
|
|
191
|
+
|
|
192
|
+
function loadDraft(code: string) {
|
|
193
|
+
form.name = drafts[code]?.name ?? "";
|
|
194
|
+
form.description = drafts[code]?.description ?? "";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function keepDraft(code: string) {
|
|
198
|
+
drafts[code] = { name: form.name, description: form.description };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function reset() {
|
|
202
|
+
for (const code of Object.keys(drafts)) delete drafts[code];
|
|
203
|
+
for (const code of Object.keys(loaded)) delete loaded[code];
|
|
204
|
+
for (const t of props.item.translations) {
|
|
205
|
+
const draft = { name: t.name ?? "", description: t.description ?? "" };
|
|
206
|
+
drafts[t.languages_code] = draft;
|
|
207
|
+
loaded[t.languages_code] = fingerprint(draft);
|
|
208
|
+
}
|
|
209
|
+
activeLang.value = isFr.value ? "fr-FR" : "en-US";
|
|
210
|
+
loadDraft(activeLang.value);
|
|
211
|
+
|
|
212
|
+
const status = STATUSES.find((s) => s.value === props.item.status);
|
|
213
|
+
statusLabel.value = status ? (isFr.value ? status.fr : status.en) : undefined;
|
|
214
|
+
|
|
215
|
+
if (props.kind === "application") {
|
|
216
|
+
const item = props.item as MetadataApplication;
|
|
217
|
+
app.prod_url = item.prod_url ?? "";
|
|
218
|
+
app.staging_url = item.staging_url ?? "";
|
|
219
|
+
app.github_front_url = item.github_front_url ?? "";
|
|
220
|
+
app.categories = [...(item.categories ?? [])];
|
|
221
|
+
} else {
|
|
222
|
+
const item = props.item as MetadataSubject;
|
|
223
|
+
subject.relative_path = item.relative_path ?? "";
|
|
224
|
+
subject.keywords = item.keywords ?? "";
|
|
225
|
+
subject.is_specialized_report = !!item.is_specialized_report;
|
|
226
|
+
}
|
|
227
|
+
error.value = "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
watch(open, (value) => value && reset());
|
|
231
|
+
|
|
232
|
+
const notify = useOipNotify();
|
|
233
|
+
const endpoint = useAppConfig().oip.panels.Metadata.endpoint;
|
|
234
|
+
|
|
235
|
+
async function save() {
|
|
236
|
+
keepDraft(activeLang.value);
|
|
237
|
+
saving.value = true;
|
|
238
|
+
error.value = "";
|
|
239
|
+
|
|
240
|
+
// Une langue vide et jamais ecrite n'est pas creee : une ligne sans nom ni
|
|
241
|
+
// description ferait une fiche muette dans le referentiel.
|
|
242
|
+
const translations = LANGUAGES.map((l) => l.value)
|
|
243
|
+
.filter((code) => {
|
|
244
|
+
const draft = drafts[code];
|
|
245
|
+
if (!draft) return false;
|
|
246
|
+
if (loaded[code] !== undefined) return fingerprint(draft) !== loaded[code];
|
|
247
|
+
return !!(draft.name || draft.description);
|
|
248
|
+
})
|
|
249
|
+
.map((code) => {
|
|
250
|
+
const existing = props.item.translations.find((t) => t.languages_code === code);
|
|
251
|
+
return {
|
|
252
|
+
...(existing?.id ? { id: existing.id } : {}),
|
|
253
|
+
languages_code: code,
|
|
254
|
+
name: drafts[code]!.name,
|
|
255
|
+
description: drafts[code]!.description,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const status = STATUSES.find((s) => (isFr.value ? s.fr : s.en) === statusLabel.value);
|
|
260
|
+
const base = {
|
|
261
|
+
...(status ? { status: status.value } : {}),
|
|
262
|
+
...(translations.length ? { translations } : {}),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const body =
|
|
266
|
+
props.kind === "application"
|
|
267
|
+
? {
|
|
268
|
+
application: {
|
|
269
|
+
...base,
|
|
270
|
+
// L'URL de production designe la fiche cote serveur : vide, on
|
|
271
|
+
// ne l'envoie pas, sinon l'app ne se retrouverait plus.
|
|
272
|
+
...(app.prod_url ? { prod_url: app.prod_url } : {}),
|
|
273
|
+
staging_url: app.staging_url || null,
|
|
274
|
+
github_front_url: app.github_front_url || null,
|
|
275
|
+
categories: app.categories,
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
: {
|
|
279
|
+
subject: {
|
|
280
|
+
...base,
|
|
281
|
+
id: props.item.id,
|
|
282
|
+
relative_path: subject.relative_path,
|
|
283
|
+
keywords: subject.keywords || null,
|
|
284
|
+
is_specialized_report: subject.is_specialized_report,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await $fetch(endpoint, { method: "PATCH", body });
|
|
290
|
+
notify(isFr.value ? "Métadonnées enregistrées" : "Metadata saved");
|
|
291
|
+
open.value = false;
|
|
292
|
+
emit("saved");
|
|
293
|
+
} catch (err: any) {
|
|
294
|
+
error.value = err?.data?.message || err?.message || (isFr.value ? "Enregistrement impossible" : "Could not save");
|
|
295
|
+
} finally {
|
|
296
|
+
saving.value = false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
</script>
|
|
300
|
+
|
|
301
|
+
<style scoped>
|
|
302
|
+
.meta_edit_tabs {
|
|
303
|
+
padding: 0 24px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.meta_edit_tabs :deep(.rm-tabbar) {
|
|
307
|
+
max-width: 100%;
|
|
308
|
+
margin-bottom: 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.meta_edit_title {
|
|
312
|
+
font-size: 16px;
|
|
313
|
+
font-weight: 600;
|
|
314
|
+
margin: 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.meta_edit_body {
|
|
318
|
+
display: flex;
|
|
319
|
+
flex-direction: column;
|
|
320
|
+
gap: 24px;
|
|
321
|
+
padding: 8px 24px 16px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.meta_edit_section {
|
|
325
|
+
display: flex;
|
|
326
|
+
flex-direction: column;
|
|
327
|
+
gap: 12px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.meta_edit_eyebrow {
|
|
331
|
+
color: var(--text-secondary);
|
|
332
|
+
font-size: 12px;
|
|
333
|
+
font-weight: 600;
|
|
334
|
+
letter-spacing: 0.04em;
|
|
335
|
+
margin: 0;
|
|
336
|
+
text-transform: uppercase;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.meta_edit_hint {
|
|
340
|
+
color: var(--text-secondary);
|
|
341
|
+
font-size: 13px;
|
|
342
|
+
margin: 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.meta_edit_error {
|
|
346
|
+
color: var(--color-danger, #d64545);
|
|
347
|
+
font-size: 13px;
|
|
348
|
+
margin: 0;
|
|
349
|
+
}
|
|
350
|
+
</style>
|