@tekkare/romulus 0.1.112 → 0.1.114

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.111",
7
+ "version": "0.1.113",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -40,6 +40,7 @@ const emit = defineEmits<{
40
40
 
41
41
  const FR = {
42
42
  view: 'Vue',
43
+ brands: 'Marques',
43
44
  trigger: 'Déclencheur',
44
45
  watched: 'Champs suivis',
45
46
  channel: 'Canaux',
@@ -100,6 +101,7 @@ const FR = {
100
101
 
101
102
  const EN = {
102
103
  view: 'View',
104
+ brands: 'Brands',
103
105
  trigger: 'Trigger',
104
106
  watched: 'Watched fields',
105
107
  channel: 'Channels',
@@ -278,8 +280,10 @@ function pick(item: RmAlertMenuItem) {
278
280
  </div>
279
281
 
280
282
  <p v-if="alert.viewTitle" class="rm-alert__view">
281
- <RmIcon name="BookmarkSimple" :size="13" />
282
- {{ T.view }} : {{ alert.viewTitle }}
283
+ <!-- Une alerte de produit ne surveille pas une vue : l'annoncer comme
284
+ telle ferait chercher une vue qui n'existe pas. -->
285
+ <RmIcon :name="alert.kind === 'product' ? 'Tag' : 'BookmarkSimple'" :size="13" />
286
+ {{ alert.kind === 'product' ? T.brands : T.view }} : {{ alert.viewTitle }}
283
287
  <span v-if="alert.shared" class="rm-alert__shared">{{ T.shared }}</span>
284
288
  </p>
285
289
 
@@ -20,6 +20,7 @@
20
20
  import { computed, ref, watch } from 'vue'
21
21
  import type {
22
22
  RmAlertCadence,
23
+ RmAlertKind,
23
24
  RmAlertDraft,
24
25
  RmAlertField,
25
26
  RmAlertTrigger,
@@ -43,6 +44,19 @@ const props = withDefaults(
43
44
  canSaveScreen?: boolean
44
45
  /** Champs surveillables de la page. Vide : seule l'apparition compte. */
45
46
  fields?: RmAlertField[]
47
+ /**
48
+ * Marques proposees au choix, pour une alerte de produit. Elles arrivent
49
+ * du serveur a chaque saisie : le formulaire ne filtre pas lui-meme.
50
+ */
51
+ brandOptions?: string[]
52
+ brandsLoading?: boolean
53
+ /** Plafond de marques, tel que l'API l'impose. */
54
+ maxBrands?: number
55
+ /**
56
+ * L'ecran permet de partir d'une vue. Faux depuis un endroit qui n'en a
57
+ * pas : le choix se reduit alors a l'alerte de produit.
58
+ */
59
+ canWatchView?: boolean
46
60
  /** Nombre d'elements que la vue tient a l'ecran, s'il est connu. */
47
61
  scopeCount?: number | null
48
62
  submitting?: boolean
@@ -64,6 +78,10 @@ const props = withDefaults(
64
78
  {
65
79
  views: () => [],
66
80
  canSaveScreen: false,
81
+ brandOptions: () => [],
82
+ brandsLoading: false,
83
+ maxBrands: 20,
84
+ canWatchView: true,
67
85
  fields: () => [],
68
86
  scopeCount: null,
69
87
  submitting: false,
@@ -83,9 +101,21 @@ const emit = defineEmits<{
83
101
  * la vue surveillee doivent dire la meme chose au moment de la capture.
84
102
  */
85
103
  'select-view': [id: string]
104
+ /** Saisie du selecteur de marques, a porter jusqu'a l'index. */
105
+ 'search-brands': [value: string]
86
106
  }>()
87
107
 
88
108
  const FR = {
109
+ kind: 'Que surveiller',
110
+ kindView: 'Une vue',
111
+ kindViewHint: 'Un écran filtré, aussi fin que vous voulez, mais limité à cette page.',
112
+ kindProduct: 'Un produit',
113
+ kindProductHint: 'Une à {max} marques, suivies partout : prix, avis HAS, indications, ANSM, PUT, MITM, Journal officiel.',
114
+ brands: 'Marques surveillées',
115
+ brandsHint: 'Cherchez une marque et ajoutez-la. L’alerte couvre tout le hub pour chacune.',
116
+ brandsRequired: 'Choisissez au moins une marque.',
117
+ brandsTooMany: 'Une alerte suit {max} marques au maximum.',
118
+ productFields: 'Tous les champs sont suivis : une alerte de produit ne se restreint pas, elle rapporte tout ce qui bouge sur ces marques.',
89
119
  screenOption: 'Écran actuel (une vue sera créée)',
90
120
  viewSelect: 'Vue à surveiller',
91
121
  viewApplied: 'La vue choisie est appliquée à l’écran : c’est lui qui fournit la requête surveillée.',
@@ -128,6 +158,16 @@ const FR = {
128
158
  }
129
159
 
130
160
  const EN = {
161
+ kind: 'What to watch',
162
+ kindView: 'A view',
163
+ kindViewHint: 'A filtered screen, as precise as you like, but tied to that page.',
164
+ kindProduct: 'A product',
165
+ kindProductHint: 'One to {max} brands, watched everywhere: prices, HAS opinions, indications, ANSM, PUT, MITM, official journal.',
166
+ brands: 'Watched brands',
167
+ brandsHint: 'Search a brand and add it. The alert covers the whole hub for each one.',
168
+ brandsRequired: 'Pick at least one brand.',
169
+ brandsTooMany: 'An alert watches at most {max} brands.',
170
+ productFields: 'Every field is watched: a product alert does not narrow down, it reports whatever moves on those brands.',
131
171
  screenOption: 'Current screen (a view will be created)',
132
172
  viewSelect: 'View to watch',
133
173
  viewApplied: 'The chosen view is applied to the screen: the screen is what provides the watched query.',
@@ -189,6 +229,16 @@ const selectedView = computed(
189
229
 
190
230
  const editing = computed(() => props.mode === 'edit')
191
231
 
232
+ /**
233
+ * Ce que l'alerte surveille. Le choix ne se pose qu'a la creation : changer
234
+ * une alerte de vue en alerte de produit, ce n'est plus la meme alerte.
235
+ */
236
+ const kind = ref<RmAlertKind>(
237
+ (props.initial?.kind as RmAlertKind) ?? (props.canWatchView ? 'view' : 'product')
238
+ )
239
+ const brands = ref<string[]>(props.initial?.brands ?? [])
240
+ const watchesProduct = computed(() => kind.value === 'product')
241
+
192
242
  const title = ref(props.initial?.title ?? props.viewTitle)
193
243
  const triggers = ref<RmAlertTrigger[]>(props.initial?.triggers ?? ['appeared'])
194
244
  const watchedFields = ref<string[]>(
@@ -212,6 +262,22 @@ watch(
212
262
  },
213
263
  )
214
264
 
265
+ /**
266
+ * Le titre suit ce qu'on surveille, tant que l'utilisateur n'y a pas touche.
267
+ *
268
+ * En passant sur « un produit », garder le nom d'une vue donnerait une alerte
269
+ * « Suivi prix anticoagulants » qui suit en realite Opdivo.
270
+ */
271
+ watch([kind, brands], () => {
272
+ if (titleTouched.value) return
273
+ if (watchesProduct.value) {
274
+ title.value = brands.value.length ? `Veille ${brands.value.join(', ')}` : ''
275
+ return
276
+ }
277
+ const view = props.views.find((entry) => entry.id === selectedViewId.value)
278
+ title.value = view?.title ?? props.viewTitle
279
+ })
280
+
215
281
  watch(selectedViewId, (id) => {
216
282
  const view = props.views.find((entry) => entry.id === id)
217
283
  if (view && !titleTouched.value) title.value = view.title
@@ -263,8 +329,19 @@ const cadenceLabel = computed(
263
329
  () => cadenceOptions.value.find((option) => option.value === cadence.value)?.label ?? '',
264
330
  )
265
331
 
332
+ const brandsInvalid = computed(
333
+ () =>
334
+ watchesProduct.value &&
335
+ (brands.value.length === 0 || brands.value.length > props.maxBrands),
336
+ )
337
+
266
338
  const valid = computed(
267
- () => title.value.trim().length > 0 && triggers.value.length > 0 && !missingFields.value,
339
+ () =>
340
+ title.value.trim().length > 0 &&
341
+ triggers.value.length > 0 &&
342
+ !brandsInvalid.value &&
343
+ // Une alerte de produit suit tout : la question des champs ne se pose pas.
344
+ (watchesProduct.value || !missingFields.value),
268
345
  )
269
346
 
270
347
  /**
@@ -286,15 +363,21 @@ function submit() {
286
363
  if (!valid.value) return
287
364
  emit('submit', {
288
365
  title: title.value.trim(),
289
- viewId: selectedViewId.value,
366
+ kind: kind.value,
367
+ brands: watchesProduct.value ? brands.value : [],
368
+ viewId: watchesProduct.value ? null : selectedViewId.value,
290
369
  triggers: triggers.value,
291
370
  // Les champs ne servent qu'au declencheur « changement » : les garder
292
371
  // sans lui ferait porter a l'alerte une promesse qu'elle ne tient pas.
293
- watchedFields: watchesChanges.value
294
- ? anyField.value
295
- ? [RM_ALERT_ANY_FIELD]
296
- : watchedFields.value
297
- : [],
372
+ // Une alerte de produit traverse sept index : une liste de champs nommes
373
+ // n'y voudrait rien dire, ils different d'un index a l'autre.
374
+ watchedFields: watchesProduct.value
375
+ ? [RM_ALERT_ANY_FIELD]
376
+ : watchesChanges.value
377
+ ? anyField.value
378
+ ? [RM_ALERT_ANY_FIELD]
379
+ : watchedFields.value
380
+ : [],
298
381
  channels: emailOn.value ? ['in_app', 'email'] : ['in_app'],
299
382
  cadence: cadence.value,
300
383
  // Le partage viendra plus tard : le schema le porte deja, l'ecran non.
@@ -315,7 +398,49 @@ function submit() {
315
398
  "
316
399
  />
317
400
 
318
- <div v-if="!editing" class="rm-alert-form__view">
401
+ <fieldset v-if="!editing && canWatchView" class="rm-alert-form__group">
402
+ <legend class="rm-alert-form__label">{{ T.kind }}</legend>
403
+ <p class="rm-alert-form__hint">
404
+ {{
405
+ watchesProduct
406
+ ? T.kindProductHint.replace('{max}', String(maxBrands))
407
+ : T.kindViewHint
408
+ }}
409
+ </p>
410
+ <RmSegmented
411
+ v-model="kind"
412
+ block
413
+ variant="soft"
414
+ :options="[
415
+ { value: 'view', label: T.kindView },
416
+ { value: 'product', label: T.kindProduct },
417
+ ]"
418
+ />
419
+ </fieldset>
420
+
421
+ <fieldset v-if="watchesProduct" class="rm-alert-form__group">
422
+ <legend class="rm-alert-form__label">{{ T.brands }}</legend>
423
+ <p class="rm-alert-form__hint">{{ T.brandsHint }}</p>
424
+ <RmMultipleSelect
425
+ v-model="brands"
426
+ label=""
427
+ search
428
+ :filter-search="false"
429
+ :options="brandOptions"
430
+ :loading="brandsLoading"
431
+ :show-amount="true"
432
+ :lang="lang"
433
+ @search="emit('search-brands', $event)"
434
+ />
435
+ <p v-if="brands.length > maxBrands" class="rm-alert-form__error">
436
+ {{ T.brandsTooMany.replace('{max}', String(maxBrands)) }}
437
+ </p>
438
+ <p v-else-if="brands.length === 0" class="rm-alert-form__hint">
439
+ {{ T.brandsRequired }}
440
+ </p>
441
+ </fieldset>
442
+
443
+ <div v-else-if="!editing" class="rm-alert-form__view">
319
444
  <p class="rm-alert-form__label">{{ T.view }}</p>
320
445
  <p class="rm-alert-form__hint">
321
446
  <!-- L'avertissement sur l'application ne vaut que pour une vue deja
@@ -344,9 +469,9 @@ function submit() {
344
469
  <p v-if="!selectedViewId" class="rm-alert-form__hint">{{ T.screenNote }}</p>
345
470
  </div>
346
471
 
347
- <p v-else class="rm-alert-form__view-name">
348
- <RmIcon name="BookmarkSimple" :size="14" />
349
- {{ T.view }} : {{ viewTitle }}
472
+ <p v-else-if="editing" class="rm-alert-form__view-name">
473
+ <RmIcon :name="watchesProduct ? 'Tag' : 'BookmarkSimple'" :size="14" />
474
+ {{ watchesProduct ? `${T.brands} : ${brands.join(', ')}` : `${T.view} : ${viewTitle}` }}
350
475
  </p>
351
476
 
352
477
  <fieldset class="rm-alert-form__group">
@@ -366,7 +491,11 @@ function submit() {
366
491
  />
367
492
  </fieldset>
368
493
 
369
- <fieldset v-if="watchesChanges" class="rm-alert-form__group">
494
+ <p v-if="watchesProduct && watchesChanges" class="rm-alert-form__hint">
495
+ {{ T.productFields }}
496
+ </p>
497
+
498
+ <fieldset v-if="watchesChanges && !watchesProduct" class="rm-alert-form__group">
370
499
  <legend class="rm-alert-form__label">{{ T.fields }}</legend>
371
500
  <p class="rm-alert-form__hint">{{ anyField ? T.anyFieldHint : T.fieldsHint }}</p>
372
501
  <template v-if="fields.length">
@@ -30,7 +30,9 @@ export interface RmAlertMenuItem {
30
30
  export interface RmAlert {
31
31
  id: string;
32
32
  title: string;
33
- /** Nom de la vue surveillee. */
33
+ /** Ce que l'alerte surveille : une vue, ou des marques. */
34
+ kind?: RmAlertKind;
35
+ /** Nom de la vue surveillee, ou les marques d'une alerte de produit. */
34
36
  viewTitle?: string | null;
35
37
  status: RmAlertStatus;
36
38
  triggers: RmAlertTrigger[];
@@ -96,9 +98,19 @@ export interface RmAlertViewOption {
96
98
  id: string;
97
99
  title: string;
98
100
  }
101
+ /**
102
+ * Ce qu'une alerte surveille.
103
+ *
104
+ * `view` fige le perimetre d'un ecran : fin, mais lie a une page. `product`
105
+ * nomme des marques et vaut pour tout le hub, sans dependre d'aucune page.
106
+ */
107
+ export type RmAlertKind = 'view' | 'product';
99
108
  /** Ce que le formulaire de creation rend a l'appelant. */
100
109
  export interface RmAlertDraft {
101
110
  title: string;
111
+ kind: RmAlertKind;
112
+ /** Marques surveillees, pour une alerte de produit. */
113
+ brands: string[];
102
114
  /**
103
115
  * Vue a surveiller. `null` : l'ecran actuel, que l'appelant enregistrera
104
116
  * comme vue avant de creer l'alerte.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.112",
3
+ "version": "0.1.114",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",