@tekkare/romulus 0.1.134 → 0.1.136
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 +8 -2
- package/dist/runtime/composables/useRmCardActions.d.ts +8 -1
- package/dist/runtime/composables/useRmCardActions.js +6 -1
- package/dist/runtime/composables/useRmExport.js +48 -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,13 +152,18 @@ 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',
|
|
157
161
|
range: 'all',
|
|
158
162
|
sections: [],
|
|
159
163
|
columns: [],
|
|
160
|
-
|
|
164
|
+
// Sans ce drapeau la page de garde taisait les sources, meme
|
|
165
|
+
// fournies : le generateur ne les imprime que si on les demande.
|
|
166
|
+
options: ['sources'],
|
|
161
167
|
fileName: stampedName.value,
|
|
162
168
|
layout: '',
|
|
163
169
|
schedule: 'once',
|
|
@@ -168,7 +174,7 @@ async function write() {
|
|
|
168
174
|
title: props.title || buttonLabel.value,
|
|
169
175
|
subtitle: props.subtitle,
|
|
170
176
|
kpi: props.kpi,
|
|
171
|
-
sources: props.sources,
|
|
177
|
+
sources: props.sources.length ? props.sources : defaults.sources,
|
|
172
178
|
sheets: props.sheets,
|
|
173
179
|
limit: props.limit,
|
|
174
180
|
},
|
|
@@ -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,29 @@ function longDate(lang) {
|
|
|
11
11
|
day: "numeric"
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}/;
|
|
15
|
+
function sourceDate(source, lang) {
|
|
16
|
+
if (!source.date) return null;
|
|
17
|
+
if (typeof source.date === "string" && !ISO_DATE.test(source.date)) return source.date;
|
|
18
|
+
const parsed = new Date(source.date);
|
|
19
|
+
if (Number.isNaN(parsed.getTime())) return String(source.date);
|
|
20
|
+
return parsed.toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
21
|
+
year: "numeric",
|
|
22
|
+
month: "long",
|
|
23
|
+
day: "numeric"
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function sourceLabel(source, lang) {
|
|
27
|
+
const date = sourceDate(source, lang);
|
|
28
|
+
return date ? `${source.name} (${date})` : source.name;
|
|
29
|
+
}
|
|
30
|
+
function sourcesOf(config, payload) {
|
|
31
|
+
const wanted = config.sections.includes("sources") || config.options.includes("sources");
|
|
32
|
+
return wanted ? payload.sources ?? [] : [];
|
|
33
|
+
}
|
|
34
|
+
function escapeXml(text) {
|
|
35
|
+
return text.replace(/[<>&"']/g, (char) => ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[char] ?? char);
|
|
36
|
+
}
|
|
14
37
|
function rowsOf(sheet, limit) {
|
|
15
38
|
return limit && sheet.data.length > limit ? sheet.data.slice(0, limit) : sheet.data;
|
|
16
39
|
}
|
|
@@ -187,15 +210,11 @@ export function useRmExport() {
|
|
|
187
210
|
(filter) => line(`${filter.label} : ${filter.value}`, { fontSize: 16, color: "#b2b2b2" })
|
|
188
211
|
);
|
|
189
212
|
}
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
213
|
+
const sources = sourcesOf(config, payload);
|
|
214
|
+
if (sources.length) {
|
|
192
215
|
rows.push(filler());
|
|
193
|
-
|
|
194
|
-
const date =
|
|
195
|
-
year: "numeric",
|
|
196
|
-
month: "long",
|
|
197
|
-
day: "numeric"
|
|
198
|
-
}) : null;
|
|
216
|
+
sources.forEach((source) => {
|
|
217
|
+
const date = sourceDate(source, lang);
|
|
199
218
|
const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
|
|
200
219
|
line(`${source.emoji ? `${source.emoji} ` : ""}Source : ${source.name}${update}`, { fontSize: 18 });
|
|
201
220
|
});
|
|
@@ -237,6 +256,16 @@ export function useRmExport() {
|
|
|
237
256
|
);
|
|
238
257
|
blocks.push(lines.join("\n"));
|
|
239
258
|
});
|
|
259
|
+
const sources = sourcesOf(config, payload);
|
|
260
|
+
if (sources.length) {
|
|
261
|
+
blocks.push(
|
|
262
|
+
sources.map((source) => {
|
|
263
|
+
const date = sourceDate(source, lang);
|
|
264
|
+
const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
|
|
265
|
+
return `# Source : ${source.name}${update}`;
|
|
266
|
+
}).join("\n")
|
|
267
|
+
);
|
|
268
|
+
}
|
|
240
269
|
const fileName = `${config.fileName}.csv`;
|
|
241
270
|
const blob = new Blob([`\uFEFF${blocks.join("\n\n")}
|
|
242
271
|
`], { type: "text/csv;charset=utf-8" });
|
|
@@ -247,7 +276,8 @@ export function useRmExport() {
|
|
|
247
276
|
if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
|
|
248
277
|
const scale = 2;
|
|
249
278
|
const pad = 24;
|
|
250
|
-
const
|
|
279
|
+
const sources = sourcesOf(config, payload);
|
|
280
|
+
const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
|
|
251
281
|
const parts = charts.map((svg) => serialize(svg));
|
|
252
282
|
const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
|
|
253
283
|
const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
|
|
@@ -265,6 +295,9 @@ export function useRmExport() {
|
|
|
265
295
|
context.fillStyle = "#6C6C8A";
|
|
266
296
|
context.font = "12px Figtree, Helvetica, sans-serif";
|
|
267
297
|
context.fillText(`${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}`, pad, 52);
|
|
298
|
+
if (sources.length) {
|
|
299
|
+
context.fillText(`Source${sources.length > 1 ? "s" : ""} : ${sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ")}`, pad, 68);
|
|
300
|
+
}
|
|
268
301
|
}
|
|
269
302
|
let y = head + pad;
|
|
270
303
|
for (const part of parts) {
|
|
@@ -282,7 +315,8 @@ export function useRmExport() {
|
|
|
282
315
|
const charts = (payload.charts ?? []).map((chart) => svgOf(chart.el)).filter(Boolean);
|
|
283
316
|
if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
|
|
284
317
|
const pad = 24;
|
|
285
|
-
const
|
|
318
|
+
const sources = sourcesOf(config, payload);
|
|
319
|
+
const head = config.options.includes("logo") ? sources.length ? 88 : 72 : 0;
|
|
286
320
|
const parts = charts.map((svg) => serialize(svg));
|
|
287
321
|
const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
|
|
288
322
|
const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
|
|
@@ -292,7 +326,7 @@ export function useRmExport() {
|
|
|
292
326
|
y += part.height + pad;
|
|
293
327
|
return markup2;
|
|
294
328
|
}).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>` : "";
|
|
329
|
+
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
330
|
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
331
|
const fileName = `${config.fileName}.svg`;
|
|
298
332
|
const blob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" });
|
|
@@ -651,12 +685,12 @@ export function useRmExport() {
|
|
|
651
685
|
y = doc.lastAutoTable?.finalY ?? margin;
|
|
652
686
|
});
|
|
653
687
|
}
|
|
654
|
-
const
|
|
655
|
-
if (
|
|
688
|
+
const sources = sourcesOf(config, payload);
|
|
689
|
+
if (sources.length) {
|
|
656
690
|
doc.setTextColor(...grey);
|
|
657
691
|
doc.setFont("helvetica", "normal");
|
|
658
692
|
doc.setFontSize(8);
|
|
659
|
-
const names =
|
|
693
|
+
const names = sources.map((source) => sourceLabel(source, lang)).join(" \xB7 ");
|
|
660
694
|
const lines = doc.splitTextToSize(printable(`Sources : ${names}`), inner);
|
|
661
695
|
doc.text(lines, margin, Math.min(y + 8, doc.internal.pageSize.getHeight() - margin));
|
|
662
696
|
}
|
|
@@ -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
|
+
}
|