diff --git a/v3/src/components/case-table/case-table-model.ts b/v3/src/components/case-table/case-table-model.ts index 37d4a77b4..82c2eefac 100644 --- a/v3/src/components/case-table/case-table-model.ts +++ b/v3/src/components/case-table/case-table-model.ts @@ -11,10 +11,20 @@ export const CaseTableModel = TileContentModel type: types.optional(types.literal(kCaseTableTileType), kCaseTableTileType), // key is attribute id; value is width columnWidths: types.map(types.number), + // Only used for serialization; volatile property used during run time + horizontalScrollOffset: 0 }) .volatile(self => ({ // entire hierarchical table scrolls as a unit horizontally - scrollLeft: 0 + _horizontalScrollOffset: 0 + })) + .actions(self => ({ + afterCreate() { + self._horizontalScrollOffset = self.horizontalScrollOffset + }, + prepareSnapshot() { + self.horizontalScrollOffset = self._horizontalScrollOffset + } })) .views(self => ({ get data() { @@ -53,8 +63,8 @@ export const CaseTableModel = TileContentModel updateAfterSharedModelChanges(sharedModel?: ISharedModel) { // TODO }, - setScrollLeft(scrollLeft: number) { - self.scrollLeft = scrollLeft + setHorizontalScrollOffset(horizontalScrollOffset: number) { + self._horizontalScrollOffset = horizontalScrollOffset } })) export interface ICaseTableModel extends Instance {} diff --git a/v3/src/components/case-table/case-table.tsx b/v3/src/components/case-table/case-table.tsx index caeb5ec84..d7935f2a3 100644 --- a/v3/src/components/case-table/case-table.tsx +++ b/v3/src/components/case-table/case-table.tsx @@ -9,11 +9,11 @@ import { useSyncScrolling } from "./use-sync-scrolling" import { CollectionContext, ParentCollectionContext } from "../../hooks/use-collection-context" import { useDataSetContext } from "../../hooks/use-data-set-context" import { useInstanceIdContext } from "../../hooks/use-instance-id-context" -import { useTileModelContext } from "../../hooks/use-tile-model-context" import { ICollectionModel } from "../../models/data/collection" import { IDataSet } from "../../models/data/data-set" import { createCollectionNotification, deleteCollectionNotification } from "../../models/data/data-set-notifications" 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" @@ -27,8 +27,6 @@ export const CaseTable = observer(function CaseTable({ setNodeRef }: IProps) { const instanceId = useInstanceIdContext() || "case-table" const data = useDataSetContext() const tableModel = useCaseTableModel() - const { isTileSelected } = useTileModelContext() - const isFocused = isTileSelected() const contentRef = useRef(null) const lastNewCollectionDrop = useRef<{ newCollectionId: string, beforeCollectionId: string } | undefined>() @@ -37,16 +35,28 @@ export const CaseTable = observer(function CaseTable({ setNodeRef }: IProps) { setNodeRef(elt) } - useEffect(function syncScrollLeft() { - // There is a bug, seemingly in React, in which the scrollLeft property gets reset - // to 0 when the order of tiles is changed (which happens on selecting/focusing tiles - // in the free tile layout), even though the CaseTable component is not re-rendered - // or unmounted/mounted. Therefore, we reset the scrollLeft property from our saved - // cache on focus change. - if (isFocused && contentRef.current) { - contentRef.current.scrollLeft = tableModel?.scrollLeft ?? 0 + useEffect(() => { + const updateScroll = (horizontalScrollOffset?: number) => { + if (horizontalScrollOffset != null && contentRef.current && + contentRef.current.scrollLeft !== horizontalScrollOffset + ) { + contentRef.current.scrollLeft = horizontalScrollOffset + } } - }, [isFocused, tableModel]) + + // Initial scroll is delayed a frame to let RDG do its thing + setTimeout(() => updateScroll(tableModel?._horizontalScrollOffset)) + + // Reaction handles changes to the model, such as via the API + return tableModel && mstReaction( + () => tableModel?._horizontalScrollOffset, + _horizontalScrollOffset => { + updateScroll(_horizontalScrollOffset) + }, + { name: "CaseTable.updateHorizontalScroll" }, + tableModel + ) + }, [tableModel]) const { handleTableScroll, syncTableScroll } = useSyncScrolling() @@ -100,7 +110,7 @@ export const CaseTable = observer(function CaseTable({ setNodeRef }: IProps) { const collections = data.collections const handleHorizontalScroll: React.UIEventHandler = () => { - tableModel?.setScrollLeft(contentRef.current?.scrollLeft ?? 0) + tableModel.setHorizontalScrollOffset(contentRef.current?.scrollLeft ?? 0) } return ( diff --git a/v3/src/data-interactive/handlers/component-handler.test.ts b/v3/src/data-interactive/handlers/component-handler.test.ts index 6fb242c76..d8fd5ebf9 100644 --- a/v3/src/data-interactive/handlers/component-handler.test.ts +++ b/v3/src/data-interactive/handlers/component-handler.test.ts @@ -1,9 +1,12 @@ import { getSnapshot } from "mobx-state-tree" import { ICaseCardModel, isCaseCardModel } from "../../components/case-card/case-card-model" import { kCaseCardIdPrefix } from "../../components/case-card/case-card-registration" +import { kCaseTableTileType } from "../../components/case-table/case-table-defs" import { ICaseTableModel, isCaseTableModel } from "../../components/case-table/case-table-model" import { kCaseTableIdPrefix } from "../../components/case-table/case-table-registration" +import { createOrShowTableOrCardForDataset } from "../../components/case-table-card-common/case-table-card-utils" import { appState } from "../../models/app-state" +import { getSharedDataSets } from "../../models/shared/shared-data-utils" import { toV3Id } from "../../utilities/codap-utils" import { V2CaseCard } from "../data-interactive-component-types" import { DIComponentInfo } from "../data-interactive-types" @@ -70,4 +73,19 @@ describe("DataInteractive ComponentHandler", () => { expect(isCaseCardModel(card2Tile.content)).toBe(true) expect((card2Tile.content as ICaseCardModel).data?.id).toBe(dataset2.id) }) + + it("update caseTable works", () => { + const { dataset } = setupTestDataset() + documentContent.createDataSet(getSnapshot(dataset)) + const sharedDataSet = getSharedDataSets(documentContent)[0] + const component = createOrShowTableOrCardForDataset(sharedDataSet, kCaseTableTileType)! + const tableContent = component.content as ICaseTableModel + + expect(handler.update?.({}, { horizontalScrollOffset: 100 }).success).toBe(false) + expect(handler.update?.({ component }).success).toBe(false) + + expect(tableContent._horizontalScrollOffset).toBe(0) + expect(handler.update?.({ component }, { horizontalScrollOffset: 100 }).success).toBe(true) + expect(tableContent._horizontalScrollOffset).toBe(100) + }) }) diff --git a/v3/src/data-interactive/handlers/component-handler.ts b/v3/src/data-interactive/handlers/component-handler.ts index 9ed3228a8..4b396ec22 100644 --- a/v3/src/data-interactive/handlers/component-handler.ts +++ b/v3/src/data-interactive/handlers/component-handler.ts @@ -3,6 +3,7 @@ import { SetRequired } from "type-fest" import { kCaseCardTileType } from "../../components/case-card/case-card-defs" import { createOrShowTableOrCardForDataset } from "../../components/case-table-card-common/case-table-card-utils" import { kCaseTableTileType } from "../../components/case-table/case-table-defs" +import { isCaseTableModel } from "../../components/case-table/case-table-model" import { attrRoleToGraphPlace, GraphAttrRole } from "../../components/data-display/data-display-types" import { AttributeDescriptionsMapSnapshot, IAttributeDescriptionSnapshot, kDataConfigurationType @@ -79,7 +80,7 @@ export const diComponentHandler: DIHandler = { return document.applyModelChange(() => { // Special case for caseCard and caseTable, which require a dataset if ([kV2CaseCardType, kV2CaseTableType].includes(type)) { - const { dataContext } = values as V2CaseTable + const { dataContext, horizontalScrollOffset } = values as V2CaseTable if (!dataContext) return dataContextRequiredResult const sharedDataSet = getSharedDataSet(dataContext) if (!sharedDataSet) return dataContextNotFoundResult @@ -92,13 +93,15 @@ export const diComponentHandler: DIHandler = { } const title = _title ?? name - const options = { cannotClose, ...dimensions, title } + const content = type === kV2CaseTableType && horizontalScrollOffset != null + ? { horizontalScrollOffset, type: kCaseTableTileType } : undefined + const options = { cannotClose, content, ...dimensions, title } const tileType = type === kV2CaseCardType ? kCaseCardTileType : kCaseTableTileType const tile = createOrShowTableOrCardForDataset(sharedDataSet, tileType, options) if (!tile) return componentNotCreatedResult - // TODO Handle horizontalScrollOffset and isIndexHidden + // TODO Handle isIndexHidden return { success: true, values: { @@ -409,7 +412,23 @@ export const diComponentHandler: DIHandler = { return { success: true } }, - update: diNotImplementedYet + update(resources: DIResources, values?: DIValues) { + const { component } = resources + if (!component) return componentNotFoundResult + const { content } = component + + if (!values) return valuesRequiredResult + + if (isCaseTableModel(content)) { + // TODO Handle isIndexHidden + const { horizontalScrollOffset } = values as V2CaseTable + if (horizontalScrollOffset != null) content.setHorizontalScrollOffset(horizontalScrollOffset) + + return { success: true } + } + + return errorResult(t("V3.DI.Error.unsupportedComponent", { vars: [content.type] })) + } } registerDIHandler("component", diComponentHandler)