mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Add "Change owner" option to editor (#15792)
This commit is contained in:
committed by
GitHub
parent
e76c45d46d
commit
5bc4e5d846
@@ -31,7 +31,7 @@ export class CredentialsPage extends BasePage {
|
|||||||
credentialDeleteButton: () =>
|
credentialDeleteButton: () =>
|
||||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
||||||
credentialMoveButton: () =>
|
credentialMoveButton: () =>
|
||||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Move'),
|
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Change owner'),
|
||||||
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(),
|
||||||
|
|||||||
@@ -644,7 +644,7 @@
|
|||||||
"credentials.empty.button.disabled.tooltip": "Your current role in the project does not allow you to create credentials",
|
"credentials.empty.button.disabled.tooltip": "Your current role in the project does not allow you to create credentials",
|
||||||
"credentials.item.open": "Open",
|
"credentials.item.open": "Open",
|
||||||
"credentials.item.delete": "Delete",
|
"credentials.item.delete": "Delete",
|
||||||
"credentials.item.move": "Move",
|
"credentials.item.move": "Change owner",
|
||||||
"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",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ describe('CredentialCard', () => {
|
|||||||
expect(badge).toHaveTextContent('John Doe');
|
expect(badge).toHaveTextContent('John Doe');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show Move action only if there is resource permission and not on community plan', async () => {
|
it('should show Change owner action only if there is resource permission and not on community plan', async () => {
|
||||||
vi.spyOn(projectsStore, 'isTeamProjectFeatureEnabled', 'get').mockReturnValue(true);
|
vi.spyOn(projectsStore, 'isTeamProjectFeatureEnabled', 'get').mockReturnValue(true);
|
||||||
|
|
||||||
const data = createCredential({
|
const data = createCredential({
|
||||||
@@ -84,7 +84,7 @@ describe('CredentialCard', () => {
|
|||||||
if (!actions) {
|
if (!actions) {
|
||||||
throw new Error('Actions menu not found');
|
throw new Error('Actions menu not found');
|
||||||
}
|
}
|
||||||
expect(actions).toHaveTextContent('Move');
|
expect(actions).toHaveTextContent('Change owner');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set readOnly variant based on prop', () => {
|
it('should set readOnly variant based on prop', () => {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
WORKFLOW_SHARE_MODAL_KEY,
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
|
PROJECT_MOVE_RESOURCE_MODAL,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
import { STORES } from '@n8n/stores';
|
import { STORES } from '@n8n/stores';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
@@ -59,6 +61,11 @@ const initialState = {
|
|||||||
enterprise: {
|
enterprise: {
|
||||||
[EnterpriseEditionFeature.Sharing]: true,
|
[EnterpriseEditionFeature.Sharing]: true,
|
||||||
[EnterpriseEditionFeature.WorkflowHistory]: true,
|
[EnterpriseEditionFeature.WorkflowHistory]: true,
|
||||||
|
projects: {
|
||||||
|
team: {
|
||||||
|
limit: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
areTagsEnabled: true,
|
areTagsEnabled: true,
|
||||||
@@ -394,6 +401,27 @@ describe('WorkflowDetails', () => {
|
|||||||
name: VIEWS.WORKFLOWS,
|
name: VIEWS.WORKFLOWS,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should call onWorkflowMenuSelect on 'Change owner' option click", async () => {
|
||||||
|
const openModalSpy = vi.spyOn(uiStore, 'openModalWithData');
|
||||||
|
|
||||||
|
workflowsStore.workflowsById = { [workflow.id]: workflow as IWorkflowDb };
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
...workflow,
|
||||||
|
scopes: ['workflow:move'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(getByTestId('workflow-menu'));
|
||||||
|
await userEvent.click(getByTestId('workflow-menu-item-change-owner'));
|
||||||
|
|
||||||
|
expect(openModalSpy).toHaveBeenCalledWith({
|
||||||
|
name: PROJECT_MOVE_RESOURCE_MODAL,
|
||||||
|
data: expect.objectContaining({ resource: expect.objectContaining({ id: workflow.id }) }),
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Archived badge', () => {
|
describe('Archived badge', () => {
|
||||||
|
|||||||
@@ -1,49 +1,39 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
|
||||||
|
import InlineTextEdit from '@/components/InlineTextEdit.vue';
|
||||||
|
import CollaborationPane from '@/components/MainHeader/CollaborationPane.vue';
|
||||||
|
import WorkflowHistoryButton from '@/components/MainHeader/WorkflowHistoryButton.vue';
|
||||||
|
import PushConnectionTracker from '@/components/PushConnectionTracker.vue';
|
||||||
|
import SaveButton from '@/components/SaveButton.vue';
|
||||||
|
import ShortenName from '@/components/ShortenName.vue';
|
||||||
|
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||||
|
import WorkflowTagsContainer from '@/components/WorkflowTagsContainer.vue';
|
||||||
|
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
||||||
import {
|
import {
|
||||||
DUPLICATE_MODAL_KEY,
|
DUPLICATE_MODAL_KEY,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
|
IMPORT_WORKFLOW_URL_MODAL_KEY,
|
||||||
MAX_WORKFLOW_NAME_LENGTH,
|
MAX_WORKFLOW_NAME_LENGTH,
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||||
|
PROJECT_MOVE_RESOURCE_MODAL,
|
||||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
WORKFLOW_MENU_ACTIONS,
|
WORKFLOW_MENU_ACTIONS,
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
WORKFLOW_SHARE_MODAL_KEY,
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
IMPORT_WORKFLOW_URL_MODAL_KEY,
|
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import ShortenName from '@/components/ShortenName.vue';
|
import { ResourceType } from '@/utils/projects.utils';
|
||||||
import WorkflowTagsContainer from '@/components/WorkflowTagsContainer.vue';
|
|
||||||
import PushConnectionTracker from '@/components/PushConnectionTracker.vue';
|
|
||||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
|
||||||
import SaveButton from '@/components/SaveButton.vue';
|
|
||||||
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
|
||||||
import InlineTextEdit from '@/components/InlineTextEdit.vue';
|
|
||||||
import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
|
|
||||||
import WorkflowHistoryButton from '@/components/MainHeader/WorkflowHistoryButton.vue';
|
|
||||||
import CollaborationPane from '@/components/MainHeader/CollaborationPane.vue';
|
|
||||||
|
|
||||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useTagsStore } from '@/stores/tags.store';
|
import { useTagsStore } from '@/stores/tags.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.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 { useProjectsStore } from '@/stores/projects.store';
|
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||||
|
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
|
||||||
import { useMessage } from '@/composables/useMessage';
|
|
||||||
import { useToast } from '@/composables/useToast';
|
|
||||||
import { getResourcePermissions } from '@/permissions';
|
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
|
||||||
import { nodeViewEventBus } from '@/event-bus';
|
|
||||||
import { hasPermission } from '@/utils/rbac/permissions';
|
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
|
||||||
import { computed, ref, useCssModule, watch } from 'vue';
|
|
||||||
import type {
|
import type {
|
||||||
ActionDropdownItem,
|
ActionDropdownItem,
|
||||||
FolderShortInfo,
|
FolderShortInfo,
|
||||||
@@ -51,14 +41,26 @@ import type {
|
|||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
IWorkflowToShare,
|
IWorkflowToShare,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import type { BaseTextKey } from '@n8n/i18n';
|
|
||||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
|
||||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import { nodeViewEventBus } from '@/event-bus';
|
||||||
|
import { getResourcePermissions } from '@/permissions';
|
||||||
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
import { useFoldersStore } from '@/stores/folders.store';
|
import { useFoldersStore } from '@/stores/folders.store';
|
||||||
|
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||||
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
|
import { hasPermission } from '@/utils/rbac/permissions';
|
||||||
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
||||||
|
import type { BaseTextKey } from '@n8n/i18n';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import { computed, ref, useCssModule, watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -106,6 +108,7 @@ const importFileRef = ref<HTMLInputElement | undefined>();
|
|||||||
|
|
||||||
const tagsEventBus = createEventBus();
|
const tagsEventBus = createEventBus();
|
||||||
const sourceControlModalEventBus = createEventBus();
|
const sourceControlModalEventBus = createEventBus();
|
||||||
|
const changeOwnerEventBus = createEventBus();
|
||||||
|
|
||||||
const hasChanged = (prev: string[], curr: string[]) => {
|
const hasChanged = (prev: string[], curr: string[]) => {
|
||||||
if (prev.length !== curr.length) {
|
if (prev.length !== curr.length) {
|
||||||
@@ -147,6 +150,14 @@ const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (workflowPermissions.value.move && projectsStore.isTeamProjectFeatureEnabled) {
|
||||||
|
actions.push({
|
||||||
|
id: WORKFLOW_MENU_ACTIONS.CHANGE_OWNER,
|
||||||
|
label: locale.baseText('workflows.item.changeOwner'),
|
||||||
|
disabled: isNewWorkflow.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!props.readOnly && !props.isArchived) {
|
if (!props.readOnly && !props.isArchived) {
|
||||||
actions.push({
|
actions.push({
|
||||||
id: WORKFLOW_MENU_ACTIONS.RENAME,
|
id: WORKFLOW_MENU_ACTIONS.RENAME,
|
||||||
@@ -609,6 +620,27 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
|||||||
await router.push({ name: VIEWS.WORKFLOWS });
|
await router.push({ name: VIEWS.WORKFLOWS });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case WORKFLOW_MENU_ACTIONS.CHANGE_OWNER: {
|
||||||
|
const workflowId = getWorkflowId();
|
||||||
|
if (!workflowId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
changeOwnerEventBus.once(
|
||||||
|
'resource-moved',
|
||||||
|
async () => await router.push({ name: VIEWS.WORKFLOWS }),
|
||||||
|
);
|
||||||
|
|
||||||
|
uiStore.openModalWithData({
|
||||||
|
name: PROJECT_MOVE_RESOURCE_MODAL,
|
||||||
|
data: {
|
||||||
|
resource: workflowsStore.workflowsById[workflowId],
|
||||||
|
resourceType: ResourceType.Workflow,
|
||||||
|
resourceTypeLabel: locale.baseText('generic.workflow').toLowerCase(),
|
||||||
|
eventBus: changeOwnerEventBus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, onMounted, h } from 'vue';
|
|
||||||
import { truncate } from '@n8n/utils/string/truncate';
|
|
||||||
import type { ICredentialsResponse, IUsedCredential, IWorkflowDb } from '@/Interface';
|
|
||||||
import { useI18n } from '@n8n/i18n';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
|
||||||
import Modal from '@/components/Modal.vue';
|
import Modal from '@/components/Modal.vue';
|
||||||
import { VIEWS } from '@/constants';
|
|
||||||
import {
|
|
||||||
splitName,
|
|
||||||
getTruncatedProjectName,
|
|
||||||
ResourceType,
|
|
||||||
MAX_NAME_LENGTH,
|
|
||||||
} from '@/utils/projects.utils';
|
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
|
||||||
import ProjectMoveSuccessToastMessage from '@/components/Projects/ProjectMoveSuccessToastMessage.vue';
|
|
||||||
import ProjectMoveResourceModalCredentialsList from '@/components/Projects/ProjectMoveResourceModalCredentialsList.vue';
|
import ProjectMoveResourceModalCredentialsList from '@/components/Projects/ProjectMoveResourceModalCredentialsList.vue';
|
||||||
|
import ProjectMoveSuccessToastMessage from '@/components/Projects/ProjectMoveSuccessToastMessage.vue';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
import type { ICredentialsResponse, IUsedCredential, IWorkflowDb } from '@/Interface';
|
||||||
import { getResourcePermissions } from '@/permissions';
|
import { getResourcePermissions } from '@/permissions';
|
||||||
import { sortByProperty } from '@n8n/utils/sort/sortByProperty';
|
|
||||||
import type { EventBus } from '@n8n/utils/event-bus';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
|
import {
|
||||||
|
getTruncatedProjectName,
|
||||||
|
MAX_NAME_LENGTH,
|
||||||
|
ResourceType,
|
||||||
|
splitName,
|
||||||
|
} from '@/utils/projects.utils';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import type { EventBus } from '@n8n/utils/event-bus';
|
||||||
|
import { sortByProperty } from '@n8n/utils/sort/sortByProperty';
|
||||||
|
import { truncate } from '@n8n/utils/string/truncate';
|
||||||
|
import { computed, h, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modalName: string;
|
modalName: string;
|
||||||
@@ -177,6 +177,8 @@ onMounted(async () => {
|
|||||||
project_from_type: projectsStore.currentProject?.type ?? projectsStore.personalProject?.type,
|
project_from_type: projectsStore.currentProject?.type ?? projectsStore.personalProject?.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await projectsStore.getAvailableProjects();
|
||||||
|
|
||||||
if (isResourceWorkflow.value) {
|
if (isResourceWorkflow.value) {
|
||||||
const [workflow, credentials] = await Promise.all([
|
const [workflow, credentials] = await Promise.all([
|
||||||
workflowsStore.fetchWorkflow(props.data.resource.id),
|
workflowsStore.fetchWorkflow(props.data.resource.id),
|
||||||
|
|||||||
@@ -628,6 +628,7 @@ export const enum WORKFLOW_MENU_ACTIONS {
|
|||||||
ARCHIVE = 'archive',
|
ARCHIVE = 'archive',
|
||||||
UNARCHIVE = 'unarchive',
|
UNARCHIVE = 'unarchive',
|
||||||
RENAME = 'rename',
|
RENAME = 'rename',
|
||||||
|
CHANGE_OWNER = 'change-owner',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user