@visns-studio/visns-components 6.6.4 → 6.15.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/README.md +1623 -18
- package/package.json +10 -4
- package/src/components/DataGrid.jsx +181 -38
- package/src/components/Fetch.jsx +80 -3
- package/src/components/Form.jsx +9 -0
- package/src/components/Navigation.jsx +109 -50
- package/src/components/Notification.jsx +279 -9
- package/src/components/TableFilter.jsx +14 -1
- package/src/components/auth/AuthBrandPanel.jsx +43 -0
- package/src/components/auth/AuthLoading.jsx +78 -0
- package/src/components/auth/AuthShell.jsx +59 -0
- package/src/components/auth/ClientAuth.jsx +161 -0
- package/src/components/auth/ClientAuthFrame.jsx +59 -0
- package/src/components/auth/ClientLogin.jsx +266 -55
- package/src/components/auth/ClientOTPVerify.jsx +587 -115
- package/src/components/auth/ImpersonateGate.jsx +254 -0
- package/src/components/auth/Login.jsx +134 -41
- package/src/components/auth/LogoutScreen.jsx +112 -0
- package/src/components/auth/Reset.jsx +134 -77
- package/src/components/auth/TwoFactorAuth.jsx +475 -297
- package/src/components/auth/Verify.jsx +237 -126
- package/src/components/auth/authEndpoints.js +105 -0
- package/src/components/auth/authFont.js +23 -0
- package/src/components/auth/authHelpers.js +465 -0
- package/src/components/auth/clientAuthProtocols.js +240 -0
- package/src/components/auth/useOptionalRouter.js +49 -0
- package/src/components/callQueue/CallQueuePop.jsx +1502 -0
- package/src/components/callQueue/CallQueueSettings.jsx +508 -0
- package/src/components/callQueue/callQueueHelpers.js +280 -0
- package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
- package/src/components/columns/ColumnRenderers.jsx +53 -7
- package/src/components/generic/GenericAuth.jsx +163 -96
- package/src/components/generic/GenericDetail.jsx +215 -90
- package/src/components/generic/GenericIndex.jsx +20 -47
- package/src/components/generic/GenericMain.jsx +5 -0
- package/src/components/generic/GroupedReportRenderer.jsx +1 -5
- package/src/components/generic/StandardModal.jsx +17 -5
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
- package/src/components/navActive.js +60 -0
- package/src/components/notify/desktopNotifications.js +256 -0
- package/src/components/sketch/SketchField.jsx +12 -2
- package/src/components/sms/SmsComposeModal.jsx +495 -0
- package/src/components/sms/SmsInbox.jsx +596 -0
- package/src/components/sms/SmsInboxBadge.jsx +468 -0
- package/src/components/sms/SmsLineSettings.jsx +854 -0
- package/src/components/sms/SmsThreadPanel.jsx +1184 -0
- package/src/components/sms/smsEndpoints.js +56 -0
- package/src/components/sms/smsHelpers.js +1075 -0
- package/src/components/sms/smsLiveState.js +132 -0
- package/src/components/sms/useSmsLive.js +307 -0
- package/src/components/styles/CallQueuePop.module.scss +646 -0
- package/src/components/styles/CallQueueSettings.module.scss +460 -0
- package/src/components/styles/ClientAuth.module.scss +229 -0
- package/src/components/styles/GenericDetail.module.scss +153 -26
- package/src/components/styles/GenericIndex.module.scss +13 -0
- package/src/components/styles/GenericMain.module.scss +19 -2
- package/src/components/styles/ImpersonateGate.module.scss +85 -0
- package/src/components/styles/Login.module.scss +13 -122
- package/src/components/styles/LogoutScreen.module.scss +89 -0
- package/src/components/styles/Navigation.module.scss +509 -223
- package/src/components/styles/Notification.module.scss +103 -0
- package/src/components/styles/Reset.module.scss +52 -186
- package/src/components/styles/Sms.module.scss +2382 -0
- package/src/components/styles/TableFilter.module.scss +118 -98
- package/src/components/styles/TwoFactorAuth.module.scss +115 -176
- package/src/components/styles/Vault.module.scss +1983 -0
- package/src/components/styles/Verify.module.scss +57 -180
- package/src/components/styles/_authBrandPanel.scss +163 -0
- package/src/components/styles/_authShell.scss +369 -0
- package/src/components/styles/global-datagrid.css +27 -2
- package/src/components/utils/buildEnv.js +67 -0
- package/src/components/utils/displayValue.js +94 -0
- package/src/components/utils/useDensity.js +345 -9
- package/src/components/vault/OtpChip.jsx +208 -0
- package/src/components/vault/PasswordGenerator.jsx +145 -0
- package/src/components/vault/QrScanner.jsx +326 -0
- package/src/components/vault/VaultAccessLog.jsx +225 -0
- package/src/components/vault/VaultConfirmPanel.jsx +113 -0
- package/src/components/vault/VaultEntryForm.jsx +740 -0
- package/src/components/vault/VaultManager.jsx +1000 -0
- package/src/components/vault/VaultQuickSearch.jsx +681 -0
- package/src/components/vault/qrDecode.js +257 -0
- package/src/components/vault/useDebouncedValue.js +22 -0
- package/src/components/vault/useVaultReveal.js +160 -0
- package/src/components/vault/vaultClipboard.js +33 -0
- package/src/components/vault/vaultEndpoints.js +40 -0
- package/src/components/vault/vaultFit.js +116 -0
- package/src/components/vault/vaultHelpers.js +547 -0
- package/src/components/vault/vaultNavigation.jsx +58 -0
- package/src/components/vault/vaultOtp.js +105 -0
- package/src/index.js +233 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the call-queue components.
|
|
3
|
+
*
|
|
4
|
+
* Split out of CallQueuePop.jsx so they can be imported by `node --test`, which
|
|
5
|
+
* cannot parse JSX. Nothing here touches React or the DOM; CallQueuePop
|
|
6
|
+
* re-exports the lot, so the component's public surface is unchanged.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Spatie permission that gates the pop, unless `monitorPermission` overrides it. */
|
|
10
|
+
export const MONITOR_PERMISSION = 'Call Queue Monitor';
|
|
11
|
+
|
|
12
|
+
/** Badge text for a queue Zoom sent without a name. */
|
|
13
|
+
export const FALLBACK_QUEUE_NAME = 'Call Queue';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Does this user hold the monitor permission?
|
|
17
|
+
*
|
|
18
|
+
* `userProfile` comes from the host app's profile endpoint, which eager-loads
|
|
19
|
+
* the Spatie shape `roles[].permissions[].name`. Direct (non-role) permissions
|
|
20
|
+
* are checked too, since Spatie allows assigning a permission straight to a
|
|
21
|
+
* user.
|
|
22
|
+
*/
|
|
23
|
+
export const hasMonitorPermission = (
|
|
24
|
+
userProfile,
|
|
25
|
+
permission = MONITOR_PERMISSION
|
|
26
|
+
) => {
|
|
27
|
+
if (!userProfile) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const roles = Array.isArray(userProfile.roles) ? userProfile.roles : [];
|
|
32
|
+
|
|
33
|
+
const viaRole = roles.some((role) =>
|
|
34
|
+
(Array.isArray(role?.permissions) ? role.permissions : []).some(
|
|
35
|
+
(item) => item?.name === permission
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (viaRole) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const direct = Array.isArray(userProfile.permissions)
|
|
44
|
+
? userProfile.permissions
|
|
45
|
+
: [];
|
|
46
|
+
|
|
47
|
+
return direct.some((item) => item?.name === permission);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reduce any incoming number to Australian local digits (0xxxxxxxxx).
|
|
52
|
+
* `+61412345678` / `61412345678` / `0412 345 678` all collapse to `0412345678`.
|
|
53
|
+
* Display formatting only — the server mirrors this same normalisation for the
|
|
54
|
+
* client match.
|
|
55
|
+
*/
|
|
56
|
+
export const toLocalDigits = (value) => {
|
|
57
|
+
if (!value) {
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let digits = String(value).replace(/\D/g, '');
|
|
62
|
+
|
|
63
|
+
// +61 / 0011 61 style country code -> single leading zero.
|
|
64
|
+
if (digits.length === 11 && digits.indexOf('61') === 0) {
|
|
65
|
+
digits = `0${digits.slice(2)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return digits;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Human AU formatting: mobiles as `04xx xxx xxx`, landlines as `0x xxxx xxxx`.
|
|
73
|
+
* libphonenumber-js is not a dependency, so this mirrors the formatter the
|
|
74
|
+
* legacy vendored call pop used (minus its `.replace('61', 0)` bug, which
|
|
75
|
+
* mangled numbers that merely contained "61").
|
|
76
|
+
*/
|
|
77
|
+
export const formatAuPhone = (value) => {
|
|
78
|
+
const digits = toLocalDigits(value);
|
|
79
|
+
|
|
80
|
+
if (digits === '') {
|
|
81
|
+
return value ? String(value) : 'Unknown number';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (digits.indexOf('04') === 0) {
|
|
85
|
+
const mobile = digits.match(/^(\d{4})(\d{3})(\d{3})$/);
|
|
86
|
+
if (mobile) {
|
|
87
|
+
return `${mobile[1]} ${mobile[2]} ${mobile[3]}`;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
const landline = digits.match(/^(\d{2})(\d{4})(\d{4})$/);
|
|
91
|
+
if (landline) {
|
|
92
|
+
return `${landline[1]} ${landline[2]} ${landline[3]}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return digits;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A meeting date as `DD/MM h:mma` (e.g. `21/08 10:00am`). Empty string for an
|
|
101
|
+
* absent or unparseable date, so the caller can just skip it.
|
|
102
|
+
*/
|
|
103
|
+
export const formatEventDate = (value) => {
|
|
104
|
+
if (!value) {
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const date = new Date(value);
|
|
109
|
+
|
|
110
|
+
if (Number.isNaN(date.getTime())) {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
115
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
116
|
+
const hours = date.getHours();
|
|
117
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
118
|
+
const suffix = hours < 12 ? 'am' : 'pm';
|
|
119
|
+
const hour12 = hours % 12 === 0 ? 12 : hours % 12;
|
|
120
|
+
|
|
121
|
+
return `${day}/${month} ${hour12}:${minutes}${suffix}`;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** A task due date as `DD/MM/YYYY`; empty string when absent or unparseable. */
|
|
125
|
+
export const formatDueDate = (value) => {
|
|
126
|
+
if (!value) {
|
|
127
|
+
return '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const date = new Date(value);
|
|
131
|
+
|
|
132
|
+
if (Number.isNaN(date.getTime())) {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
137
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
138
|
+
|
|
139
|
+
return `${day}/${month}/${date.getFullYear()}`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The `dataId` the Active Call workspace (`/call/:dataId`) expects: it rebuilds
|
|
144
|
+
* the number it searches on as `'0' + dataId.slice(2)`, so the param has to be
|
|
145
|
+
* the AU number in `61xxxxxxxxx` form. Anything that does not reduce to a local
|
|
146
|
+
* `0…` number has no workspace to open, and yields ''.
|
|
147
|
+
*/
|
|
148
|
+
export const toCallWorkspaceId = (value) => {
|
|
149
|
+
const digits = toLocalDigits(value);
|
|
150
|
+
|
|
151
|
+
if (digits.indexOf('0') !== 0 || digits.length < 9) {
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return `61${digits.slice(1)}`;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Tasks index filtered to one client: `/tasks/:param_task_type_id/:param_company_id`.
|
|
160
|
+
* Type 0 is the grid's own "All Current Tasks" option, which is the closest
|
|
161
|
+
* thing to "every open task for this client" the route can express — types 1-4
|
|
162
|
+
* each narrow it further.
|
|
163
|
+
*/
|
|
164
|
+
export const defaultClientTasksUrl = (clientId) => `/tasks/0/${clientId}`;
|
|
165
|
+
|
|
166
|
+
/** Default popup target for a task row. */
|
|
167
|
+
export const defaultTaskUrl = (task) => `/tasks/detail/${task?.id}`;
|
|
168
|
+
|
|
169
|
+
/** Default detail route for a matched client. */
|
|
170
|
+
export const defaultClientUrl = (client) => `/clients/${client?.id}`;
|
|
171
|
+
|
|
172
|
+
/** The client fields shown as label/value rows, in card order. */
|
|
173
|
+
export const CLIENT_DETAIL_FIELDS = [
|
|
174
|
+
{ key: 'adviser', label: 'Adviser' },
|
|
175
|
+
{ key: 'coding', label: 'Coding' },
|
|
176
|
+
{ key: 'age', label: 'Age' },
|
|
177
|
+
{ key: 'city', label: 'City' },
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Detail rows for a client block, with the empty ones dropped — a card only
|
|
182
|
+
* ever shows what the CRM actually knows, never a row with a blank value.
|
|
183
|
+
*/
|
|
184
|
+
export const clientDetails = (client) => {
|
|
185
|
+
if (!client || typeof client !== 'object') {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return CLIENT_DETAIL_FIELDS.map(({ key, label }) => ({
|
|
190
|
+
label,
|
|
191
|
+
value: client[key],
|
|
192
|
+
})).filter(
|
|
193
|
+
(row) =>
|
|
194
|
+
row.value !== null &&
|
|
195
|
+
row.value !== undefined &&
|
|
196
|
+
String(row.value).trim() !== ''
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** Seconds since `startedAt` rendered as m:ss (never negative). */
|
|
201
|
+
export const formatElapsed = (startedAt, now) => {
|
|
202
|
+
const started = new Date(startedAt).getTime();
|
|
203
|
+
|
|
204
|
+
if (Number.isNaN(started)) {
|
|
205
|
+
return '0:00';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const seconds = Math.max(0, Math.floor((now - started) / 1000));
|
|
209
|
+
const minutes = Math.floor(seconds / 60);
|
|
210
|
+
const remainder = seconds % 60;
|
|
211
|
+
|
|
212
|
+
return `${minutes}:${String(remainder).padStart(2, '0')}`;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Coerce a payload of unknown shape (snapshot endpoint or broadcast) into the
|
|
217
|
+
* component's call shape. Snake_case and camelCase keys are both accepted so a
|
|
218
|
+
* Laravel resource can be passed through untouched.
|
|
219
|
+
*/
|
|
220
|
+
export const normaliseCall = (raw) => {
|
|
221
|
+
if (!raw || typeof raw !== 'object') {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const callId = raw.callId ?? raw.call_id ?? raw.id;
|
|
226
|
+
|
|
227
|
+
if (callId === undefined || callId === null || callId === '') {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// The Zoom call queue this rang on. `queueId` keys the pickup-code lookup
|
|
232
|
+
// and may legitimately be absent; `queueName` is what the badge prints.
|
|
233
|
+
const rawQueueId = raw.queueId ?? raw.queue_id ?? null;
|
|
234
|
+
const queueId =
|
|
235
|
+
rawQueueId === null || rawQueueId === undefined || rawQueueId === ''
|
|
236
|
+
? null
|
|
237
|
+
: String(rawQueueId);
|
|
238
|
+
|
|
239
|
+
const rawQueueName = raw.queueName ?? raw.queue_name ?? '';
|
|
240
|
+
const queueName = String(rawQueueName).trim() || FALLBACK_QUEUE_NAME;
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
callId: String(callId),
|
|
244
|
+
queueId,
|
|
245
|
+
queueName,
|
|
246
|
+
callerNumber: raw.callerNumber ?? raw.caller_number ?? raw.from ?? '',
|
|
247
|
+
callerName: raw.callerName ?? raw.caller_name ?? '',
|
|
248
|
+
client: raw.client ?? null,
|
|
249
|
+
// Pre-supplied open tasks. Only demo calls carry these; a real call
|
|
250
|
+
// leaves it null and the expansion fetches the drill-down endpoint.
|
|
251
|
+
tasks: Array.isArray(raw.tasks) ? raw.tasks : null,
|
|
252
|
+
startedAt: raw.startedAt ?? raw.started_at ?? new Date().toISOString(),
|
|
253
|
+
status: 'ringing',
|
|
254
|
+
isDemo: raw.isDemo === true || raw.is_demo === true,
|
|
255
|
+
leaving: false,
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Coerce the snapshot's `pickup_codes` block into a plain `{ queueId: code }`
|
|
261
|
+
* map, dropping anything that is not a non-empty string on both sides. A queue
|
|
262
|
+
* absent from the map has no code, which is what suppresses its Pick up button.
|
|
263
|
+
*/
|
|
264
|
+
export const normalisePickupCodes = (raw) => {
|
|
265
|
+
if (!raw || typeof raw !== 'object') {
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const codes = {};
|
|
270
|
+
|
|
271
|
+
Object.keys(raw).forEach((queueId) => {
|
|
272
|
+
const code = raw[queueId];
|
|
273
|
+
|
|
274
|
+
if (queueId !== '' && typeof code === 'string' && code.trim() !== '') {
|
|
275
|
+
codes[queueId] = code.trim();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return codes;
|
|
280
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the call-queue settings page.
|
|
3
|
+
*
|
|
4
|
+
* Split out of CallQueueSettings.jsx so they can be imported by `node --test`,
|
|
5
|
+
* which cannot parse JSX. Nothing here touches React or the DOM; the component
|
|
6
|
+
* re-exports the lot, so its public surface is unchanged.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Zoom's pickup code rules, mirrored from CallQueueSettingsController so a typo
|
|
11
|
+
* is caught before the round trip. Returns an error string, or '' when valid.
|
|
12
|
+
*/
|
|
13
|
+
export const validateCode = (value) => {
|
|
14
|
+
const code = String(value ?? '').trim();
|
|
15
|
+
|
|
16
|
+
if (code === '') return '';
|
|
17
|
+
if (!/^\d{4}$/.test(code)) return 'Must be 4 digits.';
|
|
18
|
+
if (code[0] === '0') return 'Cannot start with 0.';
|
|
19
|
+
if (/^(\d)\1{3}$/.test(code)) return 'Cannot repeat one digit.';
|
|
20
|
+
|
|
21
|
+
const steps = [1, 2, 3].map((i) => Number(code[i]) - Number(code[i - 1]));
|
|
22
|
+
|
|
23
|
+
if (steps.every((s) => s === 1) || steps.every((s) => s === -1)) {
|
|
24
|
+
return 'Cannot be a sequence.';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return '';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Digits only — the `*` is the dial prefix, never part of the stored code. */
|
|
31
|
+
export const cleanCode = (value) =>
|
|
32
|
+
String(value ?? '')
|
|
33
|
+
.replace(/[^\d]/g, '')
|
|
34
|
+
.slice(0, 4);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Human AU formatting for the queue's own number, using the same digit logic as
|
|
38
|
+
* the call pop's formatter (callQueue/CallQueuePop.jsx): a +61 / 61 country code
|
|
39
|
+
* collapses to a single leading zero, then mobiles read as `04xx xxx xxx` and
|
|
40
|
+
* landlines as `(0x) xxxx xxxx`. Anything that does not fit a known shape is
|
|
41
|
+
* handed back untouched rather than mangled.
|
|
42
|
+
*/
|
|
43
|
+
export const formatAuNumber = (value) => {
|
|
44
|
+
if (!value) return '';
|
|
45
|
+
|
|
46
|
+
let digits = String(value).replace(/\D/g, '');
|
|
47
|
+
|
|
48
|
+
if (digits.length === 11 && digits.indexOf('61') === 0) {
|
|
49
|
+
digits = `0${digits.slice(2)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (digits.indexOf('04') === 0) {
|
|
53
|
+
const mobile = digits.match(/^(\d{4})(\d{3})(\d{3})$/);
|
|
54
|
+
|
|
55
|
+
if (mobile) return `${mobile[1]} ${mobile[2]} ${mobile[3]}`;
|
|
56
|
+
} else if (/^1[38]00/.test(digits) && digits.length === 10) {
|
|
57
|
+
// 13/1800 service numbers group as 1300 123 456.
|
|
58
|
+
return `${digits.slice(0, 4)} ${digits.slice(4, 7)} ${digits.slice(7)}`;
|
|
59
|
+
} else {
|
|
60
|
+
const landline = digits.match(/^(\d{2})(\d{4})(\d{4})$/);
|
|
61
|
+
|
|
62
|
+
if (landline) return `(${landline[1]}) ${landline[2]} ${landline[3]}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return String(value);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Zoom sends the status lower case ("active"); the pill shows it title case. */
|
|
69
|
+
export const titleCase = (value) => {
|
|
70
|
+
const text = String(value ?? '').trim();
|
|
71
|
+
|
|
72
|
+
if (text === '') return 'Unknown';
|
|
73
|
+
|
|
74
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
75
|
+
};
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
DEFAULT_INTELLIGENT_SORTING_CONFIG,
|
|
15
15
|
} from '../../utils/relationshipSortingUtils';
|
|
16
16
|
import { formatCellContent } from '../generic/shared/formatters';
|
|
17
|
+
import { arrayCountFrom } from '../utils/displayValue';
|
|
17
18
|
|
|
18
19
|
// Stage Toggle Utility Functions
|
|
19
20
|
const buildToggleUrl = (urlTemplate, stageItem, rowData) => {
|
|
@@ -380,18 +381,63 @@ export const renderDateTimeColumn = ({
|
|
|
380
381
|
};
|
|
381
382
|
};
|
|
382
383
|
|
|
383
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Array Count Column Renderer
|
|
386
|
+
*
|
|
387
|
+
* A count is a quantity, and a bare digit in a text column reads as a short
|
|
388
|
+
* piece of text — it takes a second look to tell "3" the count from "3" the
|
|
389
|
+
* label. The number goes in a small neutral pill so the eye can pick the
|
|
390
|
+
* column out and compare rows down it, and an empty list says so with a muted
|
|
391
|
+
* dash rather than an empty cell (an empty cell is what a column shows when it
|
|
392
|
+
* has no answer; here we have one — none).
|
|
393
|
+
*
|
|
394
|
+
* `arrayCountFrom` is what makes the zero case reachable at all: the old
|
|
395
|
+
* truthiness check sent `[]`, `"[]"` and a missing field down the same branch,
|
|
396
|
+
* and `.length` on a JSON string that was never decoded returned the character
|
|
397
|
+
* count.
|
|
398
|
+
*/
|
|
384
399
|
export const renderArrayCountColumn = ({ column, commonProps }) => {
|
|
385
400
|
return {
|
|
386
401
|
...commonProps,
|
|
387
402
|
name: `${column.type}.${column.id}`,
|
|
388
|
-
|
|
403
|
+
// Was hard-coded `true`. A count column is derived from a JSON field,
|
|
404
|
+
// so server-side ordering on it is lexicographic nonsense as often as
|
|
405
|
+
// not; a config can now turn the header control off. Default unchanged.
|
|
406
|
+
sortable: column.sortable ?? true,
|
|
389
407
|
render: ({ data }) => {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return
|
|
408
|
+
const count = arrayCountFrom(data ? data[column.id] : null);
|
|
409
|
+
|
|
410
|
+
if (count === 0) {
|
|
411
|
+
return (
|
|
412
|
+
<span style={{ color: 'var(--muted-color, #6e7276)' }}>
|
|
413
|
+
—
|
|
414
|
+
</span>
|
|
415
|
+
);
|
|
394
416
|
}
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<span
|
|
420
|
+
style={{
|
|
421
|
+
display: 'inline-flex',
|
|
422
|
+
alignItems: 'center',
|
|
423
|
+
justifyContent: 'center',
|
|
424
|
+
minWidth: '22px',
|
|
425
|
+
height: '20px',
|
|
426
|
+
padding: '0 7px',
|
|
427
|
+
boxSizing: 'border-box',
|
|
428
|
+
borderRadius: '999px',
|
|
429
|
+
border: '1px solid var(--border-color, #e1e1e1)',
|
|
430
|
+
background: 'var(--hover-color, rgba(0, 0, 0, 0.04))',
|
|
431
|
+
color: 'var(--paragraph-color, #221e33)',
|
|
432
|
+
fontSize: '0.75rem',
|
|
433
|
+
fontWeight: 600,
|
|
434
|
+
lineHeight: 1,
|
|
435
|
+
fontVariantNumeric: 'tabular-nums',
|
|
436
|
+
}}
|
|
437
|
+
>
|
|
438
|
+
{count}
|
|
439
|
+
</span>
|
|
440
|
+
);
|
|
395
441
|
},
|
|
396
442
|
};
|
|
397
443
|
};
|
|
@@ -2877,7 +2923,7 @@ export const renderStageCounterColumn = ({ column, commonProps, navigate, onUpda
|
|
|
2877
2923
|
rawValue !== null &&
|
|
2878
2924
|
rawValue !== undefined
|
|
2879
2925
|
) {
|
|
2880
|
-
tooltipContent = `Stage ${i + 1}
|
|
2926
|
+
tooltipContent = `Stage ${i + 1}`;
|
|
2881
2927
|
}
|
|
2882
2928
|
}
|
|
2883
2929
|
|