Compare commits

..

206 Commits

Author SHA1 Message Date
shimoda.m
3d9a254b63 2025/1/27 PH1エンハンス 本番リリース
メンテナンス告知文削除
2025-01-24 04:04:55 +00:00
shimoda.m
ff5533b647 Merged PR 1010: 2025/1/27 本番リリース 2025-01-21 05:23:54 +00:00
shimoda.m
b3845187f6 Merged PR 1009: Revert "Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース"
Revert "Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース"

Reverted commit `b5293888`.

デプロイミスによる切り戻し
2025-01-21 04:47:21 +00:00
shimoda.m
019c818a19 Merged PR 1008: Revert "2025/1/27 PH1エンハンス 本番リリース
Revert "2025/1/27 PH1エンハンス 本番リリース
メンテナンス文言削除"

Reverted commit `1137d826`.

デプロイミスによる切り戻し。
2025-01-21 04:46:02 +00:00
shimoda.m
1137d826ae 2025/1/27 PH1エンハンス 本番リリース
メンテナンス文言削除
2025-01-21 04:00:54 +00:00
shimoda.m
b529388871 Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース 2025-01-21 02:59:31 +00:00
shimoda.m
aef17893d9 Merged PR 1004: 2025/1/27 本番リリース メンテナンス文言追加
## 概要

2025/1/27の本番リリースに向けて、トップページにメンテナンス文言を追加

リテラルは以下で確認中。(日付を変えただけなので問題ない認識)
OMDS_IS-459 【リリース】エンハンスリリース(1/27)のスケジュールについて

