@oneuptime/common 10.2.4 → 10.2.6
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/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/ModelTable/BaseModelTable.tsx +988 -4
- 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/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/ModelTable/BaseModelTable.js +591 -7
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- 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,
|
|
@@ -687,6 +881,8 @@ const BaseModelTable = (props) => {
|
|
|
687
881
|
sortOrder,
|
|
688
882
|
itemsOnPage,
|
|
689
883
|
query,
|
|
884
|
+
debouncedSearchText,
|
|
885
|
+
selectedLabels,
|
|
690
886
|
props.refreshToggle,
|
|
691
887
|
]);
|
|
692
888
|
const shouldDisableSort = (columnName) => {
|
|
@@ -1137,20 +1333,408 @@ const BaseModelTable = (props) => {
|
|
|
1137
1333
|
} },
|
|
1138
1334
|
React.createElement(Pill, { text: `${planName} Plan`, color: Yellow })))));
|
|
1139
1335
|
};
|
|
1336
|
+
const collapseSearch = () => {
|
|
1337
|
+
setSearchText("");
|
|
1338
|
+
setSelectedLabels([]);
|
|
1339
|
+
setIsSearchExpanded(false);
|
|
1340
|
+
};
|
|
1341
|
+
const parseLabelMention = (value) => {
|
|
1342
|
+
const atIndex = value.lastIndexOf("@");
|
|
1343
|
+
if (atIndex < 0) {
|
|
1344
|
+
return { hasMention: false, prefix: "", atIndex: -1 };
|
|
1345
|
+
}
|
|
1346
|
+
if (atIndex > 0) {
|
|
1347
|
+
const prev = value[atIndex - 1] || "";
|
|
1348
|
+
if (prev !== " " && prev !== "\t") {
|
|
1349
|
+
return { hasMention: false, prefix: "", atIndex: -1 };
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const after = value.substring(atIndex + 1);
|
|
1353
|
+
if (after.includes(" ") || after.includes("\t") || after.includes("\n")) {
|
|
1354
|
+
return { hasMention: false, prefix: "", atIndex: -1 };
|
|
1355
|
+
}
|
|
1356
|
+
return { hasMention: true, prefix: after, atIndex };
|
|
1357
|
+
};
|
|
1358
|
+
const addLabel = (label) => {
|
|
1359
|
+
setSelectedLabels((prev) => {
|
|
1360
|
+
if (prev.find((l) => {
|
|
1361
|
+
return l.id === label.id;
|
|
1362
|
+
})) {
|
|
1363
|
+
return prev;
|
|
1364
|
+
}
|
|
1365
|
+
return [...prev, label];
|
|
1366
|
+
});
|
|
1367
|
+
// Strip the `@<prefix>` token from the input.
|
|
1368
|
+
const mention = parseLabelMention(searchText);
|
|
1369
|
+
if (mention.hasMention) {
|
|
1370
|
+
const before = searchText.substring(0, mention.atIndex);
|
|
1371
|
+
setSearchText(before.replace(/\s+$/, ""));
|
|
1372
|
+
}
|
|
1373
|
+
setLabelDropdownIndex(0);
|
|
1374
|
+
};
|
|
1375
|
+
const removeLabel = (labelId) => {
|
|
1376
|
+
setSelectedLabels((prev) => {
|
|
1377
|
+
return prev.filter((l) => {
|
|
1378
|
+
return l.id !== labelId;
|
|
1379
|
+
});
|
|
1380
|
+
});
|
|
1381
|
+
};
|
|
1382
|
+
const getSearchControl = () => {
|
|
1383
|
+
if (!props.searchableFields || props.searchableFields.length === 0) {
|
|
1384
|
+
return React.createElement(React.Fragment, null);
|
|
1385
|
+
}
|
|
1386
|
+
const pluralLabel = (props.pluralName ||
|
|
1387
|
+
model.pluralName ||
|
|
1388
|
+
"items").toLowerCase();
|
|
1389
|
+
const hasLabelSupport = Boolean(labelFilterConfig);
|
|
1390
|
+
const defaultPlaceholder = hasLabelSupport
|
|
1391
|
+
? `Search ${pluralLabel}… (try @ for labels)`
|
|
1392
|
+
: `Search ${pluralLabel} by name, description…`;
|
|
1393
|
+
const placeholder = props.searchPlaceholder || defaultPlaceholder;
|
|
1394
|
+
/*
|
|
1395
|
+
* Effective search = input minus the trailing @<prefix> mention. The pill
|
|
1396
|
+
* + result-count UI should reflect this so typing "@bug" doesn't claim
|
|
1397
|
+
* "0 matches" before the user has actually committed the label.
|
|
1398
|
+
*/
|
|
1399
|
+
const stripTrailingMentionForUi = (v) => {
|
|
1400
|
+
const m = parseLabelMention(v);
|
|
1401
|
+
return m.hasMention ? v.substring(0, m.atIndex).trimEnd() : v;
|
|
1402
|
+
};
|
|
1403
|
+
const trimmedSearch = stripTrailingMentionForUi(searchText).trim();
|
|
1404
|
+
const trimmedActive = stripTrailingMentionForUi(debouncedSearchText).trim();
|
|
1405
|
+
const isSearching = trimmedSearch.length > 0 && trimmedSearch !== trimmedActive;
|
|
1406
|
+
const hasActiveSearch = trimmedActive.length > 0;
|
|
1407
|
+
const hasSelectedLabels = selectedLabels.length > 0;
|
|
1408
|
+
const showMatchPill = !isSearching && (hasActiveSearch || hasSelectedLabels);
|
|
1409
|
+
const expanded = isSearchExpanded || hasActiveSearch || hasSelectedLabels;
|
|
1410
|
+
const mention = parseLabelMention(searchText);
|
|
1411
|
+
const showLabelDropdown = hasLabelSupport && isSearchFocused && mention.hasMention;
|
|
1412
|
+
const lowerPrefix = mention.prefix.toLowerCase();
|
|
1413
|
+
const dropdownLabels = availableLabels
|
|
1414
|
+
.filter((l) => {
|
|
1415
|
+
return !selectedLabels.find((s) => {
|
|
1416
|
+
return s.id === l.id;
|
|
1417
|
+
});
|
|
1418
|
+
})
|
|
1419
|
+
.filter((l) => {
|
|
1420
|
+
return (lowerPrefix.length === 0 || l.name.toLowerCase().includes(lowerPrefix));
|
|
1421
|
+
})
|
|
1422
|
+
.slice(0, 8);
|
|
1423
|
+
const borderClass = isSearchFocused
|
|
1424
|
+
? "border-gray-400 ring-4 ring-gray-100 shadow-sm"
|
|
1425
|
+
: hasActiveSearch || hasSelectedLabels
|
|
1426
|
+
? "border-gray-300 shadow-sm"
|
|
1427
|
+
: "border-gray-200 shadow-sm";
|
|
1428
|
+
const iconColorClass = isSearchFocused || hasActiveSearch || hasSelectedLabels
|
|
1429
|
+
? "text-gray-700"
|
|
1430
|
+
: "text-gray-400";
|
|
1431
|
+
const selectDropdownItemAt = (idx) => {
|
|
1432
|
+
const item = dropdownLabels[idx];
|
|
1433
|
+
if (item) {
|
|
1434
|
+
addLabel(item);
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
return (React.createElement("div", { className: "relative flex w-full items-center" },
|
|
1438
|
+
React.createElement("div", { className: "relative flex w-full flex-col gap-1" },
|
|
1439
|
+
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: () => {
|
|
1440
|
+
var _a;
|
|
1441
|
+
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
1442
|
+
}, role: "presentation" },
|
|
1443
|
+
React.createElement(Icon, { icon: IconProp.Search, className: `h-4 w-4 flex-none transition-colors duration-200 ${iconColorClass}` }),
|
|
1444
|
+
React.createElement("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-1.5" },
|
|
1445
|
+
selectedLabels.map((label) => {
|
|
1446
|
+
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}` },
|
|
1447
|
+
React.createElement("span", { className: "h-2 w-2 flex-none rounded-full", style: { backgroundColor: label.color }, "aria-hidden": "true" }),
|
|
1448
|
+
React.createElement("span", { className: "max-w-[8rem] truncate" }, label.name),
|
|
1449
|
+
React.createElement("button", { type: "button", onMouseDown: (e) => {
|
|
1450
|
+
e.preventDefault();
|
|
1451
|
+
}, onClick: () => {
|
|
1452
|
+
removeLabel(label.id);
|
|
1453
|
+
}, 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" },
|
|
1454
|
+
React.createElement(Icon, { icon: IconProp.Close, className: "h-3 w-3" }))));
|
|
1455
|
+
}),
|
|
1456
|
+
React.createElement("input", { ref: searchInputRef, type: "text", value: searchText, onChange: (e) => {
|
|
1457
|
+
setSearchText(e.target.value);
|
|
1458
|
+
setLabelDropdownIndex(0);
|
|
1459
|
+
}, onFocus: () => {
|
|
1460
|
+
setIsSearchFocused(true);
|
|
1461
|
+
}, onBlur: () => {
|
|
1462
|
+
setIsSearchFocused(false);
|
|
1463
|
+
/*
|
|
1464
|
+
* Collapse only when the user blurs with nothing active —
|
|
1465
|
+
* no text and no selected labels.
|
|
1466
|
+
*/
|
|
1467
|
+
if (searchText.trim().length === 0 &&
|
|
1468
|
+
selectedLabels.length === 0) {
|
|
1469
|
+
setIsSearchExpanded(false);
|
|
1470
|
+
}
|
|
1471
|
+
}, onKeyDown: (e) => {
|
|
1472
|
+
var _a;
|
|
1473
|
+
if (showLabelDropdown && dropdownLabels.length > 0) {
|
|
1474
|
+
if (e.key === "ArrowDown") {
|
|
1475
|
+
e.preventDefault();
|
|
1476
|
+
setLabelDropdownIndex((i) => {
|
|
1477
|
+
return Math.min(i + 1, dropdownLabels.length - 1);
|
|
1478
|
+
});
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (e.key === "ArrowUp") {
|
|
1482
|
+
e.preventDefault();
|
|
1483
|
+
setLabelDropdownIndex((i) => {
|
|
1484
|
+
return Math.max(i - 1, 0);
|
|
1485
|
+
});
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
if (e.key === "Enter" || e.key === "Tab") {
|
|
1489
|
+
e.preventDefault();
|
|
1490
|
+
selectDropdownItemAt(labelDropdownIndex);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (e.key === "Backspace" && searchText.length === 0) {
|
|
1495
|
+
// Pop last label when backspacing on empty input.
|
|
1496
|
+
if (selectedLabels.length > 0) {
|
|
1497
|
+
const last = selectedLabels[selectedLabels.length - 1];
|
|
1498
|
+
if (last) {
|
|
1499
|
+
removeLabel(last.id);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (e.key === "Escape") {
|
|
1505
|
+
if (showLabelDropdown) {
|
|
1506
|
+
// Just cancel the @ mention parse — clear @ prefix.
|
|
1507
|
+
const m = parseLabelMention(searchText);
|
|
1508
|
+
if (m.hasMention) {
|
|
1509
|
+
setSearchText(searchText
|
|
1510
|
+
.substring(0, m.atIndex)
|
|
1511
|
+
.replace(/\s+$/, ""));
|
|
1512
|
+
}
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
if (searchText) {
|
|
1516
|
+
setSearchText("");
|
|
1517
|
+
}
|
|
1518
|
+
else if (selectedLabels.length > 0) {
|
|
1519
|
+
setSelectedLabels([]);
|
|
1520
|
+
}
|
|
1521
|
+
else {
|
|
1522
|
+
collapseSearch();
|
|
1523
|
+
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}, 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" })),
|
|
1527
|
+
isSearching && (React.createElement("div", { className: "flex-none text-gray-400", title: "Searching\u2026" },
|
|
1528
|
+
React.createElement(Icon, { icon: IconProp.Spinner, className: "h-4 w-4 animate-spin" }))),
|
|
1529
|
+
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"}` },
|
|
1530
|
+
totalItemsCount,
|
|
1531
|
+
" ",
|
|
1532
|
+
totalItemsCount === 1 ? "match" : "matches")),
|
|
1533
|
+
searchText.length > 0 || selectedLabels.length > 0 ? (React.createElement("button", { type: "button", onMouseDown: (e) => {
|
|
1534
|
+
e.preventDefault();
|
|
1535
|
+
}, onClick: () => {
|
|
1536
|
+
collapseSearch();
|
|
1537
|
+
}, 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" },
|
|
1538
|
+
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" }, "/"))),
|
|
1539
|
+
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) => {
|
|
1540
|
+
// Prevent input blur on dropdown clicks.
|
|
1541
|
+
e.preventDefault();
|
|
1542
|
+
} },
|
|
1543
|
+
React.createElement("div", { className: "flex items-center justify-between border-b border-gray-100 px-3 py-2" },
|
|
1544
|
+
React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-gray-500" }, isLabelsLoading ? "Loading labels…" : "Filter by label"),
|
|
1545
|
+
React.createElement("span", { className: "text-[10px] text-gray-400" },
|
|
1546
|
+
React.createElement("kbd", { className: "font-mono" }, "\u2191"),
|
|
1547
|
+
React.createElement("kbd", { className: "ml-0.5 font-mono" }, "\u2193"),
|
|
1548
|
+
React.createElement("span", { className: "ml-1" }, "to navigate"),
|
|
1549
|
+
React.createElement("span", { className: "mx-1.5" }, "\u00B7"),
|
|
1550
|
+
React.createElement("kbd", { className: "font-mono" }, "\u21B5"),
|
|
1551
|
+
React.createElement("span", { className: "ml-1" }, "to select"))),
|
|
1552
|
+
React.createElement("div", { className: "max-h-64 overflow-y-auto py-1" },
|
|
1553
|
+
!isLabelsLoading && dropdownLabels.length === 0 && (React.createElement("div", { className: "px-3 py-3 text-sm text-gray-500" }, availableLabels.length === 0
|
|
1554
|
+
? "No labels available for this resource."
|
|
1555
|
+
: `No labels matching "${mention.prefix}"`)),
|
|
1556
|
+
dropdownLabels.map((label, idx) => {
|
|
1557
|
+
const isActive = idx === labelDropdownIndex;
|
|
1558
|
+
return (React.createElement("button", { key: label.id, type: "button", onMouseEnter: () => {
|
|
1559
|
+
setLabelDropdownIndex(idx);
|
|
1560
|
+
}, onMouseDown: (e) => {
|
|
1561
|
+
e.preventDefault();
|
|
1562
|
+
}, onClick: () => {
|
|
1563
|
+
var _a;
|
|
1564
|
+
addLabel(label);
|
|
1565
|
+
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
1566
|
+
}, className: `flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors ${isActive
|
|
1567
|
+
? "bg-gray-100 text-gray-900"
|
|
1568
|
+
: "text-gray-700 hover:bg-gray-50"}` },
|
|
1569
|
+
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" }),
|
|
1570
|
+
React.createElement("span", { className: "flex-1 truncate" }, label.name),
|
|
1571
|
+
isActive && (React.createElement("span", { className: "text-[10px] font-medium uppercase tracking-wide text-gray-500" }, "\u21B5"))));
|
|
1572
|
+
})))))));
|
|
1573
|
+
};
|
|
1574
|
+
const splitButtonsForHeader = (buttons) => {
|
|
1575
|
+
let mainIndex = -1;
|
|
1576
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
1577
|
+
const b = buttons[i];
|
|
1578
|
+
if (React.isValidElement(b)) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
const c = b;
|
|
1582
|
+
if (c.buttonStyle === ButtonStyleType.NORMAL && c.icon === IconProp.Add) {
|
|
1583
|
+
mainIndex = i;
|
|
1584
|
+
break;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
if (mainIndex < 0) {
|
|
1588
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
1589
|
+
const b = buttons[i];
|
|
1590
|
+
if (React.isValidElement(b)) {
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
if (b.buttonStyle === ButtonStyleType.NORMAL) {
|
|
1594
|
+
mainIndex = i;
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (mainIndex < 0) {
|
|
1600
|
+
return { main: null, rest: buttons };
|
|
1601
|
+
}
|
|
1602
|
+
const main = buttons[mainIndex];
|
|
1603
|
+
const rest = [
|
|
1604
|
+
...buttons.slice(0, mainIndex),
|
|
1605
|
+
...buttons.slice(mainIndex + 1),
|
|
1606
|
+
];
|
|
1607
|
+
return { main, rest };
|
|
1608
|
+
};
|
|
1609
|
+
const renderMainButton = (b) => {
|
|
1610
|
+
return (React.createElement(Button, { key: "model-table-main-action", title: b.title, buttonStyle: b.buttonStyle, buttonSize: b.buttonSize, className: b.className, onClick: () => {
|
|
1611
|
+
var _a;
|
|
1612
|
+
(_a = b.onClick) === null || _a === void 0 ? void 0 : _a.call(b);
|
|
1613
|
+
}, disabled: b.disabled, icon: b.icon, shortcutKey: b.shortcutKey, dataTestId: "card-button", isLoading: b.isLoading }));
|
|
1614
|
+
};
|
|
1615
|
+
/*
|
|
1616
|
+
* Icon-only card buttons (Refresh, Filter, …) have empty titles, so derive
|
|
1617
|
+
* a label from the icon for the More menu.
|
|
1618
|
+
*/
|
|
1619
|
+
// Fallback labels for icon-only buttons (Refresh, Filter, ...) in the More menu.
|
|
1620
|
+
const labelForIconButton = (icon) => {
|
|
1621
|
+
switch (icon) {
|
|
1622
|
+
case IconProp.Refresh:
|
|
1623
|
+
return "Refresh";
|
|
1624
|
+
case IconProp.Filter:
|
|
1625
|
+
return "Filter";
|
|
1626
|
+
case IconProp.Add:
|
|
1627
|
+
return "Add";
|
|
1628
|
+
case IconProp.Help:
|
|
1629
|
+
return "Help";
|
|
1630
|
+
case IconProp.Book:
|
|
1631
|
+
return "Documentation";
|
|
1632
|
+
case IconProp.Play:
|
|
1633
|
+
return "Watch Demo";
|
|
1634
|
+
case IconProp.Search:
|
|
1635
|
+
return "Search";
|
|
1636
|
+
default:
|
|
1637
|
+
return "Action";
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
const renderMoreMenu = (items) => {
|
|
1641
|
+
if (items.length === 0) {
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1644
|
+
const children = items.map((item, idx) => {
|
|
1645
|
+
if (React.isValidElement(item)) {
|
|
1646
|
+
return (React.createElement("div", { key: `more-${idx}`, className: "px-2 py-1" }, item));
|
|
1647
|
+
}
|
|
1648
|
+
const b = item;
|
|
1649
|
+
const label = b.title || labelForIconButton(b.icon);
|
|
1650
|
+
return (React.createElement(MoreMenuItem, { key: `more-${idx}`, text: label, icon: b.icon, onClick: () => {
|
|
1651
|
+
var _a;
|
|
1652
|
+
if (!b.disabled) {
|
|
1653
|
+
(_a = b.onClick) === null || _a === void 0 ? void 0 : _a.call(b);
|
|
1654
|
+
}
|
|
1655
|
+
}, className: b.disabled ? "opacity-40 pointer-events-none" : "" }));
|
|
1656
|
+
});
|
|
1657
|
+
return (React.createElement(MoreMenu, { key: "model-table-more-menu", menuIcon: IconProp.EllipsisHorizontal, text: "" }, children));
|
|
1658
|
+
};
|
|
1659
|
+
/*
|
|
1660
|
+
* Builds the right-hand side of the card header. All three slots — search
|
|
1661
|
+
* trigger/bar, main button, more menu — stay mounted at all times. State
|
|
1662
|
+
* transitions are purely CSS so the inputs keep their focus, the dropdown
|
|
1663
|
+
* keeps its open state, and there's no mount/unmount flicker.
|
|
1664
|
+
*
|
|
1665
|
+
* Layout when collapsed: [🔍 trigger] [main] [⋯]
|
|
1666
|
+
* Layout when expanded: [🔍 ━━━ wide search bar ━━━]
|
|
1667
|
+
* The main button + more menu fade and collapse-to-zero-width when the
|
|
1668
|
+
* search expands, freeing horizontal space for the bar.
|
|
1669
|
+
*/
|
|
1670
|
+
const getHeaderButtonsWithSearch = () => {
|
|
1671
|
+
const hasSearch = Boolean(props.searchableFields && props.searchableFields.length > 0);
|
|
1672
|
+
if (cardButtons.length === 0 && !hasSearch) {
|
|
1673
|
+
return cardButtons;
|
|
1674
|
+
}
|
|
1675
|
+
if (!hasSearch) {
|
|
1676
|
+
// Without search, just split into [main] [⋯]; no special wrapping.
|
|
1677
|
+
const { main, rest } = splitButtonsForHeader(cardButtons);
|
|
1678
|
+
const composed = [];
|
|
1679
|
+
if (main) {
|
|
1680
|
+
composed.push(renderMainButton(main));
|
|
1681
|
+
}
|
|
1682
|
+
const moreMenu = renderMoreMenu(rest);
|
|
1683
|
+
if (moreMenu) {
|
|
1684
|
+
composed.push(moreMenu);
|
|
1685
|
+
}
|
|
1686
|
+
return composed;
|
|
1687
|
+
}
|
|
1688
|
+
const trimmedActive = debouncedSearchText.trim();
|
|
1689
|
+
const isExpanded = isSearchExpanded || trimmedActive.length > 0 || selectedLabels.length > 0;
|
|
1690
|
+
const { main, rest } = splitButtonsForHeader(cardButtons);
|
|
1691
|
+
const moreMenu = renderMoreMenu(rest);
|
|
1692
|
+
const wrapped = (React.createElement("div", { key: "model-table-header-actions", className: "flex items-center gap-1.5" },
|
|
1693
|
+
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"}` },
|
|
1694
|
+
React.createElement("button", { type: "button", onClick: () => {
|
|
1695
|
+
setIsSearchExpanded(true);
|
|
1696
|
+
requestAnimationFrame(() => {
|
|
1697
|
+
var _a;
|
|
1698
|
+
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
1699
|
+
});
|
|
1700
|
+
}, 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
|
|
1701
|
+
? "pointer-events-none border-gray-200 opacity-0"
|
|
1702
|
+
: "border-gray-200 opacity-100 hover:border-gray-300 hover:bg-gray-50"}` },
|
|
1703
|
+
React.createElement(Icon, { icon: IconProp.Search, className: "h-4 w-4 flex-none text-gray-400" }),
|
|
1704
|
+
React.createElement("span", { className: "flex-1 truncate text-left text-gray-400" }, "Search\u2026"),
|
|
1705
|
+
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" }, "/")),
|
|
1706
|
+
React.createElement("div", { className: `transition-opacity duration-200 ease-out ${isExpanded
|
|
1707
|
+
? "opacity-100 delay-150"
|
|
1708
|
+
: "pointer-events-none opacity-0"}` }, getSearchControl())),
|
|
1709
|
+
React.createElement("div", { className: `flex items-center transition-all duration-300 ease-out ${isExpanded
|
|
1710
|
+
? "max-w-0 -ml-1.5 gap-0 opacity-0 pointer-events-none"
|
|
1711
|
+
: "max-w-[600px] gap-1.5 opacity-100"}`, "aria-hidden": isExpanded },
|
|
1712
|
+
main && renderMainButton(main),
|
|
1713
|
+
moreMenu)));
|
|
1714
|
+
return [wrapped];
|
|
1715
|
+
};
|
|
1716
|
+
const getCardHeaderTitle = (originalTitle) => {
|
|
1717
|
+
return originalTitle;
|
|
1718
|
+
};
|
|
1140
1719
|
const getCardComponent = () => {
|
|
1720
|
+
const headerButtons = getHeaderButtonsWithSearch();
|
|
1141
1721
|
if (showAs === ShowAs.Table || showAs === ShowAs.List) {
|
|
1142
1722
|
return (React.createElement("div", null,
|
|
1143
|
-
props.cardProps && (React.createElement(Card, Object.assign({}, props.cardProps, { buttons:
|
|
1723
|
+
props.cardProps && (React.createElement(Card, Object.assign({}, props.cardProps, { buttons: headerButtons, bodyClassName: showAs === ShowAs.List
|
|
1144
1724
|
? "-ml-6 -mr-6 bg-gray-50 border-top"
|
|
1145
|
-
: "", title: getCardTitle(props.cardProps.title || "") }),
|
|
1725
|
+
: "", title: getCardHeaderTitle(getCardTitle(props.cardProps.title || "")) }),
|
|
1146
1726
|
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
1727
|
tableColumns.length > 0 && showAs === ShowAs.Table ? (getTable()) : (React.createElement(React.Fragment, null)),
|
|
1148
1728
|
tableColumns.length > 0 && showAs === ShowAs.List ? (getList()) : (React.createElement(React.Fragment, null)))),
|
|
1729
|
+
!props.cardProps &&
|
|
1730
|
+
(showAs === ShowAs.Table || showAs === ShowAs.List) &&
|
|
1731
|
+
props.searchableFields &&
|
|
1732
|
+
props.searchableFields.length > 0 ? (React.createElement("div", { className: "mb-3 flex justify-end" }, getSearchControl())) : (React.createElement(React.Fragment, null)),
|
|
1149
1733
|
!props.cardProps && showAs === ShowAs.Table ? getTable() : React.createElement(React.Fragment, null),
|
|
1150
1734
|
!props.cardProps && showAs === ShowAs.List ? getList() : React.createElement(React.Fragment, null)));
|
|
1151
1735
|
}
|
|
1152
1736
|
return (React.createElement("div", null,
|
|
1153
|
-
props.cardProps && (React.createElement(Card, Object.assign({}, props.cardProps, { buttons:
|
|
1737
|
+
props.cardProps && (React.createElement(Card, Object.assign({}, props.cardProps, { buttons: headerButtons, title: getCardTitle(props.cardProps.title || "") }), getOrderedStatesList())),
|
|
1154
1738
|
!props.cardProps && getOrderedStatesList()));
|
|
1155
1739
|
};
|
|
1156
1740
|
return (React.createElement(React.Fragment, null,
|