@nocobase/plugin-multi-app-share-collection 0.9.2-alpha.1

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 (41) hide show
  1. package/client.d.ts +4 -0
  2. package/client.js +30 -0
  3. package/lib/client/TableTransfer.d.ts +3 -0
  4. package/lib/client/TableTransfer.js +500 -0
  5. package/lib/client/index.d.ts +2 -0
  6. package/lib/client/index.js +122 -0
  7. package/lib/client/locale/pt-BR.d.ts +13 -0
  8. package/lib/client/locale/pt-BR.js +19 -0
  9. package/lib/client/locale/zh-CN.d.ts +13 -0
  10. package/lib/client/locale/zh-CN.js +19 -0
  11. package/lib/client/utils.d.ts +4 -0
  12. package/lib/client/utils.js +25 -0
  13. package/lib/index.d.ts +1 -0
  14. package/lib/index.js +13 -0
  15. package/lib/server/collections/applications.d.ts +6 -0
  16. package/lib/server/collections/applications.js +27 -0
  17. package/lib/server/collections/collections.d.ts +6 -0
  18. package/lib/server/collections/collections.js +27 -0
  19. package/lib/server/index.d.ts +1 -0
  20. package/lib/server/index.js +13 -0
  21. package/lib/server/migrations/20230319111111-update-apps-collections.d.ts +4 -0
  22. package/lib/server/migrations/20230319111111-update-apps-collections.js +106 -0
  23. package/lib/server/plugin.d.ts +9 -0
  24. package/lib/server/plugin.js +408 -0
  25. package/package.json +12 -0
  26. package/server.d.ts +4 -0
  27. package/server.js +30 -0
  28. package/src/client/TableTransfer.tsx +388 -0
  29. package/src/client/index.tsx +86 -0
  30. package/src/client/locale/pt-BR.ts +13 -0
  31. package/src/client/locale/zh-CN.ts +13 -0
  32. package/src/client/utils.tsx +11 -0
  33. package/src/index.ts +1 -0
  34. package/src/server/__tests__/collection-sync.test.ts +489 -0
  35. package/src/server/__tests__/index.ts +25 -0
  36. package/src/server/collections/.gitkeep +0 -0
  37. package/src/server/collections/applications.ts +17 -0
  38. package/src/server/collections/collections.ts +17 -0
  39. package/src/server/index.ts +1 -0
  40. package/src/server/migrations/20230319111111-update-apps-collections.ts +67 -0
  41. package/src/server/plugin.ts +331 -0