## UIの変更内容
![image.png](https://dev.azure.com/ODMSCloud/6023ff7b-d41c-4fa7-9c6f-f576ba48c07c/_apis/git/repositories/302da463-a2d7-40f9-b2bb-6e8edf324fa9/pullRequests/1004/attachments/image.png)
2025-01-21 01:52:47 +00:00
nik.n
4f598b0017 Merged PR 988: pipelineエラー解消のためglobal npmアップデート削除
### 概要
Dockerfileから `RUN npm install -g npm` 削除。Node.js(v18.17.1)とnpm(11.0.0)のバージョン互換性の問題により、pipeline が失敗していたためです。
現在のNode.jsイメージに含まれている npmバージョン(9.6.7)でプロジェクトの要件は満たされているため、グローバルnpmのアップグレードは不要と判断した

### 参考リンク
失敗した Pipeline ビルド
- [#1931 • Merged PR 986: 保守対応の内容反映](https://dev.azure.com/ODMSCloud/ODMS%20Cloud/_build/results?buildId=1931&view=results)
2025-01-08 04:36:06 +00:00
SAITO-PC-3\saito.k
b71ec627d7 特別な文字列をエスケープしてからreplaceAllするように修正 2024-12-11 14:07:39 +09:00
SAITO-PC-3\saito.k
af56f8ccad Supportページにリンク追加。
画面デザイン修正
2024-12-04 10:07:12 +09:00
SAITO-PC-3\saito.k
0b451ed62f NotificationHub登録方法修正 2024-11-27 11:16:46 +09:00
saito.k
11395279af Merged PR 954: NotificationHubへのデバイス登録方法を修正
## 概要
[Task4580: NotificationHubへのデバイス登録方法を修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4580)

- NotificationHubのデバイス登録時に付与するInstallationIDをuuidからuserIdに変更

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 修正範囲がNotificationHubのserviceに閉じているため、既存のテストが通ることを確認したのみ

## 補足
- 相談、参考資料などがあれば
2024-11-26 08:45:27 +00:00
x.itou.t
a07cfe51aa Merged PR 948: PH1エンハンス先行リリース対応
## 概要
[ユーザー ストーリー 4489: 【PH1エンハンス】Dictation Finishedになったファイルのステータスを変更したい](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation-2nd/_workitems/edit/4489)
[ユーザー ストーリー 4491: 【PH1エンハンス】通知にユーザーIDを付加する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation-2nd/_workitems/edit/4491)
2024-11-11 05:23:38 +00:00
saito.k
ad397f6fe7 Merged PR 933: Functions修正
## 概要
[Task4557: Functions修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4557)

- 国情報を追加
- テスト修正

## レビューポイント
- 特になし

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
2024-10-16 07:40:27 +00:00
saito.k
85fdec2e5a Merged PR 922: Functions
## 概要
[Task4485: Functions](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4485)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-09-25 01:06:33 +00:00
saito.k
f1b75a7ff0 Merged PR 921: API修正
## 概要
[Task4478: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4478)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-09-18 01:35:28 +00:00
saito.k
6690302ac3 Merged PR 920: API修正
## 概要
[Task4336: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4336)

- 文字起こし完了時のメールを文字起こし担当のTypistに送信しないようにする

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
- ほかのテストケースがすべて通ることを確認
- メール送信処理を確認するテストケースを追加

## 補足
- 相談、参考資料などがあれば
2024-08-09 07:47:33 +00:00
saito.k
1320222d79 Update azure-pipelines-staging.yml for Azure Pipelines 2024-08-09 06:13:40 +00:00
saito.k
baea8ce5e5 Update azure-pipelines-staging.yml for Azure Pipelines 2024-08-09 04:57:41 +00:00
saito.k
d69126a980 Update azure-pipelines-staging.yml for Azure Pipelines 2024-08-09 04:55:36 +00:00
SAITO-PC-3\saito.k
79a2b9f0a3 yml修正 2024-08-09 13:42:37 +09:00
SAITO-PC-3\saito.k
2a755f2bd3 使用するVM修正 2024-08-09 12:27:25 +09:00
SAITO-PC-3\saito.k
edb8a79f94 docker-composeをダウンロードするymlに修正 2024-08-09 12:24:00 +09:00
SAITO-PC-3\saito.k
4a5136deee yml修正3 2024-08-09 11:47:36 +09:00
SAITO-PC-3\saito.k
f2eaba7e5f yml修正2 2024-08-09 11:39:51 +09:00
SAITO-PC-3\saito.k
3b576c6a47 パイプラインのYamlを修正 2024-08-09 11:36:06 +09:00
saito.k
557fc48d05 Merged PR 919: 画面修正
## 概要
[Task4331: 画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4331)

- サポートページにアプリダウンロードリンクを追加

## レビューポイント
- 特になし

## 動作確認状況
- ローカルで確認
- サポートページのみで他画面に影響なし

## 補足
- 相談、参考資料などがあれば
2024-08-09 01:23:21 +00:00
saito.k
353d5ad462 Merged PR 918: analysisLicenses修正
## 概要
[Task4313: analysisLicenses修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4313)

- ライセンス推移CSVの出力内容を修正
  - 移行ライセンスの利用状況も出力する
  - 出力する移行ライセンスの条件は以下
     - 第五階層
     - 割り当て済
     - 有効期限が当日以降
     - 年間ライセンス
- テスト修正
  - 移行ライセンスの取得・変換ロジックのテスト修正

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-07-23 03:35:55 +00:00
saito.k
df55be5e19 Merged PR 917: 画面遷移してもインターバルが初期化されないように修正
## 概要
[Task4248: 画面遷移してもインターバルが初期化されないように修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4248)

- UpdateTokenTimerを修正
  - F5キーリロードの時もトークンをチェックする
- UpdateTokenTimerを各画面ごとではなくApp.tsxに配置する
  - 画面遷移のたびにSetIntervalが初期化されるのを防ぐ
- 各画面のUpdateTokenTimerを削除
  - App.tsxで管理するため

## レビューポイント
- いかの問題が解消されるかを可能であれば確認してほしい。
  - Dictation画面でトークンのチェック~自動更新ができること
  - 画面遷移をしてもトークンチェックのインターバルが初期化されないこと
  - F5キーリロードをした時にトークンチェックが走ること

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 各画面で取得系の処理が正しく動作することを確認

## 補足
- 相談、参考資料などがあれば
2024-07-02 06:43:41 +00:00
SAITO-PC-3\saito.k
35425c576a ローカル環境のDBスキーマの参照先を変更 2024-06-14 16:14:11 +09:00
SAITO-PC-3\saito.k
d4abc0fcfb スキーマを変更 2024-06-14 11:57:28 +09:00
SAITO-PC-3\saito.k
11c0937214 メンテナンス告知を削除 2024-06-14 11:54:27 +09:00
SAITO-PC-3\saito.k
12fe7ac9bb 本番環境で参照するスキーマはomdsのため参照先を修正 2024-06-13 17:06:53 +09:00
SAITO-PC-3\saito.k
b997b928b8 Merge branch 'release-ccb' into hotfix-task4228 2024-06-13 16:45:30 +09:00
SAITO-PC-3\saito.k
10f57c6aeb Merge branch 'develop' into release-ccb 2024-06-12 14:50:18 +09:00
SAITO-PC-3\saito.k
254cbdf0d6 リテラル反映漏れ修正 2024-06-12 14:04:16 +09:00
SAITO-PC-3\saito.k
6b1286ba13 Merge branch 'develop' into release-ccb 2024-06-11 17:00:35 +09:00
saito.k
aea66f4616 Merged PR 914: ユーザー削除API修正
## 概要
[Task4223: ユーザー削除API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4223)

- Authorの削除条件の修正
  - Authorが作成したタスクがある場合は削除できない
- Typistの削除条件の修正
  - 文字起こし担当のタスクがある場合は削除できないように修正
    - 挙動は変わらない(記述を修正した)
- リテラル反映
- テストケース追加

## レビューポイント
- テストケースは足りているか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- 64行目のクエリ(Author/Typistともに)
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/Task4223?csf=1&web=1&e=UsjxWP

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - Authorが作成したタスクがある場合にそのAuthorは削除できないことを確認(タスクのステータスはFinished・Backup)
    - AuthorのAuthorIDが設定されているタスクがある場合はそのAuthorは削除できないことを確認(すべてのステータス)
    - Typistが文字起こし担当のタスクがある場合そのTypistは削除できないことを確認(タスクのステータスはFinished・Backup)

## 補足
- 相談、参考資料などがあれば
2024-06-11 05:05:53 +00:00
SAITO-PC-3\saito.k
c73340ec51 Merge branch 'develop' into release-ccb 2024-06-11 11:20:34 +09:00
saito.k
07904c35e6 Merged PR 885: メンテナンス通知画面実装
## 概要
[Task4148: メンテナンス通知画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4148)

- TOPページにメンテナンス通知を追加

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Hotfix4148?csf=1&web=1&e=aGsZP6

## 動作確認状況
- ローカルで確認
- TOPページに文言を追加したのみ
  - 画面サイズを小さくしてもデザインが崩れないことを確認

## 補足
- メンテナンスモードに入る日時については未定のため、OMDS定例で決まり次第修正し、決められた日時にmainにマージする
2024-06-11 01:24:52 +00:00
saito.k
474cdd56c6 Merged PR 913: ライセンス取得APIの修正
## 概要
[Task4220: ライセンス取得APIの修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4220)

- 割り当て可能ライセンス取得APIを修正
  - 有効期限が当日のライセンスは割り当て可能としないように修正
    - 有効期限が翌日以降のライセンスを取得
- テスト修正
- ライセンス割り当ての処理にコメント追加
  - 結果として有効期限が当日のライセンスは割り当てられないので問題はないが、記述方法が紛らわしいのでコメントを追加した。

## レビューポイント
- テストケースは足りているか
- 追加したコメントの意図は伝わるか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- クエリ部分を修正したが、基準となる日付を変更したのみなので、クエリ自体に変更はなし
  - Before/Afterで確認したが、変更はなかった

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - テストを追加して、デグレがないことを検証した

## 補足
- 相談、参考資料などがあれば
2024-06-10 02:31:35 +00:00
saito.k
83732fa1b5 Merged PR 911: 画面修正(ファイル名変更ボタンのデザイン)
## 概要
[Task4213: 画面修正(ファイル名変更ボタンのデザイン)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4213)

- ファイル名変更ボタンのデザイン修正
  - 言語を英語以外にすると、ファイル名の入力欄と重なってしまうのを修正
    - ボタンの位置を入力欄の下に修正
- リテラル修正も含めて対応
  - https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%2036-1?workitem=4214

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task4213?csf=1&web=1&e=ftBwWm

## 動作確認状況
- ローカルで確認
- デザインの修正のみのためデグレは発生しない

## 補足
- 修正したデザインに対してはOMDS様に確認済み
2024-06-06 07:36:17 +00:00
saito.k
6b1650a634 Merged PR 910: メール文面修正&ユーザー一括登録のテンプレートファイル修正
## 概要
[Task4190: メール文面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4190)
[Task4191: ユーザー一括登録のテンプレートファイル修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4191)

- ユーザー一括追加時に送信するメールの内容を修正
  - SCVとなっていたところをCSVに修正
- ユーザー一括追加用のエクセルの項目名を画面上の項目名と合わせる
  - auto_renewとなっていたところをauto_assignに修正

## レビューポイント
- 修正内容の認識は合っているか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - CSV変換のテストが通ることを確認
    - メール内容はdev動作確認で確認

## 補足
- 相談、参考資料などがあれば
2024-06-04 06:54:42 +00:00
makabe.t
4f7d65f0e8 Merged PR 905: POST /accounts/worktypes/{id}
## 概要
[Task3986: POST /accounts/worktypes/{id}](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3986)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-06-04 04:25:54 +00:00
SAITO-PC-3\saito.k
1f10a3f096 Merge branch 'develop' into release-ccb 2024-06-04 13:07:39 +09:00
saito.k
909c2a6d55 Merged PR 909: 画面修正(CSVファイルの変換処理)
## 概要
[Task4202: 画面修正(CSVファイルの変換処理)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4202)

- CSVデータの変換関数を修正
  - author_id,encryption_passwordが数値のみの場合でも、文字列として扱うようにする

## レビューポイント
- ほかに確認したほうが良いテストケースはあるか

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 既存のテストはすべて通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-06-03 06:36:05 +00:00
saito.k
37666a00c7 Merged PR 908: API修正(タスク削除条件のステータスチェックを修正)
## 概要
[Task4201: API修正(タスク削除条件のステータスチェックを修正)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4201)

- タスク削除不可条件を修正
  - Pendingの時も削除できないようにする

## レビューポイント
- 修正の認識があっているか

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - タスクの各ステータスで削除可否を確かめられるように修正

## 補足
- 相談、参考資料などがあれば
2024-06-03 05:39:53 +00:00
saito.k
b7925f311b Merged PR 907: 画面修正(タスク削除ボタンの活性条件を修正)
## 概要
[Task4200: 画面修正(タスク削除ボタンの活性条件を修正)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4200)

- タスク削除ボタンの活性条件を修正
  - Pendingの時は削除できないようにする

## レビューポイント
- 特になし

## UIの変更
- https://ndstokyo.sharepoint.com/:i:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task4200/Pending%E5%89%8A%E9%99%A4%E4%B8%8D%E5%8F%AF.png?csf=1&web=1&e=ATzEZ8

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - Inprogressのステータスで削除できないこと
  - Uploaded・Finished・backupで削除できる

## 補足
- 相談、参考資料などがあれば
2024-06-03 05:07:08 +00:00
SAITO-PC-3\saito.k
97620881d3 Merge branch 'develop' into release-ccb 2024-05-31 14:05:54 +09:00
saito.k
0e68f26c57 Merged PR 906: ユーザー認証API修正
## 概要
[Task4182: ユーザー認証API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4182)

- 認証済みチェックをパスワード変更より先に行うように修正
- パスワード変更に失敗したら、認証済みフラグをfalseにするリカバリ処理追加
  - リカバリに失敗したら手動復旧ログを出力
- メール送信に失敗したらエラーを返すように修正
  - メール送信に失敗したらリカバリ処理を行うように修正
    - リカバリに失敗したら手動復旧ログを出力
- テスト修正
  - リカバリ処理を考慮したケースを追加

## レビューポイント
- リカバリ処理の記述
- メール送信でエラーが起きたときにエラーを握りつぶさないようにしたが問題ないか
  - メール送信で失敗したときにエラーを握りつぶすと、ユーザーは届かないメールを待つしかなくなる
    - 失敗を伝えて、リカバリをしてあげると再実行してもらうことができる。

## クエリの変更
- クエリの変更はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存のテストケースをDBを使うテストに置き換え
    - 結果は変えずに通ることを確認
  - テストケースを追加し、新たな観点でテストを作成

## 補足
- 相談、参考資料などがあれば
2024-05-30 00:18:59 +00:00
makabe.t
9736d23653 Merged PR 904: POST /accounts/worktypes
## 概要
[Task3985: POST /accounts/worktypes](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3985)

- `POST /accounts/worktypes`のリクエストバリデータにテストを追加しました。

## レビューポイント
- テストケースに不足はないか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストの追加のみなので影響なし
2024-05-28 05:58:17 +00:00
makabe.t
261a5afbdd Merged PR 903: POST /notification/register
## 概要
[Task3983: POST /notification/register](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3983)

- `POST /notification/register`APIのバリデータに対するテストを追加しました。

## レビューポイント
- テストケースに不足はないか。

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストの追加のみなのでほかに影響なし。
2024-05-28 05:38:33 +00:00
SAITO-PC-3\saito.k
3a7c1765ee Merge branch 'main' into develop
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
2024-05-24 17:26:50 +09:00
SAITO-PC-3\saito.k
c16a8b023c Merge branch 'develop' into release-ccb 2024-05-24 16:17:03 +09:00
saito.k
4b16e6a004 Merged PR 900: 画面修正(フッターに規約リンク追加)
## 概要
[Task4084: 画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4084)

- フッターに規約リンクを追加
  - デザインはOMDS様に確認済み
- サポートページにフッターを追加

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Hotfix4084?csf=1&web=1&e=bQBkQI

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-05-24 05:23:26 +00:00
saito.k
bd8f035c46 Merged PR 902: POST /files/audio/upload-finished
## 概要
[Task3982: POST /files/audio/upload-finished](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3982)

- /files/audio/upload-finished のバリデーションテスト作成

## レビューポイント
- テストケースで抜けているところはないか
  - IsNotEmpty()は一つに省略

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - テスト追加のため既存機能に影響なし

## 補足
- 相談、参考資料などがあれば
2024-05-23 07:26:04 +00:00
makabe.t
bdcce37b5a Merged PR 898: 対応
## 概要
[Task4174: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4174)

- アカウント情報画面からディーラーを変更する際に、同名ディーラーがいてもそれぞれ別個に指定できるようにIDで検索するように修正しました。

## レビューポイント
- 対応方針は認識通りでしょうか?

## UIの変更
- なし

## クエリの変更
- クライアントのみ変更なのでなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ローカルで同名ディーラーでそれぞれを指定して変更できることを確認
2024-05-23 01:12:31 +00:00
SAITO-PC-3\saito.k
7af5c50a27 Merge branch 'develop' into release-ccb 2024-05-20 13:57:26 +09:00
saito.k
5860da285e Merged PR 899: 階層構造変更API修正
## 概要
[Task4179: 階層構造変更API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4179)

- 階層構造変更API修正
  - 第四⇔第五の切り替え処理の時は国・リージョンの一致を確認しない
  - 不要なエラーコード・エラーハンドリングを削除
- テスト修正

## レビューポイント
- 修正箇所の認識は合っているか
- テストケースは足りているか

## 動作確認状況
- ローカルで確認
  - 国の違うDealerに付け替える処理を行い成功した
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 国の違う第四⇔第五間の付け替えのテストが成功することを確認
    - ほかのテストが成功することを確認

## 補足
- 相談、参考資料などがあれば
2024-05-17 06:49:26 +00:00
SAITO-PC-3\saito.k
3ece576476 Merge branch 'develop' into release-ccb 2024-05-16 14:06:54 +09:00
SAITO-PC-3\saito.k
4dac420bed typeORMのモジュール設定が漏れていたので修正 2024-05-15 17:00:20 +09:00
makabe.t
ddf338bc72 Merged PR 890: 翻訳情報反映
## 概要
[Task4058: 翻訳情報反映](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4058)

- 6月末リリース分の翻訳をクライアントに適用しました。
  - いただいた翻訳内容は開発の管理ファイルに反映しています。(メンテナンスについては未反映です。)
[ラベル・メッセージ管理](https://sonyjpn.sharepoint.com//r/sites/S127-OMDS-EXTERNAL-NDS/_layouts/15/doc2.aspx?sourcedoc=%7BCBFBB1F9-3AB4-4E2E-A21D-572684D20B29%7D&file=%E3%83%A9%E3%83%99%E3%83%AB%E3%83%BB%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E7%AE%A1%E7%90%86_dictation.xlsx&action=default&mobileredirect=true)

## レビューポイント
- 翻訳の適用は適切か?

## UIの変更
- 翻訳の適用

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - リテラルの内容のみなので影響なし
2024-05-15 07:15:35 +00:00
saito.k
0cca61517c Merged PR 896: バージョンアップ用SQLを作成
## 概要
[Task4044: バージョンアップ用SQLを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4044)

- jobNumberの初期値を設定するマイグレーションファイル作成
  - タスクテーブルにレコードがある(=タスクを作成したことがある)アカウントに対しては最新のJobNumberで初期値をセットする
  - タスクテーブルにレコードがない(=タスク作成をしたことがない)アカウントに対しては`00000000`をセットする

## レビューポイント
- セットする初期値は認識あっているか
- migrate downの処理は問題ないか

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - マイグレーションファイルの作成のみなのでほかに影響はない想定

## 補足
- 相談、参考資料などがあれば
2024-05-15 06:15:22 +00:00
saito.k
fe5e8b8e1c Merged PR 895: API修正(アカウント作成系)
## 概要
[Task4043: API修正(アカウント作成系)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4043)

- アカウント作成時にJobNumberの初期値を設定するように修正
- パートナーアカウント作成時にJobNumberの初期値を設定するように修正
- リカバリ処理にJobNumberのレコード削除を追加
- テスト修正

## レビューポイント
- JobNumber作成処理の追加する箇所に問題はないか
- テストケースに不足はないか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- 既存のクエリに修正はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 既存テストが通ることを確認
    - パートナーアカウント作成のテストにメール送信内容のチェックを追加
    - ソート条件が作成・削除されていることを確認するテストを追加

## 補足
- 相談、参考資料などがあれば
2024-05-14 07:18:57 +00:00
saito.k
dfdc6a33ad Merged PR 894: API修正(アカウント削除系)
## 概要
[Task4034: API修正(アカウント削除系)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4034)

- アカウント削除時にJobNumberテーブルのレコードも削除するように修正
- パートナー削除時にJobNumberテーブルのレコードも削除するように修正
- テスト修正

## レビューポイント
- テストケースに不足はないか
- ジョブナンバーテーブルの削除順に問題はないか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- JobNumberテーブルのレコードを削除する処理を追加した
  - 既存のクエリに影響はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存のテストが通ることを確認
  - テストしていなかった観点(ソート条件も削除されているか等)も確認するように修正

## 補足
- 相談、参考資料などがあれば
2024-05-14 02:12:41 +00:00
saito.k
35e2d626a0 Merged PR 893: API修正(upload-finished)
## 概要
[Task4033: API修正(upload-finished)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4033)

- JobNumberテーブルから取得したJOBNUMBERを使用してタスクを作成する。
- テスト追加

## レビューポイント
- テストケースは足りているか
- JOBNUMBERの採番ロジックに誤りはないか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
-  JobNumberテーブルからの取得と更新クエリを追加した
  - 既存のクエリを変更はしていない

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストケースを修正し、既存テストがすべて通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-05-13 05:04:16 +00:00
saito.k
228e21ba78 Merged PR 892: migration修正
## 概要
[Task4035: migration修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4035)

- job_numberテーブル作成
- マイグレーションのコマンド修正

## レビューポイント
- インデックス・ユニーク制約・外部キー制約の認識は合っているか
- マイグレーションのコマンドは基本的にccbで認識あっているか

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - マイグレーションファイル作成のみでほかに影響なし

## 補足
- 相談、参考資料などがあれば
2024-05-10 03:56:48 +00:00
makabe.t
68df7cd728 Merged PR 887: POST /auth/token
## 概要
[Task3981: POST /auth/token](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3981)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- `POST /auth/token` のバリデータのUTを追加しました。

## レビューポイント
- テスト項目は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストの修正のみなので影響なし
2024-05-09 05:28:03 +00:00
makabe.t
ffd6eb4e68 Merged PR 886: GET /tasks
## 概要
[Task3980: GET /tasks](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3980)

- `GET /tasks` のバリデータのUTを追加しました。

## レビューポイント
- テスト項目は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストの修正のみなので影響なし
2024-05-09 01:20:57 +00:00
SAITO-PC-3\saito.k
e205209b12 Merge branch 'develop' into release-ccb 2024-05-08 16:15:14 +09:00
SAITO-PC-3\saito.k
3ea84a3597 Merge branch 'main' into develop 2024-05-08 15:55:30 +09:00
saito.k
b71c4398d2 Merged PR 891: 不具合対応
## 概要
[Task4163: 不具合対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4163)

- ビルド成果物にメール用のテンプレートも含めるように修正

## レビューポイント
- 特になし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - コードの修正はなし

## 補足
- 相談、参考資料などがあれば
2024-05-08 05:41:20 +00:00
makabe
c26ad130ed Merge branch 'develop' into release-ccb 2024-05-07 16:49:42 +09:00
makabe.t
279a9ab037 Merged PR 889: function修正
## 概要
[Task4164: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4164)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-05-07 07:11:39 +00:00
SAITO-PC-3\saito.k
4e00c03ef2 Merge branch 'main' into develop 2024-05-07 15:42:39 +09:00
saito.k
c813ddc0ac Merged PR 888: 対応
## 概要
[Task4160: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4160)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-05-07 05:37:16 +00:00
makabe
23f8b54011 Merge branch 'main' into develop 2024-05-07 12:04:03 +09:00
makabe.t
8122f6f4e1 Merged PR 884: FunctionにX-Requested-Withヘッダを適用
## 概要
[Task4142: FunctionにX-Requested-Withヘッダを適用](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4142)

- Functionのユーザー一括登録の処理中でOMDS CloudのAPIを呼び出す処理があるので、X-Requested-Withヘッダを適用しました。
- 一括登録失敗時のメール文面の翻訳でエラーがない場合のメッセージが日本語のままになっていたので各言語に対応しました。

## レビューポイント
- ヘッダの適用は適切でしょうか?
- 翻訳の適用方法で、言語ごとに割り当てる内容を定数としていますが、文面の置き換え方法に問題はないでしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ローカルからAPIを叩い検証証
2024-05-07 00:05:17 +00:00
SAITO-PC-3\saito.k
e76242bddd Functionsのテストで使用する環境変数を修正 2024-04-25 18:19:48 +09:00
saito.k
af0ba78ae9 Merged PR 883: Functions修正
## 概要
[Task4132: Functions修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4132)

- 自動割り当てを行うライセンスの取得条件を変更
  - 有効期限が近いライセンスまたは有効期限が設定されていないライセンス(新規ライセンス)を取得する
  - 有効期限が近いものから割り当てを行うので、ソートはサーバー側で行うようにした
- メール送信処理を追加

## レビューポイント
- テンプレート取得からメール送信までの実装で漏れはないか
- テストケースは足りているか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- 2行目で割り当てるライセンスを取得しているが、その条件を修正した
- https://ndstokyo.sharepoint.com/:u:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/task4132/after.log?csf=1&web=1&e=jh49c3

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 追加したメール送信処理を確認するように各テストを修正し、テストが通っている
  - テストの観点を拡充したうえでテストが通っていることを確認
    - ライセンス割り当て履歴の内容をより詳細に確認するようにした

## 補足
- 相談、参考資料などがあれば
2024-04-25 08:56:03 +00:00
makabe
1904b95adf Merge branch 'develop' into release-ccb 2024-04-24 11:35:58 +09:00
makabe.t
d43cece48a Merged PR 882: 追加開発分をステージングに反映するPipelineを作成
## 概要
[Task4016: 追加開発分をステージングに反映するPipelineを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4016)

- 追加開発分CCBをステージングに反映するPipeline定義のYamlファイルを追加しました。

## レビューポイント
- 参照元ブランチを変えた以外はSTGデプロイそのままにしていますが、対応の不足はないでしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- マージ後に確認
2024-04-22 08:01:37 +00:00
SAITO-PC-3\saito.k
ba7196cac1 本番デプロイようにコメントアウトした実装をもとに戻す 2024-04-22 10:44:56 +09:00
SAITO-PC-3\saito.k
7eecb001c6 4/22の本番環境デプロイのために確認未実施の修正をコメントアウト 2024-04-21 22:41:44 +09:00
SAITO-PC-3\saito.k
0b01da936d CacheModule作成時にTTLを設定 2024-04-19 19:15:23 +09:00
saito.k
b88c0d9b96 Merged PR 877: ディーラー取得APIの修正
## 概要
[Task4104: ディーラー取得APIの修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4104)

- メールの文面を各言語版に置き換えました。
  - 環境変数に設定されたアカウントIDのDealerはResponseに含めないように修正

## レビューポイント
- 環境変数からインスタンス変数に代入するときの処理に問題はあるか
- 環境変数のフォーマットはこれで良いか
  - もっとよいやり方があれば指摘いただきたいです
- テストケースに不足はないか

## UIの変更
- なし

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - ほかのテストに影響が出ていない
2024-04-19 04:47:32 +00:00
makabe
b7554e30ff CCBからDBの向き先を変更 2024-04-19 13:38:24 +09:00
makabe
b03cda3ccc Merge branch 'ccb' into develop 2024-04-19 11:17:17 +09:00
makabe.t
7bfd424a64 Merged PR 875: メールの多言語対応
## 概要
[Task3859: メールの多言語対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3859)

- メールの文面を各言語版に置き換えました。
  - アカウント情報消去完了通知 [U-111] 
  - ユーザー一括登録 受付通知 [U-120] 
  - ユーザー一括登録 完了通知 [U-121] 
  - ユーザー一括登録 失敗通知 [U-122] 
  - パートナーアカウント情報消去完了通知 [U-123] 

## レビューポイント
- 対応メールは適切でしょうか?
- メール文面は適切でしょうか?
- 反映内容は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ローカル確認、マージ後にdevelop動作確認
2024-04-19 02:14:05 +00:00
makabe
9ee29e91ba マイグレーションコマンド修正 2024-04-19 09:10:48 +09:00
SAITO-PC-3\saito.k
b24059b538 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
#	dictation_function/src/test/common/utility.ts
#	dictation_server/src/features/files/test/utility.ts
2024-04-18 09:57:37 +09:00
SAITO-PC-3\saito.k
a7b18d8151 Merge branch 'develop' 2024-04-17 14:02:05 +09:00
makabe.t
23862ad3ac Merged PR 874: 不具合修正
## 概要
[Task4137: 不具合修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4137)

- ファイル名変更時にダイアログを追加

## レビューポイント
- 共有
## UIの変更
- ダイアログを追加

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ローカル確認
2024-04-17 01:49:34 +00:00
saito.k
566da623bf Merged PR 873: dev動作確認不具合修正
## 概要
[Task4131: dev動作確認不具合修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4131)

- メール送信対象のアカウント取得条件を修正
  - 第五階層はDPAの規約に同意することがないため、accepted_dpa_versionは常にNULLとなる
    - 取得条件にaccepted_dpa_versionがNOTNULLを追加するとカラムに値が入る契機がないのでアカウントを取得できなくなってしまっていた。

## レビューポイント
- 特になし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - ほかのテストケースで使用しているユーザーデータをaccepted_dpa_versionはNULLの状態で作成するようにし、テストがすべて通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-04-17 01:01:31 +00:00
makabe.t
69241ed36c Merged PR 871: Function(音声ファイル自動削除)修正
## 概要
[Task4060: Function(音声ファイル自動削除)修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4060)

- 自動ファイル削除で生ファイル名を利用するように修正しUTを追従しました。

## レビューポイント
- 生ファイル名を使う方法は適切でしょうか?
- テストの修正は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- select対象をraw_file_nameに修正

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストが通ることを確認
2024-04-17 00:23:08 +00:00
makabe.t
c469f943f1 Merged PR 872: 生ファイル名の初期値を追加するマイグレーションファイル作成
## 概要
[Task4125: 生ファイル名の初期値を追加するマイグレーションファイル作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4125)

- 生ファイル名と表示ファイル名をそれぞれ修正して初期値を入れるスクリプトを追加しました。
  - 生ファイル名:これまでfile_nameカラムに入っていた値
  - file_nameの値から.zipを除いたもの

## レビューポイント
- この値の操作で問題はないか

## UIの変更
- なし
## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - migrate up/downで想定通りになることを確認
2024-04-17 00:18:48 +00:00
makabe.t
0a714f8484 Merged PR 870: ファイル情報ポップアップ画面修正
## 概要
[Task4051: ファイル情報ポップアップ画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4051)

- ファイル情報ポップアップを修正し、ファイル名を変更できるように修正しました。
- 音声ファイル名変更APIのファイル名の文字数を修正しました。
  - フロントの入力欄で64文字にしたので、プラス拡張子で68文字としました。

## レビューポイント
- 画面イメージは認識通りでしょうか?
- 表示では拡張子を取って、APIに渡す際にはつけているのですが処理として不自然ではないでしょうか?

## UIの変更
- [Task4051](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task4051?csf=1&web=1&e=nbYIE0)

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - タスク一覧、ファイル情報ポップアップが正常に見えることを確認
2024-04-16 10:12:44 +00:00
makabe.t
1d2089b0c4 Merged PR 868: 音声ファイル名変更のに伴うAPI修正
## 概要
[Task4053: 音声ファイル名変更のに伴うAPI修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4053)

- 音声ファイル名について生ファイル名を利用するように修正しました。
  - ファイルアップロード完了(タスク登録)
    - パラメータのファイル名で生ファイル名も登録
  - 音声ファイルダウンロード先取得
  - タスク削除
    - blobストレージのファイル名に生ファイル名を利用

## レビューポイント
- 対象APIは認識通りか

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - UTが通ることを確認
    - 各APIで生ファイル名を使って実行できることを確認
2024-04-16 05:24:18 +00:00
saito.k
f975ecf551 Merged PR 869: Functions修正
## 概要
[Task4085: Functions修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4085)

- メール送信対象のアカウント取得条件を修正
  - プライマリー管理者の規約同意用のカラムがNULLであった場合は、そのアカウントにはメール送信しない。
- Entityの`@Column`で設定する型が実際のパラメータの型と異なっていたため修正
  - 文字列のところをDateTimeとしていた

## レビューポイント
- 修正内容の認識あっているか
- テストケースは足りているか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/4085?csf=1&web=1&e=WRec5O
  - 35行目に変更あり(規約系のカラムがNULLではないという条件を追加)
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存のテスト結果に影響なし

## 補足
- 相談、参考資料などがあれば
2024-04-16 05:08:04 +00:00
makabe.t
e6d27d7810 Merged PR 867: 音声ファイル名変更API実装
## 概要
[Task4052: 音声ファイル名変更API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4052)

- ファイル名変更APIとそのUTを実装しました。

## レビューポイント
- リポジトリ実装のチェック内容とその順序は適切でしょうか?
- テスト項目は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - UT実行
    - ローカル実行

## 補足
- 相談、参考資料などがあれば
2024-04-15 06:52:07 +00:00
makabe.t
f209c7359e Merged PR 865: IF実装・修正
## 概要
[Task4049: IF実装・修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4049)

- 音声ファイル名変更APIのIFを実装してリクエストパラメータのテストを実装しました。
- タスク一覧取得APIのレスポンスに生ファイル名を追加しました。
- OpenAPIの更新

## レビューポイント
- ファイル名変更APIのパスは適切でしょうか?
- バリデータのチェックは適切でしょうか?

## UIの変更
- なし

## クエリの変更
- IFなのでなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 既存テストを実施して、タスク一覧についてはレスポンス期待値を修正。
    - タスク一覧画面が正常に見えることを確認
2024-04-12 01:36:49 +00:00
makabe.t
33d4ab3d2f Merged PR 863: (Sprint31対応)一部のentityでMySQL上の型がbigintのものに対してbigintTransformerで変換する処理が入っていない
## 概要
[Task3928: (Sprint31対応)一部のentityでMySQL上の型がbigintのものに対してbigintTransformerで変換する処理が入っていない](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3928)

- 現在、bigint型で変換を適用していない部分へのTransformerの適用はうまくいかないため保留。
  - 既存処理については問題ありません
- Functionのテストで日付に依存している箇所があったので修正しています。

## レビューポイント
- 共有

## UIの変更
- なし
## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - テストの修正のみ
2024-04-11 04:29:16 +00:00
makabe.t
07bca1d638 Merged PR 866: DBマイグレーションエラー修正
## 概要
[Task4120: DBマイグレーションエラー修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4120)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-04-10 09:48:40 +00:00
makabe.t
09c21eafa7 Merged PR 864: DBにカラム追加
## 概要
[Task4057: DBにカラム追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4057)

- DBマイグレーションファイルとして音声ファイルテーブルに生ファイル名カラムを追加しました。

## レビューポイント
- カラムの要件は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし
## 動作確認状況
- ローカルでmigrate:up/downできることを確認
2024-04-10 06:15:54 +00:00
makabe.t
e6d6e477d9 Merged PR 862: パートナー一覧画面&パートナー編集ポップアップ実装
## 概要
[Task3935: パートナー一覧画面&パートナー編集ポップアップ実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3935)

- パートナー一覧画面からパートナー編集ポップアップを表示して情報を変更できる画面実装をしています。

## レビューポイント
- エラーの表示は適切でしょうか?
- 画面イメージは認識通りでしょうか?

## UIの変更
- [Task3935](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3935?csf=1&web=1&e=FdaUMT)

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 新規機能なので問題なし
2024-04-08 07:41:05 +00:00
maruyama.t
915483c109 Merged PR 860: パートナー情報更新API実装
## 概要
[Task3937: パートナー情報更新API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3937)

- パートナーアカウント情報更新APIとUTを実装しました。

## レビューポイント
- エラーケースの出し分けは適切でしょうか?
- テストケースは過不足ないでしょうか?

## UIの変更
- なし

## クエリの変更
- 新規追加のため変更はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 新規追加なので問題なし。
2024-04-05 02:37:58 +00:00
SAITO-PC-3\saito.k
c600d9f818 Merge branch 'develop' 2024-04-03 18:51:31 +09:00
saito.k
5147f853ae Merged PR 861: API修正
## 概要
[Task4038: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4038)

- タスク作成時のジョブナンバーの採番ルールを修正
  - 常に最新タスクのジョブナンバー+1を採番する
- テストケース追加
- 非同期処理の呼び出しでawaitが抜けている箇所を修正

## レビューポイント
- 修正内容の認識は合っているか
- テストケースに不足はないか
- 修正箇所がほかの機能に影響していないか
  - taskRepositoryのcreateはupload-finished以外では使用されていない

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/Task4038?csf=1&web=1&e=WTIk2l
  - L107辺りが変更箇所

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 修正したcreateがほかで使用していないことを確認
    - テストケースを追加し確認
    - 既存のテスト含め成功することを確認

## 補足
- 相談、参考資料などがあれば
2024-04-03 05:35:30 +00:00
maruyama.t
0288292058 Merged PR 859: パートナーユーザー取得API実装
## 概要
[Task3936: パートナーユーザー取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3936)
- このPull Requestでの対象/対象外
パートナー変更APIの修正は別タスクで対応
- 影響範囲(他の機能にも影響があるか)
新規APIのため他の機能に影響はない

## レビューポイント
- パートナーのアカウントIDからユーザー一覧を取得する際に、Repository層ではEmai認証状態を意識した取得は行わない
 →service層でフィルタリングする実装にしたが
 (アカウントIDからユーザー一覧を取得する処理がいままでなかったので、あったほうがいいかなと思い)

## クエリの変更
新規APIのためクエリの変更はない

## 動作確認状況
- ローカルで確認
UT+POSTMAN
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか
既存機能には手を入れていない
## 補足
- 相談、参考資料などがあれば
2024-04-03 00:50:53 +00:00
maruyama.t
8752448eed Merged PR 858: エラーメッセージ修正
## 概要
[Task4013: エラーメッセージ修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4013)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)
なし

## レビューポイント
- とくになし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
UIなのでなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか
メッセージの修正のみなのでデグレなし

## 補足
- 相談、参考資料などがあれば
2024-03-26 11:30:30 +00:00
湯本 開
1d71bef7aa Merged PR 857: テスト追加
## 概要
[Task3977: テスト追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3977)

- エラー時にアラーム発報ログが出力されているかを確認するテストを追加
- contextを継承して、ログに吐き出す内容をstring型の配列として溜め込むテスト用のクラスを追加

## レビューポイント
- テストの内容は妥当か

## クエリの変更
- テストのみ追加なので変更なし

## 動作確認状況
- npm run testが通過したこと
2024-03-26 07:05:39 +00:00
maruyama.t
114ded790e Merged PR 855: API IF実装(パートナーを編集したい)
## 概要
[Task3930: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3930)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 新規追加API2本のIFを作成、controllerの返却値は仮実装(別タスクで実装)
- 影響範囲(他の機能にも影響があるか)
 新規追加のみなので影響はなし

## レビューポイント
- 特筆する点はありません

## UIの変更
なし

## クエリの変更
なし

## 動作確認状況
- ローカルで確認
 バリデーションテストとPOSTMANからの起動の確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか
完全新規のIFの実装のみなのでデグレはない想定

## 補足
- 相談、参考資料などがあれば
2024-03-26 06:22:07 +00:00
Kentaro Fukunaga
ae638b16be Merge remote-tracking branch 'origin/develop' 2024-03-25 18:03:40 +09:00
湯本 開
133db833ee Merged PR 850: staging-pipelineのテスト実行修正
## 概要
[Task3948: staging-pipelineのテスト実行修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3948)

- docker-composeを使用したテストは `ubuntu-latest` で実行する必要があったため、pipelineを修正

## レビューポイント
- 問題がありそうな記述はあるか
- 実行順の依存関係等が壊れていないか

## 動作確認状況
- 未動作確認。4月以降、CCB対応時に動かして確認を想定。
2024-03-25 08:09:16 +00:00
Kentaro Fukunaga
1f0cf50166 Merged PR 856: Clientビルド時に埋め込んでいるキーやURLの更新
## 概要
[Task3963: Clientビルド時に埋め込んでいるキーやURLの更新](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3963)

- クライアント本番用のACB2C関連の環境変数を作り直したACB2Cの値に差し替えました。

## レビューポイント
- 共有

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- 未実施
- デプロイ後に確認

## 補足
- 相談、参考資料などがあれば
2024-03-25 07:54:43 +00:00
masaaki
eb6b413adb Merged PR 848: パートナー一覧画面修正
## 概要
[Task3833: パートナー一覧画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3833)

- パートナー一覧画面のパートナーにカーソルを合わせた際に、Delete Partnerリンクが表示される。
押下した場合は、本当に削除するのかを確認するメッセージが表示され、どのPartnerを削除するかメッセージ欄に表示されること。

## レビューポイント
- 特筆する点はなし

## UIの変更
https://ndstokyo.sharepoint.com/sites/Piranha/Shared%20Documents/Forms/AllItems.aspx?csf=1&web=1&e=hzPw9b&cid=e8e0702d%2D3730%2D4295%2Dbb9d%2D40e6b1998906&FolderCTID=0x012000C0DCEE65AC2177479C3C761CD137C9C9&id=%2Fsites%2FPiranha%2FShared%20Documents%2FGeneral%2FOMDS%2F%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%2FTask3833&viewid=786a81cf%2Dd15f%2D4dc2%2D9e55%2Dc7a729fbc72f

## クエリの変更
なし

## 動作確認状況
- ローカルで確認(APIからの返却値を直接指定する方式で確認)
- 行った修正がデグレを発生させていないことを確認できるか
  新規機能追加のみなので問題なし

## 補足
- 相談、参考資料などがあれば
2024-03-22 07:50:58 +00:00
makabe.t
6e93a5be79 Merged PR 846: パートナーアカウント削除API実装
## 概要
[Task3834: パートナーアカウント削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3834)

- パートナーアカウント削除APIとUTを実装しました。

## レビューポイント
- 削除対象データは適切でしょうか?
- テストケースに不足はないでしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
  - テストとローカルで実行確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存処理への変更なし
2024-03-22 06:12:47 +00:00
湯本 開
ac3d523c0e Merged PR 826: Azure Functions実装(音声ファイル削除)
## 概要
[Task3880: Azure Functions実装(音声ファイル削除)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3880)

- 自動音声ファイル削除を実装
- 上記のテストを実装
- テストにMySQLを使用する仕組みを導入

## レビューポイント
- テストケースは十分か
- テスト内容は妥当か
- developにデプロイする前の動作確認・ユニットテストとして十分か

## クエリの変更
- 新規処理のため、既存からの変更はなし

## 動作確認状況
- DBが空の状態でローカル環境で実行し、0件削除のログが出ることを確認
  - 削除対象が正しいか等はdevelopでチェック予定
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存処理の変更はなし
2024-03-19 07:36:03 +00:00
Kentaro Fukunaga
75f0a49fc1 Merged PR 831: 親アカウント変更API実装
## 概要
[Task3853: 親アカウント変更API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3853)

- 親アカウント切り替えAPIを実装しました。

## レビューポイント
- Service層の関数の分け方に改善点ないか?
- テストケースで他にあったほうがいいものや観点などあるか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで全テスト通ることを確認
- 行った修正がデグレを発生させていないことを確認できるか
    - 新規APIの実装のため既存実装に変更なし
2024-03-18 05:47:24 +00:00
maruyama.t
cab7a75ec1 Merged PR 849: 有効なアカウント内の削除されたユーザーの割り当て履歴が集計されない
## 概要
[Task3929: 有効なアカウント内の削除されたユーザーの割り当て履歴が集計されない](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3929)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
2024-03-15 12:19:16 +00:00
masaaki
f80912c617 Merged PR 834: API IF実装
## 概要
[Task3904: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3904)

- 「プロダクト バックログ項目 1242: パートナーを削除したい」のAPI IFを作成しました
- 影響範囲(他の機能にも影響があるか)
  - 新規IFのため影響はなし

## レビューポイント
- controllerの試験実装が初なので、テストケース過不足ないか確認いただきたいです。

## UIの変更
- 無し

## クエリの変更
- 無し

## 動作確認状況
- ユニットテストが通ることを確認、ローカル環境でpostmanで呼び出せることを確認、SWAGGER UI上で追加されていることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ユニットテストが通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-03-15 07:41:56 +00:00
makabe.t
66c643677d Merged PR 829: 音声ファイルバックアップポップアップ修正
## 概要
[Task3882: 音声ファイルバックアップポップアップ修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3882)

- 音声ファイルバックアップ中にファイル削除された場合の処理を追加しました。
  - ダウンロードで対象ファイルが削除されていた場合に特別なエラーとなるようにしています。
  - タスクバックアップで対象ファイルが削除されていた場合でも成功となるようにしています。

## レビューポイント
- 対応するエラーは適切でしょうか?

## UIの変更
- [Task3882](https://ndstokyo.sharepoint.com/:i:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3882/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%89%8A%E9%99%A4%E6%B8%88%E3%81%BF.png?csf=1&web=1&e=1BKVh8)

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ダウンロード実行中にファイル削除
    - backupAPIからタスク不在エラーを返却して成功するか確認
2024-03-15 06:53:41 +00:00
Kentaro Fukunaga
5f4a05044b Merged PR 845: クライアントメッセージ変更(アカウント階層構造変更)
## 概要
[Task3917: クライアントメッセージ変更](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3917)

- メッセージレビューで変更あったため修正して自動生成しました。

## レビューポイント
- 情報共有
2024-03-15 00:53:58 +00:00
maruyama.t
9256566f89 Merged PR 839: DEV動作確認のバグ対応
## 概要
[Task3918: DEV動作確認のバグ対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3918)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
2024-03-14 15:03:17 +00:00
makabe
9b61443b2f Merge branch 'develop' into ccb 2024-03-14 11:08:35 +09:00
makabe
017276c94a Merge branch 'develop' into ccb 2024-03-14 09:10:06 +09:00
maruyama.t
2b68a9f054 Merged PR 824: AzureFunctions実装3(CSVをストレージアカウントに配置する)
## 概要
[Task3846: AzureFunctions実装3(CSVをストレージアカウントに配置する)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3846)

outputDataを追加
→outputAnalysisLicensesDataに変更(アラートルールなどでログを見るので、何の処理か理解できるように)
blobstorageService.tsに以下を追加
 - uploadFileAnalysisLicensesCSV
(ライセンスCSVを配置する)
- createContainerAnalysisを追加
(コンテナーを作成する)
環境変数の追加

## レビューポイント
- 今回追加されたJP-EASTのストレージアカウントのコンテナーが、第一階層のアカウントのものであるかどうかはソース上は特に意識していないが問題ないでしょうか。

## 動作確認状況
- ローカルで確認(モックでソース上処理が通ることのみ確認のみ)
詳細なテストは別タスクで行う。

## 補足
- 相談、参考資料などがあれば
2024-03-13 07:54:10 +00:00
Kentaro Fukunaga
83e297cc9b Merged PR 821: 画面実装(パートナーライセンス一覧画面&階層構造変更ポップアップ)
## 概要
[Task3854: 画面実装(パートナーライセンス一覧画面&階層構造変更ポップアップ)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3854)

- パートナーライセンス一覧に「Change Owner」ボタンを配置し、表示制御およびクリック時にポップアップ表示する処理の実装
- アカウント階層構造変更ポップアップの処理全体実装
- サーバー側のエラーコード定義

## レビューポイント
- 「一括」を表現するためのドロップダウンの構築や処理周りで改善点ないか(to:斎藤くん)
- コンポーネントでの状態管理でお作法に違反しているところないか(to:斎藤くん)
- 修正箇所がほかの機能に影響していないか
    - パートナーライセンス一覧の画面表示に何らか悪影響ないか?(to:ガンさん)

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3854?csf=1&web=1&e=jBGQrR

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認しました
    - 第一階層でログインしてかつ第三または第四視点での一覧を確認しているときにChangeOwnerボタンが表示される
    - ボタン押下すると、仕様通りにポップアップの表示が行われる
    - ポップアップにて入力項目に入力できる&バリデーション効いている
    - ポップアップにて実行ボタン押下するとAPI実行できる&処理結果に応じて仕様通りの挙動をすること
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - パートナーライセンス画面に新規ボタンを配置した&新規ポップアップの実装のみのため、
ポップアップでの処理が正常終了/失敗/何もせず閉じた場合に元の画面の表示が今まで通り動くことを確認済み。
2024-03-13 07:41:25 +00:00
makabe.t
9f5ccabb0c Merged PR 830: アカウント削除修正
## 概要
[Task3896: アカウント削除修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3896)

- アカウント削除を実行すると失敗するので、対応を実施しました。
  - `AccountsRepositoryModule`のインポートに`AccountArchive`を追加しました。

## レビューポイント
- 対応内容は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
  - npm run test
  - 削除実行
- 行った修正がデグレを発生させていないことを確認できるか
  - リポジトリのインポートへの追加のみなので対象の処理ができることとtestが通ることで確認
2024-03-13 02:01:28 +00:00
maruyama.t
415fd2eb58 Merged PR 825: AzureFunctions実装2(取得したデータをCSV用に変換する)
## 概要
[Task3844: AzureFunctions実装2(取得したデータをCSV用に変換する)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3844)

transferData()の実装
ccbの最新よりマージした状態での引数に変更
各ライセンス数をCSV配列に出力できるところまでを実装
(実際にローカルにCSV出力して中身を確認済み)

![image.png](https://dev.azure.com/ODMSCloud/6023ff7b-d41c-4fa7-9c6f-f576ba48c07c/_apis/git/repositories/302da463-a2d7-40f9-b2bb-6e8edf324fa9/pullRequests/825/attachments/image.png)
※UIの変更ではない為、ここにそのまま張り付けさせていただきます。

## レビューポイント
- 関数化の範囲は適切か。
テストコードは最低限の記述になるが、問題ないか
(33行*7項目の突合せをコード上に実装するのは時間的余裕がないためやってない)
詳細な動作確認は、別タスク
https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%2029-2?workitem=3861
で行います。
## 補足
- 相談、参考資料などがあれば
2024-03-12 05:43:49 +00:00
masaaki
340aa73bde Merged PR 827: CSV出力が失敗したときに手動で起動するためのFunctionsを作成
## 概要
[Task3860: CSV出力が失敗したときに手動で起動するためのFunctionsを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3860)

- analysisLicensesについて手動で起動できる処理(analysisLicensesManualRetry)を追加しました
- データベース接続の初期化処理について共通化しました

## レビューポイント
- 特筆する部分はありません

## UIの変更
- 無し

## クエリの変更
- 無し

## 動作確認状況
- ローカルでpostmanからリクエストを行うことで起動できることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - unit testが通ることを確認
    - タイマを暫定的に1分にして、ローカル環境で各タイマ処理が正常終了することを確認(DBアクセスが全処理行われることを確認)

## 補足
- 相談、参考資料などがあれば
2024-03-12 04:55:04 +00:00
湯本 開
43561f237e Merged PR 822: クエリ比較用ログ出力の仕組みを改良
## 概要
[Task3889: クエリ比較用ログ出力の仕組みを改良](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3889)

- SQLクエリを比較可能とするための仕組みを導入
  - UUIDや日付等の実行の度に変更される要素を出力段階で置き換えてしまうロガーを追加
  - テストで上記ロガーを使うよう修正
- テストで使用する環境変数が必要がないのにlocalを指定するようになっていたため、production想定でテストが実施されるようテスト用環境変数ファイルを変更

## レビューポイント
- この仕組みを使ってデグレを防ぐための「クエリの変更内容を確認する」事が問題なく出来そうか

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- npm run test が正常に実施されることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - testファイルにしか参照されない変更なので、デグレは原理的に発生しないはず
  - testも正常に成功したことを確認したので、デグレは発生していないはず
2024-03-12 03:57:29 +00:00
SAITO-PC-3\saito.k
311eb98236 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
#	dictation_server/src/app.module.ts
2024-03-12 11:48:25 +09:00
masaaki
84fc89071a Merged PR 811: AzureFunctions実装1(DBから必要な情報を取得する)
## 概要
[Task3842: AzureFunctions実装1(DBから必要な情報を取得する)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3842)

- ライセンス数推移情報CSV出力機能のAzureFuntion用関数を追加しました
- DBから必要な情報を取得する処理を実装しました

## レビューポイント
- 特にレビューしてほしい箇所
  - DBアクセス時の結合・検索条件はラフスケッチの条件に対して過不足ないか
  - テストケースに不足はないか
- 関数名、構造は分かりづらくないか

## UIの変更
- 無し

## クエリの変更
- 新規のため無し

## 動作確認状況
- ローカルで処理が正常終了することを確認
- ユニットテストが通ることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - Function全体のunittestを実施し通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-03-11 06:04:02 +00:00
makabe.t
ccc03da62d Merged PR 823: 環境変数の追従
## 概要
[Task3891: 環境変数の追従](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3891)

- `server`に追加した環境変数の追従が`.env.local.example`から漏れていたので追加

## レビューポイント
- 共有

## UIの変更
- なし

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
  - exampleを追加したのみなので動作に影響はなし
2024-03-11 02:52:55 +00:00
湯本 開
ff4cd35ed3 Merged PR 819: [3848]アカウント削除処理修正
## 概要
[Task3847: [3848]アカウント削除処理修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3847)

- AccountArchiveエンティティを追加
- アカウント削除時、Accountをアーカイブする処理を追加
- アーカイブ関連テストを追加

## レビューポイント
- 実装の修正内容は問題なさそうか
- テストケースの修正内容は問題なさそうか
- クエリの変更内容の確認方法は問題なさそうか to 斎藤さん

## レビュー対象外
- テスト用ロガーに以下の比較用前処理を追加するべきだが、別タスクを作って対応予定
- 下記クエリの変更点にて、CommentOut判定に環境変数STAGEを使用している部分にRequestIdが表示されていない部分が存在するが、テスト用環境変数の変更は上記と同じく別タスクを作って対応予定
[タスク 3889: クエリ比較用ログ出力の仕組みを改良](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_workitems/edit/3889)

## クエリの変更
- ロガーを有効にした状態でテストを実行し、ログのUUIDと日付を処理して比較できるよう加工した
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/3847?csf=1&web=1&e=xlK011

## 動作確認状況
- npm run testで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - アカウント削除テストで発行されるクエリを比較し、AccountArchiveする対象を特定するためのAccountのSELECT、AccountArchiveのINSERTとSELECTのみが追加されている事が確認できたので、デグレはないと判断
2024-03-11 02:08:29 +00:00
Kentaro Fukunaga
f386a8f7e0 Merged PR 817: API IF実装(親アカウント変更API)
## 概要
[Task3852: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3852)

- 親アカウント変更APIのIFを実装し、OpenAPIの生成もしました。
- 影響範囲(他の機能にも影響があるか)
    - なし

## レビューポイント
- controllerのメソッド名にほか良い案ないか?
- validationに過不足や間違いないか?
- ~~controllerのテストは正常系ひとつだけ追加しているが、他にあったほうがいいものあるか?~~
    - ~~個人的には、テスト追加してもnpmライブラリのvalidatorのテストになるだけな気がするため不要では?と思っています。~~
    - 「npmライブラリのvalidatorを正しいパラメータで正しく利用しているか」が目的であるとの認識を得たため異常系も追加しました。

## 動作確認状況
- apigenを実行してOpenAPI生成できることを確認、controllerテスト通ることを確認。
- 行った修正がデグレを発生させていないことを確認できるか
  - 新規APIのため無し
2024-03-11 01:29:55 +00:00
makabe.t
2e6b7c8ab5 Merged PR 818: function修正
## 概要
[Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879)

- 行番号をExcelの表記通りとなるように修正
- 途中から始められるようにforループを修正

## レビューポイント
- 共有

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-03-08 05:45:53 +00:00
Kentaro Fukunaga
85edd2296e Merged PR 820: DBマイグレーションファイル追加(アカウント階層構造変更PBI)
## 概要
[Task3863: DBマイグレーションファイル追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3863)

- licenseOrdersテーブルに、from_account_idとstatus検索のマルチカラムインデックスを追加しました。
- 影響範囲(他の機能にも影響があるか)
    - なし

## レビューポイント
- 気になる点あれば

## 動作確認状況
- ローカルでmigrate:up/downをして、想定通りindex作成/削除されることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - インデックス追加のみのため無し
2024-03-08 05:21:22 +00:00
湯本 開
869cbd43e0 Merged PR 810: アカウント退避テーブル作成
## 概要
[Task3848: アカウント退避テーブル作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3848)

- アカウントテーブルと同様の構造をしたアカウント退避テーブルを作成
  - ※差分:  company_nameを削除、active_worktype_idの外部キー制約を削除

## レビューポイント
- 上記要素以外はaccountsテーブルの要素と同等であるか
- 張られたインデックスに不足はないか
- 想定と違う構造になっていないか

## 動作確認状況
- npm run migrate:up/downを実施
- EXPLAINでindexが機能していそうな事を確認
```
EXPLAIN SELECT * FROM omds_ccb.accounts_archive where parent_account_id=1;
---------------------------------------------------
# id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra
'1', 'SIMPLE', 'accounts_archive', NULL, 'ref', 'idx_accounts_archive_parent_account_id', 'idx_accounts_archive_parent_account_id', '9', 'const', '1', '100.00', NULL
```

```
EXPLAIN SELECT * FROM omds_ccb.accounts_archive where tier=2;
---------------------------------------------------
# id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra
'1', 'SIMPLE', 'accounts_archive', NULL, 'ref', 'idx_accounts_archive_tier', 'idx_accounts_archive_tier', '4', 'const', '2', '100.00', NULL
```
```
EXPLAIN SELECT * FROM omds_ccb.accounts_archive where parent_account_id=1 AND tier=1;
----------------------------------------------------
# id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra
'1', 'SIMPLE', 'accounts_archive', NULL, 'ref', 'idx_accounts_archive_parent_account_id,idx_accounts_archive_tier', 'idx_accounts_archive_parent_account_id', '9', 'const', '1', '50.00', 'Using where'
```
2024-03-08 05:20:00 +00:00
makabe.t
146b8a6e40 Merged PR 816: function修正
## 概要
[Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-03-08 01:09:14 +00:00
SAITO-PC-3\saito.k
9dc80b4965 Merge branch 'develop' into ccb 2024-03-07 20:55:44 +09:00
makabe.t
04c726e964 Merged PR 808: function修正
## 概要
[Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-03-07 11:47:53 +00:00
SAITO-PC-3\saito.k
eda88aa048 Merge branch 'develop' into ccb 2024-03-06 11:23:50 +09:00
makabe.t
da40e8f09c Merged PR 800: 画面実装(一括追加ボタン&ポップアップ画面)
## 概要
[Task3753: 画面実装(一括追加ボタン&ポップアップ画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3753)

- ユーザー一括登録画面を実装しました。
  - 一括登録ポップアップ
    - テンプレートCSVダウンロード
    - ファイルインポート
      - エラー行表示

## レビューポイント
- 行エラーの条件、内容は認識通りでしょうか?
- 画面の表示内容は認識通りでしょうか?
- CSV変換時にworkerを有効にしているとエラーとなりうまくいかないのでOFFにしてしまいましたが問題ないでしょうか?
  - @<湯本 開> さん

## UIの変更
- [Task3753](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3753?csf=1&web=1&e=x5M6hr)

## 動作確認状況
- ローカルで確認
 -  ファイルチェックするところまで
2024-03-06 01:58:32 +00:00
makabe.t
d6a47932e7 Merged PR 786: Azure Functions実装(一括登録)
## 概要
[Task3756: Azure Functions実装(一括登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3756)

- ユーザー一括登録用のAzure Functionを実装しました。

## レビューポイント
- 処理の流れがラフスケッチと認識通りでしょうか?
- JSONファイルの内容はイメージ通りでしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
  - テスト実行

実際の詳細な動作についてはdevelop環境で確認します。
2024-03-06 01:48:02 +00:00
makabe.t
31de71f743 Merged PR 797: API実装(一括登録)
## 概要
[Task3752: API実装(一括登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3752)

- ユーザー一括登録APIとテストを実装しました。
  - メール文面はまだ翻訳が来ていないので日本語のものを使用しています。別タスクで多言語対応します。

## レビューポイント
- ファイル名は認識通りでしょうか?
- ファイルの内容は認識通りでしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-03-05 10:27:50 +00:00
makabe.t
7ff563f644 Merged PR 795: API実装(一括登録完了)
## 概要
[Task3763: API実装(一括登録完了)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3763)

- 一括登録完了APIとテストを実装しました。
  - メール文面は多言語対応がまだですのですべて日本語の文面にしています。

## レビューポイント
- 送信メールの内容は認識通りでしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-03-04 09:15:53 +00:00
湯本 開
363f12f86f Merged PR 774: 画面実装(csv読み込み部分切り出し)
## 概要
[タスク 3754: 画面実装(csv読み込み部分切り出し)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3754)

- CSVファイルの内容を入力として、JSONに変換する処理&テストを実装
- 画面実装時の想定としては、以下の流れと想定しており、本Taskの範囲は1のみ
  1. csv->jsonへパースが出来るか?(csvの形式として合っていて読み込み可能か?)
  2. json内の各パラメータは問題ないか?(データの制限や組み合わせは問題ないか?)
- 使い方を示す & 動作確認のために、client側でもテストを実施できるよう修正(※pipelineでは実行されない)

## レビューポイント
- テストケースを見て、使い方は分かるか
- CSVの形式自体が想定とズレていた場合は入力を弾く必要がある想定だが、間違っていないか
- 利用ライブラリはメジャーかつ便利そうなものを選定したが、問題なさそうか

## 動作確認状況
- npm run testを通過
2024-02-28 09:03:27 +00:00
湯本 開
71127a6db9 Merged PR 787: API I/F修正
## 概要
[Task3793: API I/F修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3793)

- OMDS様とのメール文面調整の結果、csvファイル名もAPIで受け渡す必要が出てきたためAPI I/Fを修正する
  - 一括登録依頼API、一括登録完了APIの両方に「ファイル名」を追加
- 増えたプロパティのバリデーションをするテストを追加

## レビューポイント
- プロパティ名は妥当か
- テストの内容は十分か

## 動作確認状況
- npm run testを通過
2024-02-28 05:30:09 +00:00
Kentaro Fukunaga
dd8bddc971 Merged PR 771: 音声ファイルアップロード完了API実装(ストレージ使用量超過チェック)
## 概要
[Task3687: 音声ファイルアップロード完了API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3687)

- 音声ファイルアップロード完了API実行時に、ストレージの使用量チェックを行い、必要ならメール送信をする実装を追加しました。

## レビューポイント
- 使用量チェックメソッドで他にいい関数名ないか?
- なるべく既存実装をいじりたくなかったので自動ルーティング前にチェック機構を配置したが不都合ないか?
- テストケースに過不足ないか
- 自動テストの実行方法や確認方法として適切か?ほかに代替案ないか?

## 動作確認状況
- ローカルでUT通ることを確認。
   - 実際のメール送信はdeveop動作確認でやります。
2024-02-27 02:49:52 +00:00
SAITO-PC-3\saito.k
ddd4d31f25 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_server/src/features/users/users.service.spec.ts
2024-02-27 09:26:46 +09:00
Kentaro Fukunaga
5305984b1a Merged PR 764: 第五階層ライセンス情報画面実装
## 概要
[Task3709: 第五階層ライセンス情報画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3709)

- ストレージ使用可否切り替えの画面実装をしました
- 動作確認中に、既存実装でライセンスオーダーするときとカードライセンスアクティベートするときの操作不能化処理に漏れがあったのを修正しました

## レビューポイント
- Redux周りの実装でお作法に違反しているところがないか。もしくは改善点ないか。
- ライセンス情報表示のAPI結果待ち部分のローディング処理で、最低限の改善にしたが現時点ではこれでよいか?(いつ修正するかも未定だけど、実害はないためひとまずこんな感じで。。。)
    - `licenseSummarySlice.ts` のコメント部分が該当箇所です

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3709?csf=1&web=1&e=bJVzss

## 動作確認状況
- ローカルで動作確認しました。
2024-02-27 00:01:02 +00:00
湯本 開
c95fb1e1f6 Merged PR 784: テスト失敗修正(I/F実装)
## 概要
[Task3791: テスト失敗修正(I/F実装)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3791)

- AuthorIDのチェックを行うバリデータが特定リスクエストに対してしか利用できなかったので、もっと単純な同一ロジックのバリデータを追加して利用するよう変更
- 上記ケースに対するテストを追加

## レビューポイント
- 既存実装へは影響がなさそうか

## 動作確認状況
- npm run testを通過
2024-02-26 07:48:08 +00:00
湯本 開
c1f370faaf Merged PR 766: API I/F & system権限Token実装
## 概要
[Task3764: API I/F & system権限Token実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3764)

- システムが発行したトークンの型定義を追加
- AuthGuardと同等の、システムが発行したTokenである `SystemAccessToken` を検証する `SystemAccessGuard` を追加
- API I/Fを実装

## レビューポイント
- バリデーターは適切か
- システムが発行したトークンの型定義は適切か
- API I/Fの型は問題ないか

## 動作確認状況
- ローカルでswagger UI上で確認
2024-02-26 05:13:43 +00:00
SAITO-PC-3\saito.k
13d421c2bc Merge branch 'develop' into ccb 2024-02-23 11:10:38 +09:00
makabe
a1b59de44d Merge branch 'develop' into ccb 2024-02-22 20:43:35 +09:00
makabe
439ce7de63 Merge branch 'develop' into ccb 2024-02-21 09:16:05 +09:00
Kentaro Fukunaga
f8183399e2 Merged PR 762: アカウント利用制限更新API実装
## 概要
[Task3710: アカウント利用制限更新API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3710)

- タイトルの通りです。
- 既存実装でログが不足している箇所あったのでちょろ修正もしました。

## レビューポイント
- これと言ってみて欲しいポイントはないので、何か気になる点あれば

## 動作確認状況
- ローカルで全テストが通ることを確認済み
2024-02-20 11:23:04 +00:00
makabe
e44cb3b955 Merge branch 'develop' into ccb 2024-02-20 19:24:42 +09:00
SAITO-PC-3\saito.k
562db3def9 Merge branch 'develop' into ccb 2024-02-19 21:25:54 +09:00
Kentaro Fukunaga
ecb28b9328 Merged PR 759: API IF実装
## 概要
[Task3711: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3711)

- アカウント利用制限更新APIのIFを実装し、openapi生成しました。

## レビューポイント
- メソッド名で他に案あれば。

## 動作確認状況
- 特になし
2024-02-19 04:32:59 +00:00
SAITO-PC-3\saito.k
cdef84e269 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_server/src/features/accounts/accounts.service.spec.ts
2024-02-19 12:00:01 +09:00
makabe.t
fc7d271a29 Merged PR 749: 第五階層ライセンス情報画面実装
## 概要
[Task3654: 第五階層ライセンス情報画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3654)

- 第五階層ライセンス情報画面でストレージ情報(Storage Available、Storage Used)を表示するように画面を修正しました。
  - 表示する値はGB単位となるようにし、小数点以下第三位までを表示するようにしています。

## レビューポイント
- 表示する値のフォーマットは適切でしょうか?
  - 小数点以下第三位までを固定で表示するようにしていますが問題ないでしょうか?

## UIの変更
- [Task3654](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3654?csf=1&web=1&e=1tCGoN)

## 動作確認状況
- ローカルで確認
2024-02-16 05:15:45 +00:00
Kentaro Fukunaga
aef30c8cbe Merged PR 748: 第五階層ライセンス情報取得API実装
## 概要
[Task3655: 第五階層ライセンス情報取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3655)

- 第五階層ライセンス情報取得APIに、ストレージ上限とストレージ使用量を取得する処理を追加しました。
- 既存の「割り当て済みライセンス取得処理」と「再利用可能ライセンス取得処理」に不要な条件があったため削除しました

## レビューポイント
- 上限計算方法、使用量取得条件に仕様との認識齟齬はないか?
    - もしくはテストケースで「これもあったほうがいいのでは?」などないか
- その他気になる点あれば

## 動作確認状況
- ローカルでテストが全部通ることを確認
2024-02-16 02:11:34 +00:00
x.yumoto.k
c0b99203da Merge branch 'develop' into ccb 2024-02-15 15:14:50 +09:00
SAITO-PC-3\saito.k
63191a3b61 OptionItemの作成が重複してしまっていたので片方を削除
タスク削除のテストをMysql使用するように修正
2024-02-14 12:06:19 +09:00
SAITO-PC-3\saito.k
3877a4670d Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/pages/UserListPage/index.tsx
2024-02-14 11:33:11 +09:00
makabe.t
bbbd3e757b Merged PR 743: テンプレートファイル削除画面修正
## 概要
[Task3600: テンプレートファイル削除画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3600)

- テンプレートファイル削除の画面を実装しました。

## レビューポイント
- エラー処理は適切でしょうか?

## UIの変更
- [Task3600](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3600?csf=1&web=1&e=E25Kf7)

## 動作確認状況
- ローカルで確認
2024-02-13 02:32:17 +00:00
makabe.t
83efd97bdf Merged PR 739: テンプレートファイル削除API実装
## 概要
[Task3599: テンプレートファイル削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3599)

- テンプレートファイル削除APIとテストを実装しました。

## レビューポイント
- テンプレートファイル削除できないエラーの条件は適切でしょうか?
- テストケースは適切でしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
2024-02-13 00:22:38 +00:00
makabe
447b0e280c Merge branch 'develop' into ccb 2024-02-09 19:06:45 +09:00
x.yumoto.k
cd277f3f9a Merge branch 'develop' into ccb 2024-02-09 17:37:38 +09:00
makabe.t
270122b135 Merged PR 741: アカウント情報画面修正(保存日数の表示)
## 概要
[Task3691: アカウント情報画面修正(保存日数の表示)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3691)

- アカウント情報画面にファイルの保存日数を表示するよう修正しました。
  - ファイル削除が設定されている場合は日数、されていない場合はハイフン表示となるようにしています。

## レビューポイント
- 画面イメージは想定通りでしょうか?
- 表示する保存日数は想定通りの値でしょうか?

## UIの変更
- [Task3691](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3691?csf=1&web=1&e=Gthaa8)

## 動作確認状況
- ローカルで確認
2024-02-09 01:05:15 +00:00
masaaki
32d8c6b896 Merged PR 737: ファイル削除設定API作成
## 概要
[Task3546: ファイル削除設定API作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3546)

- プロダクト バックログ項目 1199: アカウント設定を変更したい(ファイル削除設定)
- ファイル削除設定API作成を実装しました
- 変数定義時に型指定が不要で、自動的に明示的な型宣言が削除されています。非テストコードについてはeslint-disable-lineによって無視設定を行っています。テストコードについては削除したままとしています。

## レビューポイント
- 特筆するものはありません

## UIの変更
- 無し

## 動作確認状況
- ローカル及びユニットテストで確認

## 補足
- 無し
2024-02-07 02:47:41 +00:00
Kentaro Fukunaga
91c27b7684 Merged PR 736: テンプレートファイル削除API IF実装
## 概要
[Task3598: テンプレートファイル削除API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3598)

- 掲題のとおり、IF実装しました。

## レビューポイント
- 認可処理に認識違いないか?
- パラメータに不足ないか?

## 動作確認状況
- ローカルでAPI実行できることを確認

## 補足
- とくになし
2024-02-06 23:53:00 +00:00
Kentaro Fukunaga
548c7a05e9 Merged PR 729: 画面実装(ファイル削除設定ポップアップ画面)
## 概要
[Task3547: 画面実装(ファイル削除設定ポップアップ画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3547)

- ファイル削除設定ポップアップの新規実装をしました。

## レビューポイント
- reduxとの連携部分の設計でNGな部分ないか?
- 処理の抜け漏れないか?

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3547?csf=1&web=1&e=0pu92Q

## 動作確認状況
- ローカルで一通りの動作確認しました。

## 補足
- とくになし
2024-02-06 12:58:55 +00:00
makabe.t
eaf1b3c8b8 Merged PR 738: develop動作確認不具合対応
## 概要
[Task3645: develop動作確認不具合対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3645)

- 削除完了メールのタイトルを修正しました。

## レビューポイント
- 共有

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-02-06 10:26:42 +00:00
makabe.t
19b544540e Merged PR 724: API実装
## 概要
[Task3535: API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3535)

- タイピストグループ削除APIとテストを実装しました。

accountのテストがうまくいっていないようなので別途見直します。
※タイピストグループ削除のテストはうまくいっています

## レビューポイント
- エラーケースと出力されるコードは適切でしょうか?
- テストケースは適切でしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
2024-02-06 07:46:57 +00:00
湯本 開
feeec9d1f5 Merged PR 714: API実装(ユーザー削除|Repository以外)
## 概要
[Task3594: API実装(ユーザー削除|Repository以外)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3594)

ユーザー削除API実装
ユニットテスト実装

## レビューポイント
- `'E014008', // ユーザー削除エラー(削除しようとしたユーザーが自分自身だった)`が用意されているが、
`'E014002', // ユーザー削除エラー(削除しようとしたユーザーが管理者だった)`とわけて実装する必要あるか。
管理者でしか削除処理は行えない&管理者ユーザは削除できない。

- `ExistsCheckoutPermissionDeleteFailedError`
削除対象ユーザーがチェックアウト権限を持っている事が原因の削除失敗エラーは、ユーザ削除エラーの一つとして、`code.ts`にコードを用意してあげる必要があるか?
(引継ぎ時あえて用意していないように見えなくもなかったので)

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
2024-02-06 07:12:11 +00:00
x.yumoto.k
4442c7cbbe Merge branch 'develop' into ccb 2024-02-06 14:56:39 +09:00
SAITO-PC-3\saito.k
1e8bc39c7f Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
2024-02-05 21:15:24 +09:00
makabe
ce3deecd1b Merge branch 'develop' into ccb 2024-02-05 19:39:37 +09:00
makabe
8e159a1c2a Merge branch 'develop' into ccb 2024-02-05 16:36:09 +09:00
maruyama.t
df74dc358c Merged PR 731: 既存API修正(アカウント情報取得API、アカウント作成API)
## 概要
[Task3555: 既存API修正(アカウント情報取得API、アカウント作成API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3555)

Accountにauto_file_delete、file_retention_daysを追加
既存のテストでアカウントを作成している部分に項目の値を追加。

## レビューポイント
-entityの

```
@Column({ default: 30 })
```
が必要かどうか。

## UIの変更
なし

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
2024-02-05 02:27:43 +00:00
makabe
186896da15 Merge branch 'ccb' of https://dev.azure.com/ODMSCloud/ODMS%20Cloud/_git/ODMS%20Cloud into ccb 2024-02-05 09:46:49 +09:00
saito.k
4be13e002d Merged PR 710: 画面実装(削除操作)
## 概要
[Task3488: 画面実装(削除操作)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3488)

- ユーザー削除の画面実装
  - 確認ダイアログ
  - 削除API呼び出し
  - エラーハンドリング
  - 成功時のメッセージ
  - 成功時のユーザー一覧更新

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認

## 補足
- API呼び出しのエラーハンドリング部分はエラーコードが採番されたら追従します
2024-02-05 00:39:53 +00:00
makabe
cf30b97a16 Merge branch 'ccb' of https://dev.azure.com/ODMSCloud/ODMS%20Cloud/_git/ODMS%20Cloud into ccb 2024-02-05 09:37:06 +09:00
Kentaro Fukunaga
44759b1aac Merged PR 725: タイピストグループ削除 画面実装
## 概要
[Task3536: 画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3536)

- タイピストグループ画面に削除ボタン配置
- 削除ボタンクリック時に確認ダイアログ表示→OKで削除API実行&ぐるぐる表示
- API実行結果によってメッセージの出し分け&画面リスト更新
- 翻訳情報追加

## レビューポイント
- エラーコードに認識違いないか?
- 画面実装のお作法にそぐわないところがないか?
- なんか抜けてる実装などあれば

## UIの変更
大した変更ではないので、スクショ置き場に置く手間を省いてここに貼り付けます
![image (2).png](https://dev.azure.com/ODMSCloud/6023ff7b-d41c-4fa7-9c6f-f576ba48c07c/_apis/git/repositories/302da463-a2d7-40f9-b2bb-6e8edf324fa9/pullRequests/725/attachments/image%20%282%29.png)

## 動作確認状況
- 期待される動作を一通りローカルで確認しました。
    - API実行中はぐるぐる表示されること
    - API成功または削除済みの場合は成功メッセージ表示され、画面更新されること
    - API失敗時、理由によってエラーメッセージが表示分けされること
2024-02-05 00:31:47 +00:00
makabe
5ab2f02c56 Merge branch 'develop' into ccb 2024-02-05 09:26:55 +09:00
Kentaro Fukunaga
f595cae1b8 Merged PR 728: ファイル削除設定更新API IF実装
## 概要
[Task3557: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3557)

- ファイル削除設定更新APIのIFを実装してOpenAPI生成しました。

## 補足
- 別PRでOKもらってるのでそのままマージします
2024-02-03 06:22:53 +00:00
Kentaro Fukunaga
48a2bddfd9 Merged PR 721: DBマイグレーション
## 概要
[Task3556: DBマイグレーション](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3556)

- アカウントテーブルに対して、以下の2カラムを追加しました。
    - 自動ファイル削除要否
    - 文字起こし完了してからファイル削除するまでのファイル保持日数

## レビューポイント
- カラム名としてほかに適切なものはないか?
- 初期値合ってますよね?

## 動作確認状況
- ローカルでmigrate up と downが出来ることを確認

## 補足
- もしこの値の扱いが、「Finishedになってから○○日後にファイルを削除する」のものであればそのとき修正します。
2024-02-02 04:57:25 +00:00
SAITO-PC-3\saito.k
fbcafd2014 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/pages/DictationPage/index.tsx
2024-02-02 12:01:46 +09:00
makabe.t
9f7c8c99c0 Merged PR 720: API IF実装
## 概要
[Task3534: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3534)

- タイピストグループ削除APIのIFを実装し、OpenAPIを更新しました。

## レビューポイント
- パラメータとバリデータは想定通りでしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-02-02 00:27:08 +00:00
makabe
92193d499a Merge branch 'develop' into ccb 2024-01-31 10:28:39 +09:00
湯本 開
c32b38b783 Merged PR 703: API実装(ユーザー削除)/Repository実装
## 概要
[Task3521: API実装(ユーザー削除)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3521)

- ユーザー削除を行うRepository(DB操作部分)を実装
  - 削除不可条件をチェックして削除できなければエラー
  - 削除可能だった場合、以下の処理を実行
    - ユーザーをアーカイブ
    - ユーザーを削除

## レビューポイント
- 「ライセンス割り当て解除」をせずにユーザーを削除するため、ライセンスがUserテーブルに存在しないIDを指したままになってしまうが問題ないか
  - ラフスケッチ時には、UserArchiveのidには紐づく & UserArchiveに紐づくことによって期限切れのライセンスが誰に割り当たっていたかを把握できるという話だったと思うが、これは"そういう必要がある"という仕様という認識でよいか
- ロック対象の指定は妥当であるか
  - デッドロックは発生しなさそうか
    - User -> UserGroup -> Workflow -> Task -> CheckoutPermission -> Licenseの順番

## 動作確認状況
- 動作確認なし
2024-01-30 07:10:11 +00:00
SAITO-PC-3\saito.k
08a37ff264 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
2024-01-29 18:30:34 +09:00
makabe
f00861702a Merge branch 'develop' into ccb 2024-01-29 15:52:55 +09:00
湯本 開
8dfbcea0da Merged PR 702: API IF実装
## 概要
[Task3520: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3520)

- ユーザー削除APIのI/Fを実装

## レビューポイント
- バリデーターは適切に設定されているか
- 不要な処理が混入していないか
- 代行操作による実行を許可しているが、認識は間違っていないか
- マージ先ブランチは間違っていないか

## 動作確認状況
- openapi.jsonの生成成功を確認
2024-01-25 04:00:54 +00:00
makabe.t
8aa45baee8 Merged PR 697: DB関連コード修正
## 概要
[Task3509: DB関連コード修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3509)

- 参照するDBがCCB用のスキーマとなるようにserverの参照DB名を変更しました。
対象:
- server
  - .env
  - app.module.ts
- function
  - .env
  - functions/*

※マージの取り込み分も入ってしまったので他は無視してください。

## レビューポイント
- 対応箇所は適切でしょうか。

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
  - ローカルにスキーマを追加・マイグレーションしてserverを起動できることを確認
2024-01-24 02:52:13 +00:00
SAITO-PC-3\saito.k
f2f8728319 ccbの競合解消漏れを対応 2024-01-24 11:15:27 +09:00
SAITO-PC-3\saito.k
ded446de93 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/pages/UserListPage/index.tsx
#	dictation_server/package.json
#	dictation_server/src/common/test/init.ts
#	dictation_server/src/features/auth/auth.service.spec.ts
#	dictation_server/src/features/files/files.service.spec.ts
#	dictation_server/src/features/licenses/licenses.service.spec.ts
#	dictation_server/src/features/tasks/tasks.service.spec.ts
#	dictation_server/src/features/terms/terms.service.spec.ts
#	dictation_server/src/features/users/users.service.spec.ts
#	dictation_server/src/features/workflows/workflows.service.spec.ts
2024-01-24 10:46:48 +09:00
makabe.t
1524ec2473 Merged PR 701: パイプラインエラー対応
## 概要
[Task3531: パイプラインエラー対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3531)

- パイプラインエラー解消

## レビューポイント
- 共有
2024-01-24 00:48:21 +00:00
makabe.t
a7bb32ec4a Merged PR 699: DBマイグレーションファイルにCCB用の設定を追加する
## 概要
[Task3510: DBマイグレーションファイルにCCB用の設定を追加する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3510)

- DBマイグレーションファイルにCCB用の設定を追加し、コマンドを修正しました。

## レビューポイント
- 設定内容は認識通りでしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
  - CCB用のスキーマに対してmigrate:up/downできるところまで確認
2024-01-23 10:30:14 +00:00
makabe.t
d08c6c99af Merged PR 681: タスク削除API実装
## 概要
[Task3457: タスク削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3457)

- タスク削除APIとUTを実装しました。

## レビューポイント
- テストケースは適切でしょうか?
- リポジトリでの削除処理は適切でしょうか?
- エラー時のコード使い分けは適切でしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-01-16 07:55:38 +00:00
makabe.t
8793606070 Merged PR 682: タスク一覧画面修正
## 概要
[Task3458: タスク一覧画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3458)

- タスク一覧画面のタスク削除ボタンからタスクを削除する処理を実装しました。
  - タスクがInProgress、ユーザーがTypistの場合にはボタンを非活性となるようにしています。

## レビューポイント
- エラーごとの処理内容は適切でしょうか?
- ボタンの活性制御は適切でしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-01-16 00:17:45 +00:00
makabe.t
81c299dd99 Merged PR 680: タスク削除API IF実装
## 概要
[Task3456: タスク削除API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3456)

- タスク削除APIのIFを実装しopenapi.jsonを更新しました。

## レビューポイント
- パス、バリデータは想定通りでしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-01-11 08:47:28 +00:00
326 changed files with 71485 additions and 2884 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
environment_building_tools/logfile.log

View File

@ -0,0 +1,312 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger:
branches:
include:
- release-ccb
tags:
include:
- stage-*
jobs:
- job: initialize
displayName: Initialize
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
persistCredentials: true
- script: |
git fetch origin release-ccb:release-ccb
if git merge-base --is-ancestor $(Build.SourceVersion) release-ccb; then
echo "This commit is in the release-ccb branch."
else
echo "This commit is not in the release-ccb branch."
exit 1
fi
displayName: 'タグが付けられたCommitがrelease-ccbブランチに存在するか確認'
- job: backend_test
dependsOn: initialize
condition: succeeded('initialize')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_server/.devcontainer
script: |
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
- job: backend_build
dependsOn: backend_test
condition: succeeded('backend_test')
displayName: Build And Push Backend Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_server
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileServerDictation.dockerfile
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
- job: frontend_build_staging
dependsOn: backend_build
condition: succeeded('backend_build')
displayName: Build Frontend Files(staging)
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_client
verbose: false
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: cd dictation_client && npm run build:stg
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: dictation_client/build
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip'
replaceExistingArchive: true
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: function_test
dependsOn: frontend_build_staging
condition: succeeded('frontend_build_staging')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_function/.devcontainer
script: |
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci
docker-compose exec -T dictation_function sudo npm run test
- job: function_build
dependsOn: function_test
condition: succeeded('function_test')
displayName: Build And Push Function Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_function
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileFunctionDictation.dockerfile
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
- job: backend_deploy
dependsOn: function_build
condition: succeeded('function_build')
displayName: Backend Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'app-odms-dictation-stg'
deployToSlotOrASE: true
resourceGroupName: 'stg-application-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
- job: frontend_deploy
dependsOn: backend_deploy
condition: succeeded('backend_deploy')
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob download \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--file $(Build.SourcesDirectory)/$(Build.SourceVersion).zip
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: unzip $(Build.SourcesDirectory)/$(Build.SourceVersion).zip -d $(Build.SourcesDirectory)/$(Build.SourceVersion)
- task: AzureStaticWebApp@0
displayName: 'Static Web App: '
inputs:
workingDirectory: '$(Build.SourcesDirectory)'
app_location: '/$(Build.SourceVersion)'
config_file_location: /dictation_client
skip_app_build: true
skip_api_build: true
is_static_export: false
verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
- job: function_deploy
dependsOn: frontend_deploy
condition: succeeded('frontend_deploy')
displayName: Function Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureFunctionAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'func-odms-dictation-stg'
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
- job: smoke_test
dependsOn: function_deploy
condition: succeeded('function_deploy')
displayName: 'smoke test'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
# スモークテスト用にjobを確保
- job: swap_slot
dependsOn: smoke_test
condition: succeeded('smoke_test')
displayName: 'Swap Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-dictation-stg'
inputs:
azureSubscription: 'omds-service-connection-stg'
action: 'Swap Slots'
WebAppName: 'app-odms-dictation-stg'
ResourceGroupName: 'stg-application-rg'
SourceSlot: 'staging'
SwapWithProduction: true
- job: migration
dependsOn: swap_slot
condition: succeeded('swap_slot')
displayName: DB migration
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: 'omds-service-connection-stg'
KeyVaultName: kv-odms-secret-stg
- task: CmdLine@2
displayName: migration
inputs:
script: >2
# DB接続情報書き換え
sed -i -e "s/DB_NAME_CCB/$(db-name-ccb)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(admin-db-user)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PORT/$(db-port)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_HOST/$(db-host)/g" ./dictation_server/db/dbconfig.yml
sql-migrate --version
cat ./dictation_server/db/dbconfig.yml
# migration実行
sql-migrate up -config=./dictation_server/db/dbconfig.yml -env=ci_ccb

View File

@ -0,0 +1,363 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConnectionを作成しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger:
branches:
include:
- release-ph1-enhance
tags:
include:
- stage-*
jobs:
- job: initialize
displayName: Initialize
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
persistCredentials: true
- script: |
git fetch origin release-ph1-enhance:release-ph1-enhance
if git merge-base --is-ancestor $(Build.SourceVersion) release-ph1-enhance; then
echo "This commit is in the release-ph1-enhance branch."
else
echo "This commit is not in the release-ph1-enhance branch."
exit 1
fi
displayName: 'タグが付けられたCommitがrelease-ph1-enhanceブランチに存在するか確認'
- job: backend_test
dependsOn: initialize
condition: succeeded('initialize')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_server/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
- job: backend_build
dependsOn: backend_test
condition: succeeded('backend_test')
displayName: Build And Push Backend Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_server
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileServerDictation.dockerfile
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
- job: frontend_build_staging
dependsOn: backend_build
condition: succeeded('backend_build')
displayName: Build Frontend Files(staging)
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_client
verbose: false
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: cd dictation_client && npm run build:stg
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: dictation_client/build
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip'
replaceExistingArchive: true
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: frontend_build_production
dependsOn: frontend_build_staging
condition: succeeded('frontend_build_staging')
displayName: Build Frontend Files(production)
variables:
storageAccountName: saomdspipeline
environment: production
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_client
verbose: false
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: cd dictation_client && npm run build:prod
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: dictation_client/build
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip'
replaceExistingArchive: true
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: function_test
dependsOn: frontend_build_production
condition: succeeded('frontend_build_production')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_function/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci
docker-compose exec -T dictation_function sudo npm run test
- job: function_build
dependsOn: function_test
condition: succeeded('function_test')
displayName: Build And Push Function Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_function
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileFunctionDictation.dockerfile
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
- job: backend_deploy
dependsOn: function_build
condition: succeeded('function_build')
displayName: Backend Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'app-odms-dictation-stg'
deployToSlotOrASE: true
resourceGroupName: 'stg-application-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
- job: frontend_deploy
dependsOn: backend_deploy
condition: succeeded('backend_deploy')
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob download \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--file $(Build.SourcesDirectory)/$(Build.SourceVersion).zip
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: unzip $(Build.SourcesDirectory)/$(Build.SourceVersion).zip -d $(Build.SourcesDirectory)/$(Build.SourceVersion)
- task: AzureStaticWebApp@0
displayName: 'Static Web App: '
inputs:
workingDirectory: '$(Build.SourcesDirectory)'
app_location: '/$(Build.SourceVersion)'
config_file_location: /dictation_client
skip_app_build: true
skip_api_build: true
is_static_export: false
verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
- job: function_deploy
dependsOn: frontend_deploy
condition: succeeded('frontend_deploy')
displayName: Function Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureFunctionAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'func-odms-dictation-stg'
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
- job: smoke_test
dependsOn: function_deploy
condition: succeeded('function_deploy')
displayName: 'smoke test'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
# スモークテスト用にjobを確保
- job: swap_slot
dependsOn: smoke_test
condition: succeeded('smoke_test')
displayName: 'Swap Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-dictation-stg'
inputs:
azureSubscription: 'omds-service-connection-stg'
action: 'Swap Slots'
WebAppName: 'app-odms-dictation-stg'
ResourceGroupName: 'stg-application-rg'
SourceSlot: 'staging'
SwapWithProduction: true
- job: migration
dependsOn: swap_slot
condition: succeeded('swap_slot')
displayName: DB migration
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: 'omds-service-connection-stg'
KeyVaultName: kv-odms-secret-stg
- task: CmdLine@2
displayName: migration
inputs:
script: >2
# DB接続情報書き換え
sed -i -e "s/DB_NAME/$(db-name-ph1-enhance)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(admin-db-user)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PORT/$(db-port)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_HOST/$(db-host)/g" ./dictation_server/db/dbconfig.yml
sql-migrate --version
cat ./dictation_server/db/dbconfig.yml
# migration実行
sql-migrate up -config=./dictation_server/db/dbconfig.yml -env=ci

View File

@ -43,11 +43,14 @@ jobs:
targetType: inline
workingDirectory: dictation_server/.devcontainer
script: |
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
- job: backend_build
dependsOn: backend_test
condition: succeeded('backend_test')
@ -170,9 +173,32 @@ jobs:
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: function_build
- job: function_test
dependsOn: frontend_build_production
condition: succeeded('frontend_build_production')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_function/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci
docker-compose exec -T dictation_function sudo npm run test
- job: function_build
dependsOn: function_test
condition: succeeded('function_test')
displayName: Build And Push Function Image
pool:
name: odms-deploy-pipeline
@ -186,32 +212,6 @@ jobs:
command: ci
workingDir: dictation_function
verbose: false
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: 'omds-service-connection-stg'
KeyVaultName: kv-odms-secret-stg
SecretsFilter: '*'
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
script: |
cd dictation_function
npm run test
env:
TENANT_NAME: xxxxxxxxxxxx
SIGNIN_FLOW_NAME: xxxxxxxxxxxx
ADB2C_TENANT_ID: $(adb2c-tenant-id)
ADB2C_CLIENT_ID: $(adb2c-client-id)
ADB2C_CLIENT_SECRET: $(adb2c-client-secret)
ADB2C_ORIGIN: xxxxxx
SENDGRID_API_KEY: $(sendgrid-api-key)
MAIL_FROM: xxxxxx
APP_DOMAIN: xxxxxxxxx
REDIS_HOST: xxxxxxxxxxxx
REDIS_PORT: 0
REDIS_PASSWORD: xxxxxxxxxxxx
- task: Docker@0
displayName: build
inputs:

View File

@ -17,10 +17,6 @@ RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "$
&& apt-get install default-jre -y \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
# Update NPM
RUN npm install -g npm
# Install mob
RUN curl -sL install.mob.sh | sh

View File

@ -1,5 +1,5 @@
VITE_STAGE=production
VITE_B2C_CLIENTID=b0ec473b-6b2b-4f12-adc6-39a24ebe6a3f
VITE_B2C_AUTHORITY=https://adb2codmsprod.b2clogin.com/adb2codmsprod.onmicrosoft.com/b2c_1_signin_prod
VITE_B2C_KNOWNAUTHORITIES=adb2codmsprod.b2clogin.com
VITE_B2C_CLIENTID=ea6e2535-c914-4889-8659-7ca1ec2e420d
VITE_B2C_AUTHORITY=https://adb2codmsproduction.b2clogin.com/adb2codmsproduction.onmicrosoft.com/b2c_1_signin_production
VITE_B2C_KNOWNAUTHORITIES=adb2codmsproduction.b2clogin.com
VITE_DESK_TOP_APP_SCHEME=odms-desktopapp

View File

@ -1,5 +1,5 @@
VITE_STAGE=staging
VITE_B2C_CLIENTID=5d8f0db9-4506-41d6-a5bb-5ec39f6eba8d
VITE_B2C_AUTHORITY=https://adb2codmsstg.b2clogin.com/adb2codmsstg.onmicrosoft.com/b2c_1_signin_stg
VITE_B2C_KNOWNAUTHORITIES=adb2codmsstg.b2clogin.com
VITE_B2C_CLIENTID=6ddb8ca0-c39e-4eba-a3c1-d18ea289a315
VITE_B2C_AUTHORITY=https://adb2codmsstaging.b2clogin.com/adb2codmsstaging.onmicrosoft.com/b2c_1_signin_staging
VITE_B2C_KNOWNAUTHORITIES=adb2codmsstaging.b2clogin.com
VITE_DESK_TOP_APP_SCHEME=odms-desktopapp

View File

@ -27,6 +27,7 @@ module.exports = {
rules: {
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/require-default-props": "off",
"react/function-component-definition": [
"error",
{

View File

@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,8 @@
"typecheck": "tsc --noEmit",
"codegen": "sh codegen.sh",
"lint": "eslint --cache . --ext .js,.ts,.tsx",
"lint:fix": "npm run lint -- --fix"
"lint:fix": "npm run lint -- --fix",
"test": "jest"
},
"dependencies": {
"@azure/msal-browser": "^2.33.0",
@ -25,7 +26,6 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.1",
"@types/jest": "^27.5.2",
"@types/node": "^17.0.45",
"@types/react": "^18.0.14",
"@types/react-dom": "^18.0.6",
@ -38,6 +38,7 @@
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"luxon": "^3.3.0",
"papaparse": "^5.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-google-recaptcha-v3": "^1.10.0",
@ -56,8 +57,10 @@
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@mdx-js/react": "^2.1.2",
"@openapitools/openapi-generator-cli": "^2.5.2",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.191",
"@types/luxon": "^3.2.0",
"@types/papaparse": "^5.3.14",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/redux-mock-store": "^1.0.3",
@ -67,16 +70,18 @@
"babel-loader": "^8.2.5",
"eslint": "^8.19.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"jest": "^29.7.0",
"license-checker": "^25.0.1",
"prettier": "^2.7.1",
"prettier": "^2.8.8",
"redux-mock-store": "^1.5.4",
"sass": "^1.58.3",
"ts-jest": "^29.1.2",
"typescript": "^4.7.4",
"vite": "^4.1.4",
"vite-plugin-env-compatible": "^1.1.1",
@ -99,4 +104,4 @@
}
]
}
}
}

View File

@ -11,6 +11,13 @@ import { selectSnackber } from "features/ui/selectors";
import { closeSnackbar } from "features/ui/uiSlice";
import { UNAUTHORIZED_TO_CONTINUE_ERROR_CODES } from "components/auth/constants";
import { clearUserInfo } from "features/login";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
/*
UpdateTokenTimerをApp.tsxに移動する2024627
App.tsxに移動する
*/
const App = (): JSX.Element => {
const dispatch = useDispatch();
@ -82,6 +89,7 @@ const App = (): JSX.Element => {
/>
<BrowserRouter>
<AppRouter />
<UpdateTokenTimer />
</BrowserRouter>
</>
);

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,8 @@ import licenseCardIssue from "features/license/licenseCardIssue/licenseCardIssue
import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice";
import licenseSummary from "features/license/licenseSummary/licenseSummarySlice";
import partnerLicense from "features/license/partnerLicense/partnerLicenseSlice";
import licenseTrialIssue from "features/license/licenseTrialIssue/licenseTrialIssueSlice";
import searchPartners from "features/license/searchPartner/searchPartnerSlice";
import dictation from "features/dictation/dictationSlice";
import partner from "features/partner/partnerSlice";
import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice";
@ -35,6 +37,8 @@ export const store = configureStore({
licenseSummary,
licenseOrderHistory,
partnerLicense,
licenseTrialIssue,
searchPartners,
dictation,
partner,
typistGroup,

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M24.1,38l5.7-5.7l-5.7-5.6L22,28.8l2.1,2.1c-0.9,0-1.8-0.1-2.7-0.4c-0.9-0.3-1.7-0.9-2.4-1.6
c-0.7-0.7-1.2-1.4-1.5-2.3C17.2,25.8,17,24.9,17,24c0-0.6,0.1-1.1,0.2-1.7s0.4-1.1,0.6-1.7l-2.2-2.2c-0.6,0.8-1,1.7-1.2,2.6
C14.1,22.1,14,23,14,24c0,1.3,0.2,2.5,0.8,3.8C15.2,29,16,30.1,17,31s2,1.7,3.2,2.2c1.2,0.5,2.4,0.7,3.7,0.8L22,35.9L24.1,38z
M32.4,29.5c0.6-0.8,1-1.7,1.2-2.6C33.9,25.9,34,25,34,24c0-1.3-0.2-2.5-0.7-3.8s-1.2-2.4-2.2-3.3s-2.1-1.7-3.3-2.2
c-1.2-0.5-2.5-0.7-3.7-0.7l1.9-1.9L23.9,10l-5.7,5.7l5.7,5.6l2.1-2.1L23.8,17c0.9,0,1.8,0.2,2.8,0.5s1.7,0.9,2.4,1.5
s1.2,1.4,1.5,2.3c0.4,0.9,0.5,1.7,0.5,2.6c0,0.6-0.1,1.1-0.2,1.7c-0.1,0.6-0.4,1.1-0.6,1.6L32.4,29.5z M24,44
c-2.7,0-5.3-0.5-7.8-1.6s-4.6-2.5-6.4-4.3s-3.2-3.9-4.3-6.4S4,26.7,4,24c0-2.8,0.5-5.4,1.6-7.8s2.5-4.5,4.3-6.3s3.9-3.2,6.4-4.3
S21.3,4,24,4c2.8,0,5.4,0.5,7.8,1.6s4.6,2.5,6.4,4.3s3.2,3.9,4.3,6.3c1.1,2.4,1.6,5,1.6,7.8c0,2.7-0.5,5.3-1.6,7.8
c-1,2.4-2.5,4.6-4.3,6.4s-3.9,3.2-6.4,4.3S26.8,44,24,44z M24,41c4.7,0,8.8-1.7,12-5c3.3-3.3,5-7.3,5-12c0-4.7-1.6-8.8-5-12.1
c-3.3-3.3-7.3-5-12-5c-4.7,0-8.7,1.7-12,5S7,19.3,7,24c0,4.7,1.7,8.7,5,12C15.3,39.3,19.3,41,24,41z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 42.949219 37.109375 L 33.898438 28.0625 L 33.007812 28.949219 L 31.476562 27.414062 C 33.183594 24.882812 34.046875 21.945312 34.042969 19.011719 C 34.046875 15.171875 32.578125 11.320312 29.644531 8.390625 C 26.722656 5.464844 22.867188 4 19.019531 4.003906 C 15.179688 4 11.324219 5.46875 8.398438 8.390625 C 5.46875 11.320312 4 15.167969 4.003906 19.011719 C 4 22.851562 5.46875 26.703125 8.394531 29.628906 C 11.328125 32.554688 15.179688 34.019531 19.023438 34.019531 C 21.957031 34.019531 24.902344 33.160156 27.433594 31.453125 L 28.96875 32.988281 L 28.082031 33.871094 L 37.136719 42.917969 C 37.847656 43.636719 38.796875 43.996094 39.738281 43.996094 C 40.671875 43.996094 41.621094 43.636719 42.335938 42.917969 L 42.949219 42.308594 C 43.664062 41.59375 44.027344 40.644531 44.027344 39.707031 C 44.027344 38.769531 43.664062 37.824219 42.949219 37.109375 Z M 19.023438 32.003906 C 15.6875 32.003906 12.359375 30.738281 9.824219 28.199219 C 7.285156 25.667969 6.019531 22.34375 6.019531 19.011719 C 6.019531 15.675781 7.289062 12.351562 9.824219 9.820312 C 12.359375 7.285156 15.6875 6.019531 19.019531 6.019531 C 22.355469 6.019531 25.683594 7.285156 28.21875 9.820312 C 30.757812 12.351562 32.023438 15.675781 32.027344 19.011719 C 32.023438 22.34375 30.757812 25.667969 28.222656 28.199219 C 25.683594 30.738281 22.355469 32.003906 19.023438 32.003906 Z M 28.78125 30.421875 C 29.074219 30.171875 29.367188 29.910156 29.648438 29.628906 C 29.929688 29.351562 30.191406 29.058594 30.445312 28.761719 L 31.820312 30.136719 L 30.15625 31.800781 Z M 41.523438 40.882812 L 40.910156 41.492188 C 40.582031 41.820312 40.164062 41.976562 39.734375 41.980469 C 39.308594 41.980469 38.890625 41.820312 38.5625 41.496094 L 30.9375 33.875 L 33.898438 30.917969 L 41.523438 38.535156 C 41.847656 38.863281 42.007812 39.28125 42.007812 39.707031 C 42.007812 40.136719 41.847656 40.554688 41.523438 40.882812 Z M 41.523438 40.882812 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 25.695312 12.34375 C 23.855469 10.507812 21.433594 9.585938 19.023438 9.585938 C 16.609375 9.585938 14.191406 10.507812 12.351562 12.34375 C 10.511719 14.179688 9.589844 16.601562 9.59375 19.011719 C 9.589844 21.421875 10.511719 23.84375 12.351562 25.675781 C 14.191406 27.511719 16.609375 28.433594 19.019531 28.433594 C 21.433594 28.433594 23.855469 27.511719 25.695312 25.675781 C 27.535156 23.839844 28.453125 21.421875 28.453125 19.011719 C 28.457031 16.601562 27.53125 14.183594 25.695312 12.34375 Z M 24.503906 24.488281 C 22.992188 25.996094 21.011719 26.753906 19.019531 26.75 C 17.03125 26.75 15.050781 25.996094 13.539062 24.488281 C 12.027344 22.976562 11.277344 21 11.277344 19.011719 C 11.277344 17.023438 12.027344 15.042969 13.539062 13.53125 C 15.054688 12.023438 17.03125 11.269531 19.023438 11.265625 C 21.011719 11.269531 22.992188 12.023438 24.503906 13.53125 C 26.015625 15.042969 26.769531 17.023438 26.769531 19.011719 C 26.769531 21 26.015625 22.976562 24.503906 24.488281 Z M 24.503906 24.488281 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="_レイヤー_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 37.17"><defs><style>.cls-1{fill:#282828;}.cls-1,.cls-2{stroke-width:0px;}.cls-2{fill:#e6e6e6;}</style></defs><path class="cls-2" d="M42.13,35.07l-2.15,2.1L3,5.1v6.1H0V0h11.15v3h-6l36.98,32.07Z"/><path class="cls-1" d="M39.98,37.17l-2.1-2.15L74.9,3h-6.1V0h11.2v11.15h-3v-6l-37.03,32.02Z"/></svg>

After

Width:  |  Height:  |  Size: 409 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M450-313v-371L330-564l-43-43 193-193 193 193-43 43-120-120v371h-60ZM220-160q-24 0-42-18t-18-42v-143h60v143h520v-143h60v143q0 24-18 42t-42 18H220Z"/></svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@ -63,4 +63,26 @@ export const errorCodes = [
"E011004", // ワークタイプ使用中エラー
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
"E013002", // ワークフロー不在エラー
"E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった)
"E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった)
"E014003", // ユーザー削除エラー削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた
"E014004", // ユーザー削除エラー削除しようとしたTypistがWorkflowのTypist候補として指定されていた
"E014005", // ユーザー削除エラー削除しようとしたTypistがUserGroupに所属していた
"E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている)
"E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた)
"E014009", // ユーザー削除エラー削除しようとしたTypistが未完了のタスクのルーティングに設定されている
"E015001", // タイピストグループ削除済みエラー
"E015002", // タイピストグループがワークフローに紐づいているエラー
"E015003", // タイピストグループがルーティングされているエラー
"E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった)
"E016002", // テンプレートファイル削除エラー削除しようとしたテンプレートファイルがWorkflowに指定されていた
"E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた)
"E017001", // 親アカウント変更不可エラー(指定したアカウントが存在しない)
"E017002", // 親アカウント変更不可エラー(階層関係が不正)
"E017003", // 親アカウント変更不可エラー(リージョンが同一でない)
"E018001", // パートナーアカウント削除エラー(削除条件を満たしていない)
"E019001", // パートナーアカウント取得不可エラー(階層構造が不正)
"E020001", // パートナーアカウント変更エラー(変更条件を満たしていない)
"E021001", // 音声ファイル名変更不可エラー(権限不足)
"E021002", // 音声ファイル名変更不可エラー(同名ファイルが存在)
] as const;

View File

@ -6,4 +6,4 @@ export type ErrorObject = {
statusCode?: number;
};
export type ErrorCodeType = typeof errorCodes[number];
export type ErrorCodeType = (typeof errorCodes)[number];

View File

@ -0,0 +1,153 @@
/* eslint-disable @typescript-eslint/naming-convention */
// Jestによるparser.tsのテスト
import fs from "fs";
import { CSVType, parseCSV } from "./parser";
describe("parse", () => {
it("指定形式のCSV文字列をパースできる", async () => {
const text = fs.readFileSync("src/common/test/test_001.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: 1,
author_id: "HOGE",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd",
prompt: 0,
},
];
expect(actualData).toEqual(expectData);
});
it("指定形式のヘッダでない場合、例外が送出される | author_id(値がoptionial)がない", async () => {
const text = fs.readFileSync("src/common/test/test_002.csv", "utf-8");
try {
await parseCSV(text);
fail("例外が発生しませんでした");
} catch (e) {
expect(e).toEqual(new Error("Invalid CSV format"));
}
});
it("指定形式のヘッダでない場合、例外が送出される | email(値が必須)がない", async () => {
const text = fs.readFileSync("src/common/test/test_003.csv", "utf-8");
try {
await parseCSV(text);
fail("例外が発生しませんでした");
} catch (e) {
expect(e).toEqual(new Error("Invalid CSV format"));
}
});
it("指定形式のヘッダでない場合、例外が送出される | emailがスペルミス", async () => {
const text = fs.readFileSync("src/common/test/test_004.csv", "utf-8");
try {
await parseCSV(text);
fail("例外が発生しませんでした");
} catch (e) {
expect(e).toEqual(new Error("Invalid CSV format"));
}
});
it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(文字列)はnullとなる", async () => {
const text = fs.readFileSync("src/common/test/test_005.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: 1,
author_id: null,
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd",
prompt: 0,
},
];
expect(actualData).toEqual(expectData);
});
it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(数値)はnullとなる", async () => {
const text = fs.readFileSync("src/common/test/test_006.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: null,
author_id: "HOGE",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd",
prompt: 0,
},
];
expect(actualData).toEqual(expectData);
});
it("指定形式のCSV文字列をパースできる | 余計なパラメータがあっても問題はない", async () => {
const text = fs.readFileSync("src/common/test/test_007.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: 1,
author_id: "HOGE",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd",
prompt: 0,
},
{
name: "hoge2",
email: "sample2@example.com",
role: 1,
author_id: "HOGE2",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd2",
prompt: 0,
},
];
expect(actualData.length).toBe(expectData.length);
// 余計なパラメータ格納用に __parsed_extra: string[] というプロパティが作られてしまうので、既知のプロパティ毎に比較
for (let i = 0; i < actualData.length; i += 1) {
const actualValue = actualData[i];
const expectValue = expectData[i];
expect(actualValue.author_id).toEqual(expectValue.author_id);
expect(actualValue.auto_assign).toEqual(expectValue.auto_assign);
expect(actualValue.email).toEqual(expectValue.email);
expect(actualValue.encryption).toEqual(expectValue.encryption);
expect(actualValue.encryption_password).toEqual(
expectValue.encryption_password
);
expect(actualValue.name).toEqual(expectValue.name);
expect(actualValue.notification).toEqual(expectValue.notification);
expect(actualValue.prompt).toEqual(expectValue.prompt);
expect(actualValue.role).toEqual(expectValue.role);
}
});
it("author_id,encryption_passwordが数値のみの場合でも、文字列として変換できる", async () => {
const text = fs.readFileSync("src/common/test/test_008.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: 1,
author_id: "1111",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "222222",
prompt: 0,
},
];
expect(actualData).toEqual(expectData);
});
});

View File

@ -0,0 +1,74 @@
/* eslint-disable @typescript-eslint/naming-convention */
import Papa, { ParseResult } from "papaparse";
export type CSVType = {
name: string | null;
email: string | null;
role: number | null;
author_id: string | null;
auto_assign: number | null;
notification: number;
encryption: number | null;
encryption_password: string | null;
prompt: number | null;
};
// CSVTypeのプロパティ名を文字列の配列で定義する
const CSVTypeFields: (keyof CSVType)[] = [
"name",
"email",
"role",
"author_id",
"auto_assign",
"notification",
"encryption",
"encryption_password",
"prompt",
];
// 2つの配列が等しいかどうかを判定する
const equals = (lhs: string[], rhs: string[]) => {
if (lhs.length !== rhs.length) return false;
for (let i = 0; i < lhs.length; i += 1) {
if (lhs[i] !== rhs[i]) return false;
}
return true;
};
/** CSVファイルをCSVType型に変換するパーサー */
export const parseCSV = async (csvString: string): Promise<CSVType[]> =>
new Promise((resolve, reject) => {
Papa.parse<CSVType>(csvString, {
download: false,
worker: false, // XXX: workerを使うとエラーが発生するためfalseに設定
header: true,
dynamicTyping: {
// author_id, encryption_passwordは数値のみの場合、numberに変換されたくないためdynamicTypingをtrueにしない
role: true,
auto_assign: true,
notification: true,
encryption: true,
prompt: true,
},
// dynamicTypingがfalseの場合、空文字をnullに変換できないためtransformを使用する
transform: (value, field) => {
if (field === "author_id" || field === "encryption_password") {
// 空文字の場合はnullに変換する
if (value === "") {
return null;
}
}
return value;
},
complete: (results: ParseResult<CSVType>) => {
// ヘッダーがCSVTypeFieldsと一致しない場合はエラーを返す
if (!equals(results.meta.fields ?? [], CSVTypeFields)) {
reject(new Error("Invalid CSV format"));
}
resolve(results.data);
},
error: (error: Error) => {
reject(error);
},
});
});

View File

@ -0,0 +1,2 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
1 name email role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com 1 HOGE 1 1 1 abcd 0

View File

@ -0,0 +1,2 @@
name,email,role,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,2 @@
name,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,2 @@
name,emeil,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
1 name emeil role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com 1 HOGE 1 1 1 abcd 0

View File

@ -0,0 +1,2 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,,1,1,1,abcd,0
1 name email role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com 1 1 1 1 abcd 0

View File

@ -0,0 +1,2 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,,"HOGE",1,1,1,abcd,0
1 name email role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com HOGE 1 1 1 abcd 0

View File

@ -0,0 +1,3 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0,x
hoge2,sample2@example.com,1,"HOGE2",1,1,1,abcd2,0,1,32,4,aa
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,2 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,1111,1,1,1,222222,0
1 name email role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com 1 1111 1 1 1 222222 0

View File

@ -47,6 +47,7 @@ export const KEYS_TO_PRESERVE = [
"accessToken",
"refreshToken",
"displayInfo",
"filterCriteria",
"sortCriteria",
];

View File

@ -1,4 +1,4 @@
import React, { useCallback } from "react";
import React, { useCallback, useLayoutEffect, useState } from "react";
import { AppDispatch } from "app/store";
import { decodeToken } from "common/decodeToken";
import { useInterval } from "common/useInterval";
@ -17,41 +17,58 @@ import { TOKEN_UPDATE_INTERVAL_MS, TOKEN_UPDATE_TIME } from "./constants";
export const UpdateTokenTimer = () => {
const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate();
// トークンの更新中かどうか
const [isUpdating, setIsUpdating] = useState(false);
const delegattionToken = useSelector(selectDelegationAccessToken);
// 期限が分以内であれば更新APIを呼ぶ
const updateToken = useCallback(async () => {
// localStorageからトークンを取得
const jwt = loadAccessToken();
// 現在時刻を取得
const now = DateTime.local().toSeconds();
// selectorに以下の判定処理を移したかったが、初期表示時の値でしか判定できないのでComponent内に置く
if (jwt) {
const token = decodeToken(jwt);
if (token) {
const { exp } = token;
if (exp - now <= TOKEN_UPDATE_TIME) {
await dispatch(updateTokenAsync());
}
}
if (isUpdating) {
return;
}
// 代行操作トークン更新処理
if (delegattionToken) {
const token = decodeToken(delegattionToken);
if (token) {
const { exp } = token;
if (exp - now <= TOKEN_UPDATE_TIME) {
const { meta } = await dispatch(updateDelegationTokenAsync());
if (meta.requestStatus === "rejected") {
dispatch(cleanupDelegateAccount());
navigate("/partners");
setIsUpdating(true);
try {
// localStorageからトークンを取得
const jwt = loadAccessToken();
// 現在時刻を取得
const now = DateTime.local().toSeconds();
// selectorに以下の判定処理を移したかったが、初期表示時の値でしか判定できないのでComponent内に置く
if (jwt) {
const token = decodeToken(jwt);
if (token) {
const { exp } = token;
if (exp - now <= TOKEN_UPDATE_TIME) {
await dispatch(updateTokenAsync());
}
}
}
// 代行操作トークン更新処理
if (delegattionToken) {
const token = decodeToken(delegattionToken);
if (token) {
const { exp } = token;
if (exp - now <= TOKEN_UPDATE_TIME) {
const { meta } = await dispatch(updateDelegationTokenAsync());
if (meta.requestStatus === "rejected") {
dispatch(cleanupDelegateAccount());
navigate("/partners");
}
}
}
}
} catch (e) {
// eslint-disable-next-line no-console
console.error("Token update error:", e);
} finally {
setIsUpdating(false);
}
}, [dispatch, delegattionToken, navigate]);
}, [isUpdating, delegattionToken, dispatch, navigate]);
useLayoutEffect(() => {
updateToken();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useInterval(updateToken, TOKEN_UPDATE_INTERVAL_MS);

View File

@ -6,8 +6,50 @@ import { getTranslationID } from "translation";
const Footer: React.FC = () => {
const [t] = useTranslation();
return (
<footer className={`${styles.footer}`}>
<div>{t(getTranslationID("common.label.copyRight"))}</div>
<footer
className={`${styles.footer}`}
style={{
padding: "0.5rem",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div
style={{
marginBottom: "0.5rem",
textAlign: "center",
}}
>
{t(getTranslationID("common.label.copyRight"))}
</div>
<div
style={{
display: "flex",
gap: "1rem",
}}
>
<a
href="https://download.omsystem.com/pages/odms_download/odms_cloud_eula/eula/en/"
target="_blank"
className={styles.linkTx}
style={{ color: "#999999" }}
data-tag="open-eula"
rel="noreferrer"
>
{t(getTranslationID("common.label.eula"))}
</a>
<a
href="https://download.omsystem.com/pages/odms_download/odms_cloud_eula/privacy_notice/en/"
target="_blank"
className={styles.linkTx}
style={{ color: "#999999" }}
data-tag="open-privacy-notice"
rel="noreferrer"
>
{t(getTranslationID("common.label.privacyNotice"))}
</a>
</div>
</footer>
);
};

View File

@ -4,6 +4,7 @@ import {
updateAccountInfoAsync,
getAccountRelationsAsync,
deleteAccountAsync,
updateFileDeleteSettingAsync,
} from "./operations";
const initialState: AccountState = {
@ -15,6 +16,8 @@ const initialState: AccountState = {
tier: 0,
country: "",
delegationPermission: false,
autoFileDelete: false,
fileRetentionDays: 0,
},
},
dealers: [],
@ -29,6 +32,8 @@ const initialState: AccountState = {
secondryAdminUserId: undefined,
},
isLoading: false,
autoFileDelete: false,
fileRetentionDays: 0,
},
};
@ -64,6 +69,20 @@ export const accountSlice = createSlice({
const { secondryAdminUserId } = action.payload;
state.apps.updateAccountInfo.secondryAdminUserId = secondryAdminUserId;
},
changeAutoFileDelete: (
state,
action: PayloadAction<{ autoFileDelete: boolean }>
) => {
const { autoFileDelete } = action.payload;
state.apps.autoFileDelete = autoFileDelete;
},
changeFileRetentionDays: (
state,
action: PayloadAction<{ fileRetentionDays: number }>
) => {
const { fileRetentionDays } = action.payload;
state.apps.fileRetentionDays = fileRetentionDays;
},
cleanupApps: (state) => {
state.domain = initialState.domain;
},
@ -85,6 +104,10 @@ export const accountSlice = createSlice({
action.payload.accountInfo.account.primaryAdminUserId;
state.apps.updateAccountInfo.secondryAdminUserId =
action.payload.accountInfo.account.secondryAdminUserId;
state.apps.autoFileDelete =
action.payload.accountInfo.account.autoFileDelete;
state.apps.fileRetentionDays =
action.payload.accountInfo.account.fileRetentionDays;
state.apps.isLoading = false;
});
builder.addCase(getAccountRelationsAsync.rejected, (state) => {
@ -99,6 +122,15 @@ export const accountSlice = createSlice({
builder.addCase(updateAccountInfoAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateFileDeleteSettingAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateFileDeleteSettingAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateFileDeleteSettingAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteAccountAsync.pending, (state) => {
state.apps.isLoading = true;
});
@ -115,6 +147,8 @@ export const {
changeDealerPermission,
changePrimaryAdministrator,
changeSecondryAdministrator,
changeAutoFileDelete,
changeFileRetentionDays,
cleanupApps,
} = accountSlice.actions;
export default accountSlice.reducer;

View File

@ -9,6 +9,7 @@ import {
UpdateAccountInfoRequest,
UsersApi,
DeleteAccountRequest,
UpdateFileDeleteSettingRequest,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ViewAccountRelationsInfo } from "./types";
@ -38,7 +39,7 @@ export const getAccountRelationsAsync = createAsyncThunk<
headers: { authorization: `Bearer ${accessToken}` },
});
const dealers = await accountsApi.getDealers();
const users = await usersApi.getUsers({
const users = await usersApi.getUsers(undefined, undefined, {
headers: { authorization: `Bearer ${accessToken}` },
});
return {
@ -112,6 +113,58 @@ export const updateAccountInfoAsync = createAsyncThunk<
}
});
export const updateFileDeleteSettingAsync = createAsyncThunk<
{
/* Empty Object */
},
{ autoFileDelete: boolean; fileRetentionDays: number },
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accounts/updateFileDeleteSettingAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
const requestParam: UpdateFileDeleteSettingRequest = {
autoFileDelete: args.autoFileDelete,
retentionDays: args.fileRetentionDays,
};
try {
await accountApi.updateFileDeleteSetting(requestParam, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const deleteAccountAsync = createAsyncThunk<
{
/* Empty Object */

View File

@ -16,3 +16,18 @@ export const selectIsLoading = (state: RootState) =>
state.account.apps.isLoading;
export const selectUpdateAccountInfo = (state: RootState) =>
state.account.apps.updateAccountInfo;
export const selectFileDeleteSetting = (state: RootState) => {
const { autoFileDelete, fileRetentionDays } = state.account.apps;
return {
autoFileDelete,
fileRetentionDays,
};
};
export const selectInputValidationErrors = (state: RootState) => {
const { fileRetentionDays } = state.account.apps;
const hasFileRetentionDaysError =
fileRetentionDays <= 0 || fileRetentionDays >= 1000;
return {
hasFileRetentionDaysError,
};
};

View File

@ -19,4 +19,6 @@ export interface Domain {
export interface Apps {
updateAccountInfo: UpdateAccountInfoRequest;
isLoading: boolean;
autoFileDelete: boolean;
fileRetentionDays: number;
}

View File

@ -6,7 +6,7 @@ export const STATUS = {
BACKUP: "Backup",
} as const;
export type StatusType = typeof STATUS[keyof typeof STATUS];
export type StatusType = (typeof STATUS)[keyof typeof STATUS];
export const LIMIT_TASK_NUM = 100;
@ -26,7 +26,7 @@ export const SORTABLE_COLUMN = {
TranscriptionFinishedDate: "TRANSCRIPTION_FINISHED_DATE",
} as const;
export type SortableColumnType =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
(typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN];
export const isSortableColumnType = (
value: string
@ -36,14 +36,14 @@ export const isSortableColumnType = (
};
export type SortableColumnList =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
(typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN];
export const DIRECTION = {
ASC: "ASC",
DESC: "DESC",
} as const;
export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION];
export type DirectionType = (typeof DIRECTION)[keyof typeof DIRECTION];
// DirectionTypeの型チェック関数
export const isDirectionType = (arg: string): arg is DirectionType =>

View File

@ -11,6 +11,8 @@ import {
playbackAsync,
updateAssigneeAsync,
cancelAsync,
deleteTaskAsync,
renameFileAsync,
} from "./operations";
import {
SORTABLE_COLUMN,
@ -41,6 +43,8 @@ const initialState: DictationState = {
direction: DIRECTION.ASC,
paramName: SORTABLE_COLUMN.JobNumber,
selectedTask: undefined,
authorId: "",
fileName: "",
assignee: {
selected: [],
pool: [],
@ -76,6 +80,14 @@ export const dictationSlice = createSlice({
const { paramName } = action.payload;
state.apps.paramName = paramName;
},
changeAuthorId: (state, action: PayloadAction<{ authorId: string }>) => {
const { authorId } = action.payload;
state.apps.authorId = authorId;
},
changeFileName: (state, action: PayloadAction<{ fileName: string }>) => {
const { fileName } = action.payload;
state.apps.fileName = fileName;
},
changeSelectedTask: (state, action: PayloadAction<{ task: Task }>) => {
const { task } = action.payload;
state.apps.selectedTask = task;
@ -218,6 +230,25 @@ export const dictationSlice = createSlice({
builder.addCase(backupTasksAsync.rejected, (state) => {
state.apps.isDownloading = false;
});
builder.addCase(deleteTaskAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deleteTaskAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteTaskAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(renameFileAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(renameFileAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(renameFileAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
@ -225,6 +256,8 @@ export const {
changeDisplayInfo,
changeDirection,
changeParamName,
changeAuthorId,
changeFileName,
changeSelectedTask,
changeAssignee,
changeBackupTaskChecked,

View File

@ -35,6 +35,8 @@ export const listTasksAsync = createAsyncThunk<
filter?: string;
direction: DirectionType;
paramName: SortableColumnType;
authorId?: string;
fileName?: string;
},
{
// rejectした時の返却値の型
@ -43,7 +45,8 @@ export const listTasksAsync = createAsyncThunk<
};
}
>("dictations/listTasksAsync", async (args, thunkApi) => {
const { limit, offset, filter, direction, paramName } = args;
const { limit, offset, filter, direction, paramName, authorId, fileName } =
args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
@ -60,6 +63,8 @@ export const listTasksAsync = createAsyncThunk<
filter,
direction,
paramName,
authorId,
fileName,
{
headers: { authorization: `Bearer ${accessToken}` },
}
@ -80,6 +85,136 @@ export const listTasksAsync = createAsyncThunk<
}
});
export const getTaskFiltersAsync = createAsyncThunk<
{
authorId?: string;
fileName?: string;
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/getTaskFiltersAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
const usertaskfilter = await usersApi.getTaskFilter({
headers: { authorization: `Bearer ${accessToken}` },
});
const { authorId, fileName } = usertaskfilter.data;
return { authorId, fileName };
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateTaskFiltersAsync = createAsyncThunk<
{
/** empty */
},
{
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/updateTaskFiltersAsync", async (args, thunkApi) => {
const { filterConditionAuthorId, filterConditionFileName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
return await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateSortColumnAsync = createAsyncThunk<
{
/** empty */
},
{
direction: DirectionType;
paramName: SortableColumnType;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/updateSortColumnAsync", async (args, thunkApi) => {
const { direction, paramName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
return await usersApi.updateSortCriteria(
{ direction, paramName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getSortColumnAsync = createAsyncThunk<
{
direction: DirectionType;
@ -280,6 +415,8 @@ export const playbackAsync = createAsyncThunk<
direction: DirectionType;
paramName: SortableColumnType;
audioFileId: number;
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
@ -288,7 +425,13 @@ export const playbackAsync = createAsyncThunk<
};
}
>("dictations/playbackAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName } = args;
const {
audioFileId,
direction,
paramName,
filterConditionAuthorId,
filterConditionFileName,
} = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
@ -305,6 +448,12 @@ export const playbackAsync = createAsyncThunk<
headers: { authorization: `Bearer ${accessToken}` },
}
);
await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await tasksApi.checkout(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
@ -387,6 +536,8 @@ export const cancelAsync = createAsyncThunk<
paramName: SortableColumnType;
audioFileId: number;
isTypist: boolean;
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
@ -395,7 +546,14 @@ export const cancelAsync = createAsyncThunk<
};
}
>("dictations/cancelAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName, isTypist } = args;
const {
audioFileId,
direction,
paramName,
isTypist,
filterConditionAuthorId,
filterConditionFileName,
} = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
@ -406,15 +564,25 @@ export const cancelAsync = createAsyncThunk<
const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config);
try {
// ユーザーがタイピストである場合に、ソート条件を保存する
// ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
if (isTypist) {
await usersApi.updateSortCriteria(
{ direction, paramName },
{
direction,
paramName,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
}
await tasksApi.cancel(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
@ -450,6 +618,93 @@ export const cancelAsync = createAsyncThunk<
}
});
export const reopenAsync = createAsyncThunk<
{
/** empty */
},
{
direction: DirectionType;
paramName: SortableColumnType;
audioFileId: number;
isTypist: boolean;
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/reopenAsync", async (args, thunkApi) => {
const {
audioFileId,
direction,
paramName,
isTypist,
filterConditionAuthorId,
filterConditionFileName,
} = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config);
try {
// ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
if (isTypist) {
await usersApi.updateSortCriteria(
{ direction, paramName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
}
await tasksApi.reopen(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// ステータスが[Finished]以外、またはタスクが存在しない場合、またはtypistで自分のタスクでない場合
if (error.code === "E010601" || error.code === "E010603") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("dictationPage.message.reopenFailedError"),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const listBackupPopupTasksAsync = createAsyncThunk<
TasksResponse,
{
@ -480,6 +735,8 @@ export const listBackupPopupTasksAsync = createAsyncThunk<
BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ
DIRECTION.DESC,
SORTABLE_COLUMN.Status,
undefined, // backupポップアップ表示時には検索条件は未指定
undefined, // backupポップアップ表示時には検索条件は未指定
{
headers: { authorization: `Bearer ${accessToken}` },
}
@ -565,10 +822,21 @@ export const backupTasksAsync = createAsyncThunk<
a.click();
a.parentNode?.removeChild(a);
// eslint-disable-next-line no-await-in-loop
await tasksApi.backup(task.audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
// バックアップ済みに更新
try {
// eslint-disable-next-line no-await-in-loop
await tasksApi.backup(task.audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
if (error.code === "E010603") {
// タスクが削除済みの場合は成功扱いとする
} else {
throw e;
}
}
}
}
@ -580,8 +848,22 @@ export const backupTasksAsync = createAsyncThunk<
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
if (error.code === "E010603") {
// 存在しない音声ファイルをダウンロードしようとした場合
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.fileAlreadyDeletedError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
@ -592,3 +874,143 @@ export const backupTasksAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const deleteTaskAsync = createAsyncThunk<
{
// empty
},
{
// パラメータ
audioFileId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/deleteTaskAsync", async (args, thunkApi) => {
const { audioFileId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
try {
await tasksApi.deleteTask(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
let message = getTranslationID("common.message.internalServerError");
if (error.statusCode === 400) {
if (error.code === "E010603") {
// タスクが削除済みの場合は成功扱いとする
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
if (error.code === "E010601") {
// タスクがInprogressの場合はエラー
message = getTranslationID("dictationPage.message.deleteFailedError");
}
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const renameFileAsync = createAsyncThunk<
{
// empty
},
{
// パラメータ
audioFileId: number;
fileName: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/renameFileAsync", async (args, thunkApi) => {
const { audioFileId, fileName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const filesApi = new FilesApi(config);
try {
await filesApi.fileRename(
{ fileName, audioFileId },
{ headers: { authorization: `Bearer ${accessToken}` } }
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
let message = getTranslationID("common.message.internalServerError");
// 変更権限がない場合はエラー
if (error.code === "E021001") {
message = getTranslationID("dictationPage.message.fileRenameFailedError");
}
// ファイル名が既に存在する場合はエラー
if (error.code === "E021002") {
message = getTranslationID(
"dictationPage.message.fileNameAleadyExistsError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -72,6 +72,12 @@ export const selectDirection = (state: RootState) =>
export const selectParamName = (state: RootState) =>
state.dictation.apps.paramName;
export const selectAuthorId = (state: RootState) =>
state.dictation.apps.authorId;
export const selectFilename = (state: RootState) =>
state.dictation.apps.fileName;
export const selectSelectedTask = (state: RootState) =>
state.dictation.apps.selectedTask;

View File

@ -25,6 +25,8 @@ export interface Apps {
displayInfo: DisplayInfoType;
direction: DirectionType;
paramName: SortableColumnType;
authorId: string;
fileName: string;
selectedTask?: Task;
selectedFileTask?: Task;
assignee: {

View File

@ -1,5 +1,6 @@
import { createSlice } from "@reduxjs/toolkit";
import { LicenseCardActivateState } from "./state";
import { activateCardLicenseAsync } from "./operations";
const initialState: LicenseCardActivateState = {
apps: {
@ -14,6 +15,17 @@ export const licenseCardActivateSlice = createSlice({
state.apps = initialState.apps;
},
},
extraReducers: (builder) => {
builder.addCase(activateCardLicenseAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(activateCardLicenseAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(activateCardLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const { cleanupApps } = licenseCardActivateSlice.actions;

View File

@ -7,3 +7,8 @@ export const STATUS = {
// eslint-disable-next-line @typescript-eslint/naming-convention
ORDER_CANCELED: "Order Canceled",
} as const;
export const LICENSE_TYPE = {
NORMAL: "NORMAL",
TRIAL: "TRIAL",
} as const;

View File

@ -3,7 +3,12 @@ import type { RootState } from "app/store";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { getAccessToken } from "features/auth";
import { AccountsApi, LicensesApi } from "../../../api/api";
import {
AccountsApi,
LicensesApi,
SearchPartner,
PartnerLicenseInfo,
} from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
import { OrderHistoryView } from "./types";
@ -15,6 +20,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
// パラメータ
limit: number;
offset: number;
selectedRow?: PartnerLicenseInfo | SearchPartner;
},
{
// rejectした時の返却値の型
@ -23,7 +29,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
};
}
>("licenses/licenseOrderHisotyAsync", async (args, thunkApi) => {
const { limit, offset } = args;
const { limit, offset, selectedRow } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
@ -33,7 +39,6 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
const accountsApi = new AccountsApi(config);
try {
const { selectedRow } = state.partnerLicense.apps;
let accountId = 0;
let companyName = "";
// 他の画面から指定されていない場合はログインアカウントのidを取得する
@ -46,7 +51,9 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
companyName = getMyAccountResponse.data.account.companyName;
} else {
accountId = selectedRow.accountId;
companyName = selectedRow.companyName;
// パートナーライセンスとパートナー検索で型が異なるため、型ガードで推論させる
if ("companyName" in selectedRow) companyName = selectedRow.companyName;
if ("name" in selectedRow) companyName = selectedRow.name;
}
const res = await accountsApi.getOrderHistories(

View File

@ -1,6 +1,10 @@
import { createSlice } from "@reduxjs/toolkit";
import { LicenseSummaryState } from "./state";
import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations";
import {
getCompanyNameAsync,
getLicenseSummaryAsync,
updateRestrictionStatusAsync,
} from "./operations";
const initialState: LicenseSummaryState = {
domain: {
@ -35,12 +39,30 @@ export const licenseSummarySlice = createSlice({
},
},
extraReducers: (builder) => {
builder.addCase(getLicenseSummaryAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => {
state.domain.licenseSummaryInfo = action.payload;
state.apps.isLoading = false;
});
builder.addCase(getLicenseSummaryAsync.rejected, (state) => {
state.apps.isLoading = false;
});
// 画面側ではgetLicenseSummaryAsyncと並行して呼び出されているため、レーシングを考慮してこちらではisLoadingを更新しない
// 本来は両方の完了を待ってからisLoadingを更新するべきだが、現時点ではスピード重視のためケアしない。
builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => {
state.domain.accountInfo.companyName = action.payload.companyName;
});
builder.addCase(updateRestrictionStatusAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateRestrictionStatusAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateRestrictionStatusAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});

View File

@ -7,7 +7,9 @@ import {
AccountsApi,
GetCompanyNameResponse,
GetLicenseSummaryResponse,
SearchPartner,
PartnerLicenseInfo,
UpdateRestrictionStatusRequest,
} from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
@ -16,7 +18,7 @@ export const getLicenseSummaryAsync = createAsyncThunk<
// 正常時の戻り値の型
GetLicenseSummaryResponse,
// 引数
{ selectedRow?: PartnerLicenseInfo },
{ selectedRow?: PartnerLicenseInfo | SearchPartner },
{
// rejectした時の返却値の型
rejectValue: {
@ -72,7 +74,7 @@ export const getCompanyNameAsync = createAsyncThunk<
// 正常時の戻り値の型
GetCompanyNameResponse,
// 引数
{ selectedRow?: PartnerLicenseInfo },
{ selectedRow?: PartnerLicenseInfo | SearchPartner },
{
// rejectした時の返却値の型
rejectValue: {
@ -123,3 +125,58 @@ export const getCompanyNameAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const updateRestrictionStatusAsync = createAsyncThunk<
{
/* Empty Object */
},
{
accountId: number;
restricted: boolean;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accounts/updateRestrictionStatusAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
const requestParam: UpdateRestrictionStatusRequest = {
accountId: args.accountId,
restricted: args.restricted,
};
try {
await accountApi.updateRestrictionStatus(requestParam, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
// このAPIでは個別のエラーメッセージは不要
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,10 +1,11 @@
import { RootState } from "app/store";
// 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する
export const selecLicenseSummaryInfo = (state: RootState) =>
export const selectLicenseSummaryInfo = (state: RootState) =>
state.licenseSummary.domain.licenseSummaryInfo;
export const selectCompanyName = (state: RootState) =>
state.licenseSummary.domain.accountInfo.companyName;
export const selectIsLoading = (state: RootState) => state.license;
export const selectIsLoading = (state: RootState) =>
state.licenseSummary.apps.isLoading;

View File

@ -0,0 +1,2 @@
export const ISSUED_TRIAL_LICENSE_QUANTITY = 10;
export const TRIAL_LICENSE_EXPIRATION_DAY = 30;

View File

@ -0,0 +1,5 @@
export * from "./state";
export * from "./operations";
export * from "./selectors";
export * from "./licenseTrialIssueSlice";
export * from "./constants";

View File

@ -0,0 +1,60 @@
import { createSlice } from "@reduxjs/toolkit";
import { convertLocalToUTCDate } from "common/convertLocalToUTCDate";
import { LicenseTrialIssueState } from "./state";
import { issueTrialLicenseAsync } from "./operations";
import {
TRIAL_LICENSE_EXPIRATION_DAY,
ISSUED_TRIAL_LICENSE_QUANTITY,
} from "./constants";
const initialState: LicenseTrialIssueState = {
apps: {
isLoading: false,
expirationDate: "",
quantity: ISSUED_TRIAL_LICENSE_QUANTITY,
},
};
export const licenseTrialIssueSlice = createSlice({
name: "licenseTrialIssue",
initialState,
reducers: {
cleanupApps: (state) => {
state.apps = initialState.apps;
},
setExpirationDate: (state) => {
// 有効期限を設定
const currentDate = new Date();
const expiryDate = new Date();
expiryDate.setDate(currentDate.getDate() + TRIAL_LICENSE_EXPIRATION_DAY);
// タイムゾーンオフセットを考慮して、ローカルタイムでの日付を取得
const expirationDateLocal = convertLocalToUTCDate(expiryDate);
const expirationDateWithoutTime = new Date(
expirationDateLocal.getFullYear(),
expirationDateLocal.getMonth(),
expirationDateLocal.getDate()
);
const expirationYear = expirationDateWithoutTime.getFullYear();
const expirationMonth = expirationDateWithoutTime.getMonth() + 1; // getMonth() の結果は0から始まるため、1を足して実際の月に合わせる
const expirationDay = expirationDateWithoutTime.getDate();
const formattedExpirationDate = `${expirationYear}/${expirationMonth}/${expirationDay} (${TRIAL_LICENSE_EXPIRATION_DAY})`;
state.apps.expirationDate = formattedExpirationDate;
},
},
extraReducers: (builder) => {
builder.addCase(issueTrialLicenseAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(issueTrialLicenseAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(issueTrialLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const { cleanupApps, setExpirationDate } =
licenseTrialIssueSlice.actions;
export default licenseTrialIssueSlice.reducer;

View File

@ -0,0 +1,84 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { getAccessToken } from "features/auth";
import {
LicensesApi,
SearchPartner,
PartnerLicenseInfo,
} from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const issueTrialLicenseAsync = createAsyncThunk<
{
/* Empty Object */
},
{
selectedRow?: PartnerLicenseInfo | SearchPartner;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/issueTrialLicenseAsync", async (args, thunkApi) => {
const { selectedRow } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const licensesApi = new LicensesApi(config);
try {
if (!selectedRow) {
// アカウントが選択されていない場合はエラーとする。
const errorMessage = getTranslationID(
"trialLicenseIssuePopupPage.message.accountNotSelected"
);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return {};
}
// トライアルライセンス発行処理を実行
await licensesApi.issueTrialLicenses(
{
issuedAccount: selectedRow.accountId,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,10 @@
import { RootState } from "app/store";
export const selectIsLoading = (state: RootState) =>
state.licenseTrialIssue.apps.isLoading;
export const selectExpirationDate = (state: RootState) =>
state.licenseTrialIssue.apps.expirationDate;
export const selectNumberOfLicenses = (state: RootState) =>
state.licenseTrialIssue.apps.quantity;

View File

@ -0,0 +1,9 @@
export interface LicenseTrialIssueState {
apps: Apps;
}
export interface Apps {
isLoading: boolean;
expirationDate: string;
quantity: number;
}

View File

@ -105,3 +105,82 @@ export const getPartnerLicenseAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const switchParentAsync = createAsyncThunk<
{
/* Empty Object */
},
{
// パラメータ
to: number;
children: number[];
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accounts/switchParentAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
const { to, children } = args;
try {
await accountsApi.switchParent(
{
to,
children,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
let errorMessage = getTranslationID("common.message.internalServerError");
// TODO:エラー処理
if (error.code === "E017001") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.accountNotFoundError"
);
}
if (error.code === "E017002") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.hierarchyMismatchError"
);
}
if (error.code === "E017003") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.regionMismatchError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,7 +1,11 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { PartnerLicenseInfo } from "api";
import { PartnerLicensesState, HierarchicalElement } from "./state";
import { getMyAccountAsync, getPartnerLicenseAsync } from "./operations";
import {
getMyAccountAsync,
getPartnerLicenseAsync,
switchParentAsync,
} from "./operations";
import { ACCOUNTS_VIEW_LIMIT } from "./constants";
const initialState: PartnerLicensesState = {
@ -12,6 +16,8 @@ const initialState: PartnerLicensesState = {
tier: 0,
country: "",
delegationPermission: false,
autoFileDelete: false,
fileRetentionDays: 0,
},
total: 0,
ownPartnerLicense: {
@ -19,6 +25,7 @@ const initialState: PartnerLicensesState = {
tier: 0,
companyName: "",
stockLicense: 0,
allocatedLicense: 0,
issuedRequested: 0,
shortage: 0,
issueRequesting: 0,
@ -33,6 +40,9 @@ const initialState: PartnerLicensesState = {
hierarchicalElements: [],
isLoading: true,
selectedRow: undefined,
isLicenseOrderHistoryOpen: false,
isViewDetailsOpen: false,
isSearchPopupOpen: false,
},
};
@ -82,6 +92,24 @@ export const partnerLicenseSlice = createSlice({
state.apps.limit = limit;
state.apps.offset = offset;
},
setIsLicenseOrderHistoryOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isLicenseOrderHistoryOpen = action.payload.value;
},
setIsViewDetailsOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isViewDetailsOpen = action.payload.value;
},
setIsSearchPopupOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isSearchPopupOpen = action.payload.value;
},
},
extraReducers: (builder) => {
builder.addCase(getMyAccountAsync.pending, (state) => {
@ -107,6 +135,15 @@ export const partnerLicenseSlice = createSlice({
builder.addCase(getPartnerLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(switchParentAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(switchParentAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(switchParentAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {
@ -116,6 +153,9 @@ export const {
clearHierarchicalElement,
changeSelectedRow,
savePageInfo,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
setIsSearchPopupOpen,
} = partnerLicenseSlice.actions;
export default partnerLicenseSlice.reducer;

View File

@ -30,3 +30,10 @@ export const selectCurrentPage = (state: RootState) => {
};
export const selectSelectedRow = (state: RootState) =>
state.partnerLicense.apps.selectedRow;
export const selectIsLicenseOrderHistoryOpen = (state: RootState) =>
state.partnerLicense.apps.isLicenseOrderHistoryOpen;
export const selectIsViewDetailsOpen = (state: RootState) =>
state.partnerLicense.apps.isViewDetailsOpen;
export const selectIsSearchPopupOpen = (state: RootState) =>
state.partnerLicense.apps.isSearchPopupOpen;

View File

@ -20,6 +20,9 @@ export interface Apps {
hierarchicalElements: HierarchicalElement[];
isLoading: boolean;
selectedRow?: PartnerLicenseInfo;
isLicenseOrderHistoryOpen: boolean;
isViewDetailsOpen: boolean;
isSearchPopupOpen: boolean;
}
export interface HierarchicalElement {

View File

@ -0,0 +1,4 @@
export * from "./state";
export * from "./operations";
export * from "./selectors";
export * from "./searchPartnerSlice";

View File

@ -0,0 +1,96 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getAccessToken } from "features/auth";
import type { RootState } from "../../../app/store";
import { getTranslationID } from "../../../translation";
import { openSnackbar } from "../../ui/uiSlice";
import { AccountsApi, SearchPartner, PartnerHierarchy } from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const searchPartnersAsync = createAsyncThunk<
// 正常時の戻り値の型
SearchPartner[],
// 引数
{
companyName?: string;
accountId?: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/searchPartners", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { companyName, accountId } = args;
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
const searchPartnerResponse = await accountsApi.searchPartners(
companyName,
accountId,
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return searchPartnerResponse.data.searchResult;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getPartnerHierarchy = createAsyncThunk<
// 正常時の戻り値の型
PartnerHierarchy[],
// 引数
{
accountId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/getPartnerHierarchy", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { accountId } = args;
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
const partnerHierarchyResponse = await accountsApi.getPartnerHierarchy(
accountId,
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return partnerHierarchyResponse.data.accountHierarchy;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,80 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { SearchPartner } from "../../../api";
import { SearchPartnerState } from "./state";
import { searchPartnersAsync, getPartnerHierarchy } from "./operations";
const initialState: SearchPartnerState = {
domain: {
searchResult: [],
partnerHierarchy: [],
},
apps: {
isLoading: false,
selectedRow: undefined,
isLicenseOrderHistoryOpen: false,
isViewDetailsOpen: false,
},
};
export const searchPartnersSlice = createSlice({
name: "searchPartners",
initialState,
reducers: {
changeSelectedRow: (
state,
action: PayloadAction<{ value?: SearchPartner }>
) => {
const { value } = action.payload;
state.apps.selectedRow = value;
},
setIsLicenseOrderHistoryOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isLicenseOrderHistoryOpen = action.payload.value;
},
setIsViewDetailsOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isViewDetailsOpen = action.payload.value;
},
cleanupSearchResult: (state) => {
state.domain.searchResult = initialState.domain.searchResult;
},
cleanupPartnerHierarchy: (state) => {
state.domain.partnerHierarchy = initialState.domain.partnerHierarchy;
},
},
extraReducers: (builder) => {
builder.addCase(searchPartnersAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(searchPartnersAsync.fulfilled, (state, action) => {
state.domain.searchResult = action.payload;
state.apps.isLoading = false;
});
builder.addCase(searchPartnersAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getPartnerHierarchy.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getPartnerHierarchy.fulfilled, (state, action) => {
state.domain.partnerHierarchy = action.payload;
state.apps.isLoading = false;
});
builder.addCase(getPartnerHierarchy.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {
changeSelectedRow,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
cleanupSearchResult,
cleanupPartnerHierarchy,
} = searchPartnersSlice.actions;
export default searchPartnersSlice.reducer;

View File

@ -0,0 +1,14 @@
import { RootState } from "../../../app/store";
export const selectSearchResult = (state: RootState) =>
state.searchPartners.domain.searchResult;
export const selectPartnerHierarchy = (state: RootState) =>
state.searchPartners.domain.partnerHierarchy;
export const selectIsLoading = (state: RootState) =>
state.searchPartners.apps.isLoading;
export const selectSelectedRow = (state: RootState) =>
state.searchPartners.apps.selectedRow;
export const selectIsLicenseOrderHistoryOpen = (state: RootState) =>
state.searchPartners.apps.isLicenseOrderHistoryOpen;
export const selectIsViewDetailsOpen = (state: RootState) =>
state.searchPartners.apps.isViewDetailsOpen;

View File

@ -0,0 +1,18 @@
import { SearchPartner, PartnerHierarchy } from "../../../api/api";
export interface SearchPartnerState {
domain: Domain;
apps: Apps;
}
export interface Domain {
searchResult: SearchPartner[];
partnerHierarchy: PartnerHierarchy[];
}
export interface Apps {
isLoading: boolean;
selectedRow?: SearchPartner;
isLicenseOrderHistoryOpen: boolean;
isViewDetailsOpen: boolean;
}

View File

@ -8,6 +8,8 @@ import {
AccountsApi,
CreatePartnerAccountRequest,
GetPartnersResponse,
DeletePartnerAccountRequest,
GetPartnerUsersResponse,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
@ -116,3 +118,170 @@ export const getPartnerInfoAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
// パートナーアカウント削除
export const deletePartnerAccountAsync = createAsyncThunk<
{
/* Empty Object */
},
{
// パラメータ
accountId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("partner/deletePartnerAccountAsync", async (args, thunkApi) => {
const { accountId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
try {
const deletePartnerAccountRequest: DeletePartnerAccountRequest = {
targetAccountId: accountId,
};
await accountApi.deletePartnerAccount(deletePartnerAccountRequest, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
let errorMessage = getTranslationID("common.message.internalServerError");
if (error.code === "E018001") {
errorMessage = getTranslationID(
"partnerPage.message.partnerDeleteFailedError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});
// パートナーアカウントユーザー取得
export const getPartnerUsersAsync = createAsyncThunk<
GetPartnerUsersResponse,
{
// パラメータ
accountId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("partner/getPartnerUsersAsync", async (args, thunkApi) => {
const { accountId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
try {
const res = await accountApi.getPartnerUsers(
{ targetAccountId: accountId },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return res.data;
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
// パートナーアカウントユーザー編集
export const editPartnerInfoAsync = createAsyncThunk<
{
/* Empty Object */
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("partner/editPartnerInfoAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
const { id, companyName, selectedAdminId } = state.partner.apps.editPartner;
try {
await accountApi.updatePartnerInfo(
{
targetAccountId: id,
primaryAdminUserId: selectedAdminId,
companyName,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
let errorMessage = getTranslationID("common.message.internalServerError");
if (error.code === "E010502" || error.code === "E020001") {
errorMessage = getTranslationID("partnerPage.message.editFailedError");
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,6 +1,12 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { PartnerState } from "./state";
import { createPartnerAccountAsync, getPartnerInfoAsync } from "./operations";
import {
createPartnerAccountAsync,
getPartnerInfoAsync,
deletePartnerAccountAsync,
getPartnerUsersAsync,
editPartnerInfoAsync,
} from "./operations";
import { LIMIT_PARTNER_VIEW_NUM } from "./constants";
const initialState: PartnerState = {
@ -17,6 +23,13 @@ const initialState: PartnerState = {
adminName: "",
email: "",
},
editPartner: {
users: [],
id: 0,
companyName: "",
country: "",
selectedAdminId: 0,
},
limit: LIMIT_PARTNER_VIEW_NUM,
offset: 0,
isLoading: false,
@ -75,6 +88,37 @@ export const partnerSlice = createSlice({
state.apps.delegatedAccountId = undefined;
state.apps.delegatedCompanyName = undefined;
},
changeEditPartner: (
state,
action: PayloadAction<{
id: number;
companyName: string;
country: string;
}>
) => {
const { id, companyName, country } = action.payload;
state.apps.editPartner.id = id;
state.apps.editPartner.companyName = companyName;
state.apps.editPartner.country = country;
},
changeEditCompanyName: (
state,
action: PayloadAction<{ companyName: string }>
) => {
const { companyName } = action.payload;
state.apps.editPartner.companyName = companyName;
},
changeSelectedAdminId: (
state,
action: PayloadAction<{ adminId: number }>
) => {
const { adminId } = action.payload;
state.apps.editPartner.selectedAdminId = adminId;
},
cleanupPartnerAccount: (state) => {
state.apps.editPartner = initialState.apps.editPartner;
},
},
extraReducers: (builder) => {
builder.addCase(createPartnerAccountAsync.pending, (state) => {
@ -97,6 +141,37 @@ export const partnerSlice = createSlice({
builder.addCase(getPartnerInfoAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deletePartnerAccountAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deletePartnerAccountAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deletePartnerAccountAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getPartnerUsersAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getPartnerUsersAsync.fulfilled, (state, action) => {
const { users } = action.payload;
state.apps.editPartner.users = users;
state.apps.editPartner.selectedAdminId =
users.find((user) => user.isPrimaryAdmin)?.id ?? 0;
state.apps.isLoading = false;
});
builder.addCase(getPartnerUsersAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(editPartnerInfoAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(editPartnerInfoAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(editPartnerInfoAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {
@ -108,5 +183,9 @@ export const {
savePageInfo,
changeDelegateAccount,
cleanupDelegateAccount,
changeEditPartner,
changeEditCompanyName,
changeSelectedAdminId,
cleanupPartnerAccount,
} = partnerSlice.actions;
export default partnerSlice.reducer;

View File

@ -62,3 +62,17 @@ export const selectDelegatedAccountId = (state: RootState) =>
state.partner.apps.delegatedAccountId;
export const selectDelegatedCompanyName = (state: RootState) =>
state.partner.apps.delegatedCompanyName;
// edit
export const selectEditPartnerId = (state: RootState) =>
state.partner.apps.editPartner.id;
export const selectEditPartnerCompanyName = (state: RootState) =>
state.partner.apps.editPartner.companyName;
export const selectEditPartnerCountry = (state: RootState) =>
state.partner.apps.editPartner.country;
export const selectEditPartnerUsers = (state: RootState) =>
state.partner.apps.editPartner.users;
export const selectSelectedAdminId = (state: RootState) =>
state.partner.apps.editPartner.selectedAdminId;

View File

@ -1,6 +1,7 @@
import {
CreatePartnerAccountRequest,
GetPartnersResponse,
PartnerUser,
} from "../../api/api";
export interface PartnerState {
@ -19,4 +20,11 @@ export interface Apps {
isLoading: boolean;
delegatedAccountId?: number;
delegatedCompanyName?: string;
editPartner: {
users: PartnerUser[];
id: number;
companyName: string;
country: string;
selectedAdminId: number;
};
}

View File

@ -9,6 +9,7 @@ import {
UsersApi,
LicensesApi,
GetAllocatableLicensesResponse,
MultipleImportUser,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ErrorObject, createErrorObject } from "../../common/errors";
@ -17,7 +18,7 @@ export const listUsersAsync = createAsyncThunk<
// 正常時の戻り値の型
GetUsersResponse,
// 引数
void,
undefined | { userInputUserName?: string; userInputEmail?: string },
{
// rejectした時の返却値の型
rejectValue: {
@ -32,9 +33,11 @@ export const listUsersAsync = createAsyncThunk<
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
const userInputUserName = args?.userInputUserName;
const userInputEmail = args?.userInputEmail;
try {
const res = await usersApi.getUsers({
const res = await usersApi.getUsers(userInputUserName, userInputEmail, {
headers: { authorization: `Bearer ${accessToken}` },
});
@ -383,3 +386,255 @@ export const deallocateLicenseAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const deleteUserAsync = createAsyncThunk<
// 正常時の戻り値の型
{
/* Empty Object */
},
// 引数
{
userId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("users/deleteUserAsync", async (args, thunkApi) => {
const { userId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
await usersApi.deleteUser(
{
userId,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
let errorMessage = getTranslationID("common.message.internalServerError");
if (error.statusCode === 400) {
if (error.code === "E014001") {
// ユーザーが削除済みのため成功
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
}
// ユーザーに有効なライセンスが割り当たっているため削除不可
if (error.code === "E014007") {
errorMessage = getTranslationID(
"userListPage.message.userDeletionLicenseActiveError"
);
}
// 管理者ユーザーため削除不可
if (error.code === "E014002") {
errorMessage = getTranslationID(
"userListPage.message.adminUserDeletionError"
);
}
// タイピストユーザーで担当タスクがあるため削除不可
if (error.code === "E014009") {
errorMessage = getTranslationID(
"userListPage.message.typistUserDeletionTranscriptionTaskError"
);
}
// タイピストユーザーでルーティングルールに設定されているため削除不可
if (error.code === "E014004") {
errorMessage = getTranslationID(
"userListPage.message.typistDeletionRoutingRuleError"
);
}
// タイピストユーザーでTranscriptionistGroupに所属しているため削除不可
if (error.code === "E014005") {
errorMessage = getTranslationID(
"userListPage.message.typistUserDeletionTranscriptionistGroupError"
);
}
// Authorユーザーで同一AuthorIDのタスクがあるため削除不可
if (error.code === "E014006") {
errorMessage = getTranslationID(
"userListPage.message.authorUserDeletionTranscriptionTaskError"
);
}
// Authorユーザーで同一AuthorIDがルーティングルールに設定されているため削除不可
if (error.code === "E014003") {
errorMessage = getTranslationID(
"userListPage.message.authorDeletionRoutingRuleError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const confirmUserForceAsync = createAsyncThunk<
// 正常時の戻り値の型
{
/* Empty Object */
},
// 引数
{
userId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("users/confirmUserForceAsync", async (args, thunkApi) => {
const { userId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
await usersApi.confirmUserForce(
{
userId,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
let errorMessage = getTranslationID("common.message.internalServerError");
// ユーザーが既に認証済みのため、強制認証不可
if (error.code === "E010202") {
errorMessage = getTranslationID(
"userListPage.message.alreadyEmailVerifiedError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const importUsersAsync = createAsyncThunk<
// 正常時の戻り値の型
{
/* Empty Object */
},
// 引数
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("users/importUsersAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const { importFileName, importUsers } = state.user.apps;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
if (importFileName === undefined) {
throw new Error("importFileName is undefined");
}
// CSVデータをAPIに送信するためのデータに変換
const users: MultipleImportUser[] = importUsers.map((user) => ({
name: user.name ?? "",
email: user.email ?? "",
role: user.role ?? 0,
authorId: user.author_id ?? undefined,
autoRenew: user.auto_assign ?? 0,
notification: user.notification ?? 0,
encryption: user.encryption ?? undefined,
encryptionPassword: user.encryption_password ?? undefined,
prompt: user.prompt ?? undefined,
}));
await usersApi.multipleImports(
{
filename: importFileName,
users,
},
{ headers: { authorization: `Bearer ${accessToken}` } }
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("userListPage.message.importSuccess"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -382,3 +382,142 @@ const convertValueBasedOnLicenseStatus = (
remaining: undefined,
};
};
export const selectImportFileName = (state: RootState) =>
state.user.apps.importFileName;
export const selectImportValidationErrors = (state: RootState) => {
const csvUsers = state.user.apps.importUsers;
let rowNumber = 1;
const invalidInput: number[] = [];
const duplicatedEmailsMap = new Map<string, number>();
const duplicatedAuthorIdsMap = new Map<string, number>();
const overMaxRow = csvUsers.length > 100;
// eslint-disable-next-line no-restricted-syntax
for (const csvUser of csvUsers) {
rowNumber += 1;
// メールアドレスの重複がある場合、エラーとしてその行番号を追加する
const duplicatedEmailUser = csvUsers.filter(
(x) => x.email === csvUser.email
);
if (duplicatedEmailUser.length > 1) {
if (csvUser.email !== null && !duplicatedEmailsMap.has(csvUser.email)) {
duplicatedEmailsMap.set(csvUser.email, rowNumber);
}
}
// AuthorIDの重複がある場合、エラーとしてその行番号を追加する
const duplicatedAuthorIdUser = csvUsers.filter(
(x) => x.author_id === csvUser.author_id
);
if (duplicatedAuthorIdUser.length > 1) {
if (
csvUser.author_id !== null &&
!duplicatedAuthorIdsMap.has(csvUser.author_id)
) {
duplicatedAuthorIdsMap.set(csvUser.author_id, rowNumber);
}
}
// name
if (csvUser.name === null || csvUser.name.length > 225) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// email
const emailPattern =
/^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/;
if (
csvUser.name === null ||
csvUser.name.length > 225 ||
!emailPattern.test(csvUser.email ?? "")
) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// role
if (csvUser.role === null || ![0, 1, 2].includes(csvUser.role)) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// role=1(Author)
if (csvUser.role === 1) {
// author_id
if (csvUser.author_id === null || csvUser.author_id.length > 16) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// 半角英数字と_の組み合わせで文字まで
const charaTypePattern = /^[A-Z0-9_]{1,16}$/;
const charaType = new RegExp(charaTypePattern).test(csvUser.author_id);
if (!charaType) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// encryption
if (csvUser.encryption === null || ![0, 1].includes(csvUser.encryption)) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
if (csvUser.encryption === 1) {
// encryption_password
if (csvUser.encryption === 1) {
const regex = /^[!-~]{4,16}$/;
if (!regex.test(csvUser.encryption_password ?? "")) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
}
}
// prompt
if (csvUser.prompt === null || ![0, 1].includes(csvUser.prompt)) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
}
// auto_assign
if (csvUser.auto_assign === null || ![0, 1].includes(csvUser.auto_assign)) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// notification
if (
csvUser.notification === null ||
![0, 1].includes(csvUser.notification)
) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
}
const duplicatedEmails = Array.from(duplicatedEmailsMap.values());
const duplicatedAuthorIds = Array.from(duplicatedAuthorIdsMap.values());
return {
invalidInput,
duplicatedEmails,
duplicatedAuthorIds,
overMaxRow,
};
};

View File

@ -1,3 +1,4 @@
import { CSVType } from "common/parser";
import { User, AllocatableLicenseInfo } from "../../api/api";
import { AddUser, UpdateUser, LicenseAllocateUser } from "./types";
@ -19,4 +20,6 @@ export interface Apps {
selectedlicenseId: number;
hasPasswordMask: boolean;
isLoading: boolean;
importFileName: string | undefined;
importUsers: CSVType[];
}

View File

@ -54,14 +54,14 @@ export interface LicenseAllocateUser {
remaining?: number;
}
export type RoleType = typeof USER_ROLES[keyof typeof USER_ROLES];
export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES];
// 受け取った値がUSER_ROLESの型であるかどうかを判定する
export const isRoleType = (role: string): role is RoleType =>
Object.values(USER_ROLES).includes(role as RoleType);
export type LicenseStatusType =
typeof LICENSE_STATUS[keyof typeof LICENSE_STATUS];
(typeof LICENSE_STATUS)[keyof typeof LICENSE_STATUS];
// 受け取った値がLicenseStatusTypeの型であるかどうかを判定する
export const isLicenseStatusType = (

View File

@ -1,5 +1,6 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { USER_ROLES } from "components/auth/constants";
import { CSVType } from "common/parser";
import { UsersState } from "./state";
import {
addUserAsync,
@ -7,6 +8,8 @@ import {
updateUserAsync,
getAllocatableLicensesAsync,
deallocateLicenseAsync,
deleteUserAsync,
importUsersAsync,
} from "./operations";
import { RoleType, UserView } from "./types";
@ -60,6 +63,8 @@ const initialState: UsersState = {
selectedlicenseId: 0,
hasPasswordMask: false,
isLoading: false,
importFileName: undefined,
importUsers: [],
},
};
@ -241,6 +246,21 @@ export const userSlice = createSlice({
state.apps.licenseAllocateUser = initialState.apps.licenseAllocateUser;
state.apps.selectedlicenseId = initialState.apps.selectedlicenseId;
},
changeImportFileName: (
state,
action: PayloadAction<{ fileName: string }>
) => {
const { fileName } = action.payload;
state.apps.importFileName = fileName;
},
changeImportCsv: (state, action: PayloadAction<{ users: CSVType[] }>) => {
const { users } = action.payload;
state.apps.importUsers = users;
},
cleanupImportUsers: (state) => {
state.apps.importFileName = initialState.apps.importFileName;
state.apps.importUsers = initialState.apps.importUsers;
},
},
extraReducers: (builder) => {
builder.addCase(listUsersAsync.pending, (state) => {
@ -290,6 +310,24 @@ export const userSlice = createSlice({
builder.addCase(deallocateLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteUserAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deleteUserAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteUserAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(importUsersAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(importUsersAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(importUsersAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
@ -317,6 +355,9 @@ export const {
changeLicenseAllocateUser,
changeSelectedlicenseId,
cleanupLicenseAllocateInfo,
changeImportFileName,
changeImportCsv,
cleanupImportUsers,
} = userSlice.actions;
export default userSlice.reducer;

View File

@ -115,3 +115,78 @@ export const uploadTemplateAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const deleteTemplateAsync = createAsyncThunk<
{
/* Empty Object */
},
{ templateFileId: number },
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/deleteTemplateAsync", async (args, thunkApi) => {
const { templateFileId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const templateApi = new TemplatesApi(config);
try {
// ファイルを削除する
await templateApi.deleteTemplateFile(templateFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
if (error.code === "E016001") {
// テンプレートファイルが削除済みの場合は成功扱いとする
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
let message = getTranslationID("common.message.internalServerError");
// テンプレートファイルがルーティングルールに紐づく場合はエラー
if (error.code === "E016002") {
message = getTranslationID(
"templateFilePage.message.deleteFailedWorkflowAssigned"
);
}
// テンプレートファイルが未完了のタスクに紐づく場合はエラー
if (error.code === "E016003") {
message = getTranslationID(
"templateFilePage.message.deleteFailedTaskAssigned"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,6 +1,10 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { TemplateState } from "./state";
import { listTemplateAsync, uploadTemplateAsync } from "./operations";
import {
deleteTemplateAsync,
listTemplateAsync,
uploadTemplateAsync,
} from "./operations";
const initialState: TemplateState = {
apps: {
@ -45,6 +49,15 @@ export const templateSlice = createSlice({
builder.addCase(uploadTemplateAsync.rejected, (state) => {
state.apps.isUploading = false;
});
builder.addCase(deleteTemplateAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deleteTemplateAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteTemplateAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});

View File

@ -269,3 +269,70 @@ export const updateTypistGroupAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const deleteTypistGroupAsync = createAsyncThunk<
{
/* Empty Object */
},
{
typistGroupId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/deleteTypistGroupAsync", async (args, thunkApi) => {
const { typistGroupId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
await accountsApi.deleteTypistGroup(typistGroupId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// すでに削除されていた場合は成功扱いする
if (error.code === "E015001") {
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
// 以下は実際の削除失敗
let message = getTranslationID("common.message.internalServerError");
if (error.code === "E015002")
message = getTranslationID(
"typistGroupSetting.message.deleteFailedWorkflowAssigned"
);
if (error.code === "E015003")
message = getTranslationID(
"typistGroupSetting.message.deleteFailedCheckoutPermissionExisted"
);
thunkApi.dispatch(openSnackbar({ level: "error", message }));
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -6,6 +6,7 @@ import {
listTypistGroupsAsync,
listTypistsAsync,
updateTypistGroupAsync,
deleteTypistGroupAsync,
} from "./operations";
const initialState: TypistGroupState = {
@ -106,6 +107,15 @@ export const typistGroupSlice = createSlice({
builder.addCase(updateTypistGroupAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteTypistGroupAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deleteTypistGroupAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteTypistGroupAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});

View File

@ -2,7 +2,7 @@ import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "./constants";
// OPTION_ITEMS_DEFAULT_VALUE_TYPEからOptionItemDefaultValueTypeを作成する
export type OptionItemsDefaultValueType =
typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE];
(typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE)[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE];
// 受け取った値がOptionItemDefaultValueType型かどうかを判定する
export const isOptionItemDefaultValueType = (

View File

@ -0,0 +1,169 @@
import React, { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
selectInputValidationErrors,
selectFileDeleteSetting,
updateFileDeleteSettingAsync,
selectIsLoading,
getAccountRelationsAsync,
} from "features/account";
import { AppDispatch } from "app/store";
import { useTranslation } from "react-i18next";
import styles from "../../styles/app.module.scss";
import { getTranslationID } from "../../translation";
import close from "../../assets/images/close.svg";
import {
changeAutoFileDelete,
changeFileRetentionDays,
} from "../../features/account/accountSlice";
import progress_activit from "../../assets/images/progress_activit.svg";
interface FileDeleteSettingPopupProps {
// eslint-disable-next-line react/no-unused-prop-types
onClose: () => void;
}
export const FileDeleteSettingPopup: React.FC<FileDeleteSettingPopupProps> = (
props
) => {
const { onClose } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const isLoading = useSelector(selectIsLoading);
const fileDeleteSetting = useSelector(selectFileDeleteSetting);
const { hasFileRetentionDaysError } = useSelector(
selectInputValidationErrors
);
const closePopup = useCallback(() => {
if (isLoading) return;
onClose();
}, [isLoading, onClose]);
const [isPushSubmitButton, setIsPushSubmitButton] = useState<boolean>(false);
const onUpdateFileDeleteSetting = useCallback(async () => {
if (isLoading) return;
setIsPushSubmitButton(true);
if (hasFileRetentionDaysError) {
return;
}
const { meta } = await dispatch(
updateFileDeleteSettingAsync({
autoFileDelete: fileDeleteSetting.autoFileDelete,
fileRetentionDays: fileDeleteSetting.fileRetentionDays,
})
);
setIsPushSubmitButton(false);
if (meta.requestStatus === "fulfilled") {
closePopup();
dispatch(getAccountRelationsAsync());
}
}, [
closePopup,
dispatch,
fileDeleteSetting.autoFileDelete,
fileDeleteSetting.fileRetentionDays,
hasFileRetentionDaysError,
isLoading,
]);
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("fileDeleteSettingPopup.label.title"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>
{t(
getTranslationID(
"fileDeleteSettingPopup.label.autoFileDeleteCheck"
)
)}
</dt>
<dd className={styles.last}>
<p>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label>
<input
type="checkbox"
className={styles.formCheck}
checked={fileDeleteSetting.autoFileDelete}
onChange={(e) => {
dispatch(
changeAutoFileDelete({
autoFileDelete: e.target.checked,
})
);
}}
/>
</label>
</p>
<p className={styles.txWsline}>
{t(
getTranslationID(
"fileDeleteSettingPopup.label.daysAnnotation"
)
)}
</p>
<input
type="number"
min={1}
max={999}
value={fileDeleteSetting.fileRetentionDays}
className={`${styles.formInput} ${styles.short}`}
disabled={!fileDeleteSetting.autoFileDelete}
onChange={(e) => {
dispatch(
changeFileRetentionDays({
fileRetentionDays: Number(e.target.value),
})
);
}}
/>{" "}
{t(getTranslationID("fileDeleteSettingPopup.label.days"))}
{isPushSubmitButton && hasFileRetentionDaysError && (
<span className={styles.formError}>
{t(
getTranslationID(
"fileDeleteSettingPopup.label.daysValidationError"
)
)}
</span>
)}
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(
getTranslationID("fileDeleteSettingPopup.label.saveButton")
)}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading ? styles.isActive : ""
}`}
onClick={onUpdateFileDeleteSetting}
disabled={isLoading}
/>
</dd>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</dl>
</form>
</div>
</div>
);
};

View File

@ -1,5 +1,4 @@
import { AppDispatch } from "app/store";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import Footer from "components/footer";
import Header from "components/header";
import React, { useCallback, useEffect, useState } from "react";
@ -23,6 +22,7 @@ import { getTranslationID } from "translation";
import { TIERS } from "components/auth/constants";
import { isApproveTier } from "features/auth";
import { DeleteAccountPopup } from "./deleteAccountPopup";
import { FileDeleteSettingPopup } from "./fileDeleteSettingPopup";
import progress_activit from "../../assets/images/progress_activit.svg";
const AccountPage: React.FC = (): JSX.Element => {
@ -40,10 +40,17 @@ const AccountPage: React.FC = (): JSX.Element => {
const [isDeleteAccountPopupOpen, setIsDeleteAccountPopupOpen] =
useState(false);
const [isFileDeleteSettingPopupOpen, setIsFileDeleteSettingPopupOpen] =
useState(false);
const onDeleteAccountOpen = useCallback(() => {
setIsDeleteAccountPopupOpen(true);
}, [setIsDeleteAccountPopupOpen]);
const onDeleteFileDeleteSettingOpen = useCallback(() => {
setIsFileDeleteSettingPopupOpen(true);
}, [setIsFileDeleteSettingPopupOpen]);
// 階層表示用
const tierNames: { [key: number]: string } = {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -89,9 +96,15 @@ const AccountPage: React.FC = (): JSX.Element => {
}}
/>
)}
{isFileDeleteSettingPopupOpen && (
<FileDeleteSettingPopup
onClose={() => {
setIsFileDeleteSettingPopupOpen(false);
}}
/>
)}
<div className={styles.wrap}>
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
@ -102,12 +115,13 @@ const AccountPage: React.FC = (): JSX.Element => {
<section className={styles.account}>
<div className={styles.boxFlex}>
{/* File Delete Setting
<ul className={`${styles.menuAction} ${styles.box100}`}>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
href="account_setting.html"
className={`${styles.menuLink} ${styles.isActive}`}
onClick={onDeleteFileDeleteSettingOpen}
data-tag="open-file-delete-setting-popup"
>
<img
src="images/file_delete.svg"
@ -120,7 +134,6 @@ const AccountPage: React.FC = (): JSX.Element => {
</a>
</li>
</ul>
*/}
<div className={styles.marginRgt3}>
<dl className={styles.listVertical}>
@ -158,7 +171,7 @@ const AccountPage: React.FC = (): JSX.Element => {
changeDealer({
parentAccountId:
dealers.find(
(x) => x.name === event.target.value
(x) => x.id === Number(event.target.value)
)?.id || undefined,
})
);
@ -175,7 +188,7 @@ const AccountPage: React.FC = (): JSX.Element => {
)} --`}
</option>
{dealers.map((x) => (
<option key={x.name} value={x.name}>
<option key={x.id} value={x.id}>
{x.name}
</option>
))}
@ -216,9 +229,23 @@ const AccountPage: React.FC = (): JSX.Element => {
)
)}
</dd>
<dd
style={{ paddingBottom: 0 }}
className={`${styles.full}`}
/>
</>
)}
{!isTier5 && <dd>-</dd>}
<dt>
{t(
getTranslationID("accountPage.label.fileRetentionDays")
)}
</dt>
<dd>
{viewInfo.account.autoFileDelete
? viewInfo.account.fileRetentionDays
: "-"}
</dd>
</dl>
</div>

View File

@ -1,13 +1,15 @@
import React, { useCallback } from "react";
import React, { useCallback, useEffect, useState } from "react";
import styles from "styles/app.module.scss";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import {
selectSelectedFileTask,
selectIsLoading,
PRIORITY,
renameFileAsync,
} from "features/dictation";
import { getTranslationID } from "translation";
import { useTranslation } from "react-i18next";
import { AppDispatch } from "app/store";
import close from "../../assets/images/close.svg";
import lock from "../../assets/images/lock.svg";
@ -19,14 +21,55 @@ interface FilePropertyPopupProps {
export const FilePropertyPopup: React.FC<FilePropertyPopupProps> = (props) => {
const { onClose, isOpen } = props;
const [t] = useTranslation();
const dispatch: AppDispatch = useDispatch();
const isLoading = useSelector(selectIsLoading);
const [isPushSaveButton, setIsPushSaveButton] = useState<boolean>(false);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
setIsPushSaveButton(false);
onClose(false);
}, [onClose]);
const selectedFileTask = useSelector(selectSelectedFileTask);
const [fileName, setFileName] = useState<string>("");
useEffect(() => {
if (isOpen) {
setFileName(selectedFileTask?.fileName ?? "");
}
}, [selectedFileTask, isOpen]);
// ファイル名の保存処理
const saveFileName = useCallback(async () => {
setIsPushSaveButton(true);
if (fileName.length === 0) {
return;
}
// ダイアログ確認
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(
renameFileAsync({
audioFileId: selectedFileTask?.audioFileId ?? 0,
fileName,
})
);
setIsPushSaveButton(false);
if (meta.requestStatus === "fulfilled") {
onClose(true);
}
}, [t, dispatch, onClose, fileName, selectedFileTask]);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
@ -45,7 +88,41 @@ export const FilePropertyPopup: React.FC<FilePropertyPopupProps> = (props) => {
{t(getTranslationID("filePropertyPopup.label.general"))}
</dt>
<dt>{t(getTranslationID("dictationPage.label.fileName"))}</dt>
<dd>{selectedFileTask?.fileName.replace(".zip", "") ?? ""}</dd>
<dd className={styles.hasInput}>
<input
type="text"
size={40}
maxLength={64}
value={fileName}
className={`${styles.formInput} ${styles.short} ${
isPushSaveButton && fileName.length === 0 && styles.isError
}`}
onChange={(e) => setFileName(e.target.value)}
/>
<input
type="button"
name="submit"
value={t(getTranslationID("dictationPage.label.fileNameSave"))}
className={`${styles.formSubmit} ${styles.isActive}`}
style={{
position: "relative",
marginTop: "0.2rem",
right: "auto",
maxWidth: "18rem",
whiteSpace: "normal",
overflowWrap: "break-word",
fontSize: "small",
}}
onClick={saveFileName}
/>
{isPushSaveButton && fileName.length === 0 && (
<span className={styles.formError}>
{t(getTranslationID("common.message.inputEmptyError"))}
</span>
)}
</dd>
<dt>{t(getTranslationID("dictationPage.label.rawFileName"))}</dt>
<dd>{selectedFileTask?.rawFileName ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.fileSize"))}</dt>
<dd>{selectedFileTask?.fileSize ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.fileLength"))}</dt>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,203 @@
import React, { useState, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
selectChildrenPartnerLicenses,
selectIsLoading,
selectOwnPartnerLicense,
} from "features/license/partnerLicense/selectors";
import {
getMyAccountAsync,
switchParentAsync,
} from "features/license/partnerLicense/operations";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import { AppDispatch } from "app/store";
import { clearHierarchicalElement } from "features/license/partnerLicense";
import styles from "../../styles/app.module.scss";
import close from "../../assets/images/close.svg";
import shuffle from "../../assets/images/shuffle.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
interface ChangeOwnerPopupProps {
onClose: () => void;
}
const ChangeOwnerPopup: React.FC<ChangeOwnerPopupProps> = (props) => {
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
const [selectedChildId, setSelectedChildId] = useState<number | null>(null);
const [selectedChildName, setSelectedChildName] = useState<string>("");
const [destinationParentId, setDestinationParentId] = useState<string>("");
const [error, setError] = useState<string>("");
const originParentLicenseInfo = useSelector(selectOwnPartnerLicense);
const childrenLicenseInfos = useSelector(selectChildrenPartnerLicenses);
const isLoading = useSelector(selectIsLoading);
const { onClose } = props;
const closePopup = useCallback(() => {
if (isLoading) return;
onClose();
}, [isLoading, onClose]);
const bulkDisplayName = "-- Bulk --";
const bulkValue = "bulk";
const onBulkChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const { value } = e.target;
const childId = value === bulkValue ? null : Number(value);
setSelectedChildId(childId);
// 一括追加のときは子アカウント名を表示しない
let childName = "";
if (childId) {
const child = childrenLicenseInfos.find((c) => c.accountId === childId);
// childがundefinedになることはないが、コード解析対応のためのチェック
if (child) {
childName = child.companyName;
}
}
setSelectedChildName(childName);
},
[childrenLicenseInfos]
);
const onSaveClick = useCallback(async () => {
const destinationParentIdNum = Number(destinationParentId);
if (
Number.isNaN(destinationParentIdNum) || // 数値でない場合
destinationParentIdNum <= 0 || // IDにならない数値の場合
destinationParentId.length > 7 // 8桁以上の場合本システムの特徴として8桁以上になることはあり得ない
) {
setError(t(getTranslationID("changeOwnerPopup.label.invalidInputError")));
return;
}
setError("");
if (
// eslint-disable-next-line no-alert
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const children = selectedChildId
? [selectedChildId]
: childrenLicenseInfos.map((child) => child.accountId);
const { meta } = await dispatch(
switchParentAsync({ to: Number(destinationParentId), children })
);
if (meta.requestStatus === "fulfilled") {
dispatch(getMyAccountAsync());
dispatch(clearHierarchicalElement());
closePopup();
}
}, [
childrenLicenseInfos,
closePopup,
destinationParentId,
dispatch,
selectedChildId,
t,
]);
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("changeOwnerPopup.label.title"))}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions */}
<img
src={close}
className={styles.modalTitleIcon}
alt="close"
onClick={closePopup}
/>
</p>
<form action="" name="" method="" className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>
{t(getTranslationID("changeOwnerPopup.label.upperLayerId"))}
</dt>
<dd className={styles.ownerChange}>
<p className={styles.Owner}>
<input
type="text"
size={40}
name=""
value={originParentLicenseInfo.accountId}
readOnly
className={`${styles.formInput} ${styles.short}`}
/>
<span className={styles.txName}>
{originParentLicenseInfo.companyName}
</span>
</p>
<p className={styles.arrowR} />
<p className={styles.newOwner}>
<input
type="text"
size={40}
name=""
value={destinationParentId}
placeholder=" "
className={`${styles.formInput} ${styles.short}`}
onChange={(e) => setDestinationParentId(e.target.value)}
/>
<span className={styles.formError}>{error}</span>
</p>
</dd>
<dd className={styles.full}>
<img src={shuffle} className={styles.transOwner} alt="" />
</dd>
<dt>
{t(getTranslationID("changeOwnerPopup.label.lowerLayerId"))}
</dt>
<dd className={styles.lowerTrans}>
<select
name=""
className={`${styles.formInput} ${styles.short}`}
value={selectedChildId ?? bulkDisplayName}
onChange={onBulkChange}
>
<option value={bulkValue}>{bulkDisplayName}</option>
{childrenLicenseInfos.map((child) => (
<option key={child.accountId} value={child.accountId}>
{child.accountId}
</option>
))}
</select>
<span className={styles.txName}>{selectedChildName}</span>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
{/* 処理中や子アカウントが1件も存在しない場合、Saveボタンを押せないようにする */}
<input
type="button"
name="submit"
value={t(getTranslationID("common.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading && childrenLicenseInfos.length > 0
? styles.isActive
: ""
}`}
onClick={onSaveClick}
disabled={isLoading || childrenLicenseInfos.length <= 0}
/>
</dd>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</dl>
</form>
</div>
</div>
);
};
export default ChangeOwnerPopup;

View File

@ -1,6 +1,5 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, { useCallback, useEffect } from "react";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import { isApproveTier } from "features/auth";
import { TIERS } from "components/auth/constants";
import Footer from "components/footer";
@ -13,6 +12,7 @@ import { useDispatch, useSelector } from "react-redux";
import {
LIMIT_ORDER_HISORY_NUM,
STATUS,
LICENSE_TYPE,
getLicenseOrderHistoriesAsync,
selectCurrentPage,
selectIsLoading,
@ -26,20 +26,21 @@ import {
selectCompanyName,
cancelIssueAsync,
} from "features/license/licenseOrderHistory";
import { selectSelectedRow } from "features/license/partnerLicense";
import { selectDelegationAccessToken } from "features/auth/selectors";
import { DelegationBar } from "components/delegate";
import { LicenseOrder, SearchPartner, PartnerLicenseInfo } from "api/api";
import undo from "../../assets/images/undo.svg";
import history from "../../assets/images/history.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
interface LicenseOrderHistoryProps {
onReturn: () => void;
selectedRow?: PartnerLicenseInfo | SearchPartner;
}
export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
props
): JSX.Element => {
const { onReturn } = props;
const { onReturn, selectedRow } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const total = useSelector(selectTotal);
@ -47,7 +48,6 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
const offset = useSelector(selectOffset);
const currentPage = useSelector(selectCurrentPage);
const isLoading = useSelector(selectIsLoading);
const selectedRow = useSelector(selectSelectedRow);
// 代行操作用のトークンを取得する
const delegationAccessToken = useSelector(selectDelegationAccessToken);
@ -65,6 +65,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
getLicenseOrderHistoriesAsync({
limit: LIMIT_ORDER_HISORY_NUM,
offset,
selectedRow,
})
);
};
@ -152,11 +153,15 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
getLicenseOrderHistoriesAsync({
limit: LIMIT_ORDER_HISORY_NUM,
offset,
selectedRow,
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, currentPage]);
const isNotTrialLicense = (license: LicenseOrder) =>
license.type !== LICENSE_TYPE.TRIAL;
return (
<div
className={`${styles.wrap} ${delegationAccessToken ? styles.manage : ""}`}
@ -166,7 +171,6 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
delegationAccessToken && <DelegationBar />
}
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
@ -210,6 +214,11 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
getTranslationID("orderHistoriesPage.label.issueDate")
)}
</th>
<th>
{t(
getTranslationID("orderHistoriesPage.label.licenseType")
)}
</th>
<th>
{t(
getTranslationID(
@ -231,9 +240,10 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
// eslint-disable-next-line react/jsx-key
<tr>
<td>{x.orderDate}</td>
<td>{x.issueDate ? x.issueDate : "-"}</td>
<td>{x.issueDate ?? "-"}</td>
<td>{x.type}</td>
<td>{x.numberOfOrder}</td>
<td>{x.poNumber}</td>
<td>{x.poNumber ?? "-"}</td>
<td>
{(() => {
switch (x.status) {
@ -261,7 +271,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
})()}
</td>
<td>
{!selectedRow && (
{!selectedRow && isNotTrialLicense(x) && (
<ul
className={`${styles.menuAction} ${styles.inTable}`}
>
@ -286,7 +296,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
</li>
</ul>
)}
{selectedRow && (
{selectedRow && isNotTrialLicense(x) && (
<ul
className={`${styles.menuAction} ${styles.inTable}`}
>

View File

@ -45,9 +45,10 @@ export const LicenseOrderPopup: React.FC<LicenseOrderPopupProps> = (props) => {
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
if (isLoading) return;
setIsPushOrderButton(false);
onClose();
}, [onClose]);
}, [isLoading, onClose]);
// 画面からのパラメータ
const poNumber = useSelector(selectPoNumber);

View File

@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import Footer from "components/footer";
import Header from "components/header";
import styles from "styles/app.module.scss";
@ -10,12 +9,16 @@ import { useDispatch, useSelector } from "react-redux";
import {
getCompanyNameAsync,
getLicenseSummaryAsync,
selecLicenseSummaryInfo,
selectLicenseSummaryInfo,
selectCompanyName,
selectIsLoading,
updateRestrictionStatusAsync,
} from "features/license/licenseSummary";
import { selectSelectedRow } from "features/license/partnerLicense";
import { selectDelegationAccessToken } from "features/auth/selectors";
import { DelegationBar } from "components/delegate";
import { TIERS } from "components/auth/constants";
import { isAdminUser, isApproveTier } from "features/auth/utils";
import { PartnerLicenseInfo, SearchPartner } from "../../api";
import postAdd from "../../assets/images/post_add.svg";
import history from "../../assets/images/history.svg";
import key from "../../assets/images/key.svg";
@ -24,26 +27,31 @@ import circle from "../../assets/images/circle.svg";
import returnLabel from "../../assets/images/undo.svg";
import { LicenseOrderPopup } from "./licenseOrderPopup";
import { CardLicenseActivatePopup } from "./cardLicenseActivatePopup";
import { TrialLicenseIssuePopup } from "./trialLicenseIssuePopup";
// eslint-disable-next-line import/no-named-as-default
import LicenseOrderHistory from "./licenseOrderHistory";
interface LicenseSummaryProps {
onReturn?: () => void;
selectedRow?: PartnerLicenseInfo | SearchPartner;
}
export const LicenseSummary: React.FC<LicenseSummaryProps> = (
props
): JSX.Element => {
const { onReturn } = props;
const { onReturn, selectedRow } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const selectedRow = useSelector(selectSelectedRow);
// 代行操作用のトークンを取得する
const delegationAccessToken = useSelector(selectDelegationAccessToken);
const isLoading = useSelector(selectIsLoading);
// popup制御関係
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] =
useState(false);
const [isTrialLicenseIssuePopupOpen, setIsTrialLicenseIssuePopupOpen] =
useState(false);
const onlicenseOrderOpen = useCallback(() => {
setIslicenseOrderPopupOpen(true);
@ -53,6 +61,10 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
setIsCardLicenseActivatePopupOpen(true);
}, [setIsCardLicenseActivatePopupOpen]);
const onTrialLicenseIssueOpen = useCallback(() => {
setIsTrialLicenseIssuePopupOpen(true);
}, [setIsTrialLicenseIssuePopupOpen]);
// 呼び出し画面制御関係
const [islicenseOrderHistoryOpen, setIsLicenseOrderHistoryOpen] =
useState(false);
@ -62,9 +74,13 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
}, [setIsLicenseOrderHistoryOpen]);
// apiからの値取得関係
const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo);
const licenseSummaryInfo = useSelector(selectLicenseSummaryInfo);
const companyName = useSelector(selectCompanyName);
const isTier1 = isApproveTier([TIERS.TIER1]);
const isTier2 = isApproveTier([TIERS.TIER2]);
const isAdmin = isAdminUser();
useEffect(() => {
dispatch(getLicenseSummaryAsync({ selectedRow }));
dispatch(getCompanyNameAsync({ selectedRow }));
@ -78,6 +94,35 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
}
}, [onReturn]);
const onStorageAvailableChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm(
t(
getTranslationID(
"LicenseSummaryPage.message.storageUnavalableSwitchingConfirm"
)
)
)
) {
return;
}
const restricted = e.target.checked;
const accountId = selectedRow?.accountId;
// 本関数が実行されるときはselectedRowが存在する前提のため、accountIdが存在しない場合の処理は不要
if (!accountId) return;
const { meta } = await dispatch(
updateRestrictionStatusAsync({ accountId, restricted })
);
if (meta.requestStatus === "fulfilled") {
dispatch(getLicenseSummaryAsync({ selectedRow }));
}
},
[dispatch, selectedRow, t]
);
return (
<>
{/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
@ -96,11 +141,21 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
}}
/>
)}
{isTrialLicenseIssuePopupOpen && (
<TrialLicenseIssuePopup
onClose={() => {
setIsTrialLicenseIssuePopupOpen(false);
dispatch(getLicenseSummaryAsync({ selectedRow }));
}}
selectedRow={selectedRow}
/>
)}
{islicenseOrderHistoryOpen && (
<LicenseOrderHistory
onReturn={() => {
setIsLicenseOrderHistoryOpen(false);
}}
selectedRow={selectedRow}
/>
)}
{!islicenseOrderHistoryOpen && (
@ -111,8 +166,6 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
>
{delegationAccessToken && <DelegationBar />}
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div className="">
<div className={styles.pageHeader}>
@ -193,6 +246,30 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
</a>
)}
</li>
<li>
{/* 第一階層、第二階層の管理者が第五階層アカウントのライセンス情報を見ている場合は、トライアルライセンス注文ボタンを表示 */}
{selectedRow &&
isAdmin &&
selectedRow.tier.toString() === TIERS.TIER5 &&
(isTier1 || isTier2) && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={onTrialLicenseIssueOpen}
>
<img
src={postAdd}
alt=""
className={styles.menuIcon}
/>
{t(
getTranslationID(
"LicenseSummaryPage.label.issueTrialLicense"
)
)}
</a>
)}
</li>
</ul>
<div className={styles.marginRgt3}>
<dl
@ -272,6 +349,27 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
</dl>
</div>
<div>
{isTier1 && isAdmin && (
<p
className={`${styles.checkAvail} ${styles.alignRight}`}
>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label>
<input
type="checkbox"
className={styles.formCheck}
checked={licenseSummaryInfo.isStorageAvailable}
disabled={isLoading}
onChange={onStorageAvailableChange}
/>
{t(
getTranslationID(
"LicenseSummaryPage.label.storageUnavailableCheckbox"
)
)}
</label>
</p>
)}
<dl
className={`${styles.listVertical} ${styles.marginBtm3}`}
>
@ -289,17 +387,31 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
)
)}
</dt>
{/* Storage Usedの値表示をハイフンに置き換え */}
{/* <dd>{licenseSummaryInfo.storageSize}GB</dd> */}
<dd>-</dd>
<dd>
{/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている小数点以下第三位まで表示で第四位で四捨五入 */}
{(
licenseSummaryInfo.storageSize /
1000 /
1000 /
1000
).toFixed(3)}
GB
</dd>
<dt>
{t(
getTranslationID("LicenseSummaryPage.label.usedSize")
)}
</dt>
{/* Storage Usedの値表示をハイフンに置き換え */}
{/* <dd>{licenseSummaryInfo.usedSize}GB</dd> */}
<dd>-</dd>
<dd>
{/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている小数点以下第三位まで表示で第四位で四捨五入 */}
{(
licenseSummaryInfo.usedSize /
1000 /
1000 /
1000
).toFixed(3)}
GB
</dd>
<dt className={styles.overLine}>
{t(
getTranslationID(

View File

@ -1,42 +1,57 @@
import React, { useCallback, useState, useEffect } from "react";
import { PartnerLicenseInfo } from "api";
import { AppDispatch } from "app/store";
import Footer from "components/footer";
import Header from "components/header";
import styles from "styles/app.module.scss";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "app/store";
import { getTranslationID } from "translation";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { PartnerLicenseInfo } from "api";
import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
import postAdd from "../../assets/images/post_add.svg";
import { useDispatch, useSelector } from "react-redux";
import styles from "styles/app.module.scss";
import { getTranslationID } from "translation";
import changeOwnerIcon from "../../assets/images/change_circle.svg";
import history from "../../assets/images/history.svg";
import postAdd from "../../assets/images/post_add.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import returnLabel from "../../assets/images/undo.svg";
import { isApproveTier } from "../../features/auth/utils";
import searchIcon from "../../assets/images/search.svg";
import { TIERS } from "../../components/auth/constants";
import { isApproveTier } from "../../features/auth/utils";
import {
getPartnerLicenseAsync,
ACCOUNTS_VIEW_LIMIT,
selectMyAccountInfo,
selectTotal,
selectOwnPartnerLicense,
selectChildrenPartnerLicenses,
selectHierarchicalElements,
selectTotalPage,
selectIsLoading,
selectOffset,
selectCurrentPage,
pushHierarchicalElement,
changeSelectedRow,
getMyAccountAsync,
getPartnerLicenseAsync,
popHierarchicalElement,
pushHierarchicalElement,
spliceHierarchicalElement,
savePageInfo,
getMyAccountAsync,
changeSelectedRow,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
selectChildrenPartnerLicenses,
selectCurrentPage,
selectHierarchicalElements,
selectIsLoading,
selectMyAccountInfo,
selectOffset,
selectOwnPartnerLicense,
selectTotal,
selectTotalPage,
selectSelectedRow,
selectIsLicenseOrderHistoryOpen,
selectIsViewDetailsOpen,
setIsSearchPopupOpen,
selectIsSearchPopupOpen,
} from "../../features/license/partnerLicense";
import { LicenseOrderPopup } from "./licenseOrderPopup";
import {
selectIsViewDetailsOpen as selectIsViewDetailsInSearchOpen,
selectIsLicenseOrderHistoryOpen as selectIsLicenseOrderHistoryInSearchOpen,
} from "../../features/license/searchPartner";
import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
import ChangeOwnerPopup from "./changeOwnerPopup";
import { LicenseOrderHistory } from "./licenseOrderHistory";
import { LicenseOrderPopup } from "./licenseOrderPopup";
import { LicenseSummary } from "./licenseSummary";
import progress_activit from "../../assets/images/progress_activit.svg";
import { SearchPartnerPopup } from "./searchPartnerAccountPopup";
const PartnerLicense: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
@ -46,9 +61,22 @@ const PartnerLicense: React.FC = (): JSX.Element => {
const [isCardLicenseIssuePopupOpen, setIsCardLicenseIssuePopupOpen] =
useState(false);
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] =
useState(false);
const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false);
// パートナーライセンス画面のOrderHistory, ViewDetailsの表示制御
const isLicenseOrderHistoryOpen = useSelector(
selectIsLicenseOrderHistoryOpen
);
const isViewDetailsOpen = useSelector(selectIsViewDetailsOpen);
// パートナー検索ポップアップのOrderHistory, ViewDetailsの表示制御
const isLicenseOrderHistoryInSearchOpen = useSelector(
selectIsLicenseOrderHistoryInSearchOpen
);
const isViewDetailsInSearchOpen = useSelector(
selectIsViewDetailsInSearchOpen
);
const isSearchPopupOpen = useSelector(selectIsSearchPopupOpen);
const [isChangeOwnerPopupOpen, setIsChangeOwnerPopupOpen] = useState(false);
// 階層表示用
const tierNames: { [key: number]: string } = {
@ -82,6 +110,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
);
const hierarchicalElements = useSelector(selectHierarchicalElements);
const isLoading = useSelector(selectIsLoading);
const selectedRow = useSelector(selectSelectedRow) as PartnerLicenseInfo;
// ページネーション制御用
const currentPage = useSelector(selectCurrentPage);
@ -134,20 +163,29 @@ const PartnerLicense: React.FC = (): JSX.Element => {
const onClickViewDetails = useCallback(
(value?: PartnerLicenseInfo) => {
dispatch(changeSelectedRow({ value }));
setIsViewDetailsOpen(true);
dispatch(setIsViewDetailsOpen({ value: true }));
},
[dispatch, setIsViewDetailsOpen]
[dispatch]
);
// orderHistoryボタン押下時
const onClickOrderHistory = useCallback(
(value?: PartnerLicenseInfo) => {
dispatch(changeSelectedRow({ value }));
setIslicenseOrderHistoryOpen(true);
dispatch(setIsLicenseOrderHistoryOpen({ value: true }));
},
[dispatch, setIslicenseOrderHistoryOpen]
[dispatch]
);
// changeOwnerボタン押下時
const onClickChangeOwner = useCallback(() => {
setIsChangeOwnerPopupOpen(true);
}, [setIsChangeOwnerPopupOpen]);
const onOpenSearchPopup = useCallback(() => {
dispatch(setIsSearchPopupOpen({ value: true }));
}, [dispatch]);
// マウント時のみ実行
useEffect(() => {
dispatch(getMyAccountAsync());
@ -169,7 +207,8 @@ const PartnerLicense: React.FC = (): JSX.Element => {
}, [myAccountInfo]);
// 現在の表示階層に合わせたボタン制御用
const [buttonLabel, setButtonLabel] = useState("");
const [showOrderHistoryButton, setShowOrderHistoryButton] = useState(false);
const [showViewDetailsButton, setShowViewDetailsButton] = useState(false);
// パンくずリスト用stateに自アカウントを追加
useEffect(() => {
@ -187,15 +226,17 @@ const PartnerLicense: React.FC = (): JSX.Element => {
);
}
// 表内のボタン表示判定
if (hierarchicalElements.length === 1 && ownPartnerLicenseInfo.tier !== 4) {
setButtonLabel(
t(getTranslationID("partnerLicense.label.orderHistoryButton"))
);
if (ownPartnerLicenseInfo.tier !== 4) {
setShowOrderHistoryButton(true);
setShowViewDetailsButton(false);
} else if (ownPartnerLicenseInfo.tier === 4) {
setButtonLabel(t(getTranslationID("partnerLicense.label.viewDetails")));
setShowOrderHistoryButton(true);
setShowViewDetailsButton(true);
} else {
setButtonLabel("");
setShowOrderHistoryButton(false);
setShowViewDetailsButton(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ownPartnerLicenseInfo]);
@ -214,6 +255,21 @@ const PartnerLicense: React.FC = (): JSX.Element => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hierarchicalElements, currentPage]);
// パートナーライセンス画面からも検索ポップアップからもOrder History/View Detailsが表示されていない時に表示
const isVisiblePartnerLicensePage = useMemo(
() =>
!isLicenseOrderHistoryInSearchOpen &&
!isViewDetailsInSearchOpen &&
!isLicenseOrderHistoryOpen &&
!isViewDetailsOpen,
[
isLicenseOrderHistoryInSearchOpen,
isViewDetailsInSearchOpen,
isLicenseOrderHistoryOpen,
isViewDetailsOpen,
]
);
return (
<>
{/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
@ -231,24 +287,37 @@ const PartnerLicense: React.FC = (): JSX.Element => {
}}
/>
)}
{islicenseOrderHistoryOpen && (
{isLicenseOrderHistoryOpen && (
<LicenseOrderHistory
onReturn={() => {
setIslicenseOrderHistoryOpen(false);
dispatch(setIsLicenseOrderHistoryOpen({ value: false }));
}}
selectedRow={selectedRow}
/>
)}
{isViewDetailsOpen && (
<LicenseSummary
onReturn={() => {
setIsViewDetailsOpen(false);
dispatch(setIsViewDetailsOpen({ value: false }));
}}
selectedRow={selectedRow}
/>
)}
{isChangeOwnerPopupOpen && (
<ChangeOwnerPopup
onClose={() => {
setIsChangeOwnerPopupOpen(false);
}}
/>
)}
{!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
{isVisiblePartnerSearch() && isSearchPopupOpen && (
<SearchPartnerPopup
onClose={() => dispatch(setIsSearchPopupOpen({ value: true }))}
/>
)}
{isVisiblePartnerLicensePage && (
<div className={styles.wrap}>
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div className="">
<div className={styles.pageHeader}>
@ -329,6 +398,42 @@ const PartnerLicense: React.FC = (): JSX.Element => {
</a>
)}
</li>
<li>
{isVisibleChangeOwner(ownPartnerLicenseInfo.tier) && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={onClickChangeOwner}
>
<img
src={changeOwnerIcon}
alt=""
className={styles.menuIcon}
/>
{t(
getTranslationID(
"partnerLicense.label.changeOwnerButton"
)
)}
</a>
)}
</li>
<li className={styles.floatRight}>
{isVisiblePartnerSearch() && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<a
className={`${styles.menuLink} ${styles.isActive} ${styles.alignRight}`}
onClick={onOpenSearchPopup}
>
<img
src={searchIcon}
alt="search"
className={styles.menuIcon}
/>
{t(getTranslationID("partnerLicense.label.search"))}
</a>
)}
</li>
</ul>
<ul className={styles.brCrumbLicense}>
{hierarchicalElements.map((value) => (
@ -355,6 +460,13 @@ const PartnerLicense: React.FC = (): JSX.Element => {
<th>
{t(getTranslationID("partnerLicense.label.stockLicense"))}
</th>
<th>
{t(
getTranslationID(
"partnerLicense.label.allocatedLicense"
)
)}
</th>
<th>
{t(
getTranslationID("partnerLicense.label.issueRequested")
@ -380,12 +492,13 @@ const PartnerLicense: React.FC = (): JSX.Element => {
? ownPartnerLicenseInfo.stockLicense
: "-"}
</td>
<td>-</td>
<td>{ownPartnerLicenseInfo.issuedRequested}</td>
<td>
<span
className={
ownPartnerLicenseInfo.shortage > 0 &&
ownPartnerLicenseInfo.tier !== 1
ownPartnerLicenseInfo.tier !== 1
? styles.isAlert
: ""
}
@ -423,6 +536,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
<td>{tierNames[value.tier]}</td>
<td>{value.accountId}</td>
<td>{value.stockLicense}</td>
<td>{value.tier === 5 ? value.allocatedLicense : "-"}</td>
<td>{value.issuedRequested}</td>
<td>
<span
@ -439,20 +553,38 @@ const PartnerLicense: React.FC = (): JSX.Element => {
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${
buttonLabel ? styles.isActive : ""
}`}
className={`${styles.menuLink} ${showOrderHistoryButton ? styles.isActive : ""
}`}
onClick={() => {
if (ownPartnerLicenseInfo.tier === 4) {
onClickViewDetails(value);
} else {
onClickOrderHistory(value);
}
onClickOrderHistory(value);
}}
>
{buttonLabel}
{t(
getTranslationID(
"partnerLicense.label.orderHistoryButton"
)
)}
</a>
</li>
<li>
{/* Second button (only if tier is 4) */}
{showViewDetailsButton && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<a
className={`${styles.menuLink} ${showViewDetailsButton ? styles.isActive : ""
}`}
onClick={() => {
onClickViewDetails(value);
}}
>
{t(
getTranslationID(
"partnerLicense.label.viewDetails"
)
)}
</a>
)}
</li>
</ul>
</td>
</tr>
@ -480,9 +612,8 @@ const PartnerLicense: React.FC = (): JSX.Element => {
onClick={() => {
movePage(0);
}}
className={` ${
!isLoading && currentPage !== 1 ? styles.isActive : ""
}`}
className={` ${!isLoading && currentPage !== 1 ? styles.isActive : ""
}`}
>
«
</a>
@ -491,25 +622,22 @@ const PartnerLicense: React.FC = (): JSX.Element => {
onClick={() => {
movePage((currentPage - 2) * ACCOUNTS_VIEW_LIMIT);
}}
className={`${
!isLoading && currentPage !== 1 ? styles.isActive : ""
}`}
className={`${!isLoading && currentPage !== 1 ? styles.isActive : ""
}`}
>
</a>
{` ${total !== 0 ? currentPage : 0} of ${
total !== 0 ? totalPage : 0
} `}
{` ${total !== 0 ? currentPage : 0} of ${total !== 0 ? totalPage : 0
} `}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
onClick={() => {
movePage(currentPage * ACCOUNTS_VIEW_LIMIT);
}}
className={`${
!isLoading && currentPage < totalPage
className={`${!isLoading && currentPage < totalPage
? styles.isActive
: ""
}`}
}`}
>
</a>
@ -518,11 +646,10 @@ const PartnerLicense: React.FC = (): JSX.Element => {
onClick={() => {
movePage((totalPage - 1) * ACCOUNTS_VIEW_LIMIT);
}}
className={` ${
!isLoading && currentPage < totalPage
className={` ${!isLoading && currentPage < totalPage
? styles.isActive
: ""
}`}
}`}
>
»
</a>
@ -545,4 +672,14 @@ const PartnerLicense: React.FC = (): JSX.Element => {
);
};
const isVisibleChangeOwner = (partnerTier: number) =>
// 自身が第一階層または第二階層で、表示しているパートナーが第三階層または第四階層の場合のみ表示
isApproveTier([TIERS.TIER1, TIERS.TIER2]) &&
(partnerTier.toString() === TIERS.TIER3 ||
partnerTier.toString() === TIERS.TIER4);
const isVisiblePartnerSearch = () =>
// 自身が第一階層〜第四階層の場合のみ表示
isApproveTier([TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4]);
export default PartnerLicense;

View File

@ -0,0 +1,354 @@
import { SearchPartner } from "api";
import React, {
useState,
useEffect,
useRef,
useCallback,
useMemo,
} from "react";
import { AppDispatch } from "app/store";
import styles from "styles/app.module.scss";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import { useSelector, useDispatch } from "react-redux";
import {
changeSelectedRow,
cleanupSearchResult,
cleanupPartnerHierarchy,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
searchPartnersAsync,
selectIsLoading,
selectSelectedRow,
selectSearchResult,
selectPartnerHierarchy,
selectIsLicenseOrderHistoryOpen,
selectIsViewDetailsOpen,
getPartnerHierarchy,
} from "features/license/searchPartner";
import { setIsSearchPopupOpen } from "features/license/partnerLicense";
import { LicenseSummary } from "./licenseSummary";
import { LicenseOrderHistory } from "./licenseOrderHistory";
import close from "../../assets/images/close.svg";
import searchIcon from "../../assets/images/search.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
interface SearchPopupProps {
onClose: () => void;
}
export const SearchPartnerPopup: React.FC<SearchPopupProps> = (props) => {
const dispatch: AppDispatch = useDispatch();
const { onClose } = props;
const [t] = useTranslation();
const isLoading = useSelector(selectIsLoading);
const selectedRow = useSelector(selectSelectedRow) as SearchPartner;
const searchResult = useSelector(selectSearchResult);
const partnerHierarchy = useSelector(selectPartnerHierarchy);
const [accountId, setAccountId] = useState("");
const [companyName, setCompanyName] = useState("");
const [isBreadcrumbOpen, setIsBreadcrumbOpen] = useState(false);
const isViewDetailsOpen = useSelector(selectIsViewDetailsOpen);
const isLicenseOrderHistoryOpen = useSelector(
selectIsLicenseOrderHistoryOpen
);
const [popupPosition, setPopupPosition] = useState<{ x: number; y: number }>({
x: 0,
y: 0,
});
const breadcrumbRef = useRef<HTMLDivElement>(null);
// フォームの入力チェック
const searchButtonEnabled = useMemo(() => {
// 両方入力がない場合はボタンを活性化しない。
if (!companyName && !accountId) {
return false;
}
// Company Nameは3文字以上入力がない場合はボタンを活性化しない。
// サロゲートペアを考慮して、スプレッド構文でリスト化してから文字数をカウントする
// 絵文字が入力された場合は救えないが、そもそも入力されても検索できないので考慮しない。
if (companyName && [...companyName].length <= 2) {
return false;
}
// Account IDは数字ではないまたは0以下の場合はボタンを活性化しない。
if (
accountId &&
(Number.isNaN(Number(accountId)) || Number(accountId) <= 0)
) {
return false;
}
return true;
}, [companyName, accountId]);
const requestSearch = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!searchButtonEnabled) return;
dispatch(
searchPartnersAsync({
companyName,
accountId: Number(accountId),
})
);
},
[dispatch, companyName, accountId, searchButtonEnabled]
);
const handleAccountNameClick = useCallback(
async (clickAccountId: number, event: React.MouseEvent) => {
// ロード中はなにもしない。
if (isLoading) return;
event.stopPropagation();
// アカウントの階層を取得
await dispatch(getPartnerHierarchy({ accountId: clickAccountId }));
setPopupPosition({ x: event.clientX, y: event.clientY });
setIsBreadcrumbOpen(true);
},
[dispatch, setPopupPosition, setIsBreadcrumbOpen, isLoading]
);
const closeBreadcrumbPopup = () => {
setIsBreadcrumbOpen(false);
};
// ポップアップ外のクリックポップアップ非表示するイベント
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
breadcrumbRef.current &&
!breadcrumbRef.current.contains(event.target as Node)
) {
closeBreadcrumbPopup();
}
};
if (isBreadcrumbOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isBreadcrumbOpen]);
const tierNames: { [key: number]: string } = {
// eslint-disable-next-line @typescript-eslint/naming-convention
1: t(getTranslationID("common.label.tier1")),
// eslint-disable-next-line @typescript-eslint/naming-convention
2: t(getTranslationID("common.label.tier2")),
// eslint-disable-next-line @typescript-eslint/naming-convention
3: t(getTranslationID("common.label.tier3")),
// eslint-disable-next-line @typescript-eslint/naming-convention
4: t(getTranslationID("common.label.tier4")),
// eslint-disable-next-line @typescript-eslint/naming-convention
5: t(getTranslationID("common.label.tier5")),
};
const openViewDetails = useCallback(
(value: SearchPartner) => {
dispatch(changeSelectedRow({ value }));
dispatch(setIsViewDetailsOpen({ value: true }));
},
[dispatch]
);
const openOrderHistory = useCallback(
(value?: SearchPartner) => {
dispatch(changeSelectedRow({ value }));
dispatch(setIsLicenseOrderHistoryOpen({ value: true }));
},
[dispatch]
);
const handleModalClose = useCallback(() => {
// ポップアップ閉じたら、検索結果をクリアする
dispatch(cleanupSearchResult());
dispatch(cleanupPartnerHierarchy());
onClose();
dispatch(setIsSearchPopupOpen({ value: false }));
}, [dispatch, onClose]);
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div>
{isViewDetailsOpen && (
<div>
<LicenseSummary
onReturn={() => dispatch(setIsViewDetailsOpen({ value: false }))}
selectedRow={selectedRow}
/>
</div>
)}
{isLicenseOrderHistoryOpen && (
<div>
<LicenseOrderHistory
onReturn={() =>
dispatch(setIsLicenseOrderHistoryOpen({ value: false }))
}
selectedRow={selectedRow}
/>
</div>
)}
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.searchModalBox}>
<div className={styles.headerContainer}>
<p className={styles.modalTitle}>
{t(getTranslationID("searchPartnerAccountPopupPage.label.title"))}
</p>
<form
className={styles.searchBar}
onSubmit={(e) => requestSearch(e)}
>
<input
type="text"
placeholder={t(getTranslationID("partnerLicense.label.name"))}
value={companyName}
onChange={(e) => setCompanyName(e.target.value.trimStart())}
className={styles.searchInput}
/>
<input
type="text"
placeholder={t(
getTranslationID("partnerLicense.label.accountId")
)}
value={accountId}
onChange={(e) => setAccountId(e.target.value.trimStart())}
className={styles.searchInput}
/>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<button
className={`${styles.menuLink} ${!isLoading && searchButtonEnabled ? styles.isActive : ""
}`}
type="submit"
disabled={!searchButtonEnabled || isLoading}
>
<img
src={searchIcon}
alt="search"
className={styles.menuIcon}
/>
{t(getTranslationID("partnerLicense.label.search"))}
</button>
<button type="button" onClick={handleModalClose}>
<img
src={close}
className={styles.modalTitleIcon}
alt="close"
/>
</button>
</form>
</div>
<table
className={`${styles.table} ${styles.partner} ${styles.marginBtm3}`}
>
<tr className={styles.tableHeader}>
<th>
<a>{t(getTranslationID("partnerPage.label.name"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.category"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.accountId"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.country"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.primaryAdmin"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.email"))}</a>
</th>
<th>
<a>{"" /** Order History、View Details用の空カラム */}</a>
</th>
</tr>
{searchResult.map((result) => (
<tr key={result.accountId}>
<td
onClick={(event) =>
handleAccountNameClick(result.accountId, event)
}
className={styles.hoverBlue}
>
{result.name}
</td>
<td>{tierNames[result.tier]}</td>
<td>{result.accountId}</td>
<td>{result.country}</td>
<td>{result.primaryAdmin}</td>
<td>{result.email ?? "-"}</td>
<td>
<ul className={`${styles.menuAction} ${styles.inTable}`}>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
openOrderHistory(result);
}}
>
{t(
getTranslationID(
"partnerLicense.label.orderHistoryButton"
)
)}
</a>
</li>
{result.tier === 5 && (
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
openViewDetails(result);
}}
>
{t(
getTranslationID("partnerLicense.label.viewDetails")
)}
</a>
</li>
)}
</ul>
</td>
</tr>
))}
</table>
{searchResult.length === 0 && (
<p style={{ margin: "10px", textAlign: "center" }}>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{/* ローディングオーバーレイ */}
<div
style={{ display: isLoading ? "inline" : "none" }}
className={styles.overlay}
>
<img
src={progress_activit}
className={`${styles.icLoading} ${styles.alignCenter}`}
alt="Loading"
/>
</div>
</div>
{isBreadcrumbOpen && (
<div
ref={breadcrumbRef}
className={styles.breadcrumbPopup}
style={{ top: popupPosition.y, left: popupPosition.x }}
>
<ul className={styles.brCrumbPartner}>
{partnerHierarchy.map((parent) => (
<li key={parent.tier}>
<span>{parent.name}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,123 @@
import React, { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { AppDispatch } from "app/store";
import { useDispatch, useSelector } from "react-redux";
import {
issueTrialLicenseAsync,
cleanupApps,
selectIsLoading,
selectNumberOfLicenses,
selectExpirationDate,
setExpirationDate,
} from "features/license/licenseTrialIssue";
import styles from "../../styles/app.module.scss";
import { getTranslationID } from "../../translation";
import close from "../../assets/images/close.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import { SearchPartner, PartnerLicenseInfo } from "../../api";
interface TrialLicenseIssuePopupProps {
onClose: () => void;
selectedRow?: PartnerLicenseInfo | SearchPartner;
}
export const TrialLicenseIssuePopup: React.FC<TrialLicenseIssuePopupProps> = (
props
) => {
const { onClose, selectedRow } = props;
const { t } = useTranslation();
const dispatch: AppDispatch = useDispatch();
const isLoading = useSelector(selectIsLoading);
const numberOfLicenses = useSelector(selectNumberOfLicenses);
const expirationDate = useSelector(selectExpirationDate);
useEffect(
() => () => {
// useEffectのreturnとしてcleanupAppsを実行することで、ポップアップのアンマウント時に初期化を行う
dispatch(cleanupApps());
},
[dispatch]
);
// ポップアップ表示時
useEffect(() => {
// トライアルライセンスの有効期限を設定。
dispatch(setExpirationDate());
}, [dispatch]);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
if (isLoading) {
return;
}
onClose();
}, [isLoading, onClose]);
// 発行ボタン押下時
const onIssueTrialLicense = useCallback(async () => {
// トライアルライセンス発行APIの呼び出し
const { meta } = await dispatch(issueTrialLicenseAsync({ selectedRow }));
if (meta.requestStatus === "fulfilled") {
closePopup();
}
}, [dispatch, closePopup, selectedRow]);
// HTML
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("trialLicenseIssuePopupPage.label.title"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle}>
{t(getTranslationID("trialLicenseIssuePopupPage.label.subTitle"))}
</dt>
<dt className={styles.overLine}>
{t(
getTranslationID(
"trialLicenseIssuePopupPage.label.numberOfLicenses"
)
)}
</dt>
<dd>{numberOfLicenses}</dd>
<dt>
{t(
getTranslationID(
"trialLicenseIssuePopupPage.label.expirationDate"
)
)}
</dt>
<dd>{expirationDate}</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(
getTranslationID(
"trialLicenseIssuePopupPage.label.issueButton"
)
)}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading ? styles.isActive : ""
}`}
onClick={onIssueTrialLicense}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -110,6 +110,7 @@ export const AddPartnerAccountPopup: React.FC<AddPartnerAccountPopup> = (
country,
adminName,
email,
offset,
]);
return (

View File

@ -0,0 +1,178 @@
import { AppDispatch } from "app/store";
import React, { useCallback, useEffect } from "react";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { getTranslationID } from "translation";
import { useTranslation } from "react-i18next";
import {
changeEditCompanyName,
changeSelectedAdminId,
cleanupPartnerAccount,
getPartnerUsersAsync,
editPartnerInfoAsync,
selectEditPartnerCompanyName,
selectEditPartnerCountry,
selectEditPartnerId,
selectEditPartnerUsers,
selectIsLoading,
selectSelectedAdminId,
selectOffset,
getPartnerInfoAsync,
LIMIT_PARTNER_VIEW_NUM,
} from "features/partner";
import close from "../../assets/images/close.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import { COUNTRY_LIST } from "../SignupPage/constants";
interface EditPartnerAccountPopup {
isOpen: boolean;
onClose: () => void;
}
export const EditPartnerAccountPopup: React.FC<EditPartnerAccountPopup> = (
props
) => {
const { isOpen, onClose } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
const isLoading = useSelector(selectIsLoading);
const offset = useSelector(selectOffset);
const partnerId = useSelector(selectEditPartnerId);
const companyName = useSelector(selectEditPartnerCompanyName);
const country = useSelector(selectEditPartnerCountry);
const users = useSelector(selectEditPartnerUsers);
const adminUser = users.find((user) => user.isPrimaryAdmin);
const selectedAdminId = useSelector(selectSelectedAdminId);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
if (isLoading) {
return;
}
dispatch(cleanupPartnerAccount());
onClose();
}, [isLoading, onClose, dispatch]);
useEffect(() => {
if (isOpen) {
dispatch(getPartnerUsersAsync({ accountId: partnerId }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
const onEditPartner = useCallback(async () => {
// eslint-disable-next-line no-alert
if (!window.confirm(t(getTranslationID("common.message.dialogConfirm")))) {
return;
}
const { meta } = await dispatch(editPartnerInfoAsync());
if (meta.requestStatus === "fulfilled") {
dispatch(
getPartnerInfoAsync({
limit: LIMIT_PARTNER_VIEW_NUM,
offset,
})
);
closePopup();
}
}, [dispatch, closePopup, t, offset]);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("partnerPage.label.editAccount"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle}>
{t(getTranslationID("partnerPage.label.accountInformation"))}
</dt>
<dt>{t(getTranslationID("partnerPage.label.name"))}</dt>
<dd>
<input
type="text"
size={40}
maxLength={255}
value={companyName}
className={styles.formInput}
onChange={(e) => {
dispatch(
changeEditCompanyName({ companyName: e.target.value })
);
}}
/>
</dd>
<dt>{t(getTranslationID("partnerPage.label.country"))}</dt>
<dd className={styles.last}>
<input
type="text"
size={40}
value={COUNTRY_LIST.find((c) => c.value === country)?.label}
className={styles.formInput}
readOnly
/>
</dd>
<dt className={styles.formTitle}>
{t(getTranslationID("partnerPage.label.primaryAdminInfo"))}
</dt>
<dt>{t(getTranslationID("partnerPage.label.adminName"))}</dt>
<dd>
<input
type="text"
size={40}
value={adminUser?.name}
className={styles.formInput}
readOnly
/>
</dd>
<dt>{t(getTranslationID("partnerPage.label.email"))}</dt>
<dd className={styles.last}>
<select
className={styles.formInput}
onChange={(event) => {
dispatch(
changeSelectedAdminId({
adminId: Number(event.target.value),
})
);
}}
value={selectedAdminId}
>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.email}
</option>
))}
</select>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(getTranslationID("partnerPage.label.saveChanges"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading && companyName.length !== 0 ? styles.isActive : ""
}`}
onClick={onEditPartner}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -1,6 +1,5 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import { AppDispatch } from "app/store";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import Footer from "components/footer";
import Header from "components/header";
import React, { useCallback, useEffect, useState } from "react";
@ -16,23 +15,28 @@ import {
selectTotalPage,
getPartnerInfoAsync,
selectPartnersInfo,
deletePartnerAccountAsync,
} from "features/partner/index";
import {
changeDelegateAccount,
changeEditPartner,
savePageInfo,
} from "features/partner/partnerSlice";
import { getTranslationID } from "translation";
import { useTranslation } from "react-i18next";
import { getDelegationTokenAsync } from "features/auth/operations";
import { useNavigate } from "react-router-dom";
import { Partner } from "api";
import personAdd from "../../assets/images/person_add.svg";
import { TIERS } from "../../components/auth/constants";
import { AddPartnerAccountPopup } from "./addPartnerAccountPopup";
import { EditPartnerAccountPopup } from "./editPartnerAccountPopup";
import checkFill from "../../assets/images/check_fill.svg";
const PartnerPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [isEditPopupOpen, setIsEditPopupOpen] = useState(false);
const [t] = useTranslation();
const navigate = useNavigate();
const total = useSelector(selectTotal);
@ -71,6 +75,19 @@ const PartnerPage: React.FC = (): JSX.Element => {
const onOpen = useCallback(() => {
setIsPopupOpen(true);
}, [setIsPopupOpen]);
const onOpenEditPopup = useCallback(
(editPartner: Partner) => {
dispatch(
changeEditPartner({
id: editPartner.accountId,
companyName: editPartner.name,
country: editPartner.country,
})
);
setIsEditPopupOpen(true);
},
[setIsEditPopupOpen, dispatch]
);
// パートナー取得APIを呼び出す
useEffect(() => {
@ -109,6 +126,31 @@ const PartnerPage: React.FC = (): JSX.Element => {
[dispatch, navigate, t]
);
// delete account押下時処理
const onDeleteAccount = useCallback(
async (accountId: number, companyName: string) => {
// ダイアログ確認
if (
/* eslint-disable-next-line no-alert */
!window.confirm(
`${t(
getTranslationID("partnerPage.message.partnerDeleteConfirm")
)} ${companyName}`
)
) {
return;
}
const { meta } = await dispatch(deletePartnerAccountAsync({ accountId }));
if (meta.requestStatus === "fulfilled") {
dispatch(
getPartnerInfoAsync({ limit: LIMIT_PARTNER_VIEW_NUM, offset })
);
}
},
[dispatch, t, offset]
);
// HTML
return (
<>
@ -118,9 +160,14 @@ const PartnerPage: React.FC = (): JSX.Element => {
setIsPopupOpen(false);
}}
/>
<EditPartnerAccountPopup
isOpen={isEditPopupOpen}
onClose={() => {
setIsEditPopupOpen(false);
}}
/>
<div className={styles.wrap}>
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>
@ -185,10 +232,30 @@ const PartnerPage: React.FC = (): JSX.Element => {
<tr>
<td className={styles.clm0}>
<ul className={styles.menuInTable}>
{/* CCB
{isVisibleButton && (
<li>
<a>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={() => {
onOpenEditPopup(x);
}}
>
{t(
getTranslationID(
"partnerPage.label.editAccount"
)
)}
</a>
</li>
)}
{isVisibleButton && (
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={() => {
onDeleteAccount(x.accountId, x.name);
}}
>
{t(
getTranslationID(
"partnerPage.label.deleteAccount"
@ -197,7 +264,6 @@ const PartnerPage: React.FC = (): JSX.Element => {
</a>
</li>
)}
*/}
{isVisibleDealerManagement && (
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}

View File

@ -1,17 +1,24 @@
import React from "react";
import Header from "components/header";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import styles from "styles/app.module.scss";
import Footer from "components/footer";
const SupportPage: React.FC = () => {
const { t } = useTranslation();
// OMDS_IS-381 Support画面で表示する内容を充実したいの対応 2024年8月7日
const userGuideDivStyles: React.CSSProperties = {
padding: "2rem 2rem 4rem 2rem",
};
const appDLDivStyles: React.CSSProperties = {
padding: "2rem 2rem 0rem 2rem",
};
const customUlStyles: React.CSSProperties = { marginBottom: "1rem" };
return (
<div className={styles.wrap}>
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
@ -24,8 +31,8 @@ const SupportPage: React.FC = () => {
<div>
<h2>{t(getTranslationID("supportPage.label.howToUse"))}</h2>
<div className={styles.txContents}>
<ul className={styles.listDocument}>
<div className={styles.txContents} style={userGuideDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
href="https://download.omsystem.com/pages/odms_download/manual/odms_cloud/"
@ -41,10 +48,110 @@ const SupportPage: React.FC = () => {
{t(getTranslationID("supportPage.text.notResolved"))}
</p>
</div>
<h2>
{t(getTranslationID("supportPage.label.programDownload"))}
</h2>
<div className={styles.txContents} style={appDLDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
href="https://download.omsystem.com/pages/odms_download/odms_cloud_desktop/en/"
target="_blank"
className={styles.linkTx}
rel="noreferrer"
>
{t(
getTranslationID(
"supportPage.label.omdsDesktopAppDownloadLink"
)
)}
</a>
</li>
</ul>
<p className={styles.txNormal}>
{t(
getTranslationID(
"supportPage.label.omdsDesktopAppDownloadLinkDescription"
)
)}
</p>
</div>
<div className={styles.txContents} style={appDLDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
href="https://download.omsystem.com/pages/odms_download/device_customization_program/en/"
target="_blank"
className={styles.linkTx}
rel="noreferrer"
>
{t(getTranslationID("supportPage.label.dcpDownloadLink"))}
</a>
</li>
</ul>
<p className={styles.txNormal}>
{t(
getTranslationID(
"supportPage.label.dcpDownloadLinkDescription"
)
)}
</p>
</div>
<div className={styles.txContents} style={appDLDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
href="https://download.omsystem.com/pages/odms_download/odms_cloud_backup_extraction_tool/en/"
target="_blank"
className={styles.linkTx}
rel="noreferrer"
>
{t(
getTranslationID(
"supportPage.label.backupExtractionToolDownloadLink"
)
)}
</a>
</li>
</ul>
<p className={styles.txNormal}>
{t(
getTranslationID(
"supportPage.label.backupExtractionToolDownloadLinkDescription"
)
)}
</p>
</div>
<div className={styles.txContents} style={appDLDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
href="https://download.omsystem.com/pages/odms_download/odms_client_virtual_driver/en/"
target="_blank"
className={styles.linkTx}
rel="noreferrer"
>
{t(
getTranslationID(
"supportPage.label.virtualDriverDownloadLink"
)
)}
</a>
</li>
</ul>
<p className={styles.txNormal}>
{t(
getTranslationID(
"supportPage.label.virtualDriverDownloadLinkDescription"
)
)}
</p>
</div>
</div>
</section>
</div>
</main>
<Footer />
</div>
);
};

View File

@ -1,8 +1,7 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "app/store";
import Header from "components/header";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import undo from "assets/images/undo.svg";
@ -13,6 +12,7 @@ import {
selectTemplates,
listTemplateAsync,
selectIsLoading,
deleteTemplateAsync,
} from "features/workflow/template";
import { selectDelegationAccessToken } from "features/auth/selectors";
import { DelegationBar } from "components/delegate";
@ -35,6 +35,23 @@ export const TemplateFilePage: React.FC = () => {
dispatch(listTemplateAsync());
}, [dispatch]);
const onDeleteTemplate = useCallback(
async (templateFileId: number) => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(deleteTemplateAsync({ templateFileId }));
if (meta.requestStatus === "fulfilled") {
dispatch(listTemplateAsync());
}
},
[dispatch, t]
);
return (
<>
{isShowAddPopup && (
@ -51,7 +68,6 @@ export const TemplateFilePage: React.FC = () => {
>
{delegationAccessToken && <DelegationBar />}
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
@ -101,16 +117,17 @@ export const TemplateFilePage: React.FC = () => {
<td>{template.name}</td>
<td>
<ul className={`${styles.menuAction} ${styles.inTable}`}>
{/* CCB
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
href=""
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
onDeleteTemplate(template.id);
}}
>
{t(getTranslationID("common.label.delete"))}
</a>
</li>
*/}
</ul>
</td>
</tr>

View File

@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useState } from "react";
import Header from "components/header";
import Footer from "components/footer";
import styles from "styles/app.module.scss";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import progress_activit from "assets/images/progress_activit.svg";
import undo from "assets/images/undo.svg";
import group_add from "assets/images/group_add.svg";
@ -11,6 +10,7 @@ import {
selectTypistGroups,
selectIsLoading,
listTypistGroupsAsync,
deleteTypistGroupAsync,
} from "features/workflow/typistGroup";
import { AppDispatch } from "app/store";
import { useTranslation } from "react-i18next";
@ -47,6 +47,25 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
[setIsEditPopupOpen]
);
const onDeleteTypistGroup = useCallback(
async (typistGroupId: number) => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(
deleteTypistGroupAsync({ typistGroupId })
);
if (meta.requestStatus === "fulfilled") {
dispatch(listTypistGroupsAsync());
}
},
[dispatch, t]
);
useEffect(() => {
dispatch(listTypistGroupsAsync());
}, [dispatch]);
@ -73,7 +92,6 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
>
{delegationAccessToken && <DelegationBar />}
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
@ -142,6 +160,17 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
{t(getTranslationID("common.label.edit"))}
</a>
</li>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
onDeleteTypistGroup(group.id);
}}
>
{t(getTranslationID("common.label.delete"))}
</a>
</li>
</ul>
</td>
</tr>

View File

@ -28,12 +28,13 @@ import progress_activit from "../../assets/images/progress_activit.svg";
interface AllocateLicensePopupProps {
isOpen: boolean;
onClose: () => void;
clearUserSearchInputs: () => void;
}
export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
props
) => {
const { isOpen, onClose } = props;
const { isOpen, onClose, clearUserSearchInputs } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
@ -87,6 +88,7 @@ export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
if (meta.requestStatus === "fulfilled") {
closePopup();
clearUserSearchInputs();
dispatch(listUsersAsync());
}
}, [dispatch, closePopup, id, selectedlicenseId, hasErrorEmptyLicense]);
@ -219,8 +221,7 @@ export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
value={selectedlicenseId ?? ""}
>
<option value="" hidden>
{`--
${t(
{`-- ${t(
getTranslationID(
"allocateLicensePopupPage.label.dropDownHeading"
)

View File

@ -0,0 +1,258 @@
import { AppDispatch } from "app/store";
import React, { useState, useCallback } from "react";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { getTranslationID } from "translation";
import { useTranslation } from "react-i18next";
import {
selectIsLoading,
importUsersAsync,
changeImportFileName,
changeImportCsv,
selectImportFileName,
selectImportValidationErrors,
cleanupImportUsers,
} from "features/user";
import { parseCSV } from "common/parser";
import close from "../../assets/images/close.svg";
import download from "../../assets/images/download.svg";
import upload from "../../assets/images/upload.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
interface UserAddPopupProps {
isOpen: boolean;
onClose: () => void;
}
export const ImportPopup: React.FC<UserAddPopupProps> = (props) => {
const { isOpen, onClose } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
// AddUserの情報を取得
const closePopup = useCallback(() => {
setIsPushImportButton(false);
dispatch(cleanupImportUsers());
onClose();
}, [onClose, dispatch]);
const [isPushImportButton, setIsPushImportButton] = useState<boolean>(false);
const isLoading = useSelector(selectIsLoading);
const importFileName = useSelector(selectImportFileName);
const { invalidInput, duplicatedEmails, duplicatedAuthorIds, overMaxRow } =
useSelector(selectImportValidationErrors);
const onDownloadCsv = useCallback(() => {
// csvファイルダウンロード処理
const filename = `import_users.csv`;
const importCsvHeader = [
"name",
"email",
"role",
"author_id",
"auto_assign",
"notification",
"encryption",
"encryption_password",
"prompt",
].toString();
const blob = new Blob([importCsvHeader], {
type: "mime",
});
const blobURL = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobURL;
a.download = filename;
document.body.appendChild(a);
a.click();
a.parentNode?.removeChild(a);
}, []);
// ファイルが選択されたときの処理
const handleFileChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
// 選択されたファイルを取得(複数選択されても先頭を取得)
const file = event.target.files?.[0];
// ファイルが選択されていれば、storeに保存
if (file) {
const text = await file.text();
const users = await parseCSV(text.trimEnd());
dispatch(changeImportCsv({ users }));
dispatch(changeImportFileName({ fileName: file.name }));
}
// 同名のファイルを選択した場合、onChangeが発火しないため、valueをクリアする
event.target.value = "";
},
[dispatch]
);
const onImportUsers = useCallback(async () => {
setIsPushImportButton(true);
if (
invalidInput.length > 0 ||
duplicatedEmails.length > 0 ||
duplicatedAuthorIds.length > 0 ||
overMaxRow
) {
return;
}
await dispatch(importUsersAsync());
setIsPushImportButton(false);
}, [
dispatch,
invalidInput,
duplicatedEmails,
duplicatedAuthorIds,
overMaxRow,
]);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("userListPage.label.bulkImport"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl
className={`${styles.formList} ${styles.userImport} ${styles.hasbg}`}
>
<dd className={styles.full}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
style={{ marginInlineEnd: "350px", marginTop: "15px" }}
className={`${styles.menuLink} ${styles.isActive}`}
onClick={onDownloadCsv}
>
<img src={download} alt="" className={styles.menuIcon} />
{t(getTranslationID("userListPage.label.downloadCsv"))}
</a>
{t(getTranslationID("userListPage.text.downloadExplain"))}
</dd>
<dd className={styles.full}>
<label
style={{ marginInlineEnd: "350px", marginTop: "15px" }}
htmlFor="import"
className={`${styles.menuLink} ${styles.isActive}`}
>
<input
type="file"
id="import"
style={{ display: "none" }}
onChange={handleFileChange}
/>
<img src={upload} alt="" className={styles.menuIcon} />
{t(getTranslationID("userListPage.label.importCsv"))}
</label>
</dd>
<dt className={styles.formTitle}>
{t(getTranslationID("userListPage.label.inputRules"))}
</dt>
<dt>{t(getTranslationID("userListPage.label.nameLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.nameRule"))}</dd>
<dt>
{t(getTranslationID("userListPage.label.emailAddressLabel"))}
</dt>
<dd>{t(getTranslationID("userListPage.text.emailAddressRule"))}</dd>
<dt>{t(getTranslationID("userListPage.label.roleLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.roleRule"))}</dd>
<dt>{t(getTranslationID("userListPage.label.authorIdLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.authorIdRule"))}</dd>
<dt>{t(getTranslationID("userListPage.label.autoRenewLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.autoRenewRule"))}</dd>
<dt>
{t(getTranslationID("userListPage.label.notificationLabel"))}
</dt>
<dd>{t(getTranslationID("userListPage.text.notificationRule"))}</dd>
<dt>{t(getTranslationID("userListPage.label.encryptionLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.encryptionRule"))}</dd>
<dt>
{t(
getTranslationID("userListPage.label.encryptionPasswordLabel")
)}
</dt>
<dd>
{t(getTranslationID("userListPage.text.encryptionPasswordRule"))}
</dd>
<dt>{t(getTranslationID("userListPage.label.promptLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.promptRule"))}</dd>
<dd className={styles.full}>
{isPushImportButton && overMaxRow && (
<span className={styles.formError}>
{t(getTranslationID("userListPage.message.overMaxUserError"))}
</span>
)}
{isPushImportButton && invalidInput.length > 0 && (
<>
<span className={styles.formError}>
{t(
getTranslationID("userListPage.message.invalidInputError")
)}
</span>
<span className={styles.formError}>
{invalidInput.map((row) => `L${row}`).join(", ")}
</span>
</>
)}
{isPushImportButton && duplicatedEmails.length > 0 && (
<>
<span className={styles.formError}>
{t(
getTranslationID(
"userListPage.message.duplicateEmailError"
)
)}
</span>
<span className={styles.formError}>
{duplicatedEmails.map((row) => `L${row}`).join(", ")}
</span>
</>
)}
{isPushImportButton && duplicatedAuthorIds.length > 0 && (
<>
<span className={styles.formError}>
{t(
getTranslationID(
"userListPage.message.duplicateAuthorIdError"
)
)}
</span>
<span className={styles.formError}>
{duplicatedAuthorIds.map((row) => `L${row}`).join(", ")}
</span>
</>
)}
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(getTranslationID("userListPage.label.addUsers"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading && importFileName !== undefined
? styles.isActive
: ""
}`}
onClick={onImportUsers}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -3,13 +3,14 @@ import React, { useCallback, useEffect, useState } from "react";
import Header from "components/header";
import Footer from "components/footer";
import styles from "styles/app.module.scss";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import { useDispatch, useSelector } from "react-redux";
import {
listUsersAsync,
selectUserViews,
selectIsLoading,
deallocateLicenseAsync,
deleteUserAsync,
confirmUserForceAsync,
} from "features/user";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
@ -31,9 +32,12 @@ import personAdd from "../../assets/images/person_add.svg";
import checkFill from "../../assets/images/check_fill.svg";
import checkOutline from "../../assets/images/check_outline.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import upload from "../../assets/images/upload.svg";
import searchIcon from "../../assets/images/search.svg";
import { UserAddPopup } from "./popup";
import { UserUpdatePopup } from "./updatePopup";
import { AllocateLicensePopup } from "./allocateLicensePopup";
import { ImportPopup } from "./importPopup";
const UserListPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
@ -45,6 +49,9 @@ const UserListPage: React.FC = (): JSX.Element => {
const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false);
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
useState(false);
const [isImportPopupOpen, setIsImportPopupOpen] = useState(false);
const [searchEmail, setSearchEmail] = useState("");
const [searchUserName, setSearchUserName] = useState("");
const onOpen = useCallback(() => {
setIsPopupOpen(true);
@ -65,6 +72,9 @@ const UserListPage: React.FC = (): JSX.Element => {
},
[setIsAllocateLicensePopupOpen, dispatch]
);
const onImportPopupOpen = useCallback(() => {
setIsImportPopupOpen(true);
}, [setIsImportPopupOpen]);
const onLicenseDeallocation = useCallback(
async (userId: number) => {
@ -78,12 +88,72 @@ const UserListPage: React.FC = (): JSX.Element => {
const { meta } = await dispatch(deallocateLicenseAsync({ userId }));
if (meta.requestStatus === "fulfilled") {
clearUserSearchInputs();
dispatch(listUsersAsync());
}
},
[dispatch, t]
);
const onDeleteUser = useCallback(
async (userId: number) => {
// ダイアログ確認
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(deleteUserAsync({ userId }));
if (meta.requestStatus === "fulfilled") {
clearUserSearchInputs();
dispatch(listUsersAsync());
}
},
[dispatch, t]
);
const onForceEmailVerification = useCallback(
async (userId: number) => {
// ダイアログ確認
if (
/* eslint-disable-next-line no-alert */
!window.confirm(
t(
getTranslationID(
"userListPage.message.forceEmailVerificationConfirm"
)
)
)
) {
return;
}
const { meta } = await dispatch(confirmUserForceAsync({ userId }));
if (meta.requestStatus === "fulfilled") {
clearUserSearchInputs();
dispatch(listUsersAsync());
}
},
[dispatch, t]
);
const requestSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch(
listUsersAsync({
userInputUserName: searchUserName,
userInputEmail: searchEmail,
})
);
};
const clearUserSearchInputs = useCallback(() => {
setSearchEmail("");
setSearchUserName("");
}, [setSearchEmail, setSearchUserName]);
useEffect(() => {
// ユーザ一覧取得処理を呼び出す
dispatch(listUsersAsync());
@ -102,18 +172,27 @@ const UserListPage: React.FC = (): JSX.Element => {
onClose={() => {
setIsUpdatePopupOpen(false);
}}
clearUserSearchInputs={clearUserSearchInputs}
/>
<UserAddPopup
isOpen={isPopupOpen}
onClose={() => {
setIsPopupOpen(false);
}}
clearUserSearchInputs={clearUserSearchInputs}
/>
<AllocateLicensePopup
isOpen={isAllocateLicensePopupOpen}
onClose={() => {
setIsAllocateLicensePopupOpen(false);
}}
clearUserSearchInputs={clearUserSearchInputs}
/>
<ImportPopup
isOpen={isImportPopupOpen}
onClose={() => {
setIsImportPopupOpen(false);
}}
/>
<div
className={`${styles.wrap} ${
@ -122,7 +201,6 @@ const UserListPage: React.FC = (): JSX.Element => {
>
{delegationAccessToken && <DelegationBar />}
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div className="">
<div className={styles.pageHeader}>
@ -146,6 +224,60 @@ const UserListPage: React.FC = (): JSX.Element => {
{t(getTranslationID("userListPage.label.addUser"))}
</a>
</li>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={onImportPopupOpen}
>
<img src={upload} alt="" className={styles.menuIcon} />
{t(getTranslationID("userListPage.label.bulkImport"))}
</a>
</li>
<li className={styles.floatRight}>
<form
className={styles.searchBar}
onSubmit={(e) => requestSearch(e)}
>
<input
type="text"
placeholder={t(
getTranslationID("userListPage.label.name")
)}
value={searchUserName}
onChange={(e) =>
setSearchUserName(e.target.value.trimStart())
}
className={styles.searchInput}
/>
<input
type="text"
placeholder={t(
getTranslationID("userListPage.label.email")
)}
value={searchEmail}
onChange={(e) =>
setSearchEmail(e.target.value.trimStart())
}
className={styles.searchInput}
/>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<button
className={`${styles.menuLink} ${
!isLoading ? styles.isActive : ""
}`}
type="submit"
disabled={isLoading}
>
<img
src={searchIcon}
alt="search"
className={styles.menuIcon}
/>
{t(getTranslationID("userListPage.label.search"))}
</button>
</form>
</li>
</ul>
<div className={styles.tableWrap}>
<table className={`${styles.table} ${styles.user}`}>
@ -255,9 +387,13 @@ const UserListPage: React.FC = (): JSX.Element => {
</li>
</>
)}
{/* CCB
<li>
<a href="">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={() => {
onDeleteUser(user.id);
}}
>
{t(
getTranslationID(
"userListPage.label.deleteUser"
@ -265,7 +401,23 @@ const UserListPage: React.FC = (): JSX.Element => {
)}
</a>
</li>
*/}
{/* 第五階層の管理者が、メール認証済みではないユーザーの行をマウスオーバーしている場合のみ */}
{isTier5 && !user.emailVerified && (
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={() => {
onForceEmailVerification(user.id);
}}
>
{t(
getTranslationID(
"userListPage.label.forceEmailVerification"
)
)}
</a>
</li>
)}
</ul>
</td>
<td> {user.name}</td>

View File

@ -28,10 +28,11 @@ import progress_activit from "../../assets/images/progress_activit.svg";
interface UserAddPopupProps {
isOpen: boolean;
onClose: () => void;
clearUserSearchInputs: () => void;
}
export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
const { isOpen, onClose } = props;
const { isOpen, onClose, clearUserSearchInputs } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true);
@ -75,6 +76,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
if (meta.requestStatus === "fulfilled") {
closePopup();
clearUserSearchInputs();
dispatch(listUsersAsync());
}
}, [

View File

@ -28,10 +28,11 @@ import progress_activit from "../../assets/images/progress_activit.svg";
interface UserUpdatePopupProps {
isOpen: boolean;
onClose: () => void;
clearUserSearchInputs: () => void;
}
export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
const { isOpen, onClose } = props;
const { isOpen, onClose, clearUserSearchInputs } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
const closePopup = useCallback(() => {
@ -79,6 +80,7 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
if (meta.requestStatus === "fulfilled") {
closePopup();
clearUserSearchInputs();
dispatch(listUsersAsync());
}
}, [

View File

@ -15,11 +15,8 @@ const UserVerifyPage: React.FC = (): JSX.Element => {
const jwt = query.get("verify") ?? "";
useEffect(() => {
if (!jwt) {
navigate("/mail-confirm/failed");
}
dispatch(userVerifyAsync({ jwt }));
}, [navigate, dispatch, jwt]);
}, [dispatch, jwt]);
const verifyState = useSelector(VerifyStateSelector);

Some files were not shown because too many files have changed in this diff Show More