@finos/legend-application-studio 28.21.5 → 28.21.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +63 -1
- package/lib/__lib__/LegendStudioUserDataHelper.d.ts.map +1 -1
- package/lib/__lib__/LegendStudioUserDataHelper.js +185 -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 +86 -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 +60 -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 +34 -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 +23 -2
- package/lib/stores/workspace-setup/WorkspaceSetupStore.d.ts.map +1 -1
- package/lib/stores/workspace-setup/WorkspaceSetupStore.js +121 -8
- 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 +309 -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 +181 -0
- package/src/components/workspace-setup/WorkspaceSetup.tsx +96 -8
- package/src/stores/editor/EditorStore.ts +47 -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 +172 -9
- package/tsconfig.json +1 -0
|
@@ -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,56 @@ 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
|
+
// Rebuild a real Project from the cached metadata; no synthetic
|
|
382
|
+
// fields needed since we persist everything the schema requires.
|
|
383
|
+
const stub = Project.serialization.fromJson({
|
|
384
|
+
projectId: r.projectId,
|
|
385
|
+
name: r.name,
|
|
386
|
+
description: r.description,
|
|
387
|
+
webUrl: r.webUrl,
|
|
388
|
+
tags: r.tags,
|
|
389
|
+
} as PlainObject<Project>);
|
|
390
|
+
return { label: stub.name, value: stub };
|
|
391
|
+
});
|
|
392
|
+
const projectOptions: ProjectOption[] = [
|
|
393
|
+
...recentProjectOptions,
|
|
394
|
+
...loadedProjectOptions,
|
|
395
|
+
];
|
|
396
|
+
const recentProjectIdSet = new Set(
|
|
397
|
+
setupStore.recentProjects.map((p) => p.projectId),
|
|
398
|
+
);
|
|
366
399
|
const selectedProjectOption = setupStore.currentProject
|
|
367
400
|
? buildProjectOption(setupStore.currentProject)
|
|
368
401
|
: null;
|
|
369
402
|
|
|
370
403
|
const onProjectChange = (val: ProjectOption | null): void => {
|
|
371
404
|
if (val) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
405
|
+
// If the selection corresponds to a recent that hasn't been loaded
|
|
406
|
+
// from search yet, fetch the project before switching.
|
|
407
|
+
const isUnloadedRecent =
|
|
408
|
+
!loadedProjectIds.has(val.value.projectId) &&
|
|
409
|
+
recentProjectIdSet.has(val.value.projectId);
|
|
410
|
+
if (isUnloadedRecent) {
|
|
411
|
+
flowResult(setupStore.selectRecentProject(val.value.projectId)).catch(
|
|
412
|
+
applicationStore.alertUnhandledError,
|
|
413
|
+
);
|
|
414
|
+
} else {
|
|
415
|
+
flowResult(setupStore.changeProject(val.value)).catch(
|
|
416
|
+
applicationStore.alertUnhandledError,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
375
419
|
} else {
|
|
376
420
|
setupStore.resetProject();
|
|
377
421
|
}
|
|
@@ -404,9 +448,40 @@ export const WorkspaceSetup = withWorkspaceSetupStore(
|
|
|
404
448
|
};
|
|
405
449
|
|
|
406
450
|
// workspaces
|
|
407
|
-
|
|
408
|
-
|
|
451
|
+
// Recent (non-patch) workspaces for the currently selected project are
|
|
452
|
+
// floated to the top of the dropdown so users can re-enter their
|
|
453
|
+
// typical work without scanning the full list.
|
|
454
|
+
const projectIdForRecents = setupStore.currentProject?.projectId;
|
|
455
|
+
const recentWorkspaceKeys = new Set(
|
|
456
|
+
projectIdForRecents
|
|
457
|
+
? setupStore.recentWorkspaces
|
|
458
|
+
.filter((w) => w.projectId === projectIdForRecents)
|
|
459
|
+
.map((w) => `${w.workspaceType}::${w.workspaceId}`)
|
|
460
|
+
: [],
|
|
461
|
+
);
|
|
462
|
+
const allWorkspaceOptions = setupStore.workspaces.map(buildWorkspaceOption);
|
|
463
|
+
const recentWorkspaceOptions = allWorkspaceOptions
|
|
464
|
+
.filter(
|
|
465
|
+
(o) =>
|
|
466
|
+
o.value.source === undefined &&
|
|
467
|
+
recentWorkspaceKeys.has(
|
|
468
|
+
`${o.value.workspaceType}::${o.value.workspaceId}`,
|
|
469
|
+
),
|
|
470
|
+
)
|
|
471
|
+
.sort(compareLabelFn);
|
|
472
|
+
const otherWorkspaceOptions = allWorkspaceOptions
|
|
473
|
+
.filter(
|
|
474
|
+
(o) =>
|
|
475
|
+
o.value.source !== undefined ||
|
|
476
|
+
!recentWorkspaceKeys.has(
|
|
477
|
+
`${o.value.workspaceType}::${o.value.workspaceId}`,
|
|
478
|
+
),
|
|
479
|
+
)
|
|
409
480
|
.sort(compareLabelFn);
|
|
481
|
+
const workspaceOptions = [
|
|
482
|
+
...recentWorkspaceOptions,
|
|
483
|
+
...otherWorkspaceOptions,
|
|
484
|
+
];
|
|
410
485
|
const selectedWorkspaceOption = setupStore.currentWorkspace
|
|
411
486
|
? buildWorkspaceOption(setupStore.currentWorkspace)
|
|
412
487
|
: null;
|
|
@@ -489,11 +564,24 @@ export const WorkspaceSetup = withWorkspaceSetupStore(
|
|
|
489
564
|
Welcome to Legend Studio
|
|
490
565
|
</div>
|
|
491
566
|
</div>
|
|
567
|
+
<RecentWorkspacesPanel setupStore={setupStore} />
|
|
492
568
|
<div className="workspace-setup__selectors">
|
|
493
569
|
<div className="workspace-setup__selectors__container">
|
|
494
570
|
<div className="workspace-setup__selector">
|
|
495
571
|
<div className="workspace-setup__selector__header">
|
|
496
|
-
Search for an existing project
|
|
572
|
+
<span>Search for an existing project</span>
|
|
573
|
+
{(setupStore.recentProjects.length > 0 ||
|
|
574
|
+
setupStore.recentWorkspaces.length > 0) && (
|
|
575
|
+
<button
|
|
576
|
+
type="button"
|
|
577
|
+
className="workspace-setup__selector__header__clear-recents"
|
|
578
|
+
tabIndex={-1}
|
|
579
|
+
onClick={() => setupStore.clearRecents()}
|
|
580
|
+
title="Clear recently-opened projects and workspaces"
|
|
581
|
+
>
|
|
582
|
+
Clear recents
|
|
583
|
+
</button>
|
|
584
|
+
)}
|
|
497
585
|
</div>
|
|
498
586
|
<div className="workspace-setup__selector__content">
|
|
499
587
|
<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,37 @@ 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
|
+
description: this.sdlcState.currentProject.description,
|
|
846
|
+
webUrl: this.sdlcState.currentProject.webUrl,
|
|
847
|
+
tags: this.sdlcState.currentProject.tags,
|
|
848
|
+
},
|
|
849
|
+
);
|
|
850
|
+
LegendStudioUserDataHelper.workspaceSetup_recordRecentWorkspace(
|
|
851
|
+
this.applicationStore.userDataService,
|
|
852
|
+
{
|
|
853
|
+
projectId: this.sdlcState.currentProject.projectId,
|
|
854
|
+
workspaceId: this.sdlcState.currentWorkspace.workspaceId,
|
|
855
|
+
workspaceType: this.sdlcState.currentWorkspace.workspaceType,
|
|
856
|
+
},
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
813
860
|
yield Promise.all([
|
|
814
861
|
this.sdlcState.fetchCurrentRevision(
|
|
815
862
|
projectId,
|
|
@@ -63,7 +63,9 @@ import {
|
|
|
63
63
|
EqualTo,
|
|
64
64
|
EqualToRelation,
|
|
65
65
|
RelationElement,
|
|
66
|
+
RelationRowTestData,
|
|
66
67
|
observe_RelationElement,
|
|
68
|
+
observe_RelationRowTestData,
|
|
67
69
|
ModelStore,
|
|
68
70
|
RelationElementsData,
|
|
69
71
|
CORE_PURE_PATH,
|
|
@@ -723,7 +725,6 @@ export const createFunctionTest = async (
|
|
|
723
725
|
assertion.id = DEFAULT_TEST_ASSERTION_ID;
|
|
724
726
|
const expectedRelElement = new RelationElement();
|
|
725
727
|
expectedRelElement.paths = [];
|
|
726
|
-
expectedRelElement.rows = [];
|
|
727
728
|
let inferredColumns: string[] = [];
|
|
728
729
|
if (type.path === CORE_PURE_PATH.RELATION) {
|
|
729
730
|
try {
|
|
@@ -746,6 +747,9 @@ export const createFunctionTest = async (
|
|
|
746
747
|
}
|
|
747
748
|
}
|
|
748
749
|
expectedRelElement.columns = inferredColumns;
|
|
750
|
+
const emptyRow = observe_RelationRowTestData(new RelationRowTestData());
|
|
751
|
+
emptyRow.values = inferredColumns.map(() => '');
|
|
752
|
+
expectedRelElement.rows = [emptyRow];
|
|
749
753
|
observe_RelationElement(expectedRelElement);
|
|
750
754
|
assertion.expected = expectedRelElement;
|
|
751
755
|
_assertion = assertion;
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type WorkspaceType,
|
|
43
43
|
} from '@finos/legend-server-sdlc';
|
|
44
44
|
import { LEGEND_STUDIO_APP_EVENT } from '../../../__lib__/LegendStudioEvent.js';
|
|
45
|
+
import { LegendStudioUserDataHelper } from '../../../__lib__/LegendStudioUserDataHelper.js';
|
|
45
46
|
|
|
46
47
|
export enum PROJECT_OVERVIEW_ACTIVITY_MODE {
|
|
47
48
|
RELEASE = 'RELEASE',
|
|
@@ -155,6 +156,19 @@ export class ProjectOverviewState {
|
|
|
155
156
|
this.projectWorkspaces = this.projectWorkspaces.filter(
|
|
156
157
|
(w) => !areWorkspacesEquivalent(workspace, w),
|
|
157
158
|
);
|
|
159
|
+
// Drop the deleted workspace from the recents cache so the workspace
|
|
160
|
+
// setup screen doesn't keep offering a dead link. Patch workspaces are
|
|
161
|
+
// never cached, so this is a no-op for them.
|
|
162
|
+
if (workspace.source === undefined) {
|
|
163
|
+
LegendStudioUserDataHelper.workspaceSetup_removeRecentWorkspace(
|
|
164
|
+
this.editorStore.applicationStore.userDataService,
|
|
165
|
+
{
|
|
166
|
+
projectId: this.sdlcState.activeProject.projectId,
|
|
167
|
+
workspaceId: workspace.workspaceId,
|
|
168
|
+
workspaceType: workspace.workspaceType,
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
}
|
|
158
172
|
// redirect to home page if current workspace is deleted
|
|
159
173
|
if (
|
|
160
174
|
areWorkspacesEquivalent(
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import type { EditorStore } from '../EditorStore.js';
|
|
26
26
|
import type { EditorSDLCState } from '../EditorSDLCState.js';
|
|
27
27
|
import { LEGEND_STUDIO_APP_EVENT } from '../../../__lib__/LegendStudioEvent.js';
|
|
28
|
+
import { LegendStudioUserDataHelper } from '../../../__lib__/LegendStudioUserDataHelper.js';
|
|
28
29
|
import {
|
|
29
30
|
type GeneratorFn,
|
|
30
31
|
type PlainObject,
|
|
@@ -402,6 +403,19 @@ export class WorkspaceReviewState {
|
|
|
402
403
|
review.id,
|
|
403
404
|
{ message: `${review.title} [review]` },
|
|
404
405
|
);
|
|
406
|
+
// Committing a review deletes the workspace on SDLC. Drop it from
|
|
407
|
+
// the recents cache so the workspace setup screen doesn't keep
|
|
408
|
+
// offering a dead link. Patch workspaces are never cached.
|
|
409
|
+
if (this.sdlcState.activePatch === undefined) {
|
|
410
|
+
LegendStudioUserDataHelper.workspaceSetup_removeRecentWorkspace(
|
|
411
|
+
this.editorStore.applicationStore.userDataService,
|
|
412
|
+
{
|
|
413
|
+
projectId: this.sdlcState.activeProject.projectId,
|
|
414
|
+
workspaceId: this.sdlcState.activeWorkspace.workspaceId,
|
|
415
|
+
workspaceType: this.sdlcState.activeWorkspace.workspaceType,
|
|
416
|
+
},
|
|
417
|
+
);
|
|
418
|
+
}
|
|
405
419
|
this.editorStore.applicationStore.alertService.setActionAlertInfo({
|
|
406
420
|
message: 'Committed review successfully',
|
|
407
421
|
prompt:
|
|
@@ -156,6 +156,13 @@ export class DevMetadataState {
|
|
|
156
156
|
'Project Name required to push to dev mode',
|
|
157
157
|
);
|
|
158
158
|
this.pushState.inProgress();
|
|
159
|
+
LegendStudioTelemetryHelper.logEvent_DevMetadataPushLaunched(
|
|
160
|
+
this.editorStore.applicationStore.telemetryService,
|
|
161
|
+
this.editorStore.editorMode.getSourceInfo(),
|
|
162
|
+
currentProjectConfiguration.groupId,
|
|
163
|
+
currentProjectConfiguration.artifactId,
|
|
164
|
+
undefined,
|
|
165
|
+
);
|
|
159
166
|
const result =
|
|
160
167
|
(yield this.editorStore.graphManagerState.graphManager.pushToDevMetadata(
|
|
161
168
|
currentProjectConfiguration.groupId,
|
|
@@ -165,7 +172,7 @@ export class DevMetadataState {
|
|
|
165
172
|
this.editorStore.graphManagerState.graph,
|
|
166
173
|
)) as DeployProjectResponse;
|
|
167
174
|
this.result = result;
|
|
168
|
-
LegendStudioTelemetryHelper.
|
|
175
|
+
LegendStudioTelemetryHelper.logEvent_DevMetadataPushSucceeded(
|
|
169
176
|
this.editorStore.applicationStore.telemetryService,
|
|
170
177
|
this.editorStore.editorMode.getSourceInfo(),
|
|
171
178
|
currentProjectConfiguration.groupId,
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
ReviewApproval,
|
|
46
46
|
} from '@finos/legend-server-sdlc';
|
|
47
47
|
import { LEGEND_STUDIO_APP_EVENT } from '../../__lib__/LegendStudioEvent.js';
|
|
48
|
+
import { LegendStudioUserDataHelper } from '../../__lib__/LegendStudioUserDataHelper.js';
|
|
48
49
|
import { DEFAULT_TAB_SIZE } from '@finos/legend-application';
|
|
49
50
|
import type { Entity } from '@finos/legend-storage';
|
|
50
51
|
import { EntityDiffViewState } from '../editor/editor-state/entity-diff-editor-state/EntityDiffViewState.js';
|
|
@@ -457,6 +458,20 @@ export class ProjectReviewerStore {
|
|
|
457
458
|
{ message: `${this.review.title} [review]` },
|
|
458
459
|
)) as PlainObject<Review>,
|
|
459
460
|
);
|
|
461
|
+
// Committing a review deletes its workspace on SDLC. Drop the
|
|
462
|
+
// matching entry from the local recents cache (no-op if it wasn't
|
|
463
|
+
// there, e.g., the reviewer isn't the workspace author). Patch
|
|
464
|
+
// workspaces are never cached.
|
|
465
|
+
if (this.patchReleaseVersionId === undefined) {
|
|
466
|
+
LegendStudioUserDataHelper.workspaceSetup_removeRecentWorkspace(
|
|
467
|
+
this.editorStore.applicationStore.userDataService,
|
|
468
|
+
{
|
|
469
|
+
projectId: this.projectId,
|
|
470
|
+
workspaceId: this.review.workspaceId,
|
|
471
|
+
workspaceType: this.review.workspaceType,
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
}
|
|
460
475
|
} catch (error) {
|
|
461
476
|
assertErrorThrown(error);
|
|
462
477
|
this.editorStore.applicationStore.logService.error(
|
|
@@ -51,6 +51,11 @@ import {
|
|
|
51
51
|
ProjectConfigurationStatus,
|
|
52
52
|
} from './ProjectConfigurationStatus.js';
|
|
53
53
|
import { GraphManagerState } from '@finos/legend-graph';
|
|
54
|
+
import {
|
|
55
|
+
LegendStudioUserDataHelper,
|
|
56
|
+
type RecentProjectEntry,
|
|
57
|
+
type RecentWorkspaceEntry,
|
|
58
|
+
} from '../../__lib__/LegendStudioUserDataHelper.js';
|
|
54
59
|
|
|
55
60
|
interface ImportProjectSuccessReport {
|
|
56
61
|
projectId: string;
|
|
@@ -88,10 +93,15 @@ export class WorkspaceSetupStore {
|
|
|
88
93
|
loadWorkspacesState = ActionState.create();
|
|
89
94
|
createWorkspaceState = ActionState.create();
|
|
90
95
|
showCreateWorkspaceModal = false;
|
|
91
|
-
showAdvancedWorkspaceFilterOptions = false;
|
|
92
96
|
|
|
93
97
|
graphManagerState: GraphManagerState;
|
|
94
98
|
|
|
99
|
+
// Cached recents to make re-opening a project/workspace instantaneous.
|
|
100
|
+
// NOTE: patch-based workspaces are intentionally excluded from this cache.
|
|
101
|
+
recentProjects: RecentProjectEntry[] = [];
|
|
102
|
+
recentWorkspaces: RecentWorkspaceEntry[] = [];
|
|
103
|
+
selectRecentProjectState = ActionState.create();
|
|
104
|
+
|
|
95
105
|
constructor(
|
|
96
106
|
applicationStore: LegendStudioApplicationStore,
|
|
97
107
|
sdlcServerClient: SDLCServerClient,
|
|
@@ -104,7 +114,6 @@ export class WorkspaceSetupStore {
|
|
|
104
114
|
showCreateProjectModal: observable,
|
|
105
115
|
workspaces: observable,
|
|
106
116
|
currentWorkspace: observable,
|
|
107
|
-
showAdvancedWorkspaceFilterOptions: observable,
|
|
108
117
|
loadSandboxState: observable,
|
|
109
118
|
showCreateWorkspaceModal: observable,
|
|
110
119
|
sandboxProject: observable,
|
|
@@ -113,14 +122,18 @@ export class WorkspaceSetupStore {
|
|
|
113
122
|
enginePromise: observable,
|
|
114
123
|
sandboxModal: observable,
|
|
115
124
|
hasSandboxAccess: observable,
|
|
125
|
+
recentProjects: observable,
|
|
126
|
+
recentWorkspaces: observable,
|
|
116
127
|
setShowCreateProjectModal: action,
|
|
117
128
|
setShowCreateWorkspaceModal: action,
|
|
118
|
-
setShowAdvancedWorkspaceFilterOptions: action,
|
|
119
129
|
setImportProjectSuccessReport: action,
|
|
120
130
|
setSandboxModal: action,
|
|
121
131
|
changeWorkspace: action,
|
|
122
132
|
resetProject: action,
|
|
123
133
|
resetWorkspace: action,
|
|
134
|
+
removeRecentProject: action,
|
|
135
|
+
removeRecentWorkspace: action,
|
|
136
|
+
clearRecents: action,
|
|
124
137
|
initialize: flow,
|
|
125
138
|
loadProjects: flow,
|
|
126
139
|
loadSandboxProject: flow,
|
|
@@ -130,6 +143,7 @@ export class WorkspaceSetupStore {
|
|
|
130
143
|
createSandboxProject: flow,
|
|
131
144
|
createWorkspace: flow,
|
|
132
145
|
initializeEngine: flow,
|
|
146
|
+
selectRecentProject: flow,
|
|
133
147
|
});
|
|
134
148
|
|
|
135
149
|
this.applicationStore = applicationStore;
|
|
@@ -138,6 +152,14 @@ export class WorkspaceSetupStore {
|
|
|
138
152
|
applicationStore.pluginManager,
|
|
139
153
|
applicationStore.logService,
|
|
140
154
|
);
|
|
155
|
+
this.recentProjects =
|
|
156
|
+
LegendStudioUserDataHelper.workspaceSetup_getRecentProjects(
|
|
157
|
+
applicationStore.userDataService,
|
|
158
|
+
);
|
|
159
|
+
this.recentWorkspaces =
|
|
160
|
+
LegendStudioUserDataHelper.workspaceSetup_getRecentWorkspaces(
|
|
161
|
+
applicationStore.userDataService,
|
|
162
|
+
);
|
|
141
163
|
if (this.supportsCreatingSandboxProject) {
|
|
142
164
|
flowResult(this.initializeEngine()).catch(
|
|
143
165
|
applicationStore.alertUnhandledError,
|
|
@@ -158,10 +180,6 @@ export class WorkspaceSetupStore {
|
|
|
158
180
|
this.showCreateWorkspaceModal = val;
|
|
159
181
|
}
|
|
160
182
|
|
|
161
|
-
setShowAdvancedWorkspaceFilterOptions(val: boolean): void {
|
|
162
|
-
this.showAdvancedWorkspaceFilterOptions = val;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
183
|
setImportProjectSuccessReport(
|
|
166
184
|
importProjectSuccessReport: ImportProjectSuccessReport | undefined,
|
|
167
185
|
): void {
|
|
@@ -197,6 +215,73 @@ export class WorkspaceSetupStore {
|
|
|
197
215
|
this.sandboxModal = val;
|
|
198
216
|
}
|
|
199
217
|
|
|
218
|
+
// --- Recents -------------------------------------------------------------
|
|
219
|
+
// NOTE: writes to the recents cache happen from the editor (see
|
|
220
|
+
// `EditorStore.initialize`) at the moment a workspace is actually opened.
|
|
221
|
+
// This store only reads the cache (to seed dropdowns) and prunes entries
|
|
222
|
+
// that are discovered to be stale.
|
|
223
|
+
|
|
224
|
+
removeRecentProject(projectId: string): void {
|
|
225
|
+
const updated =
|
|
226
|
+
LegendStudioUserDataHelper.workspaceSetup_removeRecentProject(
|
|
227
|
+
this.applicationStore.userDataService,
|
|
228
|
+
projectId,
|
|
229
|
+
);
|
|
230
|
+
this.recentProjects = updated.projects;
|
|
231
|
+
this.recentWorkspaces = updated.workspaces;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
removeRecentWorkspace(entry: {
|
|
235
|
+
projectId: string;
|
|
236
|
+
workspaceId: string;
|
|
237
|
+
workspaceType: WorkspaceType;
|
|
238
|
+
}): void {
|
|
239
|
+
this.recentWorkspaces =
|
|
240
|
+
LegendStudioUserDataHelper.workspaceSetup_removeRecentWorkspace(
|
|
241
|
+
this.applicationStore.userDataService,
|
|
242
|
+
entry,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
clearRecents(): void {
|
|
247
|
+
LegendStudioUserDataHelper.workspaceSetup_clearRecents(
|
|
248
|
+
this.applicationStore.userDataService,
|
|
249
|
+
);
|
|
250
|
+
this.recentProjects = [];
|
|
251
|
+
this.recentWorkspaces = [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Fetches a project by id (used when the user picks a cached "recent"
|
|
256
|
+
* project that may not be in the current search results) and switches to
|
|
257
|
+
* it. If the project no longer exists, the entry is pruned from recents.
|
|
258
|
+
*
|
|
259
|
+
* NOTE: we deliberately don't short-circuit using the cached recent entry
|
|
260
|
+
* here. Going through `getProject` keeps the prune-on-404 path intact, and
|
|
261
|
+
* the cached metadata is already used elsewhere to make the UI feel fast
|
|
262
|
+
* (dropdown stubs in `WorkspaceSetup.tsx` and tile labels in
|
|
263
|
+
* `RecentWorkspacesPanel.tsx`).
|
|
264
|
+
*/
|
|
265
|
+
*selectRecentProject(projectId: string): GeneratorFn<void> {
|
|
266
|
+
this.selectRecentProjectState.inProgress();
|
|
267
|
+
try {
|
|
268
|
+
const project = Project.serialization.fromJson(
|
|
269
|
+
(yield this.sdlcServerClient.getProject(
|
|
270
|
+
projectId,
|
|
271
|
+
)) as PlainObject<Project>,
|
|
272
|
+
);
|
|
273
|
+
yield flowResult(this.changeProject(project));
|
|
274
|
+
this.selectRecentProjectState.pass();
|
|
275
|
+
} catch (error) {
|
|
276
|
+
assertErrorThrown(error);
|
|
277
|
+
this.removeRecentProject(projectId);
|
|
278
|
+
this.applicationStore.notificationService.notifyWarning(
|
|
279
|
+
`Recent project could not be opened and was removed from recents`,
|
|
280
|
+
);
|
|
281
|
+
this.selectRecentProjectState.fail();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
200
285
|
*createSandboxProject(): GeneratorFn<void> {
|
|
201
286
|
try {
|
|
202
287
|
if (!this.hasSandboxAccess) {
|
|
@@ -218,6 +303,12 @@ export class WorkspaceSetupStore {
|
|
|
218
303
|
message: `Sandbox project ${sandboxProject.projectId} created. Creating default workspace...`,
|
|
219
304
|
showLoading: true,
|
|
220
305
|
});
|
|
306
|
+
// Invalidate the cached sandbox info so loadSandboxProject re-fetches
|
|
307
|
+
// and persists the newly-created project id instead of reusing the
|
|
308
|
+
// stale "no sandbox yet" cache entry.
|
|
309
|
+
LegendStudioUserDataHelper.workspaceSetup_clearSandboxInfo(
|
|
310
|
+
this.applicationStore.userDataService,
|
|
311
|
+
);
|
|
221
312
|
yield flowResult(this.loadSandboxProject());
|
|
222
313
|
const sandbox = guaranteeType(
|
|
223
314
|
this.sandboxProject,
|
|
@@ -277,6 +368,7 @@ export class WorkspaceSetupStore {
|
|
|
277
368
|
)) as PlainObject<Project>,
|
|
278
369
|
);
|
|
279
370
|
} catch {
|
|
371
|
+
this.removeRecentProject(projectId);
|
|
280
372
|
this.applicationStore.navigationService.navigator.updateCurrentLocation(
|
|
281
373
|
generateSetupRoute(undefined, undefined),
|
|
282
374
|
);
|
|
@@ -373,10 +465,58 @@ export class WorkspaceSetupStore {
|
|
|
373
465
|
if (this.enginePromise) {
|
|
374
466
|
yield this.enginePromise;
|
|
375
467
|
}
|
|
468
|
+
|
|
469
|
+
const userId = this.sdlcServerClient.currentUser?.userId;
|
|
470
|
+
|
|
471
|
+
// Fast path — if we have a recent, user-matching cache entry, use it to
|
|
472
|
+
// avoid the `userHasPrototypeProjectAccess` graph manager call and the
|
|
473
|
+
// sandbox-tag project search. We still hit SDLC once to confirm the
|
|
474
|
+
// cached projectId is alive, but `getProject(id)` is cheaper than the
|
|
475
|
+
// tagged search and self-invalidates on 404.
|
|
476
|
+
if (userId) {
|
|
477
|
+
const cached =
|
|
478
|
+
LegendStudioUserDataHelper.workspaceSetup_getCachedSandboxInfo(
|
|
479
|
+
this.applicationStore.userDataService,
|
|
480
|
+
userId,
|
|
481
|
+
);
|
|
482
|
+
if (cached) {
|
|
483
|
+
this.hasSandboxAccess = cached.hasAccess;
|
|
484
|
+
if (!cached.hasAccess) {
|
|
485
|
+
// No access, no project — nothing else to do.
|
|
486
|
+
this.sandboxProject = true;
|
|
487
|
+
this.loadSandboxState.pass();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (cached.projectId) {
|
|
491
|
+
try {
|
|
492
|
+
this.sandboxProject = Project.serialization.fromJson(
|
|
493
|
+
(yield this.sdlcServerClient.getProject(
|
|
494
|
+
cached.projectId,
|
|
495
|
+
)) as PlainObject<Project>,
|
|
496
|
+
);
|
|
497
|
+
this.loadSandboxState.pass();
|
|
498
|
+
return;
|
|
499
|
+
} catch {
|
|
500
|
+
// Cached sandbox project no longer exists on the server; drop
|
|
501
|
+
// the cache and fall through to the full refresh.
|
|
502
|
+
LegendStudioUserDataHelper.workspaceSetup_clearSandboxInfo(
|
|
503
|
+
this.applicationStore.userDataService,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
// User has access but hasn't created a sandbox yet.
|
|
508
|
+
this.sandboxProject = true;
|
|
509
|
+
this.loadSandboxState.pass();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Slow path — original flow.
|
|
376
516
|
const sandboxProject = (
|
|
377
517
|
(yield this.sdlcServerClient.getProjects(
|
|
378
518
|
undefined,
|
|
379
|
-
|
|
519
|
+
userId,
|
|
380
520
|
[SANDBOX_SDLC_TAG],
|
|
381
521
|
1,
|
|
382
522
|
)) as PlainObject<Project>[]
|
|
@@ -384,7 +524,7 @@ export class WorkspaceSetupStore {
|
|
|
384
524
|
if (this.hasSandboxAccess === undefined) {
|
|
385
525
|
this.hasSandboxAccess =
|
|
386
526
|
(yield this.graphManagerState.graphManager.userHasPrototypeProjectAccess(
|
|
387
|
-
|
|
527
|
+
userId ?? '',
|
|
388
528
|
)) as boolean;
|
|
389
529
|
}
|
|
390
530
|
this.sandboxProject = true;
|
|
@@ -395,6 +535,23 @@ export class WorkspaceSetupStore {
|
|
|
395
535
|
} else if (sandboxProject.length === 1) {
|
|
396
536
|
this.sandboxProject = guaranteeNonNullable(sandboxProject[0]);
|
|
397
537
|
}
|
|
538
|
+
|
|
539
|
+
// Persist the fresh result for next time. We only cache when we have
|
|
540
|
+
// a userId (cache is scoped per user); anonymous sessions skip this.
|
|
541
|
+
if (userId) {
|
|
542
|
+
LegendStudioUserDataHelper.workspaceSetup_recordSandboxInfo(
|
|
543
|
+
this.applicationStore.userDataService,
|
|
544
|
+
{
|
|
545
|
+
userId,
|
|
546
|
+
hasAccess: this.hasSandboxAccess,
|
|
547
|
+
projectId:
|
|
548
|
+
this.sandboxProject instanceof Project
|
|
549
|
+
? this.sandboxProject.projectId
|
|
550
|
+
: undefined,
|
|
551
|
+
},
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
398
555
|
this.loadSandboxState.pass();
|
|
399
556
|
} catch (error) {
|
|
400
557
|
this.sandboxProject = true;
|
|
@@ -513,6 +670,12 @@ export class WorkspaceSetupStore {
|
|
|
513
670
|
if (matchingWorkspace) {
|
|
514
671
|
this.changeWorkspace(matchingWorkspace);
|
|
515
672
|
} else {
|
|
673
|
+
// Workspace no longer exists — prune from recents.
|
|
674
|
+
this.removeRecentWorkspace({
|
|
675
|
+
projectId: project.projectId,
|
|
676
|
+
workspaceId: workspaceInfo.workspaceId,
|
|
677
|
+
workspaceType: workspaceInfo.workspaceType,
|
|
678
|
+
});
|
|
516
679
|
this.applicationStore.navigationService.navigator.updateCurrentLocation(
|
|
517
680
|
generateSetupRoute(project.projectId, undefined),
|
|
518
681
|
);
|
package/tsconfig.json
CHANGED
|
@@ -374,6 +374,7 @@
|
|
|
374
374
|
"./src/components/workspace-setup/CreateProjectModal.tsx",
|
|
375
375
|
"./src/components/workspace-setup/CreateWorkspaceModal.tsx",
|
|
376
376
|
"./src/components/workspace-setup/ProjectSelectorUtils.tsx",
|
|
377
|
+
"./src/components/workspace-setup/RecentWorkspacesPanel.tsx",
|
|
377
378
|
"./src/components/workspace-setup/WorkspaceSelectorUtils.tsx",
|
|
378
379
|
"./src/components/workspace-setup/WorkspaceSetup.tsx"
|
|
379
380
|
],
|