feat(workflow): support attachment field assign in workflow create/update nodes. (#1419)

Co-authored-by: sealday <sealday@gmail.com>
Reviewed-on: daoyoucloud/tachybase#1419
This commit is contained in:
sealday 2024-08-01 05:10:10 +08:00
parent 54a7f5dc40
commit 45571c62f1
9 changed files with 5491 additions and 8706 deletions

View File

@ -83,7 +83,7 @@ export class PluginCoreClient extends Plugin {
await this.app.pm.add(PluginFieldAppends);
await this.app.pm.add(PluginCustomComponents);
await this.app.pm.add(PluginSheet);
await this.app.pm.add(PluginDemo);
// await this.app.pm.add(PluginDemo);
}
async registerSettings() {

View File

@ -20,7 +20,7 @@ export class PluginCoreServer extends Plugin {
tstzrange: TstzrangeField,
});
this.addFeature(DepartmentsPlugin);
this.addFeature(PluginDemo);
// this.addFeature(PluginDemo);
}
async load() {

View File

@ -9,7 +9,9 @@
"license": "Apache-2.0",
"main": "./dist/server/index.js",
"dependencies": {
"jsonata": "^2.0.5"
"form-data": "^4.0.0",
"jsonata": "^2.0.5",
"mime-types": "^2.1.35"
},
"devDependencies": {
"@ant-design/icons": "~5.3.7",

View File

@ -23,10 +23,7 @@ function IndividualHooksRadioWithTooltip({ onChange, ...props }) {
return;
}
const filteredValues = fields.reduce((result, item) => {
if (
item.name in valuesField.value &&
(target.value || !['hasOne', 'hasMany', 'belongsToMany'].includes(item.type))
) {
if (item.name in valuesField.value && (target.value || ![].includes(item.type))) {
result[item.name] = valuesField.value[item.name];
}
return result;
@ -111,7 +108,7 @@ export default class extends Instruction {
...values,
'x-component-props': {
filter(this, field) {
return this.params?.individualHooks || !['hasOne', 'hasMany', 'belongsToMany'].includes(field.type);
return this.params?.individualHooks || ![].includes(field.type);
},
},
},

View File

@ -79,7 +79,6 @@ export default class PluginWorkflowServer extends Plugin {
this.addFeature(PluginWorkflowJSONParseServer);
this.addFeature(PluginWorkflowJSParseServer);
this.addFeature(PluginWorkflowDataMappingServer);
// this.addFeature(PluginWorkflowAPIRegularServer);
this.addFeature(PluginInterception);
this.addFeature(PluginVariables);
this.addFeature(PluginResponse);

View File

@ -121,9 +121,6 @@ export class OmniTrigger extends Trigger {
for (const event of asyncGroup) {
this.workflow.trigger(event[0], event[1]);
}
this.workflow.noticeManager.notify('workflow:regular', {
msg: 'start',
});
await next();
};
constructor(workflow) {

View File

@ -1,4 +1,11 @@
import { parseCollectionName } from '@tachybase/data-source-manager';
import { Gateway } from '@tachybase/server';
import { uid } from '@tachybase/utils';
import axios from 'axios';
import FormData from 'form-data';
import _ from 'lodash';
import mime from 'mime-types';
import { Instruction } from '.';
import { JOB_STATUS } from '../constants';
@ -17,6 +24,81 @@ export class CreateInstruction extends Instruction {
const options = processor.getParsedValue(params, node.id);
const transaction = this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction);
const c = this.workflow.app.dataSourceManager.dataSources
.get(dataSourceName)
.collectionManager.getCollection(collectionName);
const fields = c.getFields();
const fieldNames = Object.keys(params.values);
const includesFields = fields.filter((field) => fieldNames.includes(field.options.name));
const userId = _.get(processor.getScope(node.id), '$context.user.id', '');
const token = this.workflow.app.authManager.jwt.sign({ userId });
const handleUrl = async (url) => {
const form = new FormData();
if (url.startsWith('data:')) {
const matches = url.match(/^data:(.+);base64,(.+)$/);
if (matches) {
const contentType = matches[1];
const base64Data = matches[2];
const buffer = Buffer.from(base64Data, 'base64');
const ext = mime.extension(contentType);
const filename = `${uid()}.${ext}`;
form.append('file', buffer, {
filename,
contentType,
});
} else {
throw new Error('Invalid data URL format');
}
} else {
// 下载指定 URL 的内容
const response = await axios({
method: 'get',
url,
responseType: 'stream',
});
// 获取文件的 MIME 类型
const contentType = response.headers['content-type'];
// 根据 MIME 类型获取文件扩展名
const ext = mime.extension(contentType);
const filename = `${uid()}.${ext}`;
// 创建 FormData 实例
form.append('file', response.data, {
filename,
contentType: response.headers['content-type'],
});
}
// 发送 multipart 请求
const origin = Gateway.getInstance().runAtLoop;
const uploadResponse = await axios({
method: 'post',
url: origin + '/api/attachments:create',
data: form,
headers: {
...form.getHeaders(),
Authorization: 'Bearer ' + token,
},
});
return uploadResponse.data.data;
};
// 处理文件类型
for (const attachmentField of includesFields) {
if (attachmentField.options.interface === 'attachment') {
const urls = options.values[attachmentField.options.name];
if (Array.isArray(urls)) {
for (const i in urls) {
urls[i] = await handleUrl(urls[i]);
}
} else {
const url = options.values[attachmentField.options.name];
options.values[attachmentField.options.name] = await handleUrl(url);
}
}
}
const created = await repository.create({
...options,
context: {

View File

@ -1,4 +1,11 @@
import { parseCollectionName } from '@tachybase/data-source-manager';
import { Gateway } from '@tachybase/server';
import { uid } from '@tachybase/utils';
import axios from 'axios';
import FormData from 'form-data';
import _ from 'lodash';
import mime from 'mime-types';
import { Instruction } from '.';
import { JOB_STATUS } from '../constants';
@ -15,6 +22,82 @@ export class UpdateInstruction extends Instruction {
.get(dataSourceName)
.collectionManager.getCollection(collectionName);
const options = processor.getParsedValue(params, node.id);
const c = this.workflow.app.dataSourceManager.dataSources
.get(dataSourceName)
.collectionManager.getCollection(collectionName);
const fields = c.getFields();
const fieldNames = Object.keys(params.values);
const includesFields = fields.filter((field) => fieldNames.includes(field.options.name));
const userId = _.get(processor.getScope(node.id), '$context.user.id', '');
const token = this.workflow.app.authManager.jwt.sign({ userId });
const handleUrl = async (url) => {
const form = new FormData();
if (url.startsWith('data:')) {
const matches = url.match(/^data:(.+);base64,(.+)$/);
if (matches) {
const contentType = matches[1];
const base64Data = matches[2];
const buffer = Buffer.from(base64Data, 'base64');
const ext = mime.extension(contentType);
const filename = `${uid()}.${ext}`;
form.append('file', buffer, {
filename,
contentType,
});
} else {
throw new Error('Invalid data URL format');
}
} else {
// 下载指定 URL 的内容
const response = await axios({
method: 'get',
url,
responseType: 'stream',
});
// 获取文件的 MIME 类型
const contentType = response.headers['content-type'];
// 根据 MIME 类型获取文件扩展名
const ext = mime.extension(contentType);
const filename = `${uid()}.${ext}`;
// 创建 FormData 实例
form.append('file', response.data, {
filename,
contentType: response.headers['content-type'],
});
}
// 发送 multipart 请求
const origin = Gateway.getInstance().runAtLoop;
const uploadResponse = await axios({
method: 'post',
url: origin + '/api/attachments:create',
data: form,
headers: {
...form.getHeaders(),
Authorization: 'Bearer ' + token,
},
});
return uploadResponse.data.data;
};
// 处理文件类型
for (const attachmentField of includesFields) {
if (attachmentField.options.interface === 'attachment') {
const urls = options.values[attachmentField.options.name];
if (Array.isArray(urls)) {
for (const i in urls) {
urls[i] = await handleUrl(urls[i]);
}
} else {
const url = options.values[attachmentField.options.name];
options.values[attachmentField.options.name] = [await handleUrl(url)];
}
}
}
const result = await repository.update({
...options,
context: {

File diff suppressed because it is too large Load Diff