Skip to content

Commit

Permalink
feat(datatrakWeb): RN-1358: Assign tasks from dashboard (#5770)
Browse files Browse the repository at this point in the history
* Create button

* Move modal to ui-components

* Move country selector to features folder

* Update country selector exports/imports

* Update tupaia-pin.svg

* Country selector on modal

* Move survey selector to features

* Move types

* Update survey list component to take care of fetching

* Survey selector

* Move entity selector to features

* Fix types

* Entity selector

* Styling entity selector

* Due date

* WIP

* WIP

* assignee input

* Add loading state and save user id

* Styling repeat scheduler

* Comments placholder

* Styling

* WIP

* Create task route

* Create task workflow

* Clear form when modal is reopened

* Update schemas.ts

* remove unused import

* Handle reset

* Fix datatrak tests

* Fix central server tests

* Move modal to ui-components

* Remove unused import

* Remove duplicate file

* Fix build

* Fix tests

* Update packages/central-server/src/apiV2/utilities/constructNewRecordValidationRules.js

Co-authored-by: Tom Caiger <caigertom@gmail.com>

* Fix error messages

* Handle search term in the BE

* WIP

* WIP

* Assignee Id modal

* Working assignee

* remove unused property

* Fix timezone issue

* Fix date formatting of filter

* remove unused variable

* Remove unused variable

* Fix casing

* Default to showing countries if no primary entity question

* Update AssigneeInput.tsx

* Show loader when loading project and countries

* Fix copy

* Exclude internal users

* Fix types

* Change colour of icon in entity list

* Fix modal button types

* Fix types

---------

Co-authored-by: Tom Caiger <caigertom@gmail.com>
  • Loading branch information
alexd-bes and tcaiger committed Jul 11, 2024
1 parent 696d191 commit ffa557a
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 113 deletions.
1 change: 1 addition & 0 deletions packages/datatrak-web/src/api/mutations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export { useOneTimeLogin } from './useOneTimeLogin';
export * from './useExportSurveyResponses';
export { useTupaiaRedirect } from './useTupaiaRedirect';
export { useCreateTask } from './useCreateTask';
export { useEditTask } from './useEditTask';
27 changes: 27 additions & 0 deletions packages/datatrak-web/src/api/mutations/useEditTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import { useMutation, useQueryClient } from 'react-query';
import { Task } from '@tupaia/types';
import { put } from '../api';

type Data = Partial<Task>;

export const useEditTask = (taskId: Task['id'], onSuccess?: () => void) => {
const queryClient = useQueryClient();
return useMutation<any, Error, Data, unknown>(
(data: Data) => {
return put(`tasks/${taskId}`, {
data,
});
},
{
onSuccess: () => {
queryClient.invalidateQueries('tasks');
if (onSuccess) onSuccess();
},
},
);
};
91 changes: 91 additions & 0 deletions packages/datatrak-web/src/features/Tasks/AssigneeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import React, { useState } from 'react';
import throttle from 'lodash.throttle';
import styled from 'styled-components';
import { Country, DatatrakWebSurveyUsersRequest } from '@tupaia/types';
import { Autocomplete as BaseAutocomplete } from '../../components';
import { useSurveyUsers } from '../../api';
import { Survey } from '../../types';

const Autocomplete = styled(BaseAutocomplete)`
.MuiFormLabel-root {
font-size: inherit;
font-weight: ${({ theme }) => theme.typography.fontWeightMedium};
}
.MuiInputBase-root {
font-size: 0.875rem;
}
input::placeholder {
color: ${({ theme }) => theme.palette.text.secondary};
}
.MuiOutlinedInput-notchedOutline {
border-color: ${({ theme }) => theme.palette.divider};
}
.MuiInputLabel-asterisk {
color: ${({ theme }) => theme.palette.error.main};
}
`;

type User = DatatrakWebSurveyUsersRequest.ResBody[0];

interface AssigneeInputProps {
value: string | null;
onChange: (value: User['id'] | null) => void;
inputRef?: React.Ref<any>;
countryCode?: Country['code'];
surveyCode?: Survey['code'];
required?: boolean;
name?: string;
error?: boolean;
}

export const AssigneeInput = ({
value,
onChange,
inputRef,
countryCode,
surveyCode,
required,
error,
}: AssigneeInputProps) => {
const [searchValue, setSearchValue] = useState('');

const { data: users = [], isLoading } = useSurveyUsers(surveyCode, countryCode, searchValue);

const onChangeAssignee = (_e, newSelection: User | null) => {
onChange(newSelection?.id ?? null);
};

const options =
users?.map(user => ({
...user,
value: user.id,
label: user.name,
})) ?? [];

const selection = options.find(option => option.id === value);

return (
<Autocomplete
label="Assignee"
options={options}
value={selection}
onChange={onChangeAssignee}
inputRef={inputRef}
name="assignee"
onInputChange={throttle((_, newValue) => {
setSearchValue(newValue);
}, 200)}
inputValue={searchValue}
getOptionLabel={option => option.label}
getOptionSelected={option => option.id === value}
placeholder="Search..."
loading={isLoading}
required={required}
error={error}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { useCreateTask, useUser } from '../../../api';
import { CountrySelector, useUserCountries } from '../../CountrySelector';
import { GroupedSurveyList } from '../../GroupedSurveyList';
import { DueDatePicker } from '../DueDatePicker';
import { AssigneeInput } from '../AssigneeInput';
import { RepeatScheduleInput } from './RepeatScheduleInput';
import { EntityInput } from './EntityInput';
import { AssigneeInput } from './AssigneeInput';

const CountrySelectorWrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -130,6 +130,7 @@ export const CreateTaskModal = ({ open, onClose }: CreateTaskModalProps) => {
control,
setValue,
reset,
watch,
formState: { isValid, dirtyFields },
} = formContext;

Expand Down Expand Up @@ -184,6 +185,8 @@ export const CreateTaskModal = ({ open, onClose }: CreateTaskModalProps) => {
}
}, [open]);

const surveyCode = watch('surveyCode');

return (
<Modal isOpen={open} onClose={onClose} title="New task" buttons={buttons} isLoading={isSaving}>
<Wrapper>
Expand Down Expand Up @@ -280,7 +283,8 @@ export const CreateTaskModal = ({ open, onClose }: CreateTaskModalProps) => {
value={value}
onChange={onChange}
inputRef={ref}
selectedCountry={selectedCountry}
countryCode={selectedCountry?.code}
surveyCode={surveyCode}
/>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import React from 'react';
import { TaskStatus } from '@tupaia/types';
import { generatePath, useLocation } from 'react-router';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Button } from '@tupaia/ui-components';
import { ROUTES } from '../../../constants';
import { Task } from '../../../types';

const ActionButtonComponent = styled(Button).attrs({
color: 'primary',
size: 'small',
})`
padding-inline: 1.2rem;
padding-block: 0.4rem;
width: 100%;
.MuiButton-label {
font-size: 0.75rem;
line-height: normal;
}
.cell-content:has(&) {
padding-block: 0.2rem;
padding-inline-start: 1.5rem;
}
`;

interface ActionButtonProps {
task: Task;
onAssignTask: (task: Task | null) => void;
}

export const ActionButton = ({ task, onAssignTask }: ActionButtonProps) => {
const location = useLocation();
if (!task) return null;
const { assigneeId, survey, entity, status } = task;
if (status === TaskStatus.cancelled || status === TaskStatus.completed) return null;
const openAssignTaskModal = () => {
onAssignTask(task);
};
if (!assigneeId) {
return (
<ActionButtonComponent variant="outlined" onClick={openAssignTaskModal}>
Assign
</ActionButtonComponent>
);
}

const surveyLink = generatePath(ROUTES.SURVEY, {
surveyCode: survey.code,
countryCode: entity.countryCode,
});
return (
<ActionButtonComponent
component={Link}
to={surveyLink}
variant="contained"
state={{
from: JSON.stringify(location),
}}
>
Complete
</ActionButtonComponent>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import React from 'react';
import styled from 'styled-components';
import { useForm, Controller, FormProvider } from 'react-hook-form';
import { Modal, ModalCenteredContent } from '@tupaia/ui-components';
import { AssigneeInput } from '../AssigneeInput';
import { useEditTask } from '../../../api';

const Container = styled(ModalCenteredContent)`
width: 20rem;
max-width: 100%;
margin: 0 auto;
`;

export const AssignTaskModal = ({ task, onClose }) => {
const formContext = useForm({
mode: 'onChange',
});
const {
control,
handleSubmit,
formState: { isValid },
} = formContext;

const { mutate: editTask, isLoading } = useEditTask(task?.id, onClose);

if (!task) return null;

const modalButtons = [
{
text: 'Cancel',
onClick: onClose,
variant: 'outlined',
id: 'cancel',
disabled: isLoading,
},
{
text: 'Save',
onClick: handleSubmit(editTask),
id: 'save',
type: 'submit',
disabled: isLoading || !isValid,
},
];

return (
<>
<Modal
isOpen
onClose={onClose}
title="Assign task"
buttons={modalButtons}
isLoading={isLoading}
>
<Container>
<FormProvider {...formContext}>
<form onSubmit={handleSubmit(editTask)}>
<Controller
name="assignee_id"
control={control}
rules={{ required: 'Required' }}
render={({ value, onChange, ref }, { invalid }) => (
<AssigneeInput
value={value}
required
onChange={onChange}
inputRef={ref}
countryCode={task?.entity?.countryCode}
surveyCode={task?.survey?.code}
error={invalid}
/>
)}
/>
</form>
</FormProvider>
</Container>
</Modal>
</>
);
};
Loading

0 comments on commit ffa557a

Please sign in to comment.