2023-05-17 13:37:03 +08:00
import { useForm } from '@formily/react' ;
2023-07-30 17:52:35 +08:00
import { Cascader } from 'antd' ;
2023-08-12 09:03:08 +08:00
import React , { useCallback , useEffect , useState } from 'react' ;
2023-05-17 13:37:03 +08:00
import {
2023-07-30 17:52:35 +08:00
SchemaComponentContext ,
2023-12-04 14:56:46 +08:00
SchemaInitializerItemType ,
2023-12-07 21:46:58 +08:00
css ,
2024-03-25 14:46:22 +08:00
joinCollectionName ,
parseCollectionName ,
2023-05-17 13:37:03 +08:00
useCollectionDataSource ,
2023-12-07 21:46:58 +08:00
useCollectionFilterOptions ,
2024-03-03 23:06:24 +08:00
useCollectionManager_deprecated ,
2023-07-30 17:52:35 +08:00
useCompile ,
2023-05-17 13:37:03 +08:00
} from '@nocobase/client' ;
2023-12-07 21:46:58 +08:00
import {
FieldsSelect ,
FilterDynamicComponent ,
ValueBlock ,
BaseTypeSets ,
defaultFieldNames ,
nodesOptions ,
triggerOptions ,
Instruction ,
} from '@nocobase/plugin-workflow/client' ;
import { NAMESPACE , useLang } from '../locale' ;
2023-05-17 13:37:03 +08:00
2023-09-26 23:39:50 +08:00
function matchToManyField ( field ) : boolean {
// const fieldPrefix = `${field.name}.`;
return [ 'hasMany' , 'belongsToMany' ] . includes ( field . type ) ;
2023-05-17 13:37:03 +08:00
}
2023-08-12 09:03:08 +08:00
function useAssociatedFields() {
2023-05-17 13:37:03 +08:00
const compile = useCompile ( ) ;
2023-08-12 09:03:08 +08:00
return [ nodesOptions , triggerOptions ] . map ( ( item ) = > {
2023-09-26 23:39:50 +08:00
const children = item
. useOptions ( {
types : [ matchToManyField ] ,
appends : null ,
2023-10-23 00:05:15 +08:00
depth : 4 ,
2023-09-26 23:39:50 +08:00
} )
? . filter ( Boolean ) ;
2023-05-17 13:37:03 +08:00
return {
label : compile ( item . label ) ,
value : item.value ,
key : item.value ,
children : compile ( children ) ,
disabled : children && ! children . length ,
} ;
} ) ;
2023-08-12 09:03:08 +08:00
}
function AssociatedConfig ( { value , onChange , . . . props } ) : JSX . Element {
const { setValuesIn } = useForm ( ) ;
2024-03-03 23:06:24 +08:00
const { getCollection } = useCollectionManager_deprecated ( ) ;
2023-08-12 09:03:08 +08:00
const baseOptions = useAssociatedFields ( ) ;
const [ options , setOptions ] = useState ( baseOptions ) ;
2023-05-17 13:37:03 +08:00
const { associatedKey = '' , name : fieldName } = value ? ? { } ;
let p = [ ] ;
const matched = associatedKey . match ( /^{{(.*)}}$/ ) ;
if ( matched ) {
p = [ . . . matched [ 1 ] . trim ( ) . split ( '.' ) . slice ( 0 , - 1 ) , fieldName ] ;
}
2023-08-12 09:03:08 +08:00
const loadData = async ( selectedOptions ) = > {
const option = selectedOptions [ selectedOptions . length - 1 ] ;
if ( ! option . children ? . length && ! option . isLeaf && option . loadChildren ) {
await option . loadChildren ( option ) ;
setOptions ( ( prev ) = > [ . . . prev ] ) ;
}
} ;
useEffect ( ( ) = > {
const run = async ( ) = > {
if ( ! p || options . length <= 1 ) {
return ;
}
let prevOption = null ;
for ( let i = 0 ; i < p . length ; i ++ ) {
const key = p [ i ] ;
try {
if ( i === 0 ) {
prevOption = options . find ( ( item ) = > item . value === key ) ;
} else {
if ( prevOption . loadChildren && ! prevOption . children ? . length ) {
await prevOption . loadChildren ( prevOption ) ;
}
prevOption = prevOption . children . find ( ( item ) = > item . value === key ) ;
}
} catch ( err ) {
console . error ( err ) ;
}
}
setOptions ( [ . . . options ] ) ;
} ;
run ( ) ;
// NOTE: watch `options.length` and it only happens once
} , [ value , options . length ] ) ;
2023-05-17 13:37:03 +08:00
const onSelectChange = useCallback (
( path , option ) = > {
if ( ! path ? . length ) {
setValuesIn ( 'collection' , null ) ;
onChange ( { } ) ;
return ;
}
// const associationFieldName = path.pop();
const { field } = option . pop ( ) ;
2023-09-26 23:39:50 +08:00
if ( ! field || ! [ 'hasMany' , 'belongsToMany' ] . includes ( field . type ) ) {
return ;
}
2023-05-17 13:37:03 +08:00
// need to get:
// * source collection (from node.config)
// * target collection (from field name)
2024-03-25 14:46:22 +08:00
const { collectionName , target , name , dataSourceKey } = field ;
2023-05-17 13:37:03 +08:00
2024-03-25 14:46:22 +08:00
const collection = getCollection ( collectionName , dataSourceKey ) ;
2023-05-17 13:37:03 +08:00
const primaryKeyField = collection . fields . find ( ( f ) = > f . primaryKey ) ;
2024-03-25 14:46:22 +08:00
setValuesIn ( 'collection' , ` ${ dataSourceKey } : ${ target } ` ) ;
2023-05-17 13:37:03 +08:00
onChange ( {
name ,
// primary key data path
associatedKey : ` {{ ${ path . slice ( 0 , - 1 ) . join ( '.' ) } . ${ primaryKeyField . name } }} ` ,
// data associated collection name
2024-03-25 14:46:22 +08:00
associatedCollection : joinCollectionName ( dataSourceKey , collectionName ) ,
2023-05-17 13:37:03 +08:00
} ) ;
} ,
[ onChange ] ,
) ;
2024-03-25 14:46:22 +08:00
return (
< Cascader
{ . . . props }
value = { p }
options = { options }
changeOnSelect
onChange = { onSelectChange }
loadData = { loadData as any }
/ >
) ;
2023-05-17 13:37:03 +08:00
}
// based on collection:
// { collection, field }
// based on data associated collection
// { key: '{{$context.data.id}}', collection: "collection.association", field }
// select data based
2023-12-07 21:46:58 +08:00
export default class extends Instruction {
title = ` {{t("Aggregate", { ns: " ${ NAMESPACE } " })}} ` ;
type = 'aggregate' ;
group = 'collection' ;
description = ` {{t("Counting, summing, finding maximum, minimum, and average values for multiple records of a collection or associated data of a record.", { ns: " ${ NAMESPACE } " })}} ` ;
fieldset = {
2023-05-17 13:37:03 +08:00
aggregator : {
type : 'string' ,
title : ` {{t("Aggregator function", { ns: " ${ NAMESPACE } " })}} ` ,
'x-decorator' : 'FormItem' ,
'x-component' : 'Radio.Group' ,
enum : [
{ label : 'COUNT' , value : 'count' } ,
{ label : 'SUM' , value : 'sum' } ,
{ label : 'AVG' , value : 'avg' } ,
{ label : 'MIN' , value : 'min' } ,
{ label : 'MAX' , value : 'max' } ,
] ,
required : true ,
default : 'count' ,
} ,
associated : {
type : 'boolean' ,
title : ` {{t("Target type", { ns: " ${ NAMESPACE } " })}} ` ,
'x-decorator' : 'FormItem' ,
'x-component' : 'Radio.Group' ,
enum : [
{ label : ` {{t("Data of collection", { ns: " ${ NAMESPACE } " })}} ` , value : false } ,
{ label : ` {{t("Data of associated collection", { ns: " ${ NAMESPACE } " })}} ` , value : true } ,
] ,
required : true ,
default : false ,
'x-reactions' : [
{
target : 'collection' ,
effects : [ 'onFieldValueChange' ] ,
fulfill : {
state : {
value : null ,
} ,
} ,
} ,
{
target : 'association' ,
effects : [ 'onFieldValueChange' ] ,
fulfill : {
state : {
value : null ,
} ,
} ,
} ,
] ,
} ,
collectionField : {
type : 'void' ,
'x-decorator' : 'SchemaComponentContext.Provider' ,
'x-decorator-props' : {
value : { designable : false } ,
} ,
'x-component' : 'Grid' ,
properties : {
row : {
type : 'void' ,
'x-component' : 'Grid.Row' ,
properties : {
target : {
type : 'void' ,
'x-component' : 'Grid.Col' ,
properties : {
collection : {
2023-12-07 21:46:58 +08:00
type : 'string' ,
required : true ,
'x-decorator' : 'FormItem' ,
2024-03-25 14:46:22 +08:00
'x-component' : 'DataSourceCollectionCascader' ,
2023-05-17 13:37:03 +08:00
title : ` {{t("Data of collection", { ns: " ${ NAMESPACE } " })}} ` ,
'x-reactions' : [
{
dependencies : [ 'associated' ] ,
fulfill : {
state : {
display : '{{$deps[0] ? "hidden" : "visible"}}' ,
} ,
} ,
} ,
{
target : 'params.field' ,
effects : [ 'onFieldValueChange' ] ,
fulfill : {
state : {
value : null ,
} ,
} ,
} ,
{
target : 'params.filter' ,
effects : [ 'onFieldValueChange' ] ,
fulfill : {
state : {
value : null ,
} ,
} ,
} ,
] ,
} ,
association : {
type : 'object' ,
title : ` {{t("Data of associated collection", { ns: " ${ NAMESPACE } " })}} ` ,
'x-decorator' : 'FormItem' ,
'x-component' : 'AssociatedConfig' ,
2023-09-26 23:39:50 +08:00
'x-component-props' : {
changeOnSelect : true ,
} ,
2023-05-17 13:37:03 +08:00
'x-reactions' : [
{
dependencies : [ 'associated' ] ,
fulfill : {
state : {
visible : '{{!!$deps[0]}}' ,
} ,
} ,
} ,
] ,
required : true ,
} ,
} ,
} ,
field : {
type : 'void' ,
'x-component' : 'Grid.Col' ,
properties : {
'params.field' : {
type : 'string' ,
title : ` {{t("Field to aggregate", { ns: " ${ NAMESPACE } " })}} ` ,
'x-decorator' : 'FormItem' ,
'x-component' : 'FieldsSelect' ,
'x-component-props' : {
filter ( field ) {
return (
! field . hidden &&
field . interface &&
! [ 'belongsTo' , 'hasOne' , 'hasMany' , 'belongsToMany' ] . includes ( field . type )
) ;
} ,
} ,
required : true ,
'x-reactions' : [
{
dependencies : [ 'collection' ] ,
fulfill : {
state : {
visible : '{{!!$deps[0]}}' ,
} ,
} ,
} ,
] ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
params : {
type : 'object' ,
properties : {
distinct : {
type : 'boolean' ,
title : ` {{t("Distinct", { ns: " ${ NAMESPACE } " })}} ` ,
'x-decorator' : 'FormItem' ,
'x-component' : 'Checkbox' ,
'x-reactions' : [
{
dependencies : [ 'collection' , 'aggregator' ] ,
fulfill : {
state : {
visible : '{{!!$deps[0] && ["count"].includes($deps[1])}}' ,
} ,
} ,
} ,
] ,
} ,
filter : {
2023-12-07 21:46:58 +08:00
type : 'object' ,
title : '{{t("Filter")}}' ,
'x-decorator' : 'FormItem' ,
'x-component' : 'Filter' ,
'x-component-props' : {
useProps() {
const { values } = useForm ( ) ;
2024-03-25 14:46:22 +08:00
const [ dataSourceName , collectionName ] = parseCollectionName ( values ? . collection ) ;
const options = useCollectionFilterOptions ( collectionName , dataSourceName ) ;
2023-12-07 21:46:58 +08:00
return {
options ,
className : css `
position : relative ;
width : 100 % ;
` ,
} ;
} ,
dynamicComponent : 'FilterDynamicComponent' ,
} ,
2023-05-17 13:37:03 +08:00
'x-reactions' : [
{
dependencies : [ 'collection' ] ,
fulfill : {
state : {
visible : '{{!!$deps[0]}}' ,
} ,
} ,
} ,
] ,
} ,
} ,
} ,
2023-12-07 21:46:58 +08:00
} ;
scope = {
2023-05-17 13:37:03 +08:00
useCollectionDataSource ,
2023-12-07 21:46:58 +08:00
} ;
components = {
2023-05-17 13:37:03 +08:00
SchemaComponentContext ,
FilterDynamicComponent ,
FieldsSelect ,
ValueBlock ,
AssociatedConfig ,
2023-12-07 21:46:58 +08:00
} ;
2023-10-25 23:14:54 +08:00
useVariables ( { key , title } , { types , fieldNames = defaultFieldNames } ) {
2023-05-17 13:37:03 +08:00
if (
types &&
! types . some ( ( type ) = > type in BaseTypeSets || Object . values ( BaseTypeSets ) . some ( ( set ) = > set . has ( type ) ) )
) {
return null ;
}
2023-07-06 12:27:34 +08:00
return {
2023-10-25 23:14:54 +08:00
[ fieldNames . value ] : key ,
2023-07-18 12:50:24 +08:00
[ fieldNames . label ] : title ,
2023-07-06 12:27:34 +08:00
} ;
2023-12-07 21:46:58 +08:00
}
2023-12-04 14:56:46 +08:00
useInitializers ( node ) : SchemaInitializerItemType | null {
2023-12-07 21:46:58 +08:00
// eslint-disable-next-line react-hooks/rules-of-hooks
const resultTitle = useLang ( 'Query result' ) ;
2023-05-17 13:37:03 +08:00
if ( ! node . config . collection ) {
return null ;
}
return {
2023-12-07 21:46:58 +08:00
name : ` # ${ node . id } ` ,
2023-05-17 13:37:03 +08:00
type : 'item' ,
title : node.title ? ? ` # ${ node . id } ` ,
2023-12-04 14:56:46 +08:00
Component : ValueBlock.Initializer ,
2023-05-17 13:37:03 +08:00
node ,
2023-12-07 21:46:58 +08:00
resultTitle ,
2023-05-17 13:37:03 +08:00
} ;
2023-12-07 21:46:58 +08:00
}
}