@genesislcap/pbc-notify-ui 15.8.3 → 15.9.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.
Files changed (102) hide show
  1. package/README.md +39 -0
  2. package/custom-elements-manifest.config.js +5 -0
  3. package/dist/custom-elements.json +1771 -321
  4. package/dist/dts/components/components.d.ts.map +1 -1
  5. package/dist/dts/components/foundation-inbox/inbox-base/inbox-base.d.ts +2 -0
  6. package/dist/dts/components/foundation-inbox/inbox-base/inbox-base.d.ts.map +1 -1
  7. package/dist/dts/components/foundation-inbox/inbox.styles.d.ts.map +1 -1
  8. package/dist/dts/components/foundation-inbox/inbox.template.d.ts.map +1 -1
  9. package/dist/dts/components/foundation-inbox/inbox.types.d.ts +4 -1
  10. package/dist/dts/components/foundation-inbox/inbox.types.d.ts.map +1 -1
  11. package/dist/dts/components/foundation-notification-dashboard/alert-sound-attachment.d.ts +86 -0
  12. package/dist/dts/components/foundation-notification-dashboard/alert-sound-attachment.d.ts.map +1 -0
  13. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.d.ts +39 -1
  14. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.d.ts.map +1 -1
  15. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.d.ts.map +1 -1
  16. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.d.ts +8 -0
  17. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.d.ts.map +1 -1
  18. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-management.d.ts.map +1 -1
  19. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.d.ts +41 -1
  20. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.d.ts.map +1 -1
  21. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.d.ts.map +1 -1
  22. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.d.ts +2 -1
  23. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.d.ts.map +1 -1
  24. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-management.d.ts.map +1 -1
  25. package/dist/dts/components/foundation-notification-dashboard/styles/dynamic-rule.styles.d.ts.map +1 -1
  26. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.d.ts +73 -0
  27. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.d.ts.map +1 -0
  28. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.styles.d.ts +2 -0
  29. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.styles.d.ts.map +1 -0
  30. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.template.d.ts +3 -0
  31. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.template.d.ts.map +1 -0
  32. package/dist/dts/components/foundation-sounds/foundation-sounds.d.ts +34 -0
  33. package/dist/dts/components/foundation-sounds/foundation-sounds.d.ts.map +1 -0
  34. package/dist/dts/components/foundation-sounds/foundation-sounds.styles.d.ts +3 -0
  35. package/dist/dts/components/foundation-sounds/foundation-sounds.styles.d.ts.map +1 -0
  36. package/dist/dts/index.d.ts +4 -0
  37. package/dist/dts/index.d.ts.map +1 -1
  38. package/dist/dts/react.d.ts +31 -11
  39. package/dist/dts/services/inbox.service.d.ts +6 -0
  40. package/dist/dts/services/inbox.service.d.ts.map +1 -1
  41. package/dist/dts/services/rule.service.d.ts +2 -1
  42. package/dist/dts/services/rule.service.d.ts.map +1 -1
  43. package/dist/dts/utils/alert-sound.d.ts +177 -0
  44. package/dist/dts/utils/alert-sound.d.ts.map +1 -0
  45. package/dist/dts/utils/alert-sound.test.d.ts +2 -0
  46. package/dist/dts/utils/alert-sound.test.d.ts.map +1 -0
  47. package/dist/esm/components/components.js +4 -0
  48. package/dist/esm/components/foundation-inbox/inbox-base/inbox-base.js +6 -0
  49. package/dist/esm/components/foundation-inbox/inbox.styles.js +15 -0
  50. package/dist/esm/components/foundation-inbox/inbox.template.js +16 -0
  51. package/dist/esm/components/foundation-inbox/inbox.types.js +1 -0
  52. package/dist/esm/components/foundation-notification-dashboard/alert-sound-attachment.js +218 -0
  53. package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.js +17 -2
  54. package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.js +3 -0
  55. package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-management.js +5 -1
  56. package/dist/esm/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.js +23 -2
  57. package/dist/esm/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.js +3 -0
  58. package/dist/esm/components/foundation-notification-dashboard/components/templates/template-management.js +8 -1
  59. package/dist/esm/components/foundation-notification-dashboard/styles/dynamic-rule.styles.js +31 -1
  60. package/dist/esm/components/foundation-sounds/foundation-sounds-panel.js +283 -0
  61. package/dist/esm/components/foundation-sounds/foundation-sounds-panel.styles.js +200 -0
  62. package/dist/esm/components/foundation-sounds/foundation-sounds-panel.template.js +179 -0
  63. package/dist/esm/components/foundation-sounds/foundation-sounds.js +206 -0
  64. package/dist/esm/components/foundation-sounds/foundation-sounds.styles.js +7 -0
  65. package/dist/esm/index.js +3 -0
  66. package/dist/esm/services/inbox.service.js +6 -2
  67. package/dist/esm/services/rule.service.js +35 -0
  68. package/dist/esm/utils/alert-sound.js +955 -0
  69. package/dist/esm/utils/alert-sound.test.js +25 -0
  70. package/dist/esm/utils/index.js +3 -0
  71. package/dist/pbc-notify-ui.api.json +2859 -332
  72. package/dist/pbc-notify-ui.d.ts +281 -3
  73. package/dist/react.cjs +19 -5
  74. package/dist/react.mjs +16 -4
  75. package/dist/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +23 -23
  77. package/src/components/components.ts +4 -0
  78. package/src/components/foundation-inbox/inbox-base/inbox-base.ts +7 -0
  79. package/src/components/foundation-inbox/inbox.styles.ts +15 -0
  80. package/src/components/foundation-inbox/inbox.template.ts +16 -0
  81. package/src/components/foundation-inbox/inbox.types.ts +3 -0
  82. package/src/components/foundation-notification-dashboard/alert-sound-attachment.ts +283 -0
  83. package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.ts +3 -0
  84. package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.ts +21 -2
  85. package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.ts +9 -0
  86. package/src/components/foundation-notification-dashboard/components/rules/rule-management.ts +5 -1
  87. package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.ts +3 -0
  88. package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.ts +25 -2
  89. package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.ts +2 -1
  90. package/src/components/foundation-notification-dashboard/components/templates/template-management.ts +8 -1
  91. package/src/components/foundation-notification-dashboard/styles/dynamic-rule.styles.ts +31 -1
  92. package/src/components/foundation-sounds/foundation-sounds-panel.styles.ts +201 -0
  93. package/src/components/foundation-sounds/foundation-sounds-panel.template.ts +188 -0
  94. package/src/components/foundation-sounds/foundation-sounds-panel.ts +280 -0
  95. package/src/components/foundation-sounds/foundation-sounds.styles.ts +8 -0
  96. package/src/components/foundation-sounds/foundation-sounds.ts +222 -0
  97. package/src/index.ts +8 -0
  98. package/src/services/inbox.service.ts +12 -2
  99. package/src/services/rule.service.ts +37 -1
  100. package/src/utils/alert-sound.test.ts +32 -0
  101. package/src/utils/alert-sound.ts +1135 -0
  102. package/src/utils/index.ts +3 -0
