fix(editor): Fix workflow back button navigation (#4546)

* 🐛 Fix back button navigation from recetly saved workflow

* 🐛 Fix coming-soon routes
This commit is contained in:
OlegIvaniv 2022-11-09 09:31:23 +01:00 committed by GitHub
parent 740df0c1e5
commit 825637f02a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 368 additions and 355 deletions

View file

@ -34,7 +34,6 @@ import { HIRING_BANNER, LOCAL_STORAGE_THEME, VIEWS } from './constants';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { showMessage } from './components/mixins/showMessage'; import { showMessage } from './components/mixins/showMessage';
import { IUser } from './Interface';
import { userHelpers } from './components/mixins/userHelpers'; import { userHelpers } from './components/mixins/userHelpers';
import { loadLanguage } from './plugins/i18n'; import { loadLanguage } from './plugins/i18n';
import { restApi } from '@/components/mixins/restApi'; import { restApi } from '@/components/mixins/restApi';

View file

@ -2,7 +2,7 @@
<div :class="$style.container"> <div :class="$style.container">
<n8n-menu :items="sidebarMenuItems" @select="handleSelect"> <n8n-menu :items="sidebarMenuItems" @select="handleSelect">
<template #header> <template #header>
<div :class="$style.returnButton" @click="onReturn"> <div :class="$style.returnButton" @click="$emit('return')">
<i class="mr-xs"> <i class="mr-xs">
<font-awesome-icon icon="arrow-left" /> <font-awesome-icon icon="arrow-left" />
</i> </i>
@ -22,7 +22,6 @@
<script lang="ts"> <script lang="ts">
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { mapGetters } from 'vuex';
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants'; import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
import { userHelpers } from './mixins/userHelpers'; import { userHelpers } from './mixins/userHelpers';
import { pushConnection } from "@/components/mixins/pushConnection"; import { pushConnection } from "@/components/mixins/pushConnection";
@ -123,9 +122,6 @@ export default mixins(
onVersionClick() { onVersionClick() {
this.uiStore.openModal(ABOUT_MODAL_KEY); this.uiStore.openModal(ABOUT_MODAL_KEY);
}, },
onReturn() {
this.$router.push({name: VIEWS.HOMEPAGE});
},
openUpdatesPanel() { openUpdatesPanel() {
this.uiStore.openModal(VERSIONS_MODAL_KEY); this.uiStore.openModal(VERSIONS_MODAL_KEY);
}, },

View file

@ -831,7 +831,7 @@ export const workflowHelpers = mixins(
} }
if (redirect) { if (redirect) {
this.$router.push({ this.$router.replace({
name: VIEWS.WORKFLOW, name: VIEWS.WORKFLOW,
params: { name: workflowData.id as string, action: 'workflowSave' }, params: { name: workflowData.id as string, action: 'workflowSave' },
}); });

View file

@ -9,6 +9,7 @@ import NodeView from '@/views/NodeView.vue';
import ExecutionsView from '@/components/ExecutionsView/ExecutionsView.vue'; import ExecutionsView from '@/components/ExecutionsView/ExecutionsView.vue';
import ExecutionsLandingPage from '@/components/ExecutionsView/ExecutionsLandingPage.vue'; import ExecutionsLandingPage from '@/components/ExecutionsView/ExecutionsLandingPage.vue';
import ExecutionPreview from '@/components/ExecutionsView/ExecutionPreview.vue'; import ExecutionPreview from '@/components/ExecutionsView/ExecutionPreview.vue';
import SettingsView from './views/SettingsView.vue';
import SettingsPersonalView from './views/SettingsPersonalView.vue'; import SettingsPersonalView from './views/SettingsPersonalView.vue';
import SettingsUsersView from './views/SettingsUsersView.vue'; import SettingsUsersView from './views/SettingsUsersView.vue';
import SettingsCommunityNodesView from './views/SettingsCommunityNodesView.vue'; import SettingsCommunityNodesView from './views/SettingsCommunityNodesView.vue';
@ -424,132 +425,136 @@ const router = new Router({
}, },
{ {
path: '/settings', path: '/settings',
redirect: '/settings/personal', component: SettingsView,
},
{
path: '/settings/users',
name: VIEWS.USERS_SETTINGS,
components: {
default: SettingsUsersView,
},
meta: {
telemetry: {
pageCategory: 'settings',
getProperties(route: Route) {
return {
feature: 'users',
};
},
},
permissions: {
allow: {
role: [ROLE.Default, ROLE.Owner],
},
deny: {
shouldDeny: () => {
const settingsStore = useSettingsStore();
return settingsStore.isUserManagementEnabled === false;
},
},
},
},
},
{
path: '/settings/personal',
name: VIEWS.PERSONAL_SETTINGS,
components: {
default: SettingsPersonalView,
},
meta: {
telemetry: {
pageCategory: 'settings',
getProperties(route: Route) {
return {
feature: 'personal',
};
},
},
permissions: {
allow: {
loginStatus: [LOGIN_STATUS.LoggedIn],
},
deny: {
role: [ROLE.Default],
},
},
},
},
{
path: '/settings/api',
name: VIEWS.API_SETTINGS,
components: {
default: SettingsApiView,
},
meta: {
telemetry: {
pageCategory: 'settings',
getProperties(route: Route) {
return {
feature: 'api',
};
},
},
permissions: {
allow: {
loginStatus: [LOGIN_STATUS.LoggedIn],
},
deny: {
shouldDeny: () => {
const settingsStore = useSettingsStore();
return settingsStore.isPublicApiEnabled === false;
},
},
},
},
},
{
path: '/settings/community-nodes',
name: VIEWS.COMMUNITY_NODES,
components: {
default: SettingsCommunityNodesView,
},
meta: {
telemetry: {
pageCategory: 'settings',
},
permissions: {
allow: {
role: [ROLE.Default, ROLE.Owner],
},
deny: {
shouldDeny: () => {
const settingsStore = useSettingsStore();
return settingsStore.isCommunityNodesFeatureEnabled === false;
},
},
},
},
},
{
path: '/settings/coming-soon/:featureId',
name: VIEWS.FAKE_DOOR,
component: SettingsFakeDoorView,
props: true, props: true,
meta: { children: [
telemetry: { {
pageCategory: 'settings', path: 'personal',
getProperties(route: Route) { name: VIEWS.PERSONAL_SETTINGS,
return { components: {
feature: route.params['featureId'], settingsView: SettingsPersonalView,
}; },
meta: {
telemetry: {
pageCategory: 'settings',
getProperties(route: Route) {
return {
feature: 'personal',
};
},
},
permissions: {
allow: {
loginStatus: [LOGIN_STATUS.LoggedIn],
},
deny: {
role: [ROLE.Default],
},
},
}, },
}, },
permissions: { {
allow: { path: 'users',
loginStatus: [LOGIN_STATUS.LoggedIn], name: VIEWS.USERS_SETTINGS,
components: {
settingsView: SettingsUsersView,
},
meta: {
telemetry: {
pageCategory: 'settings',
getProperties(route: Route) {
return {
feature: 'users',
};
},
},
permissions: {
allow: {
role: [ROLE.Default, ROLE.Owner],
},
deny: {
shouldDeny: () => {
const settingsStore = useSettingsStore();
return settingsStore.isUserManagementEnabled === false;
},
},
},
}, },
}, },
}, {
path: 'api',
name: VIEWS.API_SETTINGS,
components: {
settingsView: SettingsApiView,
},
meta: {
telemetry: {
pageCategory: 'settings',
getProperties(route: Route) {
return {
feature: 'api',
};
},
},
permissions: {
allow: {
loginStatus: [LOGIN_STATUS.LoggedIn],
},
deny: {
shouldDeny: () => {
const settingsStore = useSettingsStore();
return settingsStore.isPublicApiEnabled === false;
},
},
},
},
},
{
path: 'community-nodes',
name: VIEWS.COMMUNITY_NODES,
components: {
settingsView: SettingsCommunityNodesView,
},
meta: {
telemetry: {
pageCategory: 'settings',
},
permissions: {
allow: {
role: [ROLE.Default, ROLE.Owner],
},
deny: {
shouldDeny: () => {
const settingsStore = useSettingsStore();
return settingsStore.isCommunityNodesFeatureEnabled === false;
},
},
},
},
},
{
path: 'coming-soon/:featureId',
name: VIEWS.FAKE_DOOR,
components: {
settingsView: SettingsFakeDoorView,
},
meta: {
telemetry: {
pageCategory: 'settings',
getProperties(route: Route) {
return {
feature: route.params['featureId'],
};
},
},
permissions: {
allow: {
loginStatus: [LOGIN_STATUS.LoggedIn],
},
},
},
},
],
}, },
{ {
path: '*', path: '*',

View file

@ -220,7 +220,7 @@ import { useUIStore } from '@/stores/ui';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useUsersStore } from '@/stores/users'; import { useUsersStore } from '@/stores/users';
import { getNodeViewTab } from '@/components/helpers'; import { getNodeViewTab } from '@/components/helpers';
import { Route } from 'vue-router'; import { Route, RawLocation } from 'vue-router';
import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsStore } from '@/stores/workflows';
import { useRootStore } from '@/stores/n8nRootStore'; import { useRootStore } from '@/stores/n8nRootStore';
import { useNDVStore } from '@/stores/ndv'; import { useNDVStore } from '@/stores/ndv';
@ -347,11 +347,25 @@ export default mixins(
if (confirmModal === MODAL_CONFIRMED) { if (confirmModal === MODAL_CONFIRMED) {
const saved = await this.saveCurrentWorkflow({}, false); const saved = await this.saveCurrentWorkflow({}, false);
if (saved) this.settingsStore.fetchPromptsData(); if (saved) await this.settingsStore.fetchPromptsData();
this.uiStore.stateIsDirty = false; this.uiStore.stateIsDirty = false;
next();
if(from.name === VIEWS.NEW_WORKFLOW) {
// Replace the current route with the new workflow route
// before navigating to the new route when saving new workflow.
this.$router.replace({ name: VIEWS.WORKFLOW, params: { name: this.currentWorkflow } }, () => {
// We can't use next() here since vue-router
// would prevent the navigation with an error
this.$router.push(to as RawLocation);
});
} else {
next();
}
} else if (confirmModal === MODAL_CANCEL) { } else if (confirmModal === MODAL_CANCEL) {
await this.resetWorkspace();
this.uiStore.stateIsDirty = false; this.uiStore.stateIsDirty = false;
next(); next();
} else if (confirmModal === MODAL_CLOSE) { } else if (confirmModal === MODAL_CLOSE) {
next(false); next(false);

View file

@ -1,69 +1,67 @@
<template> <template>
<SettingsView> <div :class="$style.container">
<div :class="$style.container"> <div :class="$style.header">
<div :class="$style.header"> <n8n-heading size="2xlarge">
<n8n-heading size="2xlarge"> {{ $locale.baseText('settings.api') }}
{{ $locale.baseText('settings.api') }} <span :style="{ fontSize: 'var(--font-size-s)', color: 'var(--color-text-light)', }">
<span :style="{ fontSize: 'var(--font-size-s)', color: 'var(--color-text-light)', }"> ({{ $locale.baseText('beta') }})
({{ $locale.baseText('beta') }}) </span>
</span> </n8n-heading>
</n8n-heading>
</div>
<div v-if="apiKey">
<p class="mb-s">
<n8n-info-tip :bold="false">
<i18n path="settings.api.view.info" tag="span">
<template #apiAction>
<a
href="https://docs.n8n.io/api"
target="_blank"
v-text="$locale.baseText('settings.api.view.info.api')"
/>
</template>
<template #webhookAction>
<a
href="https://docs.n8n.io/integrations/core-nodes/n8n-nodes-base.webhook/"
target="_blank"
v-text="$locale.baseText('settings.api.view.info.webhook')"
/>
</template>
</i18n>
</n8n-info-tip>
</p>
<n8n-card class="mb-4xs" :class="$style.card">
<span :class="$style.delete">
<n8n-link @click="showDeleteModal" :bold="true">
{{ $locale.baseText('generic.delete') }}
</n8n-link>
</span>
<div class="ph-no-capture">
<CopyInput
:label="$locale.baseText('settings.api.view.myKey')"
:value="apiKey"
:copy-button-text="$locale.baseText('generic.clickToCopy')"
:toast-title="$locale.baseText('settings.api.view.copy.toast')"
@copy="onCopy"
/>
</div>
</n8n-card>
<div :class="$style.hint">
<n8n-text size="small">
{{ $locale.baseText('settings.api.view.tryapi') }}
</n8n-text>
<n8n-link :to="apiPlaygroundPath" :newWindow="true" size="small">
{{ $locale.baseText('settings.api.view.apiPlayground') }}
</n8n-link>
</div>
</div>
<n8n-action-box
v-else-if="mounted"
:buttonText="$locale.baseText(loading ? 'settings.api.create.button.loading' : 'settings.api.create.button')"
:description="$locale.baseText('settings.api.create.description')"
@click="createApiKey"
/>
</div> </div>
</SettingsView>
<div v-if="apiKey">
<p class="mb-s">
<n8n-info-tip :bold="false">
<i18n path="settings.api.view.info" tag="span">
<template #apiAction>
<a
href="https://docs.n8n.io/api"
target="_blank"
v-text="$locale.baseText('settings.api.view.info.api')"
/>
</template>
<template #webhookAction>
<a
href="https://docs.n8n.io/integrations/core-nodes/n8n-nodes-base.webhook/"
target="_blank"
v-text="$locale.baseText('settings.api.view.info.webhook')"
/>
</template>
</i18n>
</n8n-info-tip>
</p>
<n8n-card class="mb-4xs" :class="$style.card">
<span :class="$style.delete">
<n8n-link @click="showDeleteModal" :bold="true">
{{ $locale.baseText('generic.delete') }}
</n8n-link>
</span>
<div class="ph-no-capture">
<CopyInput
:label="$locale.baseText('settings.api.view.myKey')"
:value="apiKey"
:copy-button-text="$locale.baseText('generic.clickToCopy')"
:toast-title="$locale.baseText('settings.api.view.copy.toast')"
@copy="onCopy"
/>
</div>
</n8n-card>
<div :class="$style.hint">
<n8n-text size="small">
{{ $locale.baseText('settings.api.view.tryapi') }}
</n8n-text>
<n8n-link :to="apiPlaygroundPath" :newWindow="true" size="small">
{{ $locale.baseText('settings.api.view.apiPlayground') }}
</n8n-link>
</div>
</div>
<n8n-action-box
v-else-if="mounted"
:buttonText="$locale.baseText(loading ? 'settings.api.create.button.loading' : 'settings.api.create.button')"
:description="$locale.baseText('settings.api.create.description')"
@click="createApiKey"
/>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -71,8 +69,7 @@ import { showMessage } from '@/components/mixins/showMessage';
import { IUser } from '@/Interface'; import { IUser } from '@/Interface';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import SettingsView from './SettingsView.vue'; import CopyInput from '@/components/CopyInput.vue';
import CopyInput from '../components/CopyInput.vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useRootStore } from '@/stores/n8nRootStore'; import { useRootStore } from '@/stores/n8nRootStore';
@ -83,7 +80,6 @@ export default mixins(
).extend({ ).extend({
name: 'SettingsPersonalView', name: 'SettingsPersonalView',
components: { components: {
SettingsView,
CopyInput, CopyInput,
}, },
data() { data() {

View file

@ -1,62 +1,60 @@
<template> <template>
<SettingsView> <div :class="$style.container">
<div :class="$style.container"> <div :class="$style.headingContainer">
<div :class="$style.headingContainer"> <n8n-heading size="2xlarge">{{ $locale.baseText('settings.communityNodes') }}</n8n-heading>
<n8n-heading size="2xlarge">{{ $locale.baseText('settings.communityNodes') }}</n8n-heading> <n8n-button
<n8n-button v-if="!settingsStore.isQueueModeEnabled && communityNodesStore.getInstalledPackages.length > 0 && !loading"
v-if="!settingsStore.isQueueModeEnabled && communityNodesStore.getInstalledPackages.length > 0 && !loading" :label="$locale.baseText('settings.communityNodes.installModal.installButton.label')"
:label="$locale.baseText('settings.communityNodes.installModal.installButton.label')" size="large"
size="large" @click="openInstallModal"
@click="openInstallModal" />
/>
</div>
<div v-if="settingsStore.isQueueModeEnabled" :class="$style.actionBoxContainer">
<n8n-action-box
:heading="$locale.baseText('settings.communityNodes.empty.title')"
:description="getEmptyStateDescription"
:calloutText="actionBoxConfig.calloutText"
:calloutTheme="actionBoxConfig.calloutTheme"
/>
</div>
<div
:class="$style.cardsContainer"
v-else-if="loading"
>
<community-package-card
v-for="n in 2"
:key="'index-' + n"
:loading="true"
></community-package-card>
</div>
<div
v-else-if="communityNodesStore.getInstalledPackages.length === 0"
:class="$style.actionBoxContainer"
>
<n8n-action-box
:heading="$locale.baseText('settings.communityNodes.empty.title')"
:description="getEmptyStateDescription"
:buttonText="
shouldShowInstallButton
? $locale.baseText('settings.communityNodes.empty.installPackageLabel')
: ''
"
:calloutText="actionBoxConfig.calloutText"
:calloutTheme="actionBoxConfig.calloutTheme"
@click="openInstallModal"
/>
</div>
<div
:class="$style.cardsContainer"
v-else
>
<community-package-card
v-for="communityPackage in communityNodesStore.getInstalledPackages"
:key="communityPackage.packageName"
:communityPackage="communityPackage"
></community-package-card>
</div>
</div> </div>
</SettingsView> <div v-if="settingsStore.isQueueModeEnabled" :class="$style.actionBoxContainer">
<n8n-action-box
:heading="$locale.baseText('settings.communityNodes.empty.title')"
:description="getEmptyStateDescription"
:calloutText="actionBoxConfig.calloutText"
:calloutTheme="actionBoxConfig.calloutTheme"
/>
</div>
<div
:class="$style.cardsContainer"
v-else-if="loading"
>
<community-package-card
v-for="n in 2"
:key="'index-' + n"
:loading="true"
></community-package-card>
</div>
<div
v-else-if="communityNodesStore.getInstalledPackages.length === 0"
:class="$style.actionBoxContainer"
>
<n8n-action-box
:heading="$locale.baseText('settings.communityNodes.empty.title')"
:description="getEmptyStateDescription"
:buttonText="
shouldShowInstallButton
? $locale.baseText('settings.communityNodes.empty.installPackageLabel')
: ''
"
:calloutText="actionBoxConfig.calloutText"
:calloutTheme="actionBoxConfig.calloutTheme"
@click="openInstallModal"
/>
</div>
<div
:class="$style.cardsContainer"
v-else
>
<community-package-card
v-for="communityPackage in communityNodesStore.getInstalledPackages"
:key="communityPackage.packageName"
:communityPackage="communityPackage"
></community-package-card>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -64,10 +62,8 @@ import {
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY, COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
COMMUNITY_NODES_INSTALLATION_DOCS_URL, COMMUNITY_NODES_INSTALLATION_DOCS_URL,
COMMUNITY_NODES_NPM_INSTALLATION_URL, COMMUNITY_NODES_NPM_INSTALLATION_URL,
} from '../constants'; } from '@/constants';
import { mapGetters } from 'vuex'; import CommunityPackageCard from '@/components/CommunityPackageCard.vue';
import SettingsView from './SettingsView.vue';
import CommunityPackageCard from '../components/CommunityPackageCard.vue';
import { showMessage } from '@/components/mixins/showMessage'; import { showMessage } from '@/components/mixins/showMessage';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { PublicInstalledPackage } from 'n8n-workflow'; import { PublicInstalledPackage } from 'n8n-workflow';
@ -84,7 +80,6 @@ export default mixins(
).extend({ ).extend({
name: 'SettingsCommunityNodesView', name: 'SettingsCommunityNodesView',
components: { components: {
SettingsView,
CommunityPackageCard, CommunityPackageCard,
}, },
data () { data () {

View file

@ -1,21 +1,17 @@
<template> <template>
<SettingsView> <feature-coming-soon :featureId="featureId" showTitle />
<FeatureComingSoon :featureId="featureId" showTitle />
</SettingsView>
</template> </template>
<script lang="ts"> <script lang="ts">
import { IFakeDoor } from '@/Interface'; import { IFakeDoor } from '@/Interface';
import Vue from 'vue'; import Vue from 'vue';
import SettingsView from './SettingsView.vue'; import FeatureComingSoon from '@/components/FeatureComingSoon.vue';
import FeatureComingSoon from '../components/FeatureComingSoon.vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
export default Vue.extend({ export default Vue.extend({
name: 'SettingsFakeDoorView', name: 'SettingsFakeDoorView',
components: { components: {
SettingsView,
FeatureComingSoon, FeatureComingSoon,
}, },
props: { props: {

View file

@ -1,45 +1,44 @@
<template> <template>
<SettingsView> <div :class="$style.container">
<div :class="$style.container"> <div :class="$style.header">
<div :class="$style.header"> <n8n-heading size="2xlarge">{{ $locale.baseText('settings.personal.personalSettings') }}</n8n-heading>
<n8n-heading size="2xlarge">{{ $locale.baseText('settings.personal.personalSettings') }}</n8n-heading> <div class="ph-no-capture" :class="$style.user">
<div class="ph-no-capture" :class="$style.user"> <span :class="$style.username">
<span :class="$style.username"> <n8n-text color="text-light">{{currentUser.fullName}}</n8n-text>
<n8n-text color="text-light">{{currentUser.fullName}}</n8n-text> </span>
</span> <n8n-avatar :firstName="currentUser.firstName" :lastName="currentUser.lastName" size="large" />
<n8n-avatar :firstName="currentUser.firstName" :lastName="currentUser.lastName" size="large" />
</div>
</div>
<div>
<div :class="$style.sectionHeader">
<n8n-heading size="large">{{ $locale.baseText('settings.personal.basicInformation') }}</n8n-heading>
</div>
<div>
<n8n-form-inputs
v-if="formInputs"
:inputs="formInputs"
:eventBus="formBus"
@input="onInput"
@ready="onReadyToSubmit"
@submit="onSubmit"
/>
</div>
</div>
<div>
<div :class="$style.sectionHeader">
<n8n-heading size="large">{{ $locale.baseText('settings.personal.security') }}</n8n-heading>
</div>
<div>
<n8n-input-label :label="$locale.baseText('auth.password')">
<n8n-link @click="openPasswordModal">{{ $locale.baseText('auth.changePassword') }}</n8n-link>
</n8n-input-label>
</div>
</div>
<div>
<n8n-button float="right" :label="$locale.baseText('settings.personal.save')" size="large" :disabled="!hasAnyChanges || !readyToSubmit" @click="onSaveClick" />
</div> </div>
</div> </div>
</SettingsView> <div>
<div :class="$style.sectionHeader">
<n8n-heading size="large">{{ $locale.baseText('settings.personal.basicInformation') }}</n8n-heading>
</div>
<div>
<n8n-form-inputs
v-if="formInputs"
:inputs="formInputs"
:eventBus="formBus"
@input="onInput"
@ready="onReadyToSubmit"
@submit="onSubmit"
/>
</div>
</div>
<div>
<div :class="$style.sectionHeader">
<n8n-heading size="large">{{ $locale.baseText('settings.personal.security') }}</n8n-heading>
</div>
<div>
<n8n-input-label :label="$locale.baseText('auth.password')">
<n8n-link @click="openPasswordModal">{{ $locale.baseText('auth.changePassword') }}</n8n-link>
</n8n-input-label>
</div>
</div>
<div>
<n8n-button float="right" :label="$locale.baseText('settings.personal.save')" size="large" :disabled="!hasAnyChanges || !readyToSubmit" @click="onSaveClick" />
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -52,15 +51,10 @@ import { mapStores } from 'pinia';
import Vue from 'vue'; import Vue from 'vue';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import SettingsView from './SettingsView.vue';
export default mixins( export default mixins(
showMessage, showMessage,
).extend({ ).extend({
name: 'SettingsPersonalView', name: 'SettingsPersonalView',
components: {
SettingsView,
},
data() { data() {
return { return {
hasAnyChanges: false, hasAnyChanges: false,

View file

@ -1,49 +1,46 @@
<template> <template>
<SettingsView> <div :class="$style.container">
<div :class="$style.container"> <div>
<div> <n8n-heading size="2xlarge">{{ $locale.baseText('settings.users') }}</n8n-heading>
<n8n-heading size="2xlarge">{{ $locale.baseText('settings.users') }}</n8n-heading> <div :class="$style.buttonContainer" v-if="!usersStore.showUMSetupWarning">
<div :class="$style.buttonContainer" v-if="!usersStore.showUMSetupWarning"> <n8n-tooltip :disabled="settingsStore.isSmtpSetup" placement="bottom">
<n8n-tooltip :disabled="settingsStore.isSmtpSetup" placement="bottom"> <i18n slot="content" path="settings.users.setupSMTPToInviteUsers" tag="span">
<i18n slot="content" path="settings.users.setupSMTPToInviteUsers" tag="span"> <template #action>
<template #action> <a
<a href="https://docs.n8n.io/reference/user-management.html#step-one-smtp"
href="https://docs.n8n.io/reference/user-management.html#step-one-smtp" target="_blank"
target="_blank" v-text="$locale.baseText('settings.users.setupSMTPToInviteUsers.instructions')"
v-text="$locale.baseText('settings.users.setupSMTPToInviteUsers.instructions')" />
/> </template>
</template> </i18n>
</i18n> <div>
<div> <n8n-button :label="$locale.baseText('settings.users.invite')" @click="onInvite" size="large" :disabled="!settingsStore.isSmtpSetup" />
<n8n-button :label="$locale.baseText('settings.users.invite')" @click="onInvite" size="large" :disabled="!settingsStore.isSmtpSetup" /> </div>
</div> </n8n-tooltip>
</n8n-tooltip>
</div>
</div>
<div v-if="usersStore.showUMSetupWarning" :class="$style.setupInfoContainer">
<n8n-action-box
:heading="$locale.baseText('settings.users.setupToInviteUsers')"
:buttonText="$locale.baseText('settings.users.setupMyAccount')"
:description="$locale.baseText('settings.users.setupToInviteUsersInfo')"
@click="redirectToSetup"
/>
</div>
<div :class="$style.usersContainer" v-else>
<PageAlert
v-if="!settingsStore.isSmtpSetup"
:message="$locale.baseText('settings.users.smtpToAddUsersWarning')"
:popupClass="$style.alert"
/>
<n8n-users-list :users="usersStore.allUsers" :currentUserId="usersStore.currentUserId" @delete="onDelete" @reinvite="onReinvite" />
</div> </div>
</div> </div>
</SettingsView> <div v-if="usersStore.showUMSetupWarning" :class="$style.setupInfoContainer">
<n8n-action-box
:heading="$locale.baseText('settings.users.setupToInviteUsers')"
:buttonText="$locale.baseText('settings.users.setupMyAccount')"
:description="$locale.baseText('settings.users.setupToInviteUsersInfo')"
@click="redirectToSetup"
/>
</div>
<div :class="$style.usersContainer" v-else>
<PageAlert
v-if="!settingsStore.isSmtpSetup"
:message="$locale.baseText('settings.users.smtpToAddUsersWarning')"
:popupClass="$style.alert"
/>
<n8n-users-list :users="usersStore.allUsers" :currentUserId="usersStore.currentUserId" @delete="onDelete" @reinvite="onReinvite" />
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { INVITE_USER_MODAL_KEY, VIEWS } from '@/constants'; import { INVITE_USER_MODAL_KEY, VIEWS } from '@/constants';
import SettingsView from './SettingsView.vue';
import PageAlert from '../components/PageAlert.vue'; import PageAlert from '../components/PageAlert.vue';
import { IUser } from '@/Interface'; import { IUser } from '@/Interface';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
@ -56,7 +53,6 @@ import { useUsersStore } from '@/stores/users';
export default mixins(showMessage).extend({ export default mixins(showMessage).extend({
name: 'SettingsUsersView', name: 'SettingsUsersView',
components: { components: {
SettingsView,
PageAlert, PageAlert,
}, },
async mounted() { async mounted() {

View file

@ -1,26 +1,48 @@
<template> <template>
<div :class="$style.container"> <div :class="$style.container">
<SettingsSidebar /> <SettingsSidebar @return="onReturn" />
<div :class="$style.contentContainer"> <div :class="$style.contentContainer">
<div :class="$style.content"> <div :class="$style.content">
<slot> <!--
</slot> Because we're using nested routes the props are going to be bind to the top level route
so we need to pass them down to the child component
-->
<router-view name="settingsView" v-bind="$attrs" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import { defineComponent } from 'vue';
import { Route } from 'vue-router';
import SettingsSidebar from '../components/SettingsSidebar.vue'; import { VIEWS } from '@/constants';
import SettingsSidebar from '@/components/SettingsSidebar.vue';
export default Vue.extend({ const SettingsView = defineComponent({
name: 'SettingsView', name: 'SettingsView',
components: { components: {
SettingsSidebar, SettingsSidebar,
}, },
beforeRouteEnter(to, from, next) {
next(vm => {
(vm as unknown as InstanceType<typeof SettingsView>).previousRoute = from;
});
},
data() {
return {
previousRoute: null as Route | null,
};
},
methods: {
onReturn() {
this.$router.push(this.previousRoute ? this.previousRoute.path : { name: VIEWS.HOMEPAGE });
},
},
}); });
export default SettingsView;
</script> </script>
<style lang="scss" module> <style lang="scss" module>