Skip to content

Commit 6c77bf9

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-c4pypv
2 parents c5887e7 + 172eebb commit 6c77bf9

29 files changed

Lines changed: 1265 additions & 120 deletions
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+
'@object-ui/data-objectstack': patch
4+
---
5+
6+
fix(app-shell,data-objectstack): Studio's and Setup's object editors edit the object as its author wrote it, not as the console is served it
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
- **`@object-ui/app-shell`:** the object editor in Studio's Data pillar and the one in Setup's metadata editor (`MetadataResourceEditPage` for `object`) now take their editable copy from the layered read's author-owned layers: the pending draft as stored (`draft`) when one is pending, otherwise `overlay ?? code`. Before, the copy was the `?state=draft` read (or `effective`), which arrives translated into the console's language and governed with the platform's injected columns (`owner_id`, `queue`, the audit and scope columns), so in a Chinese console one field edit saved the Chinese labels as the author's labels and sent the platform's columns back as the author's fields. What the editor shows of the served object stays as it was: the header's system-field count still counts the platform's columns, and the records grid still shows the served `owner_id` column. When a draft is pending but the layered read does not carry it, the editor installs nothing, shows the existing "draft unreadable" refusal, and sends nothing.
12+
- **`@object-ui/data-objectstack`:** `MetadataClient.layered()` passes through the `draft` layer the server serves (`MetadataLayered.draft`, optional). It used to drop it.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): `objectstack-platform`'s plugin-development reference no longer teaches the removed `feature_flags` settings namespace
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The "Feature Flags" section listed a `feature_flags` settings manifest with `ai_enabled` / `beta_*` toggles and `OS_FEATURE_FLAGS_*` env overrides. That namespace was removed from `@objectstack/service-settings` (reading it is refused with `SETTINGS_UNKNOWN_NAMESPACE`, and the env vars do nothing). The bullet now gives the live answer: a plugin that needs an org toggle declares its own `SettingsManifest` and registers it on the `settings` service.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/service-settings': patch
3+
---
4+
5+
fix(service-settings): a namespace reset clears the value stored beneath an env pin, so the old value does not come back when the variable is removed
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **The reset clears beneath a pin.** `SettingsService.resetNamespace` and the built-in `reset` settings action skipped every key an `OS_*` override in force pins, so a value stored beneath the pin survived the reset and took over again the day the variable was removed. The reset now clears that stored value too, with the same clear a single `null` write to a pinned key makes: it changes nothing in force (the key still answers the env value, `source: 'env'`, `locked: true`), it records the same `reset` audit row and change event, and once the variable is removed the key reads its default. A pinned key with nothing stored beneath it is left alone, and a key no variable pins is reset as before.
11+
- **The count includes those keys.** `resetNamespace` returns the number of keys it cleared, pinned ones included.
12+
- **The reset's answer says what is still pinned.** When an environment variable still pins some of the namespace's keys, the `reset` action's message adds one sentence with their number ("2 setting(s) remain pinned by environment variables."), in all eleven languages the answer is written in. A namespace no variable pins gets the same message as before.

