From 20c21dcfb0724f17a8361fc8a1f6e4d543f7f6fb Mon Sep 17 00:00:00 2001 From: Teale Fristoe Date: Tue, 22 Oct 2024 20:27:58 -0700 Subject: [PATCH] 188286105 Drag from Plugins to Codap (#1532) * Use function for getOverlayDragId. * Add app wide plugin AttributeDropOverlay. * Add notify attribute dragStart, dragMove, and dragEnd handlers. * Standardize fieldRequiredResults. * Fix notifications broadcast from attributeLocation API requests. --------- Co-authored-by: Kirk Swenson --- v3/src/components/case-card/case-card.tsx | 9 +- v3/src/components/case-table/case-table.tsx | 9 +- .../case-tile-component-handler.ts | 4 +- .../case-tile-common/case-tile-types.ts | 2 + .../container/container-constants.ts | 1 + v3/src/components/container/container.tsx | 22 ++++- .../drag-drop/attribute-drag-overlay.tsx | 33 ++++++- .../drag-drop/drag-drop-constants.ts | 1 + .../drag-drop/plugin-attribute-drag.scss | 7 ++ .../drag-drop/plugin-attribute-drag.tsx | 26 ++++++ .../graph/components/graph-component.tsx | 5 +- .../map/components/map-component.tsx | 5 +- v3/src/components/web-view/web-view-defs.ts | 1 + v3/src/components/web-view/web-view.tsx | 3 +- .../data-interactive-state.ts | 77 ++++++++++++++++ .../data-interactive-types.ts | 11 ++- .../handlers/attribute-handler.ts | 88 +++++++++++++++++-- .../handlers/data-context-handler.ts | 7 +- .../data-interactive/handlers/di-results.ts | 4 + .../handlers/handler-functions.ts | 8 +- .../handlers/interactive-frame-handler.ts | 2 +- .../handlers/item-search-handler.ts | 7 +- v3/src/hooks/use-drag-drop.ts | 7 +- v3/src/lib/dnd-kit/codap-dnd-context.tsx | 7 +- v3/src/models/data/data-set-utils.ts | 2 +- 25 files changed, 291 insertions(+), 57 deletions(-) create mode 100644 v3/src/components/container/container-constants.ts create mode 100644 v3/src/components/drag-drop/drag-drop-constants.ts create mode 100644 v3/src/components/drag-drop/plugin-attribute-drag.scss create mode 100644 v3/src/components/drag-drop/plugin-attribute-drag.tsx create mode 100644 v3/src/data-interactive/data-interactive-state.ts diff --git a/v3/src/components/case-card/case-card.tsx b/v3/src/components/case-card/case-card.tsx index 76c224e9ac..d9d5e46b02 100644 --- a/v3/src/components/case-card/case-card.tsx +++ b/v3/src/components/case-card/case-card.tsx @@ -4,10 +4,11 @@ import { observer } from "mobx-react-lite" import React, { useCallback, useRef } from "react" // import { useResizeDetector } from "react-resize-detector" import { useDataSet } from "../../hooks/use-data-set" +import { getOverlayDragId } from "../../hooks/use-drag-drop" import { useInstanceIdContext } from "../../hooks/use-instance-id-context" import { prf } from "../../utilities/profiler" +import { excludeDragOverlayRegEx } from "../case-tile-common/case-tile-types" // import { DGDataContext } from "../../models/v2/dg-data-context" -import { kIndexColumnKey } from "../case-tile-common/case-tile-types" import { AttributeDragOverlay } from "../drag-drop/attribute-drag-overlay" import { CardView } from "./card-view" import { useCaseCardModel } from "./use-case-card-model" @@ -56,10 +57,6 @@ export const CaseCard = observer(function CaseCard({ setNodeRef }: IProps) { data.selectionChanges // eslint-disable-line no-unused-expressions return prf.measure("CaseCard.render", () => { - // disable the overlay for the index column - const overlayDragId = active && `${active.id}`.startsWith(instanceId) && !(`${active.id}`.endsWith(kIndexColumnKey)) - ? `${active.id}` : undefined - // const context = new DGDataContext(data) const columnWidths: Record = {} cardModel.attributeColumnWidths.forEach((colWidth, id) => { @@ -86,7 +83,7 @@ export const CaseCard = observer(function CaseCard({ setNodeRef }: IProps) { <>
- +
) diff --git a/v3/src/components/case-table/case-table.tsx b/v3/src/components/case-table/case-table.tsx index a7a010f612..c53e9e6afe 100644 --- a/v3/src/components/case-table/case-table.tsx +++ b/v3/src/components/case-table/case-table.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite" import React, { useCallback, useEffect, useRef } from "react" import { CollectionContext, ParentCollectionContext } from "../../hooks/use-collection-context" import { useDataSetContext } from "../../hooks/use-data-set-context" +import { getOverlayDragId } from "../../hooks/use-drag-drop" import { useInstanceIdContext } from "../../hooks/use-instance-id-context" import { registerCanAutoScrollCallback } from "../../lib/dnd-kit/dnd-can-auto-scroll" import { logMessageWithReplacement } from "../../lib/log-message" @@ -13,7 +14,7 @@ import { INotification } from "../../models/history/apply-model-change" import { mstReaction } from "../../utilities/mst-reaction" import { prf } from "../../utilities/profiler" import { t } from "../../utilities/translation/translate" -import { kIndexColumnKey } from "../case-tile-common/case-tile-types" +import { excludeDragOverlayRegEx } from "../case-tile-common/case-tile-types" import { AttributeHeaderDividerContext } from "../case-tile-common/use-attribute-header-divider-context" import { AttributeDragOverlay } from "../drag-drop/attribute-drag-overlay" import { CollectionTable } from "./collection-table" @@ -110,10 +111,6 @@ export const CaseTable = observer(function CaseTable({ setNodeRef }: IProps) { }, []) return prf.measure("Table.render", () => { - // disable the overlay for the index column - const overlayDragId = active && `${active.id}`.startsWith(instanceId) && !(`${active.id}`.endsWith(kIndexColumnKey)) - ? `${active.id}` : undefined - if (!tableModel || !data) return null const collections = data.collections @@ -139,7 +136,7 @@ export const CaseTable = observer(function CaseTable({ setNodeRef }: IProps) { ) })} - + diff --git a/v3/src/components/case-tile-common/case-tile-component-handler.ts b/v3/src/components/case-tile-common/case-tile-component-handler.ts index 0a2a6ab6ab..f61443e133 100644 --- a/v3/src/components/case-tile-common/case-tile-component-handler.ts +++ b/v3/src/components/case-tile-common/case-tile-component-handler.ts @@ -1,6 +1,6 @@ import { V2CaseTable } from "../../data-interactive/data-interactive-component-types" import { CreateOrShowTileFn, DIComponentHandler } from "../../data-interactive/handlers/component-handler" -import { errorResult } from "../../data-interactive/handlers/di-results" +import { errorResult, fieldRequiredResult } from "../../data-interactive/handlers/di-results" import { appState } from "../../models/app-state" import { getDataSetByNameOrId, getSharedCaseMetadataFromDataset, getSharedDataSetFromDataSetId @@ -16,7 +16,7 @@ export const caseTableCardComponentHandler: DIComponentHandler = { create({ type, values }) { const { document } = appState const { dataContext, horizontalScrollOffset } = values as V2CaseTable - const dataContextNotFound = errorResult(t("V3.DI.Error.fieldRequired", { vars: ["Create", type, "dataContext"] })) + const dataContextNotFound = fieldRequiredResult("Create", type, "dataContext") if (!dataContext) return dataContextNotFound const dataSet = getDataSetByNameOrId(document, dataContext) if (!dataSet) return dataContextNotFound diff --git a/v3/src/components/case-tile-common/case-tile-types.ts b/v3/src/components/case-tile-common/case-tile-types.ts index 615ed0576e..47586c3f33 100644 --- a/v3/src/components/case-tile-common/case-tile-types.ts +++ b/v3/src/components/case-tile-common/case-tile-types.ts @@ -16,3 +16,5 @@ export interface IDividerProps { cellElt: HTMLElement | null getDividerBounds?: GetDividerBoundsFn } + +export const excludeDragOverlayRegEx = new RegExp(`${kIndexColumnKey}$`) diff --git a/v3/src/components/container/container-constants.ts b/v3/src/components/container/container-constants.ts new file mode 100644 index 0000000000..37ba4753c2 --- /dev/null +++ b/v3/src/components/container/container-constants.ts @@ -0,0 +1 @@ +export const kContainerClass = "codap-container" diff --git a/v3/src/components/container/container.tsx b/v3/src/components/container/container.tsx index 9b39af7a33..f237b3dd49 100644 --- a/v3/src/components/container/container.tsx +++ b/v3/src/components/container/container.tsx @@ -1,16 +1,21 @@ import { useMergeRefs } from "@chakra-ui/react" +import { useDndContext } from "@dnd-kit/core" import { clsx } from "clsx" import React, { useCallback, useRef } from "react" +import { dataInteractiveState } from "../../data-interactive/data-interactive-state" import { DocumentContainerContext } from "../../hooks/use-document-container-context" import { useDocumentContent } from "../../hooks/use-document-content" -import { useContainerDroppable, getDragTileId } from "../../hooks/use-drag-drop" +import { useContainerDroppable, getDragTileId, getOverlayDragId } from "../../hooks/use-drag-drop" +import { logMessageWithReplacement, logStringifiedObjectMessage } from "../../lib/log-message" import { isFreeTileRow } from "../../models/document/free-tile-row" import { isMosaicTileRow } from "../../models/document/mosaic-tile-row" import { getSharedModelManager } from "../../models/tiles/tile-environment" import { urlParams } from "../../utilities/url-params" +import { AttributeDragOverlay } from "../drag-drop/attribute-drag-overlay" +import { PluginAttributeDrag } from "../drag-drop/plugin-attribute-drag" +import { kContainerClass } from "./container-constants" import { FreeTileRowComponent } from "./free-tile-row" import { MosaicTileRowComponent } from "./mosaic-tile-row" -import { logMessageWithReplacement, logStringifiedObjectMessage } from "../../lib/log-message" import "./container.scss" @@ -21,6 +26,7 @@ export const Container: React.FC = () => { const row = documentContent?.getRowByIndex(0) const getTile = useCallback((tileId: string) => documentContent?.getTile(tileId), [documentContent]) const containerRef = useRef(null) + const { active } = useDndContext() const handleCloseTile = useCallback((tileId: string) => { const tile = getTile(tileId) @@ -38,7 +44,7 @@ export const Container: React.FC = () => { }) }, [documentContent, getTile]) - const { setNodeRef } = useContainerDroppable("codap-container", evt => { + const { setNodeRef } = useContainerDroppable(kContainerClass, evt => { const dragTileId = getDragTileId(evt.active) if (dragTileId) { if (isFreeTileRow(row)) { @@ -58,7 +64,7 @@ export const Container: React.FC = () => { }) const mergedContainerRef = useMergeRefs(containerRef, setNodeRef) - const classes = clsx("codap-container", { "scroll-behavior-auto": isScrollBehaviorAuto }) + const classes = clsx(kContainerClass, { "scroll-behavior-auto": isScrollBehaviorAuto }) return (
@@ -66,6 +72,14 @@ export const Container: React.FC = () => { } {isFreeTileRow(row) && } + +
) diff --git a/v3/src/components/drag-drop/attribute-drag-overlay.tsx b/v3/src/components/drag-drop/attribute-drag-overlay.tsx index a28d3fb5bb..4939e27752 100644 --- a/v3/src/components/drag-drop/attribute-drag-overlay.tsx +++ b/v3/src/components/drag-drop/attribute-drag-overlay.tsx @@ -1,14 +1,18 @@ -import { DragOverlay, useDndContext } from "@dnd-kit/core" -import React from "react" +import { DragOverlay, Modifier, Modifiers, useDndContext } from "@dnd-kit/core" +import React, { CSSProperties } from "react" import { getDragAttributeInfo } from "../../hooks/use-drag-drop" import "./attribute-drag-overlay.scss" interface IProps { activeDragId?: string + overlayHeight?: number + overlayWidth?: number + xOffset?: number + yOffset?: number } -export function AttributeDragOverlay ({ activeDragId }: IProps) { +export function AttributeDragOverlay ({ activeDragId, overlayHeight, overlayWidth, xOffset, yOffset }: IProps) { const { active } = useDndContext() const { dataSet, attributeId: dragAttrId } = getDragAttributeInfo(active) || {} const attr = activeDragId && dragAttrId ? dataSet?.attrFromID(dragAttrId) : undefined @@ -18,8 +22,29 @@ export function AttributeDragOverlay ({ activeDragId }: IProps) { * Otherwise, we don't want to animate it at all. */ } + + // Drags initiated by plugins can specify the size of the overlay + const style: CSSProperties | undefined = overlayHeight && overlayWidth + ? { height: `${overlayHeight}px`, width: `${overlayWidth}px` } : undefined + + // Drags initiated by plugins have to be offset based on the location of the plugin + const modifier: Modifier | undefined = (xOffset || yOffset) ? (args => { + const { x, y, scaleX, scaleY } = args.transform + return { + x: x + (xOffset ?? 0), + y: y + (yOffset ?? 0), + scaleX, scaleY + } + }) : undefined + const modifiers: Modifiers | undefined = modifier ? [modifier] : undefined + return ( - + {attr ?
{attr?.name} diff --git a/v3/src/components/drag-drop/drag-drop-constants.ts b/v3/src/components/drag-drop/drag-drop-constants.ts new file mode 100644 index 0000000000..9617015c3d --- /dev/null +++ b/v3/src/components/drag-drop/drag-drop-constants.ts @@ -0,0 +1 @@ +export const kPluginAttributeDragId = "plugin-attribute-drag" diff --git a/v3/src/components/drag-drop/plugin-attribute-drag.scss b/v3/src/components/drag-drop/plugin-attribute-drag.scss new file mode 100644 index 0000000000..e347d1ca90 --- /dev/null +++ b/v3/src/components/drag-drop/plugin-attribute-drag.scss @@ -0,0 +1,7 @@ +#plugin-attribute-drag { + height: 1px; + opacity: 0; + position: absolute; + top: 0px; + width: 1px; +} diff --git a/v3/src/components/drag-drop/plugin-attribute-drag.tsx b/v3/src/components/drag-drop/plugin-attribute-drag.tsx new file mode 100644 index 0000000000..c24ed05080 --- /dev/null +++ b/v3/src/components/drag-drop/plugin-attribute-drag.tsx @@ -0,0 +1,26 @@ +import { observer } from "mobx-react-lite" +import React from "react" +import { dataInteractiveState } from "../../data-interactive/data-interactive-state" +import { useDraggableAttribute } from "../../hooks/use-drag-drop" +import { getDataSetFromId } from "../../models/shared/shared-data-utils" +import { appState } from "../../models/app-state" +import { kPluginAttributeDragId } from "./drag-drop-constants" + +import "./plugin-attribute-drag.scss" + +export const PluginAttributeDrag = observer(function PluginAttributeDrag() { + const dataSet = getDataSetFromId(appState.document, dataInteractiveState.draggingDatasetId) + const { attributes, listeners, setNodeRef } = useDraggableAttribute({ + attributeId: dataInteractiveState.draggingAttributeId, + dataSet, + prefix: "plugin" + }) + return ( +
+ ) +}) diff --git a/v3/src/components/graph/components/graph-component.tsx b/v3/src/components/graph/components/graph-component.tsx index a5c5eebd9e..281f395131 100644 --- a/v3/src/components/graph/components/graph-component.tsx +++ b/v3/src/components/graph/components/graph-component.tsx @@ -6,6 +6,7 @@ import {useMemo} from 'use-memo-one' import {ITileBaseProps} from '../../tiles/tile-base-props' import {useDataSet} from '../../../hooks/use-data-set' import {DataSetContext} from '../../../hooks/use-data-set-context' +import { getOverlayDragId } from '../../../hooks/use-drag-drop' import {GraphContentModelContext} from '../hooks/use-graph-content-model-context' import {useGraphController} from "../hooks/use-graph-controller" import {GraphLayoutContext} from '../hooks/use-graph-layout-context' @@ -58,8 +59,6 @@ export const GraphComponent = observer(function GraphComponent({tile}: ITileBase setNodeRef(graphRef.current ?? null) const {active} = useDndContext() - const overlayDragId = active && `${active.id}`.startsWith(instanceId) - ? `${active.id}` : undefined if (!graphModel) return null @@ -76,7 +75,7 @@ export const GraphComponent = observer(function GraphComponent({tile}: ITileBase pixiPointsArray={pixiPointsArray} /> - + diff --git a/v3/src/components/map/components/map-component.tsx b/v3/src/components/map/components/map-component.tsx index 6e3a9b4a1a..afc83ac0dc 100644 --- a/v3/src/components/map/components/map-component.tsx +++ b/v3/src/components/map/components/map-component.tsx @@ -2,6 +2,7 @@ import {useDndContext, useDroppable} from '@dnd-kit/core' import {observer} from "mobx-react-lite" import React, {useEffect, useRef} from "react" import {useResizeDetector} from "react-resize-detector" +import { getOverlayDragId } from '../../../hooks/use-drag-drop' import {InstanceIdContext, useNextInstanceId} from "../../../hooks/use-instance-id-context" import { selectAllCases } from '../../../models/data/data-set-utils' import {DataDisplayLayoutContext} from "../../data-display/hooks/use-data-display-layout" @@ -41,8 +42,6 @@ export const MapComponent = observer(function MapComponent({tile}: ITileBaseProp setNodeRef(mapRef.current ?? null) const {active} = useDndContext() - const overlayDragId = active && `${active.id}`.startsWith(instanceId) - ? `${active.id}` : undefined if (!mapModel) return null @@ -51,7 +50,7 @@ export const MapComponent = observer(function MapComponent({tile}: ITileBaseProp - + diff --git a/v3/src/components/web-view/web-view-defs.ts b/v3/src/components/web-view/web-view-defs.ts index b826adb1c7..7f47b99c10 100644 --- a/v3/src/components/web-view/web-view-defs.ts +++ b/v3/src/components/web-view/web-view-defs.ts @@ -3,3 +3,4 @@ export const kV2GameType = "game" export const kV2GuideViewType = "guideView" export const kV2WebViewType = "webView" export const kWebViewTileClass = "codap-web-view" +export const kWebViewBodyClass = "codap-web-view-body" diff --git a/v3/src/components/web-view/web-view.tsx b/v3/src/components/web-view/web-view.tsx index 897cd00382..a9678829a6 100644 --- a/v3/src/components/web-view/web-view.tsx +++ b/v3/src/components/web-view/web-view.tsx @@ -3,6 +3,7 @@ import React, { useRef } from "react" import { t } from "../../utilities/translation/translate" import { ITileBaseProps } from "../tiles/tile-base-props" import { useDataInteractiveController } from "./use-data-interactive-controller" +import { kWebViewBodyClass } from "./web-view-defs" import { WebViewDropOverlay } from "./web-view-drop-overlay" import { isWebViewModel } from "./web-view-model" @@ -17,7 +18,7 @@ export const WebViewComponent = observer(function WebViewComponent({ tile }: ITi if (!isWebViewModel(webViewModel)) return null return ( -
+
{!webViewModel.isPlugin && (
{webViewModel.url}
diff --git a/v3/src/data-interactive/data-interactive-state.ts b/v3/src/data-interactive/data-interactive-state.ts new file mode 100644 index 0000000000..9722283d6b --- /dev/null +++ b/v3/src/data-interactive/data-interactive-state.ts @@ -0,0 +1,77 @@ +import { action, makeObservable, observable } from "mobx" + +/* + DataInteractiveState represents globally accessible data interactive (plugin) state that is not undoable, is not + automatically saved, and doesn't dirty the document (and thus trigger an auto-save). + */ +export class DataInteractiveState { + @observable private _draggingDatasetId = "" + @observable private _draggingAttributeId = "" + @observable private _draggingXOffset = 0 + @observable private _draggingYOffset = 0 + @observable private _draggingOverlayHeight = 100 + @observable private _draggingOverlayWidth = 100 + + constructor() { + makeObservable(this) + } + + get draggingDatasetId() { + return this._draggingDatasetId + } + + get draggingAttributeId() { + return this._draggingAttributeId + } + + get draggingXOffset() { + return this._draggingXOffset + } + + get draggingYOffset() { + return this._draggingYOffset + } + + get draggingOverlayHeight() { + return this._draggingOverlayHeight + } + + get draggingOverlayWidth() { + return this._draggingOverlayWidth + } + + @action setDraggingDatasetId(datasetId?: string) { + this._draggingDatasetId = datasetId ?? "" + } + + @action setDraggingAttributeId(attributeId?: string) { + this._draggingAttributeId = attributeId ?? "" + } + + @action setDraggingXOffset(offset = 0) { + this._draggingXOffset = offset + } + + @action setDraggingYOffset(offset = 0) { + this._draggingYOffset = offset + } + + @action setDraggingOverlayHeight(height = 100) { + this._draggingOverlayHeight = height + } + + @action setDraggingOverlayWidth(width = 100) { + this._draggingOverlayWidth = width + } + + @action endDrag() { + this.setDraggingAttributeId() + this.setDraggingDatasetId() + this.setDraggingOverlayHeight() + this.setDraggingOverlayWidth() + this.setDraggingXOffset() + this.setDraggingYOffset() + } +} + +export const dataInteractiveState = new DataInteractiveState() diff --git a/v3/src/data-interactive/data-interactive-types.ts b/v3/src/data-interactive/data-interactive-types.ts index cfa6be2b8d..ae0e9ed33e 100644 --- a/v3/src/data-interactive/data-interactive-types.ts +++ b/v3/src/data-interactive/data-interactive-types.ts @@ -41,6 +41,13 @@ export interface DIAllCases { } } export type DIAttribute = Partial +export interface DINotifyAttribute { + mouseX?: number + mouseY?: number + overlayHeight?: number + overlayWidth?: number + request?: string +} export interface DIAttributeLocationValues { collection?: string | number position?: number @@ -178,8 +185,8 @@ export interface DIResources { } // types for values accepted as inputs by the API -export type DISingleValues = DIAttribute | DIAttributeLocationValues | DICase | DIDataContext | DINotifyDataContext | - DIGlobal | DIInteractiveFrame | DIItemValues | DICreateCollection | DINewCase | DIUpdateCase | +export type DISingleValues = DIAttribute | DINotifyAttribute | DIAttributeLocationValues | DICase | DIDataContext | + DINotifyDataContext | DIGlobal | DIInteractiveFrame | DIItemValues | DICreateCollection | DINewCase | DIUpdateCase | DINotification | DIItemSearchNotify | DILogMessage | V2SpecificComponent export type DIValues = DISingleValues | DISingleValues[] | number | string[] diff --git a/v3/src/data-interactive/handlers/attribute-handler.ts b/v3/src/data-interactive/handlers/attribute-handler.ts index 2317f1e26e..c2b87f5f70 100644 --- a/v3/src/data-interactive/handlers/attribute-handler.ts +++ b/v3/src/data-interactive/handlers/attribute-handler.ts @@ -1,12 +1,20 @@ +import { kContainerClass } from "../../components/container/container-constants" +import { kPluginAttributeDragId } from "../../components/drag-drop/drag-drop-constants" +import { kWebViewBodyClass } from "../../components/web-view/web-view-defs" +import { appState } from "../../models/app-state" import { IAttribute } from "../../models/data/attribute" import { createAttributesNotification, updateAttributesNotification } from "../../models/data/data-set-notifications" +import { IFreeTileLayout, isFreeTileRow } from "../../models/document/free-tile-row" import { getSharedCaseMetadataFromDataset } from "../../models/shared/shared-data-utils" import { t } from "../../utilities/translation/translate" import { registerDIHandler } from "../data-interactive-handler" +import { dataInteractiveState } from "../data-interactive-state" import { convertAttributeToV2, convertAttributeToV2FromResources } from "../data-interactive-type-utils" -import { DIAttribute, DIHandler, DIResources, DIValues } from "../data-interactive-types" +import { DIAttribute, DIHandler, DINotifyAttribute, DIResources, DIValues } from "../data-interactive-types" import { createAttribute, updateAttribute } from "./di-handler-utils" -import { attributeNotFoundResult, collectionNotFoundResult, dataContextNotFoundResult } from "./di-results" +import { + attributeNotFoundResult, collectionNotFoundResult, dataContextNotFoundResult, errorResult, fieldRequiredResult +} from "./di-results" export const diAttributeHandler: DIHandler = { create(resources: DIResources, _values?: DIValues) { @@ -19,12 +27,7 @@ export const diAttributeHandler: DIHandler = { // Wrap single attribute in array and bail if any new attributes are missing names const attributeValues = Array.isArray(values) ? values : [values] const attributeErrors = attributeValues.map(singleValue => { - if (!singleValue?.name) { - return { - success: false, - values: { error: t("V3.DI.Error.fieldRequired", { vars: ["Create", "attribute", "name"] }) } - } as const - } + if (!singleValue?.name) return fieldRequiredResult("Create", "attribute", "name") return { success: true } }).filter(error => !error.success) if (attributeErrors.length > 0) return attributeErrors[0] @@ -75,6 +78,75 @@ export const diAttributeHandler: DIHandler = { } }, + notify(resources: DIResources, values?: DIValues) { + const { attribute, dataContext, interactiveFrame } = resources + if (!dataContext) return dataContextNotFoundResult + if (!attribute) return attributeNotFoundResult + + const { request } = (values ?? {}) as DINotifyAttribute + if (!request) return fieldRequiredResult("Notify", "attribute", "request") + + // Common properties for synthetic events + const bubbles = true + const cancelable = true + const isPrimary = true + const pointerId = 1 + const pointerType = "mouse" + + if (request === "dragStart") { + dataInteractiveState.setDraggingDatasetId(dataContext.id) + dataInteractiveState.setDraggingAttributeId(attribute.id) + const pluginAttributeDrag = document.getElementById(kPluginAttributeDragId) + if (pluginAttributeDrag) { + // Get overlay dimensions specified by plugin + const { overlayHeight, overlayWidth } = (values ?? {}) as DINotifyAttribute + dataInteractiveState.setDraggingOverlayHeight(overlayHeight) + dataInteractiveState.setDraggingOverlayWidth(overlayWidth) + + // Determine position of drag + let clientX = 0 + let clientY = 0 + const row = appState.document.content?.firstRow + if (interactiveFrame && row && isFreeTileRow(row)) { + const layout = (row.getTileLayout(interactiveFrame.id) ?? { x: 0, y: 0 }) as IFreeTileLayout + clientX = layout.x + clientY = layout.y + } + dataInteractiveState.setDraggingXOffset(clientX - (overlayWidth ?? 0) / 2) + const containers = document.getElementsByClassName(kContainerClass) + const kCodapHeaderHeight = 95 + const containerOffset = containers.item(0)?.getBoundingClientRect()?.top ?? kCodapHeaderHeight + dataInteractiveState.setDraggingYOffset(clientY - containerOffset - (overlayHeight ?? 0)) + + // Dispatch events that will trigger a drag start + // A setTimeout is used to ensure that hooks are updated before the drag begins + setTimeout(() => { + pluginAttributeDrag.dispatchEvent(new PointerEvent("pointerdown", { + bubbles, cancelable, clientX: clientX - 10, clientY: clientY - 10, isPrimary, pointerId, pointerType + })) + document.dispatchEvent(new PointerEvent("pointermove", { + bubbles, cancelable, clientX, clientY, isPrimary, pointerId, pointerType + })) + }) + } + return { success: true } + } else if (["dragMove", "dragEnd"].includes(request) && interactiveFrame) { + const { mouseX, mouseY } = (values ?? {}) as DINotifyAttribute + const pluginTileElement = document.getElementById(interactiveFrame.id) + const pluginElement = pluginTileElement?.getElementsByClassName(kWebViewBodyClass).item(0) + const rect = pluginElement?.getBoundingClientRect() + const clientX = (mouseX ?? 0) + (rect?.x ?? 0) + const clientY = (mouseY ?? 0) + (rect?.y ?? 0) + const event = request === "dragMove" ? "pointermove" : "pointerup" + document.dispatchEvent(new PointerEvent(event, { + bubbles, cancelable, clientX, clientY, isPrimary, pointerId, pointerType + })) + return { success: true } + } + + return errorResult(t("V3.DI.Error.unknownRequest", { vars: [request] })) + }, + update(resources: DIResources, _values?: DIValues) { const { attribute, dataContext } = resources if (!attribute || Array.isArray(_values)) return attributeNotFoundResult diff --git a/v3/src/data-interactive/handlers/data-context-handler.ts b/v3/src/data-interactive/handlers/data-context-handler.ts index 1e0f9e67d9..9c7d1f3061 100644 --- a/v3/src/data-interactive/handlers/data-context-handler.ts +++ b/v3/src/data-interactive/handlers/data-context-handler.ts @@ -15,11 +15,10 @@ import { basicDataSetInfo, convertDataSetToV2 } from "../data-interactive-type-u import { getAttribute } from "../data-interactive-utils" import { findTileFromNameOrId } from "../resource-parser-utils" import { createCollection } from "./di-handler-utils" -import { dataContextNotFoundResult, errorResult } from "./di-results" +import { dataContextNotFoundResult, errorResult, fieldRequiredResult } from "./di-results" import { toV3CaseId } from "../../utilities/codap-utils" -const requestRequiedResult = - errorResult(t("V3.DI.Error.fieldRequired", { vars: ["Notify", "dataContext", "request"] })) +const requestRequiedResult = fieldRequiredResult("Notify", "dataContext", "request") export const diDataContextHandler: DIHandler = { create(_resources: DIResources, _values?: DIValues) { @@ -85,7 +84,7 @@ export const diDataContextHandler: DIHandler = { const successResult = { success: true as const, values: {} } if (request === "setAside") { - if (!caseIDs) return errorResult(t("V3.DI.Error.fieldRequired", { vars: ["Notify", "dataContext", "caseIDs"] })) + if (!caseIDs) return fieldRequiredResult("Notify", "dataContext", "caseIDs") dataContext.hideCasesOrItems(caseIDs.map(caseId => toV3CaseId(caseId))) return successResult } else if (request === "restoreSetasides") { diff --git a/v3/src/data-interactive/handlers/di-results.ts b/v3/src/data-interactive/handlers/di-results.ts index c446203dfc..a1ccbed913 100644 --- a/v3/src/data-interactive/handlers/di-results.ts +++ b/v3/src/data-interactive/handlers/di-results.ts @@ -9,3 +9,7 @@ export const couldNotParseQueryResult = errorResult(t("V3.DI.Error.couldNotParse export const dataContextNotFoundResult = errorResult(t("V3.DI.Error.dataContextNotFound")) export const itemNotFoundResult = errorResult(t("V3.DI.Error.itemNotFound")) export const valuesRequiredResult = errorResult(t("V3.DI.Error.valuesRequired")) + +export function fieldRequiredResult(action: string, resource: string, field: string) { + return errorResult(t("V3.DI.Error.fieldRequired", { vars: [action, resource, field] })) +} diff --git a/v3/src/data-interactive/handlers/handler-functions.ts b/v3/src/data-interactive/handlers/handler-functions.ts index 9ce5c65258..396860f791 100644 --- a/v3/src/data-interactive/handlers/handler-functions.ts +++ b/v3/src/data-interactive/handlers/handler-functions.ts @@ -1,13 +1,12 @@ import { updateCasesNotificationFromIds } from "../../models/data/data-set-notifications" import { ICase } from "../../models/data/data-set-types" import { toV2Id, toV3CaseId, toV3ItemId } from "../../utilities/codap-utils" -import { t } from "../../utilities/translation/translate" import { DICaseValues, DIFullCase, DIResources, DISuccessResult, DIUpdateCase, DIUpdateItemResult, DIValues } from "../data-interactive-types" import { getV2ItemResult, getCaseRequestResultValues } from "../data-interactive-type-utils" import { attrNamesToIds } from "../data-interactive-utils" -import { caseNotFoundResult, dataContextNotFoundResult, itemNotFoundResult } from "./di-results" +import { caseNotFoundResult, dataContextNotFoundResult, fieldRequiredResult, itemNotFoundResult } from "./di-results" export function deleteCaseBy(resources: DIResources, aCase?: ICase) { const { dataContext } = resources @@ -78,10 +77,7 @@ export function updateCaseBy( const { itemReturnStyle, nestedValues, resourceName } = options ?? {} - const missingFieldResult = (field: string) => ({ - success: false as const, - values: { error: t("V3.DI.Error.fieldRequired", { vars: ["update", resourceName ?? "case", field] }) } - }) + const missingFieldResult = (field: string) => fieldRequiredResult("update", resourceName ?? "case", field) if (!values) return missingFieldResult("values") let _values = values as DICaseValues diff --git a/v3/src/data-interactive/handlers/interactive-frame-handler.ts b/v3/src/data-interactive/handlers/interactive-frame-handler.ts index 81f7379c7b..44560b7064 100644 --- a/v3/src/data-interactive/handlers/interactive-frame-handler.ts +++ b/v3/src/data-interactive/handlers/interactive-frame-handler.ts @@ -11,7 +11,7 @@ export const diInteractiveFrameHandler: DIHandler = { get(resources: DIResources) { const { interactiveFrame } = resources if (!interactiveFrame) return noIFResult - + const dimensions = appState.document.content?.getTileDimensions(interactiveFrame.id) const webViewContent = isWebViewModel(interactiveFrame.content) ? interactiveFrame.content : undefined const { diff --git a/v3/src/data-interactive/handlers/item-search-handler.ts b/v3/src/data-interactive/handlers/item-search-handler.ts index a933065d4b..2c0355246d 100644 --- a/v3/src/data-interactive/handlers/item-search-handler.ts +++ b/v3/src/data-interactive/handlers/item-search-handler.ts @@ -1,10 +1,11 @@ import { toV2Id } from "../../utilities/codap-utils" -import { t } from "../../utilities/translation/translate" import { registerDIHandler } from "../data-interactive-handler" import { getV2ItemResult } from "../data-interactive-type-utils" import { ICase } from "../../models/data/data-set-types" import { DIHandler, DIItemSearchNotify, DIResources, DIValues } from "../data-interactive-types" -import { couldNotParseQueryResult, dataContextNotFoundResult, errorResult, valuesRequiredResult } from "./di-results" +import { + couldNotParseQueryResult, dataContextNotFoundResult, fieldRequiredResult, valuesRequiredResult +} from "./di-results" export const diItemSearchHandler: DIHandler = { delete(resources: DIResources) { @@ -36,7 +37,7 @@ export const diItemSearchHandler: DIHandler = { if (!values) return valuesRequiredResult const { itemOrder } = values as DIItemSearchNotify - if (!itemOrder) return errorResult(t("V3.DI.Error.fieldRequired", { vars: ["Notify", "itemSearch", "itemOrder"] })) + if (!itemOrder) return fieldRequiredResult("Notify", "itemSearch", "itemOrder") dataContext.applyModelChange(() => { const itemIds = itemSearch.map(({ __id__ }) => __id__) diff --git a/v3/src/hooks/use-drag-drop.ts b/v3/src/hooks/use-drag-drop.ts index 9216a5b991..760c387ed7 100644 --- a/v3/src/hooks/use-drag-drop.ts +++ b/v3/src/hooks/use-drag-drop.ts @@ -2,10 +2,10 @@ import { Active, DataRef, DragEndEvent, Modifier, useDndMonitor, useDraggable, UseDraggableArguments, useDroppable, UseDroppableArguments } from "@dnd-kit/core" +import { kTitleBarHeight } from "../components/constants" import { IDataSet } from "../models/data/data-set" import { useInstanceIdContext } from "./use-instance-id-context" import { useTileModelContext } from "./use-tile-model-context" -import { kTitleBarHeight } from "../components/constants" // list of draggable types const DragTypes = ["attribute", "tile"] as const @@ -29,6 +29,11 @@ export function getDragAttributeInfo(active: Active | null): Omit { // should generally include instanceId to support dragging from multiple component instances diff --git a/v3/src/lib/dnd-kit/codap-dnd-context.tsx b/v3/src/lib/dnd-kit/codap-dnd-context.tsx index bed7babefb..69ccdf7ed0 100644 --- a/v3/src/lib/dnd-kit/codap-dnd-context.tsx +++ b/v3/src/lib/dnd-kit/codap-dnd-context.tsx @@ -3,6 +3,7 @@ import { MouseSensor, PointerSensor, TraversalOrder, useSensor, useSensors } from "@dnd-kit/core" import React, { ReactNode } from "react" +import { dataInteractiveState } from "../../data-interactive/data-interactive-state" import { containerSnapToGridModifier, restrictDragToArea } from "../../hooks/use-drag-drop" import { urlParams } from "../../utilities/url-params" import { canAutoScroll } from "./dnd-can-auto-scroll" @@ -28,15 +29,17 @@ export const CodapDndContext = ({ children }: IProps) => { const useMouseSensor = useSensor(MouseSensor) const sensors = useSensors( // pointer must move three pixels before starting a drag - useSensor(PointerSensor, { activationConstraint: { distance: 3 }}), + useSensor(PointerSensor, { activationConstraint: { distance: 3 } }), useSensor(KeyboardSensor, { coordinateGetter: customCoordinatesGetter }), // mouse sensor can be enabled for cypress tests, for instance - urlParams.mouseSensor !== undefined ? useMouseSensor : null) + urlParams.mouseSensor !== undefined ? useMouseSensor : null + ) return ( dataInteractiveState.endDrag()} sensors={sensors} > {children} diff --git a/v3/src/models/data/data-set-utils.ts b/v3/src/models/data/data-set-utils.ts index 840065e261..5e2c52d0b2 100644 --- a/v3/src/models/data/data-set-utils.ts +++ b/v3/src/models/data/data-set-utils.ts @@ -82,7 +82,7 @@ export function moveAttribute({ const redoStringKey = undoable ? "DG.Redo.dataContext.moveAttribute" : undefined const logMessage = logMessageWithReplacement("Moved attribute %@ to %@ collection", { attrId, collection: targetCollection.name ?? "new" }) - const modelChangeOptions = { notifications, undoStringKey, redoStringKey, log: logMessage } + const modelChangeOptions = { notify: notifications, undoStringKey, redoStringKey, log: logMessage } if (targetCollection.id === sourceCollection?.id) { // move the attribute within a collection