@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
package/package.json
CHANGED
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
94
94
|
},
|
|
95
95
|
"name": "@visns-studio/visns-components",
|
|
96
|
-
"version": "6.
|
|
96
|
+
"version": "6.23.1",
|
|
97
97
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
98
98
|
"main": "src/index.js",
|
|
99
99
|
"files": [
|
|
@@ -37,10 +37,10 @@ import 'reactjs-popup/dist/index.css';
|
|
|
37
37
|
import parse from 'html-react-parser';
|
|
38
38
|
import { saveAs } from 'file-saver';
|
|
39
39
|
import NumberFilter from '@visns-studio/visns-datagrid-enterprise/NumberFilter';
|
|
40
|
-
import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
|
|
41
40
|
import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
|
|
42
41
|
import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
|
|
43
42
|
import NativeDateFilterEditor from './columns/NativeDateFilterEditor';
|
|
43
|
+
import SelectFilterEditor from './columns/SelectFilterEditor';
|
|
44
44
|
import { toast } from 'react-toastify';
|
|
45
45
|
import fetchUtil from '../utils/fetchUtil';
|
|
46
46
|
import { confirmDialog } from './utils/ConfirmDialog';
|
|
@@ -4387,7 +4387,11 @@ const DataGrid = forwardRef(
|
|
|
4387
4387
|
filterEditor = NumberFilter;
|
|
4388
4388
|
break;
|
|
4389
4389
|
case 'select':
|
|
4390
|
-
|
|
4390
|
+
// The grid's own SelectFilter, wrapped so
|
|
4391
|
+
// that opening a dropdown which already
|
|
4392
|
+
// holds a value clears it — see
|
|
4393
|
+
// SelectFilterEditor for the semantics.
|
|
4394
|
+
filterEditor = SelectFilterEditor;
|
|
4391
4395
|
|
|
4392
4396
|
// Inline options: `filter.options` as
|
|
4393
4397
|
// [{ id, label }].
|
|
@@ -4424,6 +4428,15 @@ const DataGrid = forwardRef(
|
|
|
4424
4428
|
column.filter.searchable === false,
|
|
4425
4429
|
// Explicitly enable clear icon
|
|
4426
4430
|
clearIcon: true,
|
|
4431
|
+
// Opening a filled dropdown clears
|
|
4432
|
+
// it (see SelectFilterEditor). A
|
|
4433
|
+
// column that must always carry a
|
|
4434
|
+
// value opts out with
|
|
4435
|
+
// `filter: { clearOnOpen: false }`;
|
|
4436
|
+
// a multi-select, excluded by
|
|
4437
|
+
// default, opts in with `true`.
|
|
4438
|
+
clearOnOpen:
|
|
4439
|
+
column.filter.clearOnOpen,
|
|
4427
4440
|
};
|
|
4428
4441
|
} else {
|
|
4429
4442
|
// Show loading state for URL-based filters without data
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import styles from './styles/JsonView.module.scss';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where a string stops being a value and starts being a document.
|
|
7
|
+
*
|
|
8
|
+
* Exported because the truncation is observable — a caller that wants to know
|
|
9
|
+
* whether a field will be cut before it renders should ask the same number the
|
|
10
|
+
* component uses rather than restate it.
|
|
11
|
+
*/
|
|
12
|
+
export const STRING_PREVIEW_LIMIT = 200;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse text that MIGHT be JSON, and answer only for the shapes worth drawing.
|
|
16
|
+
*
|
|
17
|
+
* `JSON.parse` is happy with `42`, `"a note"` and `true` — all three are valid
|
|
18
|
+
* JSON documents by the letter of the spec, and all three are also exactly what
|
|
19
|
+
* an ordinary hand-typed note looks like. Rendering "a note" as a one-row tree
|
|
20
|
+
* with a quoted string in it is worse than rendering the note, so a bare
|
|
21
|
+
* primitive is deliberately NOT JSON for this purpose: only an object or an
|
|
22
|
+
* array earns the tree.
|
|
23
|
+
*
|
|
24
|
+
* Returns the parsed value, or `null` for anything else — including the empty
|
|
25
|
+
* string, which parses as nothing at all.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} text Raw text, leading and trailing space included.
|
|
28
|
+
* @returns {object|Array|null}
|
|
29
|
+
*/
|
|
30
|
+
export const parseJsonDocument = (text) => {
|
|
31
|
+
const trimmed = String(text ?? '').trim();
|
|
32
|
+
|
|
33
|
+
// Cheap gate before the parse. A 32KB note that is plainly prose should
|
|
34
|
+
// not cost a parse on every keystroke of the field above it.
|
|
35
|
+
if (trimmed === '' || !/^[{[]/.test(trimmed)) return null;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(trimmed);
|
|
39
|
+
|
|
40
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : null;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Not JSON, which is an answer rather than a failure.
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** An object or an array — the two things that get a collapse toggle. */
|
|
48
|
+
const isContainer = (value) => value !== null && typeof value === 'object';
|
|
49
|
+
|
|
50
|
+
const childEntries = (value) =>
|
|
51
|
+
Array.isArray(value)
|
|
52
|
+
? value.map((item, index) => [index, item])
|
|
53
|
+
: Object.entries(value);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What a collapsed node says about itself.
|
|
57
|
+
*
|
|
58
|
+
* The count is the whole point: `{…}` alone tells the reader nothing about
|
|
59
|
+
* whether opening it is worth the scroll, and `[…] 129 items` tells them
|
|
60
|
+
* everything.
|
|
61
|
+
*/
|
|
62
|
+
const summarise = (value) => {
|
|
63
|
+
const count = Array.isArray(value) ? value.length : Object.keys(value).length;
|
|
64
|
+
const noun = Array.isArray(value) ? 'item' : 'key';
|
|
65
|
+
|
|
66
|
+
return `${Array.isArray(value) ? '[…]' : '{…}'} ${count} ${noun}${
|
|
67
|
+
count === 1 ? '' : 's'
|
|
68
|
+
}`;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** `{}` / `[]`, for a container with nothing in it to expand. */
|
|
72
|
+
const emptyLiteral = (value) => (Array.isArray(value) ? '[]' : '{}');
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A string value, which is the only primitive that can be too big to print.
|
|
76
|
+
*
|
|
77
|
+
* TWO reasons a string gets a toggle, and they are the same reason:
|
|
78
|
+
*
|
|
79
|
+
* It is long. 200 characters is about a line and a half here; past that the
|
|
80
|
+
* value stops being something you read in passing and starts pushing the rest
|
|
81
|
+
* of the document off the screen.
|
|
82
|
+
*
|
|
83
|
+
* It has newlines in it. This is the case that matters for the vault: a
|
|
84
|
+
* credentials backup keeps the actual credential in a `content` field with
|
|
85
|
+
* embedded line breaks, so squashing it onto one line destroys the only
|
|
86
|
+
* structure it has. A multi-line string is therefore treated as long even
|
|
87
|
+
* when it is short, and the expanded form is `pre-wrap`.
|
|
88
|
+
*
|
|
89
|
+
* The collapsed form is deliberately one line — first line, cut to the limit —
|
|
90
|
+
* because a preview that reflows is not a preview.
|
|
91
|
+
*/
|
|
92
|
+
const JsonString = ({ value }) => {
|
|
93
|
+
const [open, setOpen] = useState(false);
|
|
94
|
+
|
|
95
|
+
const multiline = value.includes('\n');
|
|
96
|
+
const long = value.length > STRING_PREVIEW_LIMIT;
|
|
97
|
+
|
|
98
|
+
if (!multiline && !long) {
|
|
99
|
+
return <span className={styles.string}>"{value}"</span>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (open) {
|
|
103
|
+
return (
|
|
104
|
+
<span className={styles.valueCell}>
|
|
105
|
+
<span className={`${styles.string} ${styles.stringOpen}`}>
|
|
106
|
+
"{value}"
|
|
107
|
+
</span>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
className={styles.more}
|
|
111
|
+
onClick={() => setOpen(false)}
|
|
112
|
+
>
|
|
113
|
+
show less
|
|
114
|
+
</button>
|
|
115
|
+
</span>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const firstLine = value.split('\n', 1)[0];
|
|
120
|
+
const preview =
|
|
121
|
+
firstLine.length > STRING_PREVIEW_LIMIT
|
|
122
|
+
? firstLine.slice(0, STRING_PREVIEW_LIMIT)
|
|
123
|
+
: firstLine;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<span className={styles.valueCell}>
|
|
127
|
+
<span className={styles.string}>"{preview}…"</span>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
className={styles.more}
|
|
131
|
+
onClick={() => setOpen(true)}
|
|
132
|
+
title={
|
|
133
|
+
multiline
|
|
134
|
+
? `${value.split('\n').length} lines, ${value.length} characters`
|
|
135
|
+
: `${value.length} characters`
|
|
136
|
+
}
|
|
137
|
+
>
|
|
138
|
+
show all
|
|
139
|
+
</button>
|
|
140
|
+
</span>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Everything that is not a container: tinted by type, so the type is readable. */
|
|
145
|
+
const JsonPrimitive = ({ value }) => {
|
|
146
|
+
if (value === null) return <span className={styles.null}>null</span>;
|
|
147
|
+
|
|
148
|
+
switch (typeof value) {
|
|
149
|
+
case 'string':
|
|
150
|
+
return <JsonString value={value} />;
|
|
151
|
+
case 'number':
|
|
152
|
+
// `String()` rather than the raw number so NaN and Infinity print
|
|
153
|
+
// as themselves: neither can come out of JSON.parse, but this
|
|
154
|
+
// component takes an already-parsed value from anywhere.
|
|
155
|
+
return <span className={styles.number}>{String(value)}</span>;
|
|
156
|
+
case 'boolean':
|
|
157
|
+
return <span className={styles.boolean}>{value ? 'true' : 'false'}</span>;
|
|
158
|
+
case 'undefined':
|
|
159
|
+
return <span className={styles.null}>undefined</span>;
|
|
160
|
+
default:
|
|
161
|
+
// A function or a symbol, which only a hand-built value can carry.
|
|
162
|
+
return <span className={styles.null}>{String(value)}</span>;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* One row: a key, and either a value or a subtree behind a toggle.
|
|
168
|
+
*
|
|
169
|
+
* The toggle is a real `<button>` wrapping the caret, the key AND the summary,
|
|
170
|
+
* so the hit target is the whole line and there is one tab stop per node rather
|
|
171
|
+
* than one per glyph. There is no `role="tree"` here on purpose: that role
|
|
172
|
+
* promises arrow-key navigation, and a tree that does not answer arrow keys is
|
|
173
|
+
* worse for a screen reader than the plain nested list this actually is.
|
|
174
|
+
*/
|
|
175
|
+
const JsonNode = ({ label, isIndex, value, depth, collapsedDepth }) => {
|
|
176
|
+
const container = isContainer(value);
|
|
177
|
+
const entries = useMemo(
|
|
178
|
+
() => (container ? childEntries(value) : []),
|
|
179
|
+
[container, value]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// The root is depth 0, so `collapsedDepth: 2` opens the root and its
|
|
183
|
+
// children and collapses everything from the grandchildren down. On the
|
|
184
|
+
// vault's backup payload that is exactly right: the document and its
|
|
185
|
+
// `credentials` array are open, and the 129 credentials inside it are 129
|
|
186
|
+
// one-line summaries rather than 129 open records.
|
|
187
|
+
const [open, setOpen] = useState(depth < collapsedDepth);
|
|
188
|
+
|
|
189
|
+
const key = label === null ? null : (
|
|
190
|
+
<span className={isIndex ? styles.index : styles.key}>
|
|
191
|
+
{String(label)}
|
|
192
|
+
<span className={styles.colon}>:</span>
|
|
193
|
+
</span>
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (!container || entries.length === 0) {
|
|
197
|
+
return (
|
|
198
|
+
<li className={styles.row}>
|
|
199
|
+
<div className={styles.line}>
|
|
200
|
+
<span className={styles.spacer} aria-hidden="true" />
|
|
201
|
+
{key}
|
|
202
|
+
{container ? (
|
|
203
|
+
<span className={styles.summary}>{emptyLiteral(value)}</span>
|
|
204
|
+
) : (
|
|
205
|
+
<JsonPrimitive value={value} />
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
</li>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<li className={styles.row}>
|
|
214
|
+
<div className={styles.line}>
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
className={styles.toggle}
|
|
218
|
+
aria-expanded={open}
|
|
219
|
+
onClick={() => setOpen((v) => !v)}
|
|
220
|
+
>
|
|
221
|
+
<span className={styles.caret} aria-hidden="true">
|
|
222
|
+
{open ? '▾' : '▸'}
|
|
223
|
+
</span>
|
|
224
|
+
{key}
|
|
225
|
+
<span className={styles.summary}>
|
|
226
|
+
{open ? (Array.isArray(value) ? '[' : '{') : summarise(value)}
|
|
227
|
+
</span>
|
|
228
|
+
</button>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{open && (
|
|
232
|
+
<>
|
|
233
|
+
<ul className={`${styles.list} ${styles.children}`}>
|
|
234
|
+
{entries.map(([childLabel, childValue]) => (
|
|
235
|
+
<JsonNode
|
|
236
|
+
key={String(childLabel)}
|
|
237
|
+
label={childLabel}
|
|
238
|
+
isIndex={Array.isArray(value)}
|
|
239
|
+
value={childValue}
|
|
240
|
+
depth={depth + 1}
|
|
241
|
+
collapsedDepth={collapsedDepth}
|
|
242
|
+
/>
|
|
243
|
+
))}
|
|
244
|
+
</ul>
|
|
245
|
+
{/* The closing bracket, so an open node reads as a pair
|
|
246
|
+
rather than as a bracket somebody forgot to shut. */}
|
|
247
|
+
<div className={styles.line}>
|
|
248
|
+
<span className={styles.spacer} aria-hidden="true" />
|
|
249
|
+
<span className={styles.summary}>
|
|
250
|
+
{Array.isArray(value) ? ']' : '}'}
|
|
251
|
+
</span>
|
|
252
|
+
</div>
|
|
253
|
+
</>
|
|
254
|
+
)}
|
|
255
|
+
</li>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* A parsed JSON value, as something a person can actually read.
|
|
261
|
+
*
|
|
262
|
+
* Written for the vault's notes, where `zoho:credentials-backup` leaves a
|
|
263
|
+
* 32KB machine-written blob — `{source, account_id, …, credentials: […]}` — in
|
|
264
|
+
* a field whose only previous rendering was a three-row textarea. Nothing in
|
|
265
|
+
* here knows that, and nothing should: it takes a value and draws it.
|
|
266
|
+
*
|
|
267
|
+
* READ-ONLY, and that is the design rather than an omission. Editing JSON as a
|
|
268
|
+
* tree means deciding what happens when a key is renamed to one that already
|
|
269
|
+
* exists, what a number that is being typed means halfway through, and what to
|
|
270
|
+
* do with a value the user has made syntactically impossible. The consumer
|
|
271
|
+
* keeps the raw text as the single source of truth and toggles between the two;
|
|
272
|
+
* see VaultEntryForm, where the notes VALUE is always the raw string and this
|
|
273
|
+
* view is presentation only.
|
|
274
|
+
*
|
|
275
|
+
* The value is passed ALREADY PARSED. A component that took text would have to
|
|
276
|
+
* decide what a parse failure looks like on screen, and the answer is always
|
|
277
|
+
* "show the text instead", which is a decision belonging to the screen and not
|
|
278
|
+
* to the viewer. `parseJsonDocument` above is the shared way to make that call.
|
|
279
|
+
*
|
|
280
|
+
* @param {*} value Any parsed JS value; objects and arrays get a tree.
|
|
281
|
+
* @param {number} collapsedDepth Nodes at this depth or deeper start collapsed.
|
|
282
|
+
* The root is depth 0. Default 2.
|
|
283
|
+
* @param {string} className Added to the root, for sizing by the caller.
|
|
284
|
+
*/
|
|
285
|
+
const JsonView = ({ value, collapsedDepth = 2, className, ...rest }) => (
|
|
286
|
+
<div className={[styles.root, className].filter(Boolean).join(' ')} {...rest}>
|
|
287
|
+
<ul className={styles.list}>
|
|
288
|
+
<JsonNode
|
|
289
|
+
label={null}
|
|
290
|
+
isIndex={false}
|
|
291
|
+
value={value}
|
|
292
|
+
depth={0}
|
|
293
|
+
collapsedDepth={collapsedDepth}
|
|
294
|
+
/>
|
|
295
|
+
</ul>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
export default JsonView;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
2
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
3
3
|
import {
|
|
4
|
+
AudioLines,
|
|
4
5
|
BarChart3,
|
|
5
6
|
Briefcase,
|
|
6
7
|
Bug,
|
|
@@ -59,6 +60,12 @@ const NAV_ICONS = {
|
|
|
59
60
|
// key. Module scope because two surfaces read it now: the header's action
|
|
60
61
|
// chips and — when a project opts in — the sidebar's pinned action rows.
|
|
61
62
|
const SETTING_ICONS = {
|
|
63
|
+
// A waveform, for recordings. `phone` is a handset and now belongs to the
|
|
64
|
+
// live Zoom Phone roster, which is a different question entirely — one chip
|
|
65
|
+
// is "listen to a call that happened", the other is "who is on a call right
|
|
66
|
+
// now", and two identical handsets in one header made them impossible to
|
|
67
|
+
// tell apart.
|
|
68
|
+
audioLines: AudioLines,
|
|
62
69
|
bug: Bug,
|
|
63
70
|
calendar: Calendar,
|
|
64
71
|
key: KeyRound,
|
|
@@ -607,7 +614,9 @@ function Navigation({
|
|
|
607
614
|
className={
|
|
608
615
|
cleanUrl(child.url) ===
|
|
609
616
|
bestMatchingUrl(
|
|
610
|
-
n.children.map(
|
|
617
|
+
n.children.map(
|
|
618
|
+
(one) => one.url
|
|
619
|
+
),
|
|
611
620
|
currentPage
|
|
612
621
|
)
|
|
613
622
|
? styles.active
|