@@ -261,6 +261,14 @@ export const FoundationInboxTemplate = html<FoundationInbox>`
261
261
  </rapid-tab>
262
262
  `,
263
263
  )}
264
+ <rapid-tab
265
+ slot="sounds-tab"
266
+ id=${InboxTab.SOUNDS}
267
+ @click=${(x) => x.inboxTabChanged(InboxTab.SOUNDS)}
268
+ appearance="secondary"
269
+ >
270
+ Sounds
271
+ </rapid-tab>
264
272
 
265
273
  <!-- Alerts -->
266
274
  ${(_) => alertsTab(InboxTab.ALERTS_NEW)} ${(_) => alertsTab(InboxTab.ALERT_HISTORY)}
@@ -372,6 +380,14 @@ export const FoundationInboxTemplate = html<FoundationInbox>`
372
380
  </rapid-tab-panel>
373
381
  `,
374
382
  )}
383
+ <rapid-tab-panel
384
+ slot="sounds-tab"
385
+ class="sounds-tab-panel"
386
+ id=${InboxTab.SOUNDS}
387
+ style=${(x) => (x.selectedTab === InboxTab.SOUNDS ? '' : 'display: none;')}
388
+ >
389
+ <foundation-sounds-panel ${ref('sounds')}></foundation-sounds-panel>
390
+ </rapid-tab-panel>
375
391
  </rapid-tabs>
376
392
  </div>
