From b7d23c408a19e5c895ef3b62a728a2ec796e050e Mon Sep 17 00:00:00 2001 From: Dunqing Date: Thu, 10 Aug 2023 17:36:02 +0800 Subject: [PATCH] feat(map-plugin): map block support select map field of association tables (#2214) * feat(map-plugin): map block support select map field of association tables * fix: update incorrect default value * fix: should support o2m and fix bugs * fix: height Close T1185 Close 1183 * fix: o2m, m2m cannot display data * fix: switch map field will break --- .../src/schema-settings/SchemaSettings.tsx | 1 + .../plugins/map/src/client/block/MapBlock.tsx | 16 ++- .../map/src/client/block/MapBlockDesigner.tsx | 61 +++++----- .../src/client/block/MapBlockInitializer.tsx | 29 ++--- .../map/src/client/block/MapBlockProvider.tsx | 8 +- .../plugins/map/src/client/block/utils.ts | 10 ++ .../map/src/client/components/AMap/Block.tsx | 106 ++++++++++-------- .../client/components/GoogleMaps/Block.tsx | 98 ++++++++-------- .../plugins/map/src/client/locale/index.ts | 6 +- packages/plugins/map/src/client/utils.ts | 9 ++ 10 files changed, 202 insertions(+), 142 deletions(-) diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index e230f531b..15cd39248 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -728,6 +728,7 @@ SchemaSettings.CascaderItem = (props: CascaderProps & { title: any }) => { onChange={onChange as any} options={options} style={{ textAlign: 'right', minWidth: 100 }} + {...props} /> diff --git a/packages/plugins/map/src/client/block/MapBlock.tsx b/packages/plugins/map/src/client/block/MapBlock.tsx index 80b396d0f..4aecd8012 100644 --- a/packages/plugins/map/src/client/block/MapBlock.tsx +++ b/packages/plugins/map/src/client/block/MapBlock.tsx @@ -1,11 +1,15 @@ -import React, { useState } from 'react'; +import { useCollection, useCollectionManager, useProps } from '@nocobase/client'; +import React, { useMemo } from 'react'; import { MapBlockComponent } from '../components'; -import { useCollection, useProps } from '@nocobase/client'; export const MapBlock = (props) => { const { fieldNames } = useProps(props); - const { getField } = useCollection(); - const field = getField(fieldNames?.field); - const fieldComponentProps = field?.uiSchema?.['x-component-props']; - return ; + const { getCollectionJoinField } = useCollectionManager(); + const { name } = useCollection(); + const collectionField = useMemo(() => { + return getCollectionJoinField([name, fieldNames?.field].flat().join('.')); + }, [name, fieldNames?.field]); + + const fieldComponentProps = collectionField?.uiSchema?.['x-component-props']; + return ; }; diff --git a/packages/plugins/map/src/client/block/MapBlockDesigner.tsx b/packages/plugins/map/src/client/block/MapBlockDesigner.tsx index f968261f1..a7910455f 100644 --- a/packages/plugins/map/src/client/block/MapBlockDesigner.tsx +++ b/packages/plugins/map/src/client/block/MapBlockDesigner.tsx @@ -13,9 +13,9 @@ import { } from '@nocobase/client'; import lodash from 'lodash'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useMapTranslation } from '../locale'; import { useMapBlockContext } from './MapBlockProvider'; +import { findNestedOption } from './utils'; export const MapBlockDesigner = () => { const { name, title } = useCollection(); @@ -23,8 +23,7 @@ export const MapBlockDesigner = () => { const fieldSchema = useFieldSchema(); const dataSource = useCollectionFilterOptions(name); const { service } = useMapBlockContext(); - const { t: mapT } = useMapTranslation(); - const { t } = useTranslation(); + const { t } = useMapTranslation(); const { dn } = useDesignable(); const { getCollectionFieldsOptions } = useCollectionManager(); const collection = useCollection(); @@ -35,17 +34,21 @@ export const MapBlockDesigner = () => { const template = useSchemaTemplate(); - const mapFieldOptions = getCollectionFieldsOptions(collection?.name, ['point', 'lineString', 'polygon']); + const mapFieldOptions = getCollectionFieldsOptions(collection?.name, ['point', 'lineString', 'polygon'], { + association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'], + }); const markerFieldOptions = getCollectionFieldsOptions(collection?.name, 'string'); + const isPointField = findNestedOption(fieldNames.field, mapFieldOptions)?.type === 'point'; return ( - { const fieldNames = field.decoratorProps.fieldNames || {}; fieldNames['field'] = v; @@ -61,34 +64,36 @@ export const MapBlockDesigner = () => { dn.refresh(); }} /> - { - const fieldNames = field.decoratorProps.fieldNames || {}; - fieldNames['marker'] = v; - field.decoratorProps.fieldNames = fieldNames; - fieldSchema['x-decorator-props']['fieldNames'] = fieldNames; - service.refresh(); - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-decorator-props': field.decoratorProps, - }, - }); - dn.refresh(); - }} - /> + {isPointField ? ( + { + const fieldNames = field.decoratorProps.fieldNames || {}; + fieldNames['marker'] = v; + field.decoratorProps.fieldNames = fieldNames; + fieldSchema['x-decorator-props']['fieldNames'] = fieldNames; + service.refresh(); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': field.decoratorProps, + }, + }); + dn.refresh(); + }} + /> + ) : null} { const { insert } = props; @@ -26,7 +26,9 @@ export const MapBlockInitializer = (props) => { componentType={'Map'} icon={} onCreateBlockSchema={async ({ item }) => { - const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon']); + const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], { + association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'], + }); const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string'); const values = await FormDialog( t('Create map block'), @@ -41,9 +43,13 @@ export const MapBlockInitializer = (props) => { title: t('Map field'), enum: mapFieldOptions, required: true, - 'x-component': 'Select', + 'x-component': 'Cascader', 'x-decorator': 'FormItem', - default: mapFieldOptions[0]?.value, + default: mapFieldOptions.length + ? [mapFieldOptions[0].value, mapFieldOptions[0].children?.[0].value].filter( + (v) => v !== undefined && v !== null, + ) + : [], }, marker: { title: t('Marker field'), @@ -52,17 +58,14 @@ export const MapBlockInitializer = (props) => { 'x-decorator': 'FormItem', 'x-reactions': (field) => { const value = field.form.values.field; - console.log('πŸš€ ~ file: MapBlockInitializer.tsx:45 ~ values ~ value:', value); - console.log( - 'πŸš€ ~ file: MapBlockInitializer.tsx:50 ~ values ~ mapFieldOptions:', - mapFieldOptions, - ); - - if (!value) { + if (!value?.length) { return; } - const item = mapFieldOptions.find((item) => item.value === value).type; - field.hidden = item !== 'point'; + const item = findNestedOption(value, mapFieldOptions); + + if (item) { + field.hidden = item.type !== 'point'; + } }, }, }, diff --git a/packages/plugins/map/src/client/block/MapBlockProvider.tsx b/packages/plugins/map/src/client/block/MapBlockProvider.tsx index dab8272b9..a29a647a3 100644 --- a/packages/plugins/map/src/client/block/MapBlockProvider.tsx +++ b/packages/plugins/map/src/client/block/MapBlockProvider.tsx @@ -33,8 +33,14 @@ const InternalMapBlockProvider = (props) => { }; export const MapBlockProvider = (props) => { + const { params, fieldNames } = props; + const appends = params.appends || []; + const { field } = fieldNames || {}; + if (Array.isArray(field) && field.length > 1) { + appends.push(field[0]); + } return ( - + ); diff --git a/packages/plugins/map/src/client/block/utils.ts b/packages/plugins/map/src/client/block/utils.ts index ca4d7066a..e1e65af80 100644 --- a/packages/plugins/map/src/client/block/utils.ts +++ b/packages/plugins/map/src/client/block/utils.ts @@ -79,3 +79,13 @@ export const createMapBlockSchema = (options) => { }; return schema; }; + +export const findNestedOption = (value: string[] | string, options = []) => { + if (typeof value === 'string') { + value = [value]; + } + return value?.reduce((cur, v, index) => { + const matched = cur?.find((item) => item.value === v); + return index === value.length - 1 ? matched : matched?.children; + }, options); +}; diff --git a/packages/plugins/map/src/client/components/AMap/Block.tsx b/packages/plugins/map/src/client/components/AMap/Block.tsx index a83c72d2a..982225607 100644 --- a/packages/plugins/map/src/client/components/AMap/Block.tsx +++ b/packages/plugins/map/src/client/components/AMap/Block.tsx @@ -2,9 +2,10 @@ import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/ import { RecursionField, Schema, useFieldSchema } from '@formily/react'; import { ActionContextProvider, - css, RecordProvider, + css, useCollection, + useCollectionManager, useCompile, useFilterAPI, useProps, @@ -15,11 +16,13 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { defaultImage, selectedImage } from '../../constants'; import { useMapTranslation } from '../../locale'; import { AMapComponent, AMapForwardedRefProps } from './Map'; +import { getSource } from '../../utils'; export const AMapBlock = (props) => { - const { fieldNames, dataSource = [], fixedBlock, zoom, setSelectedRecordKeys } = useProps(props); - const { getField, getPrimaryKey } = useCollection(); - const field = getField(fieldNames?.field); + const { collectionField, fieldNames, dataSource, fixedBlock, zoom, setSelectedRecordKeys } = useProps(props); + const { name, getPrimaryKey } = useCollection(); + const { getCollectionJoinField } = useCollectionManager(); + const primaryKey = getPrimaryKey(); const [isMapInitialization, setIsMapInitialization] = useState(false); const mapRef = useRef(); const geometryUtils: AMap.IGeometryUtil = mapRef.current?.aMap?.GeometryUtil; @@ -28,7 +31,7 @@ export const AMapBlock = (props) => { const { t } = useMapTranslation(); const compile = useCompile(); const { isConnected, doFilter } = useFilterAPI(); - const [, setPrevSelected] = useState(null); + const [, setPrevSelected] = useState(null); const selectingModeRef = useRef(selectingMode); selectingModeRef.current = selectingMode; @@ -38,7 +41,7 @@ export const AMapBlock = (props) => { extData.selected = !selected; if ('setIcon' in overlay) { overlay.setIcon( - new mapRef.current.aMap.Icon({ + new mapRef.current!.aMap.Icon({ imageSize: [19, 32], image: selected ? defaultImage : selectedImage, } as AMap.IconOpts), @@ -54,9 +57,9 @@ export const AMapBlock = (props) => { const removeSelection = () => { if (!mapRef.current) return; - mapRef.current.mouseTool().close(true); - mapRef.current.editor().setTarget(null); - mapRef.current.editor().close(); + mapRef.current?.mouseTool().close(true); + mapRef.current?.editor().setTarget(null); + mapRef.current?.editor().close(); }; // selection @@ -64,11 +67,11 @@ export const AMapBlock = (props) => { if (selectingMode !== 'selection') { return; } - if (!mapRef.current.editor()) { - mapRef.current.createEditor('polygon'); - mapRef.current.createMouseTool('polygon'); + if (!mapRef.current?.editor()) { + mapRef.current?.createEditor('polygon'); + mapRef.current?.createMouseTool('polygon'); } else { - mapRef.current.executeMouseTool('polygon'); + mapRef.current?.executeMouseTool('polygon'); } return () => { removeSelection(); @@ -79,7 +82,7 @@ export const AMapBlock = (props) => { if (selectingMode) { return () => { if (!selectingModeRef.current) { - mapRef.current.map.getAllOverlays().forEach((o) => { + mapRef.current?.map.getAllOverlays().forEach((o) => { setOverlayOptions(o, false); }); } @@ -88,47 +91,54 @@ export const AMapBlock = (props) => { }, [selectingMode]); const onSelectingComplete = useMemoizedFn(() => { - const selectingOverlay = mapRef.current.editor().getTarget(); - const overlays = mapRef.current.map.getAllOverlays(); - const selectedOverlays = overlays.filter((o) => { + const selectingOverlay = mapRef.current?.editor().getTarget(); + const overlays = mapRef.current?.map.getAllOverlays(); + const selectedOverlays = overlays?.filter((o) => { if (o === selectingOverlay || o.getExtData().id === undefined) return; if ('getPosition' in o) { - return geometryUtils.isPointInRing(o.getPosition(), selectingOverlay.getPath() as any); + return geometryUtils.isPointInRing(o.getPosition(), selectingOverlay?.getPath() as any); } - return geometryUtils.doesRingRingIntersect(o.getPath(), selectingOverlay.getPath() as any); + return geometryUtils.doesRingRingIntersect(o.getPath(), selectingOverlay?.getPath() as any); }); - const ids = selectedOverlays.map((o) => { + const ids = selectedOverlays?.map((o) => { setOverlayOptions(o, true); return o.getExtData().id; }); - setSelectedRecordKeys((lastIds) => ids.concat(lastIds)); - selectingOverlay.remove(); - mapRef.current.editor().close(); + setSelectedRecordKeys((lastIds) => ids?.concat(lastIds)); + selectingOverlay?.remove(); + mapRef.current?.editor().close(); }); useEffect(() => { - if (!field || !mapRef.current) return; + if (!collectionField || !mapRef.current || !dataSource) return; + const fieldPaths = + Array.isArray(fieldNames?.field) && fieldNames?.field.length > 1 + ? fieldNames?.field.slice(0, -1) + : fieldNames?.field; + const cf = getCollectionJoinField([name, ...fieldPaths].flat().join('.')); const overlays = dataSource .map((item) => { - const data = item[fieldNames?.field]; - if (!data) return; - const overlay = mapRef.current.setOverlay(field.type, data, { - strokeColor: '#4e9bff', - fillColor: '#4e9bff', - cursor: 'pointer', - label: { - direction: 'bottom', - offset: [0, 5], - content: fieldNames?.marker ? compile(item[fieldNames.marker]) : undefined, - }, - extData: { - id: item[getPrimaryKey()], - }, + const data = getSource(item, fieldNames?.field, cf?.interface); + if (!data?.length) return []; + return data?.filter(Boolean).map((mapItem) => { + const overlay = mapRef.current?.setOverlay(collectionField.type, mapItem, { + strokeColor: '#4e9bff', + fillColor: '#4e9bff', + cursor: 'pointer', + label: { + direction: 'bottom', + offset: [0, 5], + content: fieldNames?.marker ? compile(item[fieldNames.marker]) : undefined, + }, + extData: { + id: item[primaryKey], + }, + }); + return overlay; }); - return overlay; }) - .filter(Boolean); - mapRef.current.map?.setFitView(overlays); + .flat(); + mapRef.current?.map?.setFitView(overlays); const events = overlays.map((o: AMap.Marker) => { const onClick = (e) => { @@ -144,8 +154,8 @@ export const AMapBlock = (props) => { } return; } - const data = dataSource?.find((item) => { - return extData.id === item[getPrimaryKey()]; + const data = dataSource.find((item) => { + return extData.id === item[primaryKey]; }); // η­›ι€‰εŒΊε—ζ¨‘εΌ @@ -160,7 +170,7 @@ export const AMapBlock = (props) => { return null; } else { selectMarker(o); - doFilter(data[getPrimaryKey()], (target) => target.field || getPrimaryKey(), '$eq'); + doFilter(data[primaryKey], (target) => target.field || primaryKey, '$eq'); } return o; }); @@ -182,7 +192,7 @@ export const AMapBlock = (props) => { }); events.forEach((e) => e()); }; - }, [dataSource, isMapInitialization, fieldNames, field.type, isConnected]); + }, [dataSource, isMapInitialization, fieldNames, name, primaryKey, collectionField.type, isConnected]); useEffect(() => { setTimeout(() => { @@ -210,7 +220,7 @@ export const AMapBlock = (props) => { z-index: 999; `} > - {isMapInitialization && !mapRef.current.errMessage ? ( + {isMapInitialization && !mapRef.current?.errMessage ? (