Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

187887881 selected row sync #24

Merged
merged 16 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that line fail with null passed as selectionResult when the block above returns null?

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line can be anywhere in the functional component body, e.g. right below state declarations. As any state update will trigger re-render, and this line will be executed during this re-render.

debouncedUpdateRowSelectionInCodap(
currentDayLocationRef.current._latitude,
currentDayLocationRef.current._longitude,
day
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is not part of any useCallack and/or useEffect, you can freely use state here and simply call:
debouncedUpdateRowSelectionInCodap(latitude, 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, probably you don't need this ref update here.

}
}, []);

useEffect(() => {
const initialize = async () => {
try {
Expand All @@ -65,38 +92,66 @@
} 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.log to remove?

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]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't need to wrap that in useEffect. It can be anywhere in the functional component body. I usually place it right after the ref declaration.


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 @@
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debug line to remove?

return await codapInterface.sendRequest({
action: "delete",
resource: `dataContext[${kDataContextName}].collection[${lastCollection.name}].allCases`
Expand All @@ -37,7 +38,7 @@
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 @@
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 @@
}
};

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

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

const extractUniqueLocations = (allItems: any): ILocation[] => {
const uniqueLocations: ILocation[] = [];
Expand All @@ -117,7 +119,6 @@
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
Loading