@flamingo-stack/openframe-frontend-core 0.0.852 → 0.0.854
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/dist/build/export-map.json +1 -1
- package/dist/{chunk-G7GF4PWA.cjs → chunk-2IJMHBOK.cjs} +3 -3
- package/dist/{chunk-IC4IJJB2.cjs → chunk-3WXHPD6N.cjs} +55 -55
- package/dist/{chunk-UV45EPXN.cjs → chunk-4LTLEUY5.cjs} +5 -5
- package/dist/{chunk-BNHTBLAG.cjs → chunk-4O5N5CGK.cjs} +17 -17
- package/dist/{chunk-COWMXUHN.cjs → chunk-4SSJ3T3N.cjs} +3 -1
- package/dist/{chunk-IUDBG3HJ.cjs → chunk-5HMENXSR.cjs} +3 -3
- package/dist/{chunk-COJG2ATY.cjs → chunk-5OENPMRU.cjs} +43 -43
- package/dist/{chunk-IO6NN3SL.cjs → chunk-5TXE7BXT.cjs} +46 -46
- package/dist/{chunk-EIACEY2L.cjs → chunk-63ZV3UGG.cjs} +10 -10
- package/dist/{chunk-HTYJUMFB.cjs → chunk-6DCUGIGU.cjs} +2 -2
- package/dist/{chunk-GEZ3UXCA.cjs → chunk-6XU3RCK3.cjs} +18 -18
- package/dist/{chunk-3ZQESOPJ.cjs → chunk-7J5NSWJG.cjs} +87 -87
- package/dist/{chunk-MXY2OGEV.cjs → chunk-7MOBD44H.cjs} +28 -28
- package/dist/{chunk-U767P4JU.cjs → chunk-ACPOJE32.cjs} +9 -9
- package/dist/{chunk-CHGIACIP.cjs → chunk-BAA3YCLT.cjs} +72 -72
- package/dist/{chunk-VUHHLRAY.cjs → chunk-D2XGRWPW.cjs} +73 -73
- package/dist/{chunk-5RMBLOJ5.cjs → chunk-EIHMPQRQ.cjs} +9 -9
- package/dist/{chunk-3SJVF3HW.cjs → chunk-ETQNMR2K.cjs} +5 -5
- package/dist/{chunk-2W22LY3W.cjs → chunk-FBANHIAG.cjs} +9 -9
- package/dist/{chunk-P63CRPWI.cjs → chunk-FJXSKMQJ.cjs} +20 -20
- package/dist/{chunk-QOW2GP3V.cjs → chunk-GVHCQRUE.cjs} +21 -21
- package/dist/{chunk-KNN2GCMW.cjs → chunk-H3GBPYPS.cjs} +18 -18
- package/dist/{chunk-FW7RKWPY.cjs → chunk-JKBNK5U6.cjs} +3 -3
- package/dist/{chunk-W7OFAROY.cjs → chunk-KA3L6D5N.cjs} +3 -3
- package/dist/{chunk-USPUZO53.cjs → chunk-LQDAB5V5.cjs} +4 -4
- package/dist/{chunk-KHOB3TES.cjs → chunk-ONYANAMA.cjs} +4 -4
- package/dist/{chunk-D3H6AI7O.cjs → chunk-PRJZMZ3X.cjs} +1182 -746
- package/dist/chunk-QD727QFI.cjs +62 -0
- package/dist/{chunk-X3WIOMEE.cjs → chunk-RMI6O3AE.cjs} +32 -32
- package/dist/{chunk-MFDRCTM3.cjs → chunk-S5P6CF5R.cjs} +47 -47
- package/dist/{chunk-477P6AQH.cjs → chunk-TMPLHUUU.cjs} +6 -6
- package/dist/{chunk-UO2EJGRJ.cjs → chunk-UPQ4MAR7.cjs} +14 -14
- package/dist/{chunk-LFEURIZN.cjs → chunk-VMAOZA2V.cjs} +215 -215
- package/dist/{chunk-NMWLXHFH.cjs → chunk-YY3WP2YQ.cjs} +30 -30
- package/dist/{chunk-WFAZMASF.cjs → chunk-ZDPPNHA2.cjs} +3 -3
- package/dist/{cloud-tenants-O33HCUYB.cjs → cloud-tenants-5RNK4ZAT.cjs} +19 -19
- package/dist/components/case-studies/index.cjs +33 -33
- package/dist/components/chat/index.cjs +13 -13
- package/dist/components/contact/index.cjs +20 -20
- package/dist/components/docs/index.cjs +23 -23
- package/dist/components/embeds/index.cjs +20 -20
- package/dist/components/faq/index.cjs +21 -21
- package/dist/components/features/cloud-tenants/index.d.ts +7 -5
- package/dist/components/features/cloud-tenants/index.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/index.js +8 -1
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts +26 -0
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts.map +1 -0
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.js +176 -0
- package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts +4 -2
- package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-access-cell.js +3 -2
- package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts +12 -5
- package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-customer-cell.js +38 -7
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts +7 -0
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts.map +1 -0
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.js +17 -0
- package/dist/components/features/cloud-tenants/tenant-presentation.d.ts +19 -0
- package/dist/components/features/cloud-tenants/tenant-presentation.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-presentation.js +24 -3
- package/dist/components/features/cloud-tenants/tenant-row.d.ts +35 -8
- package/dist/components/features/cloud-tenants/tenant-row.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts +9 -3
- package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-columns.js +5 -3
- package/dist/components/features/cloud-tenants/tenants-table-view.d.ts +27 -6
- package/dist/components/features/cloud-tenants/tenants-table-view.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-view.js +75 -17
- package/dist/components/features/devices/device-directory.d.ts +18 -0
- package/dist/components/features/devices/device-directory.d.ts.map +1 -0
- package/dist/components/features/devices/device-directory.js +35 -0
- package/dist/components/features/devices/devices-filter-toolbar.d.ts +3 -1
- package/dist/components/features/devices/devices-filter-toolbar.d.ts.map +1 -1
- package/dist/components/features/devices/devices-filter-toolbar.js +2 -1
- package/dist/components/features/devices/devices-panel-view.d.ts +3 -1
- package/dist/components/features/devices/devices-panel-view.d.ts.map +1 -1
- package/dist/components/features/devices/devices-panel-view.js +11 -2
- package/dist/components/features/devices/devices-table-columns.d.ts +17 -3
- package/dist/components/features/devices/devices-table-columns.d.ts.map +1 -1
- package/dist/components/features/devices/devices-table-columns.js +80 -10
- package/dist/components/features/devices/index.d.ts +1 -0
- package/dist/components/features/devices/index.d.ts.map +1 -1
- package/dist/components/features/devices/index.js +8 -0
- package/dist/components/features/devices/types.d.ts +14 -0
- package/dist/components/features/devices/types.d.ts.map +1 -1
- package/dist/components/features/devices/use-tag-filter-modal.d.ts +1 -0
- package/dist/components/features/devices/use-tag-filter-modal.d.ts.map +1 -1
- package/dist/components/features/devices/use-tag-filter-modal.js +13 -8
- package/dist/components/features/index.cjs +33 -19
- package/dist/components/help-center-pages/index.cjs +120 -120
- package/dist/components/help-center-pages/trust-center-page.d.ts +1 -1
- package/dist/components/index.cjs +96 -76
- package/dist/components/meeting-scheduler/index.cjs +82 -82
- package/dist/components/navigation/app-layout.js +2 -2
- package/dist/components/navigation/index.cjs +22 -22
- package/dist/components/onboarding-guides/index.cjs +22 -22
- package/dist/components/platform/tool-badge-mark.d.ts +1 -1
- package/dist/components/product-demo/index.cjs +30 -30
- package/dist/components/related-content/index.cjs +8 -8
- package/dist/components/tickets/index.cjs +24 -24
- package/dist/components/ui/data-attribution.d.ts +8 -3
- package/dist/components/ui/data-attribution.d.ts.map +1 -1
- package/dist/components/ui/data-attribution.js +17 -6
- package/dist/components/ui/date-picker.d.ts +2 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/date-picker.js +19 -6
- package/dist/components/ui/index.cjs +25 -19
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/{devices-2RGGMC2B.cjs → devices-44O5PZOW.cjs} +20 -20
- package/dist/{embeddable-chat-SJEEJI4N.cjs → embeddable-chat-W3CLMGC3.cjs} +90 -90
- package/dist/hooks/index.cjs +11 -7
- package/dist/hooks/ui/use-content-breakpoint.d.ts +8 -0
- package/dist/hooks/ui/use-content-breakpoint.d.ts.map +1 -1
- package/dist/hooks/ui/use-content-breakpoint.js +17 -3
- package/dist/hooks/ui/use-media-query.d.ts +1 -0
- package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
- package/dist/hooks/ui/use-media-query.js +3 -1
- package/dist/index.cjs +44 -24
- package/dist/{knowledge-5TJIXVMD.cjs → knowledge-UUU3WVO3.cjs} +20 -20
- package/dist/{logs-HGQOTRKZ.cjs → logs-DFHNSODA.cjs} +26 -26
- package/dist/{policies-GWFSLVSM.cjs → policies-RGFUGDBP.cjs} +23 -23
- package/dist/{remote-session-SG44CNOK.cjs → remote-session-U3H77HKR.cjs} +10 -10
- package/dist/{scripts-PGJNAZF4.cjs → scripts-KOVO5DHW.cjs} +26 -26
- package/dist/{software-update-KKLSYHDI.cjs → software-update-4EKHVV5W.cjs} +23 -23
- package/dist/{tickets-board-SQY2Y6QV.cjs → tickets-board-YIFOVMDX.cjs} +10 -10
- package/dist/types/data-sync.d.ts +4 -1
- package/dist/types/data-sync.d.ts.map +1 -1
- package/dist/types/data-sync.js +4 -1
- package/dist/types/index.cjs +4 -1
- package/dist/utils/presentation-for.d.ts +6 -0
- package/dist/utils/presentation-for.d.ts.map +1 -0
- package/dist/utils/presentation-for.js +7 -0
- package/package.json +1 -1
- package/src/components/features/cloud-tenants/index.ts +8 -3
- package/src/components/features/cloud-tenants/single-customer-tenants-table-view.tsx +234 -0
- package/src/components/features/cloud-tenants/tenant-access-cell.tsx +12 -5
- package/src/components/features/cloud-tenants/tenant-customer-cell.tsx +48 -12
- package/src/components/features/cloud-tenants/tenant-licenses-cell.tsx +28 -0
- package/src/components/features/cloud-tenants/tenant-presentation.ts +46 -4
- package/src/components/features/cloud-tenants/tenant-row.ts +38 -8
- package/src/components/features/cloud-tenants/tenants-table-columns.ts +8 -5
- package/src/components/features/cloud-tenants/tenants-table-view.tsx +123 -28
- package/src/components/features/devices/device-directory.ts +55 -0
- package/src/components/features/devices/devices-filter-toolbar.tsx +4 -1
- package/src/components/features/devices/devices-panel-view.tsx +12 -2
- package/src/components/features/devices/devices-table-columns.tsx +123 -8
- package/src/components/features/devices/index.ts +7 -0
- package/src/components/features/devices/types.ts +14 -0
- package/src/components/features/devices/use-tag-filter-modal.ts +32 -14
- package/src/components/help-center-pages/trust-center-page.tsx +1 -1
- package/src/components/navigation/app-layout.tsx +4 -4
- package/src/components/platform/tool-badge-mark.ts +1 -1
- package/src/components/ui/data-attribution.tsx +20 -8
- package/src/components/ui/date-picker.tsx +21 -3
- package/src/components/ui/index.ts +2 -0
- package/src/hooks/ui/use-content-breakpoint.ts +21 -2
- package/src/hooks/ui/use-media-query.ts +1 -0
- package/src/types/data-sync.ts +4 -1
- package/src/utils/presentation-for.ts +10 -0
- package/dist/chunk-UAI4VMEF.cjs +0 -46
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from '../../ui';
|
|
24
24
|
import { DATA_TABLE_SKELETON_ROWS } from '../../ui/data-table/utils';
|
|
25
25
|
import { OSTypeBadge } from '../os-type-badge';
|
|
26
|
+
import { directorySourceLabel, directoryUserLines, directoryUserLinkLabel } from './device-directory';
|
|
26
27
|
import { getDeviceName } from './device-name';
|
|
27
28
|
import { getDeviceStatusConfig } from './device-status';
|
|
28
29
|
import { DEFAULT_VISIBLE_STATUSES } from './device-statuses';
|
|
@@ -129,6 +130,8 @@ export interface DevicesTableBodyProps<T extends DeviceRow = DeviceRow> {
|
|
|
129
130
|
actionsColumn?: ColumnDef<T>;
|
|
130
131
|
/** Column ids to drop from the base table columns (e.g. ['organization'] when scoped to a single org). */
|
|
131
132
|
hideColumns?: string[];
|
|
133
|
+
/** Adds the USER and SOURCES columns, for a host whose tenant has directories. Default: neither. */
|
|
134
|
+
directoryColumns?: boolean;
|
|
132
135
|
/** Column ids whose header filter is dropped while the column stays visible (e.g. ['status'] on the archive page). */
|
|
133
136
|
disableColumnFilters?: string[];
|
|
134
137
|
/** The facet query is still in flight, see `getDeviceTableColumns`. */
|
|
@@ -168,6 +171,7 @@ export function DevicesTableBody<T extends DeviceRow = DeviceRow>({
|
|
|
168
171
|
onColumnFiltersChange,
|
|
169
172
|
actionsColumn,
|
|
170
173
|
hideColumns,
|
|
174
|
+
directoryColumns = false,
|
|
171
175
|
disableColumnFilters,
|
|
172
176
|
filtersPending,
|
|
173
177
|
totalCount,
|
|
@@ -177,12 +181,16 @@ export function DevicesTableBody<T extends DeviceRow = DeviceRow>({
|
|
|
177
181
|
const columns = useMemo<ColumnDef<T>[]>(() => {
|
|
178
182
|
const hidden = new Set(hideColumns ?? []);
|
|
179
183
|
const unfiltered = new Set(disableColumnFilters ?? []);
|
|
180
|
-
const base = getDeviceTableColumns<T>(deviceFilters ?? null, filtersPending
|
|
184
|
+
const base = getDeviceTableColumns<T>(deviceFilters ?? null, filtersPending, {
|
|
185
|
+
directoryColumns,
|
|
186
|
+
// Without the customer column there is room for the sources from where the customer's column would have started.
|
|
187
|
+
roomyDirectory: hidden.has('organization'),
|
|
188
|
+
})
|
|
181
189
|
.filter(c => !c.id || !hidden.has(c.id))
|
|
182
190
|
.map(c => (c.id && unfiltered.has(c.id) ? { ...c, meta: { ...c.meta, filter: undefined } } : c));
|
|
183
191
|
const open = getDeviceOpenColumn<T>();
|
|
184
192
|
return actionsColumn ? [...base, actionsColumn, open] : [...base, open];
|
|
185
|
-
}, [deviceFilters, filtersPending, actionsColumn, hideColumns, disableColumnFilters]);
|
|
193
|
+
}, [deviceFilters, filtersPending, actionsColumn, hideColumns, directoryColumns, disableColumnFilters]);
|
|
186
194
|
|
|
187
195
|
// While loading the table holds one page of empty rows, drawn with the real cells: every part of a row draws its
|
|
188
196
|
// own loading form (`DataTable.Body`'s `loadingCells`), so a loading row is the loaded row's own layout.
|
|
@@ -257,11 +265,43 @@ function DeviceLastSeen({ lastSeen }: { lastSeen: string | undefined }) {
|
|
|
257
265
|
);
|
|
258
266
|
}
|
|
259
267
|
|
|
268
|
+
/** The name over its role, as the USER column reads it (Figma 865:55733); the empty mark when nobody is tied. */
|
|
269
|
+
function DirectoryUserCell({ device }: { device: DeviceRow }) {
|
|
270
|
+
const loading = useLoadingScope();
|
|
271
|
+
if (loading && !device.directoryUserLink) {
|
|
272
|
+
return <TextLineSkeleton type="h4" className="w-3/4" />;
|
|
273
|
+
}
|
|
274
|
+
const { name, detail } = directoryUserLines(device);
|
|
275
|
+
return (
|
|
276
|
+
<div className="flex min-w-0 flex-col justify-center">
|
|
277
|
+
<TruncateText>{name}</TruncateText>
|
|
278
|
+
{detail && (
|
|
279
|
+
<TruncateText variant="h6" tone="secondary">
|
|
280
|
+
{detail}
|
|
281
|
+
</TruncateText>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The directories that know the device, as tags (Figma 187:61663); nothing when none does. */
|
|
288
|
+
function DirectorySourcesCell({ sources }: { sources: readonly string[] | undefined }) {
|
|
289
|
+
return (
|
|
290
|
+
<div data-loading-optional className="flex min-w-0 flex-wrap gap-[var(--spacing-system-xxs)]">
|
|
291
|
+
{(sources ?? []).map(source => (
|
|
292
|
+
<Tag key={source} label={directorySourceLabel(source)} variant="grey" />
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
260
298
|
export interface DeviceFilterColumn {
|
|
261
299
|
key: string;
|
|
262
300
|
label: string;
|
|
263
301
|
filterable?: boolean;
|
|
264
302
|
filterOptions?: DeviceFilterOption[];
|
|
303
|
+
/** Filters in the filter window from `lg` until `xl` too: its column's header has no funnel there. */
|
|
304
|
+
inWindowLgToXl?: boolean;
|
|
265
305
|
}
|
|
266
306
|
|
|
267
307
|
/** A device filter dropdown entry: `meta.filter.options` plus the facet count. */
|
|
@@ -277,6 +317,8 @@ function buildDeviceFilterOptions(deviceFilters?: DeviceFilters | null): {
|
|
|
277
317
|
status: DeviceFilterOption[];
|
|
278
318
|
os: DeviceFilterOption[];
|
|
279
319
|
organization: DeviceFilterOption[];
|
|
320
|
+
user: DeviceFilterOption[];
|
|
321
|
+
sources: DeviceFilterOption[];
|
|
280
322
|
} {
|
|
281
323
|
// Show only DEFAULT_VISIBLE_STATUSES (DELETED and legacy ARCHIVED live on /devices/archive)
|
|
282
324
|
const status = (deviceFilters?.statuses ?? [])
|
|
@@ -294,19 +336,56 @@ function buildDeviceFilterOptions(deviceFilters?: DeviceFilters | null): {
|
|
|
294
336
|
})),
|
|
295
337
|
);
|
|
296
338
|
|
|
297
|
-
|
|
339
|
+
const user = (deviceFilters?.userLinks ?? []).map(link => ({
|
|
340
|
+
id: link.value,
|
|
341
|
+
label: directoryUserLinkLabel(link.value),
|
|
342
|
+
value: link.value,
|
|
343
|
+
count: link.count,
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
const sources = (deviceFilters?.directorySources ?? []).map(source => ({
|
|
347
|
+
id: source.value,
|
|
348
|
+
label: directorySourceLabel(source.value),
|
|
349
|
+
value: source.value,
|
|
350
|
+
count: source.count,
|
|
351
|
+
}));
|
|
352
|
+
|
|
353
|
+
return { status, os, organization, user, sources };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** The directory columns of a device list: the table and its filter window take the same ones. */
|
|
357
|
+
interface DeviceDirectoryColumnOptions {
|
|
358
|
+
directoryColumns?: boolean;
|
|
359
|
+
roomyDirectory?: boolean;
|
|
298
360
|
}
|
|
299
361
|
|
|
300
362
|
// Filter column metadata used by the external filter modal (useTagFilterModal).
|
|
301
363
|
// Kept separate from the table ColumnDef because DataTable doesn't carry the
|
|
302
364
|
// filter metadata that the external mobile filter modal needs.
|
|
303
|
-
|
|
365
|
+
// `roomyDirectory` is the table's own (`getDeviceTableColumns`): beside the customer column SOURCES shows only from
|
|
366
|
+
// `xl`, and the header has no funnel for it from `lg` until then, so the window carries it there.
|
|
367
|
+
export function getDeviceFilterColumns(
|
|
368
|
+
deviceFilters?: DeviceFilters | null,
|
|
369
|
+
{ directoryColumns = false, roomyDirectory = false }: DeviceDirectoryColumnOptions = {},
|
|
370
|
+
): DeviceFilterColumn[] {
|
|
304
371
|
const options = buildDeviceFilterOptions(deviceFilters);
|
|
305
372
|
return [
|
|
306
373
|
{
|
|
307
374
|
key: 'device',
|
|
308
375
|
label: 'DEVICE',
|
|
309
376
|
},
|
|
377
|
+
...(directoryColumns
|
|
378
|
+
? [
|
|
379
|
+
{ key: 'user', label: 'USER', filterable: true, filterOptions: options.user },
|
|
380
|
+
{
|
|
381
|
+
key: 'sources',
|
|
382
|
+
label: 'SOURCES',
|
|
383
|
+
filterable: true,
|
|
384
|
+
filterOptions: options.sources,
|
|
385
|
+
inWindowLgToXl: !roomyDirectory,
|
|
386
|
+
},
|
|
387
|
+
]
|
|
388
|
+
: []),
|
|
310
389
|
{ key: 'status', label: 'STATUS', filterable: true, filterOptions: options.status },
|
|
311
390
|
{ key: 'os', label: 'OS', filterable: true, filterOptions: options.os },
|
|
312
391
|
{ key: 'organization', label: 'CUSTOMER', filterable: true, filterOptions: options.organization },
|
|
@@ -318,17 +397,51 @@ export function getDeviceFilterColumns(deviceFilters?: DeviceFilters | null): De
|
|
|
318
397
|
* funnels drawn (inert) rather than growing them when it answers. Absent means
|
|
319
398
|
* these are the final options: an empty one then hides its funnel, which is what
|
|
320
399
|
* a list with no facet source of its own wants.
|
|
400
|
+
* @param options.directoryColumns Adds USER and SOURCES after DEVICE. Off by default: a picker or a tenant with no
|
|
401
|
+
* directories has neither.
|
|
402
|
+
* @param options.roomyDirectory No customer column beside them (one customer's list), so SOURCES shows from `lg`
|
|
403
|
+
* rather than `xl`.
|
|
321
404
|
*/
|
|
322
405
|
export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
323
406
|
deviceFilters?: DeviceFilters | null,
|
|
324
407
|
filtersPending?: boolean,
|
|
408
|
+
{ directoryColumns = false, roomyDirectory = false }: DeviceDirectoryColumnOptions = {},
|
|
325
409
|
): ColumnDef<T>[] {
|
|
326
410
|
const {
|
|
327
411
|
status: statusFilterOptions,
|
|
328
412
|
os: osFilterOptions,
|
|
329
413
|
organization: orgFilterOptions,
|
|
414
|
+
user: userFilterOptions,
|
|
415
|
+
sources: sourcesFilterOptions,
|
|
330
416
|
} = buildDeviceFilterOptions(deviceFilters);
|
|
331
417
|
|
|
418
|
+
const directory: ColumnDef<T>[] = directoryColumns
|
|
419
|
+
? [
|
|
420
|
+
{
|
|
421
|
+
id: 'user',
|
|
422
|
+
header: 'USER',
|
|
423
|
+
cell: ({ row }: { row: Row<T> }) => <DirectoryUserCell device={row.original} />,
|
|
424
|
+
enableSorting: false,
|
|
425
|
+
meta: {
|
|
426
|
+
width: 'min-w-0 content-lg:w-[15%]',
|
|
427
|
+
hideAt: 'lg',
|
|
428
|
+
filter: { options: userFilterOptions, pending: filtersPending },
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: 'sources',
|
|
433
|
+
header: 'SOURCES',
|
|
434
|
+
cell: ({ row }: { row: Row<T> }) => <DirectorySourcesCell sources={row.original.directorySources} />,
|
|
435
|
+
enableSorting: false,
|
|
436
|
+
meta: {
|
|
437
|
+
width: 'min-w-0 content-lg:w-[11%]',
|
|
438
|
+
hideAt: roomyDirectory ? 'lg' : 'xl',
|
|
439
|
+
filter: { options: sourcesFilterOptions, pending: filtersPending },
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
]
|
|
443
|
+
: [];
|
|
444
|
+
|
|
332
445
|
return [
|
|
333
446
|
{
|
|
334
447
|
accessorKey: 'device',
|
|
@@ -347,8 +460,10 @@ export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
|
347
460
|
</div>
|
|
348
461
|
);
|
|
349
462
|
},
|
|
350
|
-
|
|
463
|
+
// The name takes what the other columns leave: with two more of them, their shares below shrink to leave some.
|
|
464
|
+
meta: { width: directoryColumns ? 'flex-1 min-w-0' : 'flex-1 content-md:w-1/4' },
|
|
351
465
|
},
|
|
466
|
+
...directory,
|
|
352
467
|
{
|
|
353
468
|
accessorKey: 'status',
|
|
354
469
|
id: 'status',
|
|
@@ -366,7 +481,7 @@ export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
|
366
481
|
);
|
|
367
482
|
},
|
|
368
483
|
meta: {
|
|
369
|
-
width: 'w-auto shrink-0 content-md:w-1/5',
|
|
484
|
+
width: directoryColumns ? 'w-auto shrink-0 content-md:w-[14%]' : 'w-auto shrink-0 content-md:w-1/5',
|
|
370
485
|
// Declared unconditionally, empty options and all. `meta.filter` is what
|
|
371
486
|
// keeps a header cell visible below `lg`, so dropping it until the
|
|
372
487
|
// options query resolves made every header on a tablet disappear on
|
|
@@ -383,7 +498,7 @@ export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
|
383
498
|
<OSTypeBadge osType={row.original.osType} iconSize="w-4 h-4 content-md:w-6 content-md:h-6" />
|
|
384
499
|
),
|
|
385
500
|
meta: {
|
|
386
|
-
width: 'w-[200px] content-md:w-1/6',
|
|
501
|
+
width: directoryColumns ? 'w-[200px] content-md:w-[10%]' : 'w-[200px] content-md:w-1/6',
|
|
387
502
|
hideAt: 'md',
|
|
388
503
|
filter: { options: osFilterOptions, pending: filtersPending },
|
|
389
504
|
},
|
|
@@ -394,7 +509,7 @@ export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
|
394
509
|
header: 'CUSTOMER',
|
|
395
510
|
cell: ({ row }: { row: Row<T> }) => <OrganizationCell device={row.original} />,
|
|
396
511
|
meta: {
|
|
397
|
-
width: 'w-1/6',
|
|
512
|
+
width: directoryColumns ? 'w-[14%]' : 'w-1/6',
|
|
398
513
|
hideAt: 'lg',
|
|
399
514
|
filter: { options: orgFilterOptions, placement: 'bottom-end', pending: filtersPending },
|
|
400
515
|
},
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
export {
|
|
4
|
+
type DeviceDirectorySource,
|
|
5
|
+
type DeviceDirectoryUserLink,
|
|
6
|
+
directorySourceLabel,
|
|
7
|
+
directoryUserLines,
|
|
8
|
+
directoryUserLinkLabel,
|
|
9
|
+
} from './device-directory';
|
|
3
10
|
export { formatLastOnline } from './device-last-online';
|
|
4
11
|
export { type DeviceNameSource, getDeviceName, matchesDeviceName } from './device-name';
|
|
5
12
|
export {
|
|
@@ -34,6 +34,14 @@ export interface DeviceRow extends DeviceNameSource {
|
|
|
34
34
|
organizationImageUrl?: string | null;
|
|
35
35
|
organizationImageHash?: string | null;
|
|
36
36
|
tags?: DeviceTag[];
|
|
37
|
+
/** Who the device is tied to in a directory: PRIMARY, SHARED or NONE. The USER column reads it. */
|
|
38
|
+
directoryUserLink?: string;
|
|
39
|
+
/** The primary user's name; only PRIMARY has one. */
|
|
40
|
+
directoryUserName?: string | null;
|
|
41
|
+
/** How many users are tied to it: "N recent users" when SHARED. */
|
|
42
|
+
directoryUserCount?: number;
|
|
43
|
+
/** The directories that know the device besides the agent. The SOURCES column reads it. */
|
|
44
|
+
directorySources?: readonly string[];
|
|
37
45
|
/** The viewer's unread notifications about this device. Absent or 0: no indicator. */
|
|
38
46
|
unreadNotificationCount?: number;
|
|
39
47
|
}
|
|
@@ -63,6 +71,10 @@ export interface DeviceFilters {
|
|
|
63
71
|
organizationIds: DeviceFilterTag[];
|
|
64
72
|
tagKeys: TagFilterOption[];
|
|
65
73
|
filteredCount: number;
|
|
74
|
+
/** Behind the USER funnel; only hosts that draw the directory columns read it. */
|
|
75
|
+
userLinks?: DeviceFilterValue[];
|
|
76
|
+
/** Behind the SOURCES funnel; only hosts that draw the directory columns read it. */
|
|
77
|
+
directorySources?: DeviceFilterValue[];
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
/** What narrows a device list, in the backend's field names. */
|
|
@@ -73,4 +85,6 @@ export interface DeviceFilterInput {
|
|
|
73
85
|
organizationIds?: string[];
|
|
74
86
|
tagKeys?: string[];
|
|
75
87
|
tagValues?: string[];
|
|
88
|
+
userLinks?: string[];
|
|
89
|
+
directorySources?: string[];
|
|
76
90
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useContentLgUp, useContentMdUp, useContentXlUp } from '../../../hooks/ui/use-content-breakpoint';
|
|
5
5
|
import type { DeviceFilters } from './types';
|
|
6
6
|
|
|
7
7
|
function isCompleteTag(tag: string): boolean {
|
|
@@ -19,7 +19,13 @@ interface FilterOption {
|
|
|
19
19
|
interface UseTagFilterModalParams {
|
|
20
20
|
tags: string[];
|
|
21
21
|
deviceFilters: DeviceFilters | null;
|
|
22
|
-
columns: Array<{
|
|
22
|
+
columns: Array<{
|
|
23
|
+
key: string;
|
|
24
|
+
label: string;
|
|
25
|
+
filterable?: boolean;
|
|
26
|
+
filterOptions?: FilterOption[];
|
|
27
|
+
inWindowLgToXl?: boolean;
|
|
28
|
+
}>;
|
|
23
29
|
/** `useApiParams`' setter, each device list param is a list of ids. */
|
|
24
30
|
setParams: (params: Record<string, string[]>) => void;
|
|
25
31
|
/**
|
|
@@ -39,7 +45,11 @@ export function useTagFilterModal({
|
|
|
39
45
|
setParams,
|
|
40
46
|
onApplied = scrollMainToTop,
|
|
41
47
|
}: UseTagFilterModalParams) {
|
|
42
|
-
|
|
48
|
+
// The content area's step, not the window's: the table header follows it, and a docked side panel narrows it while
|
|
49
|
+
// the window stays wide.
|
|
50
|
+
const isMdUp = useContentMdUp();
|
|
51
|
+
const lgUp = useContentLgUp();
|
|
52
|
+
const xlUp = useContentXlUp();
|
|
43
53
|
const [isOpen, setIsOpen] = useState(false);
|
|
44
54
|
|
|
45
55
|
// FilterModal calls onFilterChange first, then onTagsChange.
|
|
@@ -49,16 +59,19 @@ export function useTagFilterModal({
|
|
|
49
59
|
const open = useCallback(() => setIsOpen(true), []);
|
|
50
60
|
const close = useCallback(() => setIsOpen(false), []);
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
// Below md the table draws no header, so every column filters here; from lg until xl only the columns whose header
|
|
63
|
+
// has no funnel there do.
|
|
64
|
+
const filterGroups = useMemo(
|
|
65
|
+
() =>
|
|
66
|
+
columns
|
|
67
|
+
.filter(column => column.filterable && (!isMdUp || (lgUp && xlUp === false && column.inWindowLgToXl)))
|
|
68
|
+
.map(column => ({
|
|
69
|
+
id: column.key,
|
|
70
|
+
title: column.label,
|
|
71
|
+
options: column.filterOptions || [],
|
|
72
|
+
})),
|
|
73
|
+
[columns, isMdUp, lgUp, xlUp],
|
|
74
|
+
);
|
|
62
75
|
|
|
63
76
|
const tagFilterKeys = useMemo(() => {
|
|
64
77
|
if (!deviceFilters?.tagKeys) return [];
|
|
@@ -79,6 +92,9 @@ export function useTagFilterModal({
|
|
|
79
92
|
pendingFiltersRef.current = columnFilters;
|
|
80
93
|
}, []);
|
|
81
94
|
|
|
95
|
+
// Only a host that draws the directory columns has the USER and SOURCES groups: another's list params stay untouched
|
|
96
|
+
const hasDirectoryGroups = useMemo(() => columns.some(column => column.key === 'user'), [columns]);
|
|
97
|
+
|
|
82
98
|
// Step 2: combine with buffered filters and flush (called second by FilterModal)
|
|
83
99
|
const handleTagsChange = useCallback(
|
|
84
100
|
(newTags: string[]) => {
|
|
@@ -87,12 +103,14 @@ export function useTagFilterModal({
|
|
|
87
103
|
statuses: cf.status || [],
|
|
88
104
|
osTypes: cf.os || [],
|
|
89
105
|
organizationIds: cf.organization || [],
|
|
106
|
+
// FilterModal drops a group with nothing ticked, so a missing group clears its param
|
|
107
|
+
...(hasDirectoryGroups && { userLinks: cf.user || [], directorySources: cf.sources || [] }),
|
|
90
108
|
tags: newTags.filter(isCompleteTag),
|
|
91
109
|
});
|
|
92
110
|
pendingFiltersRef.current = {};
|
|
93
111
|
onApplied();
|
|
94
112
|
},
|
|
95
|
-
[setParams, onApplied],
|
|
113
|
+
[setParams, onApplied, hasDirectoryGroups],
|
|
96
114
|
);
|
|
97
115
|
|
|
98
116
|
return {
|
|
@@ -103,7 +103,7 @@ export interface TrustCenterPageProps {
|
|
|
103
103
|
*/
|
|
104
104
|
beforeFaq?: ReactNode;
|
|
105
105
|
/**
|
|
106
|
-
* The "Data
|
|
106
|
+
* The "Data from <source>" line and its PDF download in the status row. Default true. Pass false when the
|
|
107
107
|
* host's own header states both (the hub's admin preview draws them in its page header), so the page never says
|
|
108
108
|
* them twice; the monitoring status stays.
|
|
109
109
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import type { CSSProperties, ReactNode } from 'react';
|
|
5
|
-
import { ContentAreaWidthContext,
|
|
5
|
+
import { ContentAreaWidthContext, contentStepFor } from '../../hooks/ui/use-content-breakpoint';
|
|
6
6
|
import { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect';
|
|
7
7
|
import type { NavigationSidebarConfig } from '../../types/navigation';
|
|
8
8
|
import { cn } from '../../utils';
|
|
@@ -203,14 +203,14 @@ export function AppLayout({
|
|
|
203
203
|
|
|
204
204
|
useIsomorphicLayoutEffect(() => {
|
|
205
205
|
if (!adaptive || !mainElement) return undefined;
|
|
206
|
-
// Every reader asks for
|
|
207
|
-
// crossing re-renders the layout: kept at the old width, the state bails out.
|
|
206
|
+
// Every reader asks for a step (`useContentBreakpoint`, `useContentXlUp`), so
|
|
207
|
+
// only a crossing re-renders the layout: kept at the old width, the state bails out.
|
|
208
208
|
// Per pixel, the uncompiled layout and every breakpoint consumer re-rendered on
|
|
209
209
|
// each frame of a sidebar toggle, a window drag or a side-panel resize.
|
|
210
210
|
const update = () => {
|
|
211
211
|
const width = mainElement.clientWidth;
|
|
212
212
|
setContentWidth(previous =>
|
|
213
|
-
previous !== null &&
|
|
213
|
+
previous !== null && contentStepFor(previous) === contentStepFor(width) ? previous : width,
|
|
214
214
|
);
|
|
215
215
|
};
|
|
216
216
|
update();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ICON_SIZE } from '../../styles/component-sizes';
|
|
2
2
|
/**
|
|
3
|
-
* A tool or source mark's 16px box (`ToolBadge`'s mark, the "Data
|
|
3
|
+
* A tool or source mark's 16px box (`ToolBadge`'s mark, the "Data from" line's source glyph): read by the marks
|
|
4
4
|
* and by every skeleton that stands for one. A leaf, so a `ui` component can read it without loading the badge.
|
|
5
5
|
*/
|
|
6
6
|
export const TOOL_MARK_CLASS = `flex ${ICON_SIZE.sm} shrink-0 items-center justify-center`;
|
|
@@ -7,6 +7,9 @@ import { formatDate } from '../../utils/format';
|
|
|
7
7
|
import { ClickupIcon } from '../icons-v2-generated/brand-logos/clickup-icon';
|
|
8
8
|
import { GithubClassicIcon } from '../icons-v2-generated/brand-logos/github-classic-icon';
|
|
9
9
|
import { HubspotIcon } from '../icons-v2-generated/brand-logos/hubspot-icon';
|
|
10
|
+
import { OpenframeLogoGreyIcon } from '../icons-v2-generated/brand-logos/openframe-logo-grey-icon';
|
|
11
|
+
import { QuickbooksIcon } from '../icons-v2-generated/brand-logos/quickbooks-icon';
|
|
12
|
+
import { StripeIcon } from '../icons-v2-generated/brand-logos/stripe-icon';
|
|
10
13
|
import { TextLineSkeleton, UnifiedSkeleton } from '../loading/unified-skeleton';
|
|
11
14
|
import { TOOL_MARK_CLASS } from '../platform/tool-badge-mark';
|
|
12
15
|
import { useLoadingScope } from './loading-scope';
|
|
@@ -39,8 +42,8 @@ function Separator() {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
|
-
* THE "Data
|
|
43
|
-
*
|
|
45
|
+
* THE "Data from <source> · Last updated: <date>" line every surface showing another system's data
|
|
46
|
+
* draws (the cap table, the financials, the Trust Center). The date is
|
|
44
47
|
* UTC (`formatDate`), so a server-rendered page and its hydrated copy print the
|
|
45
48
|
* same text.
|
|
46
49
|
*/
|
|
@@ -50,7 +53,7 @@ export function DataAttribution({ icon, source, lastUpdated, action }: DataAttri
|
|
|
50
53
|
<div className={LINE_CLASS}>
|
|
51
54
|
<div className={ITEM_CLASS}>
|
|
52
55
|
{icon}
|
|
53
|
-
<span>Data
|
|
56
|
+
<span>Data from {source}</span>
|
|
54
57
|
</div>
|
|
55
58
|
<Separator />
|
|
56
59
|
<span className={UPDATED_CLASS}>Last updated: {lastUpdated ? formatDate(lastUpdated) : 'Never'}</span>
|
|
@@ -70,7 +73,7 @@ function DataAttributionSkeleton({ icon, action }: Pick<DataAttributionProps, 'i
|
|
|
70
73
|
<div className={ITEM_CLASS}>
|
|
71
74
|
{icon ?? <UnifiedSkeleton className={cn(TOOL_MARK_CLASS, 'rounded')} />}
|
|
72
75
|
<span>
|
|
73
|
-
Data
|
|
76
|
+
Data from <TextLineSkeleton type="inherit" as="span" className={`${WORD_BAR_CLASS} w-20`} />
|
|
74
77
|
</span>
|
|
75
78
|
</div>
|
|
76
79
|
<Separator />
|
|
@@ -88,12 +91,22 @@ function DataAttributionSkeleton({ icon, action }: Pick<DataAttributionProps, 'i
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/** The mark each sync source is drawn with (the app's standard brand marks); its name is `DATA_SYNC_SOURCES`'. */
|
|
91
|
-
const DATA_SYNC_ICONS: Record<DataSyncSource,
|
|
94
|
+
const DATA_SYNC_ICONS: Record<DataSyncSource, (props: { size?: number }) => ReactNode> = {
|
|
92
95
|
clickup: ClickupIcon,
|
|
93
96
|
hubspot: HubspotIcon,
|
|
94
97
|
github: GithubClassicIcon,
|
|
98
|
+
// The OpenFrame mark drawn in `currentColor`, so it reads as the text beside it in both themes.
|
|
99
|
+
openframe: OpenframeLogoGreyIcon,
|
|
100
|
+
stripe: StripeIcon,
|
|
101
|
+
quickbooks: QuickbooksIcon,
|
|
95
102
|
};
|
|
96
103
|
|
|
104
|
+
/** A sync source's mark, the one `DataSyncLine` draws: for a page that names the source in its own words. */
|
|
105
|
+
export function DataSyncIcon({ source, size = 16 }: { source: DataSyncSource; size?: number }) {
|
|
106
|
+
const Icon = DATA_SYNC_ICONS[source];
|
|
107
|
+
return <Icon size={size} />;
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
/**
|
|
98
111
|
* THE sync line of a shared page whose records an API reads from a mirror (the roadmap and the delivery board:
|
|
99
112
|
* ClickUp; the ticket center: HubSpot; the docs and the legal documents: GitHub). The page draws whatever `sync`
|
|
@@ -121,13 +134,12 @@ export function DataSyncLine({
|
|
|
121
134
|
const inScope = useLoadingScope();
|
|
122
135
|
const loading = (loadingProp || inScope) && stated;
|
|
123
136
|
if (!loading && !(sync && sync.source in DATA_SYNC_ICONS)) return null;
|
|
124
|
-
const Icon = sync ? DATA_SYNC_ICONS[sync.source] : null;
|
|
125
137
|
const line =
|
|
126
|
-
loading || !sync
|
|
138
|
+
loading || !sync ? (
|
|
127
139
|
<DataAttributionSkeleton action={action} />
|
|
128
140
|
) : (
|
|
129
141
|
<DataAttribution
|
|
130
|
-
icon={<
|
|
142
|
+
icon={<DataSyncIcon source={sync.source} />}
|
|
131
143
|
source={DATA_SYNC_SOURCES[sync.source]}
|
|
132
144
|
lastUpdated={sync.syncedAt}
|
|
133
145
|
action={action}
|
|
@@ -42,6 +42,8 @@ export interface DatePickerBaseProps {
|
|
|
42
42
|
error?: string;
|
|
43
43
|
/** When true, renders red error border */
|
|
44
44
|
invalid?: boolean;
|
|
45
|
+
/** The trigger button's id: a label drawn outside the picker names it with `htmlFor`. */
|
|
46
|
+
id?: string;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export interface SingleDatePickerProps extends DatePickerBaseProps {
|
|
@@ -69,6 +71,10 @@ export type DatePickerProps = SingleDatePickerProps | RangeDatePickerProps;
|
|
|
69
71
|
const defaultFormatDate = (date: Date): string =>
|
|
70
72
|
formatDateWithTimezone(date, VIEWER_TIMEZONE, 'numeric', { viewerLocale: true });
|
|
71
73
|
|
|
74
|
+
// A label tied to the trigger (`id`) becomes the button's whole accessible name, so the date it shows would go unread:
|
|
75
|
+
// the trigger is described by its value instead.
|
|
76
|
+
const triggerValueId = (id: string | undefined) => (id ? `${id}-value` : undefined);
|
|
77
|
+
|
|
72
78
|
const formatDateRange = (range: DateRange | undefined, formatFn: (date: Date) => string): string => {
|
|
73
79
|
if (!range?.from) return '';
|
|
74
80
|
if (!range.to) return formatFn(range.from);
|
|
@@ -667,6 +673,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
667
673
|
label,
|
|
668
674
|
error,
|
|
669
675
|
invalid = false,
|
|
676
|
+
id,
|
|
670
677
|
} = props;
|
|
671
678
|
|
|
672
679
|
const [open, setOpen] = useState(false);
|
|
@@ -701,6 +708,8 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
701
708
|
<Button
|
|
702
709
|
variant="field"
|
|
703
710
|
type="button"
|
|
711
|
+
id={id}
|
|
712
|
+
aria-describedby={triggerValueId(id)}
|
|
704
713
|
disabled={disabled}
|
|
705
714
|
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
706
715
|
// (and scrolls to) the first field that failed validation.
|
|
@@ -709,6 +718,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
709
718
|
>
|
|
710
719
|
<Calendar className="size-6 shrink-0 text-ods-text-secondary" />
|
|
711
720
|
<span
|
|
721
|
+
id={triggerValueId(id)}
|
|
712
722
|
className={cn(
|
|
713
723
|
'flex-1 truncate text-left',
|
|
714
724
|
disabled ? 'text-ods-text-disabled' : displayValue ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
@@ -749,7 +759,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
749
759
|
);
|
|
750
760
|
|
|
751
761
|
return (
|
|
752
|
-
<FieldWrapper label={label} error={error} errorSlot={'error' in props}>
|
|
762
|
+
<FieldWrapper label={label} htmlFor={id} error={error} errorSlot={'error' in props}>
|
|
753
763
|
{picker}
|
|
754
764
|
</FieldWrapper>
|
|
755
765
|
);
|
|
@@ -799,6 +809,7 @@ export function DatePickerInput(props: DatePickerInputProps) {
|
|
|
799
809
|
label,
|
|
800
810
|
error,
|
|
801
811
|
invalid = false,
|
|
812
|
+
id,
|
|
802
813
|
} = props;
|
|
803
814
|
const [open, setOpen] = useState(false);
|
|
804
815
|
const portalContainer = usePortalContainer();
|
|
@@ -883,6 +894,8 @@ export function DatePickerInput(props: DatePickerInputProps) {
|
|
|
883
894
|
<Button
|
|
884
895
|
variant="field"
|
|
885
896
|
type="button"
|
|
897
|
+
id={id}
|
|
898
|
+
aria-describedby={triggerValueId(id)}
|
|
886
899
|
disabled={disabled}
|
|
887
900
|
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
888
901
|
// (and scrolls to) the first field that failed validation.
|
|
@@ -891,6 +904,7 @@ export function DatePickerInput(props: DatePickerInputProps) {
|
|
|
891
904
|
>
|
|
892
905
|
<Calendar className="size-6 shrink-0 text-ods-text-secondary" />
|
|
893
906
|
<span
|
|
907
|
+
id={triggerValueId(id)}
|
|
894
908
|
className={cn(
|
|
895
909
|
'flex-1 truncate text-left',
|
|
896
910
|
disabled
|
|
@@ -984,7 +998,7 @@ export function DatePickerInput(props: DatePickerInputProps) {
|
|
|
984
998
|
);
|
|
985
999
|
|
|
986
1000
|
return (
|
|
987
|
-
<FieldWrapper label={label} error={error} errorSlot={'error' in props} className={className}>
|
|
1001
|
+
<FieldWrapper label={label} htmlFor={id} error={error} errorSlot={'error' in props} className={className}>
|
|
988
1002
|
{content}
|
|
989
1003
|
</FieldWrapper>
|
|
990
1004
|
);
|
|
@@ -1050,6 +1064,7 @@ export function DatePickerInputSimple(props: DatePickerInputSimpleProps) {
|
|
|
1050
1064
|
label,
|
|
1051
1065
|
error,
|
|
1052
1066
|
invalid = false,
|
|
1067
|
+
id,
|
|
1053
1068
|
} = props;
|
|
1054
1069
|
const [open, setOpen] = useState(false);
|
|
1055
1070
|
const portalContainer = usePortalContainer();
|
|
@@ -1112,6 +1127,8 @@ export function DatePickerInputSimple(props: DatePickerInputSimpleProps) {
|
|
|
1112
1127
|
<Button
|
|
1113
1128
|
variant="field"
|
|
1114
1129
|
type="button"
|
|
1130
|
+
id={id}
|
|
1131
|
+
aria-describedby={triggerValueId(id)}
|
|
1115
1132
|
disabled={disabled}
|
|
1116
1133
|
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
1117
1134
|
// (and scrolls to) the first field that failed validation.
|
|
@@ -1120,6 +1137,7 @@ export function DatePickerInputSimple(props: DatePickerInputSimpleProps) {
|
|
|
1120
1137
|
>
|
|
1121
1138
|
<Calendar className="size-6 shrink-0 text-ods-text-secondary" />
|
|
1122
1139
|
<span
|
|
1140
|
+
id={triggerValueId(id)}
|
|
1123
1141
|
className={cn(
|
|
1124
1142
|
'flex-1 truncate text-left',
|
|
1125
1143
|
disabled
|
|
@@ -1181,7 +1199,7 @@ export function DatePickerInputSimple(props: DatePickerInputSimpleProps) {
|
|
|
1181
1199
|
);
|
|
1182
1200
|
|
|
1183
1201
|
return (
|
|
1184
|
-
<FieldWrapper label={label} error={error} errorSlot={'error' in props} className={className}>
|
|
1202
|
+
<FieldWrapper label={label} htmlFor={id} error={error} errorSlot={'error' in props} className={className}>
|
|
1185
1203
|
{content}
|
|
1186
1204
|
</FieldWrapper>
|
|
1187
1205
|
);
|
|
@@ -112,6 +112,8 @@ export * from './device-card';
|
|
|
112
112
|
export * from './device-card-compact';
|
|
113
113
|
export * from './entity-image';
|
|
114
114
|
export * from './data-attribution';
|
|
115
|
+
// The sources `DataSyncLine` / `DataSyncIcon` take, beside them, so a page draws a source's name and mark from one import.
|
|
116
|
+
export * from '../../types/data-sync';
|
|
115
117
|
export * from './download-button';
|
|
116
118
|
export * from './compliance-badge';
|
|
117
119
|
export * from './feature-card';
|