@nocobase/app-template-default 1.0.0-beta.81 → 1.0.0-beta.82
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/rules/shell.md +2 -0
- package/.agents/rules/workspace.md +9 -0
- package/.prettierignore +3 -0
- package/AGENTS.md +2 -1
- package/README.MD +4 -0
- package/client/components/attachment-list.tsx +589 -0
- package/client/components/back-button.tsx +15 -4
- package/client/components/breadcrumbs.tsx +12 -1
- package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +68 -71
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +26 -12
- package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +6 -6
- package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +3 -3
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +19 -19
- package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +27 -19
- package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +12 -8
- package/client/extensions/nocobase-file-component-ui/index.ts +6 -6
- package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +14 -11
- package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +49 -0
- package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +47 -0
- package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +48 -0
- package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +49 -0
- package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +46 -0
- package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +51 -0
- package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +49 -0
- package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +47 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +43 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +43 -0
- package/client/extensions/nocobase-file-component-ui/types.ts +3 -1
- package/client/locales/en-US.ts +2 -0
- package/client/locales/zh-CN.ts +2 -0
- package/client/pages/settings/role-form.tsx +5 -1
- package/client/routing/route-context.tsx +33 -1
- package/eslint.config.js +2 -0
- package/package.json +19 -17
- package/scripts/check-packages.mjs +46 -0
- package/tailwind.config.mjs +3 -9
- package/tests/client/list-return-state.test.tsx +202 -0
- package/tests/client/role-return.test.tsx +104 -0
- package/tests/project/internal-packages.test.ts +98 -0
- package/tests/project/tailwind-sources.test.ts +2 -2
- package/client/extensions/nocobase-file-component-ui/README.md +0 -13
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { answerApi, renderWithApp } from '@nocobase/app-testing/client';
|
|
2
|
+
import { apiClientToken, type ClientApplication } from '@nocobase/app-client';
|
|
3
|
+
import {
|
|
4
|
+
AuthorizationClient,
|
|
5
|
+
authorizationClientToken,
|
|
6
|
+
} from '@nocobase/app-plugin-authorization/client';
|
|
7
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { Outlet, Routes, Route, useLocation } from 'react-router';
|
|
10
|
+
import { expect, it, vi } from 'vitest';
|
|
11
|
+
import RoleForm from '../../client/pages/settings/role-form.js';
|
|
12
|
+
import enUS from '../../client/locales/en-US.js';
|
|
13
|
+
import zhCN from '../../client/locales/zh-CN.js';
|
|
14
|
+
|
|
15
|
+
const namespace = '@nocobase/role-return-test';
|
|
16
|
+
function Parent() {
|
|
17
|
+
return <Outlet context={{ reload: () => undefined }} />;
|
|
18
|
+
}
|
|
19
|
+
function Location() {
|
|
20
|
+
const { pathname, search } = useLocation();
|
|
21
|
+
return (
|
|
22
|
+
<output data-testid='location'>
|
|
23
|
+
{pathname}
|
|
24
|
+
{search}
|
|
25
|
+
</output>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
it.each(
|
|
29
|
+
['/roles', '/administration/roles'].flatMap((parent) =>
|
|
30
|
+
['back', 'cancel', 'save'].map((action) => ({ parent, action })),
|
|
31
|
+
),
|
|
32
|
+
)('returns to $parent on $action', async ({ parent, action }) => {
|
|
33
|
+
const calls = vi.fn(({ path }: { path: string }) => {
|
|
34
|
+
if (path === 'authorization/permissions')
|
|
35
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
36
|
+
if (path.endsWith('/options'))
|
|
37
|
+
return {
|
|
38
|
+
data: {
|
|
39
|
+
sections: [],
|
|
40
|
+
recordAccess: [],
|
|
41
|
+
subjectTypes: [],
|
|
42
|
+
collections: [],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
data: { key: 'sales', title: 'Sales', grants: [], revision: 'one' },
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
const result = await renderWithApp(
|
|
50
|
+
<>
|
|
51
|
+
<Routes>
|
|
52
|
+
<Route path={parent} element={<Parent />}>
|
|
53
|
+
<Route index element={<h1>Role list</h1>} />
|
|
54
|
+
<Route path=':key/edit' element={<RoleForm />} />
|
|
55
|
+
</Route>
|
|
56
|
+
</Routes>
|
|
57
|
+
<Location />
|
|
58
|
+
</>,
|
|
59
|
+
{
|
|
60
|
+
route: `${parent}/sales/edit?q=Sales&page=2`,
|
|
61
|
+
namespace,
|
|
62
|
+
namespaces: {
|
|
63
|
+
[namespace]: {
|
|
64
|
+
'en-US': async () => ({ default: enUS }),
|
|
65
|
+
'zh-CN': async () => ({ default: zhCN }),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
services: (app: ClientApplication) =>
|
|
69
|
+
app.container.instance(
|
|
70
|
+
authorizationClientToken,
|
|
71
|
+
new AuthorizationClient(app.container.resolve(apiClientToken)),
|
|
72
|
+
),
|
|
73
|
+
fetch: answerApi(calls),
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
expect(
|
|
77
|
+
await screen.findByRole('link', { name: enUS.settings.roles }),
|
|
78
|
+
).toHaveAttribute('href', `${parent}?q=Sales&page=2`);
|
|
79
|
+
if (action === 'back') {
|
|
80
|
+
await userEvent.click(
|
|
81
|
+
screen.getByRole('link', { name: enUS.settings.roles }),
|
|
82
|
+
);
|
|
83
|
+
} else if (action === 'save') {
|
|
84
|
+
const name = await screen.findByRole('textbox', { name: /Role name/ });
|
|
85
|
+
await userEvent.type(name, ' updated');
|
|
86
|
+
await userEvent.click(
|
|
87
|
+
screen.getByRole('button', { name: 'Save', exact: true }),
|
|
88
|
+
);
|
|
89
|
+
await waitFor(() =>
|
|
90
|
+
expect(calls).toHaveBeenCalledWith(
|
|
91
|
+
expect.objectContaining({ method: 'PATCH' }),
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
await userEvent.click(
|
|
96
|
+
await screen.findByRole('button', { name: 'Cancel' }),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
await screen.findByRole('heading', { name: 'Role list' });
|
|
100
|
+
expect(screen.getByTestId('location')).toHaveTextContent(
|
|
101
|
+
`${parent}?q=Sales&page=2`,
|
|
102
|
+
);
|
|
103
|
+
result.unmount();
|
|
104
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { expect, it } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import packageMetadata from '../../package.json' with { type: 'json' };
|
|
16
|
+
|
|
17
|
+
it('checks every internal package without selecting the root application and propagates a failed check', () => {
|
|
18
|
+
const root = mkdtempSync(path.join(os.tmpdir(), 'app-internal-checks-'));
|
|
19
|
+
try {
|
|
20
|
+
cpSync(
|
|
21
|
+
path.resolve(import.meta.dirname, '../../scripts'),
|
|
22
|
+
path.join(root, 'scripts'),
|
|
23
|
+
{ recursive: true },
|
|
24
|
+
);
|
|
25
|
+
writeFileSync(
|
|
26
|
+
path.join(root, 'pnpm-workspace.yaml'),
|
|
27
|
+
"packages: ['packages/*']\nverifyDepsBeforeRun: false\n",
|
|
28
|
+
);
|
|
29
|
+
writeFileSync(
|
|
30
|
+
path.join(root, 'package.json'),
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
name: 'fixture-app',
|
|
33
|
+
private: true,
|
|
34
|
+
scripts: {
|
|
35
|
+
'check:packages': packageMetadata.scripts['check:packages'],
|
|
36
|
+
check: 'node -e "process.exit(99)"',
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
for (const name of ['first', 'second']) {
|
|
41
|
+
const directory = path.join(root, 'packages', name);
|
|
42
|
+
mkdirSync(directory, { recursive: true });
|
|
43
|
+
writeFileSync(
|
|
44
|
+
path.join(directory, 'package.json'),
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
name: `fixture-${name}`,
|
|
47
|
+
private: true,
|
|
48
|
+
scripts: { check: 'node check.cjs' },
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
writeFileSync(
|
|
52
|
+
path.join(directory, 'check.cjs'),
|
|
53
|
+
"require('node:fs').writeFileSync('checked', 'yes');\n",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const run = () =>
|
|
57
|
+
spawnSync('pnpm', ['check:packages'], {
|
|
58
|
+
cwd: root,
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
timeout: 30_000,
|
|
61
|
+
});
|
|
62
|
+
expect(run().status).toBe(0);
|
|
63
|
+
for (const name of ['first', 'second']) {
|
|
64
|
+
expect(
|
|
65
|
+
readFileSync(path.join(root, 'packages', name, 'checked'), 'utf8'),
|
|
66
|
+
).toBe('yes');
|
|
67
|
+
}
|
|
68
|
+
writeFileSync(
|
|
69
|
+
path.join(root, 'packages/second/check.cjs'),
|
|
70
|
+
'process.exit(7);\n',
|
|
71
|
+
);
|
|
72
|
+
expect(run().status).not.toBe(0);
|
|
73
|
+
rmSync(path.join(root, 'packages/first/checked'));
|
|
74
|
+
writeFileSync(
|
|
75
|
+
path.join(root, 'packages/second/package.json'),
|
|
76
|
+
JSON.stringify({ name: 'fixture-second', scripts: { check: ' ' } }),
|
|
77
|
+
);
|
|
78
|
+
const missing = run();
|
|
79
|
+
expect(missing.status).not.toBe(0);
|
|
80
|
+
expect(missing.stderr).toContain('packages/second');
|
|
81
|
+
expect(existsSync(path.join(root, 'packages/first/checked'))).toBe(false);
|
|
82
|
+
writeFileSync(
|
|
83
|
+
path.join(root, 'packages/second/package.json'),
|
|
84
|
+
JSON.stringify({ name: 'fixture-second' }),
|
|
85
|
+
);
|
|
86
|
+
expect(run().status).not.toBe(0);
|
|
87
|
+
// pnpm workspace exclusions apply to both the preflight and the script run.
|
|
88
|
+
writeFileSync(
|
|
89
|
+
path.join(root, 'pnpm-workspace.yaml'),
|
|
90
|
+
"packages: ['packages/*', '!packages/second']\nverifyDepsBeforeRun: false\n",
|
|
91
|
+
);
|
|
92
|
+
expect(run().status).toBe(0);
|
|
93
|
+
rmSync(path.join(root, 'packages'), { recursive: true });
|
|
94
|
+
expect(run().status).toBe(0);
|
|
95
|
+
} finally {
|
|
96
|
+
rmSync(root, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}, 60_000);
|
|
@@ -101,7 +101,7 @@ describe('tailwind content sources', () => {
|
|
|
101
101
|
}
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
it('
|
|
104
|
+
it('ignores obsolete plugin-owned Registry recipes', () => {
|
|
105
105
|
const store = mkdtempSync(path.join(tmpdir(), 'nb3-tailwind-registry-'));
|
|
106
106
|
try {
|
|
107
107
|
const registry = path.join(
|
|
@@ -124,7 +124,7 @@ describe('tailwind content sources', () => {
|
|
|
124
124
|
|
|
125
125
|
const files = contentFilesIn(store);
|
|
126
126
|
|
|
127
|
-
expect(files).
|
|
127
|
+
expect(files).toEqual([]);
|
|
128
128
|
} finally {
|
|
129
129
|
rmSync(store, { recursive: true, force: true });
|
|
130
130
|
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# File Component UI
|
|
2
|
-
|
|
3
|
-
This Registry item installs editable App source for upload, list, thumbnail, and preview controls. The default destination is client/extensions/nocobase-file-component-ui. Import components and UI types from its index.ts.
|
|
4
|
-
|
|
5
|
-
Pass the public ClientFileRepository returned by the App's clientFileRepositoryManagerToken to FileUploadField as repository. Read-only components consume FileRecord arrays and their contentUrl directly; they require no repository prop. See the Registry components section in the plugin's SKILL.md for installation, dependencies, and a complete React example.
|
|
6
|
-
|
|
7
|
-
Upload success is controlled by value/onChange. Temporary progress and error items stay internal. Use onStatusChange to block form submission while uploading or after a failure. Upload cancellation forwards AbortSignal; it does not guarantee rollback after a server commit. removeOnDelete removes metadata only. App-owned business relations and physical cleanup remain the App's responsibility.
|
|
8
|
-
|
|
9
|
-
Preview handles safe raster images, PDF, audio/video and text/Markdown. The content route serves attachments, so PDF is fetched and embedded as a local blob retyped to `application/pdf`; a response carrying an active markup type is reported as a preview failure rather than embedded, because a blob URL is same-origin. DOCX/XLSX/PPTX render locally with lazily loaded `@silurus/ooxml`; legacy Office and OpenDocument formats retain Office Online, which requires an internet-accessible absolute URL and cannot use the App session. Active HTML/SVG/XML and unsafe URL schemes are rejected. Markdown does not execute raw HTML.
|
|
10
|
-
|
|
11
|
-
Follow the plugin Skill's Registry installation and preview sections for the exact viewer dependency, Vite dependency-prebundling exclusion, authentication adapter boundaries and acceptance checks. Same-origin content fetches use session cookies; external fetches omit credentials and require CORS. Bearer-only access requires changes to the App-owned source; setting `contentUrl` to an arbitrary blob URL is not supported. OOXML failures show an error and an optional download action; `download={false}` suppresses that action.
|
|
12
|
-
|
|
13
|
-
There is no public/private record flag or access-token endpoint. The item contains no extension.ts, route, collection, or Server implementation. A private disk does not authorize a content request. Installed copies belong to the App; review upgrades with a three-way merge and preserve customizations. Supply translated labels and adapt additional copy in installed source.
|