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 { createComponentRenderer } from '@/__tests__/render';
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 type { Project } 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>>;
describe('ProjectResourceListHeader', () => {
describe('ProjectHeader', () => {
beforeEach(() => {
createTestingPinia();
projectsStore = mockedStore(useProjectsStore);

View file

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

View file

@ -1,22 +1,14 @@
import { createPinia, setActivePinia } from 'pinia';
import { useRoute } from 'vue-router';
import { createComponentRenderer } from '@/__tests__/render';
import { createTestProject } from '@/__tests__/data/projects';
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 push = vi.fn();
return {
...actual,
useRoute: () => ({
params,
}),
useRouter: () => ({
push,
}),
RouterLink: vi.fn(),
};
});
const renderComponent = createComponentRenderer(ProjectTabs, {
@ -29,54 +21,22 @@ const renderComponent = createComponentRenderer(ProjectTabs, {
},
});
let route: ReturnType<typeof useRoute>;
let projectsStore: ReturnType<typeof useProjectsStore>;
describe('ProjectTabs', () => {
beforeEach(() => {
const pinia = createPinia();
setActivePinia(pinia);
route = useRoute();
projectsStore = useProjectsStore();
});
it('should render home tabs', async () => {
const { getByText, queryByText } = renderComponent();
expect(getByText('Workflows')).toBeInTheDocument();
expect(getByText('Credentials')).toBeInTheDocument();
expect(getByText('Executions')).toBeInTheDocument();
expect(queryByText('Project settings')).not.toBeInTheDocument();
});
it('should render project tab Settings if user has permissions and current project is of type Team', () => {
route.params.projectId = '123';
projectsStore.setCurrentProject(createTestProject({ scopes: ['project:update'] }));
const { getByText } = renderComponent();
it('should render project tab Settings', () => {
const { getByText } = renderComponent({ props: { showSettings: true } });
expect(getByText('Workflows')).toBeInTheDocument();
expect(getByText('Credentials')).toBeInTheDocument();
expect(getByText('Executions')).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 { VIEWS } from '@/constants';
import { useI18n } from '@/composables/useI18n';
import { useProjectsStore } from '@/stores/projects.store';
import { getResourcePermissions } from '@/permissions';
import { ProjectTypes } from '@/types/projects.types';
const props = defineProps<{
showSettings?: boolean;
}>();
const locale = useI18n();
const route = useRoute();
const projectsStore = useProjectsStore();
const selectedTab = ref<RouteRecordName | null | undefined>('');
const projectPermissions = computed(
() => getResourcePermissions(projectsStore.currentProject?.scopes).project,
);
const options = computed(() => {
const projectId = route?.params?.projectId;
const to = projectId
@ -63,11 +59,7 @@ const options = computed(() => {
},
];
if (
projectId &&
projectPermissions.value.update &&
projectsStore.currentProject?.type === ProjectTypes.Team
) {
if (props.showSettings) {
tabs.push({
label: locale.baseText('projects.settings'),
value: VIEWS.PROJECT_SETTINGS,

View file

@ -14,8 +14,7 @@ import { useExecutionsStore } from '@/stores/executions.store';
import type { PermissionsRecord } from '@/permissions';
import { getResourcePermissions } from '@/permissions';
import { useSettingsStore } from '@/stores/settings.store';
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
import ProjectResourceListHeader from '@/components/Projects/ProjectResourceListHeader.vue';
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
const props = withDefaults(
defineProps<{
@ -317,8 +316,7 @@ async function onAutoRefreshToggle(value: boolean) {
<template>
<div :class="$style.execListWrapper">
<ProjectResourceListHeader />
<ProjectTabs />
<ProjectHeader />
<div :class="$style.execList">
<div :class="$style.execListHeader">
<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 PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue';
import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue';
import ProjectResourceListHeader from '@/components/Projects/ProjectResourceListHeader.vue';
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
import { useUsersStore } from '@/stores/users.store';
import type { DatatableColumn } from 'n8n-design-system';
import { useI18n } from '@/composables/useI18n';
@ -334,8 +333,7 @@ onMounted(async () => {
<template>
<PageViewLayout>
<template #header>
<ProjectResourceListHeader />
<ProjectTabs />
<ProjectHeader />
<slot name="header" />
</template>
<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 { useTelemetry } from '@/composables/useTelemetry';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import ProjectResourceListHeader from '@/components/Projects/ProjectResourceListHeader.vue';
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
type FormDataDiff = {
name?: string;
@ -261,8 +261,7 @@ onMounted(() => {
<template>
<div :class="$style.projectSettings">
<div :class="$style.header">
<ProjectResourceListHeader />
<ProjectTabs />
<ProjectHeader />
</div>
<form @submit.prevent="onSubmit">
<fieldset>