Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { type ReactNode } from 'react';
import { editableValue, isComputed, type ColumnMeta, type Row } from '@wellformed/shared';
import { useRows } from '../api/hooks';
import { useI18n } from '../i18n';
import { useWorkspace } from '../state/workspace';
import { useCurrency } from '../state/session';
import { typeMark } from './Icons';
/**
* The address-and-value strip above the grid. It reads; it does not write.
*
* It used to offer a text field for every column, which is a promise this
* product cannot keep: a select, a date, a checkbox and a link each have a
* shape, and a bare text box can only send something the server then has to
* refuse — after a round trip, with the typing thrown away. Matching the cell
* editor's controls here would mean building that editor a second time, so
* writing stays in the cell, where every type already has the right control.
*
* What is left is what only this strip can say: which cell you are in, the
* whole of a value too long for its column, and — for a computed column — the
* expression the database is actually evaluating.
*/
export function FormulaBar(): ReactNode {
const ws = useWorkspace();
const currency = useCurrency();
const { t } = useI18n();
const { rows } = useRows({
table: ws.tableName,
viewId: ws.viewId,
filters: ws.adHocFilters,
sort: ws.sortOverride,
hidden: ws.hiddenOverride,
search: ws.search,
enabled: Boolean(ws.tableName),
});
const selection = ws.selection;
const column = ws.table?.columns.find((c) => c.name === selection?.col);
const row = rows.find((r) => r.id === selection?.rowId);
const computed = column ? isComputed(column.type) : false;
// A link's stored value is a row id. Showing it here would contradict the
// grid, which names the row, so this strip names it too.
const isLink = column?.type === 'link';
const stored = column && row ? editableValue(row.values[column.name] ?? null) : '';
const shown = computed && column ? `= ${column.expr ?? ''}` : isLink ? linkLabel(row, column) : stored;
const rowLabel = describeRow(row, ws.table?.display);
const address = selection
? `${ws.tableName}!${selection.col} · ${rowLabel}`
: t('grid.noSelection');
// With nothing selected the strip has nothing to address, but it keeps its
// row: collapsing it moved the grid up under the pointer, so the click that
// selected the first cell dropped the header onto the row you were aiming
// at — and a double-click to edit sorted the table instead.
return (
<div className={selection ? 'fbar' : 'fbar quiet'}>
<span className="addr" title={address}>
{address}
</span>
{/* Excel puts `fx` here because an Excel cell can hold a formula. A
Wellformed cell never can: the expression belongs to the column, and
this strip is showing a value. So the marker says which kind of
column you are in — the same vocabulary as the grid header — and
reads `fx` only where the field really is holding an expression. */}
<span className="mark" title={column ? t('grid.typeColumn', column.type) : undefined}>
{column ? typeMark(column.type, currency) : ''}
</span>
{/* Selectable, so a value can still be copied out of here, and scrollable
rather than clipped, so a long one can be read in full. */}
<span className="value" aria-label={t('grid.cellValue')} title={shown}>
{shown}
</span>
{computed && column && (
<span className="hint">{t('grid.evaluatedInSql', column.type)}</span>
)}
{isLink && column && (
<span className="hint">
{t('grid.foreignKey', column.target ?? t('grid.anotherTable'))}
</span>
)}
</div>
);
}
/** The name of the row a link points at, falling back to the id it stores. */
function linkLabel(row: Row | undefined, column: ColumnMeta): string {
if (!row) return '';
const value = row.values[column.name];
if (value === null || value === undefined || value === '') return '';
return row.labels[column.name] ?? String(value);
}
function describeRow(row: Row | undefined, displayColumn: string | undefined): string {
if (!row) return '';
if (!displayColumn) return row.id;
const value = row.values[displayColumn];
return value === null || value === undefined || value === '' ? row.id : String(value);
}