@nocobase/client-v2 2.1.0-beta.37 → 2.1.0-beta.38

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 (123) hide show
  1. package/es/Application.d.ts +1 -0
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/RouterManager.d.ts +1 -0
  4. package/es/components/KeepAlive.d.ts +22 -0
  5. package/es/components/RouterBridge.d.ts +9 -0
  6. package/es/data-source/ExtendCollectionsProvider.d.ts +28 -2
  7. package/es/flow/FlowPage.d.ts +2 -1
  8. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  9. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  10. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  12. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  13. package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
  14. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  15. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  16. package/es/flow/components/FlowRoute.d.ts +10 -1
  17. package/es/flow/index.d.ts +4 -0
  18. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  19. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  20. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  21. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  22. package/es/index.d.ts +1 -0
  23. package/es/index.mjs +484 -437
  24. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  25. package/es/layout-manager/LayoutManager.d.ts +22 -0
  26. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  27. package/es/layout-manager/index.d.ts +13 -0
  28. package/es/layout-manager/types.d.ts +20 -0
  29. package/es/layout-manager/utils.d.ts +14 -0
  30. package/es/settings-center/index.d.ts +1 -1
  31. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  32. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  33. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  34. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  35. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  36. package/lib/index.js +484 -437
  37. package/package.json +8 -7
  38. package/src/Application.tsx +27 -12
  39. package/src/BaseApplication.tsx +6 -0
  40. package/src/PluginSettingsManager.ts +1 -1
  41. package/src/RouterManager.tsx +17 -1
  42. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  43. package/src/__tests__/app.test.tsx +8 -1
  44. package/src/__tests__/globalDeps.test.ts +1 -0
  45. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
  46. package/src/__tests__/plugin-manager.test.tsx +177 -0
  47. package/src/__tests__/settings-center.test.tsx +24 -2
  48. package/src/components/KeepAlive.tsx +131 -0
  49. package/src/components/RouterBridge.tsx +28 -4
  50. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  51. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  52. package/src/data-source/ExtendCollectionsProvider.tsx +94 -20
  53. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  54. package/src/flow/FlowPage.tsx +35 -7
  55. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  56. package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
  57. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  58. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  59. package/src/flow/actions/aclCheck.tsx +4 -0
  60. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  61. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  62. package/src/flow/actions/linkageRules.tsx +122 -0
  63. package/src/flow/actions/openView.tsx +28 -4
  64. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  65. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  66. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  67. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  68. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
  69. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  70. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
  71. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  72. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  73. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
  74. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  75. package/src/flow/admin-shell/admin-layout/index.ts +2 -0
  76. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  77. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  78. package/src/flow/components/AdminLayout.tsx +4 -154
  79. package/src/flow/components/FlowRoute.tsx +105 -15
  80. package/src/flow/index.ts +4 -0
  81. package/src/flow/models/base/ActionModel.tsx +8 -1
  82. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  83. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  84. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  85. package/src/flow/models/base/RouteModel.tsx +1 -1
  86. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  87. package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
  88. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  89. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  90. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  91. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  92. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  93. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  94. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  95. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  96. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  97. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  98. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  99. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  100. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  101. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  102. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  103. package/src/index.ts +1 -0
  104. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  105. package/src/layout-manager/LayoutManager.tsx +185 -0
  106. package/src/layout-manager/LayoutRoute.tsx +138 -0
  107. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  108. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  109. package/src/layout-manager/index.ts +14 -0
  110. package/src/layout-manager/types.ts +22 -0
  111. package/src/layout-manager/utils.ts +37 -0
  112. package/src/nocobase-buildin-plugin/index.tsx +56 -48
  113. package/src/settings-center/index.ts +1 -1
  114. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  115. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  116. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  117. package/src/settings-center/plugin-manager/index.tsx +254 -0
  118. package/src/settings-center/plugin-manager/types.ts +35 -0
  119. package/src/settings-center/utils.tsx +8 -1
  120. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  121. package/src/theme/globalStyles.ts +10 -0
  122. package/src/utils/globalDeps.ts +2 -0
  123. package/src/settings-center/PluginManagerPage.tsx +0 -162
