diff --git a/src/common/api/narrativeService.ts b/src/common/api/narrativeService.ts index 936dc624..7eef32df 100644 --- a/src/common/api/narrativeService.ts +++ b/src/common/api/narrativeService.ts @@ -8,12 +8,14 @@ const narrativeService = dynamicService({ }); export interface NarrativeServiceParams { + copyNarrative: { nameNew: string; workspaceRef: string; workspaceId: number }; getStatus: void; renameNarrative: { nameNew: string; narrativeRef: string }; restoreNarrative: { objId: number; version: number; wsId: number }; } interface NarrativeServiceResults { + copyNarrative: unknown; getStatus: { state: string }[]; renameNarrative: unknown; restoreNarrative: unknown; @@ -21,6 +23,26 @@ interface NarrativeServiceResults { export const narrativeServiceApi = baseApi.injectEndpoints({ endpoints: (builder) => ({ + copyNarrative: builder.mutation< + NarrativeServiceResults['copyNarrative'], + NarrativeServiceParams['copyNarrative'] + >({ + query: ({ nameNew, workspaceRef, workspaceId }) => + narrativeService({ + method: 'NarrativeService.copy_narrative', + params: [{ newName: nameNew, workspaceRef, workspaceId }], + }), + }), + getStatus: builder.query< + NarrativeServiceResults['getStatus'], + NarrativeServiceParams['getStatus'] + >({ + query: () => + narrativeService({ + method: 'NarrativeService.status', + params: [], + }), + }), renameNarrative: builder.mutation< NarrativeServiceResults['renameNarrative'], NarrativeServiceParams['renameNarrative'] @@ -41,18 +63,8 @@ export const narrativeServiceApi = baseApi.injectEndpoints({ params: [{ ver: version, objid: objId, wsid: wsId }], }), }), - getStatus: builder.query< - NarrativeServiceResults['getStatus'], - NarrativeServiceParams['getStatus'] - >({ - query: () => - narrativeService({ - method: 'NarrativeService.status', - params: [], - }), - }), }), }); -export const { getStatus, renameNarrative, restoreNarrative } = +export const { copyNarrative, getStatus, renameNarrative, restoreNarrative } = narrativeServiceApi.endpoints; diff --git a/src/features/navigator/NarrativeControl/Copy.tsx b/src/features/navigator/NarrativeControl/Copy.tsx index b80d0306..6ef8426e 100644 --- a/src/features/navigator/NarrativeControl/Copy.tsx +++ b/src/features/navigator/NarrativeControl/Copy.tsx @@ -1,6 +1,10 @@ /* NarrativeControl/Copy */ import { FC } from 'react'; import { useForm } from 'react-hook-form'; +import toast from 'react-hot-toast'; +import { isKBaseBaseQueryError } from '../../../common/api/utils/common'; +import { parseError } from '../../../common/api/utils/parseError'; +import { copyNarrative } from '../../../common/api/narrativeService'; import { Button } from '../../../common/components'; import { inputRegisterFactory, @@ -8,10 +12,8 @@ import { } from '../../../common/components/Input.common'; import { Input } from '../../../common/components/Input'; import { useAppDispatch } from '../../../common/hooks'; -import { TODOAddLoadingState } from '../common'; -import { useNarrativeServiceStatus } from '../hooks'; -import { copyNarrative } from '../navigatorSlice'; -import { ControlProps } from './common'; +import { copyNarrative as copyAction, setLoading } from '../navigatorSlice'; +import { ControlProps, ErrorMessage } from './common'; export interface CopyValues { narrativeCopyName: string; @@ -22,24 +24,50 @@ export interface CopyProps extends ControlProps { } export const Copy: FC = ({ narrativeDoc, modalClose, version }) => { + /* hooks */ const dispatch = useAppDispatch(); - useNarrativeServiceStatus(); const { formState, getValues, register } = useForm({ defaultValues: { narrativeCopyName: `${narrativeDoc.narrative_title} - Copy`, }, mode: 'all', }); + const [copyTrigger] = copyNarrative.useMutation(); + /* derived values */ const inputRegister = inputRegisterFactory({ formState, register, }); + const { access_group: wsId, obj_id: objId } = narrativeDoc; + const errors = formState.errors; + const errorEntries = Object.entries(errors); + const formInvalid = errorEntries.length > 0; + /* copy narrative callback */ const copyNarrativeHandler = async () => { const { narrativeCopyName: name } = getValues(); - await TODOAddLoadingState(); - dispatch(copyNarrative({ wsId: narrativeDoc.access_group, name, version })); + const message = `Copy ${wsId}/${objId}/${version} as ${name}.`; modalClose(); + dispatch(copyAction({ wsId: narrativeDoc.access_group, name, version })); + try { + await copyTrigger({ + nameNew: name, + workspaceRef: `${wsId}/${objId}/${version}`, + workspaceId: wsId, + }).unwrap(); + dispatch(setLoading(false)); + } catch (err) { + if (!isKBaseBaseQueryError(err)) { + console.error({ err }); // eslint-disable-line no-console + toast(ErrorMessage({ err })); + return; + } + toast(ErrorMessage({ err: parseError(err) })); + dispatch(setLoading(false)); + return; + } + toast(message); }; + /* Copy component */ return ( <>

@@ -49,16 +77,31 @@ export const Copy: FC = ({ narrativeDoc, modalClose, version }) => {

Enter a name for the new Narrative.

+ {formInvalid ? ( + <> + Errors: +
    + {Object.entries(errors).map(([name, err]) => ( +
  • {err.message}
  • + ))} +
+ + ) : ( + <> + )} New Narrative Title} + maxLength={MAX_WS_METADATA_VALUE_SIZE} {...inputRegister('narrativeCopyName', { maxLength: { value: MAX_WS_METADATA_VALUE_SIZE, - message: 'too long', + message: 'The selected name is too long.', }, })} /> - +
diff --git a/src/features/navigator/NarrativeControl/Delete.tsx b/src/features/navigator/NarrativeControl/Delete.tsx index 18ec5733..76f5cf39 100644 --- a/src/features/navigator/NarrativeControl/Delete.tsx +++ b/src/features/navigator/NarrativeControl/Delete.tsx @@ -15,20 +15,23 @@ import { deleteNarrative, loading, setLoading } from '../navigatorSlice'; import { ControlProps, ErrorMessage } from './common'; export const Delete: FC = ({ narrativeDoc, modalClose }) => { + /* hooks */ const dispatch = useAppDispatch(); const loadState = useAppSelector(loading); const params = useAppSelector(getParams); const navigate = useNavigate(); const [userConfirmation, setUserConfirmation] = useState(false); const [deleteTrigger] = deleteWorkspace.useMutation(); - - const wsId = narrativeDoc.access_group; useEffect(() => { if (loadState) return; if (!userConfirmation) return; }); + /* derived values */ + const wsId = narrativeDoc.access_group; const message = `Deleted narrative ${wsId}.`; + + /* delete narrative callback */ const deleteNarrativeHandler = async () => { setUserConfirmation(true); modalClose(); @@ -49,6 +52,7 @@ export const Delete: FC = ({ narrativeDoc, modalClose }) => { toast(message); navigate(generatePathWithSearchParams('/narratives', params)); }; + /* Delete component */ return ( <>

Delete Narrative?

diff --git a/src/features/navigator/NarrativeControl/Rename.tsx b/src/features/navigator/NarrativeControl/Rename.tsx index cfdaa362..ad871261 100644 --- a/src/features/navigator/NarrativeControl/Rename.tsx +++ b/src/features/navigator/NarrativeControl/Rename.tsx @@ -66,6 +66,7 @@ export const Rename: FC<{ } toast(message); }; + /* Rename component */ return ( <>

Rename Narrative

diff --git a/src/features/navigator/Navigator.test.tsx b/src/features/navigator/Navigator.test.tsx index ee1c416c..252d9338 100644 --- a/src/features/navigator/Navigator.test.tsx +++ b/src/features/navigator/Navigator.test.tsx @@ -89,13 +89,17 @@ describe('The component...', () => { testStore.dispatch(baseApi.util.resetApiState()); }); - test('renders.', () => { - const { container } = render( - - - - - + test('renders.', async () => { + const { container } = await waitFor(() => + render( + + + + + + + + ) ); expect(container).toBeTruthy(); expect(container.querySelector('section.navigator')).toBeInTheDocument(); diff --git a/src/features/navigator/Navigator.tsx b/src/features/navigator/Navigator.tsx index 6ce6a1ac..6f604142 100644 --- a/src/features/navigator/Navigator.tsx +++ b/src/features/navigator/Navigator.tsx @@ -25,7 +25,10 @@ import { normalizeVersion, searchParams, } from './common'; -import { useNarratives } from './hooks'; +import { + useNarratives, + //useNarrativeServiceStatus // See below +} from './hooks'; import { loading, navigatorSelected, @@ -210,6 +213,10 @@ const Navigator: FC = () => { term: search, username, }); + /* This causes tests to hang which means there is probably a bug in the way + dynamic services are handled. + // useNarrativeServiceStatus(); + */ const items = useAppSelector(narrativeDocs); const narrativeSelected = getNarrativeSelected({ id, obj, verRaw, items }); // hooks that update state diff --git a/src/features/navigator/navigatorSlice.ts b/src/features/navigator/navigatorSlice.ts index 9a92ad5b..40abacf2 100644 --- a/src/features/navigator/navigatorSlice.ts +++ b/src/features/navigator/navigatorSlice.ts @@ -62,9 +62,9 @@ export const navigatorSlice = createSlice({ state, action: PayloadAction<{ name: string; version: number; wsId: number }> ) => { - const { name, version, wsId } = action.payload; - const message = `Copy version ${version} of ${wsId} with name ${name}.`; - console.log(message); // eslint-disable-line no-console + // For now, wait until the page refreshes to reflect the changes. + state.synchronizedLast = Date.now(); + state.synchronized = false; }, deleteNarrative: (state, action: PayloadAction<{ wsId: number }>) => { const { wsId } = action.payload;