377
393
  `;
@@ -9,6 +9,8 @@ export interface Alert {
9
9
  NOTIFY_SEVERITY: string;
10
10
  TABLE_ENTITY_ID: string;
11
11
  TOPIC: string;
12
+ /** Source notify template/rule that produced this alert. */
13
+ DYNAMIC_RULE_ID?: string;
12
14
  }
13
15
 
14
16
  export enum AlertStatus {
@@ -87,4 +89,5 @@ export enum InboxTab {
87
89
  ALERTS_NEW,
88
90
  ALERT_HISTORY,
89
91
  SUBSCRIBE,
92
+ SOUNDS,
90
93
  }
@@ -0,0 +1,283 @@
1
+ import { showNotification } from '@genesislcap/foundation-notifications';
2
+ import { GenesisElement, html, observable, ref, when } from '@genesislcap/web-core';
3
+ import type { RuleService } from '../../services/rule.service';
4
+ import {
5
+ AlertSoundPlayer,
6
+ base64ToBlob,
7
+ blobToBase64,
8
+ formatBytes,
9
+ MAX_SOUND_FILE_BYTES,
10
+ resolveAudioMimeType,
11
+ } from '../../utils/alert-sound';
12
+
13
+ type Constructor<T = object> = abstract new (...args: any[]) => T;
14
+
15
+ /** Sound fields included on rule/template create & update event payloads. */
16
+ export type AlertSoundPayloadFields = {
17
+ ALERT_SOUND_FILE_NAME?: string;
18
+ ALERT_SOUND_MIME_TYPE?: string;
19
+ ALERT_SOUND_DATA?: string;
20
+ CLEAR_ALERT_SOUND?: boolean;
21
+ };
22
+
23
+ function showAlertSoundNotice(title: string, body: string) {
24
+ showNotification(
25
+ {
26
+ title,
27
+ body,
28
+ config: {
29
+ snackbar: {
30
+ type: 'error',
31
+ },
32
+ },
33
+ },
34
+ 'rapid',
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Host surface required by {@link alertSoundAttachmentTemplate} and the mixin.
40
+ * @public
41
+ */
42
+ export interface AlertSoundAttachmentHost {
43
+ alertSoundFileName: string;
44
+ hasAlertSound: boolean;
45
+ fileInput: HTMLInputElement;
46
+ openAlertSoundPicker(): void;
47
+ onAlertSoundSelected(event: Event): void | Promise<void>;
48
+ testAttachedAlertSound(): void;
49
+ clearAttachedAlertSound(): void;
50
+ }
51
+
52
+ /**
53
+ * Shared Attach/Replace/Test/Clear UI for rule and template dialogs.
54
+ * @public
55
+ */
56
+ export const alertSoundAttachmentTemplate = html<AlertSoundAttachmentHost>`
57
+ <div class="alert-sound">
58
+ <input
59
+ ${ref('fileInput')}
60
+ class="file-input"
61
+ type="file"
62
+ accept="audio/*"
63
+ @change=${(x, c) => x.onAlertSoundSelected(c.event)}
64
+ />
65
+ <label>Alert sound</label>
66
+ <div class="alert-sound-row">
67
+ <span class="alert-sound-name" title=${(x) => x.alertSoundFileName || ''}>
68
+ ${(x) => x.alertSoundFileName || 'No sound attached'}
69
+ </span>
70
+ <rapid-button appearance="lightweight" @click=${(x) => x.openAlertSoundPicker()}>
71
+ ${(x) => (x.hasAlertSound ? 'Replace' : 'Attach')}
72
+ </rapid-button>
73
+ ${when(
74
+ (x) => x.hasAlertSound,
75
+ html<AlertSoundAttachmentHost>`
76
+ <rapid-button appearance="lightweight" @click=${(x) => x.testAttachedAlertSound()}>
77
+ Test
78
+ </rapid-button>
79
+ <rapid-button appearance="lightweight" @click=${(x) => x.clearAttachedAlertSound()}>
80
+ Clear
81
+ </rapid-button>
82
+ `,
83
+ )}
84
+ </div>
85
+ </div>
86
+ `;
87
+
88
+ /**
89
+ * Mixin: pick / test / clear / load server sound + apply fields to submit payload.
90
+ *
91
+ * Subclasses must provide {@link ruleService}, {@link isAlertSoundEditMode},
92
+ * {@link alertSoundEntityId}, and {@link alertSoundCacheSyncMode}.
93
+ *
94
+ * @public
95
+ */
96
+ export function AlertSoundAttachmentMixin<TBase extends Constructor<GenesisElement>>(Base: TBase) {
97
+ abstract class AlertSoundAttachment extends Base implements AlertSoundAttachmentHost {
98
+ abstract ruleService: RuleService;
99
+
100
+ /**
101
+ * True when editing an existing entity (clear should set CLEAR_ALERT_SOUND).
102
+ */
103
+ abstract get isAlertSoundEditMode(): boolean;
104
+
105
+ /**
106
+ * DYNAMIC_RULE_ID of the entity being edited (null on create).
107
+ * Used when {@link alertSoundCacheSyncMode} is `'entity'`.
108
+ */
109
+ abstract get alertSoundEntityId(): string | null;
110
+
111
+ /**
112
+ * `'entity'` — update/clear cache for {@link alertSoundEntityId} (rules).
113
+ * `'all'` — drop every cached server sound (templates; instance ids unknown).
114
+ */
115
+ abstract get alertSoundCacheSyncMode(): 'entity' | 'all';
116
+
117
+ /** Display name of the attached / pending alert sound file. */
118
+ @observable alertSoundFileName: string = '';
119
+ @observable alertSoundMimeType: string = '';
120
+ /** True when edit loaded an existing server sound (or user attached one). */
121
+ @observable hasAlertSound = false;
122
+ fileInput: HTMLInputElement;
123
+ pendingAlertSoundData: string | null = null;
124
+ /** True when the user picked a new file this session (needs upload). */
125
+ alertSoundDirty = false;
126
+ clearAlertSound = false;
127
+ /** In-progress Test playback; not private — mixin return type is exported. */
128
+ testAudio: HTMLAudioElement | null = null;
129
+
130
+ async loadExistingAlertSound(dynamicRuleId: string) {
131
+ if (!dynamicRuleId) {
132
+ return;
133
+ }
134
+ const sound = await this.ruleService.getAlertSound(dynamicRuleId);
135
+ if (!sound?.SOUND_DATA) {
136
+ return;
137
+ }
138
+ this.alertSoundFileName = sound.ALERT_SOUND_FILE_NAME || 'Attached sound';
139
+ this.alertSoundMimeType = sound.ALERT_SOUND_MIME_TYPE || 'audio/mpeg';
140
+ this.pendingAlertSoundData = sound.SOUND_DATA;
141
+ this.hasAlertSound = true;
142
+ this.alertSoundDirty = false;
143
+ this.clearAlertSound = false;
144
+ }
145
+
146
+ openAlertSoundPicker() {
147
+ this.fileInput?.click();
148
+ }
149
+
150
+ async onAlertSoundSelected(event: Event) {
151
+ const input = event.target as HTMLInputElement;
152
+ const file = input.files?.[0];
153
+ input.value = '';
154
+ if (!file) {
155
+ return;
156
+ }
157
+ if (file.size > MAX_SOUND_FILE_BYTES) {
158
+ showAlertSoundNotice(
159
+ 'Sound file too large',
160
+ `Sound file must be ${formatBytes(MAX_SOUND_FILE_BYTES)} or smaller.`,
161
+ );
162
+ return;
163
+ }
164
+ try {
165
+ this.pendingAlertSoundData = await blobToBase64(file);
166
+ this.alertSoundFileName = file.name;
167
+ this.alertSoundMimeType = resolveAudioMimeType(file.name, file.type);
168
+ this.hasAlertSound = true;
169
+ this.alertSoundDirty = true;
170
+ this.clearAlertSound = false;
171
+ } catch {
172
+ showAlertSoundNotice('Could not read sound file', 'The selected file could not be read.');
173
+ }
174
+ }
175
+
176
+ clearAttachedAlertSound() {
177
+ this.stopTestSound();
178
+ this.resetAlertSoundState();
179
+ this.clearAlertSound = this.isAlertSoundEditMode;
180
+ }
181
+
182
+ testAttachedAlertSound() {
183
+ this.stopTestSound();
184
+ if (!this.pendingAlertSoundData) {
185
+ return;
186
+ }
187
+ const blob = base64ToBlob(
188
+ this.pendingAlertSoundData,
189
+ this.alertSoundMimeType || 'audio/mpeg',
190
+ );
191
+ const url = URL.createObjectURL(blob);
192
+ const audio = new Audio(url);
193
+ this.testAudio = audio;
194
+ void audio.play().catch(() => {
195
+ showAlertSoundNotice(
196
+ 'Could not play sound',
197
+ 'The browser could not play this file. Try MP3 or WAV.',
198
+ );
199
+ });
200
+ audio.addEventListener(
201
+ 'ended',
202
+ () => {
203
+ URL.revokeObjectURL(url);
204
+ if (this.testAudio === audio) {
205
+ this.testAudio = null;
206
+ }
207
+ },
208
+ { once: true },
209
+ );
210
+ }
211
+
212
+ stopTestSound() {
213
+ if (!this.testAudio) {
214
+ return;
215
+ }
216
+ this.testAudio.pause();
217
+ const src = this.testAudio.src;
218
+ if (src.startsWith('blob:')) {
219
+ URL.revokeObjectURL(src);
220
+ }
221
+ this.testAudio = null;
222
+ }
223
+
224
+ resetAlertSoundState() {
225
+ this.alertSoundFileName = '';
226
+ this.alertSoundMimeType = '';
227
+ this.pendingAlertSoundData = null;
228
+ this.hasAlertSound = false;
229
+ this.alertSoundDirty = false;
230
+ this.clearAlertSound = false;
231
+ if (this.fileInput) {
232
+ this.fileInput.value = '';
233
+ }
234
+ }
235
+
236
+ /** Merges pending sound / clear flag onto a create/update DETAILS object. */
237
+ applyAlertSoundToPayload<T extends AlertSoundPayloadFields>(target: T): T {
238
+ if (this.clearAlertSound) {
239
+ target.CLEAR_ALERT_SOUND = true;
240
+ } else if (this.alertSoundDirty && this.pendingAlertSoundData && this.alertSoundFileName) {
241
+ target.ALERT_SOUND_FILE_NAME = this.alertSoundFileName;
242
+ target.ALERT_SOUND_MIME_TYPE = this.alertSoundMimeType;
243
+ target.ALERT_SOUND_DATA = this.pendingAlertSoundData;
244
+ }
245
+ return target;
246
+ }
247
+
248
+ /**
249
+ * Keep AlertSoundPlayer's in-memory NOTIFY_ALERT_SOUND cache in sync with
250
+ * what we just saved — otherwise the next alert still plays the old file.
251
+ */
252
+ syncAlertSoundCacheAfterSave() {
253
+ if (this.alertSoundCacheSyncMode === 'all') {
254
+ if (!this.clearAlertSound && !this.alertSoundDirty) {
255
+ return;
256
+ }
257
+ AlertSoundPlayer.shared.clearAllServerSounds();
258
+ return;
259
+ }
260
+
261
+ const id = this.alertSoundEntityId?.trim();
262
+ if (!id) {
263
+ return;
264
+ }
265
+
266
+ if (this.clearAlertSound) {
267
+ AlertSoundPlayer.shared.clearServerSound(id);
268
+ return;
269
+ }
270
+
271
+ if (this.alertSoundDirty && this.pendingAlertSoundData) {
272
+ const mimeType = this.alertSoundMimeType || 'audio/mpeg';
273
+ AlertSoundPlayer.shared.setServerSound(id, {
274
+ blob: base64ToBlob(this.pendingAlertSoundData, mimeType),
275
+ fileName: this.alertSoundFileName,
276
+ mimeType,
277
+ });
278
+ }
279
+ }
280
+ }
281
+
282
+ return AlertSoundAttachment;
283
+ }
@@ -2,6 +2,7 @@ import { sync } from '@genesislcap/foundation-utils';
2
2
  import { html, ref, repeat, when } from '@genesislcap/web-core';
3
3
  import { ConditionNode, Severity, UpdateType } from '../../../../../notify.types';
4
4
  import { eventDetail } from '../../../../../utils/eventDetail';
5
+ import { alertSoundAttachmentTemplate } from '../../../alert-sound-attachment';
5
6
  import type { RuleDialog } from './rule-dialog';
6
7
 
7
8
  const isGroup = (node: ConditionNode) => Array.isArray((node as any).CHILDREN);
@@ -124,6 +125,8 @@ export const RuleDialogTemplate = html<RuleDialog>`
124
125
  </div>
