Skip to content

Commit

Permalink
Merge pull request #24 from concord-consortium/187887881-selected-row…
Browse files Browse the repository at this point in the history
…-sync
  • Loading branch information
bacalj authored Aug 19, 2024
2 parents b6b94ad + 38df5f2 commit 997f273
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 62 deletions.
161 changes: 108 additions & 53 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,86 @@
import React, { useEffect, useState, useRef } from "react";
import React, { useEffect, useState, useRef, useCallback } from "react";
import { clsx } from "clsx";
import { ILocation } from "../types";
import { kInitialDimensions, kVersion, kPluginName, kDefaultOnAttributes, kSimulationTabDimensions, kDataContextName } from "../constants";
import { initializePlugin, codapInterface, addDataContextChangeListener, ClientNotification } from "@concord-consortium/codap-plugin-api";
import { ICurrentDayLocation, ILocation } from "../types";
import { debounce } from "../grasp-seasons/utils/utils";
import { kInitialDimensions, kVersion, kPluginName, kDefaultOnAttributes, kSimulationTabDimensions, kDataContextName, kChildCollectionName } from "../constants";
import { initializePlugin, codapInterface, selectSelf, addDataContextChangeListener, ClientNotification, getCaseByID } from "@concord-consortium/codap-plugin-api";
import { useCodapData } from "../hooks/useCodapData";
import { LocationTab } from "./location-tab";
import { SimulationTab } from "./simulation-tab";
import { Header } from "./header";

import "../assets/scss/App.scss";

const updateRowSelectionInCodap = (latitude: string, longitude: string, day: number) => {
// TODO: Issue CODAP API request to highlight appropriate case in the case table, using combination of
// Math.floor(day), latitude, and longitude.
}
const debouncedUpdateRowSelectionInCodap = debounce((
latitude: string,
longitude: string,
day: number
) => {
codapInterface.sendRequest({
action: "get",
resource: `dataContext[${kDataContextName}].collection[${kChildCollectionName}].caseSearch[calcId==${latitude},${longitude},${Math.floor(day)}]`
}).then((result: any) => {
if (result.success && result.values.length > 0) {
const caseID = result.values[0].id;
return codapInterface.sendRequest({
action: "create",
resource: `dataContext[${kDataContextName}].selectionList`,
values: [caseID]
});
} else {
return null;
}
}).then((selectionResult: any) => {
if (!selectionResult.success) {
console.warn("Selection result was not successful", selectionResult);
}
}).catch((error: any) => {
console.error("Error in selection process:", error);
});
}, 250);

