diff --git a/app/(loggedInRoutes)/kanban/layout.tsx b/app/(loggedInRoutes)/kanban/layout.tsx new file mode 100644 index 00000000..a113d3ad --- /dev/null +++ b/app/(loggedInRoutes)/kanban/layout.tsx @@ -0,0 +1,25 @@ +import { TasksClient } from "@/app/_components/FeatureComponents/Checklists/TasksClient"; +import { getCategories } from "@/app/_server/actions/category"; +import { getCurrentUser } from "@/app/_server/actions/users"; +import { Modes } from "@/app/_types/enums"; +import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils"; + +export default async function KanbanLayout({ + children, +}: { + children: React.ReactNode; +}) { + const [checklistCategories, userRecord] = await Promise.all([ + getCategories(Modes.CHECKLISTS), + getCurrentUser(), + ]); + + const categories = checklistCategories.data || []; + const user = sanitizeUserForClient(userRecord); + + return ( + + {children} + + ); +} diff --git a/app/(loggedInRoutes)/kanban/page.tsx b/app/(loggedInRoutes)/kanban/page.tsx new file mode 100644 index 00000000..676a44c3 --- /dev/null +++ b/app/(loggedInRoutes)/kanban/page.tsx @@ -0,0 +1,23 @@ +import { getUserChecklists } from "@/app/_server/actions/checklist"; +import { getCurrentUser } from "@/app/_server/actions/users"; +import { KanbanPageClient } from "@/app/_components/FeatureComponents/Kanban/KanbanPageClient"; +import { Checklist } from "@/app/_types"; +import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils"; +import { isKanbanType } from "@/app/_types/enums"; + +export const dynamic = "force-dynamic"; + +export default async function KanbanPage() { + const [listsResult, userRecord] = await Promise.all([ + getUserChecklists(), + getCurrentUser(), + ]); + + const lists = listsResult.success && listsResult.data ? listsResult.data : []; + const kanbanLists = lists.filter((list) => + isKanbanType(list.type), + ) as Checklist[]; + const user = sanitizeUserForClient(userRecord); + + return ; +} diff --git a/app/(loggedInRoutes)/tasks/page.tsx b/app/(loggedInRoutes)/tasks/page.tsx index 17feb82b..40c60c6c 100644 --- a/app/(loggedInRoutes)/tasks/page.tsx +++ b/app/(loggedInRoutes)/tasks/page.tsx @@ -3,6 +3,7 @@ import { getCurrentUser } from "@/app/_server/actions/users"; import { TasksPageClient } from "@/app/_components/FeatureComponents/Checklists/TasksPageClient"; import { Checklist } from "@/app/_types"; import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils"; +import { isKanbanType } from "@/app/_types/enums"; export const dynamic = "force-dynamic"; @@ -13,13 +14,10 @@ export default async function TasksPage() { ]); const lists = listsResult.success && listsResult.data ? listsResult.data : []; - const taskLists = lists.filter((list) => list.type === "task") as Checklist[]; + const taskLists = lists.filter((list) => + isKanbanType(list.type), + ) as Checklist[]; const user = sanitizeUserForClient(userRecord); - return ( - - ); + return ; } diff --git a/app/(loggedOutRoutes)/auth/login/page.tsx b/app/(loggedOutRoutes)/auth/login/page.tsx index 1e898f19..059fcd28 100644 --- a/app/(loggedOutRoutes)/auth/login/page.tsx +++ b/app/(loggedOutRoutes)/auth/login/page.tsx @@ -4,28 +4,31 @@ import LoginForm from "@/app/_components/GlobalComponents/Auth/LoginForm"; import { AuthShell } from "@/app/_components/GlobalComponents/Auth/AuthShell"; import { getTranslations } from "next-intl/server"; import { SsoOnlyLogin } from "@/app/_components/GlobalComponents/Auth/SsoOnlyLogin"; -import { isEnvEnabled } from "@/app/_utils/env-utils"; +import { isEnvEnabled, getAuthMode } from "@/app/_utils/env-utils"; export const dynamic = "force-dynamic"; export default async function LoginPage() { const t = await getTranslations("auth"); - const ssoEnabled = process.env.SSO_MODE === "oidc"; + const authMode = getAuthMode(); + const ssoIsOidc = authMode === "oidc"; const allowLocal = isEnvEnabled(process.env.SSO_FALLBACK_LOCAL); const hasExistingUsers = await hasUsers(); - if ((!hasExistingUsers && !ssoEnabled) || (!hasExistingUsers && allowLocal)) { + if (!hasExistingUsers && !authMode) { redirect("/auth/setup"); } - if (ssoEnabled && !allowLocal) { + if (ssoIsOidc && !allowLocal) { return ; } + const showRegisterLink = allowLocal && !hasExistingUsers; + return (
- +
); diff --git a/app/(loggedOutRoutes)/auth/setup/page.tsx b/app/(loggedOutRoutes)/auth/setup/page.tsx index 7399b7e2..0fb0935f 100644 --- a/app/(loggedOutRoutes)/auth/setup/page.tsx +++ b/app/(loggedOutRoutes)/auth/setup/page.tsx @@ -2,15 +2,14 @@ import { redirect } from "next/navigation"; import { hasUsers } from "@/app/_server/actions/users"; import SetupForm from "@/app/(loggedOutRoutes)/auth/setup/setup-form"; import { AuthShell } from "@/app/_components/GlobalComponents/Auth/AuthShell"; -import { isEnvEnabled } from "@/app/_utils/env-utils"; +import { isEnvEnabled, getAuthMode } from "@/app/_utils/env-utils"; export const dynamic = "force-dynamic"; export default async function SetupPage() { - const ssoEnabled = process.env.SSO_MODE === "oidc"; const allowLocal = isEnvEnabled(process.env.SSO_FALLBACK_LOCAL); - if (ssoEnabled && !allowLocal) { + if (getAuthMode() && !allowLocal) { redirect("/auth/login"); } diff --git a/app/_components/FeatureComponents/Checklists/Checklist.tsx b/app/_components/FeatureComponents/Checklists/Checklist.tsx index a523ddf0..f6c7419f 100644 --- a/app/_components/FeatureComponents/Checklists/Checklist.tsx +++ b/app/_components/FeatureComponents/Checklists/Checklist.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { Checklist } from "@/app/_types"; -import { KanbanBoard } from "@/app/_components/FeatureComponents/Checklists/Parts/Kanban/KanbanBoard"; +import { Kanban } from "@/app/_components/FeatureComponents/Kanban/Kanban"; import { useChecklist } from "@/app/_hooks/useChecklist"; import { ChecklistHeader } from "@/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader"; import { ChecklistHeading } from "@/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeading"; @@ -116,7 +116,7 @@ export const ChecklistView = ({ ), }, ]} - onRemove={() => {}} + onRemove={() => { }} > )} @@ -136,7 +136,7 @@ export const ChecklistView = ({ ), }, ]} - onRemove={() => {}} + onRemove={() => { }} > )} @@ -165,7 +165,7 @@ export const ChecklistView = ({ /> ) : (
- +
)} diff --git a/app/_components/FeatureComponents/Checklists/ChecklistsPageClient.tsx b/app/_components/FeatureComponents/Checklists/ChecklistsPageClient.tsx index 8af9b43c..dc037ddf 100644 --- a/app/_components/FeatureComponents/Checklists/ChecklistsPageClient.tsx +++ b/app/_components/FeatureComponents/Checklists/ChecklistsPageClient.tsx @@ -24,6 +24,7 @@ import { useSettings } from "@/app/_utils/settings-store"; import { ChecklistListItem } from "@/app/_components/GlobalComponents/Cards/ChecklistListItem"; import { ChecklistGridItem } from "@/app/_components/GlobalComponents/Cards/ChecklistGridItem"; import { useChecklistsFilter } from "@/app/_components/FeatureComponents/Checklists/ChecklistsClient"; +import { isKanbanType } from "@/app/_types/enums"; interface ChecklistsPageClientProps { initialLists: Checklist[]; @@ -34,7 +35,7 @@ export const ChecklistsPageClient = ({ initialLists, user, }: ChecklistsPageClientProps) => { - const t = useTranslations('checklists'); + const t = useTranslations("checklists"); const router = useRouter(); const { openCreateChecklistModal } = useShortcut(); const { isInitialized } = useAppMode(); @@ -63,16 +64,16 @@ export const ChecklistsPageClient = ({ filtered = filtered.filter( (list) => list.items.length > 0 && - list.items.every((item) => isItemCompleted(item, list.type)) + list.items.every((item) => isItemCompleted(item, list.type)), ); } else if (checklistFilter === "incomplete") { filtered = filtered.filter( (list) => list.items.length === 0 || - !list.items.every((item) => isItemCompleted(item, list.type)) + !list.items.every((item) => isItemCompleted(item, list.type)), ); } else if (checklistFilter === "task") { - filtered = filtered.filter((list) => list.type === "task"); + filtered = filtered.filter((list) => isKanbanType(list.type)); } else if (checklistFilter === "simple") { filtered = filtered.filter((list) => list.type === "simple"); } @@ -84,7 +85,7 @@ export const ChecklistsPageClient = ({ return selectedCategories.some( (selected) => listCategory === selected || - listCategory.startsWith(selected + "/") + listCategory.startsWith(selected + "/"), ); } return selectedCategories.includes(listCategory); @@ -93,7 +94,7 @@ export const ChecklistsPageClient = ({ return filtered.sort( (a, b) => - new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), ); }, [ initialLists, @@ -103,7 +104,14 @@ export const ChecklistsPageClient = ({ user?.pinnedLists, ]); - const { currentPage, totalPages, paginatedItems, goToPage, totalItems, handleItemsPerPageChange } = usePagination({ + const { + currentPage, + totalPages, + paginatedItems, + goToPage, + totalItems, + handleItemsPerPageChange, + } = usePagination({ items: filteredLists, itemsPerPage, onItemsPerPageChange: setItemsPerPage, @@ -117,7 +125,14 @@ export const ChecklistsPageClient = ({ onPageChange: goToPage, onItemsPerPageChange: handleItemsPerPageChange, }); - }, [currentPage, totalPages, totalItems, goToPage, handleItemsPerPageChange, setPaginationInfo]); + }, [ + currentPage, + totalPages, + totalItems, + goToPage, + handleItemsPerPageChange, + setPaginationInfo, + ]); const handleTogglePin = async (list: Checklist) => { if (!user || isTogglingPin === list.id) return; @@ -127,7 +142,7 @@ export const ChecklistsPageClient = ({ const result = await togglePin( list.id, list.category || "Uncategorized", - ItemTypes.CHECKLIST + ItemTypes.CHECKLIST, ); if (result.success) { router.refresh(); @@ -149,7 +164,7 @@ export const ChecklistsPageClient = ({ }, 0); const totalItems = initialLists.reduce( (acc, list) => acc + list.items.length, - 0 + 0, ); const completionRate = totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0; @@ -167,9 +182,9 @@ export const ChecklistsPageClient = ({ icon={ } - title={t('noChecklistsYet')} - description={t('createFirstChecklist')} - buttonText={t('newChecklist')} + title={t("noChecklistsYet")} + description={t("createFirstChecklist")} + buttonText={t("newChecklist")} onButtonClick={() => openCreateChecklistModal()} /> ); @@ -187,7 +202,9 @@ export const ChecklistsPageClient = ({
{stats.totalLists}
-
{t('lists')}
+
+ {t("lists")} +
@@ -199,7 +216,9 @@ export const ChecklistsPageClient = ({
{stats.completedItems}
-
{t('completed')}
+
+ {t("completed")} +
@@ -211,7 +230,9 @@ export const ChecklistsPageClient = ({
{stats.completionRate}%
-
{t('progress')}
+
+ {t("progress")} +
@@ -223,7 +244,9 @@ export const ChecklistsPageClient = ({
{stats.totalItems}
-
{t('totalItems')}
+
+ {t("totalItems")} +
@@ -233,15 +256,15 @@ export const ChecklistsPageClient = ({

- {t('noChecklistsFound')} + {t("noChecklistsFound")}

- {t('tryAdjustingFiltersChecklist')} + {t("tryAdjustingFiltersChecklist")}

) : (
- {viewMode === 'card' && ( + {viewMode === "card" && (
{paginatedItems.map((list) => ( handleTogglePin(list)} /> @@ -260,7 +283,7 @@ export const ChecklistsPageClient = ({
)} - {viewMode === 'list' && ( + {viewMode === "list" && (
{paginatedItems.map((list) => ( handleTogglePin(list)} /> @@ -279,7 +302,7 @@ export const ChecklistsPageClient = ({
)} - {viewMode === 'grid' && ( + {viewMode === "grid" && (
{paginatedItems.map((list) => ( handleTogglePin(list)} /> diff --git a/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx b/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx index d8f4c837..e6e1fa05 100644 --- a/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx +++ b/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import { Category, Checklist, SanitisedUser } from "@/app/_types"; import { ChecklistView } from "@/app/_components/FeatureComponents/Checklists/Checklist"; -import { KanbanBoard } from "@/app/_components/FeatureComponents/Checklists/Parts/Kanban/KanbanBoard"; +import { Kanban } from "@/app/_components/FeatureComponents/Kanban/Kanban"; import { ChecklistHeader } from "@/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader"; import { ShareModal } from "@/app/_components/GlobalComponents/Modals/SharingModals/ShareModal"; import { ConfirmModal } from "@/app/_components/GlobalComponents/Modals/ConfirmationModals/ConfirmModal"; @@ -15,7 +15,7 @@ import { CloneCategoryModal } from "@/app/_components/GlobalComponents/Modals/Co import { useNavigationGuard } from "@/app/_providers/NavigationGuardProvider"; import { Layout } from "@/app/_components/GlobalComponents/Layout/Layout"; import { useChecklist } from "@/app/_hooks/useChecklist"; -import { Modes } from "@/app/_types/enums"; +import { isKanbanType, Modes } from "@/app/_types/enums"; import { useShortcut } from "@/app/_providers/ShortcutsProvider"; import { toggleArchive } from "@/app/_server/actions/dashboard"; import { buildCategoryPath } from "@/app/_utils/global-utils"; @@ -131,9 +131,9 @@ export const ChecklistClient = ({ }); const renderContent = () => { - if (localChecklist.type === "task") { + if (isKanbanType(localChecklist.type)) { return ( -
+
- +
); } @@ -193,11 +193,11 @@ export const ChecklistClient = ({ currentType: localChecklist.type === "simple" ? t("checklists.simpleChecklist") - : t("checklists.taskProject"), + : t("checklists.kanbanBoard"), newType: getNewType(localChecklist.type) === "simple" ? t("checklists.simpleChecklist") - : t("checklists.taskProject"), + : t("checklists.kanbanBoard"), })} confirmText={t("checklists.convert")} /> diff --git a/app/_components/FeatureComponents/Checklists/Parts/ChecklistTypeSelector.tsx b/app/_components/FeatureComponents/Checklists/Parts/ChecklistTypeSelector.tsx index 61ff38c3..f9fec8bf 100644 --- a/app/_components/FeatureComponents/Checklists/Parts/ChecklistTypeSelector.tsx +++ b/app/_components/FeatureComponents/Checklists/Parts/ChecklistTypeSelector.tsx @@ -25,7 +25,7 @@ export const ChecklistTypeSelector = ({
- {([ChecklistsTypes.SIMPLE, ChecklistsTypes.TASK] as const).map((type) => ( + {([ChecklistsTypes.SIMPLE, ChecklistsTypes.KANBAN] as const).map((type) => (
{type === ChecklistsTypes.SIMPLE diff --git a/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader.tsx b/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader.tsx index 5818b1d6..69017b6c 100644 --- a/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader.tsx +++ b/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader.tsx @@ -128,12 +128,12 @@ export const ChecklistHeader = ({ }} className="h-10 w-10 p-0" title={ - checklist.type === ChecklistsTypes.TASK + checklist.type === ChecklistsTypes.KANBAN || checklist.type === ChecklistsTypes.TASK ? t('checklists.convertToSimpleChecklist') : t('checklists.convertToTaskProject') } > - {checklist.type === ChecklistsTypes.TASK ? ( + {checklist.type === ChecklistsTypes.KANBAN || checklist.type === ChecklistsTypes.TASK ? ( ) : ( @@ -174,11 +174,11 @@ export const ChecklistHeader = ({ { type: "item" as const, label: - checklist.type === ChecklistsTypes.TASK - ? "Convert to Simple Checklist" - : "Convert to Task Project", + checklist.type === ChecklistsTypes.KANBAN || checklist.type === ChecklistsTypes.TASK + ? t("checklists.convertToSimpleChecklist") + : t("checklists.convertToKanbanBoard"), icon: - checklist.type === ChecklistsTypes.TASK ? ( + checklist.type === ChecklistsTypes.KANBAN || checklist.type === ChecklistsTypes.TASK ? ( ) : ( @@ -194,7 +194,7 @@ export const ChecklistHeader = ({ ? [ { type: "item" as const, - label: "Clone", + label: t("common.clone"), icon: , onClick: onClone, }, diff --git a/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistModals.tsx b/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistModals.tsx index 01f66ace..8448e1d0 100644 --- a/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistModals.tsx +++ b/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistModals.tsx @@ -1,7 +1,7 @@ import { ShareModal } from "@/app/_components/GlobalComponents/Modals/SharingModals/ShareModal"; import { ConfirmModal } from "@/app/_components/GlobalComponents/Modals/ConfirmationModals/ConfirmModal"; import { BulkPasteModal } from "@/app/_components/GlobalComponents/Modals/BulkPasteModal/BulkPasteModal"; -import { Checklist } from "@/app/_types"; +import { Checklist, ChecklistType } from "@/app/_types"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -16,7 +16,7 @@ interface ChecklistModalsProps { showBulkPasteModal: boolean; setShowBulkPasteModal: (show: boolean) => void; handleConfirmConversion: () => void; - getNewType: (type: "simple" | "task") => "simple" | "task"; + getNewType: (type: ChecklistType) => ChecklistType; handleBulkPaste: (itemsText: string) => void; isLoading: boolean; DeleteModal: () => JSX.Element; @@ -39,8 +39,8 @@ export const ChecklistModals = ({ const router = useRouter(); const t = useTranslations(); - const currentType = localList.type === "simple" ? t("checklists.simpleChecklist") : t("checklists.taskProject"); - const newType = getNewType(localList.type) === "simple" ? t("checklists.simpleChecklist") : t("checklists.taskProject"); + const currentType = localList.type === "simple" ? t("checklists.simpleChecklist") : t("checklists.kanbanBoard"); + const newType = getNewType(localList.type) === "simple" ? t("checklists.simpleChecklist") : t("checklists.kanbanBoard"); return ( <> diff --git a/app/_components/FeatureComponents/Checklists/Parts/Kanban/KanbanBoard.tsx b/app/_components/FeatureComponents/Checklists/Parts/Kanban/KanbanBoard.tsx deleted file mode 100644 index b75a367d..00000000 --- a/app/_components/FeatureComponents/Checklists/Parts/Kanban/KanbanBoard.tsx +++ /dev/null @@ -1,358 +0,0 @@ -"use client"; - -import { useState, useEffect, useMemo } from "react"; -import { - DndContext, - DragOverlay, - PointerSensor, - KeyboardSensor, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { Checklist, KanbanStatus } from "@/app/_types"; -import { KanbanColumn } from "./KanbanColumn"; -import { KanbanItem } from "./KanbanItem"; -import { ChecklistHeading } from "../Common/ChecklistHeading"; -import { BulkPasteModal } from "@/app/_components/GlobalComponents/Modals/BulkPasteModal/BulkPasteModal"; -import { StatusManagementModal } from "./StatusManagementModal"; -import { ArchivedItemsModal } from "./ArchivedItemsModal"; -import { useKanbanBoard } from "../../../../../_hooks/useKanbanBoard"; -import { ItemTypes, TaskStatus, TaskStatusLabels } from "@/app/_types/enums"; -import { ReferencedBySection } from "../../../Notes/Parts/ReferencedBySection"; -import { getReferences } from "@/app/_utils/indexes-utils"; -import { useAppMode } from "@/app/_providers/AppModeProvider"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; -import { usePermissions } from "@/app/_providers/PermissionsProvider"; -import { Settings01Icon, Archive02Icon } from "hugeicons-react"; -import { Button } from "@/app/_components/GlobalComponents/Buttons/Button"; -import { updateChecklistStatuses } from "@/app/_server/actions/checklist"; -import { unarchiveItem } from "@/app/_server/actions/checklist-item"; -import { useTranslations } from "next-intl"; - -interface KanbanBoardProps { - checklist: Checklist; - onUpdate: (updatedChecklist: Checklist) => void; -} - -const defaultStatuses: KanbanStatus[] = [ - { - id: TaskStatus.TODO, - label: TaskStatusLabels.TODO, - order: 0, - autoComplete: false, - }, - { - id: TaskStatus.IN_PROGRESS, - label: TaskStatusLabels.IN_PROGRESS, - order: 1, - autoComplete: false, - }, - { - id: TaskStatus.COMPLETED, - label: TaskStatusLabels.COMPLETED, - order: 2, - autoComplete: true, - }, - { - id: TaskStatus.PAUSED, - label: TaskStatusLabels.PAUSED, - order: 3, - autoComplete: false, - }, -]; - -export const KanbanBoard = ({ checklist, onUpdate }: KanbanBoardProps) => { - const t = useTranslations(); - const [isClient, setIsClient] = useState(false); - const [showStatusModal, setShowStatusModal] = useState(false); - const [showArchivedModal, setShowArchivedModal] = useState(false); - const { linkIndex, notes, checklists, appSettings, allSharedItems } = - useAppMode(); - const encodedCategory = encodeCategoryPath( - checklist.category || "Uncategorized" - ); - const isShared = - allSharedItems?.checklists.some( - (sharedChecklist) => - sharedChecklist.id === checklist.id && - sharedChecklist.category === encodedCategory - ) || false; - const { permissions } = usePermissions(); - const { - localChecklist, - isLoading, - showBulkPasteModal, - setShowBulkPasteModal, - focusKey, - refreshChecklist, - getItemsByStatus, - handleDragStart, - handleDragEnd, - handleAddItem, - handleBulkPaste, - handleItemStatusUpdate, - activeItem, - } = useKanbanBoard({ checklist, onUpdate }); - - const statuses = useMemo(() => { - const currentStatuses = localChecklist.statuses || defaultStatuses; - return currentStatuses.map(status => { - if (status.id === TaskStatus.COMPLETED && status.autoComplete === undefined) { - return { ...status, autoComplete: true }; - } - return status; - }); - }, [localChecklist.statuses]); - - const columns = statuses - .sort((a, b) => a.order - b.order) - .map((status) => ({ - id: status.id, - title: status.label, - status: status.id, - })); - - const handleSaveStatuses = async (newStatuses: KanbanStatus[]) => { - const formData = new FormData(); - formData.append("uuid", localChecklist.uuid || ""); - formData.append("statusesStr", JSON.stringify(newStatuses)); - - const result = await updateChecklistStatuses(formData); - if (result.success && result.data) { - onUpdate(result.data); - await refreshChecklist(); - } - }; - - const itemsByStatus = statuses.reduce((acc, status) => { - acc[status.id] = localChecklist.items.filter( - (item) => item.status === status.id && !item.isArchived - ).length; - return acc; - }, {} as Record); - - const archivedItems = localChecklist.items.filter((item) => item.isArchived); - - const handleUnarchive = async (itemId: string) => { - const formData = new FormData(); - formData.append("listId", localChecklist.id); - formData.append("itemId", itemId); - formData.append("category", localChecklist.category || "Uncategorized"); - - const result = await unarchiveItem(formData); - if (result.success && result.data) { - onUpdate(result.data); - await refreshChecklist(); - } - }; - - const handleToggleItem = async (itemId: string, completed: boolean) => { - const newStatus = completed ? TaskStatus.COMPLETED : TaskStatus.TODO; - await handleItemStatusUpdate(itemId, newStatus); - }; - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, - delay: 100, - tolerance: 5, - }, - }), - useSensor(KeyboardSensor) - ); - - useEffect(() => { - setIsClient(true); - }, []); - - const referencingItems = useMemo(() => { - return getReferences( - linkIndex, - localChecklist.uuid, - localChecklist.category, - ItemTypes.CHECKLIST, - notes, - checklists - ); - }, [ - linkIndex, - localChecklist.uuid, - localChecklist.category, - checklists, - notes, - ]); - - return ( -
- {permissions?.canEdit && ( - setShowBulkPasteModal(true)} - isLoading={isLoading} - autoFocus={true} - focusKey={focusKey} - placeholder={t("checklists.addNewTask")} - submitButtonText={t("tasks.addTask")} - /> - )} -
- {permissions?.canEdit && ( - - )} - -
-
- {isClient ? ( - -
- {columns.map((column) => { - const items = getItemsByStatus(column.status); - return ( -
4 - ? "flex-shrink-0 min-w-[20%]" - : "min-w-[24%] " - }`} - > - s.id === column.id)?.color - } - statuses={statuses} - /> -
- ); - })} -
- - - {activeItem ? ( - - ) : null} - -
- ) : ( -
- {columns.map((column) => { - const items = getItemsByStatus(column.status); - return ( -
4 ? "flex-shrink-0" : ""} - style={columns.length > 4 ? { width: "320px" } : undefined} - > - s.id === column.id)?.color - } - statuses={statuses} - /> -
- ); - })} -
- )} - -
- {referencingItems.length > 0 && - appSettings?.editor?.enableBilateralLinks && ( - - )} -
-
- - {showBulkPasteModal && ( - setShowBulkPasteModal(false)} - onSubmit={handleBulkPaste} - isLoading={isLoading} - /> - )} - - {showStatusModal && ( - setShowStatusModal(false)} - currentStatuses={statuses} - onSave={handleSaveStatuses} - itemsByStatus={itemsByStatus} - /> - )} - - {showArchivedModal && ( - setShowArchivedModal(false)} - archivedItems={archivedItems} - onUnarchive={handleUnarchive} - statuses={statuses} - /> - )} -
- ); -}; diff --git a/app/_components/FeatureComponents/Checklists/Parts/Kanban/KanbanItemTimer.tsx b/app/_components/FeatureComponents/Checklists/Parts/Kanban/KanbanItemTimer.tsx deleted file mode 100644 index 3a3f8b96..00000000 --- a/app/_components/FeatureComponents/Checklists/Parts/Kanban/KanbanItemTimer.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import { memo, useState } from "react"; -import { Clock01Icon, TimeQuarterIcon, Add01Icon } from "hugeicons-react"; -import { PauseCircleIcon } from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { Button } from "@/app/_components/GlobalComponents/Buttons/Button"; -import { usePermissions } from "@/app/_providers/PermissionsProvider"; -import { PromptModal } from "@/app/_components/GlobalComponents/Modals/ConfirmationModals/PromptModal"; -import { useTranslations } from "next-intl"; - -interface KanbanItemTimerProps { - totalTime: number; - currentTime: number; - isRunning: boolean; - formatTimerTime: (seconds: number) => string; - onTimerToggle: () => void; - onAddManualTime: (minutes: number) => void; -} - -const KanbanItemTimerComponent = ({ - totalTime, - currentTime, - isRunning, - formatTimerTime, - onTimerToggle, - onAddManualTime, -}: KanbanItemTimerProps) => { - const { permissions } = usePermissions(); - const t = useTranslations(); - const [showTimeModal, setShowTimeModal] = useState(false); - - const handleAddTime = (e: React.MouseEvent) => { - e.stopPropagation(); - setShowTimeModal(true); - }; - - const confirmAddTime = (minutes: string) => { - if (minutes && !isNaN(Number(minutes))) { - onAddManualTime(Number(minutes)); - } - }; - - return ( -
-
-
- - {formatTimerTime(totalTime + currentTime)} -
-
e.stopPropagation()}> - - -
-
- - setShowTimeModal(false)} - onConfirm={confirmAddTime} - title={t("checklists.addTime")} - message={t("checklists.enterTimeInMinutes")} - placeholder="15" - confirmText={t("common.confirm")} - /> -
- ); -}; - -export const KanbanItemTimer = memo(KanbanItemTimerComponent); diff --git a/app/_components/FeatureComponents/Checklists/Parts/Kanban/SubtaskModal.tsx b/app/_components/FeatureComponents/Checklists/Parts/Kanban/SubtaskModal.tsx deleted file mode 100644 index 04c2d2a2..00000000 --- a/app/_components/FeatureComponents/Checklists/Parts/Kanban/SubtaskModal.tsx +++ /dev/null @@ -1,503 +0,0 @@ -"use client"; - -import { useState, useEffect, useMemo } from "react"; -import { Modal } from "@/app/_components/GlobalComponents/Modals/Modal"; -import { Button } from "@/app/_components/GlobalComponents/Buttons/Button"; -import { Item, Checklist } from "@/app/_types"; -import { - createSubItem, - updateItem, - deleteItem, - bulkToggleItems, -} from "@/app/_server/actions/checklist-item"; -import { - Add01Icon, - FloppyDiskIcon, - MultiplicationSignIcon, -} from "hugeicons-react"; -import { NestedChecklistItem } from "@/app/_components/FeatureComponents/Checklists/Parts/Simple/NestedChecklistItem"; -import { convertMarkdownToHtml } from "@/app/_utils/markdown-utils"; -import { usePermissions } from "@/app/_providers/PermissionsProvider"; -import { usePreferredDateTime } from "@/app/_hooks/usePreferredDateTime"; -import { useTranslations } from "next-intl"; - -interface SubtaskModalProps { - checklist: Checklist; - item: Item; - isOpen: boolean; - onClose: () => void; - onUpdate: (updatedChecklist: Checklist) => void; - checklistId: string; - category: string; - isShared: boolean; -} - -const sanitizeDescription = (text: string): string => { - return text.replace(/\n/g, "\\n"); -}; - -const unsanitizeDescription = (text: string): string => { - return text.replace(/\\n/g, "\n"); -}; - -export const SubtaskModal = ({ - checklist, - item: initialItem, - isOpen, - onClose, - onUpdate, - checklistId, - category, - isShared, -}: SubtaskModalProps) => { - const t = useTranslations(); - const { permissions } = usePermissions(); - const { formatDateTimeString } = usePreferredDateTime(); - - const [item, setItem] = useState(initialItem); - const [isEditing, setIsEditing] = useState(false); - const [editText, setEditText] = useState(item.text); - const [editDescription, setEditDescription] = useState( - unsanitizeDescription(item.description || "") - ); - const [newSubtaskText, setNewSubtaskText] = useState(""); - - useEffect(() => { - setItem(initialItem); - setEditText(initialItem.text); - setEditDescription(unsanitizeDescription(initialItem.description || "")); - }, [initialItem]); - - const descriptionHtml = useMemo(() => { - const noDescText = `

${t( - "checklists.noDescription" - )}

`; - if (!item.description) return noDescText; - const unsanitized = unsanitizeDescription(item.description); - const withLineBreaks = unsanitized.replace(/\n/g, " \n"); - return convertMarkdownToHtml(withLineBreaks) || noDescText; - }, [item.description, t]); - - const findItemInChecklist = ( - checklist: Checklist, - itemId: string - ): Item | null => { - const searchItems = (items: Item[]): Item | null => { - for (const item of items) { - if (item.id === itemId) return item; - if (item.children) { - const found = searchItems(item.children); - if (found) return found; - } - } - return null; - }; - return searchItems(checklist.items); - }; - - const handleSave = async () => { - const sanitizedDescription = sanitizeDescription(editDescription.trim()); - const currentUnsanitized = unsanitizeDescription(item.description || ""); - - if ( - editText.trim() !== item.text || - editDescription.trim() !== currentUnsanitized - ) { - const formData = new FormData(); - formData.append("listId", checklistId); - formData.append("itemId", item.id); - formData.append("text", editText.trim()); - formData.append("description", sanitizedDescription); - formData.append("category", category); - - const result = await updateItem(checklist, formData); - if (result.success && result.data) { - onUpdate(result.data); - const updatedItem = findItemInChecklist(result.data, item.id); - if (updatedItem) { - setItem(updatedItem); - setEditText(updatedItem.text); - setEditDescription( - unsanitizeDescription(updatedItem.description || "") - ); - } - } - } - setIsEditing(false); - }; - - const handleAddSubtask = async () => { - if (!newSubtaskText.trim()) return; - - const formData = new FormData(); - formData.append("listId", checklistId); - formData.append("parentId", item.id); - formData.append("text", newSubtaskText.trim()); - formData.append("category", category); - - const result = await createSubItem(formData); - if (result.success && result.data) { - onUpdate(result.data); - const updatedItem = findItemInChecklist(result.data, item.id); - if (updatedItem) { - setItem(updatedItem); - } - setNewSubtaskText(""); - } - }; - - const handleAddNestedSubtask = async (parentId: string, text: string) => { - const formData = new FormData(); - formData.append("listId", checklistId); - formData.append("parentId", parentId); - formData.append("text", text); - formData.append("category", category); - - const result = await createSubItem(formData); - if (result.success && result.data) { - onUpdate(result.data); - const updatedItem = findItemInChecklist(result.data, item.id); - if (updatedItem) { - setItem(updatedItem); - } - } - }; - - const handleToggleSubtask = async (subtaskId: string, completed: boolean) => { - const formData = new FormData(); - formData.append("listId", checklistId); - formData.append("itemId", subtaskId); - formData.append("completed", completed.toString()); - formData.append("category", category); - - const result = await updateItem(checklist, formData); - if (result.success && result.data) { - onUpdate(result.data); - const updatedItem = findItemInChecklist(result.data, item.id); - if (updatedItem) { - setItem(updatedItem); - } - } - }; - - const handleEditSubtask = async (subtaskId: string, text: string) => { - const formData = new FormData(); - formData.append("listId", checklistId); - formData.append("itemId", subtaskId); - formData.append("text", text); - formData.append("category", category); - - const result = await updateItem(checklist, formData); - if (result.success && result.data) { - onUpdate(result.data); - const updatedItem = findItemInChecklist(result.data, item.id); - if (updatedItem) { - setItem(updatedItem); - } - } - }; - - const handleDeleteSubtask = async (subtaskId: string) => { - const formData = new FormData(); - formData.append("listId", checklistId); - formData.append("itemId", subtaskId); - formData.append("category", category); - - const result = await deleteItem(formData); - if (result.success && result.data) { - onUpdate(result.data); - const updatedItem = findItemInChecklist(result.data, item.id); - if (updatedItem) { - setItem(updatedItem); - } - } - }; - - const handleToggleAll = async (completed: boolean) => { - if (!item.children?.length) return; - - const findTargetItems = (items: Item[]): Item[] => { - const targets: Item[] = []; - - items.forEach((subtask) => { - const shouldToggle = completed ? !subtask.completed : subtask.completed; - if (shouldToggle) { - targets.push(subtask); - } - - if (subtask.children && subtask.children.length > 0) { - targets.push(...findTargetItems(subtask.children)); - } - }); - - return targets; - }; - - const targetItems = findTargetItems(item.children); - if (targetItems.length === 0) return; - - const formData = new FormData(); - formData.append("listId", checklistId); - formData.append("completed", String(completed)); - formData.append("itemIds", JSON.stringify(targetItems.map((t) => t.id))); - formData.append("category", category); - - const result = await bulkToggleItems(formData); - if (result.success && result.data) { - onUpdate(result.data); - const updatedItem = findItemInChecklist(result.data, item.id); - if (updatedItem) { - setItem(updatedItem); - } - } - }; - - const renderMetadata = () => { - const metadata = []; - - if (item.createdBy) { - metadata.push( - t("common.createdByOn", { - user: item.createdBy, - date: formatDateTimeString(item.createdAt!), - }) - ); - } - - if (item.lastModifiedBy) { - metadata.push( - t("common.lastModifiedByOn", { - user: item.lastModifiedBy, - date: formatDateTimeString(item.lastModifiedAt!), - }) - ); - } - - if (item.history?.length) { - metadata.push(t("common.statusChanges", { count: item.history.length })); - } - - return metadata.length ? ( -
-
-
- {t("auditLogs.metadata")} -
-
- {metadata.map((text, i) => ( -

- - {text} -

- ))} -
-
-
- ) : null; - }; - - return ( - -
- {isEditing ? ( -
-
- - setEditText(e.target.value)} - className="w-full px-3 py-2 bg-background border border-input rounded-jotty focus:outline-none focus:ring-none focus:ring-ring focus:border-ring transition-all text-base" - placeholder={t("checklists.enterTaskTitle")} - onKeyDown={(e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSave(); - } else if (e.key === "Escape") { - e.preventDefault(); - setEditText(item.text); - setEditDescription( - unsanitizeDescription(item.description || "") - ); - setIsEditing(false); - } - }} - /> -
-
- -