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