Skip to content

Commit c86da40

Browse files
authored
refactor(desktop): rebuild the About page as labeled settings groups (#4571)
The About page obscured the update channel, repeated build information, and used a bespoke layout outside the settings groups. Use SettingsSection for the lead, support, and privacy groups, with channel identity read from the same packaged manifest as the updater. Keep diagnostics copyable through an unknown-channel fallback, name the row controls for assistive technology, and consolidate five privacy bullets into three without changing the commitments. Remove the unused PageHeader export and About stylesheet, and update the consuming example, stories, and tests. No persisted-data migration is required. The unused @maka/ui PageHeader export is removed. Generated-by: Maka
1 parent 6c8e749 commit c86da40

28 files changed

Lines changed: 536 additions & 462 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { ensureSidebarExpanded, expect, test } from './fixtures';
21+
22+
test('About renders channel facts, support, and privacy on a dev checkout', async ({ window: page }) => {
23+
await ensureSidebarExpanded(page);
24+
await page.getByRole('button', { name: '设置' }).click();
25+
await page.getByRole('button', { name: '关于', exact: true }).click();
26+
27+
// The channel token must agree with the version string: the fixture app is a
28+
// dev checkout, so it reads 本地开发版.
29+
await expect(page.getByText('本地开发版', { exact: true })).toBeVisible();
30+
await expect(page.getByText('本地开发构建,不检查更新。')).toBeVisible();
31+
32+
// A dev checkout follows no feed, so the whole update row is absent — it is
33+
// not a disabled button next to a sentence repeating the line above it.
34+
await expect(page.getByRole('button', { name: '检查更新' })).toHaveCount(0);
35+
36+
// Support lives outside the info conditional: usable even when `app.info`
37+
// fails, which is exactly when a user reaches for it. Each row-end control is
38+
// named by its row, not by the verb on its face.
39+
await expect(page.getByRole('heading', { name: '支持', exact: true })).toBeVisible();
40+
await expect(page.getByRole('button', { name: '复制诊断信息' })).toBeEnabled();
41+
await expect(page.getByRole('link', { name: '报告问题' })).toBeVisible();
42+
await expect(page.getByRole('button', { name: '键盘快捷键' })).toBeEnabled();
43+
44+
// Three commitments, not the old wall of five bullets.
45+
const privacyList = page.getByRole('list', { name: '隐私承诺' });
46+
await expect(privacyList.getByRole('listitem')).toHaveCount(3);
47+
});

‎apps/desktop/renderer-architecture.json‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2585,17 +2585,14 @@
25852585
"../../preload/bridge-contract.js": 1,
25862586
"../default-runtime-host-operation.js": 1,
25872587
"../locales/settings-preferences-copy.js": 1,
2588-
"../locales/settings-shared-copy.js": 1,
25892588
"./about-update-status.js": 1,
25902589
"./settings-error-copy.js": 1,
2591-
"./settings-rows.js": 1,
25922590
"./settings-section.js": 1,
25932591
"./settings-skeleton.js": 1,
25942592
"./use-action-guard.js": 1,
25952593
"@astryxdesign/core": 1,
25962594
"@astryxdesign/core/Kbd": 1,
25972595
"@maka/ui": 1,
2598-
"@maka/ui/icons": 1,
25992596
"react": 1
26002597
}
26012598
},

‎apps/desktop/src/main/__tests__/about-settings-page.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ test('keeps manual diagnostics available while About metadata is pending', () =>
3232
createElement(LocaleProvider, { locale: 'en', children: withAstryxLocale }),
3333
);
3434

