Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

4.3 kB Ruw bestand
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);
}