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.
137 lines
5.5 KiB
JavaScript
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;
|