被雨水过滤的空气-Rain 29bf187fbf
chore: optimize locators (#2833)
* test(e2e): better locators for designer buttons

* fix: make test passing

* refactor: remove DesignerControl

* chore: better locators

* fix: should not disable add-menu-item

* chore: better test id for block

* chore: optimize Action

* chore: remove role in BlockItem

* feat: improve locators

* chore: menu & add block

* chore: initializer

* chore: testid -> aria label

* chore: tabs

* chore: designers

* refactor: optimize schemaInitializer

* refactor: rename

* chore: add collectionName

* chore: block item

* chore: action

* fix: avoid crashting

* chore(e2e): add __E2E__

* chore: all dialog

* chore: add aria-label for block menu

* Revert "chore: add aria-label for block menu"

This reverts commit 6a840ef816ee1095484dc268b5dfa1bbe6cd8cbe.

* chore: optimize aria-label of Action

* chore: schema-initializer

* chore(e2e): increase timeout

* chore: schema settings

* chore: optimize table

* chore: workflow

* chore: plugin manager

* chore: collection manager and workflow

* chore: details of workflow

* chore: remove testid of Select

* test: fix unit-tests

* test: fix unit-tests

* test(e2e): passing tests

* test: fix unit test

* chore: should use hover

* test: passing tests

* chore: passing tests

* chore: fix CI

* chore: fix CI

* chore: increase timeout in CI

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
2023-10-27 15:32:17 +08:00

422 lines
12 KiB
TypeScript

import { css } from '@emotion/css';
import { Field, GeneralField } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { useRequest } from 'ahooks';
import { Col, Row } from 'antd';
import merge from 'deepmerge';
import template from 'lodash/template';
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import {
TableFieldResource,
WithoutTableFieldResource,
useAPIClient,
useActionContext,
useDesignable,
useRecord,
} from '../';
import { ACLCollectionProvider } from '../acl/ACLProvider';
import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager';
import { FilterBlockRecord } from '../filter-provider/FilterProvider';
import { useRecordIndex } from '../record-provider';
import { SharedFilterProvider } from './SharedFilterProvider';
import { useAssociationNames } from './hooks';
export const BlockResourceContext = createContext(null);
export const BlockAssociationContext = createContext(null);
export const BlockRequestContext = createContext<{
block?: string;
props?: any;
field?: GeneralField;
service?: any;
resource?: any;
allowedActions?: any;
__parent?: any;
updateAssociationValues?: any[];
}>({});
export const useBlockResource = () => {
return useContext(BlockResourceContext);
};
interface UseResourceProps {
resource: any;
association?: any;
useSourceId?: any;
collection?: any;
block?: any;
}
export const useAssociation = (props) => {
const { association } = props;
const { getCollectionField } = useCollectionManager();
if (typeof association === 'string') {
return getCollectionField(association);
} else if (association?.collectionName && association?.name) {
return getCollectionField(`${association?.collectionName}.${association?.name}`);
}
};
const useResource = (props: UseResourceProps) => {
const { block, collection, resource, useSourceId } = props;
const record = useRecord();
const api = useAPIClient();
const { fieldSchema } = useActionContext();
const isCreateAction = fieldSchema?.['x-action'] === 'create';
const association = useAssociation(props);
console.log(association);
const sourceId = useSourceId?.();
const field = useField();
const withoutTableFieldResource = useContext(WithoutTableFieldResource);
const __parent = useContext(BlockRequestContext);
if (block === 'TableField') {
const options = {
field,
api,
resource,
sourceId: !isCreateAction
? sourceId || record[association?.sourceKey || 'id'] || record?.__parent?.[association?.sourceKey || 'id']
: undefined,
};
return new TableFieldResource(options);
}
if (
!withoutTableFieldResource &&
__parent?.block === 'TableField' &&
__parent?.resource instanceof TableFieldResource
) {
return __parent.resource;
}
if (!association) {
return api.resource(resource);
}
if (sourceId) {
return api.resource(resource, sourceId);
}
if (record[association?.sourceKey || 'id']) {
return api.resource(resource, record[association?.sourceKey || 'id']);
}
if (record?.__parent?.[association?.sourceKey || 'id']) {
return api.resource(resource, record.__parent[association?.sourceKey || 'id']);
}
return api.resource(collection);
};
const useActionParams = (props) => {
const { useParams } = props;
const params = useParams?.() || {};
return { ...props.params, ...params };
};
const useResourceAction = (props, opts = {}) => {
/**
* fieldName: 来自 TableFieldProvider
*/
const { resource, action, fieldName: tableFieldName, runWhenParamsChanged = false } = props;
const { fields } = useCollection();
const params = useActionParams(props);
const api = useAPIClient();
const fieldSchema = useFieldSchema();
const { snapshot } = useActionContext();
const record = useRecord();
if (!Reflect.has(params, 'appends')) {
const appends = fields?.filter((field) => field.target).map((field) => field.name);
if (appends?.length) {
params['appends'] = appends;
}
}
const result = useRequest(
snapshot
? async () => ({
data: record[tableFieldName] ?? [],
})
: (opts) => {
if (!action) {
return Promise.resolve({});
}
const actionParams = { ...params, ...opts };
if (params?.appends) {
actionParams.appends = params.appends;
}
return resource[action](actionParams).then((res) => res.data);
},
{
...opts,
onSuccess(data, params) {
opts?.['onSuccess']?.(data, params);
if (fieldSchema['x-uid']) {
api.services[fieldSchema['x-uid']] = result;
}
},
defaultParams: [params],
refreshDeps: [runWhenParamsChanged ? null : JSON.stringify(params.appends)],
},
);
// automatic run service when params has changed
const firstRun = useRef(false);
useEffect(() => {
if (!runWhenParamsChanged) {
return;
}
if (firstRun.current) {
result?.run({ ...result?.params?.[0], ...params });
}
firstRun.current = true;
}, [JSON.stringify(params), runWhenParamsChanged]);
return result;
};
export const MaybeCollectionProvider = (props) => {
const { collection } = props;
return collection ? (
<CollectionProvider collection={collection}>
<ACLCollectionProvider>{props.children}</ACLCollectionProvider>
</CollectionProvider>
) : (
props.children
);
};
export const BlockRequestProvider = (props) => {
const field = useField<Field>();
const resource = useBlockResource();
const [allowedActions, setAllowedActions] = useState({});
const service = useResourceAction(
{ ...props, resource },
{
...props.requestOptions,
},
);
// Infinite scroll support
const serviceAllowedActions = (service?.data as any)?.meta?.allowedActions;
useEffect(() => {
if (!serviceAllowedActions) return;
setAllowedActions((last) => {
return merge(last, serviceAllowedActions ?? {});
});
}, [serviceAllowedActions]);
const __parent = useContext(BlockRequestContext);
return (
<BlockRequestContext.Provider
value={{
allowedActions,
block: props.block,
props,
field,
service,
resource,
__parent,
updateAssociationValues: props?.updateAssociationValues || [],
}}
>
{props.children}
</BlockRequestContext.Provider>
);
};
export const useBlockRequestContext = () => {
return useContext(BlockRequestContext);
};
export const RenderChildrenWithAssociationFilter: React.FC<any> = (props) => {
const fieldSchema = useFieldSchema();
const { findComponent } = useDesignable();
const field = useField();
const Component = findComponent(field.component?.[0]) || React.Fragment;
const associationFilterSchema = fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'AssociationFilter') {
return s;
}
return buf;
}, null);
if (associationFilterSchema) {
return (
<Component {...field.componentProps}>
<Row
className={css`
height: 100%;
`}
gutter={16}
wrap={false}
>
<Col
className={css`
width: 200px;
flex: 0 0 auto;
`}
style={props.associationFilterStyle}
>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => s['x-component'] === 'AssociationFilter'}
/>
</Col>
<Col
className={css`
flex: 1 1 auto;
min-width: 0;
`}
>
<div
className={css`
display: flex;
flex-direction: column;
height: 100%;
`}
>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => s['x-component'] !== 'AssociationFilter'}
/>
</div>
</Col>
</Row>
</Component>
);
}
return props.children;
};
const BlockContext = createContext<{
/** 用以区分区块的标识 */
name: string;
}>(null);
export const useBlockContext = () => {
return useContext(BlockContext);
};
export const BlockProvider = (props: {
name: string;
resource: any;
collection?: any;
association?: any;
params?: any;
children?: any;
}) => {
const { collection, association, name } = props;
const resource = useResource(props);
const params = useMemo(() => ({ ...props.params }), [props.params]);
const { appends, updateAssociationValues } = useAssociationNames();
const blockValue = useMemo(() => ({ name }), [name]);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
return (
<BlockContext.Provider value={blockValue}>
<MaybeCollectionProvider collection={collection}>
<BlockAssociationContext.Provider value={association}>
<BlockResourceContext.Provider value={resource}>
<BlockRequestProvider {...props} updateAssociationValues={updateAssociationValues} params={params}>
<SharedFilterProvider {...props} params={params}>
<FilterBlockRecord {...props} params={params}>
{props.children}
</FilterBlockRecord>
</SharedFilterProvider>
</BlockRequestProvider>
</BlockResourceContext.Provider>
</BlockAssociationContext.Provider>
</MaybeCollectionProvider>
</BlockContext.Provider>
);
};
export const useBlockAssociationContext = () => {
return useContext(BlockAssociationContext);
};
export const useFilterByTk = () => {
const { resource, __parent } = useContext(BlockRequestContext);
const recordIndex = useRecordIndex();
const record = useRecord();
const collection = useCollection();
const { getCollectionField } = useCollectionManager();
const assoc = useContext(BlockAssociationContext);
const withoutTableFieldResource = useContext(WithoutTableFieldResource);
if (!withoutTableFieldResource) {
if (resource instanceof TableFieldResource || __parent?.block === 'TableField') {
return recordIndex;
}
}
if (assoc) {
const association = getCollectionField(assoc);
return record?.[association.targetKey || 'id'];
}
return record?.[collection.filterTargetKey || 'id'];
};
export const useSourceIdFromRecord = () => {
const record = useRecord();
const { getCollectionField } = useCollectionManager();
const assoc = useContext(BlockAssociationContext);
if (assoc) {
const association = getCollectionField(assoc);
return record?.[association.sourceKey || 'id'];
}
};
export const useSourceIdFromParentRecord = () => {
const record = useRecord();
const { getCollectionField } = useCollectionManager();
const assoc = useContext(BlockAssociationContext);
if (assoc) {
const association = getCollectionField(assoc);
return record?.__parent?.[association.sourceKey || 'id'];
}
};
export const useParamsFromRecord = () => {
const filterByTk = useFilterByTk();
const record = useRecord();
const { fields } = useCollection();
const fieldSchema = useFieldSchema();
const { getCollectionJoinField } = useCollectionManager();
const collectionField = getCollectionJoinField(fieldSchema?.['x-decorator-props']?.resource);
const filterFields = fields
.filter((v) => {
return ['boolean', 'date', 'integer', 'radio', 'sort', 'string', 'time', 'uid', 'uuid'].includes(v.type);
})
.map((v) => v.name);
const filter = Object.keys(record)
.filter((key) => filterFields.includes(key))
.reduce((result, key) => {
result[key] = record[key];
return result;
}, {});
const obj = {
filterByTk: filterByTk,
};
if (record.__collection && !['oho', 'm2o', 'obo'].includes(collectionField?.interface)) {
obj['targetCollection'] = record.__collection;
}
if (!filterByTk) {
obj['filter'] = filter;
}
return obj;
};
export const RecordLink = (props) => {
const field = useField();
const record = useRecord();
const { title, to, ...others } = props;
const compiled = template(to || '');
return (
<Link {...others} to={compiled({ record: record || {} })}>
{field.title}
</Link>
);
};