diff --git a/apps/client/src/features/base/components/base-view.tsx b/apps/client/src/features/base/components/base-view.tsx index 9e1773a2d..66bd950d1 100644 --- a/apps/client/src/features/base/components/base-view.tsx +++ b/apps/client/src/features/base/components/base-view.tsx @@ -39,6 +39,8 @@ import { BaseToolbar } from "@/features/base/components/base-toolbar"; import { BaseViewDraftBanner } from "@/features/base/components/base-view-draft-banner"; import { BaseTableSkeleton } from "@/features/base/components/base-table-skeleton"; import { ViewRenderer } from "@/features/base/components/views/view-renderer"; +import { useRowDetailModal } from "@/features/base/hooks/use-row-detail-modal"; +import { RowDetailModal } from "@/features/base/components/row-detail-modal/row-detail-modal"; import classes from "@/features/base/styles/grid.module.css"; type BaseViewProps = { @@ -267,12 +269,11 @@ export function BaseView({ pageId, embedded }: BaseViewProps) { updateViewMutation, ]); - const handleCardClick = useCallback((rowId: string) => { - // Phase 5 wires this to the URL-driven row detail modal. - // Until then, noop — the click still registers and the focus ring - // appears, but nothing opens. - void rowId; - }, []); + const { openRowId, openRow, closeRow } = useRowDetailModal(); + const handleCardClick = useCallback( + (rowId: string) => openRow(rowId), + [openRow], + ); const handleRowReorder = useCallback( (rowId: string, targetRowId: string, dropPosition: "above" | "below") => { @@ -350,44 +351,7 @@ export function BaseView({ pageId, embedded }: BaseViewProps) { // Inline: banner + toolbar live inside the StickyBand (passed via // stickyBandPrelude). The page is the vertical scroll container. return ( - - {banner} - {toolbar} - - } - /> - ); - } - - // Standalone: banner + toolbar sit above the .tableScrollport, which - // is the vertical scroll container. StickyBand inside contains only - // the column-header row. - return ( -
- {banner} - {toolbar} -
+ <> + {banner} + {toolbar} + + } /> + + + ); + } + + // Standalone: banner + toolbar sit above the .tableScrollport, which + // is the vertical scroll container. StickyBand inside contains only + // the column-header row. + return ( + <> +
+ {banner} + {toolbar} +
+ +
-
+ + ); } diff --git a/apps/client/src/features/base/components/row-detail-modal/row-detail-modal.tsx b/apps/client/src/features/base/components/row-detail-modal/row-detail-modal.tsx new file mode 100644 index 000000000..272f247fa --- /dev/null +++ b/apps/client/src/features/base/components/row-detail-modal/row-detail-modal.tsx @@ -0,0 +1,74 @@ +import { Modal, Stack, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useEffect, useMemo } from "react"; +import { + IBase, + IBaseRow, +} from "@/features/base/types/base.types"; +import { useUpdateRowMutation } from "@/features/base/queries/base-row-query"; +import { RowDetailTitle } from "./row-detail-title"; + +type RowDetailModalProps = { + base: IBase; + rows: IBaseRow[]; + openRowId: string | null; + onClose: () => void; +}; + +export function RowDetailModal({ + base, + rows, + openRowId, + onClose, +}: RowDetailModalProps) { + const { t } = useTranslation(); + const updateRowMutation = useUpdateRowMutation(); + + const row = useMemo( + () => (openRowId ? rows.find((r) => r.id === openRowId) : undefined), + [openRowId, rows], + ); + const primaryProperty = useMemo( + () => base.properties.find((p) => p.isPrimary), + [base.properties], + ); + + // If a row was open and disappeared (deleted, filtered out, or not yet + // loaded into the rows page), close the modal. + const wasOpen = !!openRowId && !row; + useEffect(() => { + if (wasOpen) onClose(); + }, [wasOpen, onClose]); + + return ( + + {row ? ( + + { + if (!primaryProperty) return; + updateRowMutation.mutate({ + rowId: row.id, + pageId: base.id, + cells: { [primaryProperty.id]: value }, + }); + }} + /> + {/* Property list goes here in Task 19 */} + {/* Add-property button goes here in Task 20 */} + + ) : ( + {t("Loading…")} + )} + + ); +} diff --git a/apps/client/src/features/base/components/row-detail-modal/row-detail-title.tsx b/apps/client/src/features/base/components/row-detail-modal/row-detail-title.tsx new file mode 100644 index 000000000..114d6bfa5 --- /dev/null +++ b/apps/client/src/features/base/components/row-detail-modal/row-detail-title.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from "react"; +import { TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { IBaseProperty, IBaseRow } from "@/features/base/types/base.types"; + +type RowDetailTitleProps = { + row: IBaseRow; + primaryProperty: IBaseProperty | undefined; + onCommit: (value: string) => void; +}; + +export function RowDetailTitle({ + row, + primaryProperty, + onCommit, +}: RowDetailTitleProps) { + const { t } = useTranslation(); + const initial = primaryProperty + ? (((row.cells ?? {})[primaryProperty.id] as string) ?? "") + : ""; + const [value, setValue] = useState(initial); + + // Re-sync if the underlying row changes (e.g. another client updated it). + useEffect(() => { + setValue(initial); + }, [initial]); + + return ( + setValue(e.currentTarget.value)} + onBlur={() => { + if (value !== initial) onCommit(value); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + (e.currentTarget as HTMLInputElement).blur(); + } + }} + /> + ); +}