diff --git a/.gitignore b/.gitignore index b2d9c234..49bdd74b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist tmp /out-tsc +storybook-static # dependencies node_modules diff --git a/packages/pxweb2-ui/src/lib/components/Search/Search.tsx b/packages/pxweb2-ui/src/lib/components/Search/Search.tsx index 0ff86910..b90f1fda 100644 --- a/packages/pxweb2-ui/src/lib/components/Search/Search.tsx +++ b/packages/pxweb2-ui/src/lib/components/Search/Search.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react'; +import { useState, useRef, useEffect } from 'react'; import cl from 'clsx'; import classes from './Search.module.scss'; @@ -7,6 +7,7 @@ import { Label } from '../Typography/Label/Label'; import { Button } from '../Button/Button'; export interface SearchProps { + value?: string; variant: 'default' | 'inVariableBox'; labelText?: string; searchPlaceHolder?: string; @@ -18,6 +19,7 @@ export interface SearchProps { } export function Search({ + value = '', variant, labelText, searchPlaceHolder, @@ -28,9 +30,13 @@ export function Search({ onChange, ...rest }: SearchProps) { - const [inputValue, setInputValue] = useState(''); + const [inputValue, setInputValue] = useState(value); const inputRef = useRef(null); + useEffect(() => { + setInputValue(value); + }, [value]); + const handleClear = () => { onChange && onChange(''); setInputValue(''); diff --git a/packages/pxweb2-ui/src/lib/components/Select/Select.tsx b/packages/pxweb2-ui/src/lib/components/Select/Select.tsx index 9158399b..dec97990 100644 --- a/packages/pxweb2-ui/src/lib/components/Select/Select.tsx +++ b/packages/pxweb2-ui/src/lib/components/Select/Select.tsx @@ -171,15 +171,17 @@ function VariableBoxSelect({ const cssClasses = className.length > 0 ? ' ' + className : ''; const [isModalOpen, setModalOpen] = useState(false); - - const [selectedItem, setSelectedItem] = useState( - selectedOption, - ); const [clickedItem, setClickedItem] = useState( selectedOption, ); + + const selectedItem: SelectOption | undefined = selectedOption; + const handleOpenModal = () => { setModalOpen(true); + + // Reset clicked item to selected item, incase user made changes and then closed the modal + setClickedItem(selectedItem); }; function handleRadioChange(e: React.ChangeEvent) { @@ -189,7 +191,6 @@ function VariableBoxSelect({ const handleCloseModal = (updated: boolean) => { setModalOpen(false); if (updated) { - setSelectedItem(clickedItem); onChange(clickedItem); } else { setClickedItem(selectedItem); diff --git a/packages/pxweb2-ui/src/lib/components/VariableBox/VariableBoxContent/VariableBoxContent.tsx b/packages/pxweb2-ui/src/lib/components/VariableBox/VariableBoxContent/VariableBoxContent.tsx index 70ded09f..d88c33b7 100644 --- a/packages/pxweb2-ui/src/lib/components/VariableBox/VariableBoxContent/VariableBoxContent.tsx +++ b/packages/pxweb2-ui/src/lib/components/VariableBox/VariableBoxContent/VariableBoxContent.tsx @@ -75,7 +75,6 @@ export function VariableBoxContent({ const [currentFocusedItemIndex, setCurrentFocusedItemIndex] = useState< number | null >(null); - const [items, setItems] = useState<{ type: string; value?: Value }[]>([]); const valuesOnlyList = useRef(null); @@ -104,6 +103,10 @@ export function VariableBoxContent({ useEffect(() => { const newItems: { type: string; value?: Value }[] = []; + if (!valuesToRender || valuesToRender.length === 0) { + return; + } + if (hasSevenOrMoreValues) { newItems.push({ type: 'search' }); } @@ -251,10 +254,41 @@ export function VariableBoxContent({ } }; + const handleChangingCodeListInVariableBox = ( + selectedItem: SelectOption | undefined, + varId: string, + virtuosoRef: React.RefObject, + ) => { + // Call the parent function to change the code list + onChangeCodeList(selectedItem, varId); + + // Reset search state + if (search !== '') { + setSearch(''); + } + + // Reset the scroll show/hide state + if (scrollingDown) { + setScrollingDown(false); + } + + // Reset the virtuoso list to the top/first item + if (virtuosoRef.current) { + virtuosoRef.current.scrollToIndex(0); + } + }; + // Modify the itemRenderer to assign IDs and tabIndex const itemRenderer = (items: any, index: number) => { const item = items[index]; + // There is a race condition with virtuoso where item can be undefined + // Virtuoso will also complain if we return null or similar, so we return an empty div + // This empty div will be removed by the Virtuoso component, and won't be rendered + if (item === undefined) { + return
; + } + if (item.type === 'search') { return (
{ setSearch(value); if (value === '') { @@ -416,7 +451,13 @@ export function VariableBoxContent({ )} options={mappedCodeLists} selectedOption={selectedCodeListOrUndefined} - onChange={(selectedItem) => onChangeCodeList(selectedItem, varId)} + onChange={(selectedItem) => + handleChangingCodeListInVariableBox( + selectedItem, + varId, + virtuosoRef, + ) + } />
)} diff --git a/packages/pxweb2-ui/src/lib/components/VariableList/VariableList.module.scss b/packages/pxweb2-ui/src/lib/components/VariableList/VariableList.module.scss index ddd724b5..ed57bac9 100644 --- a/packages/pxweb2-ui/src/lib/components/VariableList/VariableList.module.scss +++ b/packages/pxweb2-ui/src/lib/components/VariableList/VariableList.module.scss @@ -7,3 +7,8 @@ flex-direction: column; gap: fixed.$spacing-3; } + +.fadeVariableList { + opacity: 0.6; + transition: 'opacity 0.3s ease-in-out'; +} diff --git a/packages/pxweb2-ui/src/lib/components/VariableList/VariableList.tsx b/packages/pxweb2-ui/src/lib/components/VariableList/VariableList.tsx index 852056d1..ef33ec57 100644 --- a/packages/pxweb2-ui/src/lib/components/VariableList/VariableList.tsx +++ b/packages/pxweb2-ui/src/lib/components/VariableList/VariableList.tsx @@ -1,3 +1,5 @@ +import cl from 'clsx'; + import styles from './VariableList.module.scss'; import { SelectedVBValues, VariableBox } from '../VariableBox/VariableBox'; import { PxTableMetadata } from '../../shared-types/pxTableMetadata'; @@ -8,6 +10,7 @@ export type VariableListProps = { pxTableMetadata: PxTableMetadata | null; isLoadingMetadata: boolean; hasLoadedDefaultSelection: boolean; + isChangingCodeList: boolean; selectedVBValues: SelectedVBValues[]; // TODO: Optimise here? Duplicate with props in VariableBox @@ -27,13 +30,18 @@ export function VariableList({ pxTableMetadata, isLoadingMetadata, hasLoadedDefaultSelection, + isChangingCodeList = false, selectedVBValues, handleCodeListChange, handleCheckboxChange, handleMixedCheckboxChange, }: VariableListProps) { return ( -
+
{!isLoadingMetadata && hasLoadedDefaultSelection && pxTableMetadata && diff --git a/packages/pxweb2/src/app/components/Selection/Selection.tsx b/packages/pxweb2/src/app/components/Selection/Selection.tsx index 37298a6f..bbe57486 100644 --- a/packages/pxweb2/src/app/components/Selection/Selection.tsx +++ b/packages/pxweb2/src/app/components/Selection/Selection.tsx @@ -31,8 +31,11 @@ function addSelectedCodeListToVariable( if (currentVariable) { newSelectedValues = selectedValuesArr.map((variable) => { if (variable.id === varId) { - variable.selectedCodeList = selectedItem.value; - variable.values = []; // Always reset values when changing codelist + return { + ...variable, + selectedCodeList: selectedItem.value, + values: [], // Always reset values when changing codelist + }; } return variable; @@ -81,6 +84,22 @@ function addValueToNewVariable( return newSelectedValues; } +async function getCodeListValues(id: string, lang: string): Promise { + let values: Value[] = []; + + await TableService.getTableCodeListById(id, lang) + .then((response) => { + response.values.forEach((value) => { + values = [...values, { code: value.code, label: value.label }]; + }); + }) + .catch((error) => { + throw new Error(error); + }); + + return values; +} + function removeValueOfVariable( selectedValuesArr: SelectedVBValues[], varId: string, @@ -232,15 +251,18 @@ export function Selection({ const { selectedVBValues, setSelectedVBValues } = useVariables(); const variables = useVariables(); const [errorMsg, setErrorMsg] = useState(''); - const [pxTableMetaToRender, setPxTableMetaToRender] = - // Metadata to render in the UI - useState(null); const { i18n, t } = useTranslation(); const { hasLoadedDefaultSelection } = useVariables(); const { isLoadingMetadata } = useVariables(); const { pxTableMetadata, setPxTableMetadata } = useVariables(); - + const [pxTableMetaToRender, setPxTableMetaToRender] = + // Metadata to render in the UI + useState(null); const [prevTableId, setPrevTableId] = useState(''); + const [isFadingVariableList, setIsFadingVariableList] = useState(false); + + // Needed to know when the language has changed, so we can reload the default selection + const [prevLang, setPrevLang] = useState(''); useEffect(() => { let shouldGetDefaultSelection = !hasLoadedDefaultSelection; @@ -249,11 +271,18 @@ export function Selection({ return; } - if (prevTableId === '' || prevTableId !== selectedTabId) { + // If the table has changed, or the language has changed, we need to reload the default selection + if ( + prevTableId === '' || + prevTableId !== selectedTabId || + prevLang !== i18n.resolvedLanguage + ) { variables.setHasLoadedDefaultSelection(false); shouldGetDefaultSelection = true; setPrevTableId(selectedTabId); + setPrevLang(i18n.resolvedLanguage || ''); } + if (isLoadingMetadata === false) { variables.setIsLoadingMetadata(true); } @@ -314,7 +343,7 @@ export function Selection({ setPxTableMetaToRender(structuredClone(pxTableMetadata)); } - function handleCodeListChange( + async function handleCodeListChange( selectedItem: SelectOption | undefined, varId: string, ) { @@ -322,10 +351,16 @@ export function Selection({ const currentVariableMetadata = pxTableMetaToRender?.variables.find( (variable) => variable.id === varId, ); - const currentVariable = prevSelectedValues.find( + const currentSelectedVariable = prevSelectedValues.find( (variable) => variable.id === varId, ); - const currentCodeList = currentVariable?.selectedCodeList; + const lang = i18n.resolvedLanguage; + + // No language, do nothing + if (lang === undefined) { + return; + } + const currentCodeList = currentSelectedVariable?.selectedCodeList; // No new selection made, do nothing if (!selectedItem || selectedItem.value === currentCodeList) { @@ -347,20 +382,33 @@ export function Selection({ const newMappedSelectedCodeList = mapCodeListToSelectOption(newSelectedCodeList); const newSelectedValues = addSelectedCodeListToVariable( - currentVariable, + currentSelectedVariable, prevSelectedValues, varId, newMappedSelectedCodeList, ); - updateAndSyncVBValues(newSelectedValues); + setIsFadingVariableList(true); - // TODO: This currently returns dummy data until we have the API call setup for it - const valuesForChosenCodeList: Value[] = getCodeListValues( + // Get the values for the chosen code list + const valuesForChosenCodeList: Value[] = await getCodeListValues( newMappedSelectedCodeList.value, - ); + lang, + ) + .finally(() => { + setIsFadingVariableList(false); + }) + .catch((error) => { + console.error( + 'Could not get values for code list: ' + + newMappedSelectedCodeList.value + + ' ' + + error, + ); + return []; + }); - if (pxTableMetaToRender === null || valuesForChosenCodeList.length < 1) { + if (valuesForChosenCodeList.length < 1) { return; } @@ -386,7 +434,10 @@ export function Selection({ }); }); - setPxTableMetaToRender(newPxTableMetaToRender); + // update the state + updateAndSyncVBValues(newSelectedValues); + setPxTableMetadata(newPxTableMetaToRender); + setPxTableMetaToRender(null); } const handleCheckboxChange = (varId: string, value: Value['code']) => { @@ -469,27 +520,13 @@ export function Selection({ variables.syncVariablesAndValues(selectedVBValues); } - const getCodeListValues = (id: string) => { - /* TODO: Implement querying the API */ - const dummyValues: Value[] = [ - { code: 'Dummy Code 1', label: 'Dummy Value 1' }, - { code: '01', label: '01 Stockholm county' }, - { code: 'Dummy Code 2', label: 'Dummy Value 2' }, - { code: 'Dummy Code 3', label: 'Dummy Value 3' }, - { code: 'Dummy Code 4', label: 'Dummy Value 4' }, - { code: 'Dummy Code 5', label: 'Dummy Value 5' }, - { code: 'Dummy Code 6', label: 'Dummy Value 6' }, - { code: 'Dummy Code 7', label: 'Dummy Value 7' }, - ]; - - return dummyValues; - }; const drawerFilter = ( = ({ children }) => { const selections: Array = []; const ids = variables.getUniqueIds(); ids.forEach((id) => { + const selectedCodeList = variables.getSelectedCodelistById(id); const selection: VariableSelection = { variableCode: id, valueCodes: variables.getSelectedValuesByIdSorted(id), }; + + // Add selected codelist to selection if it exists + if (selectedCodeList) { + selection.codeList = selectedCodeList; + } + selections.push(selection); }); diff --git a/packages/pxweb2/src/app/context/VariablesProvider.tsx b/packages/pxweb2/src/app/context/VariablesProvider.tsx index 56af3a68..b0c24fe3 100644 --- a/packages/pxweb2/src/app/context/VariablesProvider.tsx +++ b/packages/pxweb2/src/app/context/VariablesProvider.tsx @@ -8,6 +8,7 @@ export type VariablesContextType = { addSelectedValues: (variableId: string, values: string[]) => void; getSelectedValuesById: (variableId: string) => string[]; getSelectedValuesByIdSorted: (variableId: string) => string[]; + getSelectedCodelistById: (variableId: string) => string | undefined; getNumberOfSelectedValues: () => number; getUniqueIds: () => string[]; syncVariablesAndValues: (values: SelectedVBValues[]) => void; @@ -31,6 +32,7 @@ export const VariablesContext = createContext({ // eslint-disable-next-line @typescript-eslint/no-empty-function getSelectedValuesById: () => [], getSelectedValuesByIdSorted: () => [], + getSelectedCodelistById: () => undefined, // eslint-disable-next-line @typescript-eslint/no-empty-function getNumberOfSelectedValues: () => 0, // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -112,6 +114,20 @@ export const VariablesProvider: React.FC<{ children: React.ReactNode }> = ({ return sortedValues; }; + /** + * Get selected codelist for a given variable by it's id + * + * @param variableId + * @returns selected codelist for the given variable or undefined if not found + */ + const getSelectedCodelistById = (variableId: string) => { + const selectedCodelist = selectedVBValues?.find( + (item) => item.id === variableId, + )?.selectedCodeList; + + return selectedCodelist; + }; + const getNumberOfSelectedValues = () => { return variables.size; }; @@ -187,6 +203,7 @@ export const VariablesProvider: React.FC<{ children: React.ReactNode }> = ({ getNumberOfSelectedValues, getSelectedValuesById, getSelectedValuesByIdSorted, + getSelectedCodelistById, getUniqueIds, syncVariablesAndValues, toString, diff --git a/packages/pxweb2/src/mappers/TableSelectionResponseMapper.ts b/packages/pxweb2/src/mappers/TableSelectionResponseMapper.ts index bed6fff0..cabfe60d 100644 --- a/packages/pxweb2/src/mappers/TableSelectionResponseMapper.ts +++ b/packages/pxweb2/src/mappers/TableSelectionResponseMapper.ts @@ -1,15 +1,10 @@ import { SelectionResponse } from '@pxweb2/pxweb2-api-client'; import { SelectedVBValues } from '@pxweb2/pxweb2-ui'; -type VariableWithoutCodelist = Omit; -type VariableWithCodelistValue = VariableWithoutCodelist & { - selectedCodeList: string | undefined; -}; - export function mapTableSelectionResponse( response: SelectionResponse, -): VariableWithCodelistValue[] { - const selectedVBValues: VariableWithCodelistValue[] = response.selection.map( +): SelectedVBValues[] { + const selectedVBValues: SelectedVBValues[] = response.selection.map( (variable) => { return { id: variable.variableCode,