feat(bases): row detail modal shell with editable title

This commit is contained in:
Philipinho
2026-05-24 16:04:29 +01:00
parent e071de9248
commit 9237e94769
3 changed files with 183 additions and 45 deletions
@@ -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();
}
}}
/>
);
}