@camstack/ui-library 1.2.372 → 1.2.374
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 +13 -0
- package/dist/composites/device-list/generic-mode.d.ts +21 -0
- package/dist/composites/export-bulk-selection.d.ts +24 -5
- package/dist/generated/system-hooks.d.ts +2 -0
- package/dist/index.cjs +227 -94
- package/dist/index.js +227 -95
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.374] - 2026-11-02
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- A select that discovers its options can be an optional picker ('Default' choice, 'missing' label for a gone value); the device export table's Type filter is searchable like Role and Integration.
|
|
15
|
+
- A searchable select no longer throws after it closes on a page that was just left.
|
|
16
|
+
|
|
17
|
+
## [1.2.373] - 2026-11-02
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- 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
|
|
22
|
+
|
|
10
23
|
## [1.2.372] - 2026-11-02
|
|
11
24
|
|
|
12
25
|
### Changed
|
|
@@ -71,6 +71,14 @@ export interface DeviceListGenericProps<T> {
|
|
|
71
71
|
/** Label for the type multi-select trigger. Default 'Type'. */
|
|
72
72
|
readonly filterLabel?: string;
|
|
73
73
|
/**
|
|
74
|
+
* Render the type filter as the library's searchable multi `OptionSelect`
|
|
75
|
+
* (typing narrows the list), sized like a sibling selector in `filters`,
|
|
76
|
+
* instead of the chips / compact dropdown. Default false.
|
|
77
|
+
*/
|
|
78
|
+
readonly searchableTypeFilter?: boolean;
|
|
79
|
+
/** `data-testid` of the searchable type filter. Default `device-list-filter-type`. */
|
|
80
|
+
readonly typeFilterTestId?: string;
|
|
81
|
+
/**
|
|
74
82
|
* Initial type-filter selection. Empty (default) = no filter. Used once at
|
|
75
83
|
* mount — later changes to this prop are ignored (local state owns it).
|
|
76
84
|
*/
|
|
@@ -94,6 +102,19 @@ export interface DeviceListGenericProps<T> {
|
|
|
94
102
|
readonly toolbar?: ReactNode | ((ctx: {
|
|
95
103
|
readonly filtered: readonly T[];
|
|
96
104
|
}) => ReactNode);
|
|
105
|
+
/**
|
|
106
|
+
* Extra selectors rendered on the LEFT, before the type filter, so every
|
|
107
|
+
* selector sits in one group and the search box stays alone on the right.
|
|
108
|
+
* Wraps onto their own row on a narrow screen.
|
|
109
|
+
*/
|
|
110
|
+
readonly filters?: ReactNode;
|
|
111
|
+
/**
|
|
112
|
+
* Full-width row between the filter row and the table (a bulk-action bar).
|
|
113
|
+
* A function receives the rows the search and filters currently leave.
|
|
114
|
+
*/
|
|
115
|
+
readonly belowFilters?: ReactNode | ((ctx: {
|
|
116
|
+
readonly filtered: readonly T[];
|
|
117
|
+
}) => ReactNode);
|
|
97
118
|
}
|
|
98
119
|
/**
|
|
99
120
|
* 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. */
|
|
@@ -953,6 +953,8 @@ export declare const useIntegrationInstancesGetInstance: typeof trpc.integration
|
|
|
953
953
|
export declare const useIntegrationInstancesCreateInstance: typeof trpc.integrationInstances.createInstance.useMutation;
|
|
954
954
|
/** Generated alias around `trpc.integrationInstances.updateInstance.useMutation`. */
|
|
955
955
|
export declare const useIntegrationInstancesUpdateInstance: typeof trpc.integrationInstances.updateInstance.useMutation;
|
|
956
|
+
/** Generated alias around `trpc.integrationInstances.setInstanceEnabled.useMutation`. */
|
|
957
|
+
export declare const useIntegrationInstancesSetInstanceEnabled: typeof trpc.integrationInstances.setInstanceEnabled.useMutation;
|
|
956
958
|
/** Generated alias around `trpc.integrationInstances.deleteInstance.useMutation`. */
|
|
957
959
|
export declare const useIntegrationInstancesDeleteInstance: typeof trpc.integrationInstances.deleteInstance.useMutation;
|
|
958
960
|
/** Generated alias around `trpc.integrationInstances.testInstance.useMutation`. */
|
package/dist/index.cjs
CHANGED
|
@@ -11668,6 +11668,8 @@ var useIntegrationInstancesGetInstance = /*#__PURE__*/ alias(() => trpc.integrat
|
|
|
11668
11668
|
var useIntegrationInstancesCreateInstance = /*#__PURE__*/ alias(() => trpc.integrationInstances.createInstance.useMutation);
|
|
11669
11669
|
/** Generated alias around `trpc.integrationInstances.updateInstance.useMutation`. */
|
|
11670
11670
|
var useIntegrationInstancesUpdateInstance = /*#__PURE__*/ alias(() => trpc.integrationInstances.updateInstance.useMutation);
|
|
11671
|
+
/** Generated alias around `trpc.integrationInstances.setInstanceEnabled.useMutation`. */
|
|
11672
|
+
var useIntegrationInstancesSetInstanceEnabled = /*#__PURE__*/ alias(() => trpc.integrationInstances.setInstanceEnabled.useMutation);
|
|
11671
11673
|
/** Generated alias around `trpc.integrationInstances.deleteInstance.useMutation`. */
|
|
11672
11674
|
var useIntegrationInstancesDeleteInstance = /*#__PURE__*/ alias(() => trpc.integrationInstances.deleteInstance.useMutation);
|
|
11673
11675
|
/** Generated alias around `trpc.integrationInstances.testInstance.useMutation`. */
|
|
@@ -14365,6 +14367,9 @@ function OptionSelect(props) {
|
|
|
14365
14367
|
const triggerRef = (0, react$1.useRef)(null);
|
|
14366
14368
|
const listboxRef = (0, react$1.useRef)(null);
|
|
14367
14369
|
const blurTimer = (0, react$1.useRef)(null);
|
|
14370
|
+
(0, react$1.useEffect)(() => () => {
|
|
14371
|
+
if (blurTimer.current) clearTimeout(blurTimer.current);
|
|
14372
|
+
}, []);
|
|
14368
14373
|
const computeListboxCoords = () => {
|
|
14369
14374
|
const trigger = triggerRef.current;
|
|
14370
14375
|
if (trigger === null) return null;
|
|
@@ -32221,6 +32226,42 @@ function ConfigDiscoveredSelectControl({ field, values, value, onChange, disable
|
|
|
32221
32226
|
const { ready, settled, query, found } = useDiscoveredOptions(field, values);
|
|
32222
32227
|
const current = value === void 0 || value === null ? "" : String(value);
|
|
32223
32228
|
const disabledNow = disabled || field.disabled;
|
|
32229
|
+
const noneLabel = field.discoverOptions?.noneLabel;
|
|
32230
|
+
const answered = ready && settled && !query.isLoading && !query.isFetching && !query.isError && query.data !== void 0;
|
|
32231
|
+
if (noneLabel !== void 0) {
|
|
32232
|
+
const known = current === "" || found.some((option) => option.value === current);
|
|
32233
|
+
const missingLabel = field.discoverOptions?.missingLabel;
|
|
32234
|
+
const staleLabel = answered && missingLabel !== void 0 ? `${missingLabel} ${current}` : current;
|
|
32235
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32236
|
+
className: "space-y-1",
|
|
32237
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
|
|
32238
|
+
mode: "single",
|
|
32239
|
+
singleDisplay: "field",
|
|
32240
|
+
searchable: true,
|
|
32241
|
+
disabled: disabledNow,
|
|
32242
|
+
value: current,
|
|
32243
|
+
onChange,
|
|
32244
|
+
placeholder: noneLabel,
|
|
32245
|
+
options: [
|
|
32246
|
+
{
|
|
32247
|
+
value: "",
|
|
32248
|
+
label: noneLabel
|
|
32249
|
+
},
|
|
32250
|
+
...known ? [] : [{
|
|
32251
|
+
value: current,
|
|
32252
|
+
label: staleLabel
|
|
32253
|
+
}],
|
|
32254
|
+
...found
|
|
32255
|
+
],
|
|
32256
|
+
testId: controlId,
|
|
32257
|
+
optionTestIdPrefix: `${controlId}-option`
|
|
32258
|
+
}), answered ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
32259
|
+
"data-testid": `${controlId}-status`,
|
|
32260
|
+
className: `text-[11px] ${query.isError ? "text-danger" : "text-foreground-subtle"}`,
|
|
32261
|
+
children: discoveryStatus(t, ready, settled, query)
|
|
32262
|
+
})]
|
|
32263
|
+
});
|
|
32264
|
+
}
|
|
32224
32265
|
if (found.length > 0) {
|
|
32225
32266
|
const known = found.some((option) => option.value === current);
|
|
32226
32267
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
|
|
@@ -32239,11 +32280,7 @@ function ConfigDiscoveredSelectControl({ field, values, value, onChange, disable
|
|
|
32239
32280
|
optionTestIdPrefix: `${controlId}-option`
|
|
32240
32281
|
});
|
|
32241
32282
|
}
|
|
32242
|
-
|
|
32243
|
-
if (!ready) status = t("uiLibrary.configFormField.discoverNotReady", "Fill in the fields above to load the list, or type the value.");
|
|
32244
|
-
else if (!settled || query.isLoading || query.isFetching) status = t("uiLibrary.configFormField.loadingOptions", "Loading options…");
|
|
32245
|
-
else if (query.isError) status = t("uiLibrary.configFormField.discoverFailed", "Could not load the list: {{message}}. Type the value instead.", { message: query.error instanceof Error ? query.error.message : String(query.error) });
|
|
32246
|
-
else status = t("uiLibrary.configFormField.discoverEmpty", "Nothing was found. Type the value instead.");
|
|
32283
|
+
const status = discoveryStatus(t, ready, settled, query);
|
|
32247
32284
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32248
32285
|
className: "space-y-1",
|
|
32249
32286
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
@@ -32259,6 +32296,13 @@ function ConfigDiscoveredSelectControl({ field, values, value, onChange, disable
|
|
|
32259
32296
|
})]
|
|
32260
32297
|
});
|
|
32261
32298
|
}
|
|
32299
|
+
/** The one line that says why the list is not (yet) offered. */
|
|
32300
|
+
function discoveryStatus(t, ready, settled, query) {
|
|
32301
|
+
if (!ready) return t("uiLibrary.configFormField.discoverNotReady", "Fill in the fields above to load the list, or type the value.");
|
|
32302
|
+
if (!settled || query.isLoading || query.isFetching) return t("uiLibrary.configFormField.loadingOptions", "Loading options…");
|
|
32303
|
+
if (query.isError) return t("uiLibrary.configFormField.discoverFailed", "Could not load the list: {{message}}. Type the value instead.", { message: query.error instanceof Error ? query.error.message : String(query.error) });
|
|
32304
|
+
return t("uiLibrary.configFormField.discoverEmpty", "Nothing was found. Type the value instead.");
|
|
32305
|
+
}
|
|
32262
32306
|
//#endregion
|
|
32263
32307
|
//#region src/composites/config-form-field.tsx
|
|
32264
32308
|
var INPUT_CLASS = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
|
|
@@ -62173,7 +62217,7 @@ var FIXED_PAGE_SIZE = 100;
|
|
|
62173
62217
|
*/
|
|
62174
62218
|
function GenericModeList(props) {
|
|
62175
62219
|
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;
|
|
62220
|
+
const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", searchableTypeFilter = false, typeFilterTestId = "device-list-filter-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
62221
|
const [chosenPageSize, setChosenPageSize] = usePersistedPageSize(pageSizeSelector ?? {
|
|
62178
62222
|
storageKey: null,
|
|
62179
62223
|
options: [fixedPageSize],
|
|
@@ -62241,16 +62285,37 @@ function GenericModeList(props) {
|
|
|
62241
62285
|
className: "flex flex-col gap-3 h-full min-h-0",
|
|
62242
62286
|
children: [
|
|
62243
62287
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
62244
|
-
className: "flex items-
|
|
62245
|
-
children: [
|
|
62246
|
-
|
|
62247
|
-
|
|
62248
|
-
|
|
62249
|
-
|
|
62250
|
-
|
|
62251
|
-
|
|
62288
|
+
className: "flex items-start justify-between gap-3 flex-wrap shrink-0",
|
|
62289
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
62290
|
+
className: "flex min-w-0 flex-1 flex-wrap items-start gap-2",
|
|
62291
|
+
children: [filters, getFilterType && typeOptions.length > 0 && searchableTypeFilter ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
62292
|
+
className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
|
|
62293
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
|
|
62294
|
+
mode: "multi",
|
|
62295
|
+
searchable: true,
|
|
62296
|
+
options: typeOptions.map((o) => ({
|
|
62297
|
+
value: o.id,
|
|
62298
|
+
label: o.label
|
|
62299
|
+
})),
|
|
62300
|
+
value: typeFilters,
|
|
62301
|
+
onChange: setTypeFilters,
|
|
62302
|
+
placeholder: filterLabel,
|
|
62303
|
+
emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
|
|
62304
|
+
testId: typeFilterTestId
|
|
62305
|
+
})
|
|
62306
|
+
}) : getFilterType && typeOptions.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
62307
|
+
className: "flex min-w-40 flex-1",
|
|
62308
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResponsiveTypeFilter, {
|
|
62309
|
+
label: filterLabel,
|
|
62310
|
+
options: typeOptions,
|
|
62311
|
+
selected: typeFilters,
|
|
62312
|
+
onChange: setTypeFilters
|
|
62313
|
+
})
|
|
62314
|
+
}) : null]
|
|
62315
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
62316
|
+
className: "flex items-center gap-2 max-sm:w-full",
|
|
62252
62317
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
62253
|
-
className: "relative",
|
|
62318
|
+
className: "relative max-sm:flex-1",
|
|
62254
62319
|
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
62320
|
ref: searchRef,
|
|
62256
62321
|
type: "text",
|
|
@@ -62258,11 +62323,12 @@ function GenericModeList(props) {
|
|
|
62258
62323
|
onChange: (e) => setSearchInput(e.target.value),
|
|
62259
62324
|
placeholder: searchPlaceholder,
|
|
62260
62325
|
"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"
|
|
62326
|
+
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
62327
|
})]
|
|
62263
62328
|
}), typeof toolbar === "function" ? toolbar({ filtered }) : toolbar]
|
|
62264
62329
|
})]
|
|
62265
62330
|
}),
|
|
62331
|
+
typeof belowFilters === "function" ? belowFilters({ filtered }) : belowFilters,
|
|
62266
62332
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
62267
62333
|
className: "flex-1 min-h-0 overflow-y-auto",
|
|
62268
62334
|
children: isLoading && totalItems === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -62393,6 +62459,17 @@ function DeviceList(props) {
|
|
|
62393
62459
|
}
|
|
62394
62460
|
//#endregion
|
|
62395
62461
|
//#region src/composites/export-bulk-selection.ts
|
|
62462
|
+
/** The provider filter value of a device no registered integration claims. */
|
|
62463
|
+
var UNREGISTERED_PREFIX = "addon:";
|
|
62464
|
+
/**
|
|
62465
|
+
* The provider filter value of a row: the id of the integration it belongs to,
|
|
62466
|
+
* or `addon:<addonId>` when no registered integration claims it (or the list
|
|
62467
|
+
* has not answered). The raw id is never shown — `providerFilterOptions` names it.
|
|
62468
|
+
*/
|
|
62469
|
+
function providerFilterId(device, integrations) {
|
|
62470
|
+
const resolved = deviceIntegrationResolver(integrations)(device);
|
|
62471
|
+
return resolved.kind === "resolved" ? resolved.integration.id : `${UNREGISTERED_PREFIX}${device.addonId}`;
|
|
62472
|
+
}
|
|
62396
62473
|
/** The role filter value of a row that has no role. */
|
|
62397
62474
|
var NO_ROLE_FILTER_ID = "(none)";
|
|
62398
62475
|
var NO_EXPORT_FILTERS = {
|
|
@@ -62404,24 +62481,40 @@ function roleFilterId(device) {
|
|
|
62404
62481
|
return role === null || role === void 0 || role === "" ? NO_ROLE_FILTER_ID : role;
|
|
62405
62482
|
}
|
|
62406
62483
|
/** The rows the role / provider filters leave. */
|
|
62407
|
-
function applyExportFilters(devices, filters) {
|
|
62484
|
+
function applyExportFilters(devices, filters, integrations) {
|
|
62408
62485
|
const roles = new Set(filters.roles);
|
|
62409
62486
|
const providers = new Set(filters.providers);
|
|
62410
|
-
return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(d
|
|
62487
|
+
return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(providerFilterId(d, integrations))));
|
|
62411
62488
|
}
|
|
62412
|
-
function
|
|
62413
|
-
return
|
|
62414
|
-
id,
|
|
62415
|
-
label: id
|
|
62416
|
-
}));
|
|
62489
|
+
function sortedByLabel(options) {
|
|
62490
|
+
return options.toSorted((a, b) => a.label.localeCompare(b.label));
|
|
62417
62491
|
}
|
|
62418
|
-
/**
|
|
62419
|
-
|
|
62420
|
-
|
|
62492
|
+
/**
|
|
62493
|
+
* Distinct roles present among the rows, named by `labelOf` (a roleless row is
|
|
62494
|
+
* `NO_ROLE_FILTER_ID`).
|
|
62495
|
+
*/
|
|
62496
|
+
function roleFilterOptions(devices, labelOf = (id) => id) {
|
|
62497
|
+
return sortedByLabel([...new Set(devices.map(roleFilterId))].map((id) => ({
|
|
62498
|
+
id,
|
|
62499
|
+
label: labelOf(id)
|
|
62500
|
+
})));
|
|
62421
62501
|
}
|
|
62422
|
-
/**
|
|
62423
|
-
|
|
62424
|
-
|
|
62502
|
+
/**
|
|
62503
|
+
* Distinct integrations present among the rows, by NAME. A device no
|
|
62504
|
+
* registered integration claims is listed under its addon id.
|
|
62505
|
+
*/
|
|
62506
|
+
function providerFilterOptions(devices, integrations) {
|
|
62507
|
+
const nameById = new Map((integrations ?? []).map((i) => [i.id, i.name]));
|
|
62508
|
+
const options = /* @__PURE__ */ new Map();
|
|
62509
|
+
for (const d of devices) {
|
|
62510
|
+
const id = providerFilterId(d, integrations);
|
|
62511
|
+
if (options.has(id)) continue;
|
|
62512
|
+
options.set(id, nameById.get(id) ?? d.addonId);
|
|
62513
|
+
}
|
|
62514
|
+
return sortedByLabel([...options].map(([id, label]) => ({
|
|
62515
|
+
id,
|
|
62516
|
+
label
|
|
62517
|
+
})));
|
|
62425
62518
|
}
|
|
62426
62519
|
/** `current` plus every shown row. */
|
|
62427
62520
|
function selectShown(current, shown) {
|
|
@@ -62577,9 +62670,15 @@ var DeviceItemRowSchema = zod.z.object({
|
|
|
62577
62670
|
disabled: zod.z.boolean(),
|
|
62578
62671
|
parentDeviceId: zod.z.number().nullable(),
|
|
62579
62672
|
role: zod.z.string().nullable().optional(),
|
|
62673
|
+
integrationId: zod.z.string().nullable().optional(),
|
|
62580
62674
|
features: zod.z.array(zod.z.string()).optional(),
|
|
62581
62675
|
isCamera: zod.z.boolean().optional()
|
|
62582
62676
|
});
|
|
62677
|
+
var IntegrationRowsSchema = zod.z.array(zod.z.object({
|
|
62678
|
+
id: zod.z.string(),
|
|
62679
|
+
addonId: zod.z.string(),
|
|
62680
|
+
name: zod.z.string()
|
|
62681
|
+
}));
|
|
62583
62682
|
var SupportedKindsSchema = zod.z.array(zod.z.string());
|
|
62584
62683
|
var STATUS_POLL_INTERVAL_MS = 1e4;
|
|
62585
62684
|
/** Where the viewer's "rows per page" choice for this table is remembered. */
|
|
@@ -62836,6 +62935,11 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
62836
62935
|
addonId
|
|
62837
62936
|
}, { retry: false });
|
|
62838
62937
|
const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
|
|
62938
|
+
const integrationsQuery = useIntegrationsList(void 0);
|
|
62939
|
+
const integrations = (0, react$1.useMemo)(() => {
|
|
62940
|
+
const parsed = IntegrationRowsSchema.safeParse(integrationsQuery.data);
|
|
62941
|
+
return parsed.success ? parsed.data : null;
|
|
62942
|
+
}, [integrationsQuery.data]);
|
|
62839
62943
|
const entityKindsQuery = useDeviceExportListSupportedEntityKinds({
|
|
62840
62944
|
nodeId: "hub",
|
|
62841
62945
|
addonId
|
|
@@ -62953,28 +63057,28 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
62953
63057
|
const succeeded = [];
|
|
62954
63058
|
const failed = [];
|
|
62955
63059
|
const mutation = action === "expose" ? exposeMutation : unexposeMutation;
|
|
62956
|
-
(
|
|
62957
|
-
deviceId,
|
|
62958
|
-
nodeId: "hub",
|
|
62959
|
-
addonId
|
|
62960
|
-
})))).forEach((result, index) => {
|
|
62961
|
-
const deviceId = deviceIds[index] ?? "";
|
|
63060
|
+
for (const deviceId of deviceIds) {
|
|
62962
63061
|
const label = labelFor(deviceId);
|
|
62963
|
-
|
|
63062
|
+
try {
|
|
63063
|
+
await mutation.mutateAsync({
|
|
63064
|
+
deviceId,
|
|
63065
|
+
nodeId: "hub",
|
|
63066
|
+
addonId
|
|
63067
|
+
});
|
|
62964
63068
|
succeeded.push(label);
|
|
62965
|
-
|
|
63069
|
+
} catch (reason) {
|
|
63070
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
63071
|
+
console.error(`[device-export] ${action} failed`, {
|
|
63072
|
+
tags: { deviceId: Number.parseInt(deviceId, 10) },
|
|
63073
|
+
addonId,
|
|
63074
|
+
error: message
|
|
63075
|
+
});
|
|
63076
|
+
failed.push({
|
|
63077
|
+
label,
|
|
63078
|
+
message
|
|
63079
|
+
});
|
|
62966
63080
|
}
|
|
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
|
-
});
|
|
63081
|
+
}
|
|
62978
63082
|
setOutcome({
|
|
62979
63083
|
action,
|
|
62980
63084
|
total: deviceIds.length,
|
|
@@ -63035,9 +63139,19 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
63035
63139
|
running
|
|
63036
63140
|
]);
|
|
63037
63141
|
const [bulkFilters, setBulkFilters] = (0, react$1.useState)(NO_EXPORT_FILTERS);
|
|
63038
|
-
const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices),
|
|
63039
|
-
|
|
63040
|
-
|
|
63142
|
+
const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices, (id) => id === "(none)" ? t("uiLibrary.deviceExport.noRole", "No role") : deviceRoleMetaOf(id, t).label).map((o) => ({
|
|
63143
|
+
value: o.id,
|
|
63144
|
+
label: o.label
|
|
63145
|
+
})), [candidateDevices, t]);
|
|
63146
|
+
const providerOptions = (0, react$1.useMemo)(() => providerFilterOptions(candidateDevices, integrations).map((o) => ({
|
|
63147
|
+
value: o.id,
|
|
63148
|
+
label: o.label
|
|
63149
|
+
})), [candidateDevices, integrations]);
|
|
63150
|
+
const shownDevices = (0, react$1.useMemo)(() => applyExportFilters(candidateDevices, bulkFilters, integrations), [
|
|
63151
|
+
candidateDevices,
|
|
63152
|
+
bulkFilters,
|
|
63153
|
+
integrations
|
|
63154
|
+
]);
|
|
63041
63155
|
const shownRows = (0, react$1.useMemo)(() => {
|
|
63042
63156
|
const deviceRows = shownDevices.map((device) => ({
|
|
63043
63157
|
kind: "device",
|
|
@@ -63150,72 +63264,86 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
63150
63264
|
toggleSelection,
|
|
63151
63265
|
t
|
|
63152
63266
|
]);
|
|
63153
|
-
const
|
|
63267
|
+
const filterSelectors = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [roleOptions.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
63268
|
+
className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
|
|
63269
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
|
|
63270
|
+
mode: "multi",
|
|
63271
|
+
searchable: true,
|
|
63272
|
+
options: roleOptions,
|
|
63273
|
+
value: bulkFilters.roles,
|
|
63274
|
+
onChange: setRoleFilter,
|
|
63275
|
+
placeholder: t("uiLibrary.deviceExport.filterRole", "Role"),
|
|
63276
|
+
emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
|
|
63277
|
+
testId: "export-filter-role"
|
|
63278
|
+
})
|
|
63279
|
+
}), providerOptions.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
63280
|
+
className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
|
|
63281
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
|
|
63282
|
+
mode: "multi",
|
|
63283
|
+
searchable: true,
|
|
63284
|
+
options: providerOptions,
|
|
63285
|
+
value: bulkFilters.providers,
|
|
63286
|
+
onChange: setProviderFilter,
|
|
63287
|
+
placeholder: t("uiLibrary.deviceExport.filterIntegration", "Integration"),
|
|
63288
|
+
emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
|
|
63289
|
+
testId: "export-filter-integration"
|
|
63290
|
+
})
|
|
63291
|
+
})] });
|
|
63292
|
+
const renderBulkBar = (0, react$1.useCallback)(({ filtered: filteredRows }) => {
|
|
63154
63293
|
const filtered = filteredRows.flatMap((row) => row.kind === "device" ? [row.device] : []);
|
|
63155
|
-
|
|
63156
|
-
|
|
63294
|
+
if (filtered.length === 0 && selectedIds.size === 0) return null;
|
|
63295
|
+
const hasSelection = selectedIds.size > 0;
|
|
63296
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
63297
|
+
role: "toolbar",
|
|
63298
|
+
"aria-label": t("uiLibrary.deviceExport.bulkBar", "Bulk export"),
|
|
63299
|
+
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
63300
|
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
|
|
63301
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
63302
|
+
...hasSelection ? { "data-testid": "export-selection-count" } : {},
|
|
63303
|
+
className: `text-[11px] font-medium ${hasSelection ? "text-foreground" : "text-foreground-subtle"}`,
|
|
63304
|
+
children: hasSelection ? t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size }) : t("uiLibrary.deviceExport.noneSelected", "None selected")
|
|
63169
63305
|
}),
|
|
63170
63306
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63171
63307
|
size: "sm",
|
|
63172
|
-
variant: "
|
|
63308
|
+
variant: "ghost",
|
|
63173
63309
|
disabled: filtered.length === 0,
|
|
63174
63310
|
onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
|
|
63175
|
-
children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
|
|
63311
|
+
children: t("uiLibrary.deviceExport.selectShown", "Select all {{count}} shown", { count: filtered.length })
|
|
63176
63312
|
}),
|
|
63177
|
-
|
|
63313
|
+
hasSelection && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
63178
63314
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63179
63315
|
size: "sm",
|
|
63180
63316
|
variant: "ghost",
|
|
63181
63317
|
onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
|
|
63182
63318
|
children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
|
|
63183
63319
|
}),
|
|
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
63320
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63204
63321
|
size: "sm",
|
|
63205
63322
|
variant: "ghost",
|
|
63206
63323
|
onClick: clearSelection,
|
|
63207
63324
|
children: t("uiLibrary.deviceExport.clear", "Clear")
|
|
63325
|
+
}),
|
|
63326
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
63327
|
+
className: "ml-auto inline-flex flex-wrap items-center gap-1.5",
|
|
63328
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63329
|
+
size: "sm",
|
|
63330
|
+
variant: "primary",
|
|
63331
|
+
disabled: busy,
|
|
63332
|
+
onClick: exposeSelected,
|
|
63333
|
+
children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
|
|
63334
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
63335
|
+
size: "sm",
|
|
63336
|
+
variant: "secondary",
|
|
63337
|
+
disabled: busy,
|
|
63338
|
+
onClick: unexposeSelected,
|
|
63339
|
+
children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
|
|
63340
|
+
})]
|
|
63208
63341
|
})
|
|
63209
63342
|
] })
|
|
63210
63343
|
]
|
|
63211
63344
|
});
|
|
63212
63345
|
}, [
|
|
63213
63346
|
t,
|
|
63214
|
-
roleOptions,
|
|
63215
|
-
providerOptions,
|
|
63216
|
-
bulkFilters,
|
|
63217
|
-
setRoleFilter,
|
|
63218
|
-
setProviderFilter,
|
|
63219
63347
|
selectedIds.size,
|
|
63220
63348
|
busy,
|
|
63221
63349
|
exposeSelected,
|
|
@@ -63268,9 +63396,13 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
63268
63396
|
getSearchText,
|
|
63269
63397
|
getFilterType,
|
|
63270
63398
|
filterOptions: typeFilterOptions,
|
|
63399
|
+
filterLabel: t("uiLibrary.deviceExport.filterType", "Type"),
|
|
63400
|
+
searchableTypeFilter: true,
|
|
63401
|
+
typeFilterTestId: "export-filter-type",
|
|
63271
63402
|
columns,
|
|
63272
63403
|
onRowClick: onOpenDevice ? onRowClick : void 0,
|
|
63273
|
-
|
|
63404
|
+
filters: filterSelectors,
|
|
63405
|
+
belowFilters: renderBulkBar,
|
|
63274
63406
|
pageSizeSelector: DEVICE_EXPORT_PAGE_SIZE,
|
|
63275
63407
|
searchPlaceholder: t("uiLibrary.deviceExport.search", "Search by name or stableId"),
|
|
63276
63408
|
emptyLabel: t("uiLibrary.deviceExport.noMatch", "No device matches this search.")
|
|
@@ -72899,7 +73031,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
|
|
|
72899
73031
|
}
|
|
72900
73032
|
//#endregion
|
|
72901
73033
|
//#region src/ui-library-version.ts
|
|
72902
|
-
var uiLibraryVersion = "1.2.
|
|
73034
|
+
var uiLibraryVersion = "1.2.374";
|
|
72903
73035
|
//#endregion
|
|
72904
73036
|
//#region src/hooks/use-account-unit-preferences.ts
|
|
72905
73037
|
/**
|
|
@@ -75369,6 +75501,7 @@ exports.useIntegrationInstancesDescribe = useIntegrationInstancesDescribe;
|
|
|
75369
75501
|
exports.useIntegrationInstancesGetInstance = useIntegrationInstancesGetInstance;
|
|
75370
75502
|
exports.useIntegrationInstancesInstanceFormSchema = useIntegrationInstancesInstanceFormSchema;
|
|
75371
75503
|
exports.useIntegrationInstancesListInstances = useIntegrationInstancesListInstances;
|
|
75504
|
+
exports.useIntegrationInstancesSetInstanceEnabled = useIntegrationInstancesSetInstanceEnabled;
|
|
75372
75505
|
exports.useIntegrationInstancesTestInstance = useIntegrationInstancesTestInstance;
|
|
75373
75506
|
exports.useIntegrationInstancesUpdateInstance = useIntegrationInstancesUpdateInstance;
|
|
75374
75507
|
exports.useIntegrationsCreate = useIntegrationsCreate;
|