diff --git a/packages/@n8n/permissions/src/constants.ee.ts b/packages/@n8n/permissions/src/constants.ee.ts
index 7a0ebf2cb1..eda49a605b 100644
--- a/packages/@n8n/permissions/src/constants.ee.ts
+++ b/packages/@n8n/permissions/src/constants.ee.ts
@@ -22,4 +22,5 @@ export const RESOURCES = {
variable: [...DEFAULT_OPERATIONS] as const,
workersView: ['manage'] as const,
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const,
+ folder: ['create', 'read'] as const,
} as const;
diff --git a/packages/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue b/packages/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue
index c1db204000..d556770072 100644
--- a/packages/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue
+++ b/packages/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue
@@ -53,7 +53,7 @@ const hasHiddenItems = computed(() => {
});
const showEllipsis = computed(() => {
- return hasHiddenItems.value || props.pathTruncated;
+ return props.items.length && (hasHiddenItems.value || props.pathTruncated);
});
const dropdownDisabled = computed(() => {
@@ -137,7 +137,9 @@ const handleTooltipClose = () => {
>
- - {{ separator }}
+ -
+ {{ separator }}
+
- {
&.small {
display: inline-flex;
- padding: var(--spacing-4xs) var(--spacing-2xs);
+ padding: var(--spacing-4xs) var(--spacing-3xs);
}
&.border {
@@ -242,6 +244,7 @@ const handleTooltipClose = () => {
.tooltip-ellipsis {
cursor: pointer;
user-select: none;
+ color: var(--color-text-base);
}
&.disabled {
.dots,
diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts
index 883a9b753f..8f238bab7c 100644
--- a/packages/editor-ui/src/Interface.ts
+++ b/packages/editor-ui/src/Interface.ts
@@ -316,6 +316,41 @@ export interface IWorkflowDb {
meta?: WorkflowMetadata;
}
+// For workflow list we don't need the full workflow data
+export type BaseResource = {
+ id: string;
+ name: string;
+};
+
+export type WorkflowListItem = Omit<
+ IWorkflowDb,
+ 'nodes' | 'connections' | 'settings' | 'pinData' | 'versionId' | 'usedCredentials' | 'meta'
+> & {
+ resource: 'workflow';
+ parentFolder?: { id: string; name: string };
+};
+
+export type FolderShortInfo = {
+ id: string;
+ name: string;
+};
+
+export type BaseFolderItem = BaseResource & {
+ createdAt: string;
+ updatedAt: string;
+ workflowCount: number;
+ parentFolder?: FolderShortInfo;
+ homeProject?: ProjectSharingData;
+ sharedWithProjects?: ProjectSharingData[];
+ tags?: ITag[];
+};
+
+export interface FolderListItem extends BaseFolderItem {
+ resource: 'folder';
+}
+
+export type WorkflowListResource = WorkflowListItem | FolderListItem;
+
// Identical to cli.Interfaces.ts
export interface IWorkflowShortResponse {
id: string;
diff --git a/packages/editor-ui/src/api/workflows.ts b/packages/editor-ui/src/api/workflows.ts
index a76e3c1880..1302e1f1f5 100644
--- a/packages/editor-ui/src/api/workflows.ts
+++ b/packages/editor-ui/src/api/workflows.ts
@@ -4,6 +4,7 @@ import type {
IRestApiContext,
IWorkflowDb,
NewWorkflowResponse,
+ WorkflowListResource,
} from '@/Interface';
import type {
ExecutionFilters,
@@ -40,6 +41,20 @@ export async function getWorkflows(context: IRestApiContext, filter?: object, op
});
}
+export async function getWorkflowsAndFolders(
+ context: IRestApiContext,
+ filter?: object,
+ options?: object,
+ includeFolders?: boolean,
+) {
+ return await getFullApiResponse(context, 'GET', '/workflows', {
+ includeScopes: true,
+ includeFolders,
+ ...(filter ? { filter } : {}),
+ ...(options ? options : {}),
+ });
+}
+
export async function getActiveWorkflows(context: IRestApiContext) {
return await makeRestApiRequest(context, 'GET', '/active-workflows');
}
diff --git a/packages/editor-ui/src/components/CredentialCard.test.ts b/packages/editor-ui/src/components/CredentialCard.test.ts
index e21edf0321..e5b0b8b7e9 100644
--- a/packages/editor-ui/src/components/CredentialCard.test.ts
+++ b/packages/editor-ui/src/components/CredentialCard.test.ts
@@ -88,7 +88,8 @@ describe('CredentialCard', () => {
});
it('should set readOnly variant based on prop', () => {
- const { getByRole } = renderComponent({ props: { readOnly: true } });
+ const data = createCredential({});
+ const { getByRole } = renderComponent({ props: { data, readOnly: true } });
const heading = getByRole('heading');
expect(heading).toHaveTextContent('Read only');
});
diff --git a/packages/editor-ui/src/components/CredentialCard.vue b/packages/editor-ui/src/components/CredentialCard.vue
index 4d4dd131f6..5e6566e6aa 100644
--- a/packages/editor-ui/src/components/CredentialCard.vue
+++ b/packages/editor-ui/src/components/CredentialCard.vue
@@ -1,7 +1,6 @@
+
+
+
+
emit('folderOpened', { folder: props.data })">
+
+
+
+
+
+
+ {{ data.name }}
+
+
+
+
+
+ {{ data.workflowCount }} {{ i18n.baseText('generic.workflows') }}
+
+
+ {{ i18n.baseText('workerList.item.lastUpdated') }}
+
+
+
+ {{ i18n.baseText('workflows.item.created') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ projectName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/editor-ui/src/components/Folders/constants.ts b/packages/editor-ui/src/components/Folders/constants.ts
new file mode 100644
index 0000000000..9485fce77d
--- /dev/null
+++ b/packages/editor-ui/src/components/Folders/constants.ts
@@ -0,0 +1,10 @@
+export const FOLDER_LIST_ITEM_ACTIONS = {
+ OPEN: 'open',
+ CREATE: 'create',
+ CREATE_WORKFLOW: 'create_workflow',
+ RENAME: 'rename',
+ MOVE: 'move',
+ CHOWN: 'change_owner',
+ TAGS: 'manage_tags',
+ DELETE: 'delete',
+};
diff --git a/packages/editor-ui/src/components/Projects/ProjectCardBadge.vue b/packages/editor-ui/src/components/Projects/ProjectCardBadge.vue
index fcfd437052..57844522b2 100644
--- a/packages/editor-ui/src/components/Projects/ProjectCardBadge.vue
+++ b/packages/editor-ui/src/components/Projects/ProjectCardBadge.vue
@@ -3,12 +3,12 @@ import { computed } from 'vue';
import { useI18n } from '@/composables/useI18n';
import type { ResourceType } from '@/utils/projects.utils';
import { splitName } from '@/utils/projects.utils';
-import type { ICredentialsResponse, IWorkflowDb } from '@/Interface';
-import type { Project } from '@/types/projects.types';
+import type { Project, ProjectIcon as BadgeIcon } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types';
+import type { CredentialsResource, WorkflowResource } from '../layouts/ResourcesListLayout.vue';
type Props = {
- resource: IWorkflowDb | ICredentialsResponse;
+ resource: WorkflowResource | CredentialsResource;
resourceType: ResourceType;
resourceTypeLabel: string;
personalProject: Project | null;
@@ -68,16 +68,16 @@ const badgeText = computed(() => {
return name ?? email ?? '';
}
});
-const badgeIcon = computed(() => {
+const badgeIcon = computed(() => {
switch (projectState.value) {
case ProjectState.Owned:
case ProjectState.SharedOwned:
- return 'user';
+ return { type: 'icon', value: 'user' };
case ProjectState.Team:
case ProjectState.SharedTeam:
- return 'layer-group';
+ return props.resource.homeProject?.icon ?? { type: 'icon', value: 'layer-group' };
default:
- return '';
+ return { type: 'icon', value: 'layer-group' };
}
});
const badgeTooltip = computed(() => {
@@ -129,12 +129,12 @@ const badgeTooltip = computed(() => {
-
+
{{ badgeText }}
{
}
}
+.projectBadge {
+ & > span {
+ display: flex;
+ gap: var(--spacing-3xs);
+ }
+}
+
.countBadge {
margin-left: -5px;
z-index: 0;
diff --git a/packages/editor-ui/src/components/Projects/ProjectIcon.vue b/packages/editor-ui/src/components/Projects/ProjectIcon.vue
index ae3c47b4e3..7b32c8c556 100644
--- a/packages/editor-ui/src/components/Projects/ProjectIcon.vue
+++ b/packages/editor-ui/src/components/Projects/ProjectIcon.vue
@@ -3,15 +3,17 @@ import type { ProjectIcon } from '@/types/projects.types';
type Props = {
icon: ProjectIcon;
- size?: 'small' | 'medium' | 'large';
+ size?: 'mini' | 'small' | 'medium' | 'large';
round?: boolean;
borderLess?: boolean;
+ color?: 'text-light' | 'text-base' | 'text-dark';
};
const props = withDefaults(defineProps(), {
size: 'medium',
round: false,
borderLess: false,
+ color: 'text-base',
});
@@ -23,9 +25,10 @@ const props = withDefaults(defineProps(), {
]"
>
{{ icon.value }}
@@ -50,6 +53,19 @@ const props = withDefaults(defineProps(), {
}
}
+.mini {
+ width: var(--spacing-xs);
+ height: var(--spacing-xs);
+
+ .icon {
+ font-size: var(--font-size-2xs);
+ }
+
+ .emoji {
+ font-size: var(--font-size-3xs);
+ }
+}
+
.small {
min-width: var(--spacing-l);
height: var(--spacing-l);
diff --git a/packages/editor-ui/src/components/Projects/ProjectTabs.vue b/packages/editor-ui/src/components/Projects/ProjectTabs.vue
index f917a360f3..2b87c4145d 100644
--- a/packages/editor-ui/src/components/Projects/ProjectTabs.vue
+++ b/packages/editor-ui/src/components/Projects/ProjectTabs.vue
@@ -73,6 +73,9 @@ watch(
() => route?.name,
() => {
selectedTab.value = route?.name;
+ // Select workflows tab if folders tab is selected
+ selectedTab.value =
+ route.name === VIEWS.PROJECTS_FOLDERS ? VIEWS.PROJECTS_WORKFLOWS : route.name;
},
{ immediate: true },
);
diff --git a/packages/editor-ui/src/components/WorkflowCard.vue b/packages/editor-ui/src/components/WorkflowCard.vue
index 395685140e..7eaaa099d0 100644
--- a/packages/editor-ui/src/components/WorkflowCard.vue
+++ b/packages/editor-ui/src/components/WorkflowCard.vue
@@ -1,6 +1,6 @@
-
+
{{ data.name }}
@@ -280,12 +299,33 @@ const emitWorkflowActiveToggle = (value: { id: string; active: boolean }) => {
+
+
+
+
+
+ {{
+ projectName
+ }}
+
+
+
+
{
cursor: default;
}
+.home-project span {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-3xs);
+ color: var(--color-text-dark);
+}
+
@include mixins.breakpoint('sm-and-down') {
.cardLink {
--card--padding: 0 var(--spacing-s) var(--spacing-s);
diff --git a/packages/editor-ui/src/components/forms/ResourceFiltersDropdown.vue b/packages/editor-ui/src/components/forms/ResourceFiltersDropdown.vue
index 300da55ca1..7b52eccbab 100644
--- a/packages/editor-ui/src/components/forms/ResourceFiltersDropdown.vue
+++ b/packages/editor-ui/src/components/forms/ResourceFiltersDropdown.vue
@@ -15,12 +15,14 @@ const props = withDefaults(
keys?: string[];
shareable?: boolean;
reset?: () => void;
+ justIcon?: boolean;
}>(),
{
modelValue: () => ({}),
keys: () => [],
shareable: true,
reset: () => {},
+ justIcon: false,
},
);
@@ -112,18 +114,21 @@ onBeforeMount(async () => {
icon="filter"
type="tertiary"
:active="hasFilters"
- :class="$style['filter-button']"
+ :class="{
+ [$style['filter-button']]: true,
+ [$style['no-label']]: justIcon && filtersLength === 0,
+ }"
data-test-id="resources-list-filters-trigger"
>
{{ filtersLength }}
-
+
{{ i18n.baseText('forms.resourceFiltersDropdown.filters') }}
@@ -163,6 +168,12 @@ onBeforeMount(async () => {
height: 40px;
align-items: center;
+ &.no-label {
+ span + span {
+ margin: 0;
+ }
+ }
+
.filter-button-count {
margin-right: var(--spacing-4xs);
diff --git a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue
index edf00b32ec..80c777c8ac 100644
--- a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue
+++ b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue
@@ -14,28 +14,55 @@ import { useRoute, useRouter } from 'vue-router';
import type { BaseTextKey } from '@/plugins/i18n';
import type { Scope } from '@n8n/permissions';
+import type { BaseFolderItem, BaseResource, FolderShortInfo, ITag } from '@/Interface';
+import { isSharedResource, isResourceSortableByDate } from '@/utils/typeGuards';
-export type Resource = {
- id: string;
- name?: string;
- value?: string;
- key?: string;
- updatedAt?: string;
- createdAt?: string;
+type ResourceKeyType = 'credentials' | 'workflows' | 'variables' | 'folders';
+
+export type FolderResource = BaseFolderItem & {
+ resourceType: 'folder';
+ readOnly: boolean;
+};
+
+export type WorkflowResource = BaseResource & {
+ resourceType: 'workflow';
+ updatedAt: string;
+ createdAt: string;
+ active: boolean;
homeProject?: ProjectSharingData;
scopes?: Scope[];
- type?: string;
+ tags?: ITag[] | string[];
sharedWithProjects?: ProjectSharingData[];
+ readOnly: boolean;
+ parentFolder?: FolderShortInfo;
};
+export type VariableResource = BaseResource & {
+ resourceType: 'variable';
+ key?: string;
+ value?: string;
+};
+
+export type CredentialsResource = BaseResource & {
+ resourceType: 'credential';
+ updatedAt: string;
+ createdAt: string;
+ type: string;
+ homeProject?: ProjectSharingData;
+ scopes?: Scope[];
+ sharedWithProjects?: ProjectSharingData[];
+ readOnly: boolean;
+ needsSetup: boolean;
+};
+
+export type Resource = WorkflowResource | FolderResource | CredentialsResource | VariableResource;
+
export type BaseFilters = {
search: string;
homeProject: string;
[key: string]: boolean | string | string[];
};
-type ResourceKeyType = 'credentials' | 'workflows' | 'variables';
-
const route = useRoute();
const router = useRouter();
const i18n = useI18n();
@@ -106,7 +133,7 @@ const emit = defineEmits<{
'update:search': [value: string];
}>();
-defineSlots<{
+const slots = defineSlots<{
header(): unknown;
empty(): unknown;
preamble(): unknown;
@@ -120,6 +147,7 @@ defineSlots<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default(props: { data: any; updateItemSize: (data: any) => void }): unknown;
item(props: { item: unknown; index: number }): unknown;
+ breadcrumbs(): unknown;
}>();
//computed
@@ -130,6 +158,7 @@ const filtersModel = computed({
const showEmptyState = computed(() => {
return (
+ route.params.folderId === undefined &&
props.resources.length === 0 &&
// Don't show empty state if resources are refreshing or if filters are being set
!hasFilters.value &&
@@ -149,7 +178,7 @@ const filteredAndSortedResources = computed(() => {
const filtered = props.resources.filter((resource) => {
let matches = true;
- if (filtersModel.value.homeProject) {
+ if (filtersModel.value.homeProject && isSharedResource(resource)) {
matches =
matches &&
!!(resource.homeProject && resource.homeProject.id === filtersModel.value.homeProject);
@@ -168,12 +197,19 @@ const filteredAndSortedResources = computed(() => {
});
return filtered.sort((a, b) => {
+ const sortableByDate = isResourceSortableByDate(a) && isResourceSortableByDate(b);
switch (sortBy.value) {
case 'lastUpdated':
+ if (!sortableByDate) {
+ return 0;
+ }
return props.sortFns.lastUpdated
? props.sortFns.lastUpdated(a, b)
: new Date(b.updatedAt ?? '').valueOf() - new Date(a.updatedAt ?? '').valueOf();
case 'lastCreated':
+ if (!sortableByDate) {
+ return 0;
+ }
return props.sortFns.lastCreated
? props.sortFns.lastCreated(a, b)
: new Date(b.createdAt ?? '').valueOf() - new Date(a.createdAt ?? '').valueOf();
@@ -494,6 +530,7 @@ const loadPaginationFromQueryString = async () => {
+
{
-
-
-
-
-
{
/>
+
+
+
+
+
@@ -630,14 +668,16 @@ const loadPaginationFromQueryString = async () => {
align-items: center;
justify-content: space-between;
width: 100%;
+ gap: var(--spacing-2xs);
}
.filters {
display: grid;
grid-auto-flow: column;
- grid-auto-columns: max-content;
+ grid-auto-columns: 1fr max-content max-content max-content;
gap: var(--spacing-2xs);
align-items: center;
+ justify-content: end;
width: 100%;
@include mixins.breakpoint('xs-only') {
@@ -651,7 +691,7 @@ const loadPaginationFromQueryString = async () => {
}
.search {
- max-width: 240px;
+ // max-width: 240px;
@include mixins.breakpoint('sm-and-down') {
max-width: 100%;
diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts
index c630d2d837..6bb69b33fd 100644
--- a/packages/editor-ui/src/constants.ts
+++ b/packages/editor-ui/src/constants.ts
@@ -519,6 +519,8 @@ export const enum VIEWS {
PROJECTS_CREDENTIALS = 'ProjectsCredentials',
PROJECT_SETTINGS = 'ProjectSettings',
PROJECTS_EXECUTIONS = 'ProjectsExecutions',
+ FOLDERS = 'Folders',
+ PROJECTS_FOLDERS = 'ProjectsFolders',
}
export const EDITABLE_CANVAS_VIEWS = [VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW, VIEWS.EXECUTION_DEBUG];
diff --git a/packages/editor-ui/src/permissions.test.ts b/packages/editor-ui/src/permissions.test.ts
index ab3952fbeb..42857fa9a2 100644
--- a/packages/editor-ui/src/permissions.test.ts
+++ b/packages/editor-ui/src/permissions.test.ts
@@ -27,6 +27,7 @@ describe('permissions', () => {
variable: {},
workersView: {},
workflow: {},
+ folder: {},
});
});
it('getResourcePermissions', () => {
@@ -115,6 +116,7 @@ describe('permissions', () => {
share: true,
update: true,
},
+ folder: {},
};
expect(getResourcePermissions(scopes)).toEqual(permissionRecord);
diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json
index 057ed8ba86..83eecad282 100644
--- a/packages/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/editor-ui/src/plugins/i18n/locales/en.json
@@ -85,6 +85,7 @@
"generic.variable_plural": "Variables",
"generic.variable": "Variable | {count} Variables",
"generic.viewDocs": "View docs",
+ "generic.workflows": "Workflows",
"about.aboutN8n": "About n8n",
"about.close": "Close",
"about.license": "License",
@@ -2329,7 +2330,7 @@
"workflows.item.updated": "Last updated",
"workflows.item.created": "Created",
"workflows.item.readonly": "Read only",
- "workflows.search.placeholder": "Search workflows...",
+ "workflows.search.placeholder": "Search",
"workflows.filters": "Filters",
"workflows.filters.tags": "Tags",
"workflows.filters.status": "Status",
diff --git a/packages/editor-ui/src/plugins/icons/index.ts b/packages/editor-ui/src/plugins/icons/index.ts
index 343a1cec83..b0f983a992 100644
--- a/packages/editor-ui/src/plugins/icons/index.ts
+++ b/packages/editor-ui/src/plugins/icons/index.ts
@@ -71,6 +71,7 @@ import {
faFilter,
faFingerprint,
faFlask,
+ faFolder,
faFolderOpen,
faFont,
faGlobeAmericas,
@@ -252,6 +253,7 @@ export const FontAwesomePlugin: Plugin = {
addIcon(faFilter);
addIcon(faFingerprint);
addIcon(faFlask);
+ addIcon(faFolder);
addIcon(faFolderOpen);
addIcon(faFont);
addIcon(faGift);
diff --git a/packages/editor-ui/src/routes/projects.routes.ts b/packages/editor-ui/src/routes/projects.routes.ts
index d9258785a6..c216f94c8d 100644
--- a/packages/editor-ui/src/routes/projects.routes.ts
+++ b/packages/editor-ui/src/routes/projects.routes.ts
@@ -58,6 +58,19 @@ const commonChildRoutes: RouteRecordRaw[] = [
},
},
},
+ {
+ path: 'folders/:folderId?/workflows',
+ components: {
+ default: WorkflowsView,
+ sidebar: MainSidebar,
+ },
+ meta: {
+ middleware: ['authenticated', 'custom'],
+ middlewareOptions: {
+ custom: (options) => checkProjectAvailability(options?.to),
+ },
+ },
+ },
];
const commonChildRouteExtensions = {
@@ -71,6 +84,9 @@ const commonChildRouteExtensions = {
{
name: VIEWS.EXECUTIONS,
},
+ {
+ name: VIEWS.FOLDERS,
+ },
],
projects: [
{
@@ -82,6 +98,9 @@ const commonChildRouteExtensions = {
{
name: VIEWS.PROJECTS_EXECUTIONS,
},
+ {
+ name: VIEWS.PROJECTS_FOLDERS,
+ },
],
};
diff --git a/packages/editor-ui/src/stores/rbac.store.ts b/packages/editor-ui/src/stores/rbac.store.ts
index a38b0f674b..b91f32005c 100644
--- a/packages/editor-ui/src/stores/rbac.store.ts
+++ b/packages/editor-ui/src/stores/rbac.store.ts
@@ -34,6 +34,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => {
logStreaming: {},
saml: {},
securityAudit: {},
+ folder: {},
});
function addGlobalRole(role: IRole) {
diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts
index bf6d58c38c..39753532b2 100644
--- a/packages/editor-ui/src/stores/workflows.store.ts
+++ b/packages/editor-ui/src/stores/workflows.store.ts
@@ -31,6 +31,7 @@ import type {
IExecutionFlattedResponse,
IWorkflowTemplateNode,
IWorkflowDataCreate,
+ WorkflowListResource,
} from '@/Interface';
import { defineStore } from 'pinia';
import type {
@@ -483,8 +484,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
page = 1,
pageSize = DEFAULT_WORKFLOW_PAGE_SIZE,
sortBy?: string,
- filters: { name?: string; tags?: string[]; active?: boolean } = {},
- ): Promise {
+ filters: { name?: string; tags?: string[]; active?: boolean; parentFolderId?: string } = {},
+ includeFolders: boolean = false,
+ ): Promise {
const filter = { ...filters, projectId };
const options = {
skip: (page - 1) * pageSize,
@@ -492,13 +494,13 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
sortBy,
};
- const { count, data } = await workflowsApi.getWorkflows(
+ const { count, data } = await workflowsApi.getWorkflowsAndFolders(
rootStore.restApiContext,
Object.keys(filter).length ? filter : undefined,
Object.keys(options).length ? options : undefined,
+ includeFolders ? includeFolders : undefined,
);
- setWorkflows(data);
totalWorkflowCount.value = count;
return data;
}
diff --git a/packages/editor-ui/src/utils/typeGuards.ts b/packages/editor-ui/src/utils/typeGuards.ts
index fed8d837ea..4e050b2f87 100644
--- a/packages/editor-ui/src/utils/typeGuards.ts
+++ b/packages/editor-ui/src/utils/typeGuards.ts
@@ -11,6 +11,13 @@ import type { RouteLocationRaw } from 'vue-router';
import type { CanvasConnectionMode } from '@/types';
import { canvasConnectionModes } from '@/types';
import type { ComponentPublicInstance } from 'vue';
+import type {
+ CredentialsResource,
+ FolderResource,
+ Resource,
+ VariableResource,
+ WorkflowResource,
+} from '@/components/layouts/ResourcesListLayout.vue';
/*
Type guards used in editor-ui project
@@ -98,3 +105,31 @@ export function isRouteLocationRaw(value: unknown): value is RouteLocationRaw {
export function isComponentPublicInstance(value: unknown): value is ComponentPublicInstance {
return value !== null && typeof value === 'object' && '$props' in value;
}
+
+export function isWorkflowResource(value: Resource): value is WorkflowResource {
+ return value.resourceType === 'workflow';
+}
+
+export function isFolderResource(value: Resource): value is FolderResource {
+ return value.resourceType === 'folder';
+}
+
+export function isVariableResource(value: Resource): value is VariableResource {
+ return value.resourceType === 'variable';
+}
+
+export function isCredentialsResource(value: Resource): value is CredentialsResource {
+ return value.resourceType === 'credential';
+}
+
+export function isSharedResource(
+ value: Resource,
+): value is WorkflowResource | FolderResource | CredentialsResource {
+ return isWorkflowResource(value) || isFolderResource(value) || isCredentialsResource(value);
+}
+
+export function isResourceSortableByDate(
+ value: Resource,
+): value is WorkflowResource | FolderResource | CredentialsResource {
+ return isWorkflowResource(value) || isFolderResource(value) || isCredentialsResource(value);
+}
diff --git a/packages/editor-ui/src/views/CredentialsView.vue b/packages/editor-ui/src/views/CredentialsView.vue
index a8c6282d67..f520329696 100644
--- a/packages/editor-ui/src/views/CredentialsView.vue
+++ b/packages/editor-ui/src/views/CredentialsView.vue
@@ -1,7 +1,7 @@
@@ -434,7 +549,7 @@ const onWorkflowActiveToggle = (data: { id: string; active: boolean }) => {
v-model:filters="filters"
resource-key="workflows"
type="list-paginated"
- :resources="workflowResources"
+ :resources="workflowListResources"
:type-props="{ itemSize: 80 }"
:shareable="isShareable"
:initialize="initialize"
@@ -483,11 +598,36 @@ const onWorkflowActiveToggle = (data: { id: string; active: boolean }) => {
+
+
+
+
+
+ {{ projectName }}
+
+
+
+
+
+
{
transition: color 0.3s ease;
}
}
+
+.home-project {
+ display: flex;
+ align-items: center;
+}