diff --git a/app/api/dtos/ForceGraphAttributesDtoSchema.ts b/app/api/dtos/ForceGraphAttributesDtoSchema.ts index 1412054..eeadc71 100644 --- a/app/api/dtos/ForceGraphAttributesDtoSchema.ts +++ b/app/api/dtos/ForceGraphAttributesDtoSchema.ts @@ -1,8 +1,7 @@ -import { useRef } from 'react'; - +import { useRef } from 'react'; +import { useSelectiveContextListenerNumber } from '../../generic/components/selective-context/selective-context-manager-number'; import { useNormalizeForceRange } from '../../graphing/components/force-attributes-meta-data'; import { z } from 'zod'; -import { useSelectiveContextListenerNumber } from '../../generic/components/selective-context/selective-context-manager-number'; export const ForceGraphAttributesDtoSchema = z.object({ id: z.number(), centerStrength: z.number(), @@ -17,150 +16,101 @@ export const ForceGraphAttributesDtoSchema = z.object({ forceYStrength: z.number(), forceRadialStrength: z.number(), forceRadialXRelative: z.number(), - forceRadialYRelative: z.number() + forceRadialYRelative: z.number(), }); -export type ForceGraphAttributesDto = z.infer< - typeof ForceGraphAttributesDtoSchema ->; -export function useForceAttributeListeners(uniqueGraphName: string) { - const { currentState: collideStrength } = useSelectiveContextListenerNumber( - `${uniqueGraphName}-collideStrength`, - `${uniqueGraphName}-collideStrength-listener`, - 100 - ); - const { currentState: centerStrength } = useSelectiveContextListenerNumber( - `${uniqueGraphName}-centerStrength`, - `${uniqueGraphName}-centerStrength-listener`, - 100 - ); - const { currentState: manyBodyTheta } = useSelectiveContextListenerNumber( - `${uniqueGraphName}-manyBodyTheta`, - `${uniqueGraphName}-manyBodyTheta-listener`, - 100 - ); - const { currentState: forceRadialYRelative } = - useSelectiveContextListenerNumber( - `${uniqueGraphName}-forceRadialYRelative`, - `${uniqueGraphName}-forceRadialYRelative-listener`, - 100 - ); - const { currentState: forceYStrength } = useSelectiveContextListenerNumber( - `${uniqueGraphName}-forceYStrength`, - `${uniqueGraphName}-forceYStrength-listener`, - 100 - ); - const { currentState: manyBodyStrength } = useSelectiveContextListenerNumber( - `${uniqueGraphName}-manyBodyStrength`, - `${uniqueGraphName}-manyBodyStrength-listener`, - 100 - ); - const { currentState: forceXStrength } = useSelectiveContextListenerNumber( - `${uniqueGraphName}-forceXStrength`, - `${uniqueGraphName}-forceXStrength-listener`, - 100 - ); - const { currentState: linkDistance } = useSelectiveContextListenerNumber( - `${uniqueGraphName}-linkDistance`, - `${uniqueGraphName}-linkDistance-listener`, - 100 - ); - const { currentState: manyBodyMinDistance } = - useSelectiveContextListenerNumber( - `${uniqueGraphName}-manyBodyMinDistance`, - `${uniqueGraphName}-manyBodyMinDistance-listener`, - 100 - ); - const { currentState: manyBodyMaxDistance } = - useSelectiveContextListenerNumber( - `${uniqueGraphName}-manyBodyMaxDistance`, - `${uniqueGraphName}-manyBodyMaxDistance-listener`, - 100 - ); - const { currentState: forceRadialXRelative } = - useSelectiveContextListenerNumber( - `${uniqueGraphName}-forceRadialXRelative`, - `${uniqueGraphName}-forceRadialXRelative-listener`, - 100 - ); - const { currentState: forceRadialStrength } = - useSelectiveContextListenerNumber( - `${uniqueGraphName}-forceRadialStrength`, - `${uniqueGraphName}-forceRadialStrength-listener`, - 100 - ); - const { currentState: linkStrength } = useSelectiveContextListenerNumber( - `${uniqueGraphName}-linkStrength`, - `${uniqueGraphName}-linkStrength-listener`, - 100 - ); +export type ForceGraphAttributesDto = z.infer; +export function useForceAttributeListeners(uniqueGraphName: string){ +const { currentState: collideStrength } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-collideStrength`, + `${uniqueGraphName}-collideStrength-listener`, + 100 +); +const { currentState: centerStrength } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-centerStrength`, + `${uniqueGraphName}-centerStrength-listener`, + 100 +); +const { currentState: manyBodyTheta } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-manyBodyTheta`, + `${uniqueGraphName}-manyBodyTheta-listener`, + 100 +); +const { currentState: forceRadialYRelative } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-forceRadialYRelative`, + `${uniqueGraphName}-forceRadialYRelative-listener`, + 100 +); +const { currentState: forceYStrength } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-forceYStrength`, + `${uniqueGraphName}-forceYStrength-listener`, + 100 +); +const { currentState: manyBodyStrength } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-manyBodyStrength`, + `${uniqueGraphName}-manyBodyStrength-listener`, + 100 +); +const { currentState: forceXStrength } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-forceXStrength`, + `${uniqueGraphName}-forceXStrength-listener`, + 100 +); +const { currentState: linkDistance } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-linkDistance`, + `${uniqueGraphName}-linkDistance-listener`, + 100 +); +const { currentState: manyBodyMinDistance } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-manyBodyMinDistance`, + `${uniqueGraphName}-manyBodyMinDistance-listener`, + 100 +); +const { currentState: manyBodyMaxDistance } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-manyBodyMaxDistance`, + `${uniqueGraphName}-manyBodyMaxDistance-listener`, + 100 +); +const { currentState: forceRadialXRelative } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-forceRadialXRelative`, + `${uniqueGraphName}-forceRadialXRelative-listener`, + 100 +); +const { currentState: forceRadialStrength } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-forceRadialStrength`, + `${uniqueGraphName}-forceRadialStrength-listener`, + 100 +); +const { currentState: linkStrength } = useSelectiveContextListenerNumber( + `${uniqueGraphName}-linkStrength`, + `${uniqueGraphName}-linkStrength-listener`, + 100 +); - const collideStrengthNormalized = useNormalizeForceRange(collideStrength); - const collideStrengthRef = useRef(collideStrengthNormalized); - const centerStrengthNormalized = useNormalizeForceRange(centerStrength); - const centerStrengthRef = useRef(centerStrengthNormalized); - const manyBodyThetaNormalized = useNormalizeForceRange(manyBodyTheta); - const manyBodyThetaRef = useRef(manyBodyThetaNormalized); - const forceRadialYRelativeNormalized = - useNormalizeForceRange(forceRadialYRelative); - const forceRadialYRelativeRef = useRef(forceRadialYRelativeNormalized); - const forceYStrengthNormalized = useNormalizeForceRange(forceYStrength); - const forceYStrengthRef = useRef(forceYStrengthNormalized); - const manyBodyStrengthNormalized = useNormalizeForceRange( - manyBodyStrength, - 'manyBodyStrength' - ); - const manyBodyStrengthRef = useRef(manyBodyStrengthNormalized); - const forceXStrengthNormalized = useNormalizeForceRange(forceXStrength); - const forceXStrengthRef = useRef(forceXStrengthNormalized); - const linkDistanceNormalized = useNormalizeForceRange( - linkDistance, - 'linkDistance' - ); - const linkDistanceRef = useRef(linkDistanceNormalized); - const manyBodyMinDistanceNormalized = useNormalizeForceRange( - manyBodyMinDistance, - 'manyBodyMinDistance' - ); - const manyBodyMinDistanceRef = useRef(manyBodyMinDistanceNormalized); - const manyBodyMaxDistanceNormalized = useNormalizeForceRange( - manyBodyMaxDistance, - 'manyBodyMaxDistance' - ); - const manyBodyMaxDistanceRef = useRef(manyBodyMaxDistanceNormalized); - const forceRadialXRelativeNormalized = - useNormalizeForceRange(forceRadialXRelative); - const forceRadialXRelativeRef = useRef(forceRadialXRelativeNormalized); - const forceRadialStrengthNormalized = - useNormalizeForceRange(forceRadialStrength); - const forceRadialStrengthRef = useRef(forceRadialStrengthNormalized); - const linkStrengthNormalized = useNormalizeForceRange(linkStrength); - const linkStrengthRef = useRef(linkStrengthNormalized); - return { - collideStrengthNormalized, - collideStrengthRef, - centerStrengthNormalized, - centerStrengthRef, - manyBodyThetaNormalized, - manyBodyThetaRef, - forceRadialYRelativeNormalized, - forceRadialYRelativeRef, - forceYStrengthNormalized, - forceYStrengthRef, - manyBodyStrengthNormalized, - manyBodyStrengthRef, - forceXStrengthNormalized, - forceXStrengthRef, - linkDistanceNormalized, - linkDistanceRef, - manyBodyMinDistanceNormalized, - manyBodyMinDistanceRef, - manyBodyMaxDistanceNormalized, - manyBodyMaxDistanceRef, - forceRadialXRelativeNormalized, - forceRadialXRelativeRef, - forceRadialStrengthNormalized, - forceRadialStrengthRef, - linkStrengthNormalized, - linkStrengthRef - }; +const collideStrengthNormalized = useNormalizeForceRange(collideStrength); +const collideStrengthRef = useRef(collideStrengthNormalized); +const centerStrengthNormalized = useNormalizeForceRange(centerStrength); +const centerStrengthRef = useRef(centerStrengthNormalized); +const manyBodyThetaNormalized = useNormalizeForceRange(manyBodyTheta); +const manyBodyThetaRef = useRef(manyBodyThetaNormalized); +const forceRadialYRelativeNormalized = useNormalizeForceRange(forceRadialYRelative); +const forceRadialYRelativeRef = useRef(forceRadialYRelativeNormalized); +const forceYStrengthNormalized = useNormalizeForceRange(forceYStrength); +const forceYStrengthRef = useRef(forceYStrengthNormalized); +const manyBodyStrengthNormalized = useNormalizeForceRange(manyBodyStrength, 'manyBodyStrength'); +const manyBodyStrengthRef = useRef(manyBodyStrengthNormalized); +const forceXStrengthNormalized = useNormalizeForceRange(forceXStrength); +const forceXStrengthRef = useRef(forceXStrengthNormalized); +const linkDistanceNormalized = useNormalizeForceRange(linkDistance, 'linkDistance'); +const linkDistanceRef = useRef(linkDistanceNormalized); +const manyBodyMinDistanceNormalized = useNormalizeForceRange(manyBodyMinDistance, 'manyBodyMinDistance'); +const manyBodyMinDistanceRef = useRef(manyBodyMinDistanceNormalized); +const manyBodyMaxDistanceNormalized = useNormalizeForceRange(manyBodyMaxDistance, 'manyBodyMaxDistance'); +const manyBodyMaxDistanceRef = useRef(manyBodyMaxDistanceNormalized); +const forceRadialXRelativeNormalized = useNormalizeForceRange(forceRadialXRelative); +const forceRadialXRelativeRef = useRef(forceRadialXRelativeNormalized); +const forceRadialStrengthNormalized = useNormalizeForceRange(forceRadialStrength); +const forceRadialStrengthRef = useRef(forceRadialStrengthNormalized); +const linkStrengthNormalized = useNormalizeForceRange(linkStrength); +const linkStrengthRef = useRef(linkStrengthNormalized); + return { collideStrengthNormalized, collideStrengthRef, centerStrengthNormalized, centerStrengthRef, manyBodyThetaNormalized, manyBodyThetaRef, forceRadialYRelativeNormalized, forceRadialYRelativeRef, forceYStrengthNormalized, forceYStrengthRef, manyBodyStrengthNormalized, manyBodyStrengthRef, forceXStrengthNormalized, forceXStrengthRef, linkDistanceNormalized, linkDistanceRef, manyBodyMinDistanceNormalized, manyBodyMinDistanceRef, manyBodyMaxDistanceNormalized, manyBodyMaxDistanceRef, forceRadialXRelativeNormalized, forceRadialXRelativeRef, forceRadialStrengthNormalized, forceRadialStrengthRef, linkStrengthNormalized, linkStrengthRef, } } diff --git a/app/build-metrics/lesson-cycles/[schedule]/lesson-cycle-build-metrics-card.tsx b/app/build-metrics/lesson-cycles/[schedule]/lesson-cycle-build-metrics-card.tsx index 7b8e52d..b57713c 100644 --- a/app/build-metrics/lesson-cycles/[schedule]/lesson-cycle-build-metrics-card.tsx +++ b/app/build-metrics/lesson-cycles/[schedule]/lesson-cycle-build-metrics-card.tsx @@ -9,7 +9,7 @@ import { NameIdStringTuple } from '../../../api/dtos/NameIdStringTupleSchema'; import DynamicDimensionTimetable, { HeaderTransformer } from '../../../generic/components/tables/dynamic-dimension-timetable'; -import NameIdTupleParamsSelector from '../../../generic/components/dropdown/name-id-tuple-params-selector'; +import StringNameStringIdSearchParamsSelector from '../../../generic/components/dropdown/string-name-string-id-search-params-selector'; export function LessonCycleBuildMetricsCard({ nameIdStringTuples, @@ -34,7 +34,7 @@ export function LessonCycleBuildMetricsCard({ {nameIdStringTuples.length > 0 && ( - (); + +export function useSearchParamsContext() { + const searchParamsMap = useContext(SearchParamsContext); + const dispatchSearchParams = useContext(SearchParamsDispatchContext); + return { searchParamsMap, dispatchSearchParams }; +} diff --git a/app/contexts/string-map-context/search-params-context-provider.tsx b/app/contexts/string-map-context/search-params-context-provider.tsx new file mode 100644 index 0000000..22e302c --- /dev/null +++ b/app/contexts/string-map-context/search-params-context-provider.tsx @@ -0,0 +1,20 @@ +import { PropsWithChildren } from 'react'; +import { StringMapContextProvider } from './string-map-context-provider'; +import { ObjectPlaceholder } from '../../generic/components/selective-context/selective-context-manager-function'; +import { + SearchParamsContext, + SearchParamsDispatchContext +} from './search-params-context-creator'; +import { NameIdStringTuple } from '../../api/dtos/NameIdStringTupleSchema'; +import { AccessorFunction } from '../../generic/components/tables/rating/rating-table'; + +const Provider = StringMapContextProvider + +const KeyAccessor: AccessorFunction = tuple => tuple.id +export default function SearchParamsContextProvider({children}:PropsWithChildren) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/app/contexts/string-map-context/string-map-context-provider.tsx b/app/contexts/string-map-context/string-map-context-provider.tsx index a171b79..c5b38fe 100644 --- a/app/contexts/string-map-context/string-map-context-provider.tsx +++ b/app/contexts/string-map-context/string-map-context-provider.tsx @@ -17,10 +17,8 @@ export interface StringMapContextProviderProps { export function StringMapContextProvider({ initialEntityMap, - dispatchContext, mapContext, - children, mapKeyAccessor }: StringMapContextProviderProps & PropsWithChildren) { @@ -28,11 +26,9 @@ export function StringMapContextProvider({ const MapProvider = mapContext.Provider; const EntityReducer = StringMapReducer; const [currentModels, dispatch] = useReducer(EntityReducer, initialEntityMap); - const initialMapRef = useRef(initialEntityMap); useSyncStringMapToProps( initialEntityMap, - initialMapRef, dispatch, currentModels, mapKeyAccessor diff --git a/app/contexts/string-map-context/string-map-reducer.ts b/app/contexts/string-map-context/string-map-reducer.ts index c462f8b..641ed18 100644 --- a/app/contexts/string-map-context/string-map-reducer.ts +++ b/app/contexts/string-map-context/string-map-reducer.ts @@ -1,3 +1,4 @@ +'use client'; import { Dispatch, useReducer } from 'react'; import { Draft, produce } from 'immer'; diff --git a/app/contexts/string-map-context/use-sync-string-map-to-props.ts b/app/contexts/string-map-context/use-sync-string-map-to-props.ts index 1d302e2..3855f15 100644 --- a/app/contexts/string-map-context/use-sync-string-map-to-props.ts +++ b/app/contexts/string-map-context/use-sync-string-map-to-props.ts @@ -1,17 +1,23 @@ +'use client'; import { MapDispatch, MapDispatchBatch, StringMap } from './string-map-reducer'; -import { Dispatch, MutableRefObject, useEffect } from 'react'; +import { Dispatch, MutableRefObject, useEffect, useRef } from 'react'; import { AccessorFunction } from '../../generic/components/tables/rating/rating-table'; import { getPayloadArray } from '../../curriculum/delivery-models/use-editing-context-dependency'; +import { isNotUndefined } from '../../api/main'; export function useSyncStringMapToProps( initialEntityMap: StringMap, - initialMapRef: MutableRefObject>, dispatch: Dispatch | MapDispatchBatch>, currentModels: StringMap, mapKeyAccessor: AccessorFunction ) { + const initialMapRef = useRef(initialEntityMap); + useEffect(() => { - if (initialMapRef.current !== initialEntityMap) { + if ( + initialMapRef.current !== initialEntityMap && + isNotUndefined(dispatch) + ) { const payloadArray = getPayloadArray( Object.values(currentModels), mapKeyAccessor @@ -31,4 +37,4 @@ export function useSyncStringMapToProps( initialMapRef, dispatch ]); -} \ No newline at end of file +} diff --git a/app/contexts/string-map-context/writeable-string-map-context-provider.tsx b/app/contexts/string-map-context/writeable-string-map-context-provider.tsx index 08583ef..dc92b26 100644 --- a/app/contexts/string-map-context/writeable-string-map-context-provider.tsx +++ b/app/contexts/string-map-context/writeable-string-map-context-provider.tsx @@ -59,7 +59,6 @@ export function WriteableStringMapContextProvider({ useSyncStringMapToProps( initialEntityMap, - initialMapRef, dispatch, currentModels, mapKeyAccessor diff --git a/app/curriculum/delivery-models/contexts/work-task-type-context-provider.tsx b/app/curriculum/delivery-models/contexts/work-task-type-context-provider.tsx index 7087d60..2470618 100644 --- a/app/curriculum/delivery-models/contexts/work-task-type-context-provider.tsx +++ b/app/curriculum/delivery-models/contexts/work-task-type-context-provider.tsx @@ -1,7 +1,8 @@ 'use client'; import { StringMap, - StringMapReducer + StringMapReducer, + useStringMapReducer } from '../../../contexts/string-map-context/string-map-reducer'; import React, { PropsWithChildren, useReducer } from 'react'; import { useSelectiveContextControllerBoolean } from '../../../generic/components/selective-context/selective-context-manager-boolean'; @@ -14,6 +15,8 @@ import { putWorkTaskTypes } from '../../../api/actions/work-task-types'; import { getPayloadArray } from '../use-editing-context-dependency'; import { UnsavedChangesModal } from '../../../generic/components/modals/unsaved-changes-modal'; +import { useSyncStringMapToProps } from '../../../contexts/string-map-context/use-sync-string-map-to-props'; +import { IdStringFromNumberAccessor } from '../../../premises/classroom-suitability/rating-table-accessor-functions'; export const UnsavedWorkTaskTypeChanges = 'unsaved-workTaskType-changes'; export const WorkTaskTypeChangesProviderListener = @@ -27,6 +30,7 @@ export function WorkTaskTypeContextProvider({ }: { entityMap: StringMap } & PropsWithChildren) { const WorkTaskTypeReducer = StringMapReducer; const [currentModels, dispatch] = useReducer(WorkTaskTypeReducer, entityMap); + // const [currentModels, dispatch] = useStringMapReducer(entityMap); const { currentState: modalOpen, dispatchUpdate } = useSelectiveContextControllerBoolean( workTaskTypeCommitKey, @@ -40,6 +44,13 @@ export function WorkTaskTypeContextProvider({ false ); + useSyncStringMapToProps( + entityMap, + dispatch, + currentModels, + IdStringFromNumberAccessor + ); + const handleClose = () => { dispatchUpdate({ contextKey: workTaskTypeCommitKey, value: false }); }; diff --git a/app/generic/components/dropdown/dropdown-param.tsx b/app/generic/components/dropdown/dropdown-param.tsx index 5b7436a..18e90ec 100644 --- a/app/generic/components/dropdown/dropdown-param.tsx +++ b/app/generic/components/dropdown/dropdown-param.tsx @@ -3,6 +3,7 @@ import { Menu, Transition } from '@headlessui/react'; import { Fragment, useTransition } from 'react'; import { ArrowRightIcon, ChevronDownIcon } from '@heroicons/react/20/solid'; import { usePathname, useRouter } from 'next/navigation'; +import { SpanTruncateEllipsis } from './span-truncate-ellipsis'; interface DropdownParamProps { paramOptions: string[]; @@ -85,7 +86,3 @@ export default function DropdownParam({ ); } - -export function SpanTruncateEllipsis({ children }: { children: string }) { - return {children}; -} diff --git a/app/generic/components/dropdown/span-truncate-ellipsis.tsx b/app/generic/components/dropdown/span-truncate-ellipsis.tsx new file mode 100644 index 0000000..1dea90c --- /dev/null +++ b/app/generic/components/dropdown/span-truncate-ellipsis.tsx @@ -0,0 +1,3 @@ +export function SpanTruncateEllipsis({ children }: { children: string }) { + return {children}; +} diff --git a/app/generic/components/dropdown/name-id-tuple-params-selector.tsx b/app/generic/components/dropdown/string-name-string-id-search-params-selector.tsx similarity index 67% rename from app/generic/components/dropdown/name-id-tuple-params-selector.tsx rename to app/generic/components/dropdown/string-name-string-id-search-params-selector.tsx index 5a036de..f3258f9 100644 --- a/app/generic/components/dropdown/name-id-tuple-params-selector.tsx +++ b/app/generic/components/dropdown/string-name-string-id-search-params-selector.tsx @@ -1,36 +1,57 @@ 'use client'; import { Listbox, Transition } from '@headlessui/react'; import { ChevronUpDownIcon } from '@heroicons/react/24/outline'; -import React, { Fragment, startTransition } from 'react'; +import React, { Fragment, startTransition, useMemo } from 'react'; import { CheckIcon } from '@heroicons/react/20/solid'; -import { usePathname, useRouter } from 'next/navigation'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { NameIdStringTuple } from '../../../api/dtos/NameIdStringTupleSchema'; +import { isNotNull, isNotUndefined } from '../../../api/main'; +import { StringMap } from '../../../contexts/string-map-context/string-map-reducer'; -export default function NameIdTupleParamsSelector({ - selectionList, - selectedProp, - selectionDescriptor -}: { +interface NameIdTupleSearchParamSelector { selectionDescriptor: string; selectionList: NameIdStringTuple[]; - selectedProp: NameIdStringTuple; -}) { + selectedProp?: NameIdStringTuple; + searchParamKey?: string; +} + +export default function StringNameStringIdSearchParamsSelector({ + selectionList, + selectedProp, + selectionDescriptor, + searchParamKey = 'id' +}: NameIdTupleSearchParamSelector) { const { push } = useRouter(); const pathname = usePathname(); + const readonlyURLSearchParams = useSearchParams(); + const currentSelectionId = readonlyURLSearchParams?.get(searchParamKey); + const selectionToIdStringMap = useMemo(() => { + const map: StringMap = {}; + selectionList.forEach((tuple) => (map[tuple.name] = tuple)); + return map; + }, [selectionList]); + const currentSelection = isNotUndefined(selectedProp) + ? selectedProp + : isNotNull(currentSelectionId) && isNotUndefined(currentSelectionId) + ? selectionToIdStringMap[currentSelectionId] + : undefined; - const updateSearchParams = (updatedSelection: NameIdStringTuple) => { + const updateSearchParams = (updatedSelection: NameIdStringTuple | null) => { const params = new URLSearchParams(window.location.search); - params.set('id', updatedSelection.id); - + if (isNotNull(updatedSelection)) { + params.set(searchParamKey, updatedSelection.id); + } else { + params.delete(searchParamKey); + } startTransition(() => { push(`${pathname}?${params.toString()}`, { scroll: false }); }); }; return ( - +
@@ -38,7 +59,9 @@ export default function NameIdTupleParamsSelector({ {selectionDescriptor} {': '} - {selectedProp.name != '' ? selectedProp.name : 'No Selection'} + {isNotUndefined(currentSelection) + ? currentSelection.name + : 'No Selection'} ; @@ -33,12 +34,13 @@ export default function TupleSelector({ selectionDescriptor, optionTransformer: OptionTransformerComponent }: { - selectedState: NameIdStringTuple; + selectedState: NameIdStringTuple | null; selectionList: NameIdStringTuple[]; - updateSelectedState: (value: NameIdStringTuple) => void; + updateSelectedState: (value: NameIdStringTuple | null) => void; selectionDescriptor: string; optionTransformer?: OptionTransformer; }) { + console.log(selectionList); return ( updateSelectedState(value)} >
- - - - {selectionDescriptor} - {': '} - - {selectedState.name != '' ? selectedState.name : 'No Selection'} - - +
+ + + + {selectionDescriptor} + {': '} + + {isNotNull(selectedState) ? selectedState.name : 'No Selection'} + + + + + - +
, HTMLButtonElement @@ -34,139 +37,144 @@ function useSelectiveListenerKey( }, [unsavedContextKey, listenerKey]); } -const ProtectedNavigation = forwardRef( - function ProtectedNavigation(props: Props, ref) { - const { - onConfirm, - children, - isActive, - requestConfirmation, - unsavedContextKey, - unsavedListenerKey: listenerKeyProp, - className, - disabled - } = props; - let [isOpen, setIsOpen] = useState(false); +const ProtectedNavigation = forwardRef< + HTMLButtonElement, + ProtectedNavigationProps +>(function ProtectedNavigation(props: ProtectedNavigationProps, ref) { + const { + onConfirm, + children, + isActive, + requestConfirmation, + unsavedContextKey, + unsavedListenerKey: listenerKeyProp, + className, + disabled + } = props; + let [isOpen, setIsOpen] = useState(false); - const unsavedListenerKey = useSelectiveListenerKey( - unsavedContextKey, - `${paginationUnsavedListenerKey}:${listenerKeyProp}` - ); + // const unsavedListenerKey = useSelectiveListenerKey( + // unsavedContextKey, + // `${paginationUnsavedListenerKey}:${listenerKeyProp}` + // ); - const { isTrue: protectionActive } = useSelectiveContextListenerBoolean( + const { currentState: protectionActive, dispatchWithoutControl } = + useSelectiveContextDispatchBoolean( unsavedContextKey || '', - unsavedListenerKey, + listenerKeyProp || '', false ); - const finalClassNames = useMemo(() => { - return className - ? className - : classNames( - isActive - ? 'border-slate-500 text-gray-900' - : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300', - 'group inline-flex w-full rounded-md items-center px-2 py-2 border-b-2 text-sm font-medium' - ); - }, [className, isActive]); + const finalClassNames = useMemo(() => { + return className + ? className + : classNames( + isActive + ? 'border-slate-500 text-gray-900' + : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300', + 'group inline-flex w-full rounded-md items-center px-2 py-2 border-b-2 text-sm font-medium' + ); + }, [className, isActive]); - function closeModal() { - setIsOpen(false); - } + function closeModal() { + setIsOpen(false); + } - function openModal() { - setIsOpen(true); - } + function openModal() { + setIsOpen(true); + } - return ( - <> - + return ( + <> + - - - -
- + + + +
+ -
-
- - - - Unsafe Navigation - -
-

- You have unsaved changes that will be lost if you - proceed. Discard changes? -

-
+
+
+ + + + Unsafe Navigation + +
+

+ You have unsaved changes that will be lost if you proceed. + Discard changes? +

+
-
- - - - - -
-
-
-
+
+ + + + + +
+ +
-
-
- - ); - } -); +
+
+
+ + ); +}); export default ProtectedNavigation; diff --git a/app/premises/classroom-suitability/apply-search-params.tsx b/app/premises/classroom-suitability/apply-search-params.tsx new file mode 100644 index 0000000..a7e42f2 --- /dev/null +++ b/app/premises/classroom-suitability/apply-search-params.tsx @@ -0,0 +1,42 @@ +'use client'; +import { useSearchParamsContext } from '../../contexts/string-map-context/search-params-context-creator'; +import { isNotNull, isNotUndefined } from '../../api/main'; +import { startTransition, useTransition } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { PendingOverlay } from '../../generic/components/overlays/pending-overlay'; +import ProtectedNavigation, { + ProtectedNavigationProps +} from '../../navbar/protected-navigation'; +import { UnsavedAssetChanges } from '../asset-string-map-context-creator'; + +export function ApplySearchParams({ buttonLabel }: { buttonLabel?: string }) { + const { searchParamsMap } = useSearchParamsContext(); + const pathname = usePathname(); + const { push } = useRouter(); + const [pending, startTransition] = useTransition(); + const handleApplyParams = () => { + const params = new URLSearchParams(); + for (let searchParamsMapKey in searchParamsMap) { + const updatedSelection = searchParamsMap[searchParamsMapKey]; + if (isNotNull(updatedSelection)) { + params.set(searchParamsMapKey, updatedSelection.id); + } else { + params.delete(searchParamsMapKey); + } + } + startTransition(() => { + push(`${pathname}?${params.toString()}`, { scroll: false }); + }); + }; + return ( + + + {isNotUndefined(buttonLabel) ? buttonLabel : 'Apply Filters'} + + ); +} diff --git a/app/premises/classroom-suitability/asset-disclosure-list-panel.tsx b/app/premises/classroom-suitability/asset-disclosure-list-panel.tsx index 1981a92..07d54aa 100644 --- a/app/premises/classroom-suitability/asset-disclosure-list-panel.tsx +++ b/app/premises/classroom-suitability/asset-disclosure-list-panel.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useAssetStringMapContext } from '../asset-string-map-context-creator'; import { AssetDto } from '../../api/dtos/AssetDtoSchema'; import { AssetSelectionListContextKey } from './asset-suitability-table-wrapper'; @@ -18,6 +18,7 @@ import DisclosureListPanel, { import ListItemSelector from '../../generic/components/disclosure-list/list-item-selector'; import { useAssetSuitabilityStringMapContext } from '../asset-suitability-context-creator'; import { IdStringFromNumberAccessor } from './rating-table-accessor-functions'; +import { useSyncStringMapToProps } from '../../contexts/string-map-context/use-sync-string-map-to-props'; export function AssetDisclosureListPanel() { const { assetDtoStringMap } = useAssetStringMapContext(); @@ -50,8 +51,6 @@ const AssetPanelTransformer: PanelTransformer = ({ initialValue: assetSuitabilityStringMap[IdStringFromNumberAccessor(data)] }); - console.log(suitabilityList); - return ( = ({ data }: TransformerProps) => { const { assetSuitabilityStringMap } = useAssetSuitabilityStringMapContext(); - const {} = useAssetSuitabilityListController({ - contextKey: `${data.id}`, + const assetSuitabilityStringMapElement = + assetSuitabilityStringMap[IdStringFromNumberAccessor(data)]; + const stringMapFromContext = useRef(assetSuitabilityStringMapElement); + + const contextKey = useMemo(() => `${data.id}`, [data]); + const { dispatchUpdate } = useAssetSuitabilityListController({ + contextKey: contextKey, listenerKey: 'classroom-label', - initialValue: assetSuitabilityStringMap[IdStringFromNumberAccessor(data)] + initialValue: assetSuitabilityStringMapElement }); + useEffect(() => { + if (stringMapFromContext.current !== assetSuitabilityStringMapElement) { + console.log('Syncing props with context'); + dispatchUpdate({ contextKey, value: assetSuitabilityStringMapElement }); + stringMapFromContext.current = assetSuitabilityStringMapElement; + } + }, [ + stringMapFromContext, + dispatchUpdate, + assetSuitabilityStringMapElement, + contextKey + ]); + return <>{data.name}; }; diff --git a/app/premises/classroom-suitability/asset-suitability-table-wrapper.tsx b/app/premises/classroom-suitability/asset-suitability-table-wrapper.tsx index 1f065b2..92a596e 100644 --- a/app/premises/classroom-suitability/asset-suitability-table-wrapper.tsx +++ b/app/premises/classroom-suitability/asset-suitability-table-wrapper.tsx @@ -29,26 +29,17 @@ export function AssetSuitabilityTableWrapper() { IdStringFromNumberAccessor ); - const { currentState: selectedWorkTaskTypeList } = - useSelectiveContextControllerNumberList({ - contextKey: workTaskTypeSelectionListContextKey, - listenerKey: assetSuitabilityTableWrapperListenerKey, - initialValue: EmptyArray - }); const assetDtos = useMemoizedSelectionFromListAndStringMap( selectedAssetList, assetDtoStringMap ); - useMemoizedSelectionFromListAndStringMap( - selectedWorkTaskTypeList, - workTaskTypeMap - ); + const allWorkTaskTypes = Object.values(workTaskTypeMap).sort((wtt1, wtt2) => wtt1.name.localeCompare(wtt2.name) ); return ( - +
-
+
@@ -109,7 +110,9 @@ export default async function Page({ + +
diff --git a/app/premises/classroom-suitability/string-map-context-filter-selector.tsx b/app/premises/classroom-suitability/string-map-context-filter-selector.tsx new file mode 100644 index 0000000..a05566a --- /dev/null +++ b/app/premises/classroom-suitability/string-map-context-filter-selector.tsx @@ -0,0 +1,78 @@ +'use client'; +import { + Context, + PropsWithChildren, + useCallback, + useContext, + useMemo +} from 'react'; +import { StringMap } from '../../contexts/string-map-context/string-map-reducer'; +import { AccessorFunction } from '../../generic/components/tables/rating/rating-table'; +import StringNameStringIdSearchParamsSelector from '../../generic/components/dropdown/string-name-string-id-search-params-selector'; +import { NameIdStringTuple } from '../../api/dtos/NameIdStringTupleSchema'; +import { isNotNull, isNotUndefined } from '../../api/main'; +import TupleSelector from '../../generic/components/dropdown/tuple-selector'; +import { useSelectiveContextControllerString } from '../../generic/components/selective-context/selective-context-manager-string'; +import { useSearchParamsContext } from '../../contexts/string-map-context/search-params-context-creator'; + +export interface Comparator { + (element1: T, element2: T): number; +} + +interface StringMapContextFilterProps extends PropsWithChildren { + context: Context>; + idAccessor: AccessorFunction; + idSearchParamKey?: string; + labelAccessor: AccessorFunction; + labelDescriptor: string; + sortFunction?: Comparator; +} + +export function StringMapContextFilterSelector({ + context, + idAccessor, + idSearchParamKey = 'id', + labelAccessor, + labelDescriptor, + sortFunction +}: StringMapContextFilterProps) { + const stringMapTypeT = useContext(context); + const selectionList: NameIdStringTuple[] = useMemo(() => { + let values = Object.values(stringMapTypeT); + if (isNotUndefined(sortFunction)) { + values = values.sort(sortFunction); + } + return values.map((value) => ({ + id: idAccessor(value), + name: labelAccessor(value) + })); + }, [idAccessor, labelAccessor, sortFunction, stringMapTypeT]); + const { searchParamsMap, dispatchSearchParams } = useSearchParamsContext(); + const currentSelection = searchParamsMap[idSearchParamKey]; + console.log(selectionList, stringMapTypeT); + const updateSelectedState = useCallback( + (selection: NameIdStringTuple | null) => { + if (isNotNull(selection)) { + dispatchSearchParams({ + type: 'update', + payload: { key: idSearchParamKey, data: selection } + }); + } else { + dispatchSearchParams({ + type: 'delete', + payload: { key: idSearchParamKey } + }); + } + }, + [dispatchSearchParams, idSearchParamKey] + ); + + return ( + + ); +} diff --git a/app/premises/classroom-suitability/work-task-type-filter-group.tsx b/app/premises/classroom-suitability/work-task-type-filter-group.tsx new file mode 100644 index 0000000..1ed06a0 --- /dev/null +++ b/app/premises/classroom-suitability/work-task-type-filter-group.tsx @@ -0,0 +1,49 @@ +'use client'; +import { Card } from '@tremor/react'; +import { StringMapContextFilterSelector } from './string-map-context-filter-selector'; +import { + KnowledgeDomainContext, + KnowledgeLevelContext +} from '../../work-types/lessons/use-service-category-context'; +import { AccessorFunction } from '../../generic/components/tables/rating/rating-table'; +import { KnowledgeDomainDto } from '../../api/dtos/KnowledgeDomainDtoSchema'; +import { IdStringFromNumberAccessor } from './rating-table-accessor-functions'; +import SearchParamsContextProvider from '../../contexts/string-map-context/search-params-context-provider'; +import { ApplySearchParams } from './apply-search-params'; +import { useContext } from 'react'; +import { HasNameDto } from '../../api/dtos/HasNameDtoSchema'; +import { KnowledgeLevelDto } from '../../api/dtos/KnowledgeLevelDtoSchema'; + +export const NameLabelAccessor: AccessorFunction = (dto) => + dto.name; + +export const LevelOrdinalAccessor: AccessorFunction< + KnowledgeLevelDto, + string +> = (dto) => dto.levelOrdinal.toString(); + +export default function WorkTaskTypeFilterGroup() { + const knowledgeDomainDtoStringMap = useContext(KnowledgeDomainContext); + console.log(knowledgeDomainDtoStringMap); + return ( + + + + + + + + ); +} diff --git a/app/work-types/lessons/knowledge-domain-context-provider.tsx b/app/work-types/lessons/knowledge-domain-context-provider.tsx index 9e99ef8..767dc53 100644 --- a/app/work-types/lessons/knowledge-domain-context-provider.tsx +++ b/app/work-types/lessons/knowledge-domain-context-provider.tsx @@ -19,6 +19,7 @@ export default function KnowledgeDomainContextProvider({ children, knowledgeDomains }: KnowledgeDomainContextProviderProps) { + console.log(knowledgeDomains); return ( ( + levels || EmptyArray, + IdStringFromNumberAccessor + ); + const domainsMap = await convertListToStringMap( + domains || EmptyArray, + IdStringFromNumberAccessor + ); + return ( - - + + {children}