mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat(editor): Add move resources option to workflows and credentials on (#9654)
This commit is contained in:
parent
dda7901398
commit
bc35e8c33d
|
@ -1,3 +1,8 @@
|
|||
import { CredentialsModal, WorkflowPage } from '../pages';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
|
||||
export const getHomeButton = () => cy.getByTestId('project-home-menu-item');
|
||||
export const getMenuItems = () => cy.getByTestId('project-menu-item');
|
||||
export const getAddProjectButton = () => cy.getByTestId('add-project-menu-item');
|
||||
|
@ -11,8 +16,42 @@ export const getProjectSettingsCancelButton = () =>
|
|||
export const getProjectSettingsDeleteButton = () =>
|
||||
cy.getByTestId('project-settings-delete-button');
|
||||
export const getProjectMembersSelect = () => cy.getByTestId('project-members-select');
|
||||
|
||||
export const addProjectMember = (email: string) => {
|
||||
getProjectMembersSelect().click();
|
||||
getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click();
|
||||
};
|
||||
export const getProjectNameInput = () => cy.get('#projectName');
|
||||
export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal');
|
||||
export const getResourceMoveConfirmModal = () =>
|
||||
cy.getByTestId('project-move-resource-confirm-modal');
|
||||
export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select');
|
||||
|
||||
export function createProject(name: string) {
|
||||
getAddProjectButton().should('be.visible').click();
|
||||
|
||||
getProjectNameInput()
|
||||
.should('be.visible')
|
||||
.should('be.focused')
|
||||
.should('have.value', 'My project')
|
||||
.clear()
|
||||
.type(name);
|
||||
getProjectSettingsSaveButton().click();
|
||||
}
|
||||
|
||||
export function createWorkflow(fixtureKey: string, name: string) {
|
||||
workflowPage.getters.workflowImportInput().selectFile(`fixtures/${fixtureKey}`, { force: true });
|
||||
workflowPage.actions.setWorkflowName(name);
|
||||
workflowPage.getters.saveButton().should('contain', 'Saved');
|
||||
workflowPage.actions.zoomToFit();
|
||||
}
|
||||
|
||||
export function createCredential(name: string) {
|
||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
credentialsModal.getters.newCredentialTypeButton().click();
|
||||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||
credentialsModal.actions.setName(name);
|
||||
credentialsModal.actions.save();
|
||||
credentialsModal.actions.close();
|
||||
}
|
||||
|
|
|
@ -401,5 +401,152 @@ describe('Projects', () => {
|
|||
.first()
|
||||
.should('contain.text', 'Notion account personal project');
|
||||
});
|
||||
|
||||
it('should move resources between projects', () => {
|
||||
cy.signin(INSTANCE_OWNER);
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
// Create a workflow and a credential in the Home project
|
||||
workflowsPage.getters.workflowCards().should('not.have.length');
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project');
|
||||
|
||||
projects.getHomeButton().click();
|
||||
projects.getProjectTabCredentials().should('be.visible').click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Credential in Home project');
|
||||
|
||||
// Create a project and add a credential and a workflow to it
|
||||
projects.createProject('Project 1');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Credential in Project 1');
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1');
|
||||
|
||||
// Create another project and add a credential and a workflow to it
|
||||
projects.createProject('Project 2');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Credential in Project 2');
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2');
|
||||
|
||||
// Move the workflow owned by me from Home to Project 1
|
||||
projects.getHomeButton().click();
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.should('exist');
|
||||
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
|
||||
workflowsPage.getters.workflowMoveButton().click();
|
||||
|
||||
projects
|
||||
.getResourceMoveModal()
|
||||
.should('be.visible')
|
||||
.find('button:contains("Next")')
|
||||
.should('be.disabled');
|
||||
projects.getProjectMoveSelect().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 2)
|
||||
.first()
|
||||
.should('contain.text', 'Project 1')
|
||||
.click();
|
||||
projects.getResourceMoveModal().find('button:contains("Next")').click();
|
||||
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.should('be.visible')
|
||||
.find('button:contains("Confirm")')
|
||||
.should('be.disabled');
|
||||
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.find('input[type="checkbox"]')
|
||||
.first()
|
||||
.parents('label')
|
||||
.click();
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.find('button:contains("Confirm")')
|
||||
.should('be.disabled');
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.find('input[type="checkbox"]')
|
||||
.last()
|
||||
.parents('label')
|
||||
.click();
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.find('button:contains("Confirm")')
|
||||
.should('not.be.disabled')
|
||||
.click();
|
||||
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.should('not.exist');
|
||||
|
||||
// Move the credential from Project 1 to Project 2
|
||||
projects.getMenuItems().first().click();
|
||||
workflowsPage.getters.workflowCards().should('have.length', 2);
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.credentialCards().should('have.length', 1);
|
||||
credentialsPage.getters.credentialCardActions('Credential in Project 1').click();
|
||||
credentialsPage.getters.credentialMoveButton().click();
|
||||
|
||||
projects
|
||||
.getResourceMoveModal()
|
||||
.should('be.visible')
|
||||
.find('button:contains("Next")')
|
||||
.should('be.disabled');
|
||||
projects.getProjectMoveSelect().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('contain.text', 'Project 2')
|
||||
.click();
|
||||
projects.getResourceMoveModal().find('button:contains("Next")').click();
|
||||
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.should('be.visible')
|
||||
.find('button:contains("Confirm")')
|
||||
.should('be.disabled');
|
||||
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.find('input[type="checkbox"]')
|
||||
.first()
|
||||
.parents('label')
|
||||
.click();
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.find('button:contains("Confirm")')
|
||||
.should('be.disabled');
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.find('input[type="checkbox"]')
|
||||
.last()
|
||||
.parents('label')
|
||||
.click();
|
||||
projects
|
||||
.getResourceMoveConfirmModal()
|
||||
.find('button:contains("Confirm")')
|
||||
.should('not.be.disabled')
|
||||
.click();
|
||||
credentialsPage.getters.credentialCards().should('not.have.length');
|
||||
projects.getMenuItems().last().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.credentialCards().should('have.length', 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,8 @@ export class CredentialsPage extends BasePage {
|
|||
this.getters.credentialCard(credentialName).findChildByTestId('credential-card-actions'),
|
||||
credentialDeleteButton: () =>
|
||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
||||
credentialMoveButton: () =>
|
||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Move'),
|
||||
sort: () => cy.getByTestId('resources-list-sort').first(),
|
||||
sortOption: (label: string) =>
|
||||
cy.getByTestId('resources-list-sort-item').contains(label).first(),
|
||||
|
|
|
@ -24,6 +24,8 @@ export class WorkflowsPage extends BasePage {
|
|||
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'),
|
||||
workflowDeleteButton: () =>
|
||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
||||
workflowMoveButton: () =>
|
||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Move'),
|
||||
workflowFilterButton: () => cy.getByTestId('resources-list-filters-trigger').filter(':visible'),
|
||||
workflowTagsDropdown: () => cy.getByTestId('tags-dropdown'),
|
||||
workflowTagItem: (tag: string) => cy.getByTestId('tag').contains(tag),
|
||||
|
|
|
@ -14,3 +14,13 @@ export async function setCredentialSharedWith(
|
|||
data as unknown as IDataObject,
|
||||
);
|
||||
}
|
||||
|
||||
export async function moveCredentialToProject(
|
||||
context: IRestApiContext,
|
||||
id: string,
|
||||
destinationProjectId: string,
|
||||
): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'PUT', `/credentials/${id}/transfer`, {
|
||||
destinationProjectId,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,3 +14,13 @@ export async function setWorkflowSharedWith(
|
|||
data as unknown as IDataObject,
|
||||
);
|
||||
}
|
||||
|
||||
export async function moveWorkflowToProject(
|
||||
context: IRestApiContext,
|
||||
id: string,
|
||||
destinationProjectId: string,
|
||||
): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'PUT', `/workflows/${id}/transfer`, {
|
||||
destinationProjectId,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,3 +1,132 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import dateformat from 'dateformat';
|
||||
import type { ICredentialsResponse } from '@/Interface';
|
||||
import { MODAL_CONFIRM, PROJECT_MOVE_RESOURCE_MODAL } from '@/constants';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import CredentialIcon from '@/components/CredentialIcon.vue';
|
||||
import { getCredentialPermissions } from '@/permissions';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import TimeAgo from '@/components/TimeAgo.vue';
|
||||
import type { ProjectSharingData } from '@/types/projects.types';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const CREDENTIAL_LIST_ITEM_ACTIONS = {
|
||||
OPEN: 'open',
|
||||
DELETE: 'delete',
|
||||
MOVE: 'move',
|
||||
};
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: ICredentialsResponse;
|
||||
readOnly: boolean;
|
||||
}>(),
|
||||
{
|
||||
data: () => ({
|
||||
id: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
type: '',
|
||||
name: '',
|
||||
sharedWithProjects: [],
|
||||
homeProject: {} as ProjectSharingData,
|
||||
}),
|
||||
readOnly: false,
|
||||
},
|
||||
);
|
||||
|
||||
const locale = useI18n();
|
||||
const message = useMessage();
|
||||
const uiStore = useUIStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const credentialType = computed(() => credentialsStore.getCredentialTypeByName(props.data.type));
|
||||
const credentialPermissions = computed(() => getCredentialPermissions(props.data));
|
||||
const actions = computed(() => {
|
||||
const items = [
|
||||
{
|
||||
label: locale.baseText('credentials.item.open'),
|
||||
value: CREDENTIAL_LIST_ITEM_ACTIONS.OPEN,
|
||||
},
|
||||
];
|
||||
|
||||
if (credentialPermissions.value.delete) {
|
||||
items.push({
|
||||
label: locale.baseText('credentials.item.delete'),
|
||||
value: CREDENTIAL_LIST_ITEM_ACTIONS.DELETE,
|
||||
});
|
||||
}
|
||||
|
||||
if (credentialPermissions.value.move) {
|
||||
items.push({
|
||||
label: locale.baseText('credentials.item.move'),
|
||||
value: CREDENTIAL_LIST_ITEM_ACTIONS.MOVE,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
const formattedCreatedAtDate = computed(() => {
|
||||
const currentYear = new Date().getFullYear().toString();
|
||||
|
||||
return dateformat(
|
||||
props.data.createdAt,
|
||||
`d mmmm${props.data.createdAt.startsWith(currentYear) ? '' : ', yyyy'}`,
|
||||
);
|
||||
});
|
||||
|
||||
function onClick() {
|
||||
uiStore.openExistingCredential(props.data.id);
|
||||
}
|
||||
|
||||
async function onAction(action: string) {
|
||||
switch (action) {
|
||||
case CREDENTIAL_LIST_ITEM_ACTIONS.OPEN:
|
||||
onClick();
|
||||
break;
|
||||
case CREDENTIAL_LIST_ITEM_ACTIONS.DELETE:
|
||||
await deleteResource();
|
||||
break;
|
||||
case CREDENTIAL_LIST_ITEM_ACTIONS.MOVE:
|
||||
moveResource();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteResource() {
|
||||
const deleteConfirmed = await message.confirm(
|
||||
locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.message', {
|
||||
interpolate: { savedCredentialName: props.data.name },
|
||||
}),
|
||||
locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.headline'),
|
||||
{
|
||||
confirmButtonText: locale.baseText(
|
||||
'credentialEdit.credentialEdit.confirmMessage.deleteCredential.confirmButtonText',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
if (deleteConfirmed === MODAL_CONFIRM) {
|
||||
await credentialsStore.deleteCredential({ id: props.data.id });
|
||||
}
|
||||
}
|
||||
|
||||
function moveResource() {
|
||||
uiStore.openModalWithData({
|
||||
name: PROJECT_MOVE_RESOURCE_MODAL,
|
||||
data: {
|
||||
resource: props.data,
|
||||
resourceType: locale.baseText('generic.credential').toLocaleLowerCase(),
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n8n-card :class="$style.cardLink" @click="onClick">
|
||||
<template #prepend>
|
||||
|
@ -20,151 +149,19 @@
|
|||
</n8n-text>
|
||||
</div>
|
||||
<template #append>
|
||||
<ProjectCardBadge :resource="data" :personal-project="projectsStore.personalProject" />
|
||||
<div ref="cardActions" :class="$style.cardActions">
|
||||
<n8n-action-toggle :actions="actions" theme="dark" @action="onAction" @click.stop />
|
||||
<div :class="$style.cardActions" @click.stop>
|
||||
<ProjectCardBadge :resource="data" :personal-project="projectsStore.personalProject" />
|
||||
<n8n-action-toggle
|
||||
data-test-id="credential-card-actions"
|
||||
:actions="actions"
|
||||
theme="dark"
|
||||
@action="onAction"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</n8n-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import type { ICredentialsResponse, IUser } from '@/Interface';
|
||||
import type { ICredentialType } from 'n8n-workflow';
|
||||
import { MODAL_CONFIRM } from '@/constants';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import CredentialIcon from '@/components/CredentialIcon.vue';
|
||||
import type { PermissionsMap } from '@/permissions';
|
||||
import { getCredentialPermissions } from '@/permissions';
|
||||
import dateformat from 'dateformat';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import TimeAgo from '@/components/TimeAgo.vue';
|
||||
import type { ProjectSharingData } from '@/types/projects.types';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import type { CredentialScope } from '@n8n/permissions';
|
||||
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
|
||||
|
||||
export const CREDENTIAL_LIST_ITEM_ACTIONS = {
|
||||
OPEN: 'open',
|
||||
DELETE: 'delete',
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
TimeAgo,
|
||||
CredentialIcon,
|
||||
ProjectCardBadge,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<ICredentialsResponse>,
|
||||
required: true,
|
||||
default: (): ICredentialsResponse => ({
|
||||
id: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
type: '',
|
||||
name: '',
|
||||
sharedWithProjects: [],
|
||||
homeProject: {} as ProjectSharingData,
|
||||
}),
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
...useMessage(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useCredentialsStore, useUIStore, useUsersStore, useProjectsStore),
|
||||
currentUser(): IUser | null {
|
||||
return this.usersStore.currentUser;
|
||||
},
|
||||
credentialType(): ICredentialType | undefined {
|
||||
return this.credentialsStore.getCredentialTypeByName(this.data.type);
|
||||
},
|
||||
credentialPermissions(): PermissionsMap<CredentialScope> | null {
|
||||
return !this.currentUser ? null : getCredentialPermissions(this.data);
|
||||
},
|
||||
actions(): Array<{ label: string; value: string }> {
|
||||
if (!this.credentialPermissions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: this.$locale.baseText('credentials.item.open'),
|
||||
value: CREDENTIAL_LIST_ITEM_ACTIONS.OPEN,
|
||||
},
|
||||
].concat(
|
||||
this.credentialPermissions.delete
|
||||
? [
|
||||
{
|
||||
label: this.$locale.baseText('credentials.item.delete'),
|
||||
value: CREDENTIAL_LIST_ITEM_ACTIONS.DELETE,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
},
|
||||
formattedCreatedAtDate(): string {
|
||||
const currentYear = new Date().getFullYear().toString();
|
||||
|
||||
return dateformat(
|
||||
this.data.createdAt,
|
||||
`d mmmm${this.data.createdAt.startsWith(currentYear) ? '' : ', yyyy'}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onClick(event: Event) {
|
||||
const cardActionsEl = this.$refs.cardActions as HTMLDivElement | undefined;
|
||||
const clickTarget = event.target as HTMLElement | null;
|
||||
if (cardActionsEl === clickTarget || (clickTarget && cardActionsEl?.contains(clickTarget))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiStore.openExistingCredential(this.data.id);
|
||||
},
|
||||
async onAction(action: string) {
|
||||
if (action === CREDENTIAL_LIST_ITEM_ACTIONS.OPEN) {
|
||||
await this.onClick(new Event('click'));
|
||||
} else if (action === CREDENTIAL_LIST_ITEM_ACTIONS.DELETE) {
|
||||
const deleteConfirmed = await this.confirm(
|
||||
this.$locale.baseText(
|
||||
'credentialEdit.credentialEdit.confirmMessage.deleteCredential.message',
|
||||
{
|
||||
interpolate: { savedCredentialName: this.data.name },
|
||||
},
|
||||
),
|
||||
this.$locale.baseText(
|
||||
'credentialEdit.credentialEdit.confirmMessage.deleteCredential.headline',
|
||||
),
|
||||
{
|
||||
confirmButtonText: this.$locale.baseText(
|
||||
'credentialEdit.credentialEdit.confirmMessage.deleteCredential.confirmButtonText',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
if (deleteConfirmed === MODAL_CONFIRM) {
|
||||
await this.credentialsStore.deleteCredential({ id: this.data.id });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.cardLink {
|
||||
transition: box-shadow 0.3s ease;
|
||||
|
|
|
@ -1,3 +1,74 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
ABOUT_MODAL_KEY,
|
||||
CHAT_EMBED_MODAL_KEY,
|
||||
CHANGE_PASSWORD_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
|
||||
CONTACT_PROMPT_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
DELETE_USER_MODAL_KEY,
|
||||
DUPLICATE_MODAL_KEY,
|
||||
INVITE_USER_MODAL_KEY,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
PERSONALIZATION_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
NPS_SURVEY_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
WORKFLOW_SHARE_MODAL_KEY,
|
||||
IMPORT_CURL_MODAL_KEY,
|
||||
LOG_STREAM_MODAL_KEY,
|
||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||
SOURCE_CONTROL_PULL_MODAL_KEY,
|
||||
EXTERNAL_SECRETS_PROVIDER_MODAL_KEY,
|
||||
DEBUG_PAYWALL_MODAL_KEY,
|
||||
MFA_SETUP_MODAL_KEY,
|
||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||
SETUP_CREDENTIALS_MODAL_KEY,
|
||||
GENERATE_CURL_MODAL_KEY,
|
||||
PROJECT_MOVE_RESOURCE_MODAL,
|
||||
PROJECT_MOVE_RESOURCE_CONFIRM_MODAL,
|
||||
} from '@/constants';
|
||||
|
||||
import AboutModal from '@/components/AboutModal.vue';
|
||||
import ChatEmbedModal from '@/components/ChatEmbedModal.vue';
|
||||
import CommunityPackageManageConfirmModal from '@/components/CommunityPackageManageConfirmModal.vue';
|
||||
import CommunityPackageInstallModal from '@/components/CommunityPackageInstallModal.vue';
|
||||
import ChangePasswordModal from '@/components/ChangePasswordModal.vue';
|
||||
import ContactPromptModal from '@/components/ContactPromptModal.vue';
|
||||
import CredentialEdit from '@/components/CredentialEdit/CredentialEdit.vue';
|
||||
import InviteUsersModal from '@/components/InviteUsersModal.vue';
|
||||
import CredentialsSelectModal from '@/components/CredentialsSelectModal.vue';
|
||||
import DuplicateWorkflowDialog from '@/components/DuplicateWorkflowDialog.vue';
|
||||
import ModalRoot from '@/components/ModalRoot.vue';
|
||||
import OnboardingCallSignupModal from '@/components/OnboardingCallSignupModal.vue';
|
||||
import PersonalizationModal from '@/components/PersonalizationModal.vue';
|
||||
import TagsManager from '@/components/TagsManager/TagsManager.vue';
|
||||
import UpdatesPanel from '@/components/UpdatesPanel.vue';
|
||||
import NpsSurvey from '@/components/NpsSurvey.vue';
|
||||
import WorkflowLMChat from '@/components/WorkflowLMChat.vue';
|
||||
import WorkflowSettings from '@/components/WorkflowSettings.vue';
|
||||
import DeleteUserModal from '@/components/DeleteUserModal.vue';
|
||||
import ActivationModal from '@/components/ActivationModal.vue';
|
||||
import ImportCurlModal from '@/components/ImportCurlModal.vue';
|
||||
import GenerateCurlModal from '@/components/GenerateCurlModal.vue';
|
||||
import MfaSetupModal from '@/components/MfaSetupModal.vue';
|
||||
import WorkflowShareModal from '@/components/WorkflowShareModal.ee.vue';
|
||||
import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue';
|
||||
import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue';
|
||||
import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue';
|
||||
import ExternalSecretsProviderModal from '@/components/ExternalSecretsProviderModal.ee.vue';
|
||||
import DebugPaywallModal from '@/components/DebugPaywallModal.vue';
|
||||
import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue';
|
||||
import SetupWorkflowCredentialsModal from '@/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue';
|
||||
import ProjectMoveResourceModal from '@/components/Projects/ProjectMoveResourceModal.vue';
|
||||
import ProjectMoveResourceConfirmModal from '@/components/Projects/ProjectMoveResourceConfirmModal.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ModalRoot :name="CONTACT_PROMPT_MODAL_KEY">
|
||||
|
@ -167,142 +238,23 @@
|
|||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
<ModalRoot :name="PROJECT_MOVE_RESOURCE_MODAL">
|
||||
<template #default="{ modalName, data }">
|
||||
<ProjectMoveResourceModal
|
||||
data-test-id="project-move-resource-modal"
|
||||
:modal-name="modalName"
|
||||
:data="data"
|
||||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
<ModalRoot :name="PROJECT_MOVE_RESOURCE_CONFIRM_MODAL">
|
||||
<template #default="{ modalName, data }">
|
||||
<ProjectMoveResourceConfirmModal
|
||||
data-test-id="project-move-resource-confirm-modal"
|
||||
:modal-name="modalName"
|
||||
:data="data"
|
||||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
ABOUT_MODAL_KEY,
|
||||
CHAT_EMBED_MODAL_KEY,
|
||||
CHANGE_PASSWORD_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
|
||||
CONTACT_PROMPT_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
DELETE_USER_MODAL_KEY,
|
||||
DUPLICATE_MODAL_KEY,
|
||||
INVITE_USER_MODAL_KEY,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
PERSONALIZATION_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
NPS_SURVEY_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
WORKFLOW_SHARE_MODAL_KEY,
|
||||
IMPORT_CURL_MODAL_KEY,
|
||||
LOG_STREAM_MODAL_KEY,
|
||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||
SOURCE_CONTROL_PULL_MODAL_KEY,
|
||||
EXTERNAL_SECRETS_PROVIDER_MODAL_KEY,
|
||||
DEBUG_PAYWALL_MODAL_KEY,
|
||||
MFA_SETUP_MODAL_KEY,
|
||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||
SETUP_CREDENTIALS_MODAL_KEY,
|
||||
GENERATE_CURL_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
import AboutModal from './AboutModal.vue';
|
||||
import ChatEmbedModal from './ChatEmbedModal.vue';
|
||||
import CommunityPackageManageConfirmModal from './CommunityPackageManageConfirmModal.vue';
|
||||
import CommunityPackageInstallModal from './CommunityPackageInstallModal.vue';
|
||||
import ChangePasswordModal from './ChangePasswordModal.vue';
|
||||
import ContactPromptModal from './ContactPromptModal.vue';
|
||||
import CredentialEdit from './CredentialEdit/CredentialEdit.vue';
|
||||
import InviteUsersModal from './InviteUsersModal.vue';
|
||||
import CredentialsSelectModal from './CredentialsSelectModal.vue';
|
||||
import DuplicateWorkflowDialog from './DuplicateWorkflowDialog.vue';
|
||||
import ModalRoot from './ModalRoot.vue';
|
||||
import OnboardingCallSignupModal from './OnboardingCallSignupModal.vue';
|
||||
import PersonalizationModal from './PersonalizationModal.vue';
|
||||
import TagsManager from './TagsManager/TagsManager.vue';
|
||||
import UpdatesPanel from './UpdatesPanel.vue';
|
||||
import NpsSurvey from './NpsSurvey.vue';
|
||||
import WorkflowLMChat from './WorkflowLMChat.vue';
|
||||
import WorkflowSettings from './WorkflowSettings.vue';
|
||||
import DeleteUserModal from './DeleteUserModal.vue';
|
||||
import ActivationModal from './ActivationModal.vue';
|
||||
import ImportCurlModal from './ImportCurlModal.vue';
|
||||
import GenerateCurlModal from './GenerateCurlModal.vue';
|
||||
import MfaSetupModal from './MfaSetupModal.vue';
|
||||
import WorkflowShareModal from './WorkflowShareModal.ee.vue';
|
||||
import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue';
|
||||
import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue';
|
||||
import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue';
|
||||
import ExternalSecretsProviderModal from '@/components/ExternalSecretsProviderModal.ee.vue';
|
||||
import DebugPaywallModal from '@/components/DebugPaywallModal.vue';
|
||||
import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue';
|
||||
import SetupWorkflowCredentialsModal from '@/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Modals',
|
||||
components: {
|
||||
AboutModal,
|
||||
ActivationModal,
|
||||
ChatEmbedModal,
|
||||
CommunityPackageInstallModal,
|
||||
CommunityPackageManageConfirmModal,
|
||||
ContactPromptModal,
|
||||
ChangePasswordModal,
|
||||
CredentialEdit,
|
||||
CredentialsSelectModal,
|
||||
DeleteUserModal,
|
||||
DuplicateWorkflowDialog,
|
||||
InviteUsersModal,
|
||||
ModalRoot,
|
||||
OnboardingCallSignupModal,
|
||||
PersonalizationModal,
|
||||
TagsManager,
|
||||
UpdatesPanel,
|
||||
NpsSurvey,
|
||||
WorkflowLMChat,
|
||||
WorkflowSettings,
|
||||
WorkflowShareModal,
|
||||
ImportCurlModal,
|
||||
GenerateCurlModal,
|
||||
EventDestinationSettingsModal,
|
||||
SourceControlPushModal,
|
||||
SourceControlPullModal,
|
||||
ExternalSecretsProviderModal,
|
||||
DebugPaywallModal,
|
||||
MfaSetupModal,
|
||||
WorkflowHistoryVersionRestoreModal,
|
||||
SetupWorkflowCredentialsModal,
|
||||
},
|
||||
data: () => ({
|
||||
CHAT_EMBED_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
|
||||
CONTACT_PROMPT_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
ABOUT_MODAL_KEY,
|
||||
CHANGE_PASSWORD_MODAL_KEY,
|
||||
DELETE_USER_MODAL_KEY,
|
||||
DUPLICATE_MODAL_KEY,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
PERSONALIZATION_MODAL_KEY,
|
||||
INVITE_USER_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
WORKFLOW_SHARE_MODAL_KEY,
|
||||
NPS_SURVEY_MODAL_KEY,
|
||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||
IMPORT_CURL_MODAL_KEY,
|
||||
GENERATE_CURL_MODAL_KEY,
|
||||
LOG_STREAM_MODAL_KEY,
|
||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||
SOURCE_CONTROL_PULL_MODAL_KEY,
|
||||
EXTERNAL_SECRETS_PROVIDER_MODAL_KEY,
|
||||
DEBUG_PAYWALL_MODAL_KEY,
|
||||
MFA_SETUP_MODAL_KEY,
|
||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||
SETUP_CREDENTIALS_MODAL_KEY,
|
||||
}),
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import type { ICredentialsResponse, IWorkflowDb } from '@/Interface';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import { N8nCheckbox, N8nText } from 'n8n-design-system';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
||||
const props = defineProps<{
|
||||
modalName: string;
|
||||
data: {
|
||||
resource: IWorkflowDb | ICredentialsResponse;
|
||||
resourceType: 'workflow' | 'credential';
|
||||
projectId: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
const uiStore = useUIStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const checks = ref([false, false]);
|
||||
const allChecked = computed(() => checks.value.every(Boolean));
|
||||
|
||||
const moveResourceLabel = computed(() =>
|
||||
props.data.resourceType === 'workflow'
|
||||
? i18n.baseText('projects.move.workflow.confirm.modal.label')
|
||||
: i18n.baseText('projects.move.credential.confirm.modal.label'),
|
||||
);
|
||||
|
||||
const closeModal = () => {
|
||||
uiStore.closeModal(props.modalName);
|
||||
};
|
||||
|
||||
const confirm = async () => {
|
||||
try {
|
||||
await projectsStore.moveResourceToProject(
|
||||
props.data.resourceType,
|
||||
props.data.resource.id,
|
||||
props.data.projectId,
|
||||
);
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
toast.showError(
|
||||
error.message,
|
||||
i18n.baseText('projects.move.resource.error.title', {
|
||||
interpolate: {
|
||||
resourceType: props.data.resourceType,
|
||||
resourceName: props.data.resource.name,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Modal width="500px" :name="props.modalName" data-test-id="project-move-resource-confirm-modal">
|
||||
<template #header>
|
||||
<N8nHeading tag="h2" size="xlarge" class="mb-m">
|
||||
{{ i18n.baseText('projects.move.resource.confirm.modal.title') }}
|
||||
</N8nHeading>
|
||||
</template>
|
||||
<template #content>
|
||||
<N8nCheckbox v-model="checks[0]" :label="moveResourceLabel" />
|
||||
<N8nCheckbox v-model="checks[1]">
|
||||
<N8nText>
|
||||
<i18n-t keypath="projects.move.resource.confirm.modal.label">
|
||||
<template #resourceType>{{ props.data.resourceType }}</template>
|
||||
<template #numberOfUsers>{{
|
||||
i18n.baseText('projects.move.resource.confirm.modal.numberOfUsers', {
|
||||
interpolate: {
|
||||
numberOfUsers: props.data.resource.sharedWithProjects?.length ?? 0,
|
||||
},
|
||||
adjustToNumber: props.data.resource.sharedWithProjects?.length,
|
||||
})
|
||||
}}</template>
|
||||
</i18n-t>
|
||||
</N8nText>
|
||||
</N8nCheckbox>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div :class="$style.buttons">
|
||||
<N8nButton type="secondary" text class="mr-2xs" @click="closeModal">
|
||||
{{ i18n.baseText('generic.cancel') }}
|
||||
</N8nButton>
|
||||
<N8nButton :disabled="!allChecked" type="primary" @click="confirm">
|
||||
{{ i18n.baseText('projects.move.resource.confirm.modal.button.confirm') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,108 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import type { ICredentialsResponse, IWorkflowDb } from '@/Interface';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import { PROJECT_MOVE_RESOURCE_CONFIRM_MODAL } from '@/constants';
|
||||
import { splitName } from '@/utils/projects.utils';
|
||||
|
||||
const props = defineProps<{
|
||||
modalName: string;
|
||||
data: {
|
||||
resource: IWorkflowDb | ICredentialsResponse;
|
||||
resourceType: 'workflow' | 'credential';
|
||||
};
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const uiStore = useUIStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const projectId = ref<string | null>(null);
|
||||
const processedName = computed(() => {
|
||||
const { firstName, lastName, email } = splitName(props.data.resource.homeProject?.name ?? '');
|
||||
return !firstName ? email : `${firstName}${lastName ? ' ' + lastName : ''}`;
|
||||
});
|
||||
const availableProjects = computed(() => {
|
||||
return projectsStore.teamProjects.filter((p) => p.id !== props.data.resource.homeProject?.id);
|
||||
});
|
||||
|
||||
const updateProject = (value: string) => {
|
||||
projectId.value = value;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
uiStore.closeModal(props.modalName);
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
closeModal();
|
||||
uiStore.openModalWithData({
|
||||
name: PROJECT_MOVE_RESOURCE_CONFIRM_MODAL,
|
||||
data: {
|
||||
resource: props.data.resource,
|
||||
resourceType: props.data.resourceType,
|
||||
projectId: projectId.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Modal width="500px" :name="props.modalName" data-test-id="project-move-resource-modal">
|
||||
<template #header>
|
||||
<N8nHeading tag="h2" size="xlarge" class="mb-m">
|
||||
{{
|
||||
i18n.baseText('projects.move.resource.modal.title', {
|
||||
interpolate: { resourceType: props.data.resourceType },
|
||||
})
|
||||
}}
|
||||
</N8nHeading>
|
||||
<N8nText>
|
||||
<i18n-t keypath="projects.move.resource.modal.message">
|
||||
<template #resourceName
|
||||
><strong>{{ props.data.resource.name }}</strong></template
|
||||
>
|
||||
<template #resourceHomeProjectName>{{ processedName }}</template>
|
||||
<template #resourceType>{{ props.data.resourceType }}</template>
|
||||
</i18n-t>
|
||||
</N8nText>
|
||||
</template>
|
||||
<template #content>
|
||||
<div>
|
||||
<N8nSelect
|
||||
class="mr-2xs"
|
||||
:model-value="projectId"
|
||||
size="small"
|
||||
data-test-id="project-move-resource-modal-select"
|
||||
@update:model-value="updateProject"
|
||||
>
|
||||
<N8nOption
|
||||
v-for="p in availableProjects"
|
||||
:key="p.id"
|
||||
:value="p.id"
|
||||
:label="p.name"
|
||||
></N8nOption>
|
||||
</N8nSelect>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div :class="$style.buttons">
|
||||
<N8nButton type="secondary" text class="mr-2xs" @click="closeModal">
|
||||
{{ i18n.baseText('generic.cancel') }}
|
||||
</N8nButton>
|
||||
<N8nButton :disabled="!projectId" type="primary" @click="next">
|
||||
{{ i18n.baseText('generic.next') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
|
@ -255,9 +255,9 @@ onBeforeMount(async () => {
|
|||
</div>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<fieldset>
|
||||
<label for="name">{{ locale.baseText('projects.settings.name') }}</label>
|
||||
<label for="projectName">{{ locale.baseText('projects.settings.name') }}</label>
|
||||
<N8nInput
|
||||
id="name"
|
||||
id="projectName"
|
||||
ref="nameInput"
|
||||
v-model="formData.name"
|
||||
type="text"
|
||||
|
|
|
@ -10,20 +10,23 @@ import { useSettingsStore } from '@/stores/settings.store';
|
|||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const $router = {
|
||||
push: vi.fn(),
|
||||
resolve: vi.fn().mockImplementation(() => ({ href: '' })),
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowCard, {
|
||||
global: {
|
||||
mocks: {
|
||||
$router,
|
||||
},
|
||||
},
|
||||
vi.mock('vue-router', () => {
|
||||
const push = vi.fn();
|
||||
const resolve = vi.fn().mockReturnValue({ href: '' });
|
||||
return {
|
||||
useRouter: () => ({
|
||||
push,
|
||||
resolve,
|
||||
}),
|
||||
useRoute: () => ({}),
|
||||
RouterLink: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowCard);
|
||||
|
||||
const createWorkflow = (overrides = {}): IWorkflowDb => ({
|
||||
id: '1',
|
||||
name: 'My Workflow',
|
||||
|
@ -43,6 +46,7 @@ describe('WorkflowCard', () => {
|
|||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
let usersStore: ReturnType<typeof useUsersStore>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
let router: ReturnType<typeof useRouter>;
|
||||
|
||||
beforeEach(async () => {
|
||||
pinia = createPinia();
|
||||
|
@ -51,6 +55,7 @@ describe('WorkflowCard', () => {
|
|||
settingsStore = useSettingsStore();
|
||||
usersStore = useUsersStore();
|
||||
workflowsStore = useWorkflowsStore();
|
||||
router = useRouter();
|
||||
windowOpenSpy = vi.spyOn(window, 'open');
|
||||
});
|
||||
|
||||
|
@ -67,7 +72,7 @@ describe('WorkflowCard', () => {
|
|||
|
||||
await userEvent.click(cardTitle);
|
||||
await waitFor(() => {
|
||||
expect($router.push).toHaveBeenCalledWith({
|
||||
expect(router.push).toHaveBeenCalledWith({
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: data.id },
|
||||
});
|
||||
|
@ -79,7 +84,7 @@ describe('WorkflowCard', () => {
|
|||
await user.keyboard('[ControlLeft>]');
|
||||
await user.click(cardTitle);
|
||||
await waitFor(() => {
|
||||
expect($router.push).toHaveBeenCalledTimes(1);
|
||||
expect(router.push).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(windowOpenSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -98,7 +103,7 @@ describe('WorkflowCard', () => {
|
|||
|
||||
await userEvent.click(cardActions);
|
||||
await waitFor(() => {
|
||||
expect($router.push).not.toHaveBeenCalled();
|
||||
expect(router.push).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const actions = document.querySelector(`#${controllingId}`);
|
||||
|
@ -107,7 +112,7 @@ describe('WorkflowCard', () => {
|
|||
});
|
||||
await userEvent.click(actions!.querySelectorAll('li')[0]);
|
||||
await waitFor(() => {
|
||||
expect($router.push).toHaveBeenCalledWith({
|
||||
expect(router.push).toHaveBeenCalledWith({
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: data.id },
|
||||
});
|
|
@ -1,3 +1,233 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { IWorkflowDb, IUser } from '@/Interface';
|
||||
import {
|
||||
DUPLICATE_MODAL_KEY,
|
||||
MODAL_CONFIRM,
|
||||
PROJECT_MOVE_RESOURCE_MODAL,
|
||||
VIEWS,
|
||||
WORKFLOW_SHARE_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { getWorkflowPermissions } from '@/permissions';
|
||||
import dateformat from 'dateformat';
|
||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import TimeAgo from '@/components/TimeAgo.vue';
|
||||
import type { ProjectSharingData } from '@/types/projects.types';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
|
||||
const WORKFLOW_LIST_ITEM_ACTIONS = {
|
||||
OPEN: 'open',
|
||||
SHARE: 'share',
|
||||
DUPLICATE: 'duplicate',
|
||||
DELETE: 'delete',
|
||||
MOVE: 'move',
|
||||
};
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: IWorkflowDb;
|
||||
readOnly: boolean;
|
||||
}>(),
|
||||
{
|
||||
data: () => ({
|
||||
id: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
active: false,
|
||||
connections: {},
|
||||
nodes: [],
|
||||
name: '',
|
||||
sharedWithProjects: [],
|
||||
homeProject: {} as ProjectSharingData,
|
||||
versionId: '',
|
||||
}),
|
||||
readOnly: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'expand:tags'): void;
|
||||
(event: 'click:tag', tagId: string, e: PointerEvent): void;
|
||||
}>();
|
||||
|
||||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
const locale = useI18n();
|
||||
const router = useRouter();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
|
||||
const workflowPermissions = computed(() => getWorkflowPermissions(props.data));
|
||||
const actions = computed(() => {
|
||||
const items = [
|
||||
{
|
||||
label: locale.baseText('workflows.item.open'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.OPEN,
|
||||
},
|
||||
{
|
||||
label: locale.baseText('workflows.item.share'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.SHARE,
|
||||
},
|
||||
];
|
||||
|
||||
if (!props.readOnly) {
|
||||
items.push({
|
||||
label: locale.baseText('workflows.item.duplicate'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE,
|
||||
});
|
||||
}
|
||||
|
||||
if (workflowPermissions.value.move) {
|
||||
items.push({
|
||||
label: locale.baseText('workflows.item.move'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.MOVE,
|
||||
});
|
||||
}
|
||||
|
||||
if (workflowPermissions.value.delete && !props.readOnly) {
|
||||
items.push({
|
||||
label: locale.baseText('workflows.item.delete'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.DELETE,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
const formattedCreatedAtDate = computed(() => {
|
||||
const currentYear = new Date().getFullYear().toString();
|
||||
|
||||
return dateformat(
|
||||
props.data.createdAt,
|
||||
`d mmmm${String(props.data.createdAt).startsWith(currentYear) ? '' : ', yyyy'}`,
|
||||
);
|
||||
});
|
||||
|
||||
async function onClick(event?: KeyboardEvent | PointerEvent) {
|
||||
if (event?.ctrlKey || event?.metaKey) {
|
||||
const route = router.resolve({
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: props.data.id },
|
||||
});
|
||||
window.open(route.href, '_blank');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push({
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: props.data.id },
|
||||
});
|
||||
}
|
||||
|
||||
function onClickTag(tagId: string, event: PointerEvent) {
|
||||
event.stopPropagation();
|
||||
emit('click:tag', tagId, event);
|
||||
}
|
||||
|
||||
function onExpandTags() {
|
||||
emit('expand:tags');
|
||||
}
|
||||
|
||||
async function onAction(action: string) {
|
||||
switch (action) {
|
||||
case WORKFLOW_LIST_ITEM_ACTIONS.OPEN:
|
||||
await onClick();
|
||||
break;
|
||||
case WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE:
|
||||
uiStore.openModalWithData({
|
||||
name: DUPLICATE_MODAL_KEY,
|
||||
data: {
|
||||
id: props.data.id,
|
||||
name: props.data.name,
|
||||
tags: (props.data.tags ?? []).map((tag) =>
|
||||
typeof tag !== 'string' && 'id' in tag ? tag.id : tag,
|
||||
),
|
||||
},
|
||||
});
|
||||
break;
|
||||
case WORKFLOW_LIST_ITEM_ACTIONS.SHARE:
|
||||
uiStore.openModalWithData({
|
||||
name: WORKFLOW_SHARE_MODAL_KEY,
|
||||
data: { id: props.data.id },
|
||||
});
|
||||
|
||||
telemetry.track('User opened sharing modal', {
|
||||
workflow_id: props.data.id,
|
||||
user_id_sharer: currentUser.value.id,
|
||||
sub_view: 'Workflows listing',
|
||||
});
|
||||
break;
|
||||
case WORKFLOW_LIST_ITEM_ACTIONS.DELETE:
|
||||
await deleteWorkflow();
|
||||
break;
|
||||
case WORKFLOW_LIST_ITEM_ACTIONS.MOVE:
|
||||
moveResource();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteWorkflow() {
|
||||
const deleteConfirmed = await message.confirm(
|
||||
locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {
|
||||
interpolate: { workflowName: props.data.name },
|
||||
}),
|
||||
locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowDelete.confirmButtonText',
|
||||
),
|
||||
cancelButtonText: locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowDelete.cancelButtonText',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
if (deleteConfirmed !== MODAL_CONFIRM) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await workflowsStore.deleteWorkflow(props.data.id);
|
||||
} catch (error) {
|
||||
toast.showError(error, locale.baseText('generic.deleteWorkflowError'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset tab title since workflow is deleted.
|
||||
toast.showMessage({
|
||||
title: locale.baseText('mainSidebar.showMessage.handleSelect1.title'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
function moveResource() {
|
||||
uiStore.openModalWithData({
|
||||
name: PROJECT_MOVE_RESOURCE_MODAL,
|
||||
data: {
|
||||
resource: props.data,
|
||||
resourceType: locale.baseText('generic.workflow').toLocaleLowerCase(),
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n8n-card :class="$style.cardLink" @click="onClick">
|
||||
<template #header>
|
||||
|
@ -50,203 +280,6 @@
|
|||
</n8n-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import type { IWorkflowDb, IUser } from '@/Interface';
|
||||
import { DUPLICATE_MODAL_KEY, MODAL_CONFIRM, VIEWS, WORKFLOW_SHARE_MODAL_KEY } from '@/constants';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import type { PermissionsMap } from '@/permissions';
|
||||
import type { WorkflowScope } from '@n8n/permissions';
|
||||
import { getWorkflowPermissions } from '@/permissions';
|
||||
import dateformat from 'dateformat';
|
||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import TimeAgo from '@/components/TimeAgo.vue';
|
||||
import type { ProjectSharingData } from '@/types/projects.types';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
|
||||
|
||||
export const WORKFLOW_LIST_ITEM_ACTIONS = {
|
||||
OPEN: 'open',
|
||||
SHARE: 'share',
|
||||
DUPLICATE: 'duplicate',
|
||||
DELETE: 'delete',
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
TimeAgo,
|
||||
WorkflowActivator,
|
||||
ProjectCardBadge,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<IWorkflowDb>,
|
||||
required: true,
|
||||
default: (): IWorkflowDb => ({
|
||||
id: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
active: false,
|
||||
connections: {},
|
||||
nodes: [],
|
||||
name: '',
|
||||
sharedWithProjects: [],
|
||||
homeProject: {} as ProjectSharingData,
|
||||
versionId: '',
|
||||
}),
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
...useToast(),
|
||||
...useMessage(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useSettingsStore, useUIStore, useUsersStore, useWorkflowsStore, useProjectsStore),
|
||||
currentUser(): IUser {
|
||||
return this.usersStore.currentUser || ({} as IUser);
|
||||
},
|
||||
workflowPermissions(): PermissionsMap<WorkflowScope> {
|
||||
return getWorkflowPermissions(this.data);
|
||||
},
|
||||
actions(): Array<{ label: string; value: string }> {
|
||||
const actions = [
|
||||
{
|
||||
label: this.$locale.baseText('workflows.item.open'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.OPEN,
|
||||
},
|
||||
{
|
||||
label: this.$locale.baseText('workflows.item.share'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.SHARE,
|
||||
},
|
||||
];
|
||||
|
||||
if (!this.readOnly) {
|
||||
actions.push({
|
||||
label: this.$locale.baseText('workflows.item.duplicate'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.workflowPermissions.delete && !this.readOnly) {
|
||||
actions.push({
|
||||
label: this.$locale.baseText('workflows.item.delete'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.DELETE,
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
},
|
||||
formattedCreatedAtDate(): string {
|
||||
const currentYear = new Date().getFullYear().toString();
|
||||
|
||||
return dateformat(
|
||||
this.data.createdAt,
|
||||
`d mmmm${String(this.data.createdAt).startsWith(currentYear) ? '' : ', yyyy'}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onClick(event?: KeyboardEvent | PointerEvent) {
|
||||
if (event?.ctrlKey || event?.metaKey) {
|
||||
const route = this.$router.resolve({
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: this.data.id },
|
||||
});
|
||||
window.open(route.href, '_blank');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$router.push({
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: this.data.id },
|
||||
});
|
||||
},
|
||||
onClickTag(tagId: string, event: PointerEvent) {
|
||||
event.stopPropagation();
|
||||
|
||||
this.$emit('click:tag', tagId, event);
|
||||
},
|
||||
onExpandTags() {
|
||||
this.$emit('expand:tags');
|
||||
},
|
||||
async onAction(action: string) {
|
||||
if (action === WORKFLOW_LIST_ITEM_ACTIONS.OPEN) {
|
||||
await this.onClick();
|
||||
} else if (action === WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE) {
|
||||
this.uiStore.openModalWithData({
|
||||
name: DUPLICATE_MODAL_KEY,
|
||||
data: {
|
||||
id: this.data.id,
|
||||
name: this.data.name,
|
||||
tags: (this.data.tags ?? []).map((tag) =>
|
||||
typeof tag !== 'string' && 'id' in tag ? tag.id : tag,
|
||||
),
|
||||
},
|
||||
});
|
||||
} else if (action === WORKFLOW_LIST_ITEM_ACTIONS.SHARE) {
|
||||
this.uiStore.openModalWithData({
|
||||
name: WORKFLOW_SHARE_MODAL_KEY,
|
||||
data: { id: this.data.id },
|
||||
});
|
||||
|
||||
this.$telemetry.track('User opened sharing modal', {
|
||||
workflow_id: this.data.id,
|
||||
user_id_sharer: this.currentUser.id,
|
||||
sub_view: 'Workflows listing',
|
||||
});
|
||||
} else if (action === WORKFLOW_LIST_ITEM_ACTIONS.DELETE) {
|
||||
const deleteConfirmed = await this.confirm(
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {
|
||||
interpolate: { workflowName: this.data.name },
|
||||
}),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: this.$locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowDelete.confirmButtonText',
|
||||
),
|
||||
cancelButtonText: this.$locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowDelete.cancelButtonText',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
if (deleteConfirmed !== MODAL_CONFIRM) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.workflowsStore.deleteWorkflow(this.data.id);
|
||||
} catch (error) {
|
||||
this.showError(error, this.$locale.baseText('generic.deleteWorkflowError'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset tab title since workflow is deleted.
|
||||
this.showMessage({
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect1.title'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.cardLink {
|
||||
transition: box-shadow 0.3s ease;
|
||||
|
|
|
@ -159,6 +159,7 @@ import { useRoute } from 'vue-router';
|
|||
|
||||
// eslint-disable-next-line unused-imports/no-unused-imports, @typescript-eslint/no-unused-vars
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
|
||||
export type IResource = {
|
||||
id: string;
|
||||
|
@ -167,6 +168,7 @@ export type IResource = {
|
|||
updatedAt?: string;
|
||||
createdAt?: string;
|
||||
homeProject?: ProjectSharingData;
|
||||
scopes?: Scope[];
|
||||
};
|
||||
|
||||
interface IFilters {
|
||||
|
|
|
@ -65,6 +65,8 @@ export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall';
|
|||
export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
|
||||
export const WORKFLOW_HISTORY_VERSION_RESTORE = 'workflowHistoryVersionRestore';
|
||||
export const SETUP_CREDENTIALS_MODAL_KEY = 'setupCredentials';
|
||||
export const PROJECT_MOVE_RESOURCE_MODAL = 'projectMoveResourceModal';
|
||||
export const PROJECT_MOVE_RESOURCE_CONFIRM_MODAL = 'projectMoveResourceConfirmModal';
|
||||
|
||||
export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider';
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ describe('permissions', () => {
|
|||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:share',
|
||||
'credential:move',
|
||||
],
|
||||
} as ICredentialsResponse),
|
||||
).toEqual({
|
||||
|
@ -87,6 +88,7 @@ describe('permissions', () => {
|
|||
delete: true,
|
||||
list: true,
|
||||
share: true,
|
||||
move: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -101,6 +103,7 @@ describe('permissions', () => {
|
|||
'workflow:list',
|
||||
'workflow:share',
|
||||
'workflow:execute',
|
||||
'workflow:move',
|
||||
],
|
||||
} as IWorkflowDb),
|
||||
).toEqual({
|
||||
|
@ -111,6 +114,7 @@ describe('permissions', () => {
|
|||
list: true,
|
||||
share: true,
|
||||
execute: true,
|
||||
move: true,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -13,13 +13,13 @@ export type PermissionsMap<T> = {
|
|||
[K in ExtractAfterColon<T>]: boolean;
|
||||
};
|
||||
|
||||
const mapScopesToPermissions = (scopes: Scope[], scopeSet: Set<Scope>): PermissionsMap<Scope> =>
|
||||
const mapScopesToPermissions = <T extends Scope>(scopes: T[], scopeSet: Set<T>) =>
|
||||
scopes.reduce(
|
||||
(permissions: PermissionsMap<Scope>, scope: Scope) => ({
|
||||
(permissions, scope) => ({
|
||||
...permissions,
|
||||
[scope.split(':')[1]]: scopeSet.has(scope),
|
||||
}),
|
||||
{} as PermissionsMap<Scope>,
|
||||
{} as PermissionsMap<T>,
|
||||
);
|
||||
|
||||
export const getCredentialPermissions = (
|
||||
|
@ -33,6 +33,7 @@ export const getCredentialPermissions = (
|
|||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:share',
|
||||
'credential:move',
|
||||
],
|
||||
new Set(credential?.scopes ?? []),
|
||||
);
|
||||
|
@ -47,6 +48,7 @@ export const getWorkflowPermissions = (workflow: IWorkflowDb): PermissionsMap<Wo
|
|||
'workflow:list',
|
||||
'workflow:share',
|
||||
'workflow:execute',
|
||||
'workflow:move',
|
||||
],
|
||||
new Set(workflow?.scopes ?? []),
|
||||
);
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
"generic.unsavedWork.confirmMessage.cancelButtonText": "Leave without saving",
|
||||
"generic.upgrade": "Upgrade",
|
||||
"generic.upgradeNow": "Upgrade now",
|
||||
"generic.credential": "Credential",
|
||||
"generic.workflow": "Workflow",
|
||||
"generic.workflowSaved": "Workflow changes saved",
|
||||
"generic.editor": "Editor",
|
||||
|
@ -69,6 +70,7 @@
|
|||
"generic.and": "and",
|
||||
"generic.ownedByMe": "Owned by me",
|
||||
"generic.moreInfo": "More info",
|
||||
"generic.next": "Next",
|
||||
"about.aboutN8n": "About n8n",
|
||||
"about.close": "Close",
|
||||
"about.license": "License",
|
||||
|
@ -579,6 +581,7 @@
|
|||
"credentials.empty.button": "Add first credential",
|
||||
"credentials.item.open": "Open",
|
||||
"credentials.item.delete": "Delete",
|
||||
"credentials.item.move": "Move",
|
||||
"credentials.item.updated": "Last updated",
|
||||
"credentials.item.created": "Created",
|
||||
"credentials.item.owner": "Owner",
|
||||
|
@ -2178,6 +2181,7 @@
|
|||
"workflows.item.share": "Share...",
|
||||
"workflows.item.duplicate": "Duplicate",
|
||||
"workflows.item.delete": "Delete",
|
||||
"workflows.item.move": "Move",
|
||||
"workflows.item.updated": "Last updated",
|
||||
"workflows.item.created": "Created",
|
||||
"workflows.search.placeholder": "Search workflows...",
|
||||
|
@ -2494,6 +2498,15 @@
|
|||
"projects.create.limit": "{num} project | {num} projects",
|
||||
"projects.create.limitReached": "You have reached the {planName} plan limit of {limit}. Upgrade your plan to unlock more projects. {link}",
|
||||
"projects.create.limitReached.link": "View plans",
|
||||
"projects.move.resource.modal.title": "Choose a project to move this {resourceType} to",
|
||||
"projects.move.resource.modal.message": "\"{resourceName}\" is currently in the \"{resourceHomeProjectName}\" project. Which project would you like to move this {resourceType} to?",
|
||||
"projects.move.resource.confirm.modal.title": "Please confirm the following",
|
||||
"projects.move.resource.confirm.modal.button.confirm": "Confirm move to new project",
|
||||
"projects.move.workflow.confirm.modal.label": "This workflow may stop working if the credentials used with it do not exist in the project its being moved to",
|
||||
"projects.move.credential.confirm.modal.label": "Any workflows currently using this credential will stop working once this credential has been moved",
|
||||
"projects.move.resource.confirm.modal.label": "Any individual sharing currently associated with this {resourceType} will be removed. (currently shared with {numberOfUsers})",
|
||||
"projects.move.resource.confirm.modal.numberOfUsers": "{numberOfUsers} user | {numberOfUsers} users",
|
||||
"projects.move.resource.error.title": "Error moving {resourceName} {resourceType}",
|
||||
"mfa.setup.invalidAuthenticatorCode": "{code} is not a valid number",
|
||||
"mfa.setup.invalidCode": "Two-factor code failed. Please try again.",
|
||||
"mfa.code.modal.title": "Two-factor authentication",
|
||||
|
|
|
@ -3,6 +3,8 @@ import { ref, watch, computed } from 'vue';
|
|||
import { useRoute } from 'vue-router';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import * as projectsApi from '@/api/projects.api';
|
||||
import * as workflowsEEApi from '@/api/workflows.ee';
|
||||
import * as credentialsEEApi from '@/api/credentials.ee';
|
||||
import type {
|
||||
Project,
|
||||
ProjectCreateRequest,
|
||||
|
@ -14,11 +16,15 @@ import { ProjectTypes } from '@/types/projects.types';
|
|||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { hasPermission } from '@/utils/rbac/permissions';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
|
||||
export const useProjectsStore = defineStore('projects', () => {
|
||||
const route = useRoute();
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
|
||||
const projects = ref<ProjectListItem[]>([]);
|
||||
const myProjects = ref<ProjectListItem[]>([]);
|
||||
|
@ -135,6 +141,28 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||
}
|
||||
};
|
||||
|
||||
const moveResourceToProject = async (
|
||||
resourceType: 'workflow' | 'credential',
|
||||
resourceId: string,
|
||||
projectId: string,
|
||||
) => {
|
||||
if (resourceType === 'workflow') {
|
||||
await workflowsEEApi.moveWorkflowToProject(
|
||||
rootStore.getRestApiContext,
|
||||
resourceId,
|
||||
projectId,
|
||||
);
|
||||
await workflowsStore.fetchAllWorkflows(currentProjectId.value);
|
||||
} else {
|
||||
await credentialsEEApi.moveCredentialToProject(
|
||||
rootStore.getRestApiContext,
|
||||
resourceId,
|
||||
projectId,
|
||||
);
|
||||
await credentialsStore.fetchAllCredentials(currentProjectId.value);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
route,
|
||||
async (newRoute) => {
|
||||
|
@ -188,5 +216,6 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||
deleteProject,
|
||||
getProjectsCount,
|
||||
setProjectNavActiveIdByWorkflowHomeProject,
|
||||
moveResourceToProject,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -39,6 +39,8 @@ import {
|
|||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||
SETUP_CREDENTIALS_MODAL_KEY,
|
||||
GENERATE_CURL_MODAL_KEY,
|
||||
PROJECT_MOVE_RESOURCE_MODAL,
|
||||
PROJECT_MOVE_RESOURCE_CONFIRM_MODAL,
|
||||
} from '@/constants';
|
||||
import type {
|
||||
CloudUpdateLinkSourceType,
|
||||
|
@ -119,6 +121,8 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||
DEBUG_PAYWALL_MODAL_KEY,
|
||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||
SETUP_CREDENTIALS_MODAL_KEY,
|
||||
PROJECT_MOVE_RESOURCE_MODAL,
|
||||
PROJECT_MOVE_RESOURCE_CONFIRM_MODAL,
|
||||
].map((modalKey) => [modalKey, { open: false }]),
|
||||
),
|
||||
[DELETE_USER_MODAL_KEY]: {
|
||||
|
|
|
@ -114,6 +114,8 @@ export default defineComponent({
|
|||
value: '',
|
||||
updatedAt: credential.updatedAt,
|
||||
createdAt: credential.createdAt,
|
||||
homeProject: credential.homeProject,
|
||||
scopes: credential.scopes,
|
||||
}));
|
||||
},
|
||||
allCredentialTypes(): ICredentialType[] {
|
||||
|
|
Loading…
Reference in a new issue