@finos/legend-application-studio 28.21.5 → 28.21.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/lib/__lib__/LegendStudioEvent.d.ts +4 -1
- package/lib/__lib__/LegendStudioEvent.d.ts.map +1 -1
- package/lib/__lib__/LegendStudioEvent.js +3 -0
- package/lib/__lib__/LegendStudioEvent.js.map +1 -1
- package/lib/__lib__/LegendStudioTelemetryHelper.d.ts +2 -1
- package/lib/__lib__/LegendStudioTelemetryHelper.d.ts.map +1 -1
- package/lib/__lib__/LegendStudioTelemetryHelper.js +11 -3
- package/lib/__lib__/LegendStudioTelemetryHelper.js.map +1 -1
- package/lib/__lib__/LegendStudioUserDataHelper.d.ts +41 -1
- package/lib/__lib__/LegendStudioUserDataHelper.d.ts.map +1 -1
- package/lib/__lib__/LegendStudioUserDataHelper.js +120 -1
- package/lib/__lib__/LegendStudioUserDataHelper.js.map +1 -1
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js +1 -1
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js.map +1 -1
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js +2 -1
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts.map +1 -1
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.js +1 -1
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.js.map +1 -1
- package/lib/components/workspace-setup/RecentWorkspacesPanel.d.ts +22 -0
- package/lib/components/workspace-setup/RecentWorkspacesPanel.d.ts.map +1 -0
- package/lib/components/workspace-setup/RecentWorkspacesPanel.js +80 -0
- package/lib/components/workspace-setup/RecentWorkspacesPanel.js.map +1 -0
- package/lib/components/workspace-setup/WorkspaceSetup.d.ts.map +1 -1
- package/lib/components/workspace-setup/WorkspaceSetup.js +61 -6
- package/lib/components/workspace-setup/WorkspaceSetup.js.map +1 -1
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/package.json +1 -1
- package/lib/stores/editor/EditorStore.d.ts.map +1 -1
- package/lib/stores/editor/EditorStore.js +31 -0
- package/lib/stores/editor/EditorStore.js.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js +4 -2
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js.map +1 -1
- package/lib/stores/editor/sidebar-state/ProjectOverviewState.d.ts.map +1 -1
- package/lib/stores/editor/sidebar-state/ProjectOverviewState.js +11 -0
- package/lib/stores/editor/sidebar-state/ProjectOverviewState.js.map +1 -1
- package/lib/stores/editor/sidebar-state/WorkspaceReviewState.d.ts.map +1 -1
- package/lib/stores/editor/sidebar-state/WorkspaceReviewState.js +11 -0
- package/lib/stores/editor/sidebar-state/WorkspaceReviewState.js.map +1 -1
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts.map +1 -1
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js +2 -1
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js.map +1 -1
- package/lib/stores/project-reviewer/ProjectReviewerStore.d.ts.map +1 -1
- package/lib/stores/project-reviewer/ProjectReviewerStore.js +12 -0
- package/lib/stores/project-reviewer/ProjectReviewerStore.js.map +1 -1
- package/lib/stores/workspace-setup/WorkspaceSetupStore.d.ts +17 -0
- package/lib/stores/workspace-setup/WorkspaceSetupStore.d.ts.map +1 -1
- package/lib/stores/workspace-setup/WorkspaceSetupStore.js +61 -0
- package/lib/stores/workspace-setup/WorkspaceSetupStore.js.map +1 -1
- package/package.json +10 -10
- package/src/__lib__/LegendStudioEvent.ts +3 -0
- package/src/__lib__/LegendStudioTelemetryHelper.ts +35 -11
- package/src/__lib__/LegendStudioUserDataHelper.ts +204 -1
- package/src/components/editor/editor-group/data-editor/EmbeddedDataEditor.tsx +1 -0
- package/src/components/editor/editor-group/data-editor/RelationElementsDataEditor.tsx +13 -5
- package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +1 -0
- package/src/components/editor/editor-group/testable/TestableSharedComponents.tsx +1 -0
- package/src/components/workspace-setup/RecentWorkspacesPanel.tsx +161 -0
- package/src/components/workspace-setup/WorkspaceSetup.tsx +97 -8
- package/src/stores/editor/EditorStore.ts +44 -0
- package/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts +5 -1
- package/src/stores/editor/sidebar-state/ProjectOverviewState.ts +14 -0
- package/src/stores/editor/sidebar-state/WorkspaceReviewState.ts +14 -0
- package/src/stores/editor/sidebar-state/dev-metadata/DevMetadataState.ts +8 -1
- package/src/stores/project-reviewer/ProjectReviewerStore.ts +15 -0
- package/src/stores/workspace-setup/WorkspaceSetupStore.ts +93 -0
- package/tsconfig.json +1 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { UserDataService } from '@finos/legend-application';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type PlainObject,
|
|
4
|
+
SerializationFactory,
|
|
5
|
+
returnUndefOnError,
|
|
6
|
+
usingModelSchema,
|
|
7
|
+
} from '@finos/legend-shared';
|
|
8
|
+
import { WorkspaceType } from '@finos/legend-server-sdlc';
|
|
9
|
+
import { createModelSchema, list, primitive } from 'serializr';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Copyright (c) 2020-present, Goldman Sachs
|
|
@@ -29,8 +36,104 @@ export enum LEGEND_STUDIO_USER_DATA_KEY {
|
|
|
29
36
|
// drop the toggle button in the tab header, and retarget the SCSS
|
|
30
37
|
// `.database-editor--light` block at the framework's color-theme tokens.
|
|
31
38
|
DATABASE_EDITOR_THEME = 'studio-editor.database-editor.theme',
|
|
39
|
+
// Recently-opened projects and (non-patch) workspaces shown on the
|
|
40
|
+
// workspace setup screen to speed up re-opening common work.
|
|
41
|
+
WORKSPACE_SETUP_RECENTS = 'studio-editor.workspace-setup.recents',
|
|
32
42
|
}
|
|
33
43
|
|
|
44
|
+
// --- Workspace setup recents -------------------------------------------------
|
|
45
|
+
|
|
46
|
+
const WORKSPACE_SETUP_RECENTS_VERSION = 1;
|
|
47
|
+
const MAX_RECENT_PROJECTS = 10;
|
|
48
|
+
const MAX_RECENT_WORKSPACES = 20;
|
|
49
|
+
|
|
50
|
+
export class RecentProjectEntry {
|
|
51
|
+
projectId!: string;
|
|
52
|
+
name!: string;
|
|
53
|
+
lastOpenedAt!: number;
|
|
54
|
+
|
|
55
|
+
static readonly serialization = new SerializationFactory(
|
|
56
|
+
createModelSchema(RecentProjectEntry, {
|
|
57
|
+
lastOpenedAt: primitive(),
|
|
58
|
+
name: primitive(),
|
|
59
|
+
projectId: primitive(),
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class RecentWorkspaceEntry {
|
|
65
|
+
projectId!: string;
|
|
66
|
+
workspaceId!: string;
|
|
67
|
+
workspaceType!: WorkspaceType;
|
|
68
|
+
lastOpenedAt!: number;
|
|
69
|
+
|
|
70
|
+
static readonly serialization = new SerializationFactory(
|
|
71
|
+
createModelSchema(RecentWorkspaceEntry, {
|
|
72
|
+
lastOpenedAt: primitive(),
|
|
73
|
+
projectId: primitive(),
|
|
74
|
+
workspaceId: primitive(),
|
|
75
|
+
// WorkspaceType is a string enum; stored/loaded as a plain string and
|
|
76
|
+
// validated when the entry is consumed.
|
|
77
|
+
workspaceType: primitive(),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class WorkspaceSetupRecents {
|
|
83
|
+
version: number = WORKSPACE_SETUP_RECENTS_VERSION;
|
|
84
|
+
projects: RecentProjectEntry[] = [];
|
|
85
|
+
workspaces: RecentWorkspaceEntry[] = [];
|
|
86
|
+
|
|
87
|
+
static readonly serialization = new SerializationFactory(
|
|
88
|
+
createModelSchema(WorkspaceSetupRecents, {
|
|
89
|
+
projects: list(usingModelSchema(RecentProjectEntry.serialization.schema)),
|
|
90
|
+
version: primitive(),
|
|
91
|
+
workspaces: list(
|
|
92
|
+
usingModelSchema(RecentWorkspaceEntry.serialization.schema),
|
|
93
|
+
),
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isValidWorkspaceType = (v: unknown): v is WorkspaceType =>
|
|
99
|
+
v === WorkspaceType.USER || v === WorkspaceType.GROUP;
|
|
100
|
+
|
|
101
|
+
const emptyRecents = (): WorkspaceSetupRecents => new WorkspaceSetupRecents();
|
|
102
|
+
|
|
103
|
+
const readRecents = (service: UserDataService): WorkspaceSetupRecents => {
|
|
104
|
+
const raw = returnUndefOnError(() =>
|
|
105
|
+
service.getObjectValue(LEGEND_STUDIO_USER_DATA_KEY.WORKSPACE_SETUP_RECENTS),
|
|
106
|
+
);
|
|
107
|
+
if (!raw) {
|
|
108
|
+
return emptyRecents();
|
|
109
|
+
}
|
|
110
|
+
const parsed = returnUndefOnError(() =>
|
|
111
|
+
WorkspaceSetupRecents.serialization.fromJson(
|
|
112
|
+
raw as PlainObject<WorkspaceSetupRecents>,
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
if (!parsed) {
|
|
116
|
+
return emptyRecents();
|
|
117
|
+
}
|
|
118
|
+
// Defensive post-checks: drop entries with invalid enum values and enforce
|
|
119
|
+
// the LRU caps in case the persisted blob was tampered with.
|
|
120
|
+
parsed.workspaces = parsed.workspaces
|
|
121
|
+
.filter((w) => isValidWorkspaceType(w.workspaceType))
|
|
122
|
+
.slice(0, MAX_RECENT_WORKSPACES);
|
|
123
|
+
parsed.projects = parsed.projects.slice(0, MAX_RECENT_PROJECTS);
|
|
124
|
+
return parsed;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const writeRecents = (
|
|
128
|
+
service: UserDataService,
|
|
129
|
+
recents: WorkspaceSetupRecents,
|
|
130
|
+
): void => {
|
|
131
|
+
service.persistValue(
|
|
132
|
+
LEGEND_STUDIO_USER_DATA_KEY.WORKSPACE_SETUP_RECENTS,
|
|
133
|
+
WorkspaceSetupRecents.serialization.toJson(recents),
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
34
137
|
export class LegendStudioUserDataHelper {
|
|
35
138
|
static globalTestRunner_getShowDependencyPanel(
|
|
36
139
|
service: UserDataService,
|
|
@@ -70,4 +173,104 @@ export class LegendStudioUserDataHelper {
|
|
|
70
173
|
val,
|
|
71
174
|
);
|
|
72
175
|
}
|
|
176
|
+
|
|
177
|
+
// --- Workspace setup recents ---------------------------------------------
|
|
178
|
+
|
|
179
|
+
static workspaceSetup_getRecentProjects(
|
|
180
|
+
service: UserDataService,
|
|
181
|
+
): RecentProjectEntry[] {
|
|
182
|
+
return readRecents(service).projects;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static workspaceSetup_getRecentWorkspaces(
|
|
186
|
+
service: UserDataService,
|
|
187
|
+
): RecentWorkspaceEntry[] {
|
|
188
|
+
return readRecents(service).workspaces;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static workspaceSetup_recordRecentProject(
|
|
192
|
+
service: UserDataService,
|
|
193
|
+
entry: { projectId: string; name: string },
|
|
194
|
+
): RecentProjectEntry[] {
|
|
195
|
+
const recents = readRecents(service);
|
|
196
|
+
const next = new RecentProjectEntry();
|
|
197
|
+
next.projectId = entry.projectId;
|
|
198
|
+
next.name = entry.name;
|
|
199
|
+
next.lastOpenedAt = Date.now();
|
|
200
|
+
recents.projects = [
|
|
201
|
+
next,
|
|
202
|
+
...recents.projects.filter((p) => p.projectId !== entry.projectId),
|
|
203
|
+
].slice(0, MAX_RECENT_PROJECTS);
|
|
204
|
+
writeRecents(service, recents);
|
|
205
|
+
return recents.projects;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static workspaceSetup_recordRecentWorkspace(
|
|
209
|
+
service: UserDataService,
|
|
210
|
+
entry: {
|
|
211
|
+
projectId: string;
|
|
212
|
+
workspaceId: string;
|
|
213
|
+
workspaceType: WorkspaceType;
|
|
214
|
+
},
|
|
215
|
+
): RecentWorkspaceEntry[] {
|
|
216
|
+
const recents = readRecents(service);
|
|
217
|
+
const next = new RecentWorkspaceEntry();
|
|
218
|
+
next.projectId = entry.projectId;
|
|
219
|
+
next.workspaceId = entry.workspaceId;
|
|
220
|
+
next.workspaceType = entry.workspaceType;
|
|
221
|
+
next.lastOpenedAt = Date.now();
|
|
222
|
+
recents.workspaces = [
|
|
223
|
+
next,
|
|
224
|
+
...recents.workspaces.filter(
|
|
225
|
+
(w) =>
|
|
226
|
+
!(
|
|
227
|
+
w.projectId === entry.projectId &&
|
|
228
|
+
w.workspaceId === entry.workspaceId &&
|
|
229
|
+
w.workspaceType === entry.workspaceType
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
].slice(0, MAX_RECENT_WORKSPACES);
|
|
233
|
+
writeRecents(service, recents);
|
|
234
|
+
return recents.workspaces;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static workspaceSetup_removeRecentProject(
|
|
238
|
+
service: UserDataService,
|
|
239
|
+
projectId: string,
|
|
240
|
+
): WorkspaceSetupRecents {
|
|
241
|
+
const recents = readRecents(service);
|
|
242
|
+
recents.projects = recents.projects.filter(
|
|
243
|
+
(p) => p.projectId !== projectId,
|
|
244
|
+
);
|
|
245
|
+
recents.workspaces = recents.workspaces.filter(
|
|
246
|
+
(w) => w.projectId !== projectId,
|
|
247
|
+
);
|
|
248
|
+
writeRecents(service, recents);
|
|
249
|
+
return recents;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static workspaceSetup_removeRecentWorkspace(
|
|
253
|
+
service: UserDataService,
|
|
254
|
+
entry: {
|
|
255
|
+
projectId: string;
|
|
256
|
+
workspaceId: string;
|
|
257
|
+
workspaceType: WorkspaceType;
|
|
258
|
+
},
|
|
259
|
+
): RecentWorkspaceEntry[] {
|
|
260
|
+
const recents = readRecents(service);
|
|
261
|
+
recents.workspaces = recents.workspaces.filter(
|
|
262
|
+
(w) =>
|
|
263
|
+
!(
|
|
264
|
+
w.projectId === entry.projectId &&
|
|
265
|
+
w.workspaceId === entry.workspaceId &&
|
|
266
|
+
w.workspaceType === entry.workspaceType
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
writeRecents(service, recents);
|
|
270
|
+
return recents.workspaces;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
static workspaceSetup_clearRecents(service: UserDataService): void {
|
|
274
|
+
writeRecents(service, emptyRecents());
|
|
275
|
+
}
|
|
73
276
|
}
|
|
@@ -400,6 +400,7 @@ export function renderEmbeddedDataEditor(
|
|
|
400
400
|
dataState={embeddedDataState}
|
|
401
401
|
isReadOnly={isReadOnly}
|
|
402
402
|
isSharedData={isSharedData}
|
|
403
|
+
hideColumnDefinitions={true}
|
|
403
404
|
/>
|
|
404
405
|
);
|
|
405
406
|
} else if (embeddedDataState instanceof DataElementReferenceState) {
|
|
@@ -426,7 +426,8 @@ export const RelationElementEditor = observer(
|
|
|
426
426
|
</div>
|
|
427
427
|
</div>
|
|
428
428
|
) : null}
|
|
429
|
-
{embeddedData.rows.length === 0
|
|
429
|
+
{embeddedData.rows.length === 0 &&
|
|
430
|
+
embeddedData.columns.length === 0 ? (
|
|
430
431
|
<div className="relation-test-data-editor__empty-data">
|
|
431
432
|
<div className="relation-test-data-editor__empty-text">
|
|
432
433
|
No test data rows. Click "+" below to start entering
|
|
@@ -440,13 +441,20 @@ export const RelationElementEditor = observer(
|
|
|
440
441
|
<tr>
|
|
441
442
|
{embeddedData.columns.map((column, columnIndex) => (
|
|
442
443
|
<th
|
|
443
|
-
key={
|
|
444
|
+
key={`col-${guaranteeNonNullable(columnIndex)}`}
|
|
444
445
|
className="relation-test-data-editor__th"
|
|
445
446
|
>
|
|
446
447
|
<div className="relation-test-data-editor__th__inner">
|
|
447
|
-
<
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
<input
|
|
449
|
+
className="relation-test-data-editor__th__label relation-test-data-editor__th__label--editable"
|
|
450
|
+
type="text"
|
|
451
|
+
value={column}
|
|
452
|
+
onChange={(e) =>
|
|
453
|
+
updateColumn(columnIndex, e.target.value)
|
|
454
|
+
}
|
|
455
|
+
disabled={!canEditColumns}
|
|
456
|
+
title={column}
|
|
457
|
+
/>
|
|
450
458
|
<button
|
|
451
459
|
className="relation-test-data-editor__th__delete"
|
|
452
460
|
onClick={() => removeColumn(columnIndex)}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-present, Goldman Sachs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { observer } from 'mobx-react-lite';
|
|
18
|
+
import {
|
|
19
|
+
FolderIcon,
|
|
20
|
+
HistoryIcon,
|
|
21
|
+
TimesIcon,
|
|
22
|
+
UserIcon,
|
|
23
|
+
UsersIcon,
|
|
24
|
+
} from '@finos/legend-art';
|
|
25
|
+
import { WorkspaceType } from '@finos/legend-server-sdlc';
|
|
26
|
+
import type { WorkspaceSetupStore } from '../../stores/workspace-setup/WorkspaceSetupStore.js';
|
|
27
|
+
import { generateEditorRoute } from '../../__lib__/LegendStudioNavigation.js';
|
|
28
|
+
|
|
29
|
+
const MAX_TILES = 6;
|
|
30
|
+
|
|
31
|
+
const formatRelativeTime = (timestamp: number): string => {
|
|
32
|
+
const diffMs = Date.now() - timestamp;
|
|
33
|
+
if (diffMs < 0) {
|
|
34
|
+
return 'just now';
|
|
35
|
+
}
|
|
36
|
+
const minutes = Math.floor(diffMs / 60_000);
|
|
37
|
+
if (minutes < 1) {
|
|
38
|
+
return 'just now';
|
|
39
|
+
}
|
|
40
|
+
if (minutes < 60) {
|
|
41
|
+
return `${minutes}m ago`;
|
|
42
|
+
}
|
|
43
|
+
const hours = Math.floor(minutes / 60);
|
|
44
|
+
if (hours < 24) {
|
|
45
|
+
return `${hours}h ago`;
|
|
46
|
+
}
|
|
47
|
+
const days = Math.floor(hours / 24);
|
|
48
|
+
if (days < 7) {
|
|
49
|
+
return `${days}d ago`;
|
|
50
|
+
}
|
|
51
|
+
const weeks = Math.floor(days / 7);
|
|
52
|
+
if (weeks < 4) {
|
|
53
|
+
return `${weeks}w ago`;
|
|
54
|
+
}
|
|
55
|
+
const months = Math.floor(days / 30);
|
|
56
|
+
if (months < 12) {
|
|
57
|
+
return `${months}mo ago`;
|
|
58
|
+
}
|
|
59
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const RecentWorkspacesPanel = observer(
|
|
63
|
+
(props: { setupStore: WorkspaceSetupStore }) => {
|
|
64
|
+
const { setupStore } = props;
|
|
65
|
+
const applicationStore = setupStore.applicationStore;
|
|
66
|
+
|
|
67
|
+
// Recents are stored in LRU order (most-recent first). Show the top N
|
|
68
|
+
// workspaces; map each to its project name (or fall back to projectId
|
|
69
|
+
// if the matching project entry was evicted independently).
|
|
70
|
+
const tiles = setupStore.recentWorkspaces.slice(0, MAX_TILES);
|
|
71
|
+
if (tiles.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const projectNameById = new Map(
|
|
76
|
+
setupStore.recentProjects.map((p) => [p.projectId, p.name]),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const openWorkspace = (
|
|
80
|
+
projectId: string,
|
|
81
|
+
workspaceId: string,
|
|
82
|
+
workspaceType: WorkspaceType,
|
|
83
|
+
): void => {
|
|
84
|
+
applicationStore.navigationService.navigator.goToLocation(
|
|
85
|
+
generateEditorRoute(projectId, undefined, workspaceId, workspaceType),
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="workspace-setup__recents">
|
|
91
|
+
<div className="workspace-setup__recents__header">
|
|
92
|
+
<div className="workspace-setup__recents__header__title">
|
|
93
|
+
<HistoryIcon />
|
|
94
|
+
<span>Recent workspaces</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="workspace-setup__recents__grid">
|
|
98
|
+
{tiles.map((entry) => {
|
|
99
|
+
const projectName =
|
|
100
|
+
projectNameById.get(entry.projectId) ?? entry.projectId;
|
|
101
|
+
const key = `${entry.projectId}::${entry.workspaceType}::${entry.workspaceId}`;
|
|
102
|
+
const handleRemove = (
|
|
103
|
+
event: React.MouseEvent<HTMLButtonElement>,
|
|
104
|
+
): void => {
|
|
105
|
+
event.stopPropagation();
|
|
106
|
+
setupStore.removeRecentWorkspace({
|
|
107
|
+
projectId: entry.projectId,
|
|
108
|
+
workspaceId: entry.workspaceId,
|
|
109
|
+
workspaceType: entry.workspaceType,
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
return (
|
|
113
|
+
<button
|
|
114
|
+
key={key}
|
|
115
|
+
type="button"
|
|
116
|
+
className="workspace-setup__recents__tile"
|
|
117
|
+
title={`Open ${projectName} / ${entry.workspaceId}`}
|
|
118
|
+
onClick={() =>
|
|
119
|
+
openWorkspace(
|
|
120
|
+
entry.projectId,
|
|
121
|
+
entry.workspaceId,
|
|
122
|
+
entry.workspaceType,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
tabIndex={-1}
|
|
129
|
+
className="workspace-setup__recents__tile__remove"
|
|
130
|
+
title="Remove from recents"
|
|
131
|
+
onClick={handleRemove}
|
|
132
|
+
>
|
|
133
|
+
<TimesIcon />
|
|
134
|
+
</button>
|
|
135
|
+
<div className="workspace-setup__recents__tile__project">
|
|
136
|
+
<FolderIcon />
|
|
137
|
+
<span className="workspace-setup__recents__tile__project__name">
|
|
138
|
+
{projectName}
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="workspace-setup__recents__tile__workspace">
|
|
142
|
+
{entry.workspaceType === WorkspaceType.GROUP ? (
|
|
143
|
+
<UsersIcon />
|
|
144
|
+
) : (
|
|
145
|
+
<UserIcon />
|
|
146
|
+
)}
|
|
147
|
+
<span className="workspace-setup__recents__tile__workspace__name">
|
|
148
|
+
{entry.workspaceId}
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="workspace-setup__recents__tile__time">
|
|
152
|
+
{formatRelativeTime(entry.lastOpenedAt)}
|
|
153
|
+
</div>
|
|
154
|
+
</button>
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
);
|
|
@@ -52,6 +52,7 @@ import { CreateProjectModal } from './CreateProjectModal.js';
|
|
|
52
52
|
import { ActivityBarMenu } from '../editor/ActivityBar.js';
|
|
53
53
|
import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../__lib__/LegendStudioApplicationNavigationContext.js';
|
|
54
54
|
import { CreateWorkspaceModal } from './CreateWorkspaceModal.js';
|
|
55
|
+
import { RecentWorkspacesPanel } from './RecentWorkspacesPanel.js';
|
|
55
56
|
import {
|
|
56
57
|
useLegendStudioApplicationStore,
|
|
57
58
|
useLegendStudioBaseStore,
|
|
@@ -66,7 +67,12 @@ import {
|
|
|
66
67
|
buildWorkspaceOption,
|
|
67
68
|
formatWorkspaceOptionLabel,
|
|
68
69
|
} from './WorkspaceSelectorUtils.js';
|
|
69
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
debounce,
|
|
72
|
+
guaranteeNonNullable,
|
|
73
|
+
type PlainObject,
|
|
74
|
+
} from '@finos/legend-shared';
|
|
75
|
+
import { Project } from '@finos/legend-server-sdlc';
|
|
70
76
|
import { WorkspaceSetupStore } from '../../stores/workspace-setup/WorkspaceSetupStore.js';
|
|
71
77
|
import { openShowcaseManager } from '../../stores/ShowcaseManagerState.js';
|
|
72
78
|
|
|
@@ -360,18 +366,57 @@ export const WorkspaceSetup = withWorkspaceSetupStore(
|
|
|
360
366
|
applicationStore.assistantService.toggleAssistant();
|
|
361
367
|
|
|
362
368
|
// projects
|
|
363
|
-
|
|
369
|
+
// Build a unified option list: recent projects (that aren't already in the
|
|
370
|
+
// loaded set) are prepended so users can instantly re-open common work
|
|
371
|
+
// without waiting for the SDLC search to round-trip.
|
|
372
|
+
const loadedProjectOptions = setupStore.projects
|
|
364
373
|
.map(buildProjectOption)
|
|
365
374
|
.sort(compareLabelFn);
|
|
375
|
+
const loadedProjectIds = new Set(
|
|
376
|
+
setupStore.projects.map((p) => p.projectId),
|
|
377
|
+
);
|
|
378
|
+
const recentProjectOptions: ProjectOption[] = setupStore.recentProjects
|
|
379
|
+
.filter((r) => !loadedProjectIds.has(r.projectId))
|
|
380
|
+
.map((r) => {
|
|
381
|
+
// Construct a lightweight Project stand-in so the existing selector
|
|
382
|
+
// contract is preserved. The full project will be fetched on click
|
|
383
|
+
// via `selectRecentProject`.
|
|
384
|
+
const stub = Project.serialization.fromJson({
|
|
385
|
+
projectId: r.projectId,
|
|
386
|
+
name: r.name,
|
|
387
|
+
description: '',
|
|
388
|
+
webUrl: '',
|
|
389
|
+
tags: [],
|
|
390
|
+
} as PlainObject<Project>);
|
|
391
|
+
return { label: stub.name, value: stub };
|
|
392
|
+
});
|
|
393
|
+
const projectOptions: ProjectOption[] = [
|
|
394
|
+
...recentProjectOptions,
|
|
395
|
+
...loadedProjectOptions,
|
|
396
|
+
];
|
|
397
|
+
const recentProjectIdSet = new Set(
|
|
398
|
+
setupStore.recentProjects.map((p) => p.projectId),
|
|
399
|
+
);
|
|
366
400
|
const selectedProjectOption = setupStore.currentProject
|
|
367
401
|
? buildProjectOption(setupStore.currentProject)
|
|
368
402
|
: null;
|
|
369
403
|
|
|
370
404
|
const onProjectChange = (val: ProjectOption | null): void => {
|
|
371
405
|
if (val) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
406
|
+
// If the selection corresponds to a recent that hasn't been loaded
|
|
407
|
+
// from search yet, fetch the project before switching.
|
|
408
|
+
const isUnloadedRecent =
|
|
409
|
+
!loadedProjectIds.has(val.value.projectId) &&
|
|
410
|
+
recentProjectIdSet.has(val.value.projectId);
|
|
411
|
+
if (isUnloadedRecent) {
|
|
412
|
+
flowResult(setupStore.selectRecentProject(val.value.projectId)).catch(
|
|
413
|
+
applicationStore.alertUnhandledError,
|
|
414
|
+
);
|
|
415
|
+
} else {
|
|
416
|
+
flowResult(setupStore.changeProject(val.value)).catch(
|
|
417
|
+
applicationStore.alertUnhandledError,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
375
420
|
} else {
|
|
376
421
|
setupStore.resetProject();
|
|
377
422
|
}
|
|
@@ -404,9 +449,40 @@ export const WorkspaceSetup = withWorkspaceSetupStore(
|
|
|
404
449
|
};
|
|
405
450
|
|
|
406
451
|
// workspaces
|
|
407
|
-
|
|
408
|
-
|
|
452
|
+
// Recent (non-patch) workspaces for the currently selected project are
|
|
453
|
+
// floated to the top of the dropdown so users can re-enter their
|
|
454
|
+
// typical work without scanning the full list.
|
|
455
|
+
const projectIdForRecents = setupStore.currentProject?.projectId;
|
|
456
|
+
const recentWorkspaceKeys = new Set(
|
|
457
|
+
projectIdForRecents
|
|
458
|
+
? setupStore.recentWorkspaces
|
|
459
|
+
.filter((w) => w.projectId === projectIdForRecents)
|
|
460
|
+
.map((w) => `${w.workspaceType}::${w.workspaceId}`)
|
|
461
|
+
: [],
|
|
462
|
+
);
|
|
463
|
+
const allWorkspaceOptions = setupStore.workspaces.map(buildWorkspaceOption);
|
|
464
|
+
const recentWorkspaceOptions = allWorkspaceOptions
|
|
465
|
+
.filter(
|
|
466
|
+
(o) =>
|
|
467
|
+
o.value.source === undefined &&
|
|
468
|
+
recentWorkspaceKeys.has(
|
|
469
|
+
`${o.value.workspaceType}::${o.value.workspaceId}`,
|
|
470
|
+
),
|
|
471
|
+
)
|
|
472
|
+
.sort(compareLabelFn);
|
|
473
|
+
const otherWorkspaceOptions = allWorkspaceOptions
|
|
474
|
+
.filter(
|
|
475
|
+
(o) =>
|
|
476
|
+
o.value.source !== undefined ||
|
|
477
|
+
!recentWorkspaceKeys.has(
|
|
478
|
+
`${o.value.workspaceType}::${o.value.workspaceId}`,
|
|
479
|
+
),
|
|
480
|
+
)
|
|
409
481
|
.sort(compareLabelFn);
|
|
482
|
+
const workspaceOptions = [
|
|
483
|
+
...recentWorkspaceOptions,
|
|
484
|
+
...otherWorkspaceOptions,
|
|
485
|
+
];
|
|
410
486
|
const selectedWorkspaceOption = setupStore.currentWorkspace
|
|
411
487
|
? buildWorkspaceOption(setupStore.currentWorkspace)
|
|
412
488
|
: null;
|
|
@@ -489,11 +565,24 @@ export const WorkspaceSetup = withWorkspaceSetupStore(
|
|
|
489
565
|
Welcome to Legend Studio
|
|
490
566
|
</div>
|
|
491
567
|
</div>
|
|
568
|
+
<RecentWorkspacesPanel setupStore={setupStore} />
|
|
492
569
|
<div className="workspace-setup__selectors">
|
|
493
570
|
<div className="workspace-setup__selectors__container">
|
|
494
571
|
<div className="workspace-setup__selector">
|
|
495
572
|
<div className="workspace-setup__selector__header">
|
|
496
|
-
Search for an existing project
|
|
573
|
+
<span>Search for an existing project</span>
|
|
574
|
+
{(setupStore.recentProjects.length > 0 ||
|
|
575
|
+
setupStore.recentWorkspaces.length > 0) && (
|
|
576
|
+
<button
|
|
577
|
+
type="button"
|
|
578
|
+
className="workspace-setup__selector__header__clear-recents"
|
|
579
|
+
tabIndex={-1}
|
|
580
|
+
onClick={() => setupStore.clearRecents()}
|
|
581
|
+
title="Clear recently-opened projects and workspaces"
|
|
582
|
+
>
|
|
583
|
+
Clear recents
|
|
584
|
+
</button>
|
|
585
|
+
)}
|
|
497
586
|
</div>
|
|
498
587
|
<div className="workspace-setup__selector__content">
|
|
499
588
|
<div
|
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
DEFAULT_TAB_SIZE,
|
|
94
94
|
} from '@finos/legend-application';
|
|
95
95
|
import { LEGEND_STUDIO_APP_EVENT } from '../../__lib__/LegendStudioEvent.js';
|
|
96
|
+
import { LegendStudioUserDataHelper } from '../../__lib__/LegendStudioUserDataHelper.js';
|
|
96
97
|
import type { EditorMode } from './EditorMode.js';
|
|
97
98
|
import { StandardEditorMode } from './StandardEditorMode.js';
|
|
98
99
|
import { WorkspaceUpdateConflictResolutionState } from './sidebar-state/WorkspaceUpdateConflictResolutionState.js';
|
|
@@ -691,6 +692,12 @@ export class EditorStore implements CommandRegistrar {
|
|
|
691
692
|
}),
|
|
692
693
|
);
|
|
693
694
|
if (!this.sdlcState.currentProject) {
|
|
695
|
+
// The project the user navigated to doesn't exist (or isn't accessible).
|
|
696
|
+
// Drop it from the recents cache so we don't keep offering a dead link.
|
|
697
|
+
LegendStudioUserDataHelper.workspaceSetup_removeRecentProject(
|
|
698
|
+
this.applicationStore.userDataService,
|
|
699
|
+
projectId,
|
|
700
|
+
);
|
|
694
701
|
// If the project is not found or the user does not have access to it,
|
|
695
702
|
// we will not automatically redirect them to the setup page as they will lose the URL
|
|
696
703
|
// instead, we give them the option to:
|
|
@@ -740,6 +747,15 @@ export class EditorStore implements CommandRegistrar {
|
|
|
740
747
|
),
|
|
741
748
|
);
|
|
742
749
|
if (!this.sdlcState.currentWorkspace) {
|
|
750
|
+
// The workspace the user navigated to doesn't exist anymore. Drop
|
|
751
|
+
// the matching entry from the recents cache (no-op for patch
|
|
752
|
+
// workspaces, which are never cached in the first place).
|
|
753
|
+
if (patchReleaseVersionId === undefined) {
|
|
754
|
+
LegendStudioUserDataHelper.workspaceSetup_removeRecentWorkspace(
|
|
755
|
+
this.applicationStore.userDataService,
|
|
756
|
+
{ projectId, workspaceId, workspaceType },
|
|
757
|
+
);
|
|
758
|
+
}
|
|
743
759
|
// If the workspace is not found,
|
|
744
760
|
// we will not automatically redirect the user to the setup page as they will lose the URL
|
|
745
761
|
// instead, we give them the option to:
|
|
@@ -810,6 +826,34 @@ export class EditorStore implements CommandRegistrar {
|
|
|
810
826
|
onLeave(false);
|
|
811
827
|
return;
|
|
812
828
|
}
|
|
829
|
+
// At this point both the project and the workspace have been confirmed
|
|
830
|
+
// to exist on the server (the guards above bail out otherwise), so this
|
|
831
|
+
// is the authoritative "the user actually opened this workspace" moment.
|
|
832
|
+
// Record it in the recents cache so the workspace setup screen can offer
|
|
833
|
+
// it instantly next time.
|
|
834
|
+
// NOTE: patch-based workspaces are intentionally excluded from recents
|
|
835
|
+
// (they are not surfaced in the recents UI). Sandbox projects ARE
|
|
836
|
+
// included — opening one is still a meaningful "I worked here" signal,
|
|
837
|
+
// and surfacing it alongside other recents gives a faster one-click
|
|
838
|
+
// re-entry than waiting for the dedicated sandbox loader.
|
|
839
|
+
if (this.sdlcState.currentWorkspace.source === undefined) {
|
|
840
|
+
LegendStudioUserDataHelper.workspaceSetup_recordRecentProject(
|
|
841
|
+
this.applicationStore.userDataService,
|
|
842
|
+
{
|
|
843
|
+
projectId: this.sdlcState.currentProject.projectId,
|
|
844
|
+
name: this.sdlcState.currentProject.name,
|
|
845
|
+
},
|
|
846
|
+
);
|
|
847
|
+
LegendStudioUserDataHelper.workspaceSetup_recordRecentWorkspace(
|
|
848
|
+
this.applicationStore.userDataService,
|
|
849
|
+
{
|
|
850
|
+
projectId: this.sdlcState.currentProject.projectId,
|
|
851
|
+
workspaceId: this.sdlcState.currentWorkspace.workspaceId,
|
|
852
|
+
workspaceType: this.sdlcState.currentWorkspace.workspaceType,
|
|
853
|
+
},
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
813
857
|
yield Promise.all([
|
|
814
858
|
this.sdlcState.fetchCurrentRevision(
|
|
815
859
|
projectId,
|