diff --git a/packages/editor-ui/src/components/layouts/PageViewLayout.vue b/packages/editor-ui/src/components/layouts/PageViewLayout.vue
index abb1ba02a0..f5cd733a15 100644
--- a/packages/editor-ui/src/components/layouts/PageViewLayout.vue
+++ b/packages/editor-ui/src/components/layouts/PageViewLayout.vue
@@ -27,11 +27,11 @@ export default defineComponent({
diff --git a/packages/editor-ui/src/components/layouts/ResourcesListLayout.test.ts b/packages/editor-ui/src/components/layouts/ResourcesListLayout.test.ts
index b7130c0c9a..2b213a97b9 100644
--- a/packages/editor-ui/src/components/layouts/ResourcesListLayout.test.ts
+++ b/packages/editor-ui/src/components/layouts/ResourcesListLayout.test.ts
@@ -1,7 +1,12 @@
-import { setActivePinia, createPinia } from 'pinia';
+import { setActivePinia } from 'pinia';
+import { createTestingPinia } from '@pinia/testing';
+import { within, waitFor } from '@testing-library/vue';
import { createComponentRenderer } from '@/__tests__/render';
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
import type router from 'vue-router';
+import { mockedStore } from '@/__tests__/utils';
+import { useProjectsStore } from '@/stores/projects.store';
+import { type Project, ProjectTypes } from '@/types/projects.types';
vi.mock('vue-router', async (importOriginal) => {
const { RouterLink } = await importOriginal();
@@ -18,7 +23,8 @@ const renderComponent = createComponentRenderer(ResourcesListLayout);
describe('ResourcesListLayout', () => {
beforeEach(() => {
- setActivePinia(createPinia());
+ const pinia = createTestingPinia();
+ setActivePinia(pinia);
});
it('should render loading skeleton', () => {
@@ -30,4 +36,46 @@ describe('ResourcesListLayout', () => {
expect(container.querySelectorAll('.el-skeleton__p')).toHaveLength(25);
});
+
+ describe('header', () => {
+ it('should render the correct icon', async () => {
+ const projects = mockedStore(useProjectsStore);
+ const { getByTestId } = renderComponent();
+
+ expect(getByTestId('list-layout-header').querySelector('.fa-home')).toBeVisible();
+
+ projects.currentProject = { type: ProjectTypes.Personal } as Project;
+
+ await waitFor(() =>
+ expect(getByTestId('list-layout-header').querySelector('.fa-user')).toBeVisible(),
+ );
+
+ const projectName = 'My Project';
+ projects.currentProject = { name: projectName } as Project;
+
+ await waitFor(() =>
+ expect(getByTestId('list-layout-header').querySelector('.fa-layer-group')).toBeVisible(),
+ );
+ });
+
+ it('should render the correct title', async () => {
+ const projects = mockedStore(useProjectsStore);
+ const { getByTestId } = renderComponent();
+
+ expect(within(getByTestId('list-layout-header')).getByText('Home')).toBeVisible();
+
+ projects.currentProject = { type: ProjectTypes.Personal } as Project;
+
+ await waitFor(() =>
+ expect(within(getByTestId('list-layout-header')).getByText('Personal')).toBeVisible(),
+ );
+
+ const projectName = 'My Project';
+ projects.currentProject = { name: projectName } as Project;
+
+ await waitFor(() =>
+ expect(within(getByTestId('list-layout-header')).getByText(projectName)).toBeVisible(),
+ );
+ });
+ });
});
diff --git a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue
index 74f3c7d7d3..aa506f0128 100644
--- a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue
+++ b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue
@@ -2,16 +2,18 @@
import { computed, defineComponent, nextTick, ref, onMounted, watch } from 'vue';
import type { PropType } from 'vue';
-import type { ProjectSharingData } from '@/types/projects.types';
+import { type ProjectSharingData, ProjectTypes } from '@/types/projects.types';
import PageViewLayout from '@/components/layouts/PageViewLayout.vue';
import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue';
import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue';
+import ResourceListHeader from '@/components/layouts/ResourceListHeader.vue';
import { useUsersStore } from '@/stores/users.store';
import type { DatatableColumn } from 'n8n-design-system';
import { useI18n } from '@/composables/useI18n';
import { useDebounce } from '@/composables/useDebounce';
import { useTelemetry } from '@/composables/useTelemetry';
import { useRoute } from 'vue-router';
+import { useProjectsStore } from '@/stores/projects.store';
// eslint-disable-next-line unused-imports/no-unused-imports, @typescript-eslint/no-unused-vars
import type { BaseTextKey } from '@/plugins/i18n';
@@ -44,6 +46,7 @@ export default defineComponent({
PageViewLayout,
PageViewLayoutList,
ResourceFiltersDropdown,
+ ResourceListHeader,
},
props: {
resourceKey: {
@@ -113,6 +116,7 @@ export default defineComponent({
const i18n = useI18n();
const { callDebounced } = useDebounce();
const usersStore = useUsersStore();
+ const projectsStore = useProjectsStore();
const telemetry = useTelemetry();
const sortBy = ref(props.sortOptions[0]);
@@ -339,10 +343,31 @@ export default defineComponent({
}
});
+ const headerIcon = computed(() => {
+ if (projectsStore.currentProject?.type === ProjectTypes.Personal) {
+ return 'user';
+ } else if (projectsStore.currentProject?.name) {
+ return 'layer-group';
+ } else {
+ return 'home';
+ }
+ });
+
+ const projectName = computed(() => {
+ if (!projectsStore.currentProject) {
+ return i18n.baseText('projects.menu.home');
+ } else if (projectsStore.currentProject.type === ProjectTypes.Personal) {
+ return i18n.baseText('projects.menu.personal');
+ } else {
+ return projectsStore.currentProject.name;
+ }
+ });
+
return {
i18n,
search,
usersStore,
+ projectsStore,
filterKeys,
currentPage,
rowsPerPage,
@@ -362,6 +387,8 @@ export default defineComponent({
setCurrentPage,
setRowsPerPage,
onSearch,
+ headerIcon,
+ projectName,
};
},
});
@@ -369,7 +396,14 @@ export default defineComponent({
-
+
+
+
+ {{ projectName }}
+
+
+
+
diff --git a/packages/editor-ui/src/views/ProjectSettings.vue b/packages/editor-ui/src/views/ProjectSettings.vue
index b752fb2b44..9e1b1db418 100644
--- a/packages/editor-ui/src/views/ProjectSettings.vue
+++ b/packages/editor-ui/src/views/ProjectSettings.vue
@@ -8,7 +8,7 @@ import type { IUser } from '@/Interface';
import { useI18n } from '@/composables/useI18n';
import { useProjectsStore } from '@/stores/projects.store';
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
-import type { Project, ProjectRelation } from '@/types/projects.types';
+import { type Project, type ProjectRelation, ProjectTypes } from '@/types/projects.types';
import { useToast } from '@/composables/useToast';
import { VIEWS } from '@/constants';
import ProjectDeleteDialog from '@/components/Projects/ProjectDeleteDialog.vue';
@@ -18,6 +18,7 @@ import type { ProjectRole } from '@/types/roles.types';
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useTelemetry } from '@/composables/useTelemetry';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
+import ResourceListHeader from '@/components/layouts/ResourceListHeader.vue';
type FormDataDiff = {
name?: string;
@@ -247,6 +248,26 @@ watch(
{ immediate: true },
);
+const headerIcon = computed(() => {
+ if (projectsStore.currentProject?.type === ProjectTypes.Personal) {
+ return 'user';
+ } else if (projectsStore.currentProject?.name) {
+ return 'layer-group';
+ } else {
+ return 'home';
+ }
+});
+
+const projectName = computed(() => {
+ if (!projectsStore.currentProject) {
+ return locale.baseText('projects.menu.home');
+ } else if (projectsStore.currentProject.type === ProjectTypes.Personal) {
+ return locale.baseText('projects.menu.personal');
+ } else {
+ return projectsStore.currentProject.name;
+ }
+});
+
onBeforeMount(async () => {
await usersStore.fetchUsers();
});
@@ -260,6 +281,11 @@ onMounted(() => {
+
+
+ {{ projectName }}
+
+