mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Add new create first project CTA (#12189)
Co-authored-by: r00gm <raul00gm@gmail.com>
This commit is contained in:
parent
6e44c71c9c
commit
878b41904d
|
@ -1,10 +1,10 @@
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
|
||||||
import { createRouter, createMemoryHistory } from 'vue-router';
|
import { createRouter, createMemoryHistory } from 'vue-router';
|
||||||
import { createProjectListItem } from '@/__tests__/data/projects';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import ProjectsNavigation from '@/components/Projects//ProjectNavigation.vue';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
|
||||||
import { mockedStore } from '@/__tests__/utils';
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
|
import { createProjectListItem } from '@/__tests__/data/projects';
|
||||||
|
import ProjectsNavigation from '@/components/Projects/ProjectNavigation.vue';
|
||||||
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
|
||||||
vi.mock('vue-router', async () => {
|
vi.mock('vue-router', async () => {
|
||||||
const actual = await vi.importActual('vue-router');
|
const actual = await vi.importActual('vue-router');
|
||||||
|
@ -81,7 +81,7 @@ describe('ProjectsNavigation', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show "Projects" title and Personal project when the feature is enabled', async () => {
|
it('should show "Projects" title and Personal project when the feature is enabled', async () => {
|
||||||
projectsStore.isTeamProjectFeatureEnabled = true;
|
projectsStore.teamProjectsLimit = -1;
|
||||||
projectsStore.myProjects = [...personalProjects, ...teamProjects];
|
projectsStore.myProjects = [...personalProjects, ...teamProjects];
|
||||||
|
|
||||||
const { getByRole, getAllByTestId, getByTestId } = renderComponent({
|
const { getByRole, getAllByTestId, getByTestId } = renderComponent({
|
||||||
|
@ -97,7 +97,7 @@ describe('ProjectsNavigation', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show "Projects" title when the menu is collapsed', async () => {
|
it('should not show "Projects" title when the menu is collapsed', async () => {
|
||||||
projectsStore.isTeamProjectFeatureEnabled = true;
|
projectsStore.teamProjectsLimit = -1;
|
||||||
|
|
||||||
const { queryByRole } = renderComponent({
|
const { queryByRole } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -109,7 +109,7 @@ describe('ProjectsNavigation', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show "Projects" title when the feature is not enabled', async () => {
|
it('should not show "Projects" title when the feature is not enabled', async () => {
|
||||||
projectsStore.isTeamProjectFeatureEnabled = false;
|
projectsStore.teamProjectsLimit = 0;
|
||||||
|
|
||||||
const { queryByRole } = renderComponent({
|
const { queryByRole } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -121,7 +121,7 @@ describe('ProjectsNavigation', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show project icons when the menu is collapsed', async () => {
|
it('should not show project icons when the menu is collapsed', async () => {
|
||||||
projectsStore.isTeamProjectFeatureEnabled = true;
|
projectsStore.teamProjectsLimit = -1;
|
||||||
|
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -132,4 +132,43 @@ describe('ProjectsNavigation', () => {
|
||||||
expect(getByTestId('project-personal-menu-item')).toBeVisible();
|
expect(getByTestId('project-personal-menu-item')).toBeVisible();
|
||||||
expect(getByTestId('project-personal-menu-item').querySelector('svg')).not.toBeInTheDocument();
|
expect(getByTestId('project-personal-menu-item').querySelector('svg')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not show add first project button if there are projects already', async () => {
|
||||||
|
projectsStore.teamProjectsLimit = -1;
|
||||||
|
projectsStore.myProjects = [...teamProjects];
|
||||||
|
|
||||||
|
const { queryByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByTestId('add-first-project-button')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show project plus button and add first project button if user cannot create projects', async () => {
|
||||||
|
projectsStore.teamProjectsLimit = 0;
|
||||||
|
|
||||||
|
const { queryByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByTestId('project-plus-button')).not.toBeInTheDocument();
|
||||||
|
expect(queryByTestId('add-first-project-button')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show project plus button and add first project button if user can create projects', async () => {
|
||||||
|
projectsStore.teamProjectsLimit = -1;
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('project-plus-button')).toBeVisible();
|
||||||
|
expect(getByTestId('add-first-project-button')).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useI18n } from '@/composables/useI18n';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import type { ProjectListItem } from '@/types/projects.types';
|
import type { ProjectListItem } from '@/types/projects.types';
|
||||||
import { sortByProperty } from '@/utils/sortUtils';
|
import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
|
@ -16,6 +16,10 @@ const props = defineProps<Props>();
|
||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
|
const globalEntityCreation = useGlobalEntityCreation();
|
||||||
|
|
||||||
|
const isCreatingProject = computed(() => globalEntityCreation.isCreatingProject.value);
|
||||||
|
const displayProjects = computed(() => globalEntityCreation.displayProjects.value);
|
||||||
|
|
||||||
const home = computed<IMenuItem>(() => ({
|
const home = computed<IMenuItem>(() => ({
|
||||||
id: 'home',
|
id: 'home',
|
||||||
|
@ -50,11 +54,8 @@ const personalProject = computed<IMenuItem>(() => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const displayProjects = computed(() =>
|
const showAddFirstProject = computed(
|
||||||
sortByProperty(
|
() => projectsStore.isTeamProjectFeatureEnabled && !displayProjects.value.length,
|
||||||
'name',
|
|
||||||
projectsStore.myProjects.filter((p) => p.type === 'team'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -72,11 +73,20 @@ const displayProjects = computed(() =>
|
||||||
<hr v-if="projectsStore.isTeamProjectFeatureEnabled" class="mt-m mb-m" />
|
<hr v-if="projectsStore.isTeamProjectFeatureEnabled" class="mt-m mb-m" />
|
||||||
<N8nText
|
<N8nText
|
||||||
v-if="!props.collapsed && projectsStore.isTeamProjectFeatureEnabled"
|
v-if="!props.collapsed && projectsStore.isTeamProjectFeatureEnabled"
|
||||||
:class="$style.projectsLabel"
|
:class="[$style.projectsLabel]"
|
||||||
tag="h3"
|
tag="h3"
|
||||||
bold
|
bold
|
||||||
>
|
>
|
||||||
<span>{{ locale.baseText('projects.menu.title') }}</span>
|
<span>{{ locale.baseText('projects.menu.title') }}</span>
|
||||||
|
<N8nButton
|
||||||
|
v-if="projectsStore.canCreateProjects"
|
||||||
|
icon="plus"
|
||||||
|
text
|
||||||
|
data-test-id="project-plus-button"
|
||||||
|
:disabled="isCreatingProject"
|
||||||
|
:class="$style.plusBtn"
|
||||||
|
@click="globalEntityCreation.createProject"
|
||||||
|
/>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<ElMenu
|
<ElMenu
|
||||||
v-if="projectsStore.isTeamProjectFeatureEnabled"
|
v-if="projectsStore.isTeamProjectFeatureEnabled"
|
||||||
|
@ -103,6 +113,22 @@ const displayProjects = computed(() =>
|
||||||
data-test-id="project-menu-item"
|
data-test-id="project-menu-item"
|
||||||
/>
|
/>
|
||||||
</ElMenu>
|
</ElMenu>
|
||||||
|
<N8nButton
|
||||||
|
v-if="showAddFirstProject"
|
||||||
|
:class="[
|
||||||
|
$style.addFirstProjectBtn,
|
||||||
|
{
|
||||||
|
[$style.collapsed]: props.collapsed,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:disabled="isCreatingProject"
|
||||||
|
type="tertiary"
|
||||||
|
icon="plus"
|
||||||
|
data-test-id="add-first-project-button"
|
||||||
|
@click="globalEntityCreation.createProject"
|
||||||
|
>
|
||||||
|
{{ locale.baseText('projects.menu.addFirstProject') }}
|
||||||
|
</N8nButton>
|
||||||
<hr v-if="projectsStore.isTeamProjectFeatureEnabled" class="mb-m" />
|
<hr v-if="projectsStore.isTeamProjectFeatureEnabled" class="mb-m" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -114,6 +140,11 @@ const displayProjects = computed(() =>
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
&:hover {
|
||||||
|
.plusBtn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.projectItems {
|
.projectItems {
|
||||||
|
@ -132,12 +163,40 @@ const displayProjects = computed(() =>
|
||||||
}
|
}
|
||||||
|
|
||||||
.projectsLabel {
|
.projectsLabel {
|
||||||
margin: 0 var(--spacing-xs) var(--spacing-s);
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0 0 var(--spacing-s) var(--spacing-xs);
|
||||||
padding: 0 var(--spacing-s);
|
padding: 0 var(--spacing-s);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plusBtn {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--color-text-lighter);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addFirstProjectBtn {
|
||||||
|
border: 1px solid var(--color-background-dark);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: var(--spacing-3xs);
|
||||||
|
margin: 0 var(--spacing-m) var(--spacing-m);
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
> span:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
@ -35,6 +35,9 @@ export const useGlobalEntityCreation = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
const isCreatingProject = ref(false);
|
||||||
|
|
||||||
const displayProjects = computed(() =>
|
const displayProjects = computed(() =>
|
||||||
sortByProperty(
|
sortByProperty(
|
||||||
'name',
|
'name',
|
||||||
|
@ -156,6 +159,8 @@ export const useGlobalEntityCreation = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const createProject = async () => {
|
const createProject = async () => {
|
||||||
|
isCreatingProject.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newProject = await projectsStore.createProject({
|
const newProject = await projectsStore.createProject({
|
||||||
name: i18n.baseText('projects.settings.newProjectName'),
|
name: i18n.baseText('projects.settings.newProjectName'),
|
||||||
|
@ -169,6 +174,8 @@ export const useGlobalEntityCreation = () => {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.showError(error, i18n.baseText('projects.error.title'));
|
toast.showError(error, i18n.baseText('projects.error.title'));
|
||||||
|
} finally {
|
||||||
|
isCreatingProject.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -226,5 +233,8 @@ export const useGlobalEntityCreation = () => {
|
||||||
createProjectAppendSlotName,
|
createProjectAppendSlotName,
|
||||||
projectsLimitReachedMessage,
|
projectsLimitReachedMessage,
|
||||||
upgradeLabel,
|
upgradeLabel,
|
||||||
|
createProject,
|
||||||
|
isCreatingProject,
|
||||||
|
displayProjects,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2540,7 +2540,7 @@
|
||||||
"projects.menu.overview": "Overview",
|
"projects.menu.overview": "Overview",
|
||||||
"projects.menu.title": "Projects",
|
"projects.menu.title": "Projects",
|
||||||
"projects.menu.personal": "Personal",
|
"projects.menu.personal": "Personal",
|
||||||
"projects.menu.addProject": "Add project",
|
"projects.menu.addFirstProject": "Add first project",
|
||||||
"projects.settings": "Project settings",
|
"projects.settings": "Project settings",
|
||||||
"projects.settings.newProjectName": "My project",
|
"projects.settings.newProjectName": "My project",
|
||||||
"projects.settings.name": "Project name",
|
"projects.settings.name": "Project name",
|
||||||
|
|
Loading…
Reference in a new issue