From aa222fb0afcf47fb44a41bbd5414b54256eccad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Tue, 8 Aug 2023 14:05:40 +0300 Subject: [PATCH 01/18] Refactor Bolt event handlers to not be HOC's --- app-nest/src/bolt/bolt-register.service.ts | 7 +- .../src/dev-tools/dev-tools.controller.ts | 8 +-- app-nest/src/gui/tabs/home-tab.controller.ts | 68 +++++++++---------- app-nest/src/sync/sync.controller.ts | 4 +- 4 files changed, 42 insertions(+), 45 deletions(-) diff --git a/app-nest/src/bolt/bolt-register.service.ts b/app-nest/src/bolt/bolt-register.service.ts index 69057f9..789eadd 100644 --- a/app-nest/src/bolt/bolt-register.service.ts +++ b/app-nest/src/bolt/bolt-register.service.ts @@ -58,9 +58,10 @@ export class BoltRegisterService { const cref = this.moduleRef.get(discoveredMethod.parentClass.injectType, { strict: false, }); - // N.B.: Take care where you call the discovered handler method. This is an easy - // place to bind it into a wrong context. Ask me how I know. - return cref[discoveredMethod.methodName](); + + return cref[discoveredMethod.methodName].bind( + discoveredMethod.parentClass.instance, + ); } /** diff --git a/app-nest/src/dev-tools/dev-tools.controller.ts b/app-nest/src/dev-tools/dev-tools.controller.ts index 4748c3a..76ca019 100644 --- a/app-nest/src/dev-tools/dev-tools.controller.ts +++ b/app-nest/src/dev-tools/dev-tools.controller.ts @@ -7,10 +7,8 @@ export class DevToolsController { constructor(private userSyncService: UserSyncService) {} @BoltAction("sync_users") - syncUsers() { - return async ({ ack }) => { - await ack(); - await this.userSyncService.syncUsers(); - }; + async syncUsers({ ack }) { + await ack(); + await this.userSyncService.syncUsers(); } } diff --git a/app-nest/src/gui/tabs/home-tab.controller.ts b/app-nest/src/gui/tabs/home-tab.controller.ts index 58bb5cf..bba5ef8 100644 --- a/app-nest/src/gui/tabs/home-tab.controller.ts +++ b/app-nest/src/gui/tabs/home-tab.controller.ts @@ -8,44 +8,42 @@ export class HomeTabController { constructor(private userService: UserService) {} @BoltEvent("app_home_opened") - getView() { - return async ({ event, client, logger }) => { - const users = await this.userService.findAll(); - const { slackId } = users[0]; + async getView({ event, client, logger }) { + const users = await this.userService.findAll(); + const { slackId } = users[0]; - try { - const result = await client.views.publish({ - user_id: event.user, - view: { - type: "home", - blocks: [ - ...devTools, - { - type: "section", - text: { - type: "mrkdwn", - text: - "*Welcome controller, <@" + - event.user + - "> :house:* " + - slackId, - }, + try { + const result = await client.views.publish({ + user_id: event.user, + view: { + type: "home", + blocks: [ + ...devTools, + { + type: "section", + text: { + type: "mrkdwn", + text: + "*Welcome controller, <@" + + event.user + + "> :house:* " + + slackId, }, - { - type: "section", - text: { - type: "mrkdwn", - text: "Learn how home tabs can be more useful and interactive .", - }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "Learn how home tabs can be more useful and interactive .", }, - ], - }, - }); + }, + ], + }, + }); - logger.debug(result); - } catch (error) { - logger.error(error); - } - }; + logger.debug(result); + } catch (error) { + logger.error(error); + } } } diff --git a/app-nest/src/sync/sync.controller.ts b/app-nest/src/sync/sync.controller.ts index f8f11fc..6a358ce 100644 --- a/app-nest/src/sync/sync.controller.ts +++ b/app-nest/src/sync/sync.controller.ts @@ -7,7 +7,7 @@ export class SyncController { constructor(private userSyncService: UserSyncService) {} @BoltEvent("user_profile_changed") - userProfileChanged() { - return ({ event }) => this.userSyncService.syncUsers(event.user); + async userProfileChanged({ event }) { + await this.userSyncService.syncUsers(event.user); } } From ed5d0c2be1e50bb74284c99b7f4e4bc417177ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Tue, 8 Aug 2023 14:49:55 +0300 Subject: [PATCH 02/18] Refactor hard-coded event types into enums --- app-nest/src/bolt/enums/bolt-actions.enum.ts | 5 +++++ app-nest/src/bolt/enums/bolt-events.enum.ts | 6 ++++++ app-nest/src/dev-tools/dev-tools.controller.ts | 3 ++- app-nest/src/gui/tabs/home-tab.controller.ts | 3 ++- app-nest/src/sync/sync.controller.ts | 3 ++- 5 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 app-nest/src/bolt/enums/bolt-actions.enum.ts create mode 100644 app-nest/src/bolt/enums/bolt-events.enum.ts diff --git a/app-nest/src/bolt/enums/bolt-actions.enum.ts b/app-nest/src/bolt/enums/bolt-actions.enum.ts new file mode 100644 index 0000000..7dc2a02 --- /dev/null +++ b/app-nest/src/bolt/enums/bolt-actions.enum.ts @@ -0,0 +1,5 @@ +enum BoltActions { + SYNC_USERS = "sync_users", +} + +export default BoltActions; diff --git a/app-nest/src/bolt/enums/bolt-events.enum.ts b/app-nest/src/bolt/enums/bolt-events.enum.ts new file mode 100644 index 0000000..61470d6 --- /dev/null +++ b/app-nest/src/bolt/enums/bolt-events.enum.ts @@ -0,0 +1,6 @@ +enum BoltEvents { + APP_HOME_OPENED = "app_home_opened", + USER_PROFILE_CHANGED = "user_profile_changed", +} + +export default BoltEvents; diff --git a/app-nest/src/dev-tools/dev-tools.controller.ts b/app-nest/src/dev-tools/dev-tools.controller.ts index 76ca019..17096c4 100644 --- a/app-nest/src/dev-tools/dev-tools.controller.ts +++ b/app-nest/src/dev-tools/dev-tools.controller.ts @@ -1,12 +1,13 @@ import { Controller } from "@nestjs/common"; import BoltAction from "../bolt/decorators/bolt-action.decorator"; +import BoltActions from "../bolt/enums/bolt-actions.enum"; import { UserSyncService } from "../sync/user-sync.service"; @Controller() export class DevToolsController { constructor(private userSyncService: UserSyncService) {} - @BoltAction("sync_users") + @BoltAction(BoltActions.SYNC_USERS) async syncUsers({ ack }) { await ack(); await this.userSyncService.syncUsers(); diff --git a/app-nest/src/gui/tabs/home-tab.controller.ts b/app-nest/src/gui/tabs/home-tab.controller.ts index bba5ef8..5f0ad8a 100644 --- a/app-nest/src/gui/tabs/home-tab.controller.ts +++ b/app-nest/src/gui/tabs/home-tab.controller.ts @@ -1,5 +1,6 @@ import { Controller } from "@nestjs/common"; import BoltEvent from "../../bolt/decorators/bolt-event.decorator"; +import BoltEvents from "../../bolt/enums/bolt-events.enum"; import { UserService } from "../../entities/user/user.service"; import devTools from "../dev/dev-tools"; @@ -7,7 +8,7 @@ import devTools from "../dev/dev-tools"; export class HomeTabController { constructor(private userService: UserService) {} - @BoltEvent("app_home_opened") + @BoltEvent(BoltEvents.APP_HOME_OPENED) async getView({ event, client, logger }) { const users = await this.userService.findAll(); const { slackId } = users[0]; diff --git a/app-nest/src/sync/sync.controller.ts b/app-nest/src/sync/sync.controller.ts index 6a358ce..c3f41da 100644 --- a/app-nest/src/sync/sync.controller.ts +++ b/app-nest/src/sync/sync.controller.ts @@ -1,12 +1,13 @@ import { Controller } from "@nestjs/common"; import BoltEvent from "../bolt/decorators/bolt-event.decorator"; +import BoltEvents from "../bolt/enums/bolt-events.enum"; import { UserSyncService } from "./user-sync.service"; @Controller() export class SyncController { constructor(private userSyncService: UserSyncService) {} - @BoltEvent("user_profile_changed") + @BoltEvent(BoltEvents.USER_PROFILE_CHANGED) async userProfileChanged({ event }) { await this.userSyncService.syncUsers(event.user); } From 29a82f9ecd99397745793a83306ab5671c18aa5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Tue, 8 Aug 2023 14:59:37 +0300 Subject: [PATCH 03/18] Type handler function args --- app-nest/src/bolt/types/bolt-action-types.ts | 9 +++++++++ app-nest/src/bolt/types/bolt-event-types.ts | 17 +++++++++++++++++ app-nest/src/dev-tools/dev-tools.controller.ts | 3 ++- app-nest/src/gui/tabs/home-tab.controller.ts | 3 ++- app-nest/src/sync/sync.controller.ts | 3 ++- 5 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 app-nest/src/bolt/types/bolt-action-types.ts create mode 100644 app-nest/src/bolt/types/bolt-event-types.ts diff --git a/app-nest/src/bolt/types/bolt-action-types.ts b/app-nest/src/bolt/types/bolt-action-types.ts new file mode 100644 index 0000000..ff1d2f9 --- /dev/null +++ b/app-nest/src/bolt/types/bolt-action-types.ts @@ -0,0 +1,9 @@ +import { + AllMiddlewareArgs, + SlackAction, + SlackActionMiddlewareArgs, +} from "@slack/bolt"; +import { StringIndexed } from "@slack/bolt/dist/types/helpers"; + +export type BoltActionArgs = SlackActionMiddlewareArgs & + AllMiddlewareArgs; diff --git a/app-nest/src/bolt/types/bolt-event-types.ts b/app-nest/src/bolt/types/bolt-event-types.ts new file mode 100644 index 0000000..9d73a77 --- /dev/null +++ b/app-nest/src/bolt/types/bolt-event-types.ts @@ -0,0 +1,17 @@ +import { + AllMiddlewareArgs, + AppHomeOpenedEvent, + SlackEventMiddlewareArgs, + UserProfileChangedEvent, +} from "@slack/bolt"; +import { StringIndexed } from "@slack/bolt/dist/types/helpers"; + +export type AppHomeOpenedArgs = SlackEventMiddlewareArgs< + AppHomeOpenedEvent["type"] +> & + AllMiddlewareArgs; + +export type UserProfileChangedArgs = SlackEventMiddlewareArgs< + UserProfileChangedEvent["type"] +> & + AllMiddlewareArgs; diff --git a/app-nest/src/dev-tools/dev-tools.controller.ts b/app-nest/src/dev-tools/dev-tools.controller.ts index 17096c4..5d8ecb5 100644 --- a/app-nest/src/dev-tools/dev-tools.controller.ts +++ b/app-nest/src/dev-tools/dev-tools.controller.ts @@ -1,6 +1,7 @@ import { Controller } from "@nestjs/common"; import BoltAction from "../bolt/decorators/bolt-action.decorator"; import BoltActions from "../bolt/enums/bolt-actions.enum"; +import { BoltActionArgs } from "../bolt/types/bolt-action-types"; import { UserSyncService } from "../sync/user-sync.service"; @Controller() @@ -8,7 +9,7 @@ export class DevToolsController { constructor(private userSyncService: UserSyncService) {} @BoltAction(BoltActions.SYNC_USERS) - async syncUsers({ ack }) { + async syncUsers({ ack }: BoltActionArgs) { await ack(); await this.userSyncService.syncUsers(); } diff --git a/app-nest/src/gui/tabs/home-tab.controller.ts b/app-nest/src/gui/tabs/home-tab.controller.ts index 5f0ad8a..f1a781b 100644 --- a/app-nest/src/gui/tabs/home-tab.controller.ts +++ b/app-nest/src/gui/tabs/home-tab.controller.ts @@ -1,6 +1,7 @@ import { Controller } from "@nestjs/common"; import BoltEvent from "../../bolt/decorators/bolt-event.decorator"; import BoltEvents from "../../bolt/enums/bolt-events.enum"; +import { AppHomeOpenedArgs } from "../../bolt/types/bolt-event-types"; import { UserService } from "../../entities/user/user.service"; import devTools from "../dev/dev-tools"; @@ -9,7 +10,7 @@ export class HomeTabController { constructor(private userService: UserService) {} @BoltEvent(BoltEvents.APP_HOME_OPENED) - async getView({ event, client, logger }) { + async getView({ event, client, logger }: AppHomeOpenedArgs) { const users = await this.userService.findAll(); const { slackId } = users[0]; diff --git a/app-nest/src/sync/sync.controller.ts b/app-nest/src/sync/sync.controller.ts index c3f41da..8121dd0 100644 --- a/app-nest/src/sync/sync.controller.ts +++ b/app-nest/src/sync/sync.controller.ts @@ -1,6 +1,7 @@ import { Controller } from "@nestjs/common"; import BoltEvent from "../bolt/decorators/bolt-event.decorator"; import BoltEvents from "../bolt/enums/bolt-events.enum"; +import { UserProfileChangedArgs } from "../bolt/types/bolt-event-types"; import { UserSyncService } from "./user-sync.service"; @Controller() @@ -8,7 +9,7 @@ export class SyncController { constructor(private userSyncService: UserSyncService) {} @BoltEvent(BoltEvents.USER_PROFILE_CHANGED) - async userProfileChanged({ event }) { + async userProfileChanged({ event }: UserProfileChangedArgs) { await this.userSyncService.syncUsers(event.user); } } From 1c38424247aca7d118be0b94f54eb34ebcff366b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Tue, 8 Aug 2023 19:07:35 +0300 Subject: [PATCH 04/18] Registration widget draft --- app-nest/src/gui/tabs/home-tab.controller.ts | 24 +----- app-nest/src/gui/tabs/home-tab.view.ts | 78 ++++++++++++++++++++ 2 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 app-nest/src/gui/tabs/home-tab.view.ts diff --git a/app-nest/src/gui/tabs/home-tab.controller.ts b/app-nest/src/gui/tabs/home-tab.controller.ts index f1a781b..6a47971 100644 --- a/app-nest/src/gui/tabs/home-tab.controller.ts +++ b/app-nest/src/gui/tabs/home-tab.controller.ts @@ -3,7 +3,7 @@ import BoltEvent from "../../bolt/decorators/bolt-event.decorator"; import BoltEvents from "../../bolt/enums/bolt-events.enum"; import { AppHomeOpenedArgs } from "../../bolt/types/bolt-event-types"; import { UserService } from "../../entities/user/user.service"; -import devTools from "../dev/dev-tools"; +import getHomeTabBlocks from "./home-tab.view"; @Controller() export class HomeTabController { @@ -19,27 +19,7 @@ export class HomeTabController { user_id: event.user, view: { type: "home", - blocks: [ - ...devTools, - { - type: "section", - text: { - type: "mrkdwn", - text: - "*Welcome controller, <@" + - event.user + - "> :house:* " + - slackId, - }, - }, - { - type: "section", - text: { - type: "mrkdwn", - text: "Learn how home tabs can be more useful and interactive .", - }, - }, - ], + blocks: getHomeTabBlocks(), }, }); diff --git a/app-nest/src/gui/tabs/home-tab.view.ts b/app-nest/src/gui/tabs/home-tab.view.ts new file mode 100644 index 0000000..2f9a571 --- /dev/null +++ b/app-nest/src/gui/tabs/home-tab.view.ts @@ -0,0 +1,78 @@ +import devTools from "../dev/dev-tools"; + +const getHomeTabBlocks = () => [ + ...devTools, + { + type: "header", + text: { + type: "plain_text", + text: "Ilmoittautumiset", + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "Toimistolla", + }, + style: "primary", + }, + { + type: "button", + text: { + type: "plain_text", + text: "Etänä", + }, + }, + { + action_id: "text1234", + type: "static_select", + placeholder: { + type: "plain_text", + text: "Valitse toimipiste", + }, + initial_option: { + text: { + type: "plain_text", + text: "Helsinki", + }, + value: "hki", + }, + options: [ + { + text: { + type: "plain_text", + text: "Helsinki", + }, + value: "hki", + }, + { + text: { + type: "plain_text", + text: "Tampere", + }, + value: "tre", + }, + ], + }, + { + type: "overflow", + options: [ + { + text: { + type: "plain_text", + text: "Poista ilmoittautuminen", + }, + value: "value-0", + }, + ], + action_id: "overflow", + }, + ], + }, +]; + +export default getHomeTabBlocks; From 06c7eb58f0fe7f3d56c31ca321df9d162086cfd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Tue, 8 Aug 2023 21:04:32 +0300 Subject: [PATCH 05/18] Show list of dates in home tab --- app-nest/package-lock.json | 20 +++++++---- app-nest/package.json | 6 ++-- app-nest/src/gui/gui.module.ts | 2 +- app-nest/src/gui/tabs/home/day-list.blocks.ts | 33 +++++++++++++++++++ .../day-list.item.blocks.ts} | 13 +++++--- .../tabs/{ => home}/home-tab.controller.ts | 10 +++--- app-nest/src/gui/tabs/home/home-tab.view.ts | 16 +++++++++ app-nest/tsconfig.json | 1 + 8 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 app-nest/src/gui/tabs/home/day-list.blocks.ts rename app-nest/src/gui/tabs/{home-tab.view.ts => home/day-list.item.blocks.ts} (86%) rename app-nest/src/gui/tabs/{ => home}/home-tab.controller.ts (65%) create mode 100644 app-nest/src/gui/tabs/home/home-tab.view.ts diff --git a/app-nest/package-lock.json b/app-nest/package-lock.json index c1e4bc3..51a4bee 100644 --- a/app-nest/package-lock.json +++ b/app-nest/package-lock.json @@ -16,10 +16,11 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.0", "@slack/bolt": "^3.13.2", + "dayjs": "^1.11.9", + "lodash": "^4.17.21", "pg": "^8.11.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", - "slack-block-builder": "^2.7.2", "typeorm": "^0.3.17" }, "devDependencies": { @@ -29,6 +30,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/lodash": "^4.14.196", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.59.11", @@ -2117,6 +2119,12 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.196", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz", + "integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -3667,6 +3675,11 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -8234,11 +8247,6 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, - "node_modules/slack-block-builder": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/slack-block-builder/-/slack-block-builder-2.7.2.tgz", - "integrity": "sha512-EhoXDveGZWrHL0+AKnm3wu8n9K9usMjJGlbYLLwMSq62J7Qv+a4lO1iJI+NmCE04dm37d/3OlGKxBVoASnynog==" - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/app-nest/package.json b/app-nest/package.json index 2cf747a..c78f88d 100644 --- a/app-nest/package.json +++ b/app-nest/package.json @@ -35,10 +35,11 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.0", "@slack/bolt": "^3.13.2", + "dayjs": "^1.11.9", + "lodash": "^4.17.21", "pg": "^8.11.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", - "slack-block-builder": "^2.7.2", "typeorm": "^0.3.17" }, "devDependencies": { @@ -48,6 +49,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/lodash": "^4.14.196", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.59.11", @@ -81,4 +83,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} diff --git a/app-nest/src/gui/gui.module.ts b/app-nest/src/gui/gui.module.ts index 5f31ac3..afc423d 100644 --- a/app-nest/src/gui/gui.module.ts +++ b/app-nest/src/gui/gui.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { UserModule } from "../entities/user/user.module"; import { UserService } from "../entities/user/user.service"; -import { HomeTabController } from "./tabs/home-tab.controller"; +import { HomeTabController } from "./tabs/home/home-tab.controller"; @Module({ imports: [UserModule], diff --git a/app-nest/src/gui/tabs/home/day-list.blocks.ts b/app-nest/src/gui/tabs/home/day-list.blocks.ts new file mode 100644 index 0000000..8878373 --- /dev/null +++ b/app-nest/src/gui/tabs/home/day-list.blocks.ts @@ -0,0 +1,33 @@ +import dayjs, { Dayjs } from "dayjs"; +import { flatten } from "lodash"; +import getDayListItemBlocks from "./day-list.item.blocks"; + +/** + * Get range of days from today (inclusive) for the next `len` working days (defined as Mon-Fri). + */ +const dayRange = (len: number, days: Dayjs[] = [], i = 0): Dayjs[] => { + if (i >= len) { + return days; + } + + if (days.length === 0) { + days.push(dayjs()); + return dayRange(len, days, i + 1); + } + + const prevDate = days.at(-1); + const prevDay = prevDate.day(); + // Skip Saturdays (day 6) and Sundays (day 0). + const daysToAdd = prevDay === 5 ? 3 : prevDay === 6 ? 2 : 1; + days.push(prevDate.add(daysToAdd, "d")); + + return dayRange(len, days, i + 1); +}; + +const getDayListBlocks = () => { + const dates = dayRange(14); + const blockLists = dates.map((date) => getDayListItemBlocks({ date })); + return flatten(blockLists); +}; + +export default getDayListBlocks; diff --git a/app-nest/src/gui/tabs/home-tab.view.ts b/app-nest/src/gui/tabs/home/day-list.item.blocks.ts similarity index 86% rename from app-nest/src/gui/tabs/home-tab.view.ts rename to app-nest/src/gui/tabs/home/day-list.item.blocks.ts index 2f9a571..87c85de 100644 --- a/app-nest/src/gui/tabs/home-tab.view.ts +++ b/app-nest/src/gui/tabs/home/day-list.item.blocks.ts @@ -1,12 +1,15 @@ -import devTools from "../dev/dev-tools"; +import { Dayjs } from "dayjs"; -const getHomeTabBlocks = () => [ - ...devTools, +type DayListItemProps = { + date: Dayjs; +}; + +const getDayListItemBlocks = ({ date }: DayListItemProps) => [ { type: "header", text: { type: "plain_text", - text: "Ilmoittautumiset", + text: date.format("dd D.M."), }, }, { @@ -75,4 +78,4 @@ const getHomeTabBlocks = () => [ }, ]; -export default getHomeTabBlocks; +export default getDayListItemBlocks; diff --git a/app-nest/src/gui/tabs/home-tab.controller.ts b/app-nest/src/gui/tabs/home/home-tab.controller.ts similarity index 65% rename from app-nest/src/gui/tabs/home-tab.controller.ts rename to app-nest/src/gui/tabs/home/home-tab.controller.ts index 6a47971..6d8ac1b 100644 --- a/app-nest/src/gui/tabs/home-tab.controller.ts +++ b/app-nest/src/gui/tabs/home/home-tab.controller.ts @@ -1,8 +1,9 @@ import { Controller } from "@nestjs/common"; -import BoltEvent from "../../bolt/decorators/bolt-event.decorator"; -import BoltEvents from "../../bolt/enums/bolt-events.enum"; -import { AppHomeOpenedArgs } from "../../bolt/types/bolt-event-types"; -import { UserService } from "../../entities/user/user.service"; +import BoltEvent from "../../../bolt/decorators/bolt-event.decorator"; +import BoltEvents from "../../../bolt/enums/bolt-events.enum"; +import { AppHomeOpenedArgs } from "../../../bolt/types/bolt-event-types"; +import { UserService } from "../../../entities/user/user.service"; +import getDayListBlocks from "./day-list.blocks"; import getHomeTabBlocks from "./home-tab.view"; @Controller() @@ -13,6 +14,7 @@ export class HomeTabController { async getView({ event, client, logger }: AppHomeOpenedArgs) { const users = await this.userService.findAll(); const { slackId } = users[0]; + getDayListBlocks(); try { const result = await client.views.publish({ diff --git a/app-nest/src/gui/tabs/home/home-tab.view.ts b/app-nest/src/gui/tabs/home/home-tab.view.ts new file mode 100644 index 0000000..22b7f6f --- /dev/null +++ b/app-nest/src/gui/tabs/home/home-tab.view.ts @@ -0,0 +1,16 @@ +import devTools from "../../dev/dev-tools"; +import getDayListBlocks from "./day-list.blocks"; + +const getHomeTabBlocks = () => [ + ...devTools, + { + type: "header", + text: { + type: "plain_text", + text: "Ilmoittautumiset", + }, + }, + ...getDayListBlocks(), +]; + +export default getHomeTabBlocks; diff --git a/app-nest/tsconfig.json b/app-nest/tsconfig.json index 95f5641..28ed2de 100644 --- a/app-nest/tsconfig.json +++ b/app-nest/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "commonjs", + "esModuleInterop": true, "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, From 8ffcdaad1b7048e95d73856fffdeeca7047a0a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Tue, 8 Aug 2023 21:49:09 +0300 Subject: [PATCH 06/18] Add presence entity and registration action prototype --- app-nest/src/app.module.ts | 2 ++ app-nest/src/bolt/enums/bolt-actions.enum.ts | 1 + .../entities/presence/presence.controller.ts | 18 ++++++++++++++++++ .../src/entities/presence/presence.entity.ts | 15 +++++++++++++++ .../src/entities/presence/presence.module.ts | 13 +++++++++++++ .../src/entities/presence/presence.service.ts | 14 ++++++++++++++ app-nest/src/gui/dev/dev-tools.ts | 4 +++- ....item.blocks.ts => day-list-item.blocks.ts} | 3 +++ app-nest/src/gui/tabs/home/day-list.blocks.ts | 2 +- 9 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 app-nest/src/entities/presence/presence.controller.ts create mode 100644 app-nest/src/entities/presence/presence.entity.ts create mode 100644 app-nest/src/entities/presence/presence.module.ts create mode 100644 app-nest/src/entities/presence/presence.service.ts rename app-nest/src/gui/tabs/home/{day-list.item.blocks.ts => day-list-item.blocks.ts} (91%) diff --git a/app-nest/src/app.module.ts b/app-nest/src/app.module.ts index e3e0f1f..08f9c7d 100644 --- a/app-nest/src/app.module.ts +++ b/app-nest/src/app.module.ts @@ -5,6 +5,7 @@ import configuration from "./config/configuration"; import { inDevelopmentEnvironment } from "./config/utils"; import { DevToolsModule } from "./dev-tools/dev-tools.module"; import { EntitiesModule } from "./entities/entities.module"; +import { PresenceModule } from "./entities/presence/presence.module"; import { GuiModule } from "./gui/gui.module"; import { SyncModule } from "./sync/sync.module"; @@ -29,6 +30,7 @@ import { SyncModule } from "./sync/sync.module"; EntitiesModule, SyncModule, DevToolsModule, + PresenceModule, ], }) export class AppModule {} diff --git a/app-nest/src/bolt/enums/bolt-actions.enum.ts b/app-nest/src/bolt/enums/bolt-actions.enum.ts index 7dc2a02..4fc5dfc 100644 --- a/app-nest/src/bolt/enums/bolt-actions.enum.ts +++ b/app-nest/src/bolt/enums/bolt-actions.enum.ts @@ -1,5 +1,6 @@ enum BoltActions { SYNC_USERS = "sync_users", + REGISTER_PRESENCE = "register_presence", } export default BoltActions; diff --git a/app-nest/src/entities/presence/presence.controller.ts b/app-nest/src/entities/presence/presence.controller.ts new file mode 100644 index 0000000..454f114 --- /dev/null +++ b/app-nest/src/entities/presence/presence.controller.ts @@ -0,0 +1,18 @@ +import { Controller } from "@nestjs/common"; +import dayjs from "dayjs"; +import BoltAction from "../../bolt/decorators/bolt-action.decorator"; +import BoltActions from "../../bolt/enums/bolt-actions.enum"; +import { BoltActionArgs } from "../../bolt/types/bolt-action-types"; +import { PresenceService } from "./presence.service"; + +@Controller() +export class PresenceController { + constructor(private presenceService: PresenceService) {} + + @BoltAction(BoltActions.REGISTER_PRESENCE) + async registerPresence({ ack, body, payload }: BoltActionArgs) { + await ack(); + const date = dayjs(payload["value"]).toDate(); + await this.presenceService.add({ userId: body.user.id, date }); + } +} diff --git a/app-nest/src/entities/presence/presence.entity.ts b/app-nest/src/entities/presence/presence.entity.ts new file mode 100644 index 0000000..73c9d72 --- /dev/null +++ b/app-nest/src/entities/presence/presence.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity, PrimaryGeneratedColumn, Repository } from "typeorm"; + +@Entity() +export class Presence { + @PrimaryGeneratedColumn() + id: number; + + @Column() + userId: string; + + @Column({ type: "timestamptz" }) + date: Date; +} + +export type PresenceRepository = Repository; diff --git a/app-nest/src/entities/presence/presence.module.ts b/app-nest/src/entities/presence/presence.module.ts new file mode 100644 index 0000000..7f08191 --- /dev/null +++ b/app-nest/src/entities/presence/presence.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { PresenceController } from "./presence.controller"; +import { Presence } from "./presence.entity"; +import { PresenceService } from "./presence.service"; + +@Module({ + imports: [TypeOrmModule.forFeature([Presence])], + providers: [PresenceService], + controllers: [PresenceController], + exports: [TypeOrmModule], +}) +export class PresenceModule {} diff --git a/app-nest/src/entities/presence/presence.service.ts b/app-nest/src/entities/presence/presence.service.ts new file mode 100644 index 0000000..866b944 --- /dev/null +++ b/app-nest/src/entities/presence/presence.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Presence, PresenceRepository } from "./presence.entity"; + +@Injectable() +export class PresenceService { + constructor( + @InjectRepository(Presence) private presenceRepository: PresenceRepository, + ) {} + + async add(presence: Partial) { + return this.presenceRepository.save(presence); + } +} diff --git a/app-nest/src/gui/dev/dev-tools.ts b/app-nest/src/gui/dev/dev-tools.ts index b0e8b9f..eb17914 100644 --- a/app-nest/src/gui/dev/dev-tools.ts +++ b/app-nest/src/gui/dev/dev-tools.ts @@ -1,3 +1,5 @@ +import BoltActions from "../../bolt/enums/bolt-actions.enum"; + const devTools = [ { type: "header", @@ -15,7 +17,7 @@ const devTools = [ type: "plain_text", text: ":recycle: Sync Users", }, - action_id: "sync_users", + action_id: BoltActions.SYNC_USERS, }, ], }, diff --git a/app-nest/src/gui/tabs/home/day-list.item.blocks.ts b/app-nest/src/gui/tabs/home/day-list-item.blocks.ts similarity index 91% rename from app-nest/src/gui/tabs/home/day-list.item.blocks.ts rename to app-nest/src/gui/tabs/home/day-list-item.blocks.ts index 87c85de..6b728f4 100644 --- a/app-nest/src/gui/tabs/home/day-list.item.blocks.ts +++ b/app-nest/src/gui/tabs/home/day-list-item.blocks.ts @@ -1,4 +1,5 @@ import { Dayjs } from "dayjs"; +import BoltActions from "../../../bolt/enums/bolt-actions.enum"; type DayListItemProps = { date: Dayjs; @@ -22,6 +23,8 @@ const getDayListItemBlocks = ({ date }: DayListItemProps) => [ text: "Toimistolla", }, style: "primary", + action_id: BoltActions.REGISTER_PRESENCE, + value: date.toISOString(), }, { type: "button", diff --git a/app-nest/src/gui/tabs/home/day-list.blocks.ts b/app-nest/src/gui/tabs/home/day-list.blocks.ts index 8878373..388209c 100644 --- a/app-nest/src/gui/tabs/home/day-list.blocks.ts +++ b/app-nest/src/gui/tabs/home/day-list.blocks.ts @@ -1,6 +1,6 @@ import dayjs, { Dayjs } from "dayjs"; import { flatten } from "lodash"; -import getDayListItemBlocks from "./day-list.item.blocks"; +import getDayListItemBlocks from "./day-list-item.blocks"; /** * Get range of days from today (inclusive) for the next `len` working days (defined as Mon-Fri). From 45801f461a1b01f4ca54ada3da96398ee9852910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Wed, 9 Aug 2023 07:46:09 +0300 Subject: [PATCH 07/18] Improve presence model --- .../entities/presence/presence.controller.ts | 7 ++++++- .../src/entities/presence/presence.entity.ts | 17 +++++++++++------ .../src/entities/presence/presence.service.ts | 12 ++++++++++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/app-nest/src/entities/presence/presence.controller.ts b/app-nest/src/entities/presence/presence.controller.ts index 454f114..6a73d15 100644 --- a/app-nest/src/entities/presence/presence.controller.ts +++ b/app-nest/src/entities/presence/presence.controller.ts @@ -3,6 +3,7 @@ import dayjs from "dayjs"; import BoltAction from "../../bolt/decorators/bolt-action.decorator"; import BoltActions from "../../bolt/enums/bolt-actions.enum"; import { BoltActionArgs } from "../../bolt/types/bolt-action-types"; +import { PresenceType } from "./presence.entity"; import { PresenceService } from "./presence.service"; @Controller() @@ -13,6 +14,10 @@ export class PresenceController { async registerPresence({ ack, body, payload }: BoltActionArgs) { await ack(); const date = dayjs(payload["value"]).toDate(); - await this.presenceService.add({ userId: body.user.id, date }); + await this.presenceService.upsert({ + userId: body.user.id, + type: PresenceType.AT_OFFICE, + date, + }); } } diff --git a/app-nest/src/entities/presence/presence.entity.ts b/app-nest/src/entities/presence/presence.entity.ts index 73c9d72..028e15c 100644 --- a/app-nest/src/entities/presence/presence.entity.ts +++ b/app-nest/src/entities/presence/presence.entity.ts @@ -1,15 +1,20 @@ -import { Column, Entity, PrimaryGeneratedColumn, Repository } from "typeorm"; +import { Column, Entity, PrimaryColumn, Repository } from "typeorm"; + +export enum PresenceType { + AT_OFFICE = "at_office", + REMOTE = "remote", +} @Entity() export class Presence { - @PrimaryGeneratedColumn() - id: number; - - @Column() + @PrimaryColumn() userId: string; - @Column({ type: "timestamptz" }) + @PrimaryColumn({ type: "date" }) date: Date; + + @Column({ type: "enum", enum: PresenceType }) + type: PresenceType; } export type PresenceRepository = Repository; diff --git a/app-nest/src/entities/presence/presence.service.ts b/app-nest/src/entities/presence/presence.service.ts index 866b944..09850e1 100644 --- a/app-nest/src/entities/presence/presence.service.ts +++ b/app-nest/src/entities/presence/presence.service.ts @@ -1,14 +1,22 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; +import { DataSource } from "typeorm"; import { Presence, PresenceRepository } from "./presence.entity"; @Injectable() export class PresenceService { constructor( @InjectRepository(Presence) private presenceRepository: PresenceRepository, + private dataSource: DataSource, ) {} - async add(presence: Partial) { - return this.presenceRepository.save(presence); + async upsert(presence: Presence) { + return this.dataSource + .createQueryBuilder() + .insert() + .into(Presence) + .values(presence) + .orUpdate(["type"], ["userId", "date"]) + .execute(); } } From 5c7d0dcd227b2a997ec5983fc61e2cd8456c5d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Wed, 9 Aug 2023 08:14:22 +0300 Subject: [PATCH 08/18] Select office by day through UI --- app-nest/src/bolt/enums/bolt-actions.enum.ts | 1 + app-nest/src/entities/presence/presence.controller.ts | 11 +++++++++++ app-nest/src/entities/presence/presence.entity.ts | 7 +++++-- app-nest/src/entities/presence/presence.service.ts | 11 +++++++++-- app-nest/src/gui/tabs/home/day-list-item.blocks.ts | 8 ++++---- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app-nest/src/bolt/enums/bolt-actions.enum.ts b/app-nest/src/bolt/enums/bolt-actions.enum.ts index 4fc5dfc..109835f 100644 --- a/app-nest/src/bolt/enums/bolt-actions.enum.ts +++ b/app-nest/src/bolt/enums/bolt-actions.enum.ts @@ -1,6 +1,7 @@ enum BoltActions { SYNC_USERS = "sync_users", REGISTER_PRESENCE = "register_presence", + SELECT_OFFICE_FOR_DATE = "select_office_for_date", } export default BoltActions; diff --git a/app-nest/src/entities/presence/presence.controller.ts b/app-nest/src/entities/presence/presence.controller.ts index 6a73d15..9c6eca2 100644 --- a/app-nest/src/entities/presence/presence.controller.ts +++ b/app-nest/src/entities/presence/presence.controller.ts @@ -20,4 +20,15 @@ export class PresenceController { date, }); } + + @BoltAction(BoltActions.SELECT_OFFICE_FOR_DATE) + async selectOfficeForDate({ ack, body, payload }: BoltActionArgs) { + await ack(); + const { value, date } = JSON.parse(payload["selected_option"].value); + await this.presenceService.upsert({ + userId: body.user.id, + date: dayjs(date).toDate(), + office: value, + }); + } } diff --git a/app-nest/src/entities/presence/presence.entity.ts b/app-nest/src/entities/presence/presence.entity.ts index 028e15c..26d2dcb 100644 --- a/app-nest/src/entities/presence/presence.entity.ts +++ b/app-nest/src/entities/presence/presence.entity.ts @@ -13,8 +13,11 @@ export class Presence { @PrimaryColumn({ type: "date" }) date: Date; - @Column({ type: "enum", enum: PresenceType }) - type: PresenceType; + @Column({ type: "enum", enum: PresenceType, nullable: true }) + type: PresenceType | null; + + @Column({ nullable: true }) + office: string | null; } export type PresenceRepository = Repository; diff --git a/app-nest/src/entities/presence/presence.service.ts b/app-nest/src/entities/presence/presence.service.ts index 09850e1..6c3ae5c 100644 --- a/app-nest/src/entities/presence/presence.service.ts +++ b/app-nest/src/entities/presence/presence.service.ts @@ -10,13 +10,20 @@ export class PresenceService { private dataSource: DataSource, ) {} - async upsert(presence: Presence) { + async upsert(presence: Partial) { + // Select only existing cols for the upsert operation to avoid overriding + // existing data with defaults/nulls. + const primaryKeys = ["userId", "date"]; + const updatableCols = Object.keys(presence).filter( + (key) => !primaryKeys.includes(key), + ); + return this.dataSource .createQueryBuilder() .insert() .into(Presence) .values(presence) - .orUpdate(["type"], ["userId", "date"]) + .orUpdate(updatableCols, primaryKeys) .execute(); } } diff --git a/app-nest/src/gui/tabs/home/day-list-item.blocks.ts b/app-nest/src/gui/tabs/home/day-list-item.blocks.ts index 6b728f4..1f6d01b 100644 --- a/app-nest/src/gui/tabs/home/day-list-item.blocks.ts +++ b/app-nest/src/gui/tabs/home/day-list-item.blocks.ts @@ -34,7 +34,7 @@ const getDayListItemBlocks = ({ date }: DayListItemProps) => [ }, }, { - action_id: "text1234", + action_id: BoltActions.SELECT_OFFICE_FOR_DATE, type: "static_select", placeholder: { type: "plain_text", @@ -45,7 +45,7 @@ const getDayListItemBlocks = ({ date }: DayListItemProps) => [ type: "plain_text", text: "Helsinki", }, - value: "hki", + value: JSON.stringify({ value: "hki", date: date.toISOString() }), }, options: [ { @@ -53,14 +53,14 @@ const getDayListItemBlocks = ({ date }: DayListItemProps) => [ type: "plain_text", text: "Helsinki", }, - value: "hki", + value: JSON.stringify({ value: "hki", date: date.toISOString() }), }, { text: { type: "plain_text", text: "Tampere", }, - value: "tre", + value: JSON.stringify({ value: "tre", date: date.toISOString() }), }, ], }, From e1898b05c4fe53b50121d46eb35a1ff0cc5f52c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Wed, 9 Aug 2023 09:07:27 +0300 Subject: [PATCH 09/18] Set remote presence through UI --- app-nest/src/bolt/enums/bolt-actions.enum.ts | 3 ++- .../src/entities/presence/presence.controller.ts | 15 +++++++++++++-- .../src/gui/tabs/home/day-list-item.blocks.ts | 4 +++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app-nest/src/bolt/enums/bolt-actions.enum.ts b/app-nest/src/bolt/enums/bolt-actions.enum.ts index 109835f..dca1ab2 100644 --- a/app-nest/src/bolt/enums/bolt-actions.enum.ts +++ b/app-nest/src/bolt/enums/bolt-actions.enum.ts @@ -1,6 +1,7 @@ enum BoltActions { SYNC_USERS = "sync_users", - REGISTER_PRESENCE = "register_presence", + SET_OFFICE_PRESENCE = "set_office_presence", + SET_REMOTE_PRESENCE = "set_remote_presence", SELECT_OFFICE_FOR_DATE = "select_office_for_date", } diff --git a/app-nest/src/entities/presence/presence.controller.ts b/app-nest/src/entities/presence/presence.controller.ts index 9c6eca2..03f4b45 100644 --- a/app-nest/src/entities/presence/presence.controller.ts +++ b/app-nest/src/entities/presence/presence.controller.ts @@ -10,8 +10,8 @@ import { PresenceService } from "./presence.service"; export class PresenceController { constructor(private presenceService: PresenceService) {} - @BoltAction(BoltActions.REGISTER_PRESENCE) - async registerPresence({ ack, body, payload }: BoltActionArgs) { + @BoltAction(BoltActions.SET_OFFICE_PRESENCE) + async setOfficePresence({ ack, body, payload }: BoltActionArgs) { await ack(); const date = dayjs(payload["value"]).toDate(); await this.presenceService.upsert({ @@ -21,6 +21,17 @@ export class PresenceController { }); } + @BoltAction(BoltActions.SET_REMOTE_PRESENCE) + async setRemotePresence({ ack, body, payload }: BoltActionArgs) { + await ack(); + const date = dayjs(payload["value"]).toDate(); + await this.presenceService.upsert({ + userId: body.user.id, + type: PresenceType.REMOTE, + date, + }); + } + @BoltAction(BoltActions.SELECT_OFFICE_FOR_DATE) async selectOfficeForDate({ ack, body, payload }: BoltActionArgs) { await ack(); diff --git a/app-nest/src/gui/tabs/home/day-list-item.blocks.ts b/app-nest/src/gui/tabs/home/day-list-item.blocks.ts index 1f6d01b..a13df3a 100644 --- a/app-nest/src/gui/tabs/home/day-list-item.blocks.ts +++ b/app-nest/src/gui/tabs/home/day-list-item.blocks.ts @@ -23,7 +23,7 @@ const getDayListItemBlocks = ({ date }: DayListItemProps) => [ text: "Toimistolla", }, style: "primary", - action_id: BoltActions.REGISTER_PRESENCE, + action_id: BoltActions.SET_OFFICE_PRESENCE, value: date.toISOString(), }, { @@ -32,6 +32,8 @@ const getDayListItemBlocks = ({ date }: DayListItemProps) => [ type: "plain_text", text: "Etänä", }, + action_id: BoltActions.SET_REMOTE_PRESENCE, + value: date.toISOString(), }, { action_id: BoltActions.SELECT_OFFICE_FOR_DATE, From d731d42f9309c5eb2970d6a42bff1a0b00458cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Wed, 9 Aug 2023 10:28:57 +0300 Subject: [PATCH 10/18] Remove presence through UI --- app-nest/src/bolt/enums/bolt-actions.enum.ts | 1 + .../entities/presence/presence.controller.ts | 18 +++++++++++++++++- .../src/entities/presence/presence.service.ts | 4 ++++ .../src/gui/tabs/home/day-list-item.blocks.ts | 7 +++++-- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app-nest/src/bolt/enums/bolt-actions.enum.ts b/app-nest/src/bolt/enums/bolt-actions.enum.ts index dca1ab2..e80493e 100644 --- a/app-nest/src/bolt/enums/bolt-actions.enum.ts +++ b/app-nest/src/bolt/enums/bolt-actions.enum.ts @@ -3,6 +3,7 @@ enum BoltActions { SET_OFFICE_PRESENCE = "set_office_presence", SET_REMOTE_PRESENCE = "set_remote_presence", SELECT_OFFICE_FOR_DATE = "select_office_for_date", + DAY_LIST_ITEM_OVERFLOW = "day_list_item_overflow", } export default BoltActions; diff --git a/app-nest/src/entities/presence/presence.controller.ts b/app-nest/src/entities/presence/presence.controller.ts index 03f4b45..af5ea21 100644 --- a/app-nest/src/entities/presence/presence.controller.ts +++ b/app-nest/src/entities/presence/presence.controller.ts @@ -1,4 +1,4 @@ -import { Controller } from "@nestjs/common"; +import { Controller, InternalServerErrorException } from "@nestjs/common"; import dayjs from "dayjs"; import BoltAction from "../../bolt/decorators/bolt-action.decorator"; import BoltActions from "../../bolt/enums/bolt-actions.enum"; @@ -42,4 +42,20 @@ export class PresenceController { office: value, }); } + + // TODO: Should this be moved? + @BoltAction(BoltActions.DAY_LIST_ITEM_OVERFLOW) + async dayListItemOverflow({ ack, body, payload }: BoltActionArgs) { + await ack(); + const { type, date } = JSON.parse(payload["selected_option"].value); + + if (type !== "remove_presence") { + throw new InternalServerErrorException("Not implemented."); + } + + await this.presenceService.remove({ + userId: body.user.id, + date: dayjs(date).toDate(), + }); + } } diff --git a/app-nest/src/entities/presence/presence.service.ts b/app-nest/src/entities/presence/presence.service.ts index 6c3ae5c..2a9bd92 100644 --- a/app-nest/src/entities/presence/presence.service.ts +++ b/app-nest/src/entities/presence/presence.service.ts @@ -26,4 +26,8 @@ export class PresenceService { .orUpdate(updatableCols, primaryKeys) .execute(); } + + async remove(presence: Pick) { + return this.presenceRepository.delete(presence); + } } diff --git a/app-nest/src/gui/tabs/home/day-list-item.blocks.ts b/app-nest/src/gui/tabs/home/day-list-item.blocks.ts index a13df3a..c8d5fc4 100644 --- a/app-nest/src/gui/tabs/home/day-list-item.blocks.ts +++ b/app-nest/src/gui/tabs/home/day-list-item.blocks.ts @@ -74,10 +74,13 @@ const getDayListItemBlocks = ({ date }: DayListItemProps) => [ type: "plain_text", text: "Poista ilmoittautuminen", }, - value: "value-0", + value: JSON.stringify({ + type: "remove_presence", + date: date.toISOString(), + }), }, ], - action_id: "overflow", + action_id: BoltActions.DAY_LIST_ITEM_OVERFLOW, }, ], }, From b6da9ad32db9abef54927737023f12a58e97ae7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Wed, 9 Aug 2023 14:54:19 +0300 Subject: [PATCH 11/18] Refactor view code to use slack-block-builder --- app-nest/package-lock.json | 6 ++ app-nest/package.json | 1 + app-nest/src/gui/dev/dev-tools.builder.ts | 22 +++++ app-nest/src/gui/dev/dev-tools.module.ts | 5 ++ app-nest/src/gui/dev/dev-tools.ts | 38 -------- app-nest/src/gui/gui.module.ts | 8 +- .../src/gui/tabs/home/day-list-item.blocks.ts | 89 ------------------- .../gui/tabs/home/day-list-item.builder.ts | 67 ++++++++++++++ ...day-list.blocks.ts => day-list.builder.ts} | 20 +++-- .../src/gui/tabs/home/home-tab.builder.ts | 19 ++++ .../src/gui/tabs/home/home-tab.controller.ts | 19 ++-- app-nest/src/gui/tabs/home/home-tab.module.ts | 13 +++ app-nest/src/gui/tabs/home/home-tab.view.ts | 16 ---- 13 files changed, 156 insertions(+), 167 deletions(-) create mode 100644 app-nest/src/gui/dev/dev-tools.builder.ts create mode 100644 app-nest/src/gui/dev/dev-tools.module.ts delete mode 100644 app-nest/src/gui/dev/dev-tools.ts delete mode 100644 app-nest/src/gui/tabs/home/day-list-item.blocks.ts create mode 100644 app-nest/src/gui/tabs/home/day-list-item.builder.ts rename app-nest/src/gui/tabs/home/{day-list.blocks.ts => day-list.builder.ts} (61%) create mode 100644 app-nest/src/gui/tabs/home/home-tab.builder.ts create mode 100644 app-nest/src/gui/tabs/home/home-tab.module.ts delete mode 100644 app-nest/src/gui/tabs/home/home-tab.view.ts diff --git a/app-nest/package-lock.json b/app-nest/package-lock.json index 51a4bee..e7ef30b 100644 --- a/app-nest/package-lock.json +++ b/app-nest/package-lock.json @@ -21,6 +21,7 @@ "pg": "^8.11.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", + "slack-block-builder": "^2.7.2", "typeorm": "^0.3.17" }, "devDependencies": { @@ -8247,6 +8248,11 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, + "node_modules/slack-block-builder": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/slack-block-builder/-/slack-block-builder-2.7.2.tgz", + "integrity": "sha512-EhoXDveGZWrHL0+AKnm3wu8n9K9usMjJGlbYLLwMSq62J7Qv+a4lO1iJI+NmCE04dm37d/3OlGKxBVoASnynog==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/app-nest/package.json b/app-nest/package.json index c78f88d..29a5d91 100644 --- a/app-nest/package.json +++ b/app-nest/package.json @@ -40,6 +40,7 @@ "pg": "^8.11.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", + "slack-block-builder": "^2.7.2", "typeorm": "^0.3.17" }, "devDependencies": { diff --git a/app-nest/src/gui/dev/dev-tools.builder.ts b/app-nest/src/gui/dev/dev-tools.builder.ts new file mode 100644 index 0000000..1be0c51 --- /dev/null +++ b/app-nest/src/gui/dev/dev-tools.builder.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@nestjs/common"; +import { Actions, Button, Context, Divider, Header } from "slack-block-builder"; +import BoltActions from "../../bolt/enums/bolt-actions.enum"; + +@Injectable() +export class DevToolsBuilder { + build() { + return [ + Header({ text: ":wrench: Developer Tools" }), + Actions().elements( + Button({ + text: ":recycle: Sync Users", + actionId: BoltActions.SYNC_USERS, + }), + ), + Context().elements( + "In development environment, users are not synchronized between local database and Slack on app start to avoid running into API rate limits due to hot-reloads. Sync users manually when necessary.", + ), + Divider(), + ]; + } +} diff --git a/app-nest/src/gui/dev/dev-tools.module.ts b/app-nest/src/gui/dev/dev-tools.module.ts new file mode 100644 index 0000000..8aefa0a --- /dev/null +++ b/app-nest/src/gui/dev/dev-tools.module.ts @@ -0,0 +1,5 @@ +import { Module } from "@nestjs/common"; +import { DevToolsBuilder } from "./dev-tools.builder"; + +@Module({ providers: [DevToolsBuilder], exports: [DevToolsBuilder] }) +export class DevToolsModule {} diff --git a/app-nest/src/gui/dev/dev-tools.ts b/app-nest/src/gui/dev/dev-tools.ts deleted file mode 100644 index eb17914..0000000 --- a/app-nest/src/gui/dev/dev-tools.ts +++ /dev/null @@ -1,38 +0,0 @@ -import BoltActions from "../../bolt/enums/bolt-actions.enum"; - -const devTools = [ - { - type: "header", - text: { - type: "plain_text", - text: ":wrench: Developer Tools", - }, - }, - { - type: "actions", - elements: [ - { - type: "button", - text: { - type: "plain_text", - text: ":recycle: Sync Users", - }, - action_id: BoltActions.SYNC_USERS, - }, - ], - }, - { - type: "context", - elements: [ - { - type: "plain_text", - text: "In development environment, users are not synchronized between local database and Slack on app start to avoid running into API rate limits due to hot-reloads. Sync users manually when necessary.", - }, - ], - }, - { - type: "divider", - }, -]; - -export default devTools; diff --git a/app-nest/src/gui/gui.module.ts b/app-nest/src/gui/gui.module.ts index afc423d..9e0fc81 100644 --- a/app-nest/src/gui/gui.module.ts +++ b/app-nest/src/gui/gui.module.ts @@ -1,11 +1,7 @@ import { Module } from "@nestjs/common"; -import { UserModule } from "../entities/user/user.module"; -import { UserService } from "../entities/user/user.service"; -import { HomeTabController } from "./tabs/home/home-tab.controller"; +import { HomeTabModule } from "./tabs/home/home-tab.module"; @Module({ - imports: [UserModule], - providers: [UserService], - controllers: [HomeTabController], + imports: [HomeTabModule], }) export class GuiModule {} diff --git a/app-nest/src/gui/tabs/home/day-list-item.blocks.ts b/app-nest/src/gui/tabs/home/day-list-item.blocks.ts deleted file mode 100644 index c8d5fc4..0000000 --- a/app-nest/src/gui/tabs/home/day-list-item.blocks.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Dayjs } from "dayjs"; -import BoltActions from "../../../bolt/enums/bolt-actions.enum"; - -type DayListItemProps = { - date: Dayjs; -}; - -const getDayListItemBlocks = ({ date }: DayListItemProps) => [ - { - type: "header", - text: { - type: "plain_text", - text: date.format("dd D.M."), - }, - }, - { - type: "actions", - elements: [ - { - type: "button", - text: { - type: "plain_text", - text: "Toimistolla", - }, - style: "primary", - action_id: BoltActions.SET_OFFICE_PRESENCE, - value: date.toISOString(), - }, - { - type: "button", - text: { - type: "plain_text", - text: "Etänä", - }, - action_id: BoltActions.SET_REMOTE_PRESENCE, - value: date.toISOString(), - }, - { - action_id: BoltActions.SELECT_OFFICE_FOR_DATE, - type: "static_select", - placeholder: { - type: "plain_text", - text: "Valitse toimipiste", - }, - initial_option: { - text: { - type: "plain_text", - text: "Helsinki", - }, - value: JSON.stringify({ value: "hki", date: date.toISOString() }), - }, - options: [ - { - text: { - type: "plain_text", - text: "Helsinki", - }, - value: JSON.stringify({ value: "hki", date: date.toISOString() }), - }, - { - text: { - type: "plain_text", - text: "Tampere", - }, - value: JSON.stringify({ value: "tre", date: date.toISOString() }), - }, - ], - }, - { - type: "overflow", - options: [ - { - text: { - type: "plain_text", - text: "Poista ilmoittautuminen", - }, - value: JSON.stringify({ - type: "remove_presence", - date: date.toISOString(), - }), - }, - ], - action_id: BoltActions.DAY_LIST_ITEM_OVERFLOW, - }, - ], - }, -]; - -export default getDayListItemBlocks; diff --git a/app-nest/src/gui/tabs/home/day-list-item.builder.ts b/app-nest/src/gui/tabs/home/day-list-item.builder.ts new file mode 100644 index 0000000..ad71e10 --- /dev/null +++ b/app-nest/src/gui/tabs/home/day-list-item.builder.ts @@ -0,0 +1,67 @@ +import { Injectable } from "@nestjs/common"; +import { Dayjs } from "dayjs"; +import { + Actions, + Button, + Header, + Option, + OverflowMenu, + StaticSelect, +} from "slack-block-builder"; +import BoltActions from "../../../bolt/enums/bolt-actions.enum"; + +type DayListItemProps = { + date: Dayjs; +}; + +@Injectable() +export class DayListItemBuilder { + build({ date }: DayListItemProps) { + const dateString = date.toISOString(); + + return [ + Header({ text: date.format("dd D.M.") }), + Actions().elements( + Button({ + text: "Toimistolla", + actionId: BoltActions.SET_OFFICE_PRESENCE, + value: dateString, + }), + Button({ + text: "Etänä", + actionId: BoltActions.SET_REMOTE_PRESENCE, + value: dateString, + }), + StaticSelect({ + placeholder: "Valitse toimipiste", + actionId: BoltActions.SELECT_OFFICE_FOR_DATE, + }) + .initialOption( + Option({ + text: "Helsinki", + value: JSON.stringify({ value: "hki", date }), + }), + ) + .options( + Option({ + text: "Helsinki", + value: JSON.stringify({ value: "hki", date }), + }), + Option({ + text: "Tampere", + value: JSON.stringify({ value: "tre", date }), + }), + ), + OverflowMenu({ actionId: BoltActions.DAY_LIST_ITEM_OVERFLOW }).options( + Option({ + text: "Poista ilmoittautuminen", + value: JSON.stringify({ + type: "remove_presence", + date, + }), + }), + ), + ), + ]; + } +} diff --git a/app-nest/src/gui/tabs/home/day-list.blocks.ts b/app-nest/src/gui/tabs/home/day-list.builder.ts similarity index 61% rename from app-nest/src/gui/tabs/home/day-list.blocks.ts rename to app-nest/src/gui/tabs/home/day-list.builder.ts index 388209c..cf7d869 100644 --- a/app-nest/src/gui/tabs/home/day-list.blocks.ts +++ b/app-nest/src/gui/tabs/home/day-list.builder.ts @@ -1,6 +1,7 @@ +import { Injectable } from "@nestjs/common"; import dayjs, { Dayjs } from "dayjs"; import { flatten } from "lodash"; -import getDayListItemBlocks from "./day-list-item.blocks"; +import { DayListItemBuilder } from "./day-list-item.builder"; /** * Get range of days from today (inclusive) for the next `len` working days (defined as Mon-Fri). @@ -24,10 +25,15 @@ const dayRange = (len: number, days: Dayjs[] = [], i = 0): Dayjs[] => { return dayRange(len, days, i + 1); }; -const getDayListBlocks = () => { - const dates = dayRange(14); - const blockLists = dates.map((date) => getDayListItemBlocks({ date })); - return flatten(blockLists); -}; +@Injectable() +export class DayListBuilder { + constructor(private dayListItemBuilder: DayListItemBuilder) {} -export default getDayListBlocks; + async build() { + const dates = dayRange(14); + const blockLists = dates.map((date) => + this.dayListItemBuilder.build({ date }), + ); + return flatten(blockLists); + } +} diff --git a/app-nest/src/gui/tabs/home/home-tab.builder.ts b/app-nest/src/gui/tabs/home/home-tab.builder.ts new file mode 100644 index 0000000..6f76b64 --- /dev/null +++ b/app-nest/src/gui/tabs/home/home-tab.builder.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@nestjs/common"; +import { Header } from "slack-block-builder"; +import { DevToolsBuilder } from "../../dev/dev-tools.builder"; +import { DayListBuilder } from "./day-list.builder"; + +// TODO: Create interface for these kind of builder classes. +@Injectable() +export class HomeTabBuilder { + constructor( + private dayListBlocks: DayListBuilder, + private devToolsBuilder: DevToolsBuilder, + ) {} + + async build() { + const devTools = this.devToolsBuilder.build(); + const dayList = await this.dayListBlocks.build(); + return [...devTools, Header({ text: "Ilmoittautumiset" }), ...dayList]; + } +} diff --git a/app-nest/src/gui/tabs/home/home-tab.controller.ts b/app-nest/src/gui/tabs/home/home-tab.controller.ts index 6d8ac1b..cfc451a 100644 --- a/app-nest/src/gui/tabs/home/home-tab.controller.ts +++ b/app-nest/src/gui/tabs/home/home-tab.controller.ts @@ -1,28 +1,25 @@ import { Controller } from "@nestjs/common"; +import { HomeTab } from "slack-block-builder"; import BoltEvent from "../../../bolt/decorators/bolt-event.decorator"; import BoltEvents from "../../../bolt/enums/bolt-events.enum"; import { AppHomeOpenedArgs } from "../../../bolt/types/bolt-event-types"; -import { UserService } from "../../../entities/user/user.service"; -import getDayListBlocks from "./day-list.blocks"; -import getHomeTabBlocks from "./home-tab.view"; +import { HomeTabBuilder } from "./home-tab.builder"; @Controller() export class HomeTabController { - constructor(private userService: UserService) {} + constructor(private homeTabBlocks: HomeTabBuilder) {} @BoltEvent(BoltEvents.APP_HOME_OPENED) async getView({ event, client, logger }: AppHomeOpenedArgs) { - const users = await this.userService.findAll(); - const { slackId } = users[0]; - getDayListBlocks(); + const blocks = await this.homeTabBlocks.build(); + const view = HomeTab() + .blocks(...blocks) + .buildToObject(); try { const result = await client.views.publish({ user_id: event.user, - view: { - type: "home", - blocks: getHomeTabBlocks(), - }, + view, }); logger.debug(result); diff --git a/app-nest/src/gui/tabs/home/home-tab.module.ts b/app-nest/src/gui/tabs/home/home-tab.module.ts new file mode 100644 index 0000000..2c7f39b --- /dev/null +++ b/app-nest/src/gui/tabs/home/home-tab.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { DevToolsModule } from "../../dev/dev-tools.module"; +import { DayListItemBuilder } from "./day-list-item.builder"; +import { DayListBuilder } from "./day-list.builder"; +import { HomeTabBuilder } from "./home-tab.builder"; +import { HomeTabController } from "./home-tab.controller"; + +@Module({ + imports: [DevToolsModule], + providers: [HomeTabBuilder, DayListBuilder, DayListItemBuilder], + controllers: [HomeTabController], +}) +export class HomeTabModule {} diff --git a/app-nest/src/gui/tabs/home/home-tab.view.ts b/app-nest/src/gui/tabs/home/home-tab.view.ts deleted file mode 100644 index 22b7f6f..0000000 --- a/app-nest/src/gui/tabs/home/home-tab.view.ts +++ /dev/null @@ -1,16 +0,0 @@ -import devTools from "../../dev/dev-tools"; -import getDayListBlocks from "./day-list.blocks"; - -const getHomeTabBlocks = () => [ - ...devTools, - { - type: "header", - text: { - type: "plain_text", - text: "Ilmoittautumiset", - }, - }, - ...getDayListBlocks(), -]; - -export default getHomeTabBlocks; From 86d0d25d1fd8d16183d0911dce16e23044c98fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Wed, 9 Aug 2023 17:08:54 +0300 Subject: [PATCH 12/18] Add interaface for block-builder classes for better typing --- app-nest/src/gui/block-builder.interface.ts | 16 ++++++++++++++++ app-nest/src/gui/dev/dev-tools.builder.ts | 12 ++++++++++-- .../src/gui/tabs/home/day-list-item.builder.ts | 4 +++- app-nest/src/gui/tabs/home/day-list.builder.ts | 4 +++- app-nest/src/gui/tabs/home/home-tab.builder.ts | 6 +++--- 5 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 app-nest/src/gui/block-builder.interface.ts diff --git a/app-nest/src/gui/block-builder.interface.ts b/app-nest/src/gui/block-builder.interface.ts new file mode 100644 index 0000000..474a874 --- /dev/null +++ b/app-nest/src/gui/block-builder.interface.ts @@ -0,0 +1,16 @@ +import { + Appendable, + BlockBuilder as BlockBuilderType, +} from "slack-block-builder/dist/internal"; + +/** + * Interface for classes that build views with `slack-block-builder`. + * + * Type parameter should be imported from `slack-block-builder` with respect + * to the intended consumer of the class implementing this interface. This way + * the library's type system will take care of checking that only allowed + * blocks are used throughout the block tree. + */ +export interface BlockBuilder { + build(...args: unknown[]): Appendable | Promise>; +} diff --git a/app-nest/src/gui/dev/dev-tools.builder.ts b/app-nest/src/gui/dev/dev-tools.builder.ts index 1be0c51..50d2f66 100644 --- a/app-nest/src/gui/dev/dev-tools.builder.ts +++ b/app-nest/src/gui/dev/dev-tools.builder.ts @@ -1,9 +1,17 @@ import { Injectable } from "@nestjs/common"; -import { Actions, Button, Context, Divider, Header } from "slack-block-builder"; +import { + Actions, + Button, + Context, + Divider, + Header, + ViewBlockBuilder, +} from "slack-block-builder"; import BoltActions from "../../bolt/enums/bolt-actions.enum"; +import { BlockBuilder } from "../block-builder.interface"; @Injectable() -export class DevToolsBuilder { +export class DevToolsBuilder implements BlockBuilder { build() { return [ Header({ text: ":wrench: Developer Tools" }), diff --git a/app-nest/src/gui/tabs/home/day-list-item.builder.ts b/app-nest/src/gui/tabs/home/day-list-item.builder.ts index ad71e10..fcf6b8b 100644 --- a/app-nest/src/gui/tabs/home/day-list-item.builder.ts +++ b/app-nest/src/gui/tabs/home/day-list-item.builder.ts @@ -7,15 +7,17 @@ import { Option, OverflowMenu, StaticSelect, + ViewBlockBuilder, } from "slack-block-builder"; import BoltActions from "../../../bolt/enums/bolt-actions.enum"; +import { BlockBuilder } from "../../block-builder.interface"; type DayListItemProps = { date: Dayjs; }; @Injectable() -export class DayListItemBuilder { +export class DayListItemBuilder implements BlockBuilder { build({ date }: DayListItemProps) { const dateString = date.toISOString(); diff --git a/app-nest/src/gui/tabs/home/day-list.builder.ts b/app-nest/src/gui/tabs/home/day-list.builder.ts index cf7d869..1a16ac4 100644 --- a/app-nest/src/gui/tabs/home/day-list.builder.ts +++ b/app-nest/src/gui/tabs/home/day-list.builder.ts @@ -1,6 +1,8 @@ import { Injectable } from "@nestjs/common"; import dayjs, { Dayjs } from "dayjs"; import { flatten } from "lodash"; +import { ViewBlockBuilder } from "slack-block-builder"; +import { BlockBuilder } from "../../block-builder.interface"; import { DayListItemBuilder } from "./day-list-item.builder"; /** @@ -26,7 +28,7 @@ const dayRange = (len: number, days: Dayjs[] = [], i = 0): Dayjs[] => { }; @Injectable() -export class DayListBuilder { +export class DayListBuilder implements BlockBuilder { constructor(private dayListItemBuilder: DayListItemBuilder) {} async build() { diff --git a/app-nest/src/gui/tabs/home/home-tab.builder.ts b/app-nest/src/gui/tabs/home/home-tab.builder.ts index 6f76b64..41db66f 100644 --- a/app-nest/src/gui/tabs/home/home-tab.builder.ts +++ b/app-nest/src/gui/tabs/home/home-tab.builder.ts @@ -1,11 +1,11 @@ import { Injectable } from "@nestjs/common"; -import { Header } from "slack-block-builder"; +import { Header, ViewBlockBuilder } from "slack-block-builder"; +import { BlockBuilder } from "../../block-builder.interface"; import { DevToolsBuilder } from "../../dev/dev-tools.builder"; import { DayListBuilder } from "./day-list.builder"; -// TODO: Create interface for these kind of builder classes. @Injectable() -export class HomeTabBuilder { +export class HomeTabBuilder implements BlockBuilder { constructor( private dayListBlocks: DayListBuilder, private devToolsBuilder: DevToolsBuilder, From 3754c79d0cab5aa00b2036a1f76ef3eac4196144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Thu, 10 Aug 2023 13:42:19 +0300 Subject: [PATCH 13/18] Add office model --- app-nest/src/app.module.ts | 2 -- app-nest/src/entities/entities.module.ts | 4 +++- app-nest/src/entities/office/office.entity.ts | 12 ++++++++++++ app-nest/src/entities/office/office.module.ts | 11 +++++++++++ app-nest/src/entities/office/office.service.ts | 4 ++++ 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 app-nest/src/entities/office/office.entity.ts create mode 100644 app-nest/src/entities/office/office.module.ts create mode 100644 app-nest/src/entities/office/office.service.ts diff --git a/app-nest/src/app.module.ts b/app-nest/src/app.module.ts index 08f9c7d..e3e0f1f 100644 --- a/app-nest/src/app.module.ts +++ b/app-nest/src/app.module.ts @@ -5,7 +5,6 @@ import configuration from "./config/configuration"; import { inDevelopmentEnvironment } from "./config/utils"; import { DevToolsModule } from "./dev-tools/dev-tools.module"; import { EntitiesModule } from "./entities/entities.module"; -import { PresenceModule } from "./entities/presence/presence.module"; import { GuiModule } from "./gui/gui.module"; import { SyncModule } from "./sync/sync.module"; @@ -30,7 +29,6 @@ import { SyncModule } from "./sync/sync.module"; EntitiesModule, SyncModule, DevToolsModule, - PresenceModule, ], }) export class AppModule {} diff --git a/app-nest/src/entities/entities.module.ts b/app-nest/src/entities/entities.module.ts index 8898674..61cf4e2 100644 --- a/app-nest/src/entities/entities.module.ts +++ b/app-nest/src/entities/entities.module.ts @@ -1,7 +1,9 @@ import { Module } from "@nestjs/common"; +import { OfficeModule } from "./office/office.module"; +import { PresenceModule } from "./presence/presence.module"; import { UserModule } from "./user/user.module"; @Module({ - imports: [UserModule], + imports: [UserModule, PresenceModule, OfficeModule], }) export class EntitiesModule {} diff --git a/app-nest/src/entities/office/office.entity.ts b/app-nest/src/entities/office/office.entity.ts new file mode 100644 index 0000000..79f2e0f --- /dev/null +++ b/app-nest/src/entities/office/office.entity.ts @@ -0,0 +1,12 @@ +import { Column, Entity, PrimaryGeneratedColumn, Repository } from "typeorm"; + +@Entity() +export class Office { + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; +} + +export type OfficeRepository = Repository; diff --git a/app-nest/src/entities/office/office.module.ts b/app-nest/src/entities/office/office.module.ts new file mode 100644 index 0000000..5d3f2b6 --- /dev/null +++ b/app-nest/src/entities/office/office.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { Office } from "./office.entity"; +import { OfficeService } from "./office.service"; + +@Module({ + imports: [TypeOrmModule.forFeature([Office])], + providers: [OfficeService], + exports: [TypeOrmModule, OfficeService], +}) +export class OfficeModule {} diff --git a/app-nest/src/entities/office/office.service.ts b/app-nest/src/entities/office/office.service.ts new file mode 100644 index 0000000..06d1782 --- /dev/null +++ b/app-nest/src/entities/office/office.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class OfficeService {} From bc08253f27ead343e4017483119a8f03567a1887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Thu, 10 Aug 2023 14:01:30 +0300 Subject: [PATCH 14/18] Update office select to use data from db --- .../src/entities/office/office.service.ts | 12 ++++- .../gui/tabs/home/day-list-item.builder.ts | 47 ++++++++++--------- .../src/gui/tabs/home/day-list.builder.ts | 9 +++- app-nest/src/gui/tabs/home/home-tab.module.ts | 3 +- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/app-nest/src/entities/office/office.service.ts b/app-nest/src/entities/office/office.service.ts index 06d1782..519edce 100644 --- a/app-nest/src/entities/office/office.service.ts +++ b/app-nest/src/entities/office/office.service.ts @@ -1,4 +1,14 @@ import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Office, OfficeRepository } from "./office.entity"; @Injectable() -export class OfficeService {} +export class OfficeService { + constructor( + @InjectRepository(Office) private officeRepository: OfficeRepository, + ) {} + + async findAll() { + return this.officeRepository.find(); + } +} diff --git a/app-nest/src/gui/tabs/home/day-list-item.builder.ts b/app-nest/src/gui/tabs/home/day-list-item.builder.ts index fcf6b8b..0d5e430 100644 --- a/app-nest/src/gui/tabs/home/day-list-item.builder.ts +++ b/app-nest/src/gui/tabs/home/day-list-item.builder.ts @@ -10,15 +10,18 @@ import { ViewBlockBuilder, } from "slack-block-builder"; import BoltActions from "../../../bolt/enums/bolt-actions.enum"; +import { Office } from "../../../entities/office/office.entity"; import { BlockBuilder } from "../../block-builder.interface"; type DayListItemProps = { date: Dayjs; + offices: Office[]; }; @Injectable() export class DayListItemBuilder implements BlockBuilder { - build({ date }: DayListItemProps) { + build(props: DayListItemProps) { + const { date } = props; const dateString = date.toISOString(); return [ @@ -34,26 +37,7 @@ export class DayListItemBuilder implements BlockBuilder { actionId: BoltActions.SET_REMOTE_PRESENCE, value: dateString, }), - StaticSelect({ - placeholder: "Valitse toimipiste", - actionId: BoltActions.SELECT_OFFICE_FOR_DATE, - }) - .initialOption( - Option({ - text: "Helsinki", - value: JSON.stringify({ value: "hki", date }), - }), - ) - .options( - Option({ - text: "Helsinki", - value: JSON.stringify({ value: "hki", date }), - }), - Option({ - text: "Tampere", - value: JSON.stringify({ value: "tre", date }), - }), - ), + this.getOfficeBlocks(props), OverflowMenu({ actionId: BoltActions.DAY_LIST_ITEM_OVERFLOW }).options( Option({ text: "Poista ilmoittautuminen", @@ -66,4 +50,25 @@ export class DayListItemBuilder implements BlockBuilder { ), ]; } + + private getOfficeBlocks({ date, offices }: DayListItemProps) { + // Don't show office select at all if no offices exist. + if (offices.length === 0) { + return null; + } + + const Options = offices.map(({ id, name }) => + Option({ + text: name, + value: JSON.stringify({ value: id, date }), + }), + ); + + return StaticSelect({ + placeholder: "Valitse toimipiste", + actionId: BoltActions.SELECT_OFFICE_FOR_DATE, + }) + .initialOption(Options[0]) + .options(Options); + } } diff --git a/app-nest/src/gui/tabs/home/day-list.builder.ts b/app-nest/src/gui/tabs/home/day-list.builder.ts index 1a16ac4..767662d 100644 --- a/app-nest/src/gui/tabs/home/day-list.builder.ts +++ b/app-nest/src/gui/tabs/home/day-list.builder.ts @@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"; import dayjs, { Dayjs } from "dayjs"; import { flatten } from "lodash"; import { ViewBlockBuilder } from "slack-block-builder"; +import { OfficeService } from "../../../entities/office/office.service"; import { BlockBuilder } from "../../block-builder.interface"; import { DayListItemBuilder } from "./day-list-item.builder"; @@ -29,12 +30,16 @@ const dayRange = (len: number, days: Dayjs[] = [], i = 0): Dayjs[] => { @Injectable() export class DayListBuilder implements BlockBuilder { - constructor(private dayListItemBuilder: DayListItemBuilder) {} + constructor( + private dayListItemBuilder: DayListItemBuilder, + private officeService: OfficeService, + ) {} async build() { + const offices = await this.officeService.findAll(); const dates = dayRange(14); const blockLists = dates.map((date) => - this.dayListItemBuilder.build({ date }), + this.dayListItemBuilder.build({ date, offices }), ); return flatten(blockLists); } diff --git a/app-nest/src/gui/tabs/home/home-tab.module.ts b/app-nest/src/gui/tabs/home/home-tab.module.ts index 2c7f39b..773ae1c 100644 --- a/app-nest/src/gui/tabs/home/home-tab.module.ts +++ b/app-nest/src/gui/tabs/home/home-tab.module.ts @@ -1,4 +1,5 @@ import { Module } from "@nestjs/common"; +import { OfficeModule } from "../../../entities/office/office.module"; import { DevToolsModule } from "../../dev/dev-tools.module"; import { DayListItemBuilder } from "./day-list-item.builder"; import { DayListBuilder } from "./day-list.builder"; @@ -6,7 +7,7 @@ import { HomeTabBuilder } from "./home-tab.builder"; import { HomeTabController } from "./home-tab.controller"; @Module({ - imports: [DevToolsModule], + imports: [DevToolsModule, OfficeModule], providers: [HomeTabBuilder, DayListBuilder, DayListItemBuilder], controllers: [HomeTabController], }) From 55c75a26be67da4658aa253841bf0d9d43c75266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Fri, 11 Aug 2023 10:35:59 +0300 Subject: [PATCH 15/18] Use office relation in presence model --- app-nest/package-lock.json | 60 ++++++++++++++++++- app-nest/package.json | 1 + .../src/entities/presence/dto/presence.dto.ts | 8 +++ .../entities/presence/presence.controller.ts | 4 +- .../src/entities/presence/presence.entity.ts | 7 ++- .../src/entities/presence/presence.module.ts | 3 +- .../src/entities/presence/presence.service.ts | 22 ++++++- 7 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 app-nest/src/entities/presence/dto/presence.dto.ts diff --git a/app-nest/package-lock.json b/app-nest/package-lock.json index e7ef30b..df3f300 100644 --- a/app-nest/package-lock.json +++ b/app-nest/package-lock.json @@ -14,6 +14,7 @@ "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@slack/bolt": "^3.13.2", "dayjs": "^1.11.9", @@ -1558,6 +1559,25 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", + "integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.1.2.tgz", @@ -1638,6 +1658,37 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@nestjs/swagger": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.8.tgz", + "integrity": "sha512-Jpl3laGAqvyWccc3auLU0mMjl5hJ2kqzzDb63ynJi5NMbFlgBwrR8FCGBVstSsqL9YSJWLR4L1BZzVmVExcY+g==", + "dependencies": { + "@nestjs/mapped-types": "2.0.2", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "5.3.1" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.1.2.tgz", @@ -2779,8 +2830,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", @@ -6336,7 +6386,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -8543,6 +8592,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.3.1.tgz", + "integrity": "sha512-El78OvXp9zMasfPrshtkW1CRx8AugAKoZuGGOTW+8llJzOV1RtDJYqQRz/6+2OakjeWWnZuRlN2Qj1Y0ilux3w==" + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/app-nest/package.json b/app-nest/package.json index 29a5d91..557a429 100644 --- a/app-nest/package.json +++ b/app-nest/package.json @@ -33,6 +33,7 @@ "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@slack/bolt": "^3.13.2", "dayjs": "^1.11.9", diff --git a/app-nest/src/entities/presence/dto/presence.dto.ts b/app-nest/src/entities/presence/dto/presence.dto.ts new file mode 100644 index 0000000..58fe713 --- /dev/null +++ b/app-nest/src/entities/presence/dto/presence.dto.ts @@ -0,0 +1,8 @@ +import { OmitType, PickType } from "@nestjs/swagger"; +import { Presence } from "../presence.entity"; + +export class UpsertPresenceDto extends OmitType(Presence, ["office"]) {} + +export class SetOfficeDto extends PickType(Presence, ["userId", "date"]) { + officeId: number; +} diff --git a/app-nest/src/entities/presence/presence.controller.ts b/app-nest/src/entities/presence/presence.controller.ts index af5ea21..0792780 100644 --- a/app-nest/src/entities/presence/presence.controller.ts +++ b/app-nest/src/entities/presence/presence.controller.ts @@ -36,10 +36,10 @@ export class PresenceController { async selectOfficeForDate({ ack, body, payload }: BoltActionArgs) { await ack(); const { value, date } = JSON.parse(payload["selected_option"].value); - await this.presenceService.upsert({ + await this.presenceService.setOffice({ userId: body.user.id, date: dayjs(date).toDate(), - office: value, + officeId: value, }); } diff --git a/app-nest/src/entities/presence/presence.entity.ts b/app-nest/src/entities/presence/presence.entity.ts index 26d2dcb..5bada9a 100644 --- a/app-nest/src/entities/presence/presence.entity.ts +++ b/app-nest/src/entities/presence/presence.entity.ts @@ -1,4 +1,5 @@ -import { Column, Entity, PrimaryColumn, Repository } from "typeorm"; +import { Column, Entity, ManyToOne, PrimaryColumn, Repository } from "typeorm"; +import { Office } from "../office/office.entity"; export enum PresenceType { AT_OFFICE = "at_office", @@ -16,8 +17,8 @@ export class Presence { @Column({ type: "enum", enum: PresenceType, nullable: true }) type: PresenceType | null; - @Column({ nullable: true }) - office: string | null; + @ManyToOne(() => Office, { nullable: true }) + office: Office; } export type PresenceRepository = Repository; diff --git a/app-nest/src/entities/presence/presence.module.ts b/app-nest/src/entities/presence/presence.module.ts index 7f08191..ca8000d 100644 --- a/app-nest/src/entities/presence/presence.module.ts +++ b/app-nest/src/entities/presence/presence.module.ts @@ -1,11 +1,12 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; +import { OfficeModule } from "../office/office.module"; import { PresenceController } from "./presence.controller"; import { Presence } from "./presence.entity"; import { PresenceService } from "./presence.service"; @Module({ - imports: [TypeOrmModule.forFeature([Presence])], + imports: [TypeOrmModule.forFeature([Presence]), OfficeModule], providers: [PresenceService], controllers: [PresenceController], exports: [TypeOrmModule], diff --git a/app-nest/src/entities/presence/presence.service.ts b/app-nest/src/entities/presence/presence.service.ts index 2a9bd92..b5495f9 100644 --- a/app-nest/src/entities/presence/presence.service.ts +++ b/app-nest/src/entities/presence/presence.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { DataSource } from "typeorm"; +import { SetOfficeDto, UpsertPresenceDto } from "./dto/presence.dto"; import { Presence, PresenceRepository } from "./presence.entity"; @Injectable() @@ -10,7 +11,11 @@ export class PresenceService { private dataSource: DataSource, ) {} - async upsert(presence: Partial) { + async remove(presence: Pick) { + return this.presenceRepository.delete(presence); + } + + async upsert(presence: Partial) { // Select only existing cols for the upsert operation to avoid overriding // existing data with defaults/nulls. const primaryKeys = ["userId", "date"]; @@ -18,6 +23,10 @@ export class PresenceService { (key) => !primaryKeys.includes(key), ); + if (updatableCols.length === 0) { + return; + } + return this.dataSource .createQueryBuilder() .insert() @@ -27,7 +36,14 @@ export class PresenceService { .execute(); } - async remove(presence: Pick) { - return this.presenceRepository.delete(presence); + async setOffice({ userId, date, officeId }: SetOfficeDto) { + await this.upsert({ userId, date }); + + return this.presenceRepository.update( + { userId, date }, + { + office: { id: officeId }, + }, + ); } } From f370f309c796d81015cace7784ae35a1495f99a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Fri, 11 Aug 2023 13:26:23 +0300 Subject: [PATCH 16/18] Add visible office select to home tab --- .../src/gui/tabs/home/home-tab.builder.ts | 10 ++++- app-nest/src/gui/tabs/home/home-tab.module.ts | 8 +++- .../home/visible-office-select.builder.ts | 40 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 app-nest/src/gui/tabs/home/visible-office-select.builder.ts diff --git a/app-nest/src/gui/tabs/home/home-tab.builder.ts b/app-nest/src/gui/tabs/home/home-tab.builder.ts index 41db66f..849e066 100644 --- a/app-nest/src/gui/tabs/home/home-tab.builder.ts +++ b/app-nest/src/gui/tabs/home/home-tab.builder.ts @@ -3,17 +3,25 @@ import { Header, ViewBlockBuilder } from "slack-block-builder"; import { BlockBuilder } from "../../block-builder.interface"; import { DevToolsBuilder } from "../../dev/dev-tools.builder"; import { DayListBuilder } from "./day-list.builder"; +import { VisibleOfficeSelectBuilder } from "./visible-office-select.builder"; @Injectable() export class HomeTabBuilder implements BlockBuilder { constructor( private dayListBlocks: DayListBuilder, private devToolsBuilder: DevToolsBuilder, + private visibleOfficeSelectBuilder: VisibleOfficeSelectBuilder, ) {} async build() { const devTools = this.devToolsBuilder.build(); + const officeSelect = await this.visibleOfficeSelectBuilder.build(); const dayList = await this.dayListBlocks.build(); - return [...devTools, Header({ text: "Ilmoittautumiset" }), ...dayList]; + return [ + ...devTools, + Header({ text: "Ilmoittautumiset" }), + ...officeSelect, + ...dayList, + ]; } } diff --git a/app-nest/src/gui/tabs/home/home-tab.module.ts b/app-nest/src/gui/tabs/home/home-tab.module.ts index 773ae1c..be32426 100644 --- a/app-nest/src/gui/tabs/home/home-tab.module.ts +++ b/app-nest/src/gui/tabs/home/home-tab.module.ts @@ -5,10 +5,16 @@ import { DayListItemBuilder } from "./day-list-item.builder"; import { DayListBuilder } from "./day-list.builder"; import { HomeTabBuilder } from "./home-tab.builder"; import { HomeTabController } from "./home-tab.controller"; +import { VisibleOfficeSelectBuilder } from "./visible-office-select.builder"; @Module({ imports: [DevToolsModule, OfficeModule], - providers: [HomeTabBuilder, DayListBuilder, DayListItemBuilder], + providers: [ + HomeTabBuilder, + DayListBuilder, + DayListItemBuilder, + VisibleOfficeSelectBuilder, + ], controllers: [HomeTabController], }) export class HomeTabModule {} diff --git a/app-nest/src/gui/tabs/home/visible-office-select.builder.ts b/app-nest/src/gui/tabs/home/visible-office-select.builder.ts new file mode 100644 index 0000000..ff34e73 --- /dev/null +++ b/app-nest/src/gui/tabs/home/visible-office-select.builder.ts @@ -0,0 +1,40 @@ +import { Injectable } from "@nestjs/common"; +import { + Option, + Section, + StaticSelect, + ViewBlockBuilder, +} from "slack-block-builder"; +import { OfficeService } from "../../../entities/office/office.service"; +import { BlockBuilder } from "../../block-builder.interface"; + +@Injectable() +export class VisibleOfficeSelectBuilder + implements BlockBuilder +{ + constructor(private officeService: OfficeService) {} + + async build() { + const offices = await this.officeService.findAll(); + const Options = offices.map(({ id, name }) => + Option({ + text: name, + value: id.toString(), + }), + ); + + return [ + Section({ + text: "Valitse, minkä toimipisteen paikallaolijat näytetään:", + }).accessory( + StaticSelect({ + placeholder: "Valitse toimipiste", + actionId: "asd", + }) + // TODO: Use user's selected office as initial value. + .initialOption(Options[0]) + .options(Options), + ), + ]; + } +} From 02b0c0626bd41ae5a87d7e116265ed78492521da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Fri, 11 Aug 2023 13:47:38 +0300 Subject: [PATCH 17/18] Add user settings model --- app-nest/src/entities/entities.module.ts | 3 ++- .../user-settings/user-settings.entity.ts | 26 +++++++++++++++++++ .../user-settings/user-settings.module.ts | 11 ++++++++ .../user-settings/user-settings.service.ts | 11 ++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app-nest/src/entities/user-settings/user-settings.entity.ts create mode 100644 app-nest/src/entities/user-settings/user-settings.module.ts create mode 100644 app-nest/src/entities/user-settings/user-settings.service.ts diff --git a/app-nest/src/entities/entities.module.ts b/app-nest/src/entities/entities.module.ts index 61cf4e2..7bc7303 100644 --- a/app-nest/src/entities/entities.module.ts +++ b/app-nest/src/entities/entities.module.ts @@ -1,9 +1,10 @@ import { Module } from "@nestjs/common"; import { OfficeModule } from "./office/office.module"; import { PresenceModule } from "./presence/presence.module"; +import { UserSettingsModule } from "./user-settings/user-settings.module"; import { UserModule } from "./user/user.module"; @Module({ - imports: [UserModule, PresenceModule, OfficeModule], + imports: [UserModule, PresenceModule, OfficeModule, UserSettingsModule], }) export class EntitiesModule {} diff --git a/app-nest/src/entities/user-settings/user-settings.entity.ts b/app-nest/src/entities/user-settings/user-settings.entity.ts new file mode 100644 index 0000000..4e4675a --- /dev/null +++ b/app-nest/src/entities/user-settings/user-settings.entity.ts @@ -0,0 +1,26 @@ +import { + Entity, + JoinColumn, + ManyToOne, + OneToOne, + PrimaryColumn, + Repository, +} from "typeorm"; +import { Office } from "../office/office.entity"; +import { User } from "../user/user.entity"; + +@Entity() +export class UserSettings { + // Use `user` 1:1 relation as primary key. Field name must exactly match the generated column name! + @PrimaryColumn() + userSlackId: string; + + @OneToOne(() => User) + @JoinColumn() + user: User; + + @ManyToOne(() => Office) + visibleOffice: Office; +} + +export type UserSettingsRepository = Repository; diff --git a/app-nest/src/entities/user-settings/user-settings.module.ts b/app-nest/src/entities/user-settings/user-settings.module.ts new file mode 100644 index 0000000..93b9418 --- /dev/null +++ b/app-nest/src/entities/user-settings/user-settings.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { UserSettings } from "./user-settings.entity"; +import { UserSettingsService } from "./user-settings.service"; + +@Module({ + imports: [TypeOrmModule.forFeature([UserSettings])], + providers: [UserSettingsService], + exports: [TypeOrmModule, UserSettingsService], +}) +export class UserSettingsModule {} diff --git a/app-nest/src/entities/user-settings/user-settings.service.ts b/app-nest/src/entities/user-settings/user-settings.service.ts new file mode 100644 index 0000000..6a9f3a1 --- /dev/null +++ b/app-nest/src/entities/user-settings/user-settings.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { UserSettings, UserSettingsRepository } from "./user-settings.entity"; + +@Injectable() +export class UserSettingsService { + constructor( + @InjectRepository(UserSettings) + private userSettingsRepository: UserSettingsRepository, + ) {} +} From eda050c57f875a1bdd21d27b392d105c65c13e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonas=20H=C3=A4kkinen?= Date: Fri, 11 Aug 2023 15:00:56 +0300 Subject: [PATCH 18/18] Persist user's visible office setting --- app-nest/src/bolt/enums/bolt-actions.enum.ts | 1 + .../user-settings/user-settings.controller.ts | 20 +++++++++++++++++++ .../user-settings/user-settings.dto.ts | 13 ++++++++++++ .../user-settings/user-settings.module.ts | 2 ++ .../user-settings/user-settings.service.ts | 19 ++++++++++++++++++ .../home/visible-office-select.builder.ts | 3 ++- 6 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 app-nest/src/entities/user-settings/user-settings.controller.ts create mode 100644 app-nest/src/entities/user-settings/user-settings.dto.ts diff --git a/app-nest/src/bolt/enums/bolt-actions.enum.ts b/app-nest/src/bolt/enums/bolt-actions.enum.ts index e80493e..4727085 100644 --- a/app-nest/src/bolt/enums/bolt-actions.enum.ts +++ b/app-nest/src/bolt/enums/bolt-actions.enum.ts @@ -4,6 +4,7 @@ enum BoltActions { SET_REMOTE_PRESENCE = "set_remote_presence", SELECT_OFFICE_FOR_DATE = "select_office_for_date", DAY_LIST_ITEM_OVERFLOW = "day_list_item_overflow", + SET_VISIBLE_OFFICE = "set_visible_office", } export default BoltActions; diff --git a/app-nest/src/entities/user-settings/user-settings.controller.ts b/app-nest/src/entities/user-settings/user-settings.controller.ts new file mode 100644 index 0000000..94a72b4 --- /dev/null +++ b/app-nest/src/entities/user-settings/user-settings.controller.ts @@ -0,0 +1,20 @@ +import { Controller } from "@nestjs/common"; +import BoltAction from "../../bolt/decorators/bolt-action.decorator"; +import BoltActions from "../../bolt/enums/bolt-actions.enum"; +import { BoltActionArgs } from "../../bolt/types/bolt-action-types"; +import { UserSettingsService } from "./user-settings.service"; + +@Controller() +export class UserSettingsController { + constructor(private userSettingsService: UserSettingsService) {} + + @BoltAction(BoltActions.SET_VISIBLE_OFFICE) + async setVisibleOffice({ ack, payload, body }: BoltActionArgs) { + await ack(); + const visibleOfficeId = Number(payload["selected_option"].value); + await this.userSettingsService.setVisibleOffice({ + userSlackId: body.user.id, + visibleOfficeId, + }); + } +} diff --git a/app-nest/src/entities/user-settings/user-settings.dto.ts b/app-nest/src/entities/user-settings/user-settings.dto.ts new file mode 100644 index 0000000..4415ce7 --- /dev/null +++ b/app-nest/src/entities/user-settings/user-settings.dto.ts @@ -0,0 +1,13 @@ +import { OmitType, PickType } from "@nestjs/swagger"; +import { UserSettings } from "./user-settings.entity"; + +export class UpsertUserSettingsDto extends OmitType(UserSettings, [ + "visibleOffice", + "user", +]) {} + +export class SetVisibleOfficeDto extends PickType(UserSettings, [ + "userSlackId", +]) { + visibleOfficeId: number; +} diff --git a/app-nest/src/entities/user-settings/user-settings.module.ts b/app-nest/src/entities/user-settings/user-settings.module.ts index 93b9418..6c9df94 100644 --- a/app-nest/src/entities/user-settings/user-settings.module.ts +++ b/app-nest/src/entities/user-settings/user-settings.module.ts @@ -1,11 +1,13 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; +import { UserSettingsController } from "./user-settings.controller"; import { UserSettings } from "./user-settings.entity"; import { UserSettingsService } from "./user-settings.service"; @Module({ imports: [TypeOrmModule.forFeature([UserSettings])], providers: [UserSettingsService], + controllers: [UserSettingsController], exports: [TypeOrmModule, UserSettingsService], }) export class UserSettingsModule {} diff --git a/app-nest/src/entities/user-settings/user-settings.service.ts b/app-nest/src/entities/user-settings/user-settings.service.ts index 6a9f3a1..58bce80 100644 --- a/app-nest/src/entities/user-settings/user-settings.service.ts +++ b/app-nest/src/entities/user-settings/user-settings.service.ts @@ -1,5 +1,9 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; +import { + SetVisibleOfficeDto, + UpsertUserSettingsDto, +} from "./user-settings.dto"; import { UserSettings, UserSettingsRepository } from "./user-settings.entity"; @Injectable() @@ -8,4 +12,19 @@ export class UserSettingsService { @InjectRepository(UserSettings) private userSettingsRepository: UserSettingsRepository, ) {} + + async upsert(input: UpsertUserSettingsDto) { + return await this.userSettingsRepository.upsert(input, ["userSlackId"]); + } + + async setVisibleOffice({ + userSlackId, + visibleOfficeId, + }: SetVisibleOfficeDto) { + await this.upsert({ userSlackId }); + return this.userSettingsRepository.update( + { userSlackId }, + { visibleOffice: { id: visibleOfficeId } }, + ); + } } diff --git a/app-nest/src/gui/tabs/home/visible-office-select.builder.ts b/app-nest/src/gui/tabs/home/visible-office-select.builder.ts index ff34e73..44b6713 100644 --- a/app-nest/src/gui/tabs/home/visible-office-select.builder.ts +++ b/app-nest/src/gui/tabs/home/visible-office-select.builder.ts @@ -5,6 +5,7 @@ import { StaticSelect, ViewBlockBuilder, } from "slack-block-builder"; +import BoltActions from "../../../bolt/enums/bolt-actions.enum"; import { OfficeService } from "../../../entities/office/office.service"; import { BlockBuilder } from "../../block-builder.interface"; @@ -29,7 +30,7 @@ export class VisibleOfficeSelectBuilder }).accessory( StaticSelect({ placeholder: "Valitse toimipiste", - actionId: "asd", + actionId: BoltActions.SET_VISIBLE_OFFICE, }) // TODO: Use user's selected office as initial value. .initialOption(Options[0])