125
126
  </div>
126
127
 
128
+ ${alertSoundAttachmentTemplate}
129
+
127
130
  <div class="control">
128
131
  <label>Condition(s)</label>
129
132
  <div class="items">
@@ -11,6 +11,7 @@ import { NotifyService } from '../../../../../services/notify.service';
11
11
  import { RuleService } from '../../../../../services/rule.service';
12
12
  import { Field, SystemService } from '../../../../../services/system.service';
13
13
  import { humanize } from '../../../../../utils';
14
+ import { AlertSoundAttachmentMixin } from '../../../alert-sound-attachment';
14
15
  import {
15
16
  DynamicRuleUtils,
16
17
  RuleUtils,
@@ -32,7 +33,7 @@ import { Rule, RuleDialogMode, RuleDialogParams } from './rule-dialog.types';
32
33
  template: RuleDialogTemplate,
33
34
  styles: DynamicRuleDialogStyles,
34
35
  })
35
- export class RuleDialog extends GenesisElement {
36
+ export class RuleDialog extends AlertSoundAttachmentMixin(GenesisElement) {
36
37
  @RuleService ruleService: RuleService;
37
38
  @SystemService systemService: SystemService;
38
39
  @NotifyService notifyService: NotifyService;
@@ -60,6 +61,18 @@ export class RuleDialog extends GenesisElement {
60
61
  @observable fields: Array<Field> = [];
61
62
  @observable topics: string[] = [];
62
63
 
64
+ get isAlertSoundEditMode(): boolean {
65
+ return this.ruleDialogMode === RuleDialogMode.EDIT;
66
+ }
67
+
68
+ get alertSoundEntityId(): string | null {
69
+ return this.ruleId;
70
+ }
71
+
72
+ get alertSoundCacheSyncMode(): 'entity' | 'all' {
73
+ return 'entity';
74
+ }
75
+
63
76
  public async openDialog(params: RuleDialogParams) {
64
77
  this.topics = await this.notifyService.getNotifyRouteTopics();
65
78
  this.resources = await this.systemService.getResources();
@@ -80,6 +93,7 @@ export class RuleDialog extends GenesisElement {
80
93
  }
81
94
 
82
95
  close() {
96
+ this.stopTestSound();
83
97
  // Objects
84
98
  this.ruleId = null;
85
99
  this.name = null;
@@ -90,6 +104,7 @@ export class RuleDialog extends GenesisElement {
90
104
  this.topic = null;
91
105
  this.severity = null;
92
106
  this.ruleDialogMode = null;
107
+ this.resetAlertSoundState();
93
108
 
94
109
  // Arrays
95
110
  this.resources = [];
@@ -132,6 +147,7 @@ export class RuleDialog extends GenesisElement {
132
147
  this.conditions = DynamicRuleUtils.resolveLeftValues(loadedConditions, this.fields);
133
148
  this.refreshExpressionPreview();
134
149
  this.attributeAssignments(rule.RESULT_EXPRESSION?.ASSIGNMENTS);
150
+ await this.loadExistingAlertSound(rule.DYNAMIC_RULE_ID);
135
151
  }
136
152
 
137
153
  private refreshExpressionPreview() {
@@ -184,7 +200,7 @@ export class RuleDialog extends GenesisElement {
184
200
  DynamicRuleUtils.hasOrJoin(this.conditions) ||
185
201
  DynamicRuleUtils.hasGroup(this.conditions) ||
186
202
  DynamicRuleUtils.hasUnsupportedOperator(this.conditions);
187
- return {
203
+ const rule: Rule = {
188
204
  ...(this.ruleDialogMode === RuleDialogMode.EDIT && { DYNAMIC_RULE_ID: this.ruleId }),
189
205
  RULE_NAME: this.name,
190
206
  RULE_DESCRIPTION: this.description,
@@ -205,10 +221,13 @@ export class RuleDialog extends GenesisElement {
205
221
  ],
206
222
  },
207
223
  };
224
+
225
+ return this.applyAlertSoundToPayload(rule);
208
226
  }
209
227
 
210
228
  validateResponse(response) {
211
229
  if (response.MESSAGE_TYPE === 'EVENT_ACK') {
230
+ this.syncAlertSoundCacheAfterSave();
212
231
  this.close();
213
232
  return;
214
233
  }
@@ -1,4 +1,5 @@
1
1
  import { UpdateType } from '../../../../../notify.types';
2
+ import type { AlertSoundPayloadFields } from '../../../alert-sound-attachment';
2
3
 
3
4
  export const RuleDialogMode = {
4
5
  CREATE: 'create',
@@ -25,4 +26,12 @@ export type Rule = {
25
26
  RESULT_EXPRESSION?: {
26
27
  ASSIGNMENTS: Array<any>;
27
28
  };
29
+ } & AlertSoundPayloadFields;
30
+
31
+ export type NotifyAlertSound = {
32
+ ALERT_SOUND_ID: string;
33
+ DYNAMIC_RULE_ID: string;
34
+ ALERT_SOUND_FILE_NAME?: string;
35
+ ALERT_SOUND_MIME_TYPE?: string;
36
+ SOUND_DATA: string;
28
37
  };
@@ -1,6 +1,7 @@
1
1
  import { showNotificationDialog } from '@genesislcap/foundation-notifications';
2
2
  import { customElement, GenesisElement, html, ref, repeat } from '@genesislcap/web-core';
3
3
  import { RuleService } from '../../../../services/rule.service';
4
+ import { AlertSoundPlayer } from '../../../../utils/alert-sound';
4
5
  import { DynamicRuleManagementStyles } from '../../styles/dynamic-rule.styles';
5
6
  import { RULE_MANAGEMENT_COLUMNS } from './columns';
6
7
  import { RuleDialog } from './rule-dialog/rule-dialog';
@@ -61,7 +62,10 @@ export class RuleManagement extends GenesisElement {
61
62
  confirmingActions: [
62
63
  {
63
64
  label: 'Confirm',
64
- action: () => this.ruleService.deleteRule(rule.ID),
65
+ action: () => {
66
+ AlertSoundPlayer.shared.clearServerSound(rule.ID);
67
+ this.ruleService.deleteRule(rule.ID);
68
+ },
65
69
  },
66
70
  ],
67
71
  },
@@ -2,6 +2,7 @@ import { sync } from '@genesislcap/foundation-utils';
2
2
  import { html, ref, repeat, when } from '@genesislcap/web-core';
3
3
  import { ConditionNode, Severity, UpdateType } from '../../../../../notify.types';
4
4
  import { eventDetail } from '../../../../../utils/eventDetail';
5
+ import { alertSoundAttachmentTemplate } from '../../../alert-sound-attachment';
5
6
  import type { TemplateDialog } from './template-dialog';
6
7
 
7
8
  const isGroup = (node: ConditionNode) => Array.isArray((node as any).CHILDREN);
@@ -109,6 +110,8 @@ export const TemplateDialogTemplate = html<TemplateDialog>`
109
110
  </div>
110
111
  </div>
111
112
 
113
+ ${alertSoundAttachmentTemplate}
114
+
112
115
  <div class="control">
113
116
  <label>Condition(s)</label>
114
117
  <div class="items">
@@ -8,9 +8,11 @@ import {
8
8
  UpdateType,
9
9
  } from '../../../../../notify.types';
10
10
  import { NotifyService } from '../../../../../services/notify.service';
11
+ import { RuleService } from '../../../../../services/rule.service';
11
12
  import { Field, SystemService } from '../../../../../services/system.service';
12
13
  import { TemplateService } from '../../../../../services/template.service';
13
14
  import { humanize, logger } from '../../../../../utils';
15
+ import { AlertSoundAttachmentMixin } from '../../../alert-sound-attachment';
14
16
  import {
15
17
  DynamicRuleUtils,
16
18
  showNotificationError,
@@ -32,10 +34,11 @@ import { ParameterBuilderEntity, Template, TemplateDialogMode } from './template
32
34
  template: TemplateDialogTemplate,
33
35
  styles: DynamicRuleDialogStyles,
34
36
  })
35
- export class TemplateDialog extends GenesisElement {
37
+ export class TemplateDialog extends AlertSoundAttachmentMixin(GenesisElement) {
36
38
  @TemplateService templateService: TemplateService;
37
39
  @SystemService systemService: SystemService;
38
40
  @NotifyService notifyService: NotifyService;
41
+ @RuleService ruleService: RuleService;
39
42
 
40
43
  dialog: Modal;
41
44
  @observable templateDialogMode: (typeof TemplateDialogMode)[keyof typeof TemplateDialogMode] =
@@ -64,6 +67,20 @@ export class TemplateDialog extends GenesisElement {
64
67
 
65
68
  private requiredFields = () => [this.name, this.description, this.header, this.message];
66
69
 
70
+ get isAlertSoundEditMode(): boolean {
71
+ return this.templateDialogMode === TemplateDialogMode.EDIT;
72
+ }
73
+
74
+ get alertSoundEntityId(): string | null {
75
+ return this.templateId;
76
+ }
77
+
78
+ get alertSoundCacheSyncMode(): 'entity' | 'all' {
79
+ // Template sound updates propagate to instance rules by NAME on the server;
80
+ // bust the whole client cache so those instance ids re-fetch.
81
+ return 'all';
82
+ }
83
+
67
84
  public async openDialog(params: any) {
68
85
  this.topics = await this.notifyService.getNotifyRouteTopics();
69
86
  this.resources = await this.systemService.getResources(); // TODO: Tables + Views
@@ -85,6 +102,7 @@ export class TemplateDialog extends GenesisElement {
85
102
  }
86
103
 
87
104
  close() {
105
+ this.stopTestSound();
88
106
  // Objects
89
107
  this.templateId = null;
90
108
  this.name = null;
@@ -95,6 +113,7 @@ export class TemplateDialog extends GenesisElement {
95
113
  this.topic = null;
96
114
  this.severity = null;
97
115
  this.templateDialogMode = null;
116
+ this.resetAlertSoundState();
98
117
 
99
118
  // Arrays
100
119
  this.resources = [];
@@ -139,6 +158,7 @@ export class TemplateDialog extends GenesisElement {
139
158
  this.conditions = DynamicRuleUtils.resolveLeftValues(loadedConditions, this.fields);
140
159
  this.refreshExpressionPreview();
141
160
  this.attributeAssignments(template.RESULT_EXPRESSION?.ASSIGNMENTS);
161
+ await this.loadExistingAlertSound(template.DYNAMIC_RULE_ID);
142
162
  }
143
163
 
144
164
  private refreshExpressionPreview() {
@@ -203,7 +223,7 @@ export class TemplateDialog extends GenesisElement {
203
223
  DynamicRuleUtils.hasOrJoin(this.conditions) ||
204
224
  DynamicRuleUtils.hasGroup(this.conditions) ||
205
225
  DynamicRuleUtils.hasUnsupportedOperator(this.conditions);
206
- return {
226
+ const template: Template = {
207
227
  ...(this.templateDialogMode === TemplateDialogMode.EDIT && {
208
228
  DYNAMIC_RULE_ID: this.templateId,
209
229
  }),
@@ -227,10 +247,13 @@ export class TemplateDialog extends GenesisElement {
227
247
  },
228
248
  PARAMETER_DETAILS: useRaw ? {} : TemplateUtils.createParameterDetails(this.parameters),
229
249
  };
250
+
251
+ return this.applyAlertSoundToPayload(template);
230
252
  }
231
253
 
232
254
  validateResponse(response) {
233
255
  if (response.MESSAGE_TYPE === 'EVENT_ACK') {
256
+ this.syncAlertSoundCacheAfterSave();
234
257
  this.close();
235
258
  return;
236
259
  }
@@ -1,4 +1,5 @@
1
1
  import { UpdateType } from '../../../../../notify.types';
2
+ import type { AlertSoundPayloadFields } from '../../../alert-sound-attachment';
2
3
 
3
4
  export const TemplateDialogMode = {
4
5
  CREATE: 'create',
@@ -26,7 +27,7 @@ export type Template = {
26
27
  ASSIGNMENTS: Array<any>;
27
28
  };
28
29
  PARAMETER_DETAILS: any;
29
- };
30
+ } & AlertSoundPayloadFields;
30
31
 
31
32
  export type ParameterBuilderEntity = {
32
33
  UUID?: string;
@@ -1,6 +1,7 @@
1
1
  import { showNotificationDialog } from '@genesislcap/foundation-notifications';
2
2
  import { customElement, GenesisElement, html, ref, repeat } from '@genesislcap/web-core';
3
3
  import { TemplateService } from '../../../../services/template.service';
4
+ import { AlertSoundPlayer } from '../../../../utils/alert-sound';
4
5
  import { DynamicRuleManagementStyles } from '../../styles/dynamic-rule.styles';
5
6
  import { TEMPLATE_MANAGEMENT_COLUMNS } from './columns';
6
7
  import type { TemplateDialog } from './template-dialog/template-dialog';
@@ -61,7 +62,13 @@ export class TemplateManagement extends GenesisElement {
61
62
  confirmingActions: [
62
63
  {
63
64
  label: 'Confirm',
64
- action: () => this.templateService.deleteRuleTemplate(template.ID),
65
+ action: () => {
66
+ // Template sounds propagate to instance rules by NAME on the
67
+ // server; the client cache is keyed by those instance ids, not
68
+ // template.ID — bust everything so deleted audio stops playing.
69
+ AlertSoundPlayer.shared.clearAllServerSounds();
70
+ this.templateService.deleteRuleTemplate(template.ID);
71
+ },
65
72
  },
66
73
  ],
67
74
  },
@@ -97,7 +97,7 @@ export const DynamicRuleDialogStyles = css`
97
97
  display: flex;
98
98
  flex-direction: column;
99
99
  width: ${String(modalWidthPx)}px;
100
- max-height: 500px;
100
+ max-height: 600px;
101
101
  overflow-y: scroll;
102
102
  gap: calc(var(--design-unit) * 3px);
103
103
  padding: calc(var(--design-unit) * 4px);
@@ -168,5 +168,35 @@ export const DynamicRuleDialogStyles = css`
168
168
  min-height: calc(var(--design-unit) * 12px);
169
169
  resize: vertical;
170
170
  }
171
+
172
+ .alert-sound {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: calc(var(--design-unit) * 2px);
176
+ margin-top: calc(var(--design-unit) * 2px);
177
+ }
178
+
179
+ .alert-sound .file-input {
180
+ display: none;
181
+ }
182
+
183
+ .alert-sound-row {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: calc(var(--design-unit) * 2px);
187
+ flex-wrap: wrap;
188
+ border: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest);
189
+ border-radius: calc(var(--control-corner-radius) * 1px);
190
+ padding: calc(var(--design-unit) * 2px);
191
+ }
192
+
193
+ .alert-sound-name {
194
+ flex: 1;
195
+ min-width: 120px;
196
+ color: var(--neutral-foreground-hint);
197
+ overflow: hidden;
198
+ text-overflow: ellipsis;
199
+ white-space: nowrap;
200
+ }
171
201
  `;
172
202
  // #endregion