@tekkare/romulus 0.1.243 → 0.1.245

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.
Files changed (42) hide show
  1. package/dist/module.json +1 -1
  2. package/layer/app/app.config.ts +6 -0
  3. package/layer/app/components/OipShell.vue +3 -0
  4. package/layer/app/components/layout/alertBrandSync.vue +143 -0
  5. package/layer/app/components/layout/tool/adminTemplates.vue +261 -0
  6. package/layer/app/components/layout/tool/alerts.vue +969 -0
  7. package/layer/app/components/layout/tool/export.vue +590 -0
  8. package/layer/app/components/layout/tool/views.vue +933 -0
  9. package/layer/app/components/layout/viewEmptyState.vue +278 -0
  10. package/layer/app/components/layout/viewPill.vue +550 -0
  11. package/layer/app/components/panels/OipPanelStatistics.vue +447 -0
  12. package/layer/app/composables/useAdminAccess.ts +86 -0
  13. package/layer/app/composables/useAlertBrandSync.ts +85 -0
  14. package/layer/app/composables/useAlertSource.ts +210 -0
  15. package/layer/app/composables/useAlerts.ts +345 -0
  16. package/layer/app/composables/useExportSource.ts +236 -0
  17. package/layer/app/composables/useHubrEmbedded.ts +13 -0
  18. package/layer/app/composables/usePermalink.ts +108 -0
  19. package/layer/app/composables/useSavedExports.ts +286 -0
  20. package/layer/app/composables/useViewCardActions.ts +500 -0
  21. package/layer/app/composables/useViewTemplates.ts +369 -0
  22. package/layer/app/composables/useViews.ts +574 -0
  23. package/layer/app/oip-defaults/alerts.ts +34 -0
  24. package/layer/app/oip-defaults/api.ts +18 -0
  25. package/layer/app/oip-defaults/export.ts +23 -0
  26. package/layer/app/oip-defaults/ui.ts +21 -0
  27. package/layer/app/oip-defaults/views.ts +59 -0
  28. package/layer/app/types/stats.ts +49 -0
  29. package/layer/app/utils/alertFieldLabels.ts +191 -0
  30. package/layer/app/utils/oipRail.ts +29 -3
  31. package/layer/app/utils/oipScopeStore.ts +112 -0
  32. package/layer/app/utils/oipScopes.ts +376 -0
  33. package/layer/app/utils/viewPages.ts +30 -0
  34. package/layer/app/utils/viewTemplateCriteria.ts +215 -0
  35. package/layer/app/utils/viewTemplates.ts +116 -0
  36. package/layer/nuxt.config.ts +18 -0
  37. package/layer/server/api/oip/notes.get.ts +1 -1
  38. package/layer/server/api/oip/stats/users.get.ts +80 -0
  39. package/layer/server/api/oip/stats.get.ts +88 -0
  40. package/layer/server/utils/oipPosthog.ts +134 -0
  41. package/layer/server/utils/oipWorkspaces.ts +51 -0
  42. package/package.json +3 -2
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Menu des cartes de vue et ce qu'il declenche : renommer, alerte, export,
3
+ * dupliquer, partager, par defaut, supprimer.
4
+ *
5
+ * Partage par le panneau « Mes vues » du rail et l'etat vide des pages a vue
6
+ * obligatoire : les memes cartes doivent offrir le meme menu aux deux
7
+ * endroits, sans deux copies du partage a tenir a jour. Chaque appelant monte
8
+ * son propre `RmShareDialog` sur ce que renvoie le composable.
9
+ *
10
+ * `apply` reste a l'appelant : l'etat vide ramene la vue sur le chemin de la
11
+ * page, le panneau l'applique telle quelle.
12
+ */
13
+ import { useOipApi } from "#oip-app/api";
14
+ import { oipSnackbar } from "#oip-app/ui";
15
+ import { encodeCriteria, languageBoundFilters } from "../utils/viewTemplateCriteria";
16
+
17
+ export function useViewCardActions(apply: (id: string) => void) {
18
+ const { locale, t, te } = useI18n();
19
+ const views = useViews();
20
+ const alerts = useAlerts();
21
+ const editing = useViewEditing();
22
+
23
+ const source = computed(() => views.current.value);
24
+ const filterLabel = useViewFilterLabels(() => source.value?.filterNamespace);
25
+ const store = () => views.scopes.value();
26
+ const selected = computed(() => views.active.value);
27
+ const parseCriteria = parseViewCriteria;
28
+
29
+ function scopeOf(id: string) {
30
+ return store().scopeList.value.find((scope: any) => scope.id === id) ?? null;
31
+ }
32
+
33
+ /*
34
+ * Les menus de carte, declares avant `cardsOf` qui les lit.
35
+ *
36
+ * Ils vivaient plus bas, et un `watch` sur la liste des vues recues evaluait
37
+ * sa source des le `setup` : la carte d'une vue partagee allait chercher son
38
+ * menu avant la ligne qui le declare. En developpement rien ne se voyait ; le
39
+ * bundle, lui, jetait « Cannot access ... before initialization » et vidait
40
+ * l'ecran — chez le destinataire d'un partage, lui seul, puisque lui seul a
41
+ * une vue qui n'est pas la sienne.
42
+ */
43
+ /** Le libelle dit ce que le clic va faire, pas juste ce qu'est l'entree :
44
+ une vue deja par defaut doit pouvoir se retirer, pas se redefinir. */
45
+ function defaultEntry(isDefault: boolean) {
46
+ return {
47
+ key: "default",
48
+ label: isDefault
49
+ ? (locale.value === "en" ? "Remove as default" : "Retirer par défaut")
50
+ : (locale.value === "en" ? "Set as default" : "Définir par défaut"),
51
+ icon: "PushPin",
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Raccourci vers le formulaire d'alerte, sur la vue de la carte.
57
+ *
58
+ * Avant, il fallait appliquer la vue, passer sur l'onglet Alertes, ouvrir la
59
+ * modale et y rechoisir la vue. Une vue recue le propose aussi : l'API
60
+ * accepte une alerte sur toute vue que le lecteur voit.
61
+ */
62
+ const alertEntry = computed(() => ({
63
+ key: "alert",
64
+ label: locale.value === "en" ? "Create an alert" : "Créer une alerte",
65
+ icon: "Bell",
66
+ }));
67
+
68
+ /**
69
+ * Raccourci vers le classeur Excel de la vue : l'appliquer, puis laisser la
70
+ * tuile Export generer avec sa recette par defaut (tous onglets, toutes
71
+ * colonnes). Demande client : garder une selection dans Excel sans passer par
72
+ * le panneau. Absent quand la page n'exporte rien.
73
+ */
74
+ const exportSource = useExportSource();
75
+ const exportEntry = computed(() =>
76
+ exportSource.has.value
77
+ ? [{ key: "export", label: locale.value === "en" ? "Export to Excel" : "Exporter en Excel", icon: "FileXls" }]
78
+ : [],
79
+ );
80
+
81
+ const duplicateEntry = computed(() => ({
82
+ key: "duplicate",
83
+ label: locale.value === "en" ? "Duplicate" : "Dupliquer",
84
+ icon: "Copy",
85
+ }));
86
+
87
+ function ownerMenu(scope: any) {
88
+ const isDefault = scope.isDefault === true;
89
+ return [
90
+ // Le formulaire complet (nom, description, contenu), celui de la pastille
91
+ // de vue : renommer sur place ne laissait toucher qu'au titre.
92
+ {
93
+ key: "edit",
94
+ label: locale.value === "en" ? "Edit" : "Modifier",
95
+ icon: "PencilSimple",
96
+ },
97
+ alertEntry.value,
98
+ ...exportEntry.value,
99
+ duplicateEntry.value,
100
+ {
101
+ key: "share",
102
+ label: locale.value === "en" ? "Share" : "Partager",
103
+ icon: "UsersThree",
104
+ },
105
+ defaultEntry(isDefault),
106
+ {
107
+ key: "delete",
108
+ label: locale.value === "en" ? "Delete" : "Supprimer",
109
+ icon: "Trash",
110
+ danger: true,
111
+ confirm: true,
112
+ confirmText: deleteQuestion(scope),
113
+ },
114
+ ];
115
+ }
116
+
117
+ /**
118
+ * La question posee avant de supprimer. « Supprimer definitivement ? » suffit
119
+ * pour une vue que personne d'autre ne voit ; une vue partagee ou publiee en
120
+ * template disparait aussi chez ses lecteurs, et c'est ce qu'il faut dire.
121
+ */
122
+ function deleteQuestion(scope: any): string | undefined {
123
+ const en = locale.value === "en";
124
+ const title = `« ${scope.title} »`;
125
+ if (scope.isTemplate) {
126
+ return en
127
+ ? `${title} is a Tekkare template offered to client workspaces. Deleting it removes it from all of them. Delete anyway?`
128
+ : `${title} est un template Tekkare offert à des espaces clients. La supprimer le retire chez tous. Supprimer quand même ?`;
129
+ }
130
+ if (isShared(scope)) {
131
+ const n = scope.sharedWith?.length ?? 0;
132
+ const who =
133
+ scope.visibility === "workspace"
134
+ ? en
135
+ ? "the whole workspace"
136
+ : "tout l'espace"
137
+ : en
138
+ ? `${n} ${n > 1 ? "people" : "person"}`
139
+ : `${n} personne${n > 1 ? "s" : ""}`;
140
+ return en
141
+ ? `${title} is shared with ${who}. Deleting it removes it for them too. Delete anyway?`
142
+ : `${title} est partagée avec ${who}. La supprimer la retire aussi chez eux. Supprimer quand même ?`;
143
+ }
144
+ return undefined;
145
+ }
146
+
147
+ /**
148
+ * Menu d'une vue recue : la marquer et la dupliquer, rien d'autre. Le favori
149
+ * et la vue par defaut sont des marques du lecteur, elles valent ici aussi ;
150
+ * renommer, partager et supprimer appartiennent au proprietaire.
151
+ *
152
+ * Dupliquer est la sortie de secours : qui veut changer les filtres d'une vue
153
+ * recue s'en fait une copie, qui devient la sienne.
154
+ */
155
+ function readerMenu(isDefault: boolean) {
156
+ return [alertEntry.value, ...exportEntry.value, defaultEntry(isDefault), duplicateEntry.value];
157
+ }
158
+
159
+ /** La vue est ouverte a d'autres : a tout l'espace, ou a des gens nommes. */
160
+ function isShared(scope: any): boolean {
161
+ return scope.visibility === "workspace" || (scope.sharedWith?.length ?? 0) > 0;
162
+ }
163
+
164
+ // ------------------------------------------------------------------- partage
165
+
166
+ const elysia = useOipApi();
167
+ const { data: session } = useAuth();
168
+
169
+ /** Vue dont on regle le partage, ou null. */
170
+ const sharingId = ref<string | null>(null);
171
+ const sharing = computed({
172
+ get: () => sharingId.value !== null,
173
+ set: (open: boolean) => {
174
+ if (!open) sharingId.value = null;
175
+ },
176
+ });
177
+ const shareTarget = computed(() => (sharingId.value ? scopeOf(sharingId.value) : null));
178
+
179
+ const members = ref<{ userId: string; name: string; email?: string }[]>([]);
180
+ const membersLoaded = ref(false);
181
+
182
+ /**
183
+ * Publication en template : reservee a qui administre la plateforme.
184
+ *
185
+ * Le portillon et la liste des espaces ne sont demandes qu'a l'ouverture du
186
+ * dialogue : sans cela, tout le monde paierait deux allers-retours pour une
187
+ * option que presque personne ne voit.
188
+ */
189
+ const { isSuperAdmin, workspaces, ensureLoaded, ensureWorkspaces } = useAdminAccess();
190
+
191
+ /**
192
+ * Ce que la publication ne saura pas traduire, dit avant d'enregistrer.
193
+ *
194
+ * Quelques filtres portent des valeurs qui existent en deux orthographes
195
+ * dans l'index (cf. `LANGUAGE_BOUND`) : leur paire ne vit ni dans l'i18n ni
196
+ * dans le code, et la v1 ne la resout pas. Le template s'affichera alors dans
197
+ * la langue ou il a ete ecrit, ce qui se dit plutot que se decouvre.
198
+ */
199
+ const templateNote = computed(() => {
200
+ if (!shareTarget.value) return "";
201
+ const bound = languageBoundFilters(encodedCriteria(shareTarget.value));
202
+ if (!bound.length) return "";
203
+ const names = bound.map((filter) => filterLabel(filter)).join(", ");
204
+ return locale.value === "en"
205
+ ? `Kept in English: ${names}. Those values exist in one language only for now, so readers in French will see them as written.`
206
+ : `Gardé en français : ${names}. Ces valeurs n'existent pour l'instant que dans une langue, les lecteurs en anglais les verront telles quelles.`;
207
+ });
208
+
209
+ /** Les criteres d'une vue, ranges pour se relire dans les deux langues. */
210
+ function encodedCriteria(scope: any) {
211
+ return encodeCriteria(parseCriteria(scope), {
212
+ lang: locale.value === "en" ? "en" : "fr",
213
+ t,
214
+ te: (key: string) => te(key),
215
+ namespace: source.value?.filterNamespace ?? "filters",
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Membres de l'espace, moi excepte : se partager sa propre vue n'a pas de
221
+ * sens, et la ligne se lirait comme un destinataire de plus.
222
+ *
223
+ * Charges a la premiere ouverture du dialogue, pas au montage du panneau :
224
+ * la plupart des ouvertures de « Mes vues » ne partagent rien.
225
+ */
226
+ async function loadMembers() {
227
+ if (membersLoaded.value) return;
228
+ membersLoaded.value = true;
229
+ try {
230
+ const response = await elysia.profile.getWorkspaceMembers();
231
+ const me = (session.value as any)?.user ?? {};
232
+ members.value = (response.members ?? [])
233
+ .filter(
234
+ (member: any) =>
235
+ member.email !== me.email && member.userId !== (me.id ?? me.sub),
236
+ )
237
+ .map((member: any) => ({
238
+ userId: member.userId,
239
+ name:
240
+ member.displayName ||
241
+ [member.firstName, member.lastName].filter(Boolean).join(" ") ||
242
+ member.email,
243
+ email: member.email,
244
+ }));
245
+ } catch (error) {
246
+ // Le dialogue reste utilisable : « tout l'espace » ne demande aucune liste.
247
+ membersLoaded.value = false;
248
+ console.error("[views] Error loading workspace members:", error);
249
+ }
250
+ }
251
+
252
+ async function openShare(id: string) {
253
+ sharingId.value = id;
254
+ outsiders.value = [];
255
+ await Promise.all([
256
+ loadMembers(),
257
+ ensureLoaded().then((admin) => (admin ? ensureWorkspaces() : [])),
258
+ ]);
259
+ }
260
+
261
+ /**
262
+ * Personnes hors de l'espace, trouvees par la recherche du dialogue.
263
+ *
264
+ * Un administrateur de la plateforme partage ses vues avec des gens
265
+ * d'espaces clients, qui ne sont pas membres du sien : la liste des membres
266
+ * ne les connait pas, la plateforme entiere si. La recherche part a chaque
267
+ * frappe (deux lettres au moins), le dialogue filtre ensuite lui-meme ; les
268
+ * membres de l'espace, deja listes, n'y sont pas repris. Ces personnes
269
+ * portent la mention « hors de l'espace » pour qu'on sache a qui on ouvre.
270
+ */
271
+ const outsiders = ref<{ userId: string; name: string; email?: string; hint?: string }[]>([]);
272
+ let outsiderSearch: ReturnType<typeof setTimeout> | null = null;
273
+
274
+ const shareMembers = computed(() => [...members.value, ...outsiders.value]);
275
+
276
+ function onShareSearch(query: string) {
277
+ if (!isSuperAdmin.value) return;
278
+ if (outsiderSearch) clearTimeout(outsiderSearch);
279
+ if (query.length < 2) {
280
+ outsiders.value = [];
281
+ return;
282
+ }
283
+ outsiderSearch = setTimeout(async () => {
284
+ try {
285
+ const response = await elysia.admin.users({ search: query, limit: 20 });
286
+ const known = new Set(members.value.map((member) => member.userId));
287
+ const me = (session.value as any)?.user ?? {};
288
+ outsiders.value = (response.users ?? [])
289
+ .filter((user) => !known.has(user.userId) && user.email !== me.email)
290
+ .map((user) => ({
291
+ userId: user.userId,
292
+ name:
293
+ user.displayName ||
294
+ [user.firstName, user.lastName].filter(Boolean).join(" ") ||
295
+ user.email,
296
+ email: user.email,
297
+ hint: locale.value === "en" ? "outside the workspace" : "hors de l'espace",
298
+ }));
299
+ } catch (error) {
300
+ console.error("[views] Error searching platform users:", error);
301
+ }
302
+ }, 250);
303
+ }
304
+
305
+ /** Un mot dans la barre du bas. Meme mecanisme que le panneau des alertes. */
306
+ function notify(text: string) {
307
+ oipSnackbar([
308
+ { attribute: "isVisible", value: true },
309
+ { attribute: "text", value: text },
310
+ { attribute: "loader", value: false },
311
+ { attribute: "hasTimeOut", value: true },
312
+ ]);
313
+ }
314
+
315
+ /**
316
+ * Ce que le partage vient de faire, dit a l'ecran.
317
+ *
318
+ * Une vue partagee ne change presque rien a sa carte, et une publication
319
+ * refusee n'y change rien du tout : sans un mot, les deux se lisent pareil,
320
+ * c'est-a-dire comme si rien ne s'etait passe.
321
+ */
322
+ function shareOutcome(
323
+ share: { visibility: string; sharedWith: string[]; isTemplate?: boolean },
324
+ targets: number,
325
+ ): string {
326
+ const en = locale.value === "en";
327
+ if (share.isTemplate) {
328
+ if (targets === 0) {
329
+ return en
330
+ ? "View published as a Tekkare template, offered to every workspace."
331
+ : "Vue publiée comme template Tekkare, offerte à tous les espaces.";
332
+ }
333
+ return en
334
+ ? `View published as a Tekkare template for ${targets} workspace${targets > 1 ? "s" : ""}.`
335
+ : `Vue publiée comme template Tekkare pour ${targets} espace${targets > 1 ? "s" : ""}.`;
336
+ }
337
+ if (share.visibility === "workspace") {
338
+ return en ? "View shared with the whole workspace." : "Vue partagée à tout l'espace.";
339
+ }
340
+ if (share.sharedWith.length) {
341
+ const n = share.sharedWith.length;
342
+ return en
343
+ ? `View shared with ${n} ${n > 1 ? "people" : "person"}.`
344
+ : `Vue partagée à ${n} personne${n > 1 ? "s" : ""}.`;
345
+ }
346
+ return en ? "View is private again." : "Vue redevenue privée.";
347
+ }
348
+
349
+ async function confirmShare(share: {
350
+ visibility: "private" | "workspace";
351
+ sharedWith: string[];
352
+ isTemplate?: boolean;
353
+ targetWorkspaces?: string[];
354
+ }) {
355
+ const id = sharingId.value;
356
+ const target = shareTarget.value;
357
+ sharingId.value = null;
358
+ if (!id) return;
359
+
360
+ // Les criteres encodes ne partent qu'a la publication : ils vont dans leur
361
+ // propre colonne, et la vue garde les siens, ceux que l'ecran applique.
362
+ const ok = await store().shareScope(id, {
363
+ ...share,
364
+ ...(share.isTemplate && target && { templateCriteria: encodedCriteria(target) }),
365
+ });
366
+
367
+ if (ok) {
368
+ notify(shareOutcome(share, share.targetWorkspaces?.length ?? 0));
369
+ return;
370
+ }
371
+
372
+ // La publication est la seule portee que l'API peut refuser a qui a pu
373
+ // l'afficher : le portillon est relu cote serveur, et un compte qui perd
374
+ // son acces garde le dialogue ouvert jusqu'au rechargement.
375
+ notify(
376
+ share.isTemplate
377
+ ? locale.value === "en"
378
+ ? "Publishing the template was refused. Platform administration access is needed."
379
+ : "La publication du template a été refusée. Il faut un accès à l'administration de la plateforme."
380
+ : locale.value === "en"
381
+ ? "The view could not be shared."
382
+ : "Le partage de la vue n'a pas abouti.",
383
+ );
384
+ }
385
+
386
+
387
+ async function rename(id: string, title: string) {
388
+ const scope = scopeOf(id);
389
+ if (!scope) return;
390
+ await store().updateScope({
391
+ id,
392
+ title,
393
+ description: scope.description,
394
+ criteria: parseCriteria(scope),
395
+ currentPage: scope.page,
396
+ // Les criteres ne bougent pas : rien a repointer cote alertes, et la
397
+ // requete de l'ecran courant n'a rien a faire dans une vue qu'on renomme
398
+ // peut-etre depuis une autre page.
399
+ refreshAlerts: false,
400
+ });
401
+ // La vue active porte une copie : sans ce rappel, la banniere et le bandeau
402
+ // continueraient d'afficher l'ancien nom.
403
+ if (selected.value?.id === id) apply(id);
404
+ }
405
+
406
+ async function duplicate(id: string) {
407
+ const scope = scopeOf(id);
408
+ if (!scope) return;
409
+ const suffix = locale.value === "en" ? "copy" : "copie";
410
+ await store().createScope({
411
+ title: `${scope.title} (${suffix})`,
412
+ description: scope.description,
413
+ criteria: parseCriteria(scope),
414
+ currentPage: scope.page,
415
+ });
416
+ }
417
+
418
+ async function remove(id: string) {
419
+ if (selected.value?.id === id) store().clearScope();
420
+ await store().deleteScope(id);
421
+ }
422
+
423
+ function onAction(key: string, id: string) {
424
+ if (key === "edit") editView(id);
425
+ else if (key === "alert") createAlert(id);
426
+ else if (key === "export") exportView(id);
427
+ else if (key === "duplicate") duplicate(id);
428
+ else if (key === "share") openShare(id);
429
+ // Une entree de menu, deux sens : la poser sur la vue qui la porte deja la
430
+ // retire, comme le faisait le marquage local.
431
+ else if (key === "default") setDefault(id);
432
+ else if (key === "delete") remove(id);
433
+ }
434
+
435
+ /**
436
+ * Appliquer la vue, puis ouvrir son formulaire de modification : il compare la
437
+ * vue a l'ecran et retouche son contenu depuis la page, l'ecran doit donc la
438
+ * porter. Le cadre ouvre la tuile « Mes vues » des que `editing` est pose.
439
+ */
440
+ function editView(id: string) {
441
+ apply(id);
442
+ editing.value = { id };
443
+ }
444
+
445
+ /**
446
+ * Appliquer la vue, puis ouvrir le formulaire d'alerte : la requete
447
+ * Elasticsearch part de l'ecran, il doit donc porter cette vue. Le formulaire
448
+ * preselectionne la vue active, et le cadre passe sur l'onglet Alertes des
449
+ * que `creating` monte (cf. LayoutShell).
450
+ */
451
+ function createAlert(id: string) {
452
+ apply(id);
453
+ alerts.creating.value = true;
454
+ }
455
+
456
+ /**
457
+ * Appliquer la vue, puis demander le classeur : l'export part de l'ecran,
458
+ * il doit donc porter cette vue. Le cadre passe sur la tuile Export, qui
459
+ * lance la generation a son montage (cf. useExportSource.quick).
460
+ */
461
+ function exportView(id: string) {
462
+ apply(id);
463
+ exportSource.quick.value = "xlsx";
464
+ }
465
+
466
+ /** Marquer par defaut, ou retirer la marque si la vue la porte deja. */
467
+ async function setDefault(id: string) {
468
+ const scope = scopeOf(id);
469
+ if (!scope) return;
470
+ await store().setDefault(id, scope.isDefault !== true);
471
+ }
472
+
473
+ async function star(id: string) {
474
+ const scope = scopeOf(id);
475
+ if (!scope) return;
476
+ await store().setStarred(id, scope.starred !== true);
477
+ }
478
+
479
+
480
+ /** Le menu d'une carte : complet pour mes vues, reduit pour une vue recue. */
481
+ function menuOf(scope: any) {
482
+ return scope.isOwner !== false ? ownerMenu(scope) : readerMenu(scope.isDefault === true);
483
+ }
484
+
485
+ return {
486
+ menuOf,
487
+ isShared,
488
+ onAction,
489
+ rename,
490
+ star,
491
+ sharing,
492
+ shareTarget,
493
+ shareMembers,
494
+ isSuperAdmin,
495
+ workspaces,
496
+ templateNote,
497
+ onShareSearch,
498
+ confirmShare,
499
+ };
500
+ }