Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions src/components/Dialogs/CreateProjectDialogContainer.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { CreateProjectDialogContainer } from './CreateProjectDialogContainer';
import { useCreateProject, CreateProjectParams } from '../../hooks/useCreateProject';
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding';

describe('CreateProjectDialogContainer', () => {
let createProjectPayload: CreateProjectParams | null = null;

const fakeUseCreateProject: typeof useCreateProject = () => ({
createProject: async (data: CreateProjectParams): Promise<void> => {
createProjectPayload = data;
},
isLoading: false,
});

const fakeUseAuthOnboarding = (() => ({
user: {
email: 'name@domain.com',
},
})) as typeof useAuthOnboarding;

beforeEach(() => {
createProjectPayload = null;
});

it('creates a project with valid data', () => {
const setIsOpen = cy.stub();

cy.mount(
<CreateProjectDialogContainer
useCreateProject={fakeUseCreateProject}
useAuthOnboarding={fakeUseAuthOnboarding}
isOpen={true}
setIsOpen={setIsOpen}
/>,
);

const expectedPayload = {
name: 'test-project',
displayName: 'Test Project Display Name',
chargingTarget: '12345678-1234-1234-1234-123456789abc',
chargingTargetType: 'btp',
members: [
{
name: 'name@domain.com',
roles: ['admin'],
kind: 'User',
},
],
};

// Fill in the form
cy.get('#name').find('input[id*="inner"]').type('test-project');
cy.get('#displayName').find('input[id*="inner"]').type('Test Project Display Name');

// Select charging target type (should be pre-selected as 'btp')
cy.get('#chargingTargetType').click();
cy.contains('BTP').click();

// Fill charging target
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');

// Submit the form
cy.get('ui5-button').contains('Create').click();

// Verify the hook was called with correct data
cy.then(() => cy.wrap(createProjectPayload).deepEqualJson(expectedPayload));

// Dialog should close on success
cy.wrap(setIsOpen).should('have.been.calledWith', false);
});

it('validates required fields', () => {
const setIsOpen = cy.stub();

cy.mount(
<CreateProjectDialogContainer
useCreateProject={fakeUseCreateProject}
useAuthOnboarding={fakeUseAuthOnboarding}
isOpen={true}
setIsOpen={setIsOpen}
/>,
);

// Try to submit without filling required fields
cy.get('ui5-button').contains('Create').click();

// Should show validation errors
cy.get('#name').should('have.attr', 'value-state', 'Negative');
cy.contains('This field is required').should('exist');

// Dialog should not close
cy.wrap(setIsOpen).should('not.have.been.called');
});

it('validates charging target format for BTP', () => {
const setIsOpen = cy.stub();

cy.mount(
<CreateProjectDialogContainer
useCreateProject={fakeUseCreateProject}
useAuthOnboarding={fakeUseAuthOnboarding}
isOpen={true}
setIsOpen={setIsOpen}
/>,
);

cy.get('#name').find('input[id*="inner"]').type('test-project');
cy.get('#chargingTargetType').click();
cy.contains('BTP').click();

// Invalid format
cy.get('#chargingTarget').find('input[id*="inner"]').type('invalid-format');
cy.get('ui5-button').contains('Create').click();

// Should show validation error
cy.get('#chargingTarget').should('have.attr', 'value-state', 'Negative');

// Dialog should not close
cy.wrap(setIsOpen).should('not.have.been.called');
});

it('should not close dialog when creation fails', () => {
const failingUseCreateProject: typeof useCreateProject = () => ({
createProject: async (): Promise<void> => {
throw new Error('Creation failed');
},
isLoading: false,
});

const setIsOpen = cy.stub();

cy.mount(
<CreateProjectDialogContainer
useCreateProject={failingUseCreateProject}
useAuthOnboarding={fakeUseAuthOnboarding}
isOpen={true}
setIsOpen={setIsOpen}
/>,
);

// Fill in the form
cy.get('#name').find('input[id*="inner"]').type('test-project');
cy.get('#chargingTargetType').click();
cy.contains('BTP').click();
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');

// Submit the form
cy.get('ui5-button').contains('Create').click();

// Dialog should NOT close on failure
cy.wrap(setIsOpen).should('not.have.been.called');

// Dialog should still be visible
cy.contains('Create').should('be.visible');
});
});
37 changes: 16 additions & 21 deletions src/components/Dialogs/CreateProjectDialogContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useApiResourceMutation } from '../../lib/api/useApiResource';
import { ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
import { APIError } from '../../lib/api/error';
import { CreateProjectWorkspaceDialog, OnCreatePayload } from './CreateProjectWorkspaceDialog.tsx';

import { useToast } from '../../context/ToastContext.tsx';
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
import { useAuthOnboarding as _useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
import { MemberRoles } from '../../lib/api/types/shared/members.ts';

import { useTranslation } from 'react-i18next';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { CreateProject, CreateProjectResource, CreateProjectType } from '../../lib/api/types/crate/createProject.ts';
import { createProjectWorkspaceSchema } from '../../lib/api/validations/schemas.ts';
import { CreateDialogProps } from './CreateWorkspaceDialogContainer.tsx';
import { useCreateProject as _useCreateProject } from '../../hooks/useCreateProject.ts';

export function CreateProjectDialogContainer({
isOpen,
setIsOpen,
useCreateProject = _useCreateProject,
useAuthOnboarding = _useAuthOnboarding,
}: {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
useCreateProject?: typeof _useCreateProject;
useAuthOnboarding?: typeof _useAuthOnboarding;
}) {
const { t } = useTranslation();
const validationSchemaProjectWorkspace = useMemo(() => createProjectWorkspaceSchema(t), [t]);
Expand All @@ -44,6 +44,9 @@ export function CreateProjectDialogContainer({
const { user } = useAuthOnboarding();

const username = user?.email;
const { createProject } = useCreateProject();
const errorDialogRef = useRef<ErrorDialogHandle>(null);

const clearForm = useCallback(() => {
resetField('name');
resetField('chargingTarget');
Expand All @@ -60,12 +63,6 @@ export function CreateProjectDialogContainer({
}
}, [resetField, setValue, username, isOpen, clearForm]);

const toast = useToast();

const { trigger } = useApiResourceMutation<CreateProjectType>(CreateProjectResource());

const errorDialogRef = useRef<ErrorDialogHandle>(null);

const handleProjectCreate = async ({
name,
chargingTarget,
Expand All @@ -74,16 +71,14 @@ export function CreateProjectDialogContainer({
members,
}: OnCreatePayload): Promise<boolean> => {
try {
await trigger(
CreateProject(name, {
displayName: displayName,
chargingTarget: chargingTarget,
members: members,
chargingTargetType: chargingTargetType,
}),
);
await createProject({
name,
displayName,
chargingTarget,
chargingTargetType,
members,
});
setIsOpen(false);
toast.show(t('CreateProjectDialog.toastMessage'));
return true;
} catch (e) {
console.error(e);
Expand Down
41 changes: 41 additions & 0 deletions src/components/Projects/ProjectsListItemMenu.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ProjectsListItemMenu } from './ProjectsListItemMenu.tsx';
import { useDeleteProject } from '../../hooks/useDeleteProject.ts';
import '@ui5/webcomponents-cypress-commands';

describe('ProjectsListItemMenu', () => {
let deleteProjectCalled = false;

const fakeUseDeleteProject: typeof useDeleteProject = () => ({
deleteProject: async (): Promise<void> => {
deleteProjectCalled = true;
},
});

beforeEach(() => {
deleteProjectCalled = false;
});

it('deletes the project', () => {
const projectName = 'test-project';

cy.mount(<ProjectsListItemMenu projectName={projectName} useDeleteProject={fakeUseDeleteProject} />);

// Open overflow menu
cy.get('ui5-button[icon="overflow"]').click();

// Click delete option
cy.contains('Delete project').click({ force: true });

// Type confirmation text
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input(projectName);

// Verify delete not called yet
cy.then(() => cy.wrap(deleteProjectCalled).should('equal', false));

// Click delete button
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').click();

// Verify delete was called
cy.then(() => cy.wrap(deleteProjectCalled).should('equal', true));
});
});
21 changes: 10 additions & 11 deletions src/components/Projects/ProjectsListItemMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@ import '@ui5/webcomponents-icons/dist/accept';
import { useTranslation } from 'react-i18next';
import { DeleteConfirmationDialog } from '../Dialogs/DeleteConfirmationDialog.tsx';

import { useToast } from '../../context/ToastContext.tsx';
import { useApiResourceMutation } from '../../lib/api/useApiResource.ts';
import { DeleteWorkspaceType } from '../../lib/api/types/crate/deleteWorkspace.ts';
import { DeleteProjectResource } from '../../lib/api/types/crate/deleteProject.ts';
import { useDeleteProject as _useDeleteProject } from '../../hooks/useDeleteProject.ts';
import { KubectlDeleteProject } from '../Dialogs/KubectlCommandInfo/Controllers/KubectlDeleteProject.tsx';

type ProjectsListItemMenuProps = {
projectName: string;
useDeleteProject?: typeof _useDeleteProject;
};

export const ProjectsListItemMenu: FC<ProjectsListItemMenuProps> = ({ projectName }) => {
export const ProjectsListItemMenu: FC<ProjectsListItemMenuProps> = ({
projectName,
useDeleteProject = _useDeleteProject,
}) => {
const popoverRef = useRef<MenuDomRef>(null);
const [open, setOpen] = useState(false);
const [dialogDeleteProjectIsOpen, setDialogDeleteProjectIsOpen] = useState(false);
const { t } = useTranslation();
const toast = useToast();
const { trigger } = useApiResourceMutation<DeleteWorkspaceType>(DeleteProjectResource(projectName));

const { deleteProject } = useDeleteProject(projectName);

const handleOpenerClick = (e: Ui5CustomEvent<ButtonDomRef, ButtonClickEventDetail>) => {
e.stopImmediatePropagation();
e.stopPropagation();
Expand Down Expand Up @@ -59,10 +61,7 @@ export const ProjectsListItemMenu: FC<ProjectsListItemMenuProps> = ({ projectNam
kubectl={<KubectlDeleteProject projectName={projectName} />}
isOpen={dialogDeleteProjectIsOpen}
setIsOpen={setDialogDeleteProjectIsOpen}
onDeletionConfirmed={async () => {
await trigger();
toast.show(t('ProjectsListView.deleteConfirmationDialog'));
}}
onDeletionConfirmed={deleteProject}
/>
)}
</div>
Expand Down
Loading
Loading