mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
fix: align input shortcuts (#2467)
* align input shortcuts * minor fix * minor fix
This commit is contained in:
@@ -18,6 +18,7 @@ export type FieldProps = {
|
||||
rowId: string;
|
||||
readOnly: boolean;
|
||||
onChange: (value: unknown) => void;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
};
|
||||
|
||||
type FieldShellProps = {
|
||||
@@ -99,9 +100,10 @@ type DetailFieldProps = {
|
||||
row: IBaseRow;
|
||||
readOnly: boolean;
|
||||
onUpdate: (propertyId: string, value: unknown) => void;
|
||||
onEditingChange: (editing: boolean) => void;
|
||||
};
|
||||
|
||||
export function DetailField({ property, row, readOnly, onUpdate }: DetailFieldProps) {
|
||||
export function DetailField({ property, row, readOnly, onUpdate, onEditingChange }: DetailFieldProps) {
|
||||
const descriptor = getDescriptor(property.type);
|
||||
const value = descriptor?.systemAccessor
|
||||
? descriptor.systemAccessor(row)
|
||||
@@ -112,6 +114,7 @@ export function DetailField({ property, row, readOnly, onUpdate }: DetailFieldPr
|
||||
rowId: row.id,
|
||||
readOnly,
|
||||
onChange: (next: unknown) => onUpdate(property.id, next),
|
||||
onEditingChange
|
||||
};
|
||||
|
||||
switch (property.type) {
|
||||
|
||||
@@ -9,7 +9,13 @@ const normalize = (s: string) => {
|
||||
return trimmed.length ? trimmed : null;
|
||||
};
|
||||
|
||||
export function FieldLongText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
export function FieldLongText({
|
||||
property,
|
||||
value,
|
||||
readOnly,
|
||||
onChange,
|
||||
onEditingChange,
|
||||
}: FieldProps) {
|
||||
const text = toText(value);
|
||||
const [draft, setDraft] = useState(text);
|
||||
const [focused, setFocused] = useState(false);
|
||||
@@ -23,6 +29,7 @@ export function FieldLongText({ property, value, readOnly, onChange }: FieldProp
|
||||
|
||||
const commit = () => {
|
||||
setFocused(false);
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setDraft(text);
|
||||
@@ -50,7 +57,10 @@ export function FieldLongText({ property, value, readOnly, onChange }: FieldProp
|
||||
className={classes.fieldTextarea}
|
||||
classNames={{ input: classes.fieldTextareaInput }}
|
||||
value={draft}
|
||||
onFocus={() => setFocused(true)}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => setDraft(e.currentTarget.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
|
||||
@@ -11,7 +11,13 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
|
||||
const toDraft = (value: unknown) =>
|
||||
typeof value === "number" ? String(value) : "";
|
||||
|
||||
export function FieldNumber({ property, value, readOnly, onChange }: FieldProps) {
|
||||
export function FieldNumber({
|
||||
property,
|
||||
value,
|
||||
readOnly,
|
||||
onChange,
|
||||
onEditingChange,
|
||||
}: FieldProps) {
|
||||
const typeOptions = property.typeOptions as NumberTypeOptions | undefined;
|
||||
const numValue = typeof value === "number" ? value : null;
|
||||
const [draft, setDraft] = useState(toDraft(value));
|
||||
@@ -36,6 +42,7 @@ export function FieldNumber({ property, value, readOnly, onChange }: FieldProps)
|
||||
|
||||
const commit = () => {
|
||||
setFocused(false);
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setDraft(toDraft(value));
|
||||
@@ -54,6 +61,7 @@ export function FieldNumber({ property, value, readOnly, onChange }: FieldProps)
|
||||
onFocus={() => {
|
||||
setDraft(toDraft(value));
|
||||
setFocused(true);
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
|
||||
@@ -5,7 +5,13 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
|
||||
|
||||
const toText = (value: unknown) => (typeof value === "string" ? value : "");
|
||||
|
||||
export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
export function FieldText({
|
||||
property,
|
||||
value,
|
||||
readOnly,
|
||||
onChange,
|
||||
onEditingChange,
|
||||
}: FieldProps) {
|
||||
const text = toText(value);
|
||||
const [draft, setDraft] = useState(text);
|
||||
const [focused, setFocused] = useState(false);
|
||||
@@ -20,6 +26,7 @@ export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
|
||||
const commit = () => {
|
||||
setFocused(false);
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setDraft(text);
|
||||
@@ -54,7 +61,10 @@ export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
className={classes.fieldInput}
|
||||
value={draft}
|
||||
maxLength={1000}
|
||||
onFocus={() => setFocused(true)}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => setDraft(e.currentTarget.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ type PropertyRowProps = {
|
||||
onMenuOpenChange: (opened: boolean) => void;
|
||||
onMenuDirtyChange: (dirty: boolean) => void;
|
||||
onUpdate: (propertyId: string, value: unknown) => void;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
autoFocusValue?: boolean;
|
||||
onAutoFocused?: () => void;
|
||||
};
|
||||
@@ -29,6 +30,7 @@ export function PropertyRow({
|
||||
onMenuOpenChange,
|
||||
onMenuDirtyChange,
|
||||
onUpdate,
|
||||
onEditingChange,
|
||||
autoFocusValue,
|
||||
onAutoFocused,
|
||||
}: PropertyRowProps) {
|
||||
@@ -112,6 +114,7 @@ export function PropertyRow({
|
||||
row={row}
|
||||
readOnly={!canEdit}
|
||||
onUpdate={onUpdate}
|
||||
onEditingChange={onEditingChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -75,6 +75,7 @@ export function RowDetailModal({
|
||||
|
||||
const isSaving = updateRowMutation.isPending;
|
||||
const opened = !!openRowId;
|
||||
const [editingField, setEditingField] = useState(false);
|
||||
|
||||
// One field menu open at a time, mirroring the grid header's semantics.
|
||||
// The shared closeRequest atom asks an open dirty PropertyMenuContent to
|
||||
@@ -90,6 +91,7 @@ export function RowDetailModal({
|
||||
useEffect(() => {
|
||||
setOpenMenuId(null);
|
||||
menuDirtyRef.current = false;
|
||||
setEditingField(false);
|
||||
}, [openRowId]);
|
||||
|
||||
const handleMenuDirtyChange = useCallback((dirty: boolean) => {
|
||||
@@ -293,7 +295,7 @@ export function RowDetailModal({
|
||||
row={row}
|
||||
primaryProperty={primaryProperty}
|
||||
canEdit={canEdit}
|
||||
onClose={onClose}
|
||||
onEditingChange={setEditingField}
|
||||
onCommit={(value) => {
|
||||
if (!primaryProperty) return;
|
||||
updateRowMutation.mutate({
|
||||
@@ -317,6 +319,7 @@ export function RowDetailModal({
|
||||
autoFocusValue={property.id === newPropertyId}
|
||||
onAutoFocused={clearNewProperty}
|
||||
menuOpened={openMenuId === property.id}
|
||||
onEditingChange={setEditingField}
|
||||
onMenuOpenChange={(nextOpened) =>
|
||||
handleMenuOpenChange(property.id, nextOpened)
|
||||
}
|
||||
@@ -367,16 +370,38 @@ export function RowDetailModal({
|
||||
) : null}
|
||||
</div>
|
||||
<div className={classes.kbdHint}>
|
||||
{rowIndex >= 0 && rows.length > 1 && (
|
||||
{editingField ? (
|
||||
<>
|
||||
<kbd className={classes.kbd}>↑</kbd>
|
||||
<kbd className={classes.kbd}>↓</kbd>
|
||||
<span>{t("to navigate")}</span>
|
||||
<span className={classes.kbdGroup}>
|
||||
<kbd className={classes.kbd}>Ctrl/Cmd</kbd>
|
||||
<span className={classes.kbdPlus} >+</span>
|
||||
<kbd className={classes.kbd}>Enter</kbd>
|
||||
<span>{t("to save")}</span>
|
||||
</span>
|
||||
|
||||
<span className={classes.kbdSeparator} />
|
||||
|
||||
<span className={classes.kbdGroup}>
|
||||
<kbd className={classes.kbd}>Esc</kbd>
|
||||
<span>{t("to reset")}</span>
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{rowIndex >= 0 && rows.length > 1 && (
|
||||
<>
|
||||
<kbd className={classes.kbd}>↑</kbd>
|
||||
<kbd className={classes.kbd}>↓</kbd>
|
||||
<span>{t("to navigate")}</span>
|
||||
<span className={classes.kbdSeparator} />
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<kbd className={classes.kbd}>Esc</kbd>
|
||||
<span>{t("to close")}</span>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
<kbd className={classes.kbd}>Esc</kbd>
|
||||
<span>{t("to close")}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IBaseProperty, IBaseRow } from "@/ee/base/types/base.types";
|
||||
import { timeAgo } from "@/lib/time.ts";
|
||||
@@ -9,7 +9,7 @@ type RowDetailTitleProps = {
|
||||
primaryProperty: IBaseProperty | undefined;
|
||||
canEdit: boolean;
|
||||
onCommit: (value: string) => void;
|
||||
onClose: () => void;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
};
|
||||
|
||||
export function RowDetailTitle({
|
||||
@@ -17,13 +17,24 @@ export function RowDetailTitle({
|
||||
primaryProperty,
|
||||
canEdit,
|
||||
onCommit,
|
||||
onClose,
|
||||
onEditingChange,
|
||||
}: RowDetailTitleProps) {
|
||||
const { t } = useTranslation();
|
||||
const initial = primaryProperty
|
||||
? (((row.cells ?? {})[primaryProperty.id] as string) ?? "")
|
||||
: "";
|
||||
const [value, setValue] = useState(initial);
|
||||
const cancelRef = useRef(false);
|
||||
|
||||
const commit = () => {
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setValue(initial);
|
||||
return;
|
||||
}
|
||||
if (value !== initial) onCommit(value);
|
||||
};
|
||||
|
||||
// Re-sync when the row changes underneath us (navigation or remote edit).
|
||||
useEffect(() => {
|
||||
@@ -43,18 +54,18 @@ export function RowDetailTitle({
|
||||
aria-label={primaryProperty?.name ?? t("Untitled")}
|
||||
value={value}
|
||||
maxLength={1000}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
onBlur={() => {
|
||||
if (value !== initial) onCommit(value);
|
||||
onFocus={() => {
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === "Escape") {
|
||||
cancelRef.current = true;
|
||||
e.currentTarget.blur();
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
onClose();
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -416,9 +416,25 @@
|
||||
}
|
||||
|
||||
.kbdHint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kbdGroup {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.kbdPlus {
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.kbdSeparator {
|
||||
|
||||
Reference in New Issue
Block a user