@@ -0,0 +1,388 @@
1
+ import { css } from '@emotion/css';
2
+ import { connect } from '@formily/react';
3
+ import { useCollectionManager, useRecord, useRequest } from '@nocobase/client';
4
+ import { CollectionsGraph } from '@nocobase/utils/client';
5
+ import { Col, Input, Modal, Row, Select, Spin, Table, Tag } from 'antd';
6
+ import debounce from 'lodash/debounce';
7
+ import uniq from 'lodash/uniq';
8
+ import React, { useCallback, useMemo, useState } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+
11
+ const excludeCollections = ['users', 'roles', 'applications'];
12
+
13
+ const useCollectionsGraph = ({ removed = [] }) => {
14
+ const { collections } = useCollectionManager();
15
+
16
+ const findAddable = useCallback(
17
+ (name) => {
18
+ return CollectionsGraph.connectedNodes({
19
+ collections,
20
+ nodes: [name],
21
+ excludes: excludeCollections,
22
+ }).filter((name) => removed.includes(name));
23
+ },
24
+ [removed],
25
+ );
26
+
27
+ const findRemovable = useCallback(
28
+ (name) => {
29
+ return CollectionsGraph.connectedNodes({
30
+ collections,
31
+ nodes: [name],
32
+ excludes: excludeCollections,
33
+ direction: 'reverse',
34
+ }).filter((name) => !removed.includes(name));
35
+ },
36
+ [removed],
37
+ );
38
+
39
+ return {
40
+ findAddable,
41
+ findRemovable,
42
+ };
43
+ };
44
+
45
+ const useCollections = () => {
46
+ const record = useRecord();
47
+ const [selected, setSelected] = useState<any>([]);
48
+
49
+ const res1 = useRequest(
50
+ {
51
+ url: `applications/${record.name}/collectionBlacklist:list`,
52
+ params: {
53
+ paginate: false,
54
+ params: {
55
+ fields: ['name'],
56
+ },
57
+ },
58
+ },
59
+ {
60
+ onSuccess(data) {
61
+ setSelected(data.data?.map((data) => data.name));
62
+ },
63
+ },
64
+ );
65
+
66
+ const res2 = useRequest({
67
+ url: `collections`,
68
+ params: {
69
+ fields: ['name', 'title', 'hidden', 'category.name', 'category.color', 'category.sort'],
70
+ sort: 'sort',
71
+ paginate: false,
72
+ },
73
+ });
74
+
75
+ const res3 = useRequest({
76
+ url: `collectionCategories`,
77
+ params: {
78
+ sort: 'sort',
79
+ paginate: false,
80
+ },
81
+ });
82
+
83
+ return {
84
+ loading: res1.loading || res2.loading || res3.loading,
85
+ collections: (res2.data?.data || []).filter((item) => !item.hidden && !excludeCollections.includes(item.name)),
86
+ removed: selected,
87
+ setSelected,
88
+ categories: (res3.data?.data || []).map((cat) => ({ label: cat.name, value: cat.name })),
89
+ };
90
+ };
91
+
92
+ const includes = (text: string, s: string | string[]) => {
93
+ const values = Array.isArray(s) ? s : [s];
94
+ for (const val of values) {
95
+ if (text.toLowerCase().includes(val)) {
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ };
101
+
102
+ const useRemovedDataSource = ({ collections, removed }) => {
103
+ const [filter, setFilter] = useState({ name: '', category: [] });
104
+ const dataSource = useMemo(() => {
105
+ return collections.filter((collection) => {
106
+ const { name, title, category = [] } = collection;
107
+ const results = [removed.includes(collection.name)];
108
+ if (filter.name) {
109
+ results.push(includes(name, filter.name) || includes(title, filter.name));
110
+ }
111
+ if (filter.category.length > 0) {
112
+ results.push(category.some((item) => includes(item.name, filter.category)));
113
+ }
114
+ return !results.includes(false);
115
+ });
116
+ }, [collections, removed, filter]);
117
+ const setNameFilter = useMemo(
118
+ () =>
119
+ debounce((name) => {
120
+ setFilter({
121
+ ...filter,
122
+ name,
123
+ });
124
+ }, 300),
125
+ [],
126
+ );
127
+ return {
128
+ dataSource,
129
+ setNameFilter,
130
+ setCategoryFilter: (category) => {
131
+ setFilter({
132
+ ...filter,
133
+ category,
134
+ });
135
+ },
136
+ };
137
+ };
138
+
139
+ const useAddedDataSource = ({ collections, removed }) => {
140
+ const [filter, setFilter] = useState({ name: '', category: [] });
141
+ const dataSource = collections.filter((collection) => {
142
+ const { name, title, category = [] } = collection;
143
+ const results = [!removed.includes(collection.name)];
144
+ if (filter.name) {
145
+ results.push(includes(name, filter.name) || includes(title, filter.name));
146
+ }
147
+ if (filter.category.length > 0) {
148
+ results.push(category.some((item) => includes(item.name, filter.category)));
149
+ }
150
+ return !results.includes(false);
151
+ });
152
+ const setNameFilter = useMemo(
153
+ () =>
154
+ debounce((name) => {
155
+ setFilter({
156
+ ...filter,
157
+ name,
158
+ });
159
+ }, 300),
160
+ [],
161
+ );
162
+ return {
163
+ dataSource,
164
+ setNameFilter,
165
+ setCategoryFilter: (category) => {
166
+ setFilter({
167
+ ...filter,
168
+ category,
169
+ });
170
+ },
171
+ };
172
+ };
173
+
174
+ export const TableTransfer = connect((props) => {
175
+ const { onChange } = props;
176
+ const { loading, collections, categories, removed, setSelected } = useCollections();
177
+ const [selectedRowKeys1, setSelectedRowKeys1] = useState([]);
178
+ const [selectedRowKeys2, setSelectedRowKeys2] = useState([]);
179
+ const { findAddable, findRemovable } = useCollectionsGraph({ removed });
180
+ const addedDataSource = useAddedDataSource({ collections, removed });
181
+ const removedDataSource = useRemovedDataSource({ collections, removed });
182
+ const { t } = useTranslation('multi-app-share-collection');
183
+ const columns = useMemo(
184
+ () => [
185
+ {
186
+ title: t('Collection display name'),
187
+ dataIndex: 'title',
188
+ },
189
+ {
190
+ title: t('Collection name'),
191
+ dataIndex: 'name',
192
+ },
193
+ {
194
+ title: t('Collection category'),
195
+ dataIndex: 'category',
196
+ render: (categories) => categories.map((category) => <Tag color={category.color}>{category.name}</Tag>),
197
+ },
198
+ ],
199
+ [],
200
+ );
201
+ if (loading) {
202
+ return <Spin />;
203
+ }
204
+ return (
205
+ <div>
206
+ <Row
207
+ gutter={24}
208
+ className={css`
209
+ .ant-table-tbody > tr.ant-table-row:hover > td {
210
+ background: #e6f7ff;
211
+ cursor: pointer;
212
+ }
213
+ `}
214
+ >
215
+ <Col span={12}>
216
+ <div
217
+ className={css`
218
+ display: flex;
219
+ justify-content: space-between;
220
+ align-items: center;
221
+ width: 100%;
222
+ margin-bottom: 8px;
223
+ `}
224
+ >
225
+ <strong style={{ fontSize: 16 }}>{t('Unshared collections')}</strong>
226
+ <Input.Group compact style={{ width: 360 }}>
227
+ <Select
228
+ onChange={(value) => {
229
+ removedDataSource.setCategoryFilter(value);
230
+ }}
231
+ mode={'multiple'}
232
+ style={{ width: '35%' }}
233
+ size={'middle'}
234
+ placeholder={t('All categories')}
235
+ options={categories}
236
+ allowClear
237
+ />
238
+ <Input
239
+ onChange={(e) => removedDataSource.setNameFilter(e.target.value)}
240
+ style={{ width: '65%' }}
241
+ placeholder={t('Enter name or title...')}
242
+ allowClear
243
+ />
244
+ </Input.Group>
245
+ </div>
246
+ <Table
247
+ bordered
248
+ rowKey={'name'}
249
+ rowSelection={{
250
+ type: 'checkbox',
251
+ selectedRowKeys: selectedRowKeys1,
252
+ onChange(selectedRowKeys) {
253
+ const values = removed.filter((s) => !selectedRowKeys.includes(s));
254
+ setSelected(values);
255
+ onChange(values);
256
+ setSelectedRowKeys1([]);
257
+ },
258
+ }}
259
+ pagination={false}
260
+ size={'small'}
261
+ columns={columns}
262
+ // dataSource={collections.filter((collection) => removed.includes(collection.name))}
263
+ dataSource={removedDataSource.dataSource}
264
+ scroll={{ y: 'calc(100vh - 260px)' }}
265
+ onRow={({ name, disabled }) => ({
266
+ onClick: () => {
267
+ if (disabled) return;
268
+ const adding = findAddable(name);
269
+ const change = () => {
270
+ const values = removed.filter((s) => !adding.includes(s));
271
+ setSelected(values);
272
+ onChange(values);
273
+ };
274
+ if (adding.length === 1) {
275
+ return change();
276
+ }
277
+ Modal.confirm({
278
+ title: t('Are you sure to add the following collections?'),
279
+ width: '60%',
280
+ content: (
281
+ <div>
282
+ <Table
283
+ size={'small'}
284
+ columns={columns}
285
+ dataSource={collections.filter((collection) => adding.includes(collection.name))}
286
+ pagination={false}
287
+ scroll={{ y: '60vh' }}
288
+ />
289
+ </div>
290
+ ),
291
+ onOk() {
292
+ change();
293
+ },
294
+ });
295
+ },
296
+ })}
297
+ />
298
+ </Col>
299
+ <Col span={12}>
300
+ <div
301
+ className={css`
302
+ display: flex;
303
+ justify-content: space-between;
304
+ align-items: center;
305
+ width: 100%;
306
+ margin-bottom: 8px;
307
+ `}
308
+ >
309
+ <strong style={{ fontSize: 16 }}>{t('Shared collections')}</strong>
310
+ <Input.Group compact style={{ width: 360 }}>
311
+ <Select
312
+ onChange={(value) => {
313
+ addedDataSource.setCategoryFilter(value);
314
+ }}
315
+ mode={'multiple'}
316
+ style={{ width: '35%' }}
317
+ size={'middle'}
318
+ placeholder={t('All categories')}
319
+ options={categories}
320
+ allowClear
321
+ />
322
+ <Input
323
+ onChange={(e) => addedDataSource.setNameFilter(e.target.value)}
324
+ style={{ width: '65%' }}
325
+ placeholder={t('Enter name or title...')}
326
+ allowClear
327
+ />
328
+ </Input.Group>
329
+ </div>
330
+ <Table
331
+ bordered
332
+ rowKey={'name'}
333
+ rowSelection={{
334
+ type: 'checkbox',
335
+ selectedRowKeys: selectedRowKeys2,
336
+ onChange(selectedRowKeys) {
337
+ const values = uniq(removed.concat(selectedRowKeys));
338
+ setSelected(values);
339
+ onChange(values);
340
+ setSelectedRowKeys2([]);
341
+ },
342
+ }}
343
+ pagination={false}
344
+ size={'small'}
345
+ columns={columns}
346
+ dataSource={addedDataSource.dataSource}
347
+ // dataSource={collections.filter((collection) => !selected.includes(collection.name))}
348
+ scroll={{ y: 'calc(100vh - 260px)' }}
349
+ onRow={({ name }) => ({
350
+ onClick: () => {
351
+ const removing = findRemovable(name);
352
+ const change = () => {
353
+ removed.push(...removing);
354
+ const values = uniq([...removed]);
355
+ setSelected(values);
356
+ onChange(values);
357
+ };
358
+ if (removing.length === 1) {
359
+ return change();
360
+ }
361
+ Modal.confirm({
362
+ title: t('Are you sure to remove the following collections?'),
363
+ width: '60%',
364
+ content: (
365
+ <div>
366
+ <Table
367
+ size={'small'}
368
+ columns={columns}
369
+ dataSource={collections.filter((collection) => removing.includes(collection.name))}
370
+ pagination={false}
371
+ scroll={{ y: '60vh' }}
372
+ />
373
+ </div>
374
+ ),
375
+ onOk() {
376
+ change();
377
+ },
378
+ });
379
+ },
380
+ })}
381
+ />
382
+ </Col>
383
+ </Row>
384
+ </div>
385
+ );
386
+ });
387
+
388
+ export default TableTransfer;
@@ -0,0 +1,86 @@
1
+ import { useForm } from '@formily/react';
2
+ import { useActionContext, useAPIClient, useRecord } from '@nocobase/client';
3
+ import { tableActionColumnSchema } from '@nocobase/plugin-multi-app-manager/client';
4
+ import { message } from 'antd';
5
+ import React from 'react';
6
+ import { TableTransfer } from './TableTransfer';
7
+ import { i18nText } from './utils';
8
+
9
+ const useShareCollectionAction = () => {
10
+ const form = useForm();
11
+ const ctx = useActionContext();
12
+ const api = useAPIClient();
13
+ const record = useRecord();
14
+ return {
15
+ async run() {
16
+ console.log(form.values.names);
17
+ await api.request({
18
+ url: `applications/${record.name}/collectionBlacklist`,
19
+ data: form.values.names,
20
+ method: 'post',
21
+ });
22
+ ctx.setVisible(false);
23
+ form.reset();
24
+ message.success('Saved successfully');
25
+ },
26
+ };
27
+ };
28
+
29
+ const updateSchema = tableActionColumnSchema.properties.update;
30
+ const deleteSchema = tableActionColumnSchema.properties.delete;
31
+
32
+ delete tableActionColumnSchema.properties.update;
33
+ delete tableActionColumnSchema.properties.delete;
34
+
35
+ tableActionColumnSchema.properties['collection'] = {
36
+ type: 'void',
37
+ title: i18nText('Share collections'),
38
+ 'x-component': 'Action.Link',
39
+ 'x-component-props': {},
40
+ properties: {
41
+ drawer: {
42
+ type: 'void',
43
+ 'x-component': 'Action.Drawer',
44
+ 'x-component-props': {
45
+ width: '95vw',
46
+ },
47
+ 'x-decorator': 'Form',
48
+ title: i18nText('Share collections'),
49
+ properties: {
50
+ names: {
51
+ type: 'array',
52
+ 'x-component': TableTransfer,
53
+ 'x-decorator': 'FormItem',
54
+ },
55
+ footer: {
56
+ type: 'void',
57
+ 'x-component': 'Action.Drawer.Footer',
58
+ properties: {
59
+ cancel: {
60
+ title: '{{t("Cancel")}}',
61
+ 'x-component': 'Action',
62
+ 'x-component-props': {
63
+ useAction: '{{ cm.useCancelAction }}',
64
+ },
65
+ },
66
+ submit: {
67
+ title: '{{t("Submit")}}',
68
+ 'x-component': 'Action',
69
+ 'x-component-props': {
70
+ type: 'primary',
71
+ useAction: useShareCollectionAction,
72
+ },
73
+ },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ };
80
+
81
+ tableActionColumnSchema.properties.update = updateSchema;
82
+ tableActionColumnSchema.properties.delete = deleteSchema;
83
+
84
+ export default (props) => {
85
+ return <>{props.children}</>;
86
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ 'Share collections': 'Compartilhar tabelas',
3
+ 'Unshared collections': 'Tabelas não compartilhadas',
4
+ 'Shared collections': 'Tabelas compartilhadas',
5
+ 'All categories': 'Todas as categorias',
6
+ 'Enter name or title...': 'Digite o nome ou título...',
7
+ 'Are you sure to add the following collections?': 'Tem certeza de que deseja adicionar as seguintes tabelas?',
8
+ 'Are you sure to remove the following collections?': 'Tem certeza de que deseja remover as seguintes tabelas?',
9
+ 'Collection display name': 'Nome de exibição da tabela',
10
+ 'Collection name': 'Nome da tabela',
11
+ 'Collection category': 'Categoria da tabela',
12
+ };
13
+
@@ -0,0 +1,13 @@
1
+ export default {
2
+ 'Share collections': '共享数据表',
3
+ 'Unshared collections': '未共享的数据表',
4
+ 'Shared collections': '已共享的数据表',
5
+ 'All categories': '所有分类',
6
+ 'Enter name or title...': '输入数据表标题或标识',
7
+ 'Are you sure to add the following collections?': '确定添加以下数据表?',
8
+ 'Are you sure to remove the following collections?': '确定移除以下数据表?',
9
+ 'Collection display name': '标题',
10
+ 'Collection name': '标识',
11
+ 'Collection category': '分类',
12
+ };
13
+
@@ -0,0 +1,11 @@
1
+ import { useTranslation } from 'react-i18next';
2
+
3
+ export const usePluginUtils = () => {
4
+ const { t } = useTranslation('multi-app-share-collection');
5
+
6
+ return { t };
7
+ };
8
+
9
+ export const i18nText = (text) => {
10
+ return `{{t("${text}", { ns: 'multi-app-share-collection' })}}`;
11
+ };
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from './server';