mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 23:36:15 +00:00
Merge branch 'next' into 730
This commit is contained in:
commit
fcbe4496ce
@ -41,7 +41,7 @@ RUN cd /app \
|
||||
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
|
||||
|
||||
FROM node:20.13-bullseye-slim
|
||||
RUN apt-get update && apt-get install -y nginx
|
||||
RUN apt-get update && apt-get install -y nginx libaio1
|
||||
RUN rm -rf /etc/nginx/sites-enabled/default
|
||||
|
||||
COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf
|
||||
|
@ -71,11 +71,13 @@ describe('useGetSchemaInitializerMenuItems', () => {
|
||||
"key": "parent-2-item1-0",
|
||||
"label": "item1",
|
||||
"onClick": [Function],
|
||||
"style": undefined,
|
||||
},
|
||||
{
|
||||
"key": "parent-2-item2-1",
|
||||
"label": "item2",
|
||||
"onClick": [Function],
|
||||
"style": undefined,
|
||||
},
|
||||
{
|
||||
"associationField": "a.b",
|
||||
@ -139,6 +141,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
|
||||
"key": "group-0-Item 1-0",
|
||||
"label": "Item 1",
|
||||
"onClick": [Function],
|
||||
"style": undefined,
|
||||
},
|
||||
],
|
||||
"key": "group-0",
|
||||
@ -151,6 +154,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
|
||||
"key": "parent-item-group-1-Item 1-0",
|
||||
"label": "Item 1",
|
||||
"onClick": [Function],
|
||||
"style": undefined,
|
||||
},
|
||||
],
|
||||
"key": "parent-item-group-1",
|
||||
@ -204,6 +208,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
|
||||
"key": "submenu-1-SubItem 1-0",
|
||||
"label": "SubItem 1",
|
||||
"onClick": [Function],
|
||||
"style": undefined,
|
||||
},
|
||||
],
|
||||
"key": "submenu-1",
|
||||
@ -215,6 +220,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
|
||||
"key": "submenu-2-SubItem 1-0",
|
||||
"label": "SubItem 1",
|
||||
"onClick": [Function],
|
||||
"style": undefined,
|
||||
},
|
||||
],
|
||||
"key": "submenu-2",
|
||||
@ -226,6 +232,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
|
||||
"key": "submenu-3-SubItem 1-0",
|
||||
"label": "SubItem 1",
|
||||
"onClick": [Function],
|
||||
"style": undefined,
|
||||
},
|
||||
],
|
||||
"key": "submenu-3",
|
||||
@ -289,11 +296,13 @@ describe('useSchemaInitializerMenuItems', () => {
|
||||
"key": 1,
|
||||
"label": "item1",
|
||||
"onClick": [Function],
|
||||
"style": undefined,
|
||||
},
|
||||
{
|
||||
"key": 2,
|
||||
"label": "item2",
|
||||
"onClick": [Function],
|
||||
"style": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -15,7 +15,7 @@ import { SchemaInitializerOptions } from '../types';
|
||||
import { SchemaInitializerChildren } from './SchemaInitializerChildren';
|
||||
import { SchemaInitializerDivider } from './SchemaInitializerDivider';
|
||||
import { useSchemaInitializerStyles } from './style';
|
||||
|
||||
import { useMenuSearch } from './SchemaInitializerItemSearchFields';
|
||||
export interface SchemaInitializerItemGroupProps {
|
||||
title: string;
|
||||
children?: SchemaInitializerOptions['items'];
|
||||
@ -45,6 +45,8 @@ export const SchemaInitializerItemGroup: FC<SchemaInitializerItemGroupProps> = (
|
||||
* @internal
|
||||
*/
|
||||
export const SchemaInitializerItemGroupInternal = () => {
|
||||
const itemConfig = useSchemaInitializerItem<SchemaInitializerItemGroupProps>();
|
||||
return <SchemaInitializerItemGroup {...itemConfig} />;
|
||||
const itemConfig: any = useSchemaInitializerItem<SchemaInitializerItemGroupProps>();
|
||||
const searchedChildren = useMenuSearch(itemConfig);
|
||||
/* eslint-disable react/no-children-prop */
|
||||
return <SchemaInitializerItemGroup {...itemConfig} children={searchedChildren} />;
|
||||
};
|
||||
|
@ -0,0 +1,180 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { uid } from '@formily/shared';
|
||||
import { Divider, Empty, Input, MenuProps } from 'antd';
|
||||
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function getPrefixAndCompare(a, b) {
|
||||
const prefixA = a.replace(/-displayCollectionFields$/, '');
|
||||
const prefixB = b.replace(/-displayCollectionFields$/, '');
|
||||
|
||||
// 判断 a 是否包含 b,如果包含则返回 false,否则返回 true
|
||||
return !prefixA.includes(prefixB);
|
||||
}
|
||||
|
||||
export const SearchFields = ({ value: outValue, onChange, name }) => {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState<string>(outValue);
|
||||
const inputRef = useRef<any>('');
|
||||
|
||||
// 生成唯一的ID用于区分不同层级的SearchFields
|
||||
const uniqueId = useRef(`${name || Math.random().toString(10).substr(2, 9)}`);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(outValue);
|
||||
}, [outValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const focusInput = () => {
|
||||
if (
|
||||
document.activeElement?.id !== inputRef.current.input.id &&
|
||||
getPrefixAndCompare(document.activeElement?.id, inputRef.current.input.id)
|
||||
) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// 观察当前元素是否在视图中
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries.some((v) => v.isIntersecting)) {
|
||||
focusInput();
|
||||
}
|
||||
});
|
||||
if (inputRef.current?.input) {
|
||||
inputRef.current.input.id = uniqueId.current; // 设置唯一ID
|
||||
observer.observe(inputRef.current.input);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const compositionRef = useRef<boolean>(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!compositionRef.current) {
|
||||
onChange(e.target.value);
|
||||
setValue(e.target.value);
|
||||
}
|
||||
};
|
||||
const Composition = (e: React.CompositionEvent<HTMLInputElement> | any) => {
|
||||
if (e.type === 'compositionend') {
|
||||
compositionRef.current = false;
|
||||
handleChange(e);
|
||||
} else {
|
||||
compositionRef.current = true;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
allowClear
|
||||
style={{ padding: '0 4px 6px 16px', boxShadow: 'none' }}
|
||||
bordered={false}
|
||||
placeholder={t('Search')}
|
||||
defaultValue={value}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onCompositionStart={Composition}
|
||||
onCompositionEnd={Composition}
|
||||
onCompositionUpdate={Composition}
|
||||
/>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMenuSearch = (props: { children: any[]; showType?: boolean; hideSearch?: boolean; name?: string }) => {
|
||||
const { children, showType, hideSearch, name } = props;
|
||||
const items = children?.concat?.() || [];
|
||||
const [searchValue, setSearchValue] = useState(null);
|
||||
|
||||
// 处理搜索逻辑
|
||||
const limitedSearchedItems = useMemo(() => {
|
||||
if (!searchValue || searchValue === '') {
|
||||
return items;
|
||||
}
|
||||
const lowerSearchValue = searchValue.toLocaleLowerCase();
|
||||
return items.filter(
|
||||
(item) =>
|
||||
(item.label || item.title) &&
|
||||
String(item.label || item.title)
|
||||
.toLocaleLowerCase()
|
||||
.includes(lowerSearchValue),
|
||||
);
|
||||
}, [searchValue, items]);
|
||||
|
||||
// 最终结果项
|
||||
const resultItems = useMemo<MenuProps['items']>(() => {
|
||||
const res = [];
|
||||
if (!hideSearch && (items.length > 10 || searchValue)) {
|
||||
res.push({
|
||||
key: `search-${uid()}`,
|
||||
Component: () => (
|
||||
<SearchFields
|
||||
name={name}
|
||||
value={searchValue}
|
||||
onChange={(val: string) => {
|
||||
setSearchValue(val);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
onClick({ domEvent }) {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
...(showType ? { isMenuType: true } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
if (limitedSearchedItems.length > 0) {
|
||||
res.push(...limitedSearchedItems);
|
||||
} else {
|
||||
res.push({
|
||||
key: 'empty',
|
||||
style: {
|
||||
height: 150,
|
||||
},
|
||||
Component: () => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</div>
|
||||
),
|
||||
...(showType ? { isMenuType: true } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
}, [hideSearch, limitedSearchedItems, searchValue, showType]);
|
||||
|
||||
const result = processedResult(resultItems, showType, hideSearch, name);
|
||||
|
||||
return children ? result : undefined;
|
||||
};
|
||||
|
||||
// 处理嵌套子菜单
|
||||
const processedResult = (resultItems, showType, hideSearch, name) => {
|
||||
return resultItems.map((item: any) => {
|
||||
if (['subMenu', 'itemGroup'].includes(item.type)) {
|
||||
const childItems = useMenuSearch({
|
||||
children: item.children,
|
||||
showType,
|
||||
hideSearch,
|
||||
name: item.name,
|
||||
});
|
||||
return { ...item, children: childItems };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
@ -101,6 +101,7 @@ export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void)
|
||||
onClick: handleClick,
|
||||
}
|
||||
: {
|
||||
style: item.style,
|
||||
key,
|
||||
label,
|
||||
onClick: handleClick,
|
||||
|
@ -189,10 +189,6 @@ export class Collection<
|
||||
return this.model.primaryKeyAttribute;
|
||||
}
|
||||
|
||||
isMultiFilterTargetKey() {
|
||||
return Array.isArray(this.filterTargetKey) && this.filterTargetKey.length > 1;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.options.name;
|
||||
}
|
||||
@ -225,6 +221,10 @@ export class Collection<
|
||||
}
|
||||
}
|
||||
|
||||
isMultiFilterTargetKey() {
|
||||
return Array.isArray(this.filterTargetKey) && this.filterTargetKey.length > 1;
|
||||
}
|
||||
|
||||
tableName() {
|
||||
const { name, tableName } = this.options;
|
||||
const tName = tableName || name;
|
||||
@ -313,6 +313,20 @@ export class Collection<
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(this.model, 'primaryKeyField', {
|
||||
get: function () {
|
||||
if (this.primaryKeyAttribute) {
|
||||
return this.rawAttributes[this.primaryKeyAttribute].field || this.primaryKeyAttribute;
|
||||
}
|
||||
|
||||
return null;
|
||||
}.bind(this.model),
|
||||
|
||||
set(val) {
|
||||
this._primaryKeyField = val;
|
||||
},
|
||||
});
|
||||
|
||||
this.model.init(null, this.sequelizeModelOptions());
|
||||
|
||||
this.model.options.modelName = this.options.name;
|
||||
@ -639,9 +653,14 @@ export class Collection<
|
||||
updateOptions(options: CollectionOptions, mergeOptions?: any) {
|
||||
let newOptions = lodash.cloneDeep(options);
|
||||
newOptions = merge(this.options, newOptions, mergeOptions);
|
||||
this.context.database.emit('beforeUpdateCollection', this, newOptions);
|
||||
this.options = newOptions;
|
||||
|
||||
if (options.filterTargetKey) {
|
||||
newOptions.filterTargetKey = options.filterTargetKey;
|
||||
}
|
||||
|
||||
this.context.database.emit('beforeUpdateCollection', this, newOptions);
|
||||
|
||||
this.options = newOptions;
|
||||
this.setFields(options.fields, false);
|
||||
if (options.repository) {
|
||||
this.setRepository(options.repository);
|
||||
|
@ -547,7 +547,7 @@ export class PluginACLServer extends Plugin {
|
||||
|
||||
const hasFilterByTk = (params) => {
|
||||
return JSON.stringify(params).includes('filterByTk');
|
||||
}
|
||||
};
|
||||
|
||||
if (!hasFilterByTk(ctx.permission.mergedParams) || !hasFilterByTk(ctx.permission.rawParams)) {
|
||||
await next();
|
||||
@ -574,7 +574,6 @@ export class PluginACLServer extends Plugin {
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
const withACLMeta = createWithACLMetaMiddleware();
|
||||
|
||||
// append allowedActions to list & get response
|
||||
|
@ -8,12 +8,11 @@
|
||||
*/
|
||||
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { NAMESPACE } from '../constants';
|
||||
import { Configuration } from './Configuration';
|
||||
|
||||
export class PluginAPIKeysClient extends Plugin {
|
||||
async load() {
|
||||
this.pluginSettingsManager.add(NAMESPACE, {
|
||||
this.pluginSettingsManager.add('api-keys', {
|
||||
icon: 'KeyOutlined',
|
||||
title: this.t('API keys'),
|
||||
Component: Configuration,
|
||||
|
@ -47,11 +47,18 @@ export default class extends Migration {
|
||||
},
|
||||
],
|
||||
};
|
||||
if (treeCollection.options.schema) {
|
||||
collectionOptions['schema'] = treeCollection.options.schema;
|
||||
|
||||
const collectionInstance = this.db.getCollection(treeCollection.name);
|
||||
const treeCollectionSchema = collectionInstance.collectionSchema();
|
||||
|
||||
if (this.app.db.inDialect('postgres') && treeCollectionSchema != this.app.db.options.schema) {
|
||||
collectionOptions['schema'] = treeCollectionSchema;
|
||||
}
|
||||
|
||||
this.app.db.collection(collectionOptions);
|
||||
|
||||
const treeExistsInDb = await this.app.db.getCollection(name).existsInDb({ transaction });
|
||||
|
||||
if (!treeExistsInDb) {
|
||||
await this.app.db.getCollection(name).sync({ transaction } as SyncOptions);
|
||||
const opts = {
|
||||
@ -63,9 +70,11 @@ export default class extends Migration {
|
||||
{ type: 'integer', name: 'parentId' },
|
||||
],
|
||||
};
|
||||
if (treeCollection.options.schema) {
|
||||
opts['schema'] = treeCollection.options.schema;
|
||||
|
||||
if (treeCollectionSchema != this.app.db.options.schema) {
|
||||
opts['schema'] = treeCollectionSchema;
|
||||
}
|
||||
|
||||
this.app.db.collection(opts);
|
||||
const chunkSize = 1000;
|
||||
await this.app.db.getRepository(treeCollection.name).chunk({
|
||||
|
@ -7,10 +7,11 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import Database, { CollectionGroupManager, Collection as DBCollection, HasManyRepository } from '@nocobase/database';
|
||||
import Database, { Collection as DBCollection, CollectionGroupManager, HasManyRepository } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '.';
|
||||
import CollectionManagerPlugin, { CollectionRepository } from '../index';
|
||||
import { CollectionRepository } from '../index';
|
||||
import { isPg } from '@nocobase/test';
|
||||
|
||||
describe('collections repository', () => {
|
||||
let db: Database;
|
||||
@ -26,6 +27,7 @@ describe('collections repository', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllEnvs();
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
@ -380,13 +382,8 @@ describe('collections repository', () => {
|
||||
expect(afterRepository.load).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set collection schema from env', async () => {
|
||||
if (!db.inDialect('postgres')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const plugin = app.getPlugin<CollectionManagerPlugin>('data-source-main');
|
||||
plugin.schema = 'testSchema';
|
||||
it.runIf(isPg())('should set collection schema from env', async () => {
|
||||
vi.stubEnv('COLLECTION_MANAGER_SCHEMA', 'testSchema');
|
||||
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
|
@ -206,18 +206,20 @@ describe('belongsToMany', () => {
|
||||
context: {},
|
||||
});
|
||||
|
||||
const throughCollection = await Collection.repository.findOne({
|
||||
const throughCollectionRecord = await Collection.repository.findOne({
|
||||
filter: {
|
||||
name: 'post_tags',
|
||||
},
|
||||
});
|
||||
|
||||
expect(throughCollection.get('sortable')).toEqual(false);
|
||||
expect(throughCollectionRecord.get('sortable')).toEqual(false);
|
||||
const collectionManagerSchema = process.env.COLLECTION_MANAGER_SCHEMA;
|
||||
const mainSchema = process.env.DB_SCHEMA || 'public';
|
||||
|
||||
const throughCollection = db.getCollection('post_tags');
|
||||
|
||||
if (collectionManagerSchema && mainSchema != collectionManagerSchema && db.inDialect('postgres')) {
|
||||
expect(throughCollection.get('schema')).toEqual(collectionManagerSchema);
|
||||
expect(throughCollection.options.schema).toEqual(collectionManagerSchema);
|
||||
|
||||
const tableName = db.getCollection('post_tags').model.tableName;
|
||||
|
||||
|
@ -63,6 +63,10 @@ export class CollectionModel extends MagicAttributeModel {
|
||||
delete collectionOptions.schema;
|
||||
}
|
||||
|
||||
if (this.db.inDialect('postgres') && !collectionOptions.schema && collectionOptions.from !== 'db2cm') {
|
||||
collectionOptions.schema = process.env.COLLECTION_MANAGER_SCHEMA || this.db.options.schema || 'public';
|
||||
}
|
||||
|
||||
if (this.db.hasCollection(name)) {
|
||||
collection = this.db.getCollection(name);
|
||||
|
||||
|
@ -13,7 +13,6 @@ import { Plugin } from '@nocobase/server';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import lodash from 'lodash';
|
||||
import path from 'path';
|
||||
import * as process from 'process';
|
||||
import { CollectionRepository } from '.';
|
||||
import {
|
||||
afterCreateForForeignKeyField,
|
||||
@ -33,8 +32,6 @@ import { FieldIsDependedOnByOtherError } from './errors/field-is-depended-on-by-
|
||||
import { beforeCreateCheckFieldInMySQL } from './hooks/beforeCreateCheckFieldInMySQL';
|
||||
|
||||
export class PluginDataSourceMainServer extends Plugin {
|
||||
public schema: string;
|
||||
|
||||
private loadFilter: Filter = {};
|
||||
|
||||
setLoadFilter(filter: Filter) {
|
||||
@ -55,10 +52,6 @@ export class PluginDataSourceMainServer extends Plugin {
|
||||
}
|
||||
|
||||
async beforeLoad() {
|
||||
if (this.app.db.inDialect('postgres')) {
|
||||
this.schema = process.env.COLLECTION_MANAGER_SCHEMA || this.db.options.schema || 'public';
|
||||
}
|
||||
|
||||
this.app.db.registerRepositories({
|
||||
CollectionRepository,
|
||||
});
|
||||
@ -76,12 +69,6 @@ export class PluginDataSourceMainServer extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.app.db.on('collections.beforeCreate', async (model) => {
|
||||
if (this.app.db.inDialect('postgres') && this.schema && model.get('from') != 'db2cm' && !model.get('schema')) {
|
||||
model.set('schema', this.schema);
|
||||
}
|
||||
});
|
||||
|
||||
this.app.db.on('collections.beforeCreate', beforeCreateForViewCollection(this.db));
|
||||
|
||||
this.app.db.on(
|
||||
|
@ -363,7 +363,9 @@ export class PluginDataSourceManagerServer extends Plugin {
|
||||
|
||||
this.app.db.on('dataSourcesCollections.afterDestroy', async (model: DataSourcesCollectionModel) => {
|
||||
const dataSource = this.app.dataSourceManager.dataSources.get(model.get('dataSourceKey'));
|
||||
dataSource.collectionManager.removeCollection(model.get('name'));
|
||||
if (dataSource) {
|
||||
dataSource.collectionManager.removeCollection(model.get('name'));
|
||||
}
|
||||
});
|
||||
|
||||
this.app.db.on('dataSourcesFields.afterSaveWithAssociations', async (model: DataSourcesFieldModel) => {
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { BelongsToManyRepository, Database } from '@nocobase/database';
|
||||
import { AppSupervisor } from '@nocobase/server';
|
||||
import { MockServer, createMockServer, isPg } from '@nocobase/test';
|
||||
import { createMockServer, isPg, MockServer } from '@nocobase/test';
|
||||
import * as process from 'process';
|
||||
|
||||
describe.runIf(isPg())('enable plugin', () => {
|
||||
@ -491,6 +491,8 @@ describe.runIf(isPg())('collection sync', () => {
|
||||
context: {},
|
||||
});
|
||||
|
||||
const mainCollectionInstance = mainDb.getCollection('mainCollection');
|
||||
|
||||
await subApp1.runCommand('restart');
|
||||
|
||||
const subAppMainCollectionRecord = await subApp1.db.getRepository('collections').findOne({
|
||||
@ -504,7 +506,7 @@ describe.runIf(isPg())('collection sync', () => {
|
||||
const subAppMainCollection = subApp1.db.getCollection('mainCollection');
|
||||
|
||||
expect(subAppMainCollection).toBeTruthy();
|
||||
expect(subAppMainCollection.options.schema).toBe(mainCollection.options.schema || 'public');
|
||||
expect(subAppMainCollection.options.schema).toBe(mainCollectionInstance.collectionSchema());
|
||||
|
||||
await mainApp.db.getRepository('fields').create({
|
||||
values: {
|
||||
|
@ -60,4 +60,5 @@ export class PluginNotificationManagerClient extends Plugin {
|
||||
export { NotificationVariableContext, NotificationVariableProvider, useNotificationVariableOptions } from './hooks';
|
||||
export { MessageConfigForm } from './manager/message/components/MessageConfigForm';
|
||||
export { ContentConfigForm } from './manager/message/components/ContentConfigForm';
|
||||
export { UserSelect, UserAddition } from './manager/receiver/components/User';
|
||||
export default PluginNotificationManagerClient;
|
||||
|
@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ArrayItems } from '@formily/antd-v5';
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import { observer, useField } from '@formily/react';
|
||||
import { useAPIClient } from '@nocobase/client';
|
||||
@ -75,7 +76,9 @@ export const MessageConfigForm = observer<{ variableOptions: any }>(
|
||||
},
|
||||
},
|
||||
};
|
||||
return <SchemaComponent schema={createMessageFormSchema} components={{ MessageConfigForm }} scope={{ t }} />;
|
||||
return (
|
||||
<SchemaComponent schema={createMessageFormSchema} components={{ MessageConfigForm, ArrayItems }} scope={{ t }} />
|
||||
);
|
||||
},
|
||||
{ displayName: 'MessageConfigForm' },
|
||||
);
|
||||
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useField, useForm } from '@formily/react';
|
||||
import { ArrayField as ArrayFieldModel } from '@formily/core';
|
||||
import { ArrayItems } from '@formily/antd-v5';
|
||||
import { Button, Popover, Space } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useNotificationTranslation } from '../../../../locale';
|
||||
|
||||
export default function UsersAddition() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { t } = useNotificationTranslation();
|
||||
const array = ArrayItems.useArray();
|
||||
const form = useForm();
|
||||
const disabled = form?.disabled === true;
|
||||
|
||||
const onAddSelect = useCallback(() => {
|
||||
array.field.push('');
|
||||
setOpen(false);
|
||||
}, [array.field]);
|
||||
|
||||
const onAddQuery = useCallback(() => {
|
||||
array.field.push({ filter: {} });
|
||||
setOpen(false);
|
||||
}, [array.field]);
|
||||
|
||||
const button = (
|
||||
<Button icon={<PlusOutlined />} type="dashed" block disabled={disabled} className="ant-formily-array-base-addition">
|
||||
{t('Add user')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return disabled ? (
|
||||
button
|
||||
) : (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
content={
|
||||
<Space direction="vertical" size="small">
|
||||
<Button type="text" onClick={onAddSelect}>
|
||||
{t('Select users')}
|
||||
</Button>
|
||||
<Button type="text" onClick={onAddQuery}>
|
||||
{t('Query users')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{button}
|
||||
</Popover>
|
||||
);
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This program is offered under a commercial license.
|
||||
* For more information, see <https://www.nocobase.com/agreement>
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { RemoteSelect, SchemaComponent, Variable, useCollectionFilterOptions, useToken } from '@nocobase/client';
|
||||
import { useField } from '@formily/react';
|
||||
|
||||
function InternalUsersSelect({ value, onChange, variableOptions }) {
|
||||
return (
|
||||
<Variable.Input scope={variableOptions} value={value} onChange={onChange}>
|
||||
<RemoteSelect
|
||||
fieldNames={{
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
}}
|
||||
service={{
|
||||
resource: 'users',
|
||||
}}
|
||||
manual={false}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Variable.Input>
|
||||
);
|
||||
}
|
||||
|
||||
function UsersQuery(props) {
|
||||
const field = useField<any>();
|
||||
const options = useCollectionFilterOptions('users');
|
||||
const { token } = useToken();
|
||||
const FilterDynamicComponent = useCallback(
|
||||
({ value, onChange, renderSchemaComponent }) => {
|
||||
return (
|
||||
<Variable.Input value={value} onChange={onChange} scope={props.variableOptions}>
|
||||
{renderSchemaComponent()}
|
||||
</Variable.Input>
|
||||
);
|
||||
},
|
||||
[props.variableOptions],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: `1px dashed ${token.colorBorder}`,
|
||||
padding: token.paddingSM,
|
||||
}}
|
||||
>
|
||||
<SchemaComponent
|
||||
basePath={field.address}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
filter: {
|
||||
type: 'object',
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {
|
||||
options,
|
||||
dynamicComponent: FilterDynamicComponent,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserSelect(props) {
|
||||
const valueType = typeof props.value;
|
||||
|
||||
return valueType === 'object' && props.value ? <UsersQuery {...props} /> : <InternalUsersSelect {...props} />;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export { default as UserSelect } from './UserSelect';
|
||||
export { default as UserAddition } from './UserAddition';
|
@ -95,7 +95,7 @@ export default {
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
type: 'string',
|
||||
type: 'text',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
|
@ -7,7 +7,10 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export { default } from './server';
|
||||
export { BaseNotificationChannel } from './server/base-notification-channel';
|
||||
|
||||
export * from './server';
|
||||
export {
|
||||
default,
|
||||
COLLECTION_NAME,
|
||||
ChannelsCollectionDefinition,
|
||||
BaseNotificationChannel,
|
||||
parseUserSelectionConfig,
|
||||
} from './server';
|
||||
|
@ -10,5 +10,6 @@
|
||||
export { BaseNotificationChannel } from './base-notification-channel';
|
||||
export { default } from './plugin';
|
||||
export { COLLECTION_NAME, ChannelsCollectionDefinition } from '../constant';
|
||||
export { parseUserSelectionConfig } from './utils/parseUserSelectionConfig';
|
||||
|
||||
export * from './types';
|
||||
|
@ -61,7 +61,7 @@ export class NotificationManager implements NotificationManager {
|
||||
return logData;
|
||||
} catch (error) {
|
||||
logData.status = 'failure';
|
||||
this.plugin.logger.error('notification send failed', JSON.stringify(error));
|
||||
this.plugin.logger.error(`notification send failed, options: ${JSON.stringify(error)}`);
|
||||
logData.reason = JSON.stringify(error);
|
||||
this.createSendingRecord(logData);
|
||||
return logData;
|
||||
@ -69,7 +69,7 @@ export class NotificationManager implements NotificationManager {
|
||||
}
|
||||
async sendToUsers(options: SendUserOptions) {
|
||||
const { userIds, channels, message, data } = options;
|
||||
this.plugin.logger.info('notificationManager.sendToUsers options', JSON.stringify(options));
|
||||
this.plugin.logger.info(`notificationManager.sendToUsers options: ${JSON.stringify(options)}`);
|
||||
return await Promise.all(
|
||||
channels.map((channelName) =>
|
||||
this.send({ channelName, message, triggerFrom: 'sendToUsers', receivers: { value: userIds, type: 'userId' } }),
|
||||
|
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Repository } from '@nocobase/database';
|
||||
export async function parseUserSelectionConfig(
|
||||
userSelectionConfig: Array<Record<any, any> | string>,
|
||||
UserRepo: Repository,
|
||||
) {
|
||||
const SelectionConfigs = userSelectionConfig.flat().filter(Boolean);
|
||||
const users = new Set<string>();
|
||||
for (const item of SelectionConfigs) {
|
||||
if (typeof item === 'object') {
|
||||
const result = await UserRepo.find({
|
||||
...item,
|
||||
fields: ['id'],
|
||||
});
|
||||
result.forEach((item) => users.add(item.id));
|
||||
} else {
|
||||
users.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [...users];
|
||||
}
|
Loading…
Reference in New Issue
Block a user