@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.
- package/README.md +39 -0
- package/custom-elements-manifest.config.js +5 -0
- package/dist/custom-elements.json +1771 -321
- package/dist/dts/components/components.d.ts.map +1 -1
- package/dist/dts/components/foundation-inbox/inbox-base/inbox-base.d.ts +2 -0
- package/dist/dts/components/foundation-inbox/inbox-base/inbox-base.d.ts.map +1 -1
- package/dist/dts/components/foundation-inbox/inbox.styles.d.ts.map +1 -1
- package/dist/dts/components/foundation-inbox/inbox.template.d.ts.map +1 -1
- package/dist/dts/components/foundation-inbox/inbox.types.d.ts +4 -1
- package/dist/dts/components/foundation-inbox/inbox.types.d.ts.map +1 -1
- package/dist/dts/components/foundation-notification-dashboard/alert-sound-attachment.d.ts +86 -0
- package/dist/dts/components/foundation-notification-dashboard/alert-sound-attachment.d.ts.map +1 -0
- package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.d.ts +39 -1
- package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.d.ts.map +1 -1
- package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.d.ts.map +1 -1
- package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.d.ts +8 -0
- package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.d.ts.map +1 -1
- package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-management.d.ts.map +1 -1
- package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.d.ts +41 -1
- package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.d.ts.map +1 -1
- package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.d.ts.map +1 -1
- package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.d.ts +2 -1
- package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.d.ts.map +1 -1
- package/dist/dts/components/foundation-notification-dashboard/components/templates/template-management.d.ts.map +1 -1
- package/dist/dts/components/foundation-notification-dashboard/styles/dynamic-rule.styles.d.ts.map +1 -1
- package/dist/dts/components/foundation-sounds/foundation-sounds-panel.d.ts +73 -0
- package/dist/dts/components/foundation-sounds/foundation-sounds-panel.d.ts.map +1 -0
- package/dist/dts/components/foundation-sounds/foundation-sounds-panel.styles.d.ts +2 -0
- package/dist/dts/components/foundation-sounds/foundation-sounds-panel.styles.d.ts.map +1 -0
- package/dist/dts/components/foundation-sounds/foundation-sounds-panel.template.d.ts +3 -0
- package/dist/dts/components/foundation-sounds/foundation-sounds-panel.template.d.ts.map +1 -0
- package/dist/dts/components/foundation-sounds/foundation-sounds.d.ts +34 -0
- package/dist/dts/components/foundation-sounds/foundation-sounds.d.ts.map +1 -0
- package/dist/dts/components/foundation-sounds/foundation-sounds.styles.d.ts +3 -0
- package/dist/dts/components/foundation-sounds/foundation-sounds.styles.d.ts.map +1 -0
- package/dist/dts/index.d.ts +4 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/react.d.ts +31 -11
- package/dist/dts/services/inbox.service.d.ts +6 -0
- package/dist/dts/services/inbox.service.d.ts.map +1 -1
- package/dist/dts/services/rule.service.d.ts +2 -1
- package/dist/dts/services/rule.service.d.ts.map +1 -1
- package/dist/dts/utils/alert-sound.d.ts +177 -0
- package/dist/dts/utils/alert-sound.d.ts.map +1 -0
- package/dist/dts/utils/alert-sound.test.d.ts +2 -0
- package/dist/dts/utils/alert-sound.test.d.ts.map +1 -0
- package/dist/esm/components/components.js +4 -0
- package/dist/esm/components/foundation-inbox/inbox-base/inbox-base.js +6 -0
- package/dist/esm/components/foundation-inbox/inbox.styles.js +15 -0
- package/dist/esm/components/foundation-inbox/inbox.template.js +16 -0
- package/dist/esm/components/foundation-inbox/inbox.types.js +1 -0
- package/dist/esm/components/foundation-notification-dashboard/alert-sound-attachment.js +218 -0
- package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.js +17 -2
- package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.js +3 -0
- package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-management.js +5 -1
- package/dist/esm/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.js +23 -2
- package/dist/esm/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.js +3 -0
- package/dist/esm/components/foundation-notification-dashboard/components/templates/template-management.js +8 -1
- package/dist/esm/components/foundation-notification-dashboard/styles/dynamic-rule.styles.js +31 -1
- package/dist/esm/components/foundation-sounds/foundation-sounds-panel.js +283 -0
- package/dist/esm/components/foundation-sounds/foundation-sounds-panel.styles.js +200 -0
- package/dist/esm/components/foundation-sounds/foundation-sounds-panel.template.js +179 -0
- package/dist/esm/components/foundation-sounds/foundation-sounds.js +206 -0
- package/dist/esm/components/foundation-sounds/foundation-sounds.styles.js +7 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/services/inbox.service.js +6 -2
- package/dist/esm/services/rule.service.js +35 -0
- package/dist/esm/utils/alert-sound.js +955 -0
- package/dist/esm/utils/alert-sound.test.js +25 -0
- package/dist/esm/utils/index.js +3 -0
- package/dist/pbc-notify-ui.api.json +2859 -332
- package/dist/pbc-notify-ui.d.ts +281 -3
- package/dist/react.cjs +19 -5
- package/dist/react.mjs +16 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -23
- package/src/components/components.ts +4 -0
- package/src/components/foundation-inbox/inbox-base/inbox-base.ts +7 -0
- package/src/components/foundation-inbox/inbox.styles.ts +15 -0
- package/src/components/foundation-inbox/inbox.template.ts +16 -0
- package/src/components/foundation-inbox/inbox.types.ts +3 -0
- package/src/components/foundation-notification-dashboard/alert-sound-attachment.ts +283 -0
- package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.ts +3 -0
- package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.ts +21 -2
- package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.ts +9 -0
- package/src/components/foundation-notification-dashboard/components/rules/rule-management.ts +5 -1
- package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.ts +3 -0
- package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.ts +25 -2
- package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.ts +2 -1
- package/src/components/foundation-notification-dashboard/components/templates/template-management.ts +8 -1
- package/src/components/foundation-notification-dashboard/styles/dynamic-rule.styles.ts +31 -1
- package/src/components/foundation-sounds/foundation-sounds-panel.styles.ts +201 -0
- package/src/components/foundation-sounds/foundation-sounds-panel.template.ts +188 -0
- package/src/components/foundation-sounds/foundation-sounds-panel.ts +280 -0
- package/src/components/foundation-sounds/foundation-sounds.styles.ts +8 -0
- package/src/components/foundation-sounds/foundation-sounds.ts +222 -0
- package/src/index.ts +8 -0
- package/src/services/inbox.service.ts +12 -2
- package/src/services/rule.service.ts +37 -1
- package/src/utils/alert-sound.test.ts +32 -0
- package/src/utils/alert-sound.ts +1135 -0
- package/src/utils/index.ts +3 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { css } from '@genesislcap/web-core';
|
|
2
|
+
import { scrollbarStyles } from '../../styles/scrollbar.styles';
|
|
3
|
+
|
|
4
|
+
export const FoundationSoundsPanelStyles = css`
|
|
5
|
+
:host {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
height: 100%;
|
|
9
|
+
min-height: 0;
|
|
10
|
+
width: 100%;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.sounds-controls {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
flex: 1;
|
|
17
|
+
min-height: 0;
|
|
18
|
+
gap: 12px;
|
|
19
|
+
padding: calc(var(--design-unit) * 3px);
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.sounds-help-disclosure {
|
|
24
|
+
flex: 0 0 auto;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sounds-help-toggle {
|
|
28
|
+
margin-inline-start: calc(var(--design-unit) * 1px);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.sounds-help-toggle::before {
|
|
32
|
+
content: '+';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.sounds-help-disclosure[expanded] .sounds-help-toggle::before {
|
|
36
|
+
content: '−';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.sounds-help {
|
|
40
|
+
margin: 0;
|
|
41
|
+
font-size: var(--type-ramp-minus-1-font-size);
|
|
42
|
+
color: var(--neutral-foreground-hint);
|
|
43
|
+
text-align: left;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.volume-row {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
flex: 0 0 auto;
|
|
50
|
+
gap: calc(var(--design-unit) * 2px);
|
|
51
|
+
min-width: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.volume-icon {
|
|
55
|
+
flex: 0 0 auto;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.volume-label {
|
|
59
|
+
flex: 0 0 auto;
|
|
60
|
+
font-size: var(--type-ramp-minus-1-font-size);
|
|
61
|
+
color: var(--neutral-foreground-hint);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.volume-slider {
|
|
65
|
+
flex: 1 1 auto;
|
|
66
|
+
min-width: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.volume-percent {
|
|
70
|
+
flex: 0 0 auto;
|
|
71
|
+
min-width: 3ch;
|
|
72
|
+
text-align: end;
|
|
73
|
+
font-size: var(--type-ramp-minus-1-font-size);
|
|
74
|
+
color: var(--neutral-foreground-hint);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.mute-all {
|
|
78
|
+
flex: 0 0 auto;
|
|
79
|
+
margin: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.sound-rules-panel {
|
|
83
|
+
flex: 1 1 auto;
|
|
84
|
+
min-height: 0;
|
|
85
|
+
overflow: auto;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.sound-rules {
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
gap: 12px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.sound-rule {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
gap: 8px;
|
|
98
|
+
padding: 12px;
|
|
99
|
+
border: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest);
|
|
100
|
+
border-radius: calc(var(--control-corner-radius) * 1px);
|
|
101
|
+
|
|
102
|
+
/* Keep absolute listboxes from being clipped by the card itself. */
|
|
103
|
+
overflow: visible;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.sound-rule-header {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: space-between;
|
|
110
|
+
font-weight: 700;
|
|
111
|
+
font-size: var(--type-ramp-minus-1-font-size);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.sound-rule-actions {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 2px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.sound-rule-label {
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
gap: 4px;
|
|
124
|
+
font-size: var(--type-ramp-minus-1-font-size);
|
|
125
|
+
color: var(--neutral-foreground-hint);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.pattern-input,
|
|
129
|
+
.match-mode-select,
|
|
130
|
+
.rule-select,
|
|
131
|
+
.sound-file-input {
|
|
132
|
+
width: 100%;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/*
|
|
136
|
+
* Anchor the listbox to the select host and use position:fixed so it escapes
|
|
137
|
+
* .sound-rules-panel overflow:auto (and the rule card). Each select sets a
|
|
138
|
+
* unique --sounds-select-anchor / anchor-name via inline style.
|
|
139
|
+
* !important beats FAST's internal absolute top/bottom for [open] selects.
|
|
140
|
+
*/
|
|
141
|
+
.match-mode-select[open]::part(listbox),
|
|
142
|
+
.rule-select[open]::part(listbox) {
|
|
143
|
+
position: fixed !important;
|
|
144
|
+
/* stylelint-disable-next-line property-no-unknown -- CSS anchor positioning */
|
|
145
|
+
position-anchor: var(--sounds-select-anchor);
|
|
146
|
+
inset: auto !important;
|
|
147
|
+
top: anchor(bottom) !important;
|
|
148
|
+
left: anchor(left) !important;
|
|
149
|
+
right: anchor(right) !important;
|
|
150
|
+
bottom: auto !important;
|
|
151
|
+
width: auto !important;
|
|
152
|
+
margin-top: calc(var(--design-unit) * 1px);
|
|
153
|
+
z-index: 10000;
|
|
154
|
+
max-height: min(240px, 40vh);
|
|
155
|
+
overflow-x: hidden;
|
|
156
|
+
overflow-y: auto;
|
|
157
|
+
box-shadow: 0 4px 16px rgb(0 0 0 / 24%);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.match-mode-select[open][position='above']::part(listbox),
|
|
161
|
+
.rule-select[open][position='above']::part(listbox) {
|
|
162
|
+
top: auto !important;
|
|
163
|
+
bottom: anchor(top) !important;
|
|
164
|
+
margin-top: 0;
|
|
165
|
+
margin-bottom: calc(var(--design-unit) * 1px);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.sound-file-row {
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
gap: 4px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.sound-file-row .sound-file-input {
|
|
175
|
+
flex: 1;
|
|
176
|
+
min-width: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.choose-sound {
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.sound-rules-footer {
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
justify-content: space-between;
|
|
187
|
+
flex: 0 0 auto;
|
|
188
|
+
gap: 8px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.rule-count {
|
|
192
|
+
font-size: var(--type-ramp-minus-1-font-size);
|
|
193
|
+
color: var(--neutral-foreground-hint);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.file-input {
|
|
197
|
+
display: none;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
${scrollbarStyles}
|
|
201
|
+
`;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { html, ref, repeat } from '@genesislcap/web-core';
|
|
2
|
+
import type { AlertSoundRuleView } from '../../utils/alert-sound';
|
|
3
|
+
import type { FoundationSoundsPanel } from './foundation-sounds-panel';
|
|
4
|
+
|
|
5
|
+
export const FoundationSoundsPanelTemplate = html<FoundationSoundsPanel>`
|
|
6
|
+
<div class="sounds-controls" part="controls">
|
|
7
|
+
<input
|
|
8
|
+
${ref('fileInput')}
|
|
9
|
+
class="file-input"
|
|
10
|
+
type="file"
|
|
11
|
+
accept="audio/*"
|
|
12
|
+
@change=${(x, c) => x.onFileSelected(c.event)}
|
|
13
|
+
/>
|
|
14
|
+
<rapid-disclosure class="sounds-help-disclosure" appearance="lightweight">
|
|
15
|
+
<span slot="title">
|
|
16
|
+
Sound Alerts Information
|
|
17
|
+
<span class="sounds-help-toggle" aria-hidden="true"></span>
|
|
18
|
+
</span>
|
|
19
|
+
<p class="sounds-help">
|
|
20
|
+
Add up to ${(x) => x.maxRules} sounds.
|
|
21
|
+
<br />
|
|
22
|
+
Match text in MESSAGE or HEADER (a word or phrase), or a notification rule (including rules
|
|
23
|
+
created when you subscribe to a template).
|
|
24
|
+
<br />
|
|
25
|
+
The first matching rule is played.
|
|
26
|
+
</p>
|
|
27
|
+
</rapid-disclosure>
|
|
28
|
+
<div class="volume-row">
|
|
29
|
+
<rapid-icon
|
|
30
|
+
class="volume-icon"
|
|
31
|
+
name=${(x) => (x.muted || x.volumePercent === 0 ? 'volume-xmark' : 'volume-high')}
|
|
32
|
+
></rapid-icon>
|
|
33
|
+
<span class="volume-label">Volume</span>
|
|
34
|
+
<rapid-slider
|
|
35
|
+
class="volume-slider"
|
|
36
|
+
min="0"
|
|
37
|
+
max="100"
|
|
38
|
+
step="1"
|
|
39
|
+
:value=${(x) => String(x.volumePercent)}
|
|
40
|
+
@change=${(x, c) => x.onVolumeChange(c.event)}
|
|
41
|
+
></rapid-slider>
|
|
42
|
+
<span class="volume-percent">${(x) => `${x.volumePercent}%`}</span>
|
|
43
|
+
<rapid-button
|
|
44
|
+
class="mute-all"
|
|
45
|
+
appearance="lightweight"
|
|
46
|
+
title=${(x) => (x.muted ? 'Unmute alert sounds' : 'Mute all alert sounds')}
|
|
47
|
+
@click=${(x) => x.toggleMute()}
|
|
48
|
+
>
|
|
49
|
+
${(x) => (x.muted ? 'Unmute' : 'Mute all')}
|
|
50
|
+
</rapid-button>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="sound-rules-panel">
|
|
53
|
+
<div class="sound-rules">
|
|
54
|
+
${repeat(
|
|
55
|
+
(x) => x.rules,
|
|
56
|
+
html<AlertSoundRuleView, FoundationSoundsPanel>`
|
|
57
|
+
<div class="sound-rule" data-rule-index=${(x) => x.index}>
|
|
58
|
+
<div class="sound-rule-header">
|
|
59
|
+
<span>Rule ${(x) => x.index + 1}</span>
|
|
60
|
+
<div class="sound-rule-actions">
|
|
61
|
+
<rapid-button
|
|
62
|
+
class="mute-rule"
|
|
63
|
+
appearance="stealth"
|
|
64
|
+
title=${(x) => (x.muted ? 'Unmute this rule' : 'Mute this rule')}
|
|
65
|
+
@click=${(x, c) => c.parent.toggleRuleMute(x.index)}
|
|
66
|
+
>
|
|
67
|
+
<rapid-icon
|
|
68
|
+
name=${(x) => (x.muted ? 'volume-xmark' : 'volume-high')}
|
|
69
|
+
></rapid-icon>
|
|
70
|
+
</rapid-button>
|
|
71
|
+
<rapid-button
|
|
72
|
+
class="remove-rule"
|
|
73
|
+
appearance="stealth"
|
|
74
|
+
title=${(_, c) => (c.parent.canRemoveRule ? 'Remove rule' : 'Reset rule')}
|
|
75
|
+
@click=${(x, c) => c.parent.removeRule(x.index)}
|
|
76
|
+
>
|
|
77
|
+
<rapid-icon name="trash"></rapid-icon>
|
|
78
|
+
</rapid-button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<label class="sound-rule-label">
|
|
82
|
+
Match by
|
|
83
|
+
<rapid-select
|
|
84
|
+
class="match-mode-select"
|
|
85
|
+
style=${(x) =>
|
|
86
|
+
`anchor-name: --sound-match-${x.index}; --sounds-select-anchor: --sound-match-${x.index}`}
|
|
87
|
+
:value=${(x) => x.matchMode}
|
|
88
|
+
@change=${(x, c) => c.parent.onMatchModeChange(x.index, c.event)}
|
|
89
|
+
>
|
|
90
|
+
<rapid-option value="message">MESSAGE text</rapid-option>
|
|
91
|
+
<rapid-option value="header">HEADER text</rapid-option>
|
|
92
|
+
<rapid-option value="rule">Notification rule</rapid-option>
|
|
93
|
+
</rapid-select>
|
|
94
|
+
</label>
|
|
95
|
+
<label
|
|
96
|
+
class="sound-rule-label"
|
|
97
|
+
style=${(x) => (x.matchMode === 'rule' ? 'display: none;' : '')}
|
|
98
|
+
>
|
|
99
|
+
${(x) => (x.matchMode === 'header' ? 'HEADER text' : 'MESSAGE text')}
|
|
100
|
+
<rapid-text-field
|
|
101
|
+
class="pattern-input"
|
|
102
|
+
placeholder=${(x) =>
|
|
103
|
+
x.matchMode === 'header' ? 'e.g. Price alert' : 'e.g. limit breached'}
|
|
104
|
+
:value=${(x) => x.pattern}
|
|
105
|
+
@input=${(x, c) => c.parent.onPatternInput(x.index, c.event)}
|
|
106
|
+
></rapid-text-field>
|
|
107
|
+
</label>
|
|
108
|
+
<label
|
|
109
|
+
class="sound-rule-label"
|
|
110
|
+
style=${(x) => (x.matchMode === 'rule' ? '' : 'display: none;')}
|
|
111
|
+
>
|
|
112
|
+
Notification rule
|
|
113
|
+
<rapid-select
|
|
114
|
+
class="rule-select"
|
|
115
|
+
style=${(x) =>
|
|
116
|
+
`anchor-name: --sound-rule-${x.index}; --sounds-select-anchor: --sound-rule-${x.index}`}
|
|
117
|
+
:value=${(x) => x.templateId ?? ''}
|
|
118
|
+
@change=${(x, c) => c.parent.onRuleChange(x.index, c.event)}
|
|
119
|
+
>
|
|
120
|
+
<options-datasource
|
|
121
|
+
value-field="id"
|
|
122
|
+
label-field="name"
|
|
123
|
+
option-element="rapid-option"
|
|
124
|
+
:data=${(_, c) => c.parent.ruleSelectOptions}
|
|
125
|
+
></options-datasource>
|
|
126
|
+
</rapid-select>
|
|
127
|
+
</label>
|
|
128
|
+
<label class="sound-rule-label">
|
|
129
|
+
Sound file
|
|
130
|
+
<div class="sound-file-row">
|
|
131
|
+
<rapid-text-field
|
|
132
|
+
class="sound-file-input"
|
|
133
|
+
readonly
|
|
134
|
+
placeholder="No sound selected"
|
|
135
|
+
:value=${(x) => x.fileName ?? ''}
|
|
136
|
+
title=${(x) => x.fileName ?? 'No sound selected'}
|
|
137
|
+
>
|
|
138
|
+
<rapid-icon
|
|
139
|
+
slot="end"
|
|
140
|
+
class="choose-sound"
|
|
141
|
+
name="folder-open"
|
|
142
|
+
title="Choose alert sound"
|
|
143
|
+
@click=${(x, c) => {
|
|
144
|
+
c.event.stopPropagation();
|
|
145
|
+
return c.parent.openFilePicker(x.index);
|
|
146
|
+
}}
|
|
147
|
+
></rapid-icon>
|
|
148
|
+
</rapid-text-field>
|
|
149
|
+
<rapid-button
|
|
150
|
+
class="test-sound"
|
|
151
|
+
appearance="stealth"
|
|
152
|
+
title="Test play this sound"
|
|
153
|
+
?disabled=${(x) => !x.hasSound}
|
|
154
|
+
@click=${(x, c) => c.parent.testSound(x.index)}
|
|
155
|
+
>
|
|
156
|
+
<rapid-icon name="play"></rapid-icon>
|
|
157
|
+
</rapid-button>
|
|
158
|
+
<rapid-button
|
|
159
|
+
class="clear-sound"
|
|
160
|
+
appearance="stealth"
|
|
161
|
+
title="Clear sound file"
|
|
162
|
+
?disabled=${(x) => !x.hasSound}
|
|
163
|
+
@click=${(x, c) => c.parent.clearSound(x.index)}
|
|
164
|
+
>
|
|
165
|
+
<rapid-icon name="times"></rapid-icon>
|
|
166
|
+
</rapid-button>
|
|
167
|
+
</div>
|
|
168
|
+
</label>
|
|
169
|
+
</div>
|
|
170
|
+
`,
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="sound-rules-footer">
|
|
175
|
+
<rapid-button
|
|
176
|
+
class="add-rule"
|
|
177
|
+
appearance="lightweight"
|
|
178
|
+
title="Add sound rule"
|
|
179
|
+
?disabled=${(x) => !x.canAddRule}
|
|
180
|
+
@click=${(x) => x.addRule()}
|
|
181
|
+
>
|
|
182
|
+
<rapid-icon name="plus"></rapid-icon>
|
|
183
|
+
Add rule
|
|
184
|
+
</rapid-button>
|
|
185
|
+
<span class="rule-count">${(x) => `${x.rules.length} / ${x.maxRules}`}</span>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
`;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { customElement, GenesisElement, observable } from '@genesislcap/web-core';
|
|
2
|
+
import { RuleService } from '../../services/rule.service';
|
|
3
|
+
import {
|
|
4
|
+
AlertSoundMatchMode,
|
|
5
|
+
AlertSoundPlayer,
|
|
6
|
+
AlertSoundRuleView,
|
|
7
|
+
AlertSoundSelectableRule,
|
|
8
|
+
MAX_SOUND_RULES,
|
|
9
|
+
} from '../../utils/alert-sound';
|
|
10
|
+
import { FoundationSoundsPanelStyles } from './foundation-sounds-panel.styles';
|
|
11
|
+
import { FoundationSoundsPanelTemplate } from './foundation-sounds-panel.template';
|
|
12
|
+
|
|
13
|
+
type NamedRuleRow = {
|
|
14
|
+
NAME?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
RULE_NAME?: string;
|
|
17
|
+
ID?: string;
|
|
18
|
+
id?: string;
|
|
19
|
+
DYNAMIC_RULE_ID?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configures alert sound rules in the inbox Sounds tab. Persists via the shared
|
|
24
|
+
* AlertSoundPlayer; playback is handled by layout-mounted
|
|
25
|
+
* {@link FoundationSounds}.
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
@customElement({
|
|
30
|
+
name: 'foundation-sounds-panel',
|
|
31
|
+
template: FoundationSoundsPanelTemplate,
|
|
32
|
+
styles: FoundationSoundsPanelStyles,
|
|
33
|
+
})
|
|
34
|
+
export class FoundationSoundsPanel extends GenesisElement {
|
|
35
|
+
@RuleService ruleService: RuleService;
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
@observable rules: AlertSoundRuleView[] = [];
|
|
39
|
+
/** @internal */
|
|
40
|
+
@observable canAddRule = true;
|
|
41
|
+
/** @internal */
|
|
42
|
+
@observable canRemoveRule = false;
|
|
43
|
+
/** Global playback volume 0–100. @internal */
|
|
44
|
+
@observable volumePercent = 100;
|
|
45
|
+
/** @internal */
|
|
46
|
+
@observable muted = false;
|
|
47
|
+
/** Notification rules from REQ_NOTIFICATION_RULE. @internal */
|
|
48
|
+
@observable selectableRules: AlertSoundSelectableRule[] = [];
|
|
49
|
+
/** @internal */
|
|
50
|
+
@observable ruleSelectOptions: Array<{ id: string; name: string }> = [
|
|
51
|
+
{ id: '', name: 'Select a rule...' },
|
|
52
|
+
];
|
|
53
|
+
/** @internal */
|
|
54
|
+
readonly maxRules = MAX_SOUND_RULES;
|
|
55
|
+
|
|
56
|
+
fileInput: HTMLInputElement;
|
|
57
|
+
private pendingRuleIndex: number | null = null;
|
|
58
|
+
private readonly player = AlertSoundPlayer.shared;
|
|
59
|
+
private unsubscribePlayer: (() => void) | undefined;
|
|
60
|
+
/** Suppresses select change events while options are rebuilt. */
|
|
61
|
+
private ignoreSelectChanges = false;
|
|
62
|
+
|
|
63
|
+
async connectedCallback() {
|
|
64
|
+
super.connectedCallback();
|
|
65
|
+
|
|
66
|
+
await this.player.restore();
|
|
67
|
+
|
|
68
|
+
// The await above can outlive this element (e.g. the inbox flyout closing
|
|
69
|
+
// before restore() settles). Without this guard, disconnectedCallback runs
|
|
70
|
+
// first with unsubscribePlayer still unset, so it clears nothing — and we
|
|
71
|
+
// then add a listener to the shared AlertSoundPlayer.shared that nothing
|
|
72
|
+
// ever removes, plus fire a redundant loadRules() request.
|
|
73
|
+
if (!this.isConnected) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.syncSoundState();
|
|
78
|
+
this.unsubscribePlayer = this.player.subscribe(() => this.syncSoundState());
|
|
79
|
+
await this.loadRules();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
disconnectedCallback() {
|
|
83
|
+
super.disconnectedCallback();
|
|
84
|
+
this.unsubscribePlayer?.();
|
|
85
|
+
this.unsubscribePlayer = undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onMatchModeChange(index: number, event: Event): boolean {
|
|
89
|
+
const value = (event.target as HTMLSelectElement).value;
|
|
90
|
+
const matchMode: AlertSoundMatchMode =
|
|
91
|
+
value === 'rule' ? 'rule' : value === 'header' ? 'header' : 'message';
|
|
92
|
+
if (matchMode === 'rule') {
|
|
93
|
+
void this.loadRules();
|
|
94
|
+
}
|
|
95
|
+
this.player.setMatchMode(index, matchMode);
|
|
96
|
+
// FAST preventDefaults when a binding returns void — always allow the event.
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onPatternInput(index: number, event: Event): boolean {
|
|
101
|
+
const value = (event.target as HTMLInputElement).value;
|
|
102
|
+
this.player.setPattern(index, value);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onRuleChange(index: number, event: Event): boolean {
|
|
107
|
+
if (this.ignoreSelectChanges) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
const ruleId = (event.target as HTMLSelectElement).value;
|
|
111
|
+
if (!ruleId) {
|
|
112
|
+
this.player.setRule(index, null);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const rule = this.selectableRules.find((item) => item.id === ruleId);
|
|
116
|
+
if (!rule) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
this.player.setRule(index, rule);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
openFilePicker(index: number): boolean {
|
|
124
|
+
this.pendingRuleIndex = index;
|
|
125
|
+
this.fileInput?.click();
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onFileSelected(event: Event): boolean {
|
|
130
|
+
const input = event.target as HTMLInputElement;
|
|
131
|
+
const file = input.files?.[0];
|
|
132
|
+
const index = this.pendingRuleIndex;
|
|
133
|
+
this.pendingRuleIndex = null;
|
|
134
|
+
input.value = '';
|
|
135
|
+
if (!file || index == null) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
this.player.setSoundFromFile(index, file);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
clearSound(index: number): boolean {
|
|
143
|
+
this.player.clearSound(index);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
testSound(index: number): boolean {
|
|
148
|
+
this.player.playRule(index);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
toggleRuleMute(index: number): boolean {
|
|
153
|
+
this.player.toggleRuleMute(index);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
onVolumeChange(event: Event): boolean {
|
|
158
|
+
const value = Number((event.target as HTMLInputElement).value);
|
|
159
|
+
if (!Number.isFinite(value)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
this.player.setVolumePercent(value);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
toggleMute(): boolean {
|
|
167
|
+
this.player.toggleMute();
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
addRule(): boolean {
|
|
172
|
+
this.player.addRule();
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
removeRule(index: number): boolean {
|
|
177
|
+
this.player.removeRule(index);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Reloads the notification-rule picker (e.g. when the Sounds tab is opened). */
|
|
182
|
+
async reloadRules(): Promise<void> {
|
|
183
|
+
await this.loadRules();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async loadRules(): Promise<void> {
|
|
187
|
+
try {
|
|
188
|
+
const rules = await this.ruleService.getRules();
|
|
189
|
+
const ruleList = (Array.isArray(rules) ? rules : []) as NamedRuleRow[];
|
|
190
|
+
|
|
191
|
+
this.selectableRules = this.mapRules(ruleList);
|
|
192
|
+
this.ruleSelectOptions = [
|
|
193
|
+
{
|
|
194
|
+
id: '',
|
|
195
|
+
name: this.selectableRules.length ? 'Select a rule...' : 'No rules available',
|
|
196
|
+
},
|
|
197
|
+
...this.selectableRules.map((rule) => ({ id: rule.id, name: rule.name })),
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
await this.settleRuleOptions();
|
|
201
|
+
this.syncSoundState();
|
|
202
|
+
// options-datasource clearData() wipes select.value whenever :data is
|
|
203
|
+
// reassigned, and :value won't re-apply if templateId didn't change — so
|
|
204
|
+
// push persisted rule ids back onto the selects after the rebuild.
|
|
205
|
+
this.applyRuleSelectValues();
|
|
206
|
+
} catch {
|
|
207
|
+
this.selectableRules = [];
|
|
208
|
+
this.ruleSelectOptions = [{ id: '', name: 'No rules available' }];
|
|
209
|
+
// Without this, a failed REQ_NOTIFICATION_RULE leaves a restored
|
|
210
|
+
// matchMode: 'rule' row showing the "Select a rule..." placeholder for
|
|
211
|
+
// good, even though the player still holds its templateId and plays fine.
|
|
212
|
+
this.syncSoundState();
|
|
213
|
+
this.applyRuleSelectValues();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Suppresses select `change` events only while options-datasource rebuilds
|
|
219
|
+
* the DOM — not for the `getRules()` round trip itself, so a user picking a
|
|
220
|
+
* rule while a request is in flight isn't silently dropped.
|
|
221
|
+
*/
|
|
222
|
+
private async settleRuleOptions(): Promise<void> {
|
|
223
|
+
this.ignoreSelectChanges = true;
|
|
224
|
+
try {
|
|
225
|
+
await new Promise<void>((resolve) => {
|
|
226
|
+
requestAnimationFrame(() => {
|
|
227
|
+
resolve();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
} finally {
|
|
231
|
+
this.ignoreSelectChanges = false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Re-applies persisted template ids onto `.rule-select` hosts after
|
|
237
|
+
* options-datasource has rebuilt its options. We deliberately do *not* bind
|
|
238
|
+
* `:initialSelectedValue` to templateId: that path compares the option label
|
|
239
|
+
* against Select.value (the option id), always mismatches, clearData()'s the
|
|
240
|
+
* select, and leaves the next pick stuck on "Select a rule...".
|
|
241
|
+
*/
|
|
242
|
+
private applyRuleSelectValues(): void {
|
|
243
|
+
this.shadowRoot?.querySelectorAll<HTMLElement>('.sound-rule').forEach((row) => {
|
|
244
|
+
const index = Number(row.getAttribute('data-rule-index'));
|
|
245
|
+
const rule = this.rules.find((item) => item.index === index);
|
|
246
|
+
const select = row.querySelector('.rule-select') as HTMLSelectElement | null;
|
|
247
|
+
if (!select || !rule || rule.matchMode !== 'rule') {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
select.value = rule.templateId ?? '';
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private mapRules(ruleList: NamedRuleRow[]): AlertSoundSelectableRule[] {
|
|
255
|
+
const seenIds = new Set<string>();
|
|
256
|
+
return ruleList
|
|
257
|
+
.map((rule) => {
|
|
258
|
+
const name = rule?.NAME ?? rule?.RULE_NAME ?? rule?.name ?? '';
|
|
259
|
+
const id = this.ruleMatchId(rule);
|
|
260
|
+
if (!id || !name || seenIds.has(id)) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
seenIds.add(id);
|
|
264
|
+
return { id, name };
|
|
265
|
+
})
|
|
266
|
+
.filter((rule): rule is AlertSoundSelectableRule => Boolean(rule));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private ruleMatchId(rule: NamedRuleRow | undefined): string {
|
|
270
|
+
return String(rule?.DYNAMIC_RULE_ID ?? rule?.ID ?? rule?.id ?? '').trim();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private syncSoundState() {
|
|
274
|
+
this.rules = this.player.rules;
|
|
275
|
+
this.canAddRule = this.player.canAddRule;
|
|
276
|
+
this.canRemoveRule = this.player.canRemoveRule;
|
|
277
|
+
this.volumePercent = this.player.volumePercent;
|
|
278
|
+
this.muted = this.player.isMuted;
|
|
279
|
+
}
|
|
280
|
+
}
|