35-
assert.match(markup, />Copy diagnostics</);
35+
// The row LABEL also reads "Copy diagnostics", so match the control itself:
36+
// its accessible name is the aria-label, not the verb on its face.
37+
assert.match(markup, /<button[^>]*aria-label="Copy diagnostics"/);
3638
assert.match(markup, /role="status"[^>]*aria-busy="true"/);
3739
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import assert from 'node:assert/strict';
21+
import { test } from 'node:test';
22+
import {
23+
aboutChannelFacts,
24+
aboutUpdateStatusDetail,
25+
} from '../../renderer/settings/about-update-status.js';
26+
import { getSettingsPreferencesCopy } from '../../renderer/locales/settings-preferences-copy.js';
27+
28+
const copy = getSettingsPreferencesCopy('zh').about;
29+
30+
test('a packaged nightly is tokened Nightly, never 正式版', () => {
31+
const facts = aboutChannelFacts({ buildMode: 'packaged', updateChannel: 'nightly' }, copy);
32+
assert.deepEqual(facts.token, { label: 'Nightly', color: 'orange' });
33+
assert.match(facts.summary, /会覆盖正式版安装/);
34+
});
35+
36+
test('a packaged release wears no token — it is the default state', () => {
37+
const facts = aboutChannelFacts({ buildMode: 'packaged', updateChannel: 'release' }, copy);
38+
assert.equal(facts.token, null);
39+
assert.equal(facts.summary, '正式发布版,自动接收稳定更新。');
40+
});
41+
42+
test('buildMode decides before updateChannel, whose dev value is a placeholder', () => {
43+
const facts = aboutChannelFacts({ buildMode: 'dev', updateChannel: 'nightly' }, copy);
44+
assert.deepEqual(facts.token, { label: '本地开发版', color: 'gray' });
45+
assert.equal(facts.summary, '本地开发构建,不检查更新。');
46+
});
47+
48+
test('the nightly steady states each read as themselves', () => {
49+
const detail = (status: Parameters<typeof aboutUpdateStatusDetail>[0]) =>
50+
aboutUpdateStatusDetail(status, copy);
51+
52+
assert.equal(
53+
detail({
54+
state: 'downloading',
55+
currentVersion: '0.2.0-dev.11.20260831',
56+
latestVersion: '0.2.0-dev.12.20260901',
57+
progress: { percent: 42.4, bytesPerSecond: 1, transferred: 1, total: 2 },
58+
}),
59+
'正在下载 v0.2.0-dev.12.20260901(42%)…',
60+
);
61+
assert.equal(
62+
detail({
63+
state: 'verifying',
64+
currentVersion: '0.2.0-dev.11.20260831',
65+
latestVersion: '0.2.0-dev.12.20260901',
66+
}),
67+
'正在验证 v0.2.0-dev.12.20260901 的发布来源…',
68+
);
69+
assert.equal(
70+
detail({
71+
state: 'downloaded',
72+
currentVersion: '0.2.0-dev.11.20260831',
73+
latestVersion: '0.2.0-dev.12.20260901',
74+
}),
75+
'v0.2.0-dev.12.20260901 已下载,可在侧栏选择重启安装。',
76+
);
77+
});

‎apps/desktop/src/main/__tests__/app-update-attestation.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { tmpdir } from 'node:os';
2727
import { join } from 'node:path';
2828
import { test } from 'node:test';
2929
import {
30+
desktopDiagnosticUpdateChannel,
3031
desktopUpdateChannelFromManifest,
3132
verifyDownloadedUpdateAttestation,
3233
} from '../app-update-attestation.js';
@@ -294,6 +295,34 @@ test('packaged update trust accepts only an explicit release or nightly channel'
294295
);
295296
});
296297

