diff --git a/package.json b/package.json
index 6a6031f19..ae4ac7777 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"scripts": {
"start": "cd packages/app && npm start",
"db-migrate": "cd packages/app && npm run db-migrate",
+ "start:app:server": "cd packages/app && nodemon",
"start-server": "yarn nodemon",
"bootstrap": "lerna bootstrap --no-ci",
"build": "npm run build-father-build && node packages/father-build/bin/father-build.js",
diff --git a/packages/app/package.json b/packages/app/package.json
index 67adc6448..3c5f31d70 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -29,6 +29,7 @@
"@nocobase/client": "^0.3.0-alpha.0",
"@nocobase/database": "^0.3.0-alpha.0",
"@nocobase/father-build": "^0.3.0-alpha.0",
+ "@nocobase/plugin-action-logs": "^0.3.0-alpha.0",
"@nocobase/plugin-collections": "^0.3.0-alpha.0",
"@nocobase/plugin-pages": "^0.3.0-alpha.0",
"@nocobase/server": "^0.3.0-alpha.0",
diff --git a/packages/app/src/api/app.ts b/packages/app/src/api/app.ts
index de3d08065..43d281ad6 100644
--- a/packages/app/src/api/app.ts
+++ b/packages/app/src/api/app.ts
@@ -62,6 +62,7 @@ api.resourcer.registerActionHandlers({...actions.common, ...actions.associate});
// });
api.registerPlugin('plugin-collections', [path.resolve(__dirname, '../../../plugin-collections'), {}]);
+api.registerPlugin('plugin-action-logs', [path.resolve(__dirname, '../../../plugin-action-logs'), {}]);
api.registerPlugin('plugin-pages', [path.resolve(__dirname, '../../../plugin-pages'), {}]);
api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-users'), {}]);
api.registerPlugin('plugin-file-manager', [path.resolve(__dirname, '../../../plugin-file-manager'), {}]);
diff --git a/packages/app/src/api/migrations/create-action-logs-table.ts b/packages/app/src/api/migrations/create-action-logs-table.ts
new file mode 100644
index 000000000..bf10af056
--- /dev/null
+++ b/packages/app/src/api/migrations/create-action-logs-table.ts
@@ -0,0 +1,42 @@
+import api from '../app';
+import Database from '@nocobase/database';
+
+(async () => {
+ await api.loadPlugins();
+ const database: Database = api.database;
+ await api.database.sync({
+ tables: ['actions_scopes', 'action_logs', 'action_changes'],
+ });
+
+ const [Collection, Page] = database.getModels(['collections', 'pages']);
+
+ const tables = database.getTables(['actions_scopes', 'action_logs', 'action_changes']);
+
+ for (let table of tables) {
+ console.log(table.getName());
+ await Collection.import(table.getOptions(), { update: true, migrate: false });
+ }
+
+ await Page.import({
+ title: '动态',
+ type: 'layout',
+ path: '/activity',
+ icon: 'NotificationOutlined',
+ template: 'SideMenuLayout',
+ sort: 85,
+ showInMenu: true,
+ parent_id: 1,
+ children: [
+ {
+ title: '操作记录',
+ type: 'collection',
+ path: '/activity/logs',
+ icon: 'HistoryOutlined',
+ template: 'collection',
+ collection: 'action_logs',
+ sort: 80,
+ showInMenu: true,
+ },
+ ]
+ });
+})();
diff --git a/packages/app/src/api/migrations/init.ts b/packages/app/src/api/migrations/init.ts
index 9e9d4738f..5ae5848b0 100644
--- a/packages/app/src/api/migrations/init.ts
+++ b/packages/app/src/api/migrations/init.ts
@@ -74,6 +74,27 @@ const data = [
},
]
},
+ {
+ title: '动态',
+ type: 'layout',
+ path: '/activity',
+ icon: 'NotificationOutlined',
+ template: 'SideMenuLayout',
+ sort: 85,
+ showInMenu: true,
+ children: [
+ {
+ title: '操作记录',
+ type: 'collection',
+ path: '/activity/logs',
+ icon: 'HistoryOutlined',
+ template: 'collection',
+ collection: 'action_logs',
+ sort: 80,
+ showInMenu: true,
+ },
+ ]
+ },
{
title: '配置',
type: 'layout',
diff --git a/packages/app/src/components/icons/index.tsx b/packages/app/src/components/icons/index.tsx
index a0a819374..8975b514a 100644
--- a/packages/app/src/components/icons/index.tsx
+++ b/packages/app/src/components/icons/index.tsx
@@ -8,6 +8,8 @@ import {
SettingOutlined,
TableOutlined,
MenuOutlined,
+ HistoryOutlined,
+ NotificationOutlined,
} from '@ant-design/icons';
export const IconFont = createFromIconfontCN({
@@ -36,6 +38,7 @@ export function registerIcons(components) {
}
registerIcons({
+ HistoryOutlined,
MenuOutlined,
TableOutlined,
SettingOutlined,
@@ -43,6 +46,7 @@ registerIcons({
UserOutlined,
DatabaseOutlined,
DashboardOutlined,
+ NotificationOutlined,
});
interface IconProps {
diff --git a/packages/app/src/components/views/Field/index.tsx b/packages/app/src/components/views/Field/index.tsx
index a55856dd8..7068960d0 100644
--- a/packages/app/src/components/views/Field/index.tsx
+++ b/packages/app/src/components/views/Field/index.tsx
@@ -361,6 +361,20 @@ export function AttachmentFieldItem(props: any) {
);
}
+function LogField(props) {
+ const { value = {} } = props;
+ return (
+
{value.title||value.name}
+ )
+}
+
+function LogFieldValue(props) {
+ const { value, schema, data } = props;
+ return (
+ {value}
+ )
+}
+
registerFieldComponents({
string: StringField,
textarea: TextareaField,
@@ -380,10 +394,12 @@ registerFieldComponents({
subTable: SubTableField,
linkTo: LinkToField,
attachment: AttachmentField,
+ 'logs.field': LogField,
+ 'logs.fieldValue': LogFieldValue,
});
export default function Field(props: any) {
const { schema = {} } = props;
- const Component = getFieldComponent(schema.interface);
+ const Component = getFieldComponent(schema.interface||get(schema, 'component.type'));
return ;
}
diff --git a/packages/plugin-action-logs/package.json b/packages/plugin-action-logs/package.json
new file mode 100644
index 000000000..babc448a2
--- /dev/null
+++ b/packages/plugin-action-logs/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@nocobase/plugin-action-logs",
+ "version": "0.3.0-alpha.0",
+ "main": "lib/index.js",
+ "license": "MIT",
+ "dependencies": {
+ "@nocobase/database": "^0.3.0-alpha.0",
+ "@nocobase/resourcer": "^0.3.0-alpha.0"
+ }
+}
diff --git a/packages/plugin-action-logs/src/collections/action_changes.ts b/packages/plugin-action-logs/src/collections/action_changes.ts
new file mode 100644
index 000000000..a30a33cf7
--- /dev/null
+++ b/packages/plugin-action-logs/src/collections/action_changes.ts
@@ -0,0 +1,57 @@
+import { TableOptions } from '@nocobase/database';
+
+export default {
+ name: 'action_changes',
+ title: '变动值',
+ developerMode: true,
+ internal: true,
+ createdBy: false,
+ updatedBy: false,
+ createdAt: false,
+ updatedAt: false,
+ fields: [
+ {
+ interface: 'linkTo',
+ type: 'belongsTo',
+ name: 'log',
+ target: 'action_logs',
+ title: '所属操作'
+ },
+ {
+ type: 'jsonb',
+ name: 'field',
+ title: '字段信息',
+ component: {
+ type: 'logs.field',
+ showInTable: true,
+ },
+ },
+ {
+ type: 'jsonb',
+ name: 'before',
+ title: '操作前',
+ component: {
+ type: 'logs.fieldValue',
+ showInTable: true,
+ },
+ },
+ {
+ type: 'jsonb',
+ name: 'after',
+ title: '操作后',
+ component: {
+ type: 'logs.fieldValue',
+ showInTable: true,
+ },
+ }
+ ],
+ views: [
+ {
+ type: 'table',
+ name: 'table',
+ title: '列表',
+ template: 'Table',
+ default: true,
+ },
+ ],
+} as TableOptions;
diff --git a/packages/plugin-action-logs/src/collections/action_logs.ts b/packages/plugin-action-logs/src/collections/action_logs.ts
new file mode 100644
index 000000000..ad6e4cb64
--- /dev/null
+++ b/packages/plugin-action-logs/src/collections/action_logs.ts
@@ -0,0 +1,108 @@
+import { TableOptions } from '@nocobase/database';
+
+export default {
+ name: 'action_logs',
+ title: '操作记录',
+ developerMode: true,
+ internal: true,
+ createdBy: false,
+ updatedBy: false,
+ updatedAt: false,
+ fields: [
+ {
+ interface: 'createdAt',
+ name: 'created_at',
+ type: 'date',
+ title: '操作时间',
+ showTime: true,
+ component: {
+ showInTable: true,
+ showInDetail: true,
+ },
+ },
+ {
+ interface: 'linkTo',
+ type: 'belongsTo',
+ name: 'user',
+ target: 'users',
+ title: '操作用户',
+ appends: true,
+ labelField: 'nickname',
+ component: {
+ showInTable: true,
+ showInDetail: true,
+ },
+ },
+ {
+ interface: 'linkTo',
+ type: 'belongsTo',
+ name: 'collection',
+ target: 'collections',
+ title: '数据表',
+ targetKey: 'name',
+ appends: true,
+ labelField: 'title',
+ component: {
+ showInTable: true,
+ showInDetail: true,
+ },
+ },
+ {
+ interface: 'select',
+ type: 'string',
+ name: 'type',
+ title: '操作类型',
+ filterable: true,
+ dataSource: [
+ { value: 'create', label: '新增' },
+ { value: 'update', label: '更新' },
+ { value: 'destroy', label: '删除' },
+ ],
+ component: {
+ showInTable: true,
+ showInDetail: true,
+ },
+ },
+ {
+ type: 'integer',
+ name: 'index',
+ title: '对象索引',
+ component: {
+ showInTable: true,
+ },
+ },
+ {
+ interface: 'subTable',
+ type: 'hasMany',
+ name: 'changes',
+ target: 'action_changes',
+ title: '数据变动',
+ component: {
+ showInDetail: true,
+ },
+ }
+ ],
+ actions: [
+ {
+ type: 'list',
+ name: 'list',
+ title: '查看',
+ sort: '-created_at'
+ },
+ ],
+ views: [
+ {
+ type: 'table',
+ name: 'table',
+ title: '列表',
+ template: 'Table',
+ default: true
+ },
+ {
+ type: 'details',
+ name: 'details',
+ title: '详情',
+ template: 'Details',
+ },
+ ],
+} as TableOptions;
diff --git a/packages/plugin-action-logs/src/hooks/after-create.ts b/packages/plugin-action-logs/src/hooks/after-create.ts
new file mode 100644
index 000000000..6e6b0cb2a
--- /dev/null
+++ b/packages/plugin-action-logs/src/hooks/after-create.ts
@@ -0,0 +1,54 @@
+import { Field } from '@nocobase/database';
+
+export default async function(model, options) {
+ if (!options.context) {
+ return;
+ }
+ const { database: db } = model;
+ const { context, transaction = await db.sequelize.transaction() } = options;
+ const {
+ state,
+ action: {
+ params: {
+ actionName,
+ resourceName,
+ }
+ }
+ } = context;
+ const ActionLog = db.getModel('action_logs');
+ // 创建操作记录
+ const log = await ActionLog.create({
+ type: actionName,
+ collection_name: model.constructor.name,
+ index: model.get(model.constructor.primaryKeyAttribute),
+ created_at: model.get('created_at')
+ }, {
+ transaction
+ });
+
+ const fields = db.getTable(model.constructor.name).getFields();
+ const fieldsList = Array.from(fields.values());
+ const changes = [];
+ model.changed().forEach((key: string) => {
+ const field = fields.get(key) || fieldsList.find((item: Field) => item.options.field === key);
+ if (field) {
+ changes.push({
+ field: field.options,
+ after: model.get(key)
+ });
+ }
+ });
+ // TODO(bug): state.currentUser 不是 belongsTo field 的 target 实例
+ // Sequelize 会另外创建一个 Model 的继承类,无法直传 instance
+ // await log.setUser(state.currentUser, { transaction });
+ await log.updateAssociations({
+ ...(state.currentUser ? { user: state.currentUser.id } : {}),
+ changes
+ }, {
+ transaction
+ });
+
+ if (!options.transaction) {
+ await transaction.commit();
+ }
+}
diff --git a/packages/plugin-action-logs/src/hooks/after-destroy.ts b/packages/plugin-action-logs/src/hooks/after-destroy.ts
new file mode 100644
index 000000000..938984ac7
--- /dev/null
+++ b/packages/plugin-action-logs/src/hooks/after-destroy.ts
@@ -0,0 +1,53 @@
+import { Field } from '@nocobase/database';
+
+export default async function(model, options) {
+ if (!options.context) {
+ return;
+ }
+ const { database: db } = model;
+ const { context, transaction = await db.sequelize.transaction() } = options;
+ const {
+ state,
+ action: {
+ params: {
+ actionName,
+ resourceName,
+ }
+ }
+ } = context;
+ const ActionLog = db.getModel('action_logs');
+ // 创建操作记录
+ const log = await ActionLog.create({
+ // user_id: state.currentUser ? state.currentUser.id : null,
+ type: actionName,
+ collection_name: model.constructor.name,
+ index: model.get(model.constructor.primaryKeyAttribute),
+ // created_at: model.get('created_at')
+ }, {
+ transaction
+ });
+
+ const fields = db.getTable(model.constructor.name).getFields();
+ const fieldsList = Array.from(fields.values());
+ const changes = [];
+ Object.keys(model.get()).forEach((key: string) => {
+ const field = fields.get(key) || fieldsList.find((item: Field) => item.options.field === key);
+ if (field) {
+ changes.push({
+ field: field.options,
+ before: model.get(key)
+ });
+ }
+ });
+
+ await log.updateAssociations({
+ ...(state.currentUser ? { user: state.currentUser.id } : {}),
+ changes
+ }, {
+ transaction
+ });
+
+ if (!options.transaction) {
+ await transaction.commit();
+ }
+}
diff --git a/packages/plugin-action-logs/src/hooks/after-update.ts b/packages/plugin-action-logs/src/hooks/after-update.ts
new file mode 100644
index 000000000..b02ad7231
--- /dev/null
+++ b/packages/plugin-action-logs/src/hooks/after-update.ts
@@ -0,0 +1,59 @@
+import { Field } from '@nocobase/database';
+
+export default async function(model, options) {
+ if (!options.context) {
+ return;
+ }
+ const { database: db } = model;
+ const { context, transaction = await db.sequelize.transaction() } = options;
+ const {
+ state,
+ action: {
+ params: {
+ actionName,
+ resourceName,
+ }
+ }
+ } = context;
+ const ActionLog = db.getModel('action_logs');
+
+ const fields = db.getTable(model.constructor.name).getFields();
+ const fieldsList = Array.from(fields.values());
+ const changes = [];
+
+ model.changed().forEach((key: string) => {
+ const field = fields.get(key) || fieldsList.find((item: Field) => item.options.field === key);
+ if (field && field.options.type !== 'formula') {
+ changes.push({
+ field: field.options,
+ after: model.get(key),
+ before: model.previous(key)
+ });
+ }
+ });
+
+ if (changes.length === 0) {
+ return;
+ }
+
+ // 创建操作记录
+ const log = await ActionLog.create({
+ type: actionName,
+ collection_name: model.constructor.name,
+ index: model.get(model.constructor.primaryKeyAttribute),
+ created_at: model.get('updated_at')
+ }, {
+ transaction
+ });
+
+ await log.updateAssociations({
+ ...(state.currentUser ? { user: state.currentUser.id } : {}),
+ changes
+ }, {
+ transaction
+ });
+
+ if (!options.transaction) {
+ await transaction.commit();
+ }
+}
diff --git a/packages/plugin-action-logs/src/hooks/index.ts b/packages/plugin-action-logs/src/hooks/index.ts
new file mode 100644
index 000000000..799f79683
--- /dev/null
+++ b/packages/plugin-action-logs/src/hooks/index.ts
@@ -0,0 +1,10 @@
+import afterCreate from './after-create';
+import afterUpdate from './after-update';
+import afterDestroy from './after-destroy';
+
+export function addAll(Model) {
+ Model.addHook('afterCreate', afterCreate);
+ // Model.addHook('afterBulkCreate', hooks.afterBulkCreate);
+ Model.addHook('afterUpdate', afterUpdate);
+ Model.addHook('afterDestroy', afterDestroy);
+}
diff --git a/packages/plugin-action-logs/src/server.ts b/packages/plugin-action-logs/src/server.ts
new file mode 100644
index 000000000..470ec6350
--- /dev/null
+++ b/packages/plugin-action-logs/src/server.ts
@@ -0,0 +1,20 @@
+import path from 'path';
+
+import { addAll } from './hooks';
+
+export default async function() {
+ const { database } = this;
+
+ database.import({
+ directory: path.resolve(__dirname, 'collections'),
+ });
+
+ // 为所有的表都加上日志的 hooks
+ database.addHook('afterTableInit', function (table) {
+ if (['action_logs', 'action_changes'].includes(table.options.name)) {
+ return;
+ }
+ const Model = database.getModel(table.options.name);
+ addAll(Model);
+ });
+}
diff --git a/packages/plugin-pages/src/actions/getView.ts b/packages/plugin-pages/src/actions/getView.ts
index 4f218a0ec..2193e63a4 100644
--- a/packages/plugin-pages/src/actions/getView.ts
+++ b/packages/plugin-pages/src/actions/getView.ts
@@ -157,6 +157,7 @@ const transforms = {
return schema;
},
details: async (fields: Model[], context?: any) => {
+ const [Field] = context.db.getModels(['fields']) as ModelCtor[];
const arr = [];
for (const field of fields) {
if (!get(field.component, 'showInDetail')) {
@@ -167,10 +168,17 @@ const transforms = {
}
const props = {};
if (field.get('interface') === 'subTable') {
- const children = await field.getChildren({
- order: [['sort', 'asc']],
- });
- props['children'] = children.map(child => ({...child.toJSON(), dataIndex: child.name.split('.')}))
+ const children = await Field.findAll(Field.parseApiJson({
+ filter: {
+ collection_name: field.get('target'),
+ },
+ perPage: -1,
+ sort: ['sort'],
+ }));
+ // const children = await field.getChildren({
+ // order: [['sort', 'asc']],
+ // });
+ props['children'] = children.filter(item => item.get('component.showInTable')).map(child => ({...child.toJSON(), dataIndex: child.name.split('.')}))
}
arr.push({
...field.toJSON(),
diff --git a/packages/plugin-permissions/src/server.ts b/packages/plugin-permissions/src/server.ts
index 87af2fa63..48b74af09 100644
--- a/packages/plugin-permissions/src/server.ts
+++ b/packages/plugin-permissions/src/server.ts
@@ -35,7 +35,6 @@ export class Permissions {
});
database.getModel('collections').addHook('afterCreate', async (model: any, options) => {
- // console.log('plugin-permissions hook');
await model.updateAssociations({
scopes: [
{
diff --git a/packages/plugin-users/src/server.ts b/packages/plugin-users/src/server.ts
index a19e539c6..de753d01f 100644
--- a/packages/plugin-users/src/server.ts
+++ b/packages/plugin-users/src/server.ts
@@ -18,10 +18,6 @@ export default async function (options = {}) {
registerFields(fields);
- database.import({
- directory: path.resolve(__dirname, 'collections'),
- });
-
database.addHook('afterTableInit', (table) => {
let { createdBy, updatedBy, internal } = table.getOptions();
// 非内置表,默认创建 createdBy 和 updatedBy
@@ -39,6 +35,10 @@ export default async function (options = {}) {
.map(type => table.addField(makeOptions(type, fieldsToMake[type])));
});
+ database.import({
+ directory: path.resolve(__dirname, 'collections'),
+ });
+
resourcer.registerActionHandlers({
'users:login': login,
'users:register': register,