@oneuptime/common 10.2.4 → 10.2.7
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/Models/DatabaseModels/Service.ts +26 -0
- package/Server/API/GlobalConfigAPI.ts +13 -16
- package/Server/Infrastructure/Postgres/SchemaMigrations/1778582583897-MigrationName.ts +15 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/OpenTelemetryIngestService.ts +15 -0
- package/Server/Services/ServiceService.ts +37 -0
- package/Server/Types/Database/QueryHelper.ts +38 -0
- package/Server/Types/Database/QueryUtil.ts +77 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +52 -0
- package/Types/BaseDatabase/MultiSearch.ts +53 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardChartComponent.ts +2 -0
- package/Types/JSON.ts +3 -0
- package/Types/SerializableObjectDictionary.ts +2 -0
- package/UI/Components/Header/ProjectPicker/ProjectPicker.tsx +11 -6
- package/UI/Components/LogsViewer/components/ColumnSelector.tsx +58 -4
- package/UI/Components/ModelTable/BaseModelTable.tsx +1026 -10
- package/UI/Components/ModelTable/TableView.tsx +58 -32
- package/UI/Utils/GlobalConfig.ts +55 -0
- package/Utils/Dashboard/Components/DashboardChartComponent.ts +11 -0
- package/build/dist/Models/DatabaseModels/Service.js +28 -0
- package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +10 -16
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778582583897-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778582583897-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +11 -0
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +34 -0
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +33 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +64 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +44 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Types/BaseDatabase/MultiSearch.js +44 -0
- package/build/dist/Types/BaseDatabase/MultiSearch.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/JSON.js +1 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +2 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js +2 -2
- package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js +33 -3
- package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +618 -12
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/TableView.js +25 -18
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Utils/GlobalConfig.js +38 -0
- package/build/dist/UI/Utils/GlobalConfig.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardChartComponent.js +9 -0
- package/build/dist/Utils/Dashboard/Components/DashboardChartComponent.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,9 @@ import Navigation from "../../Utils/Navigation";
|
|
|
8
8
|
import PermissionUtil from "../../Utils/Permission";
|
|
9
9
|
import ProjectUtil from "../../Utils/Project";
|
|
10
10
|
import User from "../../Utils/User";
|
|
11
|
-
import { ButtonSize, ButtonStyleType } from "../Button/Button";
|
|
11
|
+
import Button, { ButtonSize, ButtonStyleType } from "../Button/Button";
|
|
12
|
+
import MoreMenu from "../MoreMenu/MoreMenu";
|
|
13
|
+
import MoreMenuItem from "../MoreMenu/MoreMenuItem";
|
|
12
14
|
import Card from "../Card/Card";
|
|
13
15
|
import { getRefreshButton } from "../Card/CardButtons/Refresh";
|
|
14
16
|
import ErrorMessage from "../ErrorMessage/ErrorMessage";
|
|
@@ -16,6 +18,7 @@ import List from "../List/List";
|
|
|
16
18
|
import ConfirmModal from "../Modal/ConfirmModal";
|
|
17
19
|
import Modal, { ModalWidth } from "../Modal/Modal";
|
|
18
20
|
import MarkdownViewer from "../Markdown.tsx/MarkdownViewer";
|
|
21
|
+
import Icon from "../Icon/Icon";
|
|
19
22
|
import OrderedStatesList from "../OrderedStatesList/OrderedStatesList";
|
|
20
23
|
import Pill from "../Pill/Pill";
|
|
21
24
|
import Table from "../Table/Table";
|
|
@@ -26,6 +29,7 @@ import Route from "../../../Types/API/Route";
|
|
|
26
29
|
import URL from "../../../Types/API/URL";
|
|
27
30
|
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
28
31
|
import Search from "../../../Types/BaseDatabase/Search";
|
|
32
|
+
import MultiSearch from "../../../Types/BaseDatabase/MultiSearch";
|
|
29
33
|
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
30
34
|
import SubscriptionPlan from "../../../Types/Billing/SubscriptionPlan";
|
|
31
35
|
import { Yellow } from "../../../Types/BrandColors";
|
|
@@ -36,7 +40,7 @@ import IconProp from "../../../Types/Icon/IconProp";
|
|
|
36
40
|
import ObjectID from "../../../Types/ObjectID";
|
|
37
41
|
import Permission, { PermissionHelper, } from "../../../Types/Permission";
|
|
38
42
|
import Typeof from "../../../Types/Typeof";
|
|
39
|
-
import React, { useEffect, useState, } from "react";
|
|
43
|
+
import React, { useEffect, useMemo, useState, } from "react";
|
|
40
44
|
import TableViewElement from "./TableView";
|
|
41
45
|
import UserPreferences, { UserPreferenceType, } from "../../../Utils/UserPreferences";
|
|
42
46
|
export var ShowAs;
|
|
@@ -105,6 +109,152 @@ const BaseModelTable = (props) => {
|
|
|
105
109
|
const [isLoading, setIsLoading] = useState(true);
|
|
106
110
|
const [error, setError] = useState("");
|
|
107
111
|
const [tableFilterError, setTableFilterError] = useState("");
|
|
112
|
+
const [searchText, setSearchText] = useState("");
|
|
113
|
+
const [debouncedSearchText, setDebouncedSearchText] = useState("");
|
|
114
|
+
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
|
115
|
+
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
|
|
116
|
+
const searchInputRef = React.useRef(null);
|
|
117
|
+
const [availableLabels, setAvailableLabels] = useState([]);
|
|
118
|
+
const [selectedLabels, setSelectedLabels] = useState([]);
|
|
119
|
+
const [isLabelsLoading, setIsLabelsLoading] = useState(false);
|
|
120
|
+
const [labelsFetched, setLabelsFetched] = useState(false);
|
|
121
|
+
const [labelDropdownIndex, setLabelDropdownIndex] = useState(0);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
const handle = setTimeout(() => {
|
|
124
|
+
setDebouncedSearchText(searchText);
|
|
125
|
+
}, 300);
|
|
126
|
+
return () => {
|
|
127
|
+
clearTimeout(handle);
|
|
128
|
+
};
|
|
129
|
+
}, [searchText]);
|
|
130
|
+
/*
|
|
131
|
+
* "/" focuses the search input — same affordance as GitHub / Linear.
|
|
132
|
+
* Skip while the user is typing in another input/textarea or has a modal open.
|
|
133
|
+
*/
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!props.searchableFields || props.searchableFields.length === 0) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const handleKey = (e) => {
|
|
139
|
+
if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const target = e.target;
|
|
143
|
+
if (target &&
|
|
144
|
+
(target.tagName === "INPUT" ||
|
|
145
|
+
target.tagName === "TEXTAREA" ||
|
|
146
|
+
target.isContentEditable)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
setIsSearchExpanded(true);
|
|
151
|
+
// Wait one frame so the input mounts/becomes visible before focusing.
|
|
152
|
+
requestAnimationFrame(() => {
|
|
153
|
+
var _a;
|
|
154
|
+
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
document.addEventListener("keydown", handleKey);
|
|
158
|
+
return () => {
|
|
159
|
+
document.removeEventListener("keydown", handleKey);
|
|
160
|
+
};
|
|
161
|
+
}, [props.searchableFields]);
|
|
162
|
+
/*
|
|
163
|
+
* Keep the search expanded whenever there is an active search term — so
|
|
164
|
+
* results stay visible alongside the box. Collapsing only happens when the
|
|
165
|
+
* user blurs an empty input.
|
|
166
|
+
*/
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if ((debouncedSearchText.trim().length > 0 || selectedLabels.length > 0) &&
|
|
169
|
+
!isSearchExpanded) {
|
|
170
|
+
setIsSearchExpanded(true);
|
|
171
|
+
}
|
|
172
|
+
}, [debouncedSearchText, selectedLabels]);
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
// reset to first page whenever the active search term or labels change
|
|
175
|
+
setCurrentPageNumber(1);
|
|
176
|
+
}, [debouncedSearchText, selectedLabels]);
|
|
177
|
+
const labelFilterConfig = useMemo(() => {
|
|
178
|
+
const filter = props.filters.find((f) => {
|
|
179
|
+
return (f.filterEntityType &&
|
|
180
|
+
f.filterEntityType.name === "Label" &&
|
|
181
|
+
f.field &&
|
|
182
|
+
f.filterDropdownField);
|
|
183
|
+
});
|
|
184
|
+
if (!filter || !filter.field || !filter.filterDropdownField) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const fieldKey = Object.keys(filter.field)[0];
|
|
188
|
+
if (!fieldKey) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
fieldKey,
|
|
193
|
+
entityType: filter.filterEntityType,
|
|
194
|
+
fetchQuery: filter.filterQuery || {},
|
|
195
|
+
labelField: filter.filterDropdownField.label,
|
|
196
|
+
};
|
|
197
|
+
}, [props.filters]);
|
|
198
|
+
// Fetch labels on first search expansion if this resource supports them.
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!isSearchExpanded || !labelFilterConfig || labelsFetched) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let cancelled = false;
|
|
204
|
+
setIsLabelsLoading(true);
|
|
205
|
+
(async () => {
|
|
206
|
+
try {
|
|
207
|
+
const result = await props.callbacks.getList({
|
|
208
|
+
modelType: labelFilterConfig.entityType,
|
|
209
|
+
query: labelFilterConfig.fetchQuery,
|
|
210
|
+
limit: 200,
|
|
211
|
+
skip: 0,
|
|
212
|
+
select: {
|
|
213
|
+
_id: true,
|
|
214
|
+
[labelFilterConfig.labelField]: true,
|
|
215
|
+
color: true,
|
|
216
|
+
},
|
|
217
|
+
sort: { [labelFilterConfig.labelField]: SortOrder.Ascending },
|
|
218
|
+
});
|
|
219
|
+
if (cancelled) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const mapped = (result.data || [])
|
|
223
|
+
.map((item) => {
|
|
224
|
+
var _a;
|
|
225
|
+
const raw = item;
|
|
226
|
+
const colorAny = raw["color"];
|
|
227
|
+
const colorHex = (colorAny &&
|
|
228
|
+
(typeof colorAny === "string"
|
|
229
|
+
? colorAny
|
|
230
|
+
: colorAny.value ||
|
|
231
|
+
(colorAny.toString && colorAny.toString()))) ||
|
|
232
|
+
"#94a3b8";
|
|
233
|
+
return {
|
|
234
|
+
id: ((_a = raw["_id"]) === null || _a === void 0 ? void 0 : _a.toString()) || "",
|
|
235
|
+
name: raw[labelFilterConfig.labelField] || "",
|
|
236
|
+
color: colorHex,
|
|
237
|
+
};
|
|
238
|
+
})
|
|
239
|
+
.filter((l) => {
|
|
240
|
+
return l.id && l.name;
|
|
241
|
+
});
|
|
242
|
+
setAvailableLabels(mapped);
|
|
243
|
+
setLabelsFetched(true);
|
|
244
|
+
}
|
|
245
|
+
catch (_a) {
|
|
246
|
+
// Silently fail — search still works without label suggestions.
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
if (!cancelled) {
|
|
250
|
+
setIsLabelsLoading(false);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
254
|
+
return () => {
|
|
255
|
+
cancelled = true;
|
|
256
|
+
};
|
|
257
|
+
}, [isSearchExpanded, labelFilterConfig, labelsFetched]);
|
|
108
258
|
const [showModel, setShowModal] = useState(false);
|
|
109
259
|
const [showFilterModal, setShowFilterModal] = useState(false);
|
|
110
260
|
const [modalType, setModalType] = useState(ModalType.Create);
|
|
@@ -439,13 +589,57 @@ const BaseModelTable = (props) => {
|
|
|
439
589
|
}
|
|
440
590
|
setIsFilterFetchLoading(false);
|
|
441
591
|
};
|
|
592
|
+
const buildSearchQueryFragment = () => {
|
|
593
|
+
const fragment = {};
|
|
594
|
+
/*
|
|
595
|
+
* Strip the trailing @<prefix> mention before searching — that token
|
|
596
|
+
* is a label-autocomplete trigger, not part of the user's free-text
|
|
597
|
+
* query.
|
|
598
|
+
*/
|
|
599
|
+
const stripTrailingMention = (v) => {
|
|
600
|
+
const atIndex = v.lastIndexOf("@");
|
|
601
|
+
if (atIndex < 0) {
|
|
602
|
+
return v;
|
|
603
|
+
}
|
|
604
|
+
if (atIndex > 0) {
|
|
605
|
+
const prev = v[atIndex - 1] || "";
|
|
606
|
+
if (prev !== " " && prev !== "\t") {
|
|
607
|
+
return v;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const after = v.substring(atIndex + 1);
|
|
611
|
+
if (after.includes(" ") ||
|
|
612
|
+
after.includes("\t") ||
|
|
613
|
+
after.includes("\n")) {
|
|
614
|
+
return v;
|
|
615
|
+
}
|
|
616
|
+
return v.substring(0, atIndex).trimEnd();
|
|
617
|
+
};
|
|
618
|
+
const effectiveSearch = stripTrailingMention(debouncedSearchText).trim();
|
|
619
|
+
if (effectiveSearch.length > 0 &&
|
|
620
|
+
props.searchableFields &&
|
|
621
|
+
props.searchableFields.length > 0) {
|
|
622
|
+
fragment._multiFieldSearch = new MultiSearch({
|
|
623
|
+
fields: props.searchableFields.map((f) => {
|
|
624
|
+
return f;
|
|
625
|
+
}),
|
|
626
|
+
value: effectiveSearch,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
if (labelFilterConfig && selectedLabels.length > 0) {
|
|
630
|
+
fragment[labelFilterConfig.fieldKey] = new Includes(selectedLabels.map((l) => {
|
|
631
|
+
return l.id;
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
return fragment;
|
|
635
|
+
};
|
|
442
636
|
const fetchAllBulkItems = async () => {
|
|
443
637
|
setError("");
|
|
444
638
|
setIsLoading(true);
|
|
445
639
|
try {
|
|
446
640
|
const listResult = await props.callbacks.getList({
|
|
447
641
|
modelType: props.modelType,
|
|
448
|
-
query: Object.assign(Object.assign({}, props.query), query),
|
|
642
|
+
query: Object.assign(Object.assign(Object.assign({}, props.query), query), buildSearchQueryFragment()),
|
|
449
643
|
limit: LIMIT_PER_PROJECT,
|
|
450
644
|
skip: 0,
|
|
451
645
|
select: {
|
|
@@ -474,7 +668,7 @@ const BaseModelTable = (props) => {
|
|
|
474
668
|
try {
|
|
475
669
|
const listResult = await props.callbacks.getList({
|
|
476
670
|
modelType: props.modelType,
|
|
477
|
-
query: Object.assign(Object.assign({}, props.query), query),
|
|
671
|
+
query: Object.assign(Object.assign(Object.assign({}, props.query), query), buildSearchQueryFragment()),
|
|
478
672
|
groupBy: Object.assign({}, props.groupBy),
|
|
479
673
|
limit: itemsOnPage,
|
|
480
674
|
skip: (currentPageNumber - 1) * itemsOnPage,
|
|
@@ -672,9 +866,6 @@ const BaseModelTable = (props) => {
|
|
|
672
866
|
icon: IconProp.Filter,
|
|
673
867
|
});
|
|
674
868
|
}
|
|
675
|
-
if (props.saveFilterProps) {
|
|
676
|
-
headerbuttons.push(getSaveFilterDropdown());
|
|
677
|
-
}
|
|
678
869
|
setCardButtons(headerbuttons);
|
|
679
870
|
};
|
|
680
871
|
useEffect(() => {
|
|
@@ -687,6 +878,8 @@ const BaseModelTable = (props) => {
|
|
|
687
878
|
sortOrder,
|
|
688
879
|
itemsOnPage,
|
|
689
880
|
query,
|
|
881
|
+
debouncedSearchText,
|
|
882
|
+
selectedLabels,
|
|
690
883
|
props.refreshToggle,
|
|
691
884
|
]);
|
|
692
885
|
const shouldDisableSort = (columnName) => {
|
|
@@ -961,6 +1154,12 @@ const BaseModelTable = (props) => {
|
|
|
961
1154
|
},
|
|
962
1155
|
};
|
|
963
1156
|
};
|
|
1157
|
+
const isSearchActive = () => {
|
|
1158
|
+
return debouncedSearchText.trim().length > 0 || selectedLabels.length > 0;
|
|
1159
|
+
};
|
|
1160
|
+
const getTableLoadingState = () => {
|
|
1161
|
+
return isSearchActive() ? false : isLoading;
|
|
1162
|
+
};
|
|
964
1163
|
const getTable = () => {
|
|
965
1164
|
var _a;
|
|
966
1165
|
return (React.createElement(Table, { onFilterChanged: (filterData) => {
|
|
@@ -1035,7 +1234,7 @@ const BaseModelTable = (props) => {
|
|
|
1035
1234
|
setSortBy(sortBy);
|
|
1036
1235
|
setSortOrder(sortOrder);
|
|
1037
1236
|
setTableView(null);
|
|
1038
|
-
}, singularLabel: props.singularName || model.singularName || "Item", pluralLabel: props.pluralName || model.pluralName || "Items", error: error, currentPageNumber: currentPageNumber, isLoading:
|
|
1237
|
+
}, singularLabel: props.singularName || model.singularName || "Item", pluralLabel: props.pluralName || model.pluralName || "Items", error: error, currentPageNumber: currentPageNumber, isLoading: getTableLoadingState(), enableDragAndDrop: props.enableDragAndDrop, dragDropIdField: "_id", dragDropIndexField: props.dragDropIndexField, totalItemsCount: totalItemsCount, data: data, id: props.id, columns: tableColumns, itemsOnPage: itemsOnPage, onDragDrop: async (id, newOrder) => {
|
|
1039
1238
|
if (!props.dragDropIndexField) {
|
|
1040
1239
|
return;
|
|
1041
1240
|
}
|
|
@@ -1104,7 +1303,7 @@ const BaseModelTable = (props) => {
|
|
|
1104
1303
|
},
|
|
1105
1304
|
});
|
|
1106
1305
|
await fetchItems();
|
|
1107
|
-
}, dragDropIdField: "_id", dragDropIndexField: props.dragDropIndexField, isLoading:
|
|
1306
|
+
}, dragDropIdField: "_id", dragDropIndexField: props.dragDropIndexField, isLoading: getTableLoadingState(), totalItemsCount: totalItemsCount, data: data, id: props.id, fields: fields, itemsOnPage: itemsOnPage, disablePagination: props.disablePagination || false, onNavigateToPage: async (pageNumber, itemsOnPage) => {
|
|
1108
1307
|
setCurrentPageNumber(pageNumber);
|
|
1109
1308
|
setItemsOnPage(itemsOnPage);
|
|
1110
1309
|
}, noItemsMessage: props.noItemsMessage || "", onRefreshClick: async () => {
|
|
@@ -1137,20 +1336,427 @@ const BaseModelTable = (props) => {
|
|
|
1137
1336
|
} },
|
|
1138
1337
|
React.createElement(Pill, { text: `${planName} Plan`, color: Yellow })))));
|
|
1139
1338
|
};
|
|
1339
|
+
const collapseSearch = () => {
|
|
1340
|
+
setSearchText("");
|
|
1341
|
+
setSelectedLabels([]);
|
|
1342
|
+
setIsSearchExpanded(false);
|
|
1343
|
+
};
|
|
1344
|
+
const parseLabelMention = (value) => {
|
|
1345
|
+
const atIndex = value.lastIndexOf("@");
|
|
1346
|
+
if (atIndex < 0) {
|
|
1347
|
+
return { hasMention: false, prefix: "", atIndex: -1 };
|
|
1348
|
+
}
|
|
1349
|
+
if (atIndex > 0) {
|
|
1350
|
+
const prev = value[atIndex - 1] || "";
|
|
1351
|
+
if (prev !== " " && prev !== "\t") {
|
|
1352
|
+
return { hasMention: false, prefix: "", atIndex: -1 };
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const after = value.substring(atIndex + 1);
|
|
1356
|
+
if (after.includes(" ") || after.includes("\t") || after.includes("\n")) {
|
|
1357
|
+
return { hasMention: false, prefix: "", atIndex: -1 };
|
|
1358
|
+
}
|
|
1359
|
+
return { hasMention: true, prefix: after, atIndex };
|
|
1360
|
+
};
|
|
1361
|
+
const addLabel = (label) => {
|
|
1362
|
+
setSelectedLabels((prev) => {
|
|
1363
|
+
if (prev.find((l) => {
|
|
1364
|
+
return l.id === label.id;
|
|
1365
|
+
})) {
|
|
1366
|
+
return prev;
|
|
1367
|
+
}
|
|
1368
|
+
return [...prev, label];
|
|
1369
|
+
});
|
|
1370
|
+
// Strip the `@<prefix>` token from the input.
|
|
1371
|
+
const mention = parseLabelMention(searchText);
|
|
1372
|
+
if (mention.hasMention) {
|
|
1373
|
+
const before = searchText.substring(0, mention.atIndex);
|
|
1374
|
+
setSearchText(before.replace(/\s+$/, ""));
|
|
1375
|
+
}
|
|
1376
|
+
setLabelDropdownIndex(0);
|
|
1377
|
+
};
|
|
1378
|
+
const removeLabel = (labelId) => {
|
|
1379
|
+
setSelectedLabels((prev) => {
|
|
1380
|
+
return prev.filter((l) => {
|
|
1381
|
+
return l.id !== labelId;
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
};
|
|
1385
|
+
const getSearchControl = () => {
|
|
1386
|
+
if (!props.searchableFields || props.searchableFields.length === 0) {
|
|
1387
|
+
return React.createElement(React.Fragment, null);
|
|
1388
|
+
}
|
|
1389
|
+
const pluralLabel = (props.pluralName ||
|
|
1390
|
+
model.pluralName ||
|
|
1391
|
+
"items").toLowerCase();
|
|
1392
|
+
const hasLabelSupport = Boolean(labelFilterConfig);
|
|
1393
|
+
const defaultPlaceholder = hasLabelSupport
|
|
1394
|
+
? `Search ${pluralLabel}… (try @ for labels)`
|
|
1395
|
+
: `Search ${pluralLabel} by name, description…`;
|
|
1396
|
+
const placeholder = props.searchPlaceholder || defaultPlaceholder;
|
|
1397
|
+
/*
|
|
1398
|
+
* Effective search = input minus the trailing @<prefix> mention. The pill
|
|
1399
|
+
* + result-count UI should reflect this so typing "@bug" doesn't claim
|
|
1400
|
+
* "0 matches" before the user has actually committed the label.
|
|
1401
|
+
*/
|
|
1402
|
+
const stripTrailingMentionForUi = (v) => {
|
|
1403
|
+
const m = parseLabelMention(v);
|
|
1404
|
+
return m.hasMention ? v.substring(0, m.atIndex).trimEnd() : v;
|
|
1405
|
+
};
|
|
1406
|
+
const trimmedSearch = stripTrailingMentionForUi(searchText).trim();
|
|
1407
|
+
const trimmedActive = stripTrailingMentionForUi(debouncedSearchText).trim();
|
|
1408
|
+
const hasActiveSearch = trimmedActive.length > 0;
|
|
1409
|
+
const hasSelectedLabels = selectedLabels.length > 0;
|
|
1410
|
+
/*
|
|
1411
|
+
* "isSearching" covers both phases — typing-in-flight (before the
|
|
1412
|
+
* debounce fires) AND the actual API request that follows. The table
|
|
1413
|
+
* loader is suppressed during the latter, so this spinner is the only
|
|
1414
|
+
* loading indicator the user sees during a search-driven fetch.
|
|
1415
|
+
*/
|
|
1416
|
+
const isTypingInFlight = trimmedSearch.length > 0 && trimmedSearch !== trimmedActive;
|
|
1417
|
+
const isSearching = isTypingInFlight || (isLoading && (hasActiveSearch || hasSelectedLabels));
|
|
1418
|
+
const showMatchPill = !isSearching && (hasActiveSearch || hasSelectedLabels);
|
|
1419
|
+
const expanded = isSearchExpanded || hasActiveSearch || hasSelectedLabels;
|
|
1420
|
+
const mention = parseLabelMention(searchText);
|
|
1421
|
+
const showLabelDropdown = hasLabelSupport && isSearchFocused && mention.hasMention;
|
|
1422
|
+
const lowerPrefix = mention.prefix.toLowerCase();
|
|
1423
|
+
const dropdownLabels = availableLabels
|
|
1424
|
+
.filter((l) => {
|
|
1425
|
+
return !selectedLabels.find((s) => {
|
|
1426
|
+
return s.id === l.id;
|
|
1427
|
+
});
|
|
1428
|
+
})
|
|
1429
|
+
.filter((l) => {
|
|
1430
|
+
return (lowerPrefix.length === 0 || l.name.toLowerCase().includes(lowerPrefix));
|
|
1431
|
+
})
|
|
1432
|
+
.slice(0, 8);
|
|
1433
|
+
const borderClass = isSearchFocused
|
|
1434
|
+
? "border-gray-400 ring-4 ring-gray-100 shadow-sm"
|
|
1435
|
+
: hasActiveSearch || hasSelectedLabels
|
|
1436
|
+
? "border-gray-300 shadow-sm"
|
|
1437
|
+
: "border-gray-200 shadow-sm";
|
|
1438
|
+
const iconColorClass = isSearchFocused || hasActiveSearch || hasSelectedLabels
|
|
1439
|
+
? "text-gray-700"
|
|
1440
|
+
: "text-gray-400";
|
|
1441
|
+
const selectDropdownItemAt = (idx) => {
|
|
1442
|
+
const item = dropdownLabels[idx];
|
|
1443
|
+
if (item) {
|
|
1444
|
+
addLabel(item);
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
return (React.createElement("div", { className: "relative flex w-full items-center" },
|
|
1448
|
+
React.createElement("div", { className: "relative flex w-full flex-col gap-1" },
|
|
1449
|
+
React.createElement("div", { className: `flex w-full items-center gap-2 rounded-lg border bg-white pl-3 pr-2 py-1.5 transition-all duration-200 ${borderClass}`, onClick: () => {
|
|
1450
|
+
var _a;
|
|
1451
|
+
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
1452
|
+
}, role: "presentation" },
|
|
1453
|
+
React.createElement(Icon, { icon: IconProp.Search, className: `h-4 w-4 flex-none transition-colors duration-200 ${iconColorClass}` }),
|
|
1454
|
+
React.createElement("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-1.5" },
|
|
1455
|
+
selectedLabels.map((label) => {
|
|
1456
|
+
return (React.createElement("span", { key: label.id, className: "inline-flex items-center gap-1 rounded-full bg-gray-50 py-0.5 pl-2 pr-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-200 transition-all hover:bg-gray-100", title: `Label: ${label.name}` },
|
|
1457
|
+
React.createElement("span", { className: "h-2 w-2 flex-none rounded-full", style: { backgroundColor: label.color }, "aria-hidden": "true" }),
|
|
1458
|
+
React.createElement("span", { className: "max-w-[8rem] truncate" }, label.name),
|
|
1459
|
+
React.createElement("button", { type: "button", onMouseDown: (e) => {
|
|
1460
|
+
e.preventDefault();
|
|
1461
|
+
}, onClick: () => {
|
|
1462
|
+
removeLabel(label.id);
|
|
1463
|
+
}, title: "Remove label", "aria-label": `Remove ${label.name}`, className: "ml-0.5 flex-none rounded-full p-0.5 text-gray-400 transition-colors hover:bg-gray-200 hover:text-gray-700" },
|
|
1464
|
+
React.createElement(Icon, { icon: IconProp.Close, className: "h-3 w-3" }))));
|
|
1465
|
+
}),
|
|
1466
|
+
React.createElement("input", { ref: searchInputRef, type: "text", value: searchText, onChange: (e) => {
|
|
1467
|
+
setSearchText(e.target.value);
|
|
1468
|
+
setLabelDropdownIndex(0);
|
|
1469
|
+
}, onFocus: () => {
|
|
1470
|
+
setIsSearchFocused(true);
|
|
1471
|
+
}, onBlur: () => {
|
|
1472
|
+
setIsSearchFocused(false);
|
|
1473
|
+
/*
|
|
1474
|
+
* Collapse only when the user blurs with nothing active —
|
|
1475
|
+
* no text and no selected labels.
|
|
1476
|
+
*/
|
|
1477
|
+
if (searchText.trim().length === 0 &&
|
|
1478
|
+
selectedLabels.length === 0) {
|
|
1479
|
+
setIsSearchExpanded(false);
|
|
1480
|
+
}
|
|
1481
|
+
}, onKeyDown: (e) => {
|
|
1482
|
+
var _a;
|
|
1483
|
+
if (showLabelDropdown && dropdownLabels.length > 0) {
|
|
1484
|
+
if (e.key === "ArrowDown") {
|
|
1485
|
+
e.preventDefault();
|
|
1486
|
+
setLabelDropdownIndex((i) => {
|
|
1487
|
+
return Math.min(i + 1, dropdownLabels.length - 1);
|
|
1488
|
+
});
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
if (e.key === "ArrowUp") {
|
|
1492
|
+
e.preventDefault();
|
|
1493
|
+
setLabelDropdownIndex((i) => {
|
|
1494
|
+
return Math.max(i - 1, 0);
|
|
1495
|
+
});
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (e.key === "Enter" || e.key === "Tab") {
|
|
1499
|
+
e.preventDefault();
|
|
1500
|
+
selectDropdownItemAt(labelDropdownIndex);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (e.key === "Backspace" && searchText.length === 0) {
|
|
1505
|
+
// Pop last label when backspacing on empty input.
|
|
1506
|
+
if (selectedLabels.length > 0) {
|
|
1507
|
+
const last = selectedLabels[selectedLabels.length - 1];
|
|
1508
|
+
if (last) {
|
|
1509
|
+
removeLabel(last.id);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (e.key === "Escape") {
|
|
1515
|
+
if (showLabelDropdown) {
|
|
1516
|
+
// Just cancel the @ mention parse — clear @ prefix.
|
|
1517
|
+
const m = parseLabelMention(searchText);
|
|
1518
|
+
if (m.hasMention) {
|
|
1519
|
+
setSearchText(searchText
|
|
1520
|
+
.substring(0, m.atIndex)
|
|
1521
|
+
.replace(/\s+$/, ""));
|
|
1522
|
+
}
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
if (searchText) {
|
|
1526
|
+
setSearchText("");
|
|
1527
|
+
}
|
|
1528
|
+
else if (selectedLabels.length > 0) {
|
|
1529
|
+
setSelectedLabels([]);
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
collapseSearch();
|
|
1533
|
+
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}, placeholder: selectedLabels.length === 0 ? placeholder : "Refine search…", spellCheck: false, autoComplete: "off", tabIndex: expanded ? 0 : -1, className: "min-w-[6rem] flex-1 bg-transparent py-1 text-sm text-gray-900 placeholder-gray-400 outline-none" })),
|
|
1537
|
+
isSearching && (React.createElement("div", { className: "flex-none text-gray-400", title: "Searching\u2026" },
|
|
1538
|
+
React.createElement(Icon, { icon: IconProp.Spinner, className: "h-4 w-4 animate-spin" }))),
|
|
1539
|
+
showMatchPill && totalItemsCount >= 0 && (React.createElement("span", { className: "flex-none whitespace-nowrap rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700", title: `${totalItemsCount} ${totalItemsCount === 1 ? "result" : "results"}` },
|
|
1540
|
+
totalItemsCount,
|
|
1541
|
+
" ",
|
|
1542
|
+
totalItemsCount === 1 ? "match" : "matches")),
|
|
1543
|
+
searchText.length > 0 || selectedLabels.length > 0 ? (React.createElement("button", { type: "button", onMouseDown: (e) => {
|
|
1544
|
+
e.preventDefault();
|
|
1545
|
+
}, onClick: () => {
|
|
1546
|
+
collapseSearch();
|
|
1547
|
+
}, title: "Clear search (Esc Esc)", "aria-label": "Clear search", className: "flex-none rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600" },
|
|
1548
|
+
React.createElement(Icon, { icon: IconProp.Close, className: "h-3.5 w-3.5" }))) : (React.createElement("kbd", { className: "hidden flex-none select-none items-center rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 font-mono text-[10px] font-medium text-gray-500 sm:inline-flex", title: "Press / to focus search" }, "/"))),
|
|
1549
|
+
showLabelDropdown && (React.createElement("div", { className: "absolute left-0 right-0 top-full z-20 mt-1.5 origin-top overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg ring-1 ring-black/5 transition-all duration-150 animate-in fade-in slide-in-from-top-1", onMouseDown: (e) => {
|
|
1550
|
+
// Prevent input blur on dropdown clicks.
|
|
1551
|
+
e.preventDefault();
|
|
1552
|
+
} },
|
|
1553
|
+
React.createElement("div", { className: "flex items-center justify-between border-b border-gray-100 px-3 py-2" },
|
|
1554
|
+
React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-gray-500" }, isLabelsLoading ? "Loading labels…" : "Filter by label"),
|
|
1555
|
+
React.createElement("span", { className: "text-[10px] text-gray-400" },
|
|
1556
|
+
React.createElement("kbd", { className: "font-mono" }, "\u2191"),
|
|
1557
|
+
React.createElement("kbd", { className: "ml-0.5 font-mono" }, "\u2193"),
|
|
1558
|
+
React.createElement("span", { className: "ml-1" }, "to navigate"),
|
|
1559
|
+
React.createElement("span", { className: "mx-1.5" }, "\u00B7"),
|
|
1560
|
+
React.createElement("kbd", { className: "font-mono" }, "\u21B5"),
|
|
1561
|
+
React.createElement("span", { className: "ml-1" }, "to select"))),
|
|
1562
|
+
React.createElement("div", { className: "max-h-64 overflow-y-auto py-1" },
|
|
1563
|
+
!isLabelsLoading && dropdownLabels.length === 0 && (React.createElement("div", { className: "px-3 py-3 text-sm text-gray-500" }, availableLabels.length === 0
|
|
1564
|
+
? "No labels available for this resource."
|
|
1565
|
+
: `No labels matching "${mention.prefix}"`)),
|
|
1566
|
+
dropdownLabels.map((label, idx) => {
|
|
1567
|
+
const isActive = idx === labelDropdownIndex;
|
|
1568
|
+
return (React.createElement("button", { key: label.id, type: "button", onMouseEnter: () => {
|
|
1569
|
+
setLabelDropdownIndex(idx);
|
|
1570
|
+
}, onMouseDown: (e) => {
|
|
1571
|
+
e.preventDefault();
|
|
1572
|
+
}, onClick: () => {
|
|
1573
|
+
var _a;
|
|
1574
|
+
addLabel(label);
|
|
1575
|
+
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
1576
|
+
}, className: `flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors ${isActive
|
|
1577
|
+
? "bg-gray-100 text-gray-900"
|
|
1578
|
+
: "text-gray-700 hover:bg-gray-50"}` },
|
|
1579
|
+
React.createElement("span", { className: "h-2.5 w-2.5 flex-none rounded-full ring-1 ring-inset ring-black/5", style: { backgroundColor: label.color }, "aria-hidden": "true" }),
|
|
1580
|
+
React.createElement("span", { className: "flex-1 truncate" }, label.name),
|
|
1581
|
+
isActive && (React.createElement("span", { className: "text-[10px] font-medium uppercase tracking-wide text-gray-500" }, "\u21B5"))));
|
|
1582
|
+
})))))));
|
|
1583
|
+
};
|
|
1584
|
+
const splitButtonsForHeader = (buttons) => {
|
|
1585
|
+
let mainIndex = -1;
|
|
1586
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
1587
|
+
const b = buttons[i];
|
|
1588
|
+
if (React.isValidElement(b)) {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const c = b;
|
|
1592
|
+
if (c.buttonStyle === ButtonStyleType.NORMAL && c.icon === IconProp.Add) {
|
|
1593
|
+
mainIndex = i;
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
if (mainIndex < 0) {
|
|
1598
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
1599
|
+
const b = buttons[i];
|
|
1600
|
+
if (React.isValidElement(b)) {
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (b.buttonStyle === ButtonStyleType.NORMAL) {
|
|
1604
|
+
mainIndex = i;
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
if (mainIndex < 0) {
|
|
1610
|
+
return { main: null, rest: buttons };
|
|
1611
|
+
}
|
|
1612
|
+
const main = buttons[mainIndex];
|
|
1613
|
+
const rest = [
|
|
1614
|
+
...buttons.slice(0, mainIndex),
|
|
1615
|
+
...buttons.slice(mainIndex + 1),
|
|
1616
|
+
];
|
|
1617
|
+
return { main, rest };
|
|
1618
|
+
};
|
|
1619
|
+
const renderMainButton = (b) => {
|
|
1620
|
+
return (React.createElement(Button, { key: "model-table-main-action", title: b.title, buttonStyle: b.buttonStyle, buttonSize: b.buttonSize, className: b.className, onClick: () => {
|
|
1621
|
+
var _a;
|
|
1622
|
+
(_a = b.onClick) === null || _a === void 0 ? void 0 : _a.call(b);
|
|
1623
|
+
}, disabled: b.disabled, icon: b.icon, shortcutKey: b.shortcutKey, dataTestId: "card-button", isLoading: b.isLoading }));
|
|
1624
|
+
};
|
|
1625
|
+
/*
|
|
1626
|
+
* Icon-only card buttons (Refresh, Filter, …) have empty titles, so derive
|
|
1627
|
+
* a label from the icon for the More menu.
|
|
1628
|
+
*/
|
|
1629
|
+
// Fallback labels for icon-only buttons (Refresh, Filter, ...) in the More menu.
|
|
1630
|
+
const labelForIconButton = (icon) => {
|
|
1631
|
+
switch (icon) {
|
|
1632
|
+
case IconProp.Refresh:
|
|
1633
|
+
return "Refresh";
|
|
1634
|
+
case IconProp.Filter:
|
|
1635
|
+
return "Filter";
|
|
1636
|
+
case IconProp.Add:
|
|
1637
|
+
return "Add";
|
|
1638
|
+
case IconProp.Help:
|
|
1639
|
+
return "Help";
|
|
1640
|
+
case IconProp.Book:
|
|
1641
|
+
return "Documentation";
|
|
1642
|
+
case IconProp.Play:
|
|
1643
|
+
return "Watch Demo";
|
|
1644
|
+
case IconProp.Search:
|
|
1645
|
+
return "Search";
|
|
1646
|
+
default:
|
|
1647
|
+
return "Action";
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
const renderMoreMenu = (items) => {
|
|
1651
|
+
if (items.length === 0) {
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
const children = items.map((item, idx) => {
|
|
1655
|
+
if (React.isValidElement(item)) {
|
|
1656
|
+
return (React.createElement("div", { key: `more-${idx}`, className: "px-2 py-1" }, item));
|
|
1657
|
+
}
|
|
1658
|
+
const b = item;
|
|
1659
|
+
const label = b.title || labelForIconButton(b.icon);
|
|
1660
|
+
return (React.createElement(MoreMenuItem, { key: `more-${idx}`, text: label, icon: b.icon, onClick: () => {
|
|
1661
|
+
var _a;
|
|
1662
|
+
if (!b.disabled) {
|
|
1663
|
+
(_a = b.onClick) === null || _a === void 0 ? void 0 : _a.call(b);
|
|
1664
|
+
}
|
|
1665
|
+
}, className: b.disabled ? "opacity-40 pointer-events-none" : "" }));
|
|
1666
|
+
});
|
|
1667
|
+
return (React.createElement(MoreMenu, { key: "model-table-more-menu", menuIcon: IconProp.EllipsisHorizontal, text: "" }, children));
|
|
1668
|
+
};
|
|
1669
|
+
/*
|
|
1670
|
+
* Builds the right-hand side of the card header. All slots — search
|
|
1671
|
+
* trigger/bar, saved views, main button, more menu — stay mounted at all
|
|
1672
|
+
* times. State transitions are purely CSS so the inputs keep their focus,
|
|
1673
|
+
* the dropdown keeps its open state, and there's no mount/unmount flicker.
|
|
1674
|
+
*
|
|
1675
|
+
* Layout when collapsed: [🔍 trigger] [Saved Views] [main] [⋯]
|
|
1676
|
+
* Layout when expanded: [🔍 ━━━ wide search bar ━━━]
|
|
1677
|
+
* Saved views + main button + more menu fade and collapse-to-zero-width
|
|
1678
|
+
* when the search expands, freeing horizontal space for the bar.
|
|
1679
|
+
*/
|
|
1680
|
+
const getHeaderButtonsWithSearch = () => {
|
|
1681
|
+
const hasSearch = Boolean(props.searchableFields && props.searchableFields.length > 0);
|
|
1682
|
+
/*
|
|
1683
|
+
* Saved views get their own first-class slot in the header — never
|
|
1684
|
+
* collapsed into the overflow ⋯ menu. Render fresh on every call so the
|
|
1685
|
+
* inner TableViewElement always sees the current query / sort / itemsOnPage.
|
|
1686
|
+
*/
|
|
1687
|
+
const savedViewsElement = props.saveFilterProps
|
|
1688
|
+
? getSaveFilterDropdown()
|
|
1689
|
+
: null;
|
|
1690
|
+
if (cardButtons.length === 0 && !hasSearch) {
|
|
1691
|
+
return savedViewsElement ? [savedViewsElement] : cardButtons;
|
|
1692
|
+
}
|
|
1693
|
+
if (!hasSearch) {
|
|
1694
|
+
// Without search, just split into [Saved Views] [main] [⋯]; no special wrapping.
|
|
1695
|
+
const { main, rest } = splitButtonsForHeader(cardButtons);
|
|
1696
|
+
const composed = [];
|
|
1697
|
+
if (savedViewsElement) {
|
|
1698
|
+
composed.push(savedViewsElement);
|
|
1699
|
+
}
|
|
1700
|
+
if (main) {
|
|
1701
|
+
composed.push(renderMainButton(main));
|
|
1702
|
+
}
|
|
1703
|
+
const moreMenu = renderMoreMenu(rest);
|
|
1704
|
+
if (moreMenu) {
|
|
1705
|
+
composed.push(moreMenu);
|
|
1706
|
+
}
|
|
1707
|
+
return composed;
|
|
1708
|
+
}
|
|
1709
|
+
const trimmedActive = debouncedSearchText.trim();
|
|
1710
|
+
const isExpanded = isSearchExpanded || trimmedActive.length > 0 || selectedLabels.length > 0;
|
|
1711
|
+
const { main, rest } = splitButtonsForHeader(cardButtons);
|
|
1712
|
+
const moreMenu = renderMoreMenu(rest);
|
|
1713
|
+
const wrapped = (React.createElement("div", { key: "model-table-header-actions", className: "flex items-center gap-1.5" },
|
|
1714
|
+
React.createElement("div", { className: `relative shrink-0 transition-[width] duration-300 ease-out ${isExpanded ? "w-[22rem] sm:w-[26rem] lg:w-[32rem]" : "w-44 sm:w-56"}` },
|
|
1715
|
+
React.createElement("button", { type: "button", onClick: () => {
|
|
1716
|
+
setIsSearchExpanded(true);
|
|
1717
|
+
requestAnimationFrame(() => {
|
|
1718
|
+
var _a;
|
|
1719
|
+
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
1720
|
+
});
|
|
1721
|
+
}, title: "Search (/)", "aria-label": "Open search", tabIndex: isExpanded ? -1 : 0, className: `absolute inset-0 inline-flex items-center gap-2 rounded-lg border bg-white pl-3 pr-2 text-sm shadow-sm transition-all duration-200 ease-out ${isExpanded
|
|
1722
|
+
? "pointer-events-none border-gray-200 opacity-0"
|
|
1723
|
+
: "border-gray-200 opacity-100 hover:border-gray-300 hover:bg-gray-50"}` },
|
|
1724
|
+
React.createElement(Icon, { icon: IconProp.Search, className: "h-4 w-4 flex-none text-gray-400" }),
|
|
1725
|
+
React.createElement("span", { className: "flex-1 truncate text-left text-gray-400" }, "Search\u2026"),
|
|
1726
|
+
React.createElement("kbd", { className: "hidden flex-none select-none items-center rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 font-mono text-[10px] font-medium text-gray-500 sm:inline-flex" }, "/")),
|
|
1727
|
+
React.createElement("div", { className: `transition-opacity duration-200 ease-out ${isExpanded
|
|
1728
|
+
? "opacity-100 delay-150"
|
|
1729
|
+
: "pointer-events-none opacity-0"}` }, getSearchControl())),
|
|
1730
|
+
React.createElement("div", { className: `flex items-center transition-all duration-300 ease-out ${isExpanded
|
|
1731
|
+
? "max-w-0 -ml-1.5 gap-0 opacity-0 pointer-events-none"
|
|
1732
|
+
: "max-w-[600px] gap-1.5 opacity-100"}`, "aria-hidden": isExpanded },
|
|
1733
|
+
savedViewsElement,
|
|
1734
|
+
main && renderMainButton(main),
|
|
1735
|
+
moreMenu)));
|
|
1736
|
+
return [wrapped];
|
|
1737
|
+
};
|
|
1738
|
+
const getCardHeaderTitle = (originalTitle) => {
|
|
1739
|
+
return originalTitle;
|
|
1740
|
+
};
|
|
1140
1741
|
const getCardComponent = () => {
|
|
1742
|
+
const headerButtons = getHeaderButtonsWithSearch();
|
|
1141
1743
|
if (showAs === ShowAs.Table || showAs === ShowAs.List) {
|
|
1142
1744
|
return (React.createElement("div", null,
|
|
1143
|
-
props.cardProps && (React.createElement(Card, Object.assign({}, props.cardProps, { buttons:
|
|
1745
|
+
props.cardProps && (React.createElement(Card, Object.assign({}, props.cardProps, { buttons: headerButtons, bodyClassName: showAs === ShowAs.List
|
|
1144
1746
|
? "-ml-6 -mr-6 bg-gray-50 border-top"
|
|
1145
|
-
: "", title: getCardTitle(props.cardProps.title || "") }),
|
|
1747
|
+
: "", title: getCardHeaderTitle(getCardTitle(props.cardProps.title || "")) }),
|
|
1146
1748
|
tableColumns.length === 0 && props.columns.length > 0 ? (React.createElement(ErrorMessage, { message: `You are not authorized to view this table. You need any one of these permissions: ${PermissionHelper.getPermissionTitles(model.getReadPermissions()).join(", ")}` })) : (React.createElement(React.Fragment, null)),
|
|
1147
1749
|
tableColumns.length > 0 && showAs === ShowAs.Table ? (getTable()) : (React.createElement(React.Fragment, null)),
|
|
1148
1750
|
tableColumns.length > 0 && showAs === ShowAs.List ? (getList()) : (React.createElement(React.Fragment, null)))),
|
|
1751
|
+
!props.cardProps &&
|
|
1752
|
+
(showAs === ShowAs.Table || showAs === ShowAs.List) &&
|
|
1753
|
+
props.searchableFields &&
|
|
1754
|
+
props.searchableFields.length > 0 ? (React.createElement("div", { className: "mb-3 flex justify-end" }, getSearchControl())) : (React.createElement(React.Fragment, null)),
|
|
1149
1755
|
!props.cardProps && showAs === ShowAs.Table ? getTable() : React.createElement(React.Fragment, null),
|
|
1150
1756
|
!props.cardProps && showAs === ShowAs.List ? getList() : React.createElement(React.Fragment, null)));
|
|
1151
1757
|
}
|
|
1152
1758
|
return (React.createElement("div", null,
|
|
1153
|
-
props.cardProps && (React.createElement(Card, Object.assign({}, props.cardProps, { buttons:
|
|
1759
|
+
props.cardProps && (React.createElement(Card, Object.assign({}, props.cardProps, { buttons: headerButtons, title: getCardTitle(props.cardProps.title || "") }), getOrderedStatesList())),
|
|
1154
1760
|
!props.cardProps && getOrderedStatesList()));
|
|
1155
1761
|
};
|
|
1156
1762
|
return (React.createElement(React.Fragment, null,
|