mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
feat(editor): Improve performance by importing routes dynamically and add route guards (no-changelog) (#7567)
**Before:** <img width="657" alt="image" src="https://github.com/n8n-io/n8n/assets/6179477/0bcced2b-9d3a-43b3-80d7-3c72619941fa"> **After:** <img width="660" alt="image" src="https://github.com/n8n-io/n8n/assets/6179477/e74e0bbf-bf33-49b4-ae11-65f640405ac8">
This commit is contained in:
parent
c92402a3ca
commit
24dfc95974
|
@ -44,7 +44,7 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '860px')
|
||||
.should('have.css', 'top', '220px')
|
||||
.should('have.css', 'top', '220px');
|
||||
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||
|
@ -62,7 +62,7 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '860px')
|
||||
.should('have.css', 'top', '220px')
|
||||
.should('have.css', 'top', '220px');
|
||||
});
|
||||
|
||||
it('should undo/redo deleting node using delete button', () => {
|
||||
|
@ -137,18 +137,18 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '740px')
|
||||
.should('have.css', 'top', '320px')
|
||||
.should('have.css', 'top', '320px');
|
||||
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '640px')
|
||||
.should('have.css', 'top', '220px')
|
||||
.should('have.css', 'top', '220px');
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.css', 'left', '740px')
|
||||
.should('have.css', 'top', '320px')
|
||||
.should('have.css', 'top', '320px');
|
||||
});
|
||||
|
||||
it('should undo/redo deleting a connection by pressing delete button', () => {
|
||||
|
@ -276,9 +276,6 @@ describe('Undo/Redo', () => {
|
|||
});
|
||||
|
||||
it('should undo/redo multiple steps', () => {
|
||||
const initialPosition = {left: '420px', top: '220px'};
|
||||
const movedPosition = {left: '540px', top: '360px'};
|
||||
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
// WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
||||
|
@ -289,18 +286,21 @@ describe('Undo/Redo', () => {
|
|||
// Disable last node
|
||||
WorkflowPage.getters.canvasNodes().last().click();
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
// Move first one
|
||||
WorkflowPage.getters.canvasNodes()
|
||||
.first()
|
||||
.should('have.css', 'left', initialPosition.left)
|
||||
.should('have.css', 'top', initialPosition.top)
|
||||
|
||||
// Move first one
|
||||
WorkflowPage.actions
|
||||
.getNodePosition(WorkflowPage.getters.canvasNodes().first())
|
||||
.then((initialPosition) => {
|
||||
WorkflowPage.getters.canvasNodes().first().click();
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
||||
WorkflowPage.getters.canvasNodes()
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||
clickToFinish: true,
|
||||
});
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.first()
|
||||
.should('have.css', 'left', movedPosition.left)
|
||||
.should('have.css', 'top', movedPosition.top)
|
||||
.should('have.css', 'left', `${initialPosition.left + 120}px`)
|
||||
.should('have.css', 'top', `${initialPosition.top + 140}px`);
|
||||
|
||||
// Delete the set node
|
||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click();
|
||||
cy.get('body').type('{backspace}');
|
||||
|
@ -311,10 +311,11 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||
// Second undo: Should move first node to it's original position
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.canvasNodes()
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.first()
|
||||
.should('have.css', 'left', initialPosition.left)
|
||||
.should('have.css', 'top', initialPosition.top)
|
||||
.should('have.css', 'left', `${initialPosition.left}px`)
|
||||
.should('have.css', 'top', `${initialPosition.top}px`);
|
||||
// Third undo: Should enable last node
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
|
@ -324,13 +325,15 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
// Second redo: Should move the first node
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.canvasNodes()
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.first()
|
||||
.should('have.css', 'left', movedPosition.left)
|
||||
.should('have.css', 'top', movedPosition.top)
|
||||
.should('have.css', 'left', `${initialPosition.left + 120}px`)
|
||||
.should('have.css', 'top', `${initialPosition.top + 140}px`);
|
||||
// Third redo: Should delete the Set node
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -176,6 +176,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
|
|
|
@ -59,6 +59,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.createWorkflowButton().click();
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json', 'Workflow W2');
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.url().then((url) => {
|
||||
workflowW2Url = url;
|
||||
});
|
||||
|
|
|
@ -84,8 +84,11 @@ describe('Canvas Actions', () => {
|
|||
|
||||
moveSticky({ top: 200, left: 200 });
|
||||
|
||||
dragRightEdge({ left: 200, top: 200, height: 160, width: 240 }, 100);
|
||||
dragRightEdge({ left: 200, top: 200, height: 160, width: 240 }, -50);
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="right"]', [100, 100]);
|
||||
checkStickiesStyle(100, 20, 160, 346);
|
||||
|
||||
cy.drag('[data-test-id="sticky"] [data-dir="right"]', [-50, -50]);
|
||||
checkStickiesStyle(100, 20, 160, 302);
|
||||
});
|
||||
|
||||
it('expands/shrinks sticky from the left edge', () => {
|
||||
|
@ -205,27 +208,6 @@ type Position = {
|
|||
left: number;
|
||||
};
|
||||
|
||||
type BoundingBox = {
|
||||
height: number;
|
||||
width: number;
|
||||
top: number;
|
||||
left: number;
|
||||
};
|
||||
|
||||
function dragRightEdge(curr: BoundingBox, move: number) {
|
||||
workflowPage.getters
|
||||
.stickies()
|
||||
.first()
|
||||
.then(($el) => {
|
||||
const { left, top, height, width } = curr;
|
||||
cy.drag(`[data-test-id="sticky"] [data-dir="right"]`, [left + width + move, 0], {
|
||||
abs: true,
|
||||
});
|
||||
stickyShouldBePositionedCorrectly({ top, left });
|
||||
stickyShouldHaveCorrectSize([height, width * 1.5 + move]);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldHaveOneSticky() {
|
||||
workflowPage.getters.stickies().should('have.length', 1);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ describe('BannerStack', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should render trial banner for opt-in cloud user', () => {
|
||||
cy.intercept('GET', '/rest/admin/cloud-plan', {
|
||||
body: planData,
|
||||
}).as('getPlanData');
|
||||
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
req.on('response', (res) => {
|
||||
res.send({
|
||||
|
@ -22,10 +26,6 @@ describe('BannerStack', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
}).as('loadSettings');
|
||||
|
||||
cy.intercept('GET', '/rest/admin/cloud-plan', {
|
||||
body: planData,
|
||||
}).as('getPlanData');
|
||||
|
||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
|
|
|
@ -100,6 +100,7 @@ describe('Workflow Actions', () => {
|
|||
cy.get('body').type(META_KEY, { release: false }).type('s');
|
||||
cy.get('body').type(META_KEY, { release: false }).type('s');
|
||||
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0));
|
||||
cy.waitForLoad();
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
cy.get('body').type(META_KEY, { release: false }).type('s');
|
||||
cy.wait('@saveWorkflow');
|
||||
|
|
|
@ -72,15 +72,17 @@ export class NDV extends BasePage {
|
|||
this.getters.resourceLocator(paramName).find('[data-test-id="rlc-mode-selector"]'),
|
||||
resourceMapperFieldsContainer: () => cy.getByTestId('mapping-fields-container'),
|
||||
resourceMapperSelectColumn: () => cy.getByTestId('matching-column-select'),
|
||||
resourceMapperRemoveFieldButton: (fieldName: string) => cy.getByTestId(`remove-field-button-${fieldName}`),
|
||||
resourceMapperColumnsOptionsButton: () => cy.getByTestId('columns-parameter-input-options-container'),
|
||||
resourceMapperRemoveFieldButton: (fieldName: string) =>
|
||||
cy.getByTestId(`remove-field-button-${fieldName}`),
|
||||
resourceMapperColumnsOptionsButton: () =>
|
||||
cy.getByTestId('columns-parameter-input-options-container'),
|
||||
resourceMapperRemoveAllFieldsOption: () => cy.getByTestId('action-removeAllFields'),
|
||||
sqlEditorContainer: () => cy.getByTestId('sql-editor-container'),
|
||||
};
|
||||
|
||||
actions = {
|
||||
pinData: () => {
|
||||
this.getters.pinDataButton().click();
|
||||
this.getters.pinDataButton().click({ force: true });
|
||||
},
|
||||
editPinnedData: () => {
|
||||
this.getters.editPinnedDataButton().click();
|
||||
|
@ -119,7 +121,7 @@ export class NDV extends BasePage {
|
|||
typeIntoParameterInput: (
|
||||
parameterName: string,
|
||||
content: string,
|
||||
opts?: { parseSpecialCharSequences: boolean, delay?: number },
|
||||
opts?: { parseSpecialCharSequences: boolean; delay?: number },
|
||||
) => {
|
||||
this.getters.parameterInput(parameterName).type(content, opts);
|
||||
},
|
||||
|
@ -204,7 +206,15 @@ export class NDV extends BasePage {
|
|||
getVisiblePopper().find('li').last().click();
|
||||
},
|
||||
|
||||
setInvalidExpression: ({ fieldName, invalidExpression, delay }: { fieldName: string, invalidExpression?: string, delay?: number }) => {
|
||||
setInvalidExpression: ({
|
||||
fieldName,
|
||||
invalidExpression,
|
||||
delay,
|
||||
}: {
|
||||
fieldName: string;
|
||||
invalidExpression?: string;
|
||||
delay?: number;
|
||||
}) => {
|
||||
this.actions.typeIntoParameterInput(fieldName, '=');
|
||||
this.actions.typeIntoParameterInput(fieldName, invalidExpression ?? "{{ $('unknown')", {
|
||||
parseSpecialCharSequences: false,
|
||||
|
|
|
@ -214,6 +214,7 @@ export class WorkflowPage extends BasePage {
|
|||
this.getters.saveButton().should('contain', 'Save');
|
||||
this.getters.saveButton().click();
|
||||
this.getters.saveButton().should('contain', 'Saved');
|
||||
cy.url().should('not.have.string', '/new');
|
||||
},
|
||||
saveWorkflowUsingKeyboardShortcut: () => {
|
||||
cy.intercept('POST', '/rest/workflows').as('createWorkflow');
|
||||
|
@ -340,5 +341,11 @@ export class WorkflowPage extends BasePage {
|
|||
cy.getByTestId('node-view-wrapper').trigger('mouseup', to[0], to[1], { force: true });
|
||||
cy.get('#select-box').should('not.be.visible');
|
||||
},
|
||||
getNodePosition: (node: Cypress.Chainable<JQuery<HTMLElement>>) => {
|
||||
return node.then(($el) => ({
|
||||
left: +$el[0].style.left.replace('px', ''),
|
||||
top: +$el[0].style.top.replace('px', ''),
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ Cypress.Commands.add('createFixtureWorkflow', (fixtureKey, workflowName) => {
|
|||
|
||||
cy.waitForLoad(false);
|
||||
workflowPage.actions.setWorkflowName(workflowName);
|
||||
|
||||
workflowPage.getters.saveButton().should('contain', 'Saved');
|
||||
workflowPage.actions.zoomToFit();
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
|
@ -33,7 +33,7 @@ Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => {
|
|||
// we can't set them up here because at this point it would be too late
|
||||
// and the requests would already have been made
|
||||
if (waitForIntercepts) {
|
||||
cy.wait(['@loadSettings']);
|
||||
cy.wait(['@loadSettings', '@loadNodeTypes']);
|
||||
}
|
||||
cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist');
|
||||
cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist');
|
||||
|
|
|
@ -18,6 +18,7 @@ beforeEach(() => {
|
|||
}
|
||||
|
||||
cy.intercept('GET', '/rest/settings').as('loadSettings');
|
||||
cy.intercept('GET', '/types/nodes.json').as('loadNodeTypes');
|
||||
|
||||
// Always intercept the request to test credentials and return a success
|
||||
cy.intercept('POST', '/rest/credentials/test', {
|
||||
|
|
|
@ -34,7 +34,7 @@ declare global {
|
|||
drag(
|
||||
selector: string | Cypress.Chainable<JQuery<HTMLElement>>,
|
||||
target: [number, number],
|
||||
options?: { abs?: boolean; index?: number; realMouse?: boolean },
|
||||
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
|
||||
): void;
|
||||
draganddrop(draggableSelector: string, droppableSelector: string): void;
|
||||
shouldNotHaveConsoleErrors(): void;
|
||||
|
|
|
@ -7,40 +7,6 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overscroll-behavior: none;
|
||||
line-height: 1;
|
||||
font-size: var(--font-size-m);
|
||||
font-weight: var(--font-weight-regular);
|
||||
color: var(--color-text-dark);
|
||||
background-color: var(--color-background-xlight);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body,
|
||||
button,
|
||||
input {
|
||||
font-family: var(--font-family);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
input {
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
button {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
|
@ -117,6 +83,40 @@ video {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overscroll-behavior: none;
|
||||
line-height: 1;
|
||||
font-size: var(--font-size-m);
|
||||
font-weight: var(--font-weight-regular);
|
||||
color: var(--color-text-dark);
|
||||
background-color: var(--color-background-xlight);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body,
|
||||
button,
|
||||
input {
|
||||
font-family: var(--font-family);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
input {
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
button {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div id="header" :class="$style.header">
|
||||
<router-view name="header"></router-view>
|
||||
</div>
|
||||
<div id="sidebar" :class="$style.sidebar">
|
||||
<div v-if="usersStore.currentUser" id="sidebar" :class="$style.sidebar">
|
||||
<router-view name="sidebar"></router-view>
|
||||
</div>
|
||||
<div id="content" :class="$style.content">
|
||||
|
@ -44,7 +44,7 @@ import { HIRING_BANNER, VIEWS } from '@/constants';
|
|||
|
||||
import { userHelpers } from '@/mixins/userHelpers';
|
||||
import { loadLanguage } from '@/plugins/i18n';
|
||||
import { useGlobalLinkActions, useTitleChange, useToast, useExternalHooks } from '@/composables';
|
||||
import { useGlobalLinkActions, useToast, useExternalHooks } from '@/composables';
|
||||
import {
|
||||
useUIStore,
|
||||
useSettingsStore,
|
||||
|
@ -59,7 +59,6 @@ import {
|
|||
import { useHistoryHelper } from '@/composables/useHistoryHelper';
|
||||
import { newVersions } from '@/mixins/newVersions';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { ExpressionEvaluatorProxy } from 'n8n-workflow';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
|
@ -101,118 +100,39 @@ export default defineComponent({
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
postAuthenticateDone: false,
|
||||
settingsInitialized: false,
|
||||
onAfterAuthenticateInitialized: false,
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async initSettings(): Promise<void> {
|
||||
// The settings should only be initialized once
|
||||
if (this.settingsInitialized) return;
|
||||
|
||||
try {
|
||||
await this.settingsStore.getSettings();
|
||||
this.settingsInitialized = true;
|
||||
// Re-compute title since settings are now available
|
||||
useTitleChange().titleReset();
|
||||
} catch (e) {
|
||||
this.showToast({
|
||||
title: this.$locale.baseText('startupError'),
|
||||
message: this.$locale.baseText('startupError.message'),
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
async loginWithCookie(): Promise<void> {
|
||||
try {
|
||||
await this.usersStore.loginWithCookie();
|
||||
} catch (e) {}
|
||||
},
|
||||
async initTemplates(): Promise<void> {
|
||||
if (!this.settingsStore.isTemplatesEnabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.settingsStore.testTemplatesEndpoint();
|
||||
} catch (e) {}
|
||||
},
|
||||
logHiringBanner() {
|
||||
if (this.settingsStore.isHiringBannerEnabled && !this.isDemoMode) {
|
||||
console.log(HIRING_BANNER);
|
||||
}
|
||||
},
|
||||
async checkForCloudData() {
|
||||
async initializeCloudData() {
|
||||
await this.cloudPlanStore.checkForCloudPlanData();
|
||||
await this.cloudPlanStore.fetchUserCloudAccount();
|
||||
},
|
||||
async initialize(): Promise<void> {
|
||||
await this.initSettings();
|
||||
ExpressionEvaluatorProxy.setEvaluator(useSettingsStore().settings.expressions.evaluator);
|
||||
await Promise.all([this.loginWithCookie(), this.initTemplates()]);
|
||||
async initializeTemplates() {
|
||||
if (this.settingsStore.isTemplatesEnabled) {
|
||||
try {
|
||||
await this.settingsStore.testTemplatesEndpoint();
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
trackPage(): void {
|
||||
this.uiStore.currentView = this.$route.name || '';
|
||||
if (this.$route?.meta?.templatesEnabled) {
|
||||
this.templatesStore.setSessionId();
|
||||
} else {
|
||||
this.templatesStore.resetSessionId(); // reset telemetry session id when user leaves template pages
|
||||
async initializeSourceControl() {
|
||||
if (this.sourceControlStore.isEnterpriseSourceControlEnabled) {
|
||||
await this.sourceControlStore.getPreferences();
|
||||
}
|
||||
|
||||
this.$telemetry.page(this.$route);
|
||||
},
|
||||
async authenticate() {
|
||||
// redirect to setup page. user should be redirected to this only once
|
||||
if (this.settingsStore.showSetupPage) {
|
||||
if (this.$route.name === VIEWS.SETUP) {
|
||||
return;
|
||||
async initializeNodeTranslationHeaders() {
|
||||
if (this.defaultLocale !== 'en') {
|
||||
await this.nodeTypesStore.getNodeTranslationHeaders();
|
||||
}
|
||||
|
||||
return this.$router.replace({ name: VIEWS.SETUP });
|
||||
}
|
||||
|
||||
if (this.canUserAccessCurrentRoute()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if cannot access page and not logged in, ask to sign in
|
||||
const user = this.usersStore.currentUser;
|
||||
if (!user) {
|
||||
const redirect =
|
||||
this.$route.query.redirect ||
|
||||
encodeURIComponent(`${window.location.pathname}${window.location.search}`);
|
||||
return this.$router.replace({ name: VIEWS.SIGNIN, query: { redirect } });
|
||||
}
|
||||
|
||||
// if cannot access page and is logged in, respect signin redirect
|
||||
if (this.$route.name === VIEWS.SIGNIN && typeof this.$route.query.redirect === 'string') {
|
||||
const redirect = decodeURIComponent(this.$route.query.redirect);
|
||||
if (redirect.startsWith('/')) {
|
||||
// protect against phishing
|
||||
return this.$router.replace(redirect);
|
||||
}
|
||||
}
|
||||
|
||||
// if cannot access page and is logged in
|
||||
return this.$router.replace({ name: VIEWS.HOMEPAGE });
|
||||
},
|
||||
async redirectIfNecessary() {
|
||||
const redirect =
|
||||
this.$route.meta &&
|
||||
typeof this.$route.meta.getRedirect === 'function' &&
|
||||
this.$route.meta.getRedirect();
|
||||
|
||||
if (redirect) {
|
||||
return this.$router.replace(redirect);
|
||||
}
|
||||
return;
|
||||
},
|
||||
async postAuthenticate() {
|
||||
if (this.postAuthenticateDone) {
|
||||
async onAfterAuthenticate() {
|
||||
if (this.onAfterAuthenticateInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -220,43 +140,31 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.sourceControlStore.isEnterpriseSourceControlEnabled) {
|
||||
await this.sourceControlStore.getPreferences();
|
||||
}
|
||||
await Promise.all([
|
||||
this.initializeCloudData(),
|
||||
this.initializeSourceControl(),
|
||||
this.initializeTemplates(),
|
||||
this.initializeNodeTranslationHeaders(),
|
||||
]);
|
||||
|
||||
this.postAuthenticateDone = true;
|
||||
this.onAfterAuthenticateInitialized = true;
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.initialize();
|
||||
async mounted() {
|
||||
this.logHiringBanner();
|
||||
await this.authenticate();
|
||||
await this.redirectIfNecessary();
|
||||
|
||||
void this.checkForNewVersions();
|
||||
await this.checkForCloudData();
|
||||
void this.postAuthenticate();
|
||||
void this.onAfterAuthenticate();
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.trackPage();
|
||||
void this.externalHooks.run('app.mount');
|
||||
|
||||
if (this.defaultLocale !== 'en') {
|
||||
await this.nodeTypesStore.getNodeTranslationHeaders();
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
watch: {
|
||||
'usersStore.currentUser'(currentValue, previousValue) {
|
||||
async 'usersStore.currentUser'(currentValue, previousValue) {
|
||||
if (currentValue && !previousValue) {
|
||||
void this.postAuthenticate();
|
||||
await this.onAfterAuthenticate();
|
||||
}
|
||||
},
|
||||
async $route() {
|
||||
await this.initSettings();
|
||||
await this.redirectIfNecessary();
|
||||
|
||||
this.trackPage();
|
||||
},
|
||||
defaultLocale(newLocale) {
|
||||
void loadLanguage(newLocale);
|
||||
},
|
||||
|
|
|
@ -1168,6 +1168,7 @@ export interface INodeCreatorState {
|
|||
}
|
||||
|
||||
export interface ISettingsState {
|
||||
initialized: boolean;
|
||||
settings: IN8nUISettings;
|
||||
promptsData: IN8nPrompts;
|
||||
userManagement: IUserManagementSettings;
|
||||
|
@ -1232,6 +1233,7 @@ export interface IVersionsState {
|
|||
}
|
||||
|
||||
export interface IUsersState {
|
||||
initialized: boolean;
|
||||
currentUserId: null | string;
|
||||
users: { [userId: string]: IUser };
|
||||
currentUserCloudInfo: Cloud.UserAccount | null;
|
||||
|
|
|
@ -2,6 +2,8 @@ import { createPinia, setActivePinia } from 'pinia';
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import router from '@/router';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { setupServer } from '@/__tests__/server';
|
||||
import { useSettingsStore } from '@/stores';
|
||||
|
||||
const App = {
|
||||
template: '<div />',
|
||||
|
@ -9,25 +11,66 @@ const App = {
|
|||
const renderComponent = createComponentRenderer(App);
|
||||
|
||||
describe('router', () => {
|
||||
beforeAll(() => {
|
||||
let server: ReturnType<typeof setupServer>;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = setupServer();
|
||||
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
renderComponent({ pinia });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
test.each([
|
||||
['/', VIEWS.WORKFLOWS],
|
||||
['/workflow', VIEWS.NEW_WORKFLOW],
|
||||
['/workflow/new', VIEWS.NEW_WORKFLOW],
|
||||
['/workflow/R9JFXwkUCL1jZBuw', VIEWS.WORKFLOW],
|
||||
['/workflow/R9JFXwkUCL1jZBuw/executions/29021', VIEWS.EXECUTION_PREVIEW],
|
||||
['/workflow/R9JFXwkUCL1jZBuw/debug/29021', VIEWS.EXECUTION_DEBUG],
|
||||
['/workflows/templates/R9JFXwkUCL1jZBuw', VIEWS.TEMPLATE_IMPORT],
|
||||
['/workflows/demo', VIEWS.DEMO],
|
||||
['/workflow/8IFYawZ9dKqJu8sT/history', VIEWS.WORKFLOW_HISTORY],
|
||||
['/workflow/8IFYawZ9dKqJu8sT/history/6513ed960252b846f3792f0c', VIEWS.WORKFLOW_HISTORY],
|
||||
])('should resolve %s to %s', async (path, name) => {
|
||||
])(
|
||||
'should resolve %s to %s',
|
||||
async (path, name) => {
|
||||
await router.push(path);
|
||||
expect(router.currentRoute.value.name).toBe(name);
|
||||
});
|
||||
},
|
||||
10000,
|
||||
);
|
||||
|
||||
test.each([
|
||||
['/workflow/R9JFXwkUCL1jZBuw/debug/29021', VIEWS.WORKFLOWS],
|
||||
['/workflow/8IFYawZ9dKqJu8sT/history', VIEWS.WORKFLOWS],
|
||||
['/workflow/8IFYawZ9dKqJu8sT/history/6513ed960252b846f3792f0c', VIEWS.WORKFLOWS],
|
||||
])(
|
||||
'should redirect %s to %s if user does not have permissions',
|
||||
async (path, name) => {
|
||||
await router.push(path);
|
||||
expect(router.currentRoute.value.name).toBe(name);
|
||||
},
|
||||
10000,
|
||||
);
|
||||
|
||||
test.each([
|
||||
['/workflow/R9JFXwkUCL1jZBuw/debug/29021', VIEWS.EXECUTION_DEBUG],
|
||||
['/workflow/8IFYawZ9dKqJu8sT/history', VIEWS.WORKFLOW_HISTORY],
|
||||
['/workflow/8IFYawZ9dKqJu8sT/history/6513ed960252b846f3792f0c', VIEWS.WORKFLOW_HISTORY],
|
||||
])(
|
||||
'should resolve %s to %s if user has permissions',
|
||||
async (path, name) => {
|
||||
const settingsStore = useSettingsStore();
|
||||
await settingsStore.getSettings();
|
||||
settingsStore.settings.enterprise.debugInEditor = true;
|
||||
settingsStore.settings.enterprise.workflowHistory = true;
|
||||
|
||||
await router.push(path);
|
||||
expect(router.currentRoute.value.name).toBe(name);
|
||||
},
|
||||
10000,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -14,12 +14,18 @@ const defaultSettings: IN8nUISettings = {
|
|||
ldap: false,
|
||||
saml: false,
|
||||
logStreaming: false,
|
||||
debugInEditor: false,
|
||||
advancedExecutionFilters: false,
|
||||
variables: true,
|
||||
sourceControl: false,
|
||||
auditLogs: false,
|
||||
versionControl: false,
|
||||
showNonProdBanner: false,
|
||||
externalSecrets: false,
|
||||
binaryDataS3: false,
|
||||
workflowHistory: false,
|
||||
},
|
||||
expressions: {
|
||||
evaluator: 'tournament',
|
||||
},
|
||||
executionMode: 'regular',
|
||||
executionTimeout: 0,
|
||||
|
@ -43,6 +49,7 @@ const defaultSettings: IN8nUISettings = {
|
|||
},
|
||||
publicApi: { enabled: false, latestVersion: 0, path: '', swaggerUi: { enabled: false } },
|
||||
pushBackend: 'websocket',
|
||||
releaseChannel: 'stable',
|
||||
saveDataErrorExecution: 'DEFAULT',
|
||||
saveDataSuccessExecution: 'DEFAULT',
|
||||
saveManualExecutions: false,
|
||||
|
@ -58,7 +65,7 @@ const defaultSettings: IN8nUISettings = {
|
|||
urlBaseEditor: '',
|
||||
urlBaseWebhook: '',
|
||||
userManagement: {
|
||||
showSetupOnFirstLoad: true,
|
||||
showSetupOnFirstLoad: false,
|
||||
smtpSetup: true,
|
||||
authenticationMethod: 'email',
|
||||
},
|
||||
|
|
|
@ -37,7 +37,6 @@ export default defineComponent({
|
|||
max-width: 1280px;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: var(--color-gray-light);
|
||||
padding: var(--spacing-l) var(--spacing-l) 0;
|
||||
@media (min-width: 1200px) {
|
||||
padding: var(--spacing-2xl) var(--spacing-2xl) 0;
|
||||
|
|
|
@ -20,9 +20,7 @@ import { GlobalComponentsPlugin } from './plugins/components';
|
|||
import { GlobalDirectivesPlugin } from './plugins/directives';
|
||||
import { FontAwesomePlugin } from './plugins/icons';
|
||||
|
||||
import { runExternalHook } from '@/utils';
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia';
|
||||
import { useWebhooksStore } from '@/stores';
|
||||
import { JsPlumbPlugin } from '@/plugins/jsplumb';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
@ -42,10 +40,6 @@ app.use(i18nInstance);
|
|||
|
||||
app.mount('#app');
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
void runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
|
||||
});
|
||||
|
||||
if (!import.meta.env.PROD) {
|
||||
// Make sure that we get all error messages properly displayed
|
||||
// as long as we are not in production mode
|
||||
|
|
|
@ -1,49 +1,54 @@
|
|||
import { useStorage } from '@vueuse/core';
|
||||
import ChangePasswordView from './views/ChangePasswordView.vue';
|
||||
import ErrorView from './views/ErrorView.vue';
|
||||
import ForgotMyPasswordView from './views/ForgotMyPasswordView.vue';
|
||||
import MainHeader from '@/components/MainHeader/MainHeader.vue';
|
||||
import MainSidebar from '@/components/MainSidebar.vue';
|
||||
import NodeView from '@/views/NodeView.vue';
|
||||
import WorkflowExecutionsList from '@/components/ExecutionsView/ExecutionsList.vue';
|
||||
import ExecutionsLandingPage from '@/components/ExecutionsView/ExecutionsLandingPage.vue';
|
||||
import ExecutionPreview from '@/components/ExecutionsView/ExecutionPreview.vue';
|
||||
import SettingsView from './views/SettingsView.vue';
|
||||
import SettingsLdapView from './views/SettingsLdapView.vue';
|
||||
import SettingsPersonalView from './views/SettingsPersonalView.vue';
|
||||
import SettingsUsersView from './views/SettingsUsersView.vue';
|
||||
import SettingsCommunityNodesView from './views/SettingsCommunityNodesView.vue';
|
||||
import SettingsApiView from './views/SettingsApiView.vue';
|
||||
import SettingsLogStreamingView from './views/SettingsLogStreamingView.vue';
|
||||
import SettingsFakeDoorView from './views/SettingsFakeDoorView.vue';
|
||||
import SetupView from './views/SetupView.vue';
|
||||
import SigninView from './views/SigninView.vue';
|
||||
import SignupView from './views/SignupView.vue';
|
||||
import type { RouteLocation, RouteRecordRaw } from 'vue-router';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
import TemplatesCollectionView from '@/views/TemplatesCollectionView.vue';
|
||||
import TemplatesWorkflowView from '@/views/TemplatesWorkflowView.vue';
|
||||
import TemplatesSearchView from '@/views/TemplatesSearchView.vue';
|
||||
import CredentialsView from '@/views/CredentialsView.vue';
|
||||
import ExecutionsView from '@/views/ExecutionsView.vue';
|
||||
import WorkflowsView from '@/views/WorkflowsView.vue';
|
||||
import VariablesView from '@/views/VariablesView.vue';
|
||||
import type { IPermissions } from './Interface';
|
||||
import { LOGIN_STATUS, ROLE } from '@/utils';
|
||||
import { isAuthorized, LOGIN_STATUS, ROLE, runExternalHook } from '@/utils';
|
||||
import { useSettingsStore } from './stores/settings.store';
|
||||
import { useUsersStore } from './stores/users.store';
|
||||
import { useTemplatesStore } from './stores/templates.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSSOStore } from './stores/sso.store';
|
||||
import SettingsUsageAndPlanVue from './views/SettingsUsageAndPlan.vue';
|
||||
import SettingsSso from './views/SettingsSso.vue';
|
||||
import SignoutView from '@/views/SignoutView.vue';
|
||||
import SamlOnboarding from '@/views/SamlOnboarding.vue';
|
||||
import SettingsSourceControl from './views/SettingsSourceControl.vue';
|
||||
import SettingsExternalSecrets from './views/SettingsExternalSecrets.vue';
|
||||
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
|
||||
import WorkflowHistory from '@/views/WorkflowHistory.vue';
|
||||
import WorkflowOnboardingView from '@/views/WorkflowOnboardingView.vue';
|
||||
import { useWebhooksStore } from '@/stores/webhooks.store';
|
||||
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||
import { useTelemetry } from '@/composables';
|
||||
|
||||
const ChangePasswordView = async () => import('./views/ChangePasswordView.vue');
|
||||
const ErrorView = async () => import('./views/ErrorView.vue');
|
||||
const ForgotMyPasswordView = async () => import('./views/ForgotMyPasswordView.vue');
|
||||
const MainHeader = async () => import('@/components/MainHeader/MainHeader.vue');
|
||||
const MainSidebar = async () => import('@/components/MainSidebar.vue');
|
||||
const NodeView = async () => import('@/views/NodeView.vue');
|
||||
const WorkflowExecutionsList = async () => import('@/components/ExecutionsView/ExecutionsList.vue');
|
||||
const ExecutionsLandingPage = async () =>
|
||||
import('@/components/ExecutionsView/ExecutionsLandingPage.vue');
|
||||
const ExecutionPreview = async () => import('@/components/ExecutionsView/ExecutionPreview.vue');
|
||||
const SettingsView = async () => import('./views/SettingsView.vue');
|
||||
const SettingsLdapView = async () => import('./views/SettingsLdapView.vue');
|
||||
const SettingsPersonalView = async () => import('./views/SettingsPersonalView.vue');
|
||||
const SettingsUsersView = async () => import('./views/SettingsUsersView.vue');
|
||||
const SettingsCommunityNodesView = async () => import('./views/SettingsCommunityNodesView.vue');
|
||||
const SettingsApiView = async () => import('./views/SettingsApiView.vue');
|
||||
const SettingsLogStreamingView = async () => import('./views/SettingsLogStreamingView.vue');
|
||||
const SettingsFakeDoorView = async () => import('./views/SettingsFakeDoorView.vue');
|
||||
const SetupView = async () => import('./views/SetupView.vue');
|
||||
const SigninView = async () => import('./views/SigninView.vue');
|
||||
const SignupView = async () => import('./views/SignupView.vue');
|
||||
const TemplatesCollectionView = async () => import('@/views/TemplatesCollectionView.vue');
|
||||
const TemplatesWorkflowView = async () => import('@/views/TemplatesWorkflowView.vue');
|
||||
const TemplatesSearchView = async () => import('@/views/TemplatesSearchView.vue');
|
||||
const CredentialsView = async () => import('@/views/CredentialsView.vue');
|
||||
const ExecutionsView = async () => import('@/views/ExecutionsView.vue');
|
||||
const WorkflowsView = async () => import('@/views/WorkflowsView.vue');
|
||||
const VariablesView = async () => import('@/views/VariablesView.vue');
|
||||
const SettingsUsageAndPlan = async () => import('./views/SettingsUsageAndPlan.vue');
|
||||
const SettingsSso = async () => import('./views/SettingsSso.vue');
|
||||
const SignoutView = async () => import('@/views/SignoutView.vue');
|
||||
const SamlOnboarding = async () => import('@/views/SamlOnboarding.vue');
|
||||
const SettingsSourceControl = async () => import('./views/SettingsSourceControl.vue');
|
||||
const SettingsExternalSecrets = async () => import('./views/SettingsExternalSecrets.vue');
|
||||
const SettingsAuditLogs = async () => import('./views/SettingsAuditLogs.vue');
|
||||
const WorkflowHistory = async () => import('@/views/WorkflowHistory.vue');
|
||||
const WorkflowOnboardingView = async () => import('@/views/WorkflowOnboardingView.vue');
|
||||
|
||||
interface IRouteConfig {
|
||||
meta: {
|
||||
|
@ -521,7 +526,7 @@ export const routes = [
|
|||
path: 'usage',
|
||||
name: VIEWS.USAGE,
|
||||
components: {
|
||||
settingsView: SettingsUsageAndPlanVue,
|
||||
settingsView: SettingsUsageAndPlan,
|
||||
},
|
||||
meta: {
|
||||
telemetry: {
|
||||
|
@ -869,4 +874,91 @@ const router = createRouter({
|
|||
routes,
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
/**
|
||||
* Initialize stores before routing
|
||||
*/
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const usersStore = useUsersStore();
|
||||
await settingsStore.initialize();
|
||||
await usersStore.initialize();
|
||||
|
||||
/**
|
||||
* Redirect to setup page. User should be redirected to this only once
|
||||
*/
|
||||
|
||||
if (settingsStore.showSetupPage) {
|
||||
if (to.name === VIEWS.SETUP) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return next({ name: VIEWS.SETUP });
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify user permissions for current route
|
||||
*/
|
||||
|
||||
const currentUser = usersStore.currentUser;
|
||||
const permissions = to.meta?.permissions as IPermissions;
|
||||
const canUserAccessCurrentRoute = permissions && isAuthorized(permissions, currentUser);
|
||||
if (canUserAccessCurrentRoute) {
|
||||
return next();
|
||||
}
|
||||
|
||||
/**
|
||||
* If user cannot access the page and is not logged in, redirect to sign in
|
||||
*/
|
||||
|
||||
if (!currentUser) {
|
||||
const redirect =
|
||||
to.query.redirect ||
|
||||
encodeURIComponent(`${window.location.pathname}${window.location.search}`);
|
||||
return next({ name: VIEWS.SIGNIN, query: { redirect } });
|
||||
}
|
||||
|
||||
/**
|
||||
* If user cannot access page but is logged in, respect sign in redirect
|
||||
*/
|
||||
|
||||
if (to.name === VIEWS.SIGNIN && typeof to.query.redirect === 'string') {
|
||||
const redirect = decodeURIComponent(to.query.redirect);
|
||||
if (redirect.startsWith('/')) {
|
||||
// protect against phishing
|
||||
return next(redirect);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Otherwise, redirect to home page
|
||||
*/
|
||||
|
||||
return next({ name: VIEWS.HOMEPAGE });
|
||||
});
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
const telemetry = useTelemetry();
|
||||
const uiStore = useUIStore();
|
||||
const templatesStore = useTemplatesStore();
|
||||
|
||||
/**
|
||||
* Run external hooks
|
||||
*/
|
||||
|
||||
void runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
|
||||
|
||||
/**
|
||||
* Track current view for telemetry
|
||||
*/
|
||||
|
||||
uiStore.currentView = (to.name as string) ?? '';
|
||||
if (to.meta?.templatesEnabled) {
|
||||
templatesStore.setSessionId();
|
||||
} else {
|
||||
templatesStore.resetSessionId(); // reset telemetry session id when user leaves template pages
|
||||
}
|
||||
telemetry.page(to);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -31,9 +31,13 @@ import { useUIStore } from './ui.store';
|
|||
import { useUsersStore } from './users.store';
|
||||
import { useVersionsStore } from './versions.store';
|
||||
import { makeRestApiRequest } from '@/utils';
|
||||
import { useTitleChange, useToast } from '@/composables';
|
||||
import { ExpressionEvaluatorProxy } from 'n8n-workflow';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
|
||||
export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||
state: (): ISettingsState => ({
|
||||
initialized: false,
|
||||
settings: {} as IN8nUISettings,
|
||||
promptsData: {} as IN8nPrompts,
|
||||
userManagement: {
|
||||
|
@ -69,7 +73,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
|||
}),
|
||||
getters: {
|
||||
isEnterpriseFeatureEnabled() {
|
||||
return (feature: EnterpriseEditionFeature): boolean => this.settings.enterprise[feature];
|
||||
return (feature: EnterpriseEditionFeature): boolean => this.settings.enterprise?.[feature];
|
||||
},
|
||||
versionCli(): string {
|
||||
return this.settings.versionCli;
|
||||
|
@ -190,6 +194,33 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
async initialize() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { showToast } = useToast();
|
||||
try {
|
||||
await this.getSettings();
|
||||
|
||||
ExpressionEvaluatorProxy.setEvaluator(this.settings.expressions.evaluator);
|
||||
|
||||
// Re-compute title since settings are now available
|
||||
useTitleChange().titleReset();
|
||||
|
||||
this.initialized = true;
|
||||
} catch (e) {
|
||||
showToast({
|
||||
title: i18n.baseText('startupError'),
|
||||
message: i18n.baseText('startupError.message'),
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
setSettings(settings: IN8nUISettings): void {
|
||||
this.settings = settings;
|
||||
this.userManagement = settings.userManagement;
|
||||
|
|
|
@ -52,6 +52,7 @@ const isInstanceOwner = (user: IUserResponse | null) =>
|
|||
|
||||
export const useUsersStore = defineStore(STORES.USERS, {
|
||||
state: (): IUsersState => ({
|
||||
initialized: false,
|
||||
currentUserId: null,
|
||||
users: {},
|
||||
currentUserCloudInfo: null,
|
||||
|
@ -122,6 +123,16 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
async initialize() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loginWithCookie();
|
||||
this.initialized = true;
|
||||
} catch (e) {}
|
||||
},
|
||||
addUsers(users: IUserResponse[]) {
|
||||
users.forEach((userResponse: IUserResponse) => {
|
||||
const prevUser = this.users[userResponse.id] || {};
|
||||
|
|
Loading…
Reference in a new issue