@@ -0,0 +1,195 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { useRequest } from 'ahooks';
11
+ import { Alert, Col, Modal, Row, Space, Spin, Table, Tabs, theme, Typography } from 'antd';
12
+ import type { TabsProps } from 'antd';
13
+ import React, { FC, useMemo } from 'react';
14
+ import { useTranslation } from 'react-i18next';
15
+ import { useApp } from '../../hooks/useApp';
16
+ import type { IPluginData } from './types';
17
+
18
+ type Author =
19
+ | string
20
+ | {
21
+ name: string;
22
+ email?: string;
23
+ url?: string;
24
+ };
25
+
26
+ interface PackageJSON {
27
+ name: string;
28
+ version: string;
29
+ description?: string;
30
+ repository?: string | { type: string; url: string };
31
+ homepage?: string;
32
+ license?: string;
33
+ author?: Author;
34
+ devDependencies?: Record<string, string>;
35
+ dependencies?: Record<string, string>;
36
+ }
37
+
38
+ interface DepCompatible {
39
+ name: string;
40
+ result: boolean;
41
+ versionRange: string;
42
+ packageVersion: string;
43
+ }
44
+
45
+ interface IPluginDetailData {
46
+ packageJson: PackageJSON;
47
+ homepage?: string;
48
+ depsCompatible: DepCompatible[] | false;
49
+ isCompatible?: boolean;
50
+ lastUpdated: string;
51
+ }
52
+
53
+ interface IPluginDetailProps {
54
+ plugin: IPluginData;
55
+ onCancel: () => void;
56
+ }
57
+
58
+ export const PluginDetail: FC<IPluginDetailProps> = ({ plugin, onCancel }) => {
59
+ const { t } = useTranslation();
60
+ const { token } = theme.useToken();
61
+ const app = useApp();
62
+
63
+ const { data, loading } = useRequest<IPluginDetailData | undefined, []>(
64
+ async () => {
65
+ const response = await app.apiClient.request({
66
+ url: 'pm:get',
67
+ params: { filterByTk: plugin.name },
68
+ skipNotify: true,
69
+ });
70
+ return response?.data?.data as IPluginDetailData | undefined;
71
+ },
72
+ {
73
+ refreshDeps: [plugin.name],
74
+ ready: !!plugin.name,
75
+ },
76
+ );
77
+
78
+ const dependenciesCompatibleTableColumns = useMemo(
79
+ () => [
80
+ { title: t('Name'), dataIndex: 'name', key: 'name' },
81
+ { title: t('Version range'), dataIndex: 'versionRange', key: 'versionRange' },
82
+ { title: t("Plugin's version"), dataIndex: 'packageVersion', key: 'packageVersion' },
83
+ {
84
+ title: t('Result'),
85
+ dataIndex: 'result',
86
+ key: 'result',
87
+ render: (result: boolean) => (
88
+ <Typography.Text type={result ? 'success' : 'danger'}>{result ? t('Yes') : t('No')}</Typography.Text>
89
+ ),
90
+ },
91
+ ],
92
+ [t],
93
+ );
94
+
95
+ const repository = useMemo(() => {
96
+ const repo = data?.packageJson?.repository;
97
+ if (!repo) return null;
98
+ const url = typeof repo === 'string' ? repo : repo.url;
99
+ return url.replace(/\.git$/, '').replace(/^git\+/, '');
100
+ }, [data]);
101
+
102
+ const author = useMemo(() => {
103
+ const a = data?.packageJson?.author;
104
+ if (!a) return null;
105
+ if (typeof a === 'string') return a;
106
+ return a.name;
107
+ }, [data]);
108
+
109
+ const infoRow = (label: string, value: React.ReactNode) => (
110
+ <Col span={24}>
111
+ <div style={{ display: 'flex', flexDirection: 'column', gap: token.marginXXS, marginBottom: token.marginSM }}>
112
+ <Typography.Text type="secondary">{label}</Typography.Text>
113
+ <Typography.Text strong>{value}</Typography.Text>
114
+ </div>
115
+ </Col>
116
+ );
117
+
118
+ const tabItems: TabsProps['items'] = [
119
+ {
120
+ key: 'readme',
121
+ label: t('Readme'),
122
+ children: (
123
+ <Row gutter={token.marginLG}>
124
+ {plugin.name && infoRow(t('Name'), plugin.name)}
125
+ {plugin.displayName && infoRow(t('DisplayName'), plugin.displayName)}
126
+ {infoRow(t('PackageName'), plugin.packageName)}
127
+ {repository && infoRow(t('Repository'), repository)}
128
+ {data?.homepage &&
129
+ infoRow(
130
+ t('Homepage'),
131
+ <a href={data.homepage} target="_blank" rel="noreferrer">
132
+ {data.homepage}
133
+ </a>,
134
+ )}
135
+ {plugin.description && infoRow(t('Description'), plugin.description)}
136
+ {data?.packageJson?.license && infoRow(t('License'), data.packageJson.license)}
137
+ {author && infoRow(t('Author'), author)}
138
+ {infoRow(t('Version'), plugin.version)}
139
+ </Row>
140
+ ),
141
+ },
142
+ {
143
+ key: 'dependencies',
144
+ label: t('Dependencies compatibility check'),
145
+ children: (
146
+ <>
147
+ {data?.depsCompatible === false ? (
148
+ <Typography.Text type="danger">
149
+ {t('`dist/externalVersion.js` not found or failed to `require`. Please rebuild this plugin.')}
150
+ </Typography.Text>
151
+ ) : (
152
+ <>
153
+ {data && !data.isCompatible && (
154
+ <Alert
155
+ showIcon
156
+ type="error"
157
+ message={t(
158
+ 'Plugin dependencies check failed, you should change the dependent version to meet the version requirements.',
159
+ )}
160
+ />
161
+ )}
162
+ <Table
163
+ style={{ marginTop: token.margin }}
164
+ rowKey="name"
165
+ pagination={false}
166
+ columns={dependenciesCompatibleTableColumns}
167
+ dataSource={Array.isArray(data?.depsCompatible) ? data.depsCompatible : []}
168
+ />
169
+ </>
170
+ )}
171
+ </>
172
+ ),
173
+ },
174
+ ];
175
+
176
+ return (
177
+ <Modal open footer={false} destroyOnClose width={600} onCancel={onCancel}>
178
+ {loading ? (
179
+ <Spin />
180
+ ) : (
181
+ <>
182
+ <Typography.Title level={3}>{plugin.packageName}</Typography.Title>
183
+ <Space split={<span>&nbsp;•&nbsp;</span>}>
184
+ <span>{plugin.version}</span>
185
+ </Space>
186
+ <Tabs
187
+ style={{ minHeight: '50vh' }}
188
+ items={tabItems}
189
+ defaultActiveKey={plugin.isCompatible === false ? 'dependencies' : 'readme'}
190
+ />
191
+ </>
192
+ )}
193
+ </Modal>
194
+ );
195
+ };
@@ -0,0 +1,254 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { MenuOutlined } from '@ant-design/icons';
11
+ import { useDebounce, useMemoizedFn, useRequest } from 'ahooks';
12
+ import { Alert, Button, Card, Divider, Flex, Grid, Input, List, Popover, Space, Spin, theme } from 'antd';
13
+ import _ from 'lodash';
14
+ import React, { useMemo, useState } from 'react';
15
+ import { useTranslation } from 'react-i18next';
16
+ import { useApp } from '../../hooks/useApp';
17
+ import { BulkEnableButton } from './BulkEnableButton';
18
+ import { PluginCard } from './PluginCard';
19
+ import type { IPluginData } from './types';
20
+
21
+ const KEYWORDS = [
22
+ 'Data model tools',
23
+ 'Data sources',
24
+ 'Collections',
25
+ 'Collection fields',
26
+ 'Blocks',
27
+ 'Actions',
28
+ 'Workflow',
29
+ 'Users & permissions',
30
+ 'AI',
31
+ 'Authentication',
32
+ 'Notification',
33
+ 'System management',
34
+ 'Security',
35
+ 'Architecture',
36
+ 'Logging and monitoring',
37
+ 'Others',
38
+ ];
39
+
40
+ const FILTER_TYPES = ['All', 'Built-in', 'Enabled', 'Not enabled', 'Problematic'] as const;
41
+ type FilterType = (typeof FILTER_TYPES)[number];
42
+
43
+ const SIDEBAR_WIDTH = 200;
44
+
45
+ function hasIntersection(arr1?: string[], arr2?: string[]) {
46
+ if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false;
47
+ return arr1.some((item) => arr2.includes(item));
48
+ }
49
+
50
+ function CategoryList({
51
+ items,
52
+ activeKeyword,
53
+ onSelect,
54
+ }: {
55
+ items: { key: string; list?: IPluginData[] }[];
56
+ activeKeyword: string | null;
57
+ onSelect: (key: string) => void;
58
+ }) {
59
+ const { t } = useTranslation();
60
+ return (
61
+ <List
62
+ size="small"
63
+ dataSource={items}
64
+ split={false}
65
+ renderItem={(item) => (
66
+ <List.Item style={{ padding: '3px 0', cursor: 'pointer' }} onClick={() => onSelect(item.key)}>
67
+ <a style={{ fontWeight: activeKeyword === item.key ? 'bold' : 'normal' }}>{t(item.key)}</a>
68
+ </List.Item>
69
+ )}
70
+ />
71
+ );
72
+ }
73
+
74
+ export const PluginManagerPage: React.FC = () => {
75
+ const { t } = useTranslation();
76
+ const { token } = theme.useToken();
77
+ const app = useApp();
78
+ const screens = Grid.useBreakpoint();
79
+ const isWide = !!screens.md;
80
+
81
+ const {
82
+ data: plugins = [],
83
+ loading,
84
+ error,
85
+ } = useRequest<IPluginData[], []>(
86
+ async () => {
87
+ const response = await app.apiClient.request({
88
+ url: 'pm:list',
89
+ skipNotify: true,
90
+ });
91
+ return Array.isArray(response?.data?.data) ? response.data.data : [];
92
+ },
93
+ { cacheKey: 'pm:list' },
94
+ );
95
+
96
+ const filterBuckets = useMemo<{ type: FilterType; list: IPluginData[] }[]>(() => {
97
+ const list = [...plugins].reverse();
98
+ return [
99
+ { type: 'All', list },
100
+ { type: 'Built-in', list: _.filter(list, (item) => item.builtIn) },
101
+ { type: 'Enabled', list: _.filter(list, (item) => item.enabled) },
102
+ { type: 'Not enabled', list: _.filter(list, (item) => !item.enabled) },
103
+ { type: 'Problematic', list: _.filter(list, (item) => !item.isCompatible) },
104
+ ];
105
+ }, [plugins]);
106
+
107
+ const [filterIndex, setFilterIndex] = useState(0);
108
+ const [searchValue, setSearchValue] = useState('');
109
+ const [keyword, setKeyword] = useState<string | null>(null);
110
+ const [categoryOpen, setCategoryOpen] = useState(false);
111
+ const debouncedSearchValue = useDebounce(searchValue, { wait: 100 });
112
+
113
+ const keywordBuckets = useMemo(
114
+ () =>
115
+ KEYWORDS.map((k) => {
116
+ if (k === 'Others') {
117
+ return { key: k, list: plugins.filter((v) => !hasIntersection(v.keywords, KEYWORDS)) };
118
+ }
119
+ return { key: k, list: plugins.filter((v) => v.keywords?.includes(k)) };
120
+ }),
121
+ [plugins],
122
+ );
123
+
124
+ const pluginList = useMemo(() => {
125
+ let list = filterBuckets[filterIndex]?.list || [];
126
+ if (keyword) {
127
+ const byKeyword = keywordBuckets.find((v) => v.key === keyword)?.list || [];
128
+ if (filterIndex === 0) {
129
+ list = byKeyword;
130
+ } else {
131
+ list = byKeyword.filter((v) => list.find((k) => k.name === v.name));
132
+ }
133
+ }
134
+ const searchLower = debouncedSearchValue.toLowerCase().trim();
135
+ if (searchLower) {
136
+ list = _.filter(
137
+ list,
138
+ (item) =>
139
+ String(item.displayName || '')
140
+ .toLowerCase()
141
+ .includes(searchLower) ||
142
+ String(item.description || '')
143
+ .toLowerCase()
144
+ .includes(searchLower),
145
+ );
146
+ }
147
+ return [...list].sort((a, b) => (a.displayName || '').localeCompare(b.displayName || ''));
148
+ }, [filterIndex, filterBuckets, debouncedSearchValue, keyword, keywordBuckets]);
149
+
150
+ const handleKeywordSelect = useMemoizedFn((key: string) => {
151
+ setKeyword((prev) => (prev === key ? null : key));
152
+ setCategoryOpen(false);
153
+ });
154
+
155
+ const handleSearchChange = useMemoizedFn((e: React.ChangeEvent<HTMLInputElement>) => {
156
+ setSearchValue(e.target.value);
157
+ });
158
+
159
+ if (loading) {
160
+ return <Spin />;
161
+ }
162
+
163
+ const filterTabs = (
164
+ <Space size={token.marginXXS} split={<Divider type="vertical" />} wrap>
165
+ {!isWide && (
166
+ <Popover
167
+ trigger="click"
168
+ placement="bottomLeft"
169
+ open={categoryOpen}
170
+ onOpenChange={setCategoryOpen}
171
+ content={
172
+ <div style={{ minWidth: 180, maxHeight: '60vh', overflowY: 'auto' }}>
173
+ <CategoryList items={keywordBuckets} activeKeyword={keyword} onSelect={handleKeywordSelect} />
174
+ </div>
175
+ }
176
+ >
177
+ <Button type="text" size="small" icon={<MenuOutlined />} aria-label={t('Category')} />
178
+ </Popover>
179
+ )}
180
+ {filterBuckets.map((item, index) => (
181
+ <a
182
+ role="button"
183
+ aria-label={item.type}
184
+ onClick={() => setFilterIndex(index)}
185
+ key={item.type}
186
+ style={{ fontWeight: filterIndex === index ? 'bold' : 'normal' }}
187
+ >
188
+ {t(item.type)}
189
+ {filterIndex === index ? `(${pluginList.length})` : null}
190
+ </a>
191
+ ))}
192
+ </Space>
193
+ );
194
+
195
+ const topBar = (
196
+ <Flex
197
+ wrap="wrap"
198
+ gap={token.margin}
199
+ align="center"
200
+ justify="space-between"
201
+ style={{ marginBottom: token.marginLG }}
202
+ >
203
+ {filterTabs}
204
+ <Flex gap={token.marginSM} align="center" style={{ flex: '1 1 auto', justifyContent: 'flex-end' }}>
205
+ <Input
206
+ allowClear
207
+ placeholder={t('Search plugin')}
208
+ onChange={handleSearchChange}
209
+ style={{ flex: '1 1 auto', maxWidth: 320, minWidth: 160 }}
210
+ />
211
+ <div style={{ flexShrink: 0 }}>
212
+ <BulkEnableButton plugins={plugins} />
213
+ </div>
214
+ </Flex>
215
+ </Flex>
216
+ );
217
+
218
+ return (
219
+ <>
220
+ {error ? (
221
+ <Alert
222
+ showIcon
223
+ type="error"
224
+ message={t('Failed to load plugins')}
225
+ description={error.message}
226
+ style={{ marginBottom: token.marginLG }}
227
+ />
228
+ ) : null}
229
+ {topBar}
230
+ <Flex gap={token.margin} align="flex-start">
231
+ {isWide && (
232
+ <Card style={{ width: SIDEBAR_WIDTH, flexShrink: 0 }} size="small">
233
+ <CategoryList items={keywordBuckets} activeKeyword={keyword} onSelect={handleKeywordSelect} />
234
+ </Card>
235
+ )}
236
+ <div
237
+ style={{
238
+ flex: '1 1 auto',
239
+ minWidth: 0,
240
+ display: 'grid',
241
+ gap: token.margin,
242
+ gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
243
+ }}
244
+ >
245
+ {pluginList.map((item) => (
246
+ <PluginCard key={item.name} data={item} />
247
+ ))}
248
+ </div>
249
+ </Flex>
250
+ </>
251
+ );
252
+ };
253
+
254
+ export default PluginManagerPage;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export interface IPluginData {
11
+ id: number;
12
+ createdAt: Date;
13
+ updatedAt: Date;
14
+ name: string;
15
+ displayName: string;
16
+ packageName: string;
17
+ version: string;
18
+ enabled: boolean;
19
+ removable?: boolean;
20
+ installed: boolean;
21
+ builtIn: boolean;
22
+ registry?: string;
23
+ authToken?: string;
24
+ compressedFileUrl?: string;
25
+ options: Record<string, unknown>;
26
+ description?: string;
27
+ type: 'npm' | 'upload' | 'url';
28
+ isCompatible?: boolean;
29
+ readmeUrl: string;
30
+ changelogUrl: string;
31
+ error: boolean;
32
+ updatable?: boolean;
33
+ homepage?: string;
34
+ keywords?: string[];
35
+ }
@@ -142,7 +142,14 @@ export function matchSettingsRoute(data: Record<string, PluginSettingsPageType>,
142
142
  }
