feat(users): allows to custom profile editing form (#5698)

* feat(users): allows to custom profile editing form

* chore: update

* fix: build

* fix: build

* fix: build

* fix: migration

* chore: update

* feat: parse schema

* feat: trigger workflows

* fix: test

* fix: test

* fix: issues

* fix: user menu

* fix: user menu

* fix: e2e

* fix: z-index

* fix: bug

* fix: designable

* fix: required validation

* fix: test

* fix: forms

* fix: version

* fix: designable
This commit is contained in:
YANG QIA 2024-12-23 22:23:18 +08:00 committed by GitHub
parent b015e69cfb
commit a15a2a9540
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1021 additions and 247 deletions

View File

@ -84,7 +84,7 @@ test.describe('z-index of dialog', () => {
await expect(page.getByTestId('drawer-Action.Drawer-Edit profile')).toBeVisible();
// click the Cancel button to close the drawer
await page.getByLabel('action-Action-Cancel').click();
await page.getByLabel('drawer-Action.Drawer-Edit profile-mask').click();
await expect(page.getByTestId('drawer-Action.Drawer-Edit profile')).not.toBeVisible();
});

View File

@ -175,4 +175,27 @@ ActionDrawer.Footer = observer(
{ displayName: 'ActionDrawer.Footer' },
);
ActionDrawer.FootBar = observer(
() => {
const field = useField();
const schema = useFieldSchema();
return (
<div
className="ant-drawer-footer"
style={{
position: 'absolute',
bottom: 0,
right: 0,
left: 0,
}}
>
<div className="footer">
<MemoizeRecursionField basePath={field.address} schema={schema} onlyRenderProperties />
</div>
</div>
);
},
{ displayName: 'ActionDrawer.FootBar' },
);
export default ActionDrawer;

View File

@ -96,4 +96,5 @@ export type ActionDrawerProps<T = DrawerProps> = T & {
export type ComposedActionDrawer<T = DrawerProps> = React.FC<ActionDrawerProps<T>> & {
Footer?: React.FC;
FootBar?: React.FC;
};

View File

@ -125,10 +125,11 @@ export const SettingsMenu: React.FC<{
},
editProfile,
changePassword,
(editProfile || changePassword) && {
key: 'divider_2',
type: 'divider',
},
editProfile ||
(changePassword && {
key: 'divider_2',
type: 'divider',
}),
switchRole,
{
key: 'divider_3',
@ -160,9 +161,9 @@ export const SettingsMenu: React.FC<{
}, [
addMenuItem,
api.auth,
editProfile,
changePassword,
controlApp,
editProfile,
languageSettings,
navigate,
redirectUrl,

View File

@ -7,133 +7,113 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ISchema, useForm } from '@formily/react';
import { useField, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { MenuProps } from 'antd';
import React, { useContext, useMemo, useState } from 'react';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActionContextProvider,
DropdownVisibleContext,
ExtendCollectionsProvider,
RemoteSchemaComponent,
SchemaComponent,
useActionContext,
useCollectValuesToSubmit,
useCollectionManager,
useCurrentUserContext,
useRequest,
useSystemSettings,
} from '../';
import { useAPIClient } from '../api-client';
const useCloseAction = () => {
const { setVisible } = useActionContext();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const useCurrentUserValues = (options) => {
const ctx = useCurrentUserContext();
return useRequest(() => Promise.resolve(ctx.data), options);
};
const useSaveCurrentUserValues = () => {
const useUpdateProfileActionProps = () => {
const ctx = useCurrentUserContext();
const { setVisible } = useActionContext();
const form = useForm();
const api = useAPIClient();
const actionSchema = useFieldSchema();
const actionField = useField();
const collectValues = useCollectValuesToSubmit();
return {
async run() {
const values = await form.submit<any>();
setVisible(false);
await api.resource('users').updateProfile({
values,
});
ctx.mutate({
data: {
...ctx?.data?.data,
...values,
},
});
type: 'primary',
htmlType: 'submit',
async onClick() {
const { triggerWorkflows, skipValidator } = actionSchema?.['x-action-settings'] ?? {};
if (!skipValidator) {
await form.submit();
}
const values = await collectValues();
actionField.data = actionField.data || {};
actionField.data.loading = true;
try {
await api.resource('users').updateProfile({
values,
triggerWorkflows: triggerWorkflows?.length
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
: undefined,
});
ctx.mutate({
data: {
...ctx?.data?.data,
...values,
},
});
await form.reset();
actionField.data.loading = false;
setVisible(false);
} catch (error) {
actionField.data.loading = false;
}
},
};
};
const schema: ISchema = {
type: 'object',
properties: {
[uid()]: {
'x-decorator': 'Form',
'x-decorator-props': {
useValues: '{{ useCurrentUserValues }}',
},
'x-component': 'Action.Drawer',
'x-component-props': {
zIndex: 10000,
},
type: 'void',
title: '{{t("Edit profile")}}',
properties: {
nickname: {
type: 'string',
title: "{{t('Nickname')}}",
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-disabled': '{{ enableEditProfile === false }}',
},
username: {
type: 'string',
title: '{{t("Username")}}',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': { username: true },
required: true,
'x-disabled': '{{ enableEditProfile === false }}',
},
email: {
type: 'string',
title: '{{t("Email")}}',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'email',
'x-disabled': '{{ enableEditProfile === false }}',
},
phone: {
type: 'string',
title: '{{t("Phone")}}',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-disabled': '{{ enableEditProfile === false }}',
},
footer: {
'x-component': 'Action.Drawer.Footer',
type: 'void',
properties: {
cancel: {
title: 'Cancel',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCloseAction }}',
},
},
submit: {
title: 'Submit',
'x-component': 'Action',
'x-disabled': '{{ enableEditProfile === false }}',
'x-component-props': {
type: 'primary',
useAction: '{{ useSaveCurrentUserValues }}',
},
},
},
},
},
const useEditProfileFormBlockDecoratorProps = () => {
const { data } = useCurrentUserContext();
return {
filterByTk: data.data?.id,
};
};
const useCancelActionProps = () => {
const { setVisible } = useActionContext();
return {
type: 'default',
onClick() {
setVisible(false);
},
},
};
};
const ProfileEditForm = () => {
const ctx = useContext(DropdownVisibleContext);
const cm = useCollectionManager();
const userCollection = cm.getCollection('users');
const collection = useMemo(
() => ({
...userCollection,
name: 'users',
fields: userCollection.fields.filter((field) => !['password', 'roles'].includes(field.name)),
}),
[userCollection],
);
useEffect(() => {
ctx?.setVisible(false);
}, [ctx]);
return (
<ExtendCollectionsProvider collections={[collection]}>
<RemoteSchemaComponent
uid="nocobase-user-profile-edit-form"
noForm={true}
scope={{
useUpdateProfileActionProps,
useEditFormBlockDecoratorProps: useEditProfileFormBlockDecoratorProps,
useCancelActionProps,
}}
/>
</ExtendCollectionsProvider>
);
};
export const useEditProfile = () => {
@ -147,8 +127,8 @@ export const useEditProfile = () => {
key: 'profile',
eventKey: 'EditProfile',
onClick: () => {
setVisible(true);
ctx?.setVisible(false);
setVisible(true);
},
label: (
<div>
@ -156,8 +136,26 @@ export const useEditProfile = () => {
<ActionContextProvider value={{ visible, setVisible }}>
<div onClick={(e) => e.stopPropagation()}>
<SchemaComponent
scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues, enableEditProfile }}
schema={schema}
components={{ ProfileEditForm }}
schema={{
type: 'object',
properties: {
[uid()]: {
'x-component': 'Action.Drawer',
'x-component-props': {
zIndex: 2000,
},
type: 'void',
title: '{{t("Edit profile")}}',
properties: {
form: {
type: 'void',
'x-component': 'ProfileEditForm',
},
},
},
},
}}
/>
</div>
</ActionContextProvider>

View File

@ -18,7 +18,6 @@
"@nocobase/client": "1.x",
"@nocobase/database": "1.x",
"@nocobase/plugin-error-handler": "1.x",
"@nocobase/plugin-users": "1.x",
"@nocobase/resourcer": "1.x",
"@nocobase/server": "1.x",
"@nocobase/test": "1.x",

View File

@ -20,6 +20,7 @@
"@nocobase/plugin-acl": "1.x",
"@nocobase/plugin-auth": "1.x",
"@nocobase/plugin-user-data-sync": "1.x",
"@nocobase/plugin-ui-schema-storage": "1.x",
"@nocobase/resourcer": "1.x",
"@nocobase/server": "1.x",
"@nocobase/test": "1.x",

View File

@ -20,6 +20,9 @@ import {
useDataBlockRequest,
useDataBlockResource,
useRequest,
RemoteSchemaComponent,
useCollectionManager,
ExtendCollectionsProvider,
useSchemaComponentContext,
} from '@nocobase/client';
import { App, Tabs, message } from 'antd';
@ -50,6 +53,7 @@ const useSubmitActionProps = () => {
const collection = useCollection();
return {
htmlType: 'submit',
type: 'primary',
async onClick() {
await form.submit();
@ -84,19 +88,60 @@ const useEditFormProps = () => {
};
};
const UsersManagementTab: React.FC = () => {
const { t } = useUsersTranslation();
const ProfileCreateForm = () => {
return <RemoteSchemaComponent uid="nocobase-admin-profile-create-form" noForm={true} />;
};
const ProfileEditForm = () => {
const cm = useCollectionManager();
const userCollection = cm.getCollection('users');
const collection = {
...userCollection,
name: 'users',
fields: userCollection.fields.filter((field) => field.name !== 'password'),
};
return (
<ExtendCollectionsProvider collections={[collection]}>
<RemoteSchemaComponent uid="nocobase-admin-profile-edit-form" noForm={true} scope={{ useCancelActionProps }} />
</ExtendCollectionsProvider>
);
};
const FilterAction = () => {
const scCtx = useSchemaComponentContext();
return (
<SchemaComponentContext.Provider value={{ ...scCtx, designable: false }}>
<SchemaComponent
schema={usersSchema}
scope={{ t, useCancelActionProps, useSubmitActionProps, useEditFormProps }}
components={{ PasswordField }}
schema={{
type: 'void',
properties: {
filter: {
type: 'void',
title: '{{ t("Filter") }}',
'x-action': 'filter',
'x-component': 'Filter.Action',
'x-use-component-props': 'useFilterActionProps',
'x-component-props': {
icon: 'FilterOutlined',
},
},
},
}}
/>
</SchemaComponentContext.Provider>
);
};
const UsersManagementTab: React.FC = () => {
const { t } = useUsersTranslation();
return (
<SchemaComponent
schema={usersSchema}
scope={{ t, useCancelActionProps, useSubmitActionProps, useEditFormProps }}
components={{ PasswordField, ProfileEditForm, ProfileCreateForm, FilterAction }}
/>
);
};
const UsersSettingsContext = createContext<any>({});
const UsersSettingsProvider = (props) => {
@ -108,7 +153,6 @@ const UsersSettingsProvider = (props) => {
const UsersSettingsTab: React.FC = () => {
const { t } = useUsersTranslation();
const scCtx = useSchemaComponentContext();
const form = useForm();
const useFormBlockProps = () => {
const result = useContext(UsersSettingsContext);
@ -139,13 +183,11 @@ const UsersSettingsTab: React.FC = () => {
};
};
return (
<SchemaComponentContext.Provider value={{ ...scCtx, designable: false }}>
<SchemaComponent
schema={usersSettingsSchema}
scope={{ t, useFormBlockProps, useSubmitActionProps }}
components={{ UsersSettingsProvider }}
/>
</SchemaComponentContext.Provider>
<SchemaComponent
schema={usersSettingsSchema}
scope={{ t, useFormBlockProps, useSubmitActionProps }}
components={{ UsersSettingsProvider }}
/>
);
};

View File

@ -0,0 +1,198 @@
/**
* 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 {
ActionContextProvider,
DropdownVisibleContext,
ExtendCollectionsProvider,
RemoteSchemaComponent,
SchemaComponent,
useAPIClient,
useActionContext,
useCollectValuesToSubmit,
useCollectionManager,
useCurrentUserContext,
useCurrentUserSettingsMenu,
useSystemSettings,
} from '@nocobase/client';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { MenuProps } from 'antd';
import { useUsersTranslation } from './locale';
import { uid } from '@formily/shared';
import { useForm, useFieldSchema, useField } from '@formily/react';
const useUpdateProfileActionProps = () => {
const ctx = useCurrentUserContext();
const { setVisible } = useActionContext();
const form = useForm();
const api = useAPIClient();
const actionSchema = useFieldSchema();
const actionField = useField();
const collectValues = useCollectValuesToSubmit();
return {
type: 'primary',
htmlType: 'submit',
async onClick() {
const { triggerWorkflows, skipValidator } = actionSchema?.['x-action-settings'] ?? {};
if (!skipValidator) {
await form.submit();
}
const values = await collectValues();
actionField.data = actionField.data || {};
actionField.data.loading = true;
try {
await api.resource('users').updateProfile({
values,
triggerWorkflows: triggerWorkflows?.length
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
: undefined,
});
ctx.mutate({
data: {
...ctx?.data?.data,
...values,
},
});
await form.reset();
actionField.data.loading = false;
setVisible(false);
} catch (error) {
actionField.data.loading = false;
}
},
};
};
const ProfileEditForm = () => {
const cm = useCollectionManager();
const userCollection = cm.getCollection('users');
const { data } = useCurrentUserContext();
const collection = useMemo(
() => ({
...userCollection,
name: 'users',
fields: userCollection.fields.filter((field) => !['password', 'roles'].includes(field.name)),
}),
[userCollection],
);
return (
<ExtendCollectionsProvider collections={[collection]}>
<RemoteSchemaComponent
uid="nocobase-user-profile-edit-form"
noForm={true}
scope={{
useUpdateProfileActionProps,
currentUserId: data.data?.id,
}}
/>
</ExtendCollectionsProvider>
);
};
const EditProfile = ({ visible, setVisible }) => {
return (
<ActionContextProvider value={{ visible, setVisible }}>
<div onClick={(e) => e.stopPropagation()}>
<SchemaComponent
components={{ ProfileEditForm }}
schema={{
type: 'object',
properties: {
[uid()]: {
'x-component': 'Action.Drawer',
'x-component-props': {
// zIndex: 10000,
},
type: 'void',
title: '{{t("Edit profile")}}',
properties: {
form: {
type: 'void',
'x-component': 'ProfileEditForm',
},
},
},
},
}}
/>
</div>
</ActionContextProvider>
);
};
export const useEditProfile = () => {
const ctx = useContext(DropdownVisibleContext);
const [visible, setVisible] = useState(false);
const { t } = useUsersTranslation();
const { data } = useSystemSettings() || {};
const { enableEditProfile } = data?.data || {};
const result = useMemo<MenuProps['items'][0]>(() => {
return {
key: 'profile',
eventKey: 'EditProfile',
onClick: () => {
setVisible(true);
ctx?.setVisible(false);
},
label: (
<div>
{t('Edit profile')}
<ActionContextProvider value={{ visible, setVisible }}>
<div onClick={(e) => e.stopPropagation()}>
<SchemaComponent
components={{ ProfileEditForm }}
schema={{
type: 'object',
properties: {
[uid()]: {
'x-component': 'Action.Drawer',
'x-component-props': {
// zIndex: 10000,
},
type: 'void',
title: '{{t("Edit profile")}}',
properties: {
form: {
type: 'void',
'x-component': 'ProfileEditForm',
},
},
},
},
}}
/>
</div>
</ActionContextProvider>
</div>
),
};
}, [visible]);
if (enableEditProfile === false) {
return null;
}
return result;
};
// Adding a user settings menu here causes the drawer to fail to open.
// This provider will not be used for now.
export const UsersProvider: React.FC = (props) => {
const { addMenuItem } = useCurrentUserSettingsMenu();
const profileItem = useEditProfile();
const { data } = useSystemSettings();
const { enableEditProfile } = data?.data || {};
useEffect(() => {
if (enableEditProfile === false) {
return;
}
addMenuItem(profileItem, { after: 'divider_1' });
}, [addMenuItem, profileItem, enableEditProfile]);
return <>{props.children}</>;
};

View File

@ -9,10 +9,9 @@
import { Plugin } from '@nocobase/client';
import { tval } from '@nocobase/utils/client';
// import { UsersManagement } from './UsersManagement';
import ACLPlugin from '@nocobase/plugin-acl/client';
// import { RoleUsersManager } from './RoleUsersManager';
import { lazy } from '@nocobase/client';
const { UsersProvider } = lazy(() => import('./UsersProvider'), 'UsersProvider');
const { UsersManagement } = lazy(() => import('./UsersManagement'), 'UsersManagement');
const { RoleUsersManager } = lazy(() => import('./RoleUsersManager'), 'RoleUsersManager');

View File

@ -157,13 +157,7 @@ export const usersSchema: ISchema = {
properties: {
filter: {
type: 'void',
title: '{{ t("Filter") }}',
'x-action': 'filter',
'x-component': 'Filter.Action',
'x-use-component-props': 'useFilterActionProps',
'x-component-props': {
icon: 'FilterOutlined',
},
'x-component': 'FilterAction',
'x-align': 'left',
},
refresh: {
@ -204,55 +198,9 @@ export const usersSchema: ISchema = {
'x-decorator': 'FormV2',
title: '{{t("Add user")}}',
properties: {
nickname: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
username: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
email: {
title: '{{t("Email")}}',
'x-component': 'Input',
'x-validator': 'email',
'x-decorator': 'FormItem',
required: false,
},
phone: {
title: '{{t("Phone")}}',
'x-component': 'Input',
'x-decorator': 'FormItem',
required: false,
},
password: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
required: true,
},
roles: {
'x-component': 'CollectionField',
'x-collection-field': 'users.roles',
'x-decorator': 'FormItem',
},
footer: {
form: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-use-component-props': 'useCancelActionProps',
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
},
'x-use-component-props': 'useSubmitActionProps',
},
},
'x-component': 'ProfileCreateForm',
},
},
},
@ -352,54 +300,11 @@ export const usersSchema: ISchema = {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'FormV2',
'x-use-decorator-props': 'useEditFormProps',
title: '{{t("Edit profile")}}',
properties: {
nickname: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
username: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
email: {
title: '{{t("Email")}}',
'x-component': 'Input',
'x-validator': 'email',
'x-decorator': 'FormItem',
required: false,
},
phone: {
title: '{{t("Phone")}}',
'x-component': 'Input',
'x-decorator': 'FormItem',
required: false,
},
roles: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.roles',
},
footer: {
form: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-use-component-props': 'useCancelActionProps',
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
},
'x-use-component-props': 'useSubmitActionProps',
},
},
'x-component': 'ProfileEditForm',
},
},
},
@ -507,19 +412,8 @@ export const usersSettingsSchema: ISchema = {
default: true,
'x-content': '{{t("Allow change password")}}',
},
},
},
footer: {
type: 'void',
'x-component': 'div',
'x-component-props': {
style: {
float: 'right',
},
},
properties: {
submit: {
title: 'Submit',
title: '{{t("Save")}}',
'x-component': 'Action',
'x-use-component-props': 'useSubmitActionProps',
},

View File

@ -23,7 +23,7 @@ describe('actions', () => {
process.env.INIT_ROOT_PASSWORD = '123456';
process.env.INIT_ROOT_NICKNAME = 'Test';
app = await createMockServer({
plugins: ['field-sort', 'auth', 'users', 'acl', 'data-source-manager', 'system-settings'],
plugins: ['field-sort', 'auth', 'users', 'acl', 'data-source-manager', 'system-settings', 'ui-schema-storage'],
});
db = app.db;
@ -49,6 +49,7 @@ describe('actions', () => {
filterByTk: adminUser.id,
values: {
nickname: 'a',
username: 'A',
},
});
expect(res1.status).toBe(401);
@ -57,6 +58,7 @@ describe('actions', () => {
filterByTk: adminUser.id,
values: {
nickname: 'a',
username: 'A',
},
});
expect(res2.status).toBe(200);

View File

@ -8,8 +8,28 @@
*/
import { Context, DEFAULT_PAGE, DEFAULT_PER_PAGE, Next } from '@nocobase/actions';
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
import _ from 'lodash';
import { namespace } from '..';
import { ValidationError, ValidationErrorItem } from 'sequelize';
function parseProfileFormSchema(schema: any) {
const properties = _.get(schema, 'properties.form.properties.edit.properties.grid.properties') || {};
const fields = [];
const requiredFields = [];
Object.values(properties).forEach((row: any) => {
const col = Object.values(row.properties)[0] as any;
const [name, props] = Object.entries(col.properties)[0];
if (props['x-read-pretty'] || props['x-disable']) {
return;
}
if (props['required']) {
requiredFields.push(name);
}
fields.push(name);
});
return { fields, requiredFields };
}
export async function updateProfile(ctx: Context, next: Next) {
const systemSettings = ctx.db.getRepository('systemSettings');
@ -24,10 +44,34 @@ export async function updateProfile(ctx: Context, next: Next) {
if (!currentUser) {
ctx.throw(401);
}
const UserRepo = ctx.db.getRepository('users');
const result = await UserRepo.update({
const schemaRepo = ctx.db.getRepository<UiSchemaRepository>('uiSchemas');
const schema = await schemaRepo.getJsonSchema('nocobase-user-profile-edit-form');
const { fields, requiredFields } = parseProfileFormSchema(schema);
const userRepo = ctx.db.getRepository('users');
const user = await userRepo.findOne({ filter: { id: currentUser.id } });
for (const field of requiredFields) {
if (!values[field]) {
// Throw a sequelize validation error and it will be caught by the error handler
// so that the field name in error message will be translated
throw new ValidationError(`${field} can not be null`, [
new ValidationErrorItem(
`${field} can not be null`,
// @ts-ignore
'notNull violation',
field,
null,
user,
'is_null',
null,
null,
),
]);
}
}
const result = await userRepo.update({
filterByTk: currentUser.id,
values: _.pick(values, ['nickname', 'username', 'email', 'phone']),
values: _.pick(values, fields),
});
ctx.body = result;
await next();

View File

@ -0,0 +1,49 @@
/**
* 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 { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
import { Migration } from '@nocobase/server';
import {
adminProfileCreateFormSchema,
adminProfileEditFormSchema,
userProfileEditFormSchema,
} from '../profile/edit-form-schema';
export default class extends Migration {
on = 'afterLoad'; // 'beforeLoad' or 'afterLoad'
appVersion = '<1.6.0-alpha.7';
async up() {
const repo = this.db.getRepository<UiSchemaRepository>('uiSchemas');
const adminCreateSchema = await repo.findOne({
filter: {
'x-uid': 'nocobase-admin-profile-create-form',
},
});
if (!adminCreateSchema) {
await repo.insert(adminProfileCreateFormSchema);
}
const adminEditSchema = await repo.findOne({
filter: {
'x-uid': 'nocobase-admin-profile-edit-form',
},
});
if (!adminEditSchema) {
await repo.insert(adminProfileEditFormSchema);
}
const userSchema = await repo.findOne({
filter: {
'x-uid': 'nocobase-user-profile-edit-form',
},
});
if (!userSchema) {
await repo.insert(userProfileEditFormSchema);
}
}
}

View File

@ -0,0 +1,493 @@
/**
* 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 const adminProfileCreateFormSchema = {
type: 'void',
'x-uid': 'nocobase-admin-profile-create-form',
properties: {
form: {
type: 'void',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
dataSource: 'main',
},
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
properties: {
create: {
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'form:configureFields',
properties: {
nickname: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
nickname: {
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.nickname',
},
},
},
},
},
username: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
username: {
type: 'string',
required: true,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.username',
},
},
},
},
},
email: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
email: {
type: 'string',
required: false,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.email',
},
},
},
},
},
phone: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
phone: {
type: 'string',
required: false,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.phone',
},
},
},
},
},
password: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
password: {
type: 'string',
required: true,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.password',
},
},
},
},
},
roles: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
roles: {
type: 'string',
required: false,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.roles',
},
},
},
},
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.FootBar',
properties: {
cancel: {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-use-component-props': 'useCancelActionProps',
},
submit: {
title: '{{ t("Submit") }}',
'x-component': 'Action',
'x-use-component-props': 'useCreateActionProps',
'x-component-props': {
type: 'primary',
htmlType: 'submit',
},
},
},
},
},
},
},
},
},
};
export const adminProfileEditFormSchema = {
type: 'void',
'x-uid': 'nocobase-admin-profile-edit-form',
properties: {
form: {
type: 'void',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
dataSource: 'main',
action: 'get',
},
'x-use-decorator-props': 'useEditFormBlockDecoratorProps',
properties: {
edit: {
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useEditFormBlockProps',
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'form:configureFields',
properties: {
nickname: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
nickname: {
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.nickname',
},
},
},
},
},
username: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
username: {
type: 'string',
required: true,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.username',
},
},
},
},
},
email: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
email: {
type: 'string',
required: false,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.email',
},
},
},
},
},
phone: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
phone: {
type: 'string',
required: false,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.phone',
},
},
},
},
},
roles: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
roles: {
type: 'string',
required: false,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.roles',
},
},
},
},
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.FootBar',
properties: {
cancel: {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-use-component-props': 'useCancelActionProps',
},
submit: {
title: '{{ t("Submit") }}',
'x-component': 'Action',
'x-use-component-props': 'useUpdateActionProps',
'x-component-props': {
type: 'primary',
htmlType: 'submit',
},
},
},
},
},
},
},
},
},
};
export const userProfileEditFormSchema = {
type: 'void',
'x-uid': 'nocobase-user-profile-edit-form',
properties: {
form: {
type: 'void',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
dataSource: 'main',
action: 'get',
},
'x-use-decorator-props': 'useEditFormBlockDecoratorProps',
properties: {
edit: {
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useEditFormBlockProps',
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'form:configureFields',
properties: {
nickname: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
nickname: {
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.nickname',
},
},
},
},
},
username: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
username: {
type: 'string',
required: true,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.username',
},
},
},
},
},
email: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
email: {
type: 'string',
required: false,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.email',
},
},
},
},
},
phone: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
phone: {
type: 'string',
required: false,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-component-props': {},
'x-collection-field': 'users.phone',
},
},
},
},
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.FootBar',
properties: {
cancel: {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-use-component-props': 'useCancelActionProps',
},
submit: {
title: '{{ t("Submit") }}',
'x-component': 'Action',
'x-use-component-props': 'useUpdateProfileActionProps',
'x-component-props': {
type: 'primary',
htmlType: 'submit',
},
},
},
},
},
},
},
},
},
};

View File

@ -8,12 +8,18 @@
*/
import { Collection, Model, Op } from '@nocobase/database';
import { Plugin } from '@nocobase/server';
import { InstallOptions, Plugin } from '@nocobase/server';
import { parse } from '@nocobase/utils';
import * as actions from './actions/users';
import { UserModel } from './models/UserModel';
import PluginUserDataSyncServer from '@nocobase/plugin-user-data-sync';
import { UserDataSyncResource } from './user-data-sync-resource';
import {
adminProfileCreateFormSchema,
adminProfileEditFormSchema,
userProfileEditFormSchema,
} from './profile/edit-form-schema';
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
export default class PluginUsersServer extends Plugin {
async beforeLoad() {
@ -200,6 +206,15 @@ export default class PluginUsersServer extends Plugin {
}
});
this.app.resourceManager.use(async (ctx, next) => {
const { resourceName, actionName } = ctx.action;
if (resourceName === 'users' && actionName === 'updateProfile') {
// for triggering workflows
ctx.action.actionName = 'update';
}
await next();
});
const userDataSyncPlugin = this.app.pm.get('user-data-sync') as PluginUserDataSyncServer;
if (userDataSyncPlugin && userDataSyncPlugin.enabled) {
userDataSyncPlugin.resourceManager.registerResource(new UserDataSyncResource(this.db, this.app.logger));
@ -231,7 +246,7 @@ export default class PluginUsersServer extends Plugin {
};
}
async install(options) {
async initUserCollection(options: InstallOptions) {
const { rootNickname, rootPassword, rootEmail, rootUsername } = this.getInstallingData(options);
const User = this.db.getCollection('users');
@ -254,4 +269,19 @@ export default class PluginUsersServer extends Plugin {
await repo.db2cm('users');
}
}
async initProfileSchema() {
const uiSchemas = this.db.getRepository<UiSchemaRepository>('uiSchemas');
if (!uiSchemas) {
return;
}
await uiSchemas.insert(adminProfileCreateFormSchema);
await uiSchemas.insert(adminProfileEditFormSchema);
await uiSchemas.insert(userProfileEditFormSchema);
}
async install(options: InstallOptions) {
await this.initUserCollection(options);
await this.initProfileSchema();
}
}