fix(editor): Move project tabs to project header

This commit is contained in:
Csaba Tuncsik 2024-11-08 07:10:12 +01:00
parent 0abec0d134
commit 2701a6ed80
No known key found for this signature in database
7 changed files with 65 additions and 86 deletions

View file

@ -1,16 +1,30 @@
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { mockedStore } from '@/__tests__/utils'; import { mockedStore } from '@/__tests__/utils';
import ProjectResourceListHeader from '@/components/Projects/ProjectResourceListHeader.vue'; import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
import { useProjectsStore } from '@/stores/projects.store'; import { useProjectsStore } from '@/stores/projects.store';
import type { Project } from '@/types/projects.types'; import type { Project } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types'; import { ProjectTypes } from '@/types/projects.types';
const renderComponent = createComponentRenderer(ProjectResourceListHeader); vi.mock('vue-router', async () => {
const actual = await vi.importActual('vue-router');
return {
...actual,
useRoute: () => ({
location: {},
}),
};
});
const renderComponent = createComponentRenderer(ProjectHeader, {
global: {
stubs: ['ProjectTabs'],
},
});
let projectsStore: ReturnType<typeof mockedStore<typeof useProjectsStore>>; let projectsStore: ReturnType<typeof mockedStore<typeof useProjectsStore>>;
describe('ProjectResourceListHeader', () => { describe('ProjectHeader', () => {
beforeEach(() => { beforeEach(() => {
createTestingPinia(); createTestingPinia();
projectsStore = mockedStore(useProjectsStore); projectsStore = mockedStore(useProjectsStore);

View file

@ -1,9 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { ProjectTypes } from '@/types/projects.types'; import { ProjectTypes } from '@/types/projects.types';
import { useProjectsStore } from '@/stores/projects.store'; import { useProjectsStore } from '@/stores/projects.store';
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
import { getResourcePermissions } from '@/permissions';
const route = useRoute();
const i18n = useI18n(); const i18n = useI18n();
const projectsStore = useProjectsStore(); const projectsStore = useProjectsStore();
@ -26,22 +30,36 @@ const projectName = computed(() => {
return projectsStore.currentProject.name; return projectsStore.currentProject.name;
} }
}); });
const projectPermissions = computed(
() => getResourcePermissions(projectsStore.currentProject?.scopes).project,
);
const showSettings = computed(
() =>
!!route?.params?.projectId &&
projectPermissions.value.update &&
projectsStore.currentProject?.type === ProjectTypes.Team,
);
</script> </script>
<template> <template>
<div :class="[$style.projectHeader]"> <div>
<div :class="[$style.icon]"> <div :class="[$style.projectHeader]">
<N8nIcon :icon="headerIcon" color="text-light"></N8nIcon> <div :class="[$style.icon]">
</div> <N8nIcon :icon="headerIcon" color="text-light"></N8nIcon>
<div> </div>
<N8nHeading bold tag="h2" size="xlarge">{{ projectName }}</N8nHeading> <div>
<N8nText v-if="$slots.subtitle" size="small" color="text-light"> <N8nHeading bold tag="h2" size="xlarge">{{ projectName }}</N8nHeading>
<slot name="subtitle" /> <N8nText v-if="$slots.subtitle" size="small" color="text-light">
</N8nText> <slot name="subtitle" />
</div> </N8nText>
<div v-if="$slots.actions" :class="[$style.actions]"> </div>
<slot name="actions"></slot> <div v-if="$slots.actions" :class="[$style.actions]">
<slot name="actions"></slot>
</div>
</div> </div>
<ProjectTabs :show-settings="showSettings" />
</div> </div>
</template> </template>

View file

@ -1,22 +1,14 @@
import { createPinia, setActivePinia } from 'pinia';
import { useRoute } from 'vue-router';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { createTestProject } from '@/__tests__/data/projects';
import ProjectTabs from '@/components/Projects/ProjectTabs.vue'; import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
import { useProjectsStore } from '@/stores/projects.store';
import { ProjectTypes } from '@/types/projects.types';
vi.mock('vue-router', () => { vi.mock('vue-router', async () => {
const actual = await vi.importActual('vue-router');
const params = {}; const params = {};
const push = vi.fn();
return { return {
...actual,
useRoute: () => ({ useRoute: () => ({
params, params,
}), }),
useRouter: () => ({
push,
}),
RouterLink: vi.fn(),
}; };
}); });
const renderComponent = createComponentRenderer(ProjectTabs, { const renderComponent = createComponentRenderer(ProjectTabs, {
@ -29,54 +21,22 @@ const renderComponent = createComponentRenderer(ProjectTabs, {
}, },
}); });
let route: ReturnType<typeof useRoute>;
let projectsStore: ReturnType<typeof useProjectsStore>;
describe('ProjectTabs', () => { describe('ProjectTabs', () => {
beforeEach(() => {
const pinia = createPinia();
setActivePinia(pinia);
route = useRoute();
projectsStore = useProjectsStore();
});
it('should render home tabs', async () => { it('should render home tabs', async () => {
const { getByText, queryByText } = renderComponent(); const { getByText, queryByText } = renderComponent();
expect(getByText('Workflows')).toBeInTheDocument(); expect(getByText('Workflows')).toBeInTheDocument();
expect(getByText('Credentials')).toBeInTheDocument(); expect(getByText('Credentials')).toBeInTheDocument();
expect(getByText('Executions')).toBeInTheDocument();
expect(queryByText('Project settings')).not.toBeInTheDocument(); expect(queryByText('Project settings')).not.toBeInTheDocument();
}); });
it('should render project tab Settings if user has permissions and current project is of type Team', () => { it('should render project tab Settings', () => {
route.params.projectId = '123'; const { getByText } = renderComponent({ props: { showSettings: true } });
projectsStore.setCurrentProject(createTestProject({ scopes: ['project:update'] }));
const { getByText } = renderComponent();
expect(getByText('Workflows')).toBeInTheDocument(); expect(getByText('Workflows')).toBeInTheDocument();
expect(getByText('Credentials')).toBeInTheDocument(); expect(getByText('Credentials')).toBeInTheDocument();
expect(getByText('Executions')).toBeInTheDocument();
expect(getByText('Project settings')).toBeInTheDocument(); expect(getByText('Project settings')).toBeInTheDocument();
}); });
it('should render project tabs without Settings if no permission', () => {
route.params.projectId = '123';
projectsStore.setCurrentProject(createTestProject({ scopes: ['project:read'] }));
const { queryByText, getByText } = renderComponent();
expect(getByText('Workflows')).toBeInTheDocument();
expect(getByText('Credentials')).toBeInTheDocument();
expect(queryByText('Project settings')).not.toBeInTheDocument();
});
it('should render project tabs without Settings if project is the Personal project', () => {
route.params.projectId = '123';
projectsStore.setCurrentProject(
createTestProject({ type: ProjectTypes.Personal, scopes: ['project:update'] }),
);
const { queryByText, getByText } = renderComponent();
expect(getByText('Workflows')).toBeInTheDocument();
expect(getByText('Credentials')).toBeInTheDocument();
expect(queryByText('Project settings')).not.toBeInTheDocument();
});
}); });

View file

@ -4,19 +4,15 @@ import type { RouteRecordName } from 'vue-router';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useProjectsStore } from '@/stores/projects.store';
import { getResourcePermissions } from '@/permissions'; const props = defineProps<{
import { ProjectTypes } from '@/types/projects.types'; showSettings?: boolean;
}>();
const locale = useI18n(); const locale = useI18n();
const route = useRoute(); const route = useRoute();
const projectsStore = useProjectsStore();
const selectedTab = ref<RouteRecordName | null | undefined>(''); const selectedTab = ref<RouteRecordName | null | undefined>('');
const projectPermissions = computed(
() => getResourcePermissions(projectsStore.currentProject?.scopes).project,
);
const options = computed(() => { const options = computed(() => {
const projectId = route?.params?.projectId; const projectId = route?.params?.projectId;
const to = projectId const to = projectId
@ -63,11 +59,7 @@ const options = computed(() => {
}, },
]; ];
if ( if (props.showSettings) {
projectId &&
projectPermissions.value.update &&
projectsStore.currentProject?.type === ProjectTypes.Team
) {
tabs.push({ tabs.push({
label: locale.baseText('projects.settings'), label: locale.baseText('projects.settings'),
value: VIEWS.PROJECT_SETTINGS, value: VIEWS.PROJECT_SETTINGS,

View file

@ -14,8 +14,7 @@ import { useExecutionsStore } from '@/stores/executions.store';
import type { PermissionsRecord } from '@/permissions'; import type { PermissionsRecord } from '@/permissions';
import { getResourcePermissions } from '@/permissions'; import { getResourcePermissions } from '@/permissions';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import ProjectTabs from '@/components/Projects/ProjectTabs.vue'; import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
import ProjectResourceListHeader from '@/components/Projects/ProjectResourceListHeader.vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -317,8 +316,7 @@ async function onAutoRefreshToggle(value: boolean) {
<template> <template>
<div :class="$style.execListWrapper"> <div :class="$style.execListWrapper">
<ProjectResourceListHeader /> <ProjectHeader />
<ProjectTabs />
<div :class="$style.execList"> <div :class="$style.execList">
<div :class="$style.execListHeader"> <div :class="$style.execListHeader">
<div :class="$style.execListHeaderControls"> <div :class="$style.execListHeaderControls">

View file

@ -5,8 +5,7 @@ import { type ProjectSharingData } from '@/types/projects.types';
import PageViewLayout from '@/components/layouts/PageViewLayout.vue'; import PageViewLayout from '@/components/layouts/PageViewLayout.vue';
import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue'; import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue';
import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue'; import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue';
import ProjectResourceListHeader from '@/components/Projects/ProjectResourceListHeader.vue'; import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import type { DatatableColumn } from 'n8n-design-system'; import type { DatatableColumn } from 'n8n-design-system';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
@ -334,8 +333,7 @@ onMounted(async () => {
<template> <template>
<PageViewLayout> <PageViewLayout>
<template #header> <template #header>
<ProjectResourceListHeader /> <ProjectHeader />
<ProjectTabs />
<slot name="header" /> <slot name="header" />
</template> </template>
<div v-if="loading" class="resource-list-loading"> <div v-if="loading" class="resource-list-loading">

View file

@ -18,7 +18,7 @@ import type { ProjectRole } from '@/types/roles.types';
import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useDocumentTitle } from '@/composables/useDocumentTitle';
import ProjectResourceListHeader from '@/components/Projects/ProjectResourceListHeader.vue'; import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
type FormDataDiff = { type FormDataDiff = {
name?: string; name?: string;
@ -261,8 +261,7 @@ onMounted(() => {
<template> <template>
<div :class="$style.projectSettings"> <div :class="$style.projectSettings">
<div :class="$style.header"> <div :class="$style.header">
<ProjectResourceListHeader /> <ProjectHeader />
<ProjectTabs />
</div> </div>
<form @submit.prevent="onSubmit"> <form @submit.prevent="onSubmit">
<fieldset> <fieldset>