diff --git a/apps/web/core/components/base-layouts/constants.ts b/apps/web/core/components/base-layouts/constants.ts new file mode 100644 index 000000000..60e45a372 --- /dev/null +++ b/apps/web/core/components/base-layouts/constants.ts @@ -0,0 +1,15 @@ +import { BoardLayoutIcon, ListLayoutIcon } from "@plane/propel/icons"; +import type { IBaseLayoutConfig } from "@plane/types"; + +export const BASE_LAYOUTS: IBaseLayoutConfig[] = [ + { + key: "list", + icon: ListLayoutIcon, + label: "List Layout", + }, + { + key: "kanban", + icon: BoardLayoutIcon, + label: "Board Layout", + }, +]; diff --git a/apps/web/core/components/base-layouts/hooks/use-group-drop-target.ts b/apps/web/core/components/base-layouts/hooks/use-group-drop-target.ts new file mode 100644 index 000000000..5fa232092 --- /dev/null +++ b/apps/web/core/components/base-layouts/hooks/use-group-drop-target.ts @@ -0,0 +1,55 @@ +import { useEffect, useRef, useState } from "react"; +import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; + +interface UseGroupDropTargetProps { + groupId: string; + enableDragDrop?: boolean; + onDrop?: (itemId: string, targetId: string | null, sourceGroupId: string, targetGroupId: string) => void; +} + +interface DragSourceData { + id: string; + groupId: string; + type: "ITEM" | "GROUP"; +} + +/** + * A hook that turns an element into a valid drop target for group drag-and-drop. + * + * @returns groupRef (attach to the droppable container) and isDraggingOver (for visual feedback) + */ +export const useGroupDropTarget = ({ groupId, enableDragDrop = false, onDrop }: UseGroupDropTargetProps) => { + const groupRef = useRef(null); + const [isDraggingOver, setIsDraggingOver] = useState(false); + + useEffect(() => { + const element = groupRef.current; + if (!element || !enableDragDrop || !onDrop) return; + + const cleanup = dropTargetForElements({ + element, + getData: () => ({ groupId, type: "GROUP" }), + + canDrop: ({ source }) => { + const data = (source?.data || {}) as Partial; + return data.type === "ITEM" && !!data.groupId && data.groupId !== groupId; + }, + + onDragEnter: () => setIsDraggingOver(true), + onDragLeave: () => setIsDraggingOver(false), + + onDrop: ({ source }) => { + setIsDraggingOver(false); + const data = (source?.data || {}) as Partial; + if (data.type !== "ITEM" || !data.id || !data.groupId) return; + if (data.groupId !== groupId) { + onDrop(data.id, null, data.groupId, groupId); + } + }, + }); + + return cleanup; + }, [groupId, enableDragDrop, onDrop]); + + return { groupRef, isDraggingOver }; +}; diff --git a/apps/web/core/components/base-layouts/hooks/use-layout-state.ts b/apps/web/core/components/base-layouts/hooks/use-layout-state.ts new file mode 100644 index 000000000..8d8bc75b2 --- /dev/null +++ b/apps/web/core/components/base-layouts/hooks/use-layout-state.ts @@ -0,0 +1,58 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; + +type UseLayoutStateProps = + | { + mode: "external"; + externalCollapsedGroups: string[]; + externalOnToggleGroup: (groupId: string) => void; + enableAutoScroll?: boolean; + } + | { + mode?: "internal"; + enableAutoScroll?: boolean; + }; + +/** + * Hook for managing layout state including: + * - Collapsed/expanded group tracking (internal or external) + * - Auto-scroll setup for drag-and-drop + */ +export const useLayoutState = (props: UseLayoutStateProps = { mode: "internal" }) => { + const containerRef = useRef(null); + + // Internal fallback state + const [internalCollapsedGroups, setInternalCollapsedGroups] = useState([]); + + // Stable internal toggle function + const internalToggleGroup = useCallback((groupId: string) => { + setInternalCollapsedGroups((prev) => + prev.includes(groupId) ? prev.filter((id) => id !== groupId) : [...prev, groupId] + ); + }, []); + + const useExternal = props.mode === "external"; + const collapsedGroups = useExternal ? props.externalCollapsedGroups : internalCollapsedGroups; + const onToggleGroup = useExternal ? props.externalOnToggleGroup : internalToggleGroup; + + // Enable auto-scroll for DnD + useEffect(() => { + const element = containerRef.current; + if (!element || !props.enableAutoScroll) return; + + const cleanup = combine( + autoScrollForElements({ + element, + }) + ); + + return cleanup; + }, [props.enableAutoScroll]); + + return { + containerRef, + collapsedGroups, + onToggleGroup, + }; +}; diff --git a/apps/web/core/components/base-layouts/kanban/group-header.tsx b/apps/web/core/components/base-layouts/kanban/group-header.tsx new file mode 100644 index 000000000..be8487995 --- /dev/null +++ b/apps/web/core/components/base-layouts/kanban/group-header.tsx @@ -0,0 +1,14 @@ +import type { IGroupHeaderProps } from "@plane/types"; + +export const GroupHeader = ({ group, itemCount, onToggleGroup }: IGroupHeaderProps) => ( + +); diff --git a/apps/web/core/components/base-layouts/kanban/group.tsx b/apps/web/core/components/base-layouts/kanban/group.tsx new file mode 100644 index 000000000..2cd291897 --- /dev/null +++ b/apps/web/core/components/base-layouts/kanban/group.tsx @@ -0,0 +1,96 @@ +import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; +import type { IBaseLayoutsKanbanItem, IBaseLayoutsKanbanGroupProps } from "@plane/types"; +import { cn } from "@plane/utils"; +import { useGroupDropTarget } from "../hooks/use-group-drop-target"; +import { GroupHeader } from "./group-header"; +import { BaseKanbanItem } from "./item"; + +export const BaseKanbanGroup = observer((props: IBaseLayoutsKanbanGroupProps) => { + const { + group, + itemIds, + items, + renderItem, + renderGroupHeader, + isCollapsed, + onToggleGroup, + enableDragDrop = false, + onDrop, + canDrag, + groupClassName, + loadMoreItems: _loadMoreItems, + } = props; + + const { t } = useTranslation(); + const { groupRef, isDraggingOver } = useGroupDropTarget({ + groupId: group.id, + enableDragDrop, + onDrop, + }); + + return ( +
+ {/* Group Header */} +
+ {renderGroupHeader ? ( + renderGroupHeader({ group, itemCount: itemIds.length, isCollapsed, onToggleGroup }) + ) : ( + + )} +
+ + {/* Group Items */} + {!isCollapsed && ( +
+ {itemIds.map((itemId, index) => { + const item = items[itemId]; + if (!item) return null; + + return ( + + ); + })} + + {itemIds.length === 0 && ( +
+ {t("common.no_items_in_this_group")} +
+ )} +
+ )} + + {isDraggingOver && enableDragDrop && ( +
+
+ {t("common.drop_here_to_move")} +
+
+ )} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/kanban/item.tsx b/apps/web/core/components/base-layouts/kanban/item.tsx new file mode 100644 index 000000000..325e6f0d6 --- /dev/null +++ b/apps/web/core/components/base-layouts/kanban/item.tsx @@ -0,0 +1,40 @@ +import { useEffect, useRef } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { observer } from "mobx-react"; +import type { IBaseLayoutsKanbanItem, IBaseLayoutsKanbanItemProps } from "@plane/types"; + +export const BaseKanbanItem = observer((props: IBaseLayoutsKanbanItemProps) => { + const { item, groupId, renderItem, enableDragDrop, canDrag } = props; + + const itemRef = useRef(null); + + const isDragAllowed = canDrag ? canDrag(item) : true; + + // Setup draggable and drop target + useEffect(() => { + const element = itemRef.current; + if (!element || !enableDragDrop) return; + + return combine( + draggable({ + element, + canDrag: () => isDragAllowed, + getInitialData: () => ({ id: item.id, type: "ITEM", groupId }), + }), + dropTargetForElements({ + element, + getData: () => ({ id: item.id, groupId, type: "ITEM" }), + canDrop: ({ source }) => source?.data?.id !== item.id, + }) + ); + }, [enableDragDrop, isDragAllowed, item.id, groupId]); + + const renderedItem = renderItem(item, groupId); + + return ( +
+ {renderedItem} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/kanban/layout.tsx b/apps/web/core/components/base-layouts/kanban/layout.tsx new file mode 100644 index 000000000..447a6dcb4 --- /dev/null +++ b/apps/web/core/components/base-layouts/kanban/layout.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { observer } from "mobx-react"; +import type { IBaseLayoutsKanbanItem, IBaseLayoutsKanbanProps } from "@plane/types"; +import { cn } from "@plane/utils"; +import { useLayoutState } from "../hooks/use-layout-state"; +import { BaseKanbanGroup } from "./group"; + +export const BaseKanbanLayout = observer((props: IBaseLayoutsKanbanProps) => { + const { + items, + groups, + groupedItemIds, + renderItem, + renderGroupHeader, + onDrop, + canDrag, + className, + groupClassName, + showEmptyGroups = true, + enableDragDrop = false, + loadMoreItems, + collapsedGroups: externalCollapsedGroups = [], + onToggleGroup: externalOnToggleGroup = () => {}, + } = props; + + const { containerRef, collapsedGroups, onToggleGroup } = useLayoutState({ + mode: "external", + externalCollapsedGroups, + externalOnToggleGroup, + }); + + return ( +
+ {groups.map((group) => { + const itemIds = groupedItemIds[group.id] || []; + const isCollapsed = collapsedGroups.includes(group.id); + + if (!showEmptyGroups && itemIds.length === 0) return null; + + return ( + + ); + })} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/layout-switcher.tsx b/apps/web/core/components/base-layouts/layout-switcher.tsx new file mode 100644 index 000000000..5fe9c57cf --- /dev/null +++ b/apps/web/core/components/base-layouts/layout-switcher.tsx @@ -0,0 +1,50 @@ +"use client"; + +import React from "react"; +import { Tooltip } from "@plane/propel/tooltip"; +import type { TBaseLayoutType } from "@plane/types"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +import { BASE_LAYOUTS } from "./constants"; + +type Props = { + layouts?: TBaseLayoutType[]; + onChange: (layout: TBaseLayoutType) => void; + selectedLayout: TBaseLayoutType | undefined; +}; + +export const LayoutSwitcher: React.FC = (props) => { + const { layouts, onChange, selectedLayout } = props; + const { isMobile } = usePlatformOS(); + + const handleOnChange = (layoutKey: TBaseLayoutType) => { + if (selectedLayout !== layoutKey) { + onChange(layoutKey); + } + }; + + return ( +
+ {BASE_LAYOUTS.filter((l) => (layouts ? layouts.includes(l.key) : true)).map((layout) => { + const Icon = layout.icon; + return ( + + + + ); + })} +
+ ); +}; diff --git a/apps/web/core/components/base-layouts/list/group-header.tsx b/apps/web/core/components/base-layouts/list/group-header.tsx new file mode 100644 index 000000000..cbdca7fad --- /dev/null +++ b/apps/web/core/components/base-layouts/list/group-header.tsx @@ -0,0 +1,12 @@ +import type { IGroupHeaderProps } from "@plane/types"; + +export const GroupHeader = ({ group, itemCount, onToggleGroup }: IGroupHeaderProps) => ( + +); diff --git a/apps/web/core/components/base-layouts/list/group.tsx b/apps/web/core/components/base-layouts/list/group.tsx new file mode 100644 index 000000000..15a8079aa --- /dev/null +++ b/apps/web/core/components/base-layouts/list/group.tsx @@ -0,0 +1,85 @@ +import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; +import type { IBaseLayoutsListItem, IBaseLayoutsListGroupProps } from "@plane/types"; +import { cn, Row } from "@plane/ui"; +import { useGroupDropTarget } from "../hooks/use-group-drop-target"; +import { GroupHeader } from "./group-header"; +import { BaseListItem } from "./item"; + +export const BaseListGroup = observer((props: IBaseLayoutsListGroupProps) => { + const { + group, + itemIds, + items, + isCollapsed, + onToggleGroup, + renderItem, + renderGroupHeader, + enableDragDrop = false, + onDrop, + canDrag, + loadMoreItems: _loadMoreItems, + } = props; + + const { t } = useTranslation(); + const { groupRef, isDraggingOver } = useGroupDropTarget({ + groupId: group.id, + enableDragDrop, + onDrop, + }); + + return ( +
+ {/* Group Header */} + + {renderGroupHeader ? ( + renderGroupHeader({ group, itemCount: itemIds.length, isCollapsed, onToggleGroup }) + ) : ( + + )} + + + {/* Group Items */} + {!isCollapsed && ( +
+ {itemIds.map((itemId: string, index: number) => { + const item = items[itemId]; + if (!item) return null; + + return ( + + ); + })} +
+ )} + + {isDraggingOver && enableDragDrop && ( +
+
+ {t("common.drop_here_to_move")} +
+
+ )} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/list/item.tsx b/apps/web/core/components/base-layouts/list/item.tsx new file mode 100644 index 000000000..cfaa74502 --- /dev/null +++ b/apps/web/core/components/base-layouts/list/item.tsx @@ -0,0 +1,38 @@ +import { useEffect, useRef } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { observer } from "mobx-react"; +import type { IBaseLayoutsListItem, IBaseLayoutsListItemProps } from "@plane/types"; + +export const BaseListItem = observer((props: IBaseLayoutsListItemProps) => { + const { item, groupId, renderItem, enableDragDrop, canDrag, isLast: _isLast, index: _index } = props; + const itemRef = useRef(null); + + const isDragAllowed = canDrag ? canDrag(item) : true; + + useEffect(() => { + const element = itemRef.current; + if (!element || !enableDragDrop) return; + + return combine( + draggable({ + element, + canDrag: () => isDragAllowed, + getInitialData: () => ({ id: item.id, type: "ITEM", groupId }), + }), + dropTargetForElements({ + element, + getData: () => ({ groupId, type: "ITEM" }), + canDrop: ({ source }) => source?.data?.id !== item.id, + }) + ); + }, [enableDragDrop, isDragAllowed, item.id, groupId]); + + const renderedItem = renderItem(item, groupId); + + return ( +
+ {renderedItem} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/list/layout.tsx b/apps/web/core/components/base-layouts/list/layout.tsx new file mode 100644 index 000000000..e423e16d5 --- /dev/null +++ b/apps/web/core/components/base-layouts/list/layout.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { observer } from "mobx-react"; +import type { IBaseLayoutsListItem, IBaseLayoutsListProps } from "@plane/types"; +import { cn } from "@plane/ui"; +import { useLayoutState } from "../hooks/use-layout-state"; +import { BaseListGroup } from "./group"; + +export const BaseListLayout = observer((props: IBaseLayoutsListProps) => { + const { + items, + groupedItemIds, + groups, + renderItem, + renderGroupHeader, + enableDragDrop = false, + onDrop, + canDrag, + showEmptyGroups = false, + collapsedGroups: externalCollapsedGroups = [], + onToggleGroup: externalOnToggleGroup = () => {}, + loadMoreItems, + className, + } = props; + + const { containerRef, collapsedGroups, onToggleGroup } = useLayoutState({ + mode: "external", + externalCollapsedGroups, + externalOnToggleGroup, + }); + + return ( +
+
+ {groups.map((group) => { + const itemIds = groupedItemIds[group.id] || []; + const isCollapsed = collapsedGroups.includes(group.id); + + if (!showEmptyGroups && itemIds.length === 0) return null; + + return ( + + ); + })} +
+
+ ); +}); diff --git a/apps/web/core/components/base-layouts/loaders/layout-loader.tsx b/apps/web/core/components/base-layouts/loaders/layout-loader.tsx new file mode 100644 index 000000000..d81d6718d --- /dev/null +++ b/apps/web/core/components/base-layouts/loaders/layout-loader.tsx @@ -0,0 +1,24 @@ +import type { TBaseLayoutType } from "@plane/types"; +import { KanbanLayoutLoader } from "@/components/ui/loader/layouts/kanban-layout-loader"; +import { ListLayoutLoader } from "@/components/ui/loader/layouts/list-layout-loader"; + +interface GenericLayoutLoaderProps { + layout: TBaseLayoutType; + /** Optional custom loaders to override defaults */ + customLoaders?: Partial>; +} + +export const GenericLayoutLoader = ({ layout, customLoaders }: GenericLayoutLoaderProps) => { + const CustomLoader = customLoaders?.[layout]; + if (CustomLoader) return ; + + switch (layout) { + case "list": + return ; + case "kanban": + return ; + default: + console.warn(`Unknown layout: ${layout}`); + return null; + } +}; diff --git a/packages/i18n/src/locales/cs/translations.ts b/packages/i18n/src/locales/cs/translations.ts index c59a5f9fa..3ac7803a7 100644 --- a/packages/i18n/src/locales/cs/translations.ts +++ b/packages/i18n/src/locales/cs/translations.ts @@ -639,6 +639,8 @@ export default { }, common: { all: "Vše", + no_items_in_this_group: "V této skupině nejsou žádné položky", + drop_here_to_move: "Přetáhněte sem pro přesunutí", states: "Stavy", state: "Stav", state_groups: "Skupiny stavů", diff --git a/packages/i18n/src/locales/de/translations.ts b/packages/i18n/src/locales/de/translations.ts index 681ae8f25..009233048 100644 --- a/packages/i18n/src/locales/de/translations.ts +++ b/packages/i18n/src/locales/de/translations.ts @@ -653,6 +653,8 @@ export default { }, common: { all: "Alle", + no_items_in_this_group: "Keine Elemente in dieser Gruppe", + drop_here_to_move: "Hier ablegen zum Verschieben", states: "Status", state: "Status", state_groups: "Statusgruppen", diff --git a/packages/i18n/src/locales/en/translations.ts b/packages/i18n/src/locales/en/translations.ts index 5c49e5e41..daa3d9e1f 100644 --- a/packages/i18n/src/locales/en/translations.ts +++ b/packages/i18n/src/locales/en/translations.ts @@ -478,6 +478,8 @@ export default { }, common: { all: "All", + no_items_in_this_group: "No items in this group", + drop_here_to_move: "Drop here to move", states: "States", state: "State", state_groups: "State groups", diff --git a/packages/i18n/src/locales/es/translations.ts b/packages/i18n/src/locales/es/translations.ts index 28390ed4f..05912437e 100644 --- a/packages/i18n/src/locales/es/translations.ts +++ b/packages/i18n/src/locales/es/translations.ts @@ -654,6 +654,8 @@ export default { }, common: { all: "Todo", + no_items_in_this_group: "No hay elementos en este grupo", + drop_here_to_move: "Suelta aquí para mover", states: "Estados", state: "Estado", state_groups: "Grupos de estados", diff --git a/packages/i18n/src/locales/fr/translations.ts b/packages/i18n/src/locales/fr/translations.ts index ded577c97..82cd79968 100644 --- a/packages/i18n/src/locales/fr/translations.ts +++ b/packages/i18n/src/locales/fr/translations.ts @@ -652,6 +652,8 @@ export default { }, common: { all: "Tout", + no_items_in_this_group: "Aucun élément dans ce groupe", + drop_here_to_move: "Déposer ici pour déplacer", states: "États", state: "État", state_groups: "Groupes d'états", diff --git a/packages/i18n/src/locales/id/translations.ts b/packages/i18n/src/locales/id/translations.ts index 50ae0e882..a856549c5 100644 --- a/packages/i18n/src/locales/id/translations.ts +++ b/packages/i18n/src/locales/id/translations.ts @@ -644,6 +644,8 @@ export default { }, common: { all: "Semua", + no_items_in_this_group: "Tidak ada item dalam grup ini", + drop_here_to_move: "Letakkan di sini untuk memindahkan", states: "Negara-negara", state: "Negara", state_groups: "Kelompok negara", diff --git a/packages/i18n/src/locales/it/translations.ts b/packages/i18n/src/locales/it/translations.ts index a8e043e7b..8036a770b 100644 --- a/packages/i18n/src/locales/it/translations.ts +++ b/packages/i18n/src/locales/it/translations.ts @@ -647,6 +647,8 @@ export default { }, common: { all: "Tutti", + no_items_in_this_group: "Nessun elemento in questo gruppo", + drop_here_to_move: "Rilascia qui per spostare", states: "Stati", state: "Stato", state_groups: "Gruppi di stati", diff --git a/packages/i18n/src/locales/ja/translations.ts b/packages/i18n/src/locales/ja/translations.ts index 85e1cf5b5..c074b0319 100644 --- a/packages/i18n/src/locales/ja/translations.ts +++ b/packages/i18n/src/locales/ja/translations.ts @@ -640,6 +640,8 @@ export default { }, common: { all: "すべて", + no_items_in_this_group: "このグループにアイテムはありません", + drop_here_to_move: "移動するにはここにドロップ", states: "ステータス", state: "ステータス", state_groups: "ステータスグループ", diff --git a/packages/i18n/src/locales/ko/translations.ts b/packages/i18n/src/locales/ko/translations.ts index dd377c5a0..2d854b6c3 100644 --- a/packages/i18n/src/locales/ko/translations.ts +++ b/packages/i18n/src/locales/ko/translations.ts @@ -634,6 +634,8 @@ export default { }, common: { all: "모두", + no_items_in_this_group: "이 그룹에 항목이 없습니다", + drop_here_to_move: "이동하려면 여기에 드롭하세요", states: "상태", state: "상태", state_groups: "상태 그룹", diff --git a/packages/i18n/src/locales/pl/translations.ts b/packages/i18n/src/locales/pl/translations.ts index c257acbe8..5d0e1a8b4 100644 --- a/packages/i18n/src/locales/pl/translations.ts +++ b/packages/i18n/src/locales/pl/translations.ts @@ -640,6 +640,8 @@ export default { }, common: { all: "Wszystko", + no_items_in_this_group: "Brak elementów w tej grupie", + drop_here_to_move: "Upuść tutaj, aby przenieść", states: "Stany", state: "Stan", state_groups: "Grupy stanów", diff --git a/packages/i18n/src/locales/pt-BR/translations.ts b/packages/i18n/src/locales/pt-BR/translations.ts index d6482cbf9..417660f59 100644 --- a/packages/i18n/src/locales/pt-BR/translations.ts +++ b/packages/i18n/src/locales/pt-BR/translations.ts @@ -651,6 +651,8 @@ export default { }, common: { all: "Todos", + no_items_in_this_group: "Nenhum item neste grupo", + drop_here_to_move: "Solte aqui para mover", states: "Estados", state: "Estado", state_groups: "Grupos de estado", diff --git a/packages/i18n/src/locales/ro/translations.ts b/packages/i18n/src/locales/ro/translations.ts index ff824443e..e4377cccd 100644 --- a/packages/i18n/src/locales/ro/translations.ts +++ b/packages/i18n/src/locales/ro/translations.ts @@ -646,6 +646,8 @@ export default { }, common: { all: "Toate", + no_items_in_this_group: "Nu există elemente în acest grup", + drop_here_to_move: "Eliberează aici pentru a muta", states: "Stări", state: "Stare", state_groups: "Grupuri de stări", diff --git a/packages/i18n/src/locales/ru/translations.ts b/packages/i18n/src/locales/ru/translations.ts index 26f35b1bf..9dc0c44fd 100644 --- a/packages/i18n/src/locales/ru/translations.ts +++ b/packages/i18n/src/locales/ru/translations.ts @@ -643,6 +643,8 @@ export default { }, common: { all: "Все", + no_items_in_this_group: "В этой группе нет элементов", + drop_here_to_move: "Перетащите сюда для перемещения", states: "Статусы", state: "Статус", state_groups: "Группы статусов", diff --git a/packages/i18n/src/locales/sk/translations.ts b/packages/i18n/src/locales/sk/translations.ts index fd30f4088..5191022e3 100644 --- a/packages/i18n/src/locales/sk/translations.ts +++ b/packages/i18n/src/locales/sk/translations.ts @@ -641,6 +641,8 @@ export default { }, common: { all: "Všetko", + no_items_in_this_group: "V tejto skupine nie sú žiadne položky", + drop_here_to_move: "Presuňte sem na presunutie", states: "Stavy", state: "Stav", state_groups: "Skupiny stavov", diff --git a/packages/i18n/src/locales/tr-TR/translations.ts b/packages/i18n/src/locales/tr-TR/translations.ts index 64eb23ecf..b094e65db 100644 --- a/packages/i18n/src/locales/tr-TR/translations.ts +++ b/packages/i18n/src/locales/tr-TR/translations.ts @@ -641,6 +641,8 @@ export default { }, common: { all: "Tümü", + no_items_in_this_group: "Bu grupta öğe yok", + drop_here_to_move: "Taşımak için buraya bırakın", states: "Durumlar", state: "Durum", state_groups: "Durum grupları", diff --git a/packages/i18n/src/locales/ua/translations.ts b/packages/i18n/src/locales/ua/translations.ts index 07c43d4fa..f0d0f474e 100644 --- a/packages/i18n/src/locales/ua/translations.ts +++ b/packages/i18n/src/locales/ua/translations.ts @@ -643,6 +643,8 @@ export default { }, common: { all: "Усе", + no_items_in_this_group: "У цій групі немає елементів", + drop_here_to_move: "Перетягніть сюди для переміщення", states: "Стани", state: "Стан", state_groups: "Групи станів", diff --git a/packages/i18n/src/locales/vi-VN/translations.ts b/packages/i18n/src/locales/vi-VN/translations.ts index e279c5103..70ca05148 100644 --- a/packages/i18n/src/locales/vi-VN/translations.ts +++ b/packages/i18n/src/locales/vi-VN/translations.ts @@ -648,6 +648,8 @@ export default { }, common: { all: "Tất cả", + no_items_in_this_group: "Không có mục nào trong nhóm này", + drop_here_to_move: "Thả vào đây để di chuyển", states: "Trạng thái", state: "Trạng thái", state_groups: "Nhóm trạng thái", diff --git a/packages/i18n/src/locales/zh-CN/translations.ts b/packages/i18n/src/locales/zh-CN/translations.ts index 8a0014fee..568379dbe 100644 --- a/packages/i18n/src/locales/zh-CN/translations.ts +++ b/packages/i18n/src/locales/zh-CN/translations.ts @@ -629,6 +629,8 @@ export default { }, common: { all: "全部", + no_items_in_this_group: "此组中没有项目", + drop_here_to_move: "拖放到此处以移动", states: "状态", state: "状态", state_groups: "状态组", diff --git a/packages/i18n/src/locales/zh-TW/translations.ts b/packages/i18n/src/locales/zh-TW/translations.ts index b3de9a0a8..eb754bf98 100644 --- a/packages/i18n/src/locales/zh-TW/translations.ts +++ b/packages/i18n/src/locales/zh-TW/translations.ts @@ -628,6 +628,8 @@ export default { }, common: { all: "全部", + no_items_in_this_group: "此群組中沒有項目", + drop_here_to_move: "拖放到此處以移動", states: "狀態", state: "狀態", state_groups: "狀態群組", diff --git a/packages/types/src/base-layouts/base.ts b/packages/types/src/base-layouts/base.ts new file mode 100644 index 000000000..9848ca425 --- /dev/null +++ b/packages/types/src/base-layouts/base.ts @@ -0,0 +1,101 @@ +import type { ReactNode } from "react"; + +// Base Types + +export interface IBaseLayoutsBaseItem { + id: string; + [key: string]: unknown; +} + +export interface IBaseLayoutsBaseGroup { + id: string; + name: string; + icon?: ReactNode; + payload?: Record; + count?: number; +} + +// Drag & Drop Types + +export interface IDragDropHandlers { + enableDragDrop?: boolean; + onDrop?: ( + sourceId: string, + destinationId: string | null, + sourceGroupId: string, + destinationGroupId: string + ) => Promise; + canDrag?: (item: T) => boolean; +} + +// Render Props + +export interface IItemRenderProps { + renderItem: (item: T, groupId: string) => ReactNode; +} + +export interface IGroupHeaderControls { + isCollapsed: boolean; + onToggleGroup: (groupId: string) => void; +} + +export interface IGroupHeaderProps extends IGroupHeaderControls { + group: IBaseLayoutsBaseGroup; + itemCount: number; +} + +export interface IGroupRenderProps { + renderGroupHeader?: (props: IGroupHeaderProps) => ReactNode; +} + +export interface IRenderProps extends IItemRenderProps, IGroupRenderProps {} + +// Layout Configuration + +export type TBaseLayoutType = "list" | "kanban"; + +export interface IBaseLayoutConfig { + key: TBaseLayoutType; + icon: React.ComponentType>; + label: string; +} + +// Base Layout Props +export interface IBaseLayoutsBaseProps extends IDragDropHandlers, IRenderProps { + items: Record; + groupedItemIds: Record; + groups: IBaseLayoutsBaseGroup[]; + + collapsedGroups?: string[]; + onToggleGroup?: (groupId: string) => void; + + isLoading?: boolean; + loadMoreItems?: (groupId: string) => void; + + showEmptyGroups?: boolean; + className?: string; +} + +// Group Props + +export interface IBaseLayoutsBaseGroupProps + extends IDragDropHandlers, + IRenderProps { + group: IBaseLayoutsBaseGroup; + itemIds: string[]; + items: Record; + isCollapsed: boolean; + onToggleGroup: (groupId: string) => void; + loadMoreItems?: (groupId: string) => void; +} + +// Item Props + +export interface IBaseLayoutsBaseItemProps + extends IDragDropHandlers, + IItemRenderProps { + item: T; + index: number; + groupId: string; + isLast: boolean; +} diff --git a/packages/types/src/base-layouts/index.ts b/packages/types/src/base-layouts/index.ts new file mode 100644 index 000000000..2545d81e2 --- /dev/null +++ b/packages/types/src/base-layouts/index.ts @@ -0,0 +1,3 @@ +export * from "./base"; +export * from "./list"; +export * from "./kanban"; diff --git a/packages/types/src/base-layouts/kanban.ts b/packages/types/src/base-layouts/kanban.ts new file mode 100644 index 000000000..318c36419 --- /dev/null +++ b/packages/types/src/base-layouts/kanban.ts @@ -0,0 +1,24 @@ +import type { + IBaseLayoutsBaseItem, + IBaseLayoutsBaseProps, + IBaseLayoutsBaseGroupProps, + IBaseLayoutsBaseItemProps, +} from "./base"; + +export type IBaseLayoutsKanbanItem = IBaseLayoutsBaseItem; + +// Main Kanban Layout Props + +export interface IBaseLayoutsKanbanProps extends IBaseLayoutsBaseProps { + groupClassName?: string; +} + +// Kanban Column/Group Props + +export interface IBaseLayoutsKanbanGroupProps extends IBaseLayoutsBaseGroupProps { + groupClassName?: string; +} + +// Kanban Card/Item Props + +export type IBaseLayoutsKanbanItemProps = IBaseLayoutsBaseItemProps; diff --git a/packages/types/src/base-layouts/list.ts b/packages/types/src/base-layouts/list.ts new file mode 100644 index 000000000..e0ac28275 --- /dev/null +++ b/packages/types/src/base-layouts/list.ts @@ -0,0 +1,20 @@ +import type { + IBaseLayoutsBaseItem, + IBaseLayoutsBaseProps, + IBaseLayoutsBaseGroupProps, + IBaseLayoutsBaseItemProps, +} from "./base"; + +export type IBaseLayoutsListItem = IBaseLayoutsBaseItem; + +// Main List Layout Props + +export type IBaseLayoutsListProps = IBaseLayoutsBaseProps; + +// Group component props + +export type IBaseLayoutsListGroupProps = IBaseLayoutsBaseGroupProps; + +// Item component props + +export type IBaseLayoutsListItemProps = IBaseLayoutsBaseItemProps; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ea6ee4080..c646506a1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -47,3 +47,4 @@ export * from "./workspace"; export * from "./workspace-draft-issues/base"; export * from "./workspace-notifications"; export * from "./workspace-views"; +export * from "./base-layouts";