Skip to content

Commit

Permalink
feat: Updating subject fetching logic (M2-7035) (#506)
Browse files Browse the repository at this point in the history
* Updating subject fetching logic

I've added a new query that allows both full account participants and team members to fetch their own subject details.

I've also modified the logic for fetching the source and target subjects. In the admin panel, a temporary relation is only created if the subjects differ.
Since this relation is required for fetching the subjects, I conditionally fetch them based on this difference.

* Fix query enabled checks

Tanstack query depends on a proper boolean value (not boolean | undefined) to disable the query

* Simplify boolean checks

At this point in the code we've already narrowed the type, so we don't have to worry about `undefined`

* Change isLoading to isFetching

According to tanstack query docs, isLoading will be true even if th query is disabled, so we should use `fetchStatus` to determine if it is actively fetching data. `isFetching` is a convenience property for this.

See https://tanstack.com/query/v4/docs/framework/react/reference/useQuery
  • Loading branch information
sultanofcardio authored Aug 1, 2024
1 parent c6855b5 commit aaba54d
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 76 deletions.
15 changes: 15 additions & 0 deletions src/entities/subject/api/useGetMySubjectQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { QueryOptions, ReturnAwaited, subjectService, useBaseQuery } from '~/shared/api';

type FetchFn = typeof subjectService.getMySubjectByAppletId;
type Options<TData> = QueryOptions<FetchFn, TData>;

export const useGetMySubjectQuery = <TData = ReturnAwaited<FetchFn>>(
appletId: string,
options?: Options<TData>,
) => {
return useBaseQuery(
['mySubject', { appletId }],
() => subjectService.getMySubjectByAppletId({ appletId }),
options,
);
};
167 changes: 92 additions & 75 deletions src/features/TakeNow/lib/useTakeNowValidation.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from 'react';

import { AxiosError } from 'axios';

import { useValidateMultiInformantAssessmentQuery } from '~/entities/answer';
import { useAppletByIdQuery } from '~/entities/applet';
import { useSubjectQuery } from '~/entities/subject';
import { useGetMySubjectQuery } from '~/entities/subject/api/useGetMySubjectQuery';
import { useUserState } from '~/entities/user/model/hooks';
import { useWorkspaceAppletRespondent } from '~/entities/workspace';
import {
TakeNowParams,
TakeNowValidatedState,
Expand All @@ -23,7 +22,8 @@ export const useTakeNowValidation = ({
respondentId,
multiInformantAssessmentId,
}: TakeNowParams): TakeNowValidatedState => {
const [workspaceId, setWorkspaceId] = useState<string | null>();
const [fetchSourceSubject, setFetchSourceSubject] = useState<boolean>();
const [fetchTargetSubject, setFetchTargetSubject] = useState<boolean>();

const {
isError: isValidationError,
Expand All @@ -37,25 +37,25 @@ export const useTakeNowValidation = ({
});

const {
isError: isSourceSubjectError,
isError: isMySubjectError,
data: mySubjectData,
isLoading: isLoadingMySubject,
} = useGetMySubjectQuery(appletId);

const {
data: sourceSubjectData,
error: sourceSubjectError,
isLoading: isLoadingSourceSubject,
} = useSubjectQuery(sourceSubjectId);
isFetching: isLoadingSourceSubject,
} = useSubjectQuery(sourceSubjectId, {
enabled: fetchSourceSubject === true,
});

const {
isError: isTargetSubjectError,
data: targetSubjectData,
error: targetSubjectError,
isLoading: isLoadingTargetSubject,
} = useSubjectQuery(targetSubjectId);

const {
isError: isRespondentError,
data: respondentData,
isLoading: isLoadingRespondent,
} = useWorkspaceAppletRespondent(workspaceId ?? '', appletId, respondentId, {
enabled: !!workspaceId,
isFetching: isLoadingTargetSubject,
} = useSubjectQuery(targetSubjectId, {
enabled: fetchTargetSubject === true,
});

const {
Expand All @@ -65,11 +65,16 @@ export const useTakeNowValidation = ({
} = useAppletByIdQuery({ isPublic: false, appletId });

useEffect(() => {
if (!workspaceId && !isLoadingApplet && appletData) {
const localWorkspaceId = appletData?.data.result.ownerId;
setWorkspaceId(localWorkspaceId || null);
if (mySubjectData?.data?.result) {
const { id: currentUserSubjectId } = mySubjectData.data.result;

setFetchSourceSubject(currentUserSubjectId !== sourceSubjectId);

setFetchTargetSubject(
currentUserSubjectId !== targetSubjectId && sourceSubjectId !== targetSubjectId,
);
}
}, [appletData, isLoadingApplet, workspaceId]);
}, [mySubjectData, sourceSubjectId, targetSubjectId]);

const { t } = useCustomTranslation();
const { user } = useUserState();
Expand Down Expand Up @@ -110,6 +115,7 @@ export const useTakeNowValidation = ({
error: t('takeNow.invalidApplet'),
});
} else if (validationError.response?.status === 422) {
/* eslint-disable @typescript-eslint/no-explicit-any */
const axiosError = validationError as AxiosError<any, any>;
const param = axiosError.response?.data?.result?.[0]?.path?.[1] as string | undefined;

Expand Down Expand Up @@ -165,81 +171,92 @@ export const useTakeNowValidation = ({
});
}

if (isLoadingTargetSubject || isLoadingSourceSubject) {
if (isLoadingMySubject) {
return loadingState;
}

if (isMySubjectError || !mySubjectData || !mySubjectData?.data?.result) {
// If we're unable to fetch the subject ID for the current user, we can't start the multi-informant flow
// eslint-disable-next-line no-console
console.error('Unable to fetch subject ID for current user');
return errorState(null);
}

const {
id: currentUserSubjectId,
nickname: currentUserSubjectNickname,
secretUserId: currentUserSecretUserId,
} = mySubjectData.data.result;

if (
fetchSourceSubject === undefined ||
isLoadingSourceSubject ||
fetchTargetSubject === undefined ||
isLoadingTargetSubject
) {
return loadingState;
}

const subjectPermissionError =
(isTargetSubjectError &&
targetSubjectError &&
'status' in targetSubjectError.response &&
targetSubjectError.response.status === 403) ||
(isSourceSubjectError &&
sourceSubjectError &&
'status' in sourceSubjectError.response &&
sourceSubjectError.response.status === 403);

if (subjectPermissionError) {
// The logged-in user doesn't have permission to fetch the subject details,
// so they probably don't have permission to perform the activity
let sourceSubjectNickname: string | null;
let targetSubjectNickname: string | null;
let sourceSecretUserId: string;
let targetSecretUserId: string;

if (!fetchSourceSubject) {
sourceSubjectNickname = currentUserSubjectNickname;
sourceSecretUserId = currentUserSecretUserId;
} else if (
sourceSubjectError &&
'status' in sourceSubjectError.response &&
sourceSubjectError.response.status === 403
) {
return errorState({
type: 'invalidRespondent',
error: t('takeNow.invalidRespondent'),
});
}

if (
isTargetSubjectError ||
!targetSubjectData?.data?.result ||
targetSubjectData.data.result.appletId !== appletId ||
isSourceSubjectError ||
} else if (
!sourceSubjectData?.data?.result ||
sourceSubjectData.data.result.appletId !== appletId
) {
return errorState({
type: 'invalidSubject',
error: t('takeNow.invalidSubject'),
});
} else {
sourceSubjectNickname = sourceSubjectData.data.result.nickname;
sourceSecretUserId = sourceSubjectData.data.result.secretUserId;
}

const { nickname: targetSubjectNickname, secretUserId: targetSecretUserId } =
targetSubjectData.data.result;

const { nickname: sourceSubjectNickname, secretUserId: sourceSecretUserId } =
sourceSubjectData.data.result;

// At this point we have the subject, applet, and activity data
// We can't fetch the workspace roles before we have the applet data
// because that's where we get the workspace ID

if (workspaceId === undefined) {
return loadingState;
}

if (workspaceId === null) {
if (!fetchTargetSubject) {
if (currentUserSubjectId === targetSubjectId) {
targetSubjectNickname = currentUserSubjectNickname;
targetSecretUserId = currentUserSecretUserId;
} else {
targetSubjectNickname = sourceSubjectNickname;
targetSecretUserId = sourceSecretUserId;
}
} else if (
targetSubjectError &&
'status' in targetSubjectError.response &&
targetSubjectError.response.status === 403
) {
return errorState({
type: 'common_loading_error',
error: t('common.loadingError'),
type: 'invalidRespondent',
error: t('takeNow.invalidRespondent'),
});
} else if (
!targetSubjectData?.data?.result ||
targetSubjectData.data.result.appletId !== appletId
) {
return errorState({
type: 'invalidSubject',
error: t('takeNow.invalidSubject'),
});
} else {
targetSubjectNickname = targetSubjectData.data.result.nickname;
targetSecretUserId = targetSubjectData.data.result.secretUserId;
}

if (isLoadingRespondent) {
return loadingState;
}

if (isRespondentError || !respondentData || !respondentData?.data?.result) {
// If we're unable to fetch the subject ID for the current user, we can't start the multi-informant flow
// eslint-disable-next-line no-console
console.error('Unable to fetch subject ID for current user');
return errorState(null);
}

const {
subjectId: currentUserSubjectId,
nickname: currentUserSubjectNickname,
secretUserId: currentUserSecretUserId,
} = respondentData.data.result;

// Finally, determine if this is an activity or activity flow for analytics.
const { activities, activityFlows } = appletData.data.result;
const activityId = activities.some(({ id }) => id === startActivityOrFlow)
Expand Down
13 changes: 12 additions & 1 deletion src/shared/api/services/subject.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { axiosService } from '~/shared/api';
import { GetSubjectByIdPayload, GetSubjectSuccessResponse } from '~/shared/api/types/subject';
import {
GetMySubjectByAppletIdPayload,
GetMySubjectSuccessResponse,
GetSubjectByIdPayload,
GetSubjectSuccessResponse,
} from '~/shared/api/types/subject';

function subjectService() {
return {
getSubjectById(payload: GetSubjectByIdPayload) {
return axiosService.get<GetSubjectSuccessResponse>(`/subjects/${payload.subjectId}`);
},

getMySubjectByAppletId(payload: GetMySubjectByAppletIdPayload) {
return axiosService.get<GetMySubjectSuccessResponse>(
`/users/me/subjects/${payload.appletId}`,
);
},
};
}

Expand Down
8 changes: 8 additions & 0 deletions src/shared/api/types/subject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ export interface GetSubjectByIdPayload {
subjectId: string;
}

export interface GetMySubjectByAppletIdPayload {
appletId: string;
}

export type SubjectDTO = {
secretUserId: string;
appletId: string;
nickname: string | null;
tag: string | null;
lastSeen: string | null;
id: string;
userId: string | null;
};

export type GetSubjectSuccessResponse = BaseSuccessResponse<SubjectDTO>;

export type GetMySubjectSuccessResponse = GetSubjectSuccessResponse;

0 comments on commit aaba54d

Please sign in to comment.