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

keyword search for quantifiers & Quantify multiple praise #499

Merged
merged 11 commits into from
Jun 30, 2022
87 changes: 86 additions & 1 deletion packages/api/src/praise/controllers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { BadRequestError, NotFoundError } from '@error/errors';
import {
BadRequestError,
InternalServerError,
NotFoundError,
} from '@error/errors';
import {
getPraiseAllInput,
getQueryInput,
Expand Down Expand Up @@ -27,6 +31,7 @@ import {
PraiseDocument,
PraiseDto,
QuantificationCreateUpdateInput,
QuantifyMultiplePraiseInput,
} from './types';
import { praiseWithScore, getPraisePeriod } from './utils/core';
import { PeriodStatusType } from '@period/types';
Expand Down Expand Up @@ -198,3 +203,83 @@ export const quantify = async (
const response = await praiseDocumentListTransformer(affectedPraises);
res.status(200).json(response);
};

/**
* Quantify multiple praise items
* @param req
* @param res
*/
export const quantifyMultiple = async (
Copy link
Collaborator

@mattyg mattyg Jun 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function will need to have same logic used in the quantify function to determine all the praises with an affected scoreRealized. It probably makes sense to pull that logic out into a utility function and use it in both controllers.

The reason for that logic is so the "Duplicate Score" display is updated when the original praise's score is updated. See the screencast below:

Kazam_screencast_00003.mp4

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function will need to have same logic used in the quantify function to determine all the praises with an affected scoreRealized. It probably makes sense to pull that logic out into a utility function and use it in both controllers.

The reason for that logic is so the "Duplicate Score" display is updated when the original praise's score is updated. See the screencast below:

Kazam_screencast_00003.mp4

☝️☝️ @nebs-dev, this remains to be done.

req: TypedRequestBody<QuantifyMultiplePraiseInput>,
res: TypedResponse<PraiseDto[]>
): Promise<void> => {
const { score, praiseIds } = req.body;

let eventLogMessage = '';
const duplicatePraises: PraiseDocument[] = [];

const praiseItems = await Promise.all(
praiseIds.map(async (id) => {
const praise = await PraiseModel.findById(id).populate(
'giver receiver forwarder'
);

if (!praise) throw new NotFoundError('Praise');

const period = await getPraisePeriod(praise);

if (!period)
throw new BadRequestError('Praise does not have an associated period');

if (!res.locals.currentUser?._id) {
throw new InternalServerError('Current user not found.');
}

const quantification = praise.quantifications.find((q) =>
q.quantifier.equals(res.locals.currentUser._id)
);

if (!quantification)
throw new BadRequestError(
'User not assigned as quantifier for praise.'
);

const praisesDuplicateOfThis = await PraiseModel.find({
quantifications: {
$elemMatch: {
quantifier: res.locals.currentUser._id,
duplicatePraise: praise._id,
},
},
}).populate('giver receiver forwarder');

if (praisesDuplicateOfThis?.length > 0)
duplicatePraises.push(...praisesDuplicateOfThis);

quantification.score = score;
quantification.dismissed = false;
quantification.duplicatePraise = undefined;

await praise.save();

eventLogMessage = `Gave a score of ${
quantification.score
} to the praise with id "${(praise._id as Types.ObjectId).toString()}"`;

await logEvent(
EventLogTypeKey.QUANTIFICATION,
eventLogMessage,
{
userId: res.locals.currentUser._id,
},
period._id
);

return praise;
})
);

const affectedPraiseItems = [...praiseItems, ...duplicatePraises];
const response = await praiseDocumentListTransformer(affectedPraiseItems);
res.status(200).json(response);
};
5 changes: 5 additions & 0 deletions packages/api/src/praise/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@ praiseRouter.patchAsync(
authMiddleware(UserRole.QUANTIFIER),
controller.quantify
);
praiseRouter.patchAsync(
'/quantify',
authMiddleware(UserRole.QUANTIFIER),
controller.quantifyMultiple
);

export { praiseRouter };
5 changes: 5 additions & 0 deletions packages/api/src/praise/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export interface QuantificationCreateUpdateInput {
duplicatePraise: string;
}

export interface QuantifyMultiplePraiseInput {
score: number;
praiseIds: string[];
}

export interface Receiver {
_id: string;
praiseCount: number;
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/components/form/MultiselectInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const MultiselectInput = ({
noSelectedMessage,
}: MultiselectInputProps): JSX.Element => {
return (
<div className="relative border border-warm-gray-400 h-[42px]">
<div className="relative border border-warm-gray-400 h-10">
<Listbox value={selected} onChange={handleChange} multiple>
<Listbox.Button className=" pl-2 pr-8 text-left h-[42px] w-full bg-transparent border-none outline-none focus:ring-0 ">
<Listbox.Button className=" pl-2 pr-8 text-left h-10 w-full bg-transparent border-none outline-none focus:ring-0 ">
{(): JSX.Element => (
<>
<span className="block truncate">
Expand Down
8 changes: 5 additions & 3 deletions packages/frontend/src/components/form/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

interface SearchInputProps {
handleChange: (element) => void;
value: string;
value?: string;
placeholder?: string;
}

const SearchInput = ({
handleChange,
value,
placeholder = 'Search',
}: SearchInputProps): JSX.Element => {
return (
<div className="relative flex items-center border border-warm-gray-400 h-[42px]">
<div className="relative flex items-center border border-warm-gray-400 h-10">
<div className="absolute inset-y-0 left-0 flex items-center pl-3">
<span className="text-warm-gray-800 dark:text-white">
<FontAwesomeIcon
Expand All @@ -25,7 +27,7 @@ const SearchInput = ({
className="block pl-8 bg-transparent border-none outline-none focus:ring-0"
name="search"
type="text"
placeholder="Search"
placeholder={placeholder}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>): void =>
handleChange(e.target.value)
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/components/form/SelectInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ const SelectInput = ({
icon,
}: SelectInputProps): JSX.Element => {
return (
<div className="relative h-[42px]">
<div className="relative h-10">
<Listbox value={selected} onChange={handleChange}>
<Listbox.Button className="h-[42px] border border-warm-gray-400 w-full py-1.5 pl-3 pr-10 text-left bg-transparent ">
<Listbox.Button className="h-10 border border-warm-gray-400 w-full py-1.5 pl-3 pr-10 text-left bg-transparent ">
<span className="block truncate">{selected.label}</span>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<span className="text-warm-gray-800 ">
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/model/periods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,9 @@ export const PeriodQuantifierReceiverPraise = selectorFamily({
const userId = get(ActiveUserId);
const listKey = periodQuantifierPraiseListKey(periodId);
const praiseList = get(AllPraiseList(listKey));

if (!praiseList) return undefined;

return praiseList.filter(
(praise) =>
praise &&
Expand Down
28 changes: 28 additions & 0 deletions packages/frontend/src/model/praise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ type useQuantifyPraiseReturn = {
duplicatePraise: string | null
) => Promise<void>;
};

/**
* Hook that returns a function to use for closing a period
*/
Expand Down Expand Up @@ -294,3 +295,30 @@ export const useQuantifyPraise = (): useQuantifyPraiseReturn => {
);
return { quantify };
};

type useQuantifyMultiplePraiseReturn = {
quantifyMultiple: (score: number, praiseIds: string[]) => Promise<void>;
};

export const useQuantifyMultiplePraise =
(): useQuantifyMultiplePraiseReturn => {
const apiAuthClient = makeApiAuthClient();

const quantifyMultiple = useRecoilCallback(
({ set }) =>
async (score: number, praiseIds: string[]): Promise<void> => {
const response: AxiosResponse<PraiseDto[]> =
await apiAuthClient.patch('/praise/quantify', {
score,
praiseIds,
});

const praises = response.data;

praises.forEach((praise) => {
set(SinglePraise(praise._id), praise);
});
}
);
return { quantifyMultiple };
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@ import { faMinusCircle } from '@fortawesome/free-solid-svg-icons';

interface Props {
disabled?: boolean;
small?: boolean;
onClick();
}

const MarkDismissedButton = ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest creating a more generic component, similar to IconButton. IconButtonRound perhaps?

disabled = false,
onClick,
small,
}: Props): JSX.Element => {
const disabledClass = disabled ? 'praise-button-disabled' : 'praise-button';
const smallClass = small ? 'praise-button-round' : '';

return (
<button
disabled={disabled}
className={
disabled
? 'praise-button-disabled space-x-2'
: 'praise-button space-x-2'
}
className={`space-x-2 ${disabledClass} ${smallClass}`}
onClick={onClick}
>
<FontAwesomeIcon icon={faMinusCircle} size="1x" />
<span>Dismiss</span>
{!small ? <span>Dismiss</span> : null}
</button>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@ import { faCopy } from '@fortawesome/free-solid-svg-icons';

interface Props {
disabled?: boolean;
small?: boolean;
onClick();
}

const MarkDuplicateButton = ({
disabled = false,
onClick,
small,
}: Props): JSX.Element => {
const disabledClass = disabled ? 'praise-button-disabled' : 'praise-button';
const smallClass = small ? 'praise-button-round' : '';

return (
<button
disabled={disabled}
className={
disabled
? 'praise-button-disabled space-x-2'
: 'praise-button space-x-2'
}
className={`space-x-2 ${disabledClass} ${smallClass}`}
onClick={onClick}
>
<FontAwesomeIcon icon={faCopy} size="1x" />
<span>Mark as duplicates</span>
{!small ? <span>Mark as duplicates</span> : null}
</button>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faScaleUnbalanced } from '@fortawesome/free-solid-svg-icons';

interface Props {
disabled?: boolean;
small?: boolean;
onClick();
}

const QuantifyMultipleButton = ({
disabled = false,
onClick,
small,
}: Props): JSX.Element => {
const disabledClass = disabled ? 'praise-button-disabled' : 'praise-button';
const smallClass = small ? 'praise-button-round' : '';

return (
<button
disabled={disabled}
className={`space-x-2 ${disabledClass} ${smallClass}`}
onClick={onClick}
>
<FontAwesomeIcon icon={faScaleUnbalanced} size="1x" />
{!small ? <span>Quantify</span> : null}
</button>
);
};

export default QuantifyMultipleButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import ScrollableDialog from '@/components/ScrollableDialog';
import QuantifyMultipleButton from '@/pages/QuantifyPeriodReceiver/components/QuantifyMultipleButton';
import QuantifySlider from '@/pages/QuantifyPeriodReceiver/components/QuantifySlider';
import { faCalculator, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { PraiseDto } from 'api/dist/praise/types';
import { useState } from 'react';

interface QuantifyMultipleDialogProps {
open: boolean;
onClose(): void;
selectedPraises: PraiseDto[];
allowedValues: number[];
onSetScore(newScore: number, selectedPraises: PraiseDto[]);
}

const QuantifyMultipleDialog = ({
open = false,
onClose,
selectedPraises,
allowedValues,
onSetScore,
}: QuantifyMultipleDialogProps): JSX.Element | null => {
const [score, setScore] = useState<number>(0);

return (
<ScrollableDialog open={open} onClose={onClose}>
<div className="w-full h-full">
<div className="flex justify-end p-6">
<button className="praise-button-round" onClick={onClose}>
<FontAwesomeIcon icon={faTimes} size="1x" />
</button>
</div>
<div className="px-20 space-y-6">
<div className="flex justify-center">
<FontAwesomeIcon icon={faCalculator} size="2x" />
</div>
<h2 className="text-center">
Quantify selected ({selectedPraises.length}) praise items.
</h2>

<div className="text-center">
<QuantifySlider
allowedScores={allowedValues}
onChange={(newScore): void => setScore(newScore)}
/>
</div>

<div className="flex justify-center">
<QuantifyMultipleButton
onClick={(): void => {
onSetScore(score, selectedPraises);
onClose();
}}
/>
</div>
</div>
</div>
</ScrollableDialog>
);
};

export default QuantifyMultipleDialog;
Loading