Files
inkreach-teable-cli/dist/lib/field-display.js
T
yeuimu 035384131c feat(view-preset): shared view presets with fork-on-edit semantics
Lands the view-preset / server-persisted personal-layer feature that has
been running in production, plus the hotfix for "the same preset shows
different filter configs to different users" (物流问题-已出轨迹 on
tblgsJTXhqwiDpsjeL5 / viwyszJmLQNOLhA9sh5).

Root cause of the divergence:
- usePersonalViewSync implicitly flushed any overlay edit by a
  view|update user INTO the shared preset row (last-writer-wins). Overlays
  are stale snapshots seeded at apply time, so one user's seed silently
  erased colleagues' filter changes (2026-09-17 02:55 the preset lost the
  流转角色=售后 condition), and active overlays never converged because
  preset updates do not propagate.

Fix (semantics: presets are shared definitions, edits are personal):
- fork-on-edit: the first user edit strips the seed markers from the
  overlay (toolbar switches to the personal config); edits only ever
  write the user's personal row
- shared presets change ONLY through the new explicit, confirmed
  "Update to current config" row action (canManage users)
- un-edited preset overlays auto-follow redefinitions: applyPreset stamps
  a presetVersion (preset.lastModifiedTime); when the preset list
  refetches with a newer row the overlay re-seeds. Edited overlays carry
  no markers and are never clobbered. Legacy rows without a version
  re-seed exactly once
- exits strip both seed markers from the persisted personal row

Also lands the pending WIP shipped with the deployed build: select-choice
visibility constraint, personal-view server sync + proxy, grid async
records scope/request tracking, CLI resource display, preset i18n (10
locales), view_preset migrations, docs/skill updates, .zcode gitignore.

Verified end-to-end on the live instance (headless chromium, real
session): apply -> edit flips toolbar to Personal with ZERO shared-row
PUTs; explicit update PUTs the shared row; API-side redefinition
auto-reseeds an un-edited overlay on the next list refetch.
2026-09-17 04:07:30 +00:00

137 lines
5.5 KiB
JavaScript

"use strict";
/**
* Human-mode display helpers for field options — make select-choice
* constraints (choices, colors, per-choice visibilityFilter) readable
* without `--json` post-processing.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatFieldDetail = exports.choiceRows = exports.describeChoiceCondition = exports.summarizeFieldOptions = void 0;
const CONDITION_MAX_LEN = 90;
/** Summarize a field's options for the `field list --with-options` column. */
function summarizeFieldOptions(field) {
const options = field.options;
const choices = options?.choices;
if (Array.isArray(choices) && choices.length) {
const conditional = choices.filter((c) => c && typeof c === 'object' && c.visibilityFilter).length;
return conditional
? `${choices.length} choices · ${conditional} conditional`
: `${choices.length} choices`;
}
if (!options || typeof options !== 'object')
return '—';
const type = field.type;
if (type === 'rating') {
return [
`max=${options.max ?? 5}`,
typeof options.icon === 'string' ? `icon=${options.icon}` : '',
]
.filter(Boolean)
.join(' ');
}
const formatting = options.formatting;
if (type === 'number' && formatting) {
return `precision=${formatting.precision ?? 0}`;
}
if ((type === 'date' || type === 'createdTime' || type === 'lastModifiedTime') && formatting) {
return [formatting.date, formatting.time && formatting.time !== 'None' ? formatting.time : '']
.filter(Boolean)
.join(' ');
}
if (typeof options.expression === 'string') {
const e = options.expression;
return `expr: ${e.length > 40 ? e.slice(0, 39) + '…' : e}`;
}
return '—';
}
exports.summarizeFieldOptions = summarizeFieldOptions;
function formatValue(value) {
if (value === null || value === undefined)
return '';
if (Array.isArray(value))
return value.map((v) => String(v)).join('|');
if (typeof value === 'object')
return JSON.stringify(value);
return String(value);
}
/** One-line human summary of a choice's visibilityFilter; `(always)` when absent. */
function describeChoiceCondition(vf, fieldNames) {
if (!vf || typeof vf !== 'object')
return '(always)';
const { conjunction, filterSet } = vf;
if (!Array.isArray(filterSet) || !filterSet.length)
return '(always)';
const joiner = conjunction === 'or' ? ' OR ' : ' AND ';
const rules = filterSet.map(({ fieldId, operator, value }) => {
const label = (fieldId && fieldNames?.[fieldId]) || fieldId || '?';
const v = formatValue(value);
return v ? `${label} ${operator ?? '?'} ${v}` : `${label} ${operator ?? '?'}`;
});
let text = '';
let kept = 0;
for (const rule of rules) {
const candidate = text ? text + joiner + rule : rule;
if (candidate.length > CONDITION_MAX_LEN && kept > 0)
break;
text = candidate;
kept++;
}
const omitted = rules.length - kept;
return omitted > 0 ? `${text}… +${omitted} more` : text;
}
exports.describeChoiceCondition = describeChoiceCondition;
/** Choices of a select field as display rows; `[]` when the field has none. */
function choiceRows(field, fieldNames) {
const choices = field.options?.choices;
if (!Array.isArray(choices))
return [];
return choices
.filter((c) => !!c && typeof c === 'object')
.map((c) => ({
name: String(c.name ?? ''),
color: typeof c.color === 'string' ? c.color : '',
when: describeChoiceCondition(c.visibilityFilter, fieldNames),
}));
}
exports.choiceRows = choiceRows;
function kvLine(key, value, keyWidth) {
const val = typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value ?? '');
return `${key.padEnd(keyWidth)} ${val}`;
}
/** Full human-mode rendering for `field get`: kv lines, with select choices
* expanded into a per-choice condition listing instead of one JSON blob. */
function formatFieldDetail(field, fieldNames) {
const lines = [];
const options = field.options;
const choices = options?.choices;
const restEntries = Object.entries({ ...field });
const keyWidth = Math.max(...restEntries.map(([k]) => k.length), 'defaultValue'.length);
for (const [k, v] of restEntries) {
if (k === 'options')
continue;
lines.push(kvLine(k, v, keyWidth));
}
if (Array.isArray(choices) && choices.length) {
const conditional = choices.filter((c) => c && typeof c === 'object' && c.visibilityFilter).length;
lines.push('');
lines.push(`choices (${choices.length} · ${conditional} conditional):`);
for (const row of choiceRows(field, fieldNames)) {
const color = row.color ? ` [${row.color}]` : '';
lines.push(` ${row.name}${color}`);
lines.push(` when: ${row.when}`);
}
const otherOpts = Object.fromEntries(Object.entries(options ?? {}).filter(([k]) => k !== 'choices'));
if (Object.keys(otherOpts).length) {
lines.push('');
lines.push(kvLine('options (other)', otherOpts, keyWidth));
}
}
else if (options && Object.keys(options).length) {
lines.push(kvLine('options', options, keyWidth));
}
else {
lines.push(kvLine('options', undefined, keyWidth));
}
return lines.join('\n');
}
exports.formatFieldDetail = formatFieldDetail;