Skip to content

Commit 824f1d9

Browse files
committed
STNDP-163 Enable users to customize the structure of the standup form (#111)
* STNDP-163 Enable users to customize the structure of the standup form * STNDP-163 Enable users to customize the structure of the standup form * STNDP-163 Enable users to customize the structure of the standup form * STNDP-163 Enable users to customize the structure of the standup form * STNDP-163 Enable users to customize the structure of the standup form * STNDP-163 Enable users to customize the structure of the standup form * STNDP-163 Enable users to customize the structure of the standup form
1 parent 3fb06ed commit 824f1d9

File tree

17 files changed

+1156
-67
lines changed

17 files changed

+1156
-67
lines changed

apps/api/src/boards/boards.service.ts

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ import {
44
InsertBoard,
55
usersToBoards,
66
Board,
7-
standupForms,
8-
StandupForm,
97
} from '../libs/db/schema';
108
import { and, count, eq, sql } from 'drizzle-orm';
119
import { Database, DATABASE_TOKEN } from 'src/db/db.module';
1210
import { UpdateBoardDto } from 'src/boards/dto/update-board.dto';
11+
import { StandupFormsService } from './standup-forms/standup-forms.service';
1312

1413
@Injectable()
1514
export class BoardsService {
1615
constructor(
1716
@Inject(DATABASE_TOKEN)
1817
private readonly db: Database,
18+
private readonly standupFormsService: StandupFormsService,
1919
) {}
2020

2121
create({
@@ -53,9 +53,9 @@ export class BoardsService {
5353
timezone,
5454
});
5555

56+
// TODO: use transaction for setup method to ensure atomicity
5657
await this.associateUser(result.id, userId);
57-
58-
await this.createDefaultForm(result.id);
58+
await this.standupFormsService.createDefault(result.id);
5959

6060
return result;
6161
}
@@ -76,52 +76,6 @@ export class BoardsService {
7676
});
7777
}
7878

79-
private async createDefaultForm(boardId: number): Promise<void> {
80-
const schema = {
81-
// title: "Today's Standup",
82-
fields: [
83-
{
84-
name: 'yesterday',
85-
label: 'What did you do yesterday?',
86-
placeholder: 'Write your reply here...',
87-
type: 'textarea',
88-
required: true,
89-
},
90-
{
91-
name: 'today',
92-
label: 'What will you do today?',
93-
placeholder: 'Write your reply here...',
94-
type: 'textarea',
95-
required: true,
96-
},
97-
{
98-
name: 'blockers',
99-
label: 'Do you have any blockers?',
100-
placeholder: 'Write your reply here...',
101-
description:
102-
'Share any challenges or obstacles that might slow down your progress',
103-
type: 'textarea',
104-
required: false,
105-
},
106-
],
107-
};
108-
109-
const result = await this.db
110-
.insert(standupForms)
111-
.values({
112-
boardId,
113-
schema,
114-
})
115-
.returning()
116-
.then((standupForms): StandupForm => standupForms[0]);
117-
118-
await this.db
119-
.update(boards)
120-
.set({
121-
activeStandupFormId: result.id,
122-
})
123-
.where(eq(boards.id, boardId));
124-
}
12579

12680
// list() {
12781
// return `This action returns all boards`;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export const DEFAULT_STANDUP_FORM_SCHEMA = {
2+
fields: [
3+
{
4+
name: 'yesterday',
5+
label: 'What did you do yesterday?',
6+
placeholder: 'Write your reply here...',
7+
type: 'textarea',
8+
required: true,
9+
},
10+
{
11+
name: 'today',
12+
label: 'What will you do today?',
13+
placeholder: 'Write your reply here...',
14+
type: 'textarea',
15+
required: true,
16+
},
17+
{
18+
name: 'blockers',
19+
label: 'Do you have any blockers?',
20+
placeholder: 'Write your reply here...',
21+
description:
22+
'Share any challenges or obstacles that might slow down your progress',
23+
type: 'textarea',
24+
required: false,
25+
},
26+
],
27+
};
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
export class CreateStandupFormDto {}
1+
import { IsNotEmpty, IsObject } from 'class-validator';
2+
3+
export class CreateStandupFormDto {
4+
@IsNotEmpty()
5+
@IsObject()
6+
schema!: object;
7+
}

apps/api/src/boards/standup-forms/standup-forms.controller.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
11
import {
22
Controller,
33
Get,
4+
Post,
5+
Body,
46
InternalServerErrorException,
57
Param,
68
ParseIntPipe,
79
Query,
10+
UseGuards,
811
} from '@nestjs/common';
912
import { StandupFormsService } from './standup-forms.service';
13+
import { CreateStandupFormDto } from './dto/create-standup-form.dto';
14+
import { AuthGuard } from 'src/auth/guards/auth.guard';
15+
import { BoardAccessGuard } from '../guards/board-access.guard';
1016

