@oneuptime/common 11.7.2 → 11.7.3
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/AnalyticsModels/KubernetesCostAllocation.ts +103 -0
- package/Models/DatabaseModels/TableView.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.ts +13 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Tests/Models/AnalyticsModels/KubernetesCostAllocation.test.ts +16 -0
- package/Tests/Types/JSONFunctions.test.ts +103 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +679 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModal.test.tsx +572 -0
- package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +1678 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1094 -0
- package/Tests/Utils/UserPreferences.test.ts +563 -0
- package/Types/JSONFunctions.ts +7 -2
- package/Types/Kubernetes/KubernetesCostIngest.ts +37 -1
- package/UI/Components/MasterPage/MasterPage.tsx +65 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +387 -4
- package/UI/Components/ModelTable/Column.ts +17 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +420 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +482 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +326 -0
- package/UI/Components/ModelTable/TableView.tsx +24 -2
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +150 -0
- package/UI/Components/SideMenu/SideMenu.tsx +24 -4
- package/UI/Components/Table/Table.tsx +14 -1
- package/UI/Components/Table/TableRow.tsx +180 -175
- package/UI/Components/Table/Types/Column.ts +2 -0
- package/Utils/UserPreferences.ts +53 -0
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +86 -0
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TableView.js +40 -0
- package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.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/Types/JSONFunctions.js +8 -3
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesCostIngest.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +50 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +239 -7
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +189 -0
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +258 -0
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +168 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/TableView.js +13 -2
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +84 -0
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -0
- package/build/dist/UI/Components/SideMenu/SideMenu.js +15 -5
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +15 -2
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +11 -6
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/Utils/UserPreferences.js +33 -0
- package/build/dist/Utils/UserPreferences.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import Column from "./Column";
|
|
2
|
+
import Columns from "./Columns";
|
|
3
|
+
import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
4
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
5
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* ---------------------------------------------------------------------------
|
|
9
|
+
* Per-viewer column layout for a ModelTable
|
|
10
|
+
* ---------------------------------------------------------------------------
|
|
11
|
+
*
|
|
12
|
+
* A layout is two lists of column ids: the order the viewer arranged the
|
|
13
|
+
* columns in, and the ones they switched off. Both are stored - "hidden" is
|
|
14
|
+
* not the complement of "order" - because a column the viewer has never seen
|
|
15
|
+
* (one shipped in a later release, or a custom field added after they last
|
|
16
|
+
* touched this table) has to default to *visible* and land in a predictable
|
|
17
|
+
* spot, rather than silently vanishing because it is missing from `order`.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is pure so the rules below can be pinned by unit tests
|
|
20
|
+
* rather than inferred from a rendered table:
|
|
21
|
+
*
|
|
22
|
+
* - ids are derived from the declared field, never from array position,
|
|
23
|
+
* because `props.columns` is edited every release;
|
|
24
|
+
* - an id that no longer matches a column is dropped on read, the same way
|
|
25
|
+
* restored filters are sanitized, so a stale entry cannot wedge the table;
|
|
26
|
+
* - a layout that would leave nothing on screen is refused.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export interface ColumnPreference {
|
|
30
|
+
// Column ids in the order the viewer arranged them.
|
|
31
|
+
order: Array<string>;
|
|
32
|
+
// Column ids the viewer switched off.
|
|
33
|
+
hidden: Array<string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ModelTableColumn<
|
|
37
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
38
|
+
> = Column<TBaseModel>;
|
|
39
|
+
|
|
40
|
+
export const EmptyColumnPreference: ColumnPreference = {
|
|
41
|
+
order: [],
|
|
42
|
+
hidden: [],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type SlugifyFunction = (value: string) => string;
|
|
46
|
+
|
|
47
|
+
const slugify: SlugifyFunction = (value: string): string => {
|
|
48
|
+
return value
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
51
|
+
.replace(/^-+|-+$/g, "");
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type GetColumnBaseIdFunction = <
|
|
55
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
56
|
+
>(
|
|
57
|
+
column: ModelTableColumn<TBaseModel>,
|
|
58
|
+
) => string;
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* The identity a column would have if no other column competed for it. An
|
|
62
|
+
* explicit `id` always wins; otherwise it is the field the column reads,
|
|
63
|
+
* which is the one thing about a column that survives re-ordering, renaming
|
|
64
|
+
* and re-styling. `selectedProperty` is part of it so that every custom field
|
|
65
|
+
* on one JSON column gets its own id ("customFields.Severity").
|
|
66
|
+
*/
|
|
67
|
+
export const getColumnBaseId: GetColumnBaseIdFunction = <
|
|
68
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
69
|
+
>(
|
|
70
|
+
column: ModelTableColumn<TBaseModel>,
|
|
71
|
+
): string => {
|
|
72
|
+
if (column.id) {
|
|
73
|
+
return column.id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fieldKey: string | undefined = column.field
|
|
77
|
+
? Object.keys(column.field)[0]
|
|
78
|
+
: undefined;
|
|
79
|
+
|
|
80
|
+
if (fieldKey) {
|
|
81
|
+
return column.selectedProperty
|
|
82
|
+
? `${fieldKey}.${column.selectedProperty}`
|
|
83
|
+
: fieldKey;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return slugify(column.title || "") || "column";
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type GetColumnIdsFunction = <
|
|
90
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
91
|
+
>(
|
|
92
|
+
columns: Columns<TBaseModel>,
|
|
93
|
+
) => Array<string>;
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* Ids for a whole column set, in declaration order and guaranteed unique.
|
|
97
|
+
*
|
|
98
|
+
* Collisions are real and common: a table often carries several cells that
|
|
99
|
+
* render from `getElement` off the same placeholder `field: { _id: true }`.
|
|
100
|
+
* Those are disambiguated by title first (stable, and what the viewer
|
|
101
|
+
* actually sees in the picker) and only then by position, which is the one
|
|
102
|
+
* part that can shift when a column is inserted. A shifted id is harmless -
|
|
103
|
+
* it simply reads as a new column, so it defaults to visible.
|
|
104
|
+
*/
|
|
105
|
+
export const getColumnIds: GetColumnIdsFunction = <
|
|
106
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
107
|
+
>(
|
|
108
|
+
columns: Columns<TBaseModel>,
|
|
109
|
+
): Array<string> => {
|
|
110
|
+
const baseIds: Array<string> = columns.map(
|
|
111
|
+
(column: ModelTableColumn<TBaseModel>) => {
|
|
112
|
+
return getColumnBaseId<TBaseModel>(column);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const baseIdCounts: Record<string, number> = {};
|
|
117
|
+
|
|
118
|
+
for (const baseId of baseIds) {
|
|
119
|
+
baseIdCounts[baseId] = (baseIdCounts[baseId] || 0) + 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const candidateIds: Array<string> = columns.map(
|
|
123
|
+
(column: ModelTableColumn<TBaseModel>, index: number) => {
|
|
124
|
+
const baseId: string = baseIds[index] as string;
|
|
125
|
+
|
|
126
|
+
if ((baseIdCounts[baseId] || 0) < 2) {
|
|
127
|
+
return baseId;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const titleSlug: string = slugify(column.title || "");
|
|
131
|
+
|
|
132
|
+
return titleSlug ? `${baseId}:${titleSlug}` : baseId;
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const usedIds: Record<string, number> = {};
|
|
137
|
+
const ids: Array<string> = [];
|
|
138
|
+
|
|
139
|
+
for (const candidateId of candidateIds) {
|
|
140
|
+
const timesUsed: number = usedIds[candidateId] || 0;
|
|
141
|
+
usedIds[candidateId] = timesUsed + 1;
|
|
142
|
+
ids.push(timesUsed === 0 ? candidateId : `${candidateId}#${timesUsed + 1}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return ids;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export interface CustomizableColumn<
|
|
149
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
150
|
+
> {
|
|
151
|
+
id: string;
|
|
152
|
+
column: ModelTableColumn<TBaseModel>;
|
|
153
|
+
isVisible: boolean;
|
|
154
|
+
// Pinned columns are always shown and cannot be moved.
|
|
155
|
+
isPinned: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type IsDefaultHiddenFunction = <
|
|
159
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
160
|
+
>(
|
|
161
|
+
column: ModelTableColumn<TBaseModel>,
|
|
162
|
+
) => boolean;
|
|
163
|
+
|
|
164
|
+
const isHiddenByDefault: IsDefaultHiddenFunction = <
|
|
165
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
166
|
+
>(
|
|
167
|
+
column: ModelTableColumn<TBaseModel>,
|
|
168
|
+
): boolean => {
|
|
169
|
+
return Boolean(column.isHiddenByDefault) && !column.isNotCustomizable;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export type GetCustomizableColumnsFunction = <
|
|
173
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
174
|
+
>(data: {
|
|
175
|
+
columns: Columns<TBaseModel>;
|
|
176
|
+
preference?: ColumnPreference | null | undefined;
|
|
177
|
+
}) => Array<CustomizableColumn<TBaseModel>>;
|
|
178
|
+
|
|
179
|
+
/*
|
|
180
|
+
* The picker's model: every column the viewer may act on, already in the
|
|
181
|
+
* order they arranged them, each tagged with whether it is currently on.
|
|
182
|
+
*
|
|
183
|
+
* Pinned columns keep their declared position rather than being dragged
|
|
184
|
+
* around by a stale layout - they are not in the picker, so the viewer has no
|
|
185
|
+
* way to put them back if a persisted `order` moved them.
|
|
186
|
+
*/
|
|
187
|
+
export const getCustomizableColumns: GetCustomizableColumnsFunction = <
|
|
188
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
189
|
+
>(data: {
|
|
190
|
+
columns: Columns<TBaseModel>;
|
|
191
|
+
preference?: ColumnPreference | null | undefined;
|
|
192
|
+
}): Array<CustomizableColumn<TBaseModel>> => {
|
|
193
|
+
const { columns, preference } = data;
|
|
194
|
+
|
|
195
|
+
const ids: Array<string> = getColumnIds<TBaseModel>(columns);
|
|
196
|
+
|
|
197
|
+
const entries: Array<CustomizableColumn<TBaseModel>> = columns.map(
|
|
198
|
+
(column: ModelTableColumn<TBaseModel>, index: number) => {
|
|
199
|
+
const id: string = ids[index] as string;
|
|
200
|
+
const isPinned: boolean = Boolean(column.isNotCustomizable);
|
|
201
|
+
|
|
202
|
+
let isVisible: boolean = !isHiddenByDefault<TBaseModel>(column);
|
|
203
|
+
|
|
204
|
+
if (preference && !isPinned) {
|
|
205
|
+
if (preference.hidden.includes(id)) {
|
|
206
|
+
isVisible = false;
|
|
207
|
+
} else if (preference.order.includes(id)) {
|
|
208
|
+
/*
|
|
209
|
+
* The viewer has arranged this column at least once and did not
|
|
210
|
+
* put it in `hidden`, so it is on - even if the table now ships it
|
|
211
|
+
* hidden by default.
|
|
212
|
+
*/
|
|
213
|
+
isVisible = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { id, column, isVisible, isPinned };
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (!preference || preference.order.length === 0) {
|
|
222
|
+
return entries;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const orderIndexById: Record<string, number> = {};
|
|
226
|
+
|
|
227
|
+
preference.order.forEach((id: string, index: number) => {
|
|
228
|
+
if (orderIndexById[id] === undefined) {
|
|
229
|
+
orderIndexById[id] = index;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
* A column the layout does not mention keeps its declared neighbours: it
|
|
235
|
+
* sorts just after the last arranged column that precedes it in the
|
|
236
|
+
* declaration, so a newly shipped column appears where its author put it
|
|
237
|
+
* rather than being swept to the end.
|
|
238
|
+
*/
|
|
239
|
+
const sortKeys: Array<number> = [];
|
|
240
|
+
let lastKnownOrderIndex: number = -1;
|
|
241
|
+
|
|
242
|
+
entries.forEach((entry: CustomizableColumn<TBaseModel>) => {
|
|
243
|
+
const knownIndex: number | undefined = entry.isPinned
|
|
244
|
+
? undefined
|
|
245
|
+
: orderIndexById[entry.id];
|
|
246
|
+
|
|
247
|
+
if (knownIndex !== undefined) {
|
|
248
|
+
lastKnownOrderIndex = knownIndex;
|
|
249
|
+
sortKeys.push(knownIndex);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
sortKeys.push(lastKnownOrderIndex + 0.5);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return entries
|
|
257
|
+
.map(
|
|
258
|
+
(
|
|
259
|
+
entry: CustomizableColumn<TBaseModel>,
|
|
260
|
+
index: number,
|
|
261
|
+
): { entry: CustomizableColumn<TBaseModel>; key: number; at: number } => {
|
|
262
|
+
return { entry, key: sortKeys[index] as number, at: index };
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
.sort(
|
|
266
|
+
(
|
|
267
|
+
a: { key: number; at: number },
|
|
268
|
+
b: { key: number; at: number },
|
|
269
|
+
): number => {
|
|
270
|
+
// Stable: equal keys keep declaration order.
|
|
271
|
+
return a.key === b.key ? a.at - b.at : a.key - b.key;
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
.map(
|
|
275
|
+
(item: {
|
|
276
|
+
entry: CustomizableColumn<TBaseModel>;
|
|
277
|
+
}): CustomizableColumn<TBaseModel> => {
|
|
278
|
+
return item.entry;
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export type ApplyColumnPreferenceFunction = <
|
|
284
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
285
|
+
>(data: {
|
|
286
|
+
columns: Columns<TBaseModel>;
|
|
287
|
+
preference?: ColumnPreference | null | undefined;
|
|
288
|
+
}) => Columns<TBaseModel>;
|
|
289
|
+
|
|
290
|
+
/*
|
|
291
|
+
* The column set the table should actually render. Refuses to return an empty
|
|
292
|
+
* set: a layout that hides everything would replace the table with an empty
|
|
293
|
+
* shell the viewer cannot get out of, so it falls back to showing everything.
|
|
294
|
+
*/
|
|
295
|
+
export const applyColumnPreference: ApplyColumnPreferenceFunction = <
|
|
296
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
297
|
+
>(data: {
|
|
298
|
+
columns: Columns<TBaseModel>;
|
|
299
|
+
preference?: ColumnPreference | null | undefined;
|
|
300
|
+
}): Columns<TBaseModel> => {
|
|
301
|
+
const entries: Array<CustomizableColumn<TBaseModel>> =
|
|
302
|
+
getCustomizableColumns<TBaseModel>(data);
|
|
303
|
+
|
|
304
|
+
const visible: Columns<TBaseModel> = entries
|
|
305
|
+
.filter((entry: CustomizableColumn<TBaseModel>) => {
|
|
306
|
+
return entry.isVisible;
|
|
307
|
+
})
|
|
308
|
+
.map((entry: CustomizableColumn<TBaseModel>) => {
|
|
309
|
+
return entry.column;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (visible.length === 0) {
|
|
313
|
+
return entries.map((entry: CustomizableColumn<TBaseModel>) => {
|
|
314
|
+
return entry.column;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return visible;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export type BuildColumnPreferenceFunction = <
|
|
322
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
323
|
+
>(
|
|
324
|
+
entries: Array<CustomizableColumn<TBaseModel>>,
|
|
325
|
+
) => ColumnPreference;
|
|
326
|
+
|
|
327
|
+
/*
|
|
328
|
+
* The inverse of getCustomizableColumns: turn what the picker is showing back
|
|
329
|
+
* into something storable. Pinned columns are left out entirely - they are
|
|
330
|
+
* not the viewer's to arrange, so recording them would only create entries
|
|
331
|
+
* that go stale when a table stops pinning a column.
|
|
332
|
+
*/
|
|
333
|
+
export const buildColumnPreference: BuildColumnPreferenceFunction = <
|
|
334
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
335
|
+
>(
|
|
336
|
+
entries: Array<CustomizableColumn<TBaseModel>>,
|
|
337
|
+
): ColumnPreference => {
|
|
338
|
+
const order: Array<string> = [];
|
|
339
|
+
const hidden: Array<string> = [];
|
|
340
|
+
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
if (entry.isPinned) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
order.push(entry.id);
|
|
347
|
+
|
|
348
|
+
if (!entry.isVisible) {
|
|
349
|
+
hidden.push(entry.id);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { order, hidden };
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export type SanitizeColumnPreferenceFunction = (data: {
|
|
357
|
+
preference: ColumnPreference | null;
|
|
358
|
+
knownColumnIds: Array<string>;
|
|
359
|
+
}) => ColumnPreference | null;
|
|
360
|
+
|
|
361
|
+
/*
|
|
362
|
+
* Drop everything that no longer names a column on this table. A stored
|
|
363
|
+
* layout outlives the release that produced it, so it routinely mentions
|
|
364
|
+
* columns that have since been renamed, removed, or - for custom fields -
|
|
365
|
+
* deleted from the project.
|
|
366
|
+
*/
|
|
367
|
+
export const sanitizeColumnPreference: SanitizeColumnPreferenceFunction =
|
|
368
|
+
(data: {
|
|
369
|
+
preference: ColumnPreference | null;
|
|
370
|
+
knownColumnIds: Array<string>;
|
|
371
|
+
}): ColumnPreference | null => {
|
|
372
|
+
const { preference, knownColumnIds } = data;
|
|
373
|
+
|
|
374
|
+
if (!preference) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const known: Set<string> = new Set(knownColumnIds);
|
|
379
|
+
|
|
380
|
+
const keep: (ids: Array<string>) => Array<string> = (
|
|
381
|
+
ids: Array<string>,
|
|
382
|
+
): Array<string> => {
|
|
383
|
+
const seen: Set<string> = new Set();
|
|
384
|
+
|
|
385
|
+
return ids.filter((id: string) => {
|
|
386
|
+
if (!known.has(id) || seen.has(id)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
seen.add(id);
|
|
390
|
+
return true;
|
|
391
|
+
});
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const sanitized: ColumnPreference = {
|
|
395
|
+
order: keep(preference.order),
|
|
396
|
+
hidden: keep(preference.hidden),
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
if (sanitized.order.length === 0 && sanitized.hidden.length === 0) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return sanitized;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
export type IsEmptyColumnPreferenceFunction = (
|
|
407
|
+
preference: ColumnPreference | null | undefined,
|
|
408
|
+
) => boolean;
|
|
409
|
+
|
|
410
|
+
export const isEmptyColumnPreference: IsEmptyColumnPreferenceFunction = (
|
|
411
|
+
preference: ColumnPreference | null | undefined,
|
|
412
|
+
): boolean => {
|
|
413
|
+
return Boolean(
|
|
414
|
+
!preference ||
|
|
415
|
+
(preference.order.length === 0 && preference.hidden.length === 0),
|
|
416
|
+
);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
export type ColumnPreferenceToJSONFunction = (
|
|
420
|
+
preference: ColumnPreference | null | undefined,
|
|
421
|
+
) => JSONObject | null;
|
|
422
|
+
|
|
423
|
+
// A layout that changes nothing is stored as nothing.
|
|
424
|
+
export const toJSON: ColumnPreferenceToJSONFunction = (
|
|
425
|
+
preference: ColumnPreference | null | undefined,
|
|
426
|
+
): JSONObject | null => {
|
|
427
|
+
if (isEmptyColumnPreference(preference)) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
order: [...preference!.order],
|
|
433
|
+
hidden: [...preference!.hidden],
|
|
434
|
+
};
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
export type ColumnPreferenceFromJSONFunction = (
|
|
438
|
+
json: JSONObject | null | undefined,
|
|
439
|
+
) => ColumnPreference | null;
|
|
440
|
+
|
|
441
|
+
/*
|
|
442
|
+
* Read a stored layout back. Anything on the way in is untrusted - it may
|
|
443
|
+
* have been hand-edited in devtools or written by an older release - so each
|
|
444
|
+
* list is validated on its own and a malformed one degrades to empty instead
|
|
445
|
+
* of taking the other down with it.
|
|
446
|
+
*/
|
|
447
|
+
export const fromJSON: ColumnPreferenceFromJSONFunction = (
|
|
448
|
+
json: JSONObject | null | undefined,
|
|
449
|
+
): ColumnPreference | null => {
|
|
450
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const readIds: (value: unknown) => Array<string> = (
|
|
455
|
+
value: unknown,
|
|
456
|
+
): Array<string> => {
|
|
457
|
+
if (!Array.isArray(value)) {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const seen: Set<string> = new Set();
|
|
462
|
+
|
|
463
|
+
return value.filter((entry: unknown): entry is string => {
|
|
464
|
+
if (typeof entry !== "string" || entry.length === 0 || seen.has(entry)) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
seen.add(entry);
|
|
468
|
+
return true;
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const preference: ColumnPreference = {
|
|
473
|
+
order: readIds((json as JSONObject)["order"]),
|
|
474
|
+
hidden: readIds((json as JSONObject)["hidden"]),
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
if (isEmptyColumnPreference(preference)) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return preference;
|
|
482
|
+
};
|