@camstack/ui-library 1.2.113 → 1.2.114
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-select-filter.d.ts +28 -2
- package/dist/index.cjs +134 -10
- package/dist/index.js +134 -10
- package/package.json +1 -1
|
@@ -20,6 +20,12 @@ export interface DeviceSelectCandidate {
|
|
|
20
20
|
readonly name: string;
|
|
21
21
|
readonly location?: string | null;
|
|
22
22
|
readonly features?: readonly string[];
|
|
23
|
+
/** Name of the container this device lives in, when it has one. */
|
|
24
|
+
readonly parentName?: string | null;
|
|
25
|
+
}
|
|
26
|
+
/** A picker option that still knows where its device is. */
|
|
27
|
+
export interface DeviceOption extends ConfigOption {
|
|
28
|
+
readonly location: string | null;
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* Does a device satisfy the filter?
|
|
@@ -37,13 +43,33 @@ export declare function deviceOptionLabel(device: DeviceSelectCandidate): string
|
|
|
37
43
|
* (value = the device id as a string). Sorted case-insensitively by label so
|
|
38
44
|
* the picker order is stable.
|
|
39
45
|
*/
|
|
40
|
-
|
|
46
|
+
/** `Location · type · in Container` — the parts a name alone does not say. */
|
|
47
|
+
export declare function deviceOptionDescription(device: DeviceSelectCandidate): string;
|
|
48
|
+
export declare function devicesToOptions(devices: readonly DeviceSelectCandidate[], filter?: DeviceSelectFilter): DeviceOption[];
|
|
49
|
+
export interface DeviceFacets {
|
|
50
|
+
readonly locations: readonly string[];
|
|
51
|
+
readonly types: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
/** The distinct locations and types on offer — the chips above the search. */
|
|
54
|
+
export declare function deviceFacets(devices: readonly DeviceSelectCandidate[]): DeviceFacets;
|
|
55
|
+
export interface FacetSelection {
|
|
56
|
+
readonly location?: string;
|
|
57
|
+
readonly type?: string;
|
|
58
|
+
}
|
|
59
|
+
/** Every chosen facet must match; an unchosen one matches everything. */
|
|
60
|
+
export declare function matchesFacets(device: DeviceSelectCandidate, facets: FacetSelection): boolean;
|
|
61
|
+
export interface SuggestedPartition<T extends DeviceOption> {
|
|
62
|
+
readonly suggested: readonly T[];
|
|
63
|
+
readonly others: readonly T[];
|
|
64
|
+
}
|
|
65
|
+
/** The devices sharing `location` come first — suggested, never pre-linked. */
|
|
66
|
+
export declare function partitionSuggested<T extends DeviceOption>(options: readonly T[], location: string | null | undefined): SuggestedPartition<T>;
|
|
41
67
|
/**
|
|
42
68
|
* Free-text option filter — case-insensitive AND over whitespace tokens, each
|
|
43
69
|
* matched against the option's `value` + `label` (so a device is found by name,
|
|
44
70
|
* id, or location). An empty query returns every option in order.
|
|
45
71
|
*/
|
|
46
|
-
export declare function filterDeviceOptions(options: readonly
|
|
72
|
+
export declare function filterDeviceOptions<T extends ConfigOption>(options: readonly T[], query: string): T[];
|
|
47
73
|
/**
|
|
48
74
|
* Resolve the currently-selected values into options IN SELECTION ORDER (so
|
|
49
75
|
* chips stay stable while searching). A selected value with no matching option
|
package/dist/index.cjs
CHANGED
|
@@ -6903,6 +6903,7 @@ function intersects(a, b) {
|
|
|
6903
6903
|
*/
|
|
6904
6904
|
function deviceMatchesFilter(device, filter) {
|
|
6905
6905
|
if (filter?.excludeDeviceId !== void 0 && device.id === filter.excludeDeviceId) return false;
|
|
6906
|
+
if (filter?.excludeTypes !== void 0 && filter.excludeTypes.includes(device.type)) return false;
|
|
6906
6907
|
const hasCaps = filter?.caps !== void 0 && filter.caps.length > 0;
|
|
6907
6908
|
const hasTypes = filter?.types !== void 0 && filter.types.length > 0;
|
|
6908
6909
|
if (!hasCaps && !hasTypes) return true;
|
|
@@ -6919,12 +6920,52 @@ function deviceOptionLabel(device) {
|
|
|
6919
6920
|
* (value = the device id as a string). Sorted case-insensitively by label so
|
|
6920
6921
|
* the picker order is stable.
|
|
6921
6922
|
*/
|
|
6923
|
+
/** `Location · type · in Container` — the parts a name alone does not say. */
|
|
6924
|
+
function deviceOptionDescription(device) {
|
|
6925
|
+
const parts = [];
|
|
6926
|
+
if (typeof device.location === "string" && device.location.length > 0) parts.push(device.location);
|
|
6927
|
+
parts.push(device.type);
|
|
6928
|
+
if (typeof device.parentName === "string" && device.parentName.length > 0) parts.push(`in ${device.parentName}`);
|
|
6929
|
+
return parts.join(" · ");
|
|
6930
|
+
}
|
|
6922
6931
|
function devicesToOptions(devices, filter) {
|
|
6923
6932
|
return devices.filter((d) => deviceMatchesFilter(d, filter)).map((d) => ({
|
|
6924
6933
|
value: String(d.id),
|
|
6925
|
-
label: deviceOptionLabel(d)
|
|
6934
|
+
label: deviceOptionLabel(d),
|
|
6935
|
+
description: deviceOptionDescription(d),
|
|
6936
|
+
location: typeof d.location === "string" && d.location.length > 0 ? d.location : null
|
|
6926
6937
|
})).toSorted((a, b) => a.label.localeCompare(b.label));
|
|
6927
6938
|
}
|
|
6939
|
+
/** The distinct locations and types on offer — the chips above the search. */
|
|
6940
|
+
function deviceFacets(devices) {
|
|
6941
|
+
const locations = /* @__PURE__ */ new Set();
|
|
6942
|
+
const types = /* @__PURE__ */ new Set();
|
|
6943
|
+
for (const d of devices) {
|
|
6944
|
+
if (typeof d.location === "string" && d.location.length > 0) locations.add(d.location);
|
|
6945
|
+
types.add(d.type);
|
|
6946
|
+
}
|
|
6947
|
+
return {
|
|
6948
|
+
locations: [...locations].toSorted((a, b) => a.localeCompare(b)),
|
|
6949
|
+
types: [...types].toSorted((a, b) => a.localeCompare(b))
|
|
6950
|
+
};
|
|
6951
|
+
}
|
|
6952
|
+
/** Every chosen facet must match; an unchosen one matches everything. */
|
|
6953
|
+
function matchesFacets(device, facets) {
|
|
6954
|
+
if (facets.location !== void 0 && device.location !== facets.location) return false;
|
|
6955
|
+
if (facets.type !== void 0 && device.type !== facets.type) return false;
|
|
6956
|
+
return true;
|
|
6957
|
+
}
|
|
6958
|
+
/** The devices sharing `location` come first — suggested, never pre-linked. */
|
|
6959
|
+
function partitionSuggested(options, location) {
|
|
6960
|
+
if (location === null || location === void 0 || location.length === 0) return {
|
|
6961
|
+
suggested: [],
|
|
6962
|
+
others: [...options]
|
|
6963
|
+
};
|
|
6964
|
+
return {
|
|
6965
|
+
suggested: options.filter((o) => o.location === location),
|
|
6966
|
+
others: options.filter((o) => o.location !== location)
|
|
6967
|
+
};
|
|
6968
|
+
}
|
|
6928
6969
|
/**
|
|
6929
6970
|
* Free-text option filter — case-insensitive AND over whitespace tokens, each
|
|
6930
6971
|
* matched against the option's `value` + `label` (so a device is found by name,
|
|
@@ -6934,7 +6975,7 @@ function filterDeviceOptions(options, query) {
|
|
|
6934
6975
|
const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
6935
6976
|
if (tokens.length === 0) return [...options];
|
|
6936
6977
|
return options.filter((opt) => {
|
|
6937
|
-
const hay = `${opt.value} ${opt.label}`.toLowerCase();
|
|
6978
|
+
const hay = `${opt.value} ${opt.label} ${opt.description ?? ""}`.toLowerCase();
|
|
6938
6979
|
return tokens.every((t) => hay.includes(t));
|
|
6939
6980
|
});
|
|
6940
6981
|
}
|
|
@@ -6973,21 +7014,61 @@ var DESC_CLASS$1 = "text-[10px] text-foreground-subtle mt-0.5 break-words";
|
|
|
6973
7014
|
/** The compact projection of the listAll rows the device fields consume. */
|
|
6974
7015
|
function toCandidates(rows) {
|
|
6975
7016
|
if (!Array.isArray(rows)) return [];
|
|
7017
|
+
const nameById = /* @__PURE__ */ new Map();
|
|
7018
|
+
for (const row of rows) {
|
|
7019
|
+
if (typeof row !== "object" || row === null) continue;
|
|
7020
|
+
const r = row;
|
|
7021
|
+
if (typeof r.id === "number" && typeof r.name === "string") nameById.set(r.id, r.name);
|
|
7022
|
+
}
|
|
6976
7023
|
const out = [];
|
|
6977
7024
|
for (const row of rows) {
|
|
6978
7025
|
if (typeof row !== "object" || row === null) continue;
|
|
6979
7026
|
const r = row;
|
|
6980
7027
|
if (typeof r.id !== "number" || typeof r.type !== "string" || typeof r.name !== "string") continue;
|
|
7028
|
+
const parentName = typeof r.parentDeviceId === "number" ? nameById.get(r.parentDeviceId) ?? null : null;
|
|
6981
7029
|
out.push({
|
|
6982
7030
|
id: r.id,
|
|
6983
7031
|
type: r.type,
|
|
6984
7032
|
name: r.name,
|
|
6985
7033
|
location: typeof r.location === "string" ? r.location : null,
|
|
6986
|
-
features: Array.isArray(r.features) ? r.features.filter((f) => typeof f === "string") : []
|
|
7034
|
+
features: Array.isArray(r.features) ? r.features.filter((f) => typeof f === "string") : [],
|
|
7035
|
+
parentName
|
|
6987
7036
|
});
|
|
6988
7037
|
}
|
|
6989
7038
|
return out;
|
|
6990
7039
|
}
|
|
7040
|
+
var FACET_CHIP = "rounded-full border px-1.5 py-0.5 text-[10px] leading-4 transition-colors cursor-pointer";
|
|
7041
|
+
var FACET_CHIP_ON = `${FACET_CHIP} border-primary bg-primary/10 text-primary`;
|
|
7042
|
+
var FACET_CHIP_OFF = `${FACET_CHIP} border-border text-foreground-subtle hover:bg-surface-hover`;
|
|
7043
|
+
var GROUP_HEADING = "px-2.5 pt-1.5 pb-0.5 text-[10px] font-medium uppercase tracking-wide text-foreground-subtle";
|
|
7044
|
+
/** One row of facet chips: `All` plus every distinct value. */
|
|
7045
|
+
function FacetRow({ label, values, active, onPick }) {
|
|
7046
|
+
if (values.length < 2) return null;
|
|
7047
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7048
|
+
className: "flex flex-wrap items-center gap-1 mb-1",
|
|
7049
|
+
role: "group",
|
|
7050
|
+
"aria-label": label,
|
|
7051
|
+
children: [
|
|
7052
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7053
|
+
className: "text-[10px] text-foreground-subtle mr-0.5",
|
|
7054
|
+
children: label
|
|
7055
|
+
}),
|
|
7056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7057
|
+
type: "button",
|
|
7058
|
+
className: active === void 0 ? FACET_CHIP_ON : FACET_CHIP_OFF,
|
|
7059
|
+
onClick: () => onPick(void 0),
|
|
7060
|
+
children: "All"
|
|
7061
|
+
}),
|
|
7062
|
+
values.map((v) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7063
|
+
type: "button",
|
|
7064
|
+
className: active === v ? FACET_CHIP_ON : FACET_CHIP_OFF,
|
|
7065
|
+
"aria-pressed": active === v,
|
|
7066
|
+
onClick: () => onPick(active === v ? void 0 : v),
|
|
7067
|
+
children: v
|
|
7068
|
+
}, v))
|
|
7069
|
+
]
|
|
7070
|
+
});
|
|
7071
|
+
}
|
|
6991
7072
|
function FieldShell({ label, description, required, translationFn, children }) {
|
|
6992
7073
|
const resolvedLabel = resolveLabel(label, translationFn);
|
|
6993
7074
|
const resolvedDescription = resolveLabel(description, translationFn);
|
|
@@ -7035,7 +7116,16 @@ function DeviceSelectField({ field, value, onChange, disabled, translationFn })
|
|
|
7035
7116
|
}
|
|
7036
7117
|
function DeviceMultiSelectField({ field, value, onChange, disabled, translationFn }) {
|
|
7037
7118
|
const { data } = useDeviceManagerListAll({});
|
|
7038
|
-
const
|
|
7119
|
+
const smart = field.suggestLocation !== void 0;
|
|
7120
|
+
const [facets, setFacets] = (0, react$1.useState)({});
|
|
7121
|
+
const candidates = (0, react$1.useMemo)(() => toCandidates(data), [data]);
|
|
7122
|
+
const facetValues = (0, react$1.useMemo)(() => deviceFacets(candidates.filter((c) => c.id !== field.deviceFilter?.excludeDeviceId)), [candidates, field.deviceFilter?.excludeDeviceId]);
|
|
7123
|
+
const candidatesInScope = (0, react$1.useMemo)(() => smart ? candidates.filter((c) => matchesFacets(c, facets)) : candidates, [
|
|
7124
|
+
candidates,
|
|
7125
|
+
facets,
|
|
7126
|
+
smart
|
|
7127
|
+
]);
|
|
7128
|
+
const options = (0, react$1.useMemo)(() => devicesToOptions(candidatesInScope, field.deviceFilter), [candidatesInScope, field.deviceFilter]);
|
|
7039
7129
|
const selected = Array.isArray(value) ? value.map(String) : [];
|
|
7040
7130
|
const isDisabled = disabled === true || field.disabled === true;
|
|
7041
7131
|
const atMax = field.maxItems !== void 0 && selected.length >= field.maxItems;
|
|
@@ -7045,7 +7135,13 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
7045
7135
|
const inputRef = (0, react$1.useRef)(null);
|
|
7046
7136
|
const blurTimer = (0, react$1.useRef)(null);
|
|
7047
7137
|
const selectedOptions = (0, react$1.useMemo)(() => selectedDeviceOptions(options, selected), [options, selected]);
|
|
7048
|
-
const
|
|
7138
|
+
const searched = (0, react$1.useMemo)(() => filterDeviceOptions(options, query), [options, query]);
|
|
7139
|
+
const partition = (0, react$1.useMemo)(() => partitionSuggested(searched, smart ? field.suggestLocation : null), [
|
|
7140
|
+
searched,
|
|
7141
|
+
smart,
|
|
7142
|
+
field.suggestLocation
|
|
7143
|
+
]);
|
|
7144
|
+
const filtered = (0, react$1.useMemo)(() => [...partition.suggested, ...partition.others], [partition]);
|
|
7049
7145
|
const toggle = (optValue) => {
|
|
7050
7146
|
if (isDisabled) return;
|
|
7051
7147
|
if (selected.includes(optValue)) onChange(selected.filter((v) => v !== optValue));
|
|
@@ -7095,6 +7191,23 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
7095
7191
|
})]
|
|
7096
7192
|
}, opt.value))
|
|
7097
7193
|
}),
|
|
7194
|
+
smart && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FacetRow, {
|
|
7195
|
+
label: "Location",
|
|
7196
|
+
values: facetValues.locations,
|
|
7197
|
+
active: facets.location,
|
|
7198
|
+
onPick: (location) => setFacets((f) => ({
|
|
7199
|
+
...f,
|
|
7200
|
+
location
|
|
7201
|
+
}))
|
|
7202
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FacetRow, {
|
|
7203
|
+
label: "Type",
|
|
7204
|
+
values: facetValues.types,
|
|
7205
|
+
active: facets.type,
|
|
7206
|
+
onPick: (type) => setFacets((f) => ({
|
|
7207
|
+
...f,
|
|
7208
|
+
type
|
|
7209
|
+
}))
|
|
7210
|
+
})] }),
|
|
7098
7211
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7099
7212
|
className: INPUT_SHELL + (isDisabled ? " opacity-50" : ""),
|
|
7100
7213
|
children: [
|
|
@@ -7144,7 +7257,12 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
7144
7257
|
}) : filtered.map((opt, i) => {
|
|
7145
7258
|
const checked = selected.includes(opt.value);
|
|
7146
7259
|
const blockedByMax = !checked && atMax;
|
|
7147
|
-
|
|
7260
|
+
const suggestedCount = partition.suggested.length;
|
|
7261
|
+
const heading = !smart ? null : i === 0 && suggestedCount > 0 ? `Suggested · ${field.suggestLocation ?? ""}` : i === suggestedCount && suggestedCount > 0 ? "Other devices" : null;
|
|
7262
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [heading !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7263
|
+
className: GROUP_HEADING,
|
|
7264
|
+
children: heading
|
|
7265
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7148
7266
|
type: "button",
|
|
7149
7267
|
role: "option",
|
|
7150
7268
|
"aria-selected": checked,
|
|
@@ -7159,11 +7277,17 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
7159
7277
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7160
7278
|
className: ["flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border", checked ? "border-primary bg-primary text-white" : "border-border"].join(" "),
|
|
7161
7279
|
children: checked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-2.5 w-2.5" })
|
|
7162
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
7163
|
-
className: "
|
|
7164
|
-
children:
|
|
7280
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7281
|
+
className: "min-w-0 flex-1",
|
|
7282
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7283
|
+
className: "block truncate text-foreground",
|
|
7284
|
+
children: opt.label
|
|
7285
|
+
}), opt.description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7286
|
+
className: "block truncate text-[10px] text-foreground-subtle",
|
|
7287
|
+
children: opt.description
|
|
7288
|
+
})]
|
|
7165
7289
|
})]
|
|
7166
|
-
}, opt.value);
|
|
7290
|
+
})] }, opt.value);
|
|
7167
7291
|
})
|
|
7168
7292
|
}),
|
|
7169
7293
|
description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
package/dist/index.js
CHANGED
|
@@ -6879,6 +6879,7 @@ function intersects(a, b) {
|
|
|
6879
6879
|
*/
|
|
6880
6880
|
function deviceMatchesFilter(device, filter) {
|
|
6881
6881
|
if (filter?.excludeDeviceId !== void 0 && device.id === filter.excludeDeviceId) return false;
|
|
6882
|
+
if (filter?.excludeTypes !== void 0 && filter.excludeTypes.includes(device.type)) return false;
|
|
6882
6883
|
const hasCaps = filter?.caps !== void 0 && filter.caps.length > 0;
|
|
6883
6884
|
const hasTypes = filter?.types !== void 0 && filter.types.length > 0;
|
|
6884
6885
|
if (!hasCaps && !hasTypes) return true;
|
|
@@ -6895,12 +6896,52 @@ function deviceOptionLabel(device) {
|
|
|
6895
6896
|
* (value = the device id as a string). Sorted case-insensitively by label so
|
|
6896
6897
|
* the picker order is stable.
|
|
6897
6898
|
*/
|
|
6899
|
+
/** `Location · type · in Container` — the parts a name alone does not say. */
|
|
6900
|
+
function deviceOptionDescription(device) {
|
|
6901
|
+
const parts = [];
|
|
6902
|
+
if (typeof device.location === "string" && device.location.length > 0) parts.push(device.location);
|
|
6903
|
+
parts.push(device.type);
|
|
6904
|
+
if (typeof device.parentName === "string" && device.parentName.length > 0) parts.push(`in ${device.parentName}`);
|
|
6905
|
+
return parts.join(" · ");
|
|
6906
|
+
}
|
|
6898
6907
|
function devicesToOptions(devices, filter) {
|
|
6899
6908
|
return devices.filter((d) => deviceMatchesFilter(d, filter)).map((d) => ({
|
|
6900
6909
|
value: String(d.id),
|
|
6901
|
-
label: deviceOptionLabel(d)
|
|
6910
|
+
label: deviceOptionLabel(d),
|
|
6911
|
+
description: deviceOptionDescription(d),
|
|
6912
|
+
location: typeof d.location === "string" && d.location.length > 0 ? d.location : null
|
|
6902
6913
|
})).toSorted((a, b) => a.label.localeCompare(b.label));
|
|
6903
6914
|
}
|
|
6915
|
+
/** The distinct locations and types on offer — the chips above the search. */
|
|
6916
|
+
function deviceFacets(devices) {
|
|
6917
|
+
const locations = /* @__PURE__ */ new Set();
|
|
6918
|
+
const types = /* @__PURE__ */ new Set();
|
|
6919
|
+
for (const d of devices) {
|
|
6920
|
+
if (typeof d.location === "string" && d.location.length > 0) locations.add(d.location);
|
|
6921
|
+
types.add(d.type);
|
|
6922
|
+
}
|
|
6923
|
+
return {
|
|
6924
|
+
locations: [...locations].toSorted((a, b) => a.localeCompare(b)),
|
|
6925
|
+
types: [...types].toSorted((a, b) => a.localeCompare(b))
|
|
6926
|
+
};
|
|
6927
|
+
}
|
|
6928
|
+
/** Every chosen facet must match; an unchosen one matches everything. */
|
|
6929
|
+
function matchesFacets(device, facets) {
|
|
6930
|
+
if (facets.location !== void 0 && device.location !== facets.location) return false;
|
|
6931
|
+
if (facets.type !== void 0 && device.type !== facets.type) return false;
|
|
6932
|
+
return true;
|
|
6933
|
+
}
|
|
6934
|
+
/** The devices sharing `location` come first — suggested, never pre-linked. */
|
|
6935
|
+
function partitionSuggested(options, location) {
|
|
6936
|
+
if (location === null || location === void 0 || location.length === 0) return {
|
|
6937
|
+
suggested: [],
|
|
6938
|
+
others: [...options]
|
|
6939
|
+
};
|
|
6940
|
+
return {
|
|
6941
|
+
suggested: options.filter((o) => o.location === location),
|
|
6942
|
+
others: options.filter((o) => o.location !== location)
|
|
6943
|
+
};
|
|
6944
|
+
}
|
|
6904
6945
|
/**
|
|
6905
6946
|
* Free-text option filter — case-insensitive AND over whitespace tokens, each
|
|
6906
6947
|
* matched against the option's `value` + `label` (so a device is found by name,
|
|
@@ -6910,7 +6951,7 @@ function filterDeviceOptions(options, query) {
|
|
|
6910
6951
|
const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
6911
6952
|
if (tokens.length === 0) return [...options];
|
|
6912
6953
|
return options.filter((opt) => {
|
|
6913
|
-
const hay = `${opt.value} ${opt.label}`.toLowerCase();
|
|
6954
|
+
const hay = `${opt.value} ${opt.label} ${opt.description ?? ""}`.toLowerCase();
|
|
6914
6955
|
return tokens.every((t) => hay.includes(t));
|
|
6915
6956
|
});
|
|
6916
6957
|
}
|
|
@@ -6949,21 +6990,61 @@ var DESC_CLASS$1 = "text-[10px] text-foreground-subtle mt-0.5 break-words";
|
|
|
6949
6990
|
/** The compact projection of the listAll rows the device fields consume. */
|
|
6950
6991
|
function toCandidates(rows) {
|
|
6951
6992
|
if (!Array.isArray(rows)) return [];
|
|
6993
|
+
const nameById = /* @__PURE__ */ new Map();
|
|
6994
|
+
for (const row of rows) {
|
|
6995
|
+
if (typeof row !== "object" || row === null) continue;
|
|
6996
|
+
const r = row;
|
|
6997
|
+
if (typeof r.id === "number" && typeof r.name === "string") nameById.set(r.id, r.name);
|
|
6998
|
+
}
|
|
6952
6999
|
const out = [];
|
|
6953
7000
|
for (const row of rows) {
|
|
6954
7001
|
if (typeof row !== "object" || row === null) continue;
|
|
6955
7002
|
const r = row;
|
|
6956
7003
|
if (typeof r.id !== "number" || typeof r.type !== "string" || typeof r.name !== "string") continue;
|
|
7004
|
+
const parentName = typeof r.parentDeviceId === "number" ? nameById.get(r.parentDeviceId) ?? null : null;
|
|
6957
7005
|
out.push({
|
|
6958
7006
|
id: r.id,
|
|
6959
7007
|
type: r.type,
|
|
6960
7008
|
name: r.name,
|
|
6961
7009
|
location: typeof r.location === "string" ? r.location : null,
|
|
6962
|
-
features: Array.isArray(r.features) ? r.features.filter((f) => typeof f === "string") : []
|
|
7010
|
+
features: Array.isArray(r.features) ? r.features.filter((f) => typeof f === "string") : [],
|
|
7011
|
+
parentName
|
|
6963
7012
|
});
|
|
6964
7013
|
}
|
|
6965
7014
|
return out;
|
|
6966
7015
|
}
|
|
7016
|
+
var FACET_CHIP = "rounded-full border px-1.5 py-0.5 text-[10px] leading-4 transition-colors cursor-pointer";
|
|
7017
|
+
var FACET_CHIP_ON = `${FACET_CHIP} border-primary bg-primary/10 text-primary`;
|
|
7018
|
+
var FACET_CHIP_OFF = `${FACET_CHIP} border-border text-foreground-subtle hover:bg-surface-hover`;
|
|
7019
|
+
var GROUP_HEADING = "px-2.5 pt-1.5 pb-0.5 text-[10px] font-medium uppercase tracking-wide text-foreground-subtle";
|
|
7020
|
+
/** One row of facet chips: `All` plus every distinct value. */
|
|
7021
|
+
function FacetRow({ label, values, active, onPick }) {
|
|
7022
|
+
if (values.length < 2) return null;
|
|
7023
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
7024
|
+
className: "flex flex-wrap items-center gap-1 mb-1",
|
|
7025
|
+
role: "group",
|
|
7026
|
+
"aria-label": label,
|
|
7027
|
+
children: [
|
|
7028
|
+
/* @__PURE__ */ jsx("span", {
|
|
7029
|
+
className: "text-[10px] text-foreground-subtle mr-0.5",
|
|
7030
|
+
children: label
|
|
7031
|
+
}),
|
|
7032
|
+
/* @__PURE__ */ jsx("button", {
|
|
7033
|
+
type: "button",
|
|
7034
|
+
className: active === void 0 ? FACET_CHIP_ON : FACET_CHIP_OFF,
|
|
7035
|
+
onClick: () => onPick(void 0),
|
|
7036
|
+
children: "All"
|
|
7037
|
+
}),
|
|
7038
|
+
values.map((v) => /* @__PURE__ */ jsx("button", {
|
|
7039
|
+
type: "button",
|
|
7040
|
+
className: active === v ? FACET_CHIP_ON : FACET_CHIP_OFF,
|
|
7041
|
+
"aria-pressed": active === v,
|
|
7042
|
+
onClick: () => onPick(active === v ? void 0 : v),
|
|
7043
|
+
children: v
|
|
7044
|
+
}, v))
|
|
7045
|
+
]
|
|
7046
|
+
});
|
|
7047
|
+
}
|
|
6967
7048
|
function FieldShell({ label, description, required, translationFn, children }) {
|
|
6968
7049
|
const resolvedLabel = resolveLabel(label, translationFn);
|
|
6969
7050
|
const resolvedDescription = resolveLabel(description, translationFn);
|
|
@@ -7011,7 +7092,16 @@ function DeviceSelectField({ field, value, onChange, disabled, translationFn })
|
|
|
7011
7092
|
}
|
|
7012
7093
|
function DeviceMultiSelectField({ field, value, onChange, disabled, translationFn }) {
|
|
7013
7094
|
const { data } = useDeviceManagerListAll({});
|
|
7014
|
-
const
|
|
7095
|
+
const smart = field.suggestLocation !== void 0;
|
|
7096
|
+
const [facets, setFacets] = useState({});
|
|
7097
|
+
const candidates = useMemo(() => toCandidates(data), [data]);
|
|
7098
|
+
const facetValues = useMemo(() => deviceFacets(candidates.filter((c) => c.id !== field.deviceFilter?.excludeDeviceId)), [candidates, field.deviceFilter?.excludeDeviceId]);
|
|
7099
|
+
const candidatesInScope = useMemo(() => smart ? candidates.filter((c) => matchesFacets(c, facets)) : candidates, [
|
|
7100
|
+
candidates,
|
|
7101
|
+
facets,
|
|
7102
|
+
smart
|
|
7103
|
+
]);
|
|
7104
|
+
const options = useMemo(() => devicesToOptions(candidatesInScope, field.deviceFilter), [candidatesInScope, field.deviceFilter]);
|
|
7015
7105
|
const selected = Array.isArray(value) ? value.map(String) : [];
|
|
7016
7106
|
const isDisabled = disabled === true || field.disabled === true;
|
|
7017
7107
|
const atMax = field.maxItems !== void 0 && selected.length >= field.maxItems;
|
|
@@ -7021,7 +7111,13 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
7021
7111
|
const inputRef = useRef(null);
|
|
7022
7112
|
const blurTimer = useRef(null);
|
|
7023
7113
|
const selectedOptions = useMemo(() => selectedDeviceOptions(options, selected), [options, selected]);
|
|
7024
|
-
const
|
|
7114
|
+
const searched = useMemo(() => filterDeviceOptions(options, query), [options, query]);
|
|
7115
|
+
const partition = useMemo(() => partitionSuggested(searched, smart ? field.suggestLocation : null), [
|
|
7116
|
+
searched,
|
|
7117
|
+
smart,
|
|
7118
|
+
field.suggestLocation
|
|
7119
|
+
]);
|
|
7120
|
+
const filtered = useMemo(() => [...partition.suggested, ...partition.others], [partition]);
|
|
7025
7121
|
const toggle = (optValue) => {
|
|
7026
7122
|
if (isDisabled) return;
|
|
7027
7123
|
if (selected.includes(optValue)) onChange(selected.filter((v) => v !== optValue));
|
|
@@ -7071,6 +7167,23 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
7071
7167
|
})]
|
|
7072
7168
|
}, opt.value))
|
|
7073
7169
|
}),
|
|
7170
|
+
smart && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(FacetRow, {
|
|
7171
|
+
label: "Location",
|
|
7172
|
+
values: facetValues.locations,
|
|
7173
|
+
active: facets.location,
|
|
7174
|
+
onPick: (location) => setFacets((f) => ({
|
|
7175
|
+
...f,
|
|
7176
|
+
location
|
|
7177
|
+
}))
|
|
7178
|
+
}), /* @__PURE__ */ jsx(FacetRow, {
|
|
7179
|
+
label: "Type",
|
|
7180
|
+
values: facetValues.types,
|
|
7181
|
+
active: facets.type,
|
|
7182
|
+
onPick: (type) => setFacets((f) => ({
|
|
7183
|
+
...f,
|
|
7184
|
+
type
|
|
7185
|
+
}))
|
|
7186
|
+
})] }),
|
|
7074
7187
|
/* @__PURE__ */ jsxs("div", {
|
|
7075
7188
|
className: INPUT_SHELL + (isDisabled ? " opacity-50" : ""),
|
|
7076
7189
|
children: [
|
|
@@ -7120,7 +7233,12 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
7120
7233
|
}) : filtered.map((opt, i) => {
|
|
7121
7234
|
const checked = selected.includes(opt.value);
|
|
7122
7235
|
const blockedByMax = !checked && atMax;
|
|
7123
|
-
|
|
7236
|
+
const suggestedCount = partition.suggested.length;
|
|
7237
|
+
const heading = !smart ? null : i === 0 && suggestedCount > 0 ? `Suggested · ${field.suggestLocation ?? ""}` : i === suggestedCount && suggestedCount > 0 ? "Other devices" : null;
|
|
7238
|
+
return /* @__PURE__ */ jsxs("div", { children: [heading !== null && /* @__PURE__ */ jsx("div", {
|
|
7239
|
+
className: GROUP_HEADING,
|
|
7240
|
+
children: heading
|
|
7241
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
7124
7242
|
type: "button",
|
|
7125
7243
|
role: "option",
|
|
7126
7244
|
"aria-selected": checked,
|
|
@@ -7135,11 +7253,17 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
|
|
|
7135
7253
|
children: [/* @__PURE__ */ jsx("span", {
|
|
7136
7254
|
className: ["flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border", checked ? "border-primary bg-primary text-white" : "border-border"].join(" "),
|
|
7137
7255
|
children: checked && /* @__PURE__ */ jsx(Check, { className: "h-2.5 w-2.5" })
|
|
7138
|
-
}), /* @__PURE__ */
|
|
7139
|
-
className: "
|
|
7140
|
-
children:
|
|
7256
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
7257
|
+
className: "min-w-0 flex-1",
|
|
7258
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
7259
|
+
className: "block truncate text-foreground",
|
|
7260
|
+
children: opt.label
|
|
7261
|
+
}), opt.description !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
7262
|
+
className: "block truncate text-[10px] text-foreground-subtle",
|
|
7263
|
+
children: opt.description
|
|
7264
|
+
})]
|
|
7141
7265
|
})]
|
|
7142
|
-
}, opt.value);
|
|
7266
|
+
})] }, opt.value);
|
|
7143
7267
|
})
|
|
7144
7268
|
}),
|
|
7145
7269
|
description && /* @__PURE__ */ jsx("p", {
|