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 { 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);
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue