Skip to content
Open
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
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
* text eol=lf

*.png binary
*.jpg binary
*.jpeg binary
*.webp binary
*.woff binary
*.woff2 binary
4 changes: 4 additions & 0 deletions src/features/dnd-board/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
export { BoardActions } from "./ui/board-actions";
export { Board } from "./ui/board/board";
export { BoardUsers } from "./ui/board-users/board-users";
export { BoardSearchBar } from "./ui/board-search-bar/board-search-bar";
export type { Board as BoardType } from "./model/types";
export { boardStoreContext, useBoardStore } from "./model/use-board-store";
export { boardDepsContext } from "./deps";
export { useSaveBoard } from "./model/use-save-board";
export { useBoard } from "./model/use-board";
export { useBoardStoreFactory } from "./model/use-board-store-factory";
export { useBoardSearchStoreFactory } from "./model/use-board-search-store-factory";
export { boardSearchStoreContext, useBoardSearchStore} from "./model/use-board-search-store";
32 changes: 32 additions & 0 deletions src/features/dnd-board/lib/use-board-users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BoardUser } from "../model/types.tsx"
import { useBoardStore } from "../model/use-board-store";
import { useGetUserById } from "./use-get-user-by-id";

type UseBoardUsersReturn = {
owner: BoardUser | null;
editors: BoardUser[]
}

export const useBoardUsers = (): UseBoardUsersReturn => {
const { board } = useBoardStore();
const { ownerId, editorsIds} = board;

const getUserById = useGetUserById();

const owner = getUserById(ownerId) ?? null;

const editors: BoardUser[] = []

editorsIds.forEach((id) => {
const editor = getUserById(id);

if (editor) {
editors.push(editor)
}
})

return {
owner,
editors
}
}
17 changes: 17 additions & 0 deletions src/features/dnd-board/lib/use-can-be-assigned-as-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useBoardStore } from "../model/use-board-store.tsx";

export const useCanBeAssignedAsEditor = ({ editorsIds }: { editorsIds: string[]}) => {
const { board } = useBoardStore();

return ({id}: {id: string}): boolean => {
const isOwner = id === board.ownerId;

if (isOwner) {
return false
}

const isEditor = editorsIds.some((editorId) => editorId === id);

return !isEditor;
}
}
18 changes: 18 additions & 0 deletions src/features/dnd-board/lib/use-can-update-board-editors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useSesssion as useSession} from "@/entities/session";
import { useBoardStore } from "../model/use-board-store";

export const useCanUpdateBoardEditors = (): boolean => {
const { board } = useBoardStore();
const { ownerId} = board;
const session = useSession();

if (!session) {
return false;
}

const { userId} = session;

const isOwner = ownerId === userId;

return isOwner;
}
12 changes: 12 additions & 0 deletions src/features/dnd-board/lib/use-get-user-by-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import { UserDto } from '@/shared/api/modules/user';
import { usersListQuery } from '@/entities/user';

export function useGetUserById(): (userId: string) => UserDto | null {
const { data: users } = useQuery({
...usersListQuery(),
initialData: [],
});

return (userId) => users.find((user) => user.id === userId) ?? null;
}
25 changes: 25 additions & 0 deletions src/features/dnd-board/lib/use-has-board-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useSesssion as useSession } from "@/entities/session";
import { useBoardStore } from "../model/use-board-store";

export const hasBoardAccess = (userId: string, {ownerId, editorsIds}: {ownerId: string, editorsIds: string[]}): boolean => {
const isOwner = ownerId === userId;

if (isOwner) {
return true;
}

const isEditor = editorsIds.some((editorId) => editorId === userId);

return isEditor;
}

export const useHasBoardAccess = (): boolean => {
const { board } = useBoardStore();
const session = useSession();

if (!session) {
return false;
}

return hasBoardAccess(session.userId, board);
}
15 changes: 15 additions & 0 deletions src/features/dnd-board/lib/use-possible-editors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useQuery } from '@tanstack/react-query';
import { usersListQuery} from '@/entities/user';
import { BoardUser } from "../model/types.tsx"
import { useCanBeAssignedAsEditor } from "./use-can-be-assigned-as-editor.ts";

export const usePossibleEditors = ({ editorsIds }: { editorsIds: string[]}): BoardUser[] => {
const { data: users } = useQuery({
...usersListQuery(),
initialData: [],
});

const canBeAssignedAsEditor = useCanBeAssignedAsEditor({ editorsIds })

return users.filter((user) => canBeAssignedAsEditor(user))
}
24 changes: 24 additions & 0 deletions src/features/dnd-board/lib/use-shown-board.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Board } from "../model/types";
import { useBoardStore} from "../model/use-board-store";
import { useBoardSearchStore } from "../model/use-board-search-store";

export const useShownBoard = (): Board => {
const { board} = useBoardStore();
const { submittedQuery: query } = useBoardSearchStore();

if (!query) {
return board;
}

const loweredQuery = query.toLowerCase();

const shownBoardCols = board.cols.map(({ items, ...restCol }) => ({
...restCol,
items: items.filter((item) => item.title.toLowerCase().includes(loweredQuery))
}));

return {
...board,
cols: shownBoardCols,
}
}
6 changes: 6 additions & 0 deletions src/features/dnd-board/model/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ export type UpdateBoardData = {
ownerId?: string;
editorsIds?: string[];
};

export type BoardUser = {
id: string;
name: string;
avatarId: string;
}
24 changes: 24 additions & 0 deletions src/features/dnd-board/model/use-board-search-store-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { BoardSearchStore } from "./use-board-search-store";
import {useRef, useState} from 'react';

export const useBoardSearchStoreFactory = (initialQuery = ''): BoardSearchStore => {
const initialQueryRef = useRef(initialQuery);
const [query, setQuery] = useState(initialQueryRef.current);
const [submittedQuery, setSubmittedQuery] = useState(initialQueryRef.current);

const submitQuery = () => {
setSubmittedQuery(query);
}
const resetQuery = () => {
setQuery(initialQueryRef.current);
setSubmittedQuery(initialQueryRef.current);
};

return {
query,
submittedQuery,
setQuery,
submitQuery,
resetQuery
}
}
13 changes: 13 additions & 0 deletions src/features/dnd-board/model/use-board-search-store.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createStrictContext, useStrictContext } from "@/shared/lib/react";

export type BoardSearchStore = {
query: string;
submittedQuery: string;
setQuery: (newQuery: string) => void;
submitQuery: () => void;
resetQuery: () => void;
};

export const boardSearchStoreContext = createStrictContext<BoardSearchStore>();

export const useBoardSearchStore = () => useStrictContext(boardSearchStoreContext);
9 changes: 9 additions & 0 deletions src/features/dnd-board/model/use-board-store-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ export const useBoardStoreFactory = (initalBoard: Board): BoardStore => {
);
};

const updateBoardEditors: BoardStore["updateBoardEditors"] = async (editorsIds: string[]) => {
setBoard(
produce((draft) => {
draft.editorsIds = editorsIds;
})
);
}

return {
board,
addColumn,
Expand All @@ -148,5 +156,6 @@ export const useBoardStoreFactory = (initalBoard: Board): BoardStore => {
updateBoardCard,
moveBoardCard,
addBoardCard,
updateBoardEditors,
};
};
2 changes: 2 additions & 0 deletions src/features/dnd-board/model/use-board-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export type BoardStore = {
start: { colId: string; index: number },
end: { colId: string; index: number },
) => Promise<void>;

updateBoardEditors: (editorsIds: string[]) => Promise<void>;
};

export const boardStoreContext = createStrictContext<BoardStore>();
Expand Down
30 changes: 30 additions & 0 deletions src/features/dnd-board/ui/board-search-bar/board-search-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import clsx from 'clsx';
import { FormEventHandler } from 'react';
import { UiButton } from "@/shared/ui/ui-button";
import { UiTextField } from "@/shared/ui/ui-text-field";
import { useBoardSearchStore } from '@/features/dnd-board';