1117
@Controller('boards/:boardId/standup-forms')
18+
@UseGuards(AuthGuard, BoardAccessGuard)
1219
export class StandupFormsController {
1320
constructor(private readonly standupFormsService: StandupFormsService) {}
1421

22+
// POST /boards/:boardId/standup-forms
23+
@Post()
24+
async create(
25+
@Param('boardId', ParseIntPipe) boardId: number,
26+
@Body() createStandupFormDto: CreateStandupFormDto,
27+
) {
28+
try {
29+
return await this.standupFormsService.createActive(
30+
boardId,
31+
createStandupFormDto.schema,
32+
);
33+
} catch (error) {
34+
console.error(error);
35+
throw new InternalServerErrorException();
36+
}
37+
}
38+
1539
// GET /boards/:boardId/standup-forms/:standupFormId
1640
@Get(':standupFormId')
1741
async get(
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { Module } from '@nestjs/common';
1+
import { Module, forwardRef } from '@nestjs/common';
22
import { StandupFormsService } from './standup-forms.service';
33
import { StandupFormsController } from './standup-forms.controller';
44
import { DbModule } from 'src/db/db.module';
5+
import { AuthModule } from 'src/auth/auth.module';
6+
import { BoardsModule } from '../boards.module';
57
@Module({
68
controllers: [StandupFormsController],
79
providers: [StandupFormsService],
8-
imports: [DbModule],
10+
imports: [DbModule, AuthModule, forwardRef(() => BoardsModule)],
11+
exports: [StandupFormsService],
912
})
1013
export class StandupFormsModule {}

apps/api/src/boards/standup-forms/standup-forms.service.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Inject, Injectable } from '@nestjs/common';
2-
import { standupForms } from 'src/libs/db/schema';
2+
import { standupForms, boards } from 'src/libs/db/schema';
33
import { and, eq, inArray } from 'drizzle-orm';
44
import { DATABASE_TOKEN } from 'src/db/db.module';
55
import { Database } from 'src/db/db.module';
6+
import { DEFAULT_STANDUP_FORM_SCHEMA } from './constants';
67

78
@Injectable()
89
export class StandupFormsService {
@@ -11,9 +12,25 @@ export class StandupFormsService {
1112
private readonly db: Database,
1213
) {}
1314

14-
// create(createStandupFormDto: CreateStandupFormDto) {
15-
// return 'This action adds a new standupForm';
16-
// }
15+
createActive(boardId: number, schema: object) {
16+
return this.db.transaction(async (tx) => {
17+
const [standupForm] = await tx
18+
.insert(standupForms)
19+
.values({ boardId, schema })
20+
.returning();
21+
22+
await tx
23+
.update(boards)
24+
.set({ activeStandupFormId: standupForm.id })
25+
.where(eq(boards.id, boardId));
26+
27+
return standupForm;
28+
});
29+
}
30+
31+
createDefault(boardId: number) {
32+
return this.createActive(boardId, DEFAULT_STANDUP_FORM_SCHEMA);
33+
}
1734

1835
// findAll() {
1936
// return `This action returns all standupForms`;
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { Module } from '@nestjs/common';
1+
import { Module, forwardRef } from '@nestjs/common';
22
import { StandupsService } from './standups.service';
33
import { StandupsController } from './standups.controller';
4-
import { BoardsService } from '../boards.service';
54
import { DbModule } from 'src/db/db.module';
65
import { AuthModule } from 'src/auth/auth.module';
6+
import { BoardsModule } from '../boards.module';
77

88
@Module({
9-
imports: [DbModule, AuthModule],
9+
imports: [DbModule, AuthModule, forwardRef(() => BoardsModule)],
1010
controllers: [StandupsController],
11-
providers: [StandupsService, BoardsService],
11+
providers: [StandupsService],
1212
})
1313
export class StandupsModule {}

apps/web/app/routes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export default [
2525
"/boards/:boardId/settings/collaborators",
2626
"routes/board-settings-collaborators-route/board-settings-collaborators-route.tsx"
2727
),
28+
route(
29+
"/boards/:boardId/settings/standups",
30+
"routes/board-settings-standups-route/board-settings-standups-route.tsx"
31+
),
2832
]
2933
),
3034
]),
@@ -76,6 +80,10 @@ export default [
7680
"/boards/:boardId/collaborators/delete",
7781
"routes/delete-board-collaborator-route/delete-board-collaborator-route.tsx"
7882
),
83+
route(
84+
"/boards/:boardId/standup-forms/create",
85+
"routes/create-board-standup-form-route/create-board-standup-form-route.tsx"
86+
),
7987
route(
8088
"/accept-invitation",
8189
"routes/accept-invitation-route/accept-invitation-route.tsx"

apps/web/app/routes/board-route/board-route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function getBoard(
4949
}).then((response) => response.json() as Promise<ApiData<Board>>);
5050
}
5151

52-
function getStandupForm(
52+
export function getStandupForm(
5353
{ standupFormId, boardId }: { standupFormId: number; boardId: number },
5454
{ accessToken }: { accessToken: string }
5555
) {

apps/web/app/routes/board-route/dynamic-form.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ import { z } from "zod";
55
import { useImperativeHandle, useEffect, useRef, type Ref } from "react";
66
import type { ComponentProps } from "react";
77

8-
function AutoSizeTextArea({ ...props }: ComponentProps<typeof TextArea>) {
8+
// TODO: rename everything in this file - use word standup-form instead of dynamic-form
9+
10+
export type StandupFormSchema = NonNullable<
11+
ReturnType<typeof validateDynamicFormSchema>
12+
>;
13+
export type StandupFormFields = StandupFormSchema["fields"];
14+
export type StandupFormField = StandupFormFields[number];
15+
16+
export function AutoSizeTextArea({
17+
...props
18+
}: ComponentProps<typeof TextArea>) {
919
const textareaRef = useRef<HTMLTextAreaElement>(null);
1020

1121
useEffect(() => {
@@ -155,7 +165,7 @@ function DynamicForm({
155165
{fields.map((field) => {
156166
return (
157167
<Flex key={field.name} direction="column" gap="2">
158-
<label>
168+
<label htmlFor={field.name}>
159169
<Flex align="center" gap="2">
160170
<Text size="2" className="font-semibold">
161171
{field.label}
@@ -173,6 +183,7 @@ function DynamicForm({
173183
control={control}
174184
render={({ field: { onChange, value } }) => (
175185
<AutoSizeTextArea
186+
id={field.name}
176187
variant="soft"
177188
className="w-full min-h-[72px]!"
178189
resize="none"

0 commit comments

Comments
 (0)