@sia-ui/cli 0.5.0 → 0.6.0
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/CHANGELOG.md +24 -0
- package/README.md +24 -0
- package/dist/index.js +30 -11
- package/dist/lifecycle.mjs +189 -4
- package/dist/lock.mjs +16 -2
- package/dist/registry/package/components/ActivityLog/index.tsx +241 -0
- package/dist/registry/package/components/ActivityLog/styles.css +31 -0
- package/dist/registry/package/components/Badge/index.tsx +58 -0
- package/dist/registry/package/components/CrudPage/dialogs.tsx +15 -2
- package/dist/registry/package/components/CrudPage/index.tsx +24 -22
- package/dist/registry/package/components/CrudPage/styles.css +7 -6
- package/dist/registry/package/components/DataTable/query.ts +23 -3
- package/dist/registry/package/components/DataTable/row-actions.tsx +1 -1
- package/dist/registry/package/components/Icons/index.tsx +88 -4
- package/dist/registry/package/components/PageHeader/index.tsx +28 -4
- package/dist/registry/package/components/PageHeader/styles.css +14 -2
- package/dist/registry/package/components/Resource/index.tsx +69 -19
- package/dist/registry/package/components/SecretFields/index.tsx +193 -0
- package/dist/registry/package/components/SecretFields/styles.css +84 -0
- package/dist/registry/registry.json +54 -6
- package/dist/registry/styles/sia-ui.css +1 -1
- package/dist/source.mjs +10 -0
- package/dist/styles.mjs +111 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @sia-ui/cli
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `sia-ui update`, la feuille commune suivie, et des erreurs HTTP en français.
|
|
8
|
+
- **`sia-ui update [nom...]`** — met à jour les entrées en retard sans perdre
|
|
9
|
+
de retouche : un fichier intact est remplacé, un fichier modifié depuis son
|
|
10
|
+
installation est gardé et signalé (`sia-ui diff`, puis `--force` pour
|
|
11
|
+
l'écraser). Les dépendances de registre apparues sont installées, les
|
|
12
|
+
paquets npm annoncés. `--dry-run` montre sans rien écrire. Une entrée dont
|
|
13
|
+
un fichier a été gardé reste « en retard » dans le verrou.
|
|
14
|
+
- **`sia-ui.css` suivie** — son empreinte est notée dans `sia-ui.lock.json`.
|
|
15
|
+
`update` la met à jour comme un composant ; `add` aussi, si elle n'a pas
|
|
16
|
+
été retouchée. `init` ne fait plus qu'initialiser. Elle est lue au même
|
|
17
|
+
registre que les composants, HTTP compris. `doctor` signale une feuille en
|
|
18
|
+
retard, modifiée ou copiée avant le suivi.
|
|
19
|
+
- `list --outdated` et `doctor` conseillent `sia-ui update` au lieu de
|
|
20
|
+
`add --overwrite`, qui écrasait aussi les retouches.
|
|
21
|
+
- **`statusMessages`** (`@sia-ui/api`) — option du client, avec
|
|
22
|
+
`FRENCH_STATUS_MESSAGES`. Remplace les messages génériques du serveur
|
|
23
|
+
(« Conflict », « Forbidden resource », « Cannot GET /x »,
|
|
24
|
+
`ThrottlerException`) et l'échec réseau de `fetch`, jamais un message métier
|
|
25
|
+
ni une erreur de champs.
|
|
26
|
+
|
|
3
27
|
## 0.5.0
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -68,6 +68,30 @@ Le contenu ci-dessous reprend intégralement `CHANGELOG.md` pour rester visible
|
|
|
68
68
|
<!-- sia:changelog:start -->
|
|
69
69
|
# @sia-ui/cli
|
|
70
70
|
|
|
71
|
+
## 0.6.0
|
|
72
|
+
|
|
73
|
+
### Minor Changes
|
|
74
|
+
|
|
75
|
+
- `sia-ui update`, la feuille commune suivie, et des erreurs HTTP en français.
|
|
76
|
+
- **`sia-ui update [nom...]`** — met à jour les entrées en retard sans perdre
|
|
77
|
+
de retouche : un fichier intact est remplacé, un fichier modifié depuis son
|
|
78
|
+
installation est gardé et signalé (`sia-ui diff`, puis `--force` pour
|
|
79
|
+
l'écraser). Les dépendances de registre apparues sont installées, les
|
|
80
|
+
paquets npm annoncés. `--dry-run` montre sans rien écrire. Une entrée dont
|
|
81
|
+
un fichier a été gardé reste « en retard » dans le verrou.
|
|
82
|
+
- **`sia-ui.css` suivie** — son empreinte est notée dans `sia-ui.lock.json`.
|
|
83
|
+
`update` la met à jour comme un composant ; `add` aussi, si elle n'a pas
|
|
84
|
+
été retouchée. `init` ne fait plus qu'initialiser. Elle est lue au même
|
|
85
|
+
registre que les composants, HTTP compris. `doctor` signale une feuille en
|
|
86
|
+
retard, modifiée ou copiée avant le suivi.
|
|
87
|
+
- `list --outdated` et `doctor` conseillent `sia-ui update` au lieu de
|
|
88
|
+
`add --overwrite`, qui écrasait aussi les retouches.
|
|
89
|
+
- **`statusMessages`** (`@sia-ui/api`) — option du client, avec
|
|
90
|
+
`FRENCH_STATUS_MESSAGES`. Remplace les messages génériques du serveur
|
|
91
|
+
(« Conflict », « Forbidden resource », « Cannot GET /x »,
|
|
92
|
+
`ThrottlerException`) et l'échec réseau de `fetch`, jamais un message métier
|
|
93
|
+
ni une erreur de champs.
|
|
94
|
+
|
|
71
95
|
## 0.5.0
|
|
72
96
|
|
|
73
97
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { access,
|
|
3
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import {
|
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
lireLock,
|
|
11
11
|
noter,
|
|
12
12
|
} from "./lock.mjs";
|
|
13
|
-
import { diff, doctor, outdated, remove } from "./lifecycle.mjs";
|
|
13
|
+
import { diff, doctor, outdated, remove, update } from "./lifecycle.mjs";
|
|
14
14
|
import { choisirSource } from "./source.mjs";
|
|
15
|
+
import { synchroniserStyles } from "./styles.mjs";
|
|
15
16
|
|
|
16
17
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const registryRoot = resolve(here, "registry");
|
|
@@ -163,13 +164,15 @@ async function init(cwd) {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
const config = await readConfig(cwd);
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
const source = await ouvrirSource(config);
|
|
168
|
+
// `init` initialise, et c'est tout : une feuille déjà là n'est pas
|
|
169
|
+
// touchée. La mettre à jour est le travail de `update`.
|
|
170
|
+
const lock = await lireLock(cwd);
|
|
171
|
+
await synchroniserStyles(
|
|
172
|
+
{ cwd, config, lock, lireStyles: source.lireStyles },
|
|
173
|
+
{ mode: "creer" },
|
|
174
|
+
);
|
|
175
|
+
await ecrireLock(cwd, lock);
|
|
173
176
|
|
|
174
177
|
console.log("SIA UI initialisé.");
|
|
175
178
|
}
|
|
@@ -228,6 +231,14 @@ async function add(cwd, names) {
|
|
|
228
231
|
if (copieComplete) noter(lock, item, empreintes);
|
|
229
232
|
}
|
|
230
233
|
|
|
234
|
+
// La feuille commune suit les composants : un composant à jour peut
|
|
235
|
+
// dépendre d'une règle qu'elle vient de gagner. Jamais écrasée ici si elle
|
|
236
|
+
// a été retouchée — `--overwrite` ne vise que les composants nommés.
|
|
237
|
+
await synchroniserStyles(
|
|
238
|
+
{ cwd, config, lock, lireStyles: source.lireStyles },
|
|
239
|
+
{ mode: "sur" },
|
|
240
|
+
);
|
|
241
|
+
|
|
231
242
|
await ecrireLock(cwd, lock);
|
|
232
243
|
console.log(`\nlock ${LOCK_FILE}`);
|
|
233
244
|
|
|
@@ -268,13 +279,16 @@ function help() {
|
|
|
268
279
|
" list lister les composants disponibles",
|
|
269
280
|
" list --outdated lister ce qui a une version plus récente",
|
|
270
281
|
" add <nom...> installer des composants et leurs dépendances",
|
|
282
|
+
" update [nom...] mettre à jour ce qui est en retard, feuille comprise",
|
|
271
283
|
" diff [nom...] comparer les fichiers installés au registre",
|
|
272
284
|
" remove <nom...> retirer des composants installés",
|
|
273
285
|
" doctor vérifier l'état de ce qui est installé",
|
|
274
286
|
"",
|
|
275
287
|
"Options :",
|
|
276
|
-
" --overwrite écraser les fichiers déjà présents",
|
|
277
|
-
" --force
|
|
288
|
+
" --overwrite add : écraser les fichiers déjà présents",
|
|
289
|
+
" --force update : écraser aussi les fichiers retouchés",
|
|
290
|
+
" remove : passer outre les avertissements",
|
|
291
|
+
" --dry-run update : montrer sans rien écrire",
|
|
278
292
|
" --registry <url> lire un registre servi en HTTP",
|
|
279
293
|
"",
|
|
280
294
|
"Les chemins d'installation viennent de sia-ui.json,",
|
|
@@ -304,6 +318,8 @@ async function contexteProjet(cwd) {
|
|
|
304
318
|
// faire dans la même écriture, sinon aucun fichier ne se retrouve sous
|
|
305
319
|
// Windows.
|
|
306
320
|
cibleDe: (fichier, conf) => cheminUnix(targetFor(fichier, conf)),
|
|
321
|
+
lireStyles: source.lireStyles,
|
|
322
|
+
resoudre: (noms) => resolveItems(source.registre, noms),
|
|
307
323
|
};
|
|
308
324
|
}
|
|
309
325
|
|
|
@@ -320,6 +336,9 @@ try {
|
|
|
320
336
|
else if (command === "remove") {
|
|
321
337
|
await remove(await contexteProjet(cwd), args, flags);
|
|
322
338
|
} else if (command === "doctor") await doctor(await contexteProjet(cwd));
|
|
339
|
+
else if (command === "update") {
|
|
340
|
+
await update(await contexteProjet(cwd), args, flags);
|
|
341
|
+
}
|
|
323
342
|
else help();
|
|
324
343
|
} catch (error) {
|
|
325
344
|
console.error(error instanceof Error ? error.message : error);
|
package/dist/lifecycle.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFile, rm } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { lignesDiff } from "./diff.mjs";
|
|
4
4
|
import { LOCK_FILE, ecrireLock, empreinte, lireLock, oublier } from "./lock.mjs";
|
|
5
|
+
import { etatStyles, synchroniserStyles } from "./styles.mjs";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Ce qu'on peut faire d'un composant **après** l'avoir copié.
|
|
@@ -180,6 +181,18 @@ export async function doctor(contexte) {
|
|
|
180
181
|
|
|
181
182
|
const problemes = [];
|
|
182
183
|
|
|
184
|
+
const { etat: feuille } = await etatStyles({
|
|
185
|
+
...contexte,
|
|
186
|
+
lock: await lireLock(contexte.cwd),
|
|
187
|
+
});
|
|
188
|
+
const conseil = {
|
|
189
|
+
absente: "absente — sia-ui init la recopie",
|
|
190
|
+
"en retard": "en retard sur le registre — sia-ui update la met à jour",
|
|
191
|
+
"non suivie": "différente de celle du registre — sia-ui update --force la remplace",
|
|
192
|
+
modifiée: "modifiée localement — sia-ui update --force écraserait la modification",
|
|
193
|
+
}[feuille];
|
|
194
|
+
if (conseil) problemes.push(`${contexte.config.styles} — ${conseil}`);
|
|
195
|
+
|
|
183
196
|
for (const etat of liste) {
|
|
184
197
|
if (etat.inconnu) {
|
|
185
198
|
problemes.push(`${etat.nom} — absent du registre de cette CLI`);
|
|
@@ -204,7 +217,7 @@ export async function doctor(contexte) {
|
|
|
204
217
|
problemes.push(`${etat.nom} — ${fichier.relatif} a disparu`);
|
|
205
218
|
} else if (fichier.etat === "modifié") {
|
|
206
219
|
// Ce n'est pas une erreur : posséder le code, c'est le droit de le
|
|
207
|
-
// modifier. Mais `
|
|
220
|
+
// modifier. Mais `update --force` écraserait la modification.
|
|
208
221
|
problemes.push(
|
|
209
222
|
`${etat.nom} — ${fichier.relatif} modifié depuis l'installation`,
|
|
210
223
|
);
|
|
@@ -238,5 +251,177 @@ export async function outdated(contexte) {
|
|
|
238
251
|
`${etat.nom.padEnd(largeur)} ${etat.verrou.version} → ${etat.item.version}`,
|
|
239
252
|
);
|
|
240
253
|
}
|
|
241
|
-
console.log(
|
|
254
|
+
console.log("\nMettre à jour, sans écraser ce qui a été retouché :\n sia-ui update");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Met à jour ce qui a une version plus récente, sans perdre de retouche.
|
|
259
|
+
*
|
|
260
|
+
* Pour chaque fichier, le verrou dit s'il a été modifié depuis son
|
|
261
|
+
* installation. Intact, il est remplacé ; retouché, il est gardé et signalé —
|
|
262
|
+
* `sia-ui diff` montre l'écart, `--force` l'écrase. `add --overwrite`, seul
|
|
263
|
+
* recours jusqu'ici, écrasait tout sans distinction.
|
|
264
|
+
*
|
|
265
|
+
* Une entrée n'est notée à sa nouvelle version que si tous ses fichiers l'ont
|
|
266
|
+
* rejointe : un fichier gardé la laisse « en retard », et la question se
|
|
267
|
+
* reposera au prochain `update` plutôt que de disparaître.
|
|
268
|
+
*/
|
|
269
|
+
export async function update(contexte, noms, flags) {
|
|
270
|
+
const { cwd, config, cibleDe, lireSource, resoudre } = contexte;
|
|
271
|
+
const dryRun = flags.has("--dry-run");
|
|
272
|
+
const force = flags.has("--force");
|
|
273
|
+
const { lock, etats: liste } = await etats(contexte, noms);
|
|
274
|
+
|
|
275
|
+
const npm = new Set();
|
|
276
|
+
const gardes = new Map();
|
|
277
|
+
const avertissements = [];
|
|
278
|
+
let misesAJour = 0;
|
|
279
|
+
|
|
280
|
+
if (dryRun) console.log("Simulation : rien ne sera écrit.\n");
|
|
281
|
+
|
|
282
|
+
/** Écrit un fichier et rend son empreinte, ou rien en simulation. */
|
|
283
|
+
const ecrire = async (relatif, contenu) => {
|
|
284
|
+
if (!dryRun) {
|
|
285
|
+
await mkdir(dirname(join(cwd, relatif)), { recursive: true });
|
|
286
|
+
await writeFile(join(cwd, relatif), contenu);
|
|
287
|
+
}
|
|
288
|
+
return empreinte(contenu);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const enRetard = [];
|
|
292
|
+
for (const etat of liste) {
|
|
293
|
+
if (etat.inconnu) {
|
|
294
|
+
avertissements.push(`${etat.nom} — absent de ce registre, laissé tel quel`);
|
|
295
|
+
} else if (etat.retire) {
|
|
296
|
+
avertissements.push(
|
|
297
|
+
`${etat.nom} — retiré du registre${etat.item.replacedBy ? ` ; le remplacer par ${etat.item.replacedBy}` : ""}`,
|
|
298
|
+
);
|
|
299
|
+
} else if (etat.desynchronise) {
|
|
300
|
+
if (etat.deprecie) {
|
|
301
|
+
avertissements.push(
|
|
302
|
+
`${etat.nom} — déprécié${etat.item.replacedBy ? `, préférer ${etat.item.replacedBy}` : ""}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
enRetard.push(etat);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const etat of enRetard) {
|
|
310
|
+
const { item, verrou } = etat;
|
|
311
|
+
console.log(`${etat.nom} ${verrou.version} → ${item.version}`);
|
|
312
|
+
for (const dependance of item.dependencies ?? []) npm.add(dependance);
|
|
313
|
+
|
|
314
|
+
const empreintes = {};
|
|
315
|
+
let complet = true;
|
|
316
|
+
|
|
317
|
+
for (const fichier of item.files) {
|
|
318
|
+
const relatif = cibleDe(fichier, config);
|
|
319
|
+
const installe = etat.fichiers.find((f) => f.relatif === relatif);
|
|
320
|
+
const contenu = await lireSource(fichier);
|
|
321
|
+
|
|
322
|
+
// Retouché depuis l'installation : le travail de quelqu'un.
|
|
323
|
+
if (installe?.etat === "modifié" && !force) {
|
|
324
|
+
console.log(` keep ${relatif} (modifié localement)`);
|
|
325
|
+
empreintes[relatif] = verrou.files[relatif];
|
|
326
|
+
complet = false;
|
|
327
|
+
const liste = gardes.get(etat.nom) ?? [];
|
|
328
|
+
liste.push(relatif);
|
|
329
|
+
gardes.set(etat.nom, liste);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Un fichier nouveau dans cette version, mais déjà présent sur le disque
|
|
334
|
+
// sans que le verrou le connaisse : il n'est pas à nous.
|
|
335
|
+
if (!installe && !force && (await existe(join(cwd, relatif)))) {
|
|
336
|
+
console.log(` keep ${relatif} (présent mais non suivi)`);
|
|
337
|
+
complet = false;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
empreintes[relatif] = await ecrire(relatif, contenu);
|
|
342
|
+
console.log(` ${installe ? "update" : "add "} ${relatif}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Un fichier que la nouvelle version ne fournit plus reste chez
|
|
346
|
+
// l'utilisateur : le supprimer d'office pourrait casser un import local.
|
|
347
|
+
for (const ancien of Object.keys(verrou.files ?? {})) {
|
|
348
|
+
if (!(ancien in empreintes) && !item.files.some((f) => cibleDe(f, config) === ancien)) {
|
|
349
|
+
console.log(` note ${ancien} n'est plus fourni — à retirer à la main s'il ne sert plus`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!dryRun) {
|
|
354
|
+
lock.items[etat.nom] = {
|
|
355
|
+
// Incomplète, l'entrée garde sa version : elle reste « en retard ».
|
|
356
|
+
version: complet ? item.version : verrou.version,
|
|
357
|
+
installedAt: complet ? new Date().toISOString() : verrou.installedAt,
|
|
358
|
+
files: Object.fromEntries(
|
|
359
|
+
Object.keys(empreintes).sort().map((f) => [f, empreintes[f]]),
|
|
360
|
+
),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (complet) misesAJour += 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Les dépendances de registre apparues avec les nouvelles versions.
|
|
367
|
+
if (enRetard.length > 0) {
|
|
368
|
+
for (const item of resoudre(enRetard.map((e) => e.nom))) {
|
|
369
|
+
if (lock.items[item.name]) continue;
|
|
370
|
+
console.log(`${item.name} nouvelle dépendance`);
|
|
371
|
+
for (const dependance of item.dependencies ?? []) npm.add(dependance);
|
|
372
|
+
|
|
373
|
+
const empreintes = {};
|
|
374
|
+
for (const fichier of item.files) {
|
|
375
|
+
const relatif = cibleDe(fichier, config);
|
|
376
|
+
if (!force && (await existe(join(cwd, relatif)))) {
|
|
377
|
+
console.log(` keep ${relatif} (présent mais non suivi)`);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
empreintes[relatif] = await ecrire(relatif, await lireSource(fichier));
|
|
381
|
+
console.log(` add ${relatif}`);
|
|
382
|
+
}
|
|
383
|
+
if (!dryRun) {
|
|
384
|
+
lock.items[item.name] = {
|
|
385
|
+
version: item.version,
|
|
386
|
+
installedAt: new Date().toISOString(),
|
|
387
|
+
files: empreintes,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// La feuille commune, qu'une entrée à jour peut supposer à jour aussi.
|
|
394
|
+
await synchroniserStyles(
|
|
395
|
+
{ ...contexte, lock },
|
|
396
|
+
{ mode: force ? "forcer" : "sur", dryRun },
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (!dryRun) await ecrireLock(cwd, lock);
|
|
400
|
+
|
|
401
|
+
console.log("");
|
|
402
|
+
if (enRetard.length === 0) console.log("Composants : tout est à jour.");
|
|
403
|
+
else {
|
|
404
|
+
console.log(
|
|
405
|
+
`${misesAJour} entrée(s) mise(s) à jour sur ${enRetard.length}${dryRun ? " (simulation)" : ""}.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (gardes.size > 0) {
|
|
410
|
+
console.log(
|
|
411
|
+
`\nFichiers retouchés gardés — comparer, puis reprendre ou forcer :\n sia-ui diff ${[...gardes.keys()].join(" ")}\n sia-ui update ${[...gardes.keys()].join(" ")} --force`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
for (const avertissement of avertissements) console.warn(`\n⚠ ${avertissement}`);
|
|
415
|
+
if (npm.size > 0) {
|
|
416
|
+
console.log(`\nÀ installer :\n pnpm add ${[...npm].sort().join(" ")}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function existe(chemin) {
|
|
421
|
+
try {
|
|
422
|
+
await access(chemin);
|
|
423
|
+
return true;
|
|
424
|
+
} catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
242
427
|
}
|
package/dist/lock.mjs
CHANGED
|
@@ -44,7 +44,13 @@ export async function lireLock(cwd) {
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
format: FORMAT,
|
|
49
|
+
items: lock.items ?? {},
|
|
50
|
+
// La feuille commune, suivie depuis qu'`add` la met à jour. Absente
|
|
51
|
+
// d'un verrou plus ancien : un champ en plus, pas un nouveau format.
|
|
52
|
+
...(lock.styles ? { styles: lock.styles } : {}),
|
|
53
|
+
};
|
|
48
54
|
} catch (error) {
|
|
49
55
|
if (error.code === "ENOENT") return structuredClone(VIDE);
|
|
50
56
|
if (error instanceof SyntaxError) {
|
|
@@ -67,7 +73,15 @@ export async function ecrireLock(cwd, lock) {
|
|
|
67
73
|
|
|
68
74
|
await writeFile(
|
|
69
75
|
join(cwd, LOCK_FILE),
|
|
70
|
-
`${JSON.stringify(
|
|
76
|
+
`${JSON.stringify(
|
|
77
|
+
{
|
|
78
|
+
format: FORMAT,
|
|
79
|
+
...(lock.styles ? { styles: lock.styles } : {}),
|
|
80
|
+
items,
|
|
81
|
+
},
|
|
82
|
+
null,
|
|
83
|
+
2,
|
|
84
|
+
)}\n`,
|
|
71
85
|
);
|
|
72
86
|
}
|
|
73
87
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { useMemo, type Key, type ReactNode } from "react";
|
|
2
|
+
import { cn, formatDateTime } from "@sia-ui/utils";
|
|
3
|
+
import type { TableQuery } from "@sia-ui/headless";
|
|
4
|
+
import type { ComponentTone } from "@sia-ui/tokens";
|
|
5
|
+
import { DataTable } from "../DataTable";
|
|
6
|
+
import type { DataTableColumn, DataTableProps } from "../DataTable/types";
|
|
7
|
+
import { FiltersBar } from "../FiltersBar";
|
|
8
|
+
import { PageHeader } from "../PageHeader";
|
|
9
|
+
import { Pagination } from "../Pagination";
|
|
10
|
+
import { SearchInput } from "../SearchInput";
|
|
11
|
+
import { Select, type SelectOption } from "../Select";
|
|
12
|
+
import { StatusBadge } from "../Badge";
|
|
13
|
+
import "./styles.css";
|
|
14
|
+
|
|
15
|
+
/** Une entrée de journal, dans sa forme la plus courante. */
|
|
16
|
+
export interface ActivityEntry {
|
|
17
|
+
id: string | number;
|
|
18
|
+
/** Quand : une date ISO, un horodatage ou une `Date`. */
|
|
19
|
+
at: string | number | Date;
|
|
20
|
+
/** Qui : un nom lisible. */
|
|
21
|
+
actor: string;
|
|
22
|
+
/** Quoi : un code — `create`, `delete`, `login`. */
|
|
23
|
+
action: string;
|
|
24
|
+
/** Sur quoi : « Projet Atlas », « Compte ovh-prod ». */
|
|
25
|
+
target?: string | undefined;
|
|
26
|
+
/** Le type de la cible : `project`, `account`. */
|
|
27
|
+
targetType?: string | undefined;
|
|
28
|
+
/** Une phrase de contexte, facultative. */
|
|
29
|
+
summary?: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Un filtre déclaré : une clé de requête et ses choix. */
|
|
33
|
+
export interface ActivityLogFilter {
|
|
34
|
+
/** La clé envoyée au serveur : `actor`, `action`, `targetType`. */
|
|
35
|
+
key: string;
|
|
36
|
+
label: string;
|
|
37
|
+
options: SelectOption[];
|
|
38
|
+
/** Cherchable au-delà d'une dizaine de choix. */
|
|
39
|
+
searchable?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ActivityLogProps<T = ActivityEntry> {
|
|
43
|
+
entries: T[];
|
|
44
|
+
/**
|
|
45
|
+
* L'état de la requête : page, taille, recherche, filtres.
|
|
46
|
+
*
|
|
47
|
+
* `useDataTableQuery()` le fournit, synchronisé avec l'URL ou le routeur ;
|
|
48
|
+
* `query.params` part tel quel au serveur. Le journal ne filtre rien
|
|
49
|
+
* lui-même : un journal se compte en milliers de lignes.
|
|
50
|
+
*/
|
|
51
|
+
query: TableQuery;
|
|
52
|
+
totalPages?: number;
|
|
53
|
+
filters?: ActivityLogFilter[];
|
|
54
|
+
/** Absente, la recherche n'est pas proposée. */
|
|
55
|
+
searchPlaceholder?: string;
|
|
56
|
+
|
|
57
|
+
/** Par défaut, les colonnes d'une `ActivityEntry`. */
|
|
58
|
+
columns?: Array<DataTableColumn<T>>;
|
|
59
|
+
getRowKey?: (row: T, index: number) => Key;
|
|
60
|
+
/** Le ton de chaque action dans les colonnes par défaut. */
|
|
61
|
+
actionTones?: Record<string, ComponentTone>;
|
|
62
|
+
/** Le libellé de chaque action — « Suppression » pour `delete`. */
|
|
63
|
+
actionLabels?: Record<string, ReactNode>;
|
|
64
|
+
|
|
65
|
+
title?: ReactNode;
|
|
66
|
+
description?: ReactNode;
|
|
67
|
+
headingLevel?: 1 | 2 | 3;
|
|
68
|
+
|
|
69
|
+
loading?: boolean;
|
|
70
|
+
error?: ReactNode;
|
|
71
|
+
onRetry?: () => void;
|
|
72
|
+
empty?: DataTableProps<T>["empty"];
|
|
73
|
+
className?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Les colonnes d'une `ActivityEntry` : quand, qui, quoi, sur quoi. */
|
|
77
|
+
export function activityColumns(
|
|
78
|
+
options: {
|
|
79
|
+
actionTones?: Record<string, ComponentTone> | undefined;
|
|
80
|
+
actionLabels?: Record<string, ReactNode> | undefined;
|
|
81
|
+
} = {},
|
|
82
|
+
): Array<DataTableColumn<ActivityEntry>> {
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
key: "at",
|
|
86
|
+
header: "Date",
|
|
87
|
+
width: "11rem",
|
|
88
|
+
cell: (entry) => (
|
|
89
|
+
<time className="sia-activity__date" dateTime={new Date(entry.at).toISOString()}>
|
|
90
|
+
{formatDateTime(entry.at)}
|
|
91
|
+
</time>
|
|
92
|
+
),
|
|
93
|
+
card: "meta",
|
|
94
|
+
},
|
|
95
|
+
{ key: "actor", header: "Acteur", card: "title" },
|
|
96
|
+
{
|
|
97
|
+
key: "action",
|
|
98
|
+
header: "Action",
|
|
99
|
+
cell: (entry) => (
|
|
100
|
+
<StatusBadge
|
|
101
|
+
value={entry.action}
|
|
102
|
+
{...(options.actionTones ? { tones: options.actionTones } : {})}
|
|
103
|
+
{...(options.actionLabels ? { labels: options.actionLabels } : {})}
|
|
104
|
+
/>
|
|
105
|
+
),
|
|
106
|
+
card: "meta",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "target",
|
|
110
|
+
header: "Cible",
|
|
111
|
+
cell: (entry) =>
|
|
112
|
+
entry.target || entry.summary ? (
|
|
113
|
+
<span className="sia-activity__target">
|
|
114
|
+
{entry.target && <span>{entry.target}</span>}
|
|
115
|
+
{entry.summary && <small>{entry.summary}</small>}
|
|
116
|
+
</span>
|
|
117
|
+
) : null,
|
|
118
|
+
card: "meta",
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Un journal d'activité, filtrable et paginé par le serveur.
|
|
125
|
+
*
|
|
126
|
+
* `AuditMeta` et `Timeline` racontent l'histoire d'un objet ; ce bloc sert
|
|
127
|
+
* l'autre question — qui a fait quoi, sur tout le système, cette semaine.
|
|
128
|
+
* Filtres déclarés, recherche temporisée, pagination et taille de page : tout
|
|
129
|
+
* passe par la requête, rien n'est filtré dans le navigateur.
|
|
130
|
+
*/
|
|
131
|
+
export function ActivityLog<T = ActivityEntry>({
|
|
132
|
+
entries,
|
|
133
|
+
query,
|
|
134
|
+
totalPages,
|
|
135
|
+
filters = [],
|
|
136
|
+
searchPlaceholder,
|
|
137
|
+
columns,
|
|
138
|
+
getRowKey,
|
|
139
|
+
actionTones,
|
|
140
|
+
actionLabels,
|
|
141
|
+
title,
|
|
142
|
+
description,
|
|
143
|
+
headingLevel,
|
|
144
|
+
loading = false,
|
|
145
|
+
error,
|
|
146
|
+
onRetry,
|
|
147
|
+
empty,
|
|
148
|
+
className,
|
|
149
|
+
}: ActivityLogProps<T>) {
|
|
150
|
+
const colonnes = useMemo(
|
|
151
|
+
() =>
|
|
152
|
+
columns ??
|
|
153
|
+
(activityColumns({ actionTones, actionLabels }) as unknown as Array<
|
|
154
|
+
DataTableColumn<T>
|
|
155
|
+
>),
|
|
156
|
+
[actionLabels, actionTones, columns],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const actifs =
|
|
160
|
+
filters.filter((filtre) => query.state.filters[filtre.key]).length +
|
|
161
|
+
(query.state.search ? 1 : 0);
|
|
162
|
+
|
|
163
|
+
const cle =
|
|
164
|
+
getRowKey ??
|
|
165
|
+
((row: T, index: number) => (row as { id?: Key }).id ?? index);
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<section className={cn("sia-activity", className)}>
|
|
169
|
+
{title && (
|
|
170
|
+
<PageHeader
|
|
171
|
+
title={title}
|
|
172
|
+
{...(description ? { description } : {})}
|
|
173
|
+
{...(headingLevel ? { level: headingLevel } : {})}
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{(filters.length > 0 || searchPlaceholder) && (
|
|
178
|
+
<FiltersBar
|
|
179
|
+
{...(searchPlaceholder
|
|
180
|
+
? {
|
|
181
|
+
search: (
|
|
182
|
+
<SearchInput
|
|
183
|
+
// Remonté à la réinitialisation : le champ reprend la
|
|
184
|
+
// recherche en vigueur plutôt que sa saisie d'avant.
|
|
185
|
+
key={query.state.search === "" ? "vide" : "saisie"}
|
|
186
|
+
defaultValue={query.state.search}
|
|
187
|
+
placeholder={searchPlaceholder}
|
|
188
|
+
searchDelay={300}
|
|
189
|
+
onSearch={query.setSearch}
|
|
190
|
+
/>
|
|
191
|
+
),
|
|
192
|
+
}
|
|
193
|
+
: {})}
|
|
194
|
+
filters={filters.map((filtre) => (
|
|
195
|
+
<div className="sia-activity__filter" key={filtre.key}>
|
|
196
|
+
<Select
|
|
197
|
+
aria-label={filtre.label}
|
|
198
|
+
placeholder={filtre.label}
|
|
199
|
+
options={filtre.options}
|
|
200
|
+
value={query.state.filters[filtre.key] ?? ""}
|
|
201
|
+
clearable
|
|
202
|
+
searchable={filtre.searchable ?? filtre.options.length > 10}
|
|
203
|
+
onValueChange={(valeur) => query.setFilter(filtre.key, valeur)}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
207
|
+
activeCount={actifs}
|
|
208
|
+
onReset={query.reset}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
<DataTable<T>
|
|
213
|
+
columns={colonnes}
|
|
214
|
+
data={entries}
|
|
215
|
+
getRowKey={cle}
|
|
216
|
+
loading={loading}
|
|
217
|
+
{...(error ? { error } : {})}
|
|
218
|
+
{...(onRetry ? { onRetry } : {})}
|
|
219
|
+
empty={
|
|
220
|
+
empty ?? {
|
|
221
|
+
title: actifs > 0 ? "Aucune activité pour ces filtres" : "Aucune activité",
|
|
222
|
+
...(actifs > 0
|
|
223
|
+
? { description: "Élargissez la période ou retirez un filtre." }
|
|
224
|
+
: {}),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/>
|
|
228
|
+
|
|
229
|
+
{totalPages !== undefined && (
|
|
230
|
+
<Pagination
|
|
231
|
+
page={query.state.page}
|
|
232
|
+
totalPages={totalPages}
|
|
233
|
+
onPageChange={query.setPage}
|
|
234
|
+
pageSize={query.state.perPage}
|
|
235
|
+
onPageSizeChange={query.setPerPage}
|
|
236
|
+
showTotal
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
|
+
</section>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Même rythme vertical que la page de liste : en-tête, filtres, tableau,
|
|
2
|
+
pagination, séparés d'un seul écart. */
|
|
3
|
+
.sia-activity {
|
|
4
|
+
container: sia-activity / inline-size;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: 1rem;
|
|
8
|
+
min-width: 0;
|
|
9
|
+
}
|
|
10
|
+
.sia-activity > .sia-page-header { margin-bottom: 0; }
|
|
11
|
+
|
|
12
|
+
/* Un filtre a une largeur de lecture, pas toute la barre : quatre filtres
|
|
13
|
+
tiennent côte à côte avant de passer à la ligne. */
|
|
14
|
+
.sia-activity__filter { width: 12rem; max-width: 100%; }
|
|
15
|
+
|
|
16
|
+
/* Étroit : chaque filtre prend la ligne, comme la recherche au-dessus —
|
|
17
|
+
des champs de largeurs différentes empilés donnent un bord droit en
|
|
18
|
+
escalier. */
|
|
19
|
+
@container sia-activity (max-width: 34rem) {
|
|
20
|
+
.sia-activity .sia-filters-bar__filters { width: 100%; }
|
|
21
|
+
.sia-activity__filter { width: 100%; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sia-activity__date {
|
|
25
|
+
color: var(--sia-muted);
|
|
26
|
+
font-variant-numeric: tabular-nums;
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.sia-activity__target { display: grid; gap: .1rem; min-width: 0; }
|
|
31
|
+
.sia-activity__target small { color: var(--sia-muted); font-size: .78rem; }
|