Skip to content

Commit b95b1ef

Browse files
fix(console): the settings hub tells a member that an administrator manages the settings (#29183)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbDGjnwXxfMMqXVdUJCFtA1U)_ Fixes #29085 ## What changes - `ui/apps/console/src/pages/settings/SettingsHub.tsx`: an empty manifest list now has two messages. The hub asks `usePermissions().hasCapabilities(['setup.access'])`, the answer `MePermissionsProvider` already holds from `/auth/me/permissions`. No request is added and no server code changes. - A caller **without** `setup.access` sees an `Empty` panel, titled "Settings are managed by your administrator" with the line "Your account cannot view or change these settings. Ask an administrator if something needs to change." It uses the same `Empty` / `EmptyTitle` / `EmptyDescription` layout as the app-level "You don't have access" screen. - A caller **with** it, and no manifests, sees the reworded no-settings message: "No settings pages are available yet. They appear here when an installed app provides them." The old text, "No settings registered. Plugins can register settings manifests via the SettingsService.", is gone. - A caller who gets manifests sees the list unchanged. A backend that reports no `systemPermissions` fails open onto the no-settings message, as every `hasCapabilities` gate does. - The capability is the exported constant `SETTINGS_DEFAULT_READ_CAPABILITY`. Its test parses it from `SettingsManifestSchema.shape.readPermission` in `@objectstack/spec/system`, so it cannot drift from the spec default. - `ui/packages/i18n/src/locales/*.ts` (all ten packs): new `console.settingsHub.adminManaged` and `console.settingsHub.adminManagedDescription`; `console.settingsHub.empty` reworded. - `.changeset/29085-settings-hub-admin-managed.md`: patch for `@object-ui/console` and `@object-ui/i18n`. ## Why `GET /api/settings` (service-settings `listManifests`) lists only the manifests the caller may read. A manifest that declares no `readPermission` reads at the spec default `setup.access`, so a plain member is answered an empty list while settings exist. On the stock showcase that member read a developer sentence under `/_console/apps/com.example.showcase/system/settings`, as measured on #28988. The client can already tell the two cases apart from the capability set it loads, so the fix is on the hub alone. ## Verification Clause-②: yes (widening) Metadata-protocol: none The Clause-② line follows the seat's claim amendment, made on this build's measurement: the built `dist/locales/en.d.ts` declares the two new keys as readonly literals, and the package entry re-exports `en` and `TranslationKeys`. - New `SettingsHub.adminManaged-29085.test.tsx` (real `MePermissionsProvider`, five cases): member with zero manifests sees the member copy; admin with zero manifests sees the no-settings copy; admin with manifests sees the list; member with a readable manifest sees the list; the constant equals the spec default. **On `origin/main`'s `SettingsHub.tsx`, 3 of 5 cases fail** (the constant, the member copy, the admin panel); all 5 pass on this branch. - `ui/apps/console/src/pages/settings/`: 33 files, 274 tests green. `packages/i18n/`: 95 files, 1443 passed, 13 skipped. - Derived gates (`node scripts/pm/dispatch-gates.mjs --commands`, 34 families) at 86db0d2: all exit 0, the `pnpm --dir ui exec vitest run scripts/` suite included (169 files passed, 2 skipped). The PR-body gates (`check-changeset-no-major`, `check-adr-0087-registration`, `check-closing-keyword-parity`, `check-empty-changeset`) re-ran with this body on the merged head ad5601c. `turbo run type-check` for `@object-ui/console` and `@object-ui/i18n`: 72 tasks green. eslint on the touched files: clean. `node scripts/check-sdui-manifest.mjs`: exit 0 (no registry change). ## Acceptance notes - A manifest may declare its own `readPermission`. A member holding that capability gets that manifest listed, so the list is not empty and the member copy never shows. An admin who holds `setup.access` but lacks the capability every registered manifest declares gets the no-settings message, which is still true for them. - Real-browser check on the stock showcase as member and admin: NOT MEASURED (it needs a full build and a running backend; the jsdom pins run the real permission provider). ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jUaBjpHEmA17JNgRPU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 01252a4 commit b95b1ef

13 files changed

Lines changed: 194 additions & 11 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/console': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(console): the settings hub tells a member that an administrator manages the settings
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
- **A member is no longer shown a developer sentence.** `GET /api/settings` lists only the manifests the caller may read, and a manifest that declares no `readPermission` reads at the spec default, `setup.access`. A plain member opening the settings hub under an app they may use was answered an empty list and read "No settings registered. Plugins can register settings manifests via the SettingsService." The hub now asks the capability answer the Console already holds (`/auth/me/permissions`, through `MePermissionsProvider`; no new request): a caller without `setup.access` reads "Settings are managed by your administrator" with a line saying to ask an administrator, in the style of the app-level "You don't have access" screen.
12+
- **The empty message for an administrator is reworded out of developer language:** "No settings pages are available yet. They appear here when an installed app provides them." A caller with manifests sees the list unchanged. An unreported capability answer fails open onto this message, as every capability gate does.
13+
- **New strings** `console.settingsHub.adminManaged` and `console.settingsHub.adminManagedDescription`, and the reworded `console.settingsHub.empty`, in all ten locale packs. The two new keys widen the exported `en` catalogue and `TranslationKeys` type; nothing is removed.

‎ui/apps/console/src/pages/settings/SettingsHub.tsx‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
* settings registry as this user may read it, so no entry is hand-kept here.
66
* The bare `/system` URL also lands here: the console host forwards it onto
77
* this route (`SystemLandingRedirect`, objectui#3743).
8+
*
9+
* An empty list has two readers. The server lists only the manifests the
10+
* caller may read, and a manifest that declares no `readPermission` reads at
11+
* {@link SETTINGS_DEFAULT_READ_CAPABILITY}, so a caller without it is shown
12+
* none of them: that reader is told an administrator manages the settings. A
13+
* caller who holds it and still gets none is told none are installed. The
14+
* capability answer is the one `MePermissionsProvider` already holds
15+
* (`/auth/me/permissions`), so no request is added; an unreported answer
16+
* fails open, as every `hasCapabilities` gate does, onto the second message.
817
*/
918

1019
import { useEffect, useMemo, useState } from 'react';
@@ -18,14 +27,27 @@ import {
1827
CardDescription,
1928
Badge,
2029
Skeleton,
30+
Empty,
31+
EmptyTitle,
32+
EmptyDescription,
2133
getLazyIcon,
2234
} from '@object-ui/components';
2335
import { useObjectTranslation } from '@object-ui/i18n';
36+
import { usePermissions } from '@object-ui/permissions';
2437
import { Settings as SettingsIcon } from 'lucide-react';
2538
import { listSettingsManifests } from './api';
2639
import { resolveLabel, type SettingsManifest } from './types';
2740
import { useSettingsLabel } from './useSettingsLabel';
2841

42+
/**
43+
* The capability a settings manifest is read at when it declares no
44+
* `readPermission`: the `SettingsManifestSchema` default in
45+
* `@objectstack/spec/system`, which the settings service applies when it
46+
* filters `GET /api/settings`. Its test parses a manifest through that schema,
47+
* so the two cannot drift apart unseen.
48+
*/
49+
export const SETTINGS_DEFAULT_READ_CAPABILITY = 'setup.access';
50+
2951
/**
3052
* One manifest card. Extracted so it can resolve the manifest's own
3153
* translated title/description via {@link useSettingsLabel} (hooks can't be
@@ -69,6 +91,8 @@ function SettingCard({ m, onOpen }: { m: SettingsManifest; onOpen: () => void })
6991
export function SettingsHub() {
7092
const navigate = useNavigate();
7193
const { t } = useObjectTranslation();
94+
const { hasCapabilities } = usePermissions();
95+
const mayReadSettings = hasCapabilities([SETTINGS_DEFAULT_READ_CAPABILITY]);
7296
const [manifests, setManifests] = useState<SettingsManifest[] | null>(null);
7397
const [error, setError] = useState<string | null>(null);
7498

@@ -119,8 +143,13 @@ export function SettingsHub() {
119143
))}
120144
</div>
121145
</div>
146+
) : manifests.length === 0 && !mayReadSettings ? (
147+
<Empty data-testid="settings-hub-admin-managed">
148+
<EmptyTitle>{t('console.settingsHub.adminManaged')}</EmptyTitle>
149+
<EmptyDescription>{t('console.settingsHub.adminManagedDescription')}</EmptyDescription>
150+
</Empty>
122151
) : manifests.length === 0 ? (
123-
<Card>
152+
<Card data-testid="settings-hub-empty">
124153
<CardContent className="py-12 text-center text-sm text-muted-foreground">
125154
{t('console.settingsHub.empty')}
126155
</CardContent>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Settings hub — an empty list says who it is empty for.
5+
*
6+
* `GET /api/settings` lists only the manifests the caller may read, and a
7+
* manifest that declares no `readPermission` reads at the spec's default, so a
8+
* plain member is answered an empty list while the settings exist. The hub drew
9+
* one developer sentence for every empty list ("No settings registered. Plugins
10+
* can register settings manifests via the SettingsService."). Now:
11+
*
12+
* - a caller without the default read capability is told an administrator
13+
* manages the settings;
14+
* - a caller who holds it and still gets none keeps a no-settings message;
15+
* - a caller with manifests sees the list, unchanged.
16+
*
17+
* The capability answer is the real `MePermissionsProvider`'s, fed the
18+
* `/auth/me/permissions` payload it would fetch. No i18n provider is mounted,
19+
* so each call site renders its key, as in the sibling `pageHeader` suite.
20+
*/
21+
22+
import '@testing-library/jest-dom/vitest';
23+
import { afterEach, describe, expect, it, vi } from 'vitest';
24+
import { cleanup, render, screen } from '@testing-library/react';
25+
import { MemoryRouter } from 'react-router-dom';
26+
import {
27+
MePermissionsProvider,
28+
type MePermissionsAuthenticatedResponse,
29+
type MePermissionsResponse,
30+
} from '@object-ui/permissions';
31+
import { SettingsManifestSchema } from '@objectstack/spec/system';
32+
33+
vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }));
34+
35+
import { SettingsHub, SETTINGS_DEFAULT_READ_CAPABILITY } from '../SettingsHub';
36+
37+
const MANIFESTS = [
38+
{ namespace: 'localization', version: 1, label: 'Localization', category: 'Platform', specifiers: [] },
39+
];
40+
41+
const MEMBER: string[] = [];
42+
const ADMIN = ['manage_org_users', 'setup.access', 'setup.write'];
43+
44+
function serveManifests(manifests: unknown[]) {
45+
vi.stubGlobal(
46+
'fetch',
47+
vi.fn(async () => ({
48+
ok: true,
49+
status: 200,
50+
statusText: 'OK',
51+
json: async () => ({ success: true, data: { manifests } }),
52+
})),
53+
);
54+
}
55+
56+
function permissionsPayload(systemPermissions: string[]): MePermissionsResponse {
57+
const base: MePermissionsAuthenticatedResponse = {
58+
authenticated: true,
59+
userId: 'u_1',
60+
tenantId: 'org_1',
61+
positions: [],
62+
hasAnyQueue: false,
63+
permissionSets: [],
64+
objects: {},
65+
fields: {},
66+
};
67+
return { ...base, systemPermissions };
68+
}
69+
70+
function renderHub(systemPermissions: string[]) {
71+
return render(
72+
<MemoryRouter>
73+
<MePermissionsProvider initialPermissions={permissionsPayload(systemPermissions)}>
74+
<SettingsHub />
75+
</MePermissionsProvider>
76+
</MemoryRouter>,
77+
);
78+
}
79+
80+
afterEach(() => {
81+
cleanup();
82+
vi.unstubAllGlobals();
83+
});
84+
85+
describe('Settings hub — an empty list says who it is empty for', () => {
86+
it('the capability the hub asks is the one a manifest reads at by default in the spec', () => {
87+
expect(SettingsManifestSchema.shape.readPermission.parse(undefined)).toBe(SETTINGS_DEFAULT_READ_CAPABILITY);
88+
});
89+
90+
it('a member answered no manifests is told an administrator manages the settings', async () => {
91+
serveManifests([]);
92+
renderHub(MEMBER);
93+
const panel = await screen.findByTestId('settings-hub-admin-managed');
94+
expect(panel).toHaveTextContent('console.settingsHub.adminManaged');
95+
expect(panel).toHaveTextContent('console.settingsHub.adminManagedDescription');
96+
expect(screen.queryByTestId('settings-hub-empty')).toBeNull();
97+
expect(screen.queryByText('console.settingsHub.empty')).toBeNull();
98+
});
99+
100+
it('an admin answered no manifests is told none are available, not that an administrator manages them', async () => {
101+
serveManifests([]);
102+
renderHub(ADMIN);
103+
expect(await screen.findByTestId('settings-hub-empty')).toHaveTextContent('console.settingsHub.empty');
104+
expect(screen.queryByTestId('settings-hub-admin-managed')).toBeNull();
105+
});
106+
107+
it('an admin answered manifests sees the list', async () => {
108+
serveManifests(MANIFESTS);
109+
renderHub(ADMIN);
110+
expect(await screen.findByText('Localization')).toBeInTheDocument();
111+
expect(screen.queryByTestId('settings-hub-empty')).toBeNull();
112+
expect(screen.queryByTestId('settings-hub-admin-managed')).toBeNull();
113+
});
114+
115+
it('a member answered manifests (a namespace it may read) sees the list too', async () => {
116+
serveManifests(MANIFESTS);
117+
renderHub(MEMBER);
118+
expect(await screen.findByText('Localization')).toBeInTheDocument();
119+
expect(screen.queryByTestId('settings-hub-admin-managed')).toBeNull();
120+
});
121+
});

