@camstack/ui-library 1.2.222 → 1.2.224
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/composites/device-picker/DevicePicker.d.ts +35 -0
- package/dist/composites/device-picker/SelectedChip.d.ts +12 -0
- package/dist/composites/device-picker/device-filter-bar.d.ts +25 -0
- package/dist/composites/device-picker/device-fleet-view.d.ts +7 -0
- package/dist/composites/device-picker/device-picker-grouping.d.ts +14 -0
- package/dist/composites/device-picker/device-picker-model.d.ts +245 -0
- package/dist/composites/device-picker/device-selection-rows.d.ts +40 -0
- package/dist/composites/device-picker/device-selection.d.ts +64 -0
- package/dist/composites/device-picker/index.d.ts +8 -0
- package/dist/composites/index.d.ts +3 -0
- package/dist/composites/placeholder-field/PlaceholderChipField.d.ts +29 -0
- package/dist/composites/placeholder-field/index.d.ts +1 -0
- package/dist/composites/unit-picker/UnitPicker.d.ts +16 -0
- package/dist/composites/unit-picker/UnitPicker.spec.d.ts +1 -0
- package/dist/composites/unit-picker/index.d.ts +1 -0
- package/dist/generated/system-hooks.d.ts +12 -0
- package/dist/index.cjs +1450 -14
- package/dist/index.js +1388 -16
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6758,6 +6758,12 @@ var useBrokerGetState = trpc.broker.getState.useQuery;
|
|
|
6758
6758
|
var useBrokerGetStatus = trpc.broker.getStatus.useQuery;
|
|
6759
6759
|
/** Generated alias around `trpc.button.press.useMutation`. */
|
|
6760
6760
|
var useButtonPress = trpc.button.press.useMutation;
|
|
6761
|
+
/** Generated alias around `trpc.calendars.listCalendars.useQuery`. */
|
|
6762
|
+
var useCalendarsListCalendars = trpc.calendars.listCalendars.useQuery;
|
|
6763
|
+
/** Generated alias around `trpc.calendars.listEvents.useQuery`. */
|
|
6764
|
+
var useCalendarsListEvents = trpc.calendars.listEvents.useQuery;
|
|
6765
|
+
/** Generated alias around `trpc.calendars.importFromHomeAssistant.useMutation`. */
|
|
6766
|
+
var useCalendarsImportFromHomeAssistant = trpc.calendars.importFromHomeAssistant.useMutation;
|
|
6761
6767
|
/** Generated alias around `trpc.cameraCredentials.getCredentials.useQuery`. */
|
|
6762
6768
|
var useCameraCredentialsGetCredentials = trpc.cameraCredentials.getCredentials.useQuery;
|
|
6763
6769
|
/** Generated alias around `trpc.cameraCredentials.getStatus.useQuery`. */
|
|
@@ -7670,6 +7676,12 @@ var useOsdManagerCopyDeviceConfiguration = trpc.osdManager.copyDeviceConfigurati
|
|
|
7670
7676
|
var useOsdManagerPreviewSlot = trpc.osdManager.previewSlot.useMutation;
|
|
7671
7677
|
/** Generated alias around `trpc.osdManager.renderDevice.useMutation`. */
|
|
7672
7678
|
var useOsdManagerRenderDevice = trpc.osdManager.renderDevice.useMutation;
|
|
7679
|
+
/** Generated alias around `trpc.osdManager.listTemplates.useQuery`. */
|
|
7680
|
+
var useOsdManagerListTemplates = trpc.osdManager.listTemplates.useQuery;
|
|
7681
|
+
/** Generated alias around `trpc.osdManager.upsertTemplate.useMutation`. */
|
|
7682
|
+
var useOsdManagerUpsertTemplate = trpc.osdManager.upsertTemplate.useMutation;
|
|
7683
|
+
/** Generated alias around `trpc.osdManager.deleteTemplate.useMutation`. */
|
|
7684
|
+
var useOsdManagerDeleteTemplate = trpc.osdManager.deleteTemplate.useMutation;
|
|
7673
7685
|
/** Generated alias around `trpc.petFeeder.feed.useMutation`. */
|
|
7674
7686
|
var usePetFeederFeed = trpc.petFeeder.feed.useMutation;
|
|
7675
7687
|
/** Generated alias around `trpc.petFeeder.cancelFeed.useMutation`. */
|
|
@@ -12355,8 +12367,8 @@ function selectedDeviceOptions(options, selected) {
|
|
|
12355
12367
|
* renders the shared form field gets them without extra registration.
|
|
12356
12368
|
*/
|
|
12357
12369
|
var INPUT_CLASS$1 = "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";
|
|
12358
|
-
var INPUT_SHELL = "flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30";
|
|
12359
|
-
var LABEL_CLASS$
|
|
12370
|
+
var INPUT_SHELL$1 = "flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30";
|
|
12371
|
+
var LABEL_CLASS$2 = "block text-[11px] font-medium text-foreground mb-1 break-words";
|
|
12360
12372
|
var DESC_CLASS$1 = "text-[10px] text-foreground-subtle mt-0.5 break-words";
|
|
12361
12373
|
/** The compact projection of the listAll rows the device fields consume. */
|
|
12362
12374
|
function toCandidates(rows) {
|
|
@@ -12393,7 +12405,7 @@ var FACET_ITEM = "flex h-7 w-full items-center gap-2 px-2 text-left text-xs text
|
|
|
12393
12405
|
* The operator asked for dropdowns over rows of chips (2026-09-05): with
|
|
12394
12406
|
* sixteen rooms and a dozen types the chips ate the panel.
|
|
12395
12407
|
*/
|
|
12396
|
-
function FacetDropdown({ label, values, chosen, onChange }) {
|
|
12408
|
+
function FacetDropdown$1({ label, values, chosen, onChange }) {
|
|
12397
12409
|
if (values.length < 2) return null;
|
|
12398
12410
|
const summary = chosen.length === 0 ? "All" : chosen.join(", ");
|
|
12399
12411
|
const toggle = (value) => {
|
|
@@ -12451,7 +12463,7 @@ function FieldShell({ label, description, required, translationFn, children }) {
|
|
|
12451
12463
|
className: "min-w-0",
|
|
12452
12464
|
children: [
|
|
12453
12465
|
resolvedLabel !== void 0 && resolvedLabel !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
12454
|
-
className: LABEL_CLASS$
|
|
12466
|
+
className: LABEL_CLASS$2,
|
|
12455
12467
|
children: [resolvedLabel, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
12456
12468
|
className: "text-danger ml-0.5",
|
|
12457
12469
|
children: "*"
|
|
@@ -12547,7 +12559,7 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
12547
12559
|
className: "min-w-0",
|
|
12548
12560
|
children: [
|
|
12549
12561
|
label !== void 0 && label !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
12550
|
-
className: LABEL_CLASS$
|
|
12562
|
+
className: LABEL_CLASS$2,
|
|
12551
12563
|
children: label
|
|
12552
12564
|
}),
|
|
12553
12565
|
selectedOptions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -12568,7 +12580,7 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
12568
12580
|
}),
|
|
12569
12581
|
smart && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
12570
12582
|
className: "mb-1.5 flex flex-wrap gap-1.5",
|
|
12571
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FacetDropdown, {
|
|
12583
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FacetDropdown$1, {
|
|
12572
12584
|
label: "Location",
|
|
12573
12585
|
values: facetValues.locations,
|
|
12574
12586
|
chosen: facets.locations,
|
|
@@ -12576,7 +12588,7 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
12576
12588
|
...f,
|
|
12577
12589
|
locations
|
|
12578
12590
|
}))
|
|
12579
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FacetDropdown, {
|
|
12591
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FacetDropdown$1, {
|
|
12580
12592
|
label: "Type",
|
|
12581
12593
|
values: facetValues.types,
|
|
12582
12594
|
chosen: facets.types,
|
|
@@ -12587,7 +12599,7 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
12587
12599
|
})]
|
|
12588
12600
|
}),
|
|
12589
12601
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
12590
|
-
className: INPUT_SHELL + (isDisabled ? " opacity-50" : ""),
|
|
12602
|
+
className: INPUT_SHELL$1 + (isDisabled ? " opacity-50" : ""),
|
|
12591
12603
|
children: [
|
|
12592
12604
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" }),
|
|
12593
12605
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -25810,7 +25822,7 @@ function TimezoneSelector({ value, onChange, disabled }) {
|
|
|
25810
25822
|
//#endregion
|
|
25811
25823
|
//#region src/composites/config-form-field.tsx
|
|
25812
25824
|
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";
|
|
25813
|
-
var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1 break-words";
|
|
25825
|
+
var LABEL_CLASS$1 = "block text-[11px] font-medium text-foreground mb-1 break-words";
|
|
25814
25826
|
var DESC_CLASS = "text-[10px] text-foreground-subtle mt-0.5 break-words";
|
|
25815
25827
|
function FieldWrapper({ label, description, required, span, children, translationFn }) {
|
|
25816
25828
|
const colSpanClass = span === 2 ? "col-span-1 @[480px]:col-span-2" : span === 3 ? "col-span-1 @[480px]:col-span-2 @[880px]:col-span-3" : span === 4 ? "col-span-1 @[480px]:col-span-2 @[1140px]:col-span-4" : "col-span-1";
|
|
@@ -25821,7 +25833,7 @@ function FieldWrapper({ label, description, required, span, children, translatio
|
|
|
25821
25833
|
className: `${colSpanClass} min-w-0`,
|
|
25822
25834
|
children: [
|
|
25823
25835
|
hasLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
25824
|
-
className: LABEL_CLASS,
|
|
25836
|
+
className: LABEL_CLASS$1,
|
|
25825
25837
|
children: [resolvedLabel, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25826
25838
|
className: "text-danger ml-0.5",
|
|
25827
25839
|
children: "*"
|
|
@@ -26511,7 +26523,7 @@ function SubTabsField({ field, values, onChange, disabled, translationFn, onTest
|
|
|
26511
26523
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
26512
26524
|
className: `${colSpanClass} min-w-0`,
|
|
26513
26525
|
children: [field.label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
26514
|
-
className: LABEL_CLASS,
|
|
26526
|
+
className: LABEL_CLASS$1,
|
|
26515
26527
|
children: resolveLabel(field.label, translationFn)
|
|
26516
26528
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
26517
26529
|
className: "@container rounded-lg border border-border-subtle bg-surface-hover/40 overflow-hidden",
|
|
@@ -27309,7 +27321,7 @@ function MultipleFieldWrapper({ field, values, onChange, disabled, translationFn
|
|
|
27309
27321
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
27310
27322
|
className: "space-y-1",
|
|
27311
27323
|
children: [itemLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
27312
|
-
className: LABEL_CLASS,
|
|
27324
|
+
className: LABEL_CLASS$1,
|
|
27313
27325
|
children: itemLabel
|
|
27314
27326
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
27315
27327
|
className: "flex items-center gap-2",
|
|
@@ -50579,7 +50591,7 @@ function SectionRows({ title, count, open, colSpan, onToggle, rows, trpc, parent
|
|
|
50579
50591
|
//#region src/composites/device-list/sort.ts
|
|
50580
50592
|
/** Human-facing type label used for the `type` comparator — matches the label
|
|
50581
50593
|
* the row renders, so the sort order reads as the user sees it. */
|
|
50582
|
-
function typeLabel(device) {
|
|
50594
|
+
function typeLabel$1(device) {
|
|
50583
50595
|
return deviceTypeMetaOf(device.type).label.toLowerCase();
|
|
50584
50596
|
}
|
|
50585
50597
|
function nameKey(device) {
|
|
@@ -50588,7 +50600,7 @@ function nameKey(device) {
|
|
|
50588
50600
|
/** Ascending comparator for a given sortable column. */
|
|
50589
50601
|
function ascComparator(columnId) {
|
|
50590
50602
|
if (columnId === "name") return (a, b) => nameKey(a).localeCompare(nameKey(b));
|
|
50591
|
-
if (columnId === "type") return (a, b) => typeLabel(a).localeCompare(typeLabel(b));
|
|
50603
|
+
if (columnId === "type") return (a, b) => typeLabel$1(a).localeCompare(typeLabel$1(b));
|
|
50592
50604
|
return compareHardware;
|
|
50593
50605
|
}
|
|
50594
50606
|
/**
|
|
@@ -55744,6 +55756,1366 @@ function VersionBadge({ version, preRelease, className }) {
|
|
|
55744
55756
|
});
|
|
55745
55757
|
}
|
|
55746
55758
|
//#endregion
|
|
55759
|
+
//#region src/composites/device-picker/SelectedChip.tsx
|
|
55760
|
+
/**
|
|
55761
|
+
* Removable selection chip + the search-box shell shared by DevicePicker and
|
|
55762
|
+
* SearchableMultiSelectField (admin-ui). Lives here so addon MFEs can render
|
|
55763
|
+
* the same picker without importing admin-ui.
|
|
55764
|
+
*/
|
|
55765
|
+
/** The search box's shell — shared with multiselect fields that mirror this picker. */
|
|
55766
|
+
var INPUT_SHELL = "flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30";
|
|
55767
|
+
/** One selected entry as a removable chip. */
|
|
55768
|
+
function SelectedChip({ label, onRemove, warning = false, iconId, testId }) {
|
|
55769
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
55770
|
+
"data-testid": testId,
|
|
55771
|
+
className: warning ? "inline-flex items-center gap-1 rounded-md bg-warning/10 px-1.5 py-0.5 text-[11px] text-warning max-w-full" : "inline-flex items-center gap-1 rounded-md bg-primary/10 px-1.5 py-0.5 text-[11px] text-primary max-w-full",
|
|
55772
|
+
children: [
|
|
55773
|
+
iconId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventKindGlyph, {
|
|
55774
|
+
iconId,
|
|
55775
|
+
size: 12
|
|
55776
|
+
}),
|
|
55777
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55778
|
+
className: "truncate",
|
|
55779
|
+
children: label
|
|
55780
|
+
}),
|
|
55781
|
+
onRemove !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
55782
|
+
type: "button",
|
|
55783
|
+
className: "shrink-0 hover:text-danger",
|
|
55784
|
+
"aria-label": `Remove ${label}`,
|
|
55785
|
+
onClick: onRemove,
|
|
55786
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-2.5 w-2.5" })
|
|
55787
|
+
})
|
|
55788
|
+
]
|
|
55789
|
+
});
|
|
55790
|
+
}
|
|
55791
|
+
//#endregion
|
|
55792
|
+
//#region src/composites/device-picker/device-picker-model.ts
|
|
55793
|
+
/** Numbers numerically, anything else by its text — the name tie-break. */
|
|
55794
|
+
function compareIds(a, b) {
|
|
55795
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
55796
|
+
return String(a).localeCompare(String(b));
|
|
55797
|
+
}
|
|
55798
|
+
/** How long the search waits after the last keystroke. */
|
|
55799
|
+
var PICKER_SEARCH_DEBOUNCE_MS = 200;
|
|
55800
|
+
/** A group with more devices than this starts collapsed (unless searching). */
|
|
55801
|
+
var PICKER_GROUP_COLLAPSE_AT = 8;
|
|
55802
|
+
/** How many selected chips are drawn before "+N more". */
|
|
55803
|
+
var PICKER_CHIP_LIMIT = 6;
|
|
55804
|
+
/** Rows a capped render draws per "show more". */
|
|
55805
|
+
var PICKER_PAGE_SIZE = 60;
|
|
55806
|
+
/** Rows a virtualised list renders per batch and initially. */
|
|
55807
|
+
var PICKER_WINDOW = 30;
|
|
55808
|
+
/** Everything a query can match on, lower-cased. */
|
|
55809
|
+
function pickerSearchText(device) {
|
|
55810
|
+
return `${[
|
|
55811
|
+
device.name,
|
|
55812
|
+
device.location ?? "",
|
|
55813
|
+
device.type ?? "",
|
|
55814
|
+
device.detail ?? ""
|
|
55815
|
+
].join(" ")} ${String(device.id)}`.toLowerCase();
|
|
55816
|
+
}
|
|
55817
|
+
/** The query's tokens; empty for a blank query. */
|
|
55818
|
+
function pickerQueryTokens(query) {
|
|
55819
|
+
return query.trim().toLowerCase().split(/\s+/).filter((token) => token.length > 0);
|
|
55820
|
+
}
|
|
55821
|
+
/** Devices matching EVERY token. A blank query keeps the list as it is. */
|
|
55822
|
+
function filterPickerDevices(devices, query) {
|
|
55823
|
+
const tokens = pickerQueryTokens(query);
|
|
55824
|
+
if (tokens.length === 0) return devices;
|
|
55825
|
+
return devices.filter((device) => {
|
|
55826
|
+
const hay = pickerSearchText(device);
|
|
55827
|
+
return tokens.every((token) => hay.includes(token));
|
|
55828
|
+
});
|
|
55829
|
+
}
|
|
55830
|
+
function compareNullableLast(a, b) {
|
|
55831
|
+
if (a === b) return 0;
|
|
55832
|
+
if (a === null) return 1;
|
|
55833
|
+
if (b === null) return -1;
|
|
55834
|
+
return a.localeCompare(b);
|
|
55835
|
+
}
|
|
55836
|
+
function blankToNull(value) {
|
|
55837
|
+
return value === void 0 || value === null || value.trim() === "" ? null : value;
|
|
55838
|
+
}
|
|
55839
|
+
/** The key a group is remembered by (collapsed / expanded). */
|
|
55840
|
+
function pickerGroupKey(type, location) {
|
|
55841
|
+
return `${type ?? ""}|${location ?? ""}`;
|
|
55842
|
+
}
|
|
55843
|
+
/** Group by type, then room; both ascending, "none" last; devices by name. */
|
|
55844
|
+
function groupPickerDevices(devices, selected) {
|
|
55845
|
+
const picked = new Set(selected);
|
|
55846
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
55847
|
+
for (const device of devices) {
|
|
55848
|
+
const type = blankToNull(device.type);
|
|
55849
|
+
const location = blankToNull(device.location);
|
|
55850
|
+
const key = pickerGroupKey(type, location);
|
|
55851
|
+
const bucket = buckets.get(key);
|
|
55852
|
+
if (bucket === void 0) buckets.set(key, {
|
|
55853
|
+
type,
|
|
55854
|
+
location,
|
|
55855
|
+
devices: [device]
|
|
55856
|
+
});
|
|
55857
|
+
else bucket.devices.push(device);
|
|
55858
|
+
}
|
|
55859
|
+
const groups = [];
|
|
55860
|
+
for (const [key, bucket] of buckets) {
|
|
55861
|
+
const sorted = [...bucket.devices].sort((a, b) => a.name.localeCompare(b.name) || compareIds(a.id, b.id));
|
|
55862
|
+
groups.push({
|
|
55863
|
+
key,
|
|
55864
|
+
type: bucket.type,
|
|
55865
|
+
location: bucket.location,
|
|
55866
|
+
devices: sorted,
|
|
55867
|
+
selectedCount: sorted.filter((device) => picked.has(device.id)).length
|
|
55868
|
+
});
|
|
55869
|
+
}
|
|
55870
|
+
return groups.sort((a, b) => compareNullableLast(a.type, b.type) || compareNullableLast(a.location, b.location));
|
|
55871
|
+
}
|
|
55872
|
+
/**
|
|
55873
|
+
* Whether a group is open. The operator's own toggle wins; otherwise a small
|
|
55874
|
+
* group is open, a big one is closed — unless a search is running (the result
|
|
55875
|
+
* IS what they asked to see) or it holds a selected device.
|
|
55876
|
+
*/
|
|
55877
|
+
function isGroupExpanded(group, toggles, searching) {
|
|
55878
|
+
const own = toggles.get(group.key);
|
|
55879
|
+
if (own !== void 0) return own;
|
|
55880
|
+
if (searching || group.selectedCount > 0) return true;
|
|
55881
|
+
return group.devices.length <= 8;
|
|
55882
|
+
}
|
|
55883
|
+
/** The groups as ONE list of rows — what a virtualised list renders. */
|
|
55884
|
+
function flattenPickerRows(groups, selected, toggles, searching) {
|
|
55885
|
+
const picked = new Set(selected);
|
|
55886
|
+
const rows = [];
|
|
55887
|
+
for (const group of groups) {
|
|
55888
|
+
const expanded = isGroupExpanded(group, toggles, searching);
|
|
55889
|
+
rows.push({
|
|
55890
|
+
kind: "header",
|
|
55891
|
+
key: `h:${group.key}`,
|
|
55892
|
+
group,
|
|
55893
|
+
expanded
|
|
55894
|
+
});
|
|
55895
|
+
if (!expanded) continue;
|
|
55896
|
+
for (const device of group.devices) rows.push({
|
|
55897
|
+
kind: "device",
|
|
55898
|
+
key: `d:${String(device.id)}`,
|
|
55899
|
+
device,
|
|
55900
|
+
groupKey: group.key,
|
|
55901
|
+
selected: picked.has(device.id)
|
|
55902
|
+
});
|
|
55903
|
+
}
|
|
55904
|
+
return rows;
|
|
55905
|
+
}
|
|
55906
|
+
/** Tick or untick one id. The order of the rest is kept. */
|
|
55907
|
+
function togglePickerDevice(value, id) {
|
|
55908
|
+
return value.includes(id) ? value.filter((v) => v !== id) : [...value, id];
|
|
55909
|
+
}
|
|
55910
|
+
/** Add every device of the group (only the ones the current search shows). */
|
|
55911
|
+
function selectPickerGroup(value, group) {
|
|
55912
|
+
const next = [...value];
|
|
55913
|
+
const have = new Set(value);
|
|
55914
|
+
for (const device of group.devices) if (!have.has(device.id)) next.push(device.id);
|
|
55915
|
+
return next;
|
|
55916
|
+
}
|
|
55917
|
+
/** Remove every device of the group; ids outside it are untouched. */
|
|
55918
|
+
function clearPickerGroup(value, group) {
|
|
55919
|
+
const drop = new Set(group.devices.map((device) => device.id));
|
|
55920
|
+
return value.filter((id) => !drop.has(id));
|
|
55921
|
+
}
|
|
55922
|
+
function chipOverflow(items, expanded) {
|
|
55923
|
+
if (expanded || items.length <= 6) return {
|
|
55924
|
+
shown: items,
|
|
55925
|
+
hidden: 0
|
|
55926
|
+
};
|
|
55927
|
+
return {
|
|
55928
|
+
shown: items.slice(0, 6),
|
|
55929
|
+
hidden: items.length - 6
|
|
55930
|
+
};
|
|
55931
|
+
}
|
|
55932
|
+
function pageRows(rows, shown) {
|
|
55933
|
+
const count = Math.max(0, Math.min(rows.length, shown));
|
|
55934
|
+
return {
|
|
55935
|
+
rows: rows.slice(0, count),
|
|
55936
|
+
remaining: rows.length - count
|
|
55937
|
+
};
|
|
55938
|
+
}
|
|
55939
|
+
/** The modes, in the order the control draws them. */
|
|
55940
|
+
var PICKER_GROUP_BY_MODES = [
|
|
55941
|
+
"zone",
|
|
55942
|
+
"location",
|
|
55943
|
+
"none"
|
|
55944
|
+
];
|
|
55945
|
+
/** What an operator who never chose gets. */
|
|
55946
|
+
var PICKER_DEFAULT_GROUP_BY = "zone";
|
|
55947
|
+
/** A stored choice; anything unreadable is the default. */
|
|
55948
|
+
function parsePickerGroupBy(raw) {
|
|
55949
|
+
return raw === "zone" || raw === "location" || raw === "none" ? raw : PICKER_DEFAULT_GROUP_BY;
|
|
55950
|
+
}
|
|
55951
|
+
/** The folded key a location label joins a zone by — `siteZoneKey`'s fold. */
|
|
55952
|
+
function pickerLocationKey(label) {
|
|
55953
|
+
return label.trim().toLowerCase();
|
|
55954
|
+
}
|
|
55955
|
+
var NO_ZONE_RANK = 1;
|
|
55956
|
+
var NO_LOCATION_RANK = 2;
|
|
55957
|
+
function byName(a, b) {
|
|
55958
|
+
return a.name.localeCompare(b.name) || compareIds(a.id, b.id);
|
|
55959
|
+
}
|
|
55960
|
+
function headingLabel(heading) {
|
|
55961
|
+
return heading.kind === "zone" || heading.kind === "location" ? heading.label : "";
|
|
55962
|
+
}
|
|
55963
|
+
var NO_LOCATION = {
|
|
55964
|
+
key: "-location",
|
|
55965
|
+
heading: { kind: "no-location" },
|
|
55966
|
+
rank: NO_LOCATION_RANK
|
|
55967
|
+
};
|
|
55968
|
+
var NO_ZONE = {
|
|
55969
|
+
key: "-zone",
|
|
55970
|
+
heading: { kind: "no-zone" },
|
|
55971
|
+
rank: NO_ZONE_RANK
|
|
55972
|
+
};
|
|
55973
|
+
/** Every zone by the folded location keys it holds; the first zone wins a key. */
|
|
55974
|
+
function zoneByLocation(zones) {
|
|
55975
|
+
const out = /* @__PURE__ */ new Map();
|
|
55976
|
+
for (const zone of zones) for (const key of zone.locationKeys) {
|
|
55977
|
+
const folded = pickerLocationKey(key);
|
|
55978
|
+
if (!out.has(folded)) out.set(folded, zone);
|
|
55979
|
+
}
|
|
55980
|
+
return out;
|
|
55981
|
+
}
|
|
55982
|
+
function placeByLocation(location) {
|
|
55983
|
+
if (location === null) return NO_LOCATION;
|
|
55984
|
+
return {
|
|
55985
|
+
key: `l:${pickerLocationKey(location)}`,
|
|
55986
|
+
heading: {
|
|
55987
|
+
kind: "location",
|
|
55988
|
+
label: location.trim()
|
|
55989
|
+
},
|
|
55990
|
+
rank: 0
|
|
55991
|
+
};
|
|
55992
|
+
}
|
|
55993
|
+
function placeByZone(location, zones) {
|
|
55994
|
+
if (location === null) return NO_LOCATION;
|
|
55995
|
+
const zone = zones.get(pickerLocationKey(location));
|
|
55996
|
+
if (zone === void 0) return NO_ZONE;
|
|
55997
|
+
return {
|
|
55998
|
+
key: `z:${zone.zoneKey}`,
|
|
55999
|
+
heading: {
|
|
56000
|
+
kind: "zone",
|
|
56001
|
+
label: zone.label
|
|
56002
|
+
},
|
|
56003
|
+
rank: 0
|
|
56004
|
+
};
|
|
56005
|
+
}
|
|
56006
|
+
/**
|
|
56007
|
+
* Group the list by zone, location or nothing.
|
|
56008
|
+
*
|
|
56009
|
+
* Zone: a group per site zone that holds a listed device (by label), then
|
|
56010
|
+
* "No zone" (a location no zone holds), then "No location". Location: a group
|
|
56011
|
+
* per location (folded; named after the first device's trimmed spelling), then "No
|
|
56012
|
+
* location". None: one flat list by name, then id.
|
|
56013
|
+
*
|
|
56014
|
+
* A zone read that is pending or failed can NOT say a location is in no zone
|
|
56015
|
+
* (D315), so Zone mode then groups by location and returns the reason as
|
|
56016
|
+
* {@link PickerGrouping.fallback} for the UI's one-line note.
|
|
56017
|
+
*/
|
|
56018
|
+
function groupPickerDevicesBy(devices, selected, groupBy, zones) {
|
|
56019
|
+
const picked = new Set(selected);
|
|
56020
|
+
const sorted = [...devices].sort(byName);
|
|
56021
|
+
const count = (list) => list.filter((device) => picked.has(device.id)).length;
|
|
56022
|
+
if (groupBy === "none") return {
|
|
56023
|
+
groupBy,
|
|
56024
|
+
fallback: null,
|
|
56025
|
+
flat: true,
|
|
56026
|
+
groups: [{
|
|
56027
|
+
key: "all",
|
|
56028
|
+
type: null,
|
|
56029
|
+
location: null,
|
|
56030
|
+
devices: sorted,
|
|
56031
|
+
selectedCount: count(sorted)
|
|
56032
|
+
}]
|
|
56033
|
+
};
|
|
56034
|
+
const fallback = groupBy === "zone" && zones.state !== "answered" ? zones.state : null;
|
|
56035
|
+
const effective = fallback === null ? groupBy : "location";
|
|
56036
|
+
const zoneMap = zones.state === "answered" ? zoneByLocation(zones.zones) : /* @__PURE__ */ new Map();
|
|
56037
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
56038
|
+
for (const device of sorted) {
|
|
56039
|
+
const location = blankToNull(device.location);
|
|
56040
|
+
const place = effective === "zone" ? placeByZone(location, zoneMap) : placeByLocation(location);
|
|
56041
|
+
const bucket = buckets.get(place.key);
|
|
56042
|
+
if (bucket === void 0) buckets.set(place.key, {
|
|
56043
|
+
heading: place.heading,
|
|
56044
|
+
rank: place.rank,
|
|
56045
|
+
devices: [device]
|
|
56046
|
+
});
|
|
56047
|
+
else bucket.devices.push(device);
|
|
56048
|
+
}
|
|
56049
|
+
return {
|
|
56050
|
+
groupBy: effective,
|
|
56051
|
+
fallback,
|
|
56052
|
+
flat: false,
|
|
56053
|
+
groups: [...buckets].sort(([, a], [, b]) => a.rank - b.rank || headingLabel(a.heading).localeCompare(headingLabel(b.heading))).map(([key, bucket]) => ({
|
|
56054
|
+
key,
|
|
56055
|
+
type: null,
|
|
56056
|
+
location: bucket.heading.kind === "location" ? bucket.heading.label : null,
|
|
56057
|
+
devices: bucket.devices,
|
|
56058
|
+
selectedCount: count(bucket.devices),
|
|
56059
|
+
heading: bucket.heading
|
|
56060
|
+
}))
|
|
56061
|
+
};
|
|
56062
|
+
}
|
|
56063
|
+
/** The grouping as rows: a flat grouping draws no header and is always open. */
|
|
56064
|
+
function flattenPickerGrouping(grouping, selected, toggles, searching) {
|
|
56065
|
+
if (!grouping.flat) return flattenPickerRows(grouping.groups, selected, toggles, searching);
|
|
56066
|
+
const picked = new Set(selected);
|
|
56067
|
+
const rows = [];
|
|
56068
|
+
for (const group of grouping.groups) for (const device of group.devices) rows.push({
|
|
56069
|
+
kind: "device",
|
|
56070
|
+
key: `d:${String(device.id)}`,
|
|
56071
|
+
device,
|
|
56072
|
+
groupKey: group.key,
|
|
56073
|
+
selected: picked.has(device.id)
|
|
56074
|
+
});
|
|
56075
|
+
return rows;
|
|
56076
|
+
}
|
|
56077
|
+
function pickerRowFacts(device, groupBy) {
|
|
56078
|
+
return {
|
|
56079
|
+
location: groupBy === "location" ? null : blankToNull(device.location),
|
|
56080
|
+
type: blankToNull(device.type)
|
|
56081
|
+
};
|
|
56082
|
+
}
|
|
56083
|
+
/** The filter key of an item with no type / no location / no zone. */
|
|
56084
|
+
var PICKER_NONE_KEY = "(none)";
|
|
56085
|
+
var PICKER_NO_FILTERS = {
|
|
56086
|
+
query: "",
|
|
56087
|
+
types: [],
|
|
56088
|
+
zones: [],
|
|
56089
|
+
locations: []
|
|
56090
|
+
};
|
|
56091
|
+
var NO_FACET_VALUE = {
|
|
56092
|
+
key: PICKER_NONE_KEY,
|
|
56093
|
+
label: null
|
|
56094
|
+
};
|
|
56095
|
+
function facetValue(item, facet, zones) {
|
|
56096
|
+
if (facet === "type") {
|
|
56097
|
+
const type = blankToNull(item.type);
|
|
56098
|
+
return type === null ? NO_FACET_VALUE : {
|
|
56099
|
+
key: type,
|
|
56100
|
+
label: type
|
|
56101
|
+
};
|
|
56102
|
+
}
|
|
56103
|
+
const location = blankToNull(item.location);
|
|
56104
|
+
if (location === null) return NO_FACET_VALUE;
|
|
56105
|
+
if (facet === "location") return {
|
|
56106
|
+
key: pickerLocationKey(location),
|
|
56107
|
+
label: location.trim()
|
|
56108
|
+
};
|
|
56109
|
+
if (zones.state !== "answered") return NO_FACET_VALUE;
|
|
56110
|
+
const zone = zoneByLocation(zones.zones).get(pickerLocationKey(location));
|
|
56111
|
+
return zone === void 0 ? NO_FACET_VALUE : {
|
|
56112
|
+
key: zone.zoneKey,
|
|
56113
|
+
label: zone.label
|
|
56114
|
+
};
|
|
56115
|
+
}
|
|
56116
|
+
/** The key an item filters by under one facet; `(none)` when it has no such value. */
|
|
56117
|
+
function pickerFacetKey(item, facet, zones) {
|
|
56118
|
+
return facetValue(item, facet, zones).key;
|
|
56119
|
+
}
|
|
56120
|
+
function compareFacetOptions(a, b) {
|
|
56121
|
+
if (a.key === "(none)" || b.key === "(none)") return a.key === b.key ? 0 : a.key === "(none)" ? 1 : -1;
|
|
56122
|
+
return (a.label ?? "").localeCompare(b.label ?? "");
|
|
56123
|
+
}
|
|
56124
|
+
/** Every value of a facet the list holds (plus chosen ones it no longer holds, at 0). */
|
|
56125
|
+
function pickerFacetOptions(items, facet, chosen, zones) {
|
|
56126
|
+
if (facet === "zone" && zones.state !== "answered") return [];
|
|
56127
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
56128
|
+
for (const item of items) {
|
|
56129
|
+
const value = facetValue(item, facet, zones);
|
|
56130
|
+
const have = byKey.get(value.key);
|
|
56131
|
+
if (have === void 0) byKey.set(value.key, {
|
|
56132
|
+
label: value.label,
|
|
56133
|
+
count: 1
|
|
56134
|
+
});
|
|
56135
|
+
else have.count++;
|
|
56136
|
+
}
|
|
56137
|
+
for (const key of chosen) if (!byKey.has(key)) byKey.set(key, {
|
|
56138
|
+
label: key === "(none)" ? null : key,
|
|
56139
|
+
count: 0
|
|
56140
|
+
});
|
|
56141
|
+
const options = [];
|
|
56142
|
+
for (const [key, entry] of byKey) options.push({
|
|
56143
|
+
key,
|
|
56144
|
+
label: entry.label,
|
|
56145
|
+
count: entry.count
|
|
56146
|
+
});
|
|
56147
|
+
return options.sort(compareFacetOptions);
|
|
56148
|
+
}
|
|
56149
|
+
/** A zone filter is chosen but the zones read has not answered: it is paused, not applied. */
|
|
56150
|
+
function pickerZoneFilterSuspended(filters, zones) {
|
|
56151
|
+
return filters.zones.length > 0 && zones.state !== "answered";
|
|
56152
|
+
}
|
|
56153
|
+
function keepFacet(items, facet, chosen, zones) {
|
|
56154
|
+
if (chosen.length === 0) return items;
|
|
56155
|
+
return items.filter((item) => chosen.includes(pickerFacetKey(item, facet, zones)));
|
|
56156
|
+
}
|
|
56157
|
+
/** Search, then Type, Zone (only from an answered read), Location. No filter: the list itself. */
|
|
56158
|
+
function applyPickerFilters(items, filters, zones) {
|
|
56159
|
+
const typed = keepFacet(filterPickerDevices(items, filters.query), "type", filters.types, zones);
|
|
56160
|
+
return keepFacet(zones.state === "answered" ? keepFacet(typed, "zone", filters.zones, zones) : typed, "location", filters.locations, zones);
|
|
56161
|
+
}
|
|
56162
|
+
/** Tick or untick one filter key. */
|
|
56163
|
+
function togglePickerKey(chosen, key) {
|
|
56164
|
+
return chosen.includes(key) ? chosen.filter((k) => k !== key) : [...chosen, key];
|
|
56165
|
+
}
|
|
56166
|
+
function pickerFiltersActive(filters) {
|
|
56167
|
+
return filters.query.trim() !== "" || filters.types.length > 0 || filters.zones.length > 0 || filters.locations.length > 0;
|
|
56168
|
+
}
|
|
56169
|
+
/** Reset clears search, Type, Zone and Location — never "Group by". */
|
|
56170
|
+
function resetPickerFilters() {
|
|
56171
|
+
return PICKER_NO_FILTERS;
|
|
56172
|
+
}
|
|
56173
|
+
/**
|
|
56174
|
+
* Selected ids the current filters hide — they stay selected, they are only not listed. Only a
|
|
56175
|
+
* LISTED device can be hidden: an id the list does not hold at all (removed, unknown, off-offer)
|
|
56176
|
+
* is not "hidden", so it is not counted.
|
|
56177
|
+
*/
|
|
56178
|
+
function pickerHiddenSelectedCount(selected, list, matches) {
|
|
56179
|
+
const listed = new Set(list.map((item) => item.id));
|
|
56180
|
+
const shown = new Set(matches.map((item) => item.id));
|
|
56181
|
+
return selected.filter((id) => listed.has(id) && !shown.has(id)).length;
|
|
56182
|
+
}
|
|
56183
|
+
/** A dropdown is drawn when it can narrow (two values or more) or a key is chosen. */
|
|
56184
|
+
function pickerFacetFilterShown(options, chosen) {
|
|
56185
|
+
return options.length >= 2 || chosen.length > 0;
|
|
56186
|
+
}
|
|
56187
|
+
//#endregion
|
|
56188
|
+
//#region src/composites/device-picker/device-filter-bar.tsx
|
|
56189
|
+
/**
|
|
56190
|
+
* The filter bar above a picker's list (D697): search, "Group by" (when the
|
|
56191
|
+
* list offers it), the Type / Zone / Location dropdowns and "Reset filters".
|
|
56192
|
+
*
|
|
56193
|
+
* It owns no filter state — the caller holds `PickerFilters` and applies them
|
|
56194
|
+
* (`applyPickerFilters`); this only draws the controls and reports changes.
|
|
56195
|
+
* The search box keeps its own text so typing is instant, and tells the
|
|
56196
|
+
* caller `PICKER_SEARCH_DEBOUNCE_MS` after the last keystroke.
|
|
56197
|
+
*
|
|
56198
|
+
* The Zone and Location dropdowns are provisional (operator answer Q1): to
|
|
56199
|
+
* drop them, delete the two `FacetDropdown` uses in {@link DeviceFilterBar}
|
|
56200
|
+
* and their option props — the model side is described in
|
|
56201
|
+
* `device-picker-model.ts`.
|
|
56202
|
+
*
|
|
56203
|
+
* - D315: a zones read that has not answered draws the Zone dropdown
|
|
56204
|
+
* DISABLED ("loading zones…" / "zones unavailable"), never empty, and a
|
|
56205
|
+
* chosen zone is reported as paused, not as matching nothing.
|
|
56206
|
+
* - "Reset filters" clears search, Type, Zone and Location and leaves the
|
|
56207
|
+
* Group-by choice alone.
|
|
56208
|
+
*/
|
|
56209
|
+
/** "media-player" → "Media player"; null → "No type". */
|
|
56210
|
+
function typeLabel(type) {
|
|
56211
|
+
if (type === null) return "No type";
|
|
56212
|
+
const spaced = type.replaceAll("-", " ");
|
|
56213
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
56214
|
+
}
|
|
56215
|
+
var GROUP_BY_LABEL = {
|
|
56216
|
+
zone: "Zone",
|
|
56217
|
+
location: "Location",
|
|
56218
|
+
none: "None"
|
|
56219
|
+
};
|
|
56220
|
+
var FACET_TITLE = {
|
|
56221
|
+
type: "Type",
|
|
56222
|
+
zone: "Zone",
|
|
56223
|
+
location: "Location"
|
|
56224
|
+
};
|
|
56225
|
+
var NO_VALUE_LABEL = {
|
|
56226
|
+
type: "No type",
|
|
56227
|
+
zone: "No zone",
|
|
56228
|
+
location: "No location"
|
|
56229
|
+
};
|
|
56230
|
+
function optionLabel(facet, option) {
|
|
56231
|
+
if (option.key === "(none)" || option.label === null) return NO_VALUE_LABEL[facet];
|
|
56232
|
+
return facet === "type" ? typeLabel(option.label) : option.label;
|
|
56233
|
+
}
|
|
56234
|
+
/** "Group by: Zone | Location | None", and why Zone is showing locations. */
|
|
56235
|
+
function GroupByControl({ grouping }) {
|
|
56236
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56237
|
+
className: "mb-1.5",
|
|
56238
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56239
|
+
role: "radiogroup",
|
|
56240
|
+
"aria-label": "Group by",
|
|
56241
|
+
className: "flex flex-wrap items-center gap-1 text-[10px] text-foreground-subtle",
|
|
56242
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56243
|
+
className: "mr-1 font-semibold uppercase tracking-wide",
|
|
56244
|
+
children: "Group by"
|
|
56245
|
+
}), PICKER_GROUP_BY_MODES.map((mode) => {
|
|
56246
|
+
const active = mode === grouping.groupBy;
|
|
56247
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
56248
|
+
type: "button",
|
|
56249
|
+
role: "radio",
|
|
56250
|
+
"aria-checked": active,
|
|
56251
|
+
onClick: () => grouping.onGroupByChange(mode),
|
|
56252
|
+
className: active ? "rounded-md border border-primary bg-primary/10 px-1.5 py-0.5 text-[11px] text-primary" : "rounded-md border border-border px-1.5 py-0.5 text-[11px] text-foreground-subtle hover:bg-surface-hover",
|
|
56253
|
+
children: GROUP_BY_LABEL[mode]
|
|
56254
|
+
}, mode);
|
|
56255
|
+
})]
|
|
56256
|
+
}), grouping.fallback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
56257
|
+
className: "mt-0.5 text-[10px] text-foreground-subtle",
|
|
56258
|
+
"data-testid": "picker-zones-note",
|
|
56259
|
+
children: grouping.fallback === "pending" ? "Loading zones — grouped by location for now" : "Zones unavailable — grouped by location"
|
|
56260
|
+
})]
|
|
56261
|
+
});
|
|
56262
|
+
}
|
|
56263
|
+
function FacetDropdown({ facet, options, chosen, onChange, unavailable, disabled }) {
|
|
56264
|
+
const title = FACET_TITLE[facet];
|
|
56265
|
+
const summary = unavailable !== void 0 ? unavailable : chosen.length === 0 ? "All" : `${String(chosen.length)} selected`;
|
|
56266
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
56267
|
+
"data-testid": `picker-filter-${facet}`,
|
|
56268
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Popover, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PopoverTrigger, {
|
|
56269
|
+
disabled: disabled || unavailable !== void 0,
|
|
56270
|
+
className: "rounded-md border border-border px-1.5 py-0.5 text-[11px] text-foreground hover:bg-surface-hover disabled:opacity-50",
|
|
56271
|
+
children: [
|
|
56272
|
+
title,
|
|
56273
|
+
": ",
|
|
56274
|
+
summary
|
|
56275
|
+
]
|
|
56276
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverContent, {
|
|
56277
|
+
className: "min-w-[200px] max-h-[40vh] overflow-y-auto p-1",
|
|
56278
|
+
children: options.map((option) => {
|
|
56279
|
+
const label = optionLabel(facet, option);
|
|
56280
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
56281
|
+
className: "flex cursor-pointer items-center gap-2 px-2 py-1 text-xs text-foreground hover:bg-surface-hover",
|
|
56282
|
+
children: [
|
|
56283
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
|
|
56284
|
+
checked: chosen.includes(option.key),
|
|
56285
|
+
onChange: () => onChange(togglePickerKey(chosen, option.key)),
|
|
56286
|
+
"aria-label": label
|
|
56287
|
+
}),
|
|
56288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56289
|
+
className: "min-w-0 flex-1 truncate",
|
|
56290
|
+
children: label
|
|
56291
|
+
}),
|
|
56292
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
56293
|
+
className: "text-[10px] text-foreground-subtle",
|
|
56294
|
+
children: [
|
|
56295
|
+
"(",
|
|
56296
|
+
option.count,
|
|
56297
|
+
")"
|
|
56298
|
+
]
|
|
56299
|
+
})
|
|
56300
|
+
]
|
|
56301
|
+
}, option.key);
|
|
56302
|
+
})
|
|
56303
|
+
})] })
|
|
56304
|
+
});
|
|
56305
|
+
}
|
|
56306
|
+
/** Why the Zone dropdown cannot be used, or undefined when the read answered. */
|
|
56307
|
+
function zoneUnavailable(zones) {
|
|
56308
|
+
if (zones.state === "pending") return "loading zones…";
|
|
56309
|
+
if (zones.state === "failed") return "zones unavailable";
|
|
56310
|
+
}
|
|
56311
|
+
function DeviceFilterBar({ filters, onFiltersChange, typeOptions, zoneOptions, locationOptions, zones, grouping, searchPlaceholder, searchLabel, disabled = false }) {
|
|
56312
|
+
const [text, setText] = (0, react$1.useState)(filters.query);
|
|
56313
|
+
const latest = (0, react$1.useRef)({
|
|
56314
|
+
filters,
|
|
56315
|
+
onFiltersChange
|
|
56316
|
+
});
|
|
56317
|
+
latest.current = {
|
|
56318
|
+
filters,
|
|
56319
|
+
onFiltersChange
|
|
56320
|
+
};
|
|
56321
|
+
(0, react$1.useEffect)(() => {
|
|
56322
|
+
if (text === latest.current.filters.query) return;
|
|
56323
|
+
const timer = setTimeout(() => latest.current.onFiltersChange({
|
|
56324
|
+
...latest.current.filters,
|
|
56325
|
+
query: text
|
|
56326
|
+
}), 200);
|
|
56327
|
+
return () => clearTimeout(timer);
|
|
56328
|
+
}, [text]);
|
|
56329
|
+
const unavailable = zoneUnavailable(zones);
|
|
56330
|
+
const zoneShown = unavailable !== void 0 || pickerFacetFilterShown(zoneOptions, filters.zones);
|
|
56331
|
+
const active = pickerFiltersActive(filters) || text.trim() !== "";
|
|
56332
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56333
|
+
className: "space-y-1.5",
|
|
56334
|
+
"data-testid": "device-filter-bar",
|
|
56335
|
+
children: [
|
|
56336
|
+
grouping !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupByControl, { grouping }),
|
|
56337
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56338
|
+
className: INPUT_SHELL,
|
|
56339
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
56340
|
+
type: "text",
|
|
56341
|
+
value: text,
|
|
56342
|
+
placeholder: searchPlaceholder,
|
|
56343
|
+
"aria-label": searchLabel,
|
|
56344
|
+
className: "min-w-0 flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-foreground-disabled",
|
|
56345
|
+
onChange: (e) => setText(e.target.value)
|
|
56346
|
+
})]
|
|
56347
|
+
}),
|
|
56348
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56349
|
+
className: "flex flex-wrap items-center gap-1",
|
|
56350
|
+
children: [
|
|
56351
|
+
pickerFacetFilterShown(typeOptions, filters.types) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FacetDropdown, {
|
|
56352
|
+
facet: "type",
|
|
56353
|
+
options: typeOptions,
|
|
56354
|
+
chosen: filters.types,
|
|
56355
|
+
onChange: (types) => onFiltersChange({
|
|
56356
|
+
...filters,
|
|
56357
|
+
types
|
|
56358
|
+
}),
|
|
56359
|
+
disabled
|
|
56360
|
+
}),
|
|
56361
|
+
zoneShown && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FacetDropdown, {
|
|
56362
|
+
facet: "zone",
|
|
56363
|
+
options: zoneOptions,
|
|
56364
|
+
chosen: filters.zones,
|
|
56365
|
+
onChange: (next) => onFiltersChange({
|
|
56366
|
+
...filters,
|
|
56367
|
+
zones: next
|
|
56368
|
+
}),
|
|
56369
|
+
...unavailable !== void 0 ? { unavailable } : {},
|
|
56370
|
+
disabled
|
|
56371
|
+
}),
|
|
56372
|
+
pickerFacetFilterShown(locationOptions, filters.locations) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FacetDropdown, {
|
|
56373
|
+
facet: "location",
|
|
56374
|
+
options: locationOptions,
|
|
56375
|
+
chosen: filters.locations,
|
|
56376
|
+
onChange: (locations) => onFiltersChange({
|
|
56377
|
+
...filters,
|
|
56378
|
+
locations
|
|
56379
|
+
}),
|
|
56380
|
+
disabled
|
|
56381
|
+
}),
|
|
56382
|
+
active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
56383
|
+
variant: "ghost",
|
|
56384
|
+
"data-testid": "picker-reset-filters",
|
|
56385
|
+
onClick: () => {
|
|
56386
|
+
setText("");
|
|
56387
|
+
onFiltersChange(resetPickerFilters());
|
|
56388
|
+
},
|
|
56389
|
+
children: "Reset filters"
|
|
56390
|
+
})
|
|
56391
|
+
]
|
|
56392
|
+
}),
|
|
56393
|
+
pickerZoneFilterSuspended(filters, zones) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
56394
|
+
className: "text-[10px] text-foreground-subtle",
|
|
56395
|
+
"data-testid": "picker-zone-filter-paused",
|
|
56396
|
+
children: "Zone filter paused — zones not loaded"
|
|
56397
|
+
})
|
|
56398
|
+
]
|
|
56399
|
+
});
|
|
56400
|
+
}
|
|
56401
|
+
//#endregion
|
|
56402
|
+
//#region src/composites/device-picker/device-picker-grouping.ts
|
|
56403
|
+
/**
|
|
56404
|
+
* The admin side of the device picker's "Group by" (Zone | Location | None):
|
|
56405
|
+
* the remembered choice, and the site-zone read. What each mode DRAWS is
|
|
56406
|
+
* `device-picker-model.ts` (shared with the viewer); this only supplies what
|
|
56407
|
+
* is side-specific.
|
|
56408
|
+
*
|
|
56409
|
+
* - The choice lives in `localStorage`, one value for every device picker in
|
|
56410
|
+
* this browser. Storage that throws or holds an unknown value is the
|
|
56411
|
+
* default (`zone`), never an error.
|
|
56412
|
+
* - The zones are read whenever the picker's panel is open (the Zone filter
|
|
56413
|
+
* needs them even in Location or None mode) — the caller mounts this with
|
|
56414
|
+
* the panel — and a failure stays a FAILURE: an
|
|
56415
|
+
* empty list would put every device under "No zone", a claim of absence
|
|
56416
|
+
* from a read that did not answer (D315). The model then groups by
|
|
56417
|
+
* location and the panel says why.
|
|
56418
|
+
*/
|
|
56419
|
+
var DEVICE_PICKER_GROUP_STORAGE_KEY = "camstack.admin.device-picker.group-by";
|
|
56420
|
+
/**
|
|
56421
|
+
* Under `listSiteZones`'s own prefix, so the Locations page's invalidation of
|
|
56422
|
+
* `[['deviceManager', 'listSiteZones']]` refreshes it too.
|
|
56423
|
+
*/
|
|
56424
|
+
var PICKER_ZONES_QUERY_KEY = [["deviceManager", "listSiteZones"], "device-picker"];
|
|
56425
|
+
var PICKER_ZONES_STALE_MS = 6e4;
|
|
56426
|
+
/** The stored choice; the default when storage is absent, throws or is unreadable. */
|
|
56427
|
+
function readStoredGroupBy() {
|
|
56428
|
+
try {
|
|
56429
|
+
const raw = globalThis.localStorage?.getItem("camstack.admin.device-picker.group-by") ?? null;
|
|
56430
|
+
return raw === null ? PICKER_DEFAULT_GROUP_BY : parsePickerGroupBy(raw);
|
|
56431
|
+
} catch {
|
|
56432
|
+
return PICKER_DEFAULT_GROUP_BY;
|
|
56433
|
+
}
|
|
56434
|
+
}
|
|
56435
|
+
/** Remember the choice; a storage that refuses keeps it for this page only. */
|
|
56436
|
+
function writeStoredGroupBy(next) {
|
|
56437
|
+
try {
|
|
56438
|
+
globalThis.localStorage?.setItem(DEVICE_PICKER_GROUP_STORAGE_KEY, next);
|
|
56439
|
+
} catch {}
|
|
56440
|
+
}
|
|
56441
|
+
function useStoredGroupBy() {
|
|
56442
|
+
const [groupBy, setState] = (0, react$1.useState)(readStoredGroupBy);
|
|
56443
|
+
const setGroupBy = (next) => {
|
|
56444
|
+
setState(next);
|
|
56445
|
+
writeStoredGroupBy(next);
|
|
56446
|
+
};
|
|
56447
|
+
return {
|
|
56448
|
+
groupBy,
|
|
56449
|
+
setGroupBy
|
|
56450
|
+
};
|
|
56451
|
+
}
|
|
56452
|
+
/** The site zones, with whether the read ANSWERED — read only while `enabled`. */
|
|
56453
|
+
function usePickerSiteZones(enabled) {
|
|
56454
|
+
const trpcClient = useSystem().trpcClient;
|
|
56455
|
+
const { data, isError } = (0, _tanstack_react_query.useQuery)({
|
|
56456
|
+
queryKey: PICKER_ZONES_QUERY_KEY,
|
|
56457
|
+
enabled,
|
|
56458
|
+
staleTime: PICKER_ZONES_STALE_MS,
|
|
56459
|
+
queryFn: () => trpcClient.deviceManager.listSiteZones.query()
|
|
56460
|
+
});
|
|
56461
|
+
return (0, react$1.useMemo)(() => {
|
|
56462
|
+
if (data !== void 0) return {
|
|
56463
|
+
state: "answered",
|
|
56464
|
+
zones: data
|
|
56465
|
+
};
|
|
56466
|
+
return isError ? { state: "failed" } : { state: "pending" };
|
|
56467
|
+
}, [data, isError]);
|
|
56468
|
+
}
|
|
56469
|
+
//#endregion
|
|
56470
|
+
//#region src/composites/device-picker/device-selection.ts
|
|
56471
|
+
function deviceListStateOf(query) {
|
|
56472
|
+
if (query.data !== void 0) return "answered";
|
|
56473
|
+
return query.isError ? "failed" : "pending";
|
|
56474
|
+
}
|
|
56475
|
+
function resolveSelectedDevices(selectedIds, cameras, state) {
|
|
56476
|
+
if (state !== "answered" || cameras === void 0) return selectedIds.map((id) => ({
|
|
56477
|
+
id,
|
|
56478
|
+
removed: false
|
|
56479
|
+
}));
|
|
56480
|
+
const byId = new Map(cameras.map((cam) => [cam.id, cam.name]));
|
|
56481
|
+
return selectedIds.map((id) => {
|
|
56482
|
+
const name = byId.get(id);
|
|
56483
|
+
return name === void 0 ? {
|
|
56484
|
+
id,
|
|
56485
|
+
removed: true
|
|
56486
|
+
} : {
|
|
56487
|
+
id,
|
|
56488
|
+
name,
|
|
56489
|
+
removed: false
|
|
56490
|
+
};
|
|
56491
|
+
});
|
|
56492
|
+
}
|
|
56493
|
+
/**
|
|
56494
|
+
* The selected ids a picker's DRAWN list does not contain, judged against the
|
|
56495
|
+
* WHOLE fleet — never against the drawn list itself. A camera picker draws
|
|
56496
|
+
* cameras, but a rule's devices can be a sensor picked on another surface: the
|
|
56497
|
+
* camera list lacking it says nothing about whether it still exists. Each row
|
|
56498
|
+
* is `removed` (the answered fleet lacks it), named (the fleet has it) or
|
|
56499
|
+
* unnamed (the fleet has not answered).
|
|
56500
|
+
*/
|
|
56501
|
+
function offListSelections(selectedIds, listed, fleet, fleetState) {
|
|
56502
|
+
const listedIds = new Set((listed ?? []).map((device) => device.id));
|
|
56503
|
+
return resolveSelectedDevices(selectedIds.filter((id) => !listedIds.has(id)), fleet, fleetState);
|
|
56504
|
+
}
|
|
56505
|
+
/** The "no cameras" hint: only an ANSWERED empty list with nothing selected. */
|
|
56506
|
+
function showsNoCamerasHint(state, cameraCount, selectedCount) {
|
|
56507
|
+
return state === "answered" && cameraCount === 0 && selectedCount === 0;
|
|
56508
|
+
}
|
|
56509
|
+
/**
|
|
56510
|
+
* The widening warning: on a picker where an empty selection means ALL
|
|
56511
|
+
* devices, an empty selection that once held devices must say so.
|
|
56512
|
+
*/
|
|
56513
|
+
function emptyDevicesWidens(emptyMeansAll, selectedCount, hadDevices) {
|
|
56514
|
+
return emptyMeansAll && selectedCount === 0 && hadDevices;
|
|
56515
|
+
}
|
|
56516
|
+
//#endregion
|
|
56517
|
+
//#region src/composites/device-picker/device-selection-rows.tsx
|
|
56518
|
+
/** Each noun's plural. */
|
|
56519
|
+
var PICKER_NOUN_PLURAL = {
|
|
56520
|
+
camera: "cameras",
|
|
56521
|
+
device: "devices",
|
|
56522
|
+
node: "nodes",
|
|
56523
|
+
package: "packages",
|
|
56524
|
+
class: "classes",
|
|
56525
|
+
zone: "zones",
|
|
56526
|
+
face: "faces",
|
|
56527
|
+
calendar: "calendars"
|
|
56528
|
+
};
|
|
56529
|
+
/** The label a picker gives a selected id its drawn list does not contain. */
|
|
56530
|
+
function orphanDeviceLabel(row, noun = "device") {
|
|
56531
|
+
const id = String(row.id);
|
|
56532
|
+
switch (noun) {
|
|
56533
|
+
case "node":
|
|
56534
|
+
if (row.removed) return `Node ${id} — unknown to the hub`;
|
|
56535
|
+
return row.name ?? `Node ${id}`;
|
|
56536
|
+
case "package":
|
|
56537
|
+
if (row.removed) return `${id} — not installed`;
|
|
56538
|
+
return row.name ?? id;
|
|
56539
|
+
case "class":
|
|
56540
|
+
if (row.removed) return `${id} — not in the class list`;
|
|
56541
|
+
return row.name ?? id;
|
|
56542
|
+
case "zone":
|
|
56543
|
+
if (row.removed) return `${id} — zone not found`;
|
|
56544
|
+
return row.name ?? `${id} — zone unavailable`;
|
|
56545
|
+
case "face":
|
|
56546
|
+
if (row.removed) return `${id} — not in the face gallery`;
|
|
56547
|
+
return row.name ?? id;
|
|
56548
|
+
case "calendar":
|
|
56549
|
+
if (row.removed) return `${id} — calendar not found`;
|
|
56550
|
+
return row.name ?? id;
|
|
56551
|
+
case "camera":
|
|
56552
|
+
case "device":
|
|
56553
|
+
if (row.removed) return `Device #${id} — removed`;
|
|
56554
|
+
return row.name ?? `Device #${id}`;
|
|
56555
|
+
}
|
|
56556
|
+
}
|
|
56557
|
+
/** Said under a picker whose empty selection means EVERY device. */
|
|
56558
|
+
function DevicesWidenWarning({ noun = "camera" }) {
|
|
56559
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
56560
|
+
className: "mt-1 text-[11px] text-warning",
|
|
56561
|
+
"data-testid": "devices-widen-warning",
|
|
56562
|
+
children: [
|
|
56563
|
+
"No ",
|
|
56564
|
+
noun,
|
|
56565
|
+
" selected: the rule will apply to ALL ",
|
|
56566
|
+
PICKER_NOUN_PLURAL[noun],
|
|
56567
|
+
"."
|
|
56568
|
+
]
|
|
56569
|
+
});
|
|
56570
|
+
}
|
|
56571
|
+
/** A list that has not answered: never an absence claim (D315). */
|
|
56572
|
+
function DevicesLoadingLine({ noun = "cameras" }) {
|
|
56573
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
56574
|
+
className: "text-[11px] text-foreground-subtle",
|
|
56575
|
+
children: [
|
|
56576
|
+
"Loading ",
|
|
56577
|
+
noun,
|
|
56578
|
+
"…"
|
|
56579
|
+
]
|
|
56580
|
+
});
|
|
56581
|
+
}
|
|
56582
|
+
/** A list that failed: said, while the selected ids stay removable. */
|
|
56583
|
+
function DevicesFailedLine({ noun = "devices" }) {
|
|
56584
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
56585
|
+
className: "text-[11px] text-warning",
|
|
56586
|
+
children: [
|
|
56587
|
+
"The hub did not return its ",
|
|
56588
|
+
noun,
|
|
56589
|
+
" — selected ones are listed by id so they can still be removed."
|
|
56590
|
+
]
|
|
56591
|
+
});
|
|
56592
|
+
}
|
|
56593
|
+
//#endregion
|
|
56594
|
+
//#region src/composites/device-picker/DevicePicker.tsx
|
|
56595
|
+
/**
|
|
56596
|
+
* The smart device picker every notification editor uses: the selection as
|
|
56597
|
+
* compact chips, and a searchable, grouped list opened only to change it.
|
|
56598
|
+
*
|
|
56599
|
+
* It replaces the long checklists (a rule's devices, a system rule's
|
|
56600
|
+
* include/exclude, the timelapse and digest cameras, the alarm's non-blocking
|
|
56601
|
+
* openings) with the same UX the viewer's picker has (D636). The decisions are
|
|
56602
|
+
* `device-picker-model.ts` (shared with the viewer, byte-identical); what a
|
|
56603
|
+
* selected id the list does not draw IS — removed, named, or unknown — is
|
|
56604
|
+
* `device-selection.ts`, and so are the two rules that shaped it:
|
|
56605
|
+
*
|
|
56606
|
+
* - D315: only a list that ANSWERED may call an id removed or say "no
|
|
56607
|
+
* cameras"; a pending list is a loading line and names nothing absent.
|
|
56608
|
+
* - D49: a read never drops a selected id; every one is a chip the operator
|
|
56609
|
+
* can remove, and only the operator removes one.
|
|
56610
|
+
*
|
|
56611
|
+
* It reads nothing. It filters the list its caller already holds, and judges
|
|
56612
|
+
* an off-list id against the caller's fleet (a camera picker draws cameras,
|
|
56613
|
+
* and a selected sensor missing from it still exists).
|
|
56614
|
+
*
|
|
56615
|
+
* Generic over the id since D637: the system family's node and package pickers
|
|
56616
|
+
* (`scope-pickers.tsx`) are this picker over string ids, with their own noun
|
|
56617
|
+
* and group titles.
|
|
56618
|
+
*
|
|
56619
|
+
* Bounded at hundreds of devices of one type: big groups start collapsed, the
|
|
56620
|
+
* rows are rendered a page at a time with "Show N more", and the chips stop at
|
|
56621
|
+
* `PICKER_CHIP_LIMIT` with a "+N more". `showAll` turns all three off, and the
|
|
56622
|
+
* list is always open: the rule editor's Devices tab exists to show every
|
|
56623
|
+
* device (R15, D690).
|
|
56624
|
+
*
|
|
56625
|
+
* "Group by" (Zone | Location | None) sits at the top of a DEVICE picker's
|
|
56626
|
+
* list only — the node and package pickers keep their own groups, and so does
|
|
56627
|
+
* the CLASS picker (D641), whose groups are the taxonomy's categories. The choice
|
|
56628
|
+
* is remembered in `localStorage`, and the site zones are read whenever the
|
|
56629
|
+
* panel is open (the Zone filter needs them; `device-picker-grouping.ts`); a zone read that has
|
|
56630
|
+
* not answered groups by location with a one-line note, never "No zone"
|
|
56631
|
+
* (D315).
|
|
56632
|
+
*/
|
|
56633
|
+
/** A "Group by" group's title: the zone or room, or which one it lacks. */
|
|
56634
|
+
function headingTitle(heading) {
|
|
56635
|
+
switch (heading.kind) {
|
|
56636
|
+
case "zone":
|
|
56637
|
+
case "location": return heading.label;
|
|
56638
|
+
case "no-zone": return "No zone";
|
|
56639
|
+
case "no-location": return "No location";
|
|
56640
|
+
}
|
|
56641
|
+
}
|
|
56642
|
+
function deviceGroupTitle(group) {
|
|
56643
|
+
if (group.heading !== void 0) return headingTitle(group.heading);
|
|
56644
|
+
return `${typeLabel(group.type)} · ${group.location ?? "No room"}`;
|
|
56645
|
+
}
|
|
56646
|
+
/** Whether a picker lists devices — the only kind with "Group by". */
|
|
56647
|
+
function isDeviceNoun(noun) {
|
|
56648
|
+
return noun === "camera" || noun === "device";
|
|
56649
|
+
}
|
|
56650
|
+
function chipsOf(value, devices, fleet, noun) {
|
|
56651
|
+
const byId = new Map(devices.map((device) => [device.id, device.name]));
|
|
56652
|
+
const orphans = new Map(offListSelections(value, devices, fleet.devices, fleet.state).map((row) => [row.id, row]));
|
|
56653
|
+
return value.map((id) => {
|
|
56654
|
+
const name = byId.get(id);
|
|
56655
|
+
if (name !== void 0) return {
|
|
56656
|
+
id,
|
|
56657
|
+
label: name,
|
|
56658
|
+
removed: false
|
|
56659
|
+
};
|
|
56660
|
+
const row = orphans.get(id) ?? {
|
|
56661
|
+
id,
|
|
56662
|
+
removed: false
|
|
56663
|
+
};
|
|
56664
|
+
return {
|
|
56665
|
+
id,
|
|
56666
|
+
label: orphanDeviceLabel(row, noun),
|
|
56667
|
+
removed: row.removed
|
|
56668
|
+
};
|
|
56669
|
+
});
|
|
56670
|
+
}
|
|
56671
|
+
/** The empty-list sentence: the device pickers keep the words they shipped with. */
|
|
56672
|
+
function noItemsSentence(noun, emptyMeansAll) {
|
|
56673
|
+
const plural = PICKER_NOUN_PLURAL[noun];
|
|
56674
|
+
if (!emptyMeansAll) return `No ${plural} available.`;
|
|
56675
|
+
if (noun === "camera" || noun === "device") return "No cameras available — the rule applies to all devices.";
|
|
56676
|
+
return `No ${plural} available — the rule applies to every ${noun}.`;
|
|
56677
|
+
}
|
|
56678
|
+
function DevicePicker({ devices, listState, fleet, value, onChange, emptyMeansAll = false, savedHadDevices = false, noun = "camera", groupTitle = deviceGroupTitle, emptyLabel, annotate, disabled = false, testId, showAll = false }) {
|
|
56679
|
+
const judge = fleet ?? {
|
|
56680
|
+
state: listState,
|
|
56681
|
+
devices: listState === "answered" ? devices : void 0
|
|
56682
|
+
};
|
|
56683
|
+
const [open, setOpen] = (0, react$1.useState)(false);
|
|
56684
|
+
const [chipsExpanded, setChipsExpanded] = (0, react$1.useState)(false);
|
|
56685
|
+
const [hadDevices, setHadDevices] = (0, react$1.useState)(savedHadDevices || value.length > 0);
|
|
56686
|
+
if (value.length > 0 && !hadDevices) setHadDevices(true);
|
|
56687
|
+
const chips = chipsOf(value, devices, judge, noun);
|
|
56688
|
+
const overflow = chipOverflow(chips, chipsExpanded || showAll);
|
|
56689
|
+
const listOpen = open || showAll;
|
|
56690
|
+
const widening = emptyDevicesWidens(emptyMeansAll, value.length, hadDevices);
|
|
56691
|
+
const plural = PICKER_NOUN_PLURAL[noun];
|
|
56692
|
+
if (listState === "answered" && showsNoCamerasHint(listState, devices.length, value.length)) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56693
|
+
"data-testid": testId,
|
|
56694
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
56695
|
+
className: "text-[10px] text-foreground-subtle mt-0.5",
|
|
56696
|
+
children: noItemsSentence(noun, emptyMeansAll)
|
|
56697
|
+
}), widening && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesWidenWarning, { noun })]
|
|
56698
|
+
});
|
|
56699
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56700
|
+
className: "mt-1 space-y-1.5",
|
|
56701
|
+
"data-testid": testId,
|
|
56702
|
+
children: [
|
|
56703
|
+
listState === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesLoadingLine, { noun: plural }),
|
|
56704
|
+
listState === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesFailedLine, { noun: noun === "camera" ? "devices" : plural }),
|
|
56705
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56706
|
+
className: "flex flex-wrap items-center gap-1",
|
|
56707
|
+
children: [
|
|
56708
|
+
chips.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56709
|
+
className: "text-[11px] text-foreground-subtle",
|
|
56710
|
+
children: emptyLabel ?? (emptyMeansAll ? `Every ${noun} — no restriction` : "None selected")
|
|
56711
|
+
}) : overflow.shown.map((chip) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectedChip, {
|
|
56712
|
+
testId: `picker-chip-${String(chip.id)}`,
|
|
56713
|
+
label: chip.label,
|
|
56714
|
+
warning: chip.removed,
|
|
56715
|
+
...!disabled ? { onRemove: () => onChange(value.filter((id) => id !== chip.id)) } : {}
|
|
56716
|
+
}, chip.id)),
|
|
56717
|
+
overflow.hidden > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
56718
|
+
type: "button",
|
|
56719
|
+
className: "rounded-md border border-border px-1.5 py-0.5 text-[11px] text-foreground-subtle hover:bg-surface-hover",
|
|
56720
|
+
onClick: () => setChipsExpanded(true),
|
|
56721
|
+
children: [
|
|
56722
|
+
"+",
|
|
56723
|
+
overflow.hidden,
|
|
56724
|
+
" more"
|
|
56725
|
+
]
|
|
56726
|
+
}),
|
|
56727
|
+
chipsExpanded && chips.length > 6 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
56728
|
+
type: "button",
|
|
56729
|
+
className: "text-[11px] text-foreground-subtle underline",
|
|
56730
|
+
onClick: () => setChipsExpanded(false),
|
|
56731
|
+
children: "Show fewer"
|
|
56732
|
+
}),
|
|
56733
|
+
listState !== "pending" && !showAll && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
56734
|
+
type: "button",
|
|
56735
|
+
disabled,
|
|
56736
|
+
"aria-expanded": open,
|
|
56737
|
+
onClick: () => setOpen((o) => !o),
|
|
56738
|
+
className: "rounded-md border border-dashed border-border px-1.5 py-0.5 text-[11px] text-foreground hover:bg-surface-hover disabled:opacity-50",
|
|
56739
|
+
children: open ? "Done" : value.length === 0 ? `Add ${plural}…` : "Edit…"
|
|
56740
|
+
})
|
|
56741
|
+
]
|
|
56742
|
+
}),
|
|
56743
|
+
listOpen && listState === "answered" && (isDeviceNoun(noun) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedPickerPanel, {
|
|
56744
|
+
devices,
|
|
56745
|
+
value,
|
|
56746
|
+
onChange,
|
|
56747
|
+
annotate,
|
|
56748
|
+
groupTitle,
|
|
56749
|
+
noun,
|
|
56750
|
+
disabled,
|
|
56751
|
+
showAll
|
|
56752
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerPanel, {
|
|
56753
|
+
devices,
|
|
56754
|
+
value,
|
|
56755
|
+
onChange,
|
|
56756
|
+
annotate,
|
|
56757
|
+
groupTitle,
|
|
56758
|
+
noun,
|
|
56759
|
+
disabled,
|
|
56760
|
+
showAll
|
|
56761
|
+
})),
|
|
56762
|
+
widening && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesWidenWarning, { noun })
|
|
56763
|
+
]
|
|
56764
|
+
});
|
|
56765
|
+
}
|
|
56766
|
+
/**
|
|
56767
|
+
* The device picker's panel: mounted only while the list is open, so the zone
|
|
56768
|
+
* read below happens only then (for Zone grouping and the Zone filter).
|
|
56769
|
+
*/
|
|
56770
|
+
function GroupedPickerPanel(props) {
|
|
56771
|
+
const { groupBy, setGroupBy } = useStoredGroupBy();
|
|
56772
|
+
const zones = usePickerSiteZones(true);
|
|
56773
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerPanel, {
|
|
56774
|
+
...props,
|
|
56775
|
+
grouping: {
|
|
56776
|
+
groupBy,
|
|
56777
|
+
onGroupByChange: setGroupBy,
|
|
56778
|
+
zones
|
|
56779
|
+
}
|
|
56780
|
+
});
|
|
56781
|
+
}
|
|
56782
|
+
/** The search box's hint: a device has a room and a type, a class a category, a node or package an id. */
|
|
56783
|
+
function searchPlaceholder(noun) {
|
|
56784
|
+
if (noun === "camera" || noun === "device") return "Search by name, room or type…";
|
|
56785
|
+
if (noun === "class") return "Search by class or category…";
|
|
56786
|
+
if (noun === "zone") return "Search by camera or zone…";
|
|
56787
|
+
return "Search by name or id…";
|
|
56788
|
+
}
|
|
56789
|
+
/** An answered read with no zones: what the pickers without "Group by" are judged against. */
|
|
56790
|
+
var NO_ZONES = {
|
|
56791
|
+
state: "answered",
|
|
56792
|
+
zones: []
|
|
56793
|
+
};
|
|
56794
|
+
/** The searchable, grouped list — rendered one page at a time. */
|
|
56795
|
+
function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, showAll, grouping }) {
|
|
56796
|
+
const [filters, setFilters] = (0, react$1.useState)(PICKER_NO_FILTERS);
|
|
56797
|
+
const [toggles, setToggles] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
56798
|
+
const [shown, setShown] = (0, react$1.useState)(60);
|
|
56799
|
+
const searching = showAll || pickerQueryTokens(filters.query).length > 0;
|
|
56800
|
+
const groupBy = grouping?.groupBy;
|
|
56801
|
+
const zones = grouping?.zones ?? NO_ZONES;
|
|
56802
|
+
const matches = (0, react$1.useMemo)(() => applyPickerFilters(devices, filters, zones), [
|
|
56803
|
+
devices,
|
|
56804
|
+
filters,
|
|
56805
|
+
zones
|
|
56806
|
+
]);
|
|
56807
|
+
const grouped = (0, react$1.useMemo)(() => groupBy !== void 0 ? groupPickerDevicesBy(matches, value, groupBy, zones) : null, [
|
|
56808
|
+
matches,
|
|
56809
|
+
value,
|
|
56810
|
+
groupBy,
|
|
56811
|
+
zones
|
|
56812
|
+
]);
|
|
56813
|
+
const rows = (0, react$1.useMemo)(() => grouped !== null ? flattenPickerGrouping(grouped, value, toggles, searching) : flattenPickerRows(groupPickerDevices(matches, value), value, toggles, searching), [
|
|
56814
|
+
grouped,
|
|
56815
|
+
matches,
|
|
56816
|
+
value,
|
|
56817
|
+
toggles,
|
|
56818
|
+
searching
|
|
56819
|
+
]);
|
|
56820
|
+
const shownBy = grouped?.groupBy ?? null;
|
|
56821
|
+
const typeOptions = (0, react$1.useMemo)(() => pickerFacetOptions(devices, "type", filters.types, zones), [
|
|
56822
|
+
devices,
|
|
56823
|
+
filters.types,
|
|
56824
|
+
zones
|
|
56825
|
+
]);
|
|
56826
|
+
const zoneOptions = (0, react$1.useMemo)(() => pickerFacetOptions(devices, "zone", filters.zones, zones), [
|
|
56827
|
+
devices,
|
|
56828
|
+
filters.zones,
|
|
56829
|
+
zones
|
|
56830
|
+
]);
|
|
56831
|
+
const locationOptions = (0, react$1.useMemo)(() => grouping === void 0 ? [] : pickerFacetOptions(devices, "location", filters.locations, zones), [
|
|
56832
|
+
devices,
|
|
56833
|
+
filters.locations,
|
|
56834
|
+
zones,
|
|
56835
|
+
grouping
|
|
56836
|
+
]);
|
|
56837
|
+
const hiddenSelected = (0, react$1.useMemo)(() => pickerHiddenSelectedCount(value, devices, matches), [
|
|
56838
|
+
devices,
|
|
56839
|
+
value,
|
|
56840
|
+
matches
|
|
56841
|
+
]);
|
|
56842
|
+
const page = pageRows(rows, showAll ? rows.length : shown);
|
|
56843
|
+
const toggleGroup = (group, expanded) => setToggles((current) => new Map([...current, [group.key, !expanded]]));
|
|
56844
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56845
|
+
className: "rounded-md border border-border bg-surface p-2",
|
|
56846
|
+
"data-testid": "device-picker-panel",
|
|
56847
|
+
children: [
|
|
56848
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceFilterBar, {
|
|
56849
|
+
filters,
|
|
56850
|
+
onFiltersChange: (next) => {
|
|
56851
|
+
setFilters(next);
|
|
56852
|
+
setShown(60);
|
|
56853
|
+
},
|
|
56854
|
+
typeOptions,
|
|
56855
|
+
zoneOptions,
|
|
56856
|
+
locationOptions,
|
|
56857
|
+
zones,
|
|
56858
|
+
...grouping !== void 0 ? { grouping: {
|
|
56859
|
+
groupBy: grouping.groupBy,
|
|
56860
|
+
onGroupByChange: grouping.onGroupByChange,
|
|
56861
|
+
fallback: grouped?.fallback ?? null
|
|
56862
|
+
} } : {},
|
|
56863
|
+
searchPlaceholder: searchPlaceholder(noun),
|
|
56864
|
+
searchLabel: noun === "camera" || noun === "device" ? "Search devices" : `Search ${PICKER_NOUN_PLURAL[noun]}`,
|
|
56865
|
+
disabled
|
|
56866
|
+
}),
|
|
56867
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
56868
|
+
className: "mt-1 text-[10px] text-foreground-subtle",
|
|
56869
|
+
children: [
|
|
56870
|
+
matches.length,
|
|
56871
|
+
" of ",
|
|
56872
|
+
devices.length,
|
|
56873
|
+
" shown · ",
|
|
56874
|
+
value.length,
|
|
56875
|
+
" selected"
|
|
56876
|
+
]
|
|
56877
|
+
}),
|
|
56878
|
+
rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
56879
|
+
className: "py-2 text-[11px] text-foreground-subtle italic",
|
|
56880
|
+
children: noun === "camera" || noun === "device" ? "No device matches." : `No ${noun} matches.`
|
|
56881
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56882
|
+
className: showAll ? "mt-1" : "mt-1 max-h-72 overflow-y-auto",
|
|
56883
|
+
"data-testid": "device-picker-rows",
|
|
56884
|
+
children: [page.rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerRowView, {
|
|
56885
|
+
row,
|
|
56886
|
+
value,
|
|
56887
|
+
onChange,
|
|
56888
|
+
onToggleGroup: toggleGroup,
|
|
56889
|
+
annotate,
|
|
56890
|
+
groupTitle,
|
|
56891
|
+
groupBy: shownBy,
|
|
56892
|
+
disabled
|
|
56893
|
+
}, row.key)), page.remaining > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
56894
|
+
type: "button",
|
|
56895
|
+
className: "mt-1 w-full rounded border border-border py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover",
|
|
56896
|
+
onClick: () => setShown((n) => n + 60),
|
|
56897
|
+
children: [
|
|
56898
|
+
"Show ",
|
|
56899
|
+
Math.min(page.remaining, 60),
|
|
56900
|
+
" more (",
|
|
56901
|
+
page.remaining,
|
|
56902
|
+
" left)"
|
|
56903
|
+
]
|
|
56904
|
+
})]
|
|
56905
|
+
}),
|
|
56906
|
+
hiddenSelected > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
56907
|
+
className: "mt-1 text-[10px] text-foreground-subtle",
|
|
56908
|
+
"data-testid": "picker-hidden-selected",
|
|
56909
|
+
children: [hiddenSelected, " selected hidden by filters"]
|
|
56910
|
+
})
|
|
56911
|
+
]
|
|
56912
|
+
});
|
|
56913
|
+
}
|
|
56914
|
+
/** The second line: under "Group by" the room (where the header lacks it) and type, then the container. */
|
|
56915
|
+
function secondLine(device, groupBy) {
|
|
56916
|
+
if (groupBy === null) return device.detail;
|
|
56917
|
+
const facts = pickerRowFacts(device, groupBy);
|
|
56918
|
+
const parts = [
|
|
56919
|
+
facts.location,
|
|
56920
|
+
facts.type === null ? null : typeLabel(facts.type),
|
|
56921
|
+
device.detail ?? null
|
|
56922
|
+
].filter((part) => part !== null);
|
|
56923
|
+
return parts.length > 0 ? parts.join(" · ") : void 0;
|
|
56924
|
+
}
|
|
56925
|
+
function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled }) {
|
|
56926
|
+
if (row.kind === "header") {
|
|
56927
|
+
const { group, expanded } = row;
|
|
56928
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56929
|
+
className: "flex items-center gap-2 border-b border-border px-1 py-1",
|
|
56930
|
+
"data-testid": "device-picker-group",
|
|
56931
|
+
children: [
|
|
56932
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
56933
|
+
type: "button",
|
|
56934
|
+
"aria-expanded": expanded,
|
|
56935
|
+
onClick: () => onToggleGroup(group, expanded),
|
|
56936
|
+
className: "flex min-w-0 flex-1 items-center gap-1 text-left text-[10px] font-semibold uppercase tracking-wide text-foreground-subtle",
|
|
56937
|
+
children: [
|
|
56938
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: expanded ? "▾" : "▸" }),
|
|
56939
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56940
|
+
className: "truncate",
|
|
56941
|
+
children: groupTitle(group)
|
|
56942
|
+
}),
|
|
56943
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
56944
|
+
className: "font-normal normal-case",
|
|
56945
|
+
children: [
|
|
56946
|
+
"(",
|
|
56947
|
+
group.selectedCount,
|
|
56948
|
+
"/",
|
|
56949
|
+
group.devices.length,
|
|
56950
|
+
")"
|
|
56951
|
+
]
|
|
56952
|
+
})
|
|
56953
|
+
]
|
|
56954
|
+
}),
|
|
56955
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
56956
|
+
type: "button",
|
|
56957
|
+
disabled,
|
|
56958
|
+
className: "text-[10px] text-primary hover:underline disabled:opacity-50",
|
|
56959
|
+
onClick: () => onChange(selectPickerGroup(value, group)),
|
|
56960
|
+
children: "All"
|
|
56961
|
+
}),
|
|
56962
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
56963
|
+
type: "button",
|
|
56964
|
+
disabled,
|
|
56965
|
+
className: "text-[10px] text-foreground-subtle hover:underline disabled:opacity-50",
|
|
56966
|
+
onClick: () => onChange(clearPickerGroup(value, group)),
|
|
56967
|
+
children: "Clear"
|
|
56968
|
+
})
|
|
56969
|
+
]
|
|
56970
|
+
});
|
|
56971
|
+
}
|
|
56972
|
+
const note = annotate?.(row.device.id);
|
|
56973
|
+
const second = secondLine(row.device, groupBy);
|
|
56974
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
56975
|
+
className: "flex cursor-pointer items-center gap-2.5 px-3 py-1 text-xs text-foreground hover:bg-surface-hover",
|
|
56976
|
+
"data-testid": "device-picker-row",
|
|
56977
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
56978
|
+
type: "checkbox",
|
|
56979
|
+
checked: row.selected,
|
|
56980
|
+
disabled,
|
|
56981
|
+
onChange: () => onChange(togglePickerDevice(value, row.device.id)),
|
|
56982
|
+
className: "accent-primary",
|
|
56983
|
+
"data-device-id": String(row.device.id)
|
|
56984
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
56985
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
56986
|
+
children: [
|
|
56987
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56988
|
+
className: "truncate",
|
|
56989
|
+
children: row.device.name
|
|
56990
|
+
}),
|
|
56991
|
+
second !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56992
|
+
className: "truncate text-[10px] text-foreground-subtle",
|
|
56993
|
+
children: second
|
|
56994
|
+
}),
|
|
56995
|
+
note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56996
|
+
className: "text-[10px] text-warning",
|
|
56997
|
+
children: note
|
|
56998
|
+
})
|
|
56999
|
+
]
|
|
57000
|
+
})]
|
|
57001
|
+
});
|
|
57002
|
+
}
|
|
57003
|
+
//#endregion
|
|
57004
|
+
//#region src/composites/unit-picker/UnitPicker.tsx
|
|
57005
|
+
/**
|
|
57006
|
+
* Same-dimension unit picker. Lists only `unitsForDimension(dimension)`; an
|
|
57007
|
+
* empty dimension disables the control and shows `emptyReason` so the operator
|
|
57008
|
+
* never sees a refused conversion as a pickable option.
|
|
57009
|
+
*/
|
|
57010
|
+
var FIELD_CLASS = "w-full min-w-0 px-1.5 py-0.5 text-[11px] bg-background border border-border rounded focus:outline-none focus:border-primary disabled:opacity-50 disabled:cursor-not-allowed";
|
|
57011
|
+
function UnitPicker({ dimension, value, onChange, disabled = false, emptyReason, inheritLabel, className, id, "aria-label": ariaLabel }) {
|
|
57012
|
+
const choices = dimension !== void 0 ? (0, _camstack_types.unitsForDimension)(dimension) : [];
|
|
57013
|
+
const isEmpty = dimension === void 0;
|
|
57014
|
+
const selectDisabled = disabled || isEmpty;
|
|
57015
|
+
const handleChange = (event) => {
|
|
57016
|
+
const raw = event.target.value;
|
|
57017
|
+
if (raw === "") {
|
|
57018
|
+
onChange("");
|
|
57019
|
+
return;
|
|
57020
|
+
}
|
|
57021
|
+
onChange((0, _camstack_types.normalizeUnit)(raw) ?? raw);
|
|
57022
|
+
};
|
|
57023
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
57024
|
+
className: "flex flex-col gap-0.5 min-w-0 w-full",
|
|
57025
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
57026
|
+
id,
|
|
57027
|
+
className: className ?? FIELD_CLASS,
|
|
57028
|
+
"aria-label": ariaLabel,
|
|
57029
|
+
value,
|
|
57030
|
+
onChange: handleChange,
|
|
57031
|
+
disabled: selectDisabled,
|
|
57032
|
+
children: [inheritLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
57033
|
+
value: "",
|
|
57034
|
+
children: inheritLabel
|
|
57035
|
+
}), choices.map((unit) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
57036
|
+
value: unit,
|
|
57037
|
+
children: unit
|
|
57038
|
+
}, unit))]
|
|
57039
|
+
}), isEmpty && emptyReason !== void 0 && emptyReason !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
57040
|
+
className: "text-[10px] text-foreground-subtle",
|
|
57041
|
+
children: emptyReason
|
|
57042
|
+
})]
|
|
57043
|
+
});
|
|
57044
|
+
}
|
|
57045
|
+
//#endregion
|
|
57046
|
+
//#region src/composites/placeholder-field/PlaceholderChipField.tsx
|
|
57047
|
+
/**
|
|
57048
|
+
* Text field with insertable placeholder chips — the same interaction model
|
|
57049
|
+
* as Notification Center's TemplateField, without the NC variable catalog.
|
|
57050
|
+
*/
|
|
57051
|
+
var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1";
|
|
57052
|
+
var TEXTAREA_CLASS = "w-full rounded-md border border-border bg-surface px-2.5 py-1.5 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary";
|
|
57053
|
+
function insertAtSelection(value, selection, token) {
|
|
57054
|
+
const before = value.slice(0, selection.start);
|
|
57055
|
+
return {
|
|
57056
|
+
value: `${before}${token}${value.slice(selection.end)}`,
|
|
57057
|
+
cursor: before.length + token.length
|
|
57058
|
+
};
|
|
57059
|
+
}
|
|
57060
|
+
function PlaceholderChipField({ label, value, onChange, chips, multiline = false, placeholder, disabled = false, testId, maxLength }) {
|
|
57061
|
+
const inputRef = (0, react$1.useRef)(null);
|
|
57062
|
+
const areaRef = (0, react$1.useRef)(null);
|
|
57063
|
+
const insert = (token) => {
|
|
57064
|
+
const element = multiline ? areaRef.current : inputRef.current;
|
|
57065
|
+
const result = insertAtSelection(value, {
|
|
57066
|
+
start: element?.selectionStart ?? value.length,
|
|
57067
|
+
end: element?.selectionEnd ?? value.length
|
|
57068
|
+
}, token);
|
|
57069
|
+
onChange(result.value);
|
|
57070
|
+
requestAnimationFrame(() => {
|
|
57071
|
+
element?.focus();
|
|
57072
|
+
element?.setSelectionRange(result.cursor, result.cursor);
|
|
57073
|
+
});
|
|
57074
|
+
};
|
|
57075
|
+
const handle = (e) => onChange(e.target.value);
|
|
57076
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
57077
|
+
className: "space-y-1",
|
|
57078
|
+
"data-testid": testId,
|
|
57079
|
+
children: [
|
|
57080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
57081
|
+
className: LABEL_CLASS,
|
|
57082
|
+
children: label
|
|
57083
|
+
}),
|
|
57084
|
+
multiline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
57085
|
+
ref: areaRef,
|
|
57086
|
+
value,
|
|
57087
|
+
rows: 2,
|
|
57088
|
+
disabled,
|
|
57089
|
+
placeholder,
|
|
57090
|
+
maxLength,
|
|
57091
|
+
onChange: handle,
|
|
57092
|
+
className: TEXTAREA_CLASS,
|
|
57093
|
+
"data-testid": `${testId}-input`
|
|
57094
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
57095
|
+
ref: inputRef,
|
|
57096
|
+
value,
|
|
57097
|
+
disabled,
|
|
57098
|
+
placeholder,
|
|
57099
|
+
maxLength,
|
|
57100
|
+
onChange: handle,
|
|
57101
|
+
"data-testid": `${testId}-input`
|
|
57102
|
+
}),
|
|
57103
|
+
chips !== void 0 && chips.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
57104
|
+
className: "flex flex-wrap gap-1 pt-0.5",
|
|
57105
|
+
"data-testid": `${testId}-chips`,
|
|
57106
|
+
children: chips.map((chip) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
57107
|
+
type: "button",
|
|
57108
|
+
disabled,
|
|
57109
|
+
className: "rounded-md bg-primary/10 px-1.5 py-0.5 text-[10px] text-primary hover:bg-primary/20 disabled:opacity-50",
|
|
57110
|
+
onClick: () => insert(chip.token),
|
|
57111
|
+
"data-testid": `${testId}-chip-${chip.id}`,
|
|
57112
|
+
children: chip.label
|
|
57113
|
+
}, chip.id))
|
|
57114
|
+
})
|
|
57115
|
+
]
|
|
57116
|
+
});
|
|
57117
|
+
}
|
|
57118
|
+
//#endregion
|
|
55747
57119
|
//#region src/contexts/zone-editing.tsx
|
|
55748
57120
|
/**
|
|
55749
57121
|
* Per-device zone-editing state shared between the player overlay
|
|
@@ -56761,6 +58133,7 @@ exports.DEFAULT_COLOR = DEFAULT_COLOR;
|
|
|
56761
58133
|
exports.DEVICE_COLUMNS = DEVICE_COLUMNS;
|
|
56762
58134
|
exports.DEVICE_LIST_PAGE_SIZE_KEY = DEVICE_LIST_PAGE_SIZE_KEY;
|
|
56763
58135
|
exports.DEVICE_LIST_PAGE_SIZE_OPTIONS = DEVICE_LIST_PAGE_SIZE_OPTIONS;
|
|
58136
|
+
exports.DEVICE_PICKER_GROUP_STORAGE_KEY = DEVICE_PICKER_GROUP_STORAGE_KEY;
|
|
56764
58137
|
exports.DEVICE_ROLE_META = DEVICE_ROLE_META;
|
|
56765
58138
|
exports.DEVICE_TYPE_CONTROL = DEVICE_TYPE_CONTROL;
|
|
56766
58139
|
exports.DEVICE_TYPE_META = DEVICE_TYPE_META;
|
|
@@ -56776,13 +58149,18 @@ exports.DeviceBatchToolbar = DeviceBatchToolbar;
|
|
|
56776
58149
|
exports.DeviceCard = DeviceCard;
|
|
56777
58150
|
exports.DeviceContextProvider = DeviceContextProvider;
|
|
56778
58151
|
exports.DeviceExportPanel = DeviceExportPanel;
|
|
58152
|
+
exports.DeviceFilterBar = DeviceFilterBar;
|
|
56779
58153
|
exports.DeviceGrid = DeviceGrid;
|
|
56780
58154
|
exports.DeviceItem = DeviceItem;
|
|
56781
58155
|
exports.DeviceList = DeviceList;
|
|
56782
58156
|
exports.DeviceMultiSelectField = DeviceMultiSelectField;
|
|
58157
|
+
exports.DevicePicker = DevicePicker;
|
|
56783
58158
|
exports.DeviceSelectField = DeviceSelectField;
|
|
56784
58159
|
exports.DeviceSelectorPicker = DeviceSelectorPicker;
|
|
56785
58160
|
exports.DeviceStepMatrix = DeviceStepMatrix;
|
|
58161
|
+
exports.DevicesFailedLine = DevicesFailedLine;
|
|
58162
|
+
exports.DevicesLoadingLine = DevicesLoadingLine;
|
|
58163
|
+
exports.DevicesWidenWarning = DevicesWidenWarning;
|
|
56786
58164
|
exports.Dialog = Dialog;
|
|
56787
58165
|
exports.DialogBody = DialogBody;
|
|
56788
58166
|
exports.DialogContent = DialogContent;
|
|
@@ -56831,6 +58209,7 @@ exports.HoverZoomImage = HoverZoomImage;
|
|
|
56831
58209
|
exports.HumidifierHeroCard = HumidifierHeroCard;
|
|
56832
58210
|
exports.HumidifierInlineControl = HumidifierInlineControl;
|
|
56833
58211
|
exports.INPUT_COMPACT = INPUT_COMPACT;
|
|
58212
|
+
exports.INPUT_SHELL = INPUT_SHELL;
|
|
56834
58213
|
exports.IconAction = IconAction;
|
|
56835
58214
|
exports.IconButton = IconButton;
|
|
56836
58215
|
exports.IconPicker = IconPicker;
|
|
@@ -56871,6 +58250,16 @@ exports.NodePicker = NodePicker;
|
|
|
56871
58250
|
exports.NodeSelectField = NodeSelectField;
|
|
56872
58251
|
exports.OfflineBadge = OfflineBadge;
|
|
56873
58252
|
exports.PHASE_CONFIG = PHASE_CONFIG;
|
|
58253
|
+
exports.PICKER_CHIP_LIMIT = PICKER_CHIP_LIMIT;
|
|
58254
|
+
exports.PICKER_DEFAULT_GROUP_BY = PICKER_DEFAULT_GROUP_BY;
|
|
58255
|
+
exports.PICKER_GROUP_BY_MODES = PICKER_GROUP_BY_MODES;
|
|
58256
|
+
exports.PICKER_GROUP_COLLAPSE_AT = PICKER_GROUP_COLLAPSE_AT;
|
|
58257
|
+
exports.PICKER_NONE_KEY = PICKER_NONE_KEY;
|
|
58258
|
+
exports.PICKER_NOUN_PLURAL = PICKER_NOUN_PLURAL;
|
|
58259
|
+
exports.PICKER_NO_FILTERS = PICKER_NO_FILTERS;
|
|
58260
|
+
exports.PICKER_PAGE_SIZE = PICKER_PAGE_SIZE;
|
|
58261
|
+
exports.PICKER_SEARCH_DEBOUNCE_MS = PICKER_SEARCH_DEBOUNCE_MS;
|
|
58262
|
+
exports.PICKER_WINDOW = PICKER_WINDOW;
|
|
56874
58263
|
exports.PRIORITY = PRIORITY;
|
|
56875
58264
|
exports.PTZOverlay = PTZOverlay;
|
|
56876
58265
|
exports.PageHeader = PageHeader;
|
|
@@ -56879,6 +58268,7 @@ exports.PipelineBuilder = PipelineBuilder;
|
|
|
56879
58268
|
exports.PipelineRuntimeSelector = PipelineRuntimeSelector;
|
|
56880
58269
|
exports.PipelineStep = PipelineStep;
|
|
56881
58270
|
exports.PipelineTreeMatrix = PipelineTreeMatrix;
|
|
58271
|
+
exports.PlaceholderChipField = PlaceholderChipField;
|
|
56882
58272
|
exports.PlayerOverlaysProvider = PlayerOverlaysProvider;
|
|
56883
58273
|
exports.Popover = Popover;
|
|
56884
58274
|
exports.PopoverContent = PopoverContent;
|
|
@@ -56922,6 +58312,7 @@ exports.ScriptHeroCard = ScriptHeroCard;
|
|
|
56922
58312
|
exports.ScriptInlineControl = ScriptInlineControl;
|
|
56923
58313
|
exports.ScrollArea = ScrollArea;
|
|
56924
58314
|
exports.Select = Select;
|
|
58315
|
+
exports.SelectedChip = SelectedChip;
|
|
56925
58316
|
exports.SemanticBadge = SemanticBadge;
|
|
56926
58317
|
exports.SensorHeroCard = SensorHeroCard;
|
|
56927
58318
|
exports.SensorInlineControl = SensorInlineControl;
|
|
@@ -56970,6 +58361,7 @@ exports.Tooltip = Tooltip;
|
|
|
56970
58361
|
exports.TooltipContent = TooltipContent;
|
|
56971
58362
|
exports.TooltipTrigger = TooltipTrigger;
|
|
56972
58363
|
exports.Trash2 = Trash2;
|
|
58364
|
+
exports.UnitPicker = UnitPicker;
|
|
56973
58365
|
exports.VacuumHeroCard = VacuumHeroCard;
|
|
56974
58366
|
exports.VacuumInlineControl = VacuumInlineControl;
|
|
56975
58367
|
exports.ValueReadout = ValueReadout;
|
|
@@ -56989,13 +58381,16 @@ exports.ZoneEditingProvider = ZoneEditingProvider;
|
|
|
56989
58381
|
exports.agentColumnKey = agentColumnKey;
|
|
56990
58382
|
exports.allDeviceTypeFilterOptions = allDeviceTypeFilterOptions;
|
|
56991
58383
|
exports.applyFleetQueryDefaults = applyFleetQueryDefaults;
|
|
58384
|
+
exports.applyPickerFilters = applyPickerFilters;
|
|
56992
58385
|
exports.ariaSortForColumn = ariaSortForColumn;
|
|
56993
58386
|
exports.badgeFor = badgeFor;
|
|
56994
58387
|
exports.badgeFor$1 = badgeFor;
|
|
56995
58388
|
exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
|
|
56996
58389
|
exports.childEntityId = childEntityId;
|
|
56997
58390
|
exports.childListName = childListName;
|
|
58391
|
+
exports.chipOverflow = chipOverflow;
|
|
56998
58392
|
exports.classifyCapability = classifyCapability;
|
|
58393
|
+
exports.clearPickerGroup = clearPickerGroup;
|
|
56999
58394
|
exports.cn = cn;
|
|
57000
58395
|
exports.columnsForContext = columnsForContext;
|
|
57001
58396
|
exports.containerChildToRef = containerChildToRef;
|
|
@@ -57014,6 +58409,7 @@ exports.deriveDeviceKind = deriveDeviceKind;
|
|
|
57014
58409
|
exports.deriveWebrtcDebugStats = deriveWebrtcDebugStats;
|
|
57015
58410
|
exports.describeDeviceSelector = describeDeviceSelector;
|
|
57016
58411
|
exports.deviceIntegrationResolver = deviceIntegrationResolver;
|
|
58412
|
+
exports.deviceListStateOf = deviceListStateOf;
|
|
57017
58413
|
exports.deviceMatchesFilter = deviceMatchesFilter;
|
|
57018
58414
|
exports.deviceOptionLabel = deviceOptionLabel;
|
|
57019
58415
|
exports.deviceRoleMeta = deviceRoleMeta;
|
|
@@ -57022,11 +58418,15 @@ exports.deviceTypeLabel = deviceTypeLabel;
|
|
|
57022
58418
|
exports.deviceTypeMeta = deviceTypeMeta;
|
|
57023
58419
|
exports.deviceTypeMetaOf = deviceTypeMetaOf;
|
|
57024
58420
|
exports.devicesToOptions = devicesToOptions;
|
|
58421
|
+
exports.emptyDevicesWidens = emptyDevicesWidens;
|
|
57025
58422
|
exports.ensureMfHostInit = ensureMfHostInit;
|
|
57026
58423
|
exports.eventKindLabel = eventKindLabel;
|
|
57027
58424
|
exports.filterDataTableRows = filterDataTableRows;
|
|
57028
58425
|
exports.filterDeviceOptions = filterDeviceOptions;
|
|
58426
|
+
exports.filterPickerDevices = filterPickerDevices;
|
|
57029
58427
|
exports.findTimezone = _camstack_types.findTimezone;
|
|
58428
|
+
exports.flattenPickerGrouping = flattenPickerGrouping;
|
|
58429
|
+
exports.flattenPickerRows = flattenPickerRows;
|
|
57030
58430
|
exports.formatControlDateTime = formatControlDateTime;
|
|
57031
58431
|
exports.formatLastSeen = formatLastSeen;
|
|
57032
58432
|
exports.formatNumeric = formatNumeric;
|
|
@@ -57036,6 +58436,8 @@ exports.getClassColor = getClassColor;
|
|
|
57036
58436
|
exports.getPhaseVisual = getPhaseVisual;
|
|
57037
58437
|
exports.groupAgentColumns = groupAgentColumns;
|
|
57038
58438
|
exports.groupChildrenByLayout = groupChildrenByLayout;
|
|
58439
|
+
exports.groupPickerDevices = groupPickerDevices;
|
|
58440
|
+
exports.groupPickerDevicesBy = groupPickerDevicesBy;
|
|
57039
58441
|
exports.hardwareLabel = hardwareLabel;
|
|
57040
58442
|
exports.hubOrigin = hubOrigin;
|
|
57041
58443
|
exports.hubPath = hubPath;
|
|
@@ -57045,10 +58447,12 @@ exports.hubWsUrl = hubWsUrl;
|
|
|
57045
58447
|
exports.humanDuration = humanDuration;
|
|
57046
58448
|
exports.humidifierTint = humidifierTint;
|
|
57047
58449
|
exports.initialScrubState = initialScrubState;
|
|
58450
|
+
exports.insertAtSelection = insertAtSelection;
|
|
57048
58451
|
exports.isAbsentProvider = isAbsentProvider;
|
|
57049
58452
|
exports.isAbsentProvider$1 = isAbsentProvider;
|
|
57050
58453
|
exports.isDataTableFilterActive = isDataTableFilterActive;
|
|
57051
58454
|
exports.isFieldVisible = isFieldVisible;
|
|
58455
|
+
exports.isGroupExpanded = isGroupExpanded;
|
|
57052
58456
|
exports.lawnMowerActivityMeta = lawnMowerActivityMeta;
|
|
57053
58457
|
exports.lightColors = require_theme_index.lightColors$1;
|
|
57054
58458
|
exports.loadRemoteBundle = loadRemoteBundle;
|
|
@@ -57063,11 +58467,28 @@ exports.nextSort = nextSort;
|
|
|
57063
58467
|
exports.nextSortDirection = nextSortDirection;
|
|
57064
58468
|
exports.nextTableSort = nextTableSort;
|
|
57065
58469
|
exports.normalizeForSearch = normalizeForSearch;
|
|
58470
|
+
exports.offListSelections = offListSelections;
|
|
57066
58471
|
exports.openTopLayer = openTopLayer;
|
|
58472
|
+
exports.orphanDeviceLabel = orphanDeviceLabel;
|
|
57067
58473
|
exports.overrideEntityIdFromLink = overrideEntityIdFromLink;
|
|
58474
|
+
exports.pageRows = pageRows;
|
|
58475
|
+
exports.parsePickerGroupBy = parsePickerGroupBy;
|
|
57068
58476
|
exports.parseRecordedServerMessage = parseRecordedServerMessage;
|
|
58477
|
+
exports.pickerFacetFilterShown = pickerFacetFilterShown;
|
|
58478
|
+
exports.pickerFacetKey = pickerFacetKey;
|
|
58479
|
+
exports.pickerFacetOptions = pickerFacetOptions;
|
|
58480
|
+
exports.pickerFiltersActive = pickerFiltersActive;
|
|
58481
|
+
exports.pickerGroupKey = pickerGroupKey;
|
|
58482
|
+
exports.pickerHiddenSelectedCount = pickerHiddenSelectedCount;
|
|
58483
|
+
exports.pickerLocationKey = pickerLocationKey;
|
|
58484
|
+
exports.pickerQueryTokens = pickerQueryTokens;
|
|
58485
|
+
exports.pickerRowFacts = pickerRowFacts;
|
|
58486
|
+
exports.pickerSearchText = pickerSearchText;
|
|
58487
|
+
exports.pickerZoneFilterSuspended = pickerZoneFilterSuspended;
|
|
57069
58488
|
exports.providerIcons = providerIcons;
|
|
58489
|
+
exports.readStoredGroupBy = readStoredGroupBy;
|
|
57070
58490
|
exports.resetHubMountForTests = resetHubMountForTests;
|
|
58491
|
+
exports.resetPickerFilters = resetPickerFilters;
|
|
57071
58492
|
exports.resolveContainerPrimary = resolveContainerPrimary;
|
|
57072
58493
|
exports.resolveControlAlign = resolveControlAlign;
|
|
57073
58494
|
exports.resolveDeviceControl = resolveDeviceControl;
|
|
@@ -57077,6 +58498,7 @@ exports.resolveEffectiveDefaultModel = resolveEffectiveDefaultModel;
|
|
|
57077
58498
|
exports.resolveEffectiveStepModel = resolveEffectiveStepModel;
|
|
57078
58499
|
exports.resolveEventKindIcon = resolveEventKindIcon;
|
|
57079
58500
|
exports.resolvePrimaryChild = resolvePrimaryChild;
|
|
58501
|
+
exports.resolveSelectedDevices = resolveSelectedDevices;
|
|
57080
58502
|
exports.resolveSensorDisplay = resolveSensorDisplay;
|
|
57081
58503
|
exports.resolveStepDefaultModel = resolveStepDefaultModel;
|
|
57082
58504
|
exports.resolveTableLayout = resolveTableLayout;
|
|
@@ -57084,11 +58506,13 @@ exports.roiTooLarge = roiTooLarge;
|
|
|
57084
58506
|
exports.roiTooLarge$1 = roiTooLarge;
|
|
57085
58507
|
exports.routerBasename = routerBasename;
|
|
57086
58508
|
exports.scrubReducer = scrubReducer;
|
|
58509
|
+
exports.selectPickerGroup = selectPickerGroup;
|
|
57087
58510
|
exports.selectedDeviceOptions = selectedDeviceOptions;
|
|
57088
58511
|
exports.serializeRecordedCommand = serializeRecordedCommand;
|
|
57089
58512
|
exports.shouldCommit = shouldCommit;
|
|
57090
58513
|
exports.shouldEmit = shouldEmit;
|
|
57091
58514
|
exports.shouldUseSingleNode = shouldUseSingleNode;
|
|
58515
|
+
exports.showsNoCamerasHint = showsNoCamerasHint;
|
|
57092
58516
|
exports.sortRows = sortRows;
|
|
57093
58517
|
exports.sortRowsByColumn = sortRowsByColumn;
|
|
57094
58518
|
exports.statusIcons = statusIcons;
|
|
@@ -57102,7 +58526,10 @@ exports.tankAlert = tankAlert;
|
|
|
57102
58526
|
exports.themeToCss = require_theme_index.themeToCss$1;
|
|
57103
58527
|
exports.toStationaryOverlayBox = toStationaryOverlayBox;
|
|
57104
58528
|
exports.toggleDataTableFacet = toggleDataTableFacet;
|
|
58529
|
+
exports.togglePickerDevice = togglePickerDevice;
|
|
58530
|
+
exports.togglePickerKey = togglePickerKey;
|
|
57105
58531
|
exports.trpc = trpc;
|
|
58532
|
+
exports.typeLabel = typeLabel;
|
|
57106
58533
|
exports.useAccessoriesGetStatus = useAccessoriesGetStatus;
|
|
57107
58534
|
exports.useAccessoriesSetChildHidden = useAccessoriesSetChildHidden;
|
|
57108
58535
|
exports.useAddonPagesListPages = useAddonPagesListPages;
|
|
@@ -57225,6 +58652,9 @@ exports.useBrokerTestConnection = useBrokerTestConnection;
|
|
|
57225
58652
|
exports.useBrokerTestSettings = useBrokerTestSettings;
|
|
57226
58653
|
exports.useBrokerUnsubscribe = useBrokerUnsubscribe;
|
|
57227
58654
|
exports.useButtonPress = useButtonPress;
|
|
58655
|
+
exports.useCalendarsImportFromHomeAssistant = useCalendarsImportFromHomeAssistant;
|
|
58656
|
+
exports.useCalendarsListCalendars = useCalendarsListCalendars;
|
|
58657
|
+
exports.useCalendarsListEvents = useCalendarsListEvents;
|
|
57228
58658
|
exports.useCameraCredentialsGetCredentials = useCameraCredentialsGetCredentials;
|
|
57229
58659
|
exports.useCameraCredentialsGetStatus = useCameraCredentialsGetStatus;
|
|
57230
58660
|
exports.useCameraGridLayoutApplyDeviceSettingsPatch = useCameraGridLayoutApplyDeviceSettingsPatch;
|
|
@@ -57715,12 +59145,15 @@ exports.useOptionalWidgetRegistry = useOptionalWidgetRegistry;
|
|
|
57715
59145
|
exports.useOsdGetStatus = useOsdGetStatus;
|
|
57716
59146
|
exports.useOsdManagerClearSlotBinding = useOsdManagerClearSlotBinding;
|
|
57717
59147
|
exports.useOsdManagerCopyDeviceConfiguration = useOsdManagerCopyDeviceConfiguration;
|
|
59148
|
+
exports.useOsdManagerDeleteTemplate = useOsdManagerDeleteTemplate;
|
|
57718
59149
|
exports.useOsdManagerGetConditionSupport = useOsdManagerGetConditionSupport;
|
|
57719
59150
|
exports.useOsdManagerGetDeviceOsd = useOsdManagerGetDeviceOsd;
|
|
57720
59151
|
exports.useOsdManagerGetSourceCatalog = useOsdManagerGetSourceCatalog;
|
|
59152
|
+
exports.useOsdManagerListTemplates = useOsdManagerListTemplates;
|
|
57721
59153
|
exports.useOsdManagerPreviewSlot = useOsdManagerPreviewSlot;
|
|
57722
59154
|
exports.useOsdManagerRenderDevice = useOsdManagerRenderDevice;
|
|
57723
59155
|
exports.useOsdManagerSetSlotBinding = useOsdManagerSetSlotBinding;
|
|
59156
|
+
exports.useOsdManagerUpsertTemplate = useOsdManagerUpsertTemplate;
|
|
57724
59157
|
exports.useOsdSetOverlay = useOsdSetOverlay;
|
|
57725
59158
|
exports.usePTZ = usePTZ;
|
|
57726
59159
|
exports.usePetFeederCallPet = usePetFeederCallPet;
|
|
@@ -57734,6 +59167,7 @@ exports.usePetFeederSetChildLock = usePetFeederSetChildLock;
|
|
|
57734
59167
|
exports.usePetFeederSetFeedSound = usePetFeederSetFeedSound;
|
|
57735
59168
|
exports.usePetFeederSetIndicatorLight = usePetFeederSetIndicatorLight;
|
|
57736
59169
|
exports.usePetFeederSetVolume = usePetFeederSetVolume;
|
|
59170
|
+
exports.usePickerSiteZones = usePickerSiteZones;
|
|
57737
59171
|
exports.usePipelineAnalyticsApplyDeviceSettingsPatch = usePipelineAnalyticsApplyDeviceSettingsPatch;
|
|
57738
59172
|
exports.usePipelineAnalyticsCancelRelocateMedia = usePipelineAnalyticsCancelRelocateMedia;
|
|
57739
59173
|
exports.usePipelineAnalyticsCancelStorageMigrationMove = usePipelineAnalyticsCancelStorageMigrationMove;
|
|
@@ -58093,6 +59527,7 @@ exports.useStorageTestLocation = useStorageTestLocation;
|
|
|
58093
59527
|
exports.useStorageUpsertLocation = useStorageUpsertLocation;
|
|
58094
59528
|
exports.useStorageWrite = useStorageWrite;
|
|
58095
59529
|
exports.useStorageWriteChunk = useStorageWriteChunk;
|
|
59530
|
+
exports.useStoredGroupBy = useStoredGroupBy;
|
|
58096
59531
|
exports.useStreamBrokerAcquireEgressTranscode = useStreamBrokerAcquireEgressTranscode;
|
|
58097
59532
|
exports.useStreamBrokerApplyDeviceSettingsPatch = useStreamBrokerApplyDeviceSettingsPatch;
|
|
58098
59533
|
exports.useStreamBrokerAssignProfile = useStreamBrokerAssignProfile;
|
|
@@ -58267,3 +59702,4 @@ exports.waterHeaterPhase = waterHeaterPhase;
|
|
|
58267
59702
|
exports.waterHeaterTint = waterHeaterTint;
|
|
58268
59703
|
exports.weatherConditionMeta = weatherConditionMeta;
|
|
58269
59704
|
exports.weatherTint = weatherTint;
|
|
59705
|
+
exports.writeStoredGroupBy = writeStoredGroupBy;
|