mirror of
https://github.com/docmost/docmost.git
synced 2026-06-11 02:36:56 +08:00
feat(bases): row detail modal shell with editable title
This commit is contained in:
@@ -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 (
|
||||
<ViewRenderer
|
||||
base={base}
|
||||
rows={rows}
|
||||
effectiveView={effectiveView}
|
||||
table={table}
|
||||
pageId={pageId}
|
||||
embedded={embedded}
|
||||
hasNextPage={!!hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onFetchNextPage={fetchNextPage}
|
||||
onCellUpdate={handleCellUpdate}
|
||||
onAddRow={handleAddRow}
|
||||
onColumnReorder={handleColumnReorder}
|
||||
onResizeEnd={handleResizeEnd}
|
||||
onRowReorder={handleRowReorder}
|
||||
onCardClick={handleCardClick}
|
||||
persistViewConfig={persistViewConfig}
|
||||
scrollportRef={scrollportRef}
|
||||
stickyBandPrelude={
|
||||
<>
|
||||
{banner}
|
||||
{toolbar}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Standalone: banner + toolbar sit above the .tableScrollport, which
|
||||
// is the vertical scroll container. StickyBand inside contains only
|
||||
// the column-header row.
|
||||
return (
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", height: "100%" }}
|
||||
>
|
||||
{banner}
|
||||
{toolbar}
|
||||
<div className={classes.tableScrollport} ref={scrollportRef}>
|
||||
<>
|
||||
<ViewRenderer
|
||||
base={base}
|
||||
rows={rows}
|
||||
@@ -406,8 +370,61 @@ export function BaseView({ pageId, embedded }: BaseViewProps) {
|
||||
onCardClick={handleCardClick}
|
||||
persistViewConfig={persistViewConfig}
|
||||
scrollportRef={scrollportRef}
|
||||
stickyBandPrelude={
|
||||
<>
|
||||
{banner}
|
||||
{toolbar}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<RowDetailModal
|
||||
base={base}
|
||||
rows={rows}
|
||||
openRowId={openRowId}
|
||||
onClose={closeRow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Standalone: banner + toolbar sit above the .tableScrollport, which
|
||||
// is the vertical scroll container. StickyBand inside contains only
|
||||
// the column-header row.
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", height: "100%" }}
|
||||
>
|
||||
{banner}
|
||||
{toolbar}
|
||||
<div className={classes.tableScrollport} ref={scrollportRef}>
|
||||
<ViewRenderer
|
||||
base={base}
|
||||
rows={rows}
|
||||
effectiveView={effectiveView}
|
||||
table={table}
|
||||
pageId={pageId}
|
||||
embedded={embedded}
|
||||
hasNextPage={!!hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onFetchNextPage={fetchNextPage}
|
||||
onCellUpdate={handleCellUpdate}
|
||||
onAddRow={handleAddRow}
|
||||
onColumnReorder={handleColumnReorder}
|
||||
onResizeEnd={handleResizeEnd}
|
||||
onRowReorder={handleRowReorder}
|
||||
onCardClick={handleCardClick}
|
||||
persistViewConfig={persistViewConfig}
|
||||
scrollportRef={scrollportRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RowDetailModal
|
||||
base={base}
|
||||
rows={rows}
|
||||
openRowId={openRowId}
|
||||
onClose={closeRow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Modal
|
||||
opened={!!row}
|
||||
onClose={onClose}
|
||||
size="lg"
|
||||
withCloseButton
|
||||
centered
|
||||
title={null}
|
||||
>
|
||||
{row ? (
|
||||
<Stack gap="md">
|
||||
<RowDetailTitle
|
||||
row={row}
|
||||
primaryProperty={primaryProperty}
|
||||
onCommit={(value) => {
|
||||
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 */}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text c="dimmed">{t("Loading…")}</Text>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<TextInput
|
||||
autoFocus
|
||||
placeholder={t("Untitled")}
|
||||
value={value}
|
||||
variant="unstyled"
|
||||
size="xl"
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
onBlur={() => {
|
||||
if (value !== initial) onCommit(value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user