‎packages/services/service-settings/src/settings-action-copy.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ export interface SettingsActionCopy {
2020
resetCleared(count: number): string;
2121
/** The built-in reset found no saved value to clear. */
2222
resetNothing: string;
23+
/**
24+
* Appended to either reset answer when `count` (> 0) of the namespace's keys
25+
* are still pinned by an environment variable, which the reset leaves in
26+
* force. Written to follow the answer directly: it carries the space its
27+
* language puts between two sentences, and none where it puts none.
28+
*/
29+
resetStillPinned(count: number): string;
2330
/** No handler is registered for `actionId` in `namespace`. */
2431
noHandler(actionId: string, namespace: string): string;
2532
}
@@ -29,33 +36,42 @@ export const SETTINGS_ACTION_COPY: Record<SettingsCopyLocale, SettingsActionCopy
2936
en: {
3037
resetCleared: (n) => `Cleared ${n} saved value(s); environment/default configuration is back in effect.`,
3138
resetNothing: 'No saved values to clear — already using environment/default configuration.',
39+
resetStillPinned: (n) => ` ${n} setting(s) remain pinned by environment variables.`,
3240
noHandler: (actionId, namespace) => `No handler registered for action '${actionId}' in '${namespace}'.`,
3341
},
3442
'zh-CN': {
3543
resetCleared: (n) => `已清除 ${n} 个已保存的值;环境变量/默认配置已重新生效。`,
3644
resetNothing: '没有可清除的已保存值:当前已在使用环境变量/默认配置。',
45+
resetStillPinned: (n) => `仍有 ${n} 个设置由环境变量固定。`,
3746
noHandler: (actionId, namespace) => `“${namespace}”中的操作“${actionId}”没有注册处理程序。`,
3847
},
3948
'zh-Hant': {
4049
resetCleared: (n) => `已清除 ${n} 個已儲存的值;環境變數/預設設定已重新生效。`,
4150
resetNothing: '沒有可清除的已儲存值:目前已在使用環境變數/預設設定。',
51+
resetStillPinned: (n) => `仍有 ${n} 個設定由環境變數固定。`,
4252
noHandler: (actionId, namespace) => `「${namespace}」中的操作「${actionId}」沒有註冊處理常式。`,
4353
},
4454
'ja-JP': {
4555
resetCleared: (n) => `保存済みの値を ${n} 件消去しました。環境変数/既定の設定が再び有効になっています。`,
4656
resetNothing: '消去する保存済みの値はありません。すでに環境変数/既定の設定を使用しています。',
57+
resetStillPinned: (n) => `${n} 件の設定は引き続き環境変数で固定されています。`,
4758
noHandler: (actionId, namespace) => `「${namespace}」のアクション「${actionId}」にはハンドラーが登録されていません。`,
4859
},
4960
ko: {
5061
resetCleared: (n) => `저장된 값 ${n}개를 지웠습니다. 환경 변수/기본 설정이 다시 적용됩니다.`,
5162
resetNothing: '지울 저장된 값이 없습니다. 이미 환경 변수/기본 설정을 사용하고 있습니다.',
63+
resetStillPinned: (n) => ` 설정 ${n}개는 여전히 환경 변수로 고정되어 있습니다.`,
5264
noHandler: (actionId, namespace) => `'${namespace}'의 '${actionId}' 작업에 등록된 핸들러가 없습니다.`,
5365
},
5466
'es-ES': {
5567
resetCleared: (n) =>
5668
`Se ${n === 1 ? 'ha borrado 1 valor guardado' : `han borrado ${n} valores guardados`}; vuelve a aplicarse `
5769
+ 'la configuración del entorno o la predeterminada.',
5870
resetNothing: 'No hay valores guardados que borrar: ya se usa la configuración del entorno o la predeterminada.',
71+
resetStillPinned: (n) =>
72+
n === 1
73+
? ' 1 ajuste sigue fijado por una variable de entorno.'
74+
: ` ${n} ajustes siguen fijados por variables de entorno.`,
5975
noHandler: (actionId, namespace) =>
6076
`No hay ningún controlador registrado para la acción «${actionId}» en «${namespace}».`,
6177
},
@@ -65,13 +81,18 @@ export const SETTINGS_ACTION_COPY: Record<SettingsCopyLocale, SettingsActionCopy
6581
one: 'قيمة محفوظة واحدة', two: 'قيمتين محفوظتين', few: 'قيم محفوظة', many: 'قيمة محفوظة', other: 'قيمة محفوظة',
6682
})}؛ وعاد إعداد البيئة أو الإعداد الافتراضي إلى العمل.`,
6783
resetNothing: 'لا توجد قيم محفوظة لمسحها، فإعداد البيئة أو الإعداد الافتراضي مستخدم بالفعل.',
84+
resetStillPinned: (n) => ` عدد الإعدادات التي لا تزال مثبّتة بمتغيرات البيئة: ${n}.`,
6885
noHandler: (actionId, namespace) => `لا يوجد معالج مسجّل للإجراء «${actionId}» في «${namespace}».`,
6986
},
7087
fr: {
7188
resetCleared: (n) =>
7289
`${n === 1 ? '1 valeur enregistrée effacée' : `${n} valeurs enregistrées effacées`} ; la configuration de `
7390
+ 'l’environnement ou par défaut s’applique de nouveau.',
7491
resetNothing: 'Aucune valeur enregistrée à effacer : la configuration de l’environnement ou par défaut est déjà utilisée.',
92+
resetStillPinned: (n) =>
93+
n === 1
94+
? ' 1 paramètre reste fixé par une variable d’environnement.'
95+
: ` ${n} paramètres restent fixés par des variables d’environnement.`,
7596
noHandler: (actionId, namespace) =>
7697
`Aucun gestionnaire n’est enregistré pour l’action « ${actionId} » dans « ${namespace} ».`,
7798
},
@@ -80,12 +101,17 @@ export const SETTINGS_ACTION_COPY: Record<SettingsCopyLocale, SettingsActionCopy
80101
`${n === 1 ? '1 valor salvo foi apagado' : `${n} valores salvos foram apagados`}; a configuração do ambiente `
81102
+ 'ou padrão voltou a valer.',
82103
resetNothing: 'Não há valores salvos para apagar: a configuração do ambiente ou padrão já está em uso.',
104+
resetStillPinned: (n) =>
105+
n === 1
106+
? ' 1 configuração continua fixada por uma variável de ambiente.'
107+
: ` ${n} configurações continuam fixadas por variáveis de ambiente.`,
83108
noHandler: (actionId, namespace) => `Nenhum manipulador registrado para a ação "${actionId}" em "${namespace}".`,
84109
},
85110
ru: {
86111
resetCleared: (n) =>
87112
`Очищено сохранённых значений: ${n}; снова действуют настройки из окружения или по умолчанию.`,
88113
resetNothing: 'Нет сохранённых значений для очистки: уже используются настройки из окружения или по умолчанию.',
114+
resetStillPinned: (n) => ` Настроек, по-прежнему закреплённых переменными окружения: ${n}.`,
89115
noHandler: (actionId, namespace) => `Для действия «${actionId}» в «${namespace}» не зарегистрирован обработчик.`,
90116
},
91117
de: {
@@ -95,6 +121,10 @@ export const SETTINGS_ACTION_COPY: Record<SettingsCopyLocale, SettingsActionCopy
95121
resetNothing:
96122
'Keine gespeicherten Werte zu löschen: Es gilt bereits die Konfiguration aus der Umgebung bzw. die '
97123
+ 'Standardkonfiguration.',
124+
resetStillPinned: (n) =>
125+
n === 1
126+
? ' 1 Einstellung bleibt durch eine Umgebungsvariable festgelegt.'
127+
: ` ${n} Einstellungen bleiben durch Umgebungsvariablen festgelegt.`,
98128
noHandler: (actionId, namespace) => `Für die Aktion „${actionId}“ in „${namespace}“ ist kein Handler registriert.`,
99129
},
100130
};

‎packages/services/service-settings/src/settings-copy-locale.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const SAMPLE_ARGS: Record<string, unknown[]> = {
2828
lockedText: ['2 objects'],
2929
unsetText: ['2 objects'],
3030
resetCleared: [3],
31+
resetStillPinned: [2],
3132
noHandler: ['probe', 'branding'],
3233
};
3334

‎packages/services/service-settings/src/settings-service.test.ts‎

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ describe('SettingsService — resetNamespace / built-in reset action', () => {
378378
expect(r.value).toBeNull();
379379
});
380380

381-
it('leaves env-locked keys untouched', async () => {
381+
it('leaves an env-locked key with nothing stored beneath it untouched', async () => {
382382
const svc = new SettingsService({ env: { OS_BRANDING_WORKSPACE_NAME: 'EnvCorp' } });
383383
svc.registerManifest(brandingSettingsManifest);
384384
await expect(svc.resetNamespace('branding')).resolves.toBe(0);
@@ -2880,3 +2880,92 @@ describe('SettingsService — a clear goes through an env pin, every other write
28802880
expect(answer.banner).toMatchObject({ value: 'Pinned banner', source: 'env', locked: true });
28812881
});
28822882
});
2883+
2884+
describe('SettingsService — a reset clears the value stored beneath an env pin', () => {
2885+
const RESETTABLE = {
2886+
namespace: 'resettable',
2887+
version: 1,
2888+
label: 'Resettable',
2889+
scope: 'tenant',
2890+
specifiers: [
2891+
{ type: 'text', key: 'motto', label: 'Motto', scope: 'tenant', default: 'none' },
2892+
{ type: 'text', key: 'banner', label: 'Banner', scope: 'tenant', default: 'none' },
2893+
{ type: 'text', key: 'footer', label: 'Footer', scope: 'tenant', default: 'none' },
2894+
],
2895+
} as any;
2896+
2897+
/** `motto` pinned over a stored value, `banner` stored and not pinned, `footer` pinned with nothing stored. */
2898+
async function namespaceWithPins() {
2899+
const env: Record<string, string | undefined> = {};
2900+
const audit: Array<Record<string, unknown>> = [];
2901+
const svc = new SettingsService({
2902+
env,
2903+
auditWriter: { write: async (row: unknown) => { audit.push(row as Record<string, unknown>); } },
2904+
} as any);
2905+
svc.registerManifest(RESETTABLE);
2906+
await svc.setMany('resettable', { motto: 'Stored motto', banner: 'Stored banner' });
2907+
env.OS_RESETTABLE_MOTTO = 'Pinned motto';
2908+
env.OS_RESETTABLE_FOOTER = 'Pinned footer';
2909+
audit.length = 0;
2910+
return { env, svc, audit };
2911+
}
2912+
2913+
it('clears the pinned key\'s stored value, counts it, and changes nothing in force while the pin holds', async () => {
2914+
const { env, svc } = await namespaceWithPins();
2915+
const changes: Array<{ key: string; action: string }> = [];
2916+
svc.subscribe('resettable', (e) => { changes.push(e as { key: string; action: string }); });
2917+
2918+
await expect(svc.resetNamespace('resettable')).resolves.toBe(2);
2919+
expect(changes.map((c) => [c.key, c.action]).sort()).toEqual([['banner', 'reset'], ['motto', 'reset']]);
2920+
expect(await svc.get('resettable', 'motto')).toMatchObject({ value: 'Pinned motto', source: 'env', locked: true });
2921+
expect(await svc.get('resettable', 'footer')).toMatchObject({ value: 'Pinned footer', source: 'env', locked: true });
2922+
expect(await svc.get('resettable', 'banner')).toMatchObject({ value: 'none', source: 'default' });
2923+
2924+
// Lift the pins: each key reads its default, not the value once stored beneath it.
2925+
delete env.OS_RESETTABLE_MOTTO;
2926+
delete env.OS_RESETTABLE_FOOTER;
2927+
expect(await svc.get('resettable', 'motto')).toMatchObject({ value: 'none', source: 'default', locked: false });
2928+
expect(await svc.get('resettable', 'footer')).toMatchObject({ value: 'none', source: 'default', locked: false });
2929+
});
2930+
2931+
it('records the pinned key\'s reset as a single clear of it records it', async () => {
2932+
const reset = await namespaceWithPins();
2933+
await reset.svc.resetNamespace('resettable');
2934+
const single = await namespaceWithPins();
2935+
await single.svc.set('resettable', 'motto', null);
2936+
2937+
const motto = (rows: Array<Record<string, unknown>>) => rows.filter((r) => r.key === 'motto');
2938+
expect(motto(reset.audit)).toEqual(motto(single.audit));
2939+
expect(motto(reset.audit)).toEqual([expect.objectContaining({ action: 'reset', newHash: null })]);
2940+
expect(reset.audit.map((r) => r.key).sort()).toEqual(['banner', 'motto']);
2941+
});
2942+
2943+
it('a pin with nothing stored beneath it is not cleared, and a second reset clears nothing', async () => {
2944+
const { svc, audit } = await namespaceWithPins();
2945+
await svc.resetNamespace('resettable');
2946+
audit.length = 0;
2947+
await expect(svc.resetNamespace('resettable')).resolves.toBe(0);
2948+
expect(audit).toEqual([]);
2949+
});
2950+
2951+
it('the reset action counts the pinned key it cleared and says how many keys the environment still pins', async () => {
2952+
const { svc } = await namespaceWithPins();
2953+
const first = await svc.runAction('resettable', 'reset', null);
2954+
expect(first).toMatchObject({ ok: true, severity: 'info' });
2955+
expect(first.message).toBe(
2956+
'Cleared 2 saved value(s); environment/default configuration is back in effect. '
2957+
+ '2 setting(s) remain pinned by environment variables.',
2958+
);
2959+
2960+
const second = await svc.runAction('resettable', 'reset', null, {}, { locale: 'zh-CN' });
2961+
expect(second.message).toBe('没有可清除的已保存值:当前已在使用环境变量/默认配置。仍有 2 个设置由环境变量固定。');
2962+
});
2963+
2964+
it('a namespace no environment variable pins answers without the pinned sentence', async () => {
2965+
const svc = new SettingsService({ env: {} });
2966+
svc.registerManifest(RESETTABLE);
2967+
await svc.set('resettable', 'motto', 'Stored motto');
2968+
const r = await svc.runAction('resettable', 'reset', null);
2969+
expect(r.message).toBe('Cleared 1 saved value(s); environment/default configuration is back in effect.');
2970+
});
2971+
});

0 commit comments

Comments
 (0)