@camstack/ui-library 1.2.372 → 1.2.373
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/CHANGELOG.md +6 -0
- package/dist/composites/device-list/generic-mode.d.ts +13 -0
- package/dist/composites/export-bulk-selection.d.ts +24 -5
- package/dist/index.cjs +159 -89
- package/dist/index.js +159 -89
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.373] - 2026-11-02
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Device export table: selectors on the left with the search on the right, searchable Role and Integration filters showing display names, a bulk bar with select all / none, and a multi export that now lands every selected device
|
|
15
|
+
|
|
10
16
|
## [1.2.372] - 2026-11-02
|
|
11
17
|
|
|
12
18
|
### Changed
|
|
@@ -94,6 +94,19 @@ export interface DeviceListGenericProps<T> {
|
|
|
94
94
|
readonly toolbar?: ReactNode | ((ctx: {
|
|
95
95
|
readonly filtered: readonly T[];
|
|
96
96
|
}) => ReactNode);
|
|
97
|
+
/**
|
|
98
|
+
* Extra selectors rendered on the LEFT, before the type filter, so every
|
|
99
|
+
* selector sits in one group and the search box stays alone on the right.
|
|
100
|
+
* Wraps onto their own row on a narrow screen.
|
|
101
|
+
*/
|
|
102
|
+
readonly filters?: ReactNode;
|
|
103
|
+
/**
|
|
104
|
+
* Full-width row between the filter row and the table (a bulk-action bar).
|
|
105
|
+
* A function receives the rows the search and filters currently leave.
|
|
106
|
+
*/
|
|
107
|
+
readonly belowFilters?: ReactNode | ((ctx: {
|
|
108
|
+
readonly filtered: readonly T[];
|
|
109
|
+
}) => ReactNode);
|
|
97
110
|
}
|
|
98
111
|
/**
|
|
99
112
|
* Generic render-prop list: search + pagination chrome shared with the device
|
|
@@ -7,12 +7,25 @@
|
|
|
7
7
|
* the only filter, D869).
|
|
8
8
|
*/
|
|
9
9
|
import type { MultiSelectOption } from "./device-list/multi-select";
|
|
10
|
+
import { type DeviceIntegrationRef } from "./device-list/device-integration";
|
|
10
11
|
/** The slice of a device row the bulk filters read. */
|
|
11
12
|
export interface ExportFilterable {
|
|
12
13
|
readonly id: number;
|
|
13
14
|
readonly addonId: string;
|
|
14
15
|
readonly role?: string | null;
|
|
16
|
+
/** The integration that created the device (`integrations.list` `id`). */
|
|
17
|
+
readonly integrationId?: string | null;
|
|
15
18
|
}
|
|
19
|
+
/** The slice of an `integrations.list` row the provider filter reads. */
|
|
20
|
+
export interface ExportIntegration extends DeviceIntegrationRef {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The provider filter value of a row: the id of the integration it belongs to,
|
|
25
|
+
* or `addon:<addonId>` when no registered integration claims it (or the list
|
|
26
|
+
* has not answered). The raw id is never shown — `providerFilterOptions` names it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function providerFilterId(device: ExportFilterable, integrations: readonly ExportIntegration[] | null | undefined): string;
|
|
16
29
|
export interface ExportBulkFilters {
|
|
17
30
|
/** Keep rows whose role is one of these; empty = no role filter. */
|
|
18
31
|
readonly roles: readonly string[];
|
|
@@ -24,11 +37,17 @@ export declare const NO_ROLE_FILTER_ID = "(none)";
|
|
|
24
37
|
export declare const NO_EXPORT_FILTERS: ExportBulkFilters;
|
|
25
38
|
export declare function roleFilterId(device: ExportFilterable): string;
|
|
26
39
|
/** The rows the role / provider filters leave. */
|
|
27
|
-
export declare function applyExportFilters<T extends ExportFilterable>(devices: readonly T[], filters: ExportBulkFilters): readonly T[];
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
export declare function applyExportFilters<T extends ExportFilterable>(devices: readonly T[], filters: ExportBulkFilters, integrations?: readonly ExportIntegration[] | null): readonly T[];
|
|
41
|
+
/**
|
|
42
|
+
* Distinct roles present among the rows, named by `labelOf` (a roleless row is
|
|
43
|
+
* `NO_ROLE_FILTER_ID`).
|
|
44
|
+
*/
|
|
45
|
+
export declare function roleFilterOptions(devices: readonly ExportFilterable[], labelOf?: (roleId: string) => string): readonly MultiSelectOption[];
|
|
46
|
+
/**
|
|
47
|
+
* Distinct integrations present among the rows, by NAME. A device no
|
|
48
|
+
* registered integration claims is listed under its addon id.
|
|
49
|
+
*/
|
|
50
|
+
export declare function providerFilterOptions(devices: readonly ExportFilterable[], integrations?: readonly ExportIntegration[] | null): readonly MultiSelectOption[];
|
|
32
51
|
/** `current` plus every shown row. */
|
|
33
52
|
export declare function selectShown(current: ReadonlySet<string>, shown: readonly ExportFilterable[]): ReadonlySet<string>;
|
|
34
53
|
/** `current` minus every shown row; selections outside the filter are kept. */
|
package/dist/index.cjs
CHANGED
|
@@ -62173,7 +62173,7 @@ var FIXED_PAGE_SIZE = 100;
|
|
|
62173
62173
|
*/
|
|
62174
62174
|
function GenericModeList(props) {
|
|
62175
62175
|
const t = useUiText();
|
|
62176
|
-
const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize: fixedPageSize = FIXED_PAGE_SIZE, pageSizeSelector, toolbar } = props;
|
|
62176
|
+
const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize: fixedPageSize = FIXED_PAGE_SIZE, pageSizeSelector, toolbar, filters, belowFilters } = props;
|
|
62177
62177
|
const [chosenPageSize, setChosenPageSize] = usePersistedPageSize(pageSizeSelector ?? {
|
|
62178
62178
|
storageKey: null,
|
|
62179
62179
|
options: [fixedPageSize],
|
|
@@ -62241,16 +62241,22 @@ function GenericModeList(props) {
|
|
|
62241
62241
|
className: "flex flex-col gap-3 h-full min-h-0",
|
|
62242
62242
|
children: [
|
|
62243
62243
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
62244
|
-
className: "flex items-
|
|
62245
|
-
children: [
|
|
62246
|
-
|
|
62247
|
-
|
|
62248
|
-
|
|
62249
|
-
|
|
62250
|
-
|
|
62251
|
-
|
|
62244
|
+
className: "flex items-start justify-between gap-3 flex-wrap shrink-0",
|
|
62245
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
62246
|
+
className: "flex min-w-0 flex-1 flex-wrap items-start gap-2",
|
|
62247
|
+
children: [filters, getFilterType && typeOptions.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
62248
|
+
className: "flex min-w-40 flex-1",
|
|
62249
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResponsiveTypeFilter, {
|
|
62250
|
+
label: filterLabel,
|
|
62251
|
+
options: typeOptions,
|
|
62252
|
+
selected: typeFilters,
|
|
62253
|
+
onChange: setTypeFilters
|
|
62254
|
+
})
|
|
62255
|
+
}) : null]
|
|
62256
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
62257
|
+
className: "flex items-center gap-2 max-sm:w-full",
|
|
62252
62258
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
62253
|
-
className: "relative",
|
|
62259
|
+
className: "relative max-sm:flex-1",
|
|
62254
62260
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-foreground-subtle pointer-events-none" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
62255
62261
|
ref: searchRef,
|
|
62256
62262
|
type: "text",
|
|
@@ -62258,11 +62264,12 @@ function GenericModeList(props) {
|
|
|
62258
62264
|
onChange: (e) => setSearchInput(e.target.value),
|
|
62259
62265
|
placeholder: searchPlaceholder,
|
|
62260
62266
|
"aria-label": searchPlaceholder,
|
|
62261
|
-
className: "h-7 pl-7 pr-2.5 rounded-md border border-border bg-surface text-[11px] text-foreground placeholder:text-foreground-subtle/60 focus:outline-none focus:ring-1 focus:ring-primary/40 w-56"
|
|
62267
|
+
className: "h-7 pl-7 pr-2.5 rounded-md border border-border bg-surface text-[11px] text-foreground placeholder:text-foreground-subtle/60 focus:outline-none focus:ring-1 focus:ring-primary/40 w-56 max-sm:w-full"
|
|
62262
62268
|
})]
|
|
62263
62269
|
}), typeof toolbar === "function" ? toolbar({ filtered }) : toolbar]
|
|
62264
62270
|
})]
|
|
62265
62271
|
}),
|
|
62272
|
+
typeof belowFilters === "function" ? belowFilters({ filtered }) : belowFilters,
|
|
62266
62273
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
62267
62274
|
className: "flex-1 min-h-0 overflow-y-auto",
|
|
62268
62275
|
children: isLoading && totalItems === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -62393,6 +62400,17 @@ function DeviceList(props) {
|
|
|
62393
62400
|
}
|
|
62394
62401
|
//#endregion
|
|
62395
62402
|
//#region src/composites/export-bulk-selection.ts
|
|
62403
|
+
/** The provider filter value of a device no registered integration claims. */
|
|
62404
|
+
var UNREGISTERED_PREFIX = "addon:";
|
|
62405
|
+
/**
|
|
62406
|
+
* The provider filter value of a row: the id of the integration it belongs to,
|
|
62407
|
+
* or `addon:<addonId>` when no registered integration claims it (or the list
|
|
62408
|
+
* has not answered). The raw id is never shown — `providerFilterOptions` names it.
|
|
62409
|
+
*/
|
|
62410
|
+
function providerFilterId(device, integrations) {
|
|
62411
|
+
const resolved = deviceIntegrationResolver(integrations)(device);
|
|
62412
|
+
return resolved.kind === "resolved" ? resolved.integration.id : `${UNREGISTERED_PREFIX}${device.addonId}`;
|
|
62413
|
+
}
|
|
62396
62414
|
/** The role filter value of a row that has no role. */
|
|
62397
62415
|
var NO_ROLE_FILTER_ID = "(none)";
|
|
62398
62416
|
var NO_EXPORT_FILTERS = {
|
|
@@ -62404,24 +62422,40 @@ function roleFilterId(device) {
|
|
|
62404
62422
|
return role === null || role === void 0 || role === "" ? NO_ROLE_FILTER_ID : role;
|
|
62405
62423
|
}
|
|
62406
62424
|
/** The rows the role / provider filters leave. */
|
|
62407
|
-
function applyExportFilters(devices, filters) {
|
|
62425
|
+
function applyExportFilters(devices, filters, integrations) {
|
|
62408
62426
|
const roles = new Set(filters.roles);
|
|
62409
62427
|
const providers = new Set(filters.providers);
|
|
62410
|
-
return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(d
|
|
62428
|
+
return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(providerFilterId(d, integrations))));
|
|
62411
62429
|
}
|
|
62412
|
-
function
|
|
62413
|
-
return
|
|
62414
|
-
id,
|
|
62415
|
-
label: id
|
|
62416
|
-
}));
|
|
62430
|
+
function sortedByLabel(options) {
|
|
62431
|
+
return options.toSorted((a, b) => a.label.localeCompare(b.label));
|
|
62417
62432
|
}
|
|
62418
|
-
/**
|
|
62419
|
-
|
|
62420
|
-
|
|
62433
|
+
/**
|
|
62434
|
+
* Distinct roles present among the rows, named by `labelOf` (a roleless row is
|
|
62435
|
+
* `NO_ROLE_FILTER_ID`).
|
|
62436
|
+
*/
|
|
62437
|
+
function roleFilterOptions(devices, labelOf = (id) => id) {
|
|
62438
|
+
return sortedByLabel([...new Set(devices.map(roleFilterId))].map((id) => ({
|
|
62439
|
+
id,
|
|
62440
|
+
label: labelOf(id)
|
|
62441
|
+
})));
|
|
62421
62442
|
}
|
|
62422
|
-
/**
|
|
62423
|
-
|
|
62424
|
-
|
|
62443
|
+
/**
|
|
62444
|
+
* Distinct integrations present among the rows, by NAME. A device no
|
|
62445
|
+
* registered integration claims is listed under its addon id.
|
|
62446
|
+
*/
|
|
62447
|
+
function providerFilterOptions(devices, integrations) {
|
|
62448
|
+
const nameById = new Map((integrations ?? []).map((i) => [i.id, i.name]));
|
|
62449
|
+
const options = /* @__PURE__ */ new Map();
|
|
62450
|
+
for (const d of devices) {
|
|
62451
|
+
const id = providerFilterId(d, integrations);
|
|
62452
|
+
if (options.has(id)) continue;
|
|
62453
|
+
options.set(id, nameById.get(id) ?? d.addonId);
|
|
62454
|
+
}
|
|
62455
|
+
return sortedByLabel([...options].map(([id, label]) => ({
|
|
62456
|
+
id,
|
|
62457
|
+
label
|
|
62458
|
+
})));
|
|
62425
62459
|
}
|
|
62426
62460
|
/** `current` plus every shown row. */
|
|
62427
62461
|
function selectShown(current, shown) {
|
|
@@ -62577,9 +62611,15 @@ var DeviceItemRowSchema = zod.z.object({
|
|
|
62577
62611
|
disabled: zod.z.boolean(),
|
|
62578
62612
|
parentDeviceId: zod.z.number().nullable(),
|
|
62579
62613
|
role: zod.z.string().nullable().optional(),
|
|
62614
|
+
integrationId: zod.z.string().nullable().optional(),
|
|
62580
62615
|
features: zod.z.array(zod.z.string()).optional(),
|
|
62581
62616
|
isCamera: zod.z.boolean().optional()
|
|
62582
62617
|
});
|
|
62618
|
+
var IntegrationRowsSchema = zod.z.array(zod.z.object({
|
|
62619
|
+
id: zod.z.string(),
|
|
62620
|
+
addonId: zod.z.string(),
|
|
62621
|
+
name: zod.z.string()
|
|
62622
|
+
}));
|
|
62583
62623
|
var SupportedKindsSchema = zod.z.array(zod.z.string());
|
|
62584
62624
|
var STATUS_POLL_INTERVAL_MS = 1e4;
|
|
62585
62625
|
/** Where the viewer's "rows per page" choice for this table is remembered. */
|
|
@@ -62836,6 +62876,11 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
62836
62876
|
addonId
|
|
62837
62877
|
}, { retry: false });
|
|
62838
62878
|
const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
|
|
62879
|
+
const integrationsQuery = useIntegrationsList(void 0);
|
|
62880
|
+
const integrations = (0, react$1.useMemo)(() => {
|
|
62881
|
+
const parsed = IntegrationRowsSchema.safeParse(integrationsQuery.data);
|
|
62882
|
+
return parsed.success ? parsed.data : null;
|
|
62883
|
+
}, [integrationsQuery.data]);
|
|
62839
62884
|
const entityKindsQuery = useDeviceExportListSupportedEntityKinds({
|
|
62840
62885
|
nodeId: "hub",
|
|
62841
62886
|
addonId
|
|
@@ -62953,28 +62998,28 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
62953
62998
|
const succeeded = [];
|
|
62954
62999
|
const failed = [];
|
|
62955
63000
|
const mutation = action === "expose" ? exposeMutation : unexposeMutation;
|
|
62956
|
-
(
|
|
62957
|
-
deviceId,
|
|
62958
|
-
nodeId: "hub",
|
|
62959
|
-
addonId
|
|
62960
|
-
})))).forEach((result, index) => {
|
|
62961
|
-
const deviceId = deviceIds[index] ?? "";
|
|
63001
|
+
for (const deviceId of deviceIds) {
|
|
62962
63002
|
const label = labelFor(deviceId);
|
|
62963
|
-
|
|
63003
|
+
try {
|
|
63004
|
+
await mutation.mutateAsync({
|
|
63005
|
+
deviceId,
|
|
63006
|
+
nodeId: "hub",
|
|
63007
|
+
addonId
|
|
63008
|
+
});
|
|
62964
63009
|
succeeded.push(label);
|
|
62965
|
-
|
|
63010
|
+
} catch (reason) {
|
|
63011
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
63012
|
+
console.error(`[device-export] ${action} failed`, {
|
|
63013
|
+
tags: { deviceId: Number.parseInt(deviceId, 10) },
|
|
63014
|
+
addonId,
|
|
63015
|
+
error: message
|
|
63016
|
+
});
|
|
63017
|
+
failed.push({
|
|
63018
|
+
label,
|
|
63019
|
+
message
|
|
63020
|
+
});
|
|
62966
63021
|
}
|
|
62967
|
-
|
|
62968
|
-
console.error(`[device-export] ${action} failed`, {
|
|
62969
|
-
tags: { deviceId: Number.parseInt(deviceId, 10) },
|
|
62970
|
-
addonId,
|
|
62971
|
-
error: message
|
|
62972
|
-
});
|
|
62973
|
-
failed.push({
|
|
62974
|
-
label,
|
|
62975
|
-
message
|
|
62976
|
-
});
|
|
62977
|
-
});
|
|
63022
|
+
}
|
|
62978
63023
|
setOutcome({
|
|
62979
63024
|
action,
|
|
62980
63025
|
total: deviceIds.length,
|
|
@@ -63035,9 +63080,19 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
63035
63080
|
running
|
|
63036
63081
|
]);
|
|
63037
63082
|
const [bulkFilters, setBulkFilters] = (0, react$1.useState)(NO_EXPORT_FILTERS);
|
|
63038
|
-
const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices),
|
|
63039
|
-
|
|
63040
|
-
|
|
63083
|
+
const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices, (id) => id === "(none)" ? t("uiLibrary.deviceExport.noRole", "No role") : deviceRoleMetaOf(id, t).label).map((o) => ({
|
|
63084
|
+
value: o.id,
|
|
63085
|
+
label: o.label
|
|
63086
|
+
})), [candidateDevices, t]);
|
|
63087
|
+
const providerOptions = (0, react$1.useMemo)(() => providerFilterOptions(candidateDevices, integrations).map((o) => ({
|
|
63088
|
+
value: o.id,
|
|
63089
|
+
label: o.label
|
|
63090
|
+
})), [candidateDevices, integrations]);
|
|
63091
|
+
const shownDevices = (0, react$1.useMemo)(() => applyExportFilters(candidateDevices, bulkFilters, integrations), [
|
|
63092
|
+
candidateDevices,
|
|
63093
|
+
bulkFilters,
|
|
63094
|
+
integrations
|
|
63095
|
+
]);
|
|
63041
63096
|
const shownRows = (0, react$1.useMemo)(() => {
|
|
63042
63097
|
const deviceRows = shownDevices.map((device) => ({
|
|
63043
63098
|
kind: "device",
|
|
@@ -63150,72 +63205,86 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
63150
63205
|
toggleSelection,
|
|
63151
63206
|
t
|
|
63152
63207
|
]);
|
|
63153
|
-
const
|
|
63208
|
+
const filterSelectors = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [roleOptions.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
63209
|
+
className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
|
|
63210
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
|
|
63211
|
+
mode: "multi",
|
|
63212
|
+
searchable: true,
|
|
63213
|
+
options: roleOptions,
|
|
63214
|
+
value: bulkFilters.roles,
|
|
63215
|
+
onChange: setRoleFilter,
|
|
63216
|
+
placeholder: t("uiLibrary.deviceExport.filterRole", "Role"),
|
|
63217
|
+
emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
|
|
63218
|
+
testId: "export-filter-role"
|
|
63219
|
+
})
|
|
63220
|
+
}), providerOptions.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
63221
|
+
className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
|
|
63222
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
|
|
63223
|
+
mode: "multi",
|
|
63224
|
+
searchable: true,
|
|
63225
|
+
options: providerOptions,
|
|
63226
|
+
value: bulkFilters.providers,
|
|
63227
|
+
onChange: setProviderFilter,
|
|
63228
|
+
placeholder: t("uiLibrary.deviceExport.filterIntegration", "Integration"),
|
|
63229
|
+
emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
|
|
63230
|
+
testId: "export-filter-integration"
|
|
63231
|
+
})
|
|
63232
|
+
})] });
|
|
63233
|
+
const renderBulkBar = (0, react$1.useCallback)(({ filtered: filteredRows }) => {
|
|
63154
63234
|
const filtered = filteredRows.flatMap((row) => row.kind === "device" ? [row.device] : []);
|
|
63155
|
-
|
|
63156
|
-
|
|
63235
|
+
if (filtered.length === 0 && selectedIds.size === 0) return null;
|
|
63236
|
+
const hasSelection = selectedIds.size > 0;
|
|
63237
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
63238
|
+
role: "toolbar",
|
|
63239
|
+
"aria-label": t("uiLibrary.deviceExport.bulkBar", "Bulk export"),
|
|
63240
|
+
className: `flex flex-wrap items-center gap-2 rounded-md border px-2.5 py-1.5 ${hasSelection ? "border-primary/40 bg-primary/10" : "border-border bg-surface"}`,
|
|
63157
63241
|
children: [
|
|
63158
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
63159
|
-
|
|
63160
|
-
|
|
63161
|
-
selected:
|
|
63162
|
-
onChange: setRoleFilter
|
|
63163
|
-
}),
|
|
63164
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
|
|
63165
|
-
label: t("uiLibrary.deviceExport.filterProvider", "Provider"),
|
|
63166
|
-
options: providerOptions,
|
|
63167
|
-
selected: bulkFilters.providers,
|
|
63168
|
-
onChange: setProviderFilter
|
|
63242
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
63243
|
+
...hasSelection ? { "data-testid": "export-selection-count" } : {},
|
|
63244
|
+
className: `text-[11px] font-medium ${hasSelection ? "text-foreground" : "text-foreground-subtle"}`,
|
|
63245
|
+
children: hasSelection ? t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size }) : t("uiLibrary.deviceExport.noneSelected", "None selected")
|
|
63169
63246
|
}),
|
|
63170
63247
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63171
63248
|
size: "sm",
|
|
63172
|
-
variant: "
|
|
63249
|
+
variant: "ghost",
|
|
63173
63250
|
disabled: filtered.length === 0,
|
|
63174
63251
|
onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
|
|
63175
|
-
children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
|
|
63252
|
+
children: t("uiLibrary.deviceExport.selectShown", "Select all {{count}} shown", { count: filtered.length })
|
|
63176
63253
|
}),
|
|
63177
|
-
|
|
63254
|
+
hasSelection && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
63178
63255
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63179
63256
|
size: "sm",
|
|
63180
63257
|
variant: "ghost",
|
|
63181
63258
|
onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
|
|
63182
63259
|
children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
|
|
63183
63260
|
}),
|
|
63184
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
63185
|
-
"data-testid": "export-selection-count",
|
|
63186
|
-
className: "text-[11px] text-foreground-subtle",
|
|
63187
|
-
children: t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size })
|
|
63188
|
-
}),
|
|
63189
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63190
|
-
size: "sm",
|
|
63191
|
-
variant: "primary",
|
|
63192
|
-
disabled: busy,
|
|
63193
|
-
onClick: exposeSelected,
|
|
63194
|
-
children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
|
|
63195
|
-
}),
|
|
63196
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63197
|
-
size: "sm",
|
|
63198
|
-
variant: "secondary",
|
|
63199
|
-
disabled: busy,
|
|
63200
|
-
onClick: unexposeSelected,
|
|
63201
|
-
children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
|
|
63202
|
-
}),
|
|
63203
63261
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63204
63262
|
size: "sm",
|
|
63205
63263
|
variant: "ghost",
|
|
63206
63264
|
onClick: clearSelection,
|
|
63207
63265
|
children: t("uiLibrary.deviceExport.clear", "Clear")
|
|
63266
|
+
}),
|
|
63267
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
63268
|
+
className: "ml-auto inline-flex flex-wrap items-center gap-1.5",
|
|
63269
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63270
|
+
size: "sm",
|
|
63271
|
+
variant: "primary",
|
|
63272
|
+
disabled: busy,
|
|
63273
|
+
onClick: exposeSelected,
|
|
63274
|
+
children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
|
|
63275
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63276
|
+
size: "sm",
|
|
63277
|
+
variant: "secondary",
|
|
63278
|
+
disabled: busy,
|
|
63279
|
+
onClick: unexposeSelected,
|
|
63280
|
+
children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
|
|
63281
|
+
})]
|
|
63208
63282
|
})
|
|
63209
63283
|
] })
|
|
63210
63284
|
]
|
|
63211
63285
|
});
|
|
63212
63286
|
}, [
|
|
63213
63287
|
t,
|
|
63214
|
-
roleOptions,
|
|
63215
|
-
providerOptions,
|
|
63216
|
-
bulkFilters,
|
|
63217
|
-
setRoleFilter,
|
|
63218
|
-
setProviderFilter,
|
|
63219
63288
|
selectedIds.size,
|
|
63220
63289
|
busy,
|
|
63221
63290
|
exposeSelected,
|
|
@@ -63270,7 +63339,8 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
63270
63339
|
filterOptions: typeFilterOptions,
|
|
63271
63340
|
columns,
|
|
63272
63341
|
onRowClick: onOpenDevice ? onRowClick : void 0,
|
|
63273
|
-
|
|
63342
|
+
filters: filterSelectors,
|
|
63343
|
+
belowFilters: renderBulkBar,
|
|
63274
63344
|
pageSizeSelector: DEVICE_EXPORT_PAGE_SIZE,
|
|
63275
63345
|
searchPlaceholder: t("uiLibrary.deviceExport.search", "Search by name or stableId"),
|
|
63276
63346
|
emptyLabel: t("uiLibrary.deviceExport.noMatch", "No device matches this search.")
|
|
@@ -72899,7 +72969,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
|
|
|
72899
72969
|
}
|
|
72900
72970
|
//#endregion
|
|
72901
72971
|
//#region src/ui-library-version.ts
|
|
72902
|
-
var uiLibraryVersion = "1.2.
|
|
72972
|
+
var uiLibraryVersion = "1.2.373";
|
|
72903
72973
|
//#endregion
|
|
72904
72974
|
//#region src/hooks/use-account-unit-preferences.ts
|
|
72905
72975
|
/**
|
package/dist/index.js
CHANGED
|
@@ -62149,7 +62149,7 @@ var FIXED_PAGE_SIZE = 100;
|
|
|
62149
62149
|
*/
|
|
62150
62150
|
function GenericModeList(props) {
|
|
62151
62151
|
const t = useUiText();
|
|
62152
|
-
const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize: fixedPageSize = FIXED_PAGE_SIZE, pageSizeSelector, toolbar } = props;
|
|
62152
|
+
const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize: fixedPageSize = FIXED_PAGE_SIZE, pageSizeSelector, toolbar, filters, belowFilters } = props;
|
|
62153
62153
|
const [chosenPageSize, setChosenPageSize] = usePersistedPageSize(pageSizeSelector ?? {
|
|
62154
62154
|
storageKey: null,
|
|
62155
62155
|
options: [fixedPageSize],
|
|
@@ -62217,16 +62217,22 @@ function GenericModeList(props) {
|
|
|
62217
62217
|
className: "flex flex-col gap-3 h-full min-h-0",
|
|
62218
62218
|
children: [
|
|
62219
62219
|
/* @__PURE__ */ jsxs("div", {
|
|
62220
|
-
className: "flex items-
|
|
62221
|
-
children: [
|
|
62222
|
-
|
|
62223
|
-
|
|
62224
|
-
|
|
62225
|
-
|
|
62226
|
-
|
|
62227
|
-
|
|
62220
|
+
className: "flex items-start justify-between gap-3 flex-wrap shrink-0",
|
|
62221
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
62222
|
+
className: "flex min-w-0 flex-1 flex-wrap items-start gap-2",
|
|
62223
|
+
children: [filters, getFilterType && typeOptions.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
62224
|
+
className: "flex min-w-40 flex-1",
|
|
62225
|
+
children: /* @__PURE__ */ jsx(ResponsiveTypeFilter, {
|
|
62226
|
+
label: filterLabel,
|
|
62227
|
+
options: typeOptions,
|
|
62228
|
+
selected: typeFilters,
|
|
62229
|
+
onChange: setTypeFilters
|
|
62230
|
+
})
|
|
62231
|
+
}) : null]
|
|
62232
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
62233
|
+
className: "flex items-center gap-2 max-sm:w-full",
|
|
62228
62234
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
62229
|
-
className: "relative",
|
|
62235
|
+
className: "relative max-sm:flex-1",
|
|
62230
62236
|
children: [/* @__PURE__ */ jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-foreground-subtle pointer-events-none" }), /* @__PURE__ */ jsx("input", {
|
|
62231
62237
|
ref: searchRef,
|
|
62232
62238
|
type: "text",
|
|
@@ -62234,11 +62240,12 @@ function GenericModeList(props) {
|
|
|
62234
62240
|
onChange: (e) => setSearchInput(e.target.value),
|
|
62235
62241
|
placeholder: searchPlaceholder,
|
|
62236
62242
|
"aria-label": searchPlaceholder,
|
|
62237
|
-
className: "h-7 pl-7 pr-2.5 rounded-md border border-border bg-surface text-[11px] text-foreground placeholder:text-foreground-subtle/60 focus:outline-none focus:ring-1 focus:ring-primary/40 w-56"
|
|
62243
|
+
className: "h-7 pl-7 pr-2.5 rounded-md border border-border bg-surface text-[11px] text-foreground placeholder:text-foreground-subtle/60 focus:outline-none focus:ring-1 focus:ring-primary/40 w-56 max-sm:w-full"
|
|
62238
62244
|
})]
|
|
62239
62245
|
}), typeof toolbar === "function" ? toolbar({ filtered }) : toolbar]
|
|
62240
62246
|
})]
|
|
62241
62247
|
}),
|
|
62248
|
+
typeof belowFilters === "function" ? belowFilters({ filtered }) : belowFilters,
|
|
62242
62249
|
/* @__PURE__ */ jsx("div", {
|
|
62243
62250
|
className: "flex-1 min-h-0 overflow-y-auto",
|
|
62244
62251
|
children: isLoading && totalItems === 0 ? /* @__PURE__ */ jsx("div", {
|
|
@@ -62369,6 +62376,17 @@ function DeviceList(props) {
|
|
|
62369
62376
|
}
|
|
62370
62377
|
//#endregion
|
|
62371
62378
|
//#region src/composites/export-bulk-selection.ts
|
|
62379
|
+
/** The provider filter value of a device no registered integration claims. */
|
|
62380
|
+
var UNREGISTERED_PREFIX = "addon:";
|
|
62381
|
+
/**
|
|
62382
|
+
* The provider filter value of a row: the id of the integration it belongs to,
|
|
62383
|
+
* or `addon:<addonId>` when no registered integration claims it (or the list
|
|
62384
|
+
* has not answered). The raw id is never shown — `providerFilterOptions` names it.
|
|
62385
|
+
*/
|
|
62386
|
+
function providerFilterId(device, integrations) {
|
|
62387
|
+
const resolved = deviceIntegrationResolver(integrations)(device);
|
|
62388
|
+
return resolved.kind === "resolved" ? resolved.integration.id : `${UNREGISTERED_PREFIX}${device.addonId}`;
|
|
62389
|
+
}
|
|
62372
62390
|
/** The role filter value of a row that has no role. */
|
|
62373
62391
|
var NO_ROLE_FILTER_ID = "(none)";
|
|
62374
62392
|
var NO_EXPORT_FILTERS = {
|
|
@@ -62380,24 +62398,40 @@ function roleFilterId(device) {
|
|
|
62380
62398
|
return role === null || role === void 0 || role === "" ? NO_ROLE_FILTER_ID : role;
|
|
62381
62399
|
}
|
|
62382
62400
|
/** The rows the role / provider filters leave. */
|
|
62383
|
-
function applyExportFilters(devices, filters) {
|
|
62401
|
+
function applyExportFilters(devices, filters, integrations) {
|
|
62384
62402
|
const roles = new Set(filters.roles);
|
|
62385
62403
|
const providers = new Set(filters.providers);
|
|
62386
|
-
return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(d
|
|
62404
|
+
return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(providerFilterId(d, integrations))));
|
|
62387
62405
|
}
|
|
62388
|
-
function
|
|
62389
|
-
return
|
|
62390
|
-
id,
|
|
62391
|
-
label: id
|
|
62392
|
-
}));
|
|
62406
|
+
function sortedByLabel(options) {
|
|
62407
|
+
return options.toSorted((a, b) => a.label.localeCompare(b.label));
|
|
62393
62408
|
}
|
|
62394
|
-
/**
|
|
62395
|
-
|
|
62396
|
-
|
|
62409
|
+
/**
|
|
62410
|
+
* Distinct roles present among the rows, named by `labelOf` (a roleless row is
|
|
62411
|
+
* `NO_ROLE_FILTER_ID`).
|
|
62412
|
+
*/
|
|
62413
|
+
function roleFilterOptions(devices, labelOf = (id) => id) {
|
|
62414
|
+
return sortedByLabel([...new Set(devices.map(roleFilterId))].map((id) => ({
|
|
62415
|
+
id,
|
|
62416
|
+
label: labelOf(id)
|
|
62417
|
+
})));
|
|
62397
62418
|
}
|
|
62398
|
-
/**
|
|
62399
|
-
|
|
62400
|
-
|
|
62419
|
+
/**
|
|
62420
|
+
* Distinct integrations present among the rows, by NAME. A device no
|
|
62421
|
+
* registered integration claims is listed under its addon id.
|
|
62422
|
+
*/
|
|
62423
|
+
function providerFilterOptions(devices, integrations) {
|
|
62424
|
+
const nameById = new Map((integrations ?? []).map((i) => [i.id, i.name]));
|
|
62425
|
+
const options = /* @__PURE__ */ new Map();
|
|
62426
|
+
for (const d of devices) {
|
|
62427
|
+
const id = providerFilterId(d, integrations);
|
|
62428
|
+
if (options.has(id)) continue;
|
|
62429
|
+
options.set(id, nameById.get(id) ?? d.addonId);
|
|
62430
|
+
}
|
|
62431
|
+
return sortedByLabel([...options].map(([id, label]) => ({
|
|
62432
|
+
id,
|
|
62433
|
+
label
|
|
62434
|
+
})));
|
|
62401
62435
|
}
|
|
62402
62436
|
/** `current` plus every shown row. */
|
|
62403
62437
|
function selectShown(current, shown) {
|
|
@@ -62553,9 +62587,15 @@ var DeviceItemRowSchema = z.object({
|
|
|
62553
62587
|
disabled: z.boolean(),
|
|
62554
62588
|
parentDeviceId: z.number().nullable(),
|
|
62555
62589
|
role: z.string().nullable().optional(),
|
|
62590
|
+
integrationId: z.string().nullable().optional(),
|
|
62556
62591
|
features: z.array(z.string()).optional(),
|
|
62557
62592
|
isCamera: z.boolean().optional()
|
|
62558
62593
|
});
|
|
62594
|
+
var IntegrationRowsSchema = z.array(z.object({
|
|
62595
|
+
id: z.string(),
|
|
62596
|
+
addonId: z.string(),
|
|
62597
|
+
name: z.string()
|
|
62598
|
+
}));
|
|
62559
62599
|
var SupportedKindsSchema = z.array(z.string());
|
|
62560
62600
|
var STATUS_POLL_INTERVAL_MS = 1e4;
|
|
62561
62601
|
/** Where the viewer's "rows per page" choice for this table is remembered. */
|
|
@@ -62812,6 +62852,11 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
62812
62852
|
addonId
|
|
62813
62853
|
}, { retry: false });
|
|
62814
62854
|
const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
|
|
62855
|
+
const integrationsQuery = useIntegrationsList(void 0);
|
|
62856
|
+
const integrations = useMemo(() => {
|
|
62857
|
+
const parsed = IntegrationRowsSchema.safeParse(integrationsQuery.data);
|
|
62858
|
+
return parsed.success ? parsed.data : null;
|
|
62859
|
+
}, [integrationsQuery.data]);
|
|
62815
62860
|
const entityKindsQuery = useDeviceExportListSupportedEntityKinds({
|
|
62816
62861
|
nodeId: "hub",
|
|
62817
62862
|
addonId
|
|
@@ -62929,28 +62974,28 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
62929
62974
|
const succeeded = [];
|
|
62930
62975
|
const failed = [];
|
|
62931
62976
|
const mutation = action === "expose" ? exposeMutation : unexposeMutation;
|
|
62932
|
-
(
|
|
62933
|
-
deviceId,
|
|
62934
|
-
nodeId: "hub",
|
|
62935
|
-
addonId
|
|
62936
|
-
})))).forEach((result, index) => {
|
|
62937
|
-
const deviceId = deviceIds[index] ?? "";
|
|
62977
|
+
for (const deviceId of deviceIds) {
|
|
62938
62978
|
const label = labelFor(deviceId);
|
|
62939
|
-
|
|
62979
|
+
try {
|
|
62980
|
+
await mutation.mutateAsync({
|
|
62981
|
+
deviceId,
|
|
62982
|
+
nodeId: "hub",
|
|
62983
|
+
addonId
|
|
62984
|
+
});
|
|
62940
62985
|
succeeded.push(label);
|
|
62941
|
-
|
|
62986
|
+
} catch (reason) {
|
|
62987
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
62988
|
+
console.error(`[device-export] ${action} failed`, {
|
|
62989
|
+
tags: { deviceId: Number.parseInt(deviceId, 10) },
|
|
62990
|
+
addonId,
|
|
62991
|
+
error: message
|
|
62992
|
+
});
|
|
62993
|
+
failed.push({
|
|
62994
|
+
label,
|
|
62995
|
+
message
|
|
62996
|
+
});
|
|
62942
62997
|
}
|
|
62943
|
-
|
|
62944
|
-
console.error(`[device-export] ${action} failed`, {
|
|
62945
|
-
tags: { deviceId: Number.parseInt(deviceId, 10) },
|
|
62946
|
-
addonId,
|
|
62947
|
-
error: message
|
|
62948
|
-
});
|
|
62949
|
-
failed.push({
|
|
62950
|
-
label,
|
|
62951
|
-
message
|
|
62952
|
-
});
|
|
62953
|
-
});
|
|
62998
|
+
}
|
|
62954
62999
|
setOutcome({
|
|
62955
63000
|
action,
|
|
62956
63001
|
total: deviceIds.length,
|
|
@@ -63011,9 +63056,19 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
63011
63056
|
running
|
|
63012
63057
|
]);
|
|
63013
63058
|
const [bulkFilters, setBulkFilters] = useState(NO_EXPORT_FILTERS);
|
|
63014
|
-
const roleOptions = useMemo(() => roleFilterOptions(candidateDevices),
|
|
63015
|
-
|
|
63016
|
-
|
|
63059
|
+
const roleOptions = useMemo(() => roleFilterOptions(candidateDevices, (id) => id === "(none)" ? t("uiLibrary.deviceExport.noRole", "No role") : deviceRoleMetaOf(id, t).label).map((o) => ({
|
|
63060
|
+
value: o.id,
|
|
63061
|
+
label: o.label
|
|
63062
|
+
})), [candidateDevices, t]);
|
|
63063
|
+
const providerOptions = useMemo(() => providerFilterOptions(candidateDevices, integrations).map((o) => ({
|
|
63064
|
+
value: o.id,
|
|
63065
|
+
label: o.label
|
|
63066
|
+
})), [candidateDevices, integrations]);
|
|
63067
|
+
const shownDevices = useMemo(() => applyExportFilters(candidateDevices, bulkFilters, integrations), [
|
|
63068
|
+
candidateDevices,
|
|
63069
|
+
bulkFilters,
|
|
63070
|
+
integrations
|
|
63071
|
+
]);
|
|
63017
63072
|
const shownRows = useMemo(() => {
|
|
63018
63073
|
const deviceRows = shownDevices.map((device) => ({
|
|
63019
63074
|
kind: "device",
|
|
@@ -63126,72 +63181,86 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
63126
63181
|
toggleSelection,
|
|
63127
63182
|
t
|
|
63128
63183
|
]);
|
|
63129
|
-
const
|
|
63184
|
+
const filterSelectors = /* @__PURE__ */ jsxs(Fragment$1, { children: [roleOptions.length > 1 && /* @__PURE__ */ jsx("div", {
|
|
63185
|
+
className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
|
|
63186
|
+
children: /* @__PURE__ */ jsx(OptionSelect, {
|
|
63187
|
+
mode: "multi",
|
|
63188
|
+
searchable: true,
|
|
63189
|
+
options: roleOptions,
|
|
63190
|
+
value: bulkFilters.roles,
|
|
63191
|
+
onChange: setRoleFilter,
|
|
63192
|
+
placeholder: t("uiLibrary.deviceExport.filterRole", "Role"),
|
|
63193
|
+
emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
|
|
63194
|
+
testId: "export-filter-role"
|
|
63195
|
+
})
|
|
63196
|
+
}), providerOptions.length > 1 && /* @__PURE__ */ jsx("div", {
|
|
63197
|
+
className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
|
|
63198
|
+
children: /* @__PURE__ */ jsx(OptionSelect, {
|
|
63199
|
+
mode: "multi",
|
|
63200
|
+
searchable: true,
|
|
63201
|
+
options: providerOptions,
|
|
63202
|
+
value: bulkFilters.providers,
|
|
63203
|
+
onChange: setProviderFilter,
|
|
63204
|
+
placeholder: t("uiLibrary.deviceExport.filterIntegration", "Integration"),
|
|
63205
|
+
emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
|
|
63206
|
+
testId: "export-filter-integration"
|
|
63207
|
+
})
|
|
63208
|
+
})] });
|
|
63209
|
+
const renderBulkBar = useCallback(({ filtered: filteredRows }) => {
|
|
63130
63210
|
const filtered = filteredRows.flatMap((row) => row.kind === "device" ? [row.device] : []);
|
|
63131
|
-
|
|
63132
|
-
|
|
63211
|
+
if (filtered.length === 0 && selectedIds.size === 0) return null;
|
|
63212
|
+
const hasSelection = selectedIds.size > 0;
|
|
63213
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
63214
|
+
role: "toolbar",
|
|
63215
|
+
"aria-label": t("uiLibrary.deviceExport.bulkBar", "Bulk export"),
|
|
63216
|
+
className: `flex flex-wrap items-center gap-2 rounded-md border px-2.5 py-1.5 ${hasSelection ? "border-primary/40 bg-primary/10" : "border-border bg-surface"}`,
|
|
63133
63217
|
children: [
|
|
63134
|
-
/* @__PURE__ */ jsx(
|
|
63135
|
-
|
|
63136
|
-
|
|
63137
|
-
selected:
|
|
63138
|
-
onChange: setRoleFilter
|
|
63139
|
-
}),
|
|
63140
|
-
/* @__PURE__ */ jsx(MultiSelectDropdown, {
|
|
63141
|
-
label: t("uiLibrary.deviceExport.filterProvider", "Provider"),
|
|
63142
|
-
options: providerOptions,
|
|
63143
|
-
selected: bulkFilters.providers,
|
|
63144
|
-
onChange: setProviderFilter
|
|
63218
|
+
/* @__PURE__ */ jsx("span", {
|
|
63219
|
+
...hasSelection ? { "data-testid": "export-selection-count" } : {},
|
|
63220
|
+
className: `text-[11px] font-medium ${hasSelection ? "text-foreground" : "text-foreground-subtle"}`,
|
|
63221
|
+
children: hasSelection ? t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size }) : t("uiLibrary.deviceExport.noneSelected", "None selected")
|
|
63145
63222
|
}),
|
|
63146
63223
|
/* @__PURE__ */ jsx(Button, {
|
|
63147
63224
|
size: "sm",
|
|
63148
|
-
variant: "
|
|
63225
|
+
variant: "ghost",
|
|
63149
63226
|
disabled: filtered.length === 0,
|
|
63150
63227
|
onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
|
|
63151
|
-
children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
|
|
63228
|
+
children: t("uiLibrary.deviceExport.selectShown", "Select all {{count}} shown", { count: filtered.length })
|
|
63152
63229
|
}),
|
|
63153
|
-
|
|
63230
|
+
hasSelection && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
63154
63231
|
/* @__PURE__ */ jsx(Button, {
|
|
63155
63232
|
size: "sm",
|
|
63156
63233
|
variant: "ghost",
|
|
63157
63234
|
onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
|
|
63158
63235
|
children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
|
|
63159
63236
|
}),
|
|
63160
|
-
/* @__PURE__ */ jsx("span", {
|
|
63161
|
-
"data-testid": "export-selection-count",
|
|
63162
|
-
className: "text-[11px] text-foreground-subtle",
|
|
63163
|
-
children: t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size })
|
|
63164
|
-
}),
|
|
63165
|
-
/* @__PURE__ */ jsx(Button, {
|
|
63166
|
-
size: "sm",
|
|
63167
|
-
variant: "primary",
|
|
63168
|
-
disabled: busy,
|
|
63169
|
-
onClick: exposeSelected,
|
|
63170
|
-
children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
|
|
63171
|
-
}),
|
|
63172
|
-
/* @__PURE__ */ jsx(Button, {
|
|
63173
|
-
size: "sm",
|
|
63174
|
-
variant: "secondary",
|
|
63175
|
-
disabled: busy,
|
|
63176
|
-
onClick: unexposeSelected,
|
|
63177
|
-
children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
|
|
63178
|
-
}),
|
|
63179
63237
|
/* @__PURE__ */ jsx(Button, {
|
|
63180
63238
|
size: "sm",
|
|
63181
63239
|
variant: "ghost",
|
|
63182
63240
|
onClick: clearSelection,
|
|
63183
63241
|
children: t("uiLibrary.deviceExport.clear", "Clear")
|
|
63242
|
+
}),
|
|
63243
|
+
/* @__PURE__ */ jsxs("span", {
|
|
63244
|
+
className: "ml-auto inline-flex flex-wrap items-center gap-1.5",
|
|
63245
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
63246
|
+
size: "sm",
|
|
63247
|
+
variant: "primary",
|
|
63248
|
+
disabled: busy,
|
|
63249
|
+
onClick: exposeSelected,
|
|
63250
|
+
children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
|
|
63251
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
63252
|
+
size: "sm",
|
|
63253
|
+
variant: "secondary",
|
|
63254
|
+
disabled: busy,
|
|
63255
|
+
onClick: unexposeSelected,
|
|
63256
|
+
children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
|
|
63257
|
+
})]
|
|
63184
63258
|
})
|
|
63185
63259
|
] })
|
|
63186
63260
|
]
|
|
63187
63261
|
});
|
|
63188
63262
|
}, [
|
|
63189
63263
|
t,
|
|
63190
|
-
roleOptions,
|
|
63191
|
-
providerOptions,
|
|
63192
|
-
bulkFilters,
|
|
63193
|
-
setRoleFilter,
|
|
63194
|
-
setProviderFilter,
|
|
63195
63264
|
selectedIds.size,
|
|
63196
63265
|
busy,
|
|
63197
63266
|
exposeSelected,
|
|
@@ -63246,7 +63315,8 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
63246
63315
|
filterOptions: typeFilterOptions,
|
|
63247
63316
|
columns,
|
|
63248
63317
|
onRowClick: onOpenDevice ? onRowClick : void 0,
|
|
63249
|
-
|
|
63318
|
+
filters: filterSelectors,
|
|
63319
|
+
belowFilters: renderBulkBar,
|
|
63250
63320
|
pageSizeSelector: DEVICE_EXPORT_PAGE_SIZE,
|
|
63251
63321
|
searchPlaceholder: t("uiLibrary.deviceExport.search", "Search by name or stableId"),
|
|
63252
63322
|
emptyLabel: t("uiLibrary.deviceExport.noMatch", "No device matches this search.")
|
|
@@ -72875,7 +72945,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
|
|
|
72875
72945
|
}
|
|
72876
72946
|
//#endregion
|
|
72877
72947
|
//#region src/ui-library-version.ts
|
|
72878
|
-
var uiLibraryVersion = "1.2.
|
|
72948
|
+
var uiLibraryVersion = "1.2.373";
|
|
72879
72949
|
//#endregion
|
|
72880
72950
|
//#region src/hooks/use-account-unit-preferences.ts
|
|
72881
72951
|
/**
|