jack zhang 2cb1203aa4
refactor(client)!: application, router and plugin (#2068)
BREAKING CHANGE:

* refactor: update umi version 3.x to version 4.x

* refactor: update react-router-dom version to 6.x

* refactor(react-router-dom): change Layout Component `props.children` to `<Outlet />`

* refactor(react-router-dom): change <Route /> props and <RouteSwitch /> correct

* refactor(react-router-dom): replace `<Redirect />` to `<Navigate replace />`

* refactor(react-router-dom): replace `useHistory` to `useNavigate`

* refactor(react-router-dom): replace `useRouteMatch` to `useParams`

* refactor(react-router-dom & dumi): fix <RouteSwitch /> & umi document bug

* refactor(react-router-dom): `useRoutes` Optimize `<RouteSwitch />` code

* refactor(react-router-dom): update `Route` types and docs

* refactor(react-router-dom): optimize RouteSwitch code

* refactor(react-router-dom): `useLocation` no generics type

* refactor(react-router-dom): add `less v3.9.0` to `resolutions` to solve the error of `gulp-less`

* refactor(react-router-dom): fix `<RouteSwitch />`  `props.routes` as an array is not handled

* chore: upgrade `dumi` and refactor docs

* fix: completed code review, add `targets` to solve browser compatibility & removed `chainWebpack`

* refactor(dumi): upgraded dumi under `packages/core/client`

* refactor(dumi): delete `packages/core/dumi-theme-nocobase`

* refactor(dumi): degrade `react`  & replace `dumi-theme-antd` to `dumi-theme-nocobase`

* refactor(dumi): solve conflicts between multiple dumi applications

* fix: login page error in react 17

* refactor(dumi): remove less resolutions

* refactor(dumi): umi add `msfu: true` config

* fix: merge bug

* fix: self code review

* fix: code reivew and test bug

* refactor: upgrade react to 18

* refactor: degrade react types to 17

* chore: fix ci error

* fix: support routerBase & fix workflow page params

* fix(doc): menu externel link

* fix: build error

* fix: delete

* fix: vitest error

* fix: react-router new code replace

* fix: vitest markdown error

* fix: title is none when refresh

* fix: merge error

* fix: sidebar width is wrong

* fix: useProps error

* fix: side-menu-width

* fix: menu selectId is wrong & useProps is string

* fix: menu selected first default & side menu hide when change

* fix: test error & v0.10 change log

* fix: new compnent doc modify

* fix: set umi `fastRefresh=false`

* refactor: application v2

* fix: improve code

* fix: bug

* fix: page = 0 error

* fix: workflow navigate error

* feat: plugin manager

* fix: afterAdd

* feat: complete basic functional refactor

* fix: performance Application

* feat: support client and server build

* refactor: nocobase build-in plugin and providers

* fix: server can't start

* refactor: all plugins package `Prodiver` change to `Plugin`

* feat: nested router and change mobile client

* feat: delete application-v1 and router-switch

* feat: improve routes

* fix: change mobile not nested

* feat: delete RouteSwitchContext and change buildin Provider to Plugin

* feat: delete RouteSwitchContext plugins

* fix: refactor SchemaComponentOptions

* feat: improve SchemaComponentOptions

* fix: add useAdminSchemaUid

* fix: merge master error

* fix: vitest error

* fix: bug

* feat: bugs

* fix: improve code

* fix: restore code

* feat: vitest

* fix: bugs

* fix: bugs

* docs: update doc

* feat: improve code

* feat: add docs and imporve code

* fix: bugs

* feat: add tests

* fix: remove deps

* fix: muti app router error

* fix: router error

* fix: workflow error

* fix: cli error

* feat: change NoCobase -> Nocobase

* fix: code review

* fix: type error

* fix: cli error and plugin demo

* feat: update doc theme

* fix: build error

* fix: mobile router

* fix: code rewview

* fix: bug

* fix: test bug

* fix: bug

* refactor: add the "client" directory to all plugins

* refactor: modify samples client and plugin template

* fix: merge error

* fix: add files in package.json

* refactor: add README to files in package.json

* fix: adjust plugins depencies

* refactor: completing plugins' devDependencies and dependencies

* fix: bug

* refactor: remove @emotion/css

* refactor: jsonwebtoken deps

* refactor: remove sequelize

* refactor: dayjs and moment deps

* fix: bugs

* fix: bug

* fix: cycle detect

* fix: merge bug

* feat: new plugin bug

* fix: lang bug

* fix: dynamic import bug

* refactor: plugins and example add father config

* feat: improve code

* fix: add AppSpin and AppError components

* Revert "refactor: plugins and example add father config"

This reverts commit 483315bca5524e4b8cbbb20cbad77986f081089d.

# Conflicts:
#	packages/plugins/auth/package.json
#	packages/plugins/multi-app-manager/package.json
#	packages/samples/command/package.json
#	packages/samples/custom-collection-template/package.json
#	packages/samples/ratelimit/package.json
#	packages/samples/shop-actions/package.json
#	packages/samples/shop-events/package.json
#	packages/samples/shop-modeling/package.json

* feat: update doc

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
2023-07-07 14:35:22 +08:00

491 lines
13 KiB
TypeScript

import { CloseCircleOutlined } from '@ant-design/icons';
import { css, cx, useCompile, Variable } from '@nocobase/client';
import { evaluators } from '@nocobase/evaluators/client';
import { Registry } from '@nocobase/utils/client';
import { Button, Select } from 'antd';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { cloneDeep } from 'lodash';
import { NodeDefaultView } from '.';
import { Branch } from '../Branch';
import { RadioWithTooltip, RadioWithTooltipOption } from '../components/RadioWithTooltip';
import { renderEngineReference } from '../components/renderEngineReference';
import { useFlowContext } from '../FlowContext';
import { lang, NAMESPACE } from '../locale';
import { branchBlockClass, nodeSubtreeClass } from '../style';
import { useWorkflowVariableOptions } from '../variable';
interface Calculator {
name: string;
type: 'boolean' | 'number' | 'string' | 'date' | 'unknown' | 'null' | 'array';
group: string;
}
export const calculators = new Registry<Calculator>();
calculators.register('equal', {
name: '=',
type: 'boolean',
group: 'boolean',
});
calculators.register('notEqual', {
name: '≠',
type: 'boolean',
group: 'boolean',
});
calculators.register('gt', {
name: '>',
type: 'boolean',
group: 'boolean',
});
calculators.register('gte', {
name: '≥',
type: 'boolean',
group: 'boolean',
});
calculators.register('lt', {
name: '<',
type: 'boolean',
group: 'boolean',
});
calculators.register('lte', {
name: '≤',
type: 'boolean',
group: 'boolean',
});
calculators.register('add', {
name: '+',
type: 'number',
group: 'number',
});
calculators.register('minus', {
name: '-',
type: 'number',
group: 'number',
});
calculators.register('multiple', {
name: '*',
type: 'number',
group: 'number',
});
calculators.register('divide', {
name: '/',
type: 'number',
group: 'number',
});
calculators.register('mod', {
name: '%',
type: 'number',
group: 'number',
});
calculators.register('includes', {
name: '{{t("contains")}}',
type: 'boolean',
group: 'string',
});
calculators.register('notIncludes', {
name: '{{t("does not contain")}}',
type: 'boolean',
group: 'string',
});
calculators.register('startsWith', {
name: '{{t("starts with")}}',
type: 'boolean',
group: 'string',
});
calculators.register('notStartsWith', {
name: '{{t("not starts with")}}',
type: 'boolean',
group: 'string',
});
calculators.register('endsWith', {
name: '{{t("ends with")}}',
type: 'boolean',
group: 'string',
});
calculators.register('notEndsWith', {
name: '{{t("not ends with")}}',
type: 'boolean',
group: 'string',
});
calculators.register('concat', {
name: `{{t("concat", { ns: "${NAMESPACE}" })}}`,
type: 'string',
group: 'string',
});
const calculatorGroups = [
{
value: 'boolean',
title: '{{t("Comparision")}}',
},
{
value: 'number',
title: `{{t("Arithmetic calculation", { ns: "${NAMESPACE}" })}}`,
},
{
value: 'string',
title: `{{t("String operation", { ns: "${NAMESPACE}" })}}`,
},
{
value: 'date',
title: `{{t("Date", { ns: "${NAMESPACE}" })}}`,
},
];
function getGroupCalculators(group) {
return Array.from(calculators.getEntities()).filter(([key, value]) => value.group === group);
}
function Calculation({ calculator, operands = [], onChange }) {
const compile = useCompile();
const options = useWorkflowVariableOptions();
return (
<fieldset
className={css`
display: flex;
gap: 0.5em;
align-items: center;
flex-wrap: wrap;
`}
>
<Variable.Input
value={operands[0]}
onChange={(v) => onChange({ calculator, operands: [v, operands[1]] })}
scope={options}
useTypedConstant
/>
<Select
value={calculator}
onChange={(v) => onChange({ operands, calculator: v })}
placeholder={lang('Calculator')}
dropdownMatchSelectWidth={false}
>
{calculatorGroups
.filter((group) => Boolean(getGroupCalculators(group.value).length))
.map((group) => (
<Select.OptGroup key={group.value} label={compile(group.title)}>
{getGroupCalculators(group.value).map(([value, { name }]) => (
<Select.Option key={value} value={value}>
{compile(name)}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
<Variable.Input
value={operands[1]}
onChange={(v) => onChange({ calculator, operands: [operands[0], v] })}
scope={options}
useTypedConstant
/>
</fieldset>
);
}
function CalculationItem({ value, onChange, onRemove }) {
if (!value) {
return null;
}
const { calculator, operands = [] } = value;
return (
<div
className={css`
display: flex;
position: relative;
margin: 0.5em 0;
`}
>
{value.group ? (
<CalculationGroup value={value.group} onChange={(group) => onChange({ ...value, group })} />
) : (
<Calculation operands={operands} calculator={calculator} onChange={onChange} />
)}
<Button onClick={onRemove} type="link" icon={<CloseCircleOutlined />} />
</div>
);
}
function CalculationGroup({ value, onChange }) {
const { t } = useTranslation();
const { type = 'and', calculations = [] } = value;
function onAddSingle() {
onChange({
...value,
calculations: [...calculations, { not: false, calculator: 'equal' }],
});
}
function onAddGroup() {
onChange({
...value,
calculations: [...calculations, { not: false, group: { type: 'and', calculations: [] } }],
});
}
function onRemove(i: number) {
calculations.splice(i, 1);
onChange({ ...value, calculations: [...calculations] });
}
function onItemChange(i: number, v) {
calculations.splice(i, 1, v);
onChange({ ...value, calculations: [...calculations] });
}
return (
<div
className={cx(
'node-type-condition-group',
css`
position: relative;
width: 100%;
.node-type-condition-group {
padding: 0.5em 1em;
border: 1px dashed #ddd;
}
+ button {
position: absolute;
right: 0;
}
`,
)}
>
<div
className={css`
display: flex;
align-items: center;
gap: 0.5em;
.ant-select {
width: auto;
min-width: 6em;
}
`}
>
<Trans>
{'Meet '}
<Select value={type} onChange={(t) => onChange({ ...value, type: t })}>
<Select.Option value="and">All</Select.Option>
<Select.Option value="or">Any</Select.Option>
</Select>
{' conditions in the group'}
</Trans>
</div>
<div className="calculation-items">
{calculations.map((calculation, i) => (
<CalculationItem
key={`${calculation.calculator}_${i}`}
value={calculation}
onChange={onItemChange.bind(this, i)}
onRemove={() => onRemove(i)}
/>
))}
</div>
<div
className={css`
button {
padding: 0;
&:not(:last-child) {
margin-right: 1em;
}
}
`}
>
<Button type="link" onClick={onAddSingle}>
{t('Add condition')}
</Button>
<Button type="link" onClick={onAddGroup}>
{t('Add condition group')}
</Button>
</div>
</div>
);
}
function CalculationConfig({ value, onChange }) {
const rule = value && Object.keys(value).length ? value : { group: { type: 'and', calculations: [] } };
return <CalculationGroup value={rule.group} onChange={(group) => onChange({ ...rule, group })} />;
}
export default {
title: `{{t("Condition", { ns: "${NAMESPACE}" })}}`,
type: 'condition',
group: 'control',
description: `{{t('Based on boolean result of the calculation to determine whether to "continue" or "exit" the process, or continue on different branches of "yes" and "no".', { ns: "${NAMESPACE}" })}}`,
fieldset: {
rejectOnFalse: {
type: 'boolean',
title: `{{t("Mode", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {
disabled: true,
},
enum: [
{
value: true,
label: `{{t('Continue when "Yes"', { ns: "${NAMESPACE}" })}}`,
},
{
value: false,
label: `{{t('Branch into "Yes" and "No"', { ns: "${NAMESPACE}" })}}`,
},
],
},
engine: {
type: 'string',
title: `{{t("Calculation engine", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'RadioWithTooltip',
'x-component-props': {
options: [
['basic', { label: `{{t("Basic", { ns: "${NAMESPACE}" })}}` }],
...Array.from(evaluators.getEntities()),
].reduce((result: RadioWithTooltipOption[], [value, options]: any) => result.concat({ value, ...options }), []),
},
required: true,
default: 'basic',
},
calculation: {
type: 'string',
title: `{{t("Condition", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'CalculationConfig',
'x-reactions': {
dependencies: ['engine'],
fulfill: {
state: {
visible: '{{$deps[0] === "basic"}}',
},
},
},
required: true,
},
expression: {
type: 'string',
title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'CalculationExpression',
['x-validator'](value, rules, { form }) {
const { values } = form;
const { evaluate } = evaluators.get(values.engine);
const exp = value.trim().replace(/{{([^{}]+)}}/g, ' 1 ');
try {
evaluate(exp);
return '';
} catch (e) {
return lang('Expression syntax error');
}
},
'x-reactions': {
dependencies: ['engine'],
fulfill: {
state: {
visible: '{{$deps[0] !== "basic"}}',
},
schema: {
description: '{{renderEngineReference($deps[0])}}',
},
},
},
required: true,
},
},
view: {},
options: [
{
label: `{{t('Continue when "Yes"', { ns: "${NAMESPACE}" })}}`,
key: 'rejectOnFalse',
value: { rejectOnFalse: true },
},
{
label: `{{t('Branch into "Yes" and "No"', { ns: "${NAMESPACE}" })}}`,
key: 'branch',
value: { rejectOnFalse: false },
},
],
component: function Component({ data }) {
const { t } = useTranslation();
const { nodes } = useFlowContext();
const {
id,
config: { rejectOnFalse },
} = data;
const trueEntry = nodes.find((item) => item.upstreamId === id && item.branchIndex === 1);
const falseEntry = nodes.find((item) => item.upstreamId === id && item.branchIndex === 0);
return (
<NodeDefaultView data={data}>
{rejectOnFalse ? null : (
<div className={cx(nodeSubtreeClass)}>
<div
className={cx(
branchBlockClass,
css`
> * > .workflow-branch-lines {
> button {
display: none;
}
}
`,
)}
>
<Branch from={data} entry={falseEntry} branchIndex={0} />
<Branch from={data} entry={trueEntry} branchIndex={1} />
</div>
<div
className={css`
position: relative;
height: 2em;
overflow: visible;
> span {
position: absolute;
top: calc(1.5em - 1px);
line-height: 1em;
color: #999;
background-color: #f0f2f5;
padding: 1px;
}
`}
>
<span
className={css`
right: 4em;
`}
>
{t('No')}
</span>
<span
className={css`
left: 4em;
`}
>
{t('Yes')}
</span>
</div>
</div>
)}
</NodeDefaultView>
);
},
scope: {
renderEngineReference,
useWorkflowVariableOptions,
},
components: {
CalculationConfig,
CalculationExpression(props) {
const scope = useWorkflowVariableOptions();
return <Variable.TextArea scope={scope} {...props} />;
},
RadioWithTooltip,
},
};