298+
test('a diagnostic report names the channel it can prove, and admits when it cannot', async () => {
299+
const appPath = await mkdtemp(join(tmpdir(), 'maka-channel-'));
300+
const channelOf = (isPackaged: boolean) => desktopDiagnosticUpdateChannel({ isPackaged, appPath });
301+
try {
302+
// Packaged: the manifest electron-builder stamped is the authority, and the
303+
// two feeds must come back distinct — they carry different signers.
304+
await writeFile(join(appPath, 'package.json'), JSON.stringify({ makaUpdateChannel: 'nightly' }));
305+
assert.equal(channelOf(true), 'nightly');
306+
await writeFile(join(appPath, 'package.json'), JSON.stringify({ makaUpdateChannel: 'release' }));
307+
assert.equal(channelOf(true), 'release');
308+
309+
// A checkout follows no feed, and never reads the manifest: the `release`
310+
// value the updater falls back to is a placeholder, not a fact.
311+
assert.equal(channelOf(false), 'dev');
312+
313+
// Unlike the strict parser, this one must not throw — the report has to
314+
// copy even when the manifest is the thing that is broken.
315+
await writeFile(join(appPath, 'package.json'), '{ not json');
316+
assert.equal(channelOf(true), 'unknown');
317+
await writeFile(join(appPath, 'package.json'), JSON.stringify({ makaUpdateChannel: 'preview' }));
318+
assert.equal(channelOf(true), 'unknown');
319+
await rm(join(appPath, 'package.json'));
320+
assert.equal(channelOf(true), 'unknown');
321+
} finally {
322+
await rm(appPath, { recursive: true, force: true });
323+
}
324+
});
325+
297326
test('TUF verifies ECDSA without breaking Ed25519 in the packaged Electron runtime', () => {
298327
assertElectronVerification(
299328
String.raw`

‎apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ import {
3232
MAIN_PROCESS_DIAGNOSTIC_LOG_MAX_BYTES,
3333
mainProcessLogBuffer,
3434
parseDesktopDiagnosticInput,
35+
type DesktopDiagnosticChannel,
3536
} from '../main-process-diagnostics.js';
3637

3738
const environment = {
3839
appVersion: '0.1.8',
3940
buildMode: 'dev' as const,
41+
updateChannel: 'dev' as const,
4042
buildCommit: 'a'.repeat(40),
4143
electronVersion: '38.0.0',
4244
nodeVersion: '22.0.0',
@@ -107,6 +109,25 @@ test('formats one redacted Desktop and Runtime Host diagnostic report', () => {
107109
assert.doesNotMatch(report, /sk-secretvalue123|\/home\/tester/);
108110
});
109111

112+
test('names the update channel, which buildMode alone cannot distinguish', () => {
113+
const reportFor = (channel: DesktopDiagnosticChannel, buildMode: 'dev' | 'packaged') =>
114+
formatDesktopDiagnosticReport(
115+
{ surface: 'manual', hostTarget: 'default' },
116+
{ ...environment, buildMode, updateChannel: channel },
117+
[],
118+
{ ok: false, error: 'not started' },
119+
undefined,
120+
new Date('2026-08-09T00:00:00Z'),
121+
);
122+
123+
// Both packaged installs report `Build: packaged`; only the channel line
124+
// says which feed and which attestation signer they trust.
125+
assert.match(reportFor('nightly', 'packaged'), /^Channel: nightly$/mu);
126+
assert.match(reportFor('release', 'packaged'), /^Channel: release$/mu);
127+
// A checkout follows no feed, so it is neither of them.
128+
assert.match(reportFor('dev', 'dev'), /^Channel: dev$/mu);
129+
});
130+
110131
test('bounds renderer diagnostic text and rejects unknown fields', () => {
111132
const input = parseDesktopDiagnosticInput({
112133
surface: 'toast',

‎apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
const diagnosticEnvironment = () => ({
3737
appVersion: '0.1.8',
3838
buildMode: 'packaged' as const,
39+
updateChannel: 'release' as const,
3940
buildCommit: null,
4041
electronVersion: '38.0.0',
4142
nodeVersion: '22.0.0',

‎apps/desktop/src/main/app-ipc-main.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type { ProjectRootController } from './project-root-controller.js';
2626
import { resolveOpenPath, type OpenPathResult } from './open-path-guard.js';
2727
import { getE2eFixtureState, type resolveE2eFixture } from './e2e-fixture.js';
2828
import type { resolveBuildInfo } from './build-info.js';
29+
import type { DesktopUpdateChannel } from './app-update-attestation.js';
2930
import type {
3031
AppUpdateInstallRequest,
3132
AppUpdateService,
@@ -47,6 +48,7 @@ export interface AppIpcDeps {
4748
getProjectRoot(sessionId: unknown): Promise<string>;
4849
workspaceRoot: string;
4950
buildInfo: BuildInfo;
51+
updateChannel: DesktopUpdateChannel;
5052
e2eFixture: E2eFixture;
5153
projectManagement: ProjectManagementService;
5254
allowLocalProjectPaths?: boolean;
@@ -89,7 +91,7 @@ export function registerAppIpc(
8991
deps: AppIpcDeps,
9092
targetIpc: ReconnectableReadIpcMain = ipcMain,
9193
): void {
92-
const { projectRoot, workspaceRoot, buildInfo, e2eFixture } = deps;
94+
const { projectRoot, workspaceRoot, buildInfo, updateChannel, e2eFixture } = deps;
9395
const allowLocalProjectPaths = deps.allowLocalProjectPaths !== false;
9496
// Call-time read of the shared project-root authority: every handler must
9597
// observe the latest selection, not a snapshot taken at registration.
@@ -120,6 +122,7 @@ export function registerAppIpc(
120122
: { isGitRepo: false },
121123
buildMode: buildInfo.mode,
122124
buildCommit: buildInfo.commit,
125+
updateChannel,
123126
};
124127
});
125128
handleReconnectableRead(targetIpc, 'projects:getSnapshot', () =>

‎apps/desktop/src/main/app-update-attestation.ts‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import { bundleFromJSON, type Bundle } from '@sigstore/bundle';
2121
import { getTrustedRoot } from '@sigstore/tuf';
2222
import { toSignedEntity, toTrustMaterial, Verifier } from '@sigstore/verify';
2323
import { createHash } from 'node:crypto';
24-
import { createReadStream } from 'node:fs';
25-
import { basename } from 'node:path';
24+
import { createReadStream, readFileSync } from 'node:fs';
25+
import { basename, join } from 'node:path';
2626

2727
const PRODUCT_REPOSITORY = 'apache/maka';
2828
const PRODUCT_RELEASE_WORKFLOW = '.github/workflows/release-cli-finalize.yml';
@@ -80,6 +80,29 @@ export function desktopUpdateChannelFromManifest(manifest: unknown): DesktopUpda
8080
return channel;
8181
}
8282

83+
/**
84+
* The channel a diagnostic report names for this binary.
85+
*
86+
* Total by construction, unlike `desktopUpdateChannelFromManifest`: a report
87+
* must still copy when the manifest is the very thing that is broken, and the
88+
* report saying `unknown` is more useful than the copy failing. A checkout
89+
* follows no feed at all, so it reports `dev` rather than the updater's
90+
* `release` placeholder.
91+
*/
92+
export function desktopDiagnosticUpdateChannel(input: {
93+
readonly isPackaged: boolean;
94+
readonly appPath: string;
95+
}): DesktopUpdateChannel | 'dev' | 'unknown' {
96+
if (!input.isPackaged) return 'dev';
97+
try {
98+
return desktopUpdateChannelFromManifest(
99+
JSON.parse(readFileSync(join(input.appPath, 'package.json'), 'utf8')),
100+
);
101+
} catch {
102+
return 'unknown';
103+
}
104+
}
105+
83106
function productWorkflowSigner(channel: DesktopUpdateChannel): RegExp {
84107
const workflow = channel === 'nightly' ? PRODUCT_NIGHTLY_WORKFLOW : PRODUCT_RELEASE_WORKFLOW;
85108
return new RegExp(

‎apps/desktop/src/main/main-process-diagnostics.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,18 @@ const INPUT_TRUNCATION_MARKER = '\n<diagnostic input truncated>';
4444
const EXECUTION_DIAGNOSTIC_TIMEOUT_MS = 2_000;
4545
export const MAIN_PROCESS_DIAGNOSTIC_LOG_MAX_BYTES = 256 * 1024;
4646

47+
/**
48+
* The release feed a build follows, as a report names it. `buildMode` alone
49+
* cannot say this: every packaged install reports `packaged`, so a nightly and
50+
* a release looked identical in a report while following different feeds and
51+
* different attestation signers. `dev` is not a feed — a checkout follows none.
52+
*/
53+
export type DesktopDiagnosticChannel = 'release' | 'nightly' | 'dev' | 'unknown';
54+
4755
export interface DesktopDiagnosticEnvironment {
4856
readonly appVersion: string;
4957
readonly buildMode: 'dev' | 'packaged';
58+
readonly updateChannel: DesktopDiagnosticChannel;
5059
readonly buildCommit: string | null;
5160
readonly electronVersion: string;
5261
readonly nodeVersion: string;
@@ -63,6 +72,7 @@ export interface DesktopDiagnosticEnvironment {
6372
export interface DesktopDiagnosticEnvironmentSource {
6473
readonly appVersion: string;
6574
readonly buildMode: 'dev' | 'packaged';
75+
readonly updateChannel: DesktopDiagnosticChannel;
6676
readonly buildCommit: string | null;
6777
readonly locale: string;
6878
readonly workspacePath: string;
@@ -431,6 +441,7 @@ export function formatDesktopDiagnosticReport(
431441
'Environment',
432442
`Maka: ${environment.appVersion}`,
433443
`Build: ${environment.buildMode}${environment.buildCommit ? ` @ ${environment.buildCommit.slice(0, 12)}` : ''}`,
444+
`Channel: ${environment.updateChannel}`,
434445
`Electron: ${environment.electronVersion}`,
435446
`Chrome: ${environment.chromeVersion}`,
436447
`Node: ${environment.nodeVersion}`,

0 commit comments

Comments
 (0)