‎ui/packages/i18n/src/locales/ar.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2018,7 +2018,9 @@ const ar = {
20182018
title: "الإعدادات",
20192019
subtitle: "اضبط النظام والتكاملات وأعلام الميزات.",
20202020
loadError: "تعذّر تحميل الإعدادات",
2021-
empty: "لا توجد إعدادات مسجّلة. يمكن للإضافات تسجيل بيانات الإعدادات عبر SettingsService.",
2021+
empty: "لا توجد صفحات إعدادات متاحة بعد. تظهر هنا عندما يوفّرها تطبيق مثبّت.",
2022+
adminManaged: "يدير المسؤول الإعدادات",
2023+
adminManagedDescription: "لا يمكن لحسابك عرض هذه الإعدادات أو تغييرها. اطلب من المسؤول إجراء أي تغيير تحتاج إليه.",
20222024
settingsCount: "{{n}} من الإعدادات",
20232025
beta: "إصدار تجريبي",
20242026
categories: {

‎ui/packages/i18n/src/locales/de.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1803,7 +1803,9 @@ const de = {
18031803
title: "Einstellungen",
18041804
subtitle: "Konfigurieren Sie Ihr System, Integrationen und Feature-Flags.",
18051805
loadError: "Einstellungen konnten nicht geladen werden",
1806-
empty: "Keine Einstellungen registriert. Plugins können Einstellungsmanifeste über den SettingsService registrieren.",
1806+
empty: "Noch keine Einstellungsseiten verfügbar. Sie erscheinen hier, sobald eine installierte App sie bereitstellt.",
1807+
adminManaged: "Einstellungen werden von Ihrem Administrator verwaltet",
1808+
adminManagedDescription: "Ihr Konto kann diese Einstellungen weder anzeigen noch ändern. Wenden Sie sich an einen Administrator, wenn etwas geändert werden muss.",
18071809
settingsCount: "{{n}} Einstellungen",
18081810
beta: "Beta",
18091811
categories: {

‎ui/packages/i18n/src/locales/en.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2227,7 +2227,9 @@ const en = {
22272227
title: 'Settings',
22282228
subtitle: 'Configure your system, integrations, and feature flags.',
22292229
loadError: 'Failed to load settings',
2230-
empty: 'No settings registered. Plugins can register settings manifests via the SettingsService.',
2230+
empty: 'No settings pages are available yet. They appear here when an installed app provides them.',
2231+
adminManaged: 'Settings are managed by your administrator',
2232+
adminManagedDescription: 'Your account cannot view or change these settings. Ask an administrator if something needs to change.',
22312233
settingsCount: '{{n}} settings',
22322234
beta: 'Beta',
22332235
// Section headers, keyed by the manifest `category` value. Unknown

‎ui/packages/i18n/src/locales/es.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1859,7 +1859,9 @@ const es = {
18591859
title: "Configuración",
18601860
subtitle: "Configure su sistema, las integraciones y los indicadores de funciones.",
18611861
loadError: "No se pudo cargar la configuración",
1862-
empty: "No hay configuraciones registradas. Los plugins pueden registrar manifiestos de configuración mediante el SettingsService.",
1862+
empty: "Todavía no hay páginas de configuración disponibles. Aparecen aquí cuando una aplicación instalada las proporciona.",
1863+
adminManaged: "La configuración la gestiona su administrador",
1864+
adminManagedDescription: "Su cuenta no puede ver ni cambiar esta configuración. Pida a un administrador cualquier cambio que necesite.",
18631865
settingsCount: "{{n}} configuraciones",
18641866
beta: "Beta",
18651867
categories: {

‎ui/packages/i18n/src/locales/fr.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1855,7 +1855,9 @@ const fr = {
18551855
title: "Paramètres",
18561856
subtitle: "Configurez votre système, vos intégrations et vos indicateurs de fonctionnalité.",
18571857
loadError: "Échec du chargement des paramètres",
1858-
empty: "Aucun paramètre enregistré. Les plugins peuvent enregistrer des manifestes de paramètres via le SettingsService.",
1858+
empty: "Aucune page de paramètres n'est encore disponible. Elles apparaissent ici lorsqu'une application installée en fournit.",
1859+
adminManaged: "Les paramètres sont gérés par votre administrateur",
1860+
adminManagedDescription: "Votre compte ne peut ni afficher ni modifier ces paramètres. Adressez-vous à un administrateur si quelque chose doit changer.",
18591861
settingsCount: "{{n}} paramètres",
18601862
beta: "Bêta",
18611863
categories: {

‎ui/packages/i18n/src/locales/ja.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1803,7 +1803,9 @@ const ja = {
18031803
title: "設定",
18041804
subtitle: "システム、連携、機能フラグを設定します。",
18051805
loadError: "設定の読み込みに失敗しました",
1806-
empty: "登録された設定はありません。プラグインは SettingsService 経由で設定マニフェストを登録できます。",
1806+
empty: "利用できる設定ページはまだありません。インストールされたアプリが設定ページを提供すると、ここに表示されます。",
1807+
adminManaged: "設定は管理者が管理しています",
1808+
adminManagedDescription: "お使いのアカウントではこれらの設定を表示・変更できません。変更が必要な場合は管理者に依頼してください。",
18071809
settingsCount: "{{n}} 件の設定",
18081810
beta: "ベータ",
18091811
categories: {

‎ui/packages/i18n/src/locales/ko.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1803,7 +1803,9 @@ const ko = {
18031803
title: "설정",
18041804
subtitle: "시스템, 연동, 기능 플래그를 구성합니다.",
18051805
loadError: "설정을 불러오지 못했습니다",
1806-
empty: "등록된 설정이 없습니다. 플러그인은 SettingsService를 통해 설정 매니페스트를 등록할 수 있습니다.",
1806+
empty: "아직 사용할 수 있는 설정 페이지가 없습니다. 설치된 앱이 설정 페이지를 제공하면 여기에 표시됩니다.",
1807+
adminManaged: "설정은 관리자가 관리합니다",
1808+
adminManagedDescription: "계정에 이 설정을 보거나 변경할 권한이 없습니다. 변경이 필요하면 관리자에게 요청하세요.",
18071809
settingsCount: "설정 {{n}}개",
18081810
beta: "베타",
18091811
categories: {

0 commit comments

Comments
 (0)