@visns-studio/visns-components 6.21.0 → 6.23.1
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/package.json +1 -1
- package/src/components/DataGrid.jsx +15 -2
- package/src/components/JsonView.jsx +299 -0
- package/src/components/Navigation.jsx +10 -1
- package/src/components/columns/SelectFilterEditor.jsx +372 -0
- package/src/components/controls/DataGridSearch.jsx +6 -1
- package/src/components/phone/ZoomPhoneBadge.jsx +625 -0
- package/src/components/phone/phonePresenceEndpoints.js +34 -0
- package/src/components/phone/phonePresenceHelpers.js +298 -0
- package/src/components/phone/useZoomPhoneLive.js +165 -0
- package/src/components/sms/SmsClientConversations.jsx +8 -0
- package/src/components/sms/SmsInbox.jsx +11 -0
- package/src/components/sms/SmsThreadPanel.jsx +258 -29
- package/src/components/sms/smsHelpers.js +37 -0
- package/src/components/styles/DataGrid.module.scss +10 -0
- package/src/components/styles/GenericDetail.module.scss +12 -2
- package/src/components/styles/JsonView.module.scss +233 -0
- package/src/components/styles/Sms.module.scss +131 -0
- package/src/components/styles/Vault.module.scss +114 -0
- package/src/components/styles/ZoomPhone.module.scss +550 -0
- package/src/components/styles/global-datagrid.css +23 -0
- package/src/components/vault/VaultAccessLog.jsx +24 -0
- package/src/components/vault/VaultEntryForm.jsx +239 -8
- package/src/components/vault/VaultManager.jsx +14 -2
- package/src/components/vault/VaultShareModal.jsx +126 -5
- package/src/components/vault/vaultShares.js +67 -3
- package/src/index.js +49 -2
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
2
|
+
import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The grid's dropdown column filter, with one added behaviour: opening a
|
|
6
|
+
* filter that already holds a value clears that value.
|
|
7
|
+
*
|
|
8
|
+
* The complaint this answers is the everyday one — the Client filter says
|
|
9
|
+
* "Smith, John", you want a different client, and the only way to get a
|
|
10
|
+
* usable list is to hit the small × first, because the combo re-opens still
|
|
11
|
+
* carrying the old selection. Clicking the filter is already the gesture for
|
|
12
|
+
* "I want to choose again", so it now IS the clear: the chip disappears, the
|
|
13
|
+
* grid refilters as if the column were never filtered, and the menu you just
|
|
14
|
+
* opened shows the whole option list with an empty search box.
|
|
15
|
+
*
|
|
16
|
+
* ---------------------------------------------------------------------------
|
|
17
|
+
* Deliberate consequence: closing without picking leaves the filter CLEARED.
|
|
18
|
+
* There is no "restore what was there" on collapse — the click was the clear,
|
|
19
|
+
* and a filter that silently came back would make the gesture unpredictable.
|
|
20
|
+
* Re-picking the old option is one click away in the list that is already open.
|
|
21
|
+
*
|
|
22
|
+
* ---------------------------------------------------------------------------
|
|
23
|
+
* Only a DELIBERATE open clears — and that distinction has to be made here,
|
|
24
|
+
* because the combo expands on focus (`expandOnFocus`, on by default). Without
|
|
25
|
+
* the gate below, Tab-ing along the filter row would wipe every filled
|
|
26
|
+
* dropdown focus passed through, which is nobody's idea of "I clicked it".
|
|
27
|
+
* So an expand clears only when it followed, within a moment, either
|
|
28
|
+
*
|
|
29
|
+
* - a pointer press inside this combo (a click on the field, its chip area
|
|
30
|
+
* or its toggle arrow), or
|
|
31
|
+
* - a key press inside this combo that means "open this / start searching"
|
|
32
|
+
* (Enter, Space, Arrow Up/Down, F4, or a printable character).
|
|
33
|
+
*
|
|
34
|
+
* A Tab is neither: the keystroke lands on the element being left, so it is
|
|
35
|
+
* both the wrong key and outside this combo. Nothing else moves the needle —
|
|
36
|
+
* a programmatic focus, a saved view restoring a filter, or the grid's own
|
|
37
|
+
* focus handling all expand without a gesture and so leave the value alone.
|
|
38
|
+
*
|
|
39
|
+
* ---------------------------------------------------------------------------
|
|
40
|
+
* Why a wrapper and not a patch.
|
|
41
|
+
*
|
|
42
|
+
* `SelectFilter` lives in `@visns-studio/visns-datagrid-*` (the Inovua fork),
|
|
43
|
+
* which is vendor code we do not edit. Everything needed is reachable from
|
|
44
|
+
* outside it:
|
|
45
|
+
*
|
|
46
|
+
* - `filterEditorProps` is spread straight onto the underlying `Combo`
|
|
47
|
+
* (SelectFilter.render -> `finalProps`), so an `onExpandedChange` handed
|
|
48
|
+
* down there is called by the combo on every expand and collapse —
|
|
49
|
+
* mouse, keyboard and programmatic alike, since they all funnel through
|
|
50
|
+
* `ComboBox.setExpanded()`. One interception point covers every way of
|
|
51
|
+
* opening the list.
|
|
52
|
+
* - The gesture behind an expand is not passed to `onExpandedChange`, and
|
|
53
|
+
* the combo neither forwards `onClick`/`onMouseDown` (its own handlers
|
|
54
|
+
* replace them on the root node) nor calls `props.onKeyDown` until after
|
|
55
|
+
* it has already expanded. So the gesture is read from the page instead:
|
|
56
|
+
* capture-phase `window` listeners record the last pointer/key press
|
|
57
|
+
* before any React handler runs, and the expand asks what that was.
|
|
58
|
+
* - The clear itself goes through the `SelectFilter` instance's own
|
|
59
|
+
* `onChange`, which is exactly what the × button ends up calling: it drops
|
|
60
|
+
* the displayed value and emits the emptied `filterValue` to the grid
|
|
61
|
+
* (honouring `filterDelay`, as the × does). It deliberately does NOT go
|
|
62
|
+
* through `Combo.setValue()`, which collapses the list when
|
|
63
|
+
* `collapseOnSelect` is set — and SelectFilter always sets it, so clearing
|
|
64
|
+
* that way would shut the menu the user just opened.
|
|
65
|
+
*
|
|
66
|
+
* The editor contract (props, the required `render` wrapper, the `setValue`
|
|
67
|
+
* the grid calls on the ref) is documented in NativeDateFilterEditor; this
|
|
68
|
+
* component forwards all of it to `SelectFilter` untouched.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Filter editors are handed `filterEditorProps` both as a top-level prop and
|
|
73
|
+
* spread across their props (renderFilter/index.tsx `filterProps`), and it may
|
|
74
|
+
* be an object or a `(props) => object` factory — SelectFilter resolves it the
|
|
75
|
+
* same way.
|
|
76
|
+
*/
|
|
77
|
+
const resolveEditorProps = (props) => {
|
|
78
|
+
let { filterEditorProps } = props;
|
|
79
|
+
|
|
80
|
+
if (filterEditorProps === undefined) {
|
|
81
|
+
filterEditorProps =
|
|
82
|
+
props.filterValue && props.filterValue.filterEditorProps;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof filterEditorProps === 'function') {
|
|
86
|
+
return filterEditorProps(props) || {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return filterEditorProps || {};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* How long after a key or pointer press an expand still counts as that
|
|
94
|
+
* gesture's doing. A click expands in the same task; this is loose enough to
|
|
95
|
+
* survive a slow frame and far too tight to catch the next thing the user does.
|
|
96
|
+
*/
|
|
97
|
+
const GESTURE_WINDOW_MS = 700;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Keys that mean "open this list", as opposed to Tab, which means "leave".
|
|
101
|
+
* A printable character counts too: typing into a filter is as deliberate as
|
|
102
|
+
* clicking it.
|
|
103
|
+
*/
|
|
104
|
+
const OPEN_KEYS = ['Enter', ' ', 'Spacebar', 'ArrowDown', 'ArrowUp', 'F4'];
|
|
105
|
+
|
|
106
|
+
const isOpenKey = (event) => {
|
|
107
|
+
const { key } = event;
|
|
108
|
+
|
|
109
|
+
if (typeof key !== 'string' || key === 'Tab') {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (OPEN_KEYS.indexOf(key) !== -1) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return key.length === 1 && !event.ctrlKey && !event.metaKey;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The last user gesture seen anywhere on the page: `{ kind, target, at }`,
|
|
122
|
+
* where `kind` is 'open' for a press that means business and 'other' for
|
|
123
|
+
* everything else (a Tab, an Escape, a modifier).
|
|
124
|
+
*/
|
|
125
|
+
let lastGesture = null;
|
|
126
|
+
let watching = false;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* One set of capture-phase listeners for the whole page, however many filters
|
|
130
|
+
* are on screen. Capture on `window` runs before React's delegated handlers,
|
|
131
|
+
* which is the point: the gesture is on record before the combo has decided to
|
|
132
|
+
* expand. They are passive and never removed — three listeners for the life of
|
|
133
|
+
* the page is cheaper than mounting and unmounting them per filter cell.
|
|
134
|
+
*/
|
|
135
|
+
const watchGestures = () => {
|
|
136
|
+
if (watching || typeof window === 'undefined') {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
watching = true;
|
|
141
|
+
|
|
142
|
+
const remember = (event, kind) => {
|
|
143
|
+
lastGesture = {
|
|
144
|
+
kind,
|
|
145
|
+
target: event.target,
|
|
146
|
+
at: Date.now(),
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const options = { capture: true, passive: true };
|
|
151
|
+
|
|
152
|
+
window.addEventListener(
|
|
153
|
+
'mousedown',
|
|
154
|
+
(event) => remember(event, 'open'),
|
|
155
|
+
options
|
|
156
|
+
);
|
|
157
|
+
window.addEventListener(
|
|
158
|
+
'touchstart',
|
|
159
|
+
(event) => remember(event, 'open'),
|
|
160
|
+
options
|
|
161
|
+
);
|
|
162
|
+
window.addEventListener(
|
|
163
|
+
'keydown',
|
|
164
|
+
(event) => remember(event, isOpenKey(event) ? 'open' : 'other'),
|
|
165
|
+
options
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Did the user just ask this combo to open, or did the combo open itself?
|
|
171
|
+
*
|
|
172
|
+
* `node` is the combo's own root element: a gesture outside it — the Tab press
|
|
173
|
+
* that lands on the element being left, a click elsewhere that moves focus —
|
|
174
|
+
* did not open this list, whatever else it did.
|
|
175
|
+
*/
|
|
176
|
+
export const openedByGesture = (
|
|
177
|
+
node,
|
|
178
|
+
gesture = lastGesture,
|
|
179
|
+
now = Date.now()
|
|
180
|
+
) => {
|
|
181
|
+
if (!gesture || gesture.kind !== 'open' || !gesture.target) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (now - gesture.at > GESTURE_WINDOW_MS) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return !!node && !!node.contains && node.contains(gesture.target);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* "Is something actually filtered here?" — `null` is the `select` filter
|
|
194
|
+
* type's emptyValue, `''` is what DataGrid's dependent-filter reset writes,
|
|
195
|
+
* and a multi-select carries an array.
|
|
196
|
+
*/
|
|
197
|
+
export const hasFilterValue = (value) => {
|
|
198
|
+
if (value === null || value === undefined || value === '') {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
return value.length > 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return true;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The whole decision, kept pure so it can be reasoned about (and tested)
|
|
211
|
+
* without a combo box.
|
|
212
|
+
*
|
|
213
|
+
* @param {boolean} expanded the state the combo just moved to
|
|
214
|
+
* @param {boolean} openedDeliberately the user opened it (see openedByGesture)
|
|
215
|
+
* rather than focus opening it for them
|
|
216
|
+
* @param {*} value what the filter currently holds
|
|
217
|
+
* @param {boolean} multiple multi-select (`operator: 'inlist'`)
|
|
218
|
+
* @param {boolean} clearOnOpen per-column opt out (or opt in, for multiple)
|
|
219
|
+
* @param {boolean} disabled
|
|
220
|
+
* @param {boolean} readOnly
|
|
221
|
+
*/
|
|
222
|
+
export const shouldClearOnOpen = ({
|
|
223
|
+
expanded,
|
|
224
|
+
openedDeliberately,
|
|
225
|
+
value,
|
|
226
|
+
multiple,
|
|
227
|
+
clearOnOpen,
|
|
228
|
+
disabled,
|
|
229
|
+
readOnly,
|
|
230
|
+
} = {}) => {
|
|
231
|
+
// Collapsing is not a clear, and an already-empty filter has nothing to
|
|
232
|
+
// clear — opening one must be a no-op so no needless refetch is posted.
|
|
233
|
+
if (!expanded || disabled || readOnly) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Focus alone expands the combo. That is not the user opening it, and
|
|
238
|
+
// clearing there would empty every filter a Tab travels past.
|
|
239
|
+
if (!openedDeliberately) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (clearOnOpen === false) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// A multi-select is opened to ADD a choice as often as to replace one, so
|
|
248
|
+
// wiping the existing selections on open would make it impossible to pick
|
|
249
|
+
// a second value. Those columns keep the old behaviour unless a column
|
|
250
|
+
// explicitly asks for the clear with `filter.clearOnOpen: true`.
|
|
251
|
+
if (multiple && clearOnOpen !== true) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return hasFilterValue(value);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const SelectFilterEditor = forwardRef((props, ref) => {
|
|
259
|
+
const { filterValue, emptyValue, onChange, disabled, readOnly } = props;
|
|
260
|
+
|
|
261
|
+
// Idempotent: the first filter cell on the page arms the listeners, the
|
|
262
|
+
// rest are no-ops. Done during render rather than in an effect so a combo
|
|
263
|
+
// that is clicked the instant it appears is already being watched.
|
|
264
|
+
watchGestures();
|
|
265
|
+
|
|
266
|
+
const innerRef = useRef(null);
|
|
267
|
+
|
|
268
|
+
// The grid reaches the editor through this ref: `GenericFilter.setValue`
|
|
269
|
+
// calls `setValue` on it (DataGrid's dependent-filter reset does exactly
|
|
270
|
+
// that), and `getInputRef` is read for focus handling. Both belong to the
|
|
271
|
+
// wrapped SelectFilter.
|
|
272
|
+
useImperativeHandle(
|
|
273
|
+
ref,
|
|
274
|
+
() => ({
|
|
275
|
+
setValue: (next) => {
|
|
276
|
+
if (innerRef.current && innerRef.current.setValue) {
|
|
277
|
+
innerRef.current.setValue(next);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
getInputRef: () =>
|
|
281
|
+
innerRef.current && innerRef.current.getInputRef
|
|
282
|
+
? innerRef.current.getInputRef()
|
|
283
|
+
: null,
|
|
284
|
+
}),
|
|
285
|
+
[]
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// `null` is the `select` filter type's emptyValue (community-edition
|
|
289
|
+
// filterTypes.ts) and what the combo's × emits; fall back to it so a grid
|
|
290
|
+
// that does not pass the prop still clears to the same value.
|
|
291
|
+
const noValue = emptyValue === undefined ? null : emptyValue;
|
|
292
|
+
|
|
293
|
+
// Kept fresh on every render so the stable handler below always sees the
|
|
294
|
+
// current props without re-arming the combo's prop on each render.
|
|
295
|
+
const clearRef = useRef(null);
|
|
296
|
+
clearRef.current = (expanded) => {
|
|
297
|
+
const instance = innerRef.current;
|
|
298
|
+
const combo =
|
|
299
|
+
instance && instance.getInputRef && instance.getInputRef();
|
|
300
|
+
const editorProps = resolveEditorProps(props);
|
|
301
|
+
|
|
302
|
+
// The displayed value is SelectFilter's own state — with a
|
|
303
|
+
// `filterDelay` in play it is ahead of the `filterValue` prop, so it,
|
|
304
|
+
// not the prop, decides whether there is anything to clear.
|
|
305
|
+
const current =
|
|
306
|
+
instance && instance.state
|
|
307
|
+
? instance.state.value
|
|
308
|
+
: filterValue && filterValue.value;
|
|
309
|
+
|
|
310
|
+
if (
|
|
311
|
+
!shouldClearOnOpen({
|
|
312
|
+
expanded,
|
|
313
|
+
openedDeliberately: openedByGesture(
|
|
314
|
+
combo && combo.getComboNode ? combo.getComboNode() : null
|
|
315
|
+
),
|
|
316
|
+
value: current,
|
|
317
|
+
multiple: editorProps.multiple,
|
|
318
|
+
clearOnOpen: editorProps.clearOnOpen,
|
|
319
|
+
disabled,
|
|
320
|
+
readOnly,
|
|
321
|
+
})
|
|
322
|
+
) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (instance && typeof instance.onChange === 'function') {
|
|
327
|
+
// The × button's path: drops the chip immediately and posts the
|
|
328
|
+
// emptied filter to the grid after `filterDelay`.
|
|
329
|
+
instance.onChange(noValue);
|
|
330
|
+
} else if (onChange) {
|
|
331
|
+
onChange({ ...filterValue, value: noValue });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Leftover search text would filter the list the user is looking at.
|
|
335
|
+
// The combo normally clears it on select/blur, so this is a backstop;
|
|
336
|
+
// `skipFilter` avoids a redundant re-filter, and passing `null` (not
|
|
337
|
+
// `''`) keeps `clearValueOnEmpty` out of it — that path calls
|
|
338
|
+
// `Combo.setValue`, which would collapse the open menu.
|
|
339
|
+
if (combo && typeof combo.setText === 'function' && combo.getText()) {
|
|
340
|
+
combo.setText(null, { skipFilter: true });
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const editorProps = resolveEditorProps(props);
|
|
345
|
+
|
|
346
|
+
// `clearOnOpen` is ours, not the combo's — and the combo hands every prop
|
|
347
|
+
// it does not recognise to a DOM node (`cleanProps` in ComboBox.render),
|
|
348
|
+
// which would put an unknown attribute on a div. Keep it out of what goes
|
|
349
|
+
// down.
|
|
350
|
+
const { clearOnOpen, ...comboProps } = editorProps;
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<SelectFilter
|
|
354
|
+
{...props}
|
|
355
|
+
ref={innerRef}
|
|
356
|
+
filterEditorProps={{
|
|
357
|
+
...comboProps,
|
|
358
|
+
onExpandedChange: (expanded) => {
|
|
359
|
+
if (editorProps.onExpandedChange) {
|
|
360
|
+
editorProps.onExpandedChange(expanded);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
clearRef.current(expanded);
|
|
364
|
+
},
|
|
365
|
+
}}
|
|
366
|
+
/>
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
SelectFilterEditor.displayName = 'SelectFilterEditor';
|
|
371
|
+
|
|
372
|
+
export default SelectFilterEditor;
|
|
@@ -240,8 +240,13 @@ const DataGridSearch = ({
|
|
|
240
240
|
<div className={styles['icon-search']}>
|
|
241
241
|
<Search strokeWidth={2} size={14} />
|
|
242
242
|
</div>
|
|
243
|
+
{/* type="search", not "text": host apps style bare text inputs
|
|
244
|
+
globally (throughlife's _forms.scss padded rule among them),
|
|
245
|
+
and that padding put the magnifier on top of the placeholder.
|
|
246
|
+
Search inputs are excluded from such rules, so the module's
|
|
247
|
+
own matched icon-offset/indent pair applies. */}
|
|
243
248
|
<input
|
|
244
|
-
type="
|
|
249
|
+
type="search"
|
|
245
250
|
placeholder="Search"
|
|
246
251
|
value={search}
|
|
247
252
|
onChange={handleChangeSearch}
|