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

feat: add an ability to edit a project gf-88 #137

Merged
merged 19 commits into from
Sep 4, 2024
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
2 changes: 2 additions & 0 deletions apps/backend/src/modules/projects/libs/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export {
type ProjectGetAllItemResponseDto,
type ProjectGetAllRequestDto,
type ProjectGetAllResponseDto,
type ProjectPatchRequestDto,
type ProjectPatchResponseDto,
} from "@git-fit/shared";
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export { projectCreateValidationSchema } from "@git-fit/shared";
export {
projectCreateValidationSchema,
projectPatchValidationSchema,
} from "@git-fit/shared";
72 changes: 71 additions & 1 deletion apps/backend/src/modules/projects/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import { ProjectsApiPath } from "./libs/enums/enums.js";
import {
type ProjectCreateRequestDto,
type ProjectGetAllRequestDto,
type ProjectPatchRequestDto,
} from "./libs/types/types.js";
import { projectCreateValidationSchema } from "./libs/validation-schemas/validation-schemas.js";
import {
projectCreateValidationSchema,
projectPatchValidationSchema,
} from "./libs/validation-schemas/validation-schemas.js";
import { type ProjectService } from "./project.service.js";

/**
Expand Down Expand Up @@ -82,6 +86,21 @@ class ProjectController extends BaseController {
method: "GET",
path: ProjectsApiPath.$ID,
});

this.addRoute({
handler: (options) =>
this.patch(
options as APIHandlerOptions<{
body: ProjectPatchRequestDto;
params: { id: string };
}>,
),
method: "PATCH",
path: ProjectsApiPath.$ID,
validation: {
body: projectPatchValidationSchema,
},
});
}

/**
Expand Down Expand Up @@ -189,6 +208,57 @@ class ProjectController extends BaseController {
status: HTTPCode.OK,
};
}

/**
* @swagger
* /projects/{id}:
* patch:
* description: Update project info
* parameters:
* - in: path
* name: id
* required: true
* description: The ID of the project to update
* schema:
* type: integer
* requestBody:
* description: Project data
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* responses:
* 200:
* description: Successful operation
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: object
* $ref: "#/components/schemas/Project"
*/

private async patch(
options: APIHandlerOptions<{
body: ProjectPatchRequestDto;
params: { id: string };
}>,
): Promise<APIHandlerResponse> {
const projectId = Number(options.params.id);

return {
payload: await this.projectService.patch(projectId, options.body),
status: HTTPCode.OK,
};
}
}

export { ProjectController };
14 changes: 14 additions & 0 deletions apps/backend/src/modules/projects/project.repository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SortType } from "~/libs/enums/enums.js";
import { type Repository } from "~/libs/types/types.js";

import { type ProjectPatchRequestDto } from "./libs/types/types.js";
import { ProjectEntity } from "./project.entity.js";
import { type ProjectModel } from "./project.model.js";

Expand Down Expand Up @@ -65,6 +66,19 @@ class ProjectRepository implements Repository {
return item ? ProjectEntity.initialize(item) : null;
}

public async patch(
id: number,
projectData: ProjectPatchRequestDto,
): Promise<ProjectEntity> {
const { description, name } = projectData;

const updatedItem = await this.projectModel
.query()
.patchAndFetchById(id, { description, name });

return ProjectEntity.initialize(updatedItem);
}

public update(): ReturnType<Repository["update"]> {
return Promise.resolve(null);
}
Expand Down
31 changes: 31 additions & 0 deletions apps/backend/src/modules/projects/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
type ProjectGetAllItemResponseDto,
type ProjectGetAllRequestDto,
type ProjectGetAllResponseDto,
type ProjectPatchRequestDto,
type ProjectPatchResponseDto,
} from "./libs/types/types.js";
import { ProjectEntity } from "./project.entity.js";
import { type ProjectRepository } from "./project.repository.js";
Expand Down Expand Up @@ -77,6 +79,35 @@ class ProjectService implements Service {
};
}

public async patch(
id: number,
projectData: ProjectPatchRequestDto,
): Promise<ProjectPatchResponseDto> {
const targetProject = await this.projectRepository.find(id);

if (!targetProject) {
throw new ProjectError({
message: ExceptionMessage.PROJECT_NOT_FOUND,
status: HTTPCode.NOT_FOUND,
});
}

const existingProject = await this.projectRepository.findByName(
projectData.name,
);

if (existingProject && existingProject.toObject().id !== id) {
throw new ProjectError({
message: ExceptionMessage.PROJECT_NAME_USED,
status: HTTPCode.CONFLICT,
});
}

const updatedItem = await this.projectRepository.patch(id, projectData);

return updatedItem.toObject();
}

public update(): ReturnType<Service["update"]> {
return Promise.resolve(null);
}
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/assets/images/icons/ellipsis.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/frontend/src/assets/images/icons/pencil.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions apps/frontend/src/libs/components/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export { IconButton } from "./icon-button/icon-button.js";
export { Input } from "./input/input.js";
export { Link, NavLink } from "./link/link.js";
export { Loader } from "./loader/loader.js";
export { Menu } from "./menu/menu.js";
export { MenuItem } from "./menu-item/menu-item.js";
export { Modal } from "./modal/modal.js";
export { PageLayout } from "./page-layout/page-layout.js";
export { Popover } from "./popover/popover.js";
Expand Down
18 changes: 15 additions & 3 deletions apps/frontend/src/libs/components/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import logoSrc from "~/assets/images/logo.svg";
import { Avatar, NavLink } from "~/libs/components/components.js";
import { AppRoute } from "~/libs/enums/enums.js";
import { useAppSelector } from "~/libs/hooks/hooks.js";
import { useAppSelector, usePopover } from "~/libs/hooks/hooks.js";

