Merge branch 'next' into 730

This commit is contained in:
Zeke Zhang 2024-11-01 15:50:25 +08:00
commit fcbe4496ce
25 changed files with 464 additions and 54 deletions

View File

@ -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

View File

@ -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,
},
]
`);

View File

@ -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} />;
};

View File

@ -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;
});
};

View File

@ -101,6 +101,7 @@ export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void)
onClick: handleClick,
}
: {
style: item.style,
key,
label,
onClick: handleClick,

View File

@ -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);

View File

@ -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

View File

@ -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,

View File

@ -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({

View File

@ -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: {

View File

@ -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;

View File

@ -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);

View File

@ -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(

View File

@ -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) => {

View File

@ -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: {

View File

@ -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;

View File

@ -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' },
);

View File

@ -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>
);
}

View File

@ -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} />;
}

View File

@ -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';

View File

@ -95,7 +95,7 @@ export default {
},
{
name: 'reason',
type: 'string',
type: 'text',
interface: 'input',
uiSchema: {
type: 'string',

View File

@ -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';

View File

@ -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';

View File

@ -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' } }),

View File

@ -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];
}