diff --git a/app/(app)/[appName]/[objectName]/[id].tsx b/app/(app)/[appName]/[objectName]/[id].tsx index 6870db2..3da13d8 100644 --- a/app/(app)/[appName]/[objectName]/[id].tsx +++ b/app/(app)/[appName]/[objectName]/[id].tsx @@ -128,6 +128,21 @@ export default function ObjectDetailScreen() { } }, [confirm, client, objectName, id, router, t, toastError]); + /* ---- Inline field edit (select/status badges on the detail) ---- */ + const handleFieldEdit = useCallback( + async (field: string, value: unknown) => { + if (!objectName || !id) return; + try { + await client.data.update(objectName, id, { [field]: value }); + await fetchRecord(); + toastSuccess(t("records.updated")); + } catch { + toastError(t("records.updateFailed")); + } + }, + [client, objectName, id, fetchRecord, toastSuccess, toastError, t], + ); + /* ---- Object actions (record_header inline, record_more overflow) ---- */ const allActions = useMemo( () => ((meta?.actions as ActionMeta[] | undefined) ?? []).filter(isActionVisible), @@ -213,6 +228,7 @@ export default function ObjectDetailScreen() { actions={headerActions} moreActions={moreActions} onAction={runAction} + onFieldEdit={handleFieldEdit} busyActionName={busyName} relatedLists={relatedLists} onRelatedRecordPress={handleRelatedRecordPress} diff --git a/components/renderers/DetailViewRenderer.tsx b/components/renderers/DetailViewRenderer.tsx index 1acb335..4c558ee 100644 --- a/components/renderers/DetailViewRenderer.tsx +++ b/components/renderers/DetailViewRenderer.tsx @@ -10,6 +10,8 @@ import { ChevronRight, AlertCircle, MoreHorizontal, + Check, + Pencil, } from "lucide-react-native"; import { cn } from "~/lib/utils"; import { getIcon } from "~/lib/getIcon"; @@ -18,7 +20,7 @@ import { Button } from "~/components/ui/Button"; import { BottomSheet } from "~/components/ui/BottomSheet"; import { Skeleton } from "~/components/ui/Skeleton"; import { isFieldVisible, isSectionVisible } from "~/lib/conditional-fields"; -import { FieldRenderer } from "./fields/FieldRenderer"; +import { FieldRenderer, isSelectType, OptionBadge } from "./fields/FieldRenderer"; import type { FieldDefinition, FormViewMeta, FormSection, ActionMeta } from "./types"; /** Skeleton placeholder that mirrors the detail's stacked section/field layout. */ @@ -93,6 +95,12 @@ export interface DetailViewRendererProps { allowDelete?: boolean; /** Extra content rendered at the end of the scroll body (e.g. lifecycle diagram). */ footer?: React.ReactNode; + /** + * Inline-edit a select/status field straight from the detail. When provided, + * those fields render a tappable badge that opens an option picker; the + * handler persists the change. Omit to keep the detail strictly read-only. + */ + onFieldEdit?: (field: string, value: unknown) => void | Promise; } export interface RelatedListConfig { @@ -470,6 +478,89 @@ function RecordNavigator({ ); } +/* ------------------------------------------------------------------ */ +/* Inline select edit */ +/* ------------------------------------------------------------------ */ + +/** + * A select/status field shown as its coloured badge with a pencil affordance. + * Tapping opens a picker; choosing a new option calls `onEdit` (which persists) + * and reflects a saving spinner until it resolves. + */ +function EditableSelectField({ + label, + field, + value, + onEdit, +}: { + label: string; + field: FieldDefinition; + value: unknown; + onEdit: (value: unknown) => void | Promise; +}) { + const { t } = useTranslation(); + const { colorScheme } = useColorScheme(); + const accent = colorScheme === "dark" ? "#60a5fa" : "#1e40af"; + const [open, setOpen] = useState(false); + const [saving, setSaving] = useState(false); + const options = field.options ?? []; + + return ( + + {label} + setOpen(true)} + accessibilityRole="button" + accessibilityLabel={t("records.editField", { field: label })} + > + {value != null && value !== "" ? ( + + ) : ( + + )} + {saving ? ( + + ) : ( + + )} + + + + {options.map((opt) => { + const selected = String(opt.value) === String(value); + return ( + { + if (selected) { + setOpen(false); + return; + } + setSaving(true); + try { + await onEdit(opt.value); + } finally { + setSaving(false); + setOpen(false); + } + }} + > + + {selected && } + + ); + })} + + + ); +} + /* ------------------------------------------------------------------ */ /* Main Component */ /* ------------------------------------------------------------------ */ @@ -497,6 +588,7 @@ export function DetailViewRenderer({ allowEdit = true, allowDelete = true, footer, + onFieldEdit, }: DetailViewRendererProps) { const { t } = useTranslation(); @@ -647,15 +739,37 @@ export function DetailViewRenderer({ fieldDef?.label ?? fieldName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); + const fieldShape = { + name: fieldName, + label, + type: fieldDef?.type ?? "text", + options: fieldDef?.options, + }; + + // Inline-editable select/status fields (when a handler is wired + // and the field carries options) become a tappable badge. + const editable = + !!onFieldEdit && + isSelectType(fieldShape.type) && + (fieldDef?.options?.length ?? 0) > 0 && + !meta?.readonly; + + if (editable) { + return ( + onFieldEdit!(fieldName, v)} + /> + ); + } + return ( diff --git a/locales/ar.json b/locales/ar.json index d0120d7..1b6832d 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -79,6 +79,9 @@ "deleteConfirm": "هل أنت متأكد من أنك تريد حذف هذا السجل؟", "deleteConfirmNamed": "هل أنت متأكد من أنك تريد حذف \"{{label}}\"؟", "deleteFailed": "فشل حذف السجل.", + "updated": "تم التحديث", + "updateFailed": "فشل تحديث السجل.", + "editField": "تعديل {{field}}", "recordSaved": "تم حفظ السجل بنجاح.", "recordDeleted": "تم حذف السجل بنجاح.", "previous": "السابق", diff --git a/locales/en.json b/locales/en.json index 154e5b8..10d2b0a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -75,6 +75,9 @@ "deleteConfirm": "Are you sure you want to delete this record?", "deleteConfirmNamed": "Are you sure you want to delete \"{{label}}\"?", "deleteFailed": "Failed to delete the record.", + "updated": "Updated", + "updateFailed": "Failed to update the record.", + "editField": "Edit {{field}}", "recordSaved": "Record saved successfully.", "recordDeleted": "Record deleted successfully.", "previous": "Previous", diff --git a/locales/zh.json b/locales/zh.json index bd4880e..a30875f 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -74,6 +74,9 @@ "deleteConfirm": "确定要删除此记录吗?", "deleteConfirmNamed": "确定要删除“{{label}}”吗?", "deleteFailed": "删除记录失败。", + "updated": "已更新", + "updateFailed": "更新记录失败。", + "editField": "编辑{{field}}", "recordSaved": "记录保存成功。", "recordDeleted": "记录删除成功。", "previous": "上一条",