import { UserPopover } from "./libs/components/components.js";
import styles from "./styles.module.css";

const Header = (): JSX.Element => {
const { isOpened, onClose, onOpen } = usePopover();

const authenticatedUser = useAppSelector(
({ auth }) => auth.authenticatedUser,
);
Expand All @@ -25,8 +27,18 @@ const Header = (): JSX.Element => {
<span className={styles["logo-text"]}>Logo</span>
</div>
</NavLink>
<UserPopover email={email} name={name}>
<Avatar name={name} />
<UserPopover
email={email}
isOpened={isOpened}
name={name}
onClose={onClose}
>
<button
className={styles["user-popover-trigger"]}
onClick={isOpened ? onClose : onOpen}
>
<Avatar name={name} />
</button>
</UserPopover>
</header>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ import styles from "./styles.module.css";
type Properties = {
children: React.ReactNode;
email: string;
isOpened: boolean;
name: string;
onClose: () => void;
};

const UserPopover = ({ children, email, name }: Properties): JSX.Element => {
const UserPopover = ({
children,
email,
isOpened,
name,
onClose,
}: Properties): JSX.Element => {
const dispatch = useAppDispatch();

const handleLogout = useCallback((): void => {
Expand All @@ -38,6 +46,8 @@ const UserPopover = ({ children, email, name }: Properties): JSX.Element => {
</div>
</div>
}
isOpened={isOpened}
onClose={onClose}
>
{children}
</Popover>
Expand Down
10 changes: 10 additions & 0 deletions apps/frontend/src/libs/components/header/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@
color: var(--color-text-primary);
cursor: pointer;
}

.user-popover-trigger {
padding: 0;
margin: 0;
text-decoration: none;
cursor: pointer;
background: none;
border: none;
border-radius: 50%;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Icon } from "~/libs/components/components.js";
import { getValidClassNames } from "~/libs/helpers/helpers.js";
import { type IconName } from "~/libs/types/types.js";

import { type IconName } from "../icon/libs/types/types.js";
import { ICON_SIZE } from "./libs/constants/constants.js";
import styles from "./styles.module.css";

Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/libs/components/icon/icon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type IconName } from "~/libs/types/types.js";

import { iconNameToSvg } from "./libs/maps/maps.js";
import { type IconName } from "./libs/types/types.js";
import styles from "./styles.module.css";

type Properties = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@ import Access from "~/assets/images/icons/access.svg?react";
import Analytics from "~/assets/images/icons/analytics.svg?react";
import Contributors from "~/assets/images/icons/contributors.svg?react";
import Cross from "~/assets/images/icons/cross.svg?react";
import Ellipsis from "~/assets/images/icons/ellipsis.svg?react";
import Eye from "~/assets/images/icons/eye.svg?react";
import LeftArrow from "~/assets/images/icons/left-arrow.svg?react";
import LeftDoubleArrow from "~/assets/images/icons/left-double-arrow.svg?react";
import Pencil from "~/assets/images/icons/pencil.svg?react";
import Project from "~/assets/images/icons/project.svg?react";
import RightArrow from "~/assets/images/icons/right-arrow.svg?react";
import RightDoubleArrow from "~/assets/images/icons/right-double-arrow.svg?react";
import Search from "~/assets/images/icons/search.svg?react";
import StrikedEye from "~/assets/images/icons/striked-eye.svg?react";

import { type IconName } from "../types/types.js";
import { type IconName } from "~/libs/types/types.js";

const iconNameToSvg: Record<IconName, FC<React.SVGProps<SVGSVGElement>>> = {
access: Access,
analytics: Analytics,
contributors: Contributors,
cross: Cross,
ellipsis: Ellipsis,
eye: Eye,
leftArrow: LeftArrow,
leftDoubleArrow: LeftDoubleArrow,
pencil: Pencil,
project: Project,
rightArrow: RightArrow,
rightDoubleArrow: RightDoubleArrow,
Expand Down

This file was deleted.

38 changes: 38 additions & 0 deletions apps/frontend/src/libs/components/menu-item/menu-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Icon } from "~/libs/components/components.js";
import { getValidClassNames } from "~/libs/helpers/helpers.js";
import { type IconName } from "~/libs/types/types.js";

import styles from "./styles.module.css";

type Properties = {
iconName: IconName;
label: string;
onClick: () => void;
variant?: "danger" | "primary";
};

const MenuItem = ({
iconName,
label,
onClick,
variant = "primary",
}: Properties): JSX.Element => {
const buttonClassName = getValidClassNames(
styles["menu-item"],
styles[`menu-item-${variant}`],
);

return (
<button
aria-label={label}
className={buttonClassName}
onClick={onClick}
type="button"
>
<Icon height={20} name={iconName} width={20} />
<span className={styles["menu-item-text"]}>{label}</span>
</button>
);
};

export { MenuItem };
31 changes: 31 additions & 0 deletions apps/frontend/src/libs/components/menu-item/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.menu-item {
display: flex;
gap: 8px;
align-items: center;
padding: 11px 15px;
text-align: left;
cursor: pointer;
background-color: transparent;
border: none;
border-radius: 4px;
}

.menu-item:hover {
background-color: var(--color-background-hover);
}

.menu-item-text {
margin: 0;
font-family: Inter, sans-serif;
font-size: 16px;
font-weight: 500;
line-height: 1.2;
}

.menu-item-primary {
color: var(--color-text-primary);
}

.menu-item-danger {
color: var(--color-danger);
}
Loading