Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(editor): Project related frontend fixes (no-changelog) #9482

Merged
75 changes: 73 additions & 2 deletions cypress/e2e/39-projects.cy.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import { INSTANCE_ADMIN, INSTANCE_MEMBERS } from '../constants';
import { WorkflowsPage, WorkflowPage, CredentialsModal, CredentialsPage } from '../pages';
import {
WorkflowsPage,
WorkflowPage,
CredentialsModal,
CredentialsPage,
WorkflowExecutionsTab,
} from '../pages';
import * as projects from '../composables/projects';

const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
const executionsTab = new WorkflowExecutionsTab();

describe('Projects', () => {
beforeEach(() => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
});

it('should handle workflows and credentials', () => {
it('should handle workflows and credentials and menu items', () => {
cy.signin(INSTANCE_ADMIN);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('not.have.length');
Expand Down Expand Up @@ -147,5 +155,68 @@ describe('Projects', () => {
cy.wait('@credentialsList').then((interception) => {
expect(interception.request.url).not.to.contain('filter');
});

let menuItems = cy.getByTestId('menu-item');

menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Home")[class*=active_]').should('exist');

projects.getMenuItems().first().click();

menuItems = cy.getByTestId('menu-item');

menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');

cy.intercept('GET', '/rest/workflows/*').as('loadWorkflow');
workflowsPage.getters.workflowCards().first().click();

cy.wait('@loadWorkflow');
menuItems = cy.getByTestId('menu-item');

menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');

cy.intercept('GET', '/rest/executions*').as('loadExecutions');
executionsTab.actions.switchToExecutionsTab();

cy.wait('@loadExecutions');
menuItems = cy.getByTestId('menu-item');

menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');

executionsTab.actions.switchToEditorTab();

menuItems = cy.getByTestId('menu-item');

menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');

cy.getByTestId('menu-item').filter(':contains("Variables")').click();
cy.getByTestId('unavailable-resources-list').should('be.visible');

menuItems = cy.getByTestId('menu-item');

menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Variables")[class*=active_]').should('exist');

projects.getHomeButton().click();
menuItems = cy.getByTestId('menu-item');

menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Home")[class*=active_]').should('exist');

workflowsPage.getters.workflowCards().should('have.length', 2).first().click();

cy.wait('@loadWorkflow');
cy.getByTestId('execute-workflow-button').should('be.visible');

menuItems = cy.getByTestId('menu-item');
menuItems.filter(':contains("Home")[class*=active_]').should('not.exist');

menuItems = cy.getByTestId('menu-item');
menuItems.filter('[class*=active_]').should('have.length', 1);
menuItems.filter(':contains("Development")[class*=active_]').should('exist');
});
});
5 changes: 4 additions & 1 deletion packages/editor-ui/src/__tests__/data/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import type {
ProjectSharingData,
ProjectType,
} from '@/features/projects/projects.types';
import { ProjectTypes } from '@/features/projects/projects.utils';

export const createProjectSharingData = (projectType?: ProjectType): ProjectSharingData => ({
id: faker.string.uuid(),
name: faker.lorem.words({ min: 1, max: 3 }),
type: projectType || 'personal',
type: projectType ?? ProjectTypes.Personal,
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.recent().toISOString(),
});

export const createProjectListItem = (projectType?: ProjectType): ProjectListItem => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import type { CredentialScope } from '@n8n/permissions';
import type { EventBus } from 'n8n-design-system/utils';
import { useRolesStore } from '@/stores/roles.store';
import type { RoleMap } from '@/types/roles.types';
import { ProjectTypes } from '@/features/projects/projects.utils';

export default defineComponent({
name: 'CredentialSharing',
Expand Down Expand Up @@ -178,7 +179,7 @@ export default defineComponent({
);
},
isHomeTeamProject(): boolean {
return this.homeProject?.type === 'team';
return this.homeProject?.type === ProjectTypes.Team;
},
numberOfMembersInHomeTeamProject(): number {
return this.teamProject?.relations.length ?? 0;
Expand Down
3 changes: 2 additions & 1 deletion packages/editor-ui/src/components/WorkflowSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ import type { WorkflowScope } from '@n8n/permissions';
import { getWorkflowPermissions } from '@/permissions';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { ProjectTypes } from '@/features/projects/projects.utils';

export default defineComponent({
name: 'WorkflowSettings',
Expand Down Expand Up @@ -604,7 +605,7 @@ export default defineComponent({
{
key: 'workflowsFromSameOwner',
value: this.$locale.baseText(
this.workflow.homeProject?.type === 'personal'
this.workflow.homeProject?.type === ProjectTypes.Personal
? 'workflowSettings.callerPolicy.options.workflowsFromPersonalProject'
: 'workflowSettings.callerPolicy.options.workflowsFromTeamProject',
{
Expand Down
3 changes: 2 additions & 1 deletion packages/editor-ui/src/components/WorkflowShareModal.ee.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ import type {
} from '@/features/projects/projects.types';
import { useRolesStore } from '@/stores/roles.store';
import type { RoleMap } from '@/types/roles.types';
import { ProjectTypes } from '@/features/projects/projects.utils';

export default defineComponent({
name: 'WorkflowShareModal',
Expand Down Expand Up @@ -238,7 +239,7 @@ export default defineComponent({
);
},
isHomeTeamProject(): boolean {
return this.workflow.homeProject?.type === 'team';
return this.workflow.homeProject?.type === ProjectTypes.Team;
},
numberOfMembersInHomeTeamProject(): number {
return this.teamProject?.relations.length ?? 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
class="pt-2xs"
:projects="projectsStore.projects"
:placeholder="$locale.baseText('forms.resourceFiltersDropdown.owner.placeholder')"
:empty-options-text="$locale.baseText('projects.sharing.noMatchingProjects')"
@update:model-value="setKeyValue('homeProject', ($event as ProjectSharingData).id)"
/>
</enterprise-edition>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useI18n } from '@/composables/useI18n';
import { splitName } from '@/features/projects/projects.utils';
import { ProjectTypes, splitName } from '@/features/projects/projects.utils';
import type { ICredentialsResponse, IWorkflowDb } from '@/Interface';
import type { Project } from '@/features/projects/projects.types';

Expand Down Expand Up @@ -29,9 +29,12 @@ const badgeText = computed(() => {
});

const badgeIcon = computed(() => {
if (props.resource.sharedWithProjects?.length && props.resource.homeProject?.type !== 'team') {
if (
props.resource.sharedWithProjects?.length &&
props.resource.homeProject?.type !== ProjectTypes.Team
) {
return 'user-friends';
} else if (props.resource.homeProject?.type === 'team') {
} else if (props.resource.homeProject?.type === ProjectTypes.Team) {
return 'archive';
} else {
return '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ const onDelete = () => {
<n8n-text color="text-dark">{{
locale.baseText('projects.settings.delete.question.transfer.title')
}}</n8n-text>
<ProjectSharing v-model="selectedProject" class="pt-2xs" :projects="props.projects" />
<ProjectSharing
v-model="selectedProject"
class="pt-2xs"
:projects="props.projects"
:empty-options-text="locale.baseText('projects.sharing.noMatchingProjects')"
/>
</div>

<el-radio
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { ref, computed, onMounted, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useRouter } from 'vue-router';
import type { IMenuItem } from 'n8n-design-system/types';
import { useI18n } from '@/composables/useI18n';
import { VIEWS } from '@/constants';
Expand All @@ -16,7 +16,6 @@ type Props = {

const props = defineProps<Props>();

const route = useRoute();
const router = useRouter();
const locale = useI18n();
const toast = useToast();
Expand All @@ -42,24 +41,6 @@ const addProject = computed<IMenuItem>(() => ({
isLoading: isCreatingProject.value,
}));

const activeTab = computed(() => {
let routes = [VIEWS.HOMEPAGE, VIEWS.WORKFLOWS, VIEWS.CREDENTIALS];
if (projectsStore.currentProjectId === undefined) {
routes = [
...routes,
VIEWS.NEW_WORKFLOW,
VIEWS.WORKFLOW_HISTORY,
VIEWS.WORKFLOW,
VIEWS.EXECUTION_HOME,
];
}
return routes.includes(route.name as VIEWS) ? 'home' : undefined;
});

const isActiveProject = (projectId: string) =>
route?.params?.projectId === projectId || projectsStore.currentProjectId === projectId
? projectId
: undefined;
const getProjectMenuItem = (project: ProjectListItem) => ({
id: project.id,
label: project.name,
Expand Down Expand Up @@ -127,7 +108,7 @@ onMounted(async () => {
:item="home"
:compact="props.collapsed"
:handle-select="homeClicked"
:active-tab="activeTab"
:active-tab="projectsStore.projectNavActiveId"
mode="tabs"
data-test-id="project-home-menu-item"
/>
Expand All @@ -146,7 +127,7 @@ onMounted(async () => {
:item="getProjectMenuItem(project)"
:compact="props.collapsed"
:handle-select="projectClicked"
:active-tab="isActiveProject(project.id)"
:active-tab="projectsStore.projectNavActiveId"
mode="tabs"
data-test-id="project-menu-item"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Props = {
readonly?: boolean;
static?: boolean;
placeholder?: string;
emptyOptionsText?: string;
};

const props = defineProps<Props>();
Expand All @@ -34,6 +35,9 @@ const selectPlaceholder = computed(
? locale.baseText('projects.sharing.placeholder')
: locale.baseText('projects.sharing.placeholder.single')),
);
const noDataText = computed(
() => props.emptyOptionsText ?? locale.baseText('projects.sharing.noMatchingUsers'),
);
const filteredProjects = computed(() =>
props.projects
.filter(
Expand Down Expand Up @@ -101,7 +105,7 @@ watch(
:filter-method="setFilter"
:placeholder="selectPlaceholder"
:default-first-option="true"
:no-data-text="locale.baseText('projects.sharing.noMatchingProjects')"
:no-data-text="noDataText"
size="large"
:disabled="props.readonly"
@update:model-value="onProjectSelected"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const processedName = computed(() => splitName(props.project.name ?? ''));
<div :class="$style.projectInfo" data-test-id="project-sharing-info">
<div>
<N8nAvatar :first-name="processedName.firstName" :last-name="processedName.lastName" />
<div class="flex flex-col">
<div :class="$style.text">
<p v-if="processedName.firstName || processedName.lastName">
{{ processedName.firstName }} {{ processedName.lastName }}
</p>
Expand Down Expand Up @@ -54,4 +54,9 @@ const processedName = computed(() => splitName(props.project.name ?? ''));
line-height: var(--font-line-height-loose);
}
}

.text {
display: flex;
flex-direction: column;
}
</style>
31 changes: 27 additions & 4 deletions packages/editor-ui/src/features/projects/projects.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from '@/features/projects/projects.types';
import { useSettingsStore } from '@/stores/settings.store';
import { hasPermission } from '@/rbac/permissions';
import { ProjectTypes } from './projects.utils';

export const useProjectsStore = defineStore('projects', () => {
const route = useRoute();
Expand All @@ -27,16 +28,19 @@ export const useProjectsStore = defineStore('projects', () => {
team: 0,
public: 0,
});
const projectNavActiveIdState = ref<string | string[] | null>(null);

const currentProjectId = computed(
() =>
(route.params?.projectId as string | undefined) ||
(route.query?.projectId as string | undefined) ||
(route.params?.projectId as string | undefined) ??
(route.query?.projectId as string | undefined) ??
currentProject.value?.id,
);
const isProjectHome = computed(() => route.path.includes('home'));
const personalProjects = computed(() => projects.value.filter((p) => p.type === 'personal'));
const teamProjects = computed(() => projects.value.filter((p) => p.type === 'team'));
const personalProjects = computed(() =>
projects.value.filter((p) => p.type === ProjectTypes.Personal),
);
const teamProjects = computed(() => projects.value.filter((p) => p.type === ProjectTypes.Team));
const teamProjectsLimit = computed(() => settingsStore.settings.enterprise.projects.team.limit);
const teamProjectsAvailable = computed<boolean>(
() => settingsStore.settings.enterprise.projects.team.limit !== 0,
Expand All @@ -56,6 +60,13 @@ export const useProjectsStore = defineStore('projects', () => {
hasPermission(['rbac'], { rbac: { scope: 'project:create' } }),
);

const projectNavActiveId = computed<string | string[] | null>({
get: () => route?.params?.projectId ?? projectNavActiveIdState.value,
set: (value: string | string[] | null) => {
projectNavActiveIdState.value = value;
},
});

const setCurrentProject = (project: Project | null) => {
currentProject.value = project;
};
Expand Down Expand Up @@ -113,10 +124,21 @@ export const useProjectsStore = defineStore('projects', () => {
watch(
route,
async (newRoute) => {
projectNavActiveId.value = null;

if (newRoute?.path?.includes('home')) {
projectNavActiveId.value = 'home';
setCurrentProject(null);
}

if (newRoute?.path?.includes('workflow/')) {
if (currentProjectId.value) {
projectNavActiveId.value = currentProjectId.value;
} else {
projectNavActiveId.value = 'home';
}
}

if (!newRoute?.params?.projectId) {
return;
}
Expand All @@ -140,6 +162,7 @@ export const useProjectsStore = defineStore('projects', () => {
canCreateProjects,
hasPermissionToCreateProjects,
teamProjectsAvailable,
projectNavActiveId,
setCurrentProject,
getAllProjects,
getMyProjects,
Expand Down
Loading
Loading