Skip to content

Commit b084171

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-gaqa04
2 parents c35f176 + d00215f commit b084171

35 files changed

Lines changed: 1912 additions & 806 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-form': patch
3+
---
4+
5+
fix(plugin-form): a `{ group }` form section no longer warns that it "also declares `collapsible`, `collapsed`" when its author wrote neither
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`@objectstack/spec` gives a form section's `collapsible` and `collapsed` a default of `false`, so every parsed section carries both, and it refuses them beside `group` only when they are `true`. The console's form renderer reported any value at all, so every `{ group }` section of a parsed form logged the group-owned-key warning once per page and told the author to move keys they never wrote to the object's `fieldGroups`. The renderer now reads those two keys the way the spec does: `true` beside `group` is still reported (a programmatic caller that skips the spec's parse), while `false` or absent is not. `name`, `label`, `description` and `visibleWhen` are still reported on any value.
11+
12+
What to do: nothing. If you moved `collapsible` / `collapsed` off a `{ group }` section because of this warning, no metadata change was needed.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-dashboard': patch
3+
---
4+
5+
fix(plugin-dashboard): a `compareTo` KPI tile shows its number when the reader picks All time, instead of the executor's "compareTo needs a dated window to shift" error
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** A dataset-bound widget with `compareTo` always sent `compareTo` to the dataset executor, but lowered a `timeDimensions` window only when its resolved filter held a closed `{ $gte, $lte }` date pair. Picking **All time** on the dashboard's date-range filter removes that pair, so the executor refused the query and the tile showed a 400 error where its number belonged. The same tile without `compareTo` showed its number.
11+
- **Now.** When the resolved filter carries no date condition at all on the compared field (on any field when `compareTo.dimension` is omitted), the widget asks for no comparison: the query is the one a widget without `compareTo` sends, and the tile shows its number with no comparison badge.
12+
- **Unchanged: an author-written window that is not closed stays an error.** A one-bound window (`{ $gte: '{current_year_start}' }`, a YTD tile), an exclusive bound or a single day is still sent with `compareTo`, and the executor still refuses it and names the fix. The executor and the spec define a comparison only against a two-bound window. Two dated fields with no `compareTo.dimension` stay the executor's ambiguity error.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
refactor(app-shell): the Console `ObjectView`'s module-level helpers move into sibling modules
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Module-level helpers, types and constants of `views/ObjectView.tsx` (the record links the list publishes, the saved-view tab, override and column helpers, and the small toolbar and filter helpers) move verbatim into `objectViewRecordLinks.ts`, `objectViewTabs.ts` and `objectViewHelpers.ts` beside it. No logic, name or rendered output changes. `ObjectView.tsx` keeps the component and re-exports every name it exported before, and the package entry is unchanged.
11+
12+
What to do: nothing.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
---
4+
5+
fix(storage)!: a file its record lets go keeps that record's read rule
6+
7+
ADR-0104 D3 promises one file, one owning record and one access rule. That rule now holds after the owning record lets the file go: until a writer who may read that record claims the file again, or the platform collects it, the file is read by that record's rule. The download door and the record-write claim path ask the same verdict, so both follow it. A file no record has owned is read as before, within its organization. The uploader still reads their own file.
8+
9+
`sys_file` gains two nullable fields, `released_ref_object` and `released_ref_id`, naming the record that last owned the file. The platform writes them when a record lets a file go and clears them when a record claims it. Existing rows hold NULL and are read as before.
10+
11+
Clause-②: no (narrowing) — a file read that a record's rule refuses is now refused after the record lets the file go. Nothing that was refused becomes accepted.
12+
13+
Metadata-protocol: none (a file read rule and two platform-managed bookkeeping fields; nothing a metadata author writes changes)
14+
15+
<!-- adr-0087: not-required (no-migration-prescription) No metadata author's surface moves: no spec key or authorable spelling is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. The platform object `sys_file` gains two nullable bookkeeping fields that only the platform writes; schema sync adds the columns and existing rows read NULL, which is the shape every row had before. What changes is which callers a file read admits after the file's owning record lets it go. The remedy is reading the file as a reader of that record, never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
16+
17+
**BREAKING** for a reader the owning record's rule refuses, shipped as `patch` because the metadata protocol does not move.
18+
19+
- **Before.** The owning record's rule did not hold for the file once the record let it go.
20+
- **Now.** It is read by the rule of the record that let it go, until it is claimed again or collected.
21+
- **Fix.** Read or claim such a file as a reader of that record.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
The built-in permission sets' names are shown in Chinese, Japanese and Spanish
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Setup → Permission Sets showed the platform's own permission sets with English names (*Administrator — Full Access*, *Member — Standard Access* …) under zh-CN, ja-JP and es-ES, next to each app's translated sets and next to their own translated descriptions. Each built-in set's label is now translated through the permission-set group of the translation bundle (`permissions.NAME.label`), beside its description, so a row reads in one language. For example, under zh-CN *Administrator — Full Access* reads 「管理员 — 完全访问」 and *Viewer — Read-Only* reads 「查看者 — 只读」.
11+
12+
No set's grants, name, English label or description changed. This changes only the text shown for each set, not its permissions.