143
143
  }
144
144
 
145
- return null;
145
+ const matchedPrefix = paths
146
+ .filter((pattern) => {
147
+ const regexPattern = pattern.replace(/:[^/]+/g, '[^/]+');
148
+ return url.match(new RegExp(`^${regexPattern}/`));
149
+ })
150
+ .sort((a, b) => b.length - a.length)[0];
151
+
152
+ return matchedPrefix ? data[matchedPrefix] : null;
146
153
  }
147
154
 
148
155
  /**
@@ -0,0 +1,24 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, it } from 'vitest';
11
+ import '../globalStyles';
12
+
13
+ describe('client-v2 global styles', () => {
14
+ it('includes flow view visibility classes used by drawer and dialog views', () => {
15
+ const styleText = Array.from(document.querySelectorAll('style'))
16
+ .map((style) => style.textContent || '')
17
+ .join('\n');
18
+
19
+ expect(styleText).toContain('.nb-hidden');
20
+ expect(styleText).toContain('display:none');
21
+ expect(styleText).toContain('.nb-dialog-overflow-hidden .ant-modal-content');
22
+ expect(styleText).toContain('overflow:hidden');
23
+ });
24
+ });
@@ -18,4 +18,14 @@ injectGlobal`
18
18
  .ant-form-item-label > label {
19
19
  font-weight: 600;
20
20
  }
21
+
22
+ .nb-hidden {
23
+ display: none;
24
+ }
25
+
26
+ .nb-dialog-overflow-hidden {
27
+ .ant-modal-content {
28
+ overflow: hidden;
29
+ }
30
+ }
21
31
  `;
@@ -10,6 +10,7 @@
10
10
  import * as antdCssinjs from '@ant-design/cssinjs';
11
11
  import * as antdIcons from '@ant-design/icons';
12
12
  import * as antdStyle from 'antd-style';
13
+ import * as ctrlTinycolor from '@ctrl/tinycolor';
13
14
  import * as emotionCss from '@emotion/css';
14
15
  import * as formilyAntdV5 from '@formily/antd-v5';
15
16
  import * as formilyCore from '@formily/core';
@@ -96,6 +97,7 @@ export function defineGlobalDeps(requirejs: RequireJS) {
96
97
  defineGlobalDep(requirejs, '@dnd-kit/sortable', dndKitSortable);
97
98
 
98
99
  // utils
100
+ defineGlobalDep(requirejs, '@ctrl/tinycolor', ctrlTinycolor);
99
101
  defineGlobalDep(requirejs, 'ahooks', ahooks);
100
102
  defineGlobalDep(requirejs, 'axios', axios);
101
103
  defineGlobalDep(requirejs, 'dayjs', dayjs);