mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 10:05:03 +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 { BaseViewDraftBanner } from "@/features/base/components/base-view-draft-banner";
|
||||||
import { BaseTableSkeleton } from "@/features/base/components/base-table-skeleton";
|
import { BaseTableSkeleton } from "@/features/base/components/base-table-skeleton";
|
||||||
import { ViewRenderer } from "@/features/base/components/views/view-renderer";
|
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";
|
import classes from "@/features/base/styles/grid.module.css";
|
||||||
|
|
||||||
type BaseViewProps = {
|
type BaseViewProps = {
|
||||||
@@ -267,12 +269,11 @@ export function BaseView({ pageId, embedded }: BaseViewProps) {
|
|||||||
updateViewMutation,
|
updateViewMutation,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCardClick = useCallback((rowId: string) => {
|
const { openRowId, openRow, closeRow } = useRowDetailModal();
|
||||||
// Phase 5 wires this to the URL-driven row detail modal.
|
const handleCardClick = useCallback(
|
||||||
// Until then, noop — the click still registers and the focus ring
|
(rowId: string) => openRow(rowId),
|
||||||
// appears, but nothing opens.
|
[openRow],
|
||||||
void rowId;
|
);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRowReorder = useCallback(
|
const handleRowReorder = useCallback(
|
||||||
(rowId: string, targetRowId: string, dropPosition: "above" | "below") => {
|
(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
|
// Inline: banner + toolbar live inside the StickyBand (passed via
|
||||||
// stickyBandPrelude). The page is the vertical scroll container.
|
// stickyBandPrelude). The page is the vertical scroll container.
|
||||||
return (
|
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
|
<ViewRenderer
|
||||||
base={base}
|
base={base}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -406,8 +370,61 @@ export function BaseView({ pageId, embedded }: BaseViewProps) {
|
|||||||
onCardClick={handleCardClick}
|
onCardClick={handleCardClick}
|
||||||
persistViewConfig={persistViewConfig}
|
persistViewConfig={persistViewConfig}
|
||||||
scrollportRef={scrollportRef}
|
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>
|
||||||
</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