Skip to content

Commit

Permalink
feat(GoalAchievement): add title unique validate
Browse files Browse the repository at this point in the history
  • Loading branch information
LamaEats committed Jun 16, 2023
1 parent 95e73f5 commit 166eae5
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 82 deletions.
32 changes: 0 additions & 32 deletions prisma/migrations/20230616115656_/migration.sql

This file was deleted.

32 changes: 32 additions & 0 deletions prisma/migrations/20230616141315_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- CreateTable
CREATE TABLE "GoalAchieveCriteria" (
"id" TEXT NOT NULL,
"goalId" TEXT NOT NULL,
"goalIdAsCriteria" TEXT,
"title" TEXT NOT NULL,
"weight" INTEGER NOT NULL,
"isDone" BOOLEAN NOT NULL DEFAULT false,
"activityId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "GoalAchieveCriteria_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "GoalAchieveCriteria_goalIdAsCriteria_key" ON "GoalAchieveCriteria"("goalIdAsCriteria");

-- CreateIndex
CREATE INDEX "GoalAchieveCriteria_title_idx" ON "GoalAchieveCriteria"("title");

-- CreateIndex
CREATE INDEX "GoalAchieveCriteria_goalId_idx" ON "GoalAchieveCriteria"("goalId");

-- AddForeignKey
ALTER TABLE "GoalAchieveCriteria" ADD CONSTRAINT "GoalAchieveCriteria_goalId_fkey" FOREIGN KEY ("goalId") REFERENCES "Goal"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "GoalAchieveCriteria" ADD CONSTRAINT "GoalAchieveCriteria_goalIdAsCriteria_fkey" FOREIGN KEY ("goalIdAsCriteria") REFERENCES "Goal"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "GoalAchieveCriteria" ADD CONSTRAINT "GoalAchieveCriteria_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "Activity"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
66 changes: 33 additions & 33 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -97,29 +97,29 @@ model Ghost {
}

model Activity {
id String @id @default(cuid())
ghost Ghost? @relation(fields: [ghostId], references: [id])
ghostId String? @unique
id String @id @default(cuid())
ghost Ghost? @relation(fields: [ghostId], references: [id])
ghostId String? @unique
user User?
filters Filter[]
comments Comment[]
reactions Reaction[]
projects Project[]
estimates Estimate[]
projectParticipant Project[] @relation("projectParticipants")
goalParticipant Goal[] @relation("goalParticipants")
goalWatchers Goal[] @relation("goalWatchers")
goalStargizers Goal[] @relation("goalStargizers")
projectWatchers Project[] @relation("projectWatchers")
projectStargizers Project[] @relation("projectStargizers")
filterStargizers Filter[] @relation("filterStargizers")
goalOwner Goal[] @relation("goalOwner")
goalIssuer Goal[] @relation("goalIssuer")
settings Settings @relation(fields: [settingsId], references: [id])
settingsId String @unique
projectParticipant Project[] @relation("projectParticipants")
goalParticipant Goal[] @relation("goalParticipants")
goalWatchers Goal[] @relation("goalWatchers")
goalStargizers Goal[] @relation("goalStargizers")
projectWatchers Project[] @relation("projectWatchers")
projectStargizers Project[] @relation("projectStargizers")
filterStargizers Filter[] @relation("filterStargizers")
goalOwner Goal[] @relation("goalOwner")
goalIssuer Goal[] @relation("goalIssuer")
settings Settings @relation(fields: [settingsId], references: [id])
settingsId String @unique
tags Tag[]
goalActions GoalHistory[] @relation("goalActions")
criterion GoalAchiveCriteria[]
goalActions GoalHistory[] @relation("goalActions")
criterion GoalAchieveCriteria[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
Expand Down Expand Up @@ -177,39 +177,39 @@ model EstimateToGoal {
}

model Goal {
id String @id @default(cuid())
scopeId Int @default(1)
id String @id @default(cuid())
scopeId Int @default(1)
title String
description String
kind String?
key Boolean?
personal Boolean?
private Boolean?
archived Boolean? @default(false)
archived Boolean? @default(false)
priority String?
estimate EstimateToGoal[]
project Project? @relation("projectGoals", fields: [projectId], references: [id])
project Project? @relation("projectGoals", fields: [projectId], references: [id])
projectId String?
teamId String?
state State? @relation("goalState", fields: [stateId], references: [id])
state State? @relation("goalState", fields: [stateId], references: [id])
stateId String?
activity Activity? @relation("goalIssuer", fields: [activityId], references: [id])
activity Activity? @relation("goalIssuer", fields: [activityId], references: [id])
activityId String?
owner Activity? @relation("goalOwner", fields: [ownerId], references: [id])
owner Activity? @relation("goalOwner", fields: [ownerId], references: [id])
ownerId String?
participants Activity[] @relation("goalParticipants")
watchers Activity[] @relation("goalWatchers")
stargizers Activity[] @relation("goalStargizers")
participants Activity[] @relation("goalParticipants")
watchers Activity[] @relation("goalWatchers")
stargizers Activity[] @relation("goalStargizers")
comments Comment[]
reactions Reaction[]
tags Tag[]
dependsOn Goal[] @relation("dependsOn")
blocks Goal[] @relation("dependsOn")
relatedTo Goal[] @relation("connected")
connected Goal[] @relation("connected")
dependsOn Goal[] @relation("dependsOn")
blocks Goal[] @relation("dependsOn")
relatedTo Goal[] @relation("connected")
connected Goal[] @relation("connected")
history GoalHistory[]
goalAchiveCriteria GoalAchiveCriteria[] @relation("GoalCriterion")
goalAsCriteria GoalAchiveCriteria? @relation("GoalAsCriteria")
goalAchiveCriteria GoalAchieveCriteria[] @relation("GoalCriterion")
goalAsCriteria GoalAchieveCriteria? @relation("GoalAsCriteria")
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
Expand Down Expand Up @@ -350,7 +350,7 @@ model GoalHistory {
@@index([goalId])
}

model GoalAchiveCriteria {
model GoalAchieveCriteria {
id String @id @default(cuid())
goal Goal @relation("GoalCriterion", fields: [goalId], references: [id])
goalId String
Expand Down
3 changes: 2 additions & 1 deletion src/components/CriteriaForm/CriteriaForm.i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"Criteria or Goal": "",
"Add achievement criteria": "",
"Weight must be in range": "Weight must be between 1 and {upTo}",
"Add": ""
"Add": "",
"Title must be unique": ""
}
3 changes: 2 additions & 1 deletion src/components/CriteriaForm/CriteriaForm.i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"Criteria or Goal": "Критерий или цель",
"Add achievement criteria": "Добавить критерий",
"Weight must be in range": "Вес критерия должен быть от 1 до {upTo}",
"Add": "Добавить"
"Add": "Добавить",
"Title must be unique": "Критерий должен быть уникальным"
}
32 changes: 27 additions & 5 deletions src/components/CriteriaForm/CriteriaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { trpc } from '../../utils/trpcClient';
import { tr } from './CriteriaForm.i18n';

const maxPossibleWeigth = 100;
const minPossibleWeight = 1;

const StyledPlainButton = styled(Button)`
background-color: unset;
Expand Down Expand Up @@ -129,7 +130,7 @@ const WeightField: React.FC<WeightFieldProps> = ({ registerProps, errorsResolver

if (Number.isNaN(parsedValue)) {
message = tr('Weight must be integer');
} else if (parsedValue <= 0 || maxValue + parsedValue > maxPossibleWeigth) {
} else if (parsedValue <= minPossibleWeight || maxValue + parsedValue > maxPossibleWeigth) {
message = tr
.raw('Weight must be in range', {
upTo: `${maxPossibleWeigth - maxValue}`,
Expand Down Expand Up @@ -161,13 +162,15 @@ const WeightField: React.FC<WeightFieldProps> = ({ registerProps, errorsResolver
interface CriteriaTitleFieldProps {
name: 'title';
value?: string;
titles: string[];
errorsResolver: (field: CriteriaTitleFieldProps['name']) => { message?: string } | undefined;
onSelect: <T extends Goal>(goal: T) => void;
onChange: ReactEventHandler<HTMLInputElement>;
setError: UseFormSetError<AddCriteriaScheme>;
}

export const CriteriaTitleField = forwardRef<HTMLInputElement, CriteriaTitleFieldProps>(
({ name, value = '', errorsResolver, onSelect, onChange }, ref) => {
({ name, value = '', errorsResolver, onSelect, onChange, titles = [], setError }, ref) => {
const [completionVisible, setCompletionVisibility] = useState(false);
const [[text, type], setQuery] = useState<[string, 'plain' | 'search']>([value, 'plain']);

Expand Down Expand Up @@ -217,6 +220,19 @@ export const CriteriaTitleField = forwardRef<HTMLInputElement, CriteriaTitleFiel
[onSelect, type],
);

const onlyUniqueTitleHandler = useCallback<React.FocusEventHandler<HTMLInputElement>>(
(event) => {
const { value } = event.target;

if (titles.some((t) => t === value)) {
setError('title', {
message: tr('Title must be unique'),
});
}
},
[titles, setError],
);

return (
<ComboBox
ref={ref}
Expand All @@ -236,6 +252,7 @@ export const CriteriaTitleField = forwardRef<HTMLInputElement, CriteriaTitleFiel
handleInputChange(...args);
onChange(...args);
}}
onBlur={onlyUniqueTitleHandler}
{...props}
/>
)}
Expand All @@ -253,10 +270,13 @@ export const CriteriaTitleField = forwardRef<HTMLInputElement, CriteriaTitleFiel
interface CriteriaFormProps {
onSubmit: (values: AddCriteriaScheme) => void;
goalId: string;
sumOfWeights: number;
validityData: {
sum: number;
title: string[];
};
}

export const CriteriaForm: React.FC<CriteriaFormProps> = ({ onSubmit, goalId, sumOfWeights = 0 }) => {
export const CriteriaForm: React.FC<CriteriaFormProps> = ({ onSubmit, goalId, validityData }) => {
const [formVisible, toggle] = useReducer((state) => !state, false);
const wrapperRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -328,14 +348,16 @@ export const CriteriaForm: React.FC<CriteriaFormProps> = ({ onSubmit, goalId, su
<CriteriaTitleField
{...field}
errorsResolver={errorResolver}
setError={setError}
onSelect={handleSelectGoal}
titles={validityData.title}
/>
)}
/>
<WeightField
registerProps={register('weight')}
errorsResolver={errorResolver}
maxValue={sumOfWeights}
maxValue={validityData.sum}
setError={setError}
/>
<StyledSubmitButton brick="left" view="primary" text={tr('Add')} size="m" type="submit" />
Expand Down
25 changes: 21 additions & 4 deletions src/components/GoalCriteria/GoalCriteria.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,13 @@ interface GoalCriteriaProps {
onAddCriteria: (val: AddCriteriaScheme) => void;
onToggleCriteria: (val: UpdateCriteriaScheme) => void;
onRemoveCriteria: (val: RemoveCriteriaScheme) => void;
renderForm: (props: { onAddCriteria: GoalCriteriaProps['onAddCriteria']; sumOfWeights: number }) => React.ReactNode;
renderForm: (props: {
onAddCriteria: GoalCriteriaProps['onAddCriteria'];
dataForValidateCriteria: {
sum: number;
title: string[];
};
}) => React.ReactNode;
}

export const GoalCriteria: React.FC<GoalCriteriaProps> = ({
Expand All @@ -353,8 +359,19 @@ export const GoalCriteria: React.FC<GoalCriteriaProps> = ({
},
[onAddCriteria, goalId],
);
const sumOfWeights = useMemo(() => {
return criteriaList.reduce((acc, { weight }) => acc + weight, 0);

const dataForValidateCriteria = useMemo(() => {
return criteriaList.reduce(
(acc, { weight, title }) => {
acc.sum += weight;
acc.title.push(title);
return acc;
},
{
sum: 0,
title: [] as string[],
},
);
}, [criteriaList]);

return (
Expand Down Expand Up @@ -385,7 +402,7 @@ export const GoalCriteria: React.FC<GoalCriteriaProps> = ({
/>
))}
</StyledTable>
{renderForm({ onAddCriteria: onAddHandler, sumOfWeights })}
{renderForm({ onAddCriteria: onAddHandler, dataForValidateCriteria })}
</Wrapper>
</StyledActivityFeedItem>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/GoalPage/GoalPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ export const GoalPage = ({ user, locale, ssrTime, params: { id } }: ExternalPage
<CriteriaForm
onSubmit={props.onAddCriteria}
goalId={goal.id}
sumOfWeights={props.sumOfWeights}
validityData={props.dataForValidateCriteria}
/>
))
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/GoalPreview/GoalPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ const GoalPreview: React.FC<GoalPreviewProps> = ({ preview, onClose, onDelete })
<CriteriaForm
onSubmit={props.onAddCriteria}
goalId={goal?.id || preview.id}
sumOfWeights={props.sumOfWeights}
validityData={props.dataForValidateCriteria}
/>
))
}
Expand Down
8 changes: 4 additions & 4 deletions trpc/router/goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ export const goal = router({

try {
const [criteria] = await Promise.all([
prisma.goalAchiveCriteria.create({
prisma.goalAchieveCriteria.create({
data: {
title: input.title,
weight: Number(input.weight),
Expand Down Expand Up @@ -855,7 +855,7 @@ export const goal = router({
}),

updateCriteriaState: protectedProcedure.input(updateCriteriaState).mutation(async ({ ctx, input }) => {
const currentCriteria = await prisma.goalAchiveCriteria.findUnique({
const currentCriteria = await prisma.goalAchieveCriteria.findUnique({
where: { id: input.id },
});

Expand All @@ -865,7 +865,7 @@ export const goal = router({
}

await Promise.all([
prisma.goalAchiveCriteria.update({
prisma.goalAchieveCriteria.update({
where: { id: input.id },
data: { isDone: input.isDone },
}),
Expand All @@ -892,7 +892,7 @@ export const goal = router({

removeCriteria: protectedProcedure.input(removeCriteria).mutation(async ({ input }) => {
try {
await prisma.goalAchiveCriteria.delete({
await prisma.goalAchieveCriteria.delete({
where: { id: input.id },
});
} catch (error: any) {
Expand Down

0 comments on commit 166eae5

Please sign in to comment.