‎docs/adr/0104-field-runtime-value-shape-contract.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,9 +1459,11 @@ exists in the acting organization. One file had two access rules.
14591459
- **One judgement, two callers.** The door's verdict is one function, and the
14601460
claim path calls it under the write's caller context; neither restates it.
14611461
It admits the uploader; a reader of the record that owns the file, through
1462-
its object's `fileAccessDelegate` where one is declared; a member of the
1463-
organization for a file no record owns; a reader of any parent for an
1464-
attachments-scope file; anyone for a `public_read` file.
1462+
its object's `fileAccessDelegate` where one is declared; for a file a record
1463+
let go, a reader of that record, until the file is claimed again or
1464+
collected (#26046); a member of the organization for a file no record has
1465+
owned; a reader of any parent for an attachments-scope file; anyone for a
1466+
`public_read` file.
14651467
- **The quiet answer.** At the write door, a file the writer may not read is
14661468
answered as one the organization-scoped lookup cannot see: the id is kept
14671469
as written, nothing is claimed or copied, and constraint verdicts do not run

‎packages/plugins/plugin-security/src/objects/permission-set-descriptions.test.ts‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import { describe, it, expect } from 'vitest';
44
import { PermissionSetSchema } from '@objectstack/spec/security';
5+
import { translateMetadataDocument } from '@objectstack/spec/system';
56
import { defaultPermissionSets } from './default-permission-sets.js';
67
import { SecurityTranslations } from '../translations/index.js';
78

89
/**
9-
* Every built-in permission set says who holds it.
10+
* Every built-in permission set says who holds it, in every shipped locale.
1011
*
1112
* Setup → Permission Sets lists the platform's own sets beside every app's,
1213
* with each set's `description`. Five of them showed none and the no-bypass
@@ -15,18 +16,25 @@ import { SecurityTranslations } from '../translations/index.js';
1516
* member set. This pins the business fact that every set in
1617
* `defaultPermissionSets` carries ONE plain sentence of at most 160
1718
* characters, and that every translated locale of this plugin's bundle
18-
* carries that sentence for it. It is deliberately not a copy of the wording,
19+
* carries that sentence and the set's label for it, so no row of the list
20+
* reads half English. It is deliberately not a copy of the wording,
1921
* which `default-permission-sets.ts` and `../translations/permission-sets.ts`
2022
* own.
2123
*
2224
* A sentence: non-empty, no line break, at most 160 characters, ends with one
2325
* full stop (`.`, or `。` in Chinese and Japanese) and holds no other sentence
2426
* break. Engineering terms stay in the code comments beside each set, so the
2527
* copy names no permission-set id and no code span.
28+
*
29+
* A label: a short noun phrase (at most 80 characters, no line break, no
30+
* closing full stop) split by ` — ` exactly as often as the English label, so
31+
* a translation keeps the "who — what access" shape the list reads by.
2632
*/
2733
type AnyRec = Record<string, any>;
2834

2935
const MAX_LENGTH = 160;
36+
const MAX_LABEL_LENGTH = 80;
37+
const LABEL_SPLIT = ' — ';
3038
const TRANSLATED_LOCALES = Object.keys(SecurityTranslations).filter((l) => l !== 'en');
3139
const SET_NAMES = defaultPermissionSets.map((ps) => ps.name);
3240

@@ -45,6 +53,21 @@ function expectOneSentence(text: unknown, where: string): void {
4553
for (const name of SET_NAMES) expect(value, `${where}: names ${name}`).not.toContain(name);
4654
}
4755

56+
function expectLabel(text: unknown, english: string, where: string): void {
57+
expect(typeof text, `${where}: label missing`).toBe('string');
58+
const value = String(text);
59+
expect(value, `${where}: padded`).toBe(value.trim());
60+
expect(value.length, `${where}: empty`).toBeGreaterThan(0);
61+
expect([...value].length, `${where}: over ${MAX_LABEL_LENGTH} characters`).toBeLessThanOrEqual(MAX_LABEL_LENGTH);
62+
expect(value, `${where}: line break`).not.toMatch(/[\r\n]/);
63+
expect(value, `${where}: ends with a full stop`).not.toMatch(/[.。]$/);
64+
expect(value, `${where}: code span`).not.toMatch(/`/);
65+
for (const name of SET_NAMES) expect(value, `${where}: names ${name}`).not.toContain(name);
66+
expect(value.split(LABEL_SPLIT).length, `${where}: not in the English label's shape`).toBe(
67+
english.split(LABEL_SPLIT).length,
68+
);
69+
}
70+
4871
describe('built-in permission set descriptions', () => {
4972
it('ships the nine built-in sets', () => {
5073
expect(SET_NAMES).toEqual([
@@ -91,8 +114,32 @@ describe('built-in permission set descriptions', () => {
91114
const entry = group[name] as AnyRec | undefined;
92115
expectOneSentence(entry?.description, `${locale} ${name}`);
93116
expect(entry?.description, `${locale} ${name}: an English copy, not a translation`).not.toBe(set.description);
94-
// Descriptions only: the labels are not translated here.
95-
expect(entry && 'label' in entry, `${locale} ${name}: carries a label`).toBe(false);
117+
},
118+
);
119+
120+
it.each(defaultPermissionSets.map((ps) => [ps.name, ps] as const))(
121+
'%s carries a translated label',
122+
(name, set) => {
123+
const entry = group[name] as AnyRec | undefined;
124+
expectLabel(entry?.label, String(set.label), `${locale} ${name}`);
125+
expect(entry?.label, `${locale} ${name}: an English copy, not a translation`).not.toBe(set.label);
126+
},
127+
);
128+
129+
it('gives no two sets the same label', () => {
130+
const labels = SET_NAMES.map((name) => (group[name] as AnyRec | undefined)?.label);
131+
expect(new Set(labels).size).toBe(labels.length);
132+
});
133+
134+
// The overlay `GET /meta/permission` applies at the REST boundary, read
135+
// through the bundle this plugin loads: the served set's label and
136+
// description are this locale's, nothing else of the set changes.
137+
it.each(defaultPermissionSets.map((ps) => [ps.name, ps] as const))(
138+
'%s is served in this locale',
139+
(name, set) => {
140+
const served = translateMetadataDocument('permission', set, SecurityTranslations, { locale });
141+
const entry = group[name] as AnyRec;
142+
expect(served).toEqual({ ...set, label: entry.label, description: entry.description });
96143
},
97144
);
98145
});

0 commit comments

Comments
 (0)