mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -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 getHomeButton = () => cy.getByTestId('project-home-menu-item');
|
||||||
export const getMenuItems = () => cy.getByTestId('project-menu-item');
|
export const getMenuItems = () => cy.getByTestId('project-menu-item');
|
||||||
export const getAddProjectButton = () => cy.getByTestId('add-project-menu-item');
|
export const getAddProjectButton = () => cy.getByTestId('add-project-menu-item');
|
||||||
|
@ -11,8 +16,42 @@ export const getProjectSettingsCancelButton = () =>
|
||||||
export const getProjectSettingsDeleteButton = () =>
|
export const getProjectSettingsDeleteButton = () =>
|
||||||
cy.getByTestId('project-settings-delete-button');
|
cy.getByTestId('project-settings-delete-button');
|
||||||
export const getProjectMembersSelect = () => cy.getByTestId('project-members-select');
|
export const getProjectMembersSelect = () => cy.getByTestId('project-members-select');
|
||||||
|
|
||||||
export const addProjectMember = (email: string) => {
|
export const addProjectMember = (email: string) => {
|
||||||
getProjectMembersSelect().click();
|
getProjectMembersSelect().click();
|
||||||
getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).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()
|
.first()
|
||||||
.should('contain.text', 'Notion account personal project');
|
.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'),
|
this.getters.credentialCard(credentialName).findChildByTestId('credential-card-actions'),
|
||||||
credentialDeleteButton: () =>
|
credentialDeleteButton: () =>
|
||||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
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(),
|
sort: () => cy.getByTestId('resources-list-sort').first(),
|
||||||
sortOption: (label: string) =>
|
sortOption: (label: string) =>
|
||||||
cy.getByTestId('resources-list-sort-item').contains(label).first(),
|
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'),
|
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'),
|
||||||
workflowDeleteButton: () =>
|
workflowDeleteButton: () =>
|
||||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
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'),
|
workflowFilterButton: () => cy.getByTestId('resources-list-filters-trigger').filter(':visible'),
|
||||||
workflowTagsDropdown: () => cy.getByTestId('tags-dropdown'),
|
workflowTagsDropdown: () => cy.getByTestId('tags-dropdown'),
|
||||||
workflowTagItem: (tag: string) => cy.getByTestId('tag').contains(tag),
|
workflowTagItem: (tag: string) => cy.getByTestId('tag').contains(tag),
|
||||||
|
|
|
@ -14,3 +14,13 @@ export async function setCredentialSharedWith(
|
||||||
data as unknown as IDataObject,
|
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,
|
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>
|
<template>
|
||||||
<n8n-card :class="$style.cardLink" @click="onClick">
|
<n8n-card :class="$style.cardLink" @click="onClick">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
|
@ -20,151 +149,19 @@
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
<template #append>
|
<template #append>
|
||||||
<ProjectCardBadge :resource="data" :personal-project="projectsStore.personalProject" />
|
<div :class="$style.cardActions" @click.stop>
|
||||||
<div ref="cardActions" :class="$style.cardActions">
|
<ProjectCardBadge :resource="data" :personal-project="projectsStore.personalProject" />
|
||||||
<n8n-action-toggle :actions="actions" theme="dark" @action="onAction" @click.stop />
|
<n8n-action-toggle
|
||||||
|
data-test-id="credential-card-actions"
|
||||||
|
:actions="actions"
|
||||||
|
theme="dark"
|
||||||
|
@action="onAction"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n8n-card>
|
</n8n-card>
|
||||||
</template>
|
</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>
|
<style lang="scss" module>
|
||||||
.cardLink {
|
.cardLink {
|
||||||
transition: box-shadow 0.3s ease;
|
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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ModalRoot :name="CONTACT_PROMPT_MODAL_KEY">
|
<ModalRoot :name="CONTACT_PROMPT_MODAL_KEY">
|
||||||
|
@ -167,142 +238,23 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ModalRoot>
|
</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>
|
</div>
|
||||||
</template>
|
</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>
|
</div>
|
||||||
<form @submit.prevent="onSubmit">
|
<form @submit.prevent="onSubmit">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="name">{{ locale.baseText('projects.settings.name') }}</label>
|
<label for="projectName">{{ locale.baseText('projects.settings.name') }}</label>
|
||||||
<N8nInput
|
<N8nInput
|
||||||
id="name"
|
id="projectName"
|
||||||
ref="nameInput"
|
ref="nameInput"
|
||||||
v-model="formData.name"
|
v-model="formData.name"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
@ -10,20 +10,23 @@ import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const $router = {
|
vi.mock('vue-router', () => {
|
||||||
push: vi.fn(),
|
const push = vi.fn();
|
||||||
resolve: vi.fn().mockImplementation(() => ({ href: '' })),
|
const resolve = vi.fn().mockReturnValue({ href: '' });
|
||||||
};
|
return {
|
||||||
|
useRouter: () => ({
|
||||||
const renderComponent = createComponentRenderer(WorkflowCard, {
|
push,
|
||||||
global: {
|
resolve,
|
||||||
mocks: {
|
}),
|
||||||
$router,
|
useRoute: () => ({}),
|
||||||
},
|
RouterLink: vi.fn(),
|
||||||
},
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(WorkflowCard);
|
||||||
|
|
||||||
const createWorkflow = (overrides = {}): IWorkflowDb => ({
|
const createWorkflow = (overrides = {}): IWorkflowDb => ({
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'My Workflow',
|
name: 'My Workflow',
|
||||||
|
@ -43,6 +46,7 @@ describe('WorkflowCard', () => {
|
||||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||||
let usersStore: ReturnType<typeof useUsersStore>;
|
let usersStore: ReturnType<typeof useUsersStore>;
|
||||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||||
|
let router: ReturnType<typeof useRouter>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
pinia = createPinia();
|
pinia = createPinia();
|
||||||
|
@ -51,6 +55,7 @@ describe('WorkflowCard', () => {
|
||||||
settingsStore = useSettingsStore();
|
settingsStore = useSettingsStore();
|
||||||
usersStore = useUsersStore();
|
usersStore = useUsersStore();
|
||||||
workflowsStore = useWorkflowsStore();
|
workflowsStore = useWorkflowsStore();
|
||||||
|
router = useRouter();
|
||||||
windowOpenSpy = vi.spyOn(window, 'open');
|
windowOpenSpy = vi.spyOn(window, 'open');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -67,7 +72,7 @@ describe('WorkflowCard', () => {
|
||||||
|
|
||||||
await userEvent.click(cardTitle);
|
await userEvent.click(cardTitle);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect($router.push).toHaveBeenCalledWith({
|
expect(router.push).toHaveBeenCalledWith({
|
||||||
name: VIEWS.WORKFLOW,
|
name: VIEWS.WORKFLOW,
|
||||||
params: { name: data.id },
|
params: { name: data.id },
|
||||||
});
|
});
|
||||||
|
@ -79,7 +84,7 @@ describe('WorkflowCard', () => {
|
||||||
await user.keyboard('[ControlLeft>]');
|
await user.keyboard('[ControlLeft>]');
|
||||||
await user.click(cardTitle);
|
await user.click(cardTitle);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect($router.push).toHaveBeenCalledTimes(1);
|
expect(router.push).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
expect(windowOpenSpy).toHaveBeenCalled();
|
expect(windowOpenSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -98,7 +103,7 @@ describe('WorkflowCard', () => {
|
||||||
|
|
||||||
await userEvent.click(cardActions);
|
await userEvent.click(cardActions);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect($router.push).not.toHaveBeenCalled();
|
expect(router.push).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
const actions = document.querySelector(`#${controllingId}`);
|
const actions = document.querySelector(`#${controllingId}`);
|
||||||
|
@ -107,7 +112,7 @@ describe('WorkflowCard', () => {
|
||||||
});
|
});
|
||||||
await userEvent.click(actions!.querySelectorAll('li')[0]);
|
await userEvent.click(actions!.querySelectorAll('li')[0]);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect($router.push).toHaveBeenCalledWith({
|
expect(router.push).toHaveBeenCalledWith({
|
||||||
name: VIEWS.WORKFLOW,
|
name: VIEWS.WORKFLOW,
|
||||||
params: { name: data.id },
|
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>
|
<template>
|
||||||
<n8n-card :class="$style.cardLink" @click="onClick">
|
<n8n-card :class="$style.cardLink" @click="onClick">
|
||||||
<template #header>
|
<template #header>
|
||||||
|
@ -50,203 +280,6 @@
|
||||||
</n8n-card>
|
</n8n-card>
|
||||||
</template>
|
</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>
|
<style lang="scss" module>
|
||||||
.cardLink {
|
.cardLink {
|
||||||
transition: box-shadow 0.3s ease;
|
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
|
// eslint-disable-next-line unused-imports/no-unused-imports, @typescript-eslint/no-unused-vars
|
||||||
import type { BaseTextKey } from '@/plugins/i18n';
|
import type { BaseTextKey } from '@/plugins/i18n';
|
||||||
|
import type { Scope } from '@n8n/permissions';
|
||||||
|
|
||||||
export type IResource = {
|
export type IResource = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -167,6 +168,7 @@ export type IResource = {
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
homeProject?: ProjectSharingData;
|
homeProject?: ProjectSharingData;
|
||||||
|
scopes?: Scope[];
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IFilters {
|
interface IFilters {
|
||||||
|
|
|
@ -65,6 +65,8 @@ export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall';
|
||||||
export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
|
export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
|
||||||
export const WORKFLOW_HISTORY_VERSION_RESTORE = 'workflowHistoryVersionRestore';
|
export const WORKFLOW_HISTORY_VERSION_RESTORE = 'workflowHistoryVersionRestore';
|
||||||
export const SETUP_CREDENTIALS_MODAL_KEY = 'setupCredentials';
|
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';
|
export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider';
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,7 @@ describe('permissions', () => {
|
||||||
'credential:delete',
|
'credential:delete',
|
||||||
'credential:list',
|
'credential:list',
|
||||||
'credential:share',
|
'credential:share',
|
||||||
|
'credential:move',
|
||||||
],
|
],
|
||||||
} as ICredentialsResponse),
|
} as ICredentialsResponse),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
|
@ -87,6 +88,7 @@ describe('permissions', () => {
|
||||||
delete: true,
|
delete: true,
|
||||||
list: true,
|
list: true,
|
||||||
share: true,
|
share: true,
|
||||||
|
move: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -101,6 +103,7 @@ describe('permissions', () => {
|
||||||
'workflow:list',
|
'workflow:list',
|
||||||
'workflow:share',
|
'workflow:share',
|
||||||
'workflow:execute',
|
'workflow:execute',
|
||||||
|
'workflow:move',
|
||||||
],
|
],
|
||||||
} as IWorkflowDb),
|
} as IWorkflowDb),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
|
@ -111,6 +114,7 @@ describe('permissions', () => {
|
||||||
list: true,
|
list: true,
|
||||||
share: true,
|
share: true,
|
||||||
execute: true,
|
execute: true,
|
||||||
|
move: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -13,13 +13,13 @@ export type PermissionsMap<T> = {
|
||||||
[K in ExtractAfterColon<T>]: boolean;
|
[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(
|
scopes.reduce(
|
||||||
(permissions: PermissionsMap<Scope>, scope: Scope) => ({
|
(permissions, scope) => ({
|
||||||
...permissions,
|
...permissions,
|
||||||
[scope.split(':')[1]]: scopeSet.has(scope),
|
[scope.split(':')[1]]: scopeSet.has(scope),
|
||||||
}),
|
}),
|
||||||
{} as PermissionsMap<Scope>,
|
{} as PermissionsMap<T>,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getCredentialPermissions = (
|
export const getCredentialPermissions = (
|
||||||
|
@ -33,6 +33,7 @@ export const getCredentialPermissions = (
|
||||||
'credential:delete',
|
'credential:delete',
|
||||||
'credential:list',
|
'credential:list',
|
||||||
'credential:share',
|
'credential:share',
|
||||||
|
'credential:move',
|
||||||
],
|
],
|
||||||
new Set(credential?.scopes ?? []),
|
new Set(credential?.scopes ?? []),
|
||||||
);
|
);
|
||||||
|
@ -47,6 +48,7 @@ export const getWorkflowPermissions = (workflow: IWorkflowDb): PermissionsMap<Wo
|
||||||
'workflow:list',
|
'workflow:list',
|
||||||
'workflow:share',
|
'workflow:share',
|
||||||
'workflow:execute',
|
'workflow:execute',
|
||||||
|
'workflow:move',
|
||||||
],
|
],
|
||||||
new Set(workflow?.scopes ?? []),
|
new Set(workflow?.scopes ?? []),
|
||||||
);
|
);
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
"generic.unsavedWork.confirmMessage.cancelButtonText": "Leave without saving",
|
"generic.unsavedWork.confirmMessage.cancelButtonText": "Leave without saving",
|
||||||
"generic.upgrade": "Upgrade",
|
"generic.upgrade": "Upgrade",
|
||||||
"generic.upgradeNow": "Upgrade now",
|
"generic.upgradeNow": "Upgrade now",
|
||||||
|
"generic.credential": "Credential",
|
||||||
"generic.workflow": "Workflow",
|
"generic.workflow": "Workflow",
|
||||||
"generic.workflowSaved": "Workflow changes saved",
|
"generic.workflowSaved": "Workflow changes saved",
|
||||||
"generic.editor": "Editor",
|
"generic.editor": "Editor",
|
||||||
|
@ -69,6 +70,7 @@
|
||||||
"generic.and": "and",
|
"generic.and": "and",
|
||||||
"generic.ownedByMe": "Owned by me",
|
"generic.ownedByMe": "Owned by me",
|
||||||
"generic.moreInfo": "More info",
|
"generic.moreInfo": "More info",
|
||||||
|
"generic.next": "Next",
|
||||||
"about.aboutN8n": "About n8n",
|
"about.aboutN8n": "About n8n",
|
||||||
"about.close": "Close",
|
"about.close": "Close",
|
||||||
"about.license": "License",
|
"about.license": "License",
|
||||||
|
@ -579,6 +581,7 @@
|
||||||
"credentials.empty.button": "Add first credential",
|
"credentials.empty.button": "Add first credential",
|
||||||
"credentials.item.open": "Open",
|
"credentials.item.open": "Open",
|
||||||
"credentials.item.delete": "Delete",
|
"credentials.item.delete": "Delete",
|
||||||
|
"credentials.item.move": "Move",
|
||||||
"credentials.item.updated": "Last updated",
|
"credentials.item.updated": "Last updated",
|
||||||
"credentials.item.created": "Created",
|
"credentials.item.created": "Created",
|
||||||
"credentials.item.owner": "Owner",
|
"credentials.item.owner": "Owner",
|
||||||
|
@ -2178,6 +2181,7 @@
|
||||||
"workflows.item.share": "Share...",
|
"workflows.item.share": "Share...",
|
||||||
"workflows.item.duplicate": "Duplicate",
|
"workflows.item.duplicate": "Duplicate",
|
||||||
"workflows.item.delete": "Delete",
|
"workflows.item.delete": "Delete",
|
||||||
|
"workflows.item.move": "Move",
|
||||||
"workflows.item.updated": "Last updated",
|
"workflows.item.updated": "Last updated",
|
||||||
"workflows.item.created": "Created",
|
"workflows.item.created": "Created",
|
||||||
"workflows.search.placeholder": "Search workflows...",
|
"workflows.search.placeholder": "Search workflows...",
|
||||||
|
@ -2494,6 +2498,15 @@
|
||||||
"projects.create.limit": "{num} project | {num} projects",
|
"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": "You have reached the {planName} plan limit of {limit}. Upgrade your plan to unlock more projects. {link}",
|
||||||
"projects.create.limitReached.link": "View plans",
|
"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.invalidAuthenticatorCode": "{code} is not a valid number",
|
||||||
"mfa.setup.invalidCode": "Two-factor code failed. Please try again.",
|
"mfa.setup.invalidCode": "Two-factor code failed. Please try again.",
|
||||||
"mfa.code.modal.title": "Two-factor authentication",
|
"mfa.code.modal.title": "Two-factor authentication",
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { ref, watch, computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import * as projectsApi from '@/api/projects.api';
|
import * as projectsApi from '@/api/projects.api';
|
||||||
|
import * as workflowsEEApi from '@/api/workflows.ee';
|
||||||
|
import * as credentialsEEApi from '@/api/credentials.ee';
|
||||||
import type {
|
import type {
|
||||||
Project,
|
Project,
|
||||||
ProjectCreateRequest,
|
ProjectCreateRequest,
|
||||||
|
@ -14,11 +16,15 @@ import { ProjectTypes } from '@/types/projects.types';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { hasPermission } from '@/utils/rbac/permissions';
|
import { hasPermission } from '@/utils/rbac/permissions';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
|
||||||
export const useProjectsStore = defineStore('projects', () => {
|
export const useProjectsStore = defineStore('projects', () => {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const credentialsStore = useCredentialsStore();
|
||||||
|
|
||||||
const projects = ref<ProjectListItem[]>([]);
|
const projects = ref<ProjectListItem[]>([]);
|
||||||
const myProjects = 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(
|
watch(
|
||||||
route,
|
route,
|
||||||
async (newRoute) => {
|
async (newRoute) => {
|
||||||
|
@ -188,5 +216,6 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||||
deleteProject,
|
deleteProject,
|
||||||
getProjectsCount,
|
getProjectsCount,
|
||||||
setProjectNavActiveIdByWorkflowHomeProject,
|
setProjectNavActiveIdByWorkflowHomeProject,
|
||||||
|
moveResourceToProject,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,6 +39,8 @@ import {
|
||||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||||
SETUP_CREDENTIALS_MODAL_KEY,
|
SETUP_CREDENTIALS_MODAL_KEY,
|
||||||
GENERATE_CURL_MODAL_KEY,
|
GENERATE_CURL_MODAL_KEY,
|
||||||
|
PROJECT_MOVE_RESOURCE_MODAL,
|
||||||
|
PROJECT_MOVE_RESOURCE_CONFIRM_MODAL,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
CloudUpdateLinkSourceType,
|
CloudUpdateLinkSourceType,
|
||||||
|
@ -119,6 +121,8 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
DEBUG_PAYWALL_MODAL_KEY,
|
DEBUG_PAYWALL_MODAL_KEY,
|
||||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||||
SETUP_CREDENTIALS_MODAL_KEY,
|
SETUP_CREDENTIALS_MODAL_KEY,
|
||||||
|
PROJECT_MOVE_RESOURCE_MODAL,
|
||||||
|
PROJECT_MOVE_RESOURCE_CONFIRM_MODAL,
|
||||||
].map((modalKey) => [modalKey, { open: false }]),
|
].map((modalKey) => [modalKey, { open: false }]),
|
||||||
),
|
),
|
||||||
[DELETE_USER_MODAL_KEY]: {
|
[DELETE_USER_MODAL_KEY]: {
|
||||||
|
|
|
@ -114,6 +114,8 @@ export default defineComponent({
|
||||||
value: '',
|
value: '',
|
||||||
updatedAt: credential.updatedAt,
|
updatedAt: credential.updatedAt,
|
||||||
createdAt: credential.createdAt,
|
createdAt: credential.createdAt,
|
||||||
|
homeProject: credential.homeProject,
|
||||||
|
scopes: credential.scopes,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
allCredentialTypes(): ICredentialType[] {
|
allCredentialTypes(): ICredentialType[] {
|
||||||
|
|
Loading…
Reference in a new issue