@cityway/bo-ui 1.1.0-beta004 → 1.1.0-beta006

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.
Binary file
Binary file
@@ -0,0 +1,198 @@
1
+ # `cw-export-button` — Bouton d'export
2
+
3
+ > Lib : `@cityway/bo-ui` · Sélecteur : `cw-export-button` · Standalone : oui
4
+ > Import : `import { ExportButtonComponent } from '@cityway/bo-ui';`
5
+
6
+ ---
7
+
8
+ > ⚠️ **Prérequis d'intégration : `<cw-toast>` doit être monté dans le layout racine.**
9
+ > Le toast d'échec passe par `ToastService`, dont la pile n'est rendue que par le conteneur
10
+ > `<cw-toast>` de `basic-ui`. **Il n'est monté nulle part dans le template BO à ce jour** :
11
+ > sans lui, un export qui échoue ne produit **aucun message visible**. À placer une seule
12
+ > fois, typiquement dans le composant racine (cf. `basic-ui/docs/components/toast.md`).
13
+
14
+ ---
15
+
16
+ ## 1. Rôle & quand l'utiliser
17
+
18
+ Bouton *Exporter* qui déclenche la production d'un fichier et le remet au navigateur.
19
+ **Une seule option** → bouton simple ; **deux ou plus** → menu déroulant listant les options.
20
+
21
+ Le composant **ne porte aucun métier** : il ne connaît ni le format exporté, ni la façon de
22
+ nommer le fichier, ni ce qui est exporté. L'hôte sérialise et nomme (`ExportPayload`) ; la
23
+ lib déclenche, restitue l'état et transporte. Conséquence : aucune dépendance à `xlsx`,
24
+ `file-saver` ou quoi que ce soit d'autre — JSON, CSV, XLSX, PDF sont indifférents.
25
+
26
+ À ne PAS utiliser pour : un import (`cw-import-button`), un simple lien de téléchargement
27
+ vers une URL connue (`cw-file-to-download` de `basic-ui`).
28
+
29
+ ---
30
+
31
+ ## 2. API
32
+
33
+ ### Inputs
34
+
35
+ | Input | Type | Défaut | Description |
36
+ |-------|------|--------|-------------|
37
+ | `options` | `ExportOptions` | — (**requis**) | Options proposées, dans l'ordre d'affichage. **Tuple non vide.** |
38
+ | `label` | `I18nKey` | `_COMMON._ACTION.EXPORT` | Clé i18n du libellé du bouton (ou du menu). |
39
+ | `errorKey` | `I18nKey` | `_COMMON._EXPORT.ERROR` | Clé i18n du message du toast d'échec. |
40
+ | `successKey` | `I18nKey \| null` | `null` | Clé i18n d'un toast de succès. `null` → **aucun toast**. |
41
+
42
+ Aucun output : ni le succès ni l'échec ne demandent d'action de l'hôte — l'export ne
43
+ modifie rien, il n'y a donc rien à recharger.
44
+
45
+ ---
46
+
47
+ ## 3. Types
48
+
49
+ ```ts
50
+ interface ExportPayload {
51
+ blob: Blob; // contenu déjà sérialisé par l'hôte
52
+ filename: string; // nom complet, extension comprise
53
+ }
54
+
55
+ interface ExportOption {
56
+ label: I18nKey; // clé i18n, traduite par le composant
57
+ fn: () => Observable<ExportPayload>; // produit le fichier
58
+ disabled?: boolean; // [Option] désactive CETTE option seule
59
+ }
60
+
61
+ /** Tuple non vide : `[]` ne compile pas. */
62
+ type ExportOptions = readonly [ExportOption, ...ExportOption[]];
63
+ ```
64
+
65
+ ### Ce que le type rend inexprimable
66
+
67
+ | Configuration invalide | Empêchée par |
68
+ |---|---|
69
+ | Aucune option | tuple non vide `[ExportOption, ...ExportOption[]]` — le rendu « bouton simple » ne peut plus lire un `options[0]` absent |
70
+ | Libellé d'option en texte déjà traduit | `label: I18nKey` — la conversion doit être écrite noir sur blanc |
71
+
72
+ **Le type nominal `I18nKey` n'est pas une garantie de contenu** : il dit d'où vient la
73
+ chaîne, pas ce qu'elle contient. `i18nKey('Tout exporter')` compile. Ce qu'il apporte : la
74
+ conversion est explicite donc visible en revue, et on ne peut plus passer une **variable**
75
+ de texte déjà localisé dans un champ de clé sans l'écrire.
76
+
77
+ ---
78
+
79
+ ## 4. Exemples
80
+
81
+ ### Deux options, dans l'en-tête de page
82
+
83
+ ```html
84
+ <cw-page-header [title]="'DOMAIN.USERS.TITLE'" [actions]="[actionEnum.add]" (action)="onAction($event)">
85
+ <cw-export-button cwPageHeaderExport [options]="exportOptions" />
86
+ </cw-page-header>
87
+ ```
88
+
89
+ ```ts
90
+ import { buildFilename, ExportOptions, i18nKey, jsonBlob, todayIsoDate } from '@cityway/bo-ui';
91
+
92
+ readonly exportOptions: ExportOptions = [
93
+ {
94
+ label: i18nKey('DOMAIN.USERS.EXPORT_ALL'),
95
+ fn: () => this.api.exportAll().pipe(
96
+ map(rows => ({
97
+ blob: jsonBlob(rows),
98
+ // Tout le métier de nommage est ICI : la lib n'en compose aucune partie.
99
+ filename: buildFilename(['utilisateurs', this.instance()?.code, todayIsoDate()], 'json'),
100
+ }))
101
+ ),
102
+ },
103
+ {
104
+ label: i18nKey('DOMAIN.USERS.EXPORT_FILTERED'),
105
+ fn: () => this.api.export(this.filters()).pipe(map(rows => ({ … }))),
106
+ // Un droit qui porte sur un périmètre, pas sur l'export entier.
107
+ disabled: !this.canExportFiltered(),
108
+ },
109
+ ];
110
+ ```
111
+
112
+ ### Export CSV / XLSX via `ExportService` de `bo-core`
113
+
114
+ Le composant ne sérialise pas ; il consomme le blob que vous produisez. `ExportService`
115
+ (`@cityway/bo-core`) télécharge lui-même via `file-saver` — pour l'utiliser ici, produisez
116
+ le blob plutôt que de laisser le service l'enregistrer, ou construisez-le directement :
117
+
118
+ ```ts
119
+ fn: () => this.api.exportAll().pipe(
120
+ map(rows => ({
121
+ blob: new Blob([toCsv(rows)], { type: 'text/csv;charset=utf-8' }),
122
+ filename: buildFilename(['tarifs', todayIsoDate()], 'csv'),
123
+ }))
124
+ ),
125
+ ```
126
+
127
+ ### Utilitaires fournis
128
+
129
+ ```ts
130
+ downloadBlob(payload) // utilisé par le composant ; disponible pour un usage direct
131
+ jsonBlob(data) // Blob JSON indenté
132
+ todayIsoDate() // 'YYYY-MM-DD', en heure LOCALE (pas toISOString, qui décale en UTC)
133
+ buildFilename(parts, extension) // ignore les fragments absents, sans tiret orphelin
134
+ ```
135
+
136
+ ---
137
+
138
+ ## 5. Notes d'intégration
139
+
140
+ - **`<cw-toast>` doit être monté** — voir l'avertissement en tête de page.
141
+ - **Pas d'interruption, pas d'annulation, délibérément.** Contrairement à l'import,
142
+ l'export **n'écrit rien** : un export abandonné n'a aucune conséquence sur l'intégrité
143
+ des données. Le traitement de l'interruption de `cw-import-wizard` ne s'applique pas
144
+ ici, et ce n'est **pas un oubli**. Ne l'« harmonisez » pas.
145
+ - **Un seul export à la fois.** Un second clic pendant qu'un export est en vol est ignoré.
146
+ Ce garde-fou est dans le code et **pas** dans le rendu : `cw-button [isLoading]` ne pose
147
+ pas `[disabled]`, le bouton reste cliquable pendant tout l'export.
148
+ - **Aucun spinner sur le bouton.** `[isLoading]` n'est pas utilisé : il injecte sa propre
149
+ région live `role="status"`, ce qui ferait deux régions concurrentes pour un seul
150
+ événement (et il n'existe de toute façon pas dans la branche `isDropdown`). L'état de
151
+ chargement est porté par la région du composant, cf. ci-dessous.
152
+ - **Restitution (RGAA)** : le composant possède **une** région `role="status"`
153
+ *visually-hidden*, permanente dans le DOM, qui annonce successivement *Export en cours* →
154
+ *Export terminé, le téléchargement de « … » a démarré* / *L'export a échoué*.
155
+ - C'est une région live et non un déplacement de focus — à la différence de
156
+ `cw-import-wizard` — parce que l'export ne fait apparaître **aucun contenu** vers
157
+ lequel déplacer le focus : le téléchargement est une action du navigateur, invisible
158
+ dans le document.
159
+ - La région est **vidée puis réécrite au tour de boucle suivant** à chaque annonce : une
160
+ région `role="status"` n'est réannoncée que si son contenu change, et deux exports
161
+ synchrones successifs y écriraient deux fois la même phrase.
162
+ - Elle double le toast d'échec, qui n'est aujourd'hui **jamais annoncé** : la chaîne
163
+ `cw-toast` → `cw-toast-item` → `cw-alert` ne porte aucun `role` ni `aria-live`.
164
+ - **Menu déroulant** : les items suivent le motif documenté de `basic-ui`
165
+ (`<button cwDropdownItem>` en direct — cf. `basic-ui/docs/components/button.md`). C'est
166
+ la classe `dropdown-item` posée par la directive sur la **cible du clic** qui permet à
167
+ `cw-button` de refermer son menu après la sélection. Un `<cw-button>` imbriqué ne la
168
+ porterait pas au bon endroit.
169
+ - **Le menu entier ne se désactive** que si **toutes** les options sont `disabled`.
170
+ - **Échec** : une erreur de l'observable, **et** une source qui complète sans rien émettre,
171
+ valent échec. Sans ce second cas, l'export se terminerait en silence — pas de fichier,
172
+ pas de message, bouton redevenu actif comme si tout allait bien.
173
+ - **Succès sans toast par défaut** : le fichier qui se télécharge *est* le retour. N'ajoutez
174
+ `successKey` que si l'hôte veut un accusé explicite.
175
+ - **Nommage du fichier** : entièrement à l'hôte. C'est ce qui garde le composant sans
176
+ métier — le code client, l'horodatage ou le périmètre n'ont pas à traverser la frontière
177
+ de la lib. `buildFilename` / `todayIsoDate` sont des aides, jamais appelées à votre place.
178
+ - **i18n** : libellés sous `_COMMON._EXPORT.*` dans le bundle **bo-ui**
179
+ (`@cityway/bo-ui/lib/assets/i18n/common.{fr,en}.json`) — voir `bo-ui/docs/i18n.md`.
180
+ `_COMMON._ACTION.EXPORT` et les titres de toast (`_COMMON._ACTION_ERROR.LABEL`,
181
+ `_COMMON._ACTION_SUCCESS.LABEL`) viennent de **basic-ui**.
182
+ - **Limite connue de `ToastService`** : deux messages identiques émis à moins d'une seconde
183
+ d'intervalle sont dédupliqués — le second est avalé. Le garde-fou anti-double-clic couvre
184
+ le cas courant.
185
+
186
+ ---
187
+
188
+ ## 6. Prompts-types (génération assistée)
189
+
190
+ - « Ajoute un bouton d'export CSV sur la page tarifs, avec deux options : tout, et la vue filtrée. »
191
+ - « Branche l'export de la liste utilisateurs sur `cw-export-button`, fichier JSON nommé avec le code client et la date. »
192
+ - « Désactive l'option "tout exporter" quand l'utilisateur n'a pas le droit correspondant. »
193
+
194
+ **Rappels pour Claude :**
195
+ - `options` est un **tuple non vide**. `label` est une **clé** (`i18nKey(...)`), pas du texte.
196
+ - `fn()` doit renvoyer un `ExportPayload` — **l'hôte sérialise et nomme**. Ne jamais ajouter de dépendance de format dans la lib.
197
+ - Ne **pas** poser `[isLoading]` sur le bouton, ni ajouter un bouton d'annulation d'export.
198
+ - Vérifier que `<cw-toast>` est monté dans le layout de l'application.
@@ -0,0 +1,134 @@
1
+ # `cw-import-button` — Bouton d'import
2
+
3
+ > Lib : `@cityway/bo-ui` · Sélecteur : `cw-import-button` · Standalone : oui
4
+ > Import : `import { ImportButtonComponent } from '@cityway/bo-ui';`
5
+
6
+ ---
7
+
8
+ ## 1. Rôle & quand l'utiliser
9
+
10
+ Bouton *Importer* qui ouvre `cw-import-wizard` dans une modale **sidepanel**. C'est la façon
11
+ standard de déclencher un import — inutile de recâbler `ModalsService` à la main.
12
+
13
+ Le composant **n'ajoute aucun contrat** : il transporte l'`ImportWizardConfig` de l'hôte
14
+ **tel quel** jusqu'à l'assistant. Pas de type miroir, pas de champ recopié — toute évolution
15
+ de `cw-import-wizard` est disponible ici sans modification symétrique.
16
+
17
+ Ce qu'il apporte, et qui serait sinon réécrit dans chaque page :
18
+ - le `ng-template` du corps de modale et l'ouverture au bon type ;
19
+ - l'absence de `labelBtnAction1`/`2` (un bouton d'action au pied **fermerait** la modale) ;
20
+ - la **séparation des deux signaux de sortie** que la SFD de l'assistant impose de ne pas
21
+ confondre (§4).
22
+
23
+ Pour tout ce qui concerne le parcours lui-même — étapes, modes, analyse, interruption,
24
+ rapport —, la référence est **`import-wizard.md`**.
25
+
26
+ ---
27
+
28
+ ## 2. API
29
+
30
+ ### Inputs
31
+
32
+ | Input | Type | Défaut | Description |
33
+ |-------|------|--------|-------------|
34
+ | `config` | `ImportWizardConfig` | — (**requis**) | Paramétrage de l'assistant, transmis tel quel. Voir `import-wizard.md` §3. |
35
+ | `modalTitle` | `I18nKey` | — (**requis**) | Clé i18n du titre de la modale. |
36
+ | `label` | `I18nKey` | `_COMMON._ACTION.IMPORT` | Clé i18n du libellé du bouton. |
37
+ | `disabled` | `boolean` | `false` | Désactive le bouton (droit utilisateur). |
38
+
39
+ ### Outputs
40
+
41
+ | Output | Payload | Description |
42
+ |--------|---------|-------------|
43
+ | `closed` | `void` | Modale fermée, **par n'importe quel chemin**. → l'hôte **recharge** ici. |
44
+ | `completed` | `ImportOutcome` | État terminal de l'assistant. → message, journalisation, action métier. |
45
+
46
+ ---
47
+
48
+ ## 3. Exemple
49
+
50
+ ```html
51
+ <cw-page-header [title]="'DOMAIN.USERS.TITLE'" [actions]="[actionEnum.add]" (action)="onAction($event)">
52
+ <cw-import-button
53
+ cwPageHeaderImport
54
+ [config]="importConfig"
55
+ [modalTitle]="importTitle"
56
+ (closed)="reload()"
57
+ (completed)="onImported($event)" />
58
+ </cw-page-header>
59
+ ```
60
+
61
+ ```ts
62
+ import {
63
+ i18nKey, ImportEventKindEnum, ImportInterruptionEnum, ImportModeEnum, ImportOutcome,
64
+ ImportButtonComponent, ImportWizardConfig
65
+ } from '@cityway/bo-ui';
66
+
67
+ readonly importTitle = i18nKey('DOMAIN.USERS.IMPORT_TITLE');
68
+
69
+ readonly importConfig: ImportWizardConfig = {
70
+ accept: '.csv,.xlsx',
71
+ acceptLabel: 'CSV, XLSX',
72
+ maxSizeBytes: 10 * 1024 * 1024,
73
+ modes: [
74
+ { mode: ImportModeEnum.addOnly, keyFieldHint: i18nKey('DOMAIN.USERS.IMPORT_DUPLICATE_HINT') },
75
+ { mode: ImportModeEnum.upsert, keyFieldHint: i18nKey('DOMAIN.USERS.IMPORT_KEY_HINT') },
76
+ ],
77
+ showErrorDetails: true,
78
+ interruption: { kind: ImportInterruptionEnum.serverCancel, cancel: ctx => this.api.cancel(ctx) },
79
+ source: {
80
+ analyze: (file, mode) => this.api.dryRun(file, mode),
81
+ execute: (file, mode, ctx) => this.api.run(file, mode, ctx),
82
+ },
83
+ };
84
+
85
+ // Signal de RÉSULTAT : uniquement ce qui dépend de l'issue. On NE FERME PAS la modale ici,
86
+ // l'utilisateur doit pouvoir lire son rapport.
87
+ onImported(outcome: ImportOutcome): void {
88
+ if (outcome.kind === ImportEventKindEnum.report && outcome.success) {
89
+ this.toaster.success('DOMAIN.USERS.IMPORT_DONE');
90
+ }
91
+ }
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 4. Notes d'intégration
97
+
98
+ - **Rechargez sur `(closed)`, pas sur `(completed)`.** C'est la règle centrale, reprise de
99
+ `import-wizard.md` §5. `closed` est émis à **tous** les chemins de fermeture — croix,
100
+ Échap, clic sur le fond, bouton *Fermer* du pied — y compris la fermeture **pendant** un
101
+ import, cas où `completed` n'a jamais été émis alors que des données ont pu être écrites.
102
+ Le composant traite les **deux** issues de la promesse de `ModalsService` (résolution
103
+ comme rejet) : le rejet est ainsi marqué comme géré, et si un chemin de fermeture venait
104
+ à la résoudre, le rechargement partirait quand même.
105
+ - **`completed` est le signal de RÉSULTAT** : message de confirmation, journalisation,
106
+ action métier. Émis à tout état terminal — succès, échec, interruption — mais il ne voit
107
+ pas la fermeture, donc il ne peut pas porter le rechargement.
108
+ - **Ne fermez pas la modale** dans le gestionnaire de `completed` : l'utilisateur doit
109
+ pouvoir lire son rapport, en particulier un rapport d'interruption en état incertain. Le
110
+ composant n'expose d'ailleurs aucune sortie de fermeture.
111
+ - **Aucun bouton d'action au pied.** Le composant ne passe pas `labelBtnAction1`/`2` : le
112
+ pied par défaut rend exactement le *Fermer* attendu, et tout bouton d'action configuré
113
+ **fermerait** la modale.
114
+ - **Placement** : dans le slot `cwPageHeaderImport` de `cw-page-header`, qui le rend à la
115
+ position habituelle du bouton *Importer*. Il fonctionne aussi seul, hors en-tête.
116
+ L'import **n'est pas** une valeur de `PageHeaderActionEnum` : le rendre depuis
117
+ `cw-page-header` obligerait celui-ci à référencer l'assistant, donc à l'embarquer dans
118
+ toute page affichant un titre — ng-packagr aplatit la lib en un FESM unique, un `@defer`
119
+ n'y change rien.
120
+ - **i18n** : `_COMMON._ACTION.IMPORT` vient de **basic-ui** ; les libellés de l'assistant
121
+ sont sous `_COMMON._IMPORT.*` dans le bundle **bo-ui**.
122
+
123
+ ---
124
+
125
+ ## 5. Prompts-types (génération assistée)
126
+
127
+ - « Branche le bouton *Importer* de la page utilisateurs sur `cw-import-button`, formats CSV/XLSX, 10 Mo max, modes ajout et mise à jour. »
128
+ - « Ajoute l'import de configuration en écrasement complet dans l'en-tête de la page paramètres. »
129
+
130
+ **Rappels pour Claude :**
131
+ - Recharger sur `(closed)`, **jamais** sur `(completed)`. Ne jamais fermer la modale depuis `completed`.
132
+ - `config` est un `ImportWizardConfig` **complet** : voir `import-wizard.md` §3 pour ses règles (tuple `modes`, `interruption` requis, `keyFieldHint`).
133
+ - `modalTitle` et `label` sont des **clés** (`i18nKey(...)`), pas du texte.
134
+ - Ne pas rouvrir `ModalsService` à la main pour un import : c'est ce composant qui le fait.