Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | import { uniqueId } from 'lodash-es'; import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState, type Dispatch, type ReactElement, type SetStateAction, } from 'react'; import { TokenType } from '@amalia/amalia-lang/tokens/types'; import { qsStringify } from '@amalia/core/http/client'; import { useBoolState, useShallowObjectMemo } from '@amalia/ext/react/hooks'; import { useNavigate, useQueryString } from '@amalia/ext/react-router-dom'; import { assert } from '@amalia/ext/typescript'; import { useDesignerLibrary } from '../hooks/use-designer-library'; import { isDrawerTokenDirty, isEditingDrawerToken, type DrawerToken, type DrawerTokenKey, type DrawerTokenWithKey, type EditableDrawerTokenTypes, type EditingDrawerToken, type ReadonlyDrawerToken, type ReadonlyDrawerTokenTypes, } from './PlanHubRuleDesignerDrawer.types'; export const buildTokenKey = (token: DrawerToken): DrawerTokenKey => `${token.tokenType}:${('id' in token && token.id) || ('identifier' in token && token.identifier) || uniqueId()}`; export const buildTokenIdentifier = ({ definitionMachineName, propertyMachineName, }: { definitionMachineName: string; propertyMachineName: string; }) => `${definitionMachineName}.${propertyMachineName}`; interface PlanHubRuleDesignerDrawerContextValues { activeDrawerKey: DrawerTokenKey | null; dirtyTokensCount: number; isAtLeastOneTokenSavedSinceLastCompute: boolean; isCurrentTokenOpenFn: (token: EditingDrawerToken | ReadonlyDrawerToken) => boolean; isDrawerOpen: boolean; closeCleanTokens: () => void; createToken: (pastTokenKey: DrawerTokenKey, newTokenId: string) => void; openTokenInDrawer: (clickedToken: DrawerToken) => void; removeToken: (tokenToRemoveKey: DrawerTokenKey) => void; saveToken: (tokenKey: DrawerTokenKey) => void; setActiveDrawerKey: Dispatch<SetStateAction<DrawerTokenKey | null>>; setAtLeastOneTokenSavedSinceLastComputeFalse: () => void; setAtLeastOneTokenSavedSinceLastComputeTrue: () => void; setDirtyToken: (tokenKey: DrawerTokenKey, dirty: boolean) => void; setTokenStack: Dispatch<SetStateAction<DrawerTokenWithKey[]>>; tokenStack: DrawerTokenWithKey[]; } const PlanHubRuleDesignerDrawerContext = createContext<PlanHubRuleDesignerDrawerContextValues | null>(null); export const PlanHubRuleDesignerDrawerContextProvider = memo(function PlanHubRuleDesignerDrawerContextProvider({ children, }: { readonly children: ReactElement; }) { const [tokenStack, setTokenStack] = useState<DrawerTokenWithKey[]>([]); const [activeDrawerKey, setActiveDrawerKey] = useState<DrawerTokenKey | null>(null); const { isAtLeastOneTokenSavedSinceLastCompute, setAtLeastOneTokenSavedSinceLastComputeTrue, setAtLeastOneTokenSavedSinceLastComputeFalse, } = useBoolState(false, 'atLeastOneTokenSavedSinceLastCompute'); const { tokenToOpen, ...otherQueryStringParams } = useQueryString(); const { allTokenIds, isPending } = useDesignerLibrary(); // Filter out all the elements that are no longer linked to a token, notably for // the case where an element have been deleted. // We couldn't do that imperatively because if the user deletes something in another // tab and react query refreshes the library, it could crash the drawer. const tokenStackFiltered = useMemo( // Early return for everything without an id (readonly, creating, duplicating...). () => tokenStack.filter((t) => ('id' in t ? allTokenIds.includes(t.id) : true)), [allTokenIds, tokenStack], ); // If the currently active token has been deleted (it's no longer in the tokenStackFiltered), put // the selector back on the first element. // add !isPending to avoid deleting the activeDrawerKey when a token is open from the url and the library is not loaded. useEffect(() => { const isActiveTokenInTokenStack = activeDrawerKey && !tokenStackFiltered.map((t) => t.key).includes(activeDrawerKey); if (!isPending && isActiveTokenInTokenStack) { setActiveDrawerKey(tokenStackFiltered.at(0)?.key ?? null); } }, [tokenStackFiltered, activeDrawerKey, isPending]); const onRemoveToken = useCallback( (tokenToRemoveKey: DrawerTokenKey) => { // Using the tokenStackFiltered here, or else we might select a deleted element // at the setActiveDrawerKey. const newTokenStack = tokenStackFiltered.filter((token) => token.key !== tokenToRemoveKey); setTokenStack(newTokenStack); setActiveDrawerKey(newTokenStack.at(0)?.key ?? null); }, [setActiveDrawerKey, setTokenStack, tokenStackFiltered], ); const onOpenToken = useCallback((clickedToken: DrawerToken) => { const key = buildTokenKey(clickedToken); setTokenStack((currentTokenStack) => { const isAlreadyOpened = currentTokenStack.some((token) => key === token.key); return isAlreadyOpened ? currentTokenStack : [{ ...clickedToken, key }, ...currentTokenStack]; }); // Set the focus on the element. setActiveDrawerKey(key); }, []); const onCreateToken = useCallback( (pastTokenKey: DrawerTokenKey, newTokenId: string) => { const pastToken = tokenStackFiltered.find((token) => pastTokenKey === token.key); if (!pastToken) { return; } const tokenType = pastToken.tokenType as EditableDrawerTokenTypes; const newKey = buildTokenKey({ tokenType, id: newTokenId } as DrawerToken); setAtLeastOneTokenSavedSinceLastComputeTrue(); setTokenStack((stack) => stack.map((token) => token.key === pastTokenKey ? { key: newKey, id: newTokenId, isDirty: false, tokenType } : token, ), ); setActiveDrawerKey(newKey); }, [tokenStackFiltered, setAtLeastOneTokenSavedSinceLastComputeTrue], ); const onSaveToken = useCallback( (tokenKey: DrawerTokenKey) => { setAtLeastOneTokenSavedSinceLastComputeTrue(); setTokenStack((stack) => stack.map((token) => (token.key === tokenKey && 'id' in token ? { ...token, isDirty: false } : token)), ); }, [setAtLeastOneTokenSavedSinceLastComputeTrue], ); const setDirtyToken = useCallback((tokenKey: DrawerTokenKey, dirty: boolean) => { setTokenStack((stack) => stack.map((token) => // Only editing tokens can come and go from clean to dirty, created are dirty until saved. isEditingDrawerToken(token) && token.key === tokenKey ? { ...token, isDirty: dirty } : token, ), ); }, []); const dirtyTokensCount = tokenStackFiltered.filter(isDrawerTokenDirty).length; const onCloseCleanTokens = useCallback(() => { setTokenStack((stack) => stack.filter(isDrawerTokenDirty)); }, []); const isCurrentTokenOpenFn = useCallback( (token: EditingDrawerToken | ReadonlyDrawerToken) => buildTokenKey(token) === activeDrawerKey, [activeDrawerKey], ); const contextValues: PlanHubRuleDesignerDrawerContextValues = useShallowObjectMemo({ activeDrawerKey, dirtyTokensCount, isAtLeastOneTokenSavedSinceLastCompute, isCurrentTokenOpenFn, isDrawerOpen: !!tokenStackFiltered.length, closeCleanTokens: onCloseCleanTokens, createToken: onCreateToken, openTokenInDrawer: onOpenToken, removeToken: onRemoveToken, saveToken: onSaveToken, setActiveDrawerKey, setAtLeastOneTokenSavedSinceLastComputeFalse, setAtLeastOneTokenSavedSinceLastComputeTrue, setDirtyToken, setTokenStack, tokenStack: tokenStackFiltered, }); const navigate = useNavigate(); // useEffect to handle the tokenToOpen from the URL (we open it, and we reset the url) useEffect(() => { if (tokenToOpen) { const [tokenType, tokenId] = tokenToOpen.split(':'); if (tokenType === TokenType.PROPERTY || tokenType === TokenType.VIRTUAL_PROPERTY) { onOpenToken({ tokenType: tokenType as ReadonlyDrawerTokenTypes, identifier: tokenId }); } else { onOpenToken({ tokenType: tokenType as EditableDrawerTokenTypes, id: tokenId }); } // remove the tokenToOpen from the URL navigate({ search: qsStringify({ ...otherQueryStringParams }) }, { replace: true }); } }, [navigate, onOpenToken, otherQueryStringParams, tokenToOpen]); return ( <PlanHubRuleDesignerDrawerContext.Provider value={contextValues}> {children} </PlanHubRuleDesignerDrawerContext.Provider> ); }); export const useDesignerDrawerContext = () => { const context = useContext(PlanHubRuleDesignerDrawerContext); assert(context, 'useDesignerDrawerContext must be used within a PlanHubRuleDesignerDrawerContextProvider'); return context; }; |