From 787f04768cda9387507c58e6f055aec6c63017a5 Mon Sep 17 00:00:00 2001 From: Chiyoung Jeong Date: Tue, 23 Jul 2024 15:46:03 +0900 Subject: [PATCH] release: 6.2430.60 (#533) * fix(deps): update dependency aws-sdk to v2.1639.0 (#396) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency pino to v9.2.0 (#397) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency pino-pretty to v11.2.1 (#398) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @tanstack/react-query to v5.45.0 (#399) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update aws-sdk-js-v3 monorepo to v3.596.0 (#400) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency prettier-plugin-tailwindcss to v0.6.4 (#393) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @swc/core to v1.5.29 (#402) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @floating-ui/react to v0.26.17 (#403) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1640.0 (#401) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @opensearch-project/opensearch to v2.10.0 (#404) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1641.0 (#405) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency mysql2 to v3.10.1 (#407) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency iron-session to v8.0.2 (#409) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update aws-sdk-js-v3 monorepo to v3.598.0 (#410) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1642.0 (#411) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @swc/core to v1.6.0 (#412) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency react-hook-form to v7.52.0 (#413) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @swc/core to v1.6.1 (#414) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency ts-jest to v29.1.5 (#415) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @tanstack/react-query to v5.45.1 (#416) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update docker/build-push-action action to v6 (#417) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to v20.14.3 (#418) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency prettier-plugin-tailwindcss to v0.6.5 (#419) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to v20.14.4 (#422) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update pnpm to v9.4.0 (#421) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update typescript-eslint monorepo to v7.13.1 (#420) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to v20.14.5 (#424) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1644.0 (#423) fix(deps): update dependency aws-sdk to v2.1643.0 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update aws-sdk-js-v3 monorepo to v3.600.0 (#425) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency eslint-plugin-react to v7.34.3 (#427) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency glob to v10.4.2 (#428) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @swc/core to v1.6.3 (#429) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency joi to v17.13.2 (#430) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency framer-motion to v11.2.11 (#431) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to v20.14.6 (#432) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency joi to v17.13.3 (#433) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency nodemailer to v6.9.14 (#434) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1645.0 (#426) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency openapi-typescript to v7 (#435) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency node to v20.15.0 (#436) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1646.0 (#438) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @headlessui/react to v2.1.0 (#440) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to v20.14.7 (#439) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @swc/core to v1.6.5 (#441) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to v20.14.8 (#442) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @playwright/test to v1.45.0 (#443) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1647.0 (#444) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @floating-ui/react to v0.26.18 (#446) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency framer-motion to v11.2.12 (#447) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update typescript-eslint monorepo to v7.14.1 (#445) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @tanstack/react-query to v5.48.0 (#448) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to v20.14.9 (#449) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update opensearchproject/opensearch docker tag to v2.15.0 (#450) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency zustand to v4.5.3 (#454) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update opensearchproject/opensearch-dashboards docker tag to v2.15.0 (#452) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1648.0 (#453) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @headlessui/react to v2.1.1 (#455) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency zustand to v4.5.4 (#456) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to v4.3.0 (#457) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1649.0 (#458) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency prom-client to v15.1.3 (#459) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1650.0 (#460) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix: docker-compose for infra (#461) fix: fix readme, docker compose with feedback Co-authored-by: Carson * chore(deps): update dependency @swc-node/jest to v1.8.3 (#462) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update aws-sdk-js-v3 monorepo to v3.606.0 (#465) fix(deps): update dependency @aws-sdk/s3-request-presigner to v3.606.0 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1651.0 (#467) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @aws-sdk/client-s3 to v3.606.0 (#466) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @tanstack/react-query to v5.49.0 (#468) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @swc/core to v1.6.6 (#469) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency postcss to v8.4.39 (#470) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @tanstack/react-table to v8.19.1 (#471) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @tanstack/react-query to v5.49.2 (#472) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @floating-ui/react to v0.26.19 (#473) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @tanstack/react-table to v8.19.2 (#474) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency mysql2 to v3.10.2 (#475) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @nestjs/config to v3.2.3 (#477) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @nestjs/swagger to v7.4.0 (#478) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update typescript-eslint monorepo to v7.15.0 (#479) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update aws-sdk-js-v3 monorepo to v3.608.0 (#480) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency aws-sdk to v2.1652.0 (#481) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency react-hook-form to v7.52.1 (#482) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @nestjs/cli to v10.4.0 (#483) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency openapi-typescript to v7.0.1 (#484) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @playwright/test to v1.45.1 (#485) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * refactor: web (#307) * tenant * tenant * tenant * add toaster in test-utils * feat: create tenant form * nextjs.js in prettier, create-tenant-form testing * temporary user model * user entity * modify user state * auth * add test page * test sign-in page * feat: test auth pages * feat: test link pages * feat: remove console * Cherry-picked pnpm-lock.yaml from commit 7ba0141 * fix: pnpm-lock.yaml * feat: test api * fix: clean script in all packages * main page * profile * remove tenant card * fix main layout * profile * fix merge * temp create project * change zustand tenant,user * change zustand tenant,user * change zustand utils * create, create-complete and dashbaord * feedback table * fix file names * issue table * refactor constants * create channel * refactoring others * setting menu inprogress * all pages * fix snapshot * schema * change cn * refactoring * modal -> popover * fix test * fix e2e test * fix webhook popover * fix test * minor bugs * change docker compose * fix e2e * fix user table * issueNames and column reset * fix e2e test * fix image slider button * fix logger * remove try catch create project * fix test * pr * chore(deps): update mysql docker tag to v8.4.1 (#486) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @babel/core to v7.24.9 (#489) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency eslint to v9 (#367) * chore(deps): update dependency eslint to v9 * fix eslint * fix lint2 * fix eslint * fix lint * fix test * fix lint in api * fix test * fix e2e test * fix unit test in web * fix unit test in web --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: chiol * chore(deps): update dependency @playwright/test to v1.45.2 (#491) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency node to v20.15.1 (#492) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency openapi-typescript to v7.0.4 (#494) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency tailwindcss to v3.4.6 (#495) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency ts-jest to v29.2.3 (#500) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency tsup to v8.1.2 (#496) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency msw to v2.3.2 (#502) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * remove console.log and fix url bugs (#493) * minor bugs * snapshot * fix(deps): update turbo monorepo to v2 (major) (#384) * fix(deps): update turbo monorepo to v2 * fix turbo.json * fix dockerfile * fix docker build * fix format --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: chiol * chore(deps): update swc monorepo (#497) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @headlessui/react to v2.1.2 (#498) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency framer-motion to v11.3.8 (#501) fix(deps): update dependency framer-motion to v11.3.7 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update tanstack-query monorepo to v5.51.9 (#503) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency i18next to v23.12.2 (#506) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update turbo monorepo to v2.0.8 (#508) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency openapi-typescript to v7.1.0 (#509) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update turbo monorepo to v2.0.9 (#511) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update pnpm to v9.5.0 (#504) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency tsup to v8.2.0 (#510) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update aws-sdk-js-v3 monorepo to v3.616.0 (#505) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency nuqs to v1.17.5 (#515) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency react-use to v17.5.1 (#516) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency eslint-plugin-prettier to v5.2.1 (#512) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency tsup to v8.2.1 (#517) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update tanstack-query monorepo to v5.51.11 (#518) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency eslint-plugin-react to v7.35.0 (#513) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency glob to v11 (#514) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency rimraf to v6 (#519) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update mysql docker tag to v9 (#521) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update pnpm to v9.6.0 (#523) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency react-i18next to v15 (#522) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency tsup to v8.2.2 (#526) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @floating-ui/react to v0.26.20 (#527) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @t3-oss/env-nextjs to ^0.11.0 (#528) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @playwright/test to v1.45.3 (#529) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix: minor web bugs after web refactoring (#525) * fix dashboard cards * change user setting form * chore(deps): update dependency @testing-library/jest-dom to v6.4.7 (#530) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: HoJeong Im <39ghwjd@naver.com> Co-authored-by: Carson --- .github/workflows/docker-dev-image.yml | 4 +- .github/workflows/docker-prod-image.yml | 4 +- .github/workflows/e2e-test.yml | 2 +- .npmrc | 4 + .nvmrc | 2 +- .vscode/settings.json | 4 + README.md | 6 +- apps/api/.eslintrc.js | 10 - apps/api/eslint.config.mjs | 57 + apps/api/package.json | 11 +- .../common/filters/http-exception.filter.ts | 1 + .../mailer-config/mailer-config.module.ts | 2 +- .../typeorm-config.datasource.ts | 2 +- apps/api/src/configs/smtp.config.ts | 12 +- .../api/src/domains/admin/auth/auth.module.ts | 1 + .../domains/admin/auth/auth.service.spec.ts | 1 + .../src/domains/admin/auth/auth.service.ts | 2 + .../channel/channel/channel.controller.ts | 1 + .../admin/feedback/feedback.os.service.ts | 2 +- .../subscribers/code-history.subscriber.ts | 1 + .../dtos/create-issue-tracker.dto.ts | 3 +- .../dtos/issue-tracker-data.dto.ts} | 18 +- .../create-issue-tracker-request.dto.ts | 6 +- .../create-issue-tracker-response.dto.ts | 6 +- .../find-issue-tracker-response.dto.ts | 6 +- .../issue-tracker.controller.spec.ts | 9 +- .../project/dtos/create-project.dto.ts | 3 +- .../admin/project/project/project.service.ts | 6 +- .../dtos/responses/get-tenant-response.dto.ts | 2 +- .../admin/user/user-password.service.spec.ts | 1 + .../admin/user/user-password.service.ts | 1 + .../api/src/domains/admin/user/user.module.ts | 1 + .../src/domains/admin/user/user.service.ts | 1 + apps/api/src/shared/code/code.module.ts | 1 + apps/api/src/test-utils/fixtures.ts | 5 +- .../providers/auth.service.providers.ts | 1 + .../user-password.service.providers.ts | 3 +- .../providers/user.service.providers.ts | 3 +- .../test-utils/stubs/code-repository.stub.ts | 1 + apps/api/src/types/config-service.type.ts | 4 +- apps/api/test/auth.e2e-spec.ts | 7 +- apps/api/test/feedback/channel.e2e-spec.ts | 8 +- apps/api/tsconfig.json | 2 +- apps/e2e/global.setup.ts | 34 +- apps/e2e/package.json | 1 + apps/e2e/playwright.config.ts | 2 +- .../no-database-seed/create-project.spec.ts | 160 +- apps/e2e/scenarios/sign-up.spec.ts | 0 .../with-database-seed/create-channel.spec.ts | 44 +- .../create-feedback.spec.ts | 57 +- apps/e2e/test.list.ts | 68 +- apps/web/.prettierignore | 1 - apps/web/JSDOMEnvironment.ts | 26 + apps/web/eslint.config.js | 20 + apps/web/jest.config.mjs | 19 - apps/web/jest.config.ts | 46 + apps/web/jest.polyfills.js | 18 + apps/web/jest.setup.ts | 74 + apps/web/next-i18next.config.js | 25 +- apps/web/next.config.mjs | 10 +- apps/web/package.json | 33 +- .../{postcss.config.js => postcss.config.cjs} | 0 apps/web/public/locales/de/common.json | 2 +- apps/web/public/locales/en/common.json | 4 +- apps/web/public/locales/ja/common.json | 2 +- apps/web/public/locales/ko/common.json | 2 +- apps/web/public/locales/zh/common.json | 2 +- apps/web/src/__mocks__/zustand.ts | 66 + apps/web/src/__test__/api/health.spec.ts | 41 + apps/web/src/__test__/api/jwt.spec.ts | 80 + apps/web/src/__test__/api/login.spec.ts | 113 + apps/web/src/__test__/api/logout.spec.ts | 54 + apps/web/src/__test__/api/oauth.spec.ts | 111 + apps/web/src/__test__/api/refresh-jwt.spec.ts | 126 + .../reset-password.spec.tsx.snap | 191 + .../auth/__snapshots__/sign-in.spec.tsx.snap | 815 ++ .../auth/__snapshots__/sign-up.spec.tsx.snap | 260 + .../src/__test__/auth/oauth-callback.spec.tsx | 49 + .../src/__test__/auth/reset-password.spec.tsx | 29 + apps/web/src/__test__/auth/sign-in.spec.tsx | 103 + apps/web/src/__test__/auth/sign-up.spec.tsx | 27 + apps/web/src/__test__/index.spec.tsx | 29 + .../reset-password.spec.tsx.snap | 248 + .../user-invitation.spec.tsx.snap | 243 + .../src/__test__/link/reset-password.spec.tsx | 29 + .../__test__/link/user-invitation.spec.tsx | 29 + .../main/__snapshots__/index.spec.tsx.snap | 200 + .../main/__snapshots__/profile.spec.tsx.snap | 164 + apps/web/src/__test__/main/index.spec.tsx | 59 + apps/web/src/__test__/main/profile.spec.tsx | 26 + .../tenant/__snapshots__/create.spec.tsx.snap | 36 + apps/web/src/__test__/tenant/create.spec.tsx | 40 + .../cards/ChannelCard/ChannelCard.tsx | 65 - .../TenantProjectCard/TenantProjectCard.tsx | 106 - .../src/components/charts/SimpleLineChart.tsx | 61 - apps/web/src/components/charts/index.ts | 17 - .../components/etc/CheckedTableHead/index.ts | 16 - .../components/etc/DashboardTable/index.ts | 16 - .../etc/DescriptionTooltip/index.ts | 16 - .../components/etc/ExpandableText/index.ts | 16 - .../src/components/etc/HelpCardDocs/index.ts | 16 - .../etc/IssueCircle/IssueCircle.tsx | 56 - .../src/components/etc/IssueCircle/index.ts | 16 - .../web/src/components/etc/SelectBox/index.ts | 18 - .../SelectBoxWithIcon/SelectBoxWithIcon.tsx | 67 - .../components/etc/SelectBoxWithIcon/index.ts | 16 - .../src/components/etc/TableCheckbox/index.ts | 16 - .../components/etc/TableLoadingRow/index.ts | 16 - .../components/etc/TablePagination/index.ts | 16 - .../src/components/etc/TableResizer/index.ts | 16 - .../components/etc/TableSearchInput/index.ts | 17 - .../src/components/etc/TableSortIcon/index.ts | 16 - .../components/etc/TimezoneSelectBox/index.ts | 16 - apps/web/src/components/etc/index.ts | 35 - apps/web/src/components/index.ts | 19 - .../src/components/layouts/Header/Header.tsx | 39 - apps/web/src/components/layouts/index.ts | 17 - .../setting-menu/SettingMenuSubtitle.tsx | 79 - .../components/layouts/setting-menu/index.ts | 18 - apps/web/src/components/popovers/index.ts | 18 - .../templates/AuthTemplate/AuthTemplate.tsx | 37 - .../templates/AuthTemplate/index.ts | 16 - .../index.ts | 16 - .../CreateProjectChannelTemplate.tsx | 173 - .../CreateProjectChannelTemplate/index.ts | 16 - .../templates/CreateSectionTemplate/index.ts | 16 - .../templates/MainTemplate/index.ts | 16 - .../templates/SettingMenuTemplate/index.ts | 16 - apps/web/src/constants/is-server.ts | 16 - .../buttons/CreateChannelButton/index.ts | 16 - .../buttons/CreateProjectButton/index.ts | 16 - .../buttons/OAuthLoginButton/index.ts | 16 - .../containers/buttons/ShareButton/index.ts | 16 - .../ChannelInfoSection.tsx | 41 - .../create-channel-complete/FieldSection.tsx | 121 - .../ImageUploadSection.tsx | 64 - .../create-channel-complete/index.ts | 19 - .../CreateChannelInputTemplate.tsx | 59 - .../create-channel/InputChannelInfo.tsx | 146 - .../containers/create-channel/InputField.tsx | 180 - .../create-channel/InputFieldPreview.tsx | 102 - .../create-channel/InputImageSetting.tsx | 319 - .../src/containers/create-channel/index.ts | 19 - .../create-project-complete/ApiKeySection.tsx | 155 - .../create-project-complete/MemberSection.tsx | 141 - .../ProjectInfoSection.tsx | 49 - .../create-project-complete/RoleSection.tsx | 46 - .../create-project-complete/index.ts | 20 - .../CreateProjectInputTemplate.tsx | 59 - .../containers/create-project/InputApiKey.tsx | 182 - .../create-project/InputIssueTracker.tsx | 191 - .../containers/create-project/InputMember.tsx | 403 - .../create-project/InputProjectInfo.tsx | 156 - .../containers/create-project/InputRole.tsx | 66 - .../src/containers/create-project/index.ts | 20 - apps/web/src/containers/dashboard/index.ts | 37 - apps/web/src/containers/main/TenantCard.tsx | 47 - apps/web/src/containers/main/index.ts | 17 - .../containers/my-profile/MyProfileForm.tsx | 119 - .../APIKeySetting/APIKeyDeleteButton.tsx | 58 - .../APIKeySetting/APIKeySetting.tsx | 239 - .../setting-menu/APIKeySetting/index.ts | 16 - .../ChannelDeleteSetting.tsx | 131 - .../ChannelDeleteSetting/index.ts | 16 - .../ChannelInfoSetting/ChannelInfoSetting.tsx | 118 - .../setting-menu/ChannelInfoSetting/index.ts | 16 - .../setting-menu/ChannelSettingMenu.tsx | 109 - .../FieldSetting/FieldSetting.tsx | 409 - .../setting-menu/FieldSetting/index.ts | 16 - .../ImageSetting/ImageSetting.tsx | 353 - .../setting-menu/ImageSetting/index.ts | 16 - .../setting-menu/IssueTrackerSetting/index.ts | 16 - .../MemberSetting/MemberInvitationDialog.tsx | 134 - .../MemberSetting/MemberSetting.tsx | 220 - .../setting-menu/MemberSetting/index.ts | 16 - .../ProjectDeleteSetting.tsx | 144 - .../ProjectDeleteSetting/index.ts | 16 - .../ProjectInfoSetting/ProjectInfoSetting.tsx | 127 - .../setting-menu/ProjectInfoSetting/index.ts | 16 - .../setting-menu/ProjectSettingMenu.tsx | 102 - .../RoleSetting/PermissionRows.tsx | 71 - .../RoleSetting/RoleSettingHead.tsx | 166 - .../RoleSetting/RoleSettingTable.tsx | 308 - .../setting-menu/SignUpSetting/OAuthInput.tsx | 74 - .../setting-menu/SignUpSetting/index.ts | 16 - .../TenantInfoSetting/TenantInfoSetting.tsx | 104 - .../setting-menu/TenantInfoSetting/index.ts | 16 - .../setting-menu/TenantSettingMenu.tsx | 72 - .../setting-menu/UserSetting/UserSetting.tsx | 303 - .../setting-menu/UserSetting/index.ts | 16 - .../WebhookSetting/WebhookSetting.tsx | 250 - .../WebhookSetting/WebhookUpsertDialog.tsx | 372 - .../setting-menu/WebhookSetting/index.ts | 16 - apps/web/src/containers/setting-menu/index.ts | 33 - .../FeedbackTable/AllExpandButton/index.ts | 16 - .../FeedbackTable/ChannelSelectBox/index.ts | 16 - .../ColumnSettingPopover/index.ts | 16 - .../FeedbackTable/DownloadButton/index.ts | 16 - .../FeedbackTable/EditableCell/index.ts | 16 - .../FeedbackTable/FeedbackCell/index.ts | 16 - .../FeedbackDeleteDialog/index.ts | 16 - .../FeedbackTable/FeedbackDetail/index.ts | 16 - .../tables/FeedbackTable/FeedbackTable.tsx | 342 - .../FeedbackTableBar/FeedbackTableBar.tsx | 238 - .../FeedbackTable/FeedbackTableBar/index.ts | 16 - .../FeedbackTable/FeedbackTableRow/index.ts | 16 - .../tables/FeedbackTable/IssueCell/index.ts | 16 - .../FeedbackTable/feedback-table-columns.tsx | 113 - .../FeedbackTable/feedback-table.context.tsx | 189 - .../containers/tables/FeedbackTable/index.ts | 17 - .../IssueTable/IssueSettingPopover/index.ts | 16 - .../tables/IssueTable/IssueTable.tsx | 523 -- .../IssueTable/IssueTableSelectBox/index.ts | 16 - .../tables/IssueTable/TableRow/index.ts | 16 - .../src/containers/tables/IssueTable/index.ts | 16 - apps/web/src/containers/tables/index.ts | 17 - .../src/contexts/create-channel.context.tsx | 152 - .../create-project-channel.context.tsx | 193 - .../src/contexts/create-project.context.tsx | 162 - apps/web/src/contexts/tenant.context.tsx | 81 - apps/web/src/contexts/user.context.tsx | 201 - .../src/entities/api-key/api-key-columns.tsx | 112 + .../src/entities/api-key/api-key.schema.ts | 23 + .../api-key}/api-key.type.ts | 16 +- apps/web/src/entities/api-key/index.ts | 17 + .../entities/api-key/ui/api-key-table.ui.tsx | 63 + .../api-key/ui/delete-api-key-button.ui.tsx | 38 + apps/web/src/entities/api-key/ui/index.ts | 16 + .../api-key/ui/update-api-key-popover.ui.tsx} | 63 +- .../src/entities/channel/channel.schema.tsx | 45 + apps/web/src/entities/channel/channel.type.ts | 27 + apps/web/src/entities/channel/index.ts | 18 + .../channel/ui/channel-info-form.ui.tsx | 65 + .../channel/ui/image-config-form.ui.tsx | 179 + .../buttons => entities/channel/ui}/index.ts | 4 +- .../dashboard}/index.ts | 2 +- apps/web/src/entities/dashboard/lib/index.ts | 16 + .../dashboard/lib/use-line-chart-data.ts} | 19 +- .../ui/create-feedback-per-issue-card.ui.tsx} | 3 +- .../dashboard/ui/feedback-line-chart.ui.tsx} | 15 +- apps/web/src/entities/dashboard/ui/index.ts | 38 + .../dashboard/ui/issue-bar-chart.ui.tsx} | 6 +- .../ui/issue-feedback-line-chart.ui.tsx} | 24 +- .../dashboard/ui/issue-line-chart.ui.tsx} | 9 +- .../dashboard/ui/issue-rank.ui.tsx} | 11 +- .../ui/seven-days-feedback-card.ui.tsx} | 3 +- .../ui/seven-days-issue-card.ui.tsx} | 3 +- .../ui/thirty-days-feedback-card.ui.tsx} | 3 +- .../ui/thirty-days-issue-card.ui.tsx} | 3 +- .../dashboard/ui/today-feedback-card.ui.tsx} | 3 +- .../dashboard/ui/today-issue-card.ui.tsx} | 3 +- .../dashboard/ui/total-feedback-card.ui.tsx} | 3 +- .../dashboard/ui/total-issue-card.ui.tsx} | 3 +- .../ui/yesterday-feedback-card.ui.tsx} | 3 +- .../dashboard/ui/yesterday-issue-card.ui.tsx} | 4 +- .../etc/Popper => entities/feedback}/index.ts | 2 +- apps/web/src/entities/feedback/lib/index.ts | 16 + .../feedback/lib/use-feedback-search.ts} | 11 +- apps/web/src/entities/field/field-columns.tsx | 140 + .../{utils => entities/field}/field-utils.ts | 16 +- .../field/field.constant.ts} | 18 +- apps/web/src/entities/field/field.schema.ts | 78 + apps/web/src/entities/field/field.type.ts | 34 + apps/web/src/entities/field/index.ts | 18 + .../ui/delete-field-option-popover.ui.tsx} | 15 +- .../ui/feedback-request-code-popover.ui.tsx} | 22 +- .../field/ui/field-setting-popover.ui.tsx} | 105 +- .../src/entities/field/ui/field-table.ui.tsx | 55 + apps/web/src/entities/field/ui/index.ts | 19 + .../field/ui/option-list-popover.ui.tsx} | 11 +- .../field/ui/preview-field-table.ui.tsx} | 115 +- apps/web/src/entities/issue-tracker/index.ts | 18 + .../issue-tracker/issue-tracker.schema.ts | 21 + .../issue-tracker}/issue-tracker.type.ts | 10 +- .../src/entities/issue-tracker/ui/index.ts | 16 + .../ui/issue-tracker-form.ui.tsx} | 46 +- apps/web/src/entities/issue/index.ts | 18 + .../issue/issue-color.constant.ts} | 16 +- .../{types => entities/issue}/issue.type.ts | 8 +- apps/web/src/entities/issue/lib/index.ts | 16 + .../issue/lib/use-issue-search.ts} | 6 +- apps/web/src/entities/issue/ui/index.ts | 17 + .../src/entities/issue/ui/issue-badge.ui.tsx | 34 + .../issue/ui/issue-circle.ui.tsx} | 34 +- .../member}/index.ts | 3 +- .../src/entities/member/member-columns.tsx | 101 + apps/web/src/entities/member/member.type.ts | 33 + .../member/ui/create-member-popover.ui.tsx | 118 + .../member/ui/delete-member-popover.ui.tsx} | 49 +- apps/web/src/entities/member/ui/index.ts | 17 + .../entities/member/ui/member-table.ui.tsx | 70 + .../member/ui/update-member-popover.ui.tsx} | 85 +- .../__mocks__/project.mock-data copy.ts | 33 + .../project/__mocks__/project.mock-data.ts | 33 + apps/web/src/entities/project/index.ts | 19 + .../src/entities/project/project.schema.tsx | 44 + .../project/project.type.ts} | 11 +- .../project/timezone.util.ts} | 12 +- apps/web/src/entities/project/ui/index.ts | 18 + .../project/ui/project-card.ui.spec.tsx | 20 + .../project/ui/project-card.ui.tsx} | 52 +- .../project/ui/project-guard.ui.tsx} | 41 +- .../project/ui/project-info-form.ui.tsx | 71 + .../project/ui/timezone-select-box.tsx} | 12 +- apps/web/src/entities/role/index.ts | 19 + .../web/src/entities/role/input-role.model.ts | 89 + .../role}/permission.type.ts | 0 apps/web/src/entities/role/role.schema.ts | 26 + .../src/{types => entities/role}/role.type.ts | 14 +- .../role/ui/create-role-popover.tsx} | 4 +- .../role/ui/delete-role-popover.ui.tsx} | 8 +- apps/web/src/entities/role/ui/index.ts | 19 + .../src/entities/role/ui/role-table.ui.tsx | 408 + .../role/ui/update-role-name-popover.ui.tsx} | 54 +- apps/web/src/entities/tenant/index.ts | 19 + apps/web/src/entities/tenant/tenant.model.ts | 39 + apps/web/src/entities/tenant/tenant.schema.ts | 55 + apps/web/src/entities/tenant/tenant.type.ts | 28 + apps/web/src/entities/tenant/ui/index.ts | 19 + .../tenant/ui/oauth-config-form.ui.tsx | 102 + .../tenant/ui/tenant-card.ui.spec.tsx | 20 + .../src/entities/tenant/ui/tenant-card.ui.tsx | 51 + .../tenant/ui/tenant-guard.ui.spec.tsx | 69 + .../tenant/ui/tenant-guard.ui.tsx} | 42 +- .../tenant/ui/tenant-info-form.ui.tsx | 52 + apps/web/src/entities/theme/index.ts | 17 + .../theme/theme.model.ts} | 10 +- apps/web/src/entities/theme/ui/index.ts | 16 + .../theme/ui/theme-toggle-button.ui.spec.tsx} | 27 +- .../theme/ui/theme-toggle-button.ui.tsx} | 10 +- apps/web/src/entities/user/index.ts | 19 + apps/web/src/entities/user/lib/index.ts | 16 + .../user/lib/use-user-search.ts} | 4 +- .../__snapshots__/user-box.ui.spec.tsx.snap | 100 + apps/web/src/entities/user/ui/index.ts | 18 + .../user/ui/invite-user-popover.tsx} | 44 +- .../user/ui/update-user-popover.ui.tsx} | 83 +- .../src/entities/user/ui/user-box.ui.spec.tsx | 80 + .../user/ui/user-box.ui.tsx} | 27 +- .../user/ui/user-management-table.ui.tsx | 136 + apps/web/src/entities/user/user-columns.tsx | 97 + apps/web/src/entities/user/user.model.ts | 95 + apps/web/src/entities/user/user.schema.ts | 49 + apps/web/src/entities/user/user.type.ts | 28 + apps/web/src/entities/webhook/index.ts | 17 + .../webhook/ui/create-webhook-popover.tsx | 108 + .../webhook/ui/delete-webhook-popover.ui.tsx} | 40 +- apps/web/src/entities/webhook/ui/index.ts | 18 + .../webhook/ui/update-webhook-popover.tsx | 109 + .../webhook/ui/webhook-event-cell.tsx} | 27 +- .../entities/webhook/ui/webhook-form.ui.tsx | 210 + .../entities/webhook/ui/webhook-switch.ui.tsx | 52 + .../entities/webhook/ui/webhook-table.ui.tsx | 59 + .../src/entities/webhook/webhook-column.tsx | 125 + .../src/entities/webhook/webhook.schema.ts | 56 + apps/web/src/entities/webhook/webhook.type.ts | 29 + apps/web/src/{env.mjs => env.ts} | 6 + .../auth/reset-password-with-email}/index.ts | 2 +- ...equest-reset-password-with-email.schema.ts | 20 + .../reset-password-with-email.schema.ts | 28 + ...reset-password-with-email.ui.spec.tsx.snap | 63 + ...-password-with-email-form.ui.spec.tsx.snap | 120 + .../reset-password-with-email/ui/index.ts | 17 + ...uest-reset-password-with-email.ui.spec.tsx | 97 + .../request-reset-password-with-email.ui.tsx | 90 + ...reset-password-with-email-form.ui.spec.tsx | 113 + .../ui/reset-password-with-email-form.ui.tsx | 114 + .../auth/sign-in-with-email}/index.ts | 2 +- .../sign-in-with-email.schema.ts | 21 + .../sign-in-with-email-form.ui.spec.tsx.snap | 161 + .../auth/sign-in-with-email/ui/index.ts | 16 + .../ui/sign-in-with-email-form.ui.spec.tsx | 157 + .../ui/sign-in-with-email-form.ui.tsx | 104 + .../sign-in-with-oauth.mock-handler.ts | 28 + .../auth/sign-in-with-oauth}/index.ts | 3 +- .../auth/sign-in-with-oauth/lib/index.ts | 16 + .../lib/use-oauth-callback.ts | 52 + ...sign-in-with-oauth-button.ui.spec.tsx.snap | 17 + .../auth/sign-in-with-oauth/ui/index.ts | 16 + .../ui/sign-in-with-oauth-button.ui.spec.tsx | 55 + .../ui/sign-in-with-oauth-button.ui.tsx} | 18 +- .../features/auth/sign-up-with-email/index.ts | 16 + .../sign-up-with-email.schema.ts | 29 + .../sign-up-with-email-form.ui.spec.tsx.snap | 132 + .../auth/sign-up-with-email/ui/index.ts | 16 + .../ui/sign-up-with-email-form.ui.spec.tsx} | 15 +- .../ui/sign-up-with-email-form.ui.tsx | 244 + .../create-api-key-button.ui.tsx | 61 + apps/web/src/features/create-api-key/index.ts | 16 + .../create-channel/create-channel-model.ts | 143 + .../create-channel/create-channel-type.tsx | 27 + .../create-channel.constant.tsx | 53 + apps/web/src/features/create-channel/index.ts | 16 + .../ui/create-channel-input-template.ui.tsx | 129 + .../create-channel/ui/create-channel.ui.tsx | 49 + .../src/features/create-channel/ui/index.ts | 17 + .../ui/input-channel-info-step.ui.tsx | 62 + .../ui/input-field-preview-step.ui.tsx | 36 + .../create-channel/ui/input-field-step.ui.tsx | 69 + .../ui/input-image-config-step.ui.tsx | 143 + .../ui/route-create-channel-button.ui.tsx} | 55 +- .../create-project/create-project-model.ts | 136 + .../create-project/create-project-type.tsx | 28 + .../create-project.constant.tsx | 59 + apps/web/src/features/create-project/index.ts | 16 + .../ui/create-project-input-template.ui.tsx | 147 + .../create-project/ui/create-project.ui.tsx | 49 + .../ui/delete-project-popover.ui.tsx | 76 + .../src/features/create-project/ui/index.ts | 18 + .../ui/input-api-key-step.ui.tsx | 66 + .../ui/input-issue-tracker-step.ui.tsx | 61 + .../ui/input-members-step.ui.tsx | 118 + .../ui/input-project-info-step.ui.tsx | 73 + .../create-project/ui/input-roles-step.ui.tsx | 68 + .../ui/route-create-project-button.ui.tsx} | 50 +- .../create-tenant-form.schema.ts} | 7 +- .../create-tenant/create-tenant-form.spec.tsx | 82 + .../create-tenant/create-tenant-form.ui.tsx | 93 + .../default-super-account.constant.ts} | 8 +- apps/web/src/features/create-tenant/index.ts | 16 + apps/web/src/features/delete-channel/index.ts | 16 + .../ui/delete-channel-popover.ui.tsx | 77 + .../src/features/delete-channel/ui/index.ts | 16 + .../delete-account-button.ui.spec.tsx.snap | 18 + .../delete-account-button.ui.spec.tsx | 106 + .../delete-user/delete-account-button.ui.tsx} | 22 +- apps/web/src/features/delete-user/index.ts | 16 + .../user-invitation-form.ui.spec.tsx.snap | 114 + apps/web/src/features/invite-user/index.ts | 16 + .../user-invitation-form.ui.spec.tsx | 104 + .../invite-user/user-invitation-form.ui.tsx | 108 + .../invite-user/user-invitation.schema.ts | 28 + .../user-profile-form.ui.spec.tsx.snap | 133 + .../change-password-form.schema.ts | 35 + .../change-password-form.ui.spec.tsx | 113 + .../update-user/change-password-form.ui.tsx} | 71 +- apps/web/src/features/update-user/index.ts | 17 + .../update-user/user-profile-form.schema.ts} | 11 +- .../update-user/user-profile-form.ui.spec.tsx | 129 + .../update-user/user-profile-form.ui.tsx | 107 + apps/web/src/hooks/index.ts | 34 - apps/web/src/hooks/useDayCount.ts | 22 - apps/web/src/hooks/useLocalStorage.ts | 26 - apps/web/src/middleware.ts | 10 +- apps/web/src/msw.ts | 49 + apps/web/src/pages/_app.tsx | 60 +- apps/web/src/pages/_document.tsx | 9 +- apps/web/src/pages/api/health.ts | 11 +- apps/web/src/pages/api/jwt.ts | 16 +- apps/web/src/pages/api/login.ts | 91 +- apps/web/src/pages/api/logout.ts | 20 +- apps/web/src/pages/api/oauth.ts | 79 +- apps/web/src/pages/api/refresh-jwt.ts | 68 +- apps/web/src/pages/auth/oauth-callback.tsx | 52 +- apps/web/src/pages/auth/reset-password.tsx | 101 +- apps/web/src/pages/auth/sign-in.tsx | 145 +- apps/web/src/pages/auth/sign-up.tsx | 278 +- apps/web/src/pages/index.tsx | 6 +- apps/web/src/pages/link/reset-password.tsx | 134 +- apps/web/src/pages/link/user-invitation.tsx | 133 +- apps/web/src/pages/main/index.tsx | 58 +- apps/web/src/pages/main/profile.tsx | 100 +- .../[projectId]/channel/create-complete.tsx | 101 +- .../project/[projectId]/channel/create.tsx | 80 +- .../main/project/[projectId]/dashboard.tsx | 254 +- .../main/project/[projectId]/feedback.tsx | 26 +- .../pages/main/project/[projectId]/issue.tsx | 24 +- .../project/[projectId]/not-permission.tsx | 8 +- .../main/project/[projectId]/setting.tsx | 97 +- .../pages/main/project/create-complete.tsx | 109 +- apps/web/src/pages/main/project/create.tsx | 71 +- apps/web/src/pages/tenant/create.tsx | 94 +- apps/web/src/server/api-handler.ts | 70 + .../src/{constants => server}/iron-option.ts | 6 +- apps/web/src/{libs => server}/logger.ts | 1 + .../constants/background-color.ts} | 21 +- .../{ => shared}/constants/chart-colors.ts | 0 .../constants/date-format.ts} | 0 apps/web/src/{ => shared}/constants/i18n.ts | 2 - apps/web/src/shared/constants/index.ts | 22 + apps/web/src/{ => shared}/constants/issues.ts | 27 +- .../constants/local-storage-key.ts | 0 apps/web/src/{ => shared}/constants/path.ts | 61 +- apps/web/src/shared/index.ts | 20 + apps/web/src/{libs => shared/lib}/client.ts | 54 +- .../webhook.type.ts => shared/lib/index.ts} | 31 +- .../{libs => shared/lib}/session-storage.ts | 15 +- .../lib/use-horizontal-scroll.ts} | 11 +- .../lib/use-local-column-setting.ts} | 0 .../lib/use-permissions.ts} | 13 +- .../lib/use-query-params-state.ts} | 58 +- .../useSort.ts => shared/lib/use-sort.ts} | 16 +- .../{hooks => shared/lib}/useOAIMutation.ts | 9 +- .../src/{hooks => shared/lib}/useOAIQuery.ts | 7 +- .../_app.css => shared/styles/global.css} | 3 + .../{ => shared}/styles/react-datepicker.css | 0 apps/web/src/{ => shared}/types/api.type.ts | 1320 ++- .../src/{ => shared}/types/date-range.type.ts | 0 .../{ => shared}/types/fetch-error.type.ts | 2 +- apps/web/src/{ => shared}/types/i18n.d.ts | 12 +- apps/web/src/shared/types/index.ts | 21 + apps/web/src/shared/types/jwt.type.ts | 19 + .../src/{ => shared}/types/openapi.type.ts | 2 +- .../types/page-with-layout.type.ts} | 9 +- .../shared/types/react-query-state.type.ts | 16 + apps/web/src/{ => shared}/types/svg.d.ts | 0 .../__snapshots__/main-card.ui.spec.tsx.snap | 69 + .../ui/charts/chart-container.tsx} | 6 +- .../ui/charts/chart-filter.tsx} | 0 .../RoleSetting => shared/ui/charts}/index.ts | 4 +- .../ui/charts/legend.tsx} | 2 +- .../ui/charts/simple-bar-chart.tsx} | 17 +- .../ui/charts/simple-line-chart.tsx} | 140 +- .../ui/create-input-template.ui.tsx} | 40 +- .../ui/create-section-template.ui.tsx.tsx} | 10 +- apps/web/src/shared/ui/create-template.ui.tsx | 136 + .../ui/dashboard-card.tsx} | 41 +- .../ui/date-range-picker.tsx} | 27 +- .../ui/description-tooltip.tsx} | 4 +- .../ui/expandable-text.ui.tsx} | 4 +- .../ui/help-card-docs.tsx} | 15 +- .../ui/image-preview-button.tsx} | 6 +- apps/web/src/shared/ui/image-slider.ui.tsx | 93 + apps/web/src/shared/ui/index.ts | 43 + .../ui/locale-select-box.ui.tsx} | 10 +- apps/web/src/shared/ui/logo-with-title.ui.tsx | 41 + .../Header/Logo.tsx => shared/ui/logo.ui.tsx} | 2 +- apps/web/src/shared/ui/main-card.ui.spec.tsx | 45 + apps/web/src/shared/ui/main-card.ui.tsx | 69 + .../Popper.tsx => shared/ui/popper.ui.tsx} | 1 + .../ui/radio-group.tsx} | 2 +- .../ui/section-template.ui.tsx} | 26 +- apps/web/src/shared/ui/select-box/index.ts | 18 + .../ui/select-box/select-box-creatable.tsx} | 16 +- .../ui/select-box/select-box.tsx} | 16 +- .../ui/share-button.tsx} | 4 +- apps/web/src/shared/ui/small-card.ui.tsx | 81 + apps/web/src/shared/ui/sub-menu.ui.tsx | 57 + .../src/shared/ui/tables/basic-table.ui.tsx | 102 + .../ui/tables/checked-table-head.tsx} | 57 +- .../ui/tables/dashboard-table.tsx} | 10 +- .../cards => shared/ui/tables}/index.ts | 15 +- .../ui/tables/table-checkbox.tsx} | 0 .../ui/tables/table-loading-row.tsx} | 0 .../ui/tables/table-pagination.tsx} | 0 .../ui/tables/table-resizer.tsx} | 14 +- .../ui/tables/table-row.tsx} | 18 +- .../ui/tables/table-search-input/index.ts | 18 + .../table-search-input-popover.tsx} | 28 +- .../table-search-input.service.ts | 85 +- .../table-search-input.tsx} | 59 +- .../ui/tables/table-sort-icon.tsx} | 6 +- .../utils/cn.ts} | 12 +- .../utils/display-string.ts} | 5 +- .../utils/empty-function.ts} | 4 +- .../utils/get-day-count.ts} | 10 +- apps/web/src/shared/utils/index.ts | 23 + .../src/shared/utils/parse-as-date-range.ts | 37 + .../src/{ => shared}/utils/path-parsing.ts | 13 +- .../utils/remove-empty-value-in-object.ts | 2 +- apps/web/src/{ => shared}/utils/reorder.ts | 0 apps/web/src/shared/utils/type-guard.ts | 28 + apps/web/src/{utils => }/test-utils.tsx | 19 +- apps/web/src/types/channel.type.ts | 38 - apps/web/src/types/color.type.ts | 23 - apps/web/src/types/field.type.ts | 60 - apps/web/src/types/locale.type.ts | 17 - apps/web/src/types/project.type.ts | 29 - apps/web/src/types/tenant.type.ts | 36 - apps/web/src/utils/is-not-empty-string.ts | 19 - apps/web/src/utils/str.ts | 20 - .../widgets/dashboard-card-slider/index.ts | 16 + .../ui/card-slider.ui.tsx | 80 + .../ui/dashboard-card-slider.ui.tsx | 59 + .../widgets/dashboard-card-slider/ui/index.ts | 16 + .../feedback-table/feedback-table-columns.tsx | 117 + apps/web/src/widgets/feedback-table/index.ts | 16 + .../src/widgets/feedback-table/lib/index.ts | 17 + .../lib/use-feedback-download.ts} | 21 +- .../lib/use-truncated-element.tsx} | 0 .../model/feedback-row.store.ts} | 15 +- .../model/feedback-table.context.tsx | 117 + .../src/widgets/feedback-table/model/index.ts | 17 + .../ui}/FeedbackTableWrapper.tsx | 24 +- .../feedback-table/ui/channel-select-box.tsx} | 24 +- .../column-setting-popover.tsx} | 102 +- .../draggable-column-item.tsx} | 8 +- .../ui/column-setting-popover}/index.ts | 2 +- .../feedback-table/ui/editable-cell.tsx} | 29 +- .../feedback-table/ui/feedback-cell.tsx} | 14 +- .../ui/feedback-delete-dialog.tsx} | 33 +- .../feedback-detail/feedback-detail-cell.tsx} | 7 +- .../feedback-detail-issue-cell.tsx} | 16 +- .../ui/feedback-detail/feedback-detail.tsx} | 145 +- .../ui/feedback-detail}/index.ts | 2 +- .../feedback-table/ui/feedback-table-bar.tsx | 207 + .../ui/feedback-table-download-button.ui.tsx} | 63 +- ...feedback-table-expand-button-group.ui.tsx} | 24 +- .../ui/feedback-table-in-issue.tsx} | 21 +- .../feedback-table/ui/feedback-table-row.tsx} | 43 +- .../feedback-table/ui/feedback-table.tsx | 363 + .../src/widgets/feedback-table/ui/index.ts | 17 + .../feedback-table/ui/issue-cell/index.ts | 16 + .../ui/issue-cell/issue-cell.tsx} | 120 +- .../ui/issue-cell/issue-setting.tsx} | 18 +- apps/web/src/widgets/index.ts | 16 + apps/web/src/widgets/issue-table/index.ts | 16 + .../issue-table/issue-table-columns.tsx | 127 + apps/web/src/widgets/issue-table/lib/index.ts | 17 + .../issue-table/lib/use-issue-count.ts} | 9 +- .../issue-table/lib/use-issue-query.ts | 82 + apps/web/src/widgets/issue-table/ui/index.ts | 16 + .../ui/issue-deletion-popover.ui.tsx | 76 + .../issue-table/ui/issue-select-box.ui.tsx} | 15 +- .../ui/issue-setting-popover.ui.tsx} | 30 +- .../widgets/issue-table/ui/issue-table.ui.tsx | 314 + .../issue-table/ui/ticket-link.ui.tsx} | 44 +- apps/web/src/widgets/main-layout/index.ts | 16 + .../main-layout.ui.spec.tsx.snap | 401 + .../main-layout/ui/breadcrumb.tsx} | 20 +- apps/web/src/widgets/main-layout/ui/index.ts | 16 + .../main-layout/ui/main-layout.ui.spec.tsx | 48 + .../widgets/main-layout/ui/main-layout.ui.tsx | 65 + .../main-layout/ui/side-nav.ui.tsx} | 27 +- apps/web/src/widgets/setting-menu/index.ts | 17 + .../setting-menu}/setting-menu.type.ts | 0 .../setting-menu/ui/channel-setting-menu.tsx | 116 + .../channel/channel-deletion-setting.ui.tsx | 95 + .../ui/channel/channel-info-setting.ui.tsx | 97 + .../ui/channel/field-setting.ui.tsx | 283 + .../ui/channel/image-config-setting.ui.tsx | 183 + .../widgets/setting-menu/ui/channel/index.ts | 19 + apps/web/src/widgets/setting-menu/ui/index.ts | 24 + .../setting-menu/ui/project-setting-menu.tsx | 105 + .../ui/project/api-key-setting.ui.tsx | 120 + .../setting-menu/ui/project}/index.ts | 11 +- .../ui/project/issue-tracker-setting.ui.tsx} | 97 +- .../ui/project/member-setting.ui.tsx | 138 + .../project/project-deletion-setting.ui.tsx | 138 + .../ui/project/project-info-setting.ui.tsx | 97 + .../ui/project/role-setting.ui.tsx} | 64 +- .../ui/project/webhook-setting.ui.tsx | 106 + .../setting-menu/ui/setting-menu-box.tsx} | 23 +- .../setting-menu/ui/setting-menu-item.tsx} | 8 +- .../ui/setting-menu-template.tsx} | 9 +- .../setting-menu/ui/tenant-setting-menu.tsx | 76 + .../ui/tenant/auth-setting.ui.tsx} | 97 +- .../widgets/setting-menu/ui/tenant/index.ts | 18 + .../ui/tenant/tenant-info-setting.ui.tsx | 86 + .../ui/tenant/user-management-setting.ui.tsx} | 23 +- apps/web/tsconfig.json | 10 +- docker/api.dockerfile | 2 +- docker/docker-compose.infra-amd64.yml | 98 + ...fra.yml => docker-compose.infra-arm64.yml} | 15 +- docker/web.dockerfile | 4 +- package.json | 19 +- packages/ufb-shared/eslint.config.js | 9 + packages/ufb-shared/package.json | 17 +- packages/ufb-shared/tsconfig.json | 2 +- packages/ufb-tailwind/eslint.config.js | 9 + packages/ufb-tailwind/package.json | 10 +- packages/ufb-ui/__mocks__/svg.js | 17 - packages/ufb-ui/eslint.config.js | 11 + packages/ufb-ui/package.json | 20 +- packages/ufb-ui/src/Badge/Badge.tsx | 9 +- packages/ufb-ui/src/Icon/Icon.tsx | 3 +- packages/ufb-ui/src/Popover/Popover.tsx | 8 +- packages/ufb-ui/src/Toast/ToastBox.tsx | 2 +- packages/ufb-ui/src/Toast/toast.tsx | 4 +- packages/ufb-ui/src/Tooltip/Tooltip.tsx | 3 + packages/ufb-ui/src/index.ts | 1 + packages/ufb-ui/src/inputs/Input.tsx | 3 +- packages/ufb-ui/src/inputs/TextInput.tsx | 20 +- packages/ufb-ui/src/types/index.ts | 16 + packages/ufb-ui/src/utils.ts | 46 + packages/ufb-ui/tailwind.config.js | 5 - packages/ufb-ui/tailwind.config.ts | 8 + packages/ufb-ui/tsconfig.json | 8 +- pnpm-lock.yaml | 7460 +++++++++-------- tooling/eslint-plugin-header/index.js | 6 + tooling/eslint-plugin-header/package.json | 5 + .../src/comment-parser.js | 18 + .../eslint-plugin-header/src/rules/header.js | 491 ++ tooling/eslint/base.js | 126 +- tooling/eslint/nestjs.js | 21 +- tooling/eslint/nextjs.js | 26 +- tooling/eslint/package.json | 40 +- tooling/eslint/react.js | 38 +- tooling/eslint/tsconfig.json | 2 +- tooling/eslint/type.d.ts | 65 + tooling/prettier/index.js | 5 + tooling/prettier/package.json | 8 +- tooling/prettier/tsconfig.json | 2 +- tooling/typescript/base.json | 22 + tooling/typescript/internal-package.json | 10 + tooling/typescript/nestjs.json | 8 +- tooling/typescript/package.json | 5 +- tooling/typescript/react.json | 22 - turbo.json | 21 +- 699 files changed, 26492 insertions(+), 18762 deletions(-) create mode 100644 .npmrc delete mode 100644 apps/api/.eslintrc.js create mode 100644 apps/api/eslint.config.mjs rename apps/{web/src/constants/default-date-range.ts => api/src/domains/admin/project/issue-tracker/dtos/issue-tracker-data.dto.ts} (69%) delete mode 100644 apps/e2e/scenarios/sign-up.spec.ts delete mode 100644 apps/web/.prettierignore create mode 100644 apps/web/JSDOMEnvironment.ts create mode 100644 apps/web/eslint.config.js delete mode 100644 apps/web/jest.config.mjs create mode 100644 apps/web/jest.config.ts create mode 100644 apps/web/jest.polyfills.js create mode 100644 apps/web/jest.setup.ts rename apps/web/{postcss.config.js => postcss.config.cjs} (100%) create mode 100644 apps/web/src/__mocks__/zustand.ts create mode 100644 apps/web/src/__test__/api/health.spec.ts create mode 100644 apps/web/src/__test__/api/jwt.spec.ts create mode 100644 apps/web/src/__test__/api/login.spec.ts create mode 100644 apps/web/src/__test__/api/logout.spec.ts create mode 100644 apps/web/src/__test__/api/oauth.spec.ts create mode 100644 apps/web/src/__test__/api/refresh-jwt.spec.ts create mode 100644 apps/web/src/__test__/auth/__snapshots__/reset-password.spec.tsx.snap create mode 100644 apps/web/src/__test__/auth/__snapshots__/sign-in.spec.tsx.snap create mode 100644 apps/web/src/__test__/auth/__snapshots__/sign-up.spec.tsx.snap create mode 100644 apps/web/src/__test__/auth/oauth-callback.spec.tsx create mode 100644 apps/web/src/__test__/auth/reset-password.spec.tsx create mode 100644 apps/web/src/__test__/auth/sign-in.spec.tsx create mode 100644 apps/web/src/__test__/auth/sign-up.spec.tsx create mode 100644 apps/web/src/__test__/index.spec.tsx create mode 100644 apps/web/src/__test__/link/__snapshots__/reset-password.spec.tsx.snap create mode 100644 apps/web/src/__test__/link/__snapshots__/user-invitation.spec.tsx.snap create mode 100644 apps/web/src/__test__/link/reset-password.spec.tsx create mode 100644 apps/web/src/__test__/link/user-invitation.spec.tsx create mode 100644 apps/web/src/__test__/main/__snapshots__/index.spec.tsx.snap create mode 100644 apps/web/src/__test__/main/__snapshots__/profile.spec.tsx.snap create mode 100644 apps/web/src/__test__/main/index.spec.tsx create mode 100644 apps/web/src/__test__/main/profile.spec.tsx create mode 100644 apps/web/src/__test__/tenant/__snapshots__/create.spec.tsx.snap create mode 100644 apps/web/src/__test__/tenant/create.spec.tsx delete mode 100644 apps/web/src/components/cards/ChannelCard/ChannelCard.tsx delete mode 100644 apps/web/src/components/cards/TenantProjectCard/TenantProjectCard.tsx delete mode 100644 apps/web/src/components/charts/SimpleLineChart.tsx delete mode 100644 apps/web/src/components/charts/index.ts delete mode 100644 apps/web/src/components/etc/CheckedTableHead/index.ts delete mode 100644 apps/web/src/components/etc/DashboardTable/index.ts delete mode 100644 apps/web/src/components/etc/DescriptionTooltip/index.ts delete mode 100644 apps/web/src/components/etc/ExpandableText/index.ts delete mode 100644 apps/web/src/components/etc/HelpCardDocs/index.ts delete mode 100644 apps/web/src/components/etc/IssueCircle/IssueCircle.tsx delete mode 100644 apps/web/src/components/etc/IssueCircle/index.ts delete mode 100644 apps/web/src/components/etc/SelectBox/index.ts delete mode 100644 apps/web/src/components/etc/SelectBoxWithIcon/SelectBoxWithIcon.tsx delete mode 100644 apps/web/src/components/etc/SelectBoxWithIcon/index.ts delete mode 100644 apps/web/src/components/etc/TableCheckbox/index.ts delete mode 100644 apps/web/src/components/etc/TableLoadingRow/index.ts delete mode 100644 apps/web/src/components/etc/TablePagination/index.ts delete mode 100644 apps/web/src/components/etc/TableResizer/index.ts delete mode 100644 apps/web/src/components/etc/TableSearchInput/index.ts delete mode 100644 apps/web/src/components/etc/TableSortIcon/index.ts delete mode 100644 apps/web/src/components/etc/TimezoneSelectBox/index.ts delete mode 100644 apps/web/src/components/etc/index.ts delete mode 100644 apps/web/src/components/index.ts delete mode 100644 apps/web/src/components/layouts/Header/Header.tsx delete mode 100644 apps/web/src/components/layouts/index.ts delete mode 100644 apps/web/src/components/layouts/setting-menu/SettingMenuSubtitle.tsx delete mode 100644 apps/web/src/components/layouts/setting-menu/index.ts delete mode 100644 apps/web/src/components/popovers/index.ts delete mode 100644 apps/web/src/components/templates/AuthTemplate/AuthTemplate.tsx delete mode 100644 apps/web/src/components/templates/AuthTemplate/index.ts delete mode 100644 apps/web/src/components/templates/CreateProjectChannelInputTemplate/index.ts delete mode 100644 apps/web/src/components/templates/CreateProjectChannelTemplate/CreateProjectChannelTemplate.tsx delete mode 100644 apps/web/src/components/templates/CreateProjectChannelTemplate/index.ts delete mode 100644 apps/web/src/components/templates/CreateSectionTemplate/index.ts delete mode 100644 apps/web/src/components/templates/MainTemplate/index.ts delete mode 100644 apps/web/src/components/templates/SettingMenuTemplate/index.ts delete mode 100644 apps/web/src/constants/is-server.ts delete mode 100644 apps/web/src/containers/buttons/CreateChannelButton/index.ts delete mode 100644 apps/web/src/containers/buttons/CreateProjectButton/index.ts delete mode 100644 apps/web/src/containers/buttons/OAuthLoginButton/index.ts delete mode 100644 apps/web/src/containers/buttons/ShareButton/index.ts delete mode 100644 apps/web/src/containers/create-channel-complete/ChannelInfoSection.tsx delete mode 100644 apps/web/src/containers/create-channel-complete/FieldSection.tsx delete mode 100644 apps/web/src/containers/create-channel-complete/ImageUploadSection.tsx delete mode 100644 apps/web/src/containers/create-channel-complete/index.ts delete mode 100644 apps/web/src/containers/create-channel/CreateChannelInputTemplate.tsx delete mode 100644 apps/web/src/containers/create-channel/InputChannelInfo.tsx delete mode 100644 apps/web/src/containers/create-channel/InputField.tsx delete mode 100644 apps/web/src/containers/create-channel/InputFieldPreview.tsx delete mode 100644 apps/web/src/containers/create-channel/InputImageSetting.tsx delete mode 100644 apps/web/src/containers/create-channel/index.ts delete mode 100644 apps/web/src/containers/create-project-complete/ApiKeySection.tsx delete mode 100644 apps/web/src/containers/create-project-complete/MemberSection.tsx delete mode 100644 apps/web/src/containers/create-project-complete/ProjectInfoSection.tsx delete mode 100644 apps/web/src/containers/create-project-complete/RoleSection.tsx delete mode 100644 apps/web/src/containers/create-project-complete/index.ts delete mode 100644 apps/web/src/containers/create-project/CreateProjectInputTemplate.tsx delete mode 100644 apps/web/src/containers/create-project/InputApiKey.tsx delete mode 100644 apps/web/src/containers/create-project/InputIssueTracker.tsx delete mode 100644 apps/web/src/containers/create-project/InputMember.tsx delete mode 100644 apps/web/src/containers/create-project/InputProjectInfo.tsx delete mode 100644 apps/web/src/containers/create-project/InputRole.tsx delete mode 100644 apps/web/src/containers/create-project/index.ts delete mode 100644 apps/web/src/containers/dashboard/index.ts delete mode 100644 apps/web/src/containers/main/TenantCard.tsx delete mode 100644 apps/web/src/containers/main/index.ts delete mode 100644 apps/web/src/containers/my-profile/MyProfileForm.tsx delete mode 100644 apps/web/src/containers/setting-menu/APIKeySetting/APIKeyDeleteButton.tsx delete mode 100644 apps/web/src/containers/setting-menu/APIKeySetting/APIKeySetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/APIKeySetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/ChannelDeleteSetting/ChannelDeleteSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/ChannelDeleteSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/ChannelInfoSetting/ChannelInfoSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/ChannelInfoSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/ChannelSettingMenu.tsx delete mode 100644 apps/web/src/containers/setting-menu/FieldSetting/FieldSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/FieldSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/ImageSetting/ImageSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/ImageSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/IssueTrackerSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/MemberSetting/MemberInvitationDialog.tsx delete mode 100644 apps/web/src/containers/setting-menu/MemberSetting/MemberSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/MemberSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/ProjectDeleteSetting/ProjectDeleteSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/ProjectDeleteSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/ProjectInfoSetting/ProjectInfoSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/ProjectInfoSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/ProjectSettingMenu.tsx delete mode 100644 apps/web/src/containers/setting-menu/RoleSetting/PermissionRows.tsx delete mode 100644 apps/web/src/containers/setting-menu/RoleSetting/RoleSettingHead.tsx delete mode 100644 apps/web/src/containers/setting-menu/RoleSetting/RoleSettingTable.tsx delete mode 100644 apps/web/src/containers/setting-menu/SignUpSetting/OAuthInput.tsx delete mode 100644 apps/web/src/containers/setting-menu/SignUpSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/TenantInfoSetting/TenantInfoSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/TenantInfoSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/TenantSettingMenu.tsx delete mode 100644 apps/web/src/containers/setting-menu/UserSetting/UserSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/UserSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/WebhookSetting/WebhookSetting.tsx delete mode 100644 apps/web/src/containers/setting-menu/WebhookSetting/WebhookUpsertDialog.tsx delete mode 100644 apps/web/src/containers/setting-menu/WebhookSetting/index.ts delete mode 100644 apps/web/src/containers/setting-menu/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/AllExpandButton/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/ChannelSelectBox/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/ColumnSettingPopover/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/DownloadButton/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/EditableCell/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/FeedbackCell/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/FeedbackDeleteDialog/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/FeedbackDetail/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/FeedbackTable.tsx delete mode 100644 apps/web/src/containers/tables/FeedbackTable/FeedbackTableBar/FeedbackTableBar.tsx delete mode 100644 apps/web/src/containers/tables/FeedbackTable/FeedbackTableBar/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/FeedbackTableRow/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/IssueCell/index.ts delete mode 100644 apps/web/src/containers/tables/FeedbackTable/feedback-table-columns.tsx delete mode 100644 apps/web/src/containers/tables/FeedbackTable/feedback-table.context.tsx delete mode 100644 apps/web/src/containers/tables/FeedbackTable/index.ts delete mode 100644 apps/web/src/containers/tables/IssueTable/IssueSettingPopover/index.ts delete mode 100644 apps/web/src/containers/tables/IssueTable/IssueTable.tsx delete mode 100644 apps/web/src/containers/tables/IssueTable/IssueTableSelectBox/index.ts delete mode 100644 apps/web/src/containers/tables/IssueTable/TableRow/index.ts delete mode 100644 apps/web/src/containers/tables/IssueTable/index.ts delete mode 100644 apps/web/src/containers/tables/index.ts delete mode 100644 apps/web/src/contexts/create-channel.context.tsx delete mode 100644 apps/web/src/contexts/create-project-channel.context.tsx delete mode 100644 apps/web/src/contexts/create-project.context.tsx delete mode 100644 apps/web/src/contexts/tenant.context.tsx delete mode 100644 apps/web/src/contexts/user.context.tsx create mode 100644 apps/web/src/entities/api-key/api-key-columns.tsx create mode 100644 apps/web/src/entities/api-key/api-key.schema.ts rename apps/web/src/{types => entities/api-key}/api-key.type.ts (75%) create mode 100644 apps/web/src/entities/api-key/index.ts create mode 100644 apps/web/src/entities/api-key/ui/api-key-table.ui.tsx create mode 100644 apps/web/src/entities/api-key/ui/delete-api-key-button.ui.tsx create mode 100644 apps/web/src/entities/api-key/ui/index.ts rename apps/web/src/{containers/setting-menu/APIKeySetting/APIKeyEditButton.tsx => entities/api-key/ui/update-api-key-popover.ui.tsx} (62%) create mode 100644 apps/web/src/entities/channel/channel.schema.tsx create mode 100644 apps/web/src/entities/channel/channel.type.ts create mode 100644 apps/web/src/entities/channel/index.ts create mode 100644 apps/web/src/entities/channel/ui/channel-info-form.ui.tsx create mode 100644 apps/web/src/entities/channel/ui/image-config-form.ui.tsx rename apps/web/src/{components/buttons => entities/channel/ui}/index.ts (82%) rename apps/web/src/{containers => entities/dashboard}/index.ts (96%) create mode 100644 apps/web/src/entities/dashboard/lib/index.ts rename apps/web/src/{hooks/useLineChartData.ts => entities/dashboard/lib/use-line-chart-data.ts} (88%) rename apps/web/src/{containers/dashboard/CreateFeedbackPerIssueCard.tsx => entities/dashboard/ui/create-feedback-per-issue-card.ui.tsx} (95%) rename apps/web/src/{containers/dashboard/FeedbackLineChart.tsx => entities/dashboard/ui/feedback-line-chart.ui.tsx} (94%) create mode 100644 apps/web/src/entities/dashboard/ui/index.ts rename apps/web/src/{containers/dashboard/IssueBarChart.tsx => entities/dashboard/ui/issue-bar-chart.ui.tsx} (90%) rename apps/web/src/{containers/dashboard/IssueFeedbackLineChart.tsx => entities/dashboard/ui/issue-feedback-line-chart.ui.tsx} (91%) rename apps/web/src/{containers/dashboard/IssueLineChart.tsx => entities/dashboard/ui/issue-line-chart.ui.tsx} (89%) rename apps/web/src/{containers/dashboard/IssueRank.tsx => entities/dashboard/ui/issue-rank.ui.tsx} (93%) rename apps/web/src/{containers/dashboard/SevenDaysFeedbackCard.tsx => entities/dashboard/ui/seven-days-feedback-card.ui.tsx} (96%) rename apps/web/src/{containers/dashboard/SevenDaysIssueCard.tsx => entities/dashboard/ui/seven-days-issue-card.ui.tsx} (96%) rename apps/web/src/{containers/dashboard/ThirtyDaysFeedbackCard.tsx => entities/dashboard/ui/thirty-days-feedback-card.ui.tsx} (96%) rename apps/web/src/{containers/dashboard/ThirtyDaysIssueCard.tsx => entities/dashboard/ui/thirty-days-issue-card.ui.tsx} (96%) rename apps/web/src/{containers/dashboard/TodayFeedbackCard.tsx => entities/dashboard/ui/today-feedback-card.ui.tsx} (96%) rename apps/web/src/{containers/dashboard/TodayIssueCard.tsx => entities/dashboard/ui/today-issue-card.ui.tsx} (96%) rename apps/web/src/{containers/dashboard/TotalFeedbackCard.tsx => entities/dashboard/ui/total-feedback-card.ui.tsx} (95%) rename apps/web/src/{containers/dashboard/TotalIssueCard.tsx => entities/dashboard/ui/total-issue-card.ui.tsx} (95%) rename apps/web/src/{containers/dashboard/YesterdayFeedbackCard.tsx => entities/dashboard/ui/yesterday-feedback-card.ui.tsx} (96%) rename apps/web/src/{containers/dashboard/YesterdayIssueCard.tsx => entities/dashboard/ui/yesterday-issue-card.ui.tsx} (96%) rename apps/web/src/{components/etc/Popper => entities/feedback}/index.ts (94%) create mode 100644 apps/web/src/entities/feedback/lib/index.ts rename apps/web/src/{hooks/useFeedbackSearch.ts => entities/feedback/lib/use-feedback-search.ts} (89%) create mode 100644 apps/web/src/entities/field/field-columns.tsx rename apps/web/src/{utils => entities/field}/field-utils.ts (72%) rename apps/web/src/{constants/colors.ts => entities/field/field.constant.ts} (84%) create mode 100644 apps/web/src/entities/field/field.schema.ts create mode 100644 apps/web/src/entities/field/field.type.ts create mode 100644 apps/web/src/entities/field/index.ts rename apps/web/src/{containers/setting-menu/FieldSetting/OptionBadge.tsx => entities/field/ui/delete-field-option-popover.ui.tsx} (91%) rename apps/web/src/{containers/setting-menu/FieldSetting/FeedbackRequestPopover.tsx => entities/field/ui/feedback-request-code-popover.ui.tsx} (87%) rename apps/web/src/{containers/setting-menu/FieldSetting/FieldSettingPopover.tsx => entities/field/ui/field-setting-popover.ui.tsx} (84%) create mode 100644 apps/web/src/entities/field/ui/field-table.ui.tsx create mode 100644 apps/web/src/entities/field/ui/index.ts rename apps/web/src/{containers/setting-menu/FieldSetting/OptionInfoPopover.tsx => entities/field/ui/option-list-popover.ui.tsx} (86%) rename apps/web/src/{containers/setting-menu/FieldSetting/PreviewTable.tsx => entities/field/ui/preview-field-table.ui.tsx} (63%) create mode 100644 apps/web/src/entities/issue-tracker/index.ts create mode 100644 apps/web/src/entities/issue-tracker/issue-tracker.schema.ts rename apps/web/src/{types => entities/issue-tracker}/issue-tracker.type.ts (79%) create mode 100644 apps/web/src/entities/issue-tracker/ui/index.ts rename apps/web/src/{containers/create-project-complete/IssueTrackerSection.tsx => entities/issue-tracker/ui/issue-tracker-form.ui.tsx} (52%) create mode 100644 apps/web/src/entities/issue/index.ts rename apps/web/src/{hooks/useChannels.ts => entities/issue/issue-color.constant.ts} (71%) rename apps/web/src/{types => entities/issue}/issue.type.ts (94%) create mode 100644 apps/web/src/entities/issue/lib/index.ts rename apps/web/src/{hooks/useIssueSearch.ts => entities/issue/lib/use-issue-search.ts} (91%) create mode 100644 apps/web/src/entities/issue/ui/index.ts create mode 100644 apps/web/src/entities/issue/ui/issue-badge.ui.tsx rename apps/web/src/{utils/rand-light-color.ts => entities/issue/ui/issue-circle.ui.tsx} (56%) rename apps/web/src/{components/cards/TenantProjectCard => entities/member}/index.ts (92%) create mode 100644 apps/web/src/entities/member/member-columns.tsx create mode 100644 apps/web/src/entities/member/member.type.ts create mode 100644 apps/web/src/entities/member/ui/create-member-popover.ui.tsx rename apps/web/src/{containers/setting-menu/MemberSetting/MemberDeleteDialog.tsx => entities/member/ui/delete-member-popover.ui.tsx} (64%) create mode 100644 apps/web/src/entities/member/ui/index.ts create mode 100644 apps/web/src/entities/member/ui/member-table.ui.tsx rename apps/web/src/{containers/setting-menu/MemberSetting/MemberUpdatePopover.tsx => entities/member/ui/update-member-popover.ui.tsx} (53%) create mode 100644 apps/web/src/entities/project/__mocks__/project.mock-data copy.ts create mode 100644 apps/web/src/entities/project/__mocks__/project.mock-data.ts create mode 100644 apps/web/src/entities/project/index.ts create mode 100644 apps/web/src/entities/project/project.schema.tsx rename apps/web/src/{components/templates/index.ts => entities/project/project.type.ts} (70%) rename apps/web/src/{utils/timezone.ts => entities/project/timezone.util.ts} (82%) create mode 100644 apps/web/src/entities/project/ui/index.ts create mode 100644 apps/web/src/entities/project/ui/project-card.ui.spec.tsx rename apps/web/src/{containers/main/ProjectCard.tsx => entities/project/ui/project-card.ui.tsx} (52%) rename apps/web/src/{containers/setting-menu/ProjectDeleteSetting/ChannelCardList.tsx => entities/project/ui/project-guard.ui.tsx} (51%) create mode 100644 apps/web/src/entities/project/ui/project-info-form.ui.tsx rename apps/web/src/{components/etc/TimezoneSelectBox/TimezoneSelectBox.tsx => entities/project/ui/timezone-select-box.tsx} (91%) create mode 100644 apps/web/src/entities/role/index.ts create mode 100644 apps/web/src/entities/role/input-role.model.ts rename apps/web/src/{types => entities/role}/permission.type.ts (100%) create mode 100644 apps/web/src/entities/role/role.schema.ts rename apps/web/src/{types => entities/role}/role.type.ts (76%) rename apps/web/src/{components/popovers/CreateRolePopover.tsx => entities/role/ui/create-role-popover.tsx} (97%) rename apps/web/src/{components/popovers/DeleteRolePopover.tsx => entities/role/ui/delete-role-popover.ui.tsx} (93%) create mode 100644 apps/web/src/entities/role/ui/index.ts create mode 100644 apps/web/src/entities/role/ui/role-table.ui.tsx rename apps/web/src/{components/popovers/UpdateRolePopover.tsx => entities/role/ui/update-role-name-popover.ui.tsx} (71%) create mode 100644 apps/web/src/entities/tenant/index.ts create mode 100644 apps/web/src/entities/tenant/tenant.model.ts create mode 100644 apps/web/src/entities/tenant/tenant.schema.ts create mode 100644 apps/web/src/entities/tenant/tenant.type.ts create mode 100644 apps/web/src/entities/tenant/ui/index.ts create mode 100644 apps/web/src/entities/tenant/ui/oauth-config-form.ui.tsx create mode 100644 apps/web/src/entities/tenant/ui/tenant-card.ui.spec.tsx create mode 100644 apps/web/src/entities/tenant/ui/tenant-card.ui.tsx create mode 100644 apps/web/src/entities/tenant/ui/tenant-guard.ui.spec.tsx rename apps/web/src/{components/templates/MainTemplate/MainTemplate.tsx => entities/tenant/ui/tenant-guard.ui.tsx} (53%) create mode 100644 apps/web/src/entities/tenant/ui/tenant-info-form.ui.tsx create mode 100644 apps/web/src/entities/theme/index.ts rename apps/web/src/{zustand/theme.store.ts => entities/theme/theme.model.ts} (84%) create mode 100644 apps/web/src/entities/theme/ui/index.ts rename apps/web/src/{hooks/useCurrentProjectId.ts => entities/theme/ui/theme-toggle-button.ui.spec.tsx} (55%) rename apps/web/src/{components/buttons/ThemeToggleButton.tsx => entities/theme/ui/theme-toggle-button.ui.tsx} (85%) create mode 100644 apps/web/src/entities/user/index.ts create mode 100644 apps/web/src/entities/user/lib/index.ts rename apps/web/src/{hooks/useUserSearch.ts => entities/user/lib/use-user-search.ts} (91%) create mode 100644 apps/web/src/entities/user/ui/__snapshots__/user-box.ui.spec.tsx.snap create mode 100644 apps/web/src/entities/user/ui/index.ts rename apps/web/src/{containers/setting-menu/UserSetting/UserInvitationDialog.tsx => entities/user/ui/invite-user-popover.tsx} (82%) rename apps/web/src/{containers/setting-menu/UserSetting/UserEditPopover.tsx => entities/user/ui/update-user-popover.ui.tsx} (63%) create mode 100644 apps/web/src/entities/user/ui/user-box.ui.spec.tsx rename apps/web/src/{components/layouts/Header/ProfileBox.tsx => entities/user/ui/user-box.ui.tsx} (82%) create mode 100644 apps/web/src/entities/user/ui/user-management-table.ui.tsx create mode 100644 apps/web/src/entities/user/user-columns.tsx create mode 100644 apps/web/src/entities/user/user.model.ts create mode 100644 apps/web/src/entities/user/user.schema.ts create mode 100644 apps/web/src/entities/user/user.type.ts create mode 100644 apps/web/src/entities/webhook/index.ts create mode 100644 apps/web/src/entities/webhook/ui/create-webhook-popover.tsx rename apps/web/src/{containers/setting-menu/WebhookSetting/WebhookDeleteDialog.tsx => entities/webhook/ui/delete-webhook-popover.ui.tsx} (64%) create mode 100644 apps/web/src/entities/webhook/ui/index.ts create mode 100644 apps/web/src/entities/webhook/ui/update-webhook-popover.tsx rename apps/web/src/{containers/setting-menu/WebhookSetting/WebhookEventTableCell.tsx => entities/webhook/ui/webhook-event-cell.tsx} (78%) create mode 100644 apps/web/src/entities/webhook/ui/webhook-form.ui.tsx create mode 100644 apps/web/src/entities/webhook/ui/webhook-switch.ui.tsx create mode 100644 apps/web/src/entities/webhook/ui/webhook-table.ui.tsx create mode 100644 apps/web/src/entities/webhook/webhook-column.tsx create mode 100644 apps/web/src/entities/webhook/webhook.schema.ts create mode 100644 apps/web/src/entities/webhook/webhook.type.ts rename apps/web/src/{env.mjs => env.ts} (89%) rename apps/web/src/{components/layouts/Header => features/auth/reset-password-with-email}/index.ts (94%) create mode 100644 apps/web/src/features/auth/reset-password-with-email/request-reset-password-with-email.schema.ts create mode 100644 apps/web/src/features/auth/reset-password-with-email/reset-password-with-email.schema.ts create mode 100644 apps/web/src/features/auth/reset-password-with-email/ui/__snapshots__/request-reset-password-with-email.ui.spec.tsx.snap create mode 100644 apps/web/src/features/auth/reset-password-with-email/ui/__snapshots__/reset-password-with-email-form.ui.spec.tsx.snap create mode 100644 apps/web/src/features/auth/reset-password-with-email/ui/index.ts create mode 100644 apps/web/src/features/auth/reset-password-with-email/ui/request-reset-password-with-email.ui.spec.tsx create mode 100644 apps/web/src/features/auth/reset-password-with-email/ui/request-reset-password-with-email.ui.tsx create mode 100644 apps/web/src/features/auth/reset-password-with-email/ui/reset-password-with-email-form.ui.spec.tsx create mode 100644 apps/web/src/features/auth/reset-password-with-email/ui/reset-password-with-email-form.ui.tsx rename apps/web/src/{components/layouts/SideNav => features/auth/sign-in-with-email}/index.ts (94%) create mode 100644 apps/web/src/features/auth/sign-in-with-email/sign-in-with-email.schema.ts create mode 100644 apps/web/src/features/auth/sign-in-with-email/ui/__snapshots__/sign-in-with-email-form.ui.spec.tsx.snap create mode 100644 apps/web/src/features/auth/sign-in-with-email/ui/index.ts create mode 100644 apps/web/src/features/auth/sign-in-with-email/ui/sign-in-with-email-form.ui.spec.tsx create mode 100644 apps/web/src/features/auth/sign-in-with-email/ui/sign-in-with-email-form.ui.tsx create mode 100644 apps/web/src/features/auth/sign-in-with-oauth/__mocks__/sign-in-with-oauth.mock-handler.ts rename apps/web/src/{components/cards/DashboardCard => features/auth/sign-in-with-oauth}/index.ts (93%) create mode 100644 apps/web/src/features/auth/sign-in-with-oauth/lib/index.ts create mode 100644 apps/web/src/features/auth/sign-in-with-oauth/lib/use-oauth-callback.ts create mode 100644 apps/web/src/features/auth/sign-in-with-oauth/ui/__snapshots__/sign-in-with-oauth-button.ui.spec.tsx.snap create mode 100644 apps/web/src/features/auth/sign-in-with-oauth/ui/index.ts create mode 100644 apps/web/src/features/auth/sign-in-with-oauth/ui/sign-in-with-oauth-button.ui.spec.tsx rename apps/web/src/{containers/buttons/OAuthLoginButton/OAuthLoginButton.tsx => features/auth/sign-in-with-oauth/ui/sign-in-with-oauth-button.ui.tsx} (75%) create mode 100644 apps/web/src/features/auth/sign-up-with-email/index.ts create mode 100644 apps/web/src/features/auth/sign-up-with-email/sign-up-with-email.schema.ts create mode 100644 apps/web/src/features/auth/sign-up-with-email/ui/__snapshots__/sign-up-with-email-form.ui.spec.tsx.snap create mode 100644 apps/web/src/features/auth/sign-up-with-email/ui/index.ts rename apps/web/src/{hooks/useProjects.ts => features/auth/sign-up-with-email/ui/sign-up-with-email-form.ui.spec.tsx} (69%) create mode 100644 apps/web/src/features/auth/sign-up-with-email/ui/sign-up-with-email-form.ui.tsx create mode 100644 apps/web/src/features/create-api-key/create-api-key-button.ui.tsx create mode 100644 apps/web/src/features/create-api-key/index.ts create mode 100644 apps/web/src/features/create-channel/create-channel-model.ts create mode 100644 apps/web/src/features/create-channel/create-channel-type.tsx create mode 100644 apps/web/src/features/create-channel/create-channel.constant.tsx create mode 100644 apps/web/src/features/create-channel/index.ts create mode 100644 apps/web/src/features/create-channel/ui/create-channel-input-template.ui.tsx create mode 100644 apps/web/src/features/create-channel/ui/create-channel.ui.tsx create mode 100644 apps/web/src/features/create-channel/ui/index.ts create mode 100644 apps/web/src/features/create-channel/ui/input-channel-info-step.ui.tsx create mode 100644 apps/web/src/features/create-channel/ui/input-field-preview-step.ui.tsx create mode 100644 apps/web/src/features/create-channel/ui/input-field-step.ui.tsx create mode 100644 apps/web/src/features/create-channel/ui/input-image-config-step.ui.tsx rename apps/web/src/{containers/buttons/CreateChannelButton/CreateChannelButton.tsx => features/create-channel/ui/route-create-channel-button.ui.tsx} (68%) create mode 100644 apps/web/src/features/create-project/create-project-model.ts create mode 100644 apps/web/src/features/create-project/create-project-type.tsx create mode 100644 apps/web/src/features/create-project/create-project.constant.tsx create mode 100644 apps/web/src/features/create-project/index.ts create mode 100644 apps/web/src/features/create-project/ui/create-project-input-template.ui.tsx create mode 100644 apps/web/src/features/create-project/ui/create-project.ui.tsx create mode 100644 apps/web/src/features/create-project/ui/delete-project-popover.ui.tsx create mode 100644 apps/web/src/features/create-project/ui/index.ts create mode 100644 apps/web/src/features/create-project/ui/input-api-key-step.ui.tsx create mode 100644 apps/web/src/features/create-project/ui/input-issue-tracker-step.ui.tsx create mode 100644 apps/web/src/features/create-project/ui/input-members-step.ui.tsx create mode 100644 apps/web/src/features/create-project/ui/input-project-info-step.ui.tsx create mode 100644 apps/web/src/features/create-project/ui/input-roles-step.ui.tsx rename apps/web/src/{containers/buttons/CreateProjectButton/CreateProjectButton.tsx => features/create-project/ui/route-create-project-button.ui.tsx} (62%) rename apps/web/src/{components/etc/DescriptionTooltip/DescriptionTooltip.spec.tsx => features/create-tenant/create-tenant-form.schema.ts} (85%) create mode 100644 apps/web/src/features/create-tenant/create-tenant-form.spec.tsx create mode 100644 apps/web/src/features/create-tenant/create-tenant-form.ui.tsx rename apps/web/src/{types/timezone-info.ts => features/create-tenant/default-super-account.constant.ts} (87%) create mode 100644 apps/web/src/features/create-tenant/index.ts create mode 100644 apps/web/src/features/delete-channel/index.ts create mode 100644 apps/web/src/features/delete-channel/ui/delete-channel-popover.ui.tsx create mode 100644 apps/web/src/features/delete-channel/ui/index.ts create mode 100644 apps/web/src/features/delete-user/__snapshots__/delete-account-button.ui.spec.tsx.snap create mode 100644 apps/web/src/features/delete-user/delete-account-button.ui.spec.tsx rename apps/web/src/{containers/my-profile/DeleteMyAccountButton.tsx => features/delete-user/delete-account-button.ui.tsx} (82%) create mode 100644 apps/web/src/features/delete-user/index.ts create mode 100644 apps/web/src/features/invite-user/__snapshots__/user-invitation-form.ui.spec.tsx.snap create mode 100644 apps/web/src/features/invite-user/index.ts create mode 100644 apps/web/src/features/invite-user/user-invitation-form.ui.spec.tsx create mode 100644 apps/web/src/features/invite-user/user-invitation-form.ui.tsx create mode 100644 apps/web/src/features/invite-user/user-invitation.schema.ts create mode 100644 apps/web/src/features/update-user/__snapshots__/user-profile-form.ui.spec.tsx.snap create mode 100644 apps/web/src/features/update-user/change-password-form.schema.ts create mode 100644 apps/web/src/features/update-user/change-password-form.ui.spec.tsx rename apps/web/src/{containers/my-profile/ChangePasswordForm.tsx => features/update-user/change-password-form.ui.tsx} (69%) create mode 100644 apps/web/src/features/update-user/index.ts rename apps/web/{jest.setup.js => src/features/update-user/user-profile-form.schema.ts} (81%) create mode 100644 apps/web/src/features/update-user/user-profile-form.ui.spec.tsx create mode 100644 apps/web/src/features/update-user/user-profile-form.ui.tsx delete mode 100644 apps/web/src/hooks/index.ts delete mode 100644 apps/web/src/hooks/useDayCount.ts delete mode 100644 apps/web/src/hooks/useLocalStorage.ts create mode 100644 apps/web/src/msw.ts create mode 100644 apps/web/src/server/api-handler.ts rename apps/web/src/{constants => server}/iron-option.ts (93%) rename apps/web/src/{libs => server}/logger.ts (94%) rename apps/web/src/{types/member.type.ts => shared/constants/background-color.ts} (67%) rename apps/web/src/{ => shared}/constants/chart-colors.ts (100%) rename apps/web/src/{constants/dayjs-format.ts => shared/constants/date-format.ts} (100%) rename apps/web/src/{ => shared}/constants/i18n.ts (86%) create mode 100644 apps/web/src/shared/constants/index.ts rename apps/web/src/{ => shared}/constants/issues.ts (66%) rename apps/web/src/{ => shared}/constants/local-storage-key.ts (100%) rename apps/web/src/{ => shared}/constants/path.ts (56%) create mode 100644 apps/web/src/shared/index.ts rename apps/web/src/{libs => shared/lib}/client.ts (79%) rename apps/web/src/{types/webhook.type.ts => shared/lib/index.ts} (52%) rename apps/web/src/{libs => shared/lib}/session-storage.ts (83%) rename apps/web/src/{hooks/useHorizontalScroll.ts => shared/lib/use-horizontal-scroll.ts} (90%) rename apps/web/src/{hooks/useLocalColumnSetting.ts => shared/lib/use-local-column-setting.ts} (100%) rename apps/web/src/{hooks/usePermissions.ts => shared/lib/use-permissions.ts} (83%) rename apps/web/src/{hooks/useQueryParamsState.ts => shared/lib/use-query-params-state.ts} (59%) rename apps/web/src/{hooks/useSort.ts => shared/lib/use-sort.ts} (79%) rename apps/web/src/{hooks => shared/lib}/useOAIMutation.ts (90%) rename apps/web/src/{hooks => shared/lib}/useOAIQuery.ts (90%) rename apps/web/src/{pages/_app.css => shared/styles/global.css} (98%) rename apps/web/src/{ => shared}/styles/react-datepicker.css (100%) rename apps/web/src/{ => shared}/types/api.type.ts (68%) rename apps/web/src/{ => shared}/types/date-range.type.ts (100%) rename apps/web/src/{ => shared}/types/fetch-error.type.ts (97%) rename apps/web/src/{ => shared}/types/i18n.d.ts (71%) create mode 100644 apps/web/src/shared/types/index.ts create mode 100644 apps/web/src/shared/types/jwt.type.ts rename apps/web/src/{ => shared}/types/openapi.type.ts (98%) rename apps/web/src/{types/user.type.ts => shared/types/page-with-layout.type.ts} (78%) create mode 100644 apps/web/src/shared/types/react-query-state.type.ts rename apps/web/src/{ => shared}/types/svg.d.ts (100%) create mode 100644 apps/web/src/shared/ui/__snapshots__/main-card.ui.spec.tsx.snap rename apps/web/src/{components/charts/ChartContainer.tsx => shared/ui/charts/chart-container.tsx} (92%) rename apps/web/src/{components/charts/ChartFilter.tsx => shared/ui/charts/chart-filter.tsx} (100%) rename apps/web/src/{containers/setting-menu/RoleSetting => shared/ui/charts}/index.ts (83%) rename apps/web/src/{components/charts/Legend.tsx => shared/ui/charts/legend.tsx} (97%) rename apps/web/src/{components/charts/SimpleBarChart.tsx => shared/ui/charts/simple-bar-chart.tsx} (88%) rename apps/web/src/{components/charts/LineChart.tsx => shared/ui/charts/simple-line-chart.tsx} (53%) rename apps/web/src/{components/templates/CreateProjectChannelInputTemplate/CreateProjectChannelInputTemplate.tsx => shared/ui/create-input-template.ui.tsx} (77%) rename apps/web/src/{components/templates/CreateSectionTemplate/CreateSectionTemplate.tsx => shared/ui/create-section-template.ui.tsx.tsx} (93%) create mode 100644 apps/web/src/shared/ui/create-template.ui.tsx rename apps/web/src/{components/cards/DashboardCard/DashboardCard.tsx => shared/ui/dashboard-card.tsx} (68%) rename apps/web/src/{components/etc/DateRangePicker/DateRangePicker.tsx => shared/ui/date-range-picker.tsx} (91%) rename apps/web/src/{components/etc/DescriptionTooltip/DescriptionTooltip.tsx => shared/ui/description-tooltip.tsx} (94%) rename apps/web/src/{components/etc/ExpandableText/ExpandableText.tsx => shared/ui/expandable-text.ui.tsx} (92%) rename apps/web/src/{components/etc/HelpCardDocs/HelpCardDocs.tsx => shared/ui/help-card-docs.tsx} (94%) rename apps/web/src/{components/buttons/ImagePreviewButton/ImagePreviewButton.tsx => shared/ui/image-preview-button.tsx} (97%) create mode 100644 apps/web/src/shared/ui/image-slider.ui.tsx create mode 100644 apps/web/src/shared/ui/index.ts rename apps/web/src/{components/layouts/Header/LocaleSelectBox.tsx => shared/ui/locale-select-box.ui.tsx} (92%) create mode 100644 apps/web/src/shared/ui/logo-with-title.ui.tsx rename apps/web/src/{components/layouts/Header/Logo.tsx => shared/ui/logo.ui.tsx} (96%) create mode 100644 apps/web/src/shared/ui/main-card.ui.spec.tsx create mode 100644 apps/web/src/shared/ui/main-card.ui.tsx rename apps/web/src/{components/etc/Popper/Popper.tsx => shared/ui/popper.ui.tsx} (99%) rename apps/web/src/{containers/setting-menu/SignUpSetting/RadioGroup.tsx => shared/ui/radio-group.tsx} (97%) rename apps/web/src/{containers/setting-menu/RoleSetting/RoleTitleRow.tsx => shared/ui/section-template.ui.tsx} (63%) create mode 100644 apps/web/src/shared/ui/select-box/index.ts rename apps/web/src/{components/etc/SelectBox/SelectBoxCreatable.tsx => shared/ui/select-box/select-box-creatable.tsx} (94%) rename apps/web/src/{components/etc/SelectBox/SelectBox.tsx => shared/ui/select-box/select-box.tsx} (94%) rename apps/web/src/{containers/buttons/ShareButton/ShareButton.tsx => shared/ui/share-button.tsx} (94%) create mode 100644 apps/web/src/shared/ui/small-card.ui.tsx create mode 100644 apps/web/src/shared/ui/sub-menu.ui.tsx create mode 100644 apps/web/src/shared/ui/tables/basic-table.ui.tsx rename apps/web/src/{components/etc/CheckedTableHead/CheckedTableHead.tsx => shared/ui/tables/checked-table-head.tsx} (60%) rename apps/web/src/{components/etc/DashboardTable/DashboardTable.tsx => shared/ui/tables/dashboard-table.tsx} (92%) rename apps/web/src/{components/cards => shared/ui/tables}/index.ts (52%) rename apps/web/src/{components/etc/TableCheckbox/TableCheckbox.tsx => shared/ui/tables/table-checkbox.tsx} (100%) rename apps/web/src/{components/etc/TableLoadingRow/TableLoadingRow.tsx => shared/ui/tables/table-loading-row.tsx} (100%) rename apps/web/src/{components/etc/TablePagination/TablePagination.tsx => shared/ui/tables/table-pagination.tsx} (100%) rename apps/web/src/{components/etc/TableResizer/TableResizer.tsx => shared/ui/tables/table-resizer.tsx} (87%) rename apps/web/src/{containers/tables/IssueTable/TableRow/TableRow.tsx => shared/ui/tables/table-row.tsx} (84%) create mode 100644 apps/web/src/shared/ui/tables/table-search-input/index.ts rename apps/web/src/{components/etc/TableSearchInput/TableSearchInputPopover.tsx => shared/ui/tables/table-search-input/table-search-input-popover.tsx} (83%) rename apps/web/src/{components/etc/TableSearchInput => shared/ui/tables/table-search-input}/table-search-input.service.ts (70%) rename apps/web/src/{components/etc/TableSearchInput/TableSearchInput.tsx => shared/ui/tables/table-search-input/table-search-input.tsx} (84%) rename apps/web/src/{components/etc/TableSortIcon/TableSortIcon.tsx => shared/ui/tables/table-sort-icon.tsx} (91%) rename apps/web/src/{types/jwt-payload.type.ts => shared/utils/cn.ts} (77%) rename apps/web/src/{utils/description-string.ts => shared/utils/display-string.ts} (86%) rename apps/web/src/{components/buttons/ImagePreviewButton/index.ts => shared/utils/empty-function.ts} (92%) rename apps/web/src/{components/etc/DescriptionTooltip/Test.tsx => shared/utils/get-day-count.ts} (80%) create mode 100644 apps/web/src/shared/utils/index.ts create mode 100644 apps/web/src/shared/utils/parse-as-date-range.ts rename apps/web/src/{ => shared}/utils/path-parsing.ts (82%) rename apps/web/src/{ => shared}/utils/remove-empty-value-in-object.ts (92%) rename apps/web/src/{ => shared}/utils/reorder.ts (100%) create mode 100644 apps/web/src/shared/utils/type-guard.ts rename apps/web/src/{utils => }/test-utils.tsx (65%) delete mode 100644 apps/web/src/types/channel.type.ts delete mode 100644 apps/web/src/types/color.type.ts delete mode 100644 apps/web/src/types/field.type.ts delete mode 100644 apps/web/src/types/locale.type.ts delete mode 100644 apps/web/src/types/project.type.ts delete mode 100644 apps/web/src/types/tenant.type.ts delete mode 100644 apps/web/src/utils/is-not-empty-string.ts delete mode 100644 apps/web/src/utils/str.ts create mode 100644 apps/web/src/widgets/dashboard-card-slider/index.ts create mode 100644 apps/web/src/widgets/dashboard-card-slider/ui/card-slider.ui.tsx create mode 100644 apps/web/src/widgets/dashboard-card-slider/ui/dashboard-card-slider.ui.tsx create mode 100644 apps/web/src/widgets/dashboard-card-slider/ui/index.ts create mode 100644 apps/web/src/widgets/feedback-table/feedback-table-columns.tsx create mode 100644 apps/web/src/widgets/feedback-table/index.ts create mode 100644 apps/web/src/widgets/feedback-table/lib/index.ts rename apps/web/src/{hooks/useDownload.ts => widgets/feedback-table/lib/use-feedback-download.ts} (77%) rename apps/web/src/{hooks/useTruncatedElement.tsx => widgets/feedback-table/lib/use-truncated-element.tsx} (100%) rename apps/web/src/{zustand/table.store.ts => widgets/feedback-table/model/feedback-row.store.ts} (78%) create mode 100644 apps/web/src/widgets/feedback-table/model/feedback-table.context.tsx create mode 100644 apps/web/src/widgets/feedback-table/model/index.ts rename apps/web/src/{containers/tables/FeedbackTable => widgets/feedback-table/ui}/FeedbackTableWrapper.tsx (76%) rename apps/web/src/{containers/tables/FeedbackTable/ChannelSelectBox/ChannelSelectBox.tsx => widgets/feedback-table/ui/channel-select-box.tsx} (77%) rename apps/web/src/{containers/tables/FeedbackTable/ColumnSettingPopover/ColumnSettingPopover.tsx => widgets/feedback-table/ui/column-setting-popover/column-setting-popover.tsx} (64%) rename apps/web/src/{containers/tables/FeedbackTable/ColumnSettingPopover/DraggableColumnItem.tsx => widgets/feedback-table/ui/column-setting-popover/draggable-column-item.tsx} (93%) rename apps/web/src/{components/cards/ChannelCard => widgets/feedback-table/ui/column-setting-popover}/index.ts (92%) rename apps/web/src/{containers/tables/FeedbackTable/EditableCell/EditableCell.tsx => widgets/feedback-table/ui/editable-cell.tsx} (81%) rename apps/web/src/{containers/tables/FeedbackTable/FeedbackCell/FeedbackCell.tsx => widgets/feedback-table/ui/feedback-cell.tsx} (82%) rename apps/web/src/{containers/tables/FeedbackTable/FeedbackDeleteDialog/FeedbackDeleteDialog.tsx => widgets/feedback-table/ui/feedback-delete-dialog.tsx} (61%) rename apps/web/src/{containers/tables/FeedbackTable/FeedbackDetail/FeedbackDetailCell.tsx => widgets/feedback-table/ui/feedback-detail/feedback-detail-cell.tsx} (94%) rename apps/web/src/{containers/tables/FeedbackTable/FeedbackDetail/FeedbackDetailIssueCell.tsx => widgets/feedback-table/ui/feedback-detail/feedback-detail-issue-cell.tsx} (81%) rename apps/web/src/{containers/tables/FeedbackTable/FeedbackDetail/FeedbackDetail.tsx => widgets/feedback-table/ui/feedback-detail/feedback-detail.tsx} (58%) rename apps/web/src/{components/etc/DateRangePicker => widgets/feedback-table/ui/feedback-detail}/index.ts (93%) create mode 100644 apps/web/src/widgets/feedback-table/ui/feedback-table-bar.tsx rename apps/web/src/{containers/tables/FeedbackTable/DownloadButton/DownloadButton.tsx => widgets/feedback-table/ui/feedback-table-download-button.ui.tsx} (74%) rename apps/web/src/{containers/tables/FeedbackTable/AllExpandButton/AllExpandButton.tsx => widgets/feedback-table/ui/feedback-table-expand-button-group.ui.tsx} (75%) rename apps/web/src/{containers/tables/FeedbackTable/FeedbackTableInIssue.tsx => widgets/feedback-table/ui/feedback-table-in-issue.tsx} (75%) rename apps/web/src/{containers/tables/FeedbackTable/FeedbackTableRow/FeedbackTableRow.tsx => widgets/feedback-table/ui/feedback-table-row.tsx} (86%) create mode 100644 apps/web/src/widgets/feedback-table/ui/feedback-table.tsx create mode 100644 apps/web/src/widgets/feedback-table/ui/index.ts create mode 100644 apps/web/src/widgets/feedback-table/ui/issue-cell/index.ts rename apps/web/src/{containers/tables/FeedbackTable/IssueCell/IssueCell.tsx => widgets/feedback-table/ui/issue-cell/issue-cell.tsx} (82%) rename apps/web/src/{containers/tables/FeedbackTable/IssueCell/IssueSetting.tsx => widgets/feedback-table/ui/issue-cell/issue-setting.tsx} (89%) create mode 100644 apps/web/src/widgets/index.ts create mode 100644 apps/web/src/widgets/issue-table/index.ts create mode 100644 apps/web/src/widgets/issue-table/issue-table-columns.tsx create mode 100644 apps/web/src/widgets/issue-table/lib/index.ts rename apps/web/src/{hooks/useIssueCount.ts => widgets/issue-table/lib/use-issue-count.ts} (86%) create mode 100644 apps/web/src/widgets/issue-table/lib/use-issue-query.ts create mode 100644 apps/web/src/widgets/issue-table/ui/index.ts create mode 100644 apps/web/src/widgets/issue-table/ui/issue-deletion-popover.ui.tsx rename apps/web/src/{containers/tables/IssueTable/IssueTableSelectBox/IssueTableSelectBox.tsx => widgets/issue-table/ui/issue-select-box.ui.tsx} (86%) rename apps/web/src/{containers/tables/IssueTable/IssueSettingPopover/IssueSettingPopover.tsx => widgets/issue-table/ui/issue-setting-popover.ui.tsx} (90%) create mode 100644 apps/web/src/widgets/issue-table/ui/issue-table.ui.tsx rename apps/web/src/{containers/tables/IssueTable/TicketLink.tsx => widgets/issue-table/ui/ticket-link.ui.tsx} (53%) create mode 100644 apps/web/src/widgets/main-layout/index.ts create mode 100644 apps/web/src/widgets/main-layout/ui/__snapshots__/main-layout.ui.spec.tsx.snap rename apps/web/src/{components/layouts/Header/HeaderName.tsx => widgets/main-layout/ui/breadcrumb.tsx} (78%) create mode 100644 apps/web/src/widgets/main-layout/ui/index.ts create mode 100644 apps/web/src/widgets/main-layout/ui/main-layout.ui.spec.tsx create mode 100644 apps/web/src/widgets/main-layout/ui/main-layout.ui.tsx rename apps/web/src/{components/layouts/SideNav/SideNav.tsx => widgets/main-layout/ui/side-nav.ui.tsx} (86%) create mode 100644 apps/web/src/widgets/setting-menu/index.ts rename apps/web/src/{types => widgets/setting-menu}/setting-menu.type.ts (100%) create mode 100644 apps/web/src/widgets/setting-menu/ui/channel-setting-menu.tsx create mode 100644 apps/web/src/widgets/setting-menu/ui/channel/channel-deletion-setting.ui.tsx create mode 100644 apps/web/src/widgets/setting-menu/ui/channel/channel-info-setting.ui.tsx create mode 100644 apps/web/src/widgets/setting-menu/ui/channel/field-setting.ui.tsx create mode 100644 apps/web/src/widgets/setting-menu/ui/channel/image-config-setting.ui.tsx create mode 100644 apps/web/src/widgets/setting-menu/ui/channel/index.ts create mode 100644 apps/web/src/widgets/setting-menu/ui/index.ts create mode 100644 apps/web/src/widgets/setting-menu/ui/project-setting-menu.tsx create mode 100644 apps/web/src/widgets/setting-menu/ui/project/api-key-setting.ui.tsx rename apps/web/src/{containers/buttons => widgets/setting-menu/ui/project}/index.ts (56%) rename apps/web/src/{containers/setting-menu/IssueTrackerSetting/IssueTrackerSetting.tsx => widgets/setting-menu/ui/project/issue-tracker-setting.ui.tsx} (53%) create mode 100644 apps/web/src/widgets/setting-menu/ui/project/member-setting.ui.tsx create mode 100644 apps/web/src/widgets/setting-menu/ui/project/project-deletion-setting.ui.tsx create mode 100644 apps/web/src/widgets/setting-menu/ui/project/project-info-setting.ui.tsx rename apps/web/src/{containers/setting-menu/RoleSetting/RoleSetting.tsx => widgets/setting-menu/ui/project/role-setting.ui.tsx} (69%) create mode 100644 apps/web/src/widgets/setting-menu/ui/project/webhook-setting.ui.tsx rename apps/web/src/{components/layouts/setting-menu/SettingMenuBox.tsx => widgets/setting-menu/ui/setting-menu-box.tsx} (71%) rename apps/web/src/{components/layouts/setting-menu/SettingMenuItem.tsx => widgets/setting-menu/ui/setting-menu-item.tsx} (93%) rename apps/web/src/{components/templates/SettingMenuTemplate/SettingMenuTemplate.tsx => widgets/setting-menu/ui/setting-menu-template.tsx} (93%) create mode 100644 apps/web/src/widgets/setting-menu/ui/tenant-setting-menu.tsx rename apps/web/src/{containers/setting-menu/SignUpSetting/SignUpSetting.tsx => widgets/setting-menu/ui/tenant/auth-setting.ui.tsx} (79%) create mode 100644 apps/web/src/widgets/setting-menu/ui/tenant/index.ts create mode 100644 apps/web/src/widgets/setting-menu/ui/tenant/tenant-info-setting.ui.tsx rename apps/web/src/{containers/create-channel-complete/FieldPreviewSection.tsx => widgets/setting-menu/ui/tenant/user-management-setting.ui.tsx} (59%) create mode 100644 docker/docker-compose.infra-amd64.yml rename docker/{docker-compose.infra.yml => docker-compose.infra-arm64.yml} (89%) create mode 100644 packages/ufb-shared/eslint.config.js create mode 100644 packages/ufb-tailwind/eslint.config.js delete mode 100644 packages/ufb-ui/__mocks__/svg.js create mode 100644 packages/ufb-ui/eslint.config.js create mode 100644 packages/ufb-ui/src/types/index.ts create mode 100644 packages/ufb-ui/src/utils.ts delete mode 100644 packages/ufb-ui/tailwind.config.js create mode 100644 packages/ufb-ui/tailwind.config.ts create mode 100644 tooling/eslint-plugin-header/index.js create mode 100644 tooling/eslint-plugin-header/package.json create mode 100644 tooling/eslint-plugin-header/src/comment-parser.js create mode 100644 tooling/eslint-plugin-header/src/rules/header.js create mode 100644 tooling/eslint/type.d.ts create mode 100644 tooling/typescript/base.json create mode 100644 tooling/typescript/internal-package.json delete mode 100644 tooling/typescript/react.json diff --git a/.github/workflows/docker-dev-image.yml b/.github/workflows/docker-dev-image.yml index 900b8727e..17f2cff8d 100644 --- a/.github/workflows/docker-dev-image.yml +++ b/.github/workflows/docker-dev-image.yml @@ -31,7 +31,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push API - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . file: ./docker/api.dockerfile @@ -64,7 +64,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Web - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . file: ./docker/web.dockerfile diff --git a/.github/workflows/docker-prod-image.yml b/.github/workflows/docker-prod-image.yml index 1e57b7f16..21ddbc661 100644 --- a/.github/workflows/docker-prod-image.yml +++ b/.github/workflows/docker-prod-image.yml @@ -33,7 +33,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push API - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . file: ./docker/api.dockerfile @@ -66,7 +66,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Web - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . file: ./docker/web.dockerfile diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index f4d96df51..97a356196 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -10,7 +10,7 @@ jobs: services: mysql: - image: mysql:8.4.0 + image: mysql:9.0.0 env: MYSQL_ROOT_PASSWORD: userfeedback MYSQL_DATABASE: e2e diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..40deb27a4 --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +node-linker=hoisted +hoist-pattern[]=!bcrypt +# hoist-pattern[]=!*typeorm* +# hoist-pattern[]=!*nestjs* diff --git a/.nvmrc b/.nvmrc index 805efa9f6..119f15a0a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.14.0 \ No newline at end of file +20.15.1 \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 6aed6502b..f65ca4b31 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], + "eslint.experimental.useFlatConfig": true, "eslint.workingDirectories": [ { "pattern": "apps/*/" }, { "pattern": "packages/*/" }, @@ -16,6 +17,9 @@ "files.associations": { "*.css": "tailwindcss" }, + "typescript.preferences.autoImportFileExcludePatterns": [ + "@testing-library/react" + ], "[ignore]": { "editor.defaultFormatter": "foxundermoon.shell-format" } diff --git a/README.md b/README.md index 4ba7e56e5..c6f2e93d2 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,9 @@ The frontend is built with NextJS and the backend is built with NestJS. We provi - SMTP - for mail verification during making accounts - [OpenSearch v7](https://opensearch.org/) - for performance on searching feedback -You can use [docker-compose.infra.yml](/docker/docker-compose.infra.yml) file for requirements. +You can use [docker-compose.infra-amd64.yml](/docker/docker-compose.infra-amd64.yml) file for requirements. + +for arm architecture, use [docker-compose.infra-arm64.yml](/docker/docker-compose.infra-arm64.yml) file ### Docker Hub Images @@ -85,7 +87,7 @@ pnpm install 2. Spin up all required infrastructure (Mysql, OpenSearch, etc.) using Docker Compose: ```bash -docker-compose -f docker/docker-compose.infra.yml up -d +docker-compose -f docker/docker-compose.infra-amd64.yml up -d ``` 3. Make an `.env` file in `apps/api` and `apps/web` by referring to `.env.example` ([web environment variables](./apps/web/README.md), [api environment variables](./apps/api/README.md)) diff --git a/apps/api/.eslintrc.js b/apps/api/.eslintrc.js deleted file mode 100644 index 6040da928..000000000 --- a/apps/api/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - root: true, - extends: ['@ufb/eslint-config/base', '@ufb/eslint-config/nestjs'], - parserOptions: { - project: 'tsconfig.json', - tsconfigRootDir: __dirname, - sourceType: 'module', - }, - ignorePatterns: ['jest.config.js', 'jest.setup.js'], -}; diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs new file mode 100644 index 000000000..d67a1b329 --- /dev/null +++ b/apps/api/eslint.config.mjs @@ -0,0 +1,57 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import tseslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: ['**/.eslintrc.js'], + }, + ...compat + .extends( + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ) + .map((config) => ({ + ...config, + files: ['src/**/*.ts'], + })), + { + files: ['src/**/*.ts'], + plugins: { + '@typescript-eslint': tseslint, + }, + languageOptions: { + globals: { ...globals.node, ...globals.jest }, + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + }, + }, +]; diff --git a/apps/api/package.json b/apps/api/package.json index 89ca5edfe..bc04c411e 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -8,7 +8,7 @@ "dev": "nest start --watch", "format": "prettier --check . --ignore-path ../../.gitignore", "format:fix": "prettier --write --list-different \"./src/**/*.{js,cjs,mjs,ts,tsx,md,json}\"", - "lint": "eslint \"src/**/*.ts\"", + "lint": "eslint", "migration:generate": "npm run typeorm -- migration:generate src/configs/modules/typeorm-config/migrations/$npm_config_name", "migration:revert": "npm run typeorm -- migration:revert", "migration:run": "npm run typeorm -- migration:run", @@ -92,13 +92,16 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/luxon": "^3.4.2", - "@types/node": "20.14.2", + "@types/node": "20.14.11", "@types/nodemailer": "^6.4.15", "@types/supertest": "^6.0.2", - "@ufb/eslint-config": "workspace:*", "@ufb/prettier-config": "workspace:*", "@ufb/tsconfig": "workspace:*", - "eslint": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", "mockdate": "^3.0.5", "supertest": "^7.0.0", diff --git a/apps/api/src/common/filters/http-exception.filter.ts b/apps/api/src/common/filters/http-exception.filter.ts index 2c4a771b2..f0b681fae 100644 --- a/apps/api/src/common/filters/http-exception.filter.ts +++ b/apps/api/src/common/filters/http-exception.filter.ts @@ -25,6 +25,7 @@ export class HttpExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); + const statusCode = exception.getStatus(); const exceptionResponse = exception.getResponse(); diff --git a/apps/api/src/configs/modules/mailer-config/mailer-config.module.ts b/apps/api/src/configs/modules/mailer-config/mailer-config.module.ts index 04e135c76..bfbbcf39e 100644 --- a/apps/api/src/configs/modules/mailer-config/mailer-config.module.ts +++ b/apps/api/src/configs/modules/mailer-config/mailer-config.module.ts @@ -37,7 +37,7 @@ import type { ConfigServiceType } from '@/types/config-service.type'; auth: username && password && { user: username, pass: password }, secure: port === 465, }, - defaults: { from: `"User feedback" <${sender}>`, sdd: '' }, + defaults: { from: `"User feedback" <${sender}>` }, template: { dir: __dirname + '/templates/', adapter: new HandlebarsAdapter(), diff --git a/apps/api/src/configs/modules/typeorm-config/typeorm-config.datasource.ts b/apps/api/src/configs/modules/typeorm-config/typeorm-config.datasource.ts index fff7dd888..4747a3815 100644 --- a/apps/api/src/configs/modules/typeorm-config/typeorm-config.datasource.ts +++ b/apps/api/src/configs/modules/typeorm-config/typeorm-config.datasource.ts @@ -23,7 +23,7 @@ import { TypeOrmConfigService } from './typeorm-config.service'; const env = mysqlConfig(); console.log('env: ', env); const configService = new ConfigService({ mysql: env }); -const typeormConfigService = new TypeOrmConfigService(configService); +const typeormConfigService = new TypeOrmConfigService(configService as any); const typeormConfig = typeormConfigService.createTypeOrmOptions() as DataSourceOptions; diff --git a/apps/api/src/configs/smtp.config.ts b/apps/api/src/configs/smtp.config.ts index c49f4179b..2b33bf0a9 100644 --- a/apps/api/src/configs/smtp.config.ts +++ b/apps/api/src/configs/smtp.config.ts @@ -28,16 +28,8 @@ export const smtpConfigSchema = Joi.object({ then: Joi.required(), otherwise: Joi.optional(), }), - SMTP_USERNAME: Joi.string().when('SMTP_USE', { - is: true, - then: Joi.optional(), - otherwise: Joi.optional(), - }), - SMTP_PASSWORD: Joi.string().when('SMTP_USE', { - is: true, - then: Joi.optional(), - otherwise: Joi.optional(), - }), + SMTP_USERNAME: Joi.string().optional(), + SMTP_PASSWORD: Joi.string().optional(), SMTP_SENDER: Joi.string().when('SMTP_USE', { is: true, then: Joi.required(), diff --git a/apps/api/src/domains/admin/auth/auth.module.ts b/apps/api/src/domains/admin/auth/auth.module.ts index fc1a29b3a..9ba58c7f3 100644 --- a/apps/api/src/domains/admin/auth/auth.module.ts +++ b/apps/api/src/domains/admin/auth/auth.module.ts @@ -21,6 +21,7 @@ import { PassportModule } from '@nestjs/passport'; import { CodeModule } from '@/shared/code/code.module'; import { MailingModule } from '@/shared/mailing/mailing.module'; + import type { ConfigServiceType } from '@/types/config-service.type'; import { ApiKeyModule } from '../project/api-key/api-key.module'; import { MemberModule } from '../project/member/member.module'; diff --git a/apps/api/src/domains/admin/auth/auth.service.spec.ts b/apps/api/src/domains/admin/auth/auth.service.spec.ts index 8fecdefa3..1c739d81f 100644 --- a/apps/api/src/domains/admin/auth/auth.service.spec.ts +++ b/apps/api/src/domains/admin/auth/auth.service.spec.ts @@ -21,6 +21,7 @@ import type { Repository } from 'typeorm'; import { CodeEntity } from '@/shared/code/code.entity'; import { NotVerifiedEmailException } from '@/shared/mailing/exceptions'; + import { emailFixture, passwordFixture, diff --git a/apps/api/src/domains/admin/auth/auth.service.ts b/apps/api/src/domains/admin/auth/auth.service.ts index 59e62a3d3..32b42af94 100644 --- a/apps/api/src/domains/admin/auth/auth.service.ts +++ b/apps/api/src/domains/admin/auth/auth.service.ts @@ -30,6 +30,7 @@ import { Transactional } from 'typeorm-transactional'; import { EmailVerificationMailingService } from '@/shared/mailing/email-verification-mailing.service'; import { NotVerifiedEmailException } from '@/shared/mailing/exceptions'; + import type { ConfigServiceType } from '@/types/config-service.type'; import { CodeTypeEnum } from '../../../shared/code/code-type.enum'; import { CodeService } from '../../../shared/code/code.service'; @@ -170,6 +171,7 @@ export class AuthService { async signIn(user: UserDto): Promise { const { email, id, department, name, type } = user; + const { state } = await this.userService.findById(id); if (state === UserStateEnum.Blocked) throw new UserBlockedException(); diff --git a/apps/api/src/domains/admin/channel/channel/channel.controller.ts b/apps/api/src/domains/admin/channel/channel/channel.controller.ts index 8d8413834..70a1fb324 100644 --- a/apps/api/src/domains/admin/channel/channel/channel.controller.ts +++ b/apps/api/src/domains/admin/channel/channel/channel.controller.ts @@ -90,6 +90,7 @@ export class ChannelController { } @UseGuards(JwtAuthGuard) @Get('/name-check') + @ApiOkResponse({ type: Boolean }) async checkName( @Param('projectId', ParseIntPipe) projectId: number, @Query('name') name: string, diff --git a/apps/api/src/domains/admin/feedback/feedback.os.service.ts b/apps/api/src/domains/admin/feedback/feedback.os.service.ts index b29ff97ac..5bca1adff 100644 --- a/apps/api/src/domains/admin/feedback/feedback.os.service.ts +++ b/apps/api/src/domains/admin/feedback/feedback.os.service.ts @@ -126,7 +126,7 @@ export class FeedbackOSService { FieldFormatEnum.number, ]), this.getMultiFieldQuery( - query[fieldKey].toString(), + (query[fieldKey] as string).toString(), fields, [FieldFormatEnum.text, FieldFormatEnum.keyword], ), diff --git a/apps/api/src/domains/admin/history/subscribers/code-history.subscriber.ts b/apps/api/src/domains/admin/history/subscribers/code-history.subscriber.ts index 99aa608bf..7c7113f7a 100644 --- a/apps/api/src/domains/admin/history/subscribers/code-history.subscriber.ts +++ b/apps/api/src/domains/admin/history/subscribers/code-history.subscriber.ts @@ -18,6 +18,7 @@ import { ClsService } from 'nestjs-cls'; import { DataSource, EventSubscriber } from 'typeorm'; import { CodeEntity } from '@/shared/code/code.entity'; + import { EntityNameEnum } from '../history-entity.enum'; import { HistoryService } from '../history.service'; import { AbstractHistorySubscriber } from './abstract-history.subscriber'; diff --git a/apps/api/src/domains/admin/project/issue-tracker/dtos/create-issue-tracker.dto.ts b/apps/api/src/domains/admin/project/issue-tracker/dtos/create-issue-tracker.dto.ts index 36d750f8a..e27ec799c 100644 --- a/apps/api/src/domains/admin/project/issue-tracker/dtos/create-issue-tracker.dto.ts +++ b/apps/api/src/domains/admin/project/issue-tracker/dtos/create-issue-tracker.dto.ts @@ -16,13 +16,14 @@ import { Expose, plainToInstance } from 'class-transformer'; import { IssueTrackerEntity } from '../issue-tracker.entity'; +import { IssueTrackerDataDto } from './issue-tracker-data.dto'; export class CreateIssueTrackerDto { @Expose() projectId: number; @Expose() - data: object; + data: IssueTrackerDataDto; public static from(params: any): CreateIssueTrackerDto { return plainToInstance(CreateIssueTrackerDto, params, { diff --git a/apps/web/src/constants/default-date-range.ts b/apps/api/src/domains/admin/project/issue-tracker/dtos/issue-tracker-data.dto.ts similarity index 69% rename from apps/web/src/constants/default-date-range.ts rename to apps/api/src/domains/admin/project/issue-tracker/dtos/issue-tracker-data.dto.ts index 050b881a4..834c2287c 100644 --- a/apps/web/src/constants/default-date-range.ts +++ b/apps/api/src/domains/admin/project/issue-tracker/dtos/issue-tracker-data.dto.ts @@ -13,12 +13,16 @@ * License for the specific language governing permissions and limitations * under the License. */ -import dayjs from 'dayjs'; -import { env } from '@/env.mjs'; -import type { DateRangeType } from '@/types/date-range.type'; +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; -export const DEFAULT_DATE_RANGE: DateRangeType = { - startDate: dayjs().subtract(env.NEXT_PUBLIC_MAX_DAYS, 'day').toDate(), - endDate: dayjs().toDate(), -}; +export class IssueTrackerDataDto { + @ApiProperty({ nullable: true }) + @Expose() + ticketDomain: string | null; + + @ApiProperty({ nullable: true }) + @Expose() + ticketKey: string | null; +} diff --git a/apps/api/src/domains/admin/project/issue-tracker/dtos/requests/create-issue-tracker-request.dto.ts b/apps/api/src/domains/admin/project/issue-tracker/dtos/requests/create-issue-tracker-request.dto.ts index 986933287..642e1c833 100644 --- a/apps/api/src/domains/admin/project/issue-tracker/dtos/requests/create-issue-tracker-request.dto.ts +++ b/apps/api/src/domains/admin/project/issue-tracker/dtos/requests/create-issue-tracker-request.dto.ts @@ -16,8 +16,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsObject } from 'class-validator'; +import { IssueTrackerDataDto } from '../issue-tracker-data.dto'; + export class CreateIssueTrackerRequestDto { - @ApiProperty() + @ApiProperty({ type: IssueTrackerDataDto }) @IsObject() - data: Record; + data: IssueTrackerDataDto; } diff --git a/apps/api/src/domains/admin/project/issue-tracker/dtos/responses/create-issue-tracker-response.dto.ts b/apps/api/src/domains/admin/project/issue-tracker/dtos/responses/create-issue-tracker-response.dto.ts index aa59648ba..e14b6b9e1 100644 --- a/apps/api/src/domains/admin/project/issue-tracker/dtos/responses/create-issue-tracker-response.dto.ts +++ b/apps/api/src/domains/admin/project/issue-tracker/dtos/responses/create-issue-tracker-response.dto.ts @@ -16,14 +16,16 @@ import { ApiProperty } from '@nestjs/swagger'; import { Expose, plainToInstance } from 'class-transformer'; +import { IssueTrackerDataDto } from '../issue-tracker-data.dto'; + export class CreateIssueTrackerResponseDto { @Expose() @ApiProperty() id: number; @Expose() - @ApiProperty() - data: object; + @ApiProperty({ type: IssueTrackerDataDto }) + data: IssueTrackerDataDto; @Expose() @ApiProperty() diff --git a/apps/api/src/domains/admin/project/issue-tracker/dtos/responses/find-issue-tracker-response.dto.ts b/apps/api/src/domains/admin/project/issue-tracker/dtos/responses/find-issue-tracker-response.dto.ts index fde919d96..b1f8ea429 100644 --- a/apps/api/src/domains/admin/project/issue-tracker/dtos/responses/find-issue-tracker-response.dto.ts +++ b/apps/api/src/domains/admin/project/issue-tracker/dtos/responses/find-issue-tracker-response.dto.ts @@ -16,14 +16,16 @@ import { ApiProperty } from '@nestjs/swagger'; import { Expose, plainToInstance } from 'class-transformer'; +import { IssueTrackerDataDto } from '../issue-tracker-data.dto'; + export class FindIssueTrackerResponseDto { @Expose() @ApiProperty() id: number; @Expose() - @ApiProperty() - data: object; + @ApiProperty({ type: IssueTrackerDataDto }) + data: IssueTrackerDataDto; public static transform(params: any): FindIssueTrackerResponseDto { return plainToInstance(FindIssueTrackerResponseDto, params, { diff --git a/apps/api/src/domains/admin/project/issue-tracker/issue-tracker.controller.spec.ts b/apps/api/src/domains/admin/project/issue-tracker/issue-tracker.controller.spec.ts index 524726a67..04f6b8747 100644 --- a/apps/api/src/domains/admin/project/issue-tracker/issue-tracker.controller.spec.ts +++ b/apps/api/src/domains/admin/project/issue-tracker/issue-tracker.controller.spec.ts @@ -18,6 +18,7 @@ import { Test } from '@nestjs/testing'; import { DataSource } from 'typeorm'; import { getMockProvider, MockDataSource } from '@/test-utils/util-functions'; +import { IssueTrackerDataDto } from './dtos/issue-tracker-data.dto'; import { IssueTrackerController } from './issue-tracker.controller'; import { IssueTrackerService } from './issue-tracker.service'; @@ -47,7 +48,9 @@ describe('IssueTrackerController', () => { jest.spyOn(MockIssueTrackerService, 'create'); const projectId = faker.number.int(); - await issueTrackerController.create(projectId, { data: {} }); + await issueTrackerController.create(projectId, { + data: {} as IssueTrackerDataDto, + }); expect(MockIssueTrackerService.create).toBeCalledTimes(1); }); }); @@ -65,7 +68,9 @@ describe('IssueTrackerController', () => { jest.spyOn(MockIssueTrackerService, 'update'); const projectId = faker.number.int(); - await issueTrackerController.updateOne(projectId, { data: {} }); + await issueTrackerController.updateOne(projectId, { + data: {} as IssueTrackerDataDto, + }); expect(MockIssueTrackerService.update).toBeCalledTimes(1); }); }); diff --git a/apps/api/src/domains/admin/project/project/dtos/create-project.dto.ts b/apps/api/src/domains/admin/project/project/dtos/create-project.dto.ts index b0261375e..250557f65 100644 --- a/apps/api/src/domains/admin/project/project/dtos/create-project.dto.ts +++ b/apps/api/src/domains/admin/project/project/dtos/create-project.dto.ts @@ -14,6 +14,7 @@ * under the License. */ +import type { IssueTrackerDataDto } from '../../issue-tracker/dtos/issue-tracker-data.dto'; import type { CreateRoleDto } from '../../role/dtos'; import type { Timezone } from '../project.entity'; @@ -30,6 +31,6 @@ export class CreateProjectDto { value: string; }[]; issueTracker?: { - data: Record; + data: IssueTrackerDataDto; }; } diff --git a/apps/api/src/domains/admin/project/project/project.service.ts b/apps/api/src/domains/admin/project/project/project.service.ts index 0f084f82a..0f9581f5c 100644 --- a/apps/api/src/domains/admin/project/project/project.service.ts +++ b/apps/api/src/domains/admin/project/project/project.service.ts @@ -187,7 +187,7 @@ export class ProjectService { @Transactional() async update(dto: UpdateProjectDto) { - const { projectId, name, description } = dto; + const { projectId, name, description, timezone } = dto; const project = await this.findById({ projectId }); if ( @@ -199,7 +199,9 @@ export class ProjectService { throw new ProjectInvalidNameException('Duplicated name'); } - await this.projectRepo.save(Object.assign(project, { name, description })); + await this.projectRepo.save( + Object.assign(project, { name, description, timezone }), + ); } @Transactional() diff --git a/apps/api/src/domains/admin/tenant/dtos/responses/get-tenant-response.dto.ts b/apps/api/src/domains/admin/tenant/dtos/responses/get-tenant-response.dto.ts index 666c94b09..0285ffbda 100644 --- a/apps/api/src/domains/admin/tenant/dtos/responses/get-tenant-response.dto.ts +++ b/apps/api/src/domains/admin/tenant/dtos/responses/get-tenant-response.dto.ts @@ -60,7 +60,7 @@ export class GetTenantResponseDto { siteName: string; @Expose() - @ApiProperty() + @ApiProperty({ nullable: true }) description: string | null; @Expose() diff --git a/apps/api/src/domains/admin/user/user-password.service.spec.ts b/apps/api/src/domains/admin/user/user-password.service.spec.ts index 28dfef6a4..c0dfca71e 100644 --- a/apps/api/src/domains/admin/user/user-password.service.spec.ts +++ b/apps/api/src/domains/admin/user/user-password.service.spec.ts @@ -21,6 +21,7 @@ import type { Repository } from 'typeorm'; import { CodeEntity } from '@/shared/code/code.entity'; import { ResetPasswordMailingService } from '@/shared/mailing/reset-password-mailing.service'; + import { TestConfig } from '@/test-utils/util-functions'; import { UserPasswordServiceProviders } from '../../../test-utils/providers/user-password.service.providers'; import { ChangePasswordDto, ResetPasswordDto } from './dtos'; diff --git a/apps/api/src/domains/admin/user/user-password.service.ts b/apps/api/src/domains/admin/user/user-password.service.ts index 16e3f249f..c5a4e61f9 100644 --- a/apps/api/src/domains/admin/user/user-password.service.ts +++ b/apps/api/src/domains/admin/user/user-password.service.ts @@ -22,6 +22,7 @@ import { Transactional } from 'typeorm-transactional'; import { CodeTypeEnum } from '@/shared/code/code-type.enum'; import { CodeService } from '@/shared/code/code.service'; import { ResetPasswordMailingService } from '@/shared/mailing/reset-password-mailing.service'; + import type { ResetPasswordDto } from './dtos'; import { ChangePasswordDto } from './dtos'; import { UserEntity } from './entities/user.entity'; diff --git a/apps/api/src/domains/admin/user/user.module.ts b/apps/api/src/domains/admin/user/user.module.ts index f337a2e09..71e95b680 100644 --- a/apps/api/src/domains/admin/user/user.module.ts +++ b/apps/api/src/domains/admin/user/user.module.ts @@ -18,6 +18,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CodeModule } from '@/shared/code/code.module'; import { MailingModule } from '@/shared/mailing/mailing.module'; + import { MemberModule } from '../project/member/member.module'; import { TenantModule } from '../tenant/tenant.module'; import { CreateUserService } from './create-user.service'; diff --git a/apps/api/src/domains/admin/user/user.service.ts b/apps/api/src/domains/admin/user/user.service.ts index f546f18e1..b66ae9d05 100644 --- a/apps/api/src/domains/admin/user/user.service.ts +++ b/apps/api/src/domains/admin/user/user.service.ts @@ -20,6 +20,7 @@ import { In, Like, Raw, Repository } from 'typeorm'; import { Transactional } from 'typeorm-transactional'; import { UserInvitationMailingService } from '@/shared/mailing/user-invitation-mailing.service'; + import { CodeTypeEnum } from '../../../shared/code/code-type.enum'; import { CodeService } from '../../../shared/code/code.service'; import { TenantService } from '../tenant/tenant.service'; diff --git a/apps/api/src/shared/code/code.module.ts b/apps/api/src/shared/code/code.module.ts index 5dab6e974..91c55e0b7 100644 --- a/apps/api/src/shared/code/code.module.ts +++ b/apps/api/src/shared/code/code.module.ts @@ -17,6 +17,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MailingModule } from '@/shared/mailing/mailing.module'; + import { CodeEntity } from './code.entity'; import { CodeService } from './code.service'; diff --git a/apps/api/src/test-utils/fixtures.ts b/apps/api/src/test-utils/fixtures.ts index 443513f2b..08c1b761c 100644 --- a/apps/api/src/test-utils/fixtures.ts +++ b/apps/api/src/test-utils/fixtures.ts @@ -17,6 +17,9 @@ import { faker } from '@faker-js/faker'; import * as bcrypt from 'bcrypt'; import { DateTime } from 'luxon'; +import { CodeTypeEnum } from '@/shared/code/code-type.enum'; +import type { CodeEntity } from '@/shared/code/code.entity'; + import { EventStatusEnum, EventTypeEnum, @@ -52,8 +55,6 @@ import { UserTypeEnum, } from '@/domains/admin/user/entities/enums'; import type { UserEntity } from '@/domains/admin/user/entities/user.entity'; -import { CodeTypeEnum } from '@/shared/code/code-type.enum'; -import type { CodeEntity } from '@/shared/code/code.entity'; export const createFieldEntity = (input: Partial) => { const format = input?.format ?? getRandomEnumValue(FieldFormatEnum); diff --git a/apps/api/src/test-utils/providers/auth.service.providers.ts b/apps/api/src/test-utils/providers/auth.service.providers.ts index f87237b6f..4c7269574 100644 --- a/apps/api/src/test-utils/providers/auth.service.providers.ts +++ b/apps/api/src/test-utils/providers/auth.service.providers.ts @@ -18,6 +18,7 @@ import { JwtService } from '@nestjs/jwt'; import { ClsService } from 'nestjs-cls'; import { EmailVerificationMailingService } from '@/shared/mailing/email-verification-mailing.service'; + import { CodeServiceProviders } from '@/test-utils/providers/code.service.providers'; import { getMockProvider } from '@/test-utils/util-functions'; import { AuthService } from '../../domains/admin/auth/auth.service'; diff --git a/apps/api/src/test-utils/providers/user-password.service.providers.ts b/apps/api/src/test-utils/providers/user-password.service.providers.ts index ccf9db1d6..168bba3eb 100644 --- a/apps/api/src/test-utils/providers/user-password.service.providers.ts +++ b/apps/api/src/test-utils/providers/user-password.service.providers.ts @@ -16,9 +16,10 @@ import { MailerService } from '@nestjs-modules/mailer'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { ResetPasswordMailingService } from '@/shared/mailing/reset-password-mailing.service'; + import { UserEntity } from '@/domains/admin/user/entities/user.entity'; import { UserPasswordService } from '@/domains/admin/user/user-password.service'; -import { ResetPasswordMailingService } from '@/shared/mailing/reset-password-mailing.service'; import { CodeServiceProviders } from '@/test-utils/providers/code.service.providers'; import { UserRepositoryStub } from '../stubs'; import { getMockProvider } from '../util-functions'; diff --git a/apps/api/src/test-utils/providers/user.service.providers.ts b/apps/api/src/test-utils/providers/user.service.providers.ts index 2c9f23411..c9e1d81e9 100644 --- a/apps/api/src/test-utils/providers/user.service.providers.ts +++ b/apps/api/src/test-utils/providers/user.service.providers.ts @@ -16,9 +16,10 @@ import { MailerService } from '@nestjs-modules/mailer'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { UserInvitationMailingService } from '@/shared/mailing/user-invitation-mailing.service'; + import { UserEntity } from '@/domains/admin/user/entities/user.entity'; import { UserService } from '@/domains/admin/user/user.service'; -import { UserInvitationMailingService } from '@/shared/mailing/user-invitation-mailing.service'; import { CodeServiceProviders } from '@/test-utils/providers/code.service.providers'; import { UserRepositoryStub } from '../stubs'; import { getMockProvider } from '../util-functions'; diff --git a/apps/api/src/test-utils/stubs/code-repository.stub.ts b/apps/api/src/test-utils/stubs/code-repository.stub.ts index 31ad0d947..8ac3a6d6b 100644 --- a/apps/api/src/test-utils/stubs/code-repository.stub.ts +++ b/apps/api/src/test-utils/stubs/code-repository.stub.ts @@ -16,6 +16,7 @@ import { faker } from '@faker-js/faker'; import type { CodeTypeEnum } from '@/shared/code/code-type.enum'; + import { codeFixture } from '../fixtures'; import { createQueryBuilder, removeUndefinedValues } from '../util-functions'; diff --git a/apps/api/src/types/config-service.type.ts b/apps/api/src/types/config-service.type.ts index 0b7f34703..2bd1c6cd1 100644 --- a/apps/api/src/types/config-service.type.ts +++ b/apps/api/src/types/config-service.type.ts @@ -21,10 +21,10 @@ import type { mysqlConfig } from '@/configs/mysql.config'; import type { opensearchConfig } from '@/configs/opensearch.config'; import type { smtpConfig } from '@/configs/smtp.config'; -export type ConfigServiceType = { +export interface ConfigServiceType { app: ConfigType; opensearch: ConfigType; smtp: ConfigType; jwt: ConfigType; mysql: ConfigType; -}; +} diff --git a/apps/api/test/auth.e2e-spec.ts b/apps/api/test/auth.e2e-spec.ts index ac2e54e2c..e5e822dcc 100644 --- a/apps/api/test/auth.e2e-spec.ts +++ b/apps/api/test/auth.e2e-spec.ts @@ -23,6 +23,10 @@ import { getDataSourceToken } from '@nestjs/typeorm'; import request from 'supertest'; import type { DataSource, Repository } from 'typeorm'; +import { CodeTypeEnum } from '@/shared/code/code-type.enum'; +import { CodeEntity } from '@/shared/code/code.entity'; +import { CodeService } from '@/shared/code/code.service'; + import { AppModule } from '@/app.module'; import { EmailUserSignInRequestDto, @@ -39,9 +43,6 @@ import { } from '@/domains/admin/user/entities/enums'; import { UserEntity } from '@/domains/admin/user/entities/user.entity'; import { UserPasswordService } from '@/domains/admin/user/user-password.service'; -import { CodeTypeEnum } from '@/shared/code/code-type.enum'; -import { CodeEntity } from '@/shared/code/code.entity'; -import { CodeService } from '@/shared/code/code.service'; import { clearEntities } from '@/test-utils/util-functions'; describe('AppController (e2e)', () => { diff --git a/apps/api/test/feedback/channel.e2e-spec.ts b/apps/api/test/feedback/channel.e2e-spec.ts index 314efd429..6ee7c69cd 100644 --- a/apps/api/test/feedback/channel.e2e-spec.ts +++ b/apps/api/test/feedback/channel.e2e-spec.ts @@ -26,8 +26,8 @@ import type { DataSource, Repository } from 'typeorm'; import { AppModule } from '@/app.module'; import { FieldFormatEnum, + FieldPropertyEnum, FieldStatusEnum, - FieldTypeEnum, } from '@/common/enums'; import { HttpExceptionFilter } from '@/common/filters'; import { ChannelEntity } from '@/domains/admin/channel/channel/channel.entity'; @@ -133,7 +133,7 @@ describe('AppController (e2e)', () => { name: 'createdAt', key: 'createdAt', format: FieldFormatEnum.date, - type: FieldTypeEnum.DEFAULT, + property: FieldPropertyEnum.READ_ONLY, status: FieldStatusEnum.ACTIVE, options: undefined, description: '', @@ -142,7 +142,7 @@ describe('AppController (e2e)', () => { name: 'updatedAt', key: 'updatedAt', format: FieldFormatEnum.date, - type: FieldTypeEnum.DEFAULT, + property: FieldPropertyEnum.READ_ONLY, status: FieldStatusEnum.ACTIVE, options: undefined, description: '', @@ -290,7 +290,7 @@ describe('AppController (e2e)', () => { const fieldEntityToDto2 = (field: FieldEntity) => ({ name: field.name, format: field.format, - type: field.type, + property: field.property, status: field.status, description: field.description, options: diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index ff80861b1..713860307 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -7,6 +7,6 @@ "@/*": ["./src/*"] } }, - "include": ["src"], + "include": ["src", "test"], "exclude": ["node_modules", "dist"] } diff --git a/apps/e2e/global.setup.ts b/apps/e2e/global.setup.ts index ed08ae17e..e41efe81a 100644 --- a/apps/e2e/global.setup.ts +++ b/apps/e2e/global.setup.ts @@ -1,23 +1,27 @@ -import { expect, test as setup } from '@playwright/test'; +import { expect, test as setup } from "@playwright/test"; -const authFile = 'playwright/.auth/user.json'; +const authFile = "playwright/.auth/user.json"; -setup('tenant create and authenticate', async ({ page }) => { - await page.goto('http://localhost:3000/en/tenant/create'); - await page.getByLabel('Site name').click(); - await page.getByLabel('Site name').fill('TestTenant'); - await page.getByRole('button', { name: 'Settings', exact: true }).click(); +setup("tenant create and authenticate", async ({ page }) => { + await page.goto("http://localhost:3000/tenant/create"); + await page.waitForTimeout(1000); - await page.goto('http://localhost:3000/en/auth/sign-in'); - await expect(page.getByRole('banner')).toHaveText(/TestTenant/, { + await page.getByLabel("Site name").click(); + await page.getByLabel("Site name").fill("TestTenant"); + await page.getByRole("button", { name: "Setting", exact: true }).click(); + + await page.goto("http://localhost:3000/auth/sign-in"); + await page.waitForTimeout(1000); + + await expect(page.getByRole("banner")).toHaveText(/TestTenant/, { timeout: 500000, }); - await page.getByPlaceholder('ID').click(); - await page.getByPlaceholder('ID').fill('user@feedback.com'); - await page.getByPlaceholder('Password').click(); - await page.getByPlaceholder('Password').fill('12345678'); - await page.getByRole('button', { name: 'Sign In', exact: true }).click(); - await page.waitForURL('http://localhost:3000/en/main'); + await page.getByPlaceholder("ID").click(); + await page.getByPlaceholder("ID").fill("user@feedback.com"); + await page.getByPlaceholder("Password").click(); + await page.getByPlaceholder("Password").fill("12345678"); + await page.getByRole("button", { name: "Sign In", exact: true }).click(); + await page.waitForURL("http://localhost:3000/main"); await page.context().storageState({ path: authFile }); }); diff --git a/apps/e2e/package.json b/apps/e2e/package.json index af9308474..4b5b6e91d 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "", "scripts": { + "clean": "git clean -xdf node_modules playwright-report test-results", "test:e2e": "playwright test" }, "devDependencies": { diff --git a/apps/e2e/playwright.config.ts b/apps/e2e/playwright.config.ts index f0da91fc1..945b8cc84 100644 --- a/apps/e2e/playwright.config.ts +++ b/apps/e2e/playwright.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 5000, + timeout: 15 * 1000, }, /* Run tests in files in parallel */ fullyParallel: true, diff --git a/apps/e2e/scenarios/no-database-seed/create-project.spec.ts b/apps/e2e/scenarios/no-database-seed/create-project.spec.ts index c51eba0cc..70a058ee4 100644 --- a/apps/e2e/scenarios/no-database-seed/create-project.spec.ts +++ b/apps/e2e/scenarios/no-database-seed/create-project.spec.ts @@ -1,97 +1,121 @@ -import { expect, test } from '@playwright/test'; +import { expect, test } from "@playwright/test"; export default () => { test.afterEach(async ({ page }) => { - await page.getByText('TestProject').click(); - await page.getByText('FeedbackIssueSetting').hover(); - await page.getByRole('button', { name: 'Settings', exact: true }).click(); - - await page.getByText('Delete Project').click(); - await page.getByRole('button', { name: 'Delete' }).click(); - await page.getByPlaceholder('Enter Value').click(); - await page.getByPlaceholder('Enter Value').fill('TestProject'); - await page.getByRole('button', { name: 'Delete' }).click(); - await expect(page.getByText('TestProject')).toHaveCount(0); + await page.getByText("TestProject").click(); + await page.getByText("FeedbackIssueSetting").hover(); + await page.getByRole("button", { name: "Settings", exact: true }).click(); + + await page.getByText("Delete Project").click(); + await page.getByRole("button", { name: "Delete" }).click(); + await page.getByPlaceholder("Enter Value").click(); + await page.getByPlaceholder("Enter Value").fill("TestProject"); + await page.getByRole("button", { name: "Delete" }).click(); + await expect(page.getByText("TestProject")).toHaveCount(0); }); - test('creating a project succeeds', async ({ page }) => { - await page.goto('http://localhost:3000'); - await page.getByRole('button', { name: 'Create Project' }).click(); - await page.getByPlaceholder('Please enter Project Name.').click(); + test("creating a project succeeds", async ({ page }) => { + await page.goto("http://localhost:3000"); + await page.waitForTimeout(1000); + + await page.getByRole("button", { name: "Create Project" }).click(); + + await page.getByPlaceholder("Please enter Project Name.").click(); await page - .getByPlaceholder('Please enter Project Name.') - .fill('TestProject'); - await page.getByPlaceholder('Please enter Project Description.').click(); + .getByPlaceholder("Please enter Project Name.") + .fill("TestProject"); + await page.getByPlaceholder("Please enter Project Description.").click(); await page - .getByPlaceholder('Please enter Project Description.') - .fill('Project for test'); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Complete' }).click(); - await expect(page.getByText('Project Creation Complete')).toBeVisible(); - await expect(page.locator('#Project\\ Name')).toHaveValue('TestProject'); - await expect(page.locator('#Project\\ Description')).toHaveValue( - 'Project for test', + .getByPlaceholder("Please enter Project Description.") + .fill("Project for test"); + + await page.getByRole("button", { name: "Next" }).click(); + await page.waitForTimeout(1500); + + await page.getByRole("button", { name: "Next" }).click(); + await page.waitForTimeout(1500); + + await page.getByRole("button", { name: "Next" }).click(); + await page.waitForTimeout(1500); + + await page.getByRole("button", { name: "Next" }).click(); + await page.waitForTimeout(1500); + + await page.getByRole("button", { name: "Complete" }).click(); + + await expect(page.getByText("Project Creation Complete")).toBeVisible(); + await expect(page.locator("#Project\\ Name")).toHaveValue("TestProject"); + await expect(page.locator("#Project\\ Description")).toHaveValue( + "Project for test" ); - await page.getByRole('button', { name: 'Later' }).click(); - await expect(page.getByText('TestProject')).toBeVisible(); + await page.getByRole("button", { name: "Later" }).click(); + await expect(page.getByText("TestProject")).toBeVisible(); }); - test('creating a project with a new role succeeds', async ({ page }) => { - await page.goto('http://localhost:3000'); - await page.getByRole('button', { name: 'Create Project' }).click(); - await page.getByPlaceholder('Please enter Project Name.').click(); + test("creating a project with a new role succeeds", async ({ page }) => { + await page.goto("http://localhost:3000"); + await page.waitForTimeout(1500); + + await page.getByRole("button", { name: "Create Project" }).click(); + + await page.getByPlaceholder("Please enter Project Name.").click(); await page - .getByPlaceholder('Please enter Project Name.') - .fill('TestProject'); - await page.getByPlaceholder('Please enter Project Description.').click(); + .getByPlaceholder("Please enter Project Name.") + .fill("TestProject"); + await page.getByPlaceholder("Please enter Project Description.").click(); await page - .getByPlaceholder('Please enter Project Description.') - .fill('Project for test'); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Create Role' }).click(); - await page.getByLabel('Role Name').click(); - await page.getByLabel('Role Name').fill('Test Role'); - await page.getByRole('button', { name: 'OK' }).click(); + .getByPlaceholder("Please enter Project Description.") + .fill("Project for test"); + await page.getByRole("button", { name: "Next" }).click(); + await page.waitForTimeout(1500); + + await page.getByRole("button", { name: "Create Role" }).click(); + await page.getByLabel("Role Name").click(); + await page.getByLabel("Role Name").fill("Test Role"); + await page.getByRole("button", { name: "OK" }).click(); await page - .getByRole('cell', { name: 'Test Role' }) - .getByRole('button') + .getByRole("cell", { name: "Test Role" }) + .getByRole("button") .click(); - await page.locator('li').filter({ hasText: 'Edit Role' }).click(); + await page.locator("li").filter({ hasText: "Edit Role" }).click(); await page - .getByRole('row', { name: 'Edit Feedback' }) - .getByRole('checkbox') + .getByRole("row", { name: "Edit Feedback" }) + .getByRole("checkbox") .nth(3) .check(); await page - .getByRole('cell', { name: 'Test Role' }) - .getByRole('button') + .getByRole("cell", { name: "Test Role" }) + .getByRole("button") .nth(1) .click(); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Complete' }).click(); - await expect(page.getByText('Project Creation Complete')).toBeVisible(); - await expect(page.locator('#Project\\ Name')).toHaveValue('TestProject'); - await expect(page.locator('#Project\\ Description')).toHaveValue( - 'Project for test', + await page.getByRole("button", { name: "Next" }).click(); + await page.waitForTimeout(1500); + + await page.getByRole("button", { name: "Next" }).click(); + await page.waitForTimeout(1500); + + await page.getByRole("button", { name: "Next" }).click(); + await page.waitForTimeout(1500); + + await page.getByRole("button", { name: "Complete" }).click(); + await expect(page.getByText("Project Creation Complete")).toBeVisible(); + await expect(page.locator("#Project\\ Name")).toHaveValue("TestProject"); + await expect(page.locator("#Project\\ Description")).toHaveValue( + "Project for test" ); await page - .locator('div') + .locator("div") .filter({ hasText: /^Role Management$/ }) - .getByRole('button') + .getByRole("button") .click(); - await expect(page.getByText('Test Role')).toBeVisible(); + await expect(page.getByText("Test Role")).toBeVisible(); await expect( page - .getByRole('row', { name: 'Edit Feedback' }) - .getByRole('checkbox') - .nth(3), + .getByRole("row", { name: "Edit Feedback" }) + .getByRole("checkbox") + .nth(3) ).toBeChecked(); - await page.getByRole('button', { name: 'Later' }).click(); - await expect(page.getByText('TestProject')).toBeVisible(); + await page.getByRole("button", { name: "Later" }).click(); + await expect(page.getByText("TestProject")).toBeVisible(); }); }; diff --git a/apps/e2e/scenarios/sign-up.spec.ts b/apps/e2e/scenarios/sign-up.spec.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/e2e/scenarios/with-database-seed/create-channel.spec.ts b/apps/e2e/scenarios/with-database-seed/create-channel.spec.ts index d358fcedb..10a1f114d 100644 --- a/apps/e2e/scenarios/with-database-seed/create-channel.spec.ts +++ b/apps/e2e/scenarios/with-database-seed/create-channel.spec.ts @@ -1,28 +1,30 @@ -import { expect, test } from '@playwright/test'; +import { expect, test } from "@playwright/test"; export default () => { - test.describe('create-channel suite', () => { - test('creating a channel succeeds', async ({ page }) => { - await page.goto('http://localhost:3000'); - await page.getByText('SeededTestProject').click(); - await page.getByText('FeedbackIssueSetting').hover(); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('button', { name: 'Create Channel' }).click(); - await page.getByPlaceholder('Please enter Channel Name.').click(); + test.describe("create-channel suite", () => { + test("creating a channel succeeds", async ({ page }) => { + await page.goto("http://localhost:3000/main"); + await page.waitForTimeout(1000); + + await page.getByText("SeededTestProject").click(); + await page.getByText("FeedbackIssueSetting").hover(); + await page.getByRole("button", { name: "Settings" }).click(); + await page.getByRole("button", { name: "Create Channel" }).click(); + await page.getByPlaceholder("Please enter Channel Name.").click(); await page - .getByPlaceholder('Please enter Channel Name.') - .fill('TestChannel'); - await page.getByPlaceholder('Please enter Channel Description.').click(); + .getByPlaceholder("Please enter Channel Name.") + .fill("TestChannel"); + await page.getByPlaceholder("Please enter Channel Description.").click(); await page - .getByPlaceholder('Please enter Channel Description.') - .fill('Channel for test'); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Next' }).click(); - await page.getByRole('button', { name: 'Complete' }).click(); - await expect(page.locator('#Channel\\ Name')).toHaveValue('TestChannel'); - await expect(page.locator('#Channel\\ Description')).toHaveValue( - 'Channel for test', + .getByPlaceholder("Please enter Channel Description.") + .fill("Channel for test"); + await page.getByRole("button", { name: "Next" }).click(); + await page.getByRole("button", { name: "Next" }).click(); + await page.getByRole("button", { name: "Next" }).click(); + await page.getByRole("button", { name: "Complete" }).click(); + await expect(page.locator("#Channel\\ Name")).toHaveValue("TestChannel"); + await expect(page.locator("#Channel\\ Description")).toHaveValue( + "Channel for test" ); }); }); diff --git a/apps/e2e/scenarios/with-database-seed/create-feedback.spec.ts b/apps/e2e/scenarios/with-database-seed/create-feedback.spec.ts index 966cb3535..d81b5f67f 100644 --- a/apps/e2e/scenarios/with-database-seed/create-feedback.spec.ts +++ b/apps/e2e/scenarios/with-database-seed/create-feedback.spec.ts @@ -1,52 +1,53 @@ -import { expect, test } from '@playwright/test'; -import axios from 'axios'; +import { expect, test } from "@playwright/test"; +import axios from "axios"; export default () => { - test.describe('create-feedback suite', () => { + test.describe("create-feedback suite", () => { test.afterEach(async ({ page }) => { await page .locator( - '#__next > div > div > main > div > div.overflow-x-auto > table > tbody > tr:nth-child(1) > td:nth-child(1) > div > input', + "#__next > div > div > main > div > div.overflow-x-auto > table > tbody > tr:nth-child(1) > td:nth-child(1) > div > input" ) .click(); - await page.getByRole('button', { name: 'Delete' }).click(); - await page.getByRole('button', { name: 'Delete' }).click(); + await page.getByRole("button", { name: "Delete" }).click(); + await page + .getByRole("dialog") + .getByRole("button", { name: "Delete" }) + .click(); - await expect(page.getByText('Deleted Successfully')).toBeVisible(); + await expect(page.getByText("Deleted Successfully")).toBeVisible(); }); - test('creating a feedback succeeds', async ({ page }) => { - await page.goto('http://localhost:3000'); - await page.getByText('SeededTestProject').click(); - await page.getByText('FeedbackIssueSetting').hover(); - await page.getByRole('button', { name: 'Feedback', exact: true }).click(); - await page.getByRole('button', { name: 'Column Settings' }).hover(); - await page.getByText('SeededTestChannel', { exact: true }).click(); + test("creating a feedback succeeds", async ({ page }) => { + await page.goto("http://localhost:3000"); + await page.waitForTimeout(1000); + + await page.getByText("SeededTestProject").click(); + await page.getByText("FeedbackIssueSetting").hover(); + await page.getByRole("button", { name: "Feedback", exact: true }).click(); + await page.getByRole("button", { name: "Column Settings" }).hover(); + await page.getByText("SeededTestChannel", { exact: true }).click(); await page.waitForURL(/.*channelId.*/, { timeout: 1000 }); const url = new URL(page.url()); const pathname = url.pathname; - const segments = pathname.split('/'); - const projectId = segments[4]; + const segments = pathname.split("/"); + const projectId = segments[3]; const params = new URLSearchParams(url.search); - const channelId = params.get('channelId'); + const channelId = params.get("channelId"); - const res = await axios.post( + await axios.post( `http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`, - { - SeededTestTextField: 'test text', - }, - { - headers: { - 'x-api-key': 'MASTER_API_KEY', - }, - }, + { SeededTestTextField: "test text" }, + { headers: { "x-api-key": "MASTER_API_KEY" } } ); await page.goto( - `http://localhost:3000/en/main/project/${projectId}/feedback?channelId=${channelId}`, + `http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}` ); - await expect(page.locator('tbody')).toContainText('test text'); + await page.waitForTimeout(1000); + + await expect(page.locator("tbody")).toContainText("test text"); }); }); }; diff --git a/apps/e2e/test.list.ts b/apps/e2e/test.list.ts index ec4e32698..0b1a0caf7 100644 --- a/apps/e2e/test.list.ts +++ b/apps/e2e/test.list.ts @@ -1,16 +1,16 @@ -import { test } from '@playwright/test'; -import { ResultSetHeader } from 'mysql2'; +import { test } from "@playwright/test"; +import { ResultSetHeader } from "mysql2"; -import { createConnection } from './database-utils'; -import createProject from './scenarios/no-database-seed/create-project.spec'; -import createChannel from './scenarios/with-database-seed/create-channel.spec'; -import createFeedback from './scenarios/with-database-seed/create-feedback.spec'; +import { createConnection } from "./database-utils"; +import createProject from "./scenarios/no-database-seed/create-project.spec"; +import createChannel from "./scenarios/with-database-seed/create-channel.spec"; +import createFeedback from "./scenarios/with-database-seed/create-feedback.spec"; -test.describe('Tests without database seed', () => { +test.describe("Tests without database seed", () => { createProject(); }); -test.describe('Tests with database seed', () => { +test.describe("Tests with database seed", () => { test.beforeEach(async () => { await seedDatabase(); }); @@ -28,12 +28,12 @@ async function seedDatabase() { try { const projects = (await connection.execute( `INSERT INTO projects (name, tenant_id) VALUES (?, ?)`, - ['SeededTestProject', 1], + ["SeededTestProject", 1] )) as ResultSetHeader[]; const channels = (await connection.execute( `INSERT INTO channels (name, project_id) VALUES (?, ?)`, - ['SeededTestChannel', projects[0].insertId], + ["SeededTestChannel", projects[0].insertId] )) as ResultSetHeader[]; await connection.query( @@ -41,43 +41,43 @@ async function seedDatabase() { [ [ [ - 'SeededTestTextField', - 'SeededTestTextField', - 'text', - 'READ_ONLY', - 'ACTIVE', + "SeededTestTextField", + "SeededTestTextField", + "text", + "READ_ONLY", + "ACTIVE", channels[0].insertId, ], - ['ID', 'id', 'number', 'READ_ONLY', 'ACTIVE', channels[0].insertId], + ["ID", "id", "number", "READ_ONLY", "ACTIVE", channels[0].insertId], [ - 'Created', - 'createdAt', - 'date', - 'READ_ONLY', - 'ACTIVE', + "Created", + "createdAt", + "date", + "READ_ONLY", + "ACTIVE", channels[0].insertId, ], [ - 'Updated', - 'updatedAt', - 'date', - 'READ_ONLY', - 'ACTIVE', + "Updated", + "updatedAt", + "date", + "READ_ONLY", + "ACTIVE", channels[0].insertId, ], [ - 'Issue', - 'issues', - 'multiSelect', - 'EDITABLE', - 'ACTIVE', + "Issue", + "issues", + "multiSelect", + "EDITABLE", + "ACTIVE", channels[0].insertId, ], ], - ], + ] ); - console.log('seeding database succeeds'); + console.log("seeding database succeeds"); } finally { await connection.end(); } @@ -87,7 +87,7 @@ async function clearDatabase() { const connection = await createConnection(); try { await connection.execute(`DELETE FROM projects WHERE name = ?`, [ - 'SeededTestProject', + "SeededTestProject", ]); } finally { await connection.end(); diff --git a/apps/web/.prettierignore b/apps/web/.prettierignore deleted file mode 100644 index 24a4db7bd..000000000 --- a/apps/web/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -src/types/api.type.ts diff --git a/apps/web/JSDOMEnvironment.ts b/apps/web/JSDOMEnvironment.ts new file mode 100644 index 000000000..28ee0d788 --- /dev/null +++ b/apps/web/JSDOMEnvironment.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import JSDOMEnvironment from 'jest-environment-jsdom'; + +// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string +export default class FixJSDOMEnvironment extends JSDOMEnvironment { + constructor(...args: ConstructorParameters) { + super(...args); + + // FIXME https://github.com/jsdom/jsdom/issues/3363 + this.global.structuredClone = structuredClone; + } +} diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js new file mode 100644 index 000000000..9024f4d9f --- /dev/null +++ b/apps/web/eslint.config.js @@ -0,0 +1,20 @@ +const baseConfig = require('@ufb/eslint-config/base'); +const nextjsConfig = require('@ufb/eslint-config/nextjs'); +const reactConfig = require('@ufb/eslint-config/react'); + +/** @type {import('typescript-eslint').Config} */ +module.exports = [ + { + ignores: [ + '.next/**', + '**/*.spec.ts', + '**/*.spec.tsx', + 'jest.setup.ts', + 'next-env.d.ts', + 'jest.polyfills.js', + ], + }, + ...baseConfig, + ...reactConfig, + ...nextjsConfig, +]; diff --git a/apps/web/jest.config.mjs b/apps/web/jest.config.mjs deleted file mode 100644 index 0168145ed..000000000 --- a/apps/web/jest.config.mjs +++ /dev/null @@ -1,19 +0,0 @@ -import nextJest from 'next/jest.js'; - -const createJestConfig = nextJest({ dir: './' }); - -// Add any custom config to be passed to Jest -/** @type {import('jest').Config} */ -const jestConfig = { - setupFilesAfterEnv: ['/jest.setup.js'], - testEnvironment: 'jest-environment-jsdom', - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, - transform: { - '^.+\\.(t|j)sx?$': '@swc/jest', - }, -}; - -// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async -export default createJestConfig(jestConfig); diff --git a/apps/web/jest.config.ts b/apps/web/jest.config.ts new file mode 100644 index 000000000..36404ffd1 --- /dev/null +++ b/apps/web/jest.config.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import nextJest from 'next/jest.js'; +import type { Config } from 'jest'; + +const createJestConfig = nextJest({ dir: './' }); + +// Add any custom config to be passed to Jest +// /** @type {import('jest').Config} */ +const jestConfig = { + coverageProvider: 'v8', + testEnvironment: './JSDOMEnvironment.ts', + setupFilesAfterEnv: ['/jest.setup.ts'], + setupFiles: ['/jest.polyfills.js'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + testEnvironmentOptions: { + customExportConditions: [''], + }, + extensionsToTreatAsEsm: ['.ts'], + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/index.ts', + ], +} satisfies Config; + +// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async +export default createJestConfig(jestConfig); diff --git a/apps/web/jest.polyfills.js b/apps/web/jest.polyfills.js new file mode 100644 index 000000000..0efcb71f5 --- /dev/null +++ b/apps/web/jest.polyfills.js @@ -0,0 +1,18 @@ +// https://mswjs.io/docs/faq/#requestresponsetextencoder-is-not-defined-jest +// https://github.com/mswjs/msw/discussions/1934 +const { TextDecoder, TextEncoder, ReadableStream } = require('node:util'); + +Object.defineProperties(globalThis, { + TextDecoder: { value: TextDecoder }, + TextEncoder: { value: TextEncoder }, + ReadableStream: { value: ReadableStream }, +}); + +const { fetch, Headers, Request, Response } = require('undici'); + +Object.defineProperties(globalThis, { + fetch: { value: fetch, writable: true }, + Headers: { value: Headers }, + Request: { value: Request }, + Response: { value: Response }, +}); diff --git a/apps/web/jest.setup.ts b/apps/web/jest.setup.ts new file mode 100644 index 000000000..dccb152e4 --- /dev/null +++ b/apps/web/jest.setup.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { faker } from '@faker-js/faker'; +import nextRouterMock from 'next-router-mock'; + +import { server } from './src/msw'; + +import '@testing-library/jest-dom'; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); +// headless-ui +global.ResizeObserver = require('resize-observer-polyfill'); + +// iron-session +const crypto = require('crypto'); + +Object.defineProperty(globalThis, 'crypto', { + value: { + getRandomValues: (arr: any[]) => crypto.randomBytes(arr.length), + }, +}); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (str: string) => str }), +})); + +// msw +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +afterEach(() => jest.resetAllMocks()); + +jest.mock('next/router', () => ({ + useRouter: () => nextRouterMock, +})); + +jest.mock('@t3-oss/env-nextjs', () => ({ + createEnv: () => ({ + API_BASE_URL: process.env.API_BASE_URL, + SESSION_PASSWORD: process.env.SESSION_PASSWORD, + NEXT_PUBLIC_MAX_DAYS: process.env.NEXT_PUBLIC_MAX_DAYS, + NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, + NODE_ENV: process.env.NODE_ENV, + }), +})); + +faker.seed(100); diff --git a/apps/web/next-i18next.config.js b/apps/web/next-i18next.config.js index 2fc4ae52a..e29be35c7 100644 --- a/apps/web/next-i18next.config.js +++ b/apps/web/next-i18next.config.js @@ -1,6 +1,27 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** @type {import('next-i18next').UserConfig} */ module.exports = { i18n: { - defaultLocale: 'default', - locales: ['default', 'de', 'en', 'ja', 'ko', 'zh'], + defaultLocale: 'en', + locales: ['de', 'en', 'ja', 'ko', 'zh'], }, + fallbackLng: { + default: ['en'], + }, + nonExplicitSupportedLngs: true, }; diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index eb6a66e35..99f8a8549 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,17 +1,17 @@ -import './src/env.mjs'; - import path from 'path'; import { fileURLToPath } from 'url'; +import createJiti from 'jiti'; -import i18nConfig from './next-i18next.config.js'; +import * as i18nConfig from './next-i18next.config.js'; +createJiti(fileURLToPath(import.meta.url))('./src/env'); const __dirname = fileURLToPath(new URL('.', import.meta.url)); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: process.env.NODE_ENV === 'production', swcMinify: true, - i18n: i18nConfig.i18n, + i18n: i18nConfig.default.i18n, output: 'standalone', experimental: { outputFileTracingRoot: path.join(__dirname, '../../') }, eslint: { ignoreDuringBuilds: true }, @@ -19,6 +19,7 @@ const nextConfig = { compiler: { removeConsole: process.env.NODE_ENV === 'production' }, images: { remotePatterns: [{ hostname: '*' }] }, webpack(config) { + // @ts-ignore const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg'), ); @@ -38,7 +39,6 @@ const nextConfig = { }, ); - // Modify the file loader rule to ignore *.svg, since we have it handled now. fileLoaderRule.exclude = /\.svg$/i; return config; diff --git a/apps/web/package.json b/apps/web/package.json index 48fc92843..9dd8bf507 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,16 +4,17 @@ "private": true, "scripts": { "build": "next build", - "clean": "git clean -xdf .next .turbo node_modules", + "clean": "git clean -xdf .next .turbo node_modules coverage .swc", "dev": "next dev", "format": "prettier --check . --ignore-path ../../.gitignore", "format:fix": "prettier --write --list-different \"./src/**/*.{js,cjs,mjs,ts,tsx,md,json}\"", - "generate-api-type": "openapi-typescript http://0.0.0.0:4000/admin-docs-json --output src/types/api.type.ts", + "generate-api-type": "openapi-typescript http://0.0.0.0:4000/admin-docs-json --output src/shared/types/api.type.ts --default-non-nullable=false --empty-objects-unknown", "lint": "eslint", "start": "next start", "test": "SKIP_ENV_VALIDATION=true jest --passWithNoTests", + "test:snapshot": "jest -u", "test:ci": "jest --ci --passWithNoTests", - "test:dev": "jest --watch --passWithNoTests", + "test:watch": "SKIP_ENV_VALIDATION=1 jest --watch --passWithNoTests", "typecheck": "tsc --noEmit" }, "prettier": "@ufb/prettier-config", @@ -31,13 +32,14 @@ "dependencies": { "@faker-js/faker": "^8.4.1", "@floating-ui/react": "^0.26.12", - "@headlessui/react": "2.0.4", + "@headlessui/react": "2.1.2", "@headlessui/tailwindcss": "^0.2.0", "@hookform/resolvers": "^3.3.4", "@mui/base": "5.0.0-beta.40", - "@t3-oss/env-nextjs": "^0.10.0", + "@t3-oss/env-nextjs": "^0.11.0", "@tanstack/react-query": "^5.31.0", "@tanstack/react-table": "^8.16.0", + "@toss/use-overlay": "^1.4.0", "@ufb/shared": "workspace:*", "@ufb/tailwind": "workspace:*", "@ufb/ui": "workspace:*", @@ -50,12 +52,12 @@ "date-fns": "^3.6.0", "dayjs": "^1.11.10", "framer-motion": "^11.1.7", - "i18next": "^23.11.2", + "i18next": "^23.11.5", "immer": "^10.0.4", "iron-session": "^8.0.0", "jwt-decode": "^4.0.0", "next": "^14.2.2", - "next-i18next": "^15.0.0", + "next-i18next": "^15.3.0", "nuqs": "^1.17.4", "pino": "^9.0.0", "react": "^18.2.0", @@ -64,11 +66,12 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.51.3", "react-hot-toast": "^2.4.1", - "react-i18next": "^14.0.0", + "react-i18next": "^15.0.0", "react-select": "^5.8.0", "react-use": "^17.5.0", "recharts": "^2.12.6", "sharp": "^0.33.0", + "tailwind-merge": "^2.3.0", "tailwind-scrollbar-hide": "^1.1.7", "zod": "^3.23.0", "zustand": "^4.5.2" @@ -79,11 +82,12 @@ "@svgr/webpack": "^8.1.0", "@swc/core": "^1.4.16", "@swc/jest": "^0.2.36", + "@tanstack/react-query-devtools": "^5.45.1", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", - "@types/node": "20.14.2", + "@types/node": "20.14.11", "@types/react": "^18.2.79", "@types/react-beautiful-dnd": "^13.1.8", "@types/react-datepicker": "^6.0.0", @@ -92,13 +96,18 @@ "@ufb/prettier-config": "workspace:*", "@ufb/tsconfig": "workspace:*", "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", + "eslint": "^9.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "openapi-typescript": "^6.7.5", + "jiti": "^1.21.6", + "msw": "^2.3.0", + "next-router-mock": "^0.9.13", + "node-mocks-http": "^1.14.1", + "openapi-typescript": "^7.0.0", "postcss": "^8.4.38", "tailwindcss": "^3.4.3", "ts-toolbelt": "^9.6.0", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "undici": "~5.28.4" } } diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.cjs similarity index 100% rename from apps/web/postcss.config.js rename to apps/web/postcss.config.cjs diff --git a/apps/web/public/locales/de/common.json b/apps/web/public/locales/de/common.json index df7aa210d..e7506b475 100644 --- a/apps/web/public/locales/de/common.json +++ b/apps/web/public/locales/de/common.json @@ -453,7 +453,7 @@ "issue-tracker": "Verknüpfen und verwalten Sie User Feedback mit Ihrem Issue-Tracking-System. Bitte geben Sie die Details Ihres Issue-Tracking-Systems ein.", "channel-info": "Definieren Sie die Feedbackfelder, die über den Kanal gesammelt werden sollen. Bitte registrieren Sie Kanalinformationen unter Berücksichtigung des Feedbackwegs und der Eigenschaften (z.B. VOC, App-Bewertung).", "field": "Definieren Sie die Feedbackfelder, die über User Feedback für jeden registrierten Kanal gesammelt werden sollen. Konfigurieren Sie vorab die Felder, die über die API gesammelt oder direkt im ADMIN registriert werden sollen.", - "image-setting": "Für Feedback in Bildformat, integrieren Sie einen Bild-Speicher und richten Sie eine Whitelist für Bild-URL-Domains ein.\nBeziehen Sie sich auf die Bild Dokumentation für Details.", + "image-config": "Für Feedback in Bildformat, integrieren Sie einen Bild-Speicher und richten Sie eine Whitelist für Bild-URL-Domains ein.\nBeziehen Sie sich auf die Bild Dokumentation für Details.", "field-preview": "Vorschau der konfigurierten Felder in der Feldverwaltung. Die Vorschau zeigt Beispieldaten zur Veranschaulichung.", "webhook": "Nutzen Sie Webhooks, um auf eine Vielzahl von Ereignissen innerhalb von User Feedback zu reagieren. Für weitere Details, beziehen Sie sich auf die Webhook Dokumentation." }, diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json index 9ca668032..16e636303 100644 --- a/apps/web/public/locales/en/common.json +++ b/apps/web/public/locales/en/common.json @@ -235,7 +235,7 @@ "sign-out": "Sign Out" }, "button": { - "setting": "Settings", + "setting": "Setting", "back": "Back", "sign-in": "Sign In", "sign-up": "Sign Up", @@ -453,7 +453,7 @@ "issue-tracker": "Link and manage User Feedback with your Issue Tracking System. Please enter your Issue Tracking System details.", "channel-info": "Define the feedback fields to collect through the channel. Please register channel information considering the feedback path and characteristics (e.g., VOC, App Review).", "field": "Define the feedback fields to be collected via User Feedback for each registered channel. Pre-configure fields to be collected through the API or those to be directly registered in the ADMIN.", - "image-setting": "For feedback in images format, integrate with Image Storage and establish a whitelist for image URL domains.\nRefer to the Image Docs for details.", + "image-config": "For feedback in images format, integrate with Image Storage and establish a whitelist for image URL domains.\nRefer to the Image Docs for details.", "field-preview": "Preview the appearance of fields configured in Field Management. Previews show sample data for illustration purposes.", "webhook": "Webhook is used for various events that occur within User Feedback. For more details, refer to the Webhook Docs." }, diff --git a/apps/web/public/locales/ja/common.json b/apps/web/public/locales/ja/common.json index edc09df3a..dd1b494dd 100644 --- a/apps/web/public/locales/ja/common.json +++ b/apps/web/public/locales/ja/common.json @@ -441,7 +441,7 @@ "issue-tracker": "UserFeedbackフィードバックとIssue Tracking Systemを接続して管理することができます。\n使用中のIssue Tracking System情報を入力してください。", "channel-info": "Channelを通じて収集したいフィードバックフィールドを定義できます。 フィードバック経路と性格を考慮してChannel情報を登録してください。(ex. VOC, APP Reivew)", "field": "登録したChannelに合わせてUser Feedbackで収集したいフィードバックフィールドを定義します。 APIを通じて収集したいフィールドやADMINで直接登録したいフィールドをあらかじめ設定してみてください。", - "image-setting": "Imageフォーマットに対してフィードバックを収集するとき、Image Storageと連動することができ、Image URLドメインに対するホワイトリストを設定することができます。\n詳細については、Image Docsを参照してください。", + "image-config": "Imageフォーマットに対してフィードバックを収集するとき、Image Storageと連動することができ、Image URLドメインに対するホワイトリストを設定することができます。\n詳細については、Image Docsを参照してください。", "field-preview": "Field管理で設定したフィールドがどのように見えるかを事前に確認することができます。 プレビューは任意のデータで表示されるため、実際とは異なります。", "webhook": "Webhook は、User Feedback 内で発生するさまざまなイベントに使用されます。 詳細はWebhook Docs をご覧ください。" }, diff --git a/apps/web/public/locales/ko/common.json b/apps/web/public/locales/ko/common.json index 8b35627df..61c9531c8 100644 --- a/apps/web/public/locales/ko/common.json +++ b/apps/web/public/locales/ko/common.json @@ -441,7 +441,7 @@ "issue-tracker": "UserFeedback 피드백과 Issue Tracking System을 연결해서 관리할 수 있습니다.\n사용 중인 Issue Tracking System 정보를 입력해 주세요.", "channel-info": "Channel을 통해 수집하고 싶은 피드백 필드를 정의할 수 있습니다. 피드백 경로와 성격을 고려하여 Channel 정보를 등록해주세요. (ex. VOC, APP Reivew)", "field": "등록한 Channel에 맞춰 UserFeedback으로 수집하고 싶은 피드백 필드를 정의합니다. API를 통해 수집하고 싶은 필드나 ADMIN에서 직접 등록하고 싶은 필드를 미리 설정해보세요. ", - "image-setting": "Image 포맷에 대해 피드백을 수집할 때 Image Storage와 연동할 수 있고 Image URL 도메인에 대한 화이트리스트를 설정할 수 있습니다.\n자세한 내용은 Image Docs를 참고해 주세요.", + "image-config": "Image 포맷에 대해 피드백을 수집할 때 Image Storage와 연동할 수 있고 Image URL 도메인에 대한 화이트리스트를 설정할 수 있습니다.\n자세한 내용은 Image Docs를 참고해 주세요.", "field-preview": "Field 관리에서 설정한 필드가 어떻게 보일지 미리 확인할 수 있습니다. 미리보기는 임의의 데이터로 보여주기 때문에 실제와 다릅니다.", "webhook": "Webhook을 사용하면 User Feedback에서 발생하는 다양한 이벤트를 활용할 수 있습니다. 자세한 내용은 Webhook Docs를 참고해 주세요." }, diff --git a/apps/web/public/locales/zh/common.json b/apps/web/public/locales/zh/common.json index 0c813c8e6..48f1a27d9 100644 --- a/apps/web/public/locales/zh/common.json +++ b/apps/web/public/locales/zh/common.json @@ -453,7 +453,7 @@ "issue-tracker": "将用户反馈与您的问题跟踪系统链接和管理。请输入您的问题跟踪系统详细信息。", "channel-info": "定义通过频道收集的反馈字段。请注册考虑到反馈路径和特点的频道信息(例如,VOC,应用评价)。", "field": "定义每个注册频道通过用户反馈收集的反馈字段。预先配置通过API收集或直接在ADMIN中注册的字段。", - "image-setting": "对于图片格式的反馈,与图片存储集成并建立图片URL域的白名单。\n有关详细信息,请参阅图片 文档。", + "image-config": "对于图片格式的反馈,与图片存储集成并建立图片URL域的白名单。\n有关详细信息,请参阅图片 文档。", "field-preview": "在字段管理中预览配置的字段外观。预览显示示例数据以便说明。", "webhook": "Webhook 用于在 User Feedback 中发生的各种事件。 有关更多详细信息,请参阅Webhook 文档。" }, diff --git a/apps/web/src/__mocks__/zustand.ts b/apps/web/src/__mocks__/zustand.ts new file mode 100644 index 000000000..2e35ff2bc --- /dev/null +++ b/apps/web/src/__mocks__/zustand.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { act } from '@testing-library/react'; +import type * as zustand from 'zustand'; + +const { create: actualCreate, createStore: actualCreateStore } = + jest.requireActual('zustand'); + +// a variable to hold reset functions for all stores declared in the app +export const storeResetFns = new Set<() => void>(); + +const createUncurried = (stateCreator: zustand.StateCreator) => { + const store = actualCreate(stateCreator); + const initialState = store.getInitialState(); + storeResetFns.add(() => { + store.setState(initialState, true); + }); + return store; +}; + +// when creating a store, we get its initial state, create a reset function and add it in the set +export const create = ((stateCreator: zustand.StateCreator) => { + // to support curried version of create + return typeof stateCreator === 'function' ? + createUncurried(stateCreator) + : createUncurried; +}) as typeof zustand.create; + +const createStoreUncurried = (stateCreator: zustand.StateCreator) => { + const store = actualCreateStore(stateCreator); + const initialState = store.getInitialState(); + storeResetFns.add(() => { + store.setState(initialState, true); + }); + return store; +}; + +// when creating a store, we get its initial state, create a reset function and add it in the set +export const createStore = ((stateCreator: zustand.StateCreator) => { + // to support curried version of createStore + return typeof stateCreator === 'function' ? + createStoreUncurried(stateCreator) + : createStoreUncurried; +}) as typeof zustand.createStore; + +// reset all stores after each test run +afterEach(() => { + act(() => { + storeResetFns.forEach((resetFn) => { + resetFn(); + }); + }); +}); diff --git a/apps/web/src/__test__/api/health.spec.ts b/apps/web/src/__test__/api/health.spec.ts new file mode 100644 index 000000000..db7538fc8 --- /dev/null +++ b/apps/web/src/__test__/api/health.spec.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { faker } from '@faker-js/faker'; +import { createMocks } from 'node-mocks-http'; + +import handler from '@/pages/api/health'; + +describe('Health API', () => { + test('should handle request_token action', async () => { + const { req, res } = createMocks({ + method: 'GET', + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(200); + }); + test('method not allowed', async () => { + const { req, res } = createMocks({ + method: faker.helpers.arrayElement(['POST', 'PUT', 'PATCH', 'DELETE']), + }); + + await handler(req, res); + + expect(res._getStatusCode()).toEqual(405); + }); +}); diff --git a/apps/web/src/__test__/api/jwt.spec.ts b/apps/web/src/__test__/api/jwt.spec.ts new file mode 100644 index 000000000..ee8094bd9 --- /dev/null +++ b/apps/web/src/__test__/api/jwt.spec.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { faker } from '@faker-js/faker'; +import * as IronSession from 'iron-session'; +import { createMocks } from 'node-mocks-http'; + +import type { Jwt } from '@/shared'; + +import handler from '@/pages/api/jwt'; + +jest.mock('iron-session'); + +describe('JWT API', () => { + test('jwt null', async () => { + const jwt = null; + + const mockSave = jest.fn(); + jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: jest.fn(), + save: mockSave, + updateConfig: jest.fn(), + jwt, + })); + + const { req, res } = createMocks({ + method: 'GET', + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(200); + expect(res._getData().jwt).toBeNull(); + }); + test('jwt not null', async () => { + const jwt = { + accessToken: faker.string.nanoid(), + refreshToken: faker.string.nanoid(), + }; + + const mockSave = jest.fn(); + jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: jest.fn(), + save: mockSave, + updateConfig: jest.fn(), + jwt, + })); + + const { req, res } = createMocks({ + method: 'GET', + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(200); + expect(res._getData().jwt).toEqual(jwt); + }); + test('method not allowed', async () => { + const { req, res } = createMocks({ + method: faker.helpers.arrayElement(['POST', 'PUT', 'PATCH', 'DELETE']), + }); + + await handler(req, res); + + expect(res._getStatusCode()).toEqual(405); + }); +}); diff --git a/apps/web/src/__test__/api/login.spec.ts b/apps/web/src/__test__/api/login.spec.ts new file mode 100644 index 000000000..71924b72d --- /dev/null +++ b/apps/web/src/__test__/api/login.spec.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { faker } from '@faker-js/faker'; +import * as IronSession from 'iron-session'; +import { createMocks } from 'node-mocks-http'; + +import { simpleMockHttp } from '@/msw'; +import handler from '@/pages/api/login'; + +jest.mock('iron-session'); + +describe('Login API', () => { + test('success', async () => { + const jwt = { + accessToken: faker.string.nanoid(), + refreshToken: faker.string.nanoid(), + }; + simpleMockHttp({ + method: 'post', + path: '/api/admin/auth/signIn/email', + status: 201, + data: jwt, + }); + const mockSave = jest.fn(); + jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: jest.fn(), + save: mockSave, + updateConfig: jest.fn(), + jwt: jwt, + })); + + const body = { + email: faker.internet.email(), + password: faker.internet.password(), + }; + const { req, res } = createMocks({ + method: 'POST', + body, + }); + + await handler(req, res); + + expect(res._getData()).toEqual(jwt); + expect(mockSave).toHaveBeenCalled(); + }); + test('error', async () => { + const data = { message: 'error' }; + simpleMockHttp({ + method: 'post', + path: '/api/admin/auth/signIn/email', + status: 500, + data: data, + }); + + const mockSave = jest.fn(); + jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: jest.fn(), + save: mockSave, + updateConfig: jest.fn(), + })); + + const body = { + email: faker.internet.email(), + password: faker.internet.password(), + }; + + const { req, res } = createMocks({ + method: 'POST', + body, + }); + + await handler(req, res); + + expect(res._getData()).toEqual(data); + expect(mockSave).not.toHaveBeenCalled(); + }); + test('invalid input', async () => { + const body = {}; + + const { req, res } = createMocks({ + method: 'POST', + body, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toEqual(400); + }); + + test('method not allowed', async () => { + const { req, res } = createMocks({ + method: faker.helpers.arrayElement(['GET', 'PUT', 'PATCH', 'DELETE']), + }); + + await handler(req, res); + + expect(res._getStatusCode()).toEqual(405); + }); +}); diff --git a/apps/web/src/__test__/api/logout.spec.ts b/apps/web/src/__test__/api/logout.spec.ts new file mode 100644 index 000000000..33f55e055 --- /dev/null +++ b/apps/web/src/__test__/api/logout.spec.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { faker } from '@faker-js/faker'; +import * as IronSession from 'iron-session'; +import { createMocks } from 'node-mocks-http'; + +import handler from '@/pages/api/logout'; + +jest.mock('iron-session'); +const mockDestory = jest.fn(); +const mockSave = jest.fn(); + +jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: mockDestory, + save: mockSave, + updateConfig: jest.fn(), +})); + +describe('Logout API', () => { + test('logout', async () => { + const { req, res } = createMocks({ + method: 'GET', + }); + + await handler(req, res); + + expect(mockDestory).toHaveBeenCalled(); + expect(mockSave).toHaveBeenCalled(); + expect(res._getData()).toEqual({ ok: true }); + }); + test('method not allowed', async () => { + const { req, res } = createMocks({ + method: faker.helpers.arrayElement(['POST', 'PUT', 'PATCH', 'DELETE']), + }); + + await handler(req, res); + + expect(res._getStatusCode()).toEqual(405); + }); +}); diff --git a/apps/web/src/__test__/api/oauth.spec.ts b/apps/web/src/__test__/api/oauth.spec.ts new file mode 100644 index 000000000..05349d643 --- /dev/null +++ b/apps/web/src/__test__/api/oauth.spec.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { faker } from '@faker-js/faker'; +import * as IronSession from 'iron-session'; +import { createMocks } from 'node-mocks-http'; + +import { simpleMockHttp } from '@/msw'; +import handler from '@/pages/api/oauth'; + +jest.mock('iron-session'); + +describe('OAuth API', () => { + test('success', async () => { + const jwt = { + accessToken: faker.string.nanoid(), + refreshToken: faker.string.nanoid(), + }; + simpleMockHttp({ + method: 'get', + path: '/api/admin/auth/signIn/oauth', + status: 200, + data: jwt, + }); + + const mockSave = jest.fn(); + jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: jest.fn(), + save: mockSave, + updateConfig: jest.fn(), + })); + + const { req, res } = createMocks({ + method: 'POST', + body: { code: faker.string.nanoid() }, + }); + + await handler(req, res); + + expect(mockSave).toHaveBeenCalled(); + expect(res._getData()).toEqual(jwt); + }); + + test('error', async () => { + const jwt = { + accessToken: faker.string.nanoid(), + refreshToken: faker.string.nanoid(), + }; + + simpleMockHttp({ + method: 'get', + path: '/api/admin/auth/signIn/oauth', + status: 500, + }); + + const mockSave = jest.fn(); + jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: jest.fn(), + save: mockSave, + updateConfig: jest.fn(), + })); + + const { req, res } = createMocks({ + method: 'POST', + body: { code: faker.string.nanoid() }, + }); + + await handler(req, res); + + expect(mockSave).not.toHaveBeenCalled(); + expect(res._getData()).not.toEqual(jwt); + }); + + test('invalid input', async () => { + const body = {}; + + const { req, res } = createMocks({ + method: 'POST', + body, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toEqual(400); + }); + + test('method not allowed', async () => { + simpleMockHttp({ method: 'get', path: '/api/admin/auth/signIn/oauth' }); + + const { req, res } = createMocks({ + method: faker.helpers.arrayElement(['GET', 'PUT', 'PATCH', 'DELETE']), + }); + + await handler(req, res); + + expect(res._getStatusCode()).toEqual(405); + }); +}); diff --git a/apps/web/src/__test__/api/refresh-jwt.spec.ts b/apps/web/src/__test__/api/refresh-jwt.spec.ts new file mode 100644 index 000000000..55b3f6b44 --- /dev/null +++ b/apps/web/src/__test__/api/refresh-jwt.spec.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { faker } from '@faker-js/faker'; +import * as IronSession from 'iron-session'; +import { createMocks } from 'node-mocks-http'; + +import { simpleMockHttp } from '@/msw'; +import handler from '@/pages/api/refresh-jwt'; + +jest.mock('iron-session'); + +describe('Refresh Jwt API', () => { + test('success', async () => { + const newJwt = { + accessToken: faker.string.nanoid(), + refreshToken: faker.string.nanoid(), + }; + simpleMockHttp({ + method: 'get', + path: '/api/admin/auth/refresh', + status: 200, + data: newJwt, + }); + const originalJwt = { + accessToken: faker.string.nanoid(), + refreshToken: faker.string.nanoid(), + }; + + const mockSave = jest.fn(); + jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: jest.fn(), + save: mockSave, + updateConfig: jest.fn(), + jwt: originalJwt, + })); + + const { req, res } = createMocks({ + method: 'GET', + }); + + await handler(req, res); + + expect(mockSave).toHaveBeenCalled(); + expect(res._getData()).toEqual(newJwt); + }); + + test('error', async () => { + const newJwt = { + accessToken: faker.string.nanoid(), + refreshToken: faker.string.nanoid(), + }; + simpleMockHttp({ + method: 'get', + path: '/api/admin/auth/refresh', + status: 200, + data: newJwt, + }); + + const mockSave = jest.fn(); + jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: jest.fn(), + save: mockSave, + updateConfig: jest.fn(), + })); + + const { req, res } = createMocks({ + method: 'GET', + }); + + await handler(req, res); + + expect(mockSave).not.toHaveBeenCalled(); + expect(res._getStatusCode()).toEqual(400); + }); + test('error', async () => { + const newJwt = { + accessToken: faker.string.nanoid(), + refreshToken: faker.string.nanoid(), + }; + simpleMockHttp({ + method: 'get', + path: '/api/admin/auth/refresh', + status: 500, + data: newJwt, + }); + + const mockSave = jest.fn(); + jest.spyOn(IronSession, 'getIronSession').mockImplementation(async () => ({ + destroy: jest.fn(), + save: mockSave, + updateConfig: jest.fn(), + })); + const { req, res } = createMocks({ + method: 'GET', + }); + + await handler(req, res); + + expect(mockSave).not.toHaveBeenCalled(); + expect(res._getStatusCode()).not.toEqual(200); + }); + + test('method not allowed', async () => { + const { req, res } = createMocks({ + method: faker.helpers.arrayElement(['POST', 'PUT', 'PATCH', 'DELETE']), + }); + + await handler(req, res); + + expect(res._getStatusCode()).toEqual(405); + }); +}); diff --git a/apps/web/src/__test__/auth/__snapshots__/reset-password.spec.tsx.snap b/apps/web/src/__test__/auth/__snapshots__/reset-password.spec.tsx.snap new file mode 100644 index 000000000..265454dd2 --- /dev/null +++ b/apps/web/src/__test__/auth/__snapshots__/reset-password.spec.tsx.snap @@ -0,0 +1,191 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Reset Password Page snapshot test 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ logo + +
+

+ auth.reset-password.title +

+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+ +
+
+
+ © ABC Studio All rights reserved +
+
+
+
+`; diff --git a/apps/web/src/__test__/auth/__snapshots__/sign-in.spec.tsx.snap b/apps/web/src/__test__/auth/__snapshots__/sign-in.spec.tsx.snap new file mode 100644 index 000000000..bfcda9385 --- /dev/null +++ b/apps/web/src/__test__/auth/__snapshots__/sign-in.spec.tsx.snap @@ -0,0 +1,815 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sign In Page should render all allow 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ + S_:GHQo.!/ + +
+
+
+
+
+ +
+ +
+
+
+
+
+ logo +
+ S_:GHQo.!/ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ + +
+ +
+
+ + OR + +
+
+ +
+ +
+
+
+ © ABC Studio All rights reserved +
+
+
+
+`; + +exports[`Sign In Page should render when isPrivate is false 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ + S_:GHQo.!/ + +
+
+
+
+
+ +
+ +
+
+
+
+
+ logo +
+ S_:GHQo.!/ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+ + OR + +
+
+ +
+
+
+
+ © ABC Studio All rights reserved +
+
+
+
+`; + +exports[`Sign In Page should render when useEmail is false 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ + S_:GHQo.!/ + +
+
+
+
+
+ +
+ +
+
+
+
+
+ logo +
+ S_:GHQo.!/ +
+
+
+ +
+
+
+
+ © ABC Studio All rights reserved +
+
+
+
+`; + +exports[`Sign In Page should render when useOAuth is false 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ + S_:GHQo.!/ + +
+
+
+
+
+ +
+ +
+
+
+
+
+ logo +
+ S_:GHQo.!/ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ + +
+ + +
+
+ © ABC Studio All rights reserved +
+
+
+
+`; diff --git a/apps/web/src/__test__/auth/__snapshots__/sign-up.spec.tsx.snap b/apps/web/src/__test__/auth/__snapshots__/sign-up.spec.tsx.snap new file mode 100644 index 000000000..5c5cfdeb4 --- /dev/null +++ b/apps/web/src/__test__/auth/__snapshots__/sign-up.spec.tsx.snap @@ -0,0 +1,260 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sign Up Page snapshot test 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ logo + +
+

+ auth.sign-up.title +

+
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ + +
+ +
+
+
+ © ABC Studio All rights reserved +
+
+
+
+`; diff --git a/apps/web/src/__test__/auth/oauth-callback.spec.tsx b/apps/web/src/__test__/auth/oauth-callback.spec.tsx new file mode 100644 index 000000000..2a33c5f28 --- /dev/null +++ b/apps/web/src/__test__/auth/oauth-callback.spec.tsx @@ -0,0 +1,49 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import userEvent from '@testing-library/user-event'; +import mockRouter from 'next-router-mock'; + +import { Path } from '@/shared'; +import * as signInWithOAuth from '@/features/auth/sign-in-with-oauth'; + +import OAuthCallbackPage from '@/pages/auth/oauth-callback'; +import { render, screen, waitFor } from '@/test-utils'; + +jest.mock('@/features/auth/sign-in-with-oauth'); + +describe('OAuthCallback Page', () => { + test('status loading', () => { + jest.spyOn(signInWithOAuth, 'useOAuthCallback').mockReturnValue({ + status: 'loading', + }); + render(); + expect(screen.queryByText('Loading...')).toBeInTheDocument(); + }); + test('status error', async () => { + jest.spyOn(signInWithOAuth, 'useOAuthCallback').mockReturnValue({ + status: 'error', + }); + render(); + + expect(screen.queryByText('Error!!!')).toBeInTheDocument(); + + await waitFor(async () => { + await userEvent.click(screen.getByRole('button')); + }); + expect(mockRouter).toMatchObject({ pathname: Path.SIGN_IN }); + }); +}); diff --git a/apps/web/src/__test__/auth/reset-password.spec.tsx b/apps/web/src/__test__/auth/reset-password.spec.tsx new file mode 100644 index 000000000..f58bc38b5 --- /dev/null +++ b/apps/web/src/__test__/auth/reset-password.spec.tsx @@ -0,0 +1,29 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import ResetPasswordPage from '@/pages/auth/reset-password'; +import { render } from '@/test-utils'; + +describe('Reset Password Page', () => { + test('snapshot test', () => { + const resetPasswordPage = ResetPasswordPage.getLayout?.( + , + ); + + const { container } = render(<>{resetPasswordPage}); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/web/src/__test__/auth/sign-in.spec.tsx b/apps/web/src/__test__/auth/sign-in.spec.tsx new file mode 100644 index 000000000..7e2e633cd --- /dev/null +++ b/apps/web/src/__test__/auth/sign-in.spec.tsx @@ -0,0 +1,103 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { faker } from '@faker-js/faker'; + +import type { Tenant } from '@/entities/tenant'; +import { useTenantStore } from '@/entities/tenant'; + +import SignInPage from '@/pages/auth/sign-in'; +import { render, screen } from '@/test-utils'; + +const DEFAULT_TENANT: Tenant = { + id: 1, + siteName: faker.string.sample(), + description: null, + allowDomains: [], + isPrivate: false, + useEmail: true, + isRestrictDomain: false, + oauthConfig: null, + useEmailVerification: true, + useOAuth: true, +}; + +describe('Sign In Page', () => { + test('should render all allow', () => { + useTenantStore.setState({ tenant: { ...DEFAULT_TENANT } }); + const signInPage = SignInPage.getLayout?.(); + + const { container } = render(<>{signInPage}); + expect(container).toMatchSnapshot(); + + const signInBtn = screen.queryByRole('button', { name: 'button.sign-in' }); + expect(signInBtn).toBeInTheDocument(); + + const signUpBtn = screen.queryByRole('button', { name: 'button.sign-up' }); + expect(signUpBtn).toBeInTheDocument(); + + const oauthBtn = screen.queryByRole('button', { name: /OAuth2.0/ }); + expect(oauthBtn).toBeInTheDocument(); + }); + test('should render when isPrivate is false', () => { + useTenantStore.setState({ tenant: { ...DEFAULT_TENANT, isPrivate: true } }); + const signInPage = SignInPage.getLayout?.(); + + const { container } = render(<>{signInPage}); + expect(container).toMatchSnapshot(); + + const signInBtn = screen.queryByRole('button', { name: 'button.sign-in' }); + expect(signInBtn).toBeInTheDocument(); + + const signUpBtn = screen.queryByRole('button', { name: 'button.sign-up' }); + expect(signUpBtn).not.toBeInTheDocument(); + + const oauthBtn = screen.queryByRole('button', { name: /OAuth2.0/ }); + expect(oauthBtn).toBeInTheDocument(); + }); + test('should render when useOAuth is false', () => { + useTenantStore.setState({ tenant: { ...DEFAULT_TENANT, useOAuth: false } }); + const signInPage = SignInPage.getLayout?.(); + + const { container } = render(<>{signInPage}); + expect(container).toMatchSnapshot(); + + const signInBtn = screen.queryByRole('button', { name: 'button.sign-in' }); + expect(signInBtn).toBeInTheDocument(); + + const signUpBtn = screen.queryByRole('button', { name: 'button.sign-up' }); + expect(signUpBtn).toBeInTheDocument(); + + const oauthBtn = screen.queryByRole('button', { name: /OAuth2.0/ }); + expect(oauthBtn).not.toBeInTheDocument(); + }); + test('should render when useEmail is false', () => { + useTenantStore.setState({ tenant: { ...DEFAULT_TENANT, useEmail: false } }); + const signInPage = SignInPage.getLayout?.(); + + const { container } = render(<>{signInPage}); + expect(container).toMatchSnapshot(); + + const signInBtn = screen.queryByRole('button', { name: 'button.sign-in' }); + expect(signInBtn).not.toBeInTheDocument(); + + const signUpBtn = screen.queryByRole('button', { name: 'button.sign-up' }); + expect(signUpBtn).not.toBeInTheDocument(); + + const oauthBtn = screen.queryByRole('button', { name: /OAuth2.0/ }); + expect(oauthBtn).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/__test__/auth/sign-up.spec.tsx b/apps/web/src/__test__/auth/sign-up.spec.tsx new file mode 100644 index 000000000..b8e4a31de --- /dev/null +++ b/apps/web/src/__test__/auth/sign-up.spec.tsx @@ -0,0 +1,27 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import SignUpPage from '@/pages/auth/sign-up'; +import { render } from '@/test-utils'; + +describe('Sign Up Page', () => { + test('snapshot test', () => { + const signUpPage = SignUpPage.getLayout?.(); + + const { container } = render(<>{signUpPage}); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/web/src/__test__/index.spec.tsx b/apps/web/src/__test__/index.spec.tsx new file mode 100644 index 000000000..c4ac47234 --- /dev/null +++ b/apps/web/src/__test__/index.spec.tsx @@ -0,0 +1,29 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { GetServerSidePropsContext } from 'next'; + +import { Path } from '@/shared'; + +import IndexPage, { getServerSideProps } from '@/pages'; +import { render } from '@/test-utils'; + +describe('Index Page', () => { + test('should render without crashing', async () => { + render(); + const value = await getServerSideProps({} as GetServerSidePropsContext); + expect(value).toMatchObject({ redirect: { destination: Path.SIGN_IN } }); + }); +}); diff --git a/apps/web/src/__test__/link/__snapshots__/reset-password.spec.tsx.snap b/apps/web/src/__test__/link/__snapshots__/reset-password.spec.tsx.snap new file mode 100644 index 000000000..1d77d5371 --- /dev/null +++ b/apps/web/src/__test__/link/__snapshots__/reset-password.spec.tsx.snap @@ -0,0 +1,248 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Reset Password Page snapshot test 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ logo + +
+

+ link.reset-password.title +

+
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ + +
+ +
+
+
+ © ABC Studio All rights reserved +
+
+
+
+`; diff --git a/apps/web/src/__test__/link/__snapshots__/user-invitation.spec.tsx.snap b/apps/web/src/__test__/link/__snapshots__/user-invitation.spec.tsx.snap new file mode 100644 index 000000000..658f5ba3d --- /dev/null +++ b/apps/web/src/__test__/link/__snapshots__/user-invitation.spec.tsx.snap @@ -0,0 +1,243 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Reset Password Page snapshot test 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+ logo + +
+

+ link.user-invitation.title +

+
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ © ABC Studio All rights reserved +
+
+
+
+`; diff --git a/apps/web/src/__test__/link/reset-password.spec.tsx b/apps/web/src/__test__/link/reset-password.spec.tsx new file mode 100644 index 000000000..d499e5b40 --- /dev/null +++ b/apps/web/src/__test__/link/reset-password.spec.tsx @@ -0,0 +1,29 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import ResetPasswordPage from '@/pages/link/reset-password'; +import { render } from '@/test-utils'; + +describe('Reset Password Page', () => { + test('snapshot test', () => { + const resetPasswordPage = ResetPasswordPage.getLayout?.( + , + ); + + const { container } = render(<>{resetPasswordPage}); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/web/src/__test__/link/user-invitation.spec.tsx b/apps/web/src/__test__/link/user-invitation.spec.tsx new file mode 100644 index 000000000..3ad86f80c --- /dev/null +++ b/apps/web/src/__test__/link/user-invitation.spec.tsx @@ -0,0 +1,29 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import UserInvitationPage from '@/pages/link/user-invitation'; +import { render } from '@/test-utils'; + +describe('Reset Password Page', () => { + test('snapshot test', () => { + const userInvitationPage = UserInvitationPage.getLayout?.( + , + ); + + const { container } = render(<>{userInvitationPage}); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/web/src/__test__/main/__snapshots__/index.spec.tsx.snap b/apps/web/src/__test__/main/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..c2c5d605e --- /dev/null +++ b/apps/web/src/__test__/main/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,200 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MainIndexPage should render without crashing 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+

+ Tenant +

+
+
+
+ +
+
+

+

+ - +

+
+
+
+
+

+ main.index.total-project +

+

+ 0 +

+
+
+

+ main.index.total-feedback +

+

+ 0 +

+
+
+
+
+
+

+ Project +

+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/apps/web/src/__test__/main/__snapshots__/profile.spec.tsx.snap b/apps/web/src/__test__/main/__snapshots__/profile.spec.tsx.snap new file mode 100644 index 000000000..636f69826 --- /dev/null +++ b/apps/web/src/__test__/main/__snapshots__/profile.spec.tsx.snap @@ -0,0 +1,164 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MainIndexPage should render without crashing 1`] = ` +
+
+
+
+ + logo + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+

+ main.profile.title + +

+
+
+
    +
  • + + + main.profile.profile-info + +
  • +
  • + + + main.profile.change-password + +
  • +
+
+
+
+
+
+
+
+
+
+`; diff --git a/apps/web/src/__test__/main/index.spec.tsx b/apps/web/src/__test__/main/index.spec.tsx new file mode 100644 index 000000000..66b036cb2 --- /dev/null +++ b/apps/web/src/__test__/main/index.spec.tsx @@ -0,0 +1,59 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; + +import { MOCK_PROJECTS } from '@/entities/project/__mocks__/project.mock-data'; +import type { Tenant } from '@/entities/tenant'; +import { useTenantStore } from '@/entities/tenant'; + +import { simpleMockHttp } from '@/msw'; +import MainIndexPage from '@/pages/main'; +import { render } from '@/test-utils'; + +describe('MainIndexPage', () => { + test('should render without crashing', () => { + useTenantStore.setState({ tenant: {} as Tenant }); + simpleMockHttp({ + method: 'get', + path: '/api/admin/projects', + status: 200, + data: { + meta: { + itemCount: 1, + totalItems: 1, + itemsPerPage: 1, + totalPages: 1, + currentPage: 1, + }, + items: MOCK_PROJECTS, + }, + }); + for (const project of MOCK_PROJECTS) { + simpleMockHttp({ + method: 'get', + path: '/api/admin/projects/{projectId}/feedback-count', + params: { projectId: project.id }, + status: 200, + data: { + total: faker.number.int(), + }, + }); + } + const page = MainIndexPage.getLayout?.(); + const { container } = render(<>{page}); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/web/src/__test__/main/profile.spec.tsx b/apps/web/src/__test__/main/profile.spec.tsx new file mode 100644 index 000000000..9944a97e9 --- /dev/null +++ b/apps/web/src/__test__/main/profile.spec.tsx @@ -0,0 +1,26 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import ProfilePage from '@/pages/main/profile'; +import { render } from '@/test-utils'; + +describe('MainIndexPage', () => { + test('should render without crashing', () => { + const page = ProfilePage.getLayout?.(); + const { container } = render(<>{page}); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/web/src/__test__/tenant/__snapshots__/create.spec.tsx.snap b/apps/web/src/__test__/tenant/__snapshots__/create.spec.tsx.snap new file mode 100644 index 000000000..244a0ec01 --- /dev/null +++ b/apps/web/src/__test__/tenant/__snapshots__/create.spec.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create Tenant Page match snapshot 1`] = ` +
+
+

+ tenant.create.title +

+ + +
+
+
+`; diff --git a/apps/web/src/__test__/tenant/create.spec.tsx b/apps/web/src/__test__/tenant/create.spec.tsx new file mode 100644 index 000000000..d013f636b --- /dev/null +++ b/apps/web/src/__test__/tenant/create.spec.tsx @@ -0,0 +1,40 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import mockRouter from 'next-router-mock'; + +import { Path } from '@/shared'; +import type { Tenant } from '@/entities/tenant'; +import { useTenantStore } from '@/entities/tenant'; + +import CreateTenantPage from '@/pages/tenant/create'; +import { render, waitFor } from '@/test-utils'; + +describe('Create Tenant Page', () => { + test('match snapshot', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + test('should route sign-in page when tenant is defined', async () => { + useTenantStore.setState({ tenant: {} as Tenant }); + const createTenantPage = CreateTenantPage.getLayout?.(); + + render(<>{createTenantPage}); + await waitFor(() => + expect(mockRouter).toMatchObject({ pathname: Path.SIGN_IN }), + ); + }); +}); diff --git a/apps/web/src/components/cards/ChannelCard/ChannelCard.tsx b/apps/web/src/components/cards/ChannelCard/ChannelCard.tsx deleted file mode 100644 index 65eda05a5..000000000 --- a/apps/web/src/components/cards/ChannelCard/ChannelCard.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { useMemo } from 'react'; - -import type { IconNameType } from '@ufb/ui'; -import { Icon } from '@ufb/ui'; - -import type { ColorType } from '@/types/color.type'; - -interface IProps extends React.PropsWithChildren { - value?: React.ReactNode; - rightChildren?: React.ReactNode; - color: ColorType; - iconName: IconNameType; - name: string; -} - -const ChannelCard: React.FC = (props) => { - const { value, rightChildren, color, iconName, name } = props; - - const { bg, icon } = useMemo(() => { - switch (color) { - case 'blue': - return { bg: 'bg-blue-quaternary', icon: 'text-blue-primary' }; - case 'green': - return { bg: 'bg-green-quaternary', icon: 'text-green-primary' }; - default: - return { bg: '', icon: '' }; - } - }, [color]); - - return ( -
-
- -
-
-

{name}

-

{value}

-
- {rightChildren} -
- ); -}; - -export default ChannelCard; diff --git a/apps/web/src/components/cards/TenantProjectCard/TenantProjectCard.tsx b/apps/web/src/components/cards/TenantProjectCard/TenantProjectCard.tsx deleted file mode 100644 index 9c1cc1afb..000000000 --- a/apps/web/src/components/cards/TenantProjectCard/TenantProjectCard.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { useTranslation } from 'react-i18next'; - -import { Icon } from '@ufb/ui'; - -import { getDescriptionStr } from '@/utils/description-string'; - -interface IProps { - name: string; - description?: string | null; - type: 'tenant' | 'project'; - total?: number; - feedbackCount?: number; - onClick?: () => void; -} - -const TenantProjectCard: React.FC = ({ - name, - type, - description, - feedbackCount, - total, - onClick, -}) => { - const { t } = useTranslation(); - return ( -
-
-
- -
-
-

{name}

-

- {getDescriptionStr(description)} -

-
-
-
-
-

- {type === 'tenant' ? - t('main.index.total-project') - : t('main.index.total-channel')} -

-

- {total?.toLocaleString()} -

-
-
-

- {t('main.index.total-feedback')} -

-

- {feedbackCount?.toLocaleString()} -

-
-
-
- ); -}; - -export default TenantProjectCard; diff --git a/apps/web/src/components/charts/SimpleLineChart.tsx b/apps/web/src/components/charts/SimpleLineChart.tsx deleted file mode 100644 index 8d3ebf4e7..000000000 --- a/apps/web/src/components/charts/SimpleLineChart.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import ChartContainer from './ChartContainer'; -import LineChart from './LineChart'; - -interface IProps { - title: string; - description?: string; - height?: number; - data: any[]; - dataKeys: { color: string; name: string }[]; - showLegend?: boolean; - filterContent?: React.ReactNode; - noLabel?: boolean; -} - -const SimpleLineChart: React.FC = (props) => { - const { - title, - description, - height, - data, - dataKeys, - showLegend, - filterContent, - noLabel, - } = props; - - return ( - - - - ); -}; - -export default SimpleLineChart; diff --git a/apps/web/src/components/charts/index.ts b/apps/web/src/components/charts/index.ts deleted file mode 100644 index 461dba7e0..000000000 --- a/apps/web/src/components/charts/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { default as SimpleBarChart } from './SimpleBarChart'; -export { default as SimpleLineChart } from './SimpleLineChart'; diff --git a/apps/web/src/components/etc/CheckedTableHead/index.ts b/apps/web/src/components/etc/CheckedTableHead/index.ts deleted file mode 100644 index a057bfc7a..000000000 --- a/apps/web/src/components/etc/CheckedTableHead/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { default } from './CheckedTableHead'; diff --git a/apps/web/src/components/etc/DashboardTable/index.ts b/apps/web/src/components/etc/DashboardTable/index.ts deleted file mode 100644 index 5b4b7db26..000000000 --- a/apps/web/src/components/etc/DashboardTable/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { default } from './DashboardTable'; diff --git a/apps/web/src/components/etc/DescriptionTooltip/index.ts b/apps/web/src/components/etc/DescriptionTooltip/index.ts deleted file mode 100644 index 80ac58193..000000000 --- a/apps/web/src/components/etc/DescriptionTooltip/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { default } from './DescriptionTooltip'; diff --git a/apps/web/src/components/etc/ExpandableText/index.ts b/apps/web/src/components/etc/ExpandableText/index.ts deleted file mode 100644 index 72b738d01..000000000 --- a/apps/web/src/components/etc/ExpandableText/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { default } from './ExpandableText'; diff --git a/apps/web/src/components/etc/HelpCardDocs/index.ts b/apps/web/src/components/etc/HelpCardDocs/index.ts deleted file mode 100644 index 156cd7a98..000000000 --- a/apps/web/src/components/etc/HelpCardDocs/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { default } from './HelpCardDocs'; diff --git a/apps/web/src/components/etc/IssueCircle/IssueCircle.tsx b/apps/web/src/components/etc/IssueCircle/IssueCircle.tsx deleted file mode 100644 index d7b7b7dd6..000000000 --- a/apps/web/src/components/etc/IssueCircle/IssueCircle.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { ISSUES } from '@/constants/issues'; - -interface IProps { - issueKey?: string; -} - -const IssueCircle: React.FC = ({ issueKey }) => { - const { t } = useTranslation(); - - const circleColor = useMemo(() => { - const issue = ISSUES(t).find((v) => v.key === issueKey); - - switch (issue?.color) { - case 'red': - return 'bg-red-primary'; - case 'blue': - return 'bg-blue-primary'; - case 'yellow': - return 'bg-yellow-primary'; - case 'green': - return 'bg-green-primary'; - case 'purple': - return 'bg-purple-primary'; - default: - return ''; - } - }, [t, issueKey]); - return ( -
- ); -}; - -export default IssueCircle; diff --git a/apps/web/src/components/etc/IssueCircle/index.ts b/apps/web/src/components/etc/IssueCircle/index.ts deleted file mode 100644 index a5ec5c39b..000000000 --- a/apps/web/src/components/etc/IssueCircle/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { default } from './IssueCircle'; diff --git a/apps/web/src/components/etc/SelectBox/index.ts b/apps/web/src/components/etc/SelectBox/index.ts deleted file mode 100644 index fc692c0a9..000000000 --- a/apps/web/src/components/etc/SelectBox/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { default } from './SelectBox'; -export { default as SelectBoxCreatable } from './SelectBoxCreatable'; -export type { ISelectBoxProps } from './SelectBox'; diff --git a/apps/web/src/components/etc/SelectBoxWithIcon/SelectBoxWithIcon.tsx b/apps/web/src/components/etc/SelectBoxWithIcon/SelectBoxWithIcon.tsx deleted file mode 100644 index 0b5534987..000000000 --- a/apps/web/src/components/etc/SelectBoxWithIcon/SelectBoxWithIcon.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { components } from 'react-select'; - -import type { ISelectBoxProps } from '../SelectBox/SelectBox'; -import SelectBox from '../SelectBox/SelectBox'; - -interface IProps - extends ISelectBoxProps { - SingleValue?: { - left?: (data: Option) => React.ReactNode; - right?: (data: Option) => React.ReactNode; - }; - Option?: { - left?: (data: Option) => React.ReactNode; - right?: (data: Option) => React.ReactNode; - }; -} - -function SelectBoxWithIcon
+
+
+`; + +exports[`UserBox snapshot when open 1`] = ` +
+
+ + +
+
+
+`; diff --git a/apps/web/src/entities/user/ui/index.ts b/apps/web/src/entities/user/ui/index.ts new file mode 100644 index 000000000..c7bf12e3e --- /dev/null +++ b/apps/web/src/entities/user/ui/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as UserBox } from './user-box.ui'; +export { default as UserManagementTable } from './user-management-table.ui'; +export { default as InviteUserPopover } from './invite-user-popover'; diff --git a/apps/web/src/containers/setting-menu/UserSetting/UserInvitationDialog.tsx b/apps/web/src/entities/user/ui/invite-user-popover.tsx similarity index 82% rename from apps/web/src/containers/setting-menu/UserSetting/UserInvitationDialog.tsx rename to apps/web/src/entities/user/ui/invite-user-popover.tsx index 1d171b7ab..0c71f09a8 100644 --- a/apps/web/src/containers/setting-menu/UserSetting/UserInvitationDialog.tsx +++ b/apps/web/src/entities/user/ui/invite-user-popover.tsx @@ -29,9 +29,8 @@ import { toast, } from '@ufb/ui'; -import { SelectBox } from '@/components'; -import type { UserTypeEnum } from '@/contexts/user.context'; -import { useOAIMutation, useOAIQuery, useProjects } from '@/hooks'; +import { SelectBox, useOAIMutation, useOAIQuery } from '@/shared'; +import type { UserTypeEnum } from '@/entities/user'; interface IProps {} @@ -41,22 +40,24 @@ interface IForm { projectId?: number; roleId?: number; } + const scheme: Zod.ZodType = z.object({ email: z.string().email(), type: z.union([z.literal('SUPER'), z.literal('GENERAL')]), projectId: z.number().optional(), roleId: z.number().optional(), }); + const defaultValues: IForm = { email: '', type: 'GENERAL', }; -const UserInvitationDialog: React.FC = () => { +const InviteUserPopover: React.FC = () => { const { t } = useTranslation(); const { register, watch, setValue, reset, handleSubmit, formState } = useForm({ resolver: zodResolver(scheme), defaultValues }); - + const { projectId, type, roleId } = watch(); const [open, setOpen] = useState(false); useEffect(() => { @@ -64,21 +65,22 @@ const UserInvitationDialog: React.FC = () => { }, [open]); useEffect(() => { - if (watch('type') !== 'SUPER') return; + if (type !== 'SUPER') return; reset({ projectId: undefined, roleId: undefined }, { keepValues: true }); - }, [watch('type')]); + }, [type]); useEffect(() => { reset({ roleId: undefined }, { keepValues: true }); - }, [watch('projectId')]); + }, [projectId]); - const { data: projectData } = useProjects(); + const { data: projectData } = useOAIQuery({ path: '/api/admin/projects' }); const { data: roleData } = useOAIQuery({ path: '/api/admin/projects/{projectId}/roles', - variables: { projectId: watch('projectId')! }, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + variables: { projectId: projectId! }, queryOptions: { - enabled: !!watch('projectId') && watch('type') === 'GENERAL', + enabled: !!projectId && type === 'GENERAL', }, }); @@ -86,12 +88,12 @@ const UserInvitationDialog: React.FC = () => { method: 'post', path: '/api/admin/users/invite', queryOptions: { - async onSuccess() { + onSuccess() { toast.positive({ title: t('toast.invite'), iconName: 'MailFill' }); setOpen(false); }, onError(error) { - toast.negative({ title: error?.message ?? 'Error' }); + toast.negative({ title: error.message }); }, }, }); @@ -140,14 +142,10 @@ const UserInvitationDialog: React.FC = () => { { label: 'SUPER', value: 'SUPER' }, { label: 'GENERAL', value: 'GENERAL' }, ]} - defaultValue={ - watch('type') ? - { label: watch('type'), value: watch('type') } - : undefined - } + defaultValue={{ label: type, value: type }} required /> - {watch('type') === 'GENERAL' && ( + {type === 'GENERAL' && ( <> = () => { getOptionLabel={(option) => option.name} isClearable /> - {watch('projectId') && ( + {projectId && ( setValue('roleId', v?.id)} value={ - roleData?.roles.find( - (role) => role.id === watch('roleId'), - ) ?? null + roleData?.roles.find((role) => role.id === roleId) ?? null } getOptionValue={(option) => String(option.id)} getOptionLabel={(option) => option.name} @@ -180,4 +176,4 @@ const UserInvitationDialog: React.FC = () => { ); }; -export default UserInvitationDialog; +export default InviteUserPopover; diff --git a/apps/web/src/containers/setting-menu/UserSetting/UserEditPopover.tsx b/apps/web/src/entities/user/ui/update-user-popover.ui.tsx similarity index 63% rename from apps/web/src/containers/setting-menu/UserSetting/UserEditPopover.tsx rename to apps/web/src/entities/user/ui/update-user-popover.ui.tsx index 2da3b2a0f..369a41579 100644 --- a/apps/web/src/containers/setting-menu/UserSetting/UserEditPopover.tsx +++ b/apps/web/src/entities/user/ui/update-user-popover.ui.tsx @@ -15,70 +15,67 @@ */ import { useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { z } from 'zod'; import { Icon, - Input, Popover, PopoverContent, PopoverHeading, PopoverTrigger, + TextInput, toast, } from '@ufb/ui'; -import { SelectBox } from '@/components'; -import type { UserTypeEnum } from '@/contexts/user.context'; -import { useOAIMutation } from '@/hooks'; -import type { UserDataType } from './UserSetting'; +import { SelectBox, useOAIMutation } from '@/shared'; -interface IProps { - data: UserDataType; - refetch: () => void; -} +import { updateUserSchema } from '../user.schema'; +import type { UpdateUser, UserMember, UserTypeEnum } from '../user.type'; -interface IForm { - type: UserTypeEnum; - name: string; - department: string | null; +interface IProps { + user: UserMember; } -const scheme: Zod.ZodType = z.object({ - type: z.union([z.literal('SUPER'), z.literal('GENERAL')]), - name: z.string(), - department: z.string().nullable(), -}); -const UserEditPopover: React.FC = ({ data, refetch }) => { +const UpdateUserPopover: React.FC = (props) => { + const { user } = props; + const queryClient = useQueryClient(); const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { register, setValue, watch, handleSubmit } = useForm({ - resolver: zodResolver(scheme), - defaultValues: { - type: data.type, - name: data.name ?? '', - department: data.department ?? '', - }, - }); + + const { register, setValue, watch, handleSubmit, formState } = + useForm({ + resolver: zodResolver(updateUserSchema), + defaultValues: { + email: user.email, + type: user.type, + name: user.name, + department: user.department, + }, + }); const { mutate, isPending } = useOAIMutation({ method: 'put', path: '/api/admin/users/{id}', - pathParams: { id: data.id }, + pathParams: { id: user.id }, queryOptions: { async onSuccess() { - await refetch(); + await queryClient.invalidateQueries({ + queryKey: ['/api/admin/users/search'], + }); toast.positive({ title: t('toast.save') }); setOpen(false); }, onError(error) { - toast.negative({ title: error?.message ?? 'Error' }); + toast.negative({ title: error.message }); }, }, }); - const onSubmit = (input: IForm) => mutate(input); + const onSubmit = (input: UpdateUser) => { + mutate(input); + }; return ( @@ -96,7 +93,7 @@ const UserEditPopover: React.FC = ({ data, refetch }) => {
- +
= ({ data, refetch }) => { ]} />
- - + +
+ + + + + + + + + + ); +}; + +export default CreateWebhookPopover; diff --git a/apps/web/src/containers/setting-menu/WebhookSetting/WebhookDeleteDialog.tsx b/apps/web/src/entities/webhook/ui/delete-webhook-popover.ui.tsx similarity index 64% rename from apps/web/src/containers/setting-menu/WebhookSetting/WebhookDeleteDialog.tsx rename to apps/web/src/entities/webhook/ui/delete-webhook-popover.ui.tsx index 59d90188e..a615d2997 100644 --- a/apps/web/src/containers/setting-menu/WebhookSetting/WebhookDeleteDialog.tsx +++ b/apps/web/src/entities/webhook/ui/delete-webhook-popover.ui.tsx @@ -16,44 +16,21 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - Icon, - Popover, - PopoverModalContent, - PopoverTrigger, - toast, -} from '@ufb/ui'; +import { Icon, Popover, PopoverModalContent, PopoverTrigger } from '@ufb/ui'; -import { useOAIMutation, usePermissions } from '@/hooks'; +import { usePermissions } from '@/shared'; interface IProps { - projectId: number; webhookId: number; - refetch: () => Promise; + onClickDelete: (webhookId: number) => void; } -const WebhookDeleteDialog: React.FC = (props) => { - const { projectId, webhookId, refetch } = props; +const DeleteWebhookPopover: React.FC = (props) => { + const { webhookId, onClickDelete } = props; const { t } = useTranslation(); - const perms = usePermissions(projectId); + const perms = usePermissions(); const [open, setOpen] = useState(false); - const { mutate, isPending } = useOAIMutation({ - method: 'delete', - path: '/api/admin/projects/{projectId}/webhooks/{webhookId}', - pathParams: { projectId, webhookId }, - queryOptions: { - async onSuccess() { - toast.negative({ title: t('toast.delete') }); - refetch(); - setOpen(false); - }, - onError(error) { - toast.negative({ title: error?.message ?? 'Error' }); - }, - }, - }); - return ( = (props) => { submitButton={{ className: 'bg-red-primary', children: t('button.delete'), - disabled: isPending, - onClick: () => mutate(undefined), + onClick: () => onClickDelete(webhookId), }} /> ); }; -export default WebhookDeleteDialog; +export default DeleteWebhookPopover; diff --git a/apps/web/src/entities/webhook/ui/index.ts b/apps/web/src/entities/webhook/ui/index.ts new file mode 100644 index 000000000..631b0ab83 --- /dev/null +++ b/apps/web/src/entities/webhook/ui/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as WebhookTable } from './webhook-table.ui'; +export { default as CreateWebhookPopover } from './create-webhook-popover'; +export { default as UpdateWebhookPopover } from './update-webhook-popover'; diff --git a/apps/web/src/entities/webhook/ui/update-webhook-popover.tsx b/apps/web/src/entities/webhook/ui/update-webhook-popover.tsx new file mode 100644 index 000000000..4a1459bab --- /dev/null +++ b/apps/web/src/entities/webhook/ui/update-webhook-popover.tsx @@ -0,0 +1,109 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useEffect, useState } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import { Icon, Popover, PopoverModalContent, PopoverTrigger } from '@ufb/ui'; + +import { useOAIQuery, usePermissions } from '@/shared'; + +import { webhookInfoSchema } from '../webhook.schema'; +import type { Webhook, WebhookInfo } from '../webhook.type'; +import WebhookForm from './webhook-form.ui'; + +interface IProps { + disabled?: boolean; + projectId: number; + webhook: Webhook; + onClickUpdate: (webhookId: number, input: WebhookInfo) => unknown; +} + +const UpdateWebhookPopover: React.FC = (props) => { + const { disabled, projectId, webhook, onClickUpdate } = props; + + const { t } = useTranslation(); + const perms = usePermissions(projectId); + + const { data } = useOAIQuery({ + path: '/api/admin/projects/{projectId}/channels', + variables: { projectId }, + }); + + const [open, setOpen] = useState(false); + + const methods = useForm({ + resolver: zodResolver(webhookInfoSchema), + }); + + useEffect(() => { + methods.reset(convertDefatulValuesToFormValues(webhook)); + }, [open, webhook]); + + const onSubmit = async (data: WebhookInfo) => { + await onClickUpdate(webhook.id, { ...data, status: webhook.status }); + setOpen(false); + }; + + return ( + + setOpen((prev) => !prev)} + className="icon-btn icon-btn-sm icon-btn-tertiary" + asChild + > + + + +
+ + + +
+
+
+ ); +}; + +const convertDefatulValuesToFormValues = (defaultValues: Webhook) => { + return { + name: defaultValues.name, + url: defaultValues.url, + status: defaultValues.status, + events: defaultValues.events.map((event) => ({ + status: event.status, + type: event.type, + channelIds: event.channels.map((channel) => channel.id), + })), + }; +}; + +export default UpdateWebhookPopover; diff --git a/apps/web/src/containers/setting-menu/WebhookSetting/WebhookEventTableCell.tsx b/apps/web/src/entities/webhook/ui/webhook-event-cell.tsx similarity index 78% rename from apps/web/src/containers/setting-menu/WebhookSetting/WebhookEventTableCell.tsx rename to apps/web/src/entities/webhook/ui/webhook-event-cell.tsx index dee586469..366481e92 100644 --- a/apps/web/src/containers/setting-menu/WebhookSetting/WebhookEventTableCell.tsx +++ b/apps/web/src/entities/webhook/ui/webhook-event-cell.tsx @@ -13,7 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { @@ -24,19 +23,27 @@ import { PopoverTrigger, } from '@ufb/ui'; -import type { ChannelType } from '@/types/channel.type'; -import type { WebhookEventEnum, WebhookStatusEnum } from '@/types/webhook.type'; -import { toCamelCase } from '@/utils/str'; +import { cn } from '@/shared'; +import type { Channel } from '@/entities/channel'; + +import type { WebhookEventType, WebhookStatus } from '../webhook.type'; + +const toCamelCase = (str: string) => { + return str + .replace(/\b(\w)/g, (_, capture: string) => capture.toUpperCase()) + .replace(/\s+/g, ''); +}; interface IProps { - webhookStatus: WebhookStatusEnum; - type: WebhookEventEnum; - channels: ChannelType[]; + webhookStatus: WebhookStatus; + type: WebhookEventType; + channels: Channel[]; } -const WebhookEventTableCell: React.FC = (props) => { +const WebhookEventCell: React.FC = (props) => { const { channels, type, webhookStatus } = props; const { t } = useTranslation(); + if (type === 'ISSUE_CREATION' || type === 'ISSUE_STATUS_CHANGE') { return

{toCamelCase(t(`text.webhook-type.${type}`))}

; } @@ -45,7 +52,7 @@ const WebhookEventTableCell: React.FC = (props) => { = (props) => { ); }; -export default WebhookEventTableCell; +export default WebhookEventCell; diff --git a/apps/web/src/entities/webhook/ui/webhook-form.ui.tsx b/apps/web/src/entities/webhook/ui/webhook-form.ui.tsx new file mode 100644 index 000000000..2e4a5a5dc --- /dev/null +++ b/apps/web/src/entities/webhook/ui/webhook-form.ui.tsx @@ -0,0 +1,210 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import { TextInput } from '@ufb/ui'; + +import { DescriptionTooltip, SelectBox } from '@/shared'; +import type { Channel } from '@/entities/channel'; + +import type { + WebhookEventType, + WebhookInfo, + WebhookStatus, +} from '../webhook.type'; + +interface IProps { + channels: Channel[]; +} + +const WebhookForm: React.FC = (props) => { + const { channels } = props; + const { t } = useTranslation(); + const { register, setValue, getValues, watch, formState } = + useFormContext(); + + const getEventChecked = (type: WebhookEventType) => + watch('events').find((e) => e.type === type)?.status === 'ACTIVE'; + + const getEventChannels = (type: WebhookEventType) => { + return channels.filter((channel) => + watch('events') + .find((e) => e.type === type) + ?.channelIds.includes(channel.id), + ); + }; + + const onChangeEventChannels = (type: WebhookEventType, ids: number[]) => { + setValue( + 'events', + getValues('events').map((event) => { + if (event.type !== type) return event; + if ( + ids.length === 0 && + (type === 'FEEDBACK_CREATION' || type === 'ISSUE_ADDITION') + ) { + return { + ...event, + status: 'INACTIVE' as WebhookStatus, + channelIds: ids, + }; + } + return { ...event, channelIds: ids }; + }), + ); + }; + + const toggleEventType = + (type: WebhookEventType) => (e: React.ChangeEvent) => { + const status: WebhookStatus = e.target.checked ? 'ACTIVE' : 'INACTIVE'; + + const channelIds = + ( + status === 'ACTIVE' && + (type === 'FEEDBACK_CREATION' || type === 'ISSUE_ADDITION') + ) ? + channels.map((v) => v.id) + : []; + + setValue( + 'events', + getValues('events').map((event) => { + if (event.type !== type) return event; + return event.type === type ? { ...event, status, channelIds } : event; + }), + ); + }; + + return ( +
+ + +
+

Event

+
+ + {getEventChecked('FEEDBACK_CREATION') && ( + + onChangeEventChannels( + 'FEEDBACK_CREATION', + v.map((v) => v.id), + ) + } + value={getEventChannels('FEEDBACK_CREATION')} + options={channels} + getOptionValue={(option) => String(option.id)} + getOptionLabel={(option) => option.name} + width={340} + height={48} + /> + )} +
+
+ + {getEventChecked('ISSUE_ADDITION') && ( + + onChangeEventChannels( + 'ISSUE_ADDITION', + v.map((v) => v.id), + ) + } + value={getEventChannels('ISSUE_ADDITION')} + options={channels} + getOptionValue={(option) => String(option.id)} + getOptionLabel={(option) => option.name} + classNames={{ container: () => 'w-[340px]' }} + /> + )} +
+
+ +
+
+ +
+
+
+ ); +}; + +export default WebhookForm; diff --git a/apps/web/src/entities/webhook/ui/webhook-switch.ui.tsx b/apps/web/src/entities/webhook/ui/webhook-switch.ui.tsx new file mode 100644 index 000000000..3663649b1 --- /dev/null +++ b/apps/web/src/entities/webhook/ui/webhook-switch.ui.tsx @@ -0,0 +1,52 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { cn, usePermissions } from '@/shared'; + +import type { Webhook, WebhookInfo } from '../webhook.type'; + +interface IProps { + webhook: Webhook; + onChangeUpdate: (webhookId: number, webhook: WebhookInfo) => void; +} + +const WebhookSwitch: React.FC = (props) => { + const { webhook, onChangeUpdate } = props; + const perms = usePermissions(); + + return ( + { + onChangeUpdate(webhook.id, { + ...webhook, + events: webhook.events.map((event) => ({ + ...event, + channelIds: event.channels.map((channel) => channel.id), + })), + status: e.target.checked ? 'ACTIVE' : 'INACTIVE', + }); + }} + disabled={!perms.includes('project_webhook_update')} + /> + ); +}; + +export default WebhookSwitch; diff --git a/apps/web/src/entities/webhook/ui/webhook-table.ui.tsx b/apps/web/src/entities/webhook/ui/webhook-table.ui.tsx new file mode 100644 index 000000000..51369c2ff --- /dev/null +++ b/apps/web/src/entities/webhook/ui/webhook-table.ui.tsx @@ -0,0 +1,59 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { useTranslation } from 'react-i18next'; + +import { Icon } from '@ufb/ui'; + +import { BasicTable } from '@/shared'; + +import { getWebhookColumns } from '../webhook-column'; +import type { Webhook, WebhookInfo } from '../webhook.type'; + +interface IProps { + isLoading?: boolean; + webhooks: Webhook[]; + projectId: number; + onUpdate: (webhookId: number, webhook: WebhookInfo) => void; + onDelete: (webhookId: number) => void; +} + +const WebhookTable: React.FC = (props) => { + const { isLoading, webhooks, projectId, onDelete, onUpdate } = props; + const { t } = useTranslation(); + + const table = useReactTable({ + columns: getWebhookColumns(projectId, onDelete, onUpdate), + data: webhooks, + getCoreRowModel: getCoreRowModel(), + enableSorting: false, + }); + + return ( + + +

{t('main.setting.register-member')}

+
+ } + /> + ); +}; + +export default WebhookTable; diff --git a/apps/web/src/entities/webhook/webhook-column.tsx b/apps/web/src/entities/webhook/webhook-column.tsx new file mode 100644 index 000000000..b65494fdb --- /dev/null +++ b/apps/web/src/entities/webhook/webhook-column.tsx @@ -0,0 +1,125 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { createColumnHelper } from '@tanstack/react-table'; +import dayjs from 'dayjs'; + +import { cn, DATE_TIME_FORMAT } from '@/shared'; + +import { UpdateWebhookPopover } from './ui'; +import DeleteWebhookPopover from './ui/delete-webhook-popover.ui'; +import WebhookEventCell from './ui/webhook-event-cell'; +import WebhookSwitch from './ui/webhook-switch.ui'; +import type { Webhook, WebhookInfo } from './webhook.type'; + +const columnHelper = createColumnHelper(); + +export const getWebhookColumns = ( + projectId: number, + onDelete: (webhookId: number) => void, + onUpdate: (webhookId: number, input: WebhookInfo) => void, +) => [ + columnHelper.accessor('status', { + header: '', + cell: ({ row }) => { + return ( +
+ +
+ ); + }, + size: 65, + }), + columnHelper.accessor('name', { + header: 'Name', + cell: ({ getValue, row }) => ( + + {getValue()} + + ), + size: 75, + }), + columnHelper.accessor('url', { + header: 'URL', + cell: ({ getValue, row }) => ( + + {getValue()} + + ), + size: 100, + }), + columnHelper.accessor('events', { + header: 'Event', + cell: ({ getValue, row }) => ( +
+ {getValue() + .filter((v) => v.status === 'ACTIVE') + .map((v) => ( + + ))} +
+ ), + size: 300, + }), + columnHelper.accessor('createdAt', { + header: 'Created', + cell: ({ getValue, row }) => ( + + {dayjs(getValue()).format(DATE_TIME_FORMAT)} + + ), + size: 150, + }), + columnHelper.display({ + id: 'edit', + header: 'Edit', + cell: ({ row }) => ( + + ), + size: 50, + }), + columnHelper.display({ + id: 'delete', + header: 'Delete', + cell: ({ row }) => ( + + ), + size: 50, + }), +]; diff --git a/apps/web/src/entities/webhook/webhook.schema.ts b/apps/web/src/entities/webhook/webhook.schema.ts new file mode 100644 index 000000000..328810675 --- /dev/null +++ b/apps/web/src/entities/webhook/webhook.schema.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { z } from 'zod'; + +import { channelSchema } from '../channel'; + +export const webhookEventSchema = z.object({ + id: z.number(), + status: z.enum(['ACTIVE', 'INACTIVE']), + type: z.enum([ + 'FEEDBACK_CREATION', + 'ISSUE_CREATION', + 'ISSUE_STATUS_CHANGE', + 'ISSUE_ADDITION', + ]), + channels: z.array(channelSchema), + createdAt: z.string(), +}); + +export const webhookSchema = z.object({ + id: z.number(), + name: z.string(), + url: z.string(), + status: z.enum(['ACTIVE', 'INACTIVE']), + events: z.array(webhookEventSchema), + createdAt: z.string(), +}); + +export const webhookInfoSchema = webhookSchema + .omit({ + id: true, + createdAt: true, + events: true, + }) + .merge( + z.object({ + events: z.array( + webhookEventSchema + .omit({ id: true, createdAt: true, channels: true }) + .merge(z.object({ channelIds: z.array(z.number()) })), + ), + }), + ); diff --git a/apps/web/src/entities/webhook/webhook.type.ts b/apps/web/src/entities/webhook/webhook.type.ts new file mode 100644 index 000000000..d15000cd7 --- /dev/null +++ b/apps/web/src/entities/webhook/webhook.type.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { z } from 'zod'; + +import type { + webhookEventSchema, + webhookInfoSchema, + webhookSchema, +} from './webhook.schema'; + +export type Webhook = z.infer; +export type WebhookEvent = z.infer; +export type WebhookStatus = Webhook['status']; +export type WebhookEventType = WebhookEvent['type']; + +export type WebhookInfo = z.infer; diff --git a/apps/web/src/env.mjs b/apps/web/src/env.ts similarity index 89% rename from apps/web/src/env.mjs rename to apps/web/src/env.ts index d97bb76d2..a71bdf5c1 100644 --- a/apps/web/src/env.mjs +++ b/apps/web/src/env.ts @@ -17,6 +17,11 @@ import { createEnv } from '@t3-oss/env-nextjs'; import { z } from 'zod'; export const env = createEnv({ + shared: { + NODE_ENV: z + .enum(['development', 'production', 'test']) + .default('development'), + }, server: { API_BASE_URL: z.string().url(), SESSION_PASSWORD: z.string().min(32), @@ -30,6 +35,7 @@ export const env = createEnv({ SESSION_PASSWORD: process.env.SESSION_PASSWORD, NEXT_PUBLIC_MAX_DAYS: process.env.NEXT_PUBLIC_MAX_DAYS, NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, + NODE_ENV: process.env.NODE_ENV, }, skipValidation: !!process.env.CI || diff --git a/apps/web/src/components/layouts/Header/index.ts b/apps/web/src/features/auth/reset-password-with-email/index.ts similarity index 94% rename from apps/web/src/components/layouts/Header/index.ts rename to apps/web/src/features/auth/reset-password-with-email/index.ts index 8f7e90b0d..43fa3d865 100644 --- a/apps/web/src/components/layouts/Header/index.ts +++ b/apps/web/src/features/auth/reset-password-with-email/index.ts @@ -13,4 +13,4 @@ * License for the specific language governing permissions and limitations * under the License. */ -export { default } from './Header'; +export * from './ui'; diff --git a/apps/web/src/features/auth/reset-password-with-email/request-reset-password-with-email.schema.ts b/apps/web/src/features/auth/reset-password-with-email/request-reset-password-with-email.schema.ts new file mode 100644 index 000000000..e19f2f686 --- /dev/null +++ b/apps/web/src/features/auth/reset-password-with-email/request-reset-password-with-email.schema.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { z } from 'zod'; + +export const requestResetPasswordWithEmailSchema = z.object({ + email: z.string().email(), +}); diff --git a/apps/web/src/features/auth/reset-password-with-email/reset-password-with-email.schema.ts b/apps/web/src/features/auth/reset-password-with-email/reset-password-with-email.schema.ts new file mode 100644 index 000000000..fc3d45c81 --- /dev/null +++ b/apps/web/src/features/auth/reset-password-with-email/reset-password-with-email.schema.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { z } from 'zod'; + +export const resetPasswordWithEmailSchema = z + .object({ + password: z.string().min(8), + confirmPassword: z.string().min(8), + code: z.string(), + email: z.string().email(), + }) + .refine((schema) => schema.password === schema.confirmPassword, { + message: 'Password not matched', + path: ['confirmPassword'], + }); diff --git a/apps/web/src/features/auth/reset-password-with-email/ui/__snapshots__/request-reset-password-with-email.ui.spec.tsx.snap b/apps/web/src/features/auth/reset-password-with-email/ui/__snapshots__/request-reset-password-with-email.ui.spec.tsx.snap new file mode 100644 index 000000000..4f06ac765 --- /dev/null +++ b/apps/web/src/features/auth/reset-password-with-email/ui/__snapshots__/request-reset-password-with-email.ui.spec.tsx.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RequestResetPasswordWithEmail match snapshot 1`] = ` +
+
+
+
+ +
+ +
+
+
+
+
+ + +
+ +
+
+`; diff --git a/apps/web/src/features/auth/reset-password-with-email/ui/__snapshots__/reset-password-with-email-form.ui.spec.tsx.snap b/apps/web/src/features/auth/reset-password-with-email/ui/__snapshots__/reset-password-with-email-form.ui.spec.tsx.snap new file mode 100644 index 000000000..2c9e59659 --- /dev/null +++ b/apps/web/src/features/auth/reset-password-with-email/ui/__snapshots__/reset-password-with-email-form.ui.spec.tsx.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResetPasswordWithEmailForm match snapshot 1`] = ` +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ + +
+ +
+
+`; diff --git a/apps/web/src/features/auth/reset-password-with-email/ui/index.ts b/apps/web/src/features/auth/reset-password-with-email/ui/index.ts new file mode 100644 index 000000000..f37cafe4c --- /dev/null +++ b/apps/web/src/features/auth/reset-password-with-email/ui/index.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as RequestResetPasswordWithEmail } from './request-reset-password-with-email.ui'; +export { default as ResetPasswordWithEmailForm } from './reset-password-with-email-form.ui'; diff --git a/apps/web/src/features/auth/reset-password-with-email/ui/request-reset-password-with-email.ui.spec.tsx b/apps/web/src/features/auth/reset-password-with-email/ui/request-reset-password-with-email.ui.spec.tsx new file mode 100644 index 000000000..aabd128eb --- /dev/null +++ b/apps/web/src/features/auth/reset-password-with-email/ui/request-reset-password-with-email.ui.spec.tsx @@ -0,0 +1,97 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import userEvent from '@testing-library/user-event'; + +import { simpleMockHttp } from '@/msw'; +import { render, screen, waitFor } from '@/test-utils'; +import RequestResetPasswordWithEmail from './request-reset-password-with-email.ui'; + +describe('RequestResetPasswordWithEmail', () => { + test('match snapshot', () => { + const component = render(); + expect(component.container).toMatchSnapshot(); + }); + + test('back button', async () => { + render(); + const backBtn = screen.getByRole('button', { name: 'button.back' }); + + await userEvent.click(backBtn); + // next-router-mock is not supported to back yet + // await waitFor(() => expect(mockRouter).toMatchObject({ pathname: '/' })); + }); + + test('validation', async () => { + render(); + const emailInput = screen.getByPlaceholderText('input.placeholder.email'); + + const submitBtn = screen.getByRole('button', { + name: 'auth.reset-password.button.send-email', + }); + + expect(submitBtn).toBeDisabled(); + + await userEvent.type(emailInput, faker.string.sample()); + + expect(submitBtn).toBeDisabled(); + + await userEvent.clear(emailInput); + await userEvent.type(emailInput, faker.internet.email()); + + expect(submitBtn).not.toBeDisabled(); + }); + + describe('Submittion', () => { + beforeEach(async () => { + render(); + const emailInput = screen.getByPlaceholderText('input.placeholder.email'); + await userEvent.type(emailInput, faker.internet.email()); + }); + + test('on Success', async () => { + simpleMockHttp({ + method: 'post', + path: '/api/admin/users/password/reset/code', + }); + + const submitBtn = screen.getByRole('button', { + name: 'auth.reset-password.button.send-email', + }); + await userEvent.click(submitBtn); + await waitFor(() => + expect( + screen.getByText(new RegExp('success', 'i')), + ).toBeInTheDocument(), + ); + }); + test('on Error', async () => { + simpleMockHttp({ + method: 'post', + path: '/api/admin/users/password/reset/code', + status: 500, + }); + + const submitBtn = screen.getByRole('button', { + name: 'auth.reset-password.button.send-email', + }); + await userEvent.click(submitBtn); + await waitFor(() => + expect(screen.getByText(new RegExp('error', 'i'))).toBeInTheDocument(), + ); + }); + }); +}); diff --git a/apps/web/src/features/auth/reset-password-with-email/ui/request-reset-password-with-email.ui.tsx b/apps/web/src/features/auth/reset-password-with-email/ui/request-reset-password-with-email.ui.tsx new file mode 100644 index 000000000..0658ee32d --- /dev/null +++ b/apps/web/src/features/auth/reset-password-with-email/ui/request-reset-password-with-email.ui.tsx @@ -0,0 +1,90 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useRouter } from 'next/router'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { z } from 'zod'; + +import { TextInput, toast } from '@ufb/ui'; + +import { Path, useOAIMutation } from '@/shared'; + +import { requestResetPasswordWithEmailSchema } from '../request-reset-password-with-email.schema'; + +type FormType = z.infer; + +interface IProps {} + +const RequestResetPasswordWithEmail: React.FC = () => { + const { t } = useTranslation(); + + const router = useRouter(); + + const { register, handleSubmit, formState } = useForm({ + resolver: zodResolver(requestResetPasswordWithEmailSchema), + }); + + const { mutate, isPending } = useOAIMutation({ + method: 'post', + path: '/api/admin/users/password/reset/code', + queryOptions: { + async onSuccess() { + await router.push(Path.SIGN_IN); + toast.positive({ title: 'Success' }); + }, + onError(error) { + toast.negative({ title: 'Error', description: error.message }); + }, + }, + }); + + return ( +
mutate(data))}> +
+ +
+
+ + +
+
+ ); +}; + +export default RequestResetPasswordWithEmail; diff --git a/apps/web/src/features/auth/reset-password-with-email/ui/reset-password-with-email-form.ui.spec.tsx b/apps/web/src/features/auth/reset-password-with-email/ui/reset-password-with-email-form.ui.spec.tsx new file mode 100644 index 000000000..991acad06 --- /dev/null +++ b/apps/web/src/features/auth/reset-password-with-email/ui/reset-password-with-email-form.ui.spec.tsx @@ -0,0 +1,113 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import userEvent from '@testing-library/user-event'; + +import { simpleMockHttp } from '@/msw'; +import { render, screen, waitFor } from '@/test-utils'; +import ResetPasswordWithEmailForm from './reset-password-with-email-form.ui'; + +describe('ResetPasswordWithEmailForm', () => { + test('match snapshot', () => { + const component = render( + , + ); + expect(component.container).toMatchSnapshot(); + }); + + test('validation', async () => { + render( + , + ); + + const sendEmailBtn = screen.getByRole('button', { + name: 'button.setting', + }); + const passwordInput = screen.getByPlaceholderText( + 'input.placeholder.password', + ); + const confirmPasswordInput = screen.getByPlaceholderText( + 'input.placeholder.confirm-password', + ); + + await userEvent.type(passwordInput, faker.string.alphanumeric(8)); + await userEvent.type(confirmPasswordInput, faker.string.alphanumeric(9)); + + expect(sendEmailBtn).toBeDisabled(); + + await userEvent.clear(passwordInput); + await userEvent.clear(confirmPasswordInput); + + const password = faker.string.alphanumeric(8); + await userEvent.type(passwordInput, password); + await userEvent.type(confirmPasswordInput, password); + + await waitFor(() => expect(sendEmailBtn).not.toBeDisabled()); + }); + describe('Submittion', () => { + beforeEach(async () => { + render( + , + ); + + const passwordInput = screen.getByPlaceholderText( + 'input.placeholder.password', + ); + const confirmPasswordInput = screen.getByPlaceholderText( + 'input.placeholder.confirm-password', + ); + const password = faker.string.alphanumeric(8); + await userEvent.type(passwordInput, password); + await userEvent.type(confirmPasswordInput, password); + }); + test('on Success', async () => { + simpleMockHttp({ + method: 'post', + path: '/api/admin/users/password/reset', + }); + + const submitBtn = screen.getByRole('button', { + name: 'button.setting', + }); + await userEvent.click(submitBtn); + + await waitFor(() => + expect( + screen.getByText(new RegExp('success', 'i')), + ).toBeInTheDocument(), + ); + }); + test('on Error', async () => { + simpleMockHttp({ + method: 'post', + path: '/api/admin/users/password/reset', + status: 500, + }); + + const submitBtn = screen.getByRole('button', { + name: 'button.setting', + }); + await userEvent.click(submitBtn); + + await waitFor(() => + expect(screen.getByText(new RegExp('error', 'i'))).toBeInTheDocument(), + ); + }); + }); +}); diff --git a/apps/web/src/features/auth/reset-password-with-email/ui/reset-password-with-email-form.ui.tsx b/apps/web/src/features/auth/reset-password-with-email/ui/reset-password-with-email-form.ui.tsx new file mode 100644 index 000000000..b1326b3a9 --- /dev/null +++ b/apps/web/src/features/auth/reset-password-with-email/ui/reset-password-with-email-form.ui.tsx @@ -0,0 +1,114 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useRouter } from 'next/router'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { z } from 'zod'; + +import { TextInput, toast } from '@ufb/ui'; + +import { Path, useOAIMutation } from '@/shared'; + +import { resetPasswordWithEmailSchema } from '../reset-password-with-email.schema'; + +type FormType = z.infer; + +interface IProps { + code: string; + email: string; +} + +const ResetPasswordWithEmailForm: React.FC = ({ code, email }) => { + const { t } = useTranslation(); + const router = useRouter(); + + const { handleSubmit, register, formState } = useForm({ + resolver: zodResolver(resetPasswordWithEmailSchema), + defaultValues: { code, email }, + }); + + const { mutate, isPending } = useOAIMutation({ + method: 'post', + path: '/api/admin/users/password/reset', + queryOptions: { + async onSuccess() { + toast.positive({ title: 'Success' }); + await router.push(Path.SIGN_IN); + }, + onError(error) { + toast.negative({ title: 'Error', description: error.message }); + }, + }, + }); + + const onSubmit = ({ password }: FormType) => + mutate({ code, email, password }); + + return ( +
+
+ + + +
+
+ + +
+
+ ); +}; + +export default ResetPasswordWithEmailForm; diff --git a/apps/web/src/components/layouts/SideNav/index.ts b/apps/web/src/features/auth/sign-in-with-email/index.ts similarity index 94% rename from apps/web/src/components/layouts/SideNav/index.ts rename to apps/web/src/features/auth/sign-in-with-email/index.ts index 8534f01ee..43fa3d865 100644 --- a/apps/web/src/components/layouts/SideNav/index.ts +++ b/apps/web/src/features/auth/sign-in-with-email/index.ts @@ -13,4 +13,4 @@ * License for the specific language governing permissions and limitations * under the License. */ -export { default } from './SideNav'; +export * from './ui'; diff --git a/apps/web/src/features/auth/sign-in-with-email/sign-in-with-email.schema.ts b/apps/web/src/features/auth/sign-in-with-email/sign-in-with-email.schema.ts new file mode 100644 index 000000000..31e76625e --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-email/sign-in-with-email.schema.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { z } from 'zod'; + +export const SignInWithEmailSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), +}); diff --git a/apps/web/src/features/auth/sign-in-with-email/ui/__snapshots__/sign-in-with-email-form.ui.spec.tsx.snap b/apps/web/src/features/auth/sign-in-with-email/ui/__snapshots__/sign-in-with-email-form.ui.spec.tsx.snap new file mode 100644 index 000000000..e622d49c9 --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-email/ui/__snapshots__/sign-in-with-email-form.ui.spec.tsx.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SignInWithEmailForm match snapshot when tenant is not private 1`] = ` +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ + +
+ +
+
+`; + +exports[`SignInWithEmailForm match snapshot when tenant is private 1`] = ` +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+`; diff --git a/apps/web/src/features/auth/sign-in-with-email/ui/index.ts b/apps/web/src/features/auth/sign-in-with-email/ui/index.ts new file mode 100644 index 000000000..66f37b09a --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-email/ui/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as SignInWithEmailForm } from './sign-in-with-email-form.ui'; diff --git a/apps/web/src/features/auth/sign-in-with-email/ui/sign-in-with-email-form.ui.spec.tsx b/apps/web/src/features/auth/sign-in-with-email/ui/sign-in-with-email-form.ui.spec.tsx new file mode 100644 index 000000000..e222de72d --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-email/ui/sign-in-with-email-form.ui.spec.tsx @@ -0,0 +1,157 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { faker } from '@faker-js/faker'; +import userEvent from '@testing-library/user-event'; + +import type { Tenant } from '@/entities/tenant'; +import { useTenantStore } from '@/entities/tenant'; +import * as user from '@/entities/user'; + +import { render, screen, waitFor } from '@/test-utils'; +import SignInWithEmailForm from './sign-in-with-email-form.ui'; + +jest.mock('@/entities/user'); + +const DEFAULT_TENANT: Tenant = { + allowDomains: [], + isRestrictDomain: false, + description: null, + id: 1, + oauthConfig: null, + siteName: 'siteName', + useEmail: true, + useEmailVerification: true, + useOAuth: true, + isPrivate: false, +}; + +describe('SignInWithEmailForm', () => { + beforeEach(() => { + useTenantStore.setState({ tenant: DEFAULT_TENANT }); + jest.spyOn(user, 'useUserStore').mockReturnValue({ + signInWithEmail: jest.fn(), + _signIn: jest.fn(), + setUser: jest.fn(), + signInWithOAuth: jest.fn(), + signOut: jest.fn(), + }); + }); + test('match snapshot when tenant is not private', () => { + useTenantStore.setState({ + tenant: { ...DEFAULT_TENANT, isPrivate: false }, + }); + const { container } = render(); + expect(container).toMatchSnapshot(); + + expect(screen.getByText('button.sign-in')).toBeInTheDocument(); + + expect(screen.queryByText('button.sign-up')).toBeInTheDocument(); + }); + test('match snapshot when tenant is private', () => { + useTenantStore.setState({ tenant: { ...DEFAULT_TENANT, isPrivate: true } }); + const { container } = render(); + expect(container).toMatchSnapshot(); + + expect(screen.getByText('button.sign-in')).toBeInTheDocument(); + expect(screen.queryByText('button.sign-up')).not.toBeInTheDocument(); + }); + + test('validation', async () => { + render(); + + const signInBtn = screen.getByRole('button', { + name: 'button.sign-in', + }); + const idInput = screen.getByPlaceholderText('ID'); + const passwordInput = screen.getByPlaceholderText('Password'); + + await userEvent.type(idInput, faker.internet.email()); + await userEvent.type(passwordInput, faker.string.alphanumeric(9)); + + expect(signInBtn).not.toBeDisabled(); + + await userEvent.clear(idInput); + await userEvent.clear(passwordInput); + + await userEvent.type(idInput, faker.string.alphanumeric(8)); + await userEvent.type(passwordInput, faker.string.alphanumeric(8)); + + expect(signInBtn).toBeDisabled(); + + await userEvent.clear(idInput); + await userEvent.clear(passwordInput); + + await userEvent.type(idInput, faker.internet.email()); + await userEvent.type(passwordInput, faker.string.alphanumeric(7)); + + expect(signInBtn).toBeDisabled(); + }); + + describe('Submittion', () => { + test('on Success', async () => { + jest.spyOn(user, 'useUserStore').mockImplementation(() => ({ + signInWithEmail: jest.fn(), + _signIn: jest.fn(), + setUser: jest.fn(), + signInWithOAuth: jest.fn(), + signOut: jest.fn(), + })); + render(); + + const idInput = screen.getByPlaceholderText('ID'); + const passwordInput = screen.getByPlaceholderText('Password'); + + await userEvent.type(idInput, faker.internet.email()); + await userEvent.type(passwordInput, faker.string.alphanumeric(9)); + + const submitBtn = screen.getByRole('button', { + name: 'button.sign-in', + }); + await userEvent.click(submitBtn); + + await waitFor(() => + expect( + screen.getByText(new RegExp('success', 'i')), + ).toBeInTheDocument(), + ); + }); + test('on Error', async () => { + jest.spyOn(user, 'useUserStore').mockImplementation(() => ({ + signInWithEmail: jest.fn().mockRejectedValue(new Error()), + _signIn: jest.fn(), + setUser: jest.fn(), + signInWithOAuth: jest.fn(), + signOut: jest.fn(), + })); + render(); + + const idInput = screen.getByPlaceholderText('ID'); + const passwordInput = screen.getByPlaceholderText('Password'); + + await userEvent.type(idInput, faker.internet.email()); + await userEvent.type(passwordInput, faker.string.alphanumeric(9)); + const submitBtn = screen.getByRole('button', { + name: 'button.sign-in', + }); + await userEvent.click(submitBtn); + + await waitFor(() => + expect(screen.getByText(new RegExp('error', 'i'))).toBeInTheDocument(), + ); + }); + }); +}); diff --git a/apps/web/src/features/auth/sign-in-with-email/ui/sign-in-with-email-form.ui.tsx b/apps/web/src/features/auth/sign-in-with-email/ui/sign-in-with-email-form.ui.tsx new file mode 100644 index 000000000..7d639709d --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-email/ui/sign-in-with-email-form.ui.tsx @@ -0,0 +1,104 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { useRouter } from 'next/router'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { z } from 'zod'; + +import { TextInput, toast } from '@ufb/ui'; + +import type { IFetchError } from '@/shared'; +import { Path } from '@/shared'; +import { useTenantStore } from '@/entities/tenant'; +import { useUserStore } from '@/entities/user'; + +import { SignInWithEmailSchema } from '../sign-in-with-email.schema'; + +type FormType = z.infer; + +interface IProps {} + +const SignInWithEmailForm: React.FC = () => { + const { tenant } = useTenantStore(); + const { t } = useTranslation(); + const router = useRouter(); + + const { signInWithEmail } = useUserStore(); + const { handleSubmit, register, formState, setError } = useForm({ + resolver: zodResolver(SignInWithEmailSchema), + }); + + const onSubmit = async (data: FormType) => { + try { + await signInWithEmail(data); + toast.positive({ title: 'Success' }); + } catch (error) { + const { message } = error as IFetchError; + setError('email', { message: 'invalid email' }); + setError('password', { message: 'invalid password' }); + toast.negative({ title: 'Error', description: message }); + } + }; + + return ( +
+
+ + +
+
+ + {!tenant?.isPrivate && ( + + )} +
+
+ ); +}; + +export default SignInWithEmailForm; diff --git a/apps/web/src/features/auth/sign-in-with-oauth/__mocks__/sign-in-with-oauth.mock-handler.ts b/apps/web/src/features/auth/sign-in-with-oauth/__mocks__/sign-in-with-oauth.mock-handler.ts new file mode 100644 index 000000000..81b4f14c4 --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-oauth/__mocks__/sign-in-with-oauth.mock-handler.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import { http, HttpResponse } from 'msw'; + +import { env } from '@/env'; + +export const signInWithOAuthMockHandlers = [ + http.get( + `${env.NEXT_PUBLIC_API_BASE_URL}/api/admin/auth/signIn/oauth/loginURL`, + () => { + return HttpResponse.json({ url: faker.internet.url() }, { status: 200 }); + }, + ), +]; diff --git a/apps/web/src/components/cards/DashboardCard/index.ts b/apps/web/src/features/auth/sign-in-with-oauth/index.ts similarity index 93% rename from apps/web/src/components/cards/DashboardCard/index.ts rename to apps/web/src/features/auth/sign-in-with-oauth/index.ts index 11692540a..cbaa61ba5 100644 --- a/apps/web/src/components/cards/DashboardCard/index.ts +++ b/apps/web/src/features/auth/sign-in-with-oauth/index.ts @@ -13,4 +13,5 @@ * License for the specific language governing permissions and limitations * under the License. */ -export { default } from './DashboardCard'; +export * from './ui'; +export * from './lib'; diff --git a/apps/web/src/features/auth/sign-in-with-oauth/lib/index.ts b/apps/web/src/features/auth/sign-in-with-oauth/lib/index.ts new file mode 100644 index 000000000..2a0df4f8b --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-oauth/lib/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { useOAuthCallback } from './use-oauth-callback'; diff --git a/apps/web/src/features/auth/sign-in-with-oauth/lib/use-oauth-callback.ts b/apps/web/src/features/auth/sign-in-with-oauth/lib/use-oauth-callback.ts new file mode 100644 index 000000000..02a7385a0 --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-oauth/lib/use-oauth-callback.ts @@ -0,0 +1,52 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useEffect, useMemo, useState } from 'react'; +import { useRouter } from 'next/router'; + +import { toast } from '@ufb/ui'; + +import { Path } from '@/shared'; +import { useUserStore } from '@/entities/user'; + +interface IQuery { + code: string; + callback_url?: string; +} + +export const useOAuthCallback = () => { + const { signInWithOAuth } = useUserStore(); + const [status, setStatus] = useState<'loading' | 'error'>('loading'); + + const router = useRouter(); + + const query = useMemo(() => { + if (!router.query.callback_url) return null; + const { code, callback_url } = router.query; + + return code ? ({ code, callback_url } as IQuery) : null; + }, [router.query]); + + useEffect(() => { + if (!query) return; + signInWithOAuth(query).catch(() => { + toast.negative({ title: 'OAuth2.0 Login Error' }); + void router.replace(Path.SIGN_IN); + setStatus('error'); + }); + }, [query]); + + return { status }; +}; diff --git a/apps/web/src/features/auth/sign-in-with-oauth/ui/__snapshots__/sign-in-with-oauth-button.ui.spec.tsx.snap b/apps/web/src/features/auth/sign-in-with-oauth/ui/__snapshots__/sign-in-with-oauth-button.ui.spec.tsx.snap new file mode 100644 index 000000000..7d7872edf --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-oauth/ui/__snapshots__/sign-in-with-oauth-button.ui.spec.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SignInWithOAuthButton match snapshot 1`] = ` +
+ +
+
+`; diff --git a/apps/web/src/features/auth/sign-in-with-oauth/ui/index.ts b/apps/web/src/features/auth/sign-in-with-oauth/ui/index.ts new file mode 100644 index 000000000..717450600 --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-oauth/ui/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as SignInWithOAuthButton } from './sign-in-with-oauth-button.ui'; diff --git a/apps/web/src/features/auth/sign-in-with-oauth/ui/sign-in-with-oauth-button.ui.spec.tsx b/apps/web/src/features/auth/sign-in-with-oauth/ui/sign-in-with-oauth-button.ui.spec.tsx new file mode 100644 index 000000000..2466fc857 --- /dev/null +++ b/apps/web/src/features/auth/sign-in-with-oauth/ui/sign-in-with-oauth-button.ui.spec.tsx @@ -0,0 +1,55 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { faker } from '@faker-js/faker'; +import userEvent from '@testing-library/user-event'; +import mockRouter from 'next-router-mock'; + +import type { Tenant } from '@/entities/tenant'; +import { useTenantStore } from '@/entities/tenant'; + +import { simpleMockHttp } from '@/msw'; +import { render, screen, waitFor } from '@/test-utils'; +import SignInWithOAuthButton from './sign-in-with-oauth-button.ui'; + +describe('SignInWithOAuthButton', () => { + test('match snapshot', () => { + const component = render(); + expect(component.container).toMatchSnapshot(); + }); + test('loginUrl', async () => { + useTenantStore.setState({ tenant: { useOAuth: true } as Tenant }); + const pathname = `/${Array.from({ + length: faker.number.int({ min: 1, max: 5 }), + }) + .map(() => faker.string.alphanumeric({ length: { min: 1, max: 5 } })) + .join('/')}`; + + simpleMockHttp({ + method: 'get', + path: '/api/admin/auth/signIn/oauth/loginURL', + status: 200, + data: { url: pathname }, + }); + + render(); + + await waitFor(() => expect(screen.getByRole('button')).not.toBeDisabled()); + await userEvent.click(screen.getByRole('button')); + + expect(mockRouter).toMatchObject({ asPath: pathname }); + }); +}); diff --git a/apps/web/src/containers/buttons/OAuthLoginButton/OAuthLoginButton.tsx b/apps/web/src/features/auth/sign-in-with-oauth/ui/sign-in-with-oauth-button.ui.tsx similarity index 75% rename from apps/web/src/containers/buttons/OAuthLoginButton/OAuthLoginButton.tsx rename to apps/web/src/features/auth/sign-in-with-oauth/ui/sign-in-with-oauth-button.ui.tsx index 9b9a99e9c..6714321a6 100644 --- a/apps/web/src/containers/buttons/OAuthLoginButton/OAuthLoginButton.tsx +++ b/apps/web/src/features/auth/sign-in-with-oauth/ui/sign-in-with-oauth-button.ui.tsx @@ -14,25 +14,20 @@ * under the License. */ -import { useMemo } from 'react'; import { useRouter } from 'next/router'; import { useTranslation } from 'react-i18next'; -import { useTenant } from '@/contexts/tenant.context'; -import { useOAIQuery } from '@/hooks'; +import { useOAIQuery } from '@/shared'; +import { useTenantStore } from '@/entities/tenant'; interface IProps {} -const OAuthLoginButton: React.FC = () => { +const SignInWithOAuthButton: React.FC = () => { const { t } = useTranslation(); const router = useRouter(); - const { tenant } = useTenant(); + const { tenant } = useTenantStore(); - const callback_url = useMemo( - () => - router.query.callback_url ? (router.query.callback_url as string) : '', - [router.query], - ); + const callback_url = (router.query.callback_url ?? '') as string; const { data } = useOAIQuery({ path: '/api/admin/auth/signIn/oauth/loginURL', @@ -44,6 +39,7 @@ const OAuthLoginButton: React.FC = () => { +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ + +
+ +
+
+`; diff --git a/apps/web/src/features/auth/sign-up-with-email/ui/index.ts b/apps/web/src/features/auth/sign-up-with-email/ui/index.ts new file mode 100644 index 000000000..943aaf513 --- /dev/null +++ b/apps/web/src/features/auth/sign-up-with-email/ui/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as SignUpWithEmailForm } from './sign-up-with-email-form.ui'; diff --git a/apps/web/src/hooks/useProjects.ts b/apps/web/src/features/auth/sign-up-with-email/ui/sign-up-with-email-form.ui.spec.tsx similarity index 69% rename from apps/web/src/hooks/useProjects.ts rename to apps/web/src/features/auth/sign-up-with-email/ui/sign-up-with-email-form.ui.spec.tsx index 58c25b9a3..88749f53b 100644 --- a/apps/web/src/hooks/useProjects.ts +++ b/apps/web/src/features/auth/sign-up-with-email/ui/sign-up-with-email-form.ui.spec.tsx @@ -13,12 +13,13 @@ * License for the specific language governing permissions and limitations * under the License. */ -import useOAIQuery from './useOAIQuery'; -const useProjects = () => { - return useOAIQuery({ - path: '/api/admin/projects', - variables: { limit: 1000, page: 1 } as any, +import { render } from '@/test-utils'; +import SignUpWithEmailForm from './sign-up-with-email-form.ui'; + +describe('SignUpWithEmailForm', () => { + test('match snapshot', () => { + const component = render(); + expect(component.container).toMatchSnapshot(); }); -}; -export default useProjects; +}); diff --git a/apps/web/src/features/auth/sign-up-with-email/ui/sign-up-with-email-form.ui.tsx b/apps/web/src/features/auth/sign-up-with-email/ui/sign-up-with-email-form.ui.tsx new file mode 100644 index 000000000..ff95321b1 --- /dev/null +++ b/apps/web/src/features/auth/sign-up-with-email/ui/sign-up-with-email-form.ui.tsx @@ -0,0 +1,244 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useState } from 'react'; +import { useRouter } from 'next/router'; +import { zodResolver } from '@hookform/resolvers/zod'; +import dayjs from 'dayjs'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useInterval } from 'react-use'; +import type { z } from 'zod'; + +import { TextInput, toast } from '@ufb/ui'; + +import { Path, useOAIMutation } from '@/shared'; + +import { signUpWithEmailSchema } from '../sign-up-with-email.schema'; + +type FormType = z.infer; + +interface IProps {} + +const SignUpWithEmailForm: React.FC = () => { + const { t } = useTranslation(); + const router = useRouter(); + + const { + handleSubmit, + register, + formState, + getValues, + setValue, + watch, + setError, + clearErrors, + } = useForm({ + resolver: zodResolver(signUpWithEmailSchema), + defaultValues: { emailState: 'NOT_VERIFIED' }, + }); + + const [expiredTime, setExpiredTime] = useState(); + const [leftTime, setLeftTime] = useState(''); + + const { mutate: signUp, status: signUpStatus } = useOAIMutation({ + method: 'post', + path: '/api/admin/auth/signUp/email', + queryOptions: { + async onSuccess() { + await router.push(Path.SIGN_IN); + toast.positive({ title: 'Success' }); + }, + onError(error) { + const { code, message } = error; + toast.negative({ title: message, description: code }); + }, + }, + }); + + const { mutate: fetchCode, status: fetchCodeStatus } = useOAIMutation({ + method: 'post', + path: '/api/admin/auth/email/code', + queryOptions: { + onSuccess(data) { + setValue('emailState', 'VERIFING'); + setExpiredTime(data?.expiredAt); + clearErrors('email'); + toast.positive({ title: 'Success' }); + }, + onError(error) { + const { message } = error; + setError('email', { message }); + toast.negative({ title: message }); + }, + }, + }); + + const { mutate: verifyCode, status: verifyCodeStatus } = useOAIMutation({ + method: 'post', + path: '/api/admin/auth/email/code/verify', + queryOptions: { + onSuccess() { + setValue('emailState', 'VERIFIED'); + clearErrors('code'); + toast.positive({ title: 'Success' }); + }, + onError(error) { + const { message } = error; + setError('code', { message }); + toast.negative({ title: message }); + }, + }, + }); + + useInterval( + () => { + const seconds = dayjs(expiredTime).diff(dayjs(), 'seconds'); + + if (seconds < 0) { + setLeftTime(`00:00`); + setValue('emailState', 'EXPIRED'); + } else { + const m = Math.floor(seconds / 60) + .toString() + .padStart(2, '0'); + const s = Math.floor(seconds % 60) + .toString() + .padStart(2, '0'); + + setLeftTime(`${m}:${s}`); + } + }, + watch('emailState') === 'VERIFING' ? 1000 : null, + ); + + return ( +
signUp(data))}> +
+ fetchCode({ email: getValues('email') })} + disabled={ + watch('emailState') === 'VERIFIED' || + fetchCodeStatus === 'pending' + } + > + {t('auth.sign-up.button.request-auth-code')} + + } + required + /> + {watch('emailState') !== 'NOT_VERIFIED' && ( + + {(watch('emailState') === 'VERIFING' || + watch('emailState') === 'EXPIRED') && ( +

{leftTime}

+ )} + +
+ } + required + /> + )} + + +
+
+ + +
+ + ); +}; + +export default SignUpWithEmailForm; diff --git a/apps/web/src/features/create-api-key/create-api-key-button.ui.tsx b/apps/web/src/features/create-api-key/create-api-key-button.ui.tsx new file mode 100644 index 000000000..59283a10e --- /dev/null +++ b/apps/web/src/features/create-api-key/create-api-key-button.ui.tsx @@ -0,0 +1,61 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; + +import { toast } from '@ufb/ui'; + +import { useOAIMutation, usePermissions } from '@/shared'; + +interface IProps { + projectId: number; +} + +const CreateApiKeyButton: React.FC = (props) => { + const { projectId } = props; + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const perms = usePermissions(projectId); + + const { mutate: createApiKey, isPending } = useOAIMutation({ + method: 'post', + path: '/api/admin/projects/{projectId}/api-keys', + pathParams: { projectId }, + queryOptions: { + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['/api/admin/projects/{projectId}/api-keys'], + }); + toast.positive({ title: t('toast.add') }); + }, + onError(error) { + toast.negative({ title: error.message }); + }, + }, + }); + + return ( + + ); +}; + +export default CreateApiKeyButton; diff --git a/apps/web/src/features/create-api-key/index.ts b/apps/web/src/features/create-api-key/index.ts new file mode 100644 index 000000000..0a1a349dd --- /dev/null +++ b/apps/web/src/features/create-api-key/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as CreateApiKeyButton } from './create-api-key-button.ui'; diff --git a/apps/web/src/features/create-channel/create-channel-model.ts b/apps/web/src/features/create-channel/create-channel-model.ts new file mode 100644 index 000000000..2fbd4b579 --- /dev/null +++ b/apps/web/src/features/create-channel/create-channel-model.ts @@ -0,0 +1,143 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +import type { ChannelImageConfig, ChannelInfo } from '@/entities/channel'; +import type { FieldInfo } from '@/entities/field'; + +import type { CreateChannelStepKey } from './create-channel-type'; +import { + CREATE_CHANNEL_STEP_KEY_LIST, + FIRST_CREATE_CHANNEL_STEP, + LAST_CREATE_CHANNEL_STEP, +} from './create-channel-type'; + +const DEFAULT_FIELDS: FieldInfo[] = [ + { + format: 'number', + property: 'READ_ONLY', + status: 'ACTIVE', + name: 'ID', + key: 'id', + description: '', + }, + { + format: 'date', + property: 'READ_ONLY', + status: 'ACTIVE', + name: 'Created', + key: 'createdAt', + description: '', + }, + { + format: 'date', + property: 'READ_ONLY', + status: 'ACTIVE', + name: 'Updated', + key: 'updatedAt', + description: '', + }, + { + format: 'multiSelect', + property: 'EDITABLE', + status: 'ACTIVE', + name: 'Issue', + key: 'issues', + description: '', + options: [], + }, +]; + +interface Input { + channelInfo: ChannelInfo; + fields: FieldInfo[]; + fieldPreview: null; + imageConfig: ChannelImageConfig; +} + +interface State { + editingStep: number; + currentStep: number; + input: Input; +} + +interface Action { + jumpStepByKey: (key: CreateChannelStepKey) => void; + jumpStep: (step: number) => void; + prevStep: () => void; + nextStep: () => void; + reset: () => void; + getCurrentStepKey: () => CreateChannelStepKey; + onChangeInput: (key: T, value: Input[T]) => void; +} + +const DEFAULT_STATE: State = { + editingStep: 0, + currentStep: 0, + input: { + channelInfo: { name: '', description: '' }, + fields: DEFAULT_FIELDS, + fieldPreview: null, + imageConfig: { + accessKeyId: '', + bucket: '', + endpoint: '', + region: '', + secretAccessKey: '', + domainWhiteList: null, + }, + }, +}; + +export const useCreateChannelStore = create()( + persist( + (set, get) => ({ + ...DEFAULT_STATE, + onChangeInput: (key: T, value: Input[T]) => { + set(({ input }) => ({ input: { ...input, [key]: value } })); + }, + getCurrentStepKey() { + const { currentStep } = get(); + return ( + CREATE_CHANNEL_STEP_KEY_LIST[currentStep] ?? + CREATE_CHANNEL_STEP_KEY_LIST[0] + ); + }, + jumpStepByKey(key) { + set({ currentStep: CREATE_CHANNEL_STEP_KEY_LIST.indexOf(key) }); + }, + jumpStep(step: number) { + set({ currentStep: step }); + }, + nextStep() { + set(({ currentStep, editingStep }) => ({ + currentStep: Math.min(currentStep + 1, LAST_CREATE_CHANNEL_STEP), + editingStep: Math.max(editingStep, currentStep + 1), + })); + }, + prevStep() { + set(({ currentStep }) => ({ + currentStep: Math.max(currentStep - 1, FIRST_CREATE_CHANNEL_STEP), + })); + }, + reset() { + set({ ...DEFAULT_STATE }); + }, + }), + { name: 'create-channel' }, + ), +); diff --git a/apps/web/src/features/create-channel/create-channel-type.tsx b/apps/web/src/features/create-channel/create-channel-type.tsx new file mode 100644 index 000000000..aad4a061b --- /dev/null +++ b/apps/web/src/features/create-channel/create-channel-type.tsx @@ -0,0 +1,27 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export const CREATE_CHANNEL_STEP_KEY_LIST = [ + 'channel-info', + 'field', + 'image-config', + 'field-preview', +] as const; + +export const LAST_CREATE_CHANNEL_STEP = CREATE_CHANNEL_STEP_KEY_LIST.length; +export const FIRST_CREATE_CHANNEL_STEP = 0; + +export type CreateChannelStepKey = + (typeof CREATE_CHANNEL_STEP_KEY_LIST)[number]; diff --git a/apps/web/src/features/create-channel/create-channel.constant.tsx b/apps/web/src/features/create-channel/create-channel.constant.tsx new file mode 100644 index 000000000..d1ba9df87 --- /dev/null +++ b/apps/web/src/features/create-channel/create-channel.constant.tsx @@ -0,0 +1,53 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { Trans } from 'next-i18next'; + +import type { CreateChannelStepKey } from './create-channel-type'; +import InputChannelInfoStep from './ui/input-channel-info-step.ui'; +import InputFieldPreviewStep from './ui/input-field-preview-step.ui'; +import InputFieldStep from './ui/input-field-step.ui'; +import InputImageConfigStep from './ui/input-image-config-step.ui'; + +export const CREATE_CHANNEL_COMPONENTS: Record< + CreateChannelStepKey, + React.ReactNode +> = { + 'channel-info': , + field: , + 'field-preview': , + 'image-config': , +}; + +export const CREATE_CHANNEL_STEPPER_TEXT: Record< + CreateChannelStepKey, + React.ReactNode +> = { + 'channel-info': , + field: , + 'field-preview': , + 'image-config': , +}; + +export const CREATE_PROJECT_HELP_TEXT: Record< + CreateChannelStepKey, + React.ReactNode +> = { + 'channel-info': , + field: , + 'field-preview': , + 'image-config': , +}; diff --git a/apps/web/src/features/create-channel/index.ts b/apps/web/src/features/create-channel/index.ts new file mode 100644 index 000000000..43fa3d865 --- /dev/null +++ b/apps/web/src/features/create-channel/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export * from './ui'; diff --git a/apps/web/src/features/create-channel/ui/create-channel-input-template.ui.tsx b/apps/web/src/features/create-channel/ui/create-channel-input-template.ui.tsx new file mode 100644 index 000000000..d17b0540a --- /dev/null +++ b/apps/web/src/features/create-channel/ui/create-channel-input-template.ui.tsx @@ -0,0 +1,129 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useRouter } from 'next/router'; +import { useQueryClient } from '@tanstack/react-query'; +import { useOverlay } from '@toss/use-overlay'; +import { useTranslation } from 'react-i18next'; + +import { ErrorCode } from '@ufb/shared'; +import { Popover, PopoverModalContent, toast } from '@ufb/ui'; + +import { CreateInputTemplate, Path, useOAIMutation } from '@/shared'; +import { isDefaultField } from '@/entities/field'; + +import { useCreateChannelStore } from '../create-channel-model'; +import { CREATE_CHANNEL_STEP_KEY_LIST } from '../create-channel-type'; +import { CREATE_CHANNEL_STEPPER_TEXT } from '../create-channel.constant'; + +interface IProps extends React.PropsWithChildren { + actionButton?: React.ReactNode; + validate?: () => Promise | boolean; + disableNextBtn?: boolean; + isLoading?: boolean; +} + +const CreateChannelInputTemplate: React.FC = (props) => { + const { children, actionButton, validate, disableNextBtn, isLoading } = props; + + const { t } = useTranslation(); + + const { + currentStep, + nextStep, + prevStep, + getCurrentStepKey, + input, + reset, + jumpStepByKey, + } = useCreateChannelStore(); + + const router = useRouter(); + const projectId = Number(router.query.projectId); + const queryClient = useQueryClient(); + + const overlay = useOverlay(); + + const openCreateChannelError = () => { + return overlay.open(({ isOpen, close }) => ( + close()}> + jumpStepByKey('channel-info'), + }} + /> + + )); + }; + + const { mutate } = useOAIMutation({ + method: 'post', + path: '/api/admin/projects/{projectId}/channels', + pathParams: { projectId }, + queryOptions: { + async onSuccess(data) { + await router.replace({ + pathname: Path.CREATE_CHANNEL_COMPLETE, + query: { projectId, channelId: data?.id }, + }); + reset(); + await queryClient.invalidateQueries({ + queryKey: ['/api/admin/projects/{projectId}/channels'], + }); + }, + onError(error) { + if (error.code === ErrorCode.Channel.ChannelAlreadyExists) { + openCreateChannelError(); + } else { + toast.negative({ title: error.message }); + } + }, + }, + }); + + const onComplete = () => { + if (projectId < 1) { + alert('Invalid Project id'); + return; + } + + mutate({ + ...input.channelInfo, + fields: input.fields.filter((v) => !isDefaultField(v)), + imageConfig: input.imageConfig, + }); + }; + + return ( + + {children} + + ); +}; + +export default CreateChannelInputTemplate; diff --git a/apps/web/src/features/create-channel/ui/create-channel.ui.tsx b/apps/web/src/features/create-channel/ui/create-channel.ui.tsx new file mode 100644 index 000000000..47d51dccc --- /dev/null +++ b/apps/web/src/features/create-channel/ui/create-channel.ui.tsx @@ -0,0 +1,49 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { CreateTemplate } from '@/shared'; + +import { useCreateChannelStore } from '../create-channel-model'; +import { CREATE_CHANNEL_STEP_KEY_LIST } from '../create-channel-type'; +import { + CREATE_CHANNEL_COMPONENTS, + CREATE_CHANNEL_STEPPER_TEXT, + CREATE_PROJECT_HELP_TEXT, +} from '../create-channel.constant'; + +interface IProps {} + +const CreateProject: React.FC = () => { + const { currentStep, editingStep, getCurrentStepKey } = + useCreateChannelStore(); + + const currentStepKey = getCurrentStepKey(); + + return ( + + {CREATE_CHANNEL_COMPONENTS[currentStepKey]} + + ); +}; + +export default CreateProject; diff --git a/apps/web/src/features/create-channel/ui/index.ts b/apps/web/src/features/create-channel/ui/index.ts new file mode 100644 index 000000000..a06fe5f88 --- /dev/null +++ b/apps/web/src/features/create-channel/ui/index.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as RouteCreateChannelButton } from './route-create-channel-button.ui'; +export { default as CreateChannel } from './create-channel.ui'; diff --git a/apps/web/src/features/create-channel/ui/input-channel-info-step.ui.tsx b/apps/web/src/features/create-channel/ui/input-channel-info-step.ui.tsx new file mode 100644 index 000000000..8f37bf015 --- /dev/null +++ b/apps/web/src/features/create-channel/ui/input-channel-info-step.ui.tsx @@ -0,0 +1,62 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { useEffect } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { EMPTY_FUNCTION } from '@/shared/utils/empty-function'; +import type { ChannelInfo } from '@/entities/channel'; +import { ChannelInfoForm, channelInfoSchema } from '@/entities/channel'; + +import { useCreateChannelStore } from '../create-channel-model'; +import CreateChannelInputTemplate from './create-channel-input-template.ui'; + +interface IProps {} + +const InputChannelInfoStep: React.FC = () => { + const { onChangeInput, input } = useCreateChannelStore(); + + const methods = useForm({ + resolver: zodResolver(channelInfoSchema), + defaultValues: input.channelInfo, + }); + + useEffect(() => { + const subscription = methods.watch((values) => { + const newValues = channelInfoSchema.safeParse(values); + if (!newValues.data) return; + onChangeInput('channelInfo', newValues.data); + }); + return () => subscription.unsubscribe(); + }, []); + + return ( + { + const isValid = await methods.trigger(); + await methods.handleSubmit(EMPTY_FUNCTION)(); + return isValid; + }} + > + + + + + ); +}; + +export default InputChannelInfoStep; diff --git a/apps/web/src/features/create-channel/ui/input-field-preview-step.ui.tsx b/apps/web/src/features/create-channel/ui/input-field-preview-step.ui.tsx new file mode 100644 index 000000000..fd0f9c82e --- /dev/null +++ b/apps/web/src/features/create-channel/ui/input-field-preview-step.ui.tsx @@ -0,0 +1,36 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { PreviewFieldTable } from '@/entities/field'; + +import { useCreateChannelStore } from '../create-channel-model'; +import CreateChannelInputTemplate from './create-channel-input-template.ui'; + +interface IProps {} + +const InputFieldPreviewStep: React.FC = () => { + const { input } = useCreateChannelStore(); + + return ( + + v.status === 'ACTIVE')} + /> + + ); +}; + +export default InputFieldPreviewStep; diff --git a/apps/web/src/features/create-channel/ui/input-field-step.ui.tsx b/apps/web/src/features/create-channel/ui/input-field-step.ui.tsx new file mode 100644 index 000000000..ce4c36a81 --- /dev/null +++ b/apps/web/src/features/create-channel/ui/input-field-step.ui.tsx @@ -0,0 +1,69 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { FieldSettingPopover } from '@/entities/field'; +import type { FieldInfo } from '@/entities/field'; +import FieldTable from '@/entities/field/ui/field-table.ui'; + +import { useCreateChannelStore } from '../create-channel-model'; +import CreateChannelInputTemplate from './create-channel-input-template.ui'; + +interface IProps {} + +const InputFieldStep: React.FC = () => { + const { input, onChangeInput } = useCreateChannelStore(); + + const createField = (field: FieldInfo) => { + onChangeInput('fields', input.fields.concat(field)); + }; + + const updateField = ({ + field, + index, + }: { + index: number; + field: FieldInfo; + }) => { + onChangeInput( + 'fields', + input.fields.map((v, i) => (i === index ? field : v)), + ); + }; + + const deleteField = ({ index }: { index: number }) => { + onChangeInput( + 'fields', + input.fields.filter((_, i) => i !== index), + ); + }; + + return ( + + } + > + + + ); +}; + +export default InputFieldStep; diff --git a/apps/web/src/features/create-channel/ui/input-image-config-step.ui.tsx b/apps/web/src/features/create-channel/ui/input-image-config-step.ui.tsx new file mode 100644 index 000000000..4e61be904 --- /dev/null +++ b/apps/web/src/features/create-channel/ui/input-image-config-step.ui.tsx @@ -0,0 +1,143 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import { toast } from '@ufb/ui'; + +import { useOAIMutation } from '@/shared'; +import { EMPTY_FUNCTION } from '@/shared/utils/empty-function'; +import type { ChannelImageConfig } from '@/entities/channel'; +import { channelImageConfigSchema } from '@/entities/channel'; +import { ImageConfigForm } from '@/entities/channel/ui'; + +import { useCreateChannelStore } from '../create-channel-model'; +import CreateChannelInputTemplate from './create-channel-input-template.ui'; + +interface IProps {} + +const InputImageConfigStep: React.FC = () => { + const { t } = useTranslation(); + const { input, onChangeInput } = useCreateChannelStore(); + + const methods = useForm({ + resolver: zodResolver(channelImageConfigSchema), + defaultValues: input.imageConfig, + }); + + useEffect(() => { + const subscription = methods.watch((values) => { + const newValues = channelImageConfigSchema.safeParse(values); + if (!newValues.data) return; + onChangeInput('imageConfig', newValues.data); + }); + return () => subscription.unsubscribe(); + }, []); + + const router = useRouter(); + const projectId = Number(router.query.projectId); + + const { mutate: testConection } = useOAIMutation({ + method: 'post', + path: '/api/admin/projects/{projectId}/channels/image-upload-url-test', + pathParams: { projectId }, + queryOptions: { + onSuccess(data) { + if (data?.success) { + toast.accent({ title: 'Test Connection Success' }); + } else { + methods.setError('accessKeyId', { message: '' }); + methods.setError('bucket', { message: '' }); + methods.setError('endpoint', { message: '' }); + methods.setError('region', { message: '' }); + methods.setError('root', { message: '' }); + methods.setError('secretAccessKey', { message: '' }); + toast.negative({ title: 'Test Connection failed' }); + } + }, + onError() { + toast.negative({ title: 'Test Connection failed' }); + }, + }, + }); + + const handleTestConnection = async () => { + let isError = false; + await methods.handleSubmit(EMPTY_FUNCTION)(); + const { accessKeyId, bucket, endpoint, region, secretAccessKey } = + methods.getValues(); + if (accessKeyId.length === 0) { + methods.setError('accessKeyId', { message: t('hint.required') }); + isError = true; + } + if (bucket.length === 0) { + methods.setError('bucket', { message: t('hint.required') }); + isError = true; + } + if (endpoint.length === 0) { + methods.setError('endpoint', { message: t('hint.required') }); + isError = true; + } + if (region.length === 0) { + methods.setError('region', { message: t('hint.required') }); + isError = true; + } + if (secretAccessKey.length === 0) { + methods.setError('secretAccessKey', { message: t('hint.required') }); + isError = true; + } + if (isError) return; + testConection(input.imageConfig); + }; + + const validate = async () => { + const isValid = await methods.trigger(); + await methods.handleSubmit(EMPTY_FUNCTION)(); + return isValid; + }; + + return ( + + Test Connection + + } + > +
+
+

+ {t('title-box.image-storage-integration')} +

+
+ + + +
+
+ ); +}; + +export default InputImageConfigStep; diff --git a/apps/web/src/containers/buttons/CreateChannelButton/CreateChannelButton.tsx b/apps/web/src/features/create-channel/ui/route-create-channel-button.ui.tsx similarity index 68% rename from apps/web/src/containers/buttons/CreateChannelButton/CreateChannelButton.tsx rename to apps/web/src/features/create-channel/ui/route-create-channel-button.ui.tsx index 4965b04b8..d1a4b706c 100644 --- a/apps/web/src/containers/buttons/CreateChannelButton/CreateChannelButton.tsx +++ b/apps/web/src/features/create-channel/ui/route-create-channel-button.ui.tsx @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ + import { useState } from 'react'; import { useRouter } from 'next/router'; import { useTranslation } from 'react-i18next'; @@ -26,14 +27,10 @@ import { TooltipTrigger, } from '@ufb/ui'; -import { - CREATE_CHANNEL_COMPLETE_STEP_INDEX_KEY, - CREATE_CHANNEL_CURRENT_STEP_KEY, - CREATE_CHANNEL_INPUT_KEY, -} from '@/constants/local-storage-key'; -import { Path } from '@/constants/path'; -import { CHANNEL_STEPS } from '@/contexts/create-channel.context'; -import { useLocalStorage, usePermissions } from '@/hooks'; +import { cn, Path, usePermissions } from '@/shared'; + +import { useCreateChannelStore } from '../create-channel-model'; +import { CREATE_CHANNEL_STEP_KEY_LIST } from '../create-channel-type'; interface IProps { projectId: number; @@ -41,7 +38,7 @@ interface IProps { placement?: 'top' | 'bottom'; } -const CreateChannelButton: React.FC = (props) => { +const RouteCreateChannelButton: React.FC = (props) => { const { projectId, type, placement } = props; const { t } = useTranslation(); @@ -49,10 +46,7 @@ const CreateChannelButton: React.FC = (props) => { const perms = usePermissions(projectId); const [open, setOpen] = useState(false); - const [step] = useLocalStorage( - CREATE_CHANNEL_COMPLETE_STEP_INDEX_KEY(projectId), - 0, - ); + const { editingStep, reset, jumpStep } = useCreateChannelStore(); const goToCreateChannel = () => router.push({ pathname: Path.CREATE_CHANNEL, query: { projectId } }); @@ -65,16 +59,16 @@ const CreateChannelButton: React.FC = (props) => {

{t('text.no-channel')}

)} - 0} placement={placement ?? 'bottom'}> + 0} placement={placement ?? 'bottom'}> + } + > + + + ); +}; + +export default InputApiKeyStep; diff --git a/apps/web/src/features/create-project/ui/input-issue-tracker-step.ui.tsx b/apps/web/src/features/create-project/ui/input-issue-tracker-step.ui.tsx new file mode 100644 index 000000000..6f19e6d39 --- /dev/null +++ b/apps/web/src/features/create-project/ui/input-issue-tracker-step.ui.tsx @@ -0,0 +1,61 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useEffect } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { EMPTY_FUNCTION } from '@/shared/utils/empty-function'; +import { IssueTrackerForm, issueTrackerSchema } from '@/entities/issue-tracker'; +import type { IssueTracker } from '@/entities/issue-tracker'; + +import { useCreateProjectStore } from '../create-project-model'; +import CreateProjectInputTemplate from './create-project-input-template.ui'; + +interface IProps {} + +const InputIssueTrackerStep: React.FC = () => { + const { input, onChangeInput } = useCreateProjectStore(); + + const methods = useForm({ + resolver: zodResolver(issueTrackerSchema), + defaultValues: input.issueTracker, + }); + + useEffect(() => { + const subscription = methods.watch((values) => { + const newValues = issueTrackerSchema.safeParse(values); + if (!newValues.data) return; + onChangeInput('issueTracker', newValues.data); + }); + return () => subscription.unsubscribe(); + }, []); + + return ( + { + const isValid = await methods.trigger(); + await methods.handleSubmit(EMPTY_FUNCTION)(); + return isValid; + }} + > + + + + + ); +}; + +export default InputIssueTrackerStep; diff --git a/apps/web/src/features/create-project/ui/input-members-step.ui.tsx b/apps/web/src/features/create-project/ui/input-members-step.ui.tsx new file mode 100644 index 000000000..79729ca00 --- /dev/null +++ b/apps/web/src/features/create-project/ui/input-members-step.ui.tsx @@ -0,0 +1,118 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useOverlay } from '@toss/use-overlay'; +import { useTranslation } from 'react-i18next'; + +import { Popover, PopoverModalContent } from '@ufb/ui'; + +import type { Member } from '@/entities/member'; +import { CreateMemberPopover, MemberTable } from '@/entities/member'; +import type { Role } from '@/entities/role'; +import { useUserSearch } from '@/entities/user'; +import type { User } from '@/entities/user'; + +import { useCreateProjectStore } from '../create-project-model'; +import CreateProjectInputTemplate from './create-project-input-template.ui'; + +interface IProps {} + +const InputMembersStep: React.FC = () => { + const { input, onChangeInput } = useCreateProjectStore(); + const overlay = useOverlay(); + + const { t } = useTranslation(); + const { data: userData } = useUserSearch({ + limit: 1000, + query: { type: 'GENERAL' }, + }); + + const createMember = (user: User, role: Role) => { + onChangeInput( + 'members', + input.members.concat({ + id: (input.members[input.members.length - 1]?.id ?? 0) + 1, + user, + role, + createdAt: new Date().toISOString(), + }), + ); + }; + + const updateMember = (member: Member) => { + onChangeInput( + 'members', + input.members.map((m) => (m.id === member.id ? member : m)), + ); + }; + + const deleteMember = (memberId: number) => { + onChangeInput( + 'members', + input.members.filter((m) => m.id !== memberId), + ); + }; + + const openInvalidateMemberModal = () => { + return overlay.open(({ isOpen, close }) => ( + close()}> + + + )); + }; + + const validate = () => { + if (!userData) return false; + if ( + !input.members.every((member) => + userData.items.some((user) => user.id === member.user.id), + ) + ) { + openInvalidateMemberModal(); + return false; + } + return true; + }; + + return ( + + } + validate={() => validate()} + > + + + ); +}; + +export default InputMembersStep; diff --git a/apps/web/src/features/create-project/ui/input-project-info-step.ui.tsx b/apps/web/src/features/create-project/ui/input-project-info-step.ui.tsx new file mode 100644 index 000000000..80760ff25 --- /dev/null +++ b/apps/web/src/features/create-project/ui/input-project-info-step.ui.tsx @@ -0,0 +1,73 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useEffect } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { client } from '@/shared'; +import { EMPTY_FUNCTION } from '@/shared/utils/empty-function'; +import type { ProjectInfo } from '@/entities/project'; +import { ProjectInfoForm, projectInfoSchema } from '@/entities/project'; + +import { useCreateProjectStore } from '../create-project-model'; +import CreateProjectInputTemplate from './create-project-input-template.ui'; + +interface IProps {} + +const InputProjectInfo: React.FC = () => { + const { onChangeInput, input } = useCreateProjectStore(); + + const methods = useForm({ + resolver: zodResolver(projectInfoSchema), + defaultValues: input.projectInfo, + }); + + useEffect(() => { + const subscription = methods.watch((values) => { + const newValues = projectInfoSchema.safeParse(values); + if (!newValues.data) return; + onChangeInput('projectInfo', newValues.data); + }); + return () => subscription.unsubscribe(); + }, []); + + const validate = async () => { + const isValid = await methods.trigger(); + await methods.handleSubmit(EMPTY_FUNCTION)(); + const response = await client.get({ + path: '/api/admin/projects/name-check', + query: { name: methods.getValues('name') }, + }); + + const isDuplicated = response.data as unknown as boolean; + + if (isDuplicated) { + methods.setError('name', { message: 'Duplicated name' }); + return false; + } + return isValid; + }; + + return ( + + + + + + ); +}; + +export default InputProjectInfo; diff --git a/apps/web/src/features/create-project/ui/input-roles-step.ui.tsx b/apps/web/src/features/create-project/ui/input-roles-step.ui.tsx new file mode 100644 index 000000000..c8c932618 --- /dev/null +++ b/apps/web/src/features/create-project/ui/input-roles-step.ui.tsx @@ -0,0 +1,68 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { Role } from '@/entities/role'; +import { CreateRolePopover, RoleTable } from '@/entities/role'; + +import { useCreateProjectStore } from '../create-project-model'; +import CreateProjectInputTemplate from './create-project-input-template.ui'; + +interface IProps {} + +const InputRolesStep: React.FC = () => { + const { onChangeInput, input } = useCreateProjectStore(); + + const onCreateRole = (name: string) => { + onChangeInput( + 'roles', + input.roles.concat({ + id: (input.roles[input.roles.length - 1]?.id ?? 0) + 1, + name, + permissions: [], + }), + ); + }; + + const onUpdateRole = (role: Role) => { + onChangeInput( + 'roles', + input.roles.map((v) => (v.id === role.id ? role : v)), + ); + }; + + const onDeleteRole = (role: Role) => { + onChangeInput( + 'roles', + input.roles.filter((v) => v.id !== role.id), + ); + }; + + return ( + + } + disableNextBtn={input.roles.length === 0} + > + + + ); +}; + +export default InputRolesStep; diff --git a/apps/web/src/containers/buttons/CreateProjectButton/CreateProjectButton.tsx b/apps/web/src/features/create-project/ui/route-create-project-button.ui.tsx similarity index 62% rename from apps/web/src/containers/buttons/CreateProjectButton/CreateProjectButton.tsx rename to apps/web/src/features/create-project/ui/route-create-project-button.ui.tsx index 6ab1cd3f2..bdb5b68c0 100644 --- a/apps/web/src/containers/buttons/CreateProjectButton/CreateProjectButton.tsx +++ b/apps/web/src/features/create-project/ui/route-create-project-button.ui.tsx @@ -26,40 +26,35 @@ import { TooltipTrigger, } from '@ufb/ui'; -import { - CREATE_PROJECT_COMPLETE_STEP_INDEX_KEY, - CREATE_PROJECT_CURRENT_STEP_KEY, - CREATE_PROJECT_INPUT_KEY, -} from '@/constants/local-storage-key'; -import { Path } from '@/constants/path'; -import { PROJECT_STEPS } from '@/contexts/create-project.context'; -import { useUser } from '@/contexts/user.context'; -import { useLocalStorage } from '@/hooks'; +import { Path } from '@/shared'; +import { useUserStore } from '@/entities/user'; + +import { useCreateProjectStore } from '../create-project-model'; +import { CREATE_PROJECT_STEP_KEY_LIST } from '../create-project-type'; interface IProps { hasProject?: boolean; } -const CreateProjectButton: React.FC = ({ hasProject }) => { +const RouteCreateProjectButton: React.FC = ({ hasProject }) => { const { t } = useTranslation(); - const [step] = useLocalStorage(CREATE_PROJECT_COMPLETE_STEP_INDEX_KEY, 0); const router = useRouter(); const [open, setOpen] = useState(false); - const { user } = useUser(); + const { user } = useUserStore(); - const goToCreateProjectPage = () => router.push(Path.CREATE_PROJECT); + const { editingStep, reset, jumpStep } = useCreateProjectStore(); return ( <> - 0} placement="bottom"> + 0} placement="bottom"> - 0 ? 'red' : 'blue'}> - {step > 0 ? + 0 ? 'red' : 'blue'}> + {editingStep > 0 ? <> {t('text.create-project-in-progress')}{' '} - ({step + 1}/{PROJECT_STEPS.length}) + ({editingStep + 1}/{CREATE_PROJECT_STEP_KEY_LIST.length}) : t('main.index.no-project')} @@ -85,15 +80,16 @@ const CreateProjectButton: React.FC = ({ hasProject }) => { submitButton={{ children: t('dialog.continue.button.continue'), className: 'btn-red', - onClick: () => goToCreateProjectPage(), + onClick: async () => { + jumpStep(editingStep); + await router.push(Path.CREATE_PROJECT); + }, }} cancelButton={{ children: t('dialog.continue.button.restart'), - onClick: () => { - localStorage.removeItem(CREATE_PROJECT_INPUT_KEY); - localStorage.removeItem(CREATE_PROJECT_CURRENT_STEP_KEY); - localStorage.removeItem(CREATE_PROJECT_COMPLETE_STEP_INDEX_KEY); - goToCreateProjectPage(); + onClick: async () => { + reset(); + await router.push(Path.CREATE_PROJECT); }, }} /> @@ -102,4 +98,4 @@ const CreateProjectButton: React.FC = ({ hasProject }) => { ); }; -export default CreateProjectButton; +export default RouteCreateProjectButton; diff --git a/apps/web/src/components/etc/DescriptionTooltip/DescriptionTooltip.spec.tsx b/apps/web/src/features/create-tenant/create-tenant-form.schema.ts similarity index 85% rename from apps/web/src/components/etc/DescriptionTooltip/DescriptionTooltip.spec.tsx rename to apps/web/src/features/create-tenant/create-tenant-form.schema.ts index 240e0699c..12cdf24c0 100644 --- a/apps/web/src/components/etc/DescriptionTooltip/DescriptionTooltip.spec.tsx +++ b/apps/web/src/features/create-tenant/create-tenant-form.schema.ts @@ -13,9 +13,8 @@ * License for the specific language governing permissions and limitations * under the License. */ +import { z } from 'zod'; -describe('DescriptionTooltip', () => { - it('renders correctly', () => { - expect(true).toBeTruthy(); - }); +export const createTenantFormSchema = z.object({ + siteName: z.string().min(2), }); diff --git a/apps/web/src/features/create-tenant/create-tenant-form.spec.tsx b/apps/web/src/features/create-tenant/create-tenant-form.spec.tsx new file mode 100644 index 000000000..fab148fd3 --- /dev/null +++ b/apps/web/src/features/create-tenant/create-tenant-form.spec.tsx @@ -0,0 +1,82 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import userEvent from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; + +import { env } from '@/env'; +import { server } from '@/msw'; +import { render, screen, waitFor } from '@/test-utils'; +import CreateTenantForm from './create-tenant-form.ui'; +import { DEFAULT_SUPER_ACCOUNT } from './default-super-account.constant'; + +describe('CreateTenantForm', () => { + test('An input length should be at least 3', async () => { + render(); + const input = screen.getByPlaceholderText('Please enter the site name'); + const submitBtn = screen.getByRole('button'); + + await userEvent.type(input, faker.string.alphanumeric(1)); + + expect(submitBtn).toBeDisabled(); + }); + + test('On Success', async () => { + server.use( + http.post(`${env.NEXT_PUBLIC_API_BASE_URL}/api/admin/tenants`, () => { + return HttpResponse.json({}, { status: 200 }); + }), + http.get(`${env.NEXT_PUBLIC_API_BASE_URL}/api/admin/tenants`, () => { + return HttpResponse.json({}, { status: 200 }); + }), + ); + + render(); + const input = screen.getByPlaceholderText('Please enter the site name'); + const submitBtn = screen.getByRole('button'); + + await userEvent.type(input, 'test'); + + expect(submitBtn).not.toBeDisabled(); + + await userEvent.click(submitBtn); + + await waitFor(() => { + expect( + screen.getByText(new RegExp(DEFAULT_SUPER_ACCOUNT.email, 'i')), + ).toBeInTheDocument(); + expect( + screen.getByText(new RegExp(DEFAULT_SUPER_ACCOUNT.password, 'i')), + ).toBeInTheDocument(); + }); + }); + test('On Error', async () => { + server.use( + http.post(`${env.NEXT_PUBLIC_API_BASE_URL}/api/admin/tenants`, () => { + return HttpResponse.json({}, { status: 500 }); + }), + ); + + render(); + const input = screen.getByPlaceholderText('Please enter the site name'); + const submitBtn = screen.getByRole('button'); + + await userEvent.type(input, 'test'); + await userEvent.click(submitBtn); + + await waitFor(() => expect(screen.getByText('Error')).toBeInTheDocument()); + }); +}); diff --git a/apps/web/src/features/create-tenant/create-tenant-form.ui.tsx b/apps/web/src/features/create-tenant/create-tenant-form.ui.tsx new file mode 100644 index 000000000..3a0c6e00d --- /dev/null +++ b/apps/web/src/features/create-tenant/create-tenant-form.ui.tsx @@ -0,0 +1,93 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useRouter } from 'next/router'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useQueryClient } from '@tanstack/react-query'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { z } from 'zod'; + +import { toast } from '@ufb/ui'; + +import { Path, useOAIMutation } from '@/shared'; +import { useTenantStore } from '@/entities/tenant'; + +import { createTenantFormSchema } from './create-tenant-form.schema'; +import { DEFAULT_SUPER_ACCOUNT } from './default-super-account.constant'; + +type FormType = z.infer; + +interface IProps {} + +const CreateTenantForm: React.FC = () => { + const { t } = useTranslation(); + const router = useRouter(); + const queryClient = useQueryClient(); + + const { refetchTenant } = useTenantStore(); + const { register, handleSubmit, formState } = useForm({ + resolver: zodResolver(createTenantFormSchema), + }); + + const { mutate: createTenant, isPending } = useOAIMutation({ + method: 'post', + path: '/api/admin/tenants', + queryOptions: { + async onSuccess() { + await queryClient.invalidateQueries({ + queryKey: ['/api/admin/tenants'], + }); + await router.replace(Path.SIGN_IN); + await refetchTenant(); + toast.positive({ + title: 'Default Super User', + description: `email: ${DEFAULT_SUPER_ACCOUNT.email} \n password: ${DEFAULT_SUPER_ACCOUNT.password}`, + }); + }, + onError(error) { + toast.negative({ title: 'Error', description: error.message }); + }, + }, + }); + + return ( +
createTenant(data))} + > +

{t('tenant.create.title')}

+ + + +
+ ); +}; + +export default CreateTenantForm; diff --git a/apps/web/src/types/timezone-info.ts b/apps/web/src/features/create-tenant/default-super-account.constant.ts similarity index 87% rename from apps/web/src/types/timezone-info.ts rename to apps/web/src/features/create-tenant/default-super-account.constant.ts index 59b6dde6a..0adafdd65 100644 --- a/apps/web/src/types/timezone-info.ts +++ b/apps/web/src/features/create-tenant/default-super-account.constant.ts @@ -13,9 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ - -export type TimezoneInfo = { - countryCode: string; - name: string; - offset: string; +export const DEFAULT_SUPER_ACCOUNT = { + email: 'user@feedback.com', + password: '12345678', }; diff --git a/apps/web/src/features/create-tenant/index.ts b/apps/web/src/features/create-tenant/index.ts new file mode 100644 index 000000000..c66e6a9e8 --- /dev/null +++ b/apps/web/src/features/create-tenant/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as CreateTenantForm } from './create-tenant-form.ui'; diff --git a/apps/web/src/features/delete-channel/index.ts b/apps/web/src/features/delete-channel/index.ts new file mode 100644 index 000000000..43fa3d865 --- /dev/null +++ b/apps/web/src/features/delete-channel/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export * from './ui'; diff --git a/apps/web/src/features/delete-channel/ui/delete-channel-popover.ui.tsx b/apps/web/src/features/delete-channel/ui/delete-channel-popover.ui.tsx new file mode 100644 index 000000000..795db2861 --- /dev/null +++ b/apps/web/src/features/delete-channel/ui/delete-channel-popover.ui.tsx @@ -0,0 +1,77 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + Popover, + PopoverModalContent, + PopoverTrigger, + TextInput, +} from '@ufb/ui'; + +import { usePermissions } from '@/shared'; +import type { Channel } from '@/entities/channel'; + +interface IProps { + projectId: number; + channel: Channel; + onClickDelete: (channelId: number) => void; +} + +const DeleteChannelPopover: React.FC = (props) => { + const { channel, onClickDelete, projectId } = props; + const { t } = useTranslation(); + const perms = usePermissions(projectId); + const [open, setOpen] = useState(false); + const [inputChannelName, setInputChannelName] = useState(''); + + return ( + + setOpen(true)} + disabled={!perms.includes('channel_delete')} + > + {t('button.delete')} + + onClickDelete(channel.id), + }} + > +

{channel.name}

+ setInputChannelName(e.target.value)} + /> +
+
+ ); +}; + +export default DeleteChannelPopover; diff --git a/apps/web/src/features/delete-channel/ui/index.ts b/apps/web/src/features/delete-channel/ui/index.ts new file mode 100644 index 000000000..03b5766d4 --- /dev/null +++ b/apps/web/src/features/delete-channel/ui/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as DeleteChannelPopover } from './delete-channel-popover.ui'; diff --git a/apps/web/src/features/delete-user/__snapshots__/delete-account-button.ui.spec.tsx.snap b/apps/web/src/features/delete-user/__snapshots__/delete-account-button.ui.spec.tsx.snap new file mode 100644 index 000000000..f8da6e0d1 --- /dev/null +++ b/apps/web/src/features/delete-user/__snapshots__/delete-account-button.ui.spec.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DeleteAccountButton match snapshot 1`] = ` +
+ +
+
+`; diff --git a/apps/web/src/features/delete-user/delete-account-button.ui.spec.tsx b/apps/web/src/features/delete-user/delete-account-button.ui.spec.tsx new file mode 100644 index 000000000..15ba9231f --- /dev/null +++ b/apps/web/src/features/delete-user/delete-account-button.ui.spec.tsx @@ -0,0 +1,106 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { faker } from '@faker-js/faker'; +import userEvent from '@testing-library/user-event'; +import { http } from 'msw'; + +import * as user from '@/entities/user'; + +import { server, simpleMockHttp } from '@/msw'; +import { render, screen, waitFor } from '@/test-utils'; +import DeleteAccountButton from './delete-account-button.ui'; + +jest.mock('@/entities/user'); + +const TEST_USER: user.User = { + id: faker.number.int(), + email: faker.internet.email(), + name: faker.string.alphanumeric(8), + type: faker.helpers.arrayElement(['GENERAL', 'SUPER']), + department: faker.helpers.arrayElement([null, faker.string.alphanumeric(8)]), + signUpMethod: 'EMAIL', +}; + +server.use( + http.get('/api/logout', () => { + return; + }), +); + +describe('DeleteAccountButton', () => { + test('match snapshot', () => { + jest.spyOn(user, 'useUserStore').mockImplementation(() => ({ + signInWithEmail: jest.fn(), + _signIn: jest.fn(), + setUser: jest.fn(), + signInWithOAuth: jest.fn(), + signOut: jest.fn(), + })); + const component = render(); + expect(component.container).toMatchSnapshot(); + }); + + describe('Submittion', () => { + const mockSignOut = jest.fn(); + beforeEach(async () => { + jest.spyOn(user, 'useUserStore').mockImplementation(() => ({ + signInWithEmail: jest.fn(), + _signIn: jest.fn(), + setUser: jest.fn(), + signInWithOAuth: jest.fn(), + signOut: mockSignOut, + })); + render(); + const btn = screen.getByRole('button', { + name: 'main.profile.button.delete-account', + }); + await userEvent.click(btn); + }); + + test('on Success', async () => { + simpleMockHttp({ + method: 'delete', + path: '/api/admin/users/{id}', + params: { id: TEST_USER.id }, + }); + + const deleteBtn = screen.getByRole('button', { + name: 'button.delete', + }); + await userEvent.click(deleteBtn); + await waitFor(() => expect(mockSignOut).toHaveBeenCalled()); + }); + test('on Error', async () => { + simpleMockHttp({ + method: 'delete', + path: '/api/admin/users/{id}', + params: { id: TEST_USER.id }, + status: 500, + }); + + const deleteBtn = screen.getByRole('button', { + name: 'button.delete', + }); + await userEvent.click(deleteBtn); + + await waitFor(() => + expect(screen.getByText(new RegExp('error', 'i'))).toBeInTheDocument(), + ); + await waitFor(() => expect(mockSignOut).not.toHaveBeenCalled()); + }); + }); +}); diff --git a/apps/web/src/containers/my-profile/DeleteMyAccountButton.tsx b/apps/web/src/features/delete-user/delete-account-button.ui.tsx similarity index 82% rename from apps/web/src/containers/my-profile/DeleteMyAccountButton.tsx rename to apps/web/src/features/delete-user/delete-account-button.ui.tsx index 43412d6c9..664972e9a 100644 --- a/apps/web/src/containers/my-profile/DeleteMyAccountButton.tsx +++ b/apps/web/src/features/delete-user/delete-account-button.ui.tsx @@ -18,36 +18,40 @@ import { useTranslation } from 'react-i18next'; import { Popover, PopoverModalContent, PopoverTrigger, toast } from '@ufb/ui'; -import { useUser } from '@/contexts/user.context'; -import { useOAIMutation } from '@/hooks'; +import { useOAIMutation } from '@/shared'; +import { useUserStore } from '@/entities/user'; +import type { User } from '@/entities/user'; -interface IProps extends React.PropsWithChildren {} +interface IProps { + user: User; +} -const DeleteMyAccountButton: React.FC = () => { +const DeleteAccountButton: React.FC = ({ user }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { user, signOut } = useUser(); + const { signOut } = useUserStore(); const { mutate, isPending } = useOAIMutation({ method: 'delete', path: '/api/admin/users/{id}', - pathParams: { id: user?.id ?? -1 }, + pathParams: { id: user.id }, queryOptions: { async onSuccess() { await signOut(); setOpen(false); }, onError(error) { - toast.negative({ title: error?.message ?? 'Error' }); + toast.negative({ title: 'Error', description: error.message }); }, }, }); + return ( setOpen(true)} > {t('main.profile.button.delete-account')} @@ -78,4 +82,4 @@ const DeleteMyAccountButton: React.FC = () => { ); }; -export default DeleteMyAccountButton; +export default DeleteAccountButton; diff --git a/apps/web/src/features/delete-user/index.ts b/apps/web/src/features/delete-user/index.ts new file mode 100644 index 000000000..b5cf2d789 --- /dev/null +++ b/apps/web/src/features/delete-user/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as DeleteAccountButton } from './delete-account-button.ui'; diff --git a/apps/web/src/features/invite-user/__snapshots__/user-invitation-form.ui.spec.tsx.snap b/apps/web/src/features/invite-user/__snapshots__/user-invitation-form.ui.spec.tsx.snap new file mode 100644 index 000000000..6cbc725f3 --- /dev/null +++ b/apps/web/src/features/invite-user/__snapshots__/user-invitation-form.ui.spec.tsx.snap @@ -0,0 +1,114 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResetPasswordWithEmailForm match snapshot 1`] = ` +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+`; diff --git a/apps/web/src/features/invite-user/index.ts b/apps/web/src/features/invite-user/index.ts new file mode 100644 index 000000000..890a2e380 --- /dev/null +++ b/apps/web/src/features/invite-user/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as UserInvitationForm } from './user-invitation-form.ui'; diff --git a/apps/web/src/features/invite-user/user-invitation-form.ui.spec.tsx b/apps/web/src/features/invite-user/user-invitation-form.ui.spec.tsx new file mode 100644 index 000000000..4ac87f57f --- /dev/null +++ b/apps/web/src/features/invite-user/user-invitation-form.ui.spec.tsx @@ -0,0 +1,104 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import userEvent from '@testing-library/user-event'; + +import { simpleMockHttp } from '@/msw'; +import { render, screen, waitFor } from '@/test-utils'; +import UserInvitationForm from './user-invitation-form.ui'; + +describe('ResetPasswordWithEmailForm', () => { + test('match snapshot', () => { + const component = render(); + expect(component.container).toMatchSnapshot(); + }); + + test('validation', async () => { + render(); + + const sendEmailBtn = screen.getByRole('button', { + name: 'button.setting', + }); + const passwordInput = screen.getByPlaceholderText( + 'input.placeholder.password', + ); + const confirmPasswordInput = screen.getByPlaceholderText( + 'input.placeholder.confirm-password', + ); + + await userEvent.type(passwordInput, faker.string.alphanumeric(8)); + await userEvent.type(confirmPasswordInput, faker.string.alphanumeric(9)); + + expect(sendEmailBtn).toBeDisabled(); + + await userEvent.clear(passwordInput); + await userEvent.clear(confirmPasswordInput); + + const password = faker.string.alphanumeric(8); + await userEvent.type(passwordInput, password); + await userEvent.type(confirmPasswordInput, password); + + await waitFor(() => expect(sendEmailBtn).not.toBeDisabled()); + }); + describe('Submittion', () => { + beforeEach(async () => { + render(); + + const passwordInput = screen.getByPlaceholderText( + 'input.placeholder.password', + ); + const confirmPasswordInput = screen.getByPlaceholderText( + 'input.placeholder.confirm-password', + ); + const password = faker.string.alphanumeric(8); + await userEvent.type(passwordInput, password); + await userEvent.type(confirmPasswordInput, password); + }); + test('on Success', async () => { + simpleMockHttp({ + method: 'post', + path: '/api/admin/auth/signUp/invitation', + }); + + const submitBtn = screen.getByRole('button', { + name: 'button.setting', + }); + await userEvent.click(submitBtn); + + await waitFor(() => + expect( + screen.getByText(new RegExp('success', 'i')), + ).toBeInTheDocument(), + ); + }); + test('on Error', async () => { + simpleMockHttp({ + method: 'post', + path: '/api/admin/auth/signUp/invitation', + status: 500, + }); + + const submitBtn = screen.getByRole('button', { + name: 'button.setting', + }); + await userEvent.click(submitBtn); + + await waitFor(() => + expect(screen.getByText(new RegExp('error', 'i'))).toBeInTheDocument(), + ); + }); + }); +}); diff --git a/apps/web/src/features/invite-user/user-invitation-form.ui.tsx b/apps/web/src/features/invite-user/user-invitation-form.ui.tsx new file mode 100644 index 000000000..5bfda7c97 --- /dev/null +++ b/apps/web/src/features/invite-user/user-invitation-form.ui.tsx @@ -0,0 +1,108 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useRouter } from 'next/router'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { z } from 'zod'; + +import { TextInput, toast } from '@ufb/ui'; + +import { Path, useOAIMutation } from '@/shared'; + +import { userInvitationSchema } from './user-invitation.schema'; + +type FormType = z.infer; + +interface IProps { + code: string; + email: string; +} + +const UserInvitationForm: React.FC = ({ code, email }) => { + const { t } = useTranslation(); + + const router = useRouter(); + + const { handleSubmit, register, formState } = useForm({ + resolver: zodResolver(userInvitationSchema), + defaultValues: { code, email }, + }); + + const { mutate, isPending } = useOAIMutation({ + method: 'post', + path: '/api/admin/auth/signUp/invitation', + queryOptions: { + async onSuccess() { + await router.push(Path.SIGN_IN); + toast.positive({ title: 'Success' }); + }, + onError(error) { + toast.negative({ title: 'Error', description: error.message }); + }, + }, + }); + + const onSubmit = ({ password, code, email }: FormType) => + mutate({ code, email, password }); + + return ( +
+
+ + + +
+
+ +
+
+ ); +}; + +export default UserInvitationForm; diff --git a/apps/web/src/features/invite-user/user-invitation.schema.ts b/apps/web/src/features/invite-user/user-invitation.schema.ts new file mode 100644 index 000000000..e1caa0784 --- /dev/null +++ b/apps/web/src/features/invite-user/user-invitation.schema.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { z } from 'zod'; + +export const userInvitationSchema = z + .object({ + password: z.string().min(8), + confirmPassword: z.string().min(8), + code: z.string(), + email: z.string().email(), + }) + .refine((schema) => schema.password === schema.confirmPassword, { + message: 'Password not matched', + path: ['confirmPassword'], + }); diff --git a/apps/web/src/features/update-user/__snapshots__/user-profile-form.ui.spec.tsx.snap b/apps/web/src/features/update-user/__snapshots__/user-profile-form.ui.spec.tsx.snap new file mode 100644 index 000000000..e3f16ad88 --- /dev/null +++ b/apps/web/src/features/update-user/__snapshots__/user-profile-form.ui.spec.tsx.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResetPasswordWithEmailForm match snapshot 1`] = ` +
+
+
+

+ main.profile.profile-info +

+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+
+`; diff --git a/apps/web/src/features/update-user/change-password-form.schema.ts b/apps/web/src/features/update-user/change-password-form.schema.ts new file mode 100644 index 000000000..adeeb9d03 --- /dev/null +++ b/apps/web/src/features/update-user/change-password-form.schema.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { z } from 'zod'; + +export const changePasswordFormSchema = z + .object({ + password: z.string().min(8), + newPassword: z.string().min(8), + confirmNewPassword: z.string().min(8), + }) + .refine(({ password, newPassword }) => password !== newPassword, { + message: 'must not equal Password', + path: ['newPassword'], + }) + .refine( + ({ newPassword, confirmNewPassword }) => newPassword === confirmNewPassword, + { + message: 'must equal New Password', + path: ['confirmNewPassword'], + }, + ); diff --git a/apps/web/src/features/update-user/change-password-form.ui.spec.tsx b/apps/web/src/features/update-user/change-password-form.ui.spec.tsx new file mode 100644 index 000000000..a47aaacbb --- /dev/null +++ b/apps/web/src/features/update-user/change-password-form.ui.spec.tsx @@ -0,0 +1,113 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import userEvent from '@testing-library/user-event'; + +import { simpleMockHttp } from '@/msw'; +import { render, screen, waitFor } from '@/test-utils'; +import ChangePasswordForm from './change-password-form.ui'; + +describe('ResetPasswordWithEmailForm', () => { + // test('match snapshot', () => { + // const component = render(); + // expect(component.container).toMatchSnapshot(); + // }); + + // test('validation', async () => { + // render(); + + // const saveBtn = screen.getByRole('button', { + // name: 'button.save', + // }); + // const passwordInput = screen.getByPlaceholderText( + // 'input.placeholder.password', + // ); + // const newPasswordInput = screen.getByPlaceholderText( + // 'main.profile.placeholder.new-password', + // ); + // const confirmPasswordInput = screen.getByPlaceholderText( + // 'main.profile.placeholder.confirm-new-password', + // ); + + // await userEvent.type(passwordInput, faker.string.alphanumeric(8)); + // await userEvent.type(newPasswordInput, faker.string.alphanumeric(9)); + // await userEvent.type(confirmPasswordInput, faker.string.alphanumeric(9)); + + // expect(saveBtn).toBeDisabled(); + + // await userEvent.clear(newPasswordInput); + // await userEvent.clear(confirmPasswordInput); + + // const password = faker.string.alphanumeric(8); + // await userEvent.type(newPasswordInput, password); + // await userEvent.type(confirmPasswordInput, password); + + // await waitFor(() => expect(saveBtn).not.toBeDisabled()); + // }); + describe('Submittion', () => { + beforeEach(async () => { + render(); + + const passwordInput = screen.getByPlaceholderText( + 'input.placeholder.password', + ); + const newPasswordInput = screen.getByPlaceholderText( + 'main.profile.placeholder.new-password', + ); + const confirmPasswordInput = screen.getByPlaceholderText( + 'main.profile.placeholder.confirm-new-password', + ); + await userEvent.type(passwordInput, faker.string.alphanumeric(8)); + + const password = faker.string.alphanumeric(8); + await userEvent.type(newPasswordInput, password); + await userEvent.type(confirmPasswordInput, password); + }); + test('on Success', async () => { + simpleMockHttp({ + method: 'post', + path: '/api/admin/users/password/change', + }); + + const submitBtn = screen.getByRole('button', { + name: 'button.save', + }); + await userEvent.click(submitBtn); + + await waitFor(() => + expect( + screen.getByText(new RegExp('toast.save', 'i')), + ).toBeInTheDocument(), + ); + }); + // test('on Error', async () => { + // simpleMockHttp({ + // method: 'post', + // path: '/api/admin/users/password/change', + // status: 500, + // }); + + // const submitBtn = screen.getByRole('button', { + // name: 'button.save', + // }); + // await userEvent.click(submitBtn); + + // await waitFor(() => + // expect(screen.getByText(new RegExp('error', 'i'))).toBeInTheDocument(), + // ); + // }); + }); +}); diff --git a/apps/web/src/containers/my-profile/ChangePasswordForm.tsx b/apps/web/src/features/update-user/change-password-form.ui.tsx similarity index 69% rename from apps/web/src/containers/my-profile/ChangePasswordForm.tsx rename to apps/web/src/features/update-user/change-password-form.ui.tsx index 59c05a2b9..d05344192 100644 --- a/apps/web/src/containers/my-profile/ChangePasswordForm.tsx +++ b/apps/web/src/features/update-user/change-password-form.ui.tsx @@ -16,63 +16,50 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { z } from 'zod'; +import type { z } from 'zod'; import { ErrorCode } from '@ufb/shared'; import { TextInput, toast } from '@ufb/ui'; -import { useOAIMutation } from '@/hooks'; +import { useOAIMutation } from '@/shared'; -type IForm = { - password: string; - newPassword: string; - confirmNewPassword: string; +import { changePasswordFormSchema } from './change-password-form.schema'; + +type FormType = z.infer; + +const DEFAULT_VALUES: FormType = { + confirmNewPassword: '', + newPassword: '', + password: '', }; -const schema: Zod.ZodType = z - .object({ - password: z.string(), - newPassword: z.string(), - confirmNewPassword: z.string(), - }) - .refine(({ password, newPassword }) => password !== newPassword, { - message: 'must not equal Password', - path: ['newPassword'], - }) - .refine( - ({ newPassword, confirmNewPassword }) => newPassword === confirmNewPassword, - { - message: 'must equal New Password', - path: ['confirmNewPassword'], - }, - ); -interface IProps extends React.PropsWithChildren {} +interface IProps {} const ChangePasswordForm: React.FC = () => { const { t } = useTranslation(); - const { register, handleSubmit, setError, formState, reset } = useForm( - { resolver: zodResolver(schema) }, - ); + const { register, handleSubmit, setError, formState, reset } = + useForm({ + resolver: zodResolver(changePasswordFormSchema), + defaultValues: DEFAULT_VALUES, + }); const { mutate, isPending } = useOAIMutation({ method: 'post', path: '/api/admin/users/password/change', queryOptions: { - async onSuccess() { - toast.accent({ title: t('toast.save') }); - reset({ confirmNewPassword: '', newPassword: '', password: '' }); + onSuccess() { + toast.positive({ title: t('toast.save') }); + reset(); }, onError(error) { - if (typeof error.message === 'string') { - if (error.code === ErrorCode.User.InvalidPassword) { - setError('password', { message: 'Invalid Password' }); - } - toast.negative({ title: error.message as string }); - } - if (Array.isArray(error.message)) { - error.message.forEach((message) => - toast.negative({ title: message }), - ); + if (error.code === ErrorCode.User.InvalidPassword) { + setError('password', { message: 'Invalid Password' }); + toast.negative({ + title: 'Error', + description: error.message, + }); + } else { + toast.negative({ title: 'Error', description: error.message }); } }, }, @@ -85,7 +72,7 @@ const ChangePasswordForm: React.FC = () => { @@ -95,7 +82,7 @@ const ChangePasswordForm: React.FC = () => { id="reset_password" className="flex flex-col gap-6" onSubmit={handleSubmit((data) => { - mutate({ ...data }); + mutate(data); })} > ({ - useTranslation: () => { - return { t: (str) => str }; - }, -})); +export const userProfileFormSchema = z.object({ + name: z.string().nullable(), + department: z.string().nullable(), +}); diff --git a/apps/web/src/features/update-user/user-profile-form.ui.spec.tsx b/apps/web/src/features/update-user/user-profile-form.ui.spec.tsx new file mode 100644 index 000000000..6604fef80 --- /dev/null +++ b/apps/web/src/features/update-user/user-profile-form.ui.spec.tsx @@ -0,0 +1,129 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import userEvent from '@testing-library/user-event'; + +import type { User } from '@/entities/user'; + +import { simpleMockHttp } from '@/msw'; +import { render, screen, waitFor } from '@/test-utils'; +import UserProfileForm from './user-profile-form.ui'; + +const TEST_USER: User = { + id: faker.number.int(), + email: faker.internet.email(), + name: faker.string.alphanumeric(8), + type: faker.helpers.arrayElement(['GENERAL', 'SUPER']), + department: faker.helpers.arrayElement([null, faker.string.alphanumeric(8)]), + signUpMethod: 'EMAIL', +}; + +describe('ResetPasswordWithEmailForm', () => { + test('match snapshot', () => { + const component = render(); + expect(component.container).toMatchSnapshot(); + }); + + test('validation', async () => { + render(); + + const saveBtn = screen.getByRole('button', { + name: 'button.save', + }); + + expect(saveBtn).toBeDisabled(); + + const nameInput = screen.getByPlaceholderText( + 'main.profile.placeholder.name', + ); + const departmentInput = screen.getByPlaceholderText( + 'main.profile.placeholder.department', + ); + + expect(nameInput).toHaveValue(TEST_USER.name); + if (TEST_USER.name) { + await userEvent.clear(nameInput); + await userEvent.type(nameInput, TEST_USER.name); + } + if (TEST_USER.department) { + await userEvent.clear(departmentInput); + await userEvent.type(departmentInput, TEST_USER.department); + } + expect(nameInput).toHaveValue(TEST_USER.name); + + expect(saveBtn).toBeDisabled(); + + await userEvent.clear(nameInput); + await userEvent.clear(departmentInput); + + await userEvent.type(nameInput, faker.string.alphanumeric(8)); + await userEvent.type(departmentInput, faker.string.alphanumeric(8)); + + await waitFor(() => expect(saveBtn).not.toBeDisabled()); + }); + describe('Submittion', () => { + beforeEach(async () => { + render(); + + const nameInput = screen.getByPlaceholderText( + 'main.profile.placeholder.name', + ); + const departmentInput = screen.getByPlaceholderText( + 'main.profile.placeholder.department', + ); + await userEvent.type(nameInput, faker.string.alphanumeric(8)); + await userEvent.type(departmentInput, faker.string.alphanumeric(8)); + }); + test('on Success', async () => { + simpleMockHttp({ + method: 'put', + path: `/api/admin/users/{id}`, + status: 200, + params: { id: TEST_USER.id }, + data: TEST_USER, + }); + + const saveBtn = screen.getByRole('button', { + name: 'button.save', + }); + await userEvent.click(saveBtn); + + await waitFor(() => + expect( + screen.getByText(new RegExp('toast.save', 'i')), + ).toBeInTheDocument(), + ); + await waitFor(() => expect(saveBtn).toBeDisabled()); + }); + test('on Error', async () => { + simpleMockHttp({ + method: 'put', + path: `/api/admin/users/{id}`, + status: 500, + params: { id: TEST_USER.id }, + }); + + const saveBtn = screen.getByRole('button', { + name: 'button.save', + }); + await userEvent.click(saveBtn); + + await waitFor(() => + expect(screen.getByText(new RegExp('error', 'i'))).toBeInTheDocument(), + ); + }); + }); +}); diff --git a/apps/web/src/features/update-user/user-profile-form.ui.tsx b/apps/web/src/features/update-user/user-profile-form.ui.tsx new file mode 100644 index 000000000..386157f7d --- /dev/null +++ b/apps/web/src/features/update-user/user-profile-form.ui.tsx @@ -0,0 +1,107 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { z } from 'zod'; + +import { TextInput, toast } from '@ufb/ui'; + +import { useOAIMutation } from '@/shared'; +import type { User } from '@/entities/user'; + +import { userProfileFormSchema } from './user-profile-form.schema'; + +type FormType = z.infer; + +interface IProps { + user: User; +} + +const UserProfileForm: React.FC = (props) => { + const { user } = props; + + const { t } = useTranslation(); + + const { register, handleSubmit, formState, reset, getValues } = + useForm({ + resolver: zodResolver(userProfileFormSchema), + defaultValues: user, + }); + + const { mutate, isPending } = useOAIMutation({ + method: 'put', + path: '/api/admin/users/{id}', + pathParams: { id: user.id }, + queryOptions: { + onSuccess() { + toast.positive({ title: t('toast.save') }); + reset(getValues()); + }, + onError(error) { + toast.negative({ title: 'Error', description: error.message }); + }, + }, + }); + + return ( +
+
+

{t('main.profile.profile-info')}

+ +
+
+
mutate(data))} + > + + + + + +
+ ); +}; + +export default UserProfileForm; diff --git a/apps/web/src/hooks/index.ts b/apps/web/src/hooks/index.ts deleted file mode 100644 index 5f19cd6e3..000000000 --- a/apps/web/src/hooks/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { default as useOAIQuery } from './useOAIQuery'; -export { default as useOAIMutation } from './useOAIMutation'; -export { default as useFeedbackSearch } from './useFeedbackSearch'; -export { default as useIssueSearch } from './useIssueSearch'; -export { default as useSort } from './useSort'; -export { default as useProjects } from './useProjects'; -export { default as useChannels } from './useChannels'; -export { default as useLocalStorage } from './useLocalStorage'; -export { default as useLocalColumnSetting } from './useLocalColumnSetting'; -export { default as usePermissions } from './usePermissions'; -export { default as useUserSearch } from './useUserSearch'; -export { default as useCurrentProjectId } from './useCurrentProjectId'; -export { default as useDownload } from './useDownload'; -export { default as useTruncatedElement } from './useTruncatedElement'; -export { default as useIssueCount } from './useIssueCount'; -export { default as useLineChartData } from './useLineChartData'; -export { default as useDayCount } from './useDayCount'; - -export { default as useHorizontalScroll } from './useHorizontalScroll'; diff --git a/apps/web/src/hooks/useDayCount.ts b/apps/web/src/hooks/useDayCount.ts deleted file mode 100644 index 9d0ae7c18..000000000 --- a/apps/web/src/hooks/useDayCount.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { useMemo } from 'react'; -import dayjs from 'dayjs'; - -const useDayCount = (from: Date, to: Date) => { - return useMemo(() => dayjs(to).diff(from, 'day') + 1, [from, to]); -}; -export default useDayCount; diff --git a/apps/web/src/hooks/useLocalStorage.ts b/apps/web/src/hooks/useLocalStorage.ts deleted file mode 100644 index eadbeaad2..000000000 --- a/apps/web/src/hooks/useLocalStorage.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright 2023 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import type { Dispatch, SetStateAction } from 'react'; -import { useLocalStorage } from 'react-use'; - -const useLocalStorageWrapper = ( - key: string, - initialValue: T, -): [T, Dispatch>] => { - const [value, setValue] = useLocalStorage(key, initialValue); - return [value as T, setValue as Dispatch>]; -}; -export default useLocalStorageWrapper; diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 3576ba015..197fc4b55 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -17,10 +17,10 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { getIronSession } from 'iron-session'; -import { DEFAULT_LOCALE } from './constants/i18n'; -import type { JwtSession } from './constants/iron-option'; -import { ironOption } from './constants/iron-option'; -import { Path } from './constants/path'; +import { DEFAULT_LOCALE, Path } from '@/shared/constants'; + +import type { JwtSession } from '@/server/iron-option'; +import { ironOption } from './server/iron-option'; export async function middleware(req: NextRequest) { const res = NextResponse.next(); @@ -56,7 +56,7 @@ export async function middleware(req: NextRequest) { if (req.nextUrl.locale === 'default') { const requestPath = `${req.nextUrl.pathname}${req.nextUrl.search}`; - const locale = req.cookies.get('NEXT_LOCALE')?.value || DEFAULT_LOCALE; + const locale = req.cookies.get('NEXT_LOCALE')?.value ?? DEFAULT_LOCALE; return NextResponse.redirect(new URL(`/${locale}${requestPath}`, req.url)); } diff --git a/apps/web/src/msw.ts b/apps/web/src/msw.ts new file mode 100644 index 000000000..a9859c281 --- /dev/null +++ b/apps/web/src/msw.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; + +import { signInWithOAuthMockHandlers } from './features/auth/sign-in-with-oauth/__mocks__/sign-in-with-oauth.mock-handler'; +import type { + OAIMethodPathKeys, + OAIMethods, + OAIPathParameters, +} from './shared'; +import { convertToColonPath, getRequestUrl } from './shared'; + +export const server = setupServer(...signInWithOAuthMockHandlers); + +export const simpleMockHttp = < + TMethod extends OAIMethods, + TPath extends OAIMethodPathKeys, +>({ + method, + path, + status = 200, + params, + data = {}, +}: { + method: TMethod; + path: TPath; + status?: 200 | 201 | 204 | 400 | 401 | 403 | 404 | 500; + params?: OAIPathParameters; + data?: Record; +}) => + server.use( + http[method](`${convertToColonPath(getRequestUrl(path, params))}`, () => + HttpResponse.json(data, { status }), + ), + ); diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index 541aec917..2bc2c41e0 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -13,28 +13,37 @@ * License for the specific language governing permissions and limitations * under the License. */ -import type { ReactElement, ReactNode } from 'react'; -import { useState } from 'react'; -import type { NextPage } from 'next'; +import { useEffect, useState } from 'react'; import type { AppProps } from 'next/app'; import Head from 'next/head'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { DehydratedState } from '@tanstack/react-query'; +import { + HydrationBoundary, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { OverlayProvider } from '@toss/use-overlay'; +import axios from 'axios'; import { appWithTranslation } from 'next-i18next'; import { Toaster } from '@ufb/ui'; -import { TenantProvider } from '@/contexts/tenant.context'; -import { UserProvider } from '@/contexts/user.context'; +import { sessionStorage } from '@/shared'; +import type { Jwt, NextPageWithLayout } from '@/shared/types'; +import { TenantGuard } from '@/entities/tenant'; +import { useUserStore } from '@/entities/user'; + // NOTE: DON'T Change the following import order import 'react-datepicker/dist/react-datepicker.css'; -import '@/styles/react-datepicker.css'; -import './_app.css'; +import '@/shared/styles/react-datepicker.css'; +import '@/shared/styles/global.css'; -export type NextPageWithLayout

= NextPage & { - getLayout?: (page: ReactElement) => ReactNode; -}; +interface PageProps { + dehydratedState?: DehydratedState; +} -type AppPropsWithLayout = AppProps & { +type AppPropsWithLayout = AppProps & { Component: NextPageWithLayout; }; @@ -42,6 +51,18 @@ function App({ Component, pageProps }: AppPropsWithLayout) { const [queryClient] = useState(() => new QueryClient()); const getLayout = Component.getLayout ?? ((page) => page); + const { setUser } = useUserStore(); + + const initializeJwt = async () => { + const { data } = await axios.get<{ jwt?: Jwt }>('/api/jwt'); + if (!data.jwt) return; + sessionStorage.setItem('jwt', data.jwt); + setUser(); + }; + + useEffect(() => { + void initializeJwt(); + }, []); return ( <> @@ -50,12 +71,15 @@ function App({ Component, pageProps }: AppPropsWithLayout) { - - - {getLayout()} - - - + + + + {getLayout()} + + + + {process.env.NODE_ENV === 'development' && } + ); diff --git a/apps/web/src/pages/_document.tsx b/apps/web/src/pages/_document.tsx index 6a6a254e8..8aee32b4d 100644 --- a/apps/web/src/pages/_document.tsx +++ b/apps/web/src/pages/_document.tsx @@ -13,17 +13,10 @@ * License for the specific language governing permissions and limitations * under the License. */ -import type { DocumentContext, DocumentInitialProps } from 'next/document'; import Document, { Head, Html, Main, NextScript } from 'next/document'; class MyDocument extends Document { - static async getInitialProps( - ctx: DocumentContext, - ): Promise { - const initialProps = await Document.getInitialProps(ctx); - return initialProps; - } - render(): JSX.Element { + render() { return ( diff --git a/apps/web/src/pages/api/health.ts b/apps/web/src/pages/api/health.ts index 1e2acca6e..c0cce5c6e 100644 --- a/apps/web/src/pages/api/health.ts +++ b/apps/web/src/pages/api/health.ts @@ -13,9 +13,12 @@ * License for the specific language governing permissions and limitations * under the License. */ -import type { NextApiHandler } from 'next'; -const handler: NextApiHandler = (req, res) => { - res.status(200).json({ status: 'ok' }); -}; +import { createNextApiHandler } from '@/server/api-handler'; + +const handler = createNextApiHandler({ + GET: (_, res) => { + res.status(200).json({ status: 'ok' }); + }, +}); export default handler; diff --git a/apps/web/src/pages/api/jwt.ts b/apps/web/src/pages/api/jwt.ts index e75742c0b..aefffefe9 100644 --- a/apps/web/src/pages/api/jwt.ts +++ b/apps/web/src/pages/api/jwt.ts @@ -16,12 +16,16 @@ import type { NextApiHandler } from 'next'; import { getIronSession } from 'iron-session'; -import type { JwtSession } from '@/constants/iron-option'; -import { ironOption } from '@/constants/iron-option'; +import { createNextApiHandler } from '@/server/api-handler'; +import type { JwtSession } from '@/server/iron-option'; +import { ironOption } from '@/server/iron-option'; -const handler: NextApiHandler = async (req, res) => { - const session = await getIronSession(req, res, ironOption); - res.send({ jwt: session.jwt ?? null }); -}; +const handler: NextApiHandler = createNextApiHandler({ + GET: async (req, res) => { + const session = await getIronSession(req, res, ironOption); + + res.send({ jwt: session.jwt ?? null }); + }, +}); export default handler; diff --git a/apps/web/src/pages/api/login.ts b/apps/web/src/pages/api/login.ts index 5803b6690..3cd84a570 100644 --- a/apps/web/src/pages/api/login.ts +++ b/apps/web/src/pages/api/login.ts @@ -13,46 +13,61 @@ * License for the specific language governing permissions and limitations * under the License. */ -import type { NextApiHandler } from 'next'; +import type { AxiosResponse } from 'axios'; import axios, { AxiosError } from 'axios'; import { getIronSession } from 'iron-session'; +import { z } from 'zod'; -import type { JwtSession } from '@/constants/iron-option'; -import { ironOption } from '@/constants/iron-option'; -import { env } from '@/env.mjs'; -import getLogger from '@/libs/logger'; - -const handler: NextApiHandler = async (req, res) => { - const { email, password } = req.body; - try { - const response = await axios.post( - `${env.API_BASE_URL}/api/admin/auth/signIn/email`, - { email, password }, - ); - - if (response.status !== 201) { - return res.status(response.status).send(response.data); - } - const session = await getIronSession(req, res, ironOption); - - session.jwt = response.data; - await session.save(); - - return res.send(response.data); - } catch (error) { - if (error instanceof TypeError) { - getLogger('/api/login').error(error); - return res.status(500).send({ message: error.message, code: error.name }); - } else if (error instanceof AxiosError && error.response) { - const { status, data } = error.response; - getLogger('/api/login').error(error.response); - return res.status(status).send(data); - } else if (error instanceof Error) { - const { message, name, cause, stack } = error; - getLogger('/api/login').error({ message, name, cause, stack }); - } - return res.status(500).send({ message: 'Unknown Error' }); - } -}; +import type { Jwt } from '@/shared'; + +import { env } from '@/env'; +import { createNextApiHandler, procedure } from '@/server/api-handler'; +import type { JwtSession } from '@/server/iron-option'; +import { ironOption } from '@/server/iron-option'; +import getLogger from '@/server/logger'; + +const handler = createNextApiHandler({ + POST: procedure + .input(z.object({ email: z.string().email(), password: z.string().min(8) })) + .handle(async (req, res) => { + const { email, password } = req.body; + + try { + const { status, data } = await axios.post( + `${env.API_BASE_URL}/api/admin/auth/signIn/email`, + { email, password }, + ); + + if (status !== 201) { + return res.status(status).send(data); + } + + const session = await getIronSession(req, res, ironOption); + + session.jwt = data; + await session.save(); + + return res.send(data); + } catch (error) { + if (error instanceof TypeError) { + getLogger('/api/login').error(error); + return res + .status(500) + .send({ message: error.message, code: error.name }); + } else if (error instanceof AxiosError && error.response) { + const { status, data } = error.response as AxiosResponse< + unknown, + unknown + >; + getLogger('/api/login').error(error.response); + return res.status(status).send(data); + } else if (error instanceof Error) { + const { message, name, cause, stack } = error; + getLogger('/api/login').error({ message, name, cause, stack }); + } + return res.status(500).send({ message: 'Unknown Error' }); + } + }), +}); export default handler; diff --git a/apps/web/src/pages/api/logout.ts b/apps/web/src/pages/api/logout.ts index 5ad4c0a45..e645a8971 100644 --- a/apps/web/src/pages/api/logout.ts +++ b/apps/web/src/pages/api/logout.ts @@ -13,18 +13,20 @@ * License for the specific language governing permissions and limitations * under the License. */ -import type { NextApiHandler } from 'next'; import { getIronSession } from 'iron-session'; -import type { JwtSession } from '@/constants/iron-option'; -import { ironOption } from '@/constants/iron-option'; +import { createNextApiHandler } from '@/server/api-handler'; +import type { JwtSession } from '@/server/iron-option'; +import { ironOption } from '@/server/iron-option'; -const handler: NextApiHandler = async (req, res) => { - const session = await getIronSession(req, res, ironOption); - session.destroy(); +const handler = createNextApiHandler({ + GET: async (req, res) => { + const session = await getIronSession(req, res, ironOption); - await session.save(); - res.send({ ok: true }); -}; + session.destroy(); + await session.save(); + res.send({ ok: true }); + }, +}); export default handler; diff --git a/apps/web/src/pages/api/oauth.ts b/apps/web/src/pages/api/oauth.ts index 0b34be8d7..dff87c749 100644 --- a/apps/web/src/pages/api/oauth.ts +++ b/apps/web/src/pages/api/oauth.ts @@ -13,41 +13,50 @@ * License for the specific language governing permissions and limitations * under the License. */ -import type { NextApiHandler } from 'next'; +import axios from 'axios'; import { getIronSession } from 'iron-session'; - -import type { JwtSession } from '@/constants/iron-option'; -import { ironOption } from '@/constants/iron-option'; -import { env } from '@/env.mjs'; -import getLogger from '@/libs/logger'; - -const handler: NextApiHandler = async (req, res) => { - const { code } = req.body; - - try { - const params = new URLSearchParams({ code }); - const response = await fetch( - `${env.API_BASE_URL}/api/admin/auth/signIn/oauth?${params}`, - ); - - const data = await response.json(); - - if (response.status !== 200) { - return res.status(response.status).send(data); - } - const session = await getIronSession(req, res, ironOption); - - session.jwt = data; - await session.save(); - - return res.send(data); - } catch (error) { - getLogger('/api/oauth').error(error); - if (error instanceof TypeError) { - return res.status(500).send({ message: error.message, code: error.name }); - } - return res.status(500).send({ message: 'Unknown Error' }); - } -}; +import { z } from 'zod'; + +import type { Jwt } from '@/shared'; + +import { env } from '@/env'; +import { createNextApiHandler, procedure } from '@/server/api-handler'; +import type { JwtSession } from '@/server/iron-option'; +import { ironOption } from '@/server/iron-option'; +import getLogger from '@/server/logger'; + +const handler = createNextApiHandler({ + POST: procedure + .input(z.object({ code: z.string() })) + .handle(async (req, res) => { + const { code } = req.body; + + try { + const params = new URLSearchParams({ code }); + const { status, data } = await axios.get( + `${env.API_BASE_URL}/api/admin/auth/signIn/oauth?${params.toString()}`, + ); + + if (status !== 200) { + return res.status(status).send(data); + } + + const session = await getIronSession(req, res, ironOption); + + session.jwt = data; + await session.save(); + + return res.send(data); + } catch (error) { + getLogger('/api/oauth').error(error); + if (error instanceof TypeError) { + return res + .status(500) + .send({ message: error.message, code: error.name }); + } + return res.status(500).send({ message: 'Unknown Error' }); + } + }), +}); export default handler; diff --git a/apps/web/src/pages/api/refresh-jwt.ts b/apps/web/src/pages/api/refresh-jwt.ts index 7ed85c2e8..ed01d5c65 100644 --- a/apps/web/src/pages/api/refresh-jwt.ts +++ b/apps/web/src/pages/api/refresh-jwt.ts @@ -13,44 +13,52 @@ * License for the specific language governing permissions and limitations * under the License. */ -import type { NextApiHandler } from 'next'; +import type { AxiosResponse } from 'axios'; import axios, { AxiosError } from 'axios'; import { getIronSession } from 'iron-session'; -import type { JwtSession } from '@/constants/iron-option'; -import { ironOption } from '@/constants/iron-option'; -import { env } from '@/env.mjs'; -import getLogger from '@/libs/logger'; +import type { Jwt } from '@/shared'; -const handler: NextApiHandler = async (req, res) => { - const session = await getIronSession(req, res, ironOption); +import { env } from '@/env'; +import { createNextApiHandler } from '@/server/api-handler'; +import type { JwtSession } from '@/server/iron-option'; +import { ironOption } from '@/server/iron-option'; +import getLogger from '@/server/logger'; - const { jwt } = session; - if (!jwt) return res.status(400).end(); +const handler = createNextApiHandler({ + GET: async (req, res) => { + const session = await getIronSession(req, res, ironOption); - try { - const response = await axios.get( - `${env.API_BASE_URL}/api/admin/auth/refresh`, - { headers: { Authorization: `Bearer ${jwt?.refreshToken}` } }, - ); + if (!session.jwt) return res.status(400).end(); - if (response.status !== 200) { - return res.status(response.status).send(response.data); - } + try { + const { status, data } = await axios.get( + `${env.API_BASE_URL}/api/admin/auth/refresh`, + { headers: { Authorization: `Bearer ${session.jwt.refreshToken}` } }, + ); + + if (status !== 200) return res.status(status).send(data); + + session.jwt = data; + await session.save(); - session.jwt = response.data; - await session.save(); - return res.send({ jwt: response.data }); - } catch (error) { - getLogger('/api/refrech-jwt').error(error); - if (error instanceof TypeError) { - return res.status(500).send({ message: error.message, code: error.name }); - } else if (error instanceof AxiosError && error.response) { - const { status, data } = error.response; - return res.status(status).send(data); + return res.send(data); + } catch (error) { + getLogger('/api/refrech-jwt').error(error); + if (error instanceof TypeError) { + return res + .status(500) + .send({ message: error.message, code: error.name }); + } else if (error instanceof AxiosError && error.response) { + const { status, data } = error.response as AxiosResponse< + unknown, + unknown + >; + return res.status(status).send(data); + } + return res.status(500).send({ message: 'Unknown Error' }); } - return res.status(500).send({ message: 'Unknown Error' }); - } -}; + }, +}); export default handler; diff --git a/apps/web/src/pages/auth/oauth-callback.tsx b/apps/web/src/pages/auth/oauth-callback.tsx index ef3326123..d901faaa3 100644 --- a/apps/web/src/pages/auth/oauth-callback.tsx +++ b/apps/web/src/pages/auth/oauth-callback.tsx @@ -13,54 +13,30 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { useEffect, useMemo, useState } from 'react'; import type { NextPage } from 'next'; import { useRouter } from 'next/router'; -import { toast } from '@ufb/ui'; - -import { Path } from '@/constants/path'; -import { useUser } from '@/contexts/user.context'; - -interface IQuery { - code: string; - callback_url?: string; -} +import { Path } from '@/shared'; +import { useOAuthCallback } from '@/features/auth/sign-in-with-oauth'; interface IProps {} const OAuthCallbackPage: NextPage = () => { - const { signInOAuth } = useUser(); - const [status, setStatus] = useState<'loading' | 'error'>('loading'); - + const { status } = useOAuthCallback(); const router = useRouter(); - - const query = useMemo(() => { - if (!router.query) return null; - const { code, callback_url } = router.query; - - if (!code) return null; - return { code, callback_url } as IQuery; - }, [router.query]); - - useEffect(() => { - if (!query) return; - signInOAuth(query).catch(() => { - toast.negative({ title: 'OAuth2.0 Login Error' }); - router.replace(Path.SIGN_IN); - setStatus('error'); - }); - }, [query]); - return (

-

- {status === 'loading' ? - 'Loading...' - : status === 'error' ? - 'Error!!!' - : ''} -

+ {status === 'loading' && ( +

Loading...

+ )} + {status === 'error' && ( +
+

Error!!!

+ +
+ )}
); }; diff --git a/apps/web/src/pages/auth/reset-password.tsx b/apps/web/src/pages/auth/reset-password.tsx index 2661bafa4..d9697e806 100644 --- a/apps/web/src/pages/auth/reset-password.tsx +++ b/apps/web/src/pages/auth/reset-password.tsx @@ -14,110 +14,27 @@ * under the License. */ import type { GetStaticProps } from 'next'; -import Image from 'next/image'; -import { useRouter } from 'next/router'; -import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { Icon, TextInput, toast } from '@ufb/ui'; - -import AuthTemplate from '@/components/templates/AuthTemplate'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { Path } from '@/constants/path'; -import { useOAIMutation } from '@/hooks'; -import type { NextPageWithLayout } from '../_app'; - -interface IForm { - email: string; -} -const defaultValues: IForm = { - email: '', -}; - -const schema = z.object({ - email: z.string().email(), -}); +import { DEFAULT_LOCALE, LogoWithTitle } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { RequestResetPasswordWithEmail } from '@/features/auth/reset-password-with-email'; +import { MainLayout } from '@/widgets'; const ResetPasswordPage: NextPageWithLayout = () => { const { t } = useTranslation(); - const router = useRouter(); - - const { register, handleSubmit, formState, setError } = useForm({ - resolver: zodResolver(schema), - defaultValues, - }); - - const { mutate, isPending } = useOAIMutation({ - method: 'post', - path: '/api/admin/users/password/reset/code', - queryOptions: { - async onSuccess() { - toast.positive({ title: 'Success' }); - router.push(Path.SIGN_IN); - }, - onError(error) { - toast.negative({ title: error.message }); - setError('email', { message: error.message }); - }, - }, - }); - - const onSubmit = (data: IForm) => mutate(data); return ( -
-
-
- logo - -
-

{t('auth.reset-password.title')}

-
-
-
- -
-
- - -
-
+
+ +
); }; -ResetPasswordPage.getLayout = function getLayout(page) { - return {page}; +ResetPasswordPage.getLayout = (page) => { + return {page}; }; export const getStaticProps: GetStaticProps = async ({ locale }) => { diff --git a/apps/web/src/pages/auth/sign-in.tsx b/apps/web/src/pages/auth/sign-in.tsx index 380540dab..ff0893811 100644 --- a/apps/web/src/pages/auth/sign-in.tsx +++ b/apps/web/src/pages/auth/sign-in.tsx @@ -16,65 +16,22 @@ import type { GetStaticProps } from 'next'; import Image from 'next/image'; import Link from 'next/link'; -import { useRouter } from 'next/router'; -import { zodResolver } from '@hookform/resolvers/zod'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { z } from 'zod'; -import { TextInput, toast } from '@ufb/ui'; - -import AuthTemplate from '@/components/templates/AuthTemplate'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { Path } from '@/constants/path'; -import { OAuthLoginButton } from '@/containers/buttons'; -import { useTenant } from '@/contexts/tenant.context'; -import { useUser } from '@/contexts/user.context'; -import type { NextPageWithLayout } from '@/pages/_app'; -import type { IFetchError } from '@/types/fetch-error.type'; - -interface IForm { - email: string; - password: string; - remember: boolean; -} - -const schema: Zod.ZodType = z.object({ - email: z.string().email(), - password: z.string().min(8), - remember: z.boolean(), -}); -const defaultValues: IForm = { email: '', password: '', remember: true }; +import { DEFAULT_LOCALE, Path } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { useTenantStore } from '@/entities/tenant'; +import { SignInWithEmailForm } from '@/features/auth/sign-in-with-email'; +import { SignInWithOAuthButton } from '@/features/auth/sign-in-with-oauth'; +import { MainLayout } from '@/widgets'; const SignInPage: NextPageWithLayout = () => { const { t } = useTranslation(); - const router = useRouter(); - const { signIn } = useUser(); - const { tenant } = useTenant(); - - const { handleSubmit, register, formState, setError } = useForm({ - resolver: zodResolver(schema), - defaultValues, - mode: 'onSubmit', - reValidateMode: 'onSubmit', - }); - - const { isSubmitted, errors, isSubmitting } = formState; - - const onSubmit = async (data: IForm) => { - try { - await signIn(data); - } catch (error) { - const { message } = error as IFetchError; - setError('email', { message: 'invalid email' }); - setError('password', { message: 'invalid password' }); - toast.negative({ title: message, description: message }); - } - }; + const { tenant } = useTenantStore(); return ( -
+
{ {tenant?.siteName}
- {tenant?.useEmail && ( - <> -
- - -
- - )} -
- {tenant?.useEmail && ( - - )} - {tenant?.useEmail && !tenant?.isPrivate && ( - - )} - {tenant?.useEmail && tenant?.useOAuth && ( + {tenant?.useEmail && } +
+ {tenant?.useEmail && tenant.useOAuth && (
OR @@ -135,38 +53,25 @@ const SignInPage: NextPageWithLayout = () => {
)} - {tenant?.useOAuth && } + {tenant?.useOAuth && }
- - ); -}; - -const ResetPassword: React.FC = () => { - const { tenant } = useTenant(); - const { t } = useTranslation(); - if (!tenant?.useEmail || tenant.isPrivate) return <>; - return ( -
- - {t('auth.sign-in.reset-password')} - + {tenant?.useEmail && !tenant.isPrivate && ( +
+ + {t('auth.sign-in.reset-password')} + +
+ )}
); }; -function Layout(page: React.ReactNode) { - return ( - - {page} - - - ); -} - -SignInPage.getLayout = Layout; +SignInPage.getLayout = (page) => { + return {page}; +}; export const getStaticProps: GetStaticProps = async ({ locale }) => { return { diff --git a/apps/web/src/pages/auth/sign-up.tsx b/apps/web/src/pages/auth/sign-up.tsx index fd0a7d4d0..d018f3b10 100644 --- a/apps/web/src/pages/auth/sign-up.tsx +++ b/apps/web/src/pages/auth/sign-up.tsx @@ -13,288 +13,28 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { useState } from 'react'; import type { GetStaticProps } from 'next'; -import Image from 'next/image'; -import { useRouter } from 'next/router'; -import { zodResolver } from '@hookform/resolvers/zod'; -import dayjs from 'dayjs'; import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useForm } from 'react-hook-form'; -import { useInterval } from 'react-use'; -import { z } from 'zod'; -import { Icon, TextInput, toast } from '@ufb/ui'; - -import AuthTemplate from '@/components/templates/AuthTemplate'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { Path } from '@/constants/path'; -import { useUser } from '@/contexts/user.context'; -import client from '@/libs/client'; -import type { IFetchError } from '@/types/fetch-error.type'; -import type { NextPageWithLayout } from '../_app'; - -type EmailState = 'NOT_VERIFIED' | 'VERIFING' | 'EXPIRED' | 'VERIFIED'; - -interface IForm { - email: string; - password: string; - confirmPassword: string; - code?: string; - emailState: EmailState; -} - -const schema = z - .object({ - email: z.string().email(), - emailState: z.enum(['NOT_VERIFIED', 'VERIFING', 'EXPIRED', 'VERIFIED']), - code: z.string().length(6), - password: z.string().min(8), - confirmPassword: z.string().min(8), - }) - .refine( - (schema) => schema.password === schema.confirmPassword, - 'Password not matched', - ); - -const defaultValues: IForm = { - email: '', - emailState: 'NOT_VERIFIED', - password: '', - confirmPassword: '', - code: '', -}; +import { DEFAULT_LOCALE, LogoWithTitle } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { SignUpWithEmailForm } from '@/features/auth/sign-up-with-email'; +import { MainLayout } from '@/widgets'; const SignUpPage: NextPageWithLayout = () => { const { t } = useTranslation(); - const router = useRouter(); - - const { signUp } = useUser(); - - const { - handleSubmit, - register, - formState, - getValues, - setValue, - watch, - setError, - clearErrors, - } = useForm({ - resolver: zodResolver(schema), - defaultValues, - mode: 'onSubmit', - }); - - const { isValid } = formState; - - const [expiredTime, setExpiredTime] = useState(); - const [leftTime, setLeftTime] = useState(''); - const [codeStatus, setCodeStatus] = useState< - 'isSubmitting' | 'isSubmitted' - >(); - const [emailInputStatus, setEmailInputStatus] = useState< - 'isSubmitting' | 'isSubmitted' - >(); - - const onSubmit = async (data: IForm) => { - const { email, password } = data; - try { - await signUp({ email, password }); - router.push(Path.SIGN_IN); - toast.positive({ title: 'Success' }); - } catch (error) { - const { code, message } = error as IFetchError; - toast.negative({ title: message, description: code }); - } - }; - - useInterval( - () => { - const seconds = dayjs(expiredTime).diff(dayjs(), 'seconds'); - - if (seconds < 0) { - setLeftTime(`00:00`); - setValue('emailState', 'EXPIRED'); - } else { - const m = Math.floor(seconds / 60) - .toString() - .padStart(2, '0'); - const s = Math.floor(seconds % 60) - .toString() - .padStart(2, '0'); - - setLeftTime(`${m}:${s}`); - } - }, - watch('emailState') === 'VERIFING' ? 1000 : null, - ); - - const getVerificationCode = async () => { - setEmailInputStatus('isSubmitting'); - - try { - const { data } = await client.post({ - path: '/api/admin/auth/email/code', - body: { email: getValues('email') }, - }); - setValue('emailState', 'VERIFING'); - setExpiredTime(data?.expiredAt); - clearErrors('email'); - toast.positive({ title: 'Success' }); - } catch (error) { - const { message } = error as IFetchError; - setError('email', { message }); - toast.negative({ title: message }); - } finally { - setEmailInputStatus('isSubmitted'); - } - }; - - const verifyCode = async () => { - const code = getValues('code'); - setCodeStatus('isSubmitting'); - if (!code) return; - if (code.length !== 6) return; - try { - await client.post({ - path: '/api/admin/auth/email/code/verify', - body: { code, email: getValues('email') }, - }); - setValue('emailState', 'VERIFIED'); - clearErrors('code'); - toast.positive({ title: 'Success' }); - } catch (error) { - const { message } = error as IFetchError; - setError('code', { message }); - toast.negative({ title: message }); - } finally { - setCodeStatus('isSubmitted'); - } - }; return ( -
-
-
- logo - -
-

{t('auth.sign-up.title')}

-
-
-
- - {t('auth.sign-up.button.request-auth-code')} - - } - required - /> - {watch('emailState') !== 'NOT_VERIFIED' && ( - - {(watch('emailState') === 'VERIFING' || - watch('emailState') === 'EXPIRED') && ( -

{leftTime}

- )} - -
- } - required - /> - )} - - -
-
- - -
- +
+ +
); }; -SignUpPage.getLayout = function getLayout(page) { - return {page}; +SignUpPage.getLayout = (page) => { + return {page}; }; export const getStaticProps: GetStaticProps = async ({ locale }) => { diff --git a/apps/web/src/pages/index.tsx b/apps/web/src/pages/index.tsx index 5fa3e9d4d..3053cd548 100644 --- a/apps/web/src/pages/index.tsx +++ b/apps/web/src/pages/index.tsx @@ -15,13 +15,15 @@ */ import type { GetServerSideProps, NextPage } from 'next'; -import { Path } from '@/constants/path'; +import { Path } from '@/shared'; const IndexPage: NextPage = () => { return <>; }; export const getServerSideProps: GetServerSideProps = async () => { - return { redirect: { destination: Path.SIGN_IN, permanent: true } }; + return Promise.resolve({ + redirect: { destination: Path.SIGN_IN, permanent: true }, + }); }; export default IndexPage; diff --git a/apps/web/src/pages/link/reset-password.tsx b/apps/web/src/pages/link/reset-password.tsx index 879e9a09e..028c36526 100644 --- a/apps/web/src/pages/link/reset-password.tsx +++ b/apps/web/src/pages/link/reset-password.tsx @@ -13,130 +13,44 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { useMemo } from 'react'; -import type { GetStaticProps } from 'next'; -import Image from 'next/image'; -import { useRouter } from 'next/router'; -import { zodResolver } from '@hookform/resolvers/zod'; +import type { GetServerSideProps } from 'next'; import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { Icon, TextInput, toast } from '@ufb/ui'; +import { DEFAULT_LOCALE, LogoWithTitle } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { ResetPasswordWithEmailForm } from '@/features/auth/reset-password-with-email'; +import { MainLayout } from '@/widgets'; -import { MainTemplate } from '@/components'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { Path } from '@/constants/path'; -import { useOAIMutation } from '@/hooks'; -import type { NextPageWithLayout } from '../_app'; - -interface IForm { - password: string; - confirmPassword: string; +interface IProps { + code: string; + email: string; } - -const schema = z - .object({ - password: z.string().min(8), - confirmPassword: z.string().min(8), - }) - .refine( - (schema) => schema.password === schema.confirmPassword, - 'Password not matched', - ); - -const defaultValues = { password: '', confirmPassword: '' }; - -const ResetPasswordPage: NextPageWithLayout = () => { +const ResetPasswordPage: NextPageWithLayout = (props) => { + const { code, email } = props; const { t } = useTranslation(); - const router = useRouter(); - - const code = useMemo(() => router.query?.code as string, [router.query]); - const email = useMemo(() => router.query?.email as string, [router.query]); - - const { handleSubmit, register, formState } = useForm({ - resolver: zodResolver(schema), - defaultValues, - }); - - const { mutate, isPending } = useOAIMutation({ - method: 'post', - path: '/api/admin/users/password/reset', - queryOptions: { - async onSuccess() { - toast.positive({ title: 'Success' }); - router.push(Path.SIGN_IN); - }, - onError(error) { - toast.negative({ title: error.message }); - }, - }, - }); - - const onSubmit = async ({ password }: IForm) => - mutate({ code, email, password }); return ( - -
-
-
- logo - -
-

{t('link.reset-password.title')}

-
-
-
- - - -
-
- -
-
-
-
+
+ + +
); }; -export const getStaticProps: GetStaticProps = async ({ locale }) => { +ResetPasswordPage.getLayout = (page) => { + return {page}; +}; + +export const getServerSideProps: GetServerSideProps = async ({ + locale, + query, +}) => { return { props: { ...(await serverSideTranslations(locale ?? DEFAULT_LOCALE)), + code: (query.code ?? '') as string, + email: (query.email ?? '') as string, }, }; }; diff --git a/apps/web/src/pages/link/user-invitation.tsx b/apps/web/src/pages/link/user-invitation.tsx index 11c19efde..9ccb61b78 100644 --- a/apps/web/src/pages/link/user-invitation.tsx +++ b/apps/web/src/pages/link/user-invitation.tsx @@ -15,133 +15,40 @@ */ import { useMemo } from 'react'; import type { GetStaticProps } from 'next'; -import Image from 'next/image'; import { useRouter } from 'next/router'; -import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { Icon, TextInput, toast } from '@ufb/ui'; - -import AuthTemplate from '@/components/templates/AuthTemplate'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { Path } from '@/constants/path'; -import { useOAIMutation } from '@/hooks'; -import type { NextPageWithLayout } from '../_app'; - -interface IForm { - password: string; - confirmPassword: string; -} - -const schema: Zod.ZodType = z - .object({ - password: z.string().min(8), - confirmPassword: z.string().min(8), - }) - .refine((schema) => schema.password === schema.confirmPassword, { - message: 'Password not matched', - path: ['confirmPassword'], - }); - -const defaultValues: IForm = { - password: '', - confirmPassword: '', -}; +import { DEFAULT_LOCALE, LogoWithTitle } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { UserInvitationForm } from '@/features/invite-user'; +import { MainLayout } from '@/widgets'; const UserInvitationPage: NextPageWithLayout = () => { const { t } = useTranslation(); const router = useRouter(); - const code = useMemo(() => router.query?.code as string, [router.query]); - const email = useMemo(() => router.query?.email as string, [router.query]); - - const { handleSubmit, register, formState, setError } = useForm({ - resolver: zodResolver(schema), - defaultValues, - }); - - const { mutate, isPending } = useOAIMutation({ - method: 'post', - path: '/api/admin/auth/signUp/invitation', - queryOptions: { - async onSuccess() { - toast.positive({ title: 'Success' }); - router.push(Path.SIGN_IN); - }, - onError(error) { - setError('password', { message: error.message }); - setError('confirmPassword', { message: error.message }); - toast.negative({ title: error.message }); - }, - }, - }); - - const onSubmit = async ({ password }: IForm) => - mutate({ code, email, password }); + const code = useMemo( + () => (router.query.code ?? '') as string, + [router.query], + ); + const email = useMemo( + () => (router.query.email ?? '') as string, + [router.query], + ); return ( - -
-
- logo - -
-

{t('link.user-invitation.title')}

-
-
-
- - - -
-
- -
-
-
+
+ + +
); }; +UserInvitationPage.getLayout = (page) => { + return {page}; +}; + export const getStaticProps: GetStaticProps = async ({ locale }) => { return { props: { diff --git a/apps/web/src/pages/main/index.tsx b/apps/web/src/pages/main/index.tsx index a0ac0d8fa..bdffbc639 100644 --- a/apps/web/src/pages/main/index.tsx +++ b/apps/web/src/pages/main/index.tsx @@ -16,43 +16,47 @@ import type { GetStaticProps } from 'next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { MainTemplate } from '@/components'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { CreateProjectButton } from '@/containers/buttons'; -import { ProjectCard, TenantCard } from '@/containers/main'; -import { useProjects } from '@/hooks'; -import type { NextPageWithLayout } from '../_app'; - -const CARD_BORDER_CSS = - 'border-fill-tertiary h-[204px] w-[452px] rounded border'; +import { DEFAULT_LOCALE, SectionTemplate, useOAIQuery } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { ProjectCard } from '@/entities/project'; +import { TenantCard, useTenantStore } from '@/entities/tenant'; +import { RouteCreateProjectButton } from '@/features/create-project'; +import { MainLayout } from '@/widgets'; const MainIndexPage: NextPageWithLayout = () => { - const { data } = useProjects(); + const { data } = useOAIQuery({ + path: '/api/admin/projects', + variables: { limit: 1000, page: 1 }, + }); + const { tenant } = useTenantStore(); return ( -
-

Tenant

-
- -
-

Project

-
- {data?.items.map(({ id }) => )} -
- +
+ + {tenant && } + + +
+ {data?.items.map((project) => ( + + ))} +
+ +
-
+
); }; MainIndexPage.getLayout = (page) => { - return {page}; + return {page}; }; export const getStaticProps: GetStaticProps = async ({ locale }) => { diff --git a/apps/web/src/pages/main/profile.tsx b/apps/web/src/pages/main/profile.tsx index ff63c9ab1..e9048d47c 100644 --- a/apps/web/src/pages/main/profile.tsx +++ b/apps/web/src/pages/main/profile.tsx @@ -18,71 +18,73 @@ import type { GetStaticProps } from 'next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@ufb/ui'; +import { + DEFAULT_LOCALE, + DescriptionTooltip, + SectionTemplate, + SubMenu, +} from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { useUserStore } from '@/entities/user'; +import { DeleteAccountButton } from '@/features/delete-user'; +import { ChangePasswordForm, UserProfileForm } from '@/features/update-user'; +import { MainLayout } from '@/widgets'; -import { DescriptionTooltip, MainTemplate } from '@/components'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import ChangePasswordForm from '@/containers/my-profile/ChangePasswordForm'; -import MyProfileForm from '@/containers/my-profile/MyProfileForm'; -import { useUser } from '@/contexts/user.context'; -import type { NextPageWithLayout } from '../_app'; - -const menuItems = [ +const MENU_ITEMS = [ { key: 'profile-info', iconName: 'InfoCircleFill' }, { key: 'change-password', iconName: 'LockFill' }, ] as const; const ProfilePage: NextPageWithLayout = () => { const { t } = useTranslation(); - const { user } = useUser(); - const [tabIndex, setTabIndex] = useState<'profile-info' | 'change-password'>( - menuItems[0].key, + + const { user } = useUserStore(); + + const [tabKey, setTabKey] = useState<(typeof MENU_ITEMS)[number]['key']>( + MENU_ITEMS[0].key, ); return ( - <> -

- {t('main.profile.title')} - -

-
-
-
    - {menuItems.map(({ key, iconName }) => { - const isDisabled = - user?.signUpMethod === 'OAUTH' && key === 'change-password'; - return ( -
  • !isDisabled && setTabIndex(key)} - className={[ - 'mx-1 flex items-center gap-2 rounded p-2', - tabIndex === key ? 'bg-fill-tertiary' : '', - isDisabled ? - 'text-tertiary cursor-not-allowed' - : 'hover:bg-fill-secondary cursor-pointer', - ].join(' ')} - > - - - {t(`main.profile.${key}`)} - -
  • - ); - })} -
+ + {t('main.profile.title')} + + + } + > +
+
+ ({ + iconName, + name: t(`main.profile.${key}`), + active: tabKey === key, + onClick: () => setTabKey(key), + disabled: + key === 'change-password' && user?.signUpMethod === 'OAUTH', + }))} + />
-
- {tabIndex === menuItems[0].key && } - {tabIndex === menuItems[1].key && } +
+ {tabKey === MENU_ITEMS[0].key && user && ( + <> + +
+ +
+ + )} + {tabKey === MENU_ITEMS[1].key && }
- + ); }; -ProfilePage.getLayout = function getLayout(page) { - return {page}; +ProfilePage.getLayout = (page) => { + return {page}; }; export const getStaticProps: GetStaticProps = async ({ locale }) => { diff --git a/apps/web/src/pages/main/project/[projectId]/channel/create-complete.tsx b/apps/web/src/pages/main/project/[projectId]/channel/create-complete.tsx index 6c11439b6..973cae57f 100644 --- a/apps/web/src/pages/main/project/[projectId]/channel/create-complete.tsx +++ b/apps/web/src/pages/main/project/[projectId]/channel/create-complete.tsx @@ -13,46 +13,56 @@ * License for the specific language governing permissions and limitations * under the License. */ -import React, { useMemo } from 'react'; -import type { GetServerSideProps, NextPage } from 'next'; +import { useEffect } from 'react'; +import type { GetServerSideProps } from 'next'; import Image from 'next/image'; import { useRouter } from 'next/router'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { FormProvider, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { Icon } from '@ufb/ui'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { Path } from '@/constants/path'; +import type { NextPageWithLayout } from '@/shared'; import { - ChannelInfoSection, - FieldPreviewSection, - FieldSection, - ImageUploadSection, -} from '@/containers/create-channel-complete'; -import { useOAIQuery } from '@/hooks'; + CreateSectionTemplate, + DEFAULT_LOCALE, + Path, + useOAIQuery, +} from '@/shared'; +import { ChannelInfoForm, ImageConfigForm } from '@/entities/channel'; +import type { ChannelImageConfig, ChannelInfo } from '@/entities/channel'; +import { FieldTable, PreviewFieldTable } from '@/entities/field'; +import { ProjectGuard } from '@/entities/project'; -const CreateCompletePage: NextPage = () => { +interface IProps { + projectId: number; +} + +const CompleteChannelCreationPage: NextPageWithLayout = () => { const { t } = useTranslation(); const router = useRouter(); - const { channelId, projectId } = useMemo( - () => ({ - channelId: Number(router.query.channelId), - projectId: Number(router.query.projectId), - }), - [router.query], - ); + const { channelId, projectId } = { + channelId: Number(router.query.channelId), + projectId: Number(router.query.projectId), + }; const { data } = useOAIQuery({ path: '/api/admin/projects/{projectId}/channels/{channelId}', variables: { channelId, projectId }, }); - const gotoFeedback = () => { - router.push({ - pathname: Path.FEEDBACK, - query: { channelId, projectId }, - }); - }; + + const channelInfoFormMethods = useForm(); + const imageConfigFormMethods = useForm(); + + useEffect(() => { + if (!data) return; + channelInfoFormMethods.reset(data); + imageConfigFormMethods.reset(data.imageConfig); + }, [data]); + + const gotoFeedback = () => + router.push({ pathname: Path.FEEDBACK, query: { channelId, projectId } }); return (
@@ -67,14 +77,26 @@ const CreateCompletePage: NextPage = () => { {t('main.create-channel.complete-title')}

- {data && ( - <> - - - - - - )} + + + + + + + + + + + + + + + + +
void }> = ({ goOut }) => {
); }; -export const getServerSideProps: GetServerSideProps = async ({ locale }) => { + +CompleteChannelCreationPage.getLayout = (page: React.ReactElement) => { + return {page}; +}; + +export const getServerSideProps: GetServerSideProps = async ({ + locale, + query, +}) => { + const projectId = parseInt(query.projectId as string); + return { props: { ...(await serverSideTranslations(locale ?? DEFAULT_LOCALE)), + projectId, }, }; }; -export default CreateCompletePage; +export default CompleteChannelCreationPage; diff --git a/apps/web/src/pages/main/project/[projectId]/channel/create.tsx b/apps/web/src/pages/main/project/[projectId]/channel/create.tsx index de8a86114..0156c391a 100644 --- a/apps/web/src/pages/main/project/[projectId]/channel/create.tsx +++ b/apps/web/src/pages/main/project/[projectId]/channel/create.tsx @@ -13,78 +13,38 @@ * License for the specific language governing permissions and limitations * under the License. */ -import React, { useMemo } from 'react'; -import type { GetServerSideProps, NextPage } from 'next'; +import type { GetServerSideProps } from 'next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useTranslation } from 'react-i18next'; -import { CreateProjectChannelTemplate, HelpCardDocs } from '@/components'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { - InputChannelInfo, - InputField, - InputFieldPreview, - InputImageSetting, -} from '@/containers/create-channel'; -import { - CreateChannelProvider, - useCreateChannel, -} from '@/contexts/create-channel.context'; -import type { ChannelStepType } from '@/contexts/create-channel.context'; +import type { NextPageWithLayout } from '@/shared'; +import { DEFAULT_LOCALE } from '@/shared'; +import { ProjectGuard } from '@/entities/project'; +import { CreateChannel } from '@/features/create-channel'; -const CreatePage: NextPage = () => { - return ( - - - - ); -}; -const CreateChannel: NextPage = () => { - const { t } = useTranslation(); - const { completeStepIndex, currentStepIndex, currentStep, stepperText } = - useCreateChannel(); +interface IProps { + projectId: number; +} - const HELP_TEXT: Record = useMemo(() => { - return { - channelInfo: t('help-card.channel-info'), - fields: t('help-card.field'), - imageUpload: , - fieldPreview: t('help-card.field-preview'), - }; - }, [t]); +const CreateChannelPage: NextPageWithLayout = () => { + return ; +}; - return ( - - - - ); +CreateChannelPage.getLayout = (page: React.ReactElement) => { + return {page}; }; -const Contents: React.FC = () => { - const { currentStep } = useCreateChannel(); +export const getServerSideProps: GetServerSideProps = async ({ + locale, + query, +}) => { + const projectId = parseInt(query.projectId as string); - return ( - <> - {currentStep === 'channelInfo' && } - {currentStep === 'fields' && } - {currentStep === 'imageUpload' && } - {currentStep === 'fieldPreview' && } - - ); -}; -export const getServerSideProps: GetServerSideProps = async ({ locale }) => { return { props: { ...(await serverSideTranslations(locale ?? DEFAULT_LOCALE)), + projectId, }, }; }; -export default CreatePage; +export default CreateChannelPage; diff --git a/apps/web/src/pages/main/project/[projectId]/dashboard.tsx b/apps/web/src/pages/main/project/[projectId]/dashboard.tsx index 84c290ec8..9df5eca97 100644 --- a/apps/web/src/pages/main/project/[projectId]/dashboard.tsx +++ b/apps/web/src/pages/main/project/[projectId]/dashboard.tsx @@ -13,135 +13,81 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import type { GetServerSideProps } from 'next'; import dayjs from 'dayjs'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useTranslation } from 'react-i18next'; +import { useQueryState } from 'nuqs'; +import { Trans, useTranslation } from 'react-i18next'; -import { Icon } from '@ufb/ui'; - -import { DateRangePicker, MainTemplate } from '@/components'; -import { DATE_FORMAT } from '@/constants/dayjs-format'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; +import { DateRangePicker, DEFAULT_LOCALE, parseAsDateRange } from '@/shared'; +import type { DateRangeType, NextPageWithLayout } from '@/shared/types'; import { - CreateFeedbackPerIssueCard, + FeedbackLineChart, IssueBarChart, IssueFeedbackLineChart, IssueLineChart, IssueRank, - SevenDaysFeedbackCard, - SevenDaysIssueCard, - ThirtyDaysFeedbackCard, - ThirtyDaysIssueCard, - TodayFeedbackCard, - TodayIssueCard, - TotalFeedbackCard, - TotalIssueCard, - YesterdayFeedbackCard, - YesterdayIssueCard, -} from '@/containers/dashboard'; -import FeedbackLineChart from '@/containers/dashboard/FeedbackLineChart'; -import useQueryParamsState from '@/hooks/useQueryParamsState'; -import type { NextPageWithLayout } from '@/pages/_app'; -import type { DateRangeType } from '@/types/date-range.type'; +} from '@/entities/dashboard'; +import { ProjectGuard } from '@/entities/project'; +import { MainLayout } from '@/widgets'; +import { DashbaordCardSlider } from '@/widgets/dashboard-card-slider'; interface IProps { projectId: number; } -const DEFAULT_DATE_RANGE: DateRangeType = { + +const DEFAULT_DATE_RANGE = { startDate: dayjs().subtract(31, 'day').startOf('day').toDate(), endDate: dayjs().subtract(1, 'day').endOf('day').toDate(), }; -const DEFAULT_DATE_RANGE_STRING = { - createdAt: `${dayjs(DEFAULT_DATE_RANGE.startDate).format( - 'YYYY-MM-DD', - )}~${dayjs(DEFAULT_DATE_RANGE.endDate).format('YYYY-MM-DD')}`, -}; + +const options = [ + { + label: , + startDate: dayjs().subtract(1, 'days').startOf('day').toDate(), + endDate: dayjs().subtract(1, 'days').endOf('day').toDate(), + }, + { + label: , + startDate: dayjs().subtract(8, 'days').toDate(), + endDate: dayjs().subtract(1, 'days').toDate(), + }, + { + label: , + startDate: dayjs().subtract(31, 'days').toDate(), + endDate: dayjs().subtract(1, 'days').toDate(), + }, + { + label: , + startDate: dayjs().subtract(91, 'days').toDate(), + endDate: dayjs().subtract(1, 'days').toDate(), + }, + { + label: , + startDate: dayjs().subtract(181, 'days').toDate(), + endDate: dayjs().subtract(1, 'days').toDate(), + }, + { + label: , + startDate: dayjs().subtract(366, 'days').toDate(), + endDate: dayjs().subtract(1, 'days').toDate(), + }, +]; const DashboardPage: NextPageWithLayout = ({ projectId }) => { const { t } = useTranslation(); - const { query, setQuery } = useQueryParamsState( - { projectId }, - DEFAULT_DATE_RANGE_STRING, - (input) => { - if (!input.createdAt) return false; - const [starDate, endDate] = input.createdAt.split('~'); - if (dayjs(endDate).isAfter(dayjs(), 'day')) return false; - if (dayjs(endDate).isBefore(dayjs(starDate), 'day')) return false; - return true; - }, + const [dateRange, setDateRange] = useQueryState( + 'dateRange', + parseAsDateRange.withDefault(DEFAULT_DATE_RANGE), ); - const dateRange = useMemo(() => { - const queryStr = - query['createdAt'] ?? DEFAULT_DATE_RANGE_STRING['createdAt']; - - const [startDateStr, endDateStr] = queryStr.split('~'); - - return { - startDate: dayjs(startDateStr).toDate(), - endDate: dayjs(endDateStr).toDate(), - }; - }, [query]); - - const setDateRange = useCallback( - (dateRange: DateRangeType) => { - setQuery({ - ...query, - createdAt: - dateRange ? - `${dayjs(dateRange.startDate).format(DATE_FORMAT)}~${dayjs( - dateRange.endDate, - ).format(DATE_FORMAT)}` - : undefined, - }); - }, - [query], - ); - - const currentDate = useMemo( - () => dayjs().format('YYYY-MM-DD HH:mm'), - [dateRange], - ); - - const options = useMemo(() => { - return [ - { - label: t('text.date.yesterday'), - startDate: dayjs().subtract(1, 'days').startOf('day').toDate(), - endDate: dayjs().subtract(1, 'days').endOf('day').toDate(), - }, - { - label: t('text.date.before-days', { day: 7 }), - startDate: dayjs().subtract(8, 'days').toDate(), - endDate: dayjs().subtract(1, 'days').toDate(), - }, - { - label: t('text.date.before-days', { day: 30 }), - startDate: dayjs().subtract(31, 'days').toDate(), - endDate: dayjs().subtract(1, 'days').toDate(), - }, - { - label: t('text.date.before-days', { day: 90 }), - startDate: dayjs().subtract(91, 'days').toDate(), - endDate: dayjs().subtract(1, 'days').toDate(), - }, - { - label: t('text.date.before-days', { day: 180 }), - startDate: dayjs().subtract(181, 'days').toDate(), - endDate: dayjs().subtract(1, 'days').toDate(), - }, - { - label: t('text.date.before-days', { day: 365 }), - startDate: dayjs().subtract(366, 'days').toDate(), - endDate: dayjs().subtract(1, 'days').toDate(), - }, - ]; - }, [t]); + const onChangeDateRange = async (v: DateRangeType) => { + if (!v?.startDate || !v.endDate) return; + await setDateRange({ startDate: v.startDate, endDate: v.endDate }); + }; - if (!dateRange || !dateRange.startDate || !dateRange.endDate) return <>; + const currentDate = dayjs().format('YYYY-MM-DD HH:mm'); return (
@@ -155,40 +101,19 @@ const DashboardPage: NextPageWithLayout = ({ projectId }) => {
- - - - - - - - - + - - - - - = ({ projectId }) => { ); }; -interface ICardSliderProps extends React.PropsWithChildren {} - -const CardSlider: React.FC = ({ children }) => { - const containerRef = useRef(null); - - const [showRightButton, setShowRightButton] = useState(false); - const [showLeftButton, setShowLeftButton] = useState(false); - - useLayoutEffect(() => { - if (!containerRef.current) return; - const observer = new ResizeObserver(([entry]) => { - if (!entry) return; - const { width } = entry.contentRect; - setShowRightButton(width < 1380); - }); - observer.observe(containerRef.current); - return () => { - if (!containerRef.current) return; - observer.unobserve(containerRef.current); - }; - }, [containerRef.current]); - - const scrollLeft = () => { - if (!containerRef.current) return; - setShowLeftButton(false); - setShowRightButton(true); - containerRef.current.scrollTo({ left: 0, behavior: 'smooth' }); - }; - const scrollRight = () => { - if (!containerRef.current) return; - setShowLeftButton(true); - setShowRightButton(false); - containerRef.current.scrollTo({ left: 1380, behavior: 'smooth' }); - }; - +DashboardPage.getLayout = (page: React.ReactElement) => { return ( -
-
-
{children}
-
- {showRightButton && ( - - )} - {showLeftButton && ( - - )} -
+ + {page} + ); }; @@ -298,8 +171,5 @@ export const getServerSideProps: GetServerSideProps = async ({ }, }; }; -DashboardPage.getLayout = function getLayout(page) { - return {page}; -}; export default DashboardPage; diff --git a/apps/web/src/pages/main/project/[projectId]/feedback.tsx b/apps/web/src/pages/main/project/[projectId]/feedback.tsx index c277af2e0..19cb1b7f6 100644 --- a/apps/web/src/pages/main/project/[projectId]/feedback.tsx +++ b/apps/web/src/pages/main/project/[projectId]/feedback.tsx @@ -17,12 +17,12 @@ import type { GetServerSideProps } from 'next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useTranslation } from 'react-i18next'; -import { MainTemplate } from '@/components'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { FeedbackTable } from '@/containers'; -import { CreateChannelButton } from '@/containers/buttons'; -import { useOAIQuery } from '@/hooks'; -import type { NextPageWithLayout } from '@/pages/_app'; +import { DEFAULT_LOCALE, useOAIQuery } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { ProjectGuard } from '@/entities/project'; +import { RouteCreateChannelButton } from '@/features/create-channel'; +import { MainLayout } from '@/widgets'; +import { FeedbackTable } from '@/widgets/feedback-table'; interface IProps { projectId: number; @@ -48,18 +48,22 @@ const FeedbackManagementPage: NextPageWithLayout = (props) => { {status === 'pending' ?

Loading...

: status === 'error' ? -

Not Permission

+

Error

: data.meta.totalItems === 0 ? -
- +
+
: } ); }; -FeedbackManagementPage.getLayout = function getLayout(page) { - return {page}; +FeedbackManagementPage.getLayout = (page: React.ReactElement) => { + return ( + + {page} + + ); }; export const getServerSideProps: GetServerSideProps = async ({ diff --git a/apps/web/src/pages/main/project/[projectId]/issue.tsx b/apps/web/src/pages/main/project/[projectId]/issue.tsx index 001e082a1..7aad17a1f 100644 --- a/apps/web/src/pages/main/project/[projectId]/issue.tsx +++ b/apps/web/src/pages/main/project/[projectId]/issue.tsx @@ -17,12 +17,12 @@ import type { GetServerSideProps } from 'next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useTranslation } from 'react-i18next'; -import { MainTemplate } from '@/components'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { CreateChannelButton } from '@/containers/buttons'; -import { IssueTable } from '@/containers/tables'; -import { useOAIQuery } from '@/hooks'; -import type { NextPageWithLayout } from '../../../_app'; +import { DEFAULT_LOCALE, useOAIQuery } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { ProjectGuard } from '@/entities/project'; +import { RouteCreateChannelButton } from '@/features/create-channel'; +import { MainLayout } from '@/widgets'; +import { IssueTable } from '@/widgets/issue-table'; interface IProps { projectId: number; @@ -43,16 +43,20 @@ const IssueMangementPage: NextPageWithLayout = (props) => { : status === 'error' ?

Not Permission

: data.meta.totalItems === 0 ? -
- +
+
: } ); }; -IssueMangementPage.getLayout = function getLayout(page) { - return {page}; +IssueMangementPage.getLayout = (page: React.ReactElement) => { + return ( + + {page} + + ); }; export const getServerSideProps: GetServerSideProps = async ({ diff --git a/apps/web/src/pages/main/project/[projectId]/not-permission.tsx b/apps/web/src/pages/main/project/[projectId]/not-permission.tsx index faad89b49..982b50a30 100644 --- a/apps/web/src/pages/main/project/[projectId]/not-permission.tsx +++ b/apps/web/src/pages/main/project/[projectId]/not-permission.tsx @@ -13,15 +13,15 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { MainTemplate } from '@/components'; -import type { NextPageWithLayout } from '@/pages/_app'; +import type { NextPageWithLayout } from '@/shared/types'; +import { MainLayout } from '@/widgets'; const NotPermissionPage: NextPageWithLayout = () => { return
Not Permissions
; }; -NotPermissionPage.getLayout = function getLayout(page) { - return {page}; +NotPermissionPage.getLayout = (page) => { + return {page}; }; export default NotPermissionPage; diff --git a/apps/web/src/pages/main/project/[projectId]/setting.tsx b/apps/web/src/pages/main/project/[projectId]/setting.tsx index aff1f6978..afb2ce4de 100644 --- a/apps/web/src/pages/main/project/[projectId]/setting.tsx +++ b/apps/web/src/pages/main/project/[projectId]/setting.tsx @@ -13,39 +13,40 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; import type { GetServerSideProps } from 'next'; import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { parseAsInteger, useQueryState } from 'nuqs'; import { Icon } from '@ufb/ui'; -import { MainTemplate } from '@/components'; -import { SettingMenuBox } from '@/components/layouts/setting-menu'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { Path } from '@/constants/path'; +import { DEFAULT_LOCALE, Path } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { ProjectGuard } from '@/entities/project'; +import { MainLayout } from '@/widgets'; +import type { SettingMenuType } from '@/widgets/setting-menu'; import { - APIKeySetting, - ChannelDeleteSetting, + ApiKeySetting, + AuthSetting, + ChannelDeletionSetting, ChannelInfoSetting, ChannelSettingMenu, FieldSetting, - ImageSetting, + ImageConfigSetting, IssueTrackerSetting, MemberSetting, - ProjectDeleteSetting, + ProjectDeletionSetting, ProjectInfoSetting, ProjectSettingMenu, RoleSetting, - SignUpSetting, + SettingMenuBox, TenantInfoSetting, TenantSettingMenu, - UserSetting, + UserManagementSetting, WebhookSetting, -} from '@/containers/setting-menu'; -import type { SettingMenuType } from '@/types/setting-menu.type'; -import type { NextPageWithLayout } from '../../../_app'; +} from '@/widgets/setting-menu'; interface IProps { projectId: number; @@ -55,27 +56,15 @@ const SettingPage: NextPageWithLayout = ({ projectId }) => { const { t } = useTranslation(); const router = useRouter(); - const channelId = - router.query?.channelId ? (Number(router.query.channelId) as number) : null; - - const setChannelId = (channelId: number | null) => - router.push({ - pathname: Path.SETTINGS, - query: { ...router.query, channelId }, - }); - + const [channelId, setChannelId] = useQueryState('channelId', parseAsInteger); const settingMenu = - router.query?.menu ? (router.query.menu as SettingMenuType) : null; + router.query.menu ? (router.query.menu as SettingMenuType) : null; const setSettingMenu = (menu: SettingMenuType | null) => - router.push({ - pathname: Path.SETTINGS, - query: { ...router.query, menu }, - }); + router.push({ pathname: Path.SETTINGS, query: { ...router.query, menu } }); + + const onClickReset = () => setSettingMenu(null); - const onClickReset = () => { - setSettingMenu(null); - }; const showList = useMemo(() => { switch (settingMenu) { case 'TENANT_INFO': @@ -100,12 +89,6 @@ const SettingPage: NextPageWithLayout = ({ projectId }) => { } }, [settingMenu]); - useEffect(() => {}, [router.query]); - - const onClickTarget = (target: SettingMenuType | null) => () => { - setSettingMenu(target); - }; - return ( <>

{t('main.setting.title')}

@@ -113,26 +96,24 @@ const SettingPage: NextPageWithLayout = ({ projectId }) => { - {projectId && ( - - )} + {projectId && ( @@ -143,8 +124,8 @@ const SettingPage: NextPageWithLayout = ({ projectId }) => { {settingMenu === 'TENANT_INFO' && } - {settingMenu === 'SIGNUP_SETTING' && } - {settingMenu === 'USER_MANAGEMENT' && } + {settingMenu === 'SIGNUP_SETTING' && } + {settingMenu === 'USER_MANAGEMENT' && } {settingMenu === 'PROJECT_INFO' && ( )} @@ -155,7 +136,7 @@ const SettingPage: NextPageWithLayout = ({ projectId }) => { )} {settingMenu === 'API_KEY_MANAGEMENT' && ( - + )} {settingMenu === 'TICKET_MANAGEMENT' && ( @@ -164,7 +145,7 @@ const SettingPage: NextPageWithLayout = ({ projectId }) => { )} {settingMenu === 'DELETE_PROJECT' && ( - + )} {settingMenu === 'CHANNEL_INFO' && channelId && ( @@ -173,10 +154,10 @@ const SettingPage: NextPageWithLayout = ({ projectId }) => { )} {settingMenu === 'IMAGE_UPLOAD_SETTING' && channelId && ( - + )} {settingMenu === 'DELETE_CHANNEL' && channelId && ( - @@ -188,8 +169,12 @@ const SettingPage: NextPageWithLayout = ({ projectId }) => { ); }; -SettingPage.getLayout = function getLayout(page) { - return {page}; +SettingPage.getLayout = (page: React.ReactElement) => { + return ( + + {page} + + ); }; export const getServerSideProps: GetServerSideProps = async ({ diff --git a/apps/web/src/pages/main/project/create-complete.tsx b/apps/web/src/pages/main/project/create-complete.tsx index 9deae3e34..a887a993d 100644 --- a/apps/web/src/pages/main/project/create-complete.tsx +++ b/apps/web/src/pages/main/project/create-complete.tsx @@ -13,41 +13,66 @@ * License for the specific language governing permissions and limitations * under the License. */ -import React, { useMemo } from 'react'; +import React, { useEffect } from 'react'; import type { GetStaticProps, NextPage } from 'next'; import Image from 'next/image'; import { useRouter } from 'next/router'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { FormProvider, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { Icon } from '@ufb/ui'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { Path } from '@/constants/path'; import { - ApiKeySection, - IssueTrackerSection, - MemberSection, - ProjectInfoSection, - RoleSection, -} from '@/containers/create-project-complete'; -import { useOAIQuery } from '@/hooks'; + CreateSectionTemplate, + DEFAULT_LOCALE, + Path, + useOAIQuery, +} from '@/shared'; +import { ApiKeyTable } from '@/entities/api-key'; +import type { IssueTracker } from '@/entities/issue-tracker'; +import { IssueTrackerForm } from '@/entities/issue-tracker'; +import { MemberTable } from '@/entities/member'; +import type { ProjectInfo } from '@/entities/project'; +import { ProjectInfoForm } from '@/entities/project'; +import { RoleTable } from '@/entities/role'; -const CreateCompletePage: NextPage = () => { +const CompleteProjectCreationPage: NextPage = () => { const { t } = useTranslation(); const router = useRouter(); - const { projectId } = useMemo( - () => ({ - projectId: Number(router.query.projectId), - }), - [router.query], - ); + const projectId = Number(router.query.projectId); - const { data } = useOAIQuery({ + const { data: project } = useOAIQuery({ path: '/api/admin/projects/{projectId}', variables: { projectId }, }); + const { data: apiKeys } = useOAIQuery({ + path: '/api/admin/projects/{projectId}/api-keys', + variables: { projectId }, + }); + const { data: roles } = useOAIQuery({ + path: '/api/admin/projects/{projectId}/roles', + variables: { projectId }, + }); + const { data: issueTracker } = useOAIQuery({ + path: '/api/admin/projects/{projectId}/issue-tracker', + variables: { projectId }, + }); + const { data: members } = useOAIQuery({ + path: '/api/admin/projects/{projectId}/members', + variables: { projectId, createdAt: 'ASC' }, + }); + + const projectInfoFormMethods = useForm(); + const issueTrackerFormMethods = useForm(); + + useEffect(() => { + if (!project || !issueTracker) return; + projectInfoFormMethods.reset(project); + issueTrackerFormMethods.reset(issueTracker.data); + }, [project, issueTracker]); + return (
@@ -61,15 +86,33 @@ const CreateCompletePage: NextPage = () => { {t('main.create-project.complete-title')}

- {data && ( - <> - - - - - - - )} + + + + + + + + + + + + + + + + + + +
{
@@ -140,4 +181,4 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => { }; }; -export default CreateCompletePage; +export default CompleteProjectCreationPage; diff --git a/apps/web/src/pages/main/project/create.tsx b/apps/web/src/pages/main/project/create.tsx index f4134868f..df415850e 100644 --- a/apps/web/src/pages/main/project/create.tsx +++ b/apps/web/src/pages/main/project/create.tsx @@ -13,76 +13,17 @@ * License for the specific language governing permissions and limitations * under the License. */ -import React, { useMemo } from 'react'; +import React from 'react'; import type { GetStaticProps, NextPage } from 'next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useTranslation } from 'react-i18next'; -import { CreateProjectChannelTemplate, HelpCardDocs } from '@/components'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { - InputApiKey, - InputIssueTracker, - InputMember, - InputProjectInfo, - InputRole, -} from '@/containers/create-project'; -import type { ProjectStepType } from '@/contexts/create-project.context'; -import { - CreateProjectProvider, - useCreateProject, -} from '@/contexts/create-project.context'; +import { DEFAULT_LOCALE } from '@/shared'; +import { CreateProject } from '@/features/create-project'; -const CreatePage: NextPage = () => { - return ( - - - - ); +const CreateProjectPage: NextPage = () => { + return ; }; -const CreateProject: React.FC = () => { - const { t } = useTranslation(); - const { completeStepIndex, currentStepIndex, currentStep, stepperText } = - useCreateProject(); - const HELP_TEXT: Record = - useMemo(() => { - return { - projectInfo: t('help-card.project-info'), - roles: t('help-card.role'), - members: t('help-card.member'), - apiKeys: , - issueTracker: t('help-card.issue-tracker'), - }; - }, []); - - return ( - - - - ); -}; - -const Contents: React.FC = () => { - const { currentStep } = useCreateProject(); - - return ( - <> - {currentStep === 'projectInfo' && } - {currentStep === 'roles' && } - {currentStep === 'members' && } - {currentStep === 'apiKeys' && } - {currentStep === 'issueTracker' && } - - ); -}; export const getStaticProps: GetStaticProps = async ({ locale }) => { return { props: { @@ -91,4 +32,4 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => { }; }; -export default CreatePage; +export default CreateProjectPage; diff --git a/apps/web/src/pages/tenant/create.tsx b/apps/web/src/pages/tenant/create.tsx index f5aa21509..4f2a50189 100644 --- a/apps/web/src/pages/tenant/create.tsx +++ b/apps/web/src/pages/tenant/create.tsx @@ -13,92 +13,32 @@ * License for the specific language governing permissions and limitations * under the License. */ +import { useEffect } from 'react'; import type { GetStaticProps } from 'next'; import { useRouter } from 'next/router'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { toast } from '@ufb/ui'; - -import AuthTemplate from '@/components/templates/AuthTemplate'; -import { DEFAULT_LOCALE } from '@/constants/i18n'; -import { Path } from '@/constants/path'; -import { useTenant } from '@/contexts/tenant.context'; -import { useOAIMutation } from '@/hooks'; -import type { NextPageWithLayout } from '../_app'; - -interface IForm { - siteName: string; -} - -const schema: Zod.ZodType = z.object({ - siteName: z.string(), -}); - -const defaultValues: IForm = { - siteName: '', -}; - -const CreatePage: NextPageWithLayout = () => { - const { t } = useTranslation(); +import { DEFAULT_LOCALE, Path } from '@/shared'; +import type { NextPageWithLayout } from '@/shared/types'; +import { useTenantStore } from '@/entities/tenant'; +import { CreateTenantForm } from '@/features/create-tenant'; +import { MainLayout } from '@/widgets'; +const CreateTenantPage: NextPageWithLayout = () => { const router = useRouter(); - const { refetch } = useTenant(); - const { register, handleSubmit } = useForm({ - resolver: zodResolver(schema), - defaultValues, - }); + const { tenant } = useTenantStore(); - const { mutate, isPending } = useOAIMutation({ - method: 'post', - path: '/api/admin/tenants', - queryOptions: { - async onSuccess() { - toast.positive({ title: 'Success' }); - toast.positive({ - title: 'create Default Super User', - description: 'email: user@feedback.com \n password: 12345678', - }); - router.push(Path.SIGN_IN); - refetch(); - }, - onError(error) { - toast.negative({ title: error?.message ?? 'Error' }); - }, - }, - }); - const onSubmit = (data: IForm) => mutate(data); + useEffect(() => { + if (!tenant) return; + void router.replace(Path.SIGN_IN); + }, [tenant]); - return ( - -
-
-
-

{t('tenant.create.title')}

- + return ; +}; - - -
-
-
- ); +CreateTenantPage.getLayout = (page) => { + return {page}; }; export const getStaticProps: GetStaticProps = async ({ locale }) => { @@ -109,4 +49,4 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => { }; }; -export default CreatePage; +export default CreateTenantPage; diff --git a/apps/web/src/server/api-handler.ts b/apps/web/src/server/api-handler.ts new file mode 100644 index 000000000..fd070e381 --- /dev/null +++ b/apps/web/src/server/api-handler.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; + +const METHOD_LIST = [ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'PATCH', + 'HEAD', + 'OPTIONS', +] as const; + +type Method = (typeof METHOD_LIST)[number]; + +type CustomNextApiRequest> = Omit< + NextApiRequest, + 'body' +> & { body: T }; + +type CustomNextApiHandler = ( + req: CustomNextApiRequest, + res: NextApiResponse, +) => unknown; + +const handler = + >(schema?: T) => + (input: CustomNextApiHandler) => { + if (!schema) return input; + + return (req: NextApiRequest, res: NextApiResponse) => { + const { success, error, data } = schema.safeParse(req.body); + req.body = data; + if (success) return input(req, res); + + return res + .status(400) + .json({ error: { message: 'Invalid request', error } }); + }; + }; + +export const procedure = { + input: (schema?: T) => ({ handle: handler(schema) }), + handle: handler(), +}; + +export const createNextApiHandler = ( + input: Partial>, +) => { + return ((req, res) => { + const handler = input[req.method?.toUpperCase() as Method]; + + if (!handler) return res.status(405).send('Method not allowed'); + return handler(req, res); + }) as NextApiHandler; +}; diff --git a/apps/web/src/constants/iron-option.ts b/apps/web/src/server/iron-option.ts similarity index 93% rename from apps/web/src/constants/iron-option.ts rename to apps/web/src/server/iron-option.ts index bb8b400d5..37481911e 100644 --- a/apps/web/src/constants/iron-option.ts +++ b/apps/web/src/server/iron-option.ts @@ -15,7 +15,7 @@ */ import type { SessionOptions } from 'iron-session'; -import { env } from '@/env.mjs'; +import { env } from '@/env'; export const ironOption: SessionOptions = { cookieName: 'user-feedback', @@ -26,6 +26,6 @@ export const ironOption: SessionOptions = { secure: process.env.NODE_ENV === 'production', }, }; -export type JwtSession = { +export interface JwtSession { jwt?: { accessToken: string; refreshToken: string }; -}; +} diff --git a/apps/web/src/libs/logger.ts b/apps/web/src/server/logger.ts similarity index 94% rename from apps/web/src/libs/logger.ts rename to apps/web/src/server/logger.ts index 5b19f52ef..27d5117b2 100644 --- a/apps/web/src/libs/logger.ts +++ b/apps/web/src/server/logger.ts @@ -20,6 +20,7 @@ const getLogger = (name: string) => pino({ name, base: { env: process.env.NODE_ENV }, + enabled: process.env.NODE_ENV !== 'test', }); export default getLogger; diff --git a/apps/web/src/types/member.type.ts b/apps/web/src/shared/constants/background-color.ts similarity index 67% rename from apps/web/src/types/member.type.ts rename to apps/web/src/shared/constants/background-color.ts index e02286900..58279e2f1 100644 --- a/apps/web/src/types/member.type.ts +++ b/apps/web/src/shared/constants/background-color.ts @@ -13,17 +13,14 @@ * License for the specific language governing permissions and limitations * under the License. */ +import type { ColorType } from '@ufb/ui'; -import type { RoleType } from './role.type'; -import type { UserType } from './user.type'; - -export type MemberType = { - id: number; - user: UserType; - role: RoleType; - createdAt: string; -}; - -export type InputMemberType = Omit & { - roleId: number; +export const BACKGROUND_COLOR_MAP: Record = { + red: 'bg-red-primary', + blue: 'bg-blue-primary', + yellow: 'bg-yellow-primary', + green: 'bg-green-primary', + purple: 'bg-purple-primary', + navy: 'bg-navy-primary', + orange: 'bg-orange-primary', }; diff --git a/apps/web/src/constants/chart-colors.ts b/apps/web/src/shared/constants/chart-colors.ts similarity index 100% rename from apps/web/src/constants/chart-colors.ts rename to apps/web/src/shared/constants/chart-colors.ts diff --git a/apps/web/src/constants/dayjs-format.ts b/apps/web/src/shared/constants/date-format.ts similarity index 100% rename from apps/web/src/constants/dayjs-format.ts rename to apps/web/src/shared/constants/date-format.ts diff --git a/apps/web/src/constants/i18n.ts b/apps/web/src/shared/constants/i18n.ts similarity index 86% rename from apps/web/src/constants/i18n.ts rename to apps/web/src/shared/constants/i18n.ts index 45cde12dc..d156b1211 100644 --- a/apps/web/src/constants/i18n.ts +++ b/apps/web/src/shared/constants/i18n.ts @@ -13,7 +13,5 @@ * License for the specific language governing permissions and limitations * under the License. */ -import i18nConfig from '../../next-i18next.config.js'; export const DEFAULT_LOCALE = 'en'; -export const LOCALES = i18nConfig.i18n?.locales; diff --git a/apps/web/src/shared/constants/index.ts b/apps/web/src/shared/constants/index.ts new file mode 100644 index 000000000..87ae3e57e --- /dev/null +++ b/apps/web/src/shared/constants/index.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export * from './background-color'; +export * from './chart-colors'; +export * from './date-format'; +export * from './i18n'; +export * from './path'; +export * from './local-storage-key'; +export * from './issues'; diff --git a/apps/web/src/constants/issues.ts b/apps/web/src/shared/constants/issues.ts similarity index 66% rename from apps/web/src/constants/issues.ts rename to apps/web/src/shared/constants/issues.ts index 35ce9eb4b..2c49ec1ed 100644 --- a/apps/web/src/constants/issues.ts +++ b/apps/web/src/shared/constants/issues.ts @@ -15,35 +15,20 @@ */ import type { TFunction } from 'next-i18next'; -import type { ColorType } from '@/types/color.type'; -import type { IssueStatus } from '@/types/issue.type'; +import type { ColorType } from '@ufb/ui'; -export type IssuesItemType = { +import type { IssueStatus } from '@/entities/issue'; + +export interface IssuesItem { key: IssueStatus; name: string; color: ColorType; -}; +} -export const ISSUES: (t: TFunction) => IssuesItemType[] = (t) => [ +export const ISSUES: (t: TFunction) => IssuesItem[] = (t) => [ { key: 'INIT', name: t('text.issue.init'), color: 'red' }, { key: 'ON_REVIEW', name: t('text.issue.onReview'), color: 'blue' }, { key: 'IN_PROGRESS', name: t('text.issue.inProgress'), color: 'yellow' }, { key: 'RESOLVED', name: t('text.issue.resolved'), color: 'green' }, { key: 'PENDING', name: t('text.issue.pending'), color: 'purple' }, ]; -export const getStatusColor = (status: IssueStatus): ColorType => { - switch (status) { - case 'INIT': - return 'red'; - case 'ON_REVIEW': - return 'blue'; - case 'IN_PROGRESS': - return 'yellow'; - case 'RESOLVED': - return 'green'; - case 'PENDING': - return 'purple'; - default: - return 'red'; - } -}; diff --git a/apps/web/src/constants/local-storage-key.ts b/apps/web/src/shared/constants/local-storage-key.ts similarity index 100% rename from apps/web/src/constants/local-storage-key.ts rename to apps/web/src/shared/constants/local-storage-key.ts diff --git a/apps/web/src/constants/path.ts b/apps/web/src/shared/constants/path.ts similarity index 56% rename from apps/web/src/constants/path.ts rename to apps/web/src/shared/constants/path.ts index c1c962746..8d09ed616 100644 --- a/apps/web/src/constants/path.ts +++ b/apps/web/src/shared/constants/path.ts @@ -16,59 +16,28 @@ class PathV3 { private static instance: PathV3; public static get Instance(): PathV3 { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return this.instance || (this.instance = new this()); } - get CREATE_TENANT() { - return '/tenant/create'; - } - get SIGN_IN() { - return '/auth/sign-in'; - } - - get SIGN_UP() { - return '/auth/sign-up'; - } - - get PASSWORD_RESET() { - return '/auth/reset-password'; - } - - get MAIN() { - return '/main'; - } - - get CREATE_PROJECT() { - return '/main/project/create'; - } - get CREATE_PROJECT_COMPLETE() { - return '/main/project/create-complete'; - } - get CREATE_CHANNEL() { - return '/main/project/[projectId]/channel/create'; - } - get CREATE_CHANNEL_COMPLETE() { - return '/main/project/[projectId]/channel/create-complete'; - } + readonly CREATE_TENANT = '/tenant/create'; + readonly SIGN_IN = '/auth/sign-in'; + readonly SIGN_UP = '/auth/sign-up'; + readonly PASSWORD_RESET = '/auth/reset-password'; + readonly MAIN = '/main'; + readonly CREATE_PROJECT = '/main/project/create'; + readonly CREATE_PROJECT_COMPLETE = '/main/project/create-complete'; + readonly CREATE_CHANNEL = '/main/project/[projectId]/channel/create'; + readonly CREATE_CHANNEL_COMPLETE = + '/main/project/[projectId]/channel/create-complete'; get PROJECT_MAIN() { return this.DASHBOARD; } - get DASHBOARD() { - return '/main/project/[projectId]/dashboard'; - } - - get FEEDBACK() { - return '/main/project/[projectId]/feedback'; - } - - get ISSUE() { - return '/main/project/[projectId]/issue'; - } - - get SETTINGS() { - return '/main/project/[projectId]/setting'; - } + readonly DASHBOARD = '/main/project/[projectId]/dashboard'; + readonly FEEDBACK = '/main/project/[projectId]/feedback'; + readonly ISSUE = '/main/project/[projectId]/issue'; + readonly SETTINGS = '/main/project/[projectId]/setting'; isErrorPage(pathname: string) { return pathname.startsWith('/error'); diff --git a/apps/web/src/shared/index.ts b/apps/web/src/shared/index.ts new file mode 100644 index 000000000..cb742f2e5 --- /dev/null +++ b/apps/web/src/shared/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export * from './ui'; +export * from './utils'; +export * from './constants'; +export * from './types'; +export * from './lib'; diff --git a/apps/web/src/libs/client.ts b/apps/web/src/shared/lib/client.ts similarity index 79% rename from apps/web/src/libs/client.ts rename to apps/web/src/shared/lib/client.ts index 2225c0738..cf096fb55 100644 --- a/apps/web/src/libs/client.ts +++ b/apps/web/src/shared/lib/client.ts @@ -13,22 +13,22 @@ * License for the specific language governing permissions and limitations * under the License. */ -import type { AxiosRequestConfig } from 'axios'; +import type { AxiosRequestConfig, AxiosResponse } from 'axios'; import axios from 'axios'; import createAuthRefreshInterceptor from 'axios-auth-refresh'; -import { Path } from '@/constants/path'; -import { env } from '@/env.mjs'; +import { getRequestUrl, Path, sessionStorage } from '@/shared'; import type { + Jwt, OAIMethodPathKeys, OAIMutationResponse, OAIPathParameters, OAIQueryParameters, OAIRequestBody, OAIResponse, -} from '@/types/openapi.type'; -import { getRequestUrl } from '@/utils/path-parsing'; -import sessionStorage from './session-storage'; +} from '@/shared'; + +import { env } from '@/env'; class client { private axiosInstance = axios.create({ @@ -37,39 +37,43 @@ class client { private static instance: client; public static get Instance(): client { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return this.instance || (this.instance = new this()); } constructor() { this.axiosInstance.interceptors.request.use((config) => { const token = sessionStorage.getItem('jwt'); - if (token) { config.headers.setAuthorization(`Bearer ${token.accessToken}`); } - return config; }); - createAuthRefreshInterceptor(this.axiosInstance, async (failedRequest) => { - try { - const { data } = await axios.get('/api/refresh-jwt'); - sessionStorage.setItem('jwt', data.jwt); - failedRequest.response.config.headers.setAuthorization( - `Bearer ${data.jwt.accessToken}`, - ); - } catch (error) { - await axios.get('/api/logout'); - sessionStorage.removeItem('jwt'); - window.location.assign(Path.SIGN_IN); - } - }); + createAuthRefreshInterceptor( + this.axiosInstance, + async (failedRequest: { response: AxiosResponse }) => { + try { + const { data } = await axios.get('/api/refresh-jwt'); + sessionStorage.setItem('jwt', data); + + failedRequest.response.config.headers.setAuthorization( + `Bearer ${data.accessToken}`, + ); + } catch (error) { + await axios.get('/api/logout'); + sessionStorage.removeItem('jwt'); + window.location.assign(Path.SIGN_IN); + } + }, + ); this.axiosInstance.interceptors.response.use( (response) => response, - (error) => Promise.reject(error.response?.data ?? error), + (error: { response?: AxiosResponse }) => + Promise.reject(error.response?.data ?? error), ); } - request(config: AxiosRequestConfig) { - return this.axiosInstance.request(config); + request(config: AxiosRequestConfig) { + return this.axiosInstance.request(config); } get< @@ -85,7 +89,7 @@ class client { }: { path: TPath } & (TPathParams extends undefined ? { pathParams?: TPathParams } : { pathParams: TPathParams }) & - (TQuery extends undefined ? { query?: any } : { query: TQuery }) & { + (TQuery extends undefined ? { query?: unknown } : { query: TQuery }) & { options?: Omit; }) { return this.axiosInstance.get(getRequestUrl(path, pathParams), { diff --git a/apps/web/src/types/webhook.type.ts b/apps/web/src/shared/lib/index.ts similarity index 52% rename from apps/web/src/types/webhook.type.ts rename to apps/web/src/shared/lib/index.ts index 19ae82ac0..7b3f46f9c 100644 --- a/apps/web/src/types/webhook.type.ts +++ b/apps/web/src/shared/lib/index.ts @@ -13,29 +13,16 @@ * License for the specific language governing permissions and limitations * under the License. */ -import type { ChannelType } from './channel.type'; +export { default as useOAIQuery } from './useOAIQuery'; +export { default as useOAIMutation } from './useOAIMutation'; -export type WebhookType = { - id: number; - name: string; - url: string; - status: WebhookStatusEnum; - events: WebhookEvent[]; - createdAt: string; -}; +export { default as useQueryParamsState } from './use-query-params-state'; -export type WebhookEvent = { - id: number; - status: WebhookStatusEnum; - type: WebhookEventEnum; - channels: ChannelType[]; - createdAt: string; -}; +export { default as useSort } from './use-sort'; +export { default as useLocalColumnSetting } from './use-local-column-setting'; +export { default as usePermissions } from './use-permissions'; -export type WebhookStatusEnum = 'ACTIVE' | 'INACTIVE'; +export { default as useHorizontalScroll } from './use-horizontal-scroll'; -export type WebhookEventEnum = - | 'FEEDBACK_CREATION' - | 'ISSUE_CREATION' - | 'ISSUE_STATUS_CHANGE' - | 'ISSUE_ADDITION'; +export { default as sessionStorage } from './session-storage'; +export { default as client } from './client'; diff --git a/apps/web/src/libs/session-storage.ts b/apps/web/src/shared/lib/session-storage.ts similarity index 83% rename from apps/web/src/libs/session-storage.ts rename to apps/web/src/shared/lib/session-storage.ts index 9168fcb25..d37a68c31 100644 --- a/apps/web/src/libs/session-storage.ts +++ b/apps/web/src/shared/lib/session-storage.ts @@ -15,6 +15,8 @@ */ import type { O } from 'ts-toolbelt'; +import { EMPTY_FUNCTION } from '../utils/empty-function'; + interface IData { jwt: { accessToken: string; @@ -31,6 +33,7 @@ class sessionStorage { private static instance: sessionStorage; public static get Instance(): sessionStorage { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return this.instance || (this.instance = new this()); } @@ -48,7 +51,7 @@ class sessionStorage { key: T, value: SessionStorageValueType, ) { - this.storage.setItem(key, JSON.stringify(value) as any); + this.storage.setItem(key, JSON.stringify(value)); } removeItem(key: T) { @@ -61,7 +64,9 @@ class sessionStorage { parseJSON(value: string | null): T | string | undefined | null { try { - return value === 'undefined' ? undefined : JSON.parse(value ?? ''); + return value === 'undefined' ? undefined : ( + (JSON.parse(value ?? '') as T | string | undefined | null) + ); } catch { return value; } @@ -70,9 +75,9 @@ class sessionStorage { const severSessionStorage: Storage = { getItem: (_: string) => null, - setItem: () => {}, - removeItem: () => {}, - clear: () => {}, + setItem: EMPTY_FUNCTION, + removeItem: EMPTY_FUNCTION, + clear: EMPTY_FUNCTION, key: (_: number) => null, length: 0, }; diff --git a/apps/web/src/hooks/useHorizontalScroll.ts b/apps/web/src/shared/lib/use-horizontal-scroll.ts similarity index 90% rename from apps/web/src/hooks/useHorizontalScroll.ts rename to apps/web/src/shared/lib/use-horizontal-scroll.ts index 877d7b20d..3c01b2e6e 100644 --- a/apps/web/src/hooks/useHorizontalScroll.ts +++ b/apps/web/src/shared/lib/use-horizontal-scroll.ts @@ -39,7 +39,8 @@ const useHorizontalScroll = (input: { }); }, [containerRef]); - const scrollLeft = () => { + const scrollToLeft = (e: React.MouseEvent) => { + e.stopPropagation(); if (!containerRef.current) return; const { scrollWidth, scrollLeft } = containerRef.current; @@ -50,7 +51,9 @@ const useHorizontalScroll = (input: { containerRef.current.scrollTo({ left, behavior: 'smooth' }); }; - const scrollRight = () => { + const scrollToRight = (e: React.MouseEvent) => { + e.stopPropagation(); + if (!containerRef.current) return; const { scrollWidth, scrollLeft } = containerRef.current; @@ -63,8 +66,8 @@ const useHorizontalScroll = (input: { return { containerRef, - scrollLeft, - scrollRight, + scrollToLeft, + scrollToRight, showLeftButton, showRightButton, }; diff --git a/apps/web/src/hooks/useLocalColumnSetting.ts b/apps/web/src/shared/lib/use-local-column-setting.ts similarity index 100% rename from apps/web/src/hooks/useLocalColumnSetting.ts rename to apps/web/src/shared/lib/use-local-column-setting.ts diff --git a/apps/web/src/hooks/usePermissions.ts b/apps/web/src/shared/lib/use-permissions.ts similarity index 83% rename from apps/web/src/hooks/usePermissions.ts rename to apps/web/src/shared/lib/use-permissions.ts index 9092ea7fe..f9f3c30c8 100644 --- a/apps/web/src/hooks/usePermissions.ts +++ b/apps/web/src/shared/lib/use-permissions.ts @@ -16,13 +16,14 @@ import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/router'; -import { useUser } from '@/contexts/user.context'; -import type { PermissionType } from '@/types/permission.type'; -import { PermissionList } from '@/types/permission.type'; +import { PermissionList } from '@/entities/role'; +import type { PermissionType } from '@/entities/role'; +import { useUserStore } from '@/entities/user'; + import useOAIQuery from './useOAIQuery'; const usePermissions = (inputProjectId?: number | null) => { - const { user } = useUser(); + const { user } = useUserStore(); const [permissions, setPermissions] = useState([]); const router = useRouter(); @@ -36,7 +37,7 @@ const usePermissions = (inputProjectId?: number | null) => { const projectId = useMemo(() => { if (inputProjectId) return inputProjectId; if (!router.query.projectId) return null; - return +router.query.projectId as number; + return Number(router.query.projectId); }, [router, inputProjectId]); useEffect(() => { @@ -44,7 +45,7 @@ const usePermissions = (inputProjectId?: number | null) => { if (!data) return; const role = data.roles.find((v) => v.project.id === projectId); if (!role) setPermissions([]); - else setPermissions(role.permissions as PermissionType[]); + else setPermissions(role.permissions); }, [data, projectId]); return permissions; diff --git a/apps/web/src/hooks/useQueryParamsState.ts b/apps/web/src/shared/lib/use-query-params-state.ts similarity index 59% rename from apps/web/src/hooks/useQueryParamsState.ts rename to apps/web/src/shared/lib/use-query-params-state.ts index 89a2b7a2a..1519e25ff 100644 --- a/apps/web/src/hooks/useQueryParamsState.ts +++ b/apps/web/src/shared/lib/use-query-params-state.ts @@ -17,51 +17,49 @@ import { useCallback, useMemo } from 'react'; import { useRouter } from 'next/router'; import dayjs from 'dayjs'; -import { DATE_FORMAT } from '@/constants/dayjs-format'; -import { removeEmptyValueInObject } from '@/utils/remove-empty-value-in-object'; +import { DATE_FORMAT, isDateQuery, removeEmptyValueInObject } from '@/shared'; const useQueryParamsState = ( - defaultNextQuery: Record, + defaultNextQuery: Record, defaultQuery?: Record, - validate?: (input: Record) => boolean, + validate?: (input: Record) => boolean, ) => { const router = useRouter(); - const query = useMemo(() => { + const query: Record = useMemo(() => { const newQuery = Object.entries(router.query).reduce( - (acc, [key, value]) => { - if (key in defaultNextQuery) return acc; - return { ...acc, [key]: value }; - }, - {} as Record, + (acc, [key, value]) => + key in defaultNextQuery ? acc : { ...acc, [key]: value }, + {}, ); - const result = { - ...defaultQuery, - ...removeEmptyValueInObject(newQuery), - }; + + const result = { ...defaultQuery, ...removeEmptyValueInObject(newQuery) }; + if (validate && !validate(result)) { - router.replace({ pathname: router.pathname, query: defaultNextQuery }); + void router.replace({ + pathname: router.pathname, + query: defaultNextQuery, + }); + return defaultQuery ?? {}; } return result; }, [router.query]); const setQuery = useCallback( - (input: Record) => { + (input: Record) => { const newQuery: Record = Object.entries( removeEmptyValueInObject(input), - ).reduce( - (acc, [key, value]) => { - if (typeof value === 'object' && isDate(value)) { - value = `${dayjs(value.gte).format(DATE_FORMAT)}~${dayjs( - value.lt, - ).format(DATE_FORMAT)}`; - } - return { ...acc, [key]: value }; - }, - {} as Record, - ); - router.push( + ).reduce((acc, [key, value]) => { + if (typeof value === 'object' && isDateQuery(value)) { + value = `${dayjs(value.gte).format(DATE_FORMAT)}~${dayjs( + value.lt, + ).format(DATE_FORMAT)}`; + } + + return { ...acc, [key]: value }; + }, {}); + void router.push( { pathname: router.pathname, query: { ...defaultNextQuery, ...defaultQuery, ...newQuery }, @@ -76,7 +74,3 @@ const useQueryParamsState = ( }; export default useQueryParamsState; - -const isDate = (value: any) => { - return 'gte' in value && 'lt' in value; -}; diff --git a/apps/web/src/hooks/useSort.ts b/apps/web/src/shared/lib/use-sort.ts similarity index 79% rename from apps/web/src/hooks/useSort.ts rename to apps/web/src/shared/lib/use-sort.ts index ebd84c2e0..e677591ea 100644 --- a/apps/web/src/hooks/useSort.ts +++ b/apps/web/src/shared/lib/use-sort.ts @@ -17,13 +17,15 @@ import { useMemo } from 'react'; import type { SortingState } from '@tanstack/react-table'; const useSort = (sorting: SortingState) => { - return useMemo(() => { - const result: Record = {}; - for (const item of sorting) { - result[item.id] = item.desc ? 'DESC' : 'ASC'; - } - return result; - }, [sorting]); + const sortObject = useMemo( + () => + sorting.reduce( + (acc, item) => ({ ...acc, [item.id]: item.desc ? 'DESC' : 'ASC' }), + {}, + ), + [sorting], + ); + return sortObject; }; export default useSort; diff --git a/apps/web/src/hooks/useOAIMutation.ts b/apps/web/src/shared/lib/useOAIMutation.ts similarity index 90% rename from apps/web/src/hooks/useOAIMutation.ts rename to apps/web/src/shared/lib/useOAIMutation.ts index 1a0fb1e07..4ff9e1a5e 100644 --- a/apps/web/src/hooks/useOAIMutation.ts +++ b/apps/web/src/shared/lib/useOAIMutation.ts @@ -16,15 +16,14 @@ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; -import client from '@/libs/client'; -import type { IFetchError } from '@/types/fetch-error.type'; import type { + IFetchError, OAIMethodPathKeys, OAIMutationResponse, OAIPathParameters, OAIRequestBody, -} from '@/types/openapi.type'; -import { getRequestUrl } from '@/utils/path-parsing'; +} from '@/shared'; +import { client, getRequestUrl } from '@/shared'; export default function useOAIMutation< TMethods extends 'post' | 'put' | 'patch' | 'delete', @@ -47,7 +46,7 @@ export default function useOAIMutation< TBody, (Record | TPath | undefined)[] >, - 'queryKey' | 'queryFn' + 'mutationKey' | 'mutationFn' >; } & (TPathParams extends undefined ? { pathParams?: undefined } : { pathParams: TPathParams })) { diff --git a/apps/web/src/hooks/useOAIQuery.ts b/apps/web/src/shared/lib/useOAIQuery.ts similarity index 90% rename from apps/web/src/hooks/useOAIQuery.ts rename to apps/web/src/shared/lib/useOAIQuery.ts index 7248a0f11..b341aa499 100644 --- a/apps/web/src/hooks/useOAIQuery.ts +++ b/apps/web/src/shared/lib/useOAIQuery.ts @@ -17,14 +17,13 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; import type { O } from 'ts-toolbelt'; -import client from '@/libs/client'; -import type { IFetchError } from '@/types/fetch-error.type'; +import { client, getRequestUrl } from '@/shared'; import type { + IFetchError, OAIMethodPathKeys, OAIParameters, OAIResponse, -} from '@/types/openapi.type'; -import { getRequestUrl } from '@/utils/path-parsing'; +} from '@/shared'; export default function useOAIQuery< TPath extends OAIMethodPathKeys<'get'>, diff --git a/apps/web/src/pages/_app.css b/apps/web/src/shared/styles/global.css similarity index 98% rename from apps/web/src/pages/_app.css rename to apps/web/src/shared/styles/global.css index 2f92d9015..aa59fdf03 100644 --- a/apps/web/src/pages/_app.css +++ b/apps/web/src/shared/styles/global.css @@ -25,6 +25,9 @@ animation: table-loading 1s ease-out infinite; content: ''; } + .card { + @apply border-fill-tertiary bg-tertiary rounded border p-6; + } } @keyframes table-loading { diff --git a/apps/web/src/styles/react-datepicker.css b/apps/web/src/shared/styles/react-datepicker.css similarity index 100% rename from apps/web/src/styles/react-datepicker.css rename to apps/web/src/shared/styles/react-datepicker.css diff --git a/apps/web/src/types/api.type.ts b/apps/web/src/shared/types/api.type.ts similarity index 68% rename from apps/web/src/types/api.type.ts rename to apps/web/src/shared/types/api.type.ts index 3e968a2b9..896e5c780 100644 --- a/apps/web/src/types/api.type.ts +++ b/apps/web/src/shared/types/api.type.ts @@ -1,208 +1,918 @@ /** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. */ export interface paths { '/api/admin/auth/email/code': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['AuthController_sendCode']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/auth/email/code/verify': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['AuthController_verifyEmailCode']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/auth/signUp/email': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['AuthController_signUpEmailUser']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/auth/signUp/invitation': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['AuthController_signUpInvitationUser']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/auth/signUp/oauth': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['AuthController_signUpOAuthUser']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/auth/signIn/email': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['AuthController_signInEmail']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/auth/signIn/oauth/loginURL': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['AuthController_redirectToLoginURL']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/auth/signIn/oauth': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['AuthController_handleCallback']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/auth/refresh': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['AuthController_refreshToken']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/users': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['UserController_getAllUsers']; + put?: never; + post?: never; delete: operations['UserController_deleteUsers']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/users/search': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['UserController_searchUsers']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/users/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['UserController_getUser']; put: operations['UserController_updateUser']; + post?: never; delete: operations['UserController_deleteUser']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/users/{userId}/roles': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['UserController_getRoles']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/users/invite': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['UserController_inviteUser']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/users/password/reset/code': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['UserController_requestResetPassword']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/users/password/reset': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['UserController_resetPassword']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/users/password/change': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['UserController_changePassword']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/tenants': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['TenantController_get']; put: operations['TenantController_update']; post: operations['TenantController_setup']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/tenants/{tenantId}/feedback-count': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['TenantController_countFeedbacks']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/roles': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['RoleController_getAllRolesByProjectId']; + put?: never; post: operations['RoleController_createRole']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/roles/{roleId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; put: operations['RoleController_updateRole']; + post?: never; delete: operations['RoleController_deleteRole']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/members': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['MemberController_getAllRolesByProjectId']; + put?: never; post: operations['MemberController_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/members/{memberId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; put: operations['MemberController_update']; + post?: never; delete: operations['MemberController_delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/api-keys': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['ApiKeyController_findAll']; + put?: never; post: operations['ApiKeyController_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/api-keys/{apiKeyId}/soft': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; delete: operations['ApiKeyController_softDelete']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/api-keys/{apiKeyId}/recover': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; delete: operations['ApiKeyController_recover']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/api-keys/{apiKeyId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; delete: operations['ApiKeyController_delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['ChannelController_findAllByProjectId']; + put?: never; post: operations['ChannelController_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels/name-check': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['ChannelController_checkName']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels/{channelId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['ChannelController_findOne']; put: operations['ChannelController_updateOne']; + post?: never; delete: operations['ChannelController_delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels/{channelId}/fields': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; put: operations['ChannelController_updateFields']; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels/image-upload-url-test': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['ChannelController_getImageUploadUrlTest']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/fields/{fieldId}/options': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['OptionController_getOptions']; + put?: never; post: operations['OptionController_createOption']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['ProjectController_findAll']; + put?: never; post: operations['ProjectController_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/name-check': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['ProjectController_checkName']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['ProjectController_findOne']; put: operations['ProjectController_updateOne']; + post?: never; delete: operations['ProjectController_delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/feedback-count': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['ProjectController_countFeedbacks']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/issue-count': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['ProjectController_countIssues']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels/{channelId}/feedbacks': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['FeedbackController_create']; delete: operations['FeedbackController_deleteMany']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels/{channelId}/feedbacks/search': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['FeedbackController_findByChannelId']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/issue/{issueId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['FeedbackController_addIssue']; delete: operations['FeedbackController_removeIssue']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels/{channelId}/feedbacks/export': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['FeedbackController_exportFeedbacks']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; put: operations['FeedbackController_updateFeedback']; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/issues': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['IssueController_create']; delete: operations['IssueController_deleteMany']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/issues/{issueId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['IssueController_findById']; put: operations['IssueController_update']; + post?: never; delete: operations['IssueController_delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/issues/search': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; post: operations['IssueController_findAllByProjectId']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/statistics/issue/count': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['IssueStatisticsController_getCount']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/statistics/issue/count-by-date': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['IssueStatisticsController_getCountByDate']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/statistics/issue/count-by-status': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['IssueStatisticsController_getCountByStatus']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/statistics/feedback': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['FeedbackStatisticsController_getCountByDateByChannel']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/statistics/feedback/count': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['FeedbackStatisticsController_getCount']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/statistics/feedback/issued-ratio': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['FeedbackStatisticsController_getIssuedRatio']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/statistics/feedback-issue': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['FeedbackIssueStatisticsController_getCountByDateByIssue']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/issue-tracker': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['IssueTrackerController_findOne']; put: operations['IssueTrackerController_updateOne']; post: operations['IssueTrackerController_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/webhooks': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['WebhookController_getByProjectId']; + put?: never; post: operations['WebhookController_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/admin/projects/{projectId}/webhooks/{webhookId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; get: operations['WebhookController_get']; put: operations['WebhookController_update']; + post?: never; delete: operations['WebhookController_delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; }; } - export type webhooks = Record; - export interface components { schemas: { EmailVerificationMailingRequestDto: { @@ -439,7 +1149,7 @@ export interface components { GetTenantResponseDto: { id: number; siteName: string; - description: string; + description: string | null; useEmail: boolean; useOAuth: boolean; isPrivate: boolean; @@ -815,8 +1525,12 @@ export interface components { CreateApiKeyByValueDto: { value: string; }; + IssueTrackerDataDto: { + ticketDomain: string | null; + ticketKey: string | null; + }; CreateIssueTrackerRequestDto: { - data: Record; + data: components['schemas']['IssueTrackerDataDto']; }; CreateProjectRequestDto: { name: string; @@ -868,19 +1582,15 @@ export interface components { * @example payment */ searchText?: string; - /** - * @example { - * "gte": "2023-01-01", - * "lt": "2023-12-31" - * } - */ + /** @example { + * "gte": "2023-01-01", + * "lt": "2023-12-31" + * } */ createdAt?: components['schemas']['TimeRange']; - /** - * @example { - * "gte": "2023-01-01", - * "lt": "2023-12-31" - * } - */ + /** @example { + * "gte": "2023-01-01", + * "lt": "2023-12-31" + * } */ updatedAt?: components['schemas']['TimeRange']; }; FindFeedbacksByChannelIdRequestDto: { @@ -899,28 +1609,26 @@ export interface components { /** * @description You can sort by specific feedback key with sort method values: 'ASC', 'DESC' * @example { - * "createdAt": "ASC" - * } + * "createdAt": "ASC" + * } */ - sort?: Record; + sort?: Record; }; - Feedback: Record; + Feedback: Record; FindFeedbacksByChannelIdResponseDto: { meta: components['schemas']['PaginationMetaDto']; - /** - * @example [ - * { - * "id": 1, - * "name": "feedback", - * "issues": [ + /** @example [ * { * "id": 1, - * "name": "issue" + * "name": "feedback", + * "issues": [ + * { + * "id": 1, + * "name": "issue" + * } + * ] * } - * ] - * } - * ] - */ + * ] */ items: components['schemas']['Feedback'][]; }; AddIssueResponseDto: { @@ -951,10 +1659,10 @@ export interface components { /** * @description You can sort by specific feedback key with sort method values: 'ASC', 'DESC' * @example { - * "createdAt": "ASC" - * } + * "createdAt": "ASC" + * } */ - sort?: Record; + sort?: Record; type: string; fieldIds?: number[]; }; @@ -962,9 +1670,9 @@ export interface components { /** * @description Feedback ids in an array * @example [ - * 1, - * 2 - * ] + * 1, + * 2 + * ] */ feedbackIds: number[]; }; @@ -1041,17 +1749,17 @@ export interface components { /** * @description You can query by key-value with this object. If you want to search by text, you can use 'searchText' key. * @example { - * "name": "issue name" - * } + * "name": "issue name" + * } */ - query?: Record; + query?: Record; /** * @description You can sort by specific feedback key with sort method values: 'ASC', 'DESC' * @example { - * "createdAt": "ASC" - * } + * "createdAt": "ASC" + * } */ - sort?: Record; + sort?: Record; }; FindIssuesByProjectIdResponseDto: { meta: components['schemas']['PaginationMetaDto']; @@ -1084,10 +1792,10 @@ export interface components { /** * @description Issue ids in an array to delete in chunk * @example [ - * 1, - * 2, - * 3 - * ] + * 1, + * 2, + * 3 + * ] */ issueIds: number[]; }; @@ -1140,20 +1848,20 @@ export interface components { }; CreateIssueTrackerResponseDto: { id: number; - data: Record; + data: components['schemas']['IssueTrackerDataDto']; /** Format: date-time */ createdAt: string; }; FindIssueTrackerResponseDto: { id: number; - data: Record; + data: components['schemas']['IssueTrackerDataDto']; }; UpdateIssueTrackerRequestDto: { - data: Record; + data: components['schemas']['IssueTrackerDataDto']; }; UpdateIssueTrackerResponseDto: { id: number; - data: Record; + data: components['schemas']['IssueTrackerDataDto']; /** Format: date-time */ createdAt: string; }; @@ -1222,13 +1930,15 @@ export interface components { headers: never; pathItems: never; } - export type $defs = Record; - -export type external = Record; - export interface operations { AuthController_sendCode: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['EmailVerificationMailingRequestDto']; @@ -1236,6 +1946,7 @@ export interface operations { }; responses: { 201: { + headers: Record; content: { 'application/json': components['schemas']['SendEmailCodeResponseDto']; }; @@ -1243,6 +1954,12 @@ export interface operations { }; }; AuthController_verifyEmailCode: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['EmailVerificationCodeRequestDto']; @@ -1250,11 +1967,18 @@ export interface operations { }; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; AuthController_signUpEmailUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['EmailUserSignUpRequestDto']; @@ -1262,11 +1986,18 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; AuthController_signUpInvitationUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['InvitationUserSignUpRequestDto']; @@ -1274,11 +2005,18 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; AuthController_signUpOAuthUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['OAuthUserSignUpRequestDto']; @@ -1286,11 +2024,18 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; AuthController_signInEmail: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['EmailUserSignInRequestDto']; @@ -1298,6 +2043,7 @@ export interface operations { }; responses: { 201: { + headers: Record; content: { 'application/json': components['schemas']['SignInResponseDto']; }; @@ -1309,9 +2055,14 @@ export interface operations { query?: { callback_url?: string; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['OAuthLoginUrlResponseDto']; }; @@ -1319,15 +2070,31 @@ export interface operations { }; }; AuthController_handleCallback: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; AuthController_refreshToken: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['SignInResponseDto']; }; @@ -1342,9 +2109,14 @@ export interface operations { /** @example 1 */ page?: number; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['GetAllUserResponseDto']; }; @@ -1352,6 +2124,12 @@ export interface operations { }; }; UserController_deleteUsers: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['DeleteUsersRequestDto']; @@ -1359,11 +2137,18 @@ export interface operations { }; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; UserController_searchUsers: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['GetAllUsersRequestDto']; @@ -1371,6 +2156,7 @@ export interface operations { }; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['GetAllUserResponseDto']; }; @@ -1379,12 +2165,17 @@ export interface operations { }; UserController_getUser: { parameters: { + query?: never; + header?: never; path: { id: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['UserDto']; }; @@ -1393,9 +2184,12 @@ export interface operations { }; UserController_updateUser: { parameters: { + query?: never; + header?: never; path: { id: number; }; + cookie?: never; }; requestBody: { content: { @@ -1404,30 +2198,41 @@ export interface operations { }; responses: { 204: { - content: never; + headers: Record; + content?: never; }; }; }; UserController_deleteUser: { parameters: { + query?: never; + header?: never; path: { id: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; UserController_getRoles: { parameters: { + query?: never; + header?: never; path: { userId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['GetRolesByIdResponseDto']; }; @@ -1435,6 +2240,12 @@ export interface operations { }; }; UserController_inviteUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['UserInvitationRequestDto']; @@ -1442,11 +2253,18 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; UserController_requestResetPassword: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['ResetPasswordMailingRequestDto']; @@ -1454,11 +2272,18 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; UserController_resetPassword: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['ResetPasswordRequestDto']; @@ -1466,11 +2291,18 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; UserController_changePassword: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['ChangePasswordRequestDto']; @@ -1478,13 +2310,22 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; TenantController_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['GetTenantResponseDto']; }; @@ -1492,6 +2333,12 @@ export interface operations { }; }; TenantController_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['UpdateTenantRequestDto']; @@ -1499,11 +2346,18 @@ export interface operations { }; responses: { 204: { - content: never; + headers: Record; + content?: never; }; }; }; TenantController_setup: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['SetupTenantRequestDto']; @@ -1511,18 +2365,24 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; TenantController_countFeedbacks: { parameters: { + query?: never; + header?: never; path: { tenantId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['CountFeedbacksByTenantIdResponseDto']; }; @@ -1531,12 +2391,17 @@ export interface operations { }; RoleController_getAllRolesByProjectId: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['GetAllRolesResponseDto']; }; @@ -1545,9 +2410,12 @@ export interface operations { }; RoleController_createRole: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1556,16 +2424,20 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; RoleController_updateRole: { parameters: { + query?: never; + header?: never; path: { projectId: number; roleId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1574,20 +2446,26 @@ export interface operations { }; responses: { 204: { - content: never; + headers: Record; + content?: never; }; }; }; RoleController_deleteRole: { parameters: { + query?: never; + header?: never; path: { roleId: number; projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; @@ -1596,12 +2474,16 @@ export interface operations { query: { createdAt: string; }; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['GetAllMemberResponseDto']; }; @@ -1610,9 +2492,12 @@ export interface operations { }; MemberController_create: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1621,16 +2506,20 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; MemberController_update: { parameters: { + query?: never; + header?: never; path: { memberId: number; projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1639,31 +2528,42 @@ export interface operations { }; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; MemberController_delete: { parameters: { + query?: never; + header?: never; path: { memberId: number; projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; ApiKeyController_findAll: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindApiKeysResponseDto']; }; @@ -1672,9 +2572,12 @@ export interface operations { }; ApiKeyController_create: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1683,6 +2586,7 @@ export interface operations { }; responses: { 201: { + headers: Record; content: { 'application/json': components['schemas']['CreateApiKeyResponseDto']; }; @@ -1691,37 +2595,52 @@ export interface operations { }; ApiKeyController_softDelete: { parameters: { + query?: never; + header?: never; path: { apiKeyId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; ApiKeyController_recover: { parameters: { + query?: never; + header?: never; path: { apiKeyId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; ApiKeyController_delete: { parameters: { + query?: never; + header?: never; path: { apiKeyId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; @@ -1734,12 +2653,16 @@ export interface operations { page?: number; searchText?: string; }; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindChannelsByProjectIdResponseDto']; }; @@ -1748,9 +2671,12 @@ export interface operations { }; ChannelController_create: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1759,6 +2685,7 @@ export interface operations { }; responses: { 201: { + headers: Record; content: { 'application/json': components['schemas']['CreateChannelResponseDto']; }; @@ -1770,25 +2697,36 @@ export interface operations { query: { name: string; }; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content: { + 'application/json': boolean; + }; }; }; }; ChannelController_findOne: { parameters: { + query?: never; + header?: never; path: { channelId: number; projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindChannelByIdResponseDto']; }; @@ -1797,10 +2735,13 @@ export interface operations { }; ChannelController_updateOne: { parameters: { + query?: never; + header?: never; path: { channelId: number; projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1809,29 +2750,38 @@ export interface operations { }; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; ChannelController_delete: { parameters: { + query?: never; + header?: never; path: { channelId: number; projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; ChannelController_updateFields: { parameters: { + query?: never; + header?: never; path: { channelId: number; projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1840,15 +2790,19 @@ export interface operations { }; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; ChannelController_getImageUploadUrlTest: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1857,6 +2811,7 @@ export interface operations { }; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['ImageUploadUrlTestResponseDto']; }; @@ -1865,12 +2820,17 @@ export interface operations { }; OptionController_getOptions: { parameters: { + query?: never; + header?: never; path: { fieldId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindOptionByFieldIdResponseDto'][]; }; @@ -1879,9 +2839,12 @@ export interface operations { }; OptionController_createOption: { parameters: { + query?: never; + header?: never; path: { fieldId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1890,6 +2853,7 @@ export interface operations { }; responses: { 201: { + headers: Record; content: { 'application/json': components['schemas']['CreateOptionResponseDto']; }; @@ -1905,9 +2869,14 @@ export interface operations { page?: number; searchText?: string; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindProjectsResponseDto']; }; @@ -1915,6 +2884,12 @@ export interface operations { }; }; ProjectController_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': components['schemas']['CreateProjectRequestDto']; @@ -1922,6 +2897,7 @@ export interface operations { }; responses: { 201: { + headers: Record; content: { 'application/json': components['schemas']['CreateProjectResponseDto']; }; @@ -1933,21 +2909,31 @@ export interface operations { query: { name: string; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; ProjectController_findOne: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindProjectByIdResponseDto']; }; @@ -1956,9 +2942,12 @@ export interface operations { }; ProjectController_updateOne: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -1967,6 +2956,7 @@ export interface operations { }; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['UpdateProjectResponseDto']; }; @@ -1975,24 +2965,34 @@ export interface operations { }; ProjectController_delete: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; ProjectController_countFeedbacks: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['CountFeedbacksByIdResponseDto']; }; @@ -2001,12 +3001,17 @@ export interface operations { }; ProjectController_countIssues: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['CountIssuesByIdResponseDto']; }; @@ -2015,23 +3020,31 @@ export interface operations { }; FeedbackController_create: { parameters: { + query?: never; + header?: never; path: { projectId: number; channelId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; FeedbackController_deleteMany: { parameters: { + query?: never; + header?: never; path: { channelId: number; projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2040,16 +3053,20 @@ export interface operations { }; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; FeedbackController_findByChannelId: { parameters: { + query?: never; + header?: never; path: { channelId: number; projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2058,6 +3075,7 @@ export interface operations { }; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindFeedbacksByChannelIdResponseDto']; }; @@ -2066,15 +3084,20 @@ export interface operations { }; FeedbackController_addIssue: { parameters: { + query?: never; + header?: never; path: { channelId: number; feedbackId: number; issueId: number; projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['AddIssueResponseDto']; }; @@ -2083,15 +3106,20 @@ export interface operations { }; FeedbackController_removeIssue: { parameters: { + query?: never; + header?: never; path: { channelId: number; feedbackId: number; issueId: number; projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['AddIssueResponseDto']; }; @@ -2100,10 +3128,13 @@ export interface operations { }; FeedbackController_exportFeedbacks: { parameters: { + query?: never; + header?: never; path: { channelId: number; projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2112,29 +3143,38 @@ export interface operations { }; responses: { 201: { - content: never; + headers: Record; + content?: never; }; }; }; FeedbackController_updateFeedback: { parameters: { + query?: never; + header?: never; path: { channelId: number; feedbackId: number; projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; IssueController_create: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2143,6 +3183,7 @@ export interface operations { }; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['CreateIssueResponseDto']; }; @@ -2151,9 +3192,12 @@ export interface operations { }; IssueController_deleteMany: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2162,19 +3206,25 @@ export interface operations { }; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; IssueController_findById: { parameters: { + query?: never; + header?: never; path: { issueId: number; projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindIssueByIdResponseDto'][]; }; @@ -2183,10 +3233,13 @@ export interface operations { }; IssueController_update: { parameters: { + query?: never; + header?: never; path: { projectId: number; issueId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2195,28 +3248,37 @@ export interface operations { }; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; IssueController_delete: { parameters: { + query?: never; + header?: never; path: { issueId: number; projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { - content: never; + headers: Record; + content?: never; }; }; }; IssueController_findAllByProjectId: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2225,6 +3287,7 @@ export interface operations { }; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindIssuesByProjectIdResponseDto']; }; @@ -2238,9 +3301,14 @@ export interface operations { to: string; projectId: number; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindCountResponseDto']; }; @@ -2255,9 +3323,14 @@ export interface operations { interval: string; projectId: number; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindCountByDateResponseDto']; }; @@ -2269,9 +3342,14 @@ export interface operations { query: { projectId: number; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindCountByStatusResponseDto']; }; @@ -2286,9 +3364,14 @@ export interface operations { interval: string; channelIds: string; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindCountByDateByChannelResponseDto']; }; @@ -2302,9 +3385,14 @@ export interface operations { to: string; projectId: number; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindCountResponseDto']; }; @@ -2318,9 +3406,14 @@ export interface operations { to: string; projectId: number; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindIssuedRateResponseDto']; }; @@ -2335,9 +3428,14 @@ export interface operations { interval: string; issueIds: string; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindCountByDateByIssueResponseDto']; }; @@ -2346,12 +3444,17 @@ export interface operations { }; IssueTrackerController_findOne: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['FindIssueTrackerResponseDto']; }; @@ -2360,9 +3463,12 @@ export interface operations { }; IssueTrackerController_updateOne: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2371,6 +3477,7 @@ export interface operations { }; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['UpdateIssueTrackerResponseDto']; }; @@ -2379,9 +3486,12 @@ export interface operations { }; IssueTrackerController_create: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2390,6 +3500,7 @@ export interface operations { }; responses: { 201: { + headers: Record; content: { 'application/json': components['schemas']['CreateIssueTrackerResponseDto']; }; @@ -2398,12 +3509,17 @@ export interface operations { }; WebhookController_getByProjectId: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['GetWebhooksByProjectIdResponseDto']; }; @@ -2412,9 +3528,12 @@ export interface operations { }; WebhookController_create: { parameters: { + query?: never; + header?: never; path: { projectId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2423,6 +3542,7 @@ export interface operations { }; responses: { 201: { + headers: Record; content: { 'application/json': components['schemas']['CreateWebhookResponseDto']; }; @@ -2431,12 +3551,17 @@ export interface operations { }; WebhookController_get: { parameters: { + query?: never; + header?: never; path: { webhookId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['GetWebhookByIdResponseDto']; }; @@ -2445,10 +3570,13 @@ export interface operations { }; WebhookController_update: { parameters: { + query?: never; + header?: never; path: { projectId: number; webhookId: number; }; + cookie?: never; }; requestBody: { content: { @@ -2457,6 +3585,7 @@ export interface operations { }; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['UpdateWebhookResponseDto']; }; @@ -2465,12 +3594,17 @@ export interface operations { }; WebhookController_delete: { parameters: { + query?: never; + header?: never; path: { webhookId: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { + headers: Record; content: { 'application/json': components['schemas']['GetWebhookByIdResponseDto']; }; diff --git a/apps/web/src/types/date-range.type.ts b/apps/web/src/shared/types/date-range.type.ts similarity index 100% rename from apps/web/src/types/date-range.type.ts rename to apps/web/src/shared/types/date-range.type.ts diff --git a/apps/web/src/types/fetch-error.type.ts b/apps/web/src/shared/types/fetch-error.type.ts similarity index 97% rename from apps/web/src/types/fetch-error.type.ts rename to apps/web/src/shared/types/fetch-error.type.ts index 92215c91b..db510953b 100644 --- a/apps/web/src/types/fetch-error.type.ts +++ b/apps/web/src/shared/types/fetch-error.type.ts @@ -16,7 +16,7 @@ export interface IFetchError { code: string; - message: string; + message?: string; statusCode: number; path: string; } diff --git a/apps/web/src/types/i18n.d.ts b/apps/web/src/shared/types/i18n.d.ts similarity index 71% rename from apps/web/src/types/i18n.d.ts rename to apps/web/src/shared/types/i18n.d.ts index 4c7d0b16a..96adcbe35 100644 --- a/apps/web/src/types/i18n.d.ts +++ b/apps/web/src/shared/types/i18n.d.ts @@ -15,14 +15,18 @@ */ import 'i18next'; -import en from '../../public/locales/en/common.json'; -import jp from '../../public/locales/jp/common.json'; -import ko from '../../public/locales/ko/common.json'; +import de from '../../../public/locales/de/common.json'; +import en from '../../../public/locales/en/common.json'; +import ja from '../../../public/locales/ja/common.json'; +import ko from '../../../public/locales/ko/common.json'; +import zh from '../../../public/locales/zh/common.json'; export const resources = { + en: { common: de }, en: { common: en }, ko: { common: ko }, - jp: { common: jp }, + ja: { common: ja }, + ja: { common: zh }, } as const; declare module 'i18next' { diff --git a/apps/web/src/shared/types/index.ts b/apps/web/src/shared/types/index.ts new file mode 100644 index 000000000..91a5d6198 --- /dev/null +++ b/apps/web/src/shared/types/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export type { NextPageWithLayout } from './page-with-layout.type'; +export * from './jwt.type'; +export * from './date-range.type'; +export * from './api.type'; +export * from './fetch-error.type'; +export * from './openapi.type'; diff --git a/apps/web/src/shared/types/jwt.type.ts b/apps/web/src/shared/types/jwt.type.ts new file mode 100644 index 000000000..ac4a9f4ed --- /dev/null +++ b/apps/web/src/shared/types/jwt.type.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export interface Jwt { + accessToken: string; + refreshToken: string; +} diff --git a/apps/web/src/types/openapi.type.ts b/apps/web/src/shared/types/openapi.type.ts similarity index 98% rename from apps/web/src/types/openapi.type.ts rename to apps/web/src/shared/types/openapi.type.ts index 7330c02fd..7786d2cab 100644 --- a/apps/web/src/types/openapi.type.ts +++ b/apps/web/src/shared/types/openapi.type.ts @@ -15,7 +15,7 @@ */ import type { O } from 'ts-toolbelt'; -import type { paths } from '@/types/api.type'; +import type { paths } from './api.type'; export type OAIPathKeys = keyof paths; export type OAIMethods = 'get' | 'put' | 'post' | 'delete' | 'patch'; diff --git a/apps/web/src/types/user.type.ts b/apps/web/src/shared/types/page-with-layout.type.ts similarity index 78% rename from apps/web/src/types/user.type.ts rename to apps/web/src/shared/types/page-with-layout.type.ts index df2cc121a..3e32c1852 100644 --- a/apps/web/src/types/user.type.ts +++ b/apps/web/src/shared/types/page-with-layout.type.ts @@ -13,9 +13,8 @@ * License for the specific language governing permissions and limitations * under the License. */ -export type UserType = { - id: number; - email: string; - name: string | null; - department: string | null; +import type { NextPage } from 'next'; + +export type NextPageWithLayout

= NextPage & { + getLayout?: (page: React.ReactElement) => React.ReactNode; }; diff --git a/apps/web/src/shared/types/react-query-state.type.ts b/apps/web/src/shared/types/react-query-state.type.ts new file mode 100644 index 000000000..6f25a66c7 --- /dev/null +++ b/apps/web/src/shared/types/react-query-state.type.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export type ReactQueryState = 'error' | 'success' | 'pending'; diff --git a/apps/web/src/types/svg.d.ts b/apps/web/src/shared/types/svg.d.ts similarity index 100% rename from apps/web/src/types/svg.d.ts rename to apps/web/src/shared/types/svg.d.ts diff --git a/apps/web/src/shared/ui/__snapshots__/main-card.ui.spec.tsx.snap b/apps/web/src/shared/ui/__snapshots__/main-card.ui.spec.tsx.snap new file mode 100644 index 000000000..c65df2c29 --- /dev/null +++ b/apps/web/src/shared/ui/__snapshots__/main-card.ui.spec.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MainCard snapshot 1`] = ` +

+
+
+
+ +
+
+

+ est +

+

+ tutamen arcesso abeo +

+
+
+
+
+

+ speciosus +

+

+ 2,507,328,496,599,040 +

+
+
+

+ cupiditas +

+

+ 3,823,714,539,929,600 +

+
+
+
+
+
+`; diff --git a/apps/web/src/components/charts/ChartContainer.tsx b/apps/web/src/shared/ui/charts/chart-container.tsx similarity index 92% rename from apps/web/src/components/charts/ChartContainer.tsx rename to apps/web/src/shared/ui/charts/chart-container.tsx index 55d438121..f2caadc19 100644 --- a/apps/web/src/components/charts/ChartContainer.tsx +++ b/apps/web/src/shared/ui/charts/chart-container.tsx @@ -14,9 +14,9 @@ * under the License. */ -import { DescriptionTooltip } from '../etc'; -import ChartFilter from './ChartFilter'; -import Legend from './Legend'; +import DescriptionTooltip from '../description-tooltip'; +import ChartFilter from './chart-filter'; +import Legend from './legend'; interface IProps extends React.PropsWithChildren { title: string; diff --git a/apps/web/src/components/charts/ChartFilter.tsx b/apps/web/src/shared/ui/charts/chart-filter.tsx similarity index 100% rename from apps/web/src/components/charts/ChartFilter.tsx rename to apps/web/src/shared/ui/charts/chart-filter.tsx diff --git a/apps/web/src/containers/setting-menu/RoleSetting/index.ts b/apps/web/src/shared/ui/charts/index.ts similarity index 83% rename from apps/web/src/containers/setting-menu/RoleSetting/index.ts rename to apps/web/src/shared/ui/charts/index.ts index 38d8f5d78..77cd44e1a 100644 --- a/apps/web/src/containers/setting-menu/RoleSetting/index.ts +++ b/apps/web/src/shared/ui/charts/index.ts @@ -13,5 +13,5 @@ * License for the specific language governing permissions and limitations * under the License. */ -export { default } from './RoleSetting'; -export { default as RoleSettingTable } from './RoleSettingTable'; +export { default as SimpleBarChart } from './simple-bar-chart'; +export { default as SimpleLineChart } from './simple-line-chart'; diff --git a/apps/web/src/components/charts/Legend.tsx b/apps/web/src/shared/ui/charts/legend.tsx similarity index 97% rename from apps/web/src/components/charts/Legend.tsx rename to apps/web/src/shared/ui/charts/legend.tsx index 2013d560d..82267131a 100644 --- a/apps/web/src/components/charts/Legend.tsx +++ b/apps/web/src/shared/ui/charts/legend.tsx @@ -20,7 +20,7 @@ interface IProps { const Legend: React.FC = ({ dataKeys }) => { return (
- {dataKeys?.map((v, i) => ( + {dataKeys.map((v, i) => (
void; + onClick?: (data?: Data) => void; } const SimpleBarChart: React.FC = (props) => { @@ -50,7 +55,9 @@ const SimpleBarChart: React.FC = (props) => { data={data} margin={{ left: -5, right: 10, top: 10, bottom: 10 }} barSize={16} - onClick={(e) => onClick?.(e.activePayload?.[0].payload)} + onClick={(e: { activePayload?: { payload: Data }[] }) => + onClick?.(e.activePayload?.[0]?.payload) + } > = (props) => { tickLine={false} /> v.toLocaleString()} + tickFormatter={(v: string) => v.toLocaleString()} className="font-10-regular text-secondary" tickSize={15} tickLine={false} diff --git a/apps/web/src/components/charts/LineChart.tsx b/apps/web/src/shared/ui/charts/simple-line-chart.tsx similarity index 53% rename from apps/web/src/components/charts/LineChart.tsx rename to apps/web/src/shared/ui/charts/simple-line-chart.tsx index 3a767fe9a..96c57aa4b 100644 --- a/apps/web/src/components/charts/LineChart.tsx +++ b/apps/web/src/shared/ui/charts/simple-line-chart.tsx @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ + import { useMemo } from 'react'; import dayjs from 'dayjs'; import { useTranslation } from 'react-i18next'; @@ -20,7 +21,7 @@ import type { TooltipProps } from 'recharts'; import { CartesianGrid, Line, - LineChart as LineRechart, + LineChart, ResponsiveContainer, Tooltip, XAxis, @@ -31,71 +32,96 @@ import type { ValueType, } from 'recharts/types/component/DefaultTooltipContent'; +import ChartContainer from './chart-container'; + interface IProps { - dataKeys: { color: string; name: string }[]; + title: string; + description?: string; height?: number; - data: any[]; + data: unknown[]; + dataKeys: { color: string; name: string }[]; + showLegend?: boolean; + filterContent?: React.ReactNode; noLabel?: boolean; } -const LineChart: React.FC = ({ - dataKeys, - height, - data, - noLabel = false, -}) => { +const SimpleLineChart: React.FC = (props) => { + const { + title, + description, + height, + data, + dataKeys, + showLegend, + filterContent, + noLabel = false, + } = props; + return ( - - - - } - formatter={(value) => value.toLocaleString()} - /> - - v.toLocaleString()} - className="font-10-regular text-secondary" - tickSize={15} - tickLine={false} - /> - {dataKeys.map(({ color, name }) => ( - + + + - ))} - - + } + formatter={(value) => value.toLocaleString()} + /> + + v.toLocaleString()} + className="font-10-regular text-secondary" + tickSize={15} + tickLine={false} + min={0} + /> + {dataKeys.map(({ color, name }) => ( + + ))} + + + ); }; -const CustomTooltip: React.FC< - TooltipProps & { noLabel: boolean } -> = (props) => { +interface ICustomTooltipProps + extends Omit, 'label'> { + noLabel: boolean; + label?: string; +} + +const CustomTooltip: React.FC = (props) => { const { active, payload, label, noLabel } = props; const { t } = useTranslation(); const days = useMemo(() => { @@ -142,4 +168,4 @@ const CustomTooltip: React.FC< ); }; -export default LineChart; +export default SimpleLineChart; diff --git a/apps/web/src/components/templates/CreateProjectChannelInputTemplate/CreateProjectChannelInputTemplate.tsx b/apps/web/src/shared/ui/create-input-template.ui.tsx similarity index 77% rename from apps/web/src/components/templates/CreateProjectChannelInputTemplate/CreateProjectChannelInputTemplate.tsx rename to apps/web/src/shared/ui/create-input-template.ui.tsx index 1eba1afd7..79dc77fae 100644 --- a/apps/web/src/components/templates/CreateProjectChannelInputTemplate/CreateProjectChannelInputTemplate.tsx +++ b/apps/web/src/shared/ui/create-input-template.ui.tsx @@ -15,19 +15,22 @@ */ import { useTranslation } from 'react-i18next'; +import { cn } from '../utils'; + interface IProps extends React.PropsWithChildren { actionButton?: React.ReactNode; - onNext: () => void; - onPrev: () => void; - onComplete?: () => void; - title: string; - currentStepIndex: number; - lastStepIndex: number; + title: React.ReactNode; + currentStep: number; + lastStep: number; validate?: () => Promise | boolean; disableNextBtn?: boolean; + + onNext: () => void; + onPrev: () => void; + onComplete: () => void; } -const CreateProjectChannelInputTemplate: React.FC = (props) => { +const CreateInputTemplate: React.FC = (props) => { const { onNext, onPrev, @@ -35,13 +38,16 @@ const CreateProjectChannelInputTemplate: React.FC = (props) => { actionButton, title, children, - currentStepIndex, - lastStepIndex, + currentStep, + lastStep, validate, disableNextBtn, } = props; + const { t } = useTranslation(); + const isLastStep = currentStep === lastStep; + return (
@@ -53,7 +59,7 @@ const CreateProjectChannelInputTemplate: React.FC = (props) => {
{children}
- {currentStepIndex !== 0 && ( + {currentStep !== 0 && ( )}
); }; -export default CreateProjectChannelInputTemplate; +export default CreateInputTemplate; diff --git a/apps/web/src/components/templates/CreateSectionTemplate/CreateSectionTemplate.tsx b/apps/web/src/shared/ui/create-section-template.ui.tsx.tsx similarity index 93% rename from apps/web/src/components/templates/CreateSectionTemplate/CreateSectionTemplate.tsx rename to apps/web/src/shared/ui/create-section-template.ui.tsx.tsx index 0df4ce4c6..774cf6ac3 100644 --- a/apps/web/src/components/templates/CreateSectionTemplate/CreateSectionTemplate.tsx +++ b/apps/web/src/shared/ui/create-section-template.ui.tsx.tsx @@ -17,6 +17,8 @@ import { useState } from 'react'; import { Icon } from '@ufb/ui'; +import { cn } from '../utils'; + interface IProps extends React.PropsWithChildren { title: string; defaultOpen?: boolean; @@ -39,18 +41,18 @@ const CreateSectionTemplate: React.FC = ({
{children}
diff --git a/apps/web/src/shared/ui/create-template.ui.tsx b/apps/web/src/shared/ui/create-template.ui.tsx new file mode 100644 index 000000000..9801d1557 --- /dev/null +++ b/apps/web/src/shared/ui/create-template.ui.tsx @@ -0,0 +1,136 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import React, { Fragment } from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { useTranslation } from 'react-i18next'; + +import { Icon } from '@ufb/ui'; + +import { Path } from '@/shared'; + +import { cn } from '../utils'; + +type Target = 'project' | 'channel'; + +interface IProps + extends React.PropsWithChildren { + type: Target; + steps: readonly T[]; + stepTitle: Record; + helpText: React.ReactNode; + currentStep: number; + editingStep: number; +} + +const CreateTemplate = (props: IProps) => { + const { + children, + type, + currentStep, + steps, + editingStep, + stepTitle, + helpText, + } = props; + + const { t } = useTranslation(); + const router = useRouter(); + const ROUTE: Record void> = { + channel: () => + router.push({ + pathname: Path.PROJECT_MAIN, + query: { projectId: router.query.projectId }, + }), + project: () => router.push({ pathname: Path.MAIN }), + }; + + return ( +
+ {/* header */} +
+
+ logo + +
+ +
+ {/* title */} +

+ {type === 'project' && t('main.create-project.title')} + {type === 'channel' && t('main.create-channel.title')} +

+ {/* stepper */} +
+ {steps.map((step, i) => ( + +
+
+ {i + 1 > editingStep ? + i + 1 + : + } +
+
{stepTitle[step]}
+
+ {steps.length - 1 !== i && ( +
+ )} + + ))} +
+ {/* helper box */} +
+
+ +

{t('text.helper')}

+
+

{helpText}

+
+ {children} +
+ ); +}; + +export default CreateTemplate; diff --git a/apps/web/src/components/cards/DashboardCard/DashboardCard.tsx b/apps/web/src/shared/ui/dashboard-card.tsx similarity index 68% rename from apps/web/src/components/cards/DashboardCard/DashboardCard.tsx rename to apps/web/src/shared/ui/dashboard-card.tsx index bd5bf8426..899615dfa 100644 --- a/apps/web/src/components/cards/DashboardCard/DashboardCard.tsx +++ b/apps/web/src/shared/ui/dashboard-card.tsx @@ -15,7 +15,7 @@ */ import { Icon } from '@ufb/ui'; -import { DescriptionTooltip } from '@/components/etc'; +import { cn, DescriptionTooltip } from '@/shared'; interface IProps { title: string; @@ -28,7 +28,7 @@ const DashboardCard: React.FC = (props) => { const { title, data, percentage, description } = props; return ( -
+

{title} {description && ( @@ -39,33 +39,36 @@ const DashboardCard: React.FC = (props) => {

{typeof data === 'number' ? data.toLocaleString() : data}

- {typeof percentage === 'undefined' ? - <> - : isNaN(percentage) || !isFinite(percentage) ? - - :
- {percentage === 0 ? - - : percentage > 0 ? - - : - } -

+ 0 ? + 'TriangleUp' + : 'TriangleDown' + } className={ percentage === 0 ? 'text-secondary' : percentage > 0 ? 'text-blue-primary' : 'text-red-primary' } + size={16} + /> +

0 && 'text-blue-primary', + percentage < 0 && 'text-red-primary', + )} > {parseFloat(Math.abs(percentage).toFixed(1))}%

- } + : typeof percentage !== 'undefined' && isNaN(percentage) ? + + : <>}
); diff --git a/apps/web/src/components/etc/DateRangePicker/DateRangePicker.tsx b/apps/web/src/shared/ui/date-range-picker.tsx similarity index 91% rename from apps/web/src/components/etc/DateRangePicker/DateRangePicker.tsx rename to apps/web/src/shared/ui/date-range-picker.tsx index ff15559ea..daf109116 100644 --- a/apps/web/src/components/etc/DateRangePicker/DateRangePicker.tsx +++ b/apps/web/src/shared/ui/date-range-picker.tsx @@ -22,7 +22,8 @@ import { useTranslation } from 'react-i18next'; import { Icon, Popover, PopoverContent, PopoverTrigger, toast } from '@ufb/ui'; -import type { DateRangeType } from '@/types/date-range.type'; +import type { DateRangeType } from '../types/date-range.type'; +import { cn } from '../utils'; dayjs.extend(weekday); @@ -36,7 +37,7 @@ interface IProps { maxDays?: number; isClearable?: boolean; options?: { - label: string; + label: string | React.ReactNode; startDate: Date; endDate: Date; }[]; @@ -47,6 +48,7 @@ const DateRangePicker: React.FC = (props) => { props; const { t, i18n } = useTranslation(); + const items = useMemo(() => { return [ { @@ -76,6 +78,7 @@ const DateRangePicker: React.FC = (props) => { }, ]; }, [t]); + const [currentValue, setCurrentValue] = useState(value); const [isOpen, setIsOpen] = useState(false); @@ -98,7 +101,7 @@ const DateRangePicker: React.FC = (props) => { }; const handleApply = () => { - if (!currentValue?.startDate || !currentValue?.endDate) return; + if (!currentValue?.startDate || !currentValue.endDate) return; if (maxDays && isOverMaxDays(currentValue, maxDays)) { toast.negative({ title: t('text.date.date-range-over-max-days', { maxDays }), @@ -113,21 +116,21 @@ const DateRangePicker: React.FC = (props) => {
setIsOpen(true)} >

{currentValue ? `${ - currentValue?.startDate ? - dayjs(currentValue?.startDate).format(DATE_FORMAT) + currentValue.startDate ? + dayjs(currentValue.startDate).format(DATE_FORMAT) : '' } ~ ${ - currentValue?.endDate ? + currentValue.endDate ? dayjs(currentValue.endDate).format(DATE_FORMAT) : '' }` @@ -154,10 +157,10 @@ const DateRangePicker: React.FC = (props) => {

    {(options ?? items).map(({ label, startDate, endDate }, index) => (
  • @@ -193,7 +196,7 @@ const DateRangePicker: React.FC = (props) => { + )} + {showLeftButton && ( + + )} +
+
+
+ {urls.map((url) => ( +
window.open(url, '_blank')} + > +
+ + preview +
+ ))} +
+
+
+ ); +}; +export default ImageSlider; diff --git a/apps/web/src/shared/ui/index.ts b/apps/web/src/shared/ui/index.ts new file mode 100644 index 000000000..4cbab2082 --- /dev/null +++ b/apps/web/src/shared/ui/index.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default as Logo } from './logo.ui'; +export { default as LocaleSelectBox } from './locale-select-box.ui'; +export { default as LogoWithTitle } from './logo-with-title.ui'; + +export { default as SectionTemplate } from './section-template.ui'; +export { default as MainCard } from './main-card.ui'; + +export { default as SubMenu } from './sub-menu.ui'; +export { default as CreateTemplate } from './create-template.ui'; +export { default as CreateInputTemplate } from './create-input-template.ui'; +export { default as CreateSectionTemplate } from './create-section-template.ui.tsx'; +export { default as DashboardCard } from './dashboard-card'; + +export { default as ExpandableText } from './expandable-text.ui'; +export { default as ImagePreviewButton } from './image-preview-button'; +export { default as ShareButton } from './share-button'; +export { default as DateRangePicker } from './date-range-picker'; +export { default as DescriptionTooltip } from './description-tooltip'; +export { default as HelpCardDocs } from './help-card-docs'; +export { default as Popper } from './popper.ui'; +export { default as ImageSlider } from './image-slider.ui'; +export { default as RadioGroup } from './radio-group'; + +export { default as SmallCard } from './small-card.ui'; + +export * from './tables'; +export * from './charts'; +export * from './select-box'; diff --git a/apps/web/src/components/layouts/Header/LocaleSelectBox.tsx b/apps/web/src/shared/ui/locale-select-box.ui.tsx similarity index 92% rename from apps/web/src/components/layouts/Header/LocaleSelectBox.tsx rename to apps/web/src/shared/ui/locale-select-box.ui.tsx index 9887dec73..769b53a2b 100644 --- a/apps/web/src/components/layouts/Header/LocaleSelectBox.tsx +++ b/apps/web/src/shared/ui/locale-select-box.ui.tsx @@ -20,15 +20,17 @@ import { setCookie } from 'cookies-next'; import { Icon } from '@ufb/ui'; +import { cn } from '../utils'; + interface IProps extends React.PropsWithChildren {} const LocaleSelectBox: React.FC = () => { const router = useRouter(); const onToggleLanguageClick = useCallback( - (newLocale: string) => { + async (newLocale: string) => { const { pathname, asPath, query } = router; setCookie('NEXT_LOCALE', newLocale); - router.push({ pathname, query }, asPath, { locale: newLocale }); + await router.push({ pathname, query }, asPath, { locale: newLocale }); }, [router], ); @@ -56,10 +58,10 @@ const LocaleSelectBox: React.FC = () => { key={v} value={v} className={({ selected }) => - [ + cn([ 'hover:bg-secondary cursor-pointer select-none p-2 text-center font-extrabold uppercase', selected ? 'font-bold' : 'font-normal', - ].join(' ') + ]) } > {v} diff --git a/apps/web/src/shared/ui/logo-with-title.ui.tsx b/apps/web/src/shared/ui/logo-with-title.ui.tsx new file mode 100644 index 000000000..e216b2d0e --- /dev/null +++ b/apps/web/src/shared/ui/logo-with-title.ui.tsx @@ -0,0 +1,41 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import Image from 'next/image'; + +import { Icon } from '@ufb/ui'; + +interface IProps { + title: string; +} + +const LogoWithTitle: React.FC = ({ title }) => { + return ( +
+
+ logo + +
+

{title}

+
+ ); +}; + +export default LogoWithTitle; diff --git a/apps/web/src/components/layouts/Header/Logo.tsx b/apps/web/src/shared/ui/logo.ui.tsx similarity index 96% rename from apps/web/src/components/layouts/Header/Logo.tsx rename to apps/web/src/shared/ui/logo.ui.tsx index 9bc019ccb..5d43ad0bc 100644 --- a/apps/web/src/components/layouts/Header/Logo.tsx +++ b/apps/web/src/shared/ui/logo.ui.tsx @@ -19,7 +19,7 @@ import { useRouter } from 'next/router'; import { Icon } from '@ufb/ui'; -import { Path } from '@/constants/path'; +import { Path } from '@/shared'; interface IProps {} diff --git a/apps/web/src/shared/ui/main-card.ui.spec.tsx b/apps/web/src/shared/ui/main-card.ui.spec.tsx new file mode 100644 index 000000000..59565ae54 --- /dev/null +++ b/apps/web/src/shared/ui/main-card.ui.spec.tsx @@ -0,0 +1,45 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; + +import { IconNames } from '@ufb/ui'; + +import { render } from '@/test-utils'; +import MainCard from './main-card.ui'; + +describe('MainCard', () => { + test('snapshot', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/web/src/shared/ui/main-card.ui.tsx b/apps/web/src/shared/ui/main-card.ui.tsx new file mode 100644 index 000000000..0b41c3439 --- /dev/null +++ b/apps/web/src/shared/ui/main-card.ui.tsx @@ -0,0 +1,69 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { Icon } from '@ufb/ui'; +import type { IconNameType } from '@ufb/ui'; + +import { cn, displayString } from '../utils'; + +interface IProps { + onClick?: () => void; + icon: { iconName: IconNameType; bgColor: '#5D7BE7' | '#48DECC' }; + title: string; + description?: string | null; + leftContent: { title: string; count?: number }; + rightContent: { title: string; count?: number }; +} +const MainCard: React.FC = (props) => { + const { onClick, icon, title, description, leftContent, rightContent } = + props; + + return ( +
+
+
+ +
+
+

{title}

+

+ {displayString(description)} +

+
+
+
+
+

{leftContent.title}

+

{leftContent.count?.toLocaleString()}

+
+
+

{rightContent.title}

+

{rightContent.count?.toLocaleString()}

+
+
+
+ ); +}; + +export default MainCard; diff --git a/apps/web/src/components/etc/Popper/Popper.tsx b/apps/web/src/shared/ui/popper.ui.tsx similarity index 99% rename from apps/web/src/components/etc/Popper/Popper.tsx rename to apps/web/src/shared/ui/popper.ui.tsx index 939ec0de7..96c085648 100644 --- a/apps/web/src/components/etc/Popper/Popper.tsx +++ b/apps/web/src/shared/ui/popper.ui.tsx @@ -37,6 +37,7 @@ const Popper: React.FC = (props) => { placement, offset = 6, } = props; + const buttonRef = useRef(null); const containerRef = useRef(null); diff --git a/apps/web/src/containers/setting-menu/SignUpSetting/RadioGroup.tsx b/apps/web/src/shared/ui/radio-group.tsx similarity index 97% rename from apps/web/src/containers/setting-menu/SignUpSetting/RadioGroup.tsx rename to apps/web/src/shared/ui/radio-group.tsx index 9a4a5f542..703f479df 100644 --- a/apps/web/src/containers/setting-menu/SignUpSetting/RadioGroup.tsx +++ b/apps/web/src/shared/ui/radio-group.tsx @@ -24,7 +24,7 @@ interface IProps { const RadioGroup: React.FC = ({ name, radios }) => { return ( -
+
{radios.map((radio, index) => (