export const App: React.FC = () => {
const [activeTab, setActiveTab] = useState<"location" | "simulation">("location");
const [latitude, setLatitude] = useState("");
const [longitude, setLongitude] = useState("");
const [dayOfYear, /*setDayOfYear */] = useState(171);
const [dayOfYear, setDayOfYear] = useState(171);
const [locations, setLocations] = useState<ILocation[]>([]);
const [locationSearch, setLocationSearch] = useState<string>("");
const [selectedAttrs, setSelectedAttributes] = useState<string[]>(kDefaultOnAttributes);
const [dataContext, setDataContext] = useState<any>(null);

const handleDayUpdateInTheSimTab = (day: number) => {
// console.log("The day of the year has been updated in the simulation tab to: ", day); // TODO: implement this
// We might to debounce this call, as if the animation is on, or user is dragging the slider, there will be
// lot of events and API calls to CODAP.
updateRowSelectionInCodap(latitude, longitude, Math.floor(day));
// Note that we do not need to update dayOfYear state variable. It's useful only for the opposite direction
// of the sync process, when user select a row in CODAP and we want to update the day in the simulation tab.
};

// TODO: Handle case selection - sync sim tab with CODAP selection
// const handleCaseSelectionInCodap = (_latitude: string, _longitude: string, day: number) => {
// // Option 1. Update as much of the plugin state as we can when user selects a case in CODAP. I think this might
// // be too much, as it'll clear all the inputs in all the tabs and the user will have to re-enter everything
// // if they were in the middle of something.
// // setDayOfYear(day);
// // setLatitude(_latitude);
// // setLongitude(_longitude);
// // ...OR...
// // Option 2. Update only the day of the year, as that's reasonably unobtrusive and useful. We can first check
// // if user actually selected the case from the same location, and only then update the day of the year.
// if (latitude === _latitude && longitude === _longitude) {
// setDayOfYear(day);
// }
// }
const currentDayLocationRef = useRef<ICurrentDayLocation>({
_latitude: "",
_longitude: "",
_dayOfYear: 171
});

const { getUniqueLocationsInCodapData } = useCodapData();
// Store a ref to getUniqueLocationsInCodapData so we can call inside useEffect without triggering unnecessary re-runs
const getUniqueLocationsRef = useRef(getUniqueLocationsInCodapData);

const handleDayUpdateInTheSimTab = (day: number) => {
currentDayLocationRef.current._dayOfYear = day;
debouncedUpdateRowSelectionInCodap(
currentDayLocationRef.current._latitude,
currentDayLocationRef.current._longitude,
day
);
};

const handleCaseSelectionInCodap = useCallback((
selectedLatitude: string,
selectedLongitude: string,
selectedDay: number
) => {
const { _latitude, _longitude, _dayOfYear } = currentDayLocationRef.current;
const rowInLocation = `${_latitude},${_longitude}` === `${selectedLatitude},${selectedLongitude}`;
const newDayChoice = `${_dayOfYear}` !== `${selectedDay}`;
if (rowInLocation && newDayChoice) {
setDayOfYear(selectedDay);
currentDayLocationRef.current._dayOfYear = selectedDay;
}
}, []);

useEffect(() => {
const initialize = async () => {
try {
Expand All @@ -65,38 +92,66 @@ export const App: React.FC = () => {
} catch (e) {
console.error("Failed to initialize plugin, error:", e);
}

const casesDeletedFromCodapListener = async (listenerRes: ClientNotification) => {
const { resource, values } = listenerRes;
const isResource = resource === `dataContextChangeNotice[${kDataContextName}]`;
if (!isResource) return;

const casesDeleted =
values.operation === "selectCases"
&& values.result.cases
&& values.result.cases.length === 0
&& values.result.success;

if ( casesDeleted ) {
const uniqeLocations = await getUniqueLocationsRef.current();
if (uniqeLocations) setLocations(uniqeLocations);
}
};
addDataContextChangeListener(kDataContextName, casesDeletedFromCodapListener);
};

initialize();
}, []);

const handleDataContextChange = useCallback(async (listenerRes: ClientNotification) => {
console.log("| dataContextChangeNotice: ", listenerRes);

Check warning on line 101 in src/components/App.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Unexpected console statement

Check warning on line 101 in src/components/App.tsx

View workflow job for this annotation

GitHub Actions / S3 Deploy

Unexpected console statement
const { resource, values } = listenerRes;
const isResource = resource === `dataContextChangeNotice[${kDataContextName}]`;
if (!isResource || !values.result.success) return;

const casesDeleted = values.operation === "selectCases" && values.result.cases.length === 0
const caseSelected = values.operation === "selectCases" && values.result.cases.length === 1;

//TODO: there is an unhandled path when we edit the location name
// we can use this to update the location name in the UI

if (casesDeleted) {
const uniqueLocations = await getUniqueLocationsRef.current();
if (uniqueLocations) setLocations(uniqueLocations);
}
else if (caseSelected) {
const parentCaseId = values.result.cases[0].parent;
const selectedDay = values.result.cases[0].values.dayOfYear;
const parentCase = await getCaseByID(kDataContextName, parentCaseId);
const selectedLatitude = parentCase.values.case.values.latitude;
const selectedLongitude = parentCase.values.case.values.longitude;
handleCaseSelectionInCodap(
selectedLatitude,
selectedLongitude,
selectedDay
);
}
}, [handleCaseSelectionInCodap]);

useEffect(() => {
addDataContextChangeListener(kDataContextName, handleDataContextChange);
}, [handleDataContextChange]);

useEffect(() => {
currentDayLocationRef.current = {
_latitude: latitude,
_longitude: longitude,
_dayOfYear: dayOfYear
};
}, [latitude, longitude, dayOfYear]);

const handleTabClick = (tab: "location" | "simulation") => {
setActiveTab(tab);
// Update dimensions of the plugin window when switching tabs.
codapInterface.sendRequest({
action: "update",
resource: "interactiveFrame",
values: {
dimensions: tab === "location" ? kInitialDimensions : kSimulationTabDimensions
}
}).then(() => {
// This brings the plugin window to the front within CODAP
selectSelf();
}).catch((error) => {
console.error("Error updating dimensions or selecting self:", error);
});
};

Expand Down
7 changes: 4 additions & 3 deletions src/components/location-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect } from "react";
import { useCodapData } from "../hooks/useCodapData";
import { kChildCollectionAttributes } from "../constants";
import { ILocation } from "../types";
import { ICodapDataContextInfo, ILocation } from "../types";
import { LocationPicker } from "./location-picker";
import { locationsEqual } from "../utils/daylight-utils";

Expand All @@ -13,12 +13,12 @@ interface LocationTabProps {
locations: ILocation[];
locationSearch: string;
selectedAttrs: string[];
dataContext: any; // TODO the type
dataContext: ICodapDataContextInfo | null;
setLatitude: (latitude: string) => void;
setLongitude: (longitude: string) => void;
setLocationSearch: (search: string) => void;
setSelectedAttributes: (attrs: string[]) => void;
setDataContext: (context: any) => void; // TODO the type
setDataContext: (context: any) => void;
setLocations: (locations: ILocation[]) => void;
}

Expand All @@ -34,6 +34,7 @@ export const LocationTab: React.FC<LocationTabProps> = ({
setSelectedAttributes,
setLocations
}) => {

const {
dataContext,
handleClearData,
Expand Down
17 changes: 17 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export const kChildCollectionAttributes = [
precision: "day",
description: "Date"
},
{
name: "dayOfYear",
title: "Day of year",
type: "numeric",
hasToken: false,
hidden: true,
description: "Day of year"
},
{
name: "Day length",
title: "Day length",
Expand Down Expand Up @@ -114,6 +122,15 @@ export const kChildCollectionAttributes = [
title: "Season",
type: "categorical",
hasToken: true
},
{
name: "calcId",
title: "calcId",
type: "categorical",
hasToken: false,
hidden: true,
description: "unique identifier for each location on a day - concatenation of latitude, longitude, and dayOfYear",
formula: "latitude + ',' + longitude + ',' + dayOfYear"
}
];

Expand Down
2 changes: 2 additions & 0 deletions src/grasp-seasons/components/seasons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,15 @@ const Seasons: React.FC<IProps> = ({ lang = "en_us", initialState = {}, log = (a
setSimState(prevState => {
// Make sure day is within [0, 364] range
const day = (prevState.day + increment + 365) % 365;
setDayOfYear(day);
return { ...prevState, day };
});
};

const handleMonthIncrement = (monthIncrement: number) => () => {
setSimState(prevState => {
const day = changeMonthOfDayOfYear(prevState.day, monthIncrement);
setDayOfYear(day);
return { ...prevState, day };
});
};
Expand Down
17 changes: 17 additions & 0 deletions src/grasp-seasons/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,20 @@ export default function getURLParam(name: string, defaultValue = null) {
if (value === "false") return false;
return value;
}

export function debounce<T extends (...args: any[]) => any>(
func: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout> | null = null;

return function (this: any, ...args: Parameters<T>) {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}

timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
11 changes: 6 additions & 5 deletions src/hooks/useCodapData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useCallback, useState } from "react";
import { kDataContextName, kChildCollectionName, kParentCollectionName, kParentCollectionAttributes, kChildCollectionAttributes } from "../constants";
import { DaylightCalcOptions, ILocation } from "../types";
import { getDayLightInfo, locationsEqual } from "../utils/daylight-utils";
Expand All @@ -23,6 +23,7 @@ export const useCodapData = () => {
if (result.success) {
let dc = result.values;
let lastCollection = dc.collections[dc.collections.length - 1];
console.trace();

Check warning on line 26 in src/hooks/useCodapData.ts

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Unexpected console statement

Check warning on line 26 in src/hooks/useCodapData.ts

View workflow job for this annotation

GitHub Actions / S3 Deploy

Unexpected console statement
return await codapInterface.sendRequest({
action: "delete",
resource: `dataContext[${kDataContextName}].collection[${lastCollection.name}].allCases`
Expand All @@ -37,7 +38,7 @@ export const useCodapData = () => {
const calcOptions: DaylightCalcOptions = {
latitude: location.latitude,
longitude: location.longitude,
year: 2024 // NOTE: If data are to be historical, add dynamic year attribute
year: new Date().getFullYear()
};

const solarEvents = getDayLightInfo(calcOptions);
Expand Down Expand Up @@ -67,6 +68,7 @@ export const useCodapData = () => {
longitude: location.longitude,
location: location.name,
date: solarEvent.day,
dayOfYear: solarEvent.dayOfYear,
rawSunrise: solarEvent.rawSunrise,
rawSunset: solarEvent.rawSunset,
"Day length": solarEvent.dayLength,
Expand All @@ -83,7 +85,7 @@ export const useCodapData = () => {
}
};

const updateAttributeVisibility = (attributeName: string, hidden: boolean) => {
const updateAttributeVisibility = useCallback((attributeName: string, hidden: boolean) => {
if (!dataContext) return;

try {
Expand All @@ -97,7 +99,7 @@ export const useCodapData = () => {
} catch (error) {
console.error("Error updating attribute visibility:", error);
}
};
}, [dataContext]);

const extractUniqueLocations = (allItems: any): ILocation[] => {
const uniqueLocations: ILocation[] = [];
Expand All @@ -117,7 +119,6 @@ export const useCodapData = () => {
return uniqueLocations;
}


const getUniqueLocationsInCodapData = async () => {
const locationAttr = await getAttribute(kDataContextName, kParentCollectionName, "location");
if (locationAttr.success){
Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
export interface ICodapDataContextInfo {
id: number;
name: string;
title: string;
}

export interface ICurrentDayLocation {
_latitude: string;
_longitude: string;
_dayOfYear: number;
}

export interface ILocation {
name: string;
Expand All @@ -13,6 +24,7 @@ export interface DaylightCalcOptions {

export interface DaylightInfo {
day: string; // read into CODAP as an ISO date
dayOfYear: number;
rawSunrise: string; // read into CODAP as an ISO date
rawSunset: string; // read into CODAP as an ISO date
dayLength: number;
Expand Down
Loading

0 comments on commit 997f273

Please sign in to comment.