mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Implement folder move functionality (no-changelog) (#13922)
This commit is contained in:
committed by
GitHub
parent
042aa39024
commit
1c17d12209
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import { createEventBus, type EventBus } from '@n8n/utils/event-bus';
|
||||
@@ -7,6 +7,7 @@ import { useI18n } from '@/composables/useI18n';
|
||||
import { useFoldersStore } from '@/stores/folders.store';
|
||||
import { useRoute } from 'vue-router';
|
||||
import type { FolderListItem } from '@/Interface';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
|
||||
const props = defineProps<{
|
||||
modalName: string;
|
||||
@@ -26,34 +27,25 @@ const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const foldersStore = useFoldersStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const operation = ref('');
|
||||
const deleteConfirmText = ref('');
|
||||
const selectedFolderId = ref<string | null>(null);
|
||||
const selectedFolder = ref<{ id: string; name: string } | null>(null);
|
||||
const projectFolders = ref<FolderListItem[]>([]);
|
||||
|
||||
const currentFolder = computed(() => {
|
||||
return projectFolders.value.find((folder) => folder.id === props.activeId);
|
||||
});
|
||||
|
||||
// Available folders to transfer are all folders except the current folder, it's parent and its children
|
||||
const availableFolders = computed(() => {
|
||||
return projectFolders.value.filter(
|
||||
(folder) =>
|
||||
folder.id !== props.activeId &&
|
||||
folder.parentFolder?.id !== props.activeId &&
|
||||
folder.id !== currentFolder.value?.parentFolder?.id,
|
||||
);
|
||||
});
|
||||
|
||||
const folderToDelete = computed(() => {
|
||||
if (!props.activeId) return null;
|
||||
return foldersStore.breadcrumbsCache[props.activeId];
|
||||
});
|
||||
|
||||
const isPending = computed(() => {
|
||||
return folderToDelete.value ? !folderToDelete.value.name : false;
|
||||
return selectedFolder.value ? !selectedFolder.value.name : false;
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
@@ -74,12 +66,35 @@ const enabled = computed(() => {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (operation.value === 'transfer' && selectedFolderId.value) {
|
||||
if (operation.value === 'transfer' && selectedFolder.value) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const folderContentWarningMessage = computed(() => {
|
||||
const folderCount = props.data.content.subFolderCount ?? 0;
|
||||
const workflowCount = props.data.content.workflowCount ?? 0;
|
||||
|
||||
let folderText = '';
|
||||
let workflowText = '';
|
||||
if (folderCount > 0) {
|
||||
folderText = i18n.baseText('folder.count', { interpolate: { count: folderCount } });
|
||||
}
|
||||
if (workflowCount > 0) {
|
||||
workflowText = i18n.baseText('workflow.count', { interpolate: { count: workflowCount } });
|
||||
}
|
||||
if (folderCount > 0 && workflowCount > 0) {
|
||||
folderText += ` ${i18n.baseText('folder.and.workflow.separator')} `;
|
||||
}
|
||||
return i18n.baseText('folder.delete.modal.confirmation', {
|
||||
interpolate: {
|
||||
folders: folderText,
|
||||
workflows: workflowText,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
async function onSubmit() {
|
||||
if (!enabled.value) {
|
||||
return;
|
||||
@@ -90,16 +105,13 @@ async function onSubmit() {
|
||||
await foldersStore.deleteFolder(
|
||||
route.params.projectId as string,
|
||||
props.activeId,
|
||||
selectedFolderId.value ?? undefined,
|
||||
selectedFolder.value?.id ?? undefined,
|
||||
);
|
||||
|
||||
let message = '';
|
||||
if (selectedFolderId.value) {
|
||||
const selectedFolder = availableFolders.value.find(
|
||||
(folder) => folder.id === selectedFolderId.value,
|
||||
);
|
||||
if (selectedFolder.value) {
|
||||
message = i18n.baseText('folders.transfer.confirm.message', {
|
||||
interpolate: { folderName: selectedFolder?.name ?? '' },
|
||||
interpolate: { folderName: selectedFolder.value.name ?? '' },
|
||||
});
|
||||
}
|
||||
showMessage({
|
||||
@@ -107,7 +119,11 @@ async function onSubmit() {
|
||||
title: i18n.baseText('folders.delete.success.message'),
|
||||
message,
|
||||
});
|
||||
props.data.workflowListEventBus.emit('folder-deleted', { folderId: props.activeId });
|
||||
props.data.workflowListEventBus.emit('folder-deleted', {
|
||||
folderId: props.activeId,
|
||||
workflowCount: props.data.content.workflowCount,
|
||||
folderCount: props.data.content.subFolderCount,
|
||||
});
|
||||
modalBus.emit('close');
|
||||
} catch (error) {
|
||||
showError(error, i18n.baseText('folders.delete.error.message'));
|
||||
@@ -116,9 +132,9 @@ async function onSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
projectFolders.value = await foldersStore.fetchProjectFolders(route.params.projectId as string);
|
||||
});
|
||||
const onFolderSelected = (payload: { id: string; name: string }) => {
|
||||
selectedFolder.value = payload;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -139,9 +155,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div v-else :class="$style.content">
|
||||
<div>
|
||||
<n8n-text color="text-base">{{
|
||||
i18n.baseText('folder.delete.modal.confirmation')
|
||||
}}</n8n-text>
|
||||
<n8n-text color="text-base">{{ folderContentWarningMessage }}</n8n-text>
|
||||
</div>
|
||||
<el-radio
|
||||
v-model="operation"
|
||||
@@ -155,24 +169,13 @@ onMounted(async () => {
|
||||
<n8n-text color="text-dark">{{
|
||||
i18n.baseText('folders.transfer.selectFolder')
|
||||
}}</n8n-text>
|
||||
<N8nSelect
|
||||
v-model="selectedFolderId"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
:placeholder="i18n.baseText('folders.transfer.selectFolder')"
|
||||
>
|
||||
<N8nOption
|
||||
v-for="folder in availableFolders"
|
||||
:key="folder.id"
|
||||
:value="folder.id"
|
||||
:label="folder.name"
|
||||
>
|
||||
<div :class="$style['folder-select-item']">
|
||||
<n8n-icon icon="folder" />
|
||||
<span> {{ folder.name }}</span>
|
||||
</div>
|
||||
</N8nOption>
|
||||
</N8nSelect>
|
||||
<MoveToFolderDropdown
|
||||
v-if="projectsStore.currentProject"
|
||||
:current-folder-id="props.activeId"
|
||||
:current-project-id="projectsStore.currentProject?.id"
|
||||
:parent-folder-id="currentFolder?.parentFolder?.id"
|
||||
@folder:selected="onFolderSelected"
|
||||
/>
|
||||
</div>
|
||||
<el-radio
|
||||
v-model="operation"
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { FolderListItem } from '@/Interface';
|
||||
import { useFoldersStore } from '@/stores/folders.store';
|
||||
import { N8nSelect } from '@n8n/design-system';
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* This component is used to select a folder to move a resource (folder or workflow) to.
|
||||
* Based on the provided resource type, it fetches the available folders and displays them in a dropdown.
|
||||
* For folders, it filters out current folder parent and all off it's children (done in the back-end)
|
||||
* For workflows, it only filters out the current workflows's folder.
|
||||
*/
|
||||
|
||||
type Props = {
|
||||
currentProjectId: string;
|
||||
currentFolderId?: string;
|
||||
parentFolderId?: string;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
currentFolderId: '',
|
||||
parentFolderId: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'folder:selected': [value: { id: string; name: string }];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const foldersStore = useFoldersStore();
|
||||
|
||||
const moveFolderDropdown = ref<InstanceType<typeof N8nSelect>>();
|
||||
const selectedFolderId = ref<string | null>(null);
|
||||
const availableFolders = ref<FolderListItem[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchAvailableFolders = async (query?: string) => {
|
||||
if (!query) {
|
||||
availableFolders.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
const folders = await foldersStore.fetchFoldersAvailableForMove(
|
||||
props.currentProjectId,
|
||||
props.currentFolderId,
|
||||
{ name: query ?? undefined },
|
||||
);
|
||||
if (!props.parentFolderId) {
|
||||
availableFolders.value = folders;
|
||||
} else {
|
||||
availableFolders.value = folders.filter((folder) => folder.id !== props.parentFolderId);
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const onFolderSelected = (folderId: string) => {
|
||||
const selectedFolder = availableFolders.value.find((folder) => folder.id === folderId);
|
||||
if (!selectedFolder) {
|
||||
return;
|
||||
}
|
||||
emit('folder:selected', { id: folderId, name: selectedFolder.name });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style['move-folder-dropdown']" data-test-id="move-to-folder-dropdown">
|
||||
<N8nSelect
|
||||
ref="moveFolderDropdown"
|
||||
v-model="selectedFolderId"
|
||||
:filterable="true"
|
||||
:remote="true"
|
||||
:remote-method="fetchAvailableFolders"
|
||||
:loading="loading"
|
||||
:placeholder="i18n.baseText('folders.move.modal.select.placeholder')"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
@update:model-value="onFolderSelected"
|
||||
>
|
||||
<N8nOption
|
||||
v-for="folder in availableFolders"
|
||||
:key="folder.id"
|
||||
:value="folder.id"
|
||||
:label="folder.name"
|
||||
>
|
||||
<div :class="$style['folder-select-item']">
|
||||
<n8n-icon :class="$style['folder-icon']" icon="folder" />
|
||||
<span :class="$style['folder-name']"> {{ folder.name }}</span>
|
||||
</div>
|
||||
</N8nOption>
|
||||
</N8nSelect>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.move-folder-dropdown {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.folder-select-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-2xs);
|
||||
align-items: center;
|
||||
max-width: 90%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.folder-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import MoveToFolderModal from './MoveToFolderModal.vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { cleanupAppModals, createAppModals, mockedStore } from '@/__tests__/utils';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { MOVE_FOLDER_MODAL_KEY } from '@/constants';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import { screen } from '@testing-library/vue';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
const push = vi.fn();
|
||||
const resolve = vi.fn().mockReturnValue({ href: '/projects/1/folders/1' });
|
||||
return {
|
||||
useRouter: vi.fn().mockReturnValue({
|
||||
push,
|
||||
resolve,
|
||||
}),
|
||||
useRoute: vi.fn().mockReturnValue({
|
||||
params: {
|
||||
projectId: '1',
|
||||
|
||||
folderId: '1',
|
||||
},
|
||||
query: {},
|
||||
}),
|
||||
RouterLink: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const renderComponent = createComponentRenderer(MoveToFolderModal, {
|
||||
props: {
|
||||
modalName: MOVE_FOLDER_MODAL_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
const TEST_FOLDER_RESOURCE = {
|
||||
id: 'test-folder-id',
|
||||
name: 'Test Folder',
|
||||
parentFolderId: 'test-parent-folder-id',
|
||||
};
|
||||
|
||||
const TEST_WORKFLOW_RESOURCE = {
|
||||
id: 'test-workflow-id',
|
||||
name: 'Test Workflow',
|
||||
parentFolderId: 'test-parent-folder-id',
|
||||
};
|
||||
|
||||
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
|
||||
describe('MoveToFolderModal', () => {
|
||||
beforeEach(() => {
|
||||
createAppModals();
|
||||
createTestingPinia();
|
||||
uiStore = mockedStore(useUIStore);
|
||||
uiStore.modalsById = {
|
||||
[MOVE_FOLDER_MODAL_KEY]: {
|
||||
open: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupAppModals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render for folder resource', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
data: {
|
||||
resource: TEST_FOLDER_RESOURCE,
|
||||
resourceType: 'folder',
|
||||
},
|
||||
},
|
||||
});
|
||||
await waitFor(() => expect(getByTestId('moveFolder-modal')).toBeInTheDocument());
|
||||
expect(
|
||||
screen.getByText(`Move "${TEST_FOLDER_RESOURCE.name}" to another folder`),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('move-folder-description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render for workflow resource', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
data: {
|
||||
resource: TEST_WORKFLOW_RESOURCE,
|
||||
resourceType: 'workflow',
|
||||
},
|
||||
},
|
||||
});
|
||||
await waitFor(() => expect(getByTestId('moveFolder-modal')).toBeInTheDocument());
|
||||
expect(
|
||||
screen.getByText(`Move "${TEST_WORKFLOW_RESOURCE.name}" to another folder`),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('move-folder-description')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { MOVE_FOLDER_MODAL_KEY } from '@/constants';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { type EventBus } from '@n8n/utils/event-bus';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* This modal is used to move a resource (folder or workflow) to a different folder.
|
||||
*/
|
||||
|
||||
type Props = {
|
||||
modalName: string;
|
||||
data: {
|
||||
resourceType: 'folder' | 'workflow';
|
||||
resource: {
|
||||
id: string;
|
||||
name: string;
|
||||
parentFolderId?: string;
|
||||
};
|
||||
workflowListEventBus: EventBus;
|
||||
};
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const projectsStore = useProjectsStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const selectedFolder = ref<{ id: string; name: string } | null>(null);
|
||||
|
||||
const title = computed(() => {
|
||||
return i18n.baseText('folders.move.modal.title', {
|
||||
interpolate: { folderName: props.data.resource.name },
|
||||
});
|
||||
});
|
||||
|
||||
const currentFolder = computed(() => {
|
||||
if (props.data.resourceType === 'workflow') {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
id: props.data.resource.id,
|
||||
name: props.data.resource.name,
|
||||
};
|
||||
});
|
||||
|
||||
const onFolderSelected = (payload: { id: string; name: string }) => {
|
||||
selectedFolder.value = payload;
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (props.data.resourceType === 'folder') {
|
||||
props.data.workflowListEventBus.emit('folder-moved', {
|
||||
newParent: selectedFolder.value,
|
||||
folder: { id: props.data.resource.id, name: props.data.resource.name },
|
||||
});
|
||||
} else {
|
||||
props.data.workflowListEventBus.emit('workflow-moved', {
|
||||
newParent: selectedFolder.value,
|
||||
workflow: {
|
||||
id: props.data.resource.id,
|
||||
name: props.data.resource.name,
|
||||
oldParentId: props.data.resource.parentFolderId,
|
||||
},
|
||||
});
|
||||
}
|
||||
uiStore.closeModal(MOVE_FOLDER_MODAL_KEY);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :name="modalName" :title="title" width="500" :class="$style.container">
|
||||
<template #content>
|
||||
<MoveToFolderDropdown
|
||||
v-if="projectsStore.currentProject"
|
||||
:current-folder-id="currentFolder?.id"
|
||||
:current-project-id="projectsStore.currentProject?.id"
|
||||
:parent-folder-id="props.data.resource.parentFolderId"
|
||||
:exclude-only-parent="props.data.resourceType === 'workflow'"
|
||||
@folder:selected="onFolderSelected"
|
||||
/>
|
||||
<p
|
||||
v-if="props.data.resourceType === 'folder'"
|
||||
:class="$style.description"
|
||||
data-test-id="move-folder-description"
|
||||
>
|
||||
{{ i18n.baseText('folders.move.modal.description') }}
|
||||
</p>
|
||||
</template>
|
||||
<template #footer="{ close }">
|
||||
<div :class="$style.footer">
|
||||
<n8n-button
|
||||
type="secondary"
|
||||
:label="i18n.baseText('generic.cancel')"
|
||||
float="right"
|
||||
data-test-id="cancel-move-folder-button"
|
||||
@click="close"
|
||||
/>
|
||||
<n8n-button
|
||||
:disabled="!selectedFolder"
|
||||
:label="i18n.baseText('folders.move.modal.confirm')"
|
||||
float="right"
|
||||
data-test-id="confirm-move-folder-button"
|
||||
@click="onSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
h1 {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--font-size-s);
|
||||
margin: var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: var(--spacing-2xs);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
PROMPT_MFA_CODE_MODAL_KEY,
|
||||
COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
||||
DELETE_FOLDER_MODAL_KEY,
|
||||
MOVE_FOLDER_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
import AboutModal from '@/components/AboutModal.vue';
|
||||
@@ -287,5 +288,11 @@ import type { EventBus } from '@n8n/utils/event-bus';
|
||||
<DeleteFolderModal :modal-name="modalName" :active-id="activeId" :data="data" />
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="MOVE_FOLDER_MODAL_KEY">
|
||||
<template #default="{ modalName, activeId, data }">
|
||||
<MoveToFolderModal :modal-name="modalName" :active-id="activeId" :data="data" />
|
||||
</template>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -56,8 +56,9 @@ const showSettings = computed(
|
||||
);
|
||||
|
||||
const homeProject = computed(() => projectsStore.currentProject ?? projectsStore.personalProject);
|
||||
const isFoldersFeatureEnabled = computed(() => settingsStore.settings.folders.enabled);
|
||||
const isProjectPage = computed(() => route.name === VIEWS.PROJECTS_WORKFLOWS);
|
||||
const showFolders = computed(() => {
|
||||
return settingsStore.isFoldersFeatureEnabled && route.name !== VIEWS.WORKFLOWS;
|
||||
});
|
||||
|
||||
const ACTION_TYPES = {
|
||||
WORKFLOW: 'workflow',
|
||||
@@ -85,7 +86,7 @@ const menu = computed(() => {
|
||||
!getResourcePermissions(homeProject.value?.scopes).credential.create,
|
||||
},
|
||||
];
|
||||
if (isFoldersFeatureEnabled.value && isProjectPage.value) {
|
||||
if (showFolders.value) {
|
||||
items.push({
|
||||
value: ACTION_TYPES.FOLDER,
|
||||
label: i18n.baseText('projects.header.create.folder'),
|
||||
|
||||
@@ -20,7 +20,7 @@ import TimeAgo from '@/components/TimeAgo.vue';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { ResourceType } from '@/utils/projects.utils';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
@@ -33,6 +33,7 @@ const WORKFLOW_LIST_ITEM_ACTIONS = {
|
||||
DUPLICATE: 'duplicate',
|
||||
DELETE: 'delete',
|
||||
MOVE: 'move',
|
||||
MOVE_TO_FOLDER: 'moveToFolder',
|
||||
};
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -52,12 +53,14 @@ const emit = defineEmits<{
|
||||
'click:tag': [tagId: string, e: PointerEvent];
|
||||
'workflow:deleted': [];
|
||||
'workflow:active-toggle': [value: { id: string; active: boolean }];
|
||||
'action:move-to-folder': [value: { id: string; name: string; parentFolderId?: string }];
|
||||
}>();
|
||||
|
||||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
const locale = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -69,6 +72,11 @@ const projectsStore = useProjectsStore();
|
||||
const resourceTypeLabel = computed(() => locale.baseText('generic.workflow').toLowerCase());
|
||||
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
|
||||
const workflowPermissions = computed(() => getResourcePermissions(props.data.scopes).workflow);
|
||||
|
||||
const showFolders = computed(() => {
|
||||
return settingsStore.isFoldersFeatureEnabled && route.name !== VIEWS.WORKFLOWS;
|
||||
});
|
||||
|
||||
const actions = computed(() => {
|
||||
const items = [
|
||||
{
|
||||
@@ -88,6 +96,13 @@ const actions = computed(() => {
|
||||
});
|
||||
}
|
||||
|
||||
if (workflowPermissions.value.update && !props.readOnly && showFolders.value) {
|
||||
items.push({
|
||||
label: locale.baseText('folders.actions.moveToFolder'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.MOVE_TO_FOLDER,
|
||||
});
|
||||
}
|
||||
|
||||
if (workflowPermissions.value.move && projectsStore.isTeamProjectFeatureEnabled) {
|
||||
items.push({
|
||||
label: locale.baseText('workflows.item.changeOwner'),
|
||||
@@ -175,6 +190,13 @@ async function onAction(action: string) {
|
||||
case WORKFLOW_LIST_ITEM_ACTIONS.MOVE:
|
||||
moveResource();
|
||||
break;
|
||||
case WORKFLOW_LIST_ITEM_ACTIONS.MOVE_TO_FOLDER:
|
||||
emit('action:move-to-folder', {
|
||||
id: props.data.id,
|
||||
name: props.data.name,
|
||||
parentFolderId: props.data.parentFolder?.id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ const loadPaginationFromQueryString = async () => {
|
||||
<n8n-input
|
||||
ref="search"
|
||||
:model-value="filtersModel.search"
|
||||
:class="[$style['search'], 'mr-2xs']"
|
||||
:class="$style.search"
|
||||
:placeholder="i18n.baseText(`${resourceKey}.search.placeholder` as BaseTextKey)"
|
||||
clearable
|
||||
data-test-id="resources-list-search"
|
||||
|
||||
Reference in New Issue
Block a user