Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add By Participant view (M2-7798) #1938

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/modules/Dashboard/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,14 @@ export const getAppletTargetSubjectActivitiesApi = (
signal,
});

export const getAppletRespondentSubjectActivitiesApi = (
{ appletId, subjectId }: GetSubjectActivitiesParams,
signal?: AbortSignal,
): Promise<AxiosResponse<AppletParticipantActivitiesResponse>> =>
authApiClient.get(`/activities/applet/${appletId}/respondent/${subjectId}`, {
signal,
});

export const createTemporaryMultiInformantRelationApi = (
{ subjectId, sourceSubjectId }: CreateTemporaryMultiInformantRelation,
signal?: AbortSignal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,27 @@ import {
variables,
} from 'shared/styles';

export const StyledActivityListItem = styled(StyledFlexTopCenter)`
export const StyledActivityListItem = styled(StyledFlexTopCenter)(
({ onClick }: { onClick?: () => void }) => `
flex-wrap: wrap;
padding: ${theme.spacing(1.5)};
gap: ${theme.spacing(0.8, 4.8)};
border: ${variables.borderWidth.md} solid ${variables.palette.surface_variant};
border-radius: ${variables.borderRadius.lg2};
background-color: ${variables.palette.surface};
transition: ${variables.transitions.bgColor};
cursor: pointer;

&:hover,
&:focus {
background-color: ${variables.palette.on_surface_variant_alfa8};
${
onClick &&
`cursor: pointer;

&:hover,
&:focus {
background-color: ${variables.palette.on_surface_variant_alfa8};
}`
}
`;
`,
);

export const StyledActivityName = styled(StyledTitleLargish)`
${ellipsisTextCss}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const ActivityListItem = ({
onClick,
children,
}: ActivityListItemProps) => (
<StyledActivityListItem as="button" onClick={onClick}>
<StyledActivityListItem as={onClick ? 'a' : 'div'} onClick={onClick}>
<StyledFlexTopCenter sx={{ gap: 0.8 }}>
<StyledActivityThumbnailContainer sx={{ width: '5.6rem', height: '5.6rem', mr: 0.8 }}>
{isFlow ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { DataExportPopup } from 'modules/Dashboard/features/Respondents/Popups';
import { useTakeNowModal } from 'modules/Dashboard/components/TakeNowModal/TakeNowModal';
import { ActivityAssignDrawer, ActivityUnassignDrawer } from 'modules/Dashboard/components';
import { EditablePerformanceTasks } from 'modules/Builder/features/Activities/Activities.const';
import { RespondentDetails } from 'modules/Dashboard/types';

import { UseAssignmentsTabProps } from './AssignmentsTab.types';

Expand Down Expand Up @@ -77,6 +78,14 @@ export const useAssignmentsTab = ({
const handleCloseDrawer = (shouldRefetch?: boolean) => {
setShowActivityAssign(false);
setShowActivityUnassign(false);

// Allow drawer to transition out before clearing activity/flow to prevent unintended
// rendering of possible empty state before drawer closes
setTimeout(() => {
setSelectedActivityOrFlow(undefined);
setSelectedTargetSubjectId(undefined);
}, 300);

if (shouldRefetch) handleRefetch?.();
};

Expand Down Expand Up @@ -146,10 +155,15 @@ export const useAssignmentsTab = ({
[appletId, navigate],
);

const onClickAssign = useCallback((activityOrFlow?: ParticipantActivityOrFlow) => {
if (activityOrFlow) setSelectedActivityOrFlow(activityOrFlow);
setShowActivityAssign(true);
}, []);
const onClickAssign = useCallback(
(activityOrFlow?: ParticipantActivityOrFlow, targetSubjectArg?: RespondentDetails) => {
if (activityOrFlow) setSelectedActivityOrFlow(activityOrFlow);
setSelectedTargetSubjectId(targetSubjectArg?.id ?? targetSubject?.id);

setShowActivityAssign(true);
},
[targetSubject],
);

const getActionsMenu = useCallback(
(activityOrFlow: ParticipantActivityOrFlow) => {
Expand All @@ -174,17 +188,23 @@ export const useAssignmentsTab = ({
const isAssignable =
status === ActivityAssignmentStatus.Active || status === ActivityAssignmentStatus.Inactive;
const isTargetTeamMember = targetSubject?.tag === 'Team';
const isAssigned = !!assignments?.some((a) => a.targetSubject.id === targetSubject?.id);
const isAssignDisplayed = canAssign && !autoAssign;
const isAssigned = !!assignments?.some(
(a) =>
a.targetSubject.id === targetSubject?.id ||
a.respondentSubject.id === respondentSubject?.id,
);
const isAssignDisplayed =
canAssign && (!autoAssign || status === ActivityAssignmentStatus.Hidden);
const isAssignDisabled = !isAssignable || isTargetTeamMember;
const isUnassignDisplayed = canAssign && isAssignable && (autoAssign || isAssigned);

let assignTooltip: string | undefined;
if (isTargetTeamMember) {
assignTooltip = t('assignToTeamMemberTooltip');
} else if (!isAssignable) {
if (status === ActivityAssignmentStatus.Hidden) {
assignTooltip = isFlow
? t('assignFlowDisabledTooltip')
: t('assignActivityDisabledTooltip');
} else if (isTargetTeamMember) {
assignTooltip = t('assignToTeamMemberTooltip');
}

const showDivider =
Expand All @@ -208,15 +228,15 @@ export const useAssignmentsTab = ({
disabled: !id,
icon: <Svg id="export" />,
title: t('exportData'),
isDisplayed: canAccessData,
isDisplayed: canAccessData && !!targetSubject,
},
{
'data-testid': `${dataTestId}-assign`,
action: () => onClickAssign(activityOrFlow),
icon: <Svg id="file-plus" />,
title: isFlow ? t('assignFlow') : t('assignActivity'),
isDisplayed: canAssign && !autoAssign,
disabled: !isAssignable || isTargetTeamMember,
isDisplayed: isAssignDisplayed,
disabled: isAssignDisabled,
tooltip: assignTooltip,
},
{
Expand Down Expand Up @@ -312,7 +332,7 @@ export const useAssignmentsTab = ({
}
activityFlowId={selectedActivityOrFlow?.isFlow ? selectedActivityOrFlow.id : undefined}
respondentSubjectId={respondentSubject?.id}
targetSubjectId={targetSubject?.id}
targetSubjectId={selectedTargetSubjectId}
/>

<ActivityUnassignDrawer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,106 @@
import { AssignmentsTab } from '../AssignmentsTab';
import { useCallback, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';

const ByParticipant = () => <AssignmentsTab />;
import { useAsync } from 'shared/hooks';
import { getAppletRespondentSubjectActivitiesApi } from 'api';
import { users } from 'redux/modules';
import { ActionsMenu, Spinner } from 'shared/components';

import { AssignmentsTab, useAssignmentsTab } from '../AssignmentsTab';
import { EmptyState } from './EmptyState';
import { ActivitiesList } from '../ActivitiesList';
import { ActivityListItem } from '../ActivityListItem';

const dataTestId = 'participant-details-about-participant';

const ByParticipant = () => {
const { t } = useTranslation('app');
const { appletId, subjectId } = useParams();
const { useSubject, useSubjectStatus } = users;
const isLoadingSubject = useSubjectStatus() !== 'success';
const { result: respondentSubject } = useSubject() ?? {};

const {
execute: fetchActivities,
isLoading: isLoadingActivities,
value: fetchedActivities,
} = useAsync(getAppletRespondentSubjectActivitiesApi, { retainValue: true });

const activities = fetchedActivities?.data.result ?? [];

const handleRefetch = useCallback(() => {
// Avoid fetching activities for respondent if respondent is a limited account
if (!appletId || !subjectId || !respondentSubject?.userId) return;

fetchActivities({ appletId, subjectId });
}, [appletId, fetchActivities, respondentSubject?.userId, subjectId]);

const {
getActionsMenu,
onClickAssign,
isLoading: isLoadingHook,
modals,
} = useAssignmentsTab({ appletId, respondentSubject, handleRefetch, dataTestId });

/*
TODO: Handler for navigating to data when card is expanded
https://mindlogger.atlassian.net/browse/M2-7921
const handleClickNavigateToData = (activityOrFlow: ParticipantActivityOrFlow, targetSubject: RespondentDetails) => {
if (!respondentSubject) return;

onClickNavigateToData(activityOrFlow, targetSubject.id);
};
*/

useEffect(() => {
handleRefetch();
}, [handleRefetch]);

const isLoading = isLoadingSubject || isLoadingActivities || isLoadingHook;

return (
<AssignmentsTab>
{isLoading && <Spinner />}

{!isLoading && !activities.length && (
<EmptyState onClickAssign={onClickAssign} isLimitedAccount={!respondentSubject?.userId} />
)}

{!!activities.length && (
<ActivitiesList
title={t('participantDetails.activitiesAndFlows')}
count={fetchedActivities?.data.count ?? 0}
>
{activities.map((activity, index) => (
<ActivityListItem key={activity.id} activityOrFlow={activity}>
<ActionsMenu
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: -6, horizontal: 'right' }}
buttonColor="secondary"
menuItems={getActionsMenu(activity)}
data-testid={`${dataTestId}-${index}`}
/>

{/* TODO: Add expand/collapse button
https://mindlogger.atlassian.net/browse/M2-7921 */}
</ActivityListItem>
))}

{/* TODO: Add lazy load button
https://mindlogger.atlassian.net/browse/M2-7827 */}
</ActivitiesList>
)}

{/* TODO: Add deleted entries
https://mindlogger.atlassian.net/browse/M2-7827
<ActivitiesList title={t('deleted')} count={deleted.length}>
{deleted.map(…)}
</ActivitiesList> */}

{modals}
</AssignmentsTab>
);
};

export default ByParticipant;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Button } from '@mui/material';
import { useTranslation } from 'react-i18next';

import { Svg } from 'shared/components';
import { StyledFlexAllCenter, StyledFlexColumn, StyledHeadline, variables } from 'shared/styles';

import { EmptyStateProps } from './EmptyState.types';

export const EmptyState = ({ onClickAssign, isLimitedAccount }: EmptyStateProps) => {
const { t } = useTranslation('app', { keyPrefix: 'participantDetails' });

return (
<StyledFlexAllCenter sx={{ flexDirection: 'column', flex: 1, m: 'auto', textAlign: 'center' }}>
<StyledFlexColumn sx={{ alignItems: 'center', gap: 1.6, maxWidth: '50.7rem' }}>
<Svg id="by-participant" width="80" height="80" fill={variables.palette.outline} />
<StyledHeadline as="h2" sx={{ color: variables.palette.outline, m: 0 }}>
{isLimitedAccount ? t('byParticipantEmptyLimitedAccount') : t('byParticipantEmpty')}
</StyledHeadline>
</StyledFlexColumn>
{!isLimitedAccount && (
<Button variant="contained" color="primary" onClick={onClickAssign} sx={{ mt: 2.4 }}>
{t('assignActivityButton')}
</Button>
)}
</StyledFlexAllCenter>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type EmptyStateProps = {
onClickAssign: () => void;
isLimitedAccount: boolean;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './EmptyState';
2 changes: 2 additions & 0 deletions src/resources/app-en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,8 @@
"byParticipant": "By Participant",
"aboutParticipant": "About Participant",
"aboutParticipantEmpty": "No Activities are assigned to be completed about this Participant.",
"byParticipantEmpty": "No Activities are assigned for this Participant to complete.",
"byParticipantEmptyLimitedAccount": "This Participant has a Limited Account and cannot be assigned to complete Activities.",
"aboutParticipantEmptyTeamMember": "This Participant is a Team Member and Activities cannot be completed about them.",
"assignActivityButton": "Assign Activity",
"activitiesAndFlows": "Activities & Flows"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/app-fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,8 @@
"aboutParticipant": "Concernant le participant",
"aboutParticipantEmpty": "Aucune activité n'est attribuée à être complétée à propos de ce participant.",
"aboutParticipantEmptyTeamMember": "Ce participant est un membre de l'équipe et les activités ne peuvent pas être complétées à son sujet.",
"byParticipantEmpty": "Aucune activité n'est attribuée à ce participant pour être complétée.",
"byParticipantEmptyLimitedAccount": "Ce participant a un compte limité et ne peut pas être assigné pour compléter des activités.",
"assignActivityButton": "Attribuer une activité",
"activitiesAndFlows": "Activités & Flux"
},
Expand Down
Loading