mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 08:34:07 -08:00
fix(editor): Move project tabs to project header
This commit is contained in:
parent
0abec0d134
commit
2701a6ed80
|
@ -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);
|
|
@ -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,9 +30,21 @@ 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>
|
||||
<div :class="[$style.projectHeader]">
|
||||
<div :class="[$style.icon]">
|
||||
<N8nIcon :icon="headerIcon" color="text-light"></N8nIcon>
|
||||
|
@ -43,6 +59,8 @@ const projectName = computed(() => {
|
|||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<ProjectTabs :show-settings="showSettings" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue