@nocobase/app-template-default 1.0.0-beta.84 → 1.0.0-beta.85
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.
- package/AGENTS.md +1 -1
- package/client/components/data-table/data-table.tsx +86 -32
- package/client/components/list-feedback.tsx +174 -0
- package/client/components/permission-editor.tsx +40 -33
- package/client/extensions/nocobase-workflow-management/README.md +3 -1
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -7
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +61 -48
- package/client/hooks/use-list-data.ts +1 -2
- package/client/locales/system/en-US.ts +14 -1
- package/client/locales/system/settings-en-US.ts +0 -10
- package/client/locales/system/settings-zh-CN.ts +0 -8
- package/client/locales/system/zh-CN.ts +14 -1
- package/client/pages/settings/default-access.tsx +50 -72
- package/client/pages/settings/permissions-model.ts +0 -1
- package/client/pages/settings/roles.tsx +63 -97
- package/client/pages/settings/users.tsx +72 -70
- package/client/runtime.ts +0 -4
- package/config.example.yml +24 -0
- package/package.json +19 -17
- package/server/app.ts +0 -2
- package/server/config/auth.ts +2 -0
- package/server/config/concurrency.ts +3 -0
- package/server/config/index.ts +3 -0
- package/server/config/jobs.ts +1 -13
- package/server/config/notification.ts +1 -1
- package/server/config/queue.ts +1 -7
- package/server/plugins.ts +2 -0
- package/tests/client/data-table.test.tsx +9 -4
- package/tests/client/default-access.test.tsx +43 -5
- package/tests/client/list-feedback.test.tsx +301 -0
- package/tests/client/manual-list-retry.test.tsx +190 -0
- package/tests/client/permission-workspace.test.ts +13 -0
- package/tests/client/settings-continuity.test.tsx +80 -1
- package/tests/client/settings-feedback.test.tsx +7 -15
- package/tests/fixtures/permission-alignment.html +16 -0
- package/tests/fixtures/permission-alignment.tsx +124 -0
- package/tests/playwright/permission-alignment.test.ts +109 -0
- package/tests/server/auth-email-config.test.ts +79 -0
- package/tests/server/concurrency.test.ts +61 -0
- package/tests/server/config.test.ts +34 -14
- package/client/route-overrides.ts +0 -9
- 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,61 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { createAppTest } from '@nocobase/app-testing/server';
|
|
3
|
+
import {
|
|
4
|
+
concurrencyServiceToken,
|
|
5
|
+
type ConcurrencyService,
|
|
6
|
+
} from '@nocobase/app-plugin-concurrency/server';
|
|
7
|
+
import { createStandaloneServer } from '../../server/standalone.js';
|
|
8
|
+
import concurrency from '../../server/config/concurrency.js';
|
|
9
|
+
|
|
10
|
+
class BusinessConsumer {
|
|
11
|
+
public constructor(private readonly service: ConcurrencyService) {}
|
|
12
|
+
public manager() {
|
|
13
|
+
return this.service.getManager('memory');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const test = createAppTest({
|
|
18
|
+
createServer: createStandaloneServer,
|
|
19
|
+
connections: ['main'],
|
|
20
|
+
config: {
|
|
21
|
+
app: { publicOrigin: 'http://localhost' },
|
|
22
|
+
auth: { secret: 'test-auth-secret-at-least-32-characters' },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('the real default composition supplies one memory lock domain to two business consumers', async ({
|
|
27
|
+
app,
|
|
28
|
+
}) => {
|
|
29
|
+
const service = app.container.resolve(concurrencyServiceToken);
|
|
30
|
+
const first = new BusinessConsumer(service);
|
|
31
|
+
const second = new BusinessConsumer(service);
|
|
32
|
+
expect(app.config.get('concurrency')).toEqual({
|
|
33
|
+
default: 'memory',
|
|
34
|
+
memory: { adapter: 'mutex' },
|
|
35
|
+
});
|
|
36
|
+
const [left, right] = await Promise.all([first.manager(), second.manager()]);
|
|
37
|
+
expect(left).toBe(right);
|
|
38
|
+
expect(left).toBe(await service.getManager());
|
|
39
|
+
const lock = await left.createLock('concurrency-test:business');
|
|
40
|
+
const executor = await right.createSynchronous('concurrency-test:business');
|
|
41
|
+
const lease = await lock.lock();
|
|
42
|
+
try {
|
|
43
|
+
expect(await executor.tryRun(() => 'should-not-run')).toEqual({
|
|
44
|
+
acquired: false,
|
|
45
|
+
});
|
|
46
|
+
} finally {
|
|
47
|
+
await lease.release();
|
|
48
|
+
}
|
|
49
|
+
expect(await executor.tryRun(() => 'done')).toEqual({
|
|
50
|
+
acquired: true,
|
|
51
|
+
value: 'done',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('declares only the default selector environment variable and no public configuration', () => {
|
|
56
|
+
expect(concurrency.rules?.public).toEqual([]);
|
|
57
|
+
expect(Object.keys(concurrency.rules?.env ?? {})).toEqual([
|
|
58
|
+
'CONCURRENCY_DEFAULT',
|
|
59
|
+
]);
|
|
60
|
+
expect(concurrency.rules?.validators).toHaveLength(1);
|
|
61
|
+
});
|
|
@@ -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
|
|
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;
|