@nocobase/app-template-default 1.0.0-beta.84 → 1.0.0-beta.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/AGENTS.md +1 -1
  2. package/client/components/data-table/data-table.tsx +86 -32
  3. package/client/components/list-feedback.tsx +174 -0
  4. package/client/components/permission-editor.tsx +40 -33
  5. package/client/extensions/nocobase-workflow-management/README.md +3 -1
  6. package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -7
  7. package/client/extensions/nocobase-workflow-management/execution-list.tsx +61 -48
  8. package/client/hooks/use-list-data.ts +1 -2
  9. package/client/locales/system/en-US.ts +14 -1
  10. package/client/locales/system/settings-en-US.ts +0 -10
  11. package/client/locales/system/settings-zh-CN.ts +0 -8
  12. package/client/locales/system/zh-CN.ts +14 -1
  13. package/client/pages/settings/default-access.tsx +50 -72
  14. package/client/pages/settings/permissions-model.ts +0 -1
  15. package/client/pages/settings/roles.tsx +63 -97
  16. package/client/pages/settings/users.tsx +72 -70
  17. package/client/runtime.ts +0 -4
  18. package/config.example.yml +24 -0
  19. package/package.json +33 -31
  20. package/server/app.ts +0 -2
  21. package/server/config/auth.ts +2 -0
  22. package/server/config/concurrency.ts +3 -0
  23. package/server/config/index.ts +3 -0
  24. package/server/config/jobs.ts +1 -13
  25. package/server/config/notification.ts +1 -1
  26. package/server/config/queue.ts +1 -7
  27. package/server/plugins.ts +2 -0
  28. package/tests/client/data-table.test.tsx +9 -4
  29. package/tests/client/default-access.test.tsx +43 -5
  30. package/tests/client/list-feedback.test.tsx +301 -0
  31. package/tests/client/manual-list-retry.test.tsx +190 -0
  32. package/tests/client/permission-workspace.test.ts +13 -0
  33. package/tests/client/settings-continuity.test.tsx +80 -1
  34. package/tests/client/settings-feedback.test.tsx +7 -15
  35. package/tests/fixtures/permission-alignment.html +16 -0
  36. package/tests/fixtures/permission-alignment.tsx +124 -0
  37. package/tests/playwright/permission-alignment.test.ts +109 -0
  38. package/tests/server/auth-email-config.test.ts +79 -0
  39. package/tests/server/concurrency.test.ts +125 -0
  40. package/tests/server/config.test.ts +34 -14
  41. package/client/route-overrides.ts +0 -9
  42. package/client/source-extensions.ts +0 -19
@@ -0,0 +1,124 @@
1
+ import { useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { PermissionEditor } from '../../client/components/permission-editor';
4
+ import { Button } from '../../client/components/ui/button';
5
+ import { AppThemeProvider } from '../../client/theme';
6
+ import '../../client/styles.css';
7
+
8
+ // A browser fixture for real editor geometry; no server permissions are changed.
9
+ const fields = [
10
+ 'Alerts',
11
+ 'Monitoring rules',
12
+ 'Business processes',
13
+ 'Rule calculations with a deliberately long English label',
14
+ 'Process nodes',
15
+ ];
16
+ const levels = [
17
+ { value: '', label: 'No additional record access' },
18
+ { value: 'all', label: 'All records' },
19
+ { value: 'custom', label: 'Custom conditions' },
20
+ ];
21
+
22
+ export default function Fixture() {
23
+ const params = new URLSearchParams(location.search);
24
+ const readOnly = params.has('readonly');
25
+ const chinese = params.has('chinese');
26
+ const [enabled, setEnabled] = useState<Record<string, boolean>>({});
27
+ const [values, setValues] = useState<Record<string, string>>({});
28
+ const [condition, setCondition] = useState(false);
29
+ return (
30
+ <AppThemeProvider>
31
+ <main className='mx-auto grid max-w-6xl gap-4 p-4'>
32
+ <h1 className='text-xl'>
33
+ Roles and permissions · Alignment verification
34
+ </h1>
35
+ <PermissionEditor
36
+ readOnly={readOnly}
37
+ sections={[
38
+ {
39
+ id: 'business',
40
+ title: 'Business operations',
41
+ groups: [
42
+ ...['Alerts', 'Monitoring rules', 'Responsible units'].map(
43
+ (group) => ({
44
+ id: group,
45
+ title: group,
46
+ rows: (group === 'Monitoring rules'
47
+ ? ['View', 'Configure', 'Run']
48
+ : ['Manage']
49
+ ).map((action) => {
50
+ const id = `${action} ${group.toLowerCase()}`;
51
+ return {
52
+ id,
53
+ label: id,
54
+ levels: [],
55
+ hint: 'Existing permission descriptions remain visible.',
56
+ enabled: enabled[id] ?? true,
57
+ scopes: (group === 'Responsible units'
58
+ ? ['Responsible units']
59
+ : group === 'Alerts'
60
+ ? fields.slice(0, 2)
61
+ : fields
62
+ ).map((label, index) => {
63
+ const scopeId = `${id}:${index}`;
64
+ return {
65
+ id: scopeId,
66
+ label:
67
+ chinese && index === 3
68
+ ? '需要完整显示名称的跨部门监控规则计算数据范围长标签'
69
+ : label,
70
+ levels,
71
+ level:
72
+ values[scopeId] ?? (index === 0 ? 'custom' : ''),
73
+ hint:
74
+ index % 2 === 0
75
+ ? 'Other roles and default rules still apply.'
76
+ : undefined,
77
+ configuration:
78
+ index === 0 ? (
79
+ <Button
80
+ size='sm'
81
+ variant='outline'
82
+ disabled={readOnly}
83
+ onClick={() =>
84
+ setCondition((value) => !value)
85
+ }
86
+ >
87
+ Conditions
88
+ </Button>
89
+ ) : undefined,
90
+ };
91
+ }),
92
+ };
93
+ }),
94
+ }),
95
+ ),
96
+ {
97
+ id: 'without-ranges',
98
+ title: 'Other operations',
99
+ rows: [
100
+ { id: 'create', label: 'Create alert', enabled: true },
101
+ { id: 'delete', label: 'Delete alert', enabled: false },
102
+ ],
103
+ },
104
+ ],
105
+ },
106
+ ]}
107
+ onEnabledChange={(id, value) =>
108
+ setEnabled((current) => ({ ...current, [id]: value }))
109
+ }
110
+ onLevelChange={(id, value) =>
111
+ setValues((current) => ({ ...current, [id]: value }))
112
+ }
113
+ />
114
+ <p role='status'>
115
+ {condition
116
+ ? 'Condition editor requested'
117
+ : 'No condition editor requested'}
118
+ </p>
119
+ </main>
120
+ </AppThemeProvider>
121
+ );
122
+ }
123
+
124
+ createRoot(document.getElementById('root')!).render(<Fixture />);
@@ -0,0 +1,109 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ const applicationUrl = process.env.APP_URL;
4
+ test.skip(!applicationUrl, 'Set APP_URL to the running application entry.');
5
+ const fixture = `${applicationUrl?.replace(/\/$/, '')}/tests/fixtures/permission-alignment.html`;
6
+
7
+ for (const width of [1440, 1024, 768, 390, 375, 360]) {
8
+ test(`aligns scopes across groups and actions at ${width}px`, async ({
9
+ page,
10
+ }) => {
11
+ await page.setViewportSize({ width, height: 1100 });
12
+ await page.goto(fixture);
13
+ await expect(page.getByRole('combobox')).toHaveCount(18);
14
+ const geometry = await Promise.all(
15
+ (await page.getByRole('combobox').all()).map(async (node) => {
16
+ const rect = (await node.boundingBox())!;
17
+ return { x: Math.round(rect.x), width: Math.round(rect.width) };
18
+ }),
19
+ );
20
+ expect(new Set(geometry.map((item) => item.x)).size).toBe(
21
+ width >= 1024 ? 2 : 1,
22
+ );
23
+ expect(new Set(geometry.map((item) => item.width)).size).toBe(1);
24
+ expect(
25
+ await page.locator('html').evaluate((element) => {
26
+ const root = element as unknown as {
27
+ scrollWidth: number;
28
+ clientWidth: number;
29
+ };
30
+ return root.scrollWidth <= root.clientWidth;
31
+ }),
32
+ ).toBe(true);
33
+ for (const row of await page.locator('[data-slot=permission-row]').all()) {
34
+ const checkbox = await row.getByRole('checkbox').boundingBox();
35
+ const controls = row.getByRole('combobox');
36
+ if (await controls.count()) {
37
+ const first = await controls.first().boundingBox();
38
+ expect(first!.y).toBeGreaterThan(checkbox!.y + checkbox!.height);
39
+ expect(Math.round(first!.x)).toBe(
40
+ Math.min(...geometry.map((item) => item.x)),
41
+ );
42
+ if ((await controls.count()) % 2 === 1) {
43
+ const last = await controls.last().boundingBox();
44
+ expect(Math.round(last!.x)).toBe(Math.round(first!.x));
45
+ }
46
+ }
47
+ }
48
+ for (const scope of await page
49
+ .locator('[data-slot=permission-scope]')
50
+ .all()) {
51
+ const trigger = await scope.getByRole('combobox').boundingBox();
52
+ if (width < 416) {
53
+ const label = await scope.locator('span[title]').boundingBox();
54
+ expect(label!.y + label!.height).toBeLessThanOrEqual(trigger!.y);
55
+ expect(Math.round(label!.x)).toBe(Math.round(trigger!.x));
56
+ }
57
+ const conditions = scope.getByRole('button', {
58
+ name: 'Conditions',
59
+ exact: true,
60
+ });
61
+ if (await conditions.count()) {
62
+ const button = await conditions.boundingBox();
63
+ expect(button!.y).toBeGreaterThanOrEqual(trigger!.y + trigger!.height);
64
+ expect(Math.round(button!.x)).toBe(Math.round(trigger!.x));
65
+ }
66
+ }
67
+ });
68
+ }
69
+
70
+ test('preserves long labels, independent scope changes and ungranted visibility', async ({
71
+ page,
72
+ }) => {
73
+ await page.goto(`${fixture}?chinese`);
74
+ const label = '需要完整显示名称的跨部门监控规则计算数据范围长标签';
75
+ await expect(page.getByTitle(label)).toHaveCount(3);
76
+ const control = page.getByRole('combobox', {
77
+ name: `How far View monitoring rules · ${label} reaches`,
78
+ });
79
+ await control.click();
80
+ await page.getByRole('option', { name: 'All records', exact: true }).click();
81
+ await expect(control.locator('[data-slot=select-value]')).toHaveText(
82
+ 'All records',
83
+ );
84
+ await expect(
85
+ page.getByRole('checkbox', { name: 'View monitoring rules', exact: true }),
86
+ ).toBeChecked();
87
+ await page
88
+ .getByRole('checkbox', { name: 'Manage responsible units', exact: true })
89
+ .click();
90
+ await expect(page.getByRole('combobox')).toHaveCount(17);
91
+ await page.getByRole('tab', { name: 'Ungranted', exact: true }).click();
92
+ await expect(page.getByRole('combobox')).toHaveCount(0);
93
+ });
94
+
95
+ test('keeps readonly scopes visible and disables editing', async ({ page }) => {
96
+ await page.goto(`${fixture}?readonly`);
97
+ await expect(page.getByRole('combobox')).toHaveCount(18);
98
+ for (const control of await page.getByRole('combobox').all()) {
99
+ await expect(control).toBeDisabled();
100
+ }
101
+ for (const button of await page
102
+ .getByRole('button', { name: 'Conditions', exact: true })
103
+ .all()) {
104
+ await expect(button).toBeDisabled();
105
+ }
106
+ await expect(
107
+ page.getByRole('checkbox', { name: 'Manage alerts', exact: true }),
108
+ ).toHaveAttribute('aria-disabled', 'true');
109
+ });
@@ -0,0 +1,79 @@
1
+ // @vitest-environment node
2
+
3
+ import { readFile, mkdtemp, writeFile, rm } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { expect, it } from 'vitest';
7
+ import {
8
+ AppConfig,
9
+ defaultAppConfigs,
10
+ buildVariablesManifest,
11
+ } from '@nocobase/app-server/config';
12
+ import auth from '../../server/config/auth.js';
13
+
14
+ it('inherits private authentication sender mappings without changing other mail consumers', async () => {
15
+ const defaults = defaultAppConfigs({ auth });
16
+ const config = new AppConfig();
17
+ await config.loadAll();
18
+ config.mergeDefaults(defaults({} as never));
19
+ config.defineSections(defaults.sections!);
20
+ await config.loadSectionEnvironment({
21
+ AUTH_EMAIL_PROVIDER: 'smtp',
22
+ AUTH_EMAIL_HOST: 'smtp.example.test',
23
+ AUTH_EMAIL_PORT: '587',
24
+ AUTH_EMAIL_SECURE: 'false',
25
+ AUTH_EMAIL_FROM: 'sender@example.test',
26
+ AUTH_EMAIL_PASSWORD: 'synthetic-password',
27
+ AUTH_EMAIL_USER: 'synthetic',
28
+ MAIL_HOST: 'unrelated.example.test',
29
+ });
30
+ expect(await config.validate()).toEqual([]);
31
+ expect(config.get('auth.email.host')).toBe('smtp.example.test');
32
+ expect(config.get('auth.email.port')).toBe(587);
33
+ expect(config.get('auth.email.secure')).toBe(false);
34
+ expect(JSON.stringify(config.publicValues())).not.toContain(
35
+ 'synthetic-password',
36
+ );
37
+ expect(config.get('auth.passwordReset.enabled')).toBeUndefined();
38
+ const variables = buildVariablesManifest({
39
+ app: { name: 'synthetic' },
40
+ variables: config.environmentVariableMappings(),
41
+ defaults: config.layers().defaults,
42
+ }).variables;
43
+ expect(
44
+ variables.filter((variable) => variable.name.startsWith('AUTH_EMAIL_')),
45
+ ).toHaveLength(9);
46
+ expect(
47
+ variables
48
+ .filter((variable) => variable.name.startsWith('AUTH_EMAIL_'))
49
+ .every((variable) => variable.required === false),
50
+ ).toBe(true);
51
+ });
52
+
53
+ it('ships separate, valid SMTP and Resend authentication examples', async () => {
54
+ const example = await readFile(
55
+ new URL('../../config.example.yml', import.meta.url),
56
+ 'utf8',
57
+ );
58
+ const blocks = [...example.matchAll(/^ {2}# email:\n(?: {2}# {3}.*\n)+/gm)];
59
+ expect(blocks).toHaveLength(2);
60
+ const directory = await mkdtemp(
61
+ path.join(os.tmpdir(), 'auth-email-example-'),
62
+ );
63
+ try {
64
+ for (const block of blocks) {
65
+ const text = 'auth:\n' + block[0].replace(/^ {2}# /gm, ' ');
66
+ const file = path.join(directory, 'config.yml');
67
+ await writeFile(file, text);
68
+ const config = new AppConfig().loadFile(file);
69
+ await config.loadAll();
70
+ const defaults = defaultAppConfigs({ auth });
71
+ config.mergeDefaults(defaults({} as never));
72
+ config.defineSections(defaults.sections!);
73
+ expect(await config.validate()).toEqual([]);
74
+ }
75
+ } finally {
76
+ await rm(directory, { recursive: true, force: true });
77
+ }
78
+ expect(example).not.toContain('emailChannel:');
79
+ });
@@ -0,0 +1,125 @@
1
+ import { expect, it, vi } from 'vitest';
2
+ import { ConcurrentAbortError } from '@nocobase/concurrency';
3
+ import { createAppTest } from '@nocobase/app-testing/server';
4
+ import {
5
+ concurrencyServiceToken,
6
+ type ConcurrencyService,
7
+ } from '@nocobase/app-plugin-concurrency/server';
8
+ import { createStandaloneServer } from '../../server/standalone.js';
9
+ import concurrency from '../../server/config/concurrency.js';
10
+
11
+ class BusinessConsumer {
12
+ public constructor(private readonly service: ConcurrencyService) {}
13
+ public manager() {
14
+ return this.service.getManager('memory');
15
+ }
16
+ }
17
+
18
+ const test = createAppTest({
19
+ createServer: createStandaloneServer,
20
+ connections: ['main'],
21
+ config: {
22
+ app: { publicOrigin: 'http://localhost' },
23
+ auth: { secret: 'test-auth-secret-at-least-32-characters' },
24
+ },
25
+ });
26
+
27
+ test('explicit ttl keeps local Lock and Executor mutually exclusive until actual release', async ({
28
+ app,
29
+ }) => {
30
+ const manager = await app.container
31
+ .resolve(concurrencyServiceToken)
32
+ .getManager();
33
+ const lock = await manager.createLock('concurrency-test:ttl', { ttl: 1 });
34
+ const executor = await manager.createSynchronous('concurrency-test:ttl', {
35
+ ttl: 2,
36
+ });
37
+ vi.useFakeTimers();
38
+ try {
39
+ const held = await lock.lock();
40
+ let entered = false;
41
+ const done = Promise.withResolvers<string>();
42
+ const running = executor.run(() => {
43
+ entered = true;
44
+ return done.promise;
45
+ });
46
+ try {
47
+ await vi.advanceTimersByTimeAsync(1000);
48
+ expect(entered).toBe(false);
49
+ await held.release();
50
+ await vi.advanceTimersByTimeAsync(0);
51
+ expect(entered).toBe(true);
52
+ let successorEntered = false;
53
+ const next = lock.lock().then((lease) => {
54
+ successorEntered = true;
55
+ return lease;
56
+ });
57
+ await vi.advanceTimersByTimeAsync(1000);
58
+ expect(successorEntered).toBe(false);
59
+ done.resolve('actual');
60
+ expect(await running).toBe('actual');
61
+ const successor = await next;
62
+ await held.release();
63
+ const attempt = executor.tryRun(() => 'wrong', 10);
64
+ await vi.advanceTimersByTimeAsync(10);
65
+ expect(await attempt).toEqual({ acquired: false });
66
+ await successor.release();
67
+ } finally {
68
+ done.resolve('actual');
69
+ await running;
70
+ await held.release();
71
+ }
72
+ } finally {
73
+ vi.useRealTimers();
74
+ }
75
+ });
76
+ test('the real default composition supplies one memory lock domain to two business consumers', async ({
77
+ app,
78
+ }) => {
79
+ const service = app.container.resolve(concurrencyServiceToken);
80
+ const first = new BusinessConsumer(service);
81
+ const second = new BusinessConsumer(service);
82
+ expect(app.config.get('concurrency')).toEqual({
83
+ default: 'memory',
84
+ memory: { adapter: 'mutex' },
85
+ });
86
+ const [left, right] = await Promise.all([first.manager(), second.manager()]);
87
+ expect(left).toBe(right);
88
+ expect(left).toBe(await service.getManager());
89
+ const lock = await left.createLock('concurrency-test:business');
90
+ const executor = await right.createSynchronous('concurrency-test:business');
91
+ const lease = await lock.lock();
92
+ try {
93
+ expect(await executor.tryRun(() => 'should-not-run')).toEqual({
94
+ acquired: false,
95
+ });
96
+ } finally {
97
+ await lease.release();
98
+ }
99
+ expect(await executor.tryRun(() => 'done')).toEqual({
100
+ acquired: true,
101
+ value: 'done',
102
+ });
103
+ const cancellation = new AbortController();
104
+ const held = await lock.lock();
105
+ const cancelled = lock
106
+ .lock({ signal: cancellation.signal })
107
+ .catch((error: unknown) => error);
108
+ cancellation.abort();
109
+ expect(await cancelled).toBeInstanceOf(ConcurrentAbortError);
110
+ await held.release();
111
+ const active = await lock.lock();
112
+ const interrupted = lock.lock().catch((error: unknown) => error);
113
+ await app.shutdown();
114
+ expect(await interrupted).toBeInstanceOf(ConcurrentAbortError);
115
+ await active.release();
116
+ await expect(lock.lock()).rejects.toBeInstanceOf(ConcurrentAbortError);
117
+ });
118
+
119
+ it('declares only the default selector environment variable and no public configuration', () => {
120
+ expect(concurrency.rules?.public).toEqual([]);
121
+ expect(Object.keys(concurrency.rules?.env ?? {})).toEqual([
122
+ 'CONCURRENCY_DEFAULT',
123
+ ]);
124
+ expect(concurrency.rules?.validators).toHaveLength(1);
125
+ });
@@ -1,5 +1,5 @@
1
1
  import { fileURLToPath } from 'node:url';
2
- import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
 
@@ -33,6 +33,37 @@ describe('application config', () => {
33
33
  await rm(configRoot, { recursive: true, force: true });
34
34
  });
35
35
 
36
+ it('merges the Redis YAML example with the real template defaults', async () => {
37
+ const source = await readFile(
38
+ new URL('../../config.example.yml', import.meta.url),
39
+ 'utf8',
40
+ );
41
+ const example = source.match(/^# jobs:\n(?:#(?: {2,}.*)?\n)*/m)?.[0];
42
+ expect(example).toBeDefined();
43
+ const examplePath = path.join(configRoot, 'redis-example.yml');
44
+ await writeFile(
45
+ examplePath,
46
+ source.replace(example!, example!.replace(/^# ?/gm, '')),
47
+ );
48
+ const runtime = await resolveStandaloneAppRuntime(appRuntime, {
49
+ rootDir: templateRootDir,
50
+ configPath: examplePath,
51
+ env: { AUTH_SECRET: 'test-auth-secret-at-least-32-characters' },
52
+ });
53
+
54
+ expect(runtime.config.get<AppJobsConfig>('jobs')).toEqual({
55
+ memory: {
56
+ adapter: 'memory',
57
+ persistence: { path: runtime.paths.storage('jobs') },
58
+ },
59
+ default: 'redis',
60
+ redis: {
61
+ adapter: 'redis',
62
+ connection: { host: '127.0.0.1', port: 6379 },
63
+ },
64
+ });
65
+ });
66
+
36
67
  it('assembles module defaults in the runtime', async () => {
37
68
  const runtime = await resolveStandaloneAppRuntime(appRuntime, {
38
69
  rootDir: templateRootDir,
@@ -76,24 +107,12 @@ describe('application config', () => {
76
107
  adapter: 'inMemory',
77
108
  persistence: { path: runtime.paths.storage('queue') },
78
109
  },
79
- redis: {
80
- adapter: 'redis',
81
- connection: { host: '127.0.0.1', port: 6379, db: 0 },
82
- removeOnComplete: { count: 1000 },
83
- removeOnFail: { age: 604_800 },
84
- },
85
110
  });
86
111
  expect(runtime.config.get<AppJobsConfig>('jobs')).toEqual({
87
112
  memory: {
88
113
  adapter: 'memory',
89
114
  persistence: { path: runtime.paths.storage('jobs') },
90
115
  },
91
- redis: {
92
- adapter: 'redis',
93
- connection: { host: '127.0.0.1', port: 6379, db: 0 },
94
- removeOnComplete: { count: 1000 },
95
- removeOnFail: { age: 604_800 },
96
- },
97
116
  });
98
117
  expect(runtime.config.get<AppSessionConfigInput>('session')!.default).toBe(
99
118
  'memory',
@@ -183,7 +202,8 @@ describe('application config', () => {
183
202
  });
184
203
  expect(runtime.config.get('server.port')).toBe(14001);
185
204
  expect(runtime.config.get('server.startLog')).toBe(false);
186
- expect(runtime.config.get('queue.redis.connection.host')).toBe('127.0.0.1');
205
+ expect(runtime.config.get('queue.redis')).toBeUndefined();
206
+ expect(runtime.config.get('queue.memory.adapter')).toBe('inMemory');
187
207
  expect(runtime.config.get('session.stores.redis.host')).toBe('127.0.0.1');
188
208
  expect(runtime.config.get('logging.console.pretty')).toBe(false);
189
209
  expect(runtime.config.get('session.cookie.secure')).toBe(true);
@@ -1,9 +0,0 @@
1
- import {
2
- defineClientRouteComponentOverrides,
3
- type AppClientRouteComponentOverrideDefinition,
4
- } from '@nocobase/app-client/plugins';
5
-
6
- export const routeComponentOverrides: readonly AppClientRouteComponentOverrideDefinition[] =
7
- defineClientRouteComponentOverrides([]);
8
-
9
- export default routeComponentOverrides;
@@ -1,19 +0,0 @@
1
- import type { AppClientSourceExtension } from '@nocobase/app-client/plugins';
2
-
3
- interface AppClientSourceExtensionModule {
4
- readonly default: AppClientSourceExtension;
5
- }
6
-
7
- const extensionModules = import.meta.glob<AppClientSourceExtensionModule>(
8
- './extensions/*/extension.ts',
9
- { eager: true },
10
- );
11
-
12
- export const sourceExtensions: readonly AppClientSourceExtension[] =
13
- Object.freeze(
14
- Object.entries(extensionModules)
15
- .sort(([left], [right]) => left.localeCompare(right))
16
- .map(([, module]) => module.default),
17
- );
18
-
19
- export default sourceExtensions;