@tekkare/romulus 0.1.134 → 0.1.135
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/components/CardActions.vue +9 -1
- package/dist/runtime/components/ChartCard.vue +10 -0
- package/dist/runtime/components/ExportButton.vue +5 -1
- package/dist/runtime/composables/useRmCardActions.d.ts +8 -1
- package/dist/runtime/composables/useRmCardActions.js +6 -1
- package/dist/runtime/composables/useRmExport.js +46 -14
- package/dist/runtime/composables/useRmExportDefaults.d.ts +9 -0
- package/dist/runtime/composables/useRmExportDefaults.js +13 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
-->
|
|
24
24
|
<script setup lang="ts">
|
|
25
25
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
26
|
-
import type { RmExportPayload } from '../utils/export'
|
|
26
|
+
import type { RmExportPayload, RmExportSource } from '../utils/export'
|
|
27
27
|
import { rmCardCharts, rmCardOf } from '../utils/cardData'
|
|
28
28
|
import { useRmCardActions } from '../composables/useRmCardActions'
|
|
29
29
|
|
|
@@ -34,6 +34,12 @@ const props = withDefaults(defineProps<{
|
|
|
34
34
|
link?: string
|
|
35
35
|
/** Nom du fichier et de l'onglet. A defaut, le titre lu dans la carte. */
|
|
36
36
|
name?: string
|
|
37
|
+
/**
|
|
38
|
+
* Source citee par le pied de la carte. Dernier recours : la page
|
|
39
|
+
* (`payload.sources`) puis le cadre (`provideRmExportDefaults`) passent
|
|
40
|
+
* devant, parce qu'eux lisent le referentiel et sa date de mise a jour.
|
|
41
|
+
*/
|
|
42
|
+
sources?: RmExportSource[]
|
|
37
43
|
/**
|
|
38
44
|
* Ce que la page a de mieux que le rendu de la carte : ses onglets complets
|
|
39
45
|
* (`sheets`), mais aussi ses chiffres cles, ses sources, son sous-titre. Ce
|
|
@@ -54,6 +60,7 @@ const props = withDefaults(defineProps<{
|
|
|
54
60
|
lang: 'fr',
|
|
55
61
|
link: undefined,
|
|
56
62
|
name: undefined,
|
|
63
|
+
sources: undefined,
|
|
57
64
|
payload: undefined,
|
|
58
65
|
visual: undefined,
|
|
59
66
|
})
|
|
@@ -168,6 +175,7 @@ async function act(action: 'png' | 'svg' | 'csv' | 'xlsx' | 'link') {
|
|
|
168
175
|
lang: props.lang,
|
|
169
176
|
name: props.name,
|
|
170
177
|
link: props.link,
|
|
178
|
+
sources: props.sources,
|
|
171
179
|
payload: props.payload,
|
|
172
180
|
})
|
|
173
181
|
if (action === 'link') {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
-->
|
|
12
12
|
<script setup lang="ts">
|
|
13
13
|
import { computed } from 'vue'
|
|
14
|
+
import type { RmExportPayload } from '../utils/export'
|
|
14
15
|
import { rmDate } from '../utils/format'
|
|
15
16
|
|
|
16
17
|
const props = withDefaults(defineProps<{
|
|
@@ -37,6 +38,12 @@ const props = withDefaults(defineProps<{
|
|
|
37
38
|
* qui est le bon lien pour une carte d'analyse : elle vit dans sa page.
|
|
38
39
|
*/
|
|
39
40
|
link?: string
|
|
41
|
+
/**
|
|
42
|
+
* Ce que la page a de mieux que le rendu de la carte pour son export :
|
|
43
|
+
* onglets complets, sources, sous-titre (cf. RmCardActions). En fonction
|
|
44
|
+
* des qu'une liste monte la carte plusieurs fois.
|
|
45
|
+
*/
|
|
46
|
+
payload?: Partial<RmExportPayload> | (() => Partial<RmExportPayload> | null | undefined | Promise<Partial<RmExportPayload> | null | undefined>)
|
|
40
47
|
/**
|
|
41
48
|
* Etat de la donnee. Les trois variantes se posent dans le CORPS ; la tete
|
|
42
49
|
* et le pied restent stables, pour que le titre et la source restent sous
|
|
@@ -56,6 +63,7 @@ const props = withDefaults(defineProps<{
|
|
|
56
63
|
extra: undefined,
|
|
57
64
|
actions: true,
|
|
58
65
|
link: undefined,
|
|
66
|
+
payload: undefined,
|
|
59
67
|
state: 'ready',
|
|
60
68
|
retryLabel: '',
|
|
61
69
|
emptyActionLabel: '',
|
|
@@ -135,6 +143,8 @@ const verifiedText = computed(() => {
|
|
|
135
143
|
:lang="lang"
|
|
136
144
|
:link="link"
|
|
137
145
|
:name="context ? `${title} (${context})` : title"
|
|
146
|
+
:sources="[{ name: source, date: verifiedText }]"
|
|
147
|
+
:payload="payload"
|
|
138
148
|
@export-png="emit('exportPng')"
|
|
139
149
|
@export-svg="emit('exportSvg')"
|
|
140
150
|
@export-csv="emit('exportCsv')"
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
<script setup lang="ts">
|
|
43
43
|
import { computed, ref } from 'vue'
|
|
44
44
|
import { useRmExport } from '../composables/useRmExport'
|
|
45
|
+
import { rmExportDefaults } from '../composables/useRmExportDefaults'
|
|
45
46
|
import type { RmExportKpi, RmExportSheet, RmExportSource } from '../utils/export'
|
|
46
47
|
|
|
47
48
|
const props = withDefaults(
|
|
@@ -151,6 +152,9 @@ async function write() {
|
|
|
151
152
|
if (held.length) await Promise.all(held)
|
|
152
153
|
|
|
153
154
|
const { run } = useRmExport()
|
|
155
|
+
// Sans sources declarees, celles du cadre : la page les cite deja dans
|
|
156
|
+
// son panneau « Sources », le classeur ne doit pas sortir sans.
|
|
157
|
+
const defaults = rmExportDefaults()
|
|
154
158
|
const result = await run(
|
|
155
159
|
{
|
|
156
160
|
format: 'xlsx',
|
|
@@ -168,7 +172,7 @@ async function write() {
|
|
|
168
172
|
title: props.title || buttonLabel.value,
|
|
169
173
|
subtitle: props.subtitle,
|
|
170
174
|
kpi: props.kpi,
|
|
171
|
-
sources: props.sources,
|
|
175
|
+
sources: props.sources.length ? props.sources : defaults.sources,
|
|
172
176
|
sheets: props.sheets,
|
|
173
177
|
limit: props.limit,
|
|
174
178
|
},
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* d'une declaration de la page : c'est ce qui rend le menu utilisable sur les
|
|
13
13
|
* cartes deja ecrites, sans les rouvrir une par une.
|
|
14
14
|
*/
|
|
15
|
-
import type { RmExportPayload } from '../utils/export.js';
|
|
15
|
+
import type { RmExportPayload, RmExportSource } from '../utils/export.js';
|
|
16
16
|
/** Les actions qu'une carte propose. `link` copie, il ne telecharge pas. */
|
|
17
17
|
export type RmCardAction = 'png' | 'svg' | 'csv' | 'xlsx' | 'link';
|
|
18
18
|
export type RmCardPayload = Partial<RmExportPayload>;
|
|
@@ -24,6 +24,13 @@ export interface RmCardActionContext {
|
|
|
24
24
|
name?: string;
|
|
25
25
|
/** Lien copie par « Copier le lien direct ». A defaut, l'URL courante. */
|
|
26
26
|
link?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Ce que le pied de la carte dit de sa source. Ne sert que si ni la page
|
|
29
|
+
* (`payload`) ni le cadre (`provideRmExportDefaults`) n'ont mieux : le cadre
|
|
30
|
+
* lit le referentiel des sources, le pied d'une carte peut n'en porter
|
|
31
|
+
* qu'un libelle.
|
|
32
|
+
*/
|
|
33
|
+
sources?: RmExportSource[];
|
|
27
34
|
/**
|
|
28
35
|
* Ce que la page a de mieux que le rendu a l'ecran : ses onglets complets,
|
|
29
36
|
* ses chiffres cles, ses sources, son sous-titre. Ce qui est pose ici
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { rmCardCharts, rmCardSheets, rmCardTitle } from "../utils/cardData.js";
|
|
2
2
|
import { useRmExport } from "./useRmExport.js";
|
|
3
|
+
import { rmExportDefaults } from "./useRmExportDefaults.js";
|
|
3
4
|
const FILE_NAME_MAX = 60;
|
|
4
5
|
function fileNameOf(label) {
|
|
5
6
|
const flat = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -42,15 +43,19 @@ export function useRmCardActions() {
|
|
|
42
43
|
async function runCardAction(action, context) {
|
|
43
44
|
const { card, lang = "fr" } = context;
|
|
44
45
|
const provided = await (typeof context.payload === "function" ? context.payload() : context.payload) ?? {};
|
|
46
|
+
const defaults = rmExportDefaults();
|
|
45
47
|
const title = context.name || provided.title || rmCardTitle(card) || "Export";
|
|
48
|
+
const sources = provided.sources?.length ? provided.sources : defaults.sources?.length ? defaults.sources : context.sources;
|
|
46
49
|
const format = action === "link" ? "link" : action;
|
|
47
50
|
const sheets = action === "png" || action === "svg" ? [] : provided.sheets?.length ? provided.sheets : await rmCardSheets(card, title, lang);
|
|
48
51
|
return run(configOf(format, fileNameOf(title)), {
|
|
52
|
+
...defaults,
|
|
49
53
|
title,
|
|
50
54
|
charts: rmCardCharts(card).map((el) => ({ title, el })),
|
|
51
55
|
permalink: absolute(context.link),
|
|
52
56
|
...provided,
|
|
53
|
-
sheets
|
|
57
|
+
sheets,
|
|
58
|
+
sources
|
|
54
59
|
}, lang);
|
|
55
60
|
}
|
|
56
61
|
async function toggleCardFullscreen(card) {
|
|
@@ -11,6 +11,27 @@ function longDate(lang) {
|
|
|
11
11
|
day: "numeric"
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
|
+
function sourceDate(source, lang) {
|
|
15
|
+
if (!source.date) return null;
|
|
16
|
+
const parsed = new Date(source.date);
|
|
17
|
+
if (Number.isNaN(parsed.getTime())) return String(source.date);
|
|
18
|
+
return parsed.toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
19
|
+
year: "numeric",
|
|
20
|
+
month: "long",
|
|
21
|
+
day: "numeric"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function sourceLabel(source, lang) {
|
|
25
|
+
const date = sourceDate(source, lang);
|
|
26
|
+
return date ? `${source.name} (${date})` : source.name;
|
|
27
|
+
}
|
|
28
|
+
function sourcesOf(config, payload) {
|
|
29
|
+
const wanted = config.sections.includes("sources") || config.options.includes("sources");
|
|
30
|
+
return wanted ? payload.sources ?? [] : [];
|
|
31
|
+
}
|
|
32
|
+
function escapeXml(text) {
|
|
33
|
+
return text.replace(/[<>&"']/g, (char) => ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[char] ?? char);
|
|
34
|
+
}
|
|
14
35
|
function rowsOf(sheet, limit) {
|
|
15
36
|
return limit && sheet.data.length > limit ? sheet.data.slice(0, limit) : sheet.data;
|
|
16
37
|
}
|
|
@@ -187,15 +208,11 @@ export function useRmExport() {
|
|
|
187
208
|
(filter) => line(`${filter.label} : ${filter.value}`, { fontSize: 16, color: "#b2b2b2" })
|
|
188
209
|
);
|
|
189
210
|
}
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
211
|
+
const sources = sourcesOf(config, payload);
|
|
212
|
+
if (sources.length) {
|
|
192
213
|
rows.push(filler());
|
|
193
|
-
|
|
194
|
-
const date =
|
|
195
|
-
year: "numeric",
|
|
196
|
-
month: "long",
|
|
197
|
-
day: "numeric"
|
|
198
|
-
}) : null;
|
|
214
|
+
sources.forEach((source) => {
|
|
215
|
+
const date = sourceDate(source, lang);
|
|
199
216
|
const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
|
|
200
217
|
line(`${source.emoji ? `${source.emoji} ` : ""}Source : ${source.name}${update}`, { fontSize: 18 });
|
|
201
218
|
});
|
|
@@ -237,6 +254,16 @@ export function useRmExport() {
|
|
|
237
254
|
);
|
|
238
255
|
blocks.push(lines.join("\n"));
|
|
239
256
|
});
|
|
257
|
+
const sources = sourcesOf(config, payload);
|
|
258
|
+
if (sources.length) {
|
|
259
|
+
blocks.push(
|
|
260
|
+
sources.map((source) => {
|
|
261
|
+
const date = sourceDate(source, lang);
|
|
262
|
+
const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
|
|
263
|
+
return `# Source : ${source.name}${update}`;
|
|
264
|
+
}).join("\n")
|
|
265
|
+
);
|
|
266
|
+
}
|
|
240
267
|
const fileName = `${config.fileName}.csv`;
|
|
241
268
|
const blob = new Blob([`\uFEFF${blocks.join("\n\n")}
|
|
242
269
|
`], { type: "text/csv;charset=utf-8" });
|
|
@@ -247,7 +274,8 @@ export function useRmExport() {
|
|
|
247
274
|
if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
|
|
248
275
|
const scale = 2;
|
|
249
276
|
const pad = 24;
|
|
250
|
-
const
|
|
277
|
+
const sources = sourcesOf(config, payload);
|
|
278
|
+
const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
|
|
251
279
|
const parts = charts.map((svg) => serialize(svg));
|
|
252
280
|
const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
|
|
253
281
|
const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
|
|
@@ -265,6 +293,9 @@ export function useRmExport() {
|
|
|
265
293
|
context.fillStyle = "#6C6C8A";
|
|
266
294
|
context.font = "12px Figtree, Helvetica, sans-serif";
|
|
267
295
|
context.fillText(`${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}`, pad, 52);
|
|
296
|
+
if (sources.length) {
|
|
297
|
+
context.fillText(`Source${sources.length > 1 ? "s" : ""} : ${sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ")}`, pad, 68);
|
|
298
|
+
}
|
|
268
299
|
}
|
|
269
300
|
let y = head + pad;
|
|
270
301
|
for (const part of parts) {
|
|
@@ -282,7 +313,8 @@ export function useRmExport() {
|
|
|
282
313
|
const charts = (payload.charts ?? []).map((chart) => svgOf(chart.el)).filter(Boolean);
|
|
283
314
|
if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
|
|
284
315
|
const pad = 24;
|
|
285
|
-
const
|
|
316
|
+
const sources = sourcesOf(config, payload);
|
|
317
|
+
const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
|
|
286
318
|
const parts = charts.map((svg) => serialize(svg));
|
|
287
319
|
const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
|
|
288
320
|
const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
|
|
@@ -292,7 +324,7 @@ export function useRmExport() {
|
|
|
292
324
|
y += part.height + pad;
|
|
293
325
|
return markup2;
|
|
294
326
|
}).join("\n");
|
|
295
|
-
const header = head ? `<text x="${pad}" y="32" font-family="Figtree, Helvetica, sans-serif" font-size="20" font-weight="bold" fill="#1A1A2E">${BRAND_TITLE}</text><text x="${pad}" y="52" font-family="Figtree, Helvetica, sans-serif" font-size="12" fill="#6C6C8A">${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}</text>` : "";
|
|
327
|
+
const header = head ? `<text x="${pad}" y="32" font-family="Figtree, Helvetica, sans-serif" font-size="20" font-weight="bold" fill="#1A1A2E">${BRAND_TITLE}</text><text x="${pad}" y="52" font-family="Figtree, Helvetica, sans-serif" font-size="12" fill="#6C6C8A">${escapeXml(`${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}`)}</text>` + (sources.length ? `<text x="${pad}" y="68" font-family="Figtree, Helvetica, sans-serif" font-size="12" fill="#6C6C8A">${escapeXml(`Source${sources.length > 1 ? "s" : ""} : ${sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ")}`)}</text>` : "") : "";
|
|
296
328
|
const markup = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="${width}" height="${height}" fill="#FFFFFF"/>${header}${nested}</svg>`;
|
|
297
329
|
const fileName = `${config.fileName}.svg`;
|
|
298
330
|
const blob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" });
|
|
@@ -651,12 +683,12 @@ export function useRmExport() {
|
|
|
651
683
|
y = doc.lastAutoTable?.finalY ?? margin;
|
|
652
684
|
});
|
|
653
685
|
}
|
|
654
|
-
const
|
|
655
|
-
if (
|
|
686
|
+
const sources = sourcesOf(config, payload);
|
|
687
|
+
if (sources.length) {
|
|
656
688
|
doc.setTextColor(...grey);
|
|
657
689
|
doc.setFont("helvetica", "normal");
|
|
658
690
|
doc.setFontSize(8);
|
|
659
|
-
const names =
|
|
691
|
+
const names = sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ");
|
|
660
692
|
const lines = doc.splitTextToSize(printable(`Sources : ${names}`), inner);
|
|
661
693
|
doc.text(lines, margin, Math.min(y + 8, doc.internal.pageSize.getHeight() - margin));
|
|
662
694
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RmExportPayload } from '../utils/export.js';
|
|
2
|
+
export type RmExportDefaultsBuild = () => Partial<RmExportPayload> | null | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Pose les defauts de la page affichee. A appeler dans le `setup` du cadre
|
|
5
|
+
* (layout) : ils sont retires quand ce cadre est demonte.
|
|
6
|
+
*/
|
|
7
|
+
export declare function provideRmExportDefaults(build: RmExportDefaultsBuild): void;
|
|
8
|
+
/** Les defauts courants, relus a chaque export : ils suivent la navigation. */
|
|
9
|
+
export declare function rmExportDefaults(): Partial<RmExportPayload>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getCurrentScope, onScopeDispose, shallowRef } from "vue";
|
|
2
|
+
const registry = shallowRef(null);
|
|
3
|
+
export function provideRmExportDefaults(build) {
|
|
4
|
+
registry.value = build;
|
|
5
|
+
if (getCurrentScope()) {
|
|
6
|
+
onScopeDispose(() => {
|
|
7
|
+
if (registry.value === build) registry.value = null;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function rmExportDefaults() {
|
|
12
|
+
return registry.value?.() ?? {};
|
|
13
|
+
}
|