@@ -10711,6 +10711,195 @@ const viewPageMountRemoved: MetadataConversion = {
1071110711 },
1071210712};
1071310713
10714+ /**
10715+ * The per-kind blocks the legacy list-overlay `options` bag could carry, as
10716+ * the bag declared them when it closed (#25923): every list-view `type` that
10717+ * also names a block on the list-view shape (`grid` names none). Spelled out
10718+ * rather than derived, because this conversion replays a shape that is gone:
10719+ * a kind added later never had a bag spelling to fold.
10720+ */
10721+ const LIST_VIEW_OPTIONS_BAG_KINDS = ['kanban', 'calendar', 'gantt', 'timeline', 'map', 'gallery', 'chart', 'tree'] as const;
10722+
10723+ /**
10724+ * The flattened list overlay's legacy `options` bag folds onto the top-level
10725+ * per-kind blocks (protocol 18, #25923 — Prime Directive #12, one key per
10726+ * concept; this supersedes ruling direction A on objectui#10380).
10727+ *
10728+ * Until this release the list overlay door accepted `options.KIND` beside the
10729+ * top-level `KIND` block, judged key by key by the kind's own schema with
10730+ * every key optional, and stored it. The bag was declared an UNDERLAY: where
10731+ * both set a key, the top-level block wins. objectui's `ListView` read it that
10732+ * way for kanban, calendar, gallery, timeline, gantt and map
10733+ * (`{ ...options.KIND, ...KIND }`), so this fold is that same merge, written
10734+ * once into the row:
10735+ *
10736+ * - `options.KIND` with no top-level `KIND` ⇒ it becomes `KIND`.
10737+ * - Both present ⇒ `KIND = { ...options.KIND, ...KIND }`, the top-level value
10738+ * kept where both set a key.
10739+ * - `chart` is the one kind the renderer did NOT merge: it read
10740+ * `chart || options.chart`, the block replacing the bag WHOLE. So a bag
10741+ * beside a top-level `chart` block is dropped (it never drew), and a bag
10742+ * with no block becomes the block. Merging it per key would draw keys the
10743+ * author never saw.
10744+ * - A bag left empty is removed.
10745+ *
10746+ * **Retired from the load path** — the door refuses `options` by name with
10747+ * the prescription (`LIST_OVERLAY_OPTIONS_REFUSED` in `ui/view.zod.ts`), so a
10748+ * live author is taught the top-level spelling. The entry exists because a
10749+ * STORED list overlay can carry the bag, and every read of a stored `view` row
10750+ * replays the chain through `applyConversionsToStoredItem` as
10751+ * `{ views: [row] }`; `os migrate meta` lists the edit for sources.
10752+ *
10753+ * **Left as stored, with a TODO** (no lossless rewrite): a bag that is not an
10754+ * object; an `options.grid` (a grid has no block, and the door refused it
10755+ * already); a bag key that is not a kind; an `options.KIND` that is not an
10756+ * object, or whose top-level `KIND` is not one; and any bag on a FORM overlay
10757+ * (`viewKind: 'form'`), whose blocks have no home on a form. The row keeps its
10758+ * bag and is badged invalid until its owner decides. The fold also cannot
10759+ * supply a key the block REQUIRES: the bag was judged with every key
10760+ * optional, so a bag block can fold into a block still missing one (a
10761+ * `gantt` with no `titleField`, say). That row is served badged invalid,
10762+ * and the D3 entry `ui-list-view-options-bag-folded` names the remedy.
10763+ *
10764+ * ⚠️ Scoped to the FLATTENED spelling, as {@link viewOverlayOwnerHiddenRemoved}
10765+ * is: a body with no `config` and no container slot. A view item record
10766+ * (`config` present) has refused the bag since #20051 with its own
10767+ * prescription, and the two authoring shapes never declared it.
10768+ */
10769+ const listViewOptionsBagFolded: MetadataConversion = {
10770+ id: 'list-view-options-bag-folded',
10771+ toMajor: 18,
10772+ retiredFromLoadPath: true,
10773+ retiredAfter: '18.2.0',
10774+ surface: 'view.options.<kind> — on a flattened list view overlay ({ name, object, viewKind, …, no config })',
10775+ summary:
10776+ "flattened list overlay 'options.KIND' folded onto the top-level 'KIND' block (the top-level "
10777+ + "value wins per key; for 'chart' the block wins whole), and the bag removed: the per-kind "
10778+ + 'blocks are top-level keys of the view, their one spelling',
10779+ apply(stack, emit, context) {
10780+ const fold = (view: Dict, path: string): Dict => {
10781+ if (!('options' in view)) return view;
10782+ if (view.config !== undefined) return view;
10783+ if (view.list !== undefined || view.form !== undefined) return view;
10784+ if (view.listViews !== undefined || view.formViews !== undefined) return view;
10785+ const bag = view.options;
10786+ if (view.viewKind === 'form' || !isDict(bag)) {
10787+ context?.reportTodo?.({
10788+ path: `${path}.options`,
10789+ from: JSON.stringify(bag),
10790+ reason: view.viewKind === 'form'
10791+ ? 'a form view overlay carries an `options` bag; its per-kind blocks belong to a list view and have no '
10792+ + 'home on a form. Remove `options`, or save the body as a list view.'
10793+ : 'the `options` bag is not an object, so there is no per-kind block to fold. Remove `options`.',
10794+ });
10795+ return view;
10796+ }
10797+ const next: Dict = { ...view };
10798+ const rest: Dict = { ...bag };
10799+ let changed = false;
10800+ for (const kind of LIST_VIEW_OPTIONS_BAG_KINDS) {
10801+ if (!(kind in rest)) continue;
10802+ const under = rest[kind];
10803+ const top = next[kind];
10804+ if (!isDict(under) || (top !== undefined && !isDict(top))) continue;
10805+ delete rest[kind];
10806+ changed = true;
10807+ if (top === undefined) {
10808+ next[kind] = under;
10809+ emit({ from: `options.${kind}`, to: kind, path: `${path}.options.${kind}` });
10810+ } else if (kind === 'chart') {
10811+ emit({ from: 'options.chart', to: '(removed: the top-level chart block replaces it whole)', path: `${path}.options.chart` });
10812+ } else {
10813+ next[kind] = { ...under, ...top };
10814+ emit({ from: `options.${kind}`, to: `${kind} (top-level keys kept)`, path: `${path}.options.${kind}` });
10815+ }
10816+ }
10817+ if (Object.keys(rest).length === 0) {
10818+ delete next.options;
10819+ if (!changed) emit({ from: 'options', to: '(removed: empty)', path: `${path}.options` });
10820+ return next;
10821+ }
10822+ context?.reportTodo?.({
10823+ path: `${path}.options`,
10824+ from: JSON.stringify(rest),
10825+ reason: `the bag keeps ${Object.keys(rest).map((k) => `\`${k}\``).join(', ')}, which has no top-level block `
10826+ + 'to fold onto (a grid\'s settings are top-level keys of the view; a non-object block or one beside a '
10827+ + 'non-object top-level block cannot be merged). Move what you meant to the view\'s own keys, then remove `options`.',
10828+ });
10829+ if (!changed) return view;
10830+ next.options = rest;
10831+ return next;
10832+ };
10833+ return mapCollection(
10834+ mapCollection(stack, 'views', fold),
10835+ ASSEMBLED_VIEW_ITEMS_KEY,
10836+ fold,
10837+ );
10838+ },
10839+ fixture: {
10840+ before: {
10841+ // The assembled-manifest channel: an overlay a package export carried.
10842+ viewItems: [
10843+ { name: 'crm_deal.map', object: 'crm_deal', viewKind: 'list', type: 'map', options: { map: { latitudeField: 'lat', longitudeField: 'lng' } } },
10844+ ],
10845+ views: [
10846+ // Per key: the top-level `kanban` keeps `groupByField`, the bag adds
10847+ // `titleField`; the bag's `groupByField` is the one that never drew.
10848+ {
10849+ name: 'crm_deal.board',
10850+ object: 'crm_deal',
10851+ viewKind: 'list',
10852+ type: 'kanban',
10853+ kanban: { groupByField: 'stage' },
10854+ options: { kanban: { groupByField: 'owner', titleField: 'name' }, calendar: { startDateField: 'close_date' } },
10855+ },
10856+ // `chart` is replaced whole by its block, so the bag is dropped.
10857+ {
10858+ name: 'crm_deal.chart',
10859+ object: 'crm_deal',
10860+ viewKind: 'list',
10861+ type: 'chart',
10862+ chart: { dataset: 'deals', values: ['amount'] },
10863+ options: { chart: { dataset: 'old_deals', values: ['amount'], dimensions: ['stage'] } },
10864+ },
10865+ // An empty bag is removed.
10866+ { name: 'crm_deal.empty', object: 'crm_deal', viewKind: 'list', options: {} },
10867+ // A record and a container are not flattened overlays: untouched.
10868+ { name: 'crm_deal.rec', object: 'crm_deal', viewKind: 'list', config: { columns: ['name'] } },
10869+ { object: 'crm_deal', list: { type: 'grid', columns: ['name'] } },
10870+ ],
10871+ },
10872+ after: {
10873+ viewItems: [
10874+ { name: 'crm_deal.map', object: 'crm_deal', viewKind: 'list', type: 'map', map: { latitudeField: 'lat', longitudeField: 'lng' } },
10875+ ],
10876+ views: [
10877+ {
10878+ name: 'crm_deal.board',
10879+ object: 'crm_deal',
10880+ viewKind: 'list',
10881+ type: 'kanban',
10882+ kanban: { groupByField: 'stage', titleField: 'name' },
10883+ calendar: { startDateField: 'close_date' },
10884+ },
10885+ {
10886+ name: 'crm_deal.chart',
10887+ object: 'crm_deal',
10888+ viewKind: 'list',
10889+ type: 'chart',
10890+ chart: { dataset: 'deals', values: ['amount'] },
10891+ },
10892+ { name: 'crm_deal.empty', object: 'crm_deal', viewKind: 'list' },
10893+ { name: 'crm_deal.rec', object: 'crm_deal', viewKind: 'list', config: { columns: ['name'] } },
10894+ { object: 'crm_deal', list: { type: 'grid', columns: ['name'] } },
10895+ ],
10896+ },
10897+ // One notice per folded block (map, kanban, calendar, chart) plus the
10898+ // empty bag's removal.
10899+ expectedNotices: 5,
10900+ },
10901+ };
10902+
1071410903/**
1071510904 * [#17053] The bare string `sort` clause on a list-view payload becomes the
1071610905 * `{ field, order }[]` array — one sort orthography platform-wide.
@@ -12329,195 +12518,6 @@ const viewOverlayOwnerHiddenRemoved: MetadataConversion = {
1232912518 },
1233012519};
1233112520
12332- /**
12333- * The per-kind blocks the legacy list-overlay `options` bag could carry, as
12334- * the bag declared them when it closed (#25923): every list-view `type` that
12335- * also names a block on the list-view shape (`grid` names none). Spelled out
12336- * rather than derived, because this conversion replays a shape that is gone:
12337- * a kind added later never had a bag spelling to fold.
12338- */
12339- const LIST_VIEW_OPTIONS_BAG_KINDS = ['kanban', 'calendar', 'gantt', 'timeline', 'map', 'gallery', 'chart', 'tree'] as const;
12340-
12341- /**
12342- * The flattened list overlay's legacy `options` bag folds onto the top-level
12343- * per-kind blocks (protocol 18, #25923 — Prime Directive #12, one key per
12344- * concept; this supersedes ruling direction A on objectui#10380).
12345- *
12346- * Until this release the list overlay door accepted `options.KIND` beside the
12347- * top-level `KIND` block, judged key by key by the kind's own schema with
12348- * every key optional, and stored it. The bag was declared an UNDERLAY: where
12349- * both set a key, the top-level block wins. objectui's `ListView` read it that
12350- * way for kanban, calendar, gallery, timeline, gantt and map
12351- * (`{ ...options.KIND, ...KIND }`), so this fold is that same merge, written
12352- * once into the row:
12353- *
12354- * - `options.KIND` with no top-level `KIND` ⇒ it becomes `KIND`.
12355- * - Both present ⇒ `KIND = { ...options.KIND, ...KIND }`, the top-level value
12356- * kept where both set a key.
12357- * - `chart` is the one kind the renderer did NOT merge: it read
12358- * `chart || options.chart`, the block replacing the bag WHOLE. So a bag
12359- * beside a top-level `chart` block is dropped (it never drew), and a bag
12360- * with no block becomes the block. Merging it per key would draw keys the
12361- * author never saw.
12362- * - A bag left empty is removed.
12363- *
12364- * **Retired from the load path** — the door refuses `options` by name with
12365- * the prescription (`LIST_OVERLAY_OPTIONS_REFUSED` in `ui/view.zod.ts`), so a
12366- * live author is taught the top-level spelling. The entry exists because a
12367- * STORED list overlay can carry the bag, and every read of a stored `view` row
12368- * replays the chain through `applyConversionsToStoredItem` as
12369- * `{ views: [row] }`; `os migrate meta` lists the edit for sources.
12370- *
12371- * **Left as stored, with a TODO** (no lossless rewrite): a bag that is not an
12372- * object; an `options.grid` (a grid has no block, and the door refused it
12373- * already); a bag key that is not a kind; an `options.KIND` that is not an
12374- * object, or whose top-level `KIND` is not one; and any bag on a FORM overlay
12375- * (`viewKind: 'form'`), whose blocks have no home on a form. The row keeps its
12376- * bag and is badged invalid until its owner decides. The fold also cannot
12377- * supply a key the block REQUIRES: the bag was judged with every key
12378- * optional, so a bag block can fold into a block still missing one (a
12379- * `gantt` with no `titleField`, say). That row is served badged invalid,
12380- * and the D3 entry `ui-list-view-options-bag-folded` names the remedy.
12381- *
12382- * ⚠️ Scoped to the FLATTENED spelling, as {@link viewOverlayOwnerHiddenRemoved}
12383- * is: a body with no `config` and no container slot. A view item record
12384- * (`config` present) has refused the bag since #20051 with its own
12385- * prescription, and the two authoring shapes never declared it.
12386- */
12387- const listViewOptionsBagFolded: MetadataConversion = {
12388- id: 'list-view-options-bag-folded',
12389- toMajor: 18,
12390- retiredFromLoadPath: true,
12391- retiredAfter: '18.2.0',
12392- surface: 'view.options.<kind> — on a flattened list view overlay ({ name, object, viewKind, …, no config })',
12393- summary:
12394- "flattened list overlay 'options.KIND' folded onto the top-level 'KIND' block (the top-level "
12395- + "value wins per key; for 'chart' the block wins whole), and the bag removed: the per-kind "
12396- + 'blocks are top-level keys of the view, their one spelling',
12397- apply(stack, emit, context) {
12398- const fold = (view: Dict, path: string): Dict => {
12399- if (!('options' in view)) return view;
12400- if (view.config !== undefined) return view;
12401- if (view.list !== undefined || view.form !== undefined) return view;
12402- if (view.listViews !== undefined || view.formViews !== undefined) return view;
12403- const bag = view.options;
12404- if (view.viewKind === 'form' || !isDict(bag)) {
12405- context?.reportTodo?.({
12406- path: `${path}.options`,
12407- from: JSON.stringify(bag),
12408- reason: view.viewKind === 'form'
12409- ? 'a form view overlay carries an `options` bag; its per-kind blocks belong to a list view and have no '
12410- + 'home on a form. Remove `options`, or save the body as a list view.'
12411- : 'the `options` bag is not an object, so there is no per-kind block to fold. Remove `options`.',
12412- });
12413- return view;
12414- }
12415- const next: Dict = { ...view };
12416- const rest: Dict = { ...bag };
12417- let changed = false;
12418- for (const kind of LIST_VIEW_OPTIONS_BAG_KINDS) {
12419- if (!(kind in rest)) continue;
12420- const under = rest[kind];
12421- const top = next[kind];
12422- if (!isDict(under) || (top !== undefined && !isDict(top))) continue;
12423- delete rest[kind];
12424- changed = true;
12425- if (top === undefined) {
12426- next[kind] = under;
12427- emit({ from: `options.${kind}`, to: kind, path: `${path}.options.${kind}` });
12428- } else if (kind === 'chart') {
12429- emit({ from: 'options.chart', to: '(removed: the top-level chart block replaces it whole)', path: `${path}.options.chart` });
12430- } else {
12431- next[kind] = { ...under, ...top };
12432- emit({ from: `options.${kind}`, to: `${kind} (top-level keys kept)`, path: `${path}.options.${kind}` });
12433- }
12434- }
12435- if (Object.keys(rest).length === 0) {
12436- delete next.options;
12437- if (!changed) emit({ from: 'options', to: '(removed: empty)', path: `${path}.options` });
12438- return next;
12439- }
12440- context?.reportTodo?.({
12441- path: `${path}.options`,
12442- from: JSON.stringify(rest),
12443- reason: `the bag keeps ${Object.keys(rest).map((k) => `\`${k}\``).join(', ')}, which has no top-level block `
12444- + 'to fold onto (a grid\'s settings are top-level keys of the view; a non-object block or one beside a '
12445- + 'non-object top-level block cannot be merged). Move what you meant to the view\'s own keys, then remove `options`.',
12446- });
12447- if (!changed) return view;
12448- next.options = rest;
12449- return next;
12450- };
12451- return mapCollection(
12452- mapCollection(stack, 'views', fold),
12453- ASSEMBLED_VIEW_ITEMS_KEY,
12454- fold,
12455- );
12456- },
12457- fixture: {
12458- before: {
12459- // The assembled-manifest channel: an overlay a package export carried.
12460- viewItems: [
12461- { name: 'crm_deal.map', object: 'crm_deal', viewKind: 'list', type: 'map', options: { map: { latitudeField: 'lat', longitudeField: 'lng' } } },
12462- ],
12463- views: [
12464- // Per key: the top-level `kanban` keeps `groupByField`, the bag adds
12465- // `titleField`; the bag's `groupByField` is the one that never drew.
12466- {
12467- name: 'crm_deal.board',
12468- object: 'crm_deal',
12469- viewKind: 'list',
12470- type: 'kanban',
12471- kanban: { groupByField: 'stage' },
12472- options: { kanban: { groupByField: 'owner', titleField: 'name' }, calendar: { startDateField: 'close_date' } },
12473- },
12474- // `chart` is replaced whole by its block, so the bag is dropped.
12475- {
12476- name: 'crm_deal.chart',
12477- object: 'crm_deal',
12478- viewKind: 'list',
12479- type: 'chart',
12480- chart: { dataset: 'deals', values: ['amount'] },
12481- options: { chart: { dataset: 'old_deals', values: ['amount'], dimensions: ['stage'] } },
12482- },
12483- // An empty bag is removed.
12484- { name: 'crm_deal.empty', object: 'crm_deal', viewKind: 'list', options: {} },
12485- // A record and a container are not flattened overlays: untouched.
12486- { name: 'crm_deal.rec', object: 'crm_deal', viewKind: 'list', config: { columns: ['name'] } },
12487- { object: 'crm_deal', list: { type: 'grid', columns: ['name'] } },
12488- ],
12489- },
12490- after: {
12491- viewItems: [
12492- { name: 'crm_deal.map', object: 'crm_deal', viewKind: 'list', type: 'map', map: { latitudeField: 'lat', longitudeField: 'lng' } },
12493- ],
12494- views: [
12495- {
12496- name: 'crm_deal.board',
12497- object: 'crm_deal',
12498- viewKind: 'list',
12499- type: 'kanban',
12500- kanban: { groupByField: 'stage', titleField: 'name' },
12501- calendar: { startDateField: 'close_date' },
12502- },
12503- {
12504- name: 'crm_deal.chart',
12505- object: 'crm_deal',
12506- viewKind: 'list',
12507- type: 'chart',
12508- chart: { dataset: 'deals', values: ['amount'] },
12509- },
12510- { name: 'crm_deal.empty', object: 'crm_deal', viewKind: 'list' },
12511- { name: 'crm_deal.rec', object: 'crm_deal', viewKind: 'list', config: { columns: ['name'] } },
12512- { object: 'crm_deal', list: { type: 'grid', columns: ['name'] } },
12513- ],
12514- },
12515- // One notice per folded block (map, kanban, calendar, chart) plus the
12516- // empty bag's removal.
12517- expectedNotices: 5,
12518- },
12519- };
12520-
1252112521/**
1252212522 * The list view's `filterableFields` leaves the authorable surface (protocol
1252312523 * 18, #24640 — ADR-0049 enforce-or-remove; maintainer ruling C, record
0 commit comments