@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,419 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<RmModal
|
|
3
|
+
v-model="open"
|
|
4
|
+
size="lg"
|
|
5
|
+
mode="free"
|
|
6
|
+
:label="isFr ? 'Modifier la source' : 'Edit source'"
|
|
7
|
+
>
|
|
8
|
+
<template #header>
|
|
9
|
+
<h2 class="source_edit_title">{{ isFr ? "Modifier la source" : "Edit source" }}</h2>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<template #tabs>
|
|
13
|
+
<div class="source_edit_tabs">
|
|
14
|
+
<RmTabBar
|
|
15
|
+
v-model="activeLang"
|
|
16
|
+
:tabs="langTabs"
|
|
17
|
+
:aria-label="isFr ? 'Langue de la fiche' : 'Card language'"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<div class="source_edit_body">
|
|
23
|
+
|
|
24
|
+
<section class="source_edit_section">
|
|
25
|
+
<p class="source_edit_eyebrow">{{ isFr ? "Source" : "Source" }}</p>
|
|
26
|
+
|
|
27
|
+
<div v-if="!activeTranslation" class="source_edit_fallback">
|
|
28
|
+
<p class="source_edit_hint">
|
|
29
|
+
{{ sourceFallback
|
|
30
|
+
? (isFr
|
|
31
|
+
? `Rien dans cette langue. La fiche affiche la version ${langLabel(sourceFallback)}.`
|
|
32
|
+
: `Nothing in this language. The card shows the ${langLabel(sourceFallback)} version.`)
|
|
33
|
+
: (isFr
|
|
34
|
+
? "Rien dans cette langue. Elle sera créée à l'enregistrement."
|
|
35
|
+
: "Nothing in this language. It will be created on save.") }}
|
|
36
|
+
</p>
|
|
37
|
+
<RmButton v-if="sourceFallback" variant="text" @click="copySourceFallback">
|
|
38
|
+
{{ isFr ? "Partir de cette version" : "Start from that version" }}
|
|
39
|
+
</RmButton>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<RmInput
|
|
43
|
+
v-model="form.source_name"
|
|
44
|
+
:label="isFr ? 'Nom de la source' : 'Source name'"
|
|
45
|
+
/>
|
|
46
|
+
<RmRichText
|
|
47
|
+
v-model="form.source_description"
|
|
48
|
+
:label="isFr ? 'Description de la source' : 'Source description'"
|
|
49
|
+
:rows="6"
|
|
50
|
+
:lang="isFr ? 'fr' : 'en'"
|
|
51
|
+
/>
|
|
52
|
+
<RmInput
|
|
53
|
+
v-model="form.source_link"
|
|
54
|
+
:label="isFr ? 'Lien' : 'Link'"
|
|
55
|
+
placeholder="https://"
|
|
56
|
+
/>
|
|
57
|
+
<RmDropdownSelect
|
|
58
|
+
v-model="frequencyLabel"
|
|
59
|
+
:label="isFr ? 'Fréquence de mise à jour' : 'Update frequency'"
|
|
60
|
+
:options="frequencyLabels"
|
|
61
|
+
:search="false"
|
|
62
|
+
:lang="isFr ? 'fr' : 'en'"
|
|
63
|
+
/>
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
<section class="source_edit_section">
|
|
67
|
+
<p class="source_edit_eyebrow">{{ isFr ? "Fournisseur" : "Provider" }}</p>
|
|
68
|
+
<p class="source_edit_hint">
|
|
69
|
+
{{ isFr
|
|
70
|
+
? "Le fournisseur porte toutes ses sources : le corriger ici change le titre de chacune d'elles."
|
|
71
|
+
: "The provider carries all its sources: editing it here changes every one of their titles." }}
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
<div v-if="!activeProviderTranslation" class="source_edit_fallback">
|
|
75
|
+
<p class="source_edit_hint">
|
|
76
|
+
{{ providerFallback
|
|
77
|
+
? (isFr
|
|
78
|
+
? `Rien dans cette langue. La fiche affiche la version ${langLabel(providerFallback)}.`
|
|
79
|
+
: `Nothing in this language. The card shows the ${langLabel(providerFallback)} version.`)
|
|
80
|
+
: (isFr
|
|
81
|
+
? "Rien dans cette langue. Elle sera créée à l'enregistrement."
|
|
82
|
+
: "Nothing in this language. It will be created on save.") }}
|
|
83
|
+
</p>
|
|
84
|
+
<RmButton v-if="providerFallback" variant="text" @click="copyProviderFallback">
|
|
85
|
+
{{ isFr ? "Partir de cette version" : "Start from that version" }}
|
|
86
|
+
</RmButton>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<RmInput
|
|
90
|
+
v-model="form.provider_name"
|
|
91
|
+
:label="isFr ? 'Nom du fournisseur' : 'Provider name'"
|
|
92
|
+
/>
|
|
93
|
+
<RmRichText
|
|
94
|
+
v-model="form.provider_description"
|
|
95
|
+
:label="isFr ? 'Description du fournisseur' : 'Provider description'"
|
|
96
|
+
:rows="6"
|
|
97
|
+
:lang="isFr ? 'fr' : 'en'"
|
|
98
|
+
/>
|
|
99
|
+
</section>
|
|
100
|
+
|
|
101
|
+
<p v-if="error" class="source_edit_error">{{ error }}</p>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<template #footer>
|
|
105
|
+
<RmButton variant="tertiary" @click="open = false">
|
|
106
|
+
{{ isFr ? "Annuler" : "Cancel" }}
|
|
107
|
+
</RmButton>
|
|
108
|
+
<RmButton variant="primary" :disabled="saving" @click="save">
|
|
109
|
+
{{ saving ? (isFr ? "Enregistrement..." : "Saving...") : (isFr ? "Enregistrer" : "Save") }}
|
|
110
|
+
</RmButton>
|
|
111
|
+
</template>
|
|
112
|
+
</RmModal>
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<script setup lang="ts">
|
|
116
|
+
const sourcesEndpoint = useAppConfig().oip.panels.Sources.endpoint;
|
|
117
|
+
/**
|
|
118
|
+
* Correction d'une fiche de source, reservee aux superadmins.
|
|
119
|
+
*
|
|
120
|
+
* Trois collections Directus derriere le meme formulaire : la fiche (lien,
|
|
121
|
+
* frequence), sa traduction dans la langue choisie (nom, description) et celle
|
|
122
|
+
* du fournisseur. Le rail n'affiche qu'une langue a la fois, mais la correction
|
|
123
|
+
* porte souvent sur la langue qu'on ne lit pas : le selecteur reste.
|
|
124
|
+
*
|
|
125
|
+
* La liste de langues est fermee aux deux langues que cette app sert, le
|
|
126
|
+
* francais et l'anglais. Elle ne vient pas de la table `languages` de Directus,
|
|
127
|
+
* qui porte des codes abimes (un `ja-JP` suivi d'une tabulation) qu'il ne faut
|
|
128
|
+
* pas propager, et ouvrir une langue que l'app n'affiche pas reviendrait a
|
|
129
|
+
* faire ecrire du texte que personne ne lira.
|
|
130
|
+
*
|
|
131
|
+
* Une langue sans traduction n'est pas cachee : le referentiel est presque
|
|
132
|
+
* entierement anglais (741 traductions de source sur 819), et une source
|
|
133
|
+
* internationale n'a souvent que ca. Le formulaire s'ouvre vide et la ligne
|
|
134
|
+
* est creee a l'enregistrement.
|
|
135
|
+
*
|
|
136
|
+
* Les descriptions sont du HTML redactionnel Directus : elles passent par
|
|
137
|
+
* RmRichText, qui les montre mises en forme plutot qu'en balises et en entites
|
|
138
|
+
* (`é`). Le bouton « HTML » du champ reste la pour les cas tordus.
|
|
139
|
+
*/
|
|
140
|
+
const props = defineProps<{ source: any; lang: string }>();
|
|
141
|
+
const emit = defineEmits<{ saved: [] }>();
|
|
142
|
+
|
|
143
|
+
const open = defineModel<boolean>({ default: false });
|
|
144
|
+
|
|
145
|
+
const isFr = computed(() => props.lang === "fr");
|
|
146
|
+
|
|
147
|
+
const FREQUENCIES: Record<string, { fr: string; en: string }> = {
|
|
148
|
+
daily: { fr: "Quotidienne", en: "Daily" },
|
|
149
|
+
weekly: { fr: "Hebdomadaire", en: "Weekly" },
|
|
150
|
+
monthly: { fr: "Mensuelle", en: "Monthly" },
|
|
151
|
+
bimonthly: { fr: "Bimestrielle", en: "Bi-monthly" },
|
|
152
|
+
quarterly: { fr: "Trimestrielle", en: "Quarterly" },
|
|
153
|
+
biannually: { fr: "Semestrielle", en: "Biannually" },
|
|
154
|
+
annually: { fr: "Annuelle", en: "Annually" },
|
|
155
|
+
irregular: { fr: "Irrégulière", en: "Irregular" },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Anglais et francais d'abord : ce sont les deux langues que les apps servent.
|
|
160
|
+
* Les quatre suivantes sont optionnelles, presentes pour les referentiels qui
|
|
161
|
+
* les portent deja.
|
|
162
|
+
*/
|
|
163
|
+
const LANGUAGES = [
|
|
164
|
+
{ value: "fr-FR", name: "Français" },
|
|
165
|
+
{ value: "en-US", name: "English" },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const translations = computed<any[]>(() =>
|
|
169
|
+
Array.isArray(props.source?.translations) ? props.source.translations : []
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Les codes de langue de Directus portent parfois une tabulation finale
|
|
173
|
+
// (`ja-JP\t`) : sans nettoyage, la ligne existante reste introuvable et on en
|
|
174
|
+
// creerait une seconde a cote.
|
|
175
|
+
const langCode = (t: any) => String(t?.languages_code || "").trim();
|
|
176
|
+
|
|
177
|
+
const langTabs = LANGUAGES;
|
|
178
|
+
|
|
179
|
+
const activeLang = ref("");
|
|
180
|
+
|
|
181
|
+
const form = reactive({
|
|
182
|
+
source_name: "",
|
|
183
|
+
source_description: "",
|
|
184
|
+
source_link: "",
|
|
185
|
+
provider_name: "",
|
|
186
|
+
provider_description: "",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const frequencyLabels = computed(() =>
|
|
190
|
+
Object.values(FREQUENCIES).map((f) => (isFr.value ? f.fr : f.en))
|
|
191
|
+
);
|
|
192
|
+
const frequencyLabel = ref("");
|
|
193
|
+
|
|
194
|
+
function frequencyKeyOf(label: string) {
|
|
195
|
+
const entry = Object.entries(FREQUENCIES).find(
|
|
196
|
+
([, f]) => f.fr === label || f.en === label
|
|
197
|
+
);
|
|
198
|
+
return entry?.[0] ?? null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const providerTranslations = computed<any[]>(() =>
|
|
202
|
+
Array.isArray(props.source?.provider?.translations)
|
|
203
|
+
? props.source.provider.translations
|
|
204
|
+
: []
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const activeTranslation = computed(() =>
|
|
208
|
+
translations.value.find((t) => langCode(t) === activeLang.value)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const activeProviderTranslation = computed(() =>
|
|
212
|
+
providerTranslations.value.find((t) => langCode(t) === activeLang.value)
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Ce que la fiche affiche a la place quand la langue choisie est vide.
|
|
217
|
+
*
|
|
218
|
+
* Le panneau retombe sur la premiere traduction venue, en pratique l'anglais :
|
|
219
|
+
* 5 fournisseurs sur 300 ont une fiche francaise, 27 sources sur 741. Sans le
|
|
220
|
+
* dire, un formulaire vide devant une carte pleine se lit comme une panne.
|
|
221
|
+
*/
|
|
222
|
+
function fallbackOf(rows: any[]) {
|
|
223
|
+
return (
|
|
224
|
+
rows.find((t) => langCode(t) === "en-US") ||
|
|
225
|
+
rows.find((t) => langCode(t) !== activeLang.value) ||
|
|
226
|
+
null
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const sourceFallback = computed(() =>
|
|
231
|
+
activeTranslation.value ? null : fallbackOf(translations.value)
|
|
232
|
+
);
|
|
233
|
+
const providerFallback = computed(() =>
|
|
234
|
+
activeProviderTranslation.value ? null : fallbackOf(providerTranslations.value)
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* « la version anglaise » se lit mieux que « la version EN » dans une phrase.
|
|
239
|
+
* Les langues absentes des onglets restent listees : le referentiel est partage
|
|
240
|
+
* avec les autres apps pays, une fiche peut retomber sur une langue qu'on
|
|
241
|
+
* n'edite pas ici, et il vaut mieux la nommer que montrer un code.
|
|
242
|
+
*/
|
|
243
|
+
const LANG_NAMES: Record<string, { fr: string; en: string }> = {
|
|
244
|
+
"en-US": { fr: "anglaise", en: "English" },
|
|
245
|
+
"fr-FR": { fr: "française", en: "French" },
|
|
246
|
+
"es-ES": { fr: "espagnole", en: "Spanish" },
|
|
247
|
+
"de-DE": { fr: "allemande", en: "German" },
|
|
248
|
+
"ja-JP": { fr: "japonaise", en: "Japanese" },
|
|
249
|
+
"ko-KR": { fr: "coréenne", en: "Korean" },
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const langLabel = (translation: any) => {
|
|
253
|
+
const code = langCode(translation);
|
|
254
|
+
const name = LANG_NAMES[code];
|
|
255
|
+
if (!name) return code;
|
|
256
|
+
return isFr.value ? name.fr : name.en;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
function copySourceFallback() {
|
|
260
|
+
form.source_name = sourceFallback.value?.source_name ?? "";
|
|
261
|
+
form.source_description = sourceFallback.value?.source_description ?? "";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function copyProviderFallback() {
|
|
265
|
+
form.provider_name = providerFallback.value?.provider_name ?? "";
|
|
266
|
+
form.provider_description = providerFallback.value?.provider_description ?? "";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function loadTranslation() {
|
|
270
|
+
const t = activeTranslation.value;
|
|
271
|
+
form.source_name = t?.source_name ?? "";
|
|
272
|
+
form.source_description = t?.source_description ?? "";
|
|
273
|
+
const pt = activeProviderTranslation.value;
|
|
274
|
+
form.provider_name = pt?.provider_name ?? "";
|
|
275
|
+
form.provider_description = pt?.provider_description ?? "";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function reset() {
|
|
279
|
+
// La langue du rail si elle est ouverte a la saisie, l'anglais sinon : c'est
|
|
280
|
+
// celle qui porte presque toutes les fiches.
|
|
281
|
+
activeLang.value =
|
|
282
|
+
LANGUAGES.find((l) => l.value.startsWith(props.lang))?.value || "en-US";
|
|
283
|
+
loadTranslation();
|
|
284
|
+
form.source_link = props.source?.source_link ?? "";
|
|
285
|
+
const freq = String(props.source?.update_frequency || "").trim().toLowerCase();
|
|
286
|
+
frequencyLabel.value = FREQUENCIES[freq]
|
|
287
|
+
? isFr.value
|
|
288
|
+
? FREQUENCIES[freq].fr
|
|
289
|
+
: FREQUENCIES[freq].en
|
|
290
|
+
: "";
|
|
291
|
+
error.value = "";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
watch(open, (value) => value && reset());
|
|
295
|
+
watch(activeLang, loadTranslation);
|
|
296
|
+
|
|
297
|
+
const saving = ref(false);
|
|
298
|
+
const error = ref("");
|
|
299
|
+
|
|
300
|
+
/** Une ligne existante se corrige par son id, une ligne absente se cree. */
|
|
301
|
+
function target(translation: any) {
|
|
302
|
+
return translation?.id
|
|
303
|
+
? { id: Number(translation.id) }
|
|
304
|
+
: { languages_code: activeLang.value };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function save() {
|
|
308
|
+
saving.value = true;
|
|
309
|
+
error.value = "";
|
|
310
|
+
try {
|
|
311
|
+
await $fetch(sourcesEndpoint, {
|
|
312
|
+
method: "PATCH",
|
|
313
|
+
body: {
|
|
314
|
+
uuid: props.source?.source_uuid,
|
|
315
|
+
source: {
|
|
316
|
+
source_link: form.source_link || null,
|
|
317
|
+
update_frequency: frequencyKeyOf(frequencyLabel.value),
|
|
318
|
+
},
|
|
319
|
+
providerUuid: props.source?.provider?.provider_uuid,
|
|
320
|
+
// Une langue laissee vide n'est pas creee : une ligne sans nom ni
|
|
321
|
+
// description ferait une fiche muette dans le referentiel.
|
|
322
|
+
...(activeTranslation.value || form.source_name || form.source_description
|
|
323
|
+
? {
|
|
324
|
+
translation: {
|
|
325
|
+
...target(activeTranslation.value),
|
|
326
|
+
source_name: form.source_name,
|
|
327
|
+
source_description: form.source_description,
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
: {}),
|
|
331
|
+
...(activeProviderTranslation.value || form.provider_name || form.provider_description
|
|
332
|
+
? {
|
|
333
|
+
providerTranslation: {
|
|
334
|
+
...target(activeProviderTranslation.value),
|
|
335
|
+
provider_name: form.provider_name,
|
|
336
|
+
provider_description: form.provider_description,
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
: {}),
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
open.value = false;
|
|
343
|
+
emit("saved");
|
|
344
|
+
} catch (err: any) {
|
|
345
|
+
error.value =
|
|
346
|
+
err?.data?.message ||
|
|
347
|
+
err?.message ||
|
|
348
|
+
(isFr.value ? "Enregistrement impossible" : "Could not save");
|
|
349
|
+
} finally {
|
|
350
|
+
saving.value = false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
</script>
|
|
354
|
+
|
|
355
|
+
<style scoped>
|
|
356
|
+
/*
|
|
357
|
+
* RmTabBar est dimensionne pour l'en-tete d'une page de detail : marge basse et
|
|
358
|
+
* largeur bridee pour laisser respirer le contenu autour. Dans une modale, ces
|
|
359
|
+
* marges appartiennent au conteneur, qui pose deja ses propres retraits.
|
|
360
|
+
*/
|
|
361
|
+
.source_edit_tabs {
|
|
362
|
+
padding: 0 24px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.source_edit_tabs :deep(.rm-tabbar) {
|
|
366
|
+
max-width: 100%;
|
|
367
|
+
margin-bottom: 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.source_edit_title {
|
|
371
|
+
font-size: 16px;
|
|
372
|
+
font-weight: 600;
|
|
373
|
+
margin: 0;
|
|
374
|
+
color: var(--text-primary);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.source_edit_body {
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
gap: 24px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.source_edit_section {
|
|
384
|
+
display: flex;
|
|
385
|
+
flex-direction: column;
|
|
386
|
+
gap: 16px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.source_edit_eyebrow {
|
|
390
|
+
margin: 0;
|
|
391
|
+
font-size: 12px;
|
|
392
|
+
font-weight: 600;
|
|
393
|
+
text-transform: uppercase;
|
|
394
|
+
letter-spacing: 0.04em;
|
|
395
|
+
color: var(--text-muted);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.source_edit_fallback {
|
|
399
|
+
align-items: baseline;
|
|
400
|
+
background: var(--bg-surface-2);
|
|
401
|
+
border-radius: 8px;
|
|
402
|
+
display: flex;
|
|
403
|
+
gap: 8px;
|
|
404
|
+
justify-content: space-between;
|
|
405
|
+
padding: 8px 12px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.source_edit_hint {
|
|
409
|
+
margin: 0;
|
|
410
|
+
font-size: 12px;
|
|
411
|
+
color: var(--text-muted);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.source_edit_error {
|
|
415
|
+
margin: 0;
|
|
416
|
+
font-size: 13px;
|
|
417
|
+
color: var(--color-danger);
|
|
418
|
+
}
|
|
419
|
+
</style>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
OipSourceGroups (layer OIP) - la liste de sources d'un panneau « Sources », en deux
|
|
3
|
+
sections.
|
|
4
|
+
|
|
5
|
+
La liste d'uuids d'un ecran est ordonnee : la premiere source est celle qui
|
|
6
|
+
porte l'ecran, les suivantes ne font que la completer. Les separer, ouvrir la
|
|
7
|
+
principale et la cadrer en couleur evite de chercher laquelle regarder.
|
|
8
|
+
|
|
9
|
+
Quand aucune source n'est designee (`primary-uuid` vide sur une page qui les
|
|
10
|
+
agrege toutes, cf. Company dans Drug Statistics) la liste reste plate : les
|
|
11
|
+
jeux se valent.
|
|
12
|
+
-->
|
|
13
|
+
<template>
|
|
14
|
+
<ClientOnly>
|
|
15
|
+
<template v-if="safeUuids.length">
|
|
16
|
+
<template v-if="resolvedPrimary">
|
|
17
|
+
<section class="src_group src_primary">
|
|
18
|
+
<p class="src_group_title">
|
|
19
|
+
{{ lang === 'fr' ? 'Source principale' : 'Primary source' }}
|
|
20
|
+
</p>
|
|
21
|
+
<OipDirectusSources
|
|
22
|
+
:uuids="[resolvedPrimary]"
|
|
23
|
+
:source-to-open="resolvedPrimary"
|
|
24
|
+
:lang="lang"
|
|
25
|
+
highlight
|
|
26
|
+
:editable="editable"
|
|
27
|
+
/>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<section v-if="resolvedSecondaries.length" class="src_group">
|
|
31
|
+
<p class="src_group_title">
|
|
32
|
+
{{ lang === 'fr' ? 'Sources secondaires' : 'Additional sources' }}
|
|
33
|
+
</p>
|
|
34
|
+
<OipDirectusSources :uuids="resolvedSecondaries" :lang="lang" :editable="editable" />
|
|
35
|
+
</section>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<OipDirectusSources v-else :uuids="safeUuids" :lang="lang" :editable="editable" />
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<div v-else class="source_placeholder">
|
|
42
|
+
{{ lang === 'fr' ? 'Aucune source pour cette page' : 'No source for this page' }}
|
|
43
|
+
</div>
|
|
44
|
+
</ClientOnly>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup lang="ts">
|
|
48
|
+
const props = withDefaults(
|
|
49
|
+
defineProps<{
|
|
50
|
+
uuids: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Source mise en avant. Par defaut la premiere de la liste ; une chaine
|
|
53
|
+
* vide explicite rend la liste plate.
|
|
54
|
+
*/
|
|
55
|
+
primaryUuid?: string | null;
|
|
56
|
+
lang?: string;
|
|
57
|
+
/** Crayon de correction sur chaque fiche (superadmin). */
|
|
58
|
+
editable?: boolean;
|
|
59
|
+
}>(),
|
|
60
|
+
{ primaryUuid: undefined, lang: 'en', editable: false },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Les uuids viennent de tables ecrites a la main : une entree vide sortirait
|
|
64
|
+
// une fiche « Chargement... » qui ne se remplit jamais.
|
|
65
|
+
const safeUuids = computed<string[]>(() =>
|
|
66
|
+
(Array.isArray(props.uuids) ? props.uuids : []).filter(
|
|
67
|
+
(uuid): uuid is string => typeof uuid === 'string' && uuid.length > 0,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const resolvedPrimary = computed(() =>
|
|
72
|
+
props.primaryUuid === undefined ? (safeUuids.value[0] ?? '') : (props.primaryUuid ?? ''),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const resolvedSecondaries = computed(() =>
|
|
76
|
+
safeUuids.value.filter((uuid) => uuid !== resolvedPrimary.value),
|
|
77
|
+
);
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<style scoped>
|
|
81
|
+
.src_group + .src_group {
|
|
82
|
+
margin-top: 24px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.src_group_title {
|
|
86
|
+
margin-bottom: 8px;
|
|
87
|
+
color: var(--medium);
|
|
88
|
+
font-size: 11px;
|
|
89
|
+
font-weight: 700;
|
|
90
|
+
letter-spacing: 0.04em;
|
|
91
|
+
text-transform: uppercase;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* La source principale se distingue par son cadre, pas par un traitement de
|
|
95
|
+
texte : le cadre est pose par la prop `highlight` de RmSourceCard, ici il ne
|
|
96
|
+
reste que le titre de section. */
|
|
97
|
+
.src_primary .src_group_title {
|
|
98
|
+
color: var(--primary);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.source_placeholder {
|
|
102
|
+
padding: 8px;
|
|
103
|
+
color: var(--dark);
|
|
104
|
+
font-size: 12px;
|
|
105
|
+
line-height: 14px;
|
|
106
|
+
}
|
|
107
|
+
</style>
|