export function BoardSearchBar({ className }: { className?: string }) {
const { query, submittedQuery, setQuery, submitQuery, resetQuery } = useBoardSearchStore();
const onSearch: FormEventHandler<HTMLFormElement> = (evt) => {
evt?.preventDefault();
submitQuery();
};

const onReset: FormEventHandler<HTMLFormElement> = (evt) => {
evt?.preventDefault();
resetQuery();
};

const isSearchDisabled = query === submittedQuery;

return <form className={clsx(className, "flex justify-end gap-2")} onSubmit={onSearch} onReset={onReset}>
<UiTextField inputProps={{
placeholder: 'Искать задачу',
value: query,
onInput: (event) => setQuery(event.currentTarget.value)
}} />
<UiButton type="submit" variant="primary" disabled={isSearchDisabled}>Поиск</UiButton>
<UiButton type="reset" variant="secondary">Сбросить</UiButton>
</form>
}
45 changes: 45 additions & 0 deletions src/features/dnd-board/ui/board-users/board-users.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import clsx from "clsx";
import { UserPreview } from "@/entities/user";
import { useBoardUsers } from '../../lib/use-board-users.ts';
import { useHasBoardAccess } from "../../lib/use-has-board-access.ts";
import { UpdateBoardEditorsButton } from "./update-board-editors-button.tsx"

export function BoardUsers({ className }: { className?: string }) {
const { owner, editors} = useBoardUsers();
const hasBoardAccess = useHasBoardAccess();

if (!hasBoardAccess) {
return <div className={clsx("flex flex-col gap-2", className)}>
<h2 className="text-2xl">Пользователи доски</h2>
<p className="text-base">У Bас нет прав для просмотра пользователей этой доски</p>
</div>
}

return (
<div className={clsx("flex flex-col gap-2", className)}>
<h2 className="text-2xl">Пользователи доски</h2>

<div className="flex gap-2 items-center">
<h3 className="text-xl">Владелец:</h3>
{!owner ?
<p className="text-base mt-0.5">Не назначен</p> :
<div className="p-1 border rounded-xl">
<UserPreview name={owner.name} avatarId={owner.avatarId} size="md" />
</div>}
</div>

<div className="flex gap-2 items-center">
<h3 className="text-xl">Редакторы:</h3>
<div className="flex flex-wrap gap-2 items-center">
{!editors.length ?
<p className="text-base mt-0.5">Не назначены</p> :
editors.map((editor) =>
<div key={editor.id} className="p-1 border rounded-xl">
<UserPreview name={editor.name} avatarId={editor.avatarId} size="md" />
</div>)}
</div>
<UpdateBoardEditorsButton />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import clsx from 'clsx';
import { UpdateIcon } from '@/shared/ui/ui-icons.tsx';
import { useCanUpdateBoardEditors } from "../../lib/use-can-update-board-editors";
import {
useUpdateBoardEditorsModal
} from "../update-board-editors/use-update-board-editors-modal";

export function UpdateBoardEditorsButton({ className }: { className?: string}) {
const canUpdateBoardEditors = useCanUpdateBoardEditors();
const { modal, updateBoardEditors } = useUpdateBoardEditorsModal();

if (!canUpdateBoardEditors) {
return null;
}

return <>
<button
className={clsx(className, "text-teal-600 p-1 rounded-full hover:bg-teal-100 transition-all action")}
onClick={updateBoardEditors}
>
<UpdateIcon className="w-5 h-5" />
</button>
{modal}
</>
}
8 changes: 5 additions & 3 deletions src/features/dnd-board/ui/board/board.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import clsx from "clsx";
import { BoardColumn } from "./board-column";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import { BoardColumn } from "./board-column";
import { useBoardStore } from "../..";
import { useShownBoard } from "../../lib/use-shown-board";

export function Board({ className }: { className?: string }) {
const { board, moveColumn, moveBoardCard } = useBoardStore();
const columns = board.cols;
const { moveColumn, moveBoardCard } = useBoardStore();
const shownBoard = useShownBoard();
const columns = shownBoard.cols;

return (
<DragDropContext
Expand Down
Loading