Compare commits

..

158 Commits

Author SHA1 Message Date
xue jiahao
43ad0f5dd8 show domain page for all user
1. show ドメイン管理
2. hide ドメイン適用
2024-11-11 17:50:13 +08:00
方 柏
f5b5607297 Merged PR 2: APP バージョン 履歴管理
- python3.12.4
- add app table
- flow domainid->domainurl add app flowhistory
2024-11-11 07:03:49 +00:00
dd814993f1 flow domainid->domainurl add app flowhistory 2024-11-09 15:56:13 +09:00
9dce750ee5 add app table 2024-11-05 15:35:40 +09:00
2ffa1d9438 python3.12.4 2024-11-05 12:01:22 +09:00
xiaozhe.ma
9d853944cb 設計資料更新 2024-11-03 18:15:57 +09:00
Shohtetsu Ma
5a670e7ef9 Merged PR 101: Bug694:ツール以外でJavaScriptをURL指定でアップロードするとデプロイエラーになる
変更内容:
1.Bug694:ツール以外でアップロードされたファイルがURL指定の場合、デプロイを対応できるように修正しました。
 2. 階層化ドロップダウンの属性設定ダイアログの保存ボタン⇒確定ボタンへ変更

Related work items: #694
2024-10-02 05:50:32 +00:00
xiaozhe.ma
b8e76e0dc1 Bug694:ツール以外でJavaScriptをURL指定でアップロードするとデプロイエラーになる 2024-10-02 14:41:20 +09:00
Yukina Mori
3a29aad32e Merged PR 100: BUG678 [日時加減算]フロー上表示の結果変数修正
BUG678 [日時加減算]フロー上表示の変数が結果変数修正

Related work items: #678
2024-10-01 08:23:23 +00:00
Moriyukina2
b7dd258c0a BUG678 [日時加減算]フロー上表示の変数が結果変数修正 2024-10-01 17:01:26 +09:00
Yu Wang
2d7c9a5c3f Merged PR 99: Bug687修正(日付指定に値非対応時エラー表示ないBug)
Bug687修正(日付指定に値非対応時エラー表示ないBug)

Related work items: #687
2024-09-26 07:49:23 +00:00
王玉
9ddd3783f6 Bug687修正(日付指定に値非対応時エラー表示ないBug) 2024-09-26 15:09:33 +09:00
xiaozhe.ma
886969e941 デプロイ不具合修正 2024-09-25 11:39:50 +09:00
Shohtetsu Ma
ae67ec8751 Merged PR 98: 階層化ドロップダウンの実装完了
階層化ドロップダウンの実装完了しました。

Related work items: #227
2024-09-24 01:31:02 +00:00
Shohtetsu Ma
138ede6191 Merged PR 97: BUG680:値変更イベント適用しない設定は無効
修正内容:
下記値変更イベントをeventactionマスタについてしても適用されない問題を修正しました。
app.record.create.change
app.record.edit.change

Related work items: #680
2024-09-24 01:30:42 +00:00
xiaozhe.ma
f30ffaa137 Conflicts解決 2024-09-24 09:59:44 +09:00
xiaozhe.ma
30f44ca923 アクションをイベントに適用設定の不具合修正 2024-09-24 01:26:19 +09:00
xiaozhe.ma
58bf916810 階層化ドロップダウンの不具合修正、実装完了 2024-09-24 01:01:35 +09:00
Mouriya
843db5f10c レイアウトを改善し、ドロップダウンの対象をクリアするボタンを追加する、同じ値のラベルを持つ対象要素の選択を許可しない 2024-09-19 22:12:30 +09:00
Mouriya
1e4cb27998 数値が誤ってブール値として判断される問題を修正 2024-09-19 21:49:23 +09:00
Mouriya
df408ff2a8 未使用のキャッシュがクリアされる 2024-09-19 21:33:04 +09:00
Mouriya
32ffee0c93 ハッシュ計算関数にフィールド引数を追加, k-tune'でコンフィギュレーションを変更すると、キャッシュも無効になります。 2024-09-19 21:17:30 +09:00
Mouriya
7ec2b6df28 間違ったイテレータを修正. 不正なインデックス比較を修正 2024-09-19 21:03:00 +09:00
Mouriya
662af6a226 編集イベントに新しい関数でスタイルを追加する 2024-09-18 21:37:09 +09:00
Mouriya
4eb66684da 不適切なフィルター条件を修正 2024-09-17 10:06:46 +09:00
Mouriya
20ca47c004 コンパイルの失敗を修正 2024-09-13 15:17:16 +09:00
Shohtetsu Ma
90bcfc30b9 Merged PR 95: TASK675:デプロイ際、このアプリで生成したJavaScriptおよびCSS以外のファイルが削除されない対応
デプロイ際、このアプリで生成したJavaScriptおよびCSS以外のファイルが削除されないように対応しました

Related work items: #675
2024-09-13 05:46:37 +00:00
xiaozhe.ma
db8d942eaf デプロイ際、このアプリで生成したJavaScriptおよびCSS以外のファイルが削除されない対応 2024-09-13 14:29:38 +09:00
Yu Wang
c2793698c5 Merged PR 94: feat:#608月末算出,#607日付指定
feat:#608月末算出,#607日付指定

Related work items: #607, #608
2024-09-13 00:29:31 +00:00
王玉
1654387fe5 feat:#608月末算出,#607日付指定 2024-09-13 08:58:24 +09:00
Mouriya
9dd2ffd549 ファイル名の修正 2024-09-13 06:55:35 +09:00
Mouriya
04200193a8 カスケード式ドロップダウンメニューを追加するコード 2024-09-13 06:25:35 +09:00
Mouriya
91d52cb6e2 ダイアログはボタングループなしで使用できます。 2024-09-13 05:33:33 +09:00
Mouriya
fc510c6aec フィールドを選択すると、ブラックリストによって一部の結果がブロックされることがある。 2024-09-13 05:32:56 +09:00
Yukina Mori
d416b5e5cb Merged PR 93: feat: #258 日付の加算・減算の追加
feat: #258 日付の加算・減算の追加
日付の加算・減算コンポーネントの新規追加です。

Related work items: #258
2024-09-12 07:47:33 +00:00
tenraku ou
cc8e5389d4 Merged PR 92: 条件付き書式アクションを追加する(一覧) TASK267
条件付き書式アクションを追加する
2024-09-12 06:15:24 +00:00
Moriyukina2
8859a6c57a feat: #258 日付の加算・減算の追加
日付の加算・減算コンポーネントの新規追加です。
2024-09-12 15:11:44 +09:00
xiaozhe.ma
9f7b6f0c83 条件付き書式アクションを追加する 2024-09-10 09:34:23 +09:00
Shohtetsu Ma
b3c65fb4b5 Merged PR 91: feat:TASK617-681修正
feat:TASK617-681修正
617:属性更新時Toast表示
618:フィールドの表示件数、初期ソード順変更
その他:フローエディタの不具合修正

Related work items: #617, #618
2024-09-09 04:37:02 +00:00
Kanaru Tsuda
c907fd8473 Merged PR 90: feat:#594 全角/半角を変換する
指定したフィールドの文字列を全角/半角指定した書式に変換する

Related work items: #594
2024-09-09 04:36:52 +00:00
xiaozhe.ma
ad827c1dc8 feat:TASK617-681修正
617:属性更新時Toast表示
618:フィールドの表示件数、初期ソード順変更
その他:フローエディタの不具合修正
2024-09-06 09:08:56 +09:00
kanarutsuda
c7eb8171ef feat: #594 全角/半角を変換する
{detail}:指定したフィールドを全角か半角に変換する
2024-09-05 14:49:03 +09:00
Shohtetsu Ma
a7783987a8 Merged PR 89: すべてフローの一括保存機能追加
すべてフローの一括保存機能追加

Related work items: #614
2024-09-02 05:27:19 +00:00
xiaozhe.ma
3925a0a721 すべてフローの一括保存機能追加 2024-09-02 14:02:20 +09:00
Shohtetsu Ma
814e0b1842 Merged PR 87: [ルックアップ更新]条件式の不具合対応
[ルックアップ更新]条件式の不具合対応
1.条件式は更新先のフィールドを設定できるように変更しました
2.ルックアップ更新のDB登録情報も更新しました。
3.その他:色選択の必須設定追加

Related work items: #589
2024-08-29 03:55:00 +00:00
xiaozhe.ma
5fc03c6fe0 [ルックアップ更新]条件式の不具合対応 2024-08-29 12:33:00 +09:00
Shohtetsu Ma
60359ed9bd Merged PR 86: fix:BUG541 ユーザーフィールド未選択の際、Contains条件の不具合対応
fix:BUG541 ユーザーフィールド未選択の際、Contains条件の不具合対応
障害原因:配列値を比較する際、配列が空値の場合、containsとnotcontainsの比較がtrueで返されてしまう。
対策方法:配列が空値の場合、falseで返すように修正した。

又は、必須チェックの属性UIに、DB側必須チェックの設定を追加しました。

Related work items: #541
2024-08-27 05:05:36 +00:00
xiaozhe.ma
e492659dbf fix:BUG541 ユーザーフィールド未選択の際、Contains条件の不具合対応 2024-08-27 11:45:32 +09:00
xiaozhe.ma
c482b9ff5f 属性プロパティにチェックルール設定追加(DB変更) 2024-08-26 12:01:19 +09:00
Shohtetsu Ma
fcbbecea75 Merged PR 85: BUG600:ルックアップのエラーメッセージ表示障害
BUG600:ルックアップのエラーメッセージ表示障害修正。
障害原因:複数ルックアップの更新先は同一アプリ、エラーメッセージの表示はAPPごとに表示するので、
区別できなくなるため
修正方法:エラーメッセージは更新先のアプリ+キー項目名で特定するように変更する

Related work items: #600
2024-08-23 05:48:08 +00:00
xiaozhe.ma
329b28c459 BUG600:ルックアップのエラーメッセージ表示障害 2024-08-23 14:41:21 +09:00
Shohtetsu Ma
55e69380aa Merged PR 83: 属性UI(プロパティ)にチェックルール設定追加
属性UI(プロパティ)にチェックルール設定追加
属性UIのJSON定義に下記のフィールドを追加しました。
1.required : boolean  入力必須かどうかを設定する
2.requiredMessage: string  未入力の場合表示するエラーメッセージを設定する
  (未設定の場合既定メッセージを表示する)
3. rules: [アロー関数]
     必須チェック以外に、入力範囲など制限したい場合下記のように指定する
     [val=>!!val ||'数値を入力してください',val=>val<=100 && val>=1 || '1-100の範囲内の数値を入力してください']

Related work items: #299
2024-08-22 09:06:17 +00:00
xiaozhe.ma
af22f6e603 属性UI(プロパティ)にチェックルール設定追加 2024-08-22 17:15:49 +09:00
Shohtetsu Ma
1e3b2d6392 Merged PR 82: feat:アクション選択UIの改善
以下の内容を改修しました。
1.TASK581:アクションのカテゴリおよび並び順を設定できるようにする
2.各アクションは対応イベント以外に設置できないようにする
3.DB構造を変更しました。
  * action: categoryidとnosort列追加
  * category: アクションのカテゴリマスタ追加
  * eventaction: アクションごと設置できないイベントIDを登録
4.デプロイの際、scripts\kintoneToolDB_20240820_update.sqlを実行してDBを更新してください。

Related work items: #524, #581
2024-08-21 01:30:01 +00:00
xiaozhe.ma
d1634b6e81 feat:アクション選択の改善 2024-08-21 09:51:12 +09:00
Shohtetsu Ma
ccb64a020b Merged PR 81: ユーザー/ドメインの登録、編集のUI
下記の管理機能を追加しました。
1.ユーザー登録、編集、削除
2.ドメインの登録、編集、削除
3.ユーザーを使用可能なドメインの適用設定
2024-08-20 11:37:55 +00:00
xiaozhe.ma
c3f6de6733 feat:管理者機能追加 2024-08-20 17:21:30 +09:00
xiaozhe.ma
82ef3ebde0 feat:アクション選択UI改善 2024-08-20 14:49:35 +09:00
Mouriya
1cbb519c92 エラーを修正しました。 2024-08-19 21:54:02 +09:00
Mouriya
b5fa5cdf57 ユーザードメインページを調整し、ユーザーがシステム管理者の場合、他のユーザーのユーザードメインを管理できるようにしました。 2024-08-19 21:44:53 +09:00
Mouriya
acf8f0489d ユーザー管理ページを追加しました。 2024-08-19 21:25:05 +09:00
Mouriya
2f11323193 システム管理者専用のページを追加しました。 2024-08-19 21:24:38 +09:00
Mouriya
9eb87fe3f3 ドメインページのページネーションパラメータを調整しました。 2024-08-19 21:23:49 +09:00
Mouriya
fe311a2be4 分割線の間隔を調整 2024-08-19 21:22:47 +09:00
Mouriya
8b9f83ab25 ログイン後にユーザー情報を取得 2024-08-19 21:22:12 +09:00
Mouriya
22a8bf99ca インターフェースにオプションのuserIdを追加します。渡された場合はそのuserIdを使用し、渡されない場合はトークン内のIDを使用します。 2024-08-19 21:21:13 +09:00
Mouriya
f699e25090 新しいUserを作成したとき、UserDomainを維持する際に、開いているUserDomainが存在しない場合は、例外をスローする必要はありません。 2024-08-19 21:20:09 +09:00
Mouriya
cca7a1ba22 ユーザードメインページを最適化しました : ページネーションを無限大に設定し、仮想リストを有効にしました。 2024-08-19 11:56:17 +09:00
Mouriya
53e5a449d4 グローバル認証状態にpermission情報を追加 2024-08-19 11:21:24 +09:00
Shohtetsu Ma
c723b500b3 Merged PR 80: BUG582:UIメッセージの改善と統一
下記の箇所修正しました。
1.画面中UIメッセージの改善と統一(アクションの説明はDBの設定のため、含まれておりません)
2.kintoneドメイン登録、ユーザー使用可能なkintoneドメイン設定画面追加

Related work items: #582
2024-08-09 07:47:11 +00:00
xiaozhe.ma
4fcbf25233 BUG582:UIメッセージの改善と統一(画面上テキスト) 2024-08-09 16:34:46 +09:00
xiaozhe.ma
b3bc646147 Merge branch 'feature/domain-management' of https://dev.azure.com/rj-se-service-design/App%20Builder%20for%20kintone/_git/App%20Builder%20for%20kintone into feature/domain-management 2024-08-08 17:00:46 +09:00
Mouriya
392b7caa7f axiosでトークン取得を修正する
axiosでトークン取得を修正する

axiosでトークン取得を修正する

axiosでトークン取得を修正する
2024-08-08 16:58:58 +09:00
Mouriya
d9b3f57191 axiosでトークン取得を修正する 2024-08-08 16:45:44 +09:00
Mouriya
5889874720 axiosでトークン取得を修正する 2024-08-08 16:09:45 +09:00
Mouriya
7e6143cac7 axiosでトークン取得を修正する 2024-08-08 15:48:33 +09:00
Mouriya
4a4a9d72e6 axiosでトークン取得を修正する 2024-08-08 15:48:09 +09:00
Mouriya
914b0d85df ドメインページを最適化する 2024-08-08 15:30:09 +09:00
Mouriya
ad96c923b2 userdomain ページの最適化 2024-08-08 13:49:58 +09:00
Mouriya
43994ca213 UserDomain 一括追加 API を修正する 2024-08-08 13:49:11 +09:00
Mouriya
29cfed37f4 pinia-plugin-persistedstate を使用して pinia に永続性を提供し、authStore に userId メンバーを追加する 2024-08-08 13:47:39 +09:00
Yukina Mori
119091eaee Merged PR 79: BUG511 保存成功時イベントの際の、非同期処理を同期させ、値を挿入させる改修
BUG511 保存成功時イベントの際の、非同期処理を同期させ、値を挿入させる改修
2024-08-08 01:25:51 +00:00
Moriyukina2
d7e48483e9 BUG511 保存成功時イベントの際の、非同期処理を同期させ、値を挿入させる改修 2024-08-08 09:16:06 +09:00
Shohtetsu Ma
a6d49c3f96 Merged PR 78: fix:BUG553 ルックアップのキー重複許可する際エラーの処理
fix:BUG553 ルックアップのキー重複許可する際エラーの処理
ルックアップのキー重複許可する時、エラーメッセージを表示するように対応しました。

Related work items: #553
2024-08-07 08:33:01 +00:00
xiaozhe.ma
a2f57f06cf fix:BUG553 ルックアップのキー重複許可する際エラーの処理 2024-08-07 17:29:01 +09:00
Yukina Mori
f626f5722b Merged PR 77: bug540 ログインユーザー変数.isGuestの値挿入修正
bug540 ログインユーザー変数.isGuestの値挿入修正
2024-08-07 07:33:41 +00:00
Moriyukina2
5cc4ece713 bug540 ログインユーザ変数. isGuestの値挿入修正 2024-08-07 16:14:17 +09:00
Moriyukina2
b6db5f274e BUG540 ログインユーザ変数. isGuestの値挿入修正 2024-08-07 16:09:34 +09:00
Yukina Mori
936ef54072 Merged PR 76: bug540 550 変数の値挿入、条件式の修正
bug540 550 変数の値挿入、条件式の修正
2024-08-07 05:45:35 +00:00
Moriyukina2
dfaa77f2b9 bug540 550 変数の値挿入、条件式の修正 2024-08-07 14:39:33 +09:00
Shohtetsu Ma
26d0805dd9 Merged PR 75: feat:一覧画面の値変更イベント を利用不可にする
修正内容:
1.一覧画面の値変更イベント を利用不可にする
2.値変更イベント の設定可能な項目は以下の種別を限定する
- ラジオボタン
- ドロップダウン
-  チェックボックス
- •複数選択
-  ユーザー選択
- •組織選択
-  グループ選択
-  日付
- 時刻
-  日時
- 文字列1行 *1
- 数値 *2

Related work items: #579
2024-08-06 05:59:10 +00:00
xiaozhe.ma
850383d1d2 feat:一覧画面の値変更イベント を利用不可にする 2024-08-06 14:27:54 +09:00
Shohtetsu Ma
a4e9d73f3e Merged PR 74: feat:kintone APIのパスワードの暗号化対応
kintoneのログイン情報の暗号化を対応しました

Related work items: #555
2024-08-05 23:33:15 +00:00
xiaozhe.ma
35df63664e feat:kintone APIのパスワードの暗号化対応 2024-08-05 19:26:31 +09:00
Shohtetsu Ma
6cd4fb9327 Merged PR 72: bugfix:条件式の関連修正
下記条件式関連を修正しました
1.ユーザー、組織などのオブジェクト比較
2.配列値の比較(チェックボックス、複数値選択、ユーザー選択など)

Related work items: #511, #512, #513, #541, #543
2024-08-02 02:51:53 +00:00
Yukina Mori
f1b0b0a820 Merged PR 71: bug550 ,bug 540 挿入する値の空白文字チェックの改修
bug550 ,bug 540 挿入する値の空白文字チェックの改修
2024-08-02 01:30:07 +00:00
Moriyukina2
70aa9ef914 bug550 ,bug 540 挿入する値の空白文字チェックの改修 2024-08-02 10:16:41 +09:00
xiaozhe.ma
bf4ddba490 bugfix:条件式の関連修正 2024-08-02 10:03:55 +09:00
Mouriya
48f2c4a2d1 BUG555: Chacha20アルゴリズムで暗号化。注:このコミットは全員の開発環境に存在する必要があります。その後、/#/domainページにアクセスし、暗号化されていないアカウントの「編集」をクリックして直接保存し、暗号化されていないアカウントを暗号化します。 2024-08-02 09:30:12 +09:00
Shohtetsu Ma
24fca834e0 Merged PR 70: BUG527:エラーの共通処理追加
各の内容を対応しました。
1.エラーの共通処理追加
2.各アクションのtry..catch中のエラー関連処理の対応
3.BUG527,521,522,514,504修正

Related work items: #504, #514, #521, #522, #525, #527
2024-07-31 09:50:18 +00:00
xiaozhe.ma
a81f5e8c7f BUG514,521,522エラー処理の関連修正 2024-07-31 18:39:53 +09:00
xiaozhe.ma
b6a68198f5 BUG527:エラーの共通処理追加 2024-07-31 18:28:25 +09:00
Yukina Mori
7bfba06317 Merged PR 69: Bug550 Bug540 ユーザー選択、ログインユーザー取得し、値挿入できない点を再修正
Bug550 Bug540 ユーザー選択、ログインユーザー取得し、値挿入できない点を再修正
2024-07-31 08:44:17 +00:00
Moriyukina2
1dd13487bd Bug550 Bug540 ユーザー選択、ログインユーザー取得し、値挿入できない点を再修正 2024-07-31 17:27:59 +09:00
Yukina Mori
df2dbe7b8b Merged PR 68: BUg550 再テスト後、文字列フィールド以外に、変数nullを挿入しようとした場合のエラーメッセージ修正 、Bug540 再テスト後、保存成功時に変数の値が
BUg550 再テスト後、文字列フィールド以外に、変数nullを挿入しようとした場合のエラーメッセージ修正 
・2回目の再テスト後、文字列フィールド以外に、値がnullの変数を代入しようとしているときは、下記のようにエラーメッセージを修正いたしました。

Bug540 再テスト後、保存成功時に変数の値が挿入されないのを修正
・2回目の再テスト後、保存成功時のログインユーザー/ユーザー選択取得の変数の値がユーザー選択挿入できていなかったため、エラーを修正いたしました。
2024-07-31 02:27:45 +00:00
Moriyukina2
48d2d9c473 BUg550 再テスト後、文字列フィールド以外に、変数nullを挿入しようとした場合のエラーメッセージ修正 、Bug540 再テスト後、保存成功時に変数の値が挿入されないのを修正 2024-07-31 11:17:04 +09:00
Yukina Mori
748ccb8029 Merged PR 67: BUG540 値取得と値挿入の組合で、データを挿入できるよう修正 、BUG550・548 値取得と値挿入の組合で取得元の未入力の空白(’’)値のエラー修正
BUG540 値取得と値挿入の組合で、データを挿入できるよう修正 
値挿入、ログインユーザー取得のコンポーネントから取得したユーザーオブジェクトを値挿入できるよう、コードを修正いたしました。
1.値の挿入先が「ユーザーフィールド」の場合、変数名だけで

 ユーザー名が挿入される。(内部的に 変数.codeを呼び出し、挿入)

2.値の挿入先が「文字列」など「ユーザーフィールド」以外の場合、

 変数名だけで、何も指定がなければユーザー名が挿入される。(内部的に 変数.codeを呼び出し、挿入)

3.「変数.code」「変数.name」「変数.mail」などを入れていた場合は、その指定通りに挿入される。

4.また、値取得のコンポーネントで作成された、ユーザーオブジェクトに複数人設定されている場合は、複数人フィールドに挿入される

BUG550・548 値取得と値挿入の組合で取得元の未入力の空白(’’)値のエラー修正
・文字列1行、文字列複数行、リンクなどの文字列フィールドには、手入力の値も変数の値も、空白文字の混入や空文字の上書きを許可いたしました。その他のフィールドは、手入力の値とも変数の値も、入力エラー(空白文字の混入)がないことをチェックするコードを追加いたしました

Related work items: #540, #548, #550
2024-07-29 04:45:33 +00:00
Moriyukina2
c50e84a01f BUG540 値取得と値挿入の組合で、データを挿入できるよう修正 、BUG550・548 値取得と値挿入の組合で取得元の未入力の空白(’’)値のエラー修正 2024-07-29 13:25:56 +09:00
Shohtetsu Ma
1262f6040b Merged PR 66: Task544:名前変更に伴うWebサイトの名称変更
プロダクトの名前は「k-tune | kintoneジェネレーター」に変更しました。

Related work items: #544
2024-07-29 02:47:58 +00:00
xiaozhe.ma
838388fe08 TASK544:ツールの名称変更 2024-07-29 11:34:05 +09:00
Shohtetsu Ma
770e31accd Merged PR 65: BUG533:アプリインポート時エラー発生時のメッセージ表示
BUG533:アプリインポート時エラー発生時のメッセージ表示
原因:フィールド作成時エラーが発生するとき、Backend側exceptionがThrowされない
対策:フィールド作成時エラーが発生するとき、例外をThrowして、frontend側を正しい表示するように対応しました

Related work items: #533
2024-07-26 02:10:03 +00:00
xiaozhe.ma
6023237db9 BUG533:アプリインポート時エラー発生時のメッセージ表示 2024-07-26 09:56:12 +09:00
tenraku ou
ce7973a635 Merged PR 64: fixed bug545
fixed bug545

Related work items: #545
2024-07-24 09:26:35 +00:00
wtl
d2b1e03a5f fixed bug545 2024-07-24 16:02:33 +09:00
xiaozhe.ma
96722d9c2f bugfix:フィールド表示の不具合を修正 2024-07-23 13:23:29 +09:00
Shohtetsu Ma
9186cfb3d0 Merged PR 63: BUG539:詳細画面表示時のボタン押すイベント設定できない障害対応
原因:詳細画面のボタンクリックのイベントIDは間違っています。
対策:詳細画面のボタンクリックのイベントIDを修正します。
他の修正:フィールド選択UIの設定値を表示されない問題の修正

Related work items: #539
2024-07-23 01:13:15 +00:00
tenraku ou
5079dffc25 Merged PR 62: fixed bug528
fixed bug528

Related work items: #528
2024-07-23 01:12:52 +00:00
xiaozhe.ma
f0b76057bb BUG539:詳細画面表示時のボタン押すイベント設定できない障害対応 2024-07-23 09:40:15 +09:00
wtl
a96477be9a update bug528 2024-07-23 09:07:09 +09:00
wtl
8b63bfc784 fixed bug528 2024-07-23 09:01:33 +09:00
Shohtetsu Ma
9cbd07db37 Merged PR 61: Bug538:不要なメニューを削除しました
開発時よく使う便利のリンクですが、正式リリース時削除されるように修正しました。

Related work items: #538
2024-07-22 09:55:13 +00:00
Shohtetsu Ma
0a3182431f Merged PR 57: Bug 534:アプリ件数100件超える場合、100件しか表示される問題修正
原因:kintone APIは一回取得できるアプリの件数は100件で制限されている
対策:一回取得されたアプリ件数は最大100件の場合、offsetを指定して繰り返し取得するように対応しました。
kintoneのアプリはスタンダードコース最大1000個、ワイドコース最大3000個アプリがありますので、
上記の対策は最大10~30回取得する可能性がありますが、通常は問題がありません。
もしアプリは1000件超えて、性能問題がある場合は、frontend側で改ページするように検討が必要です。

Related work items: #534
2024-07-22 09:54:50 +00:00
Shohtetsu Ma
0bbd98ad78 Merged PR 56: bug537:配置スペース自動全チェック問題修正
ボタン配置スペース自動全選択の問題修正
原因:スペースのラベル名は同じ場合、選択キーはラベルが設定さてているため、特定できなくなる。
対策:スペースはUniqIDを自動付けることで区別できるように修正しました。

Related work items: #537
2024-07-22 09:54:26 +00:00
xiaozhe.ma
95bc3575d2 不要なメニューリンクを削除する 2024-07-22 18:37:16 +09:00
xiaozhe.ma
f0f282afe0 bug534:アプリ最大100件超える場合の対応 2024-07-22 18:28:25 +09:00
xiaozhe.ma
e9eafdaf1a bug534:アプリ最大100件超える場合の対応 2024-07-22 18:00:03 +09:00
Yukina Mori
92864eb6ad Merged PR 54: fix 502 532 :[値挿入]現在日時取得コンポーネントと組み合わせて使用できるよう修正、[値挿入]時刻にエラーメッセージ文修正
fix 502 532 :[値挿入]現在日時取得コンポーネントと組み合わせて使用できるよう修正
[値挿入]時刻にエラーメッセージ文修正
2024-07-22 08:40:10 +00:00
xiaozhe.ma
cc4276b727 bug537:配置スペース自動全チェック問題修正 2024-07-22 17:29:42 +09:00
Mouriya
b9a7dd99da fix 534: ループを使用してアプリの詳細情報を取得する 2024-07-22 16:40:52 +09:00
Moriyukina2
d4ade4c167 fix 502 532 :[値挿入]現在日時取得コンポーネントと組み合わせて使用できるよう修正、[値挿入]時刻にエラーメッセージ文修正 2024-07-22 13:42:22 +09:00
Shohtetsu Ma
c6a577b5ec Merged PR 53: 条件設定関連No511,512,513障害修正
条件設定関連No511,512障害修正
条件エディタは選択項目を選択するときの不具合対応しました。

Related work items: #511, #512, #513
2024-07-18 09:58:42 +00:00
xiaozhe.ma
6fff3ec006 条件設定関連No511,512障害修正 2024-07-18 17:40:17 +09:00
Yukina Mori
64e72a66d5 Merged PR 52: fix: 値挿入のコンポーネント BUG523 [値挿入]日時に挿入できない BUG517 [値挿入]対象イベント編集成功後に挿入されない の修正
fix: 値挿入のコンポーネント BUG523 [値挿入]日時に挿入できない BUG517 [値挿入]対象イベント編集成功後に挿入されない の修正

BUG523 「2024-07-05T01:32:25.552Z」(ミリ秒を含めた)日時形式と「2024-07-28T17:00:00Z」(ミリ秒を含めない)日時形式も日時フィールドに値挿入できるよう、修正いたしました。

BUG517 保存成功時イベントに値が挿入されないため、保存成功時イベントはasync/await による非同期処理でフィールドに値を挿入、レコードを更新するよう修正いたしました。

Related work items: #517, #523
2024-07-18 06:21:19 +00:00
Moriyukina2
af5f27c8c5 fix: 値挿入のコンポーネント BUG523 [値挿入]日時に挿入できない BUG517 [値挿入]対象イベント編集成功後に挿入されない の修正 2024-07-18 14:53:04 +09:00
Yu Wang
5823c989c2 Merged PR 51: Bug510保存成功後イベントで文字結合複数設定の非同期処理修正
Bug510保存成功後イベントで、文字結合の複数設定の非同期処理修正。
feat239文字結合。

Related work items: #239, #510
2024-07-17 13:28:35 +00:00
王玉
ee362a6a93 Bug510保存成功後イベントで文字結合複数設定できるように非同期処理修正 2024-07-17 21:34:47 +09:00
Kanaru Tsuda
6ba1e0d958 Merged PR 49: Bug 518,519 feat 236,284 全半角チェックのバグ修正の為のコード再構築
Bug 518,519 feat 236,284
全半角チェックのバグ修正の為のコード再構築
エラーメッセージの正常時に表示されていたバグの解消

Related work items: #236, #284, #518, #519
2024-07-17 05:11:03 +00:00
kanarutsuda
fde66aa480 全半角チェックのバグ修正の為のコード再構築 2024-07-17 14:00:18 +09:00
Shohtetsu Ma
79a8598468 Merged PR 48: レコード作成および更新アクション
以下の内容を更新しました。
1.指定アプリのレコード作成および更新アクション
2.関連の共通部分のUIの修正

Related work items: #252, #253
2024-07-12 13:17:35 +00:00
xiaozhe.ma
f70c27d814 confilct解決 2024-07-12 19:37:45 +09:00
xiaozhe.ma
432e52d322 Merge branch 'dev' of https://dev.azure.com/rj-se-service-design/App%20Builder%20for%20kintone/_git/App%20Builder%20for%20kintone into dev 2024-07-12 19:20:59 +09:00
xiaozhe.ma
f3893c2500 Conflict解決 2024-07-12 19:17:06 +09:00
xiaozhe.ma
e726843189 データ追加&更新処理アクション修正完了 2024-07-12 19:05:15 +09:00
tenraku ou
183abeba41 Merged PR 44: email-check-Fix Fixed505&507
email-check-Fix Fixed505&507
メールアドレスチェック厳格の修正

Related work items: #505, #507
2024-07-12 05:28:50 +00:00
Shohtetsu Ma
70d2513cd7 Merged PR 45: 値変更イベント非同期処理対応
原因:
ChangeイベントはPromiseの返し値を待たせないので、アクション処理途中event完了してしまうことがあります
対策:
値変更イベントは処理完了後、kintone.app.recordをリセットするように対応します

Related work items: #508
2024-07-12 05:28:28 +00:00
xiaozhe.ma
0fda3d143c 条件式の障害対応(511,512,513) 2024-07-12 09:50:28 +09:00
xiaozhe.ma
a85a3683f2 Merge branch 'feature/data-update' into dev 2024-07-11 23:22:50 +09:00
xiaozhe.ma
14287b6948 data-mappingはdata-updateへ変更 2024-07-11 23:20:33 +09:00
Mouriya
0443257f86 コードのコメントとその他のコンテンツの追加 2024-07-11 21:17:26 +09:00
Mouriya
18b97c249a bug修复 2024-07-11 20:34:32 +09:00
Mouriya
4ac4c9e9f4 「kintone lookup」と組み合わせるとロックされたフィールドを除外します 2024-07-11 20:23:48 +09:00
Mouriya
24a70aed2e 「DataMapping」コンポーネントとプラグイン 新機能「更新対象」 2024-07-11 18:57:30 +09:00
xiaozhe.ma
79e38ba6dd 値変更イベント非同期処理対応 2024-07-11 15:25:10 +09:00
wtl
303a3ffc23 email-check-Fix Fixed505&507 2024-07-11 14:10:16 +09:00
Mouriya
af86edd3e2 損傷した部品 2024-07-11 10:59:36 +09:00
Shohtetsu Ma
c87cff4181 Merged PR 43: データ集計計算アクション
以下の変更があります。
- データ集計アクション
- データ集計関連の共通UIの追加
- 条件式エディタの変更
2024-07-11 00:44:23 +00:00
xiaozhe.ma
05db5a0522 feat:データ集計処理実装完了 2024-07-10 17:26:47 +09:00
xiaozhe.ma
bac7020c15 feat:データ集計処理実装完了 2024-07-10 17:21:07 +09:00
111 changed files with 10171 additions and 2811 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
.vscode
.mypy_cache
docker-stack.yml
backend/pyvenv.cfg
backend/Include/
backend/Scripts/

File diff suppressed because one or more lines are too long

View File

@@ -194,8 +194,9 @@ def addfieldstokintone(app:str,fields:dict,c:config.KINTONE_ENV,revision:str = N
else:
data = {"app":app,"properties":fields}
r = httpx.post(url,headers=headers,data=json.dumps(data))
r.raise_for_status()
return r.json()
def updatefieldstokintone(app:str,revision:str,fields:dict,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
@@ -380,30 +381,70 @@ def uploadkintonefiles(file,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
data ={'name':'file','filename':os.path.basename(file)}
url = f"{c.BASE_URL}/k/v1/file.json"
r = httpx.post(url,headers=headers,data=data,files=upload_files)
r = httpx.post(url,headers=headers,data=data,files=upload_files)
#{"name":data['filename'],'fileKey':r['fileKey']}
return r.json()
def updateappjscss(app,uploads,c:config.KINTONE_ENV):
dsjs = []
dscss = []
#mobile側
mbjs = []
mbcss = []
customize = getappcustomize(app, c)
current_js = customize['desktop'].get('js', [])
current_css = customize['desktop'].get('css', [])
current_mobile_js = customize['mobile'].get('js', [])
current_mobile_css = customize['mobile'].get('css', [])
current_js = [item for item in current_js if not (item.get('type') == 'URL' and item.get('url', '').endswith('alc_runtime.js'))]
for upload in uploads:
for key in upload:
filename = os.path.basename(key)
if key.endswith('.js'):
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
else:
existing_js = next((item for item in current_js
if item.get('type') == 'FILE' and item['file']['name'] == filename
), None)
if existing_js:
current_js = [item for item in current_js if item.get('type') == 'URL' or item['file'].get('name') != filename]
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
else:
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
else:
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
elif key.endswith('.css'):
dscss.append({'type':'FILE','file':{'fileKey':upload[key]}})
existing_css = next((item for item in current_css
if item.get('type') == 'FILE' and item['file']['name'] == filename
), None)
if existing_css:
current_css = [item for item in current_css if item.get('type') == 'URL' or item['file'].get('name') != filename]
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
else:
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
#現在のJSとCSSがdsjsに追加する
dsjs.extend(current_js)
dscss.extend(current_css)
mbjs.extend(current_mobile_js)
mbcss.extend(current_mobile_css)
ds ={'js':dsjs,'css':dscss}
mb ={'js':[],'css':[]}
data = {'app':app,'scope':'ALL','desktop':ds,'mobile':mb}
mb ={'js':mbjs,'css':mbcss}
data = {'app':app,'scope':'ALL','desktop':ds,'mobile':mb,'revision':customize["revision"]}
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
print(data)
print(json.dumps(data))
r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json()
#kintone カスタマイズ情報
def getappcustomize(app,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
params = {"app":app}
r = httpx.get(url,headers=headers,params=params)
return r.json()
def getTempPath(filename):
scriptdir = Path(__file__).resolve().parent
rootdir = scriptdir.parent.parent.parent.parent
@@ -516,10 +557,22 @@ async def allapps(request:Request,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}{config.API_V1_STR}/apps.json"
r = httpx.get(url,headers=headers)
return r.json()
offset = 0
limit = 100
all_apps = []
while True:
r = httpx.get(f"{url}?limit={limit}&offset={offset}", headers=headers)
json_data = r.json()
apps = json_data.get("apps",[])
all_apps.extend(apps)
if len(apps)<limit:
break
offset += limit
return {"apps": all_apps}
except Exception as e:
raise APIException('kintone:allapps',request.url._url, f"Error occurred while get allapps({c.DOMAIN_NAME}):",e)
raise APIException('kintone:allapps', request.url._url, f"Error occurred while get allapps({c.DOMAIN_NAME}):", e)
@r.get("/appfields")
async def appfields(request:Request,app:str,env = Depends(getkintoneenv)):
@@ -714,6 +767,7 @@ async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Dep
for file in files:
upload = uploadkintonefiles(file,env)
if upload.get('fileKey') != None:
print(upload)
jscs.append({ file :upload['fileKey']})
appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None:

View File

@@ -1,14 +1,76 @@
from fastapi import Request,Depends, APIRouter, UploadFile,HTTPException,File
from fastapi import Query, Request,Depends, APIRouter, UploadFile,HTTPException,File
from app.core.operation import log_operation
from app.db import Base,engine
from app.db.session import get_db
from app.db.crud import *
from app.db.schemas import *
from typing import List
from typing import List, Optional
from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
import httpx
import app.core.config as config
platform_router = r = APIRouter()
@r.get(
"/apps",
response_model=List[AppList],
response_model_exclude_none=True,
)
async def apps_list(
request: Request,
user = Depends(get_current_user),
db=Depends(get_db),
):
try:
platformapps = get_apps(db)
domain = get_activedomain(db, user.id)
kintoneevn = config.KINTONE_ENV(domain)
headers={config.API_V1_AUTH_KEY:kintoneevn.API_V1_AUTH_VALUE}
url = f"{kintoneevn.BASE_URL}{config.API_V1_STR}/apps.json"
offset = 0
limit = 100
all_apps = []
while True:
r = httpx.get(f"{url}?limit={limit}&offset={offset}", headers=headers)
json_data = r.json()
apps = json_data.get("apps",[])
all_apps.extend(apps)
if len(apps)<limit:
break
offset += limit
tempapps = platformapps.copy()
for papp in tempapps:
exist = False
for kapp in all_apps:
if kapp['appId'] == papp.appid:
exist = True
break
if not exist:
platformapps.remove(papp)
return platformapps
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get apps:",e)
@r.post("/apps", response_model=AppList, response_model_exclude_none=True)
async def apps_update(
request: Request,
app: AppVersion,
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
return update_appversion(db, app,user.id)
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get create app :",e)
@r.get(
"/appsettings/{id}",
response_model=App,
@@ -129,7 +191,7 @@ async def flow_list(
try:
domain = get_activedomain(db, user.id)
print("domain=>",domain)
flows = get_flows_by_app(db, domain.id, appid)
flows = get_flows_by_app(db, domain.url, appid)
return flows
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
@@ -144,7 +206,7 @@ async def flow_create(
):
try:
domain = get_activedomain(db, user.id)
return create_flow(db, domain.id, flow)
return create_flow(db, domain.url, flow)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
@@ -233,16 +295,17 @@ async def domain_delete(
@r.get(
"/domain",
response_model=List[Domain],
# response_model=List[Domain],
response_model_exclude_none=True,
)
async def userdomain_details(
request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domains = get_domain(db, user.id)
domains = get_domain(db, userId if userId is not None else user.id)
return domains
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
@@ -254,7 +317,7 @@ async def userdomain_details(
async def create_userdomain(
request: Request,
userid: int,
domainids:list,
domainids:List[int] ,
db=Depends(get_db),
):
try:
@@ -285,11 +348,14 @@ async def userdomain_delete(
)
async def get_useractivedomain(
request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
# domain = get_activedomain(db, user.id)
domain = get_activedomain(db, userId if userId is not None else user.id)
return domain
except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while get user({user.id}) activedomain:",e)
@@ -301,11 +367,12 @@ async def get_useractivedomain(
async def update_activeuserdomain(
request: Request,
domainid:int,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domain = active_userdomain(db, user.id,domainid)
domain = active_userdomain(db, userId if userId is not None else user.id,domainid)
return domain
except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while update user({user.id}) activedomain:",e)

View File

@@ -1,22 +1,35 @@
from fastapi import HTTPException, status
import httpx
from app.db.schemas import ErrorCreate
from app.db.session import SessionLocal
from app.db.crud import create_log
class APIException(Exception):
def __init__(self,location:str,title:str,content:str,e:Exception):
if(str(e) == ''):
content += e.detail
self.detail = e.detail
self.status_code = e.status_code
else:
self.detail = str(e)
content += str(e)
self.status_code = 500
if(len(content) > 5000):
content =content[0:5000]
self.error = ErrorCreate(location=location,title=title,content=content)
def __init__(self, location: str, title: str, content: str, e: Exception):
self.detail = str(e)
self.status_code = 500
if isinstance(e,httpx.HTTPStatusError):
try:
error_response = e.response.json()
self.detail = error_response.get('message', self.detail)
self.status_code = e.response.status_code
content += self.detail
except ValueError:
pass
elif hasattr(e, 'detail'):
self.detail = e.detail
self.status_code = e.status_code if hasattr(e, 'status_code') else 500
content += e.detail
else:
self.detail = str(e)
self.status_code = 500
content += str(e)
if len(content) > 5000:
content = content[:5000]
self.error = ErrorCreate(location=location, title=title, content=content)
super().__init__(self.error)
def writedblog(exc: APIException):
db = SessionLocal()

View File

@@ -1,11 +1,13 @@
import os
import base64
PROJECT_NAME = "KintoneAppBuilder"
#SQLALCHEMY_DATABASE_URI = "postgres://maxz64:m@xz1205@alicornkintone.postgres.database.azure.com/postgres"
SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/postgres"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/unittest"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/dev"
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://kabAdmin:P%40ssw0rd!@kintonetooldb.postgres.database.azure.com/dev_v2"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/test"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@ktune-prod-db.postgres.database.azure.com/postgres"
API_V1_STR = "/k/v1"
API_V1_AUTH_KEY = "X-Cybozu-Authorization"
@@ -13,12 +15,13 @@ API_V1_AUTH_KEY = "X-Cybozu-Authorization"
DEPLOY_MODE = "PROD" #DEV,PROD
DEPLOY_JS_URL = "https://ka-addin.azurewebsites.net/alc_runtime.js"
#DEPLOY_JS_URL = "https://ce1c-133-139-70-194.ngrok-free.app/alc_runtime.js"
KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"]
KINTONE_FIELD_PROPERTY=['label','code','type','required','unique','maxValue','minValue','maxLength','minLength','defaultValue','defaultNowValue','options','expression','hideExpression','digit','protocol','displayScale','unit','unitPosition']
KINTONE_PSW_CRYPTO_KEY=bytes.fromhex("53 6c 93 bd 48 ad b5 c0 93 df a1 27 25 a1 a3 32 a2 03 3b a0 27 1f 51 dc 20 0e 6c d7 be fc fb ea")
class KINTONE_ENV:
BASE_URL = ""
@@ -36,4 +39,4 @@ class KINTONE_ENV:
self.DOMAIN_ID=domain.id
self.BASE_URL = domain.url
self.KINTONE_USER = domain.kintoneuser
self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.kintonepwd}","utf-8"))
self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.decrypt_kintonepwd()}","utf-8"))

View File

@@ -2,6 +2,10 @@ import jwt
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from datetime import datetime, timedelta
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
import os
import base64
from app.core import config
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
@@ -29,3 +33,32 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def chacha20Encrypt(plaintext:str, key=config.KINTONE_PSW_CRYPTO_KEY):
if plaintext is None or plaintext == '':
return None
nonce = os.urandom(16)
algorithm = algorithms.ChaCha20(key, nonce)
cipher = Cipher(algorithm, mode=None)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext.encode('utf-8')) + encryptor.finalize()
return base64.b64encode(nonce +'𒀸'.encode('utf-8')+ ciphertext).decode('utf-8')
def chacha20Decrypt(encoded_str:str, key=config.KINTONE_PSW_CRYPTO_KEY):
try:
decoded_data = base64.b64decode(encoded_str)
if len(decoded_data) < 18:
return encoded_str
special_char = decoded_data[16:20]
if special_char != '𒀸'.encode('utf-8'):
return encoded_str
nonce = decoded_data[:16]
ciphertext = decoded_data[20:]
except Exception as e:
print(f"An error occurred: {e}")
return encoded_str
algorithm = algorithms.ChaCha20(key, nonce)
cipher = Cipher(algorithm, mode=None)
decryptor = cipher.decryptor()
plaintext_bytes = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext_bytes.decode('utf-8')

View File

@@ -4,7 +4,7 @@ from sqlalchemy import and_
import typing as t
from . import models, schemas
from app.core.security import get_password_hash
from app.core.security import chacha20Decrypt, get_password_hash
def get_user(db: Session, user_id: int):
@@ -69,6 +69,46 @@ def edit_user(
db.refresh(db_user)
return db_user
def get_apps(
db: Session
) -> t.List[schemas.AppList]:
return db.query(models.App).all()
def update_appversion(db: Session, appedit: schemas.AppVersion,userid:int):
app = db.query(models.App).filter(and_(models.App.domainurl == appedit.domainurl,models.App.appid == appedit.appid)).first()
if app:
app.version = app.version + 1
db_app = app
appver = app.version
else:
appver = 1
db_app = models.App(
domainurl = appedit.domainurl,
appid=appedit.appid,
appname=appedit.appname,
version = 1,
updateuser= userid
)
db.add(db_app)
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == appedit.domainurl,models.App.appid == appedit.appid))
for flow in flows:
db_flowhistory = models.FlowHistory(
flowid = flow.flowid,
appid = flow.appid,
eventid = flow.eventid,
domainurl = flow.domainurl,
name = flow.name,
content = flow.content,
createuser = userid,
version = appver
)
db.add(db_flowhistory)
db.commit()
db.refresh(db_app)
return db_app
def get_appsetting(db: Session, id: int):
app = db.query(models.AppSetting).get(id)
@@ -125,12 +165,12 @@ def get_actions(db: Session):
return actions
def create_flow(db: Session, domainid: int, flow: schemas.FlowBase):
def create_flow(db: Session, domainurl: str, flow: schemas.FlowBase):
db_flow = models.Flow(
flowid=flow.flowid,
appid=flow.appid,
eventid=flow.eventid,
domainid=domainid,
domainurl=domainurl,
name=flow.name,
content=flow.content
)
@@ -177,13 +217,14 @@ def get_flow(db: Session, flowid: str):
raise HTTPException(status_code=404, detail="Data not found")
return flow
def get_flows_by_app(db: Session, domainid: int, appid: str):
flows = db.query(models.Flow).filter(and_(models.Flow.domainid == domainid,models.Flow.appid == appid)).all()
def get_flows_by_app(db: Session,domainurl: str, appid: str):
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid == appid)).all()
if not flows:
raise Exception("Data not found")
return flows
def create_domain(db: Session, domain: schemas.DomainBase):
domain.encrypt_kintonepwd()
db_domain = models.Domain(
tenantid = domain.tenantid,
name=domain.name,
@@ -208,30 +249,26 @@ def delete_domain(db: Session,id: int):
def edit_domain(
db: Session, domain: schemas.DomainBase
) -> schemas.Domain:
domain.encrypt_kintonepwd()
db_domain = db.query(models.Domain).get(domain.id)
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
update_data = domain.dict(exclude_unset=True)
for key, value in update_data.items():
if(key != "id"):
setattr(db_domain, key, value)
if key != "id" and not (key == "kintonepwd" and (value is None or value == "")):
setattr(db_domain, key, value)
print(str(db_domain))
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
def add_userdomain(db: Session, userid:int,domainids:list):
for domainid in domainids:
db_domain = models.UserDomain(
userid = userid,
domainid = domainid
)
db.add(db_domain)
def add_userdomain(db: Session, userid:int,domainids:list[str]):
dbCommits = list(map(lambda domainid: models.UserDomain(userid = userid, domainid = domainid ), domainids))
db.bulk_save_objects(dbCommits)
db.commit()
db.refresh(db_domain)
return db_domain
return dbCommits
def delete_userdomain(db: Session, userid: int,domainid: int):
db_domain = db.query(models.UserDomain).filter(and_(models.UserDomain.userid == userid,models.UserDomain.domainid == domainid)).first()
@@ -256,20 +293,26 @@ def active_userdomain(db: Session, userid: int,domainid: int):
def get_activedomain(db: Session, userid: int):
db_domain = db.query(models.Domain).join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ).filter(and_(models.UserDomain.userid == userid,models.UserDomain.active == True)).first()
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
# if not db_domain:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
return db_domain
def get_domain(db: Session, userid: str):
domains = db.query(models.Domain).join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ).filter(models.UserDomain.userid == userid).all()
if not domains:
raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
return domains
def get_domains(db: Session,tenantid:str):
domains = db.query(models.Domain).filter(models.Domain.tenantid == tenantid ).all()
if not domains:
raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
return domains
def get_events(db: Session):
@@ -278,9 +321,35 @@ def get_events(db: Session):
raise HTTPException(status_code=404, detail="Data not found")
return events
def get_category(db:Session):
categorys=db.query(models.Category).all()
return categorys
def get_eventactions(db: Session,eventid: str):
#eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid == models.Action.id ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all()
eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid != models.Action.id and models.EventAction.eventid == eventid ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all()
#category = get_category(db)
blackactions = (
db.query(models.EventAction.actionid)
.filter(models.EventAction.eventid == eventid)
.subquery()
)
eveactions = (
db.query(
models.Action.id,
models.Action.name,
models.Action.title,
models.Action.subtitle,
models.Action.outputpoints,
models.Action.property,
models.Action.categoryid,
models.Action.nosort,
models.Category.categoryname)
.join(models.Category,models.Category.id == models.Action.categoryid)
.filter(models.Action.id.notin_(blackactions))
.order_by(models.Category.nosort,models.Action.nosort)
.all()
)
if not eveactions:
raise HTTPException(status_code=404, detail="Data not found")
return eveactions

View File

@@ -1,7 +1,10 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime,ForeignKey
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.orm import relationship
from datetime import datetime
from app.core.security import chacha20Decrypt
@as_declarative()
class Base:
id = Column(Integer, primary_key=True, index=True)
@@ -18,6 +21,16 @@ class User(Base):
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
class App(Base):
__tablename__ = "app"
domainurl = Column(String(200), nullable=False)
appname = Column(String(200), nullable=False)
appid = Column(String(100), index=True, nullable=False)
version = Column(Integer)
updateuser = Column(Integer,ForeignKey("user.id"))
user = relationship('User')
class AppSetting(Base):
__tablename__ = "appsetting"
@@ -40,6 +53,8 @@ class Action(Base):
subtitle = Column(String(500))
outputpoints = Column(String)
property = Column(String)
categoryid = Column(Integer,ForeignKey("category.id"))
nosort = Column(Integer)
class Flow(Base):
__tablename__ = "flow"
@@ -47,10 +62,22 @@ class Flow(Base):
flowid = Column(String(100), index=True, nullable=False)
appid = Column(String(100), index=True, nullable=False)
eventid = Column(String(100), index=True, nullable=False)
domainid = Column(Integer,ForeignKey("domain.id"))
domainurl = Column(String(200))
name = Column(String(200))
content = Column(String)
class FlowHistory(Base):
__tablename__ = "flowhistory"
flowid = Column(String(100), index=True, nullable=False)
appid = Column(String(100), index=True, nullable=False)
eventid = Column(String(100), index=True, nullable=False)
domainurl = Column(String(200))
name = Column(String(200))
content = Column(String)
createuser = Column(Integer,ForeignKey("user.id"))
version = Column(Integer)
class Tenant(Base):
__tablename__ = "tenant"
@@ -68,6 +95,9 @@ class Domain(Base):
url = Column(String(200), nullable=False)
kintoneuser = Column(String(100), nullable=False)
kintonepwd = Column(String(100), nullable=False)
def decrypt_kintonepwd(self):
decrypted_pwd = chacha20Decrypt(self.kintonepwd)
return decrypted_pwd
class UserDomain(Base):
@@ -90,7 +120,7 @@ class Event(Base):
class EventAction(Base):
__tablename__ = "eventaction"
eventid = Column(Integer,ForeignKey("event.id"))
eventid = Column(String(100),ForeignKey("event.eventid"))
actionid = Column(Integer,ForeignKey("action.id"))
@@ -101,6 +131,17 @@ class ErrorLog(Base):
location = Column(String(500))
content = Column(String(5000))
class OperationLog(Base):
__tablename__ = "operationlog"
tenantid = Column(String(100))
domainurl = Column(String(200))
userid = Column(Integer,ForeignKey("user.id"))
operation = Column(String(200))
function = Column(String(200))
detail = Column(String(200))
user = relationship('User')
class KintoneFormat(Base):
__tablename__ = "kintoneformat"
@@ -110,4 +151,10 @@ class KintoneFormat(Base):
typecolumn =Column(Integer)
codecolumn =Column(Integer)
field = Column(String(5000))
trueformat = Column(String(10))
trueformat = Column(String(10))
class Category(Base):
__tablename__ = "category"
categoryname = Column(String(20))
nosort = Column(Integer)

View File

@@ -2,6 +2,7 @@ from pydantic import BaseModel
from datetime import datetime
import typing as t
from app.core.security import chacha20Decrypt, chacha20Encrypt
class Base(BaseModel):
create_time: datetime
@@ -27,21 +28,21 @@ class UserCreate(UserBase):
is_active:bool
is_superuser:bool
class Config:
class ConfigDict:
orm_mode = True
class UserEdit(UserBase):
password: t.Optional[str] = None
class Config:
class ConfigDict:
orm_mode = True
class User(UserBase):
id: int
class Config:
class ConfigDict:
orm_mode = True
@@ -49,6 +50,17 @@ class Token(BaseModel):
access_token: str
token_type: str
class AppList(Base):
domainurl: str
appname: str
appid:str
version:int
user:UserOut
class AppVersion(BaseModel):
domainurl: str
appname: str
appid:str
class TokenData(BaseModel):
id:int = 0
@@ -67,7 +79,7 @@ class AppBase(BaseModel):
class App(AppBase):
id: int
class Config:
class ConfigDict:
orm_mode = True
@@ -78,7 +90,7 @@ class Kintone(BaseModel):
desc: str = None
content: str = None
class Config:
class ConfigDict:
orm_mode = True
class Action(BaseModel):
@@ -88,8 +100,10 @@ class Action(BaseModel):
subtitle: str = None
outputpoints: str = None
property: str = None
class Config:
categoryid: int = None
nosort: int
categoryname : str =None
class ConfigDict:
orm_mode = True
class FlowBase(BaseModel):
@@ -104,11 +118,11 @@ class Flow(Base):
flowid: str
appid: str
eventid: str
domainid: int
domainurl: str
name: str = None
content: str = None
class Config:
class ConfigDict:
orm_mode = True
class DomainBase(BaseModel):
@@ -119,6 +133,10 @@ class DomainBase(BaseModel):
kintoneuser: str
kintonepwd: str
def encrypt_kintonepwd(self):
encrypted_pwd = chacha20Encrypt(self.kintonepwd)
self.kintonepwd = encrypted_pwd
class Domain(Base):
id: int
tenantid: str
@@ -126,8 +144,7 @@ class Domain(Base):
url: str
kintoneuser: str
kintonepwd: str
class Config:
class ConfigDict:
orm_mode = True
class Event(Base):
@@ -139,7 +156,7 @@ class Event(Base):
mobile: bool
eventgroup: bool
class Config:
class ConfigDict:
orm_mode = True
class ErrorCreate(BaseModel):

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +1,8 @@
{
"name": "kintone-automate",
"name": "k-tune",
"version": "0.2.0",
"description": "Kintoneアプリの自動生成とデプロイを支援ツールです",
"productName": "kintone Automate",
"productName": "k-tune | kintoneジェネレーター",
"author": "maxiaozhe@alicorns.co.jp <maxiaozhe@alicorns.co.jp>",
"private": true,
"scripts": {
@@ -12,14 +12,15 @@
"dev": "quasar dev",
"dev:local": "set \"LOCAL=true\" && quasar dev",
"build": "set \"SOURCE_MAP=false\" && quasar build",
"build:dev":"set \"SOURCE_MAP=true\" && quasar build"
"build:dev": "set \"SOURCE_MAP=true\" && quasar build"
},
"dependencies": {
"@quasar/extras": "^1.16.4",
"@vueuse/core": "^10.9.0",
"axios": "^1.4.0",
"jwt-decode": "^4.0.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"quasar": "^2.6.0",
"uuid": "^9.0.0",
"vue": "^3.0.0",

View File

@@ -2,6 +2,7 @@ import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios';
import {router} from 'src/router';
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$axios: AxiosInstance;
@@ -15,30 +16,10 @@ declare module '@vue/runtime-core' {
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL });
const token=localStorage.getItem('token')||'';
if(token!==''){
api.defaults.headers["Authorization"]='Bearer ' + token;
}
//axios例外キャプチャー
api.interceptors.response.use(
(response)=>response,
(error)=>{
if (error.response && error.response.status === 401) {
// 認証エラーの場合再ログインする
console.error('(; ゚Д゚)/認証エラー(401)', error);
localStorage.removeItem('token');
router.replace({
path:"/login",
query:{redirect:router.currentRoute.value.fullPath}
});
}
return Promise.reject(error);
}
)
export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file

View File

@@ -3,20 +3,46 @@
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table v-else row-key="index" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows"
class="action-table"
flat bordered
virtual-scroll
:pagination="pagination"
:rows-per-page-options="[0]"
:filter="filter"
<q-splitter
v-model="splitterModel"
style="height: 100%"
before-class="tab"
unit="px"
v-else
>
</q-table>
<template v-slot:before>
<q-tabs
v-model="tab"
vertical
active-color="white"
indicator-color="primary"
active-bg-color="primary"
class="bg-grey-2 text-grey-8"
dense
>
<q-tab :name="cate"
:label="cate"
v-for="(cate,) in categorys"
:key="cate"
></q-tab>
</q-tabs>
</template>
<template v-slot:after>
<q-table row-key="index" :selection="type" v-model:selected="selected" :columns="columns" :rows="actionForTab"
class="action-table"
flat bordered
virtual-scroll
:pagination="pagination"
:rows-per-page-options="[0]"
:filter="filter"></q-table>
</template>
</q-splitter>
</div>
</template>
<script>
import { ref,onMounted,reactive } from 'vue'
import { ref,onMounted,reactive,watchEffect,computed,watch } from 'vue'
import { api } from 'boot/axios';
import { useFlowEditorStore } from 'stores/flowEditor';
export default {
name: 'actionSelect',
@@ -25,30 +51,74 @@ export default {
type: String,
filter:String
},
setup(props) {
emits:[
"clearFilter"
],
setup(props,{emit}) {
const isLoaded=ref(false);
const columns = [
{ name: 'name', required: true,label: 'アクション名',align: 'left',field: 'name',sortable: true},
{ name: 'desc', align: 'left', label: '説明', field: 'desc', sortable: true },
// { name: 'content', label: '内容', field: 'content', sortable: true }
];
const rows = reactive([])
const store = useFlowEditorStore();
let actionData =reactive([]);
const categorys = ref('');
const tab=ref('');
const actionForTab=computed(()=>{
const rows=[];
const actions= props.filter? actionData:actionData.filter(x=>x.categoryname===tab.value);
actions.forEach((item,index) =>{
rows.push({index,
name:item.name,
desc:item.title,
outputPoints:item.outputpoints,
property:item.property});
});
return rows;
});
onMounted(async () => {
const res =await api.get('api/actions');
res.data.forEach((item,index) =>
{
rows.push({index,name:item.name,desc:item.title,outputPoints:item.outputpoints,property:item.property});
});
let eventId='';
if(store.selectedEvent ){
eventId=store.selectedEvent.header!=='DELETABLE'? store.selectedEvent.eventId : store.selectedEvent.parentId;
}
const res =await api.get(`api/eventactions/${eventId}`);
actionData= res.data;
const categoryNames = Array.from(new Set(actionData.map(x=>x.categoryname)));
categorys.value=categoryNames;
tab.value = categoryNames.length>0? categoryNames[0]:'';
isLoaded.value=true;
});
// watch(props.filter,()=>{
// if(props.filter && props.filter!==''){
// tab.value='';
// }
// });
watch(tab,()=>{
if(tab.value!==''){
emit('clearFilter','');
}
});
// watchEffect(()=>{
// if(props.filter && props.filter!==''){
// tab.value='';
// }
// if(tab.value!==''){
// emit('update:filter','');
// }
// });
return {
columns,
rows,
selected: ref([]),
pagination:ref({
rowsPerPage:0
}),
isLoaded,
tab,
actionData,
categorys,
splitterModel: ref(150),
actionForTab
}
},
@@ -58,5 +128,6 @@ export default {
.action-table{
min-height: 10vh;
max-height: 68vh;
min-width: 550px;
}
</style>

View File

@@ -33,7 +33,7 @@
<div class="row">
<field-select ref="fieldDlg" name="フィールド" :type="selectType" :updateSelectFields="updateSelectFields"
:appId="selField?.app?.id" not_page :filter="fieldFilter"
:selectedFields="selField.fields"></field-select>
:selectedFields="selField.fields" :fieldTypes="fieldTypes"></field-select>
</div>
</div>
</div>
@@ -92,7 +92,10 @@ export default defineComponent({
type: String,
default: 'single'
},
fieldTypes:{
type:Array,
default:()=>[]
}
},
setup(props, { emit }) {
const showSelectApp = ref(false);
@@ -105,7 +108,7 @@ export default defineComponent({
const updateSelectApp = (newAppinfo: IApp) => {
selField.app = newAppinfo
}
const updateSelectFields = (newFields: IField[]) => {
selField.fields = newFields
}

View File

@@ -21,7 +21,7 @@
</div>
</template>
</q-field>
<q-field stack-label full-width label="アプリ説明">
<q-field stack-label full-width label="アプリ説明">
<template v-slot:control>
<div class="self-center full-width no-outline" tabindex="0">
{{ appinfo?.description }}

View File

@@ -0,0 +1,271 @@
<template>
<div>
<q-stepper v-model="step" ref="stepper" color="primary" animated flat>
<q-step :name="1" title="データソースの設定" icon="app_registration" :done="step > 1">
<div class="row justify-between items-center">
<div>アプリの選択 :</div>
<div>
<a v-if="data.sourceApp?.name" class="q-mr-xs"
:href="data.sourceApp ? `${authStore.currentDomain.kintoneUrl}/k/${data.sourceApp.id}` : ''"
target="_blank" title="Kiontoneへ">
{{ data.sourceApp?.name }}
</a>
<div v-else class="text-red">APPを選択してください</div>
<q-btn v-if="data.sourceApp?.name" flat color="grey" icon="clear" size="sm" padding="none"
@click="clearSelectedApp" />
</div>
<q-btn outline dense label="変更" padding="xs sm" color="primary" @click="showAppDialog" />
</div>
<!-- フィールド設定部分 -->
<template v-if="data.sourceApp?.name">
<q-separator class="q-mt-md" />
<div class="q-my-md row justify-between items-center">
データ階層を設定する :
<q-btn icon="add" size="sm" padding="xs" outline color="primary" @click="addRow" />
</div>
<q-virtual-scroll style="max-height: 13.5rem;" :items="data.fieldList" separator v-slot="{ item, index }">
<div class="row justify-between items-center q-my-md">
<div>{{ index + 1 }}階層 :</div>
<div>{{ item.source?.name }}</div>
<q-btn-group outline>
<q-btn outline dense label="変更" padding="xs sm" color="primary"
@click="() => showFieldDialog(item, 'source')" />
<q-btn outline dense label="削除" padding="xs sm" color="primary" @click="() => delRow(index)" />
</q-btn-group>
</div>
</q-virtual-scroll>
</template>
<!-- アプリ選択ダイアログ -->
<ShowDialog v-model:visible="data.sourceApp.showSelectApp" name="アプリ選択" @close="closeAppDialog" min-width="50vw"
min-height="50vh">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="data.sourceApp.appFilter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<AppSelectBox ref="appDg" name="アプリ" type="single" :filter="data.sourceApp.appFilter" />
</ShowDialog>
</q-step>
<q-step :name="2" title="ドロップダウンフィールドの設定" icon="multiple_stop" :done="step > 2">
<div class="row q-pa-sm q-col-gutter-x-sm flex-center">
<div class="col-grow row q-col-gutter-x-sm">
<div class="col-6">データソース</div>
<div class="col-6">ドロップダウンフィールド</div>
</div>
<div class="col-auto">
<div style="width: 88px; height: 1px;"></div>
</div>
</div>
<div v-for="(item) in data.fieldList" :key="item.id" class="row q-pa-sm q-col-gutter-x-sm flex-center">
<div class="col-grow row q-col-gutter-x-sm">
<div class="col-6">{{ item.source.name }}</div>
<div class="col-6">{{ item.dropDown?.name }}</div>
</div>
<div class="col-auto">
<div class="row justify-end">
<q-btn-group outline>
<q-btn outline dense label="設定" padding="xs sm" color="primary"
@click="() => showFieldDialog(item, 'dropDown')" />
<q-btn outline dense label="クリア" padding="xs sm" color="primary"
@click="() => item.dropDown = undefined" />
</q-btn-group>
</div>
</div>
</div>
</q-step>
<!-- ステップナビゲーション -->
<template v-slot:navigation>
<q-stepper-navigation>
<div class="row justify-end q-mt-md">
<q-btn v-if="step > 1" flat color="primary" @click="$refs.stepper.previous()" label="戻る" class="q-ml-sm" />
<q-btn @click="stepperNext" color="primary" :label="step === 2 ? '確定' : '次へ'"
:disable="nextBtnCheck()" />
</div>
</q-stepper-navigation>
</template>
</q-stepper>
<!-- フィールド選択ダイアログ -->
<template v-for="(item, index) in data.fieldList" :key="`dg${item.id}`">
<show-dialog v-model:visible="item.sourceDg.show" name="フィールド一覧" min-width="400px">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="item.sourceDg.filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<FieldSelect name="フィールド" :appId="data.sourceApp.id" :selectedFields="item.source"
:filter="item.sourceDg.filter" :updateSelectFields="(f) => updateSelectField(f, item, index, 'source')"
:blackListLabel="blackListLabel" />
</show-dialog>
<show-dialog v-model:visible="item.dropDownDg.show" name="フィールド一覧" min-width="400px">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="item.dropDownDg.filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<FieldSelect name="フィールド" :appId="data.dropDownApp.id" :selectedFields="item.source"
:filter="item.dropDownDg.filter" :updateSelectFields="(f) => updateSelectField(f, item, index, 'dropDown')"
:blackListLabel="blackListLabel" />
</show-dialog>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, watchEffect,watch } from 'vue';
import ShowDialog from './ShowDialog.vue';
import AppSelectBox from './AppSelectBox.vue';
import FieldSelect from './FieldSelect.vue';
import { useAuthStore } from 'src/stores/useAuthStore';
import { useFlowEditorStore } from 'src/stores/flowEditor';
import { v4 as uuidv4 } from 'uuid';
import { useQuasar } from 'quasar';
export default defineComponent({
name: 'CascadingDropDownBox',
inheritAttrs: false,
components: { ShowDialog, AppSelectBox, FieldSelect },
props: {
modelValue: {
type: Object,
default: () => ({})
},
finishDialogHandler: Function,
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const authStore = useAuthStore();
const flowStore = useFlowEditorStore();
const $q = useQuasar();
const appDg = ref();
const stepper = ref();
const step = ref(1);
const data =ref(props.modelValue);
// const data = ref({
// sourceApp: props.modelValue.sourceApp ?? { appFilter: '', showSelectApp: false },
// dropDownApp: props.modelValue.dropDownApp,
// fieldList: props.modelValue.fieldList ?? [],
// });
// アプリ関連の関数
const showAppDialog = () => data.value.sourceApp.showSelectApp = true;
const clearSelectedApp = () => {
data.value.sourceApp = { appFilter: '', showSelectApp: false };
data.value.fieldList = [];
};
const closeAppDialog = (val: 'OK' | 'Cancel') => {
data.value.sourceApp.showSelectApp = false;
const selected = appDg.value?.selected[0];
if (val === 'OK' && selected) {
if (flowStore.appInfo?.appId === selected.id) {
$q.notify({
type: 'negative',
caption: "エラー",
message: 'データソースを現在のアプリにすることはできません。'
});
} else if (selected.id !== data.value.sourceApp.id) {
clearSelectedApp();
Object.assign(data.value.sourceApp, { id: selected.id, name: selected.name });
}
}
};
// フィールド関連の関数
const defaultRow = () => ({
id: uuidv4(),
source: undefined,
dropDown: undefined,
sourceDg: { show: false, filter: '' },
dropDownDg: { show: false, filter: '' },
});
const addRow = () => data.value.fieldList.push(defaultRow());
const delRow = (index: number) => data.value.fieldList.splice(index, 1);
const showFieldDialog = (item: any, keyName: string) => item[`${keyName}Dg`].show = true;
const updateSelectField = (f: any, item: any, index: number, keyName: 'source' | 'dropDown') => {
const [selected] = f.value;
const isDuplicate = data.value.fieldList.some((field, idx) =>
idx !== index && (field[keyName]?.code === selected.code || field[keyName]?.label === selected.label)
);
if (isDuplicate) {
$q.notify({
type: 'negative',
caption: "エラー",
message: '重複したフィールドは選択できません'
});
} else {
item[keyName] = selected;
}
};
// ステッパー関連の関数
const nextBtnCheck = () => {
const stepNo = step.value
if (stepNo === 1) {
return !(data.value.sourceApp?.id && data.value.fieldList?.length > 0 && data.value.fieldList?.every(f => f.source?.name));
} else if (stepNo === 2) {
return !data.value.fieldList?.every(f => f.dropDown?.name);
}
return true;
};
const stepperNext = () => {
if (step.value === 2) {
props.finishDialogHandler?.(data.value);
} else {
data.value.dropDownApp = { name: flowStore.appInfo?.name, id: flowStore.appInfo?.appId };
stepper.value?.next();
}
};
// // データ変更の監視
// watchEffect(() =>{
// emit('update:modelValue', data.value);
// });
return {
// 状態と参照
authStore,
step,
stepper,
appDg,
data,
// アプリ関連の関数
showAppDialog,
closeAppDialog,
clearSelectedApp,
// フィールド関連の関数
addRow,
delRow,
showFieldDialog,
updateSelectField,
// ステッパー関連の関数
nextBtnCheck,
stepperNext,
// 定数
blackListLabel: ['レコード番号', '作業者', '更新者', '更新日時', '作成日時', '作成者', 'カテゴリー', 'ステータス'],
};
}
});
</script>

View File

@@ -52,12 +52,12 @@ import { useQuasar } from 'quasar';
const tree = ref(props.conditionTree);
const closeDg = (val:string) => {
if (val == 'OK') {
if(tree.value.root.children.length===0){
$q.notify({
type: 'negative',
message: `条件式を設定してください。`
});
}
// if(tree.value.root.children.length===0){
// $q.notify({
// type: 'negative',
// message: `条件式を設定してください。`
// });
// }
context.emit("update:conditionTree",tree.value);
}
showflg.value=false;

View File

@@ -1,41 +1,45 @@
<template>
<q-field labelColor="primary" class="condition-object" :clearable="isSelected" stack-label :dense="true"
:outlined="true">
<template v-slot:control>
<!-- <q-chip color="primary" text-color="white" v-if="isSelected && selectedObject.objectType==='field'" :dense="true" class="selected-obj">
{{ selectedObject.name }}
</q-chip>
<q-chip color="info" text-color="white" v-if="isSelected && selectedObject.objectType==='variable'" :dense="true" class="selected-obj">
{{ selectedObject.name.name }}
</q-chip> -->
{{ selectedObject?.sharedText }}
</template>
<template v-slot:append>
<q-icon name="search" class="cursor-pointer" @click="showDg" />
</template>
</q-field>
<show-dialog v-model:visible="show" name="設定項目" @close="closeDg" min-width="400px">
<!-- <template v-slot:toolbar>
<q-input dense debounce="200" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<condition-objects ref="appDg" name="フィールド" type="single" :filter="filter" :appId="store.appInfo?.appId" :vars="vars"></condition-objects>
-->
<DynamicItemInput v-model:selectedObject="selectedObject" :canInput="config.canInput"
:buttonsConfig="config.buttonsConfig" :appId="store.appInfo?.appId" />
<q-field labelColor="primary" class="condition-object" dense outlined :label="label" :disable="disabled"
:clearable="isSelected">
<template v-slot:control>
<q-chip color="primary" text-color="white" v-if="isSelected && selectedObject.objectType==='field'" :dense="true" class="selected-obj">
{{ selectedObject.name }}
</q-chip>
<q-chip color="info" text-color="white" v-if="isSelected && selectedObject.objectType==='variable'" :dense="true" class="selected-obj">
{{ selectedObject.name.name }}
</q-chip>
<div v-if="isSelected && selectedObject.objectType==='text'">{{ selectedObject?.sharedText }}</div>
</template>
<template v-slot:append>
<q-icon name="search" class="cursor-pointer" @click="showDg" />
</template>
</q-field>
<show-dialog v-model:visible="show" name="設定項目" @close="closeDg" min-width="400px">
<!-- <template v-slot:toolbar>
<q-input dense debounce="200" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<condition-objects ref="appDg" name="フィールド" type="single" :filter="filter" :appId="store.appInfo?.appId" :vars="vars"></condition-objects>
-->
<DynamicItemInput v-model:selectedObject="selectedObject" :canInput="config.canInput"
:buttonsConfig="config.buttonsConfig" :appId="store.appInfo?.appId" :options="options" ref="inputRef" />
</show-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, watchEffect, computed } from 'vue';
import { defineComponent, reactive, ref, watchEffect, computed ,PropType} from 'vue';
import ShowDialog from '../ShowDialog.vue';
// import ConditionObjects from '../ConditionObjects.vue';
import DynamicItemInput from '../DynamicItemInput/DynamicItemInput.vue';
import { useFlowEditorStore } from '../../stores/flowEditor';
import { IActionFlow, IActionNode, IActionVariable } from '../../types/ActionTypes';
import { IDynamicInputConfig } from 'src/types/ComponentTypes';
export default defineComponent({
name: 'ConditionObject',
components: {
@@ -44,8 +48,16 @@ export default defineComponent({
// ConditionObjects
},
props: {
disabled: {
type: Boolean,
default: false
},
label: {
type: String,
default: undefined
},
config: {
type: Object,
type: Object as PropType<IDynamicInputConfig>,
default: () => {
return {
canInput: false,
@@ -56,6 +68,12 @@ export default defineComponent({
};
}
},
options:
{
type:Array as PropType< string[]>,
default:()=>[]
},
modelValue: {
type: Object,
default: null
@@ -63,12 +81,13 @@ export default defineComponent({
},
setup(props, { emit }) {
// const appDg = ref();
const inputRef=ref();
const show = ref(false);
const selectedObject = ref(props.modelValue);
const store = useFlowEditorStore();
// const sharedText = ref(''); // 共享的文本状态
const isSelected = computed(() => {
return selectedObject?.value?.sharedText !== '';
return selectedObject.value?.sharedText !== '';
});
// const isSelected = computed(()=>{
// return selectedObject.value!==null && typeof selectedObject.value === 'object' && ('name' in selectedObject.value)
@@ -85,6 +104,7 @@ export default defineComponent({
const closeDg = (val: string) => {
if (val == 'OK') {
// selectedObject.value = appDg.value.selected[0];
selectedObject.value = inputRef.value.selectedObjectRef
}
};
@@ -93,6 +113,7 @@ export default defineComponent({
});
return {
inputRef,
store,
// appDg,
show,

View File

@@ -68,18 +68,20 @@
<div @click.stop @keypress.stop v-else >
<div class="row no-wrap items-center q-my-xs">
<ConditionObject v-bind="prop.node" v-model="prop.node.object" :config="leftDynamicItemConfig" class="col-4"/>
<q-select v-model="prop.node.operator" :options="operators" class="operator" :outlined="true" :dense="true"></q-select>
<ConditionObject v-bind="prop.node" v-model="prop.node.value" :config="rightDynamicItemConfig" class="col-4"/>
<q-select v-model="prop.node.operator" :options="operators" class="operator" :outlined="true" :dense="true"></q-select>
<ConditionObject v-bind="prop.node" v-model="prop.node.value" :config="rightDynamicItemConfig" class="col-4"
:options="objectValueOptions(prop.node?.object?.options)"
/>
<!-- <ConditionObject v-bind="prop.node" v-model="prop.node.object" class="col-4"/> -->
<!-- <q-input v-if="!prop.node.object || !('options' in prop.node.object)"
v-model="prop.node.value"
class="condition-value" :outlined="true" :dense="true" ></q-input> -->
<q-select v-if="prop.node.object && ('options' in prop.node.object)"
<!-- <q-select v-if="prop.node.object && ('options' in prop.node.object)"
v-model="prop.node.value"
:options="objectValueOptions(prop.node.object.options)"
clearable
value-key="index"
class="condition-value" :outlined="true" :dense="true" ></q-select>
class="condition-value" :outlined="true" :dense="true" ></q-select> -->
<q-btn flat round dense icon="more_horiz" size="sm" >
<q-menu auto-close anchor="top right">
<q-list>
@@ -118,6 +120,7 @@ import { finished } from 'stream';
import { defineComponent,ref,reactive, computed, inject } from 'vue';
import { INode,ConditionTree,GroupNode,ConditionNode, LogicalOperator,Operator,NodeType } from '../../types/Conditions';
import ConditionObject from './ConditionObject.vue';
import { IDynamicInputConfig } from 'src/types/ComponentTypes';
export default defineComponent( {
name: 'NodeCondition',
components: {
@@ -145,17 +148,18 @@ export default defineComponent( {
return opts;
});
const operator = inject('Operator')
const operators =computed(()=>{
return operator ? operator : Object.values(Operator);
});
const operatorSet = inject<Array<any>>('Operator')
const operators = ref(operatorSet ? operatorSet : Object.values(Operator));
const tree = reactive(props.conditionTree);
const conditionString = computed(()=>{
return tree.buildConditionString(tree.root);
});
const objectValueOptions=(options:any):any[]=>{
const objectValueOptions=(options:any):any[]|null=>{
if(!options){
return null;
}
const opts:any[] =[];
Object.keys(options).forEach((key) =>
{
@@ -222,13 +226,14 @@ export default defineComponent( {
ticked.value=[];
}
const expanded=computed(()=>tree.getGroups(tree.root));
// addCondition(tree.root);
const leftDynamicItemConfig = inject<IDynamicInputConfig>('leftDynamicItemConfig');
const rightDynamicItemConfig = inject<IDynamicInputConfig>('rightDynamicItemConfig');
return {
leftDynamicItemConfig :inject('leftDynamicItemConfig'),
rightDynamicItemConfig:inject('rightDynamicItemConfig'),
leftDynamicItemConfig,
rightDynamicItemConfig,
showingCondition,
conditionString,
tree,
@@ -260,10 +265,12 @@ export default defineComponent( {
max-height: 40px;
margin: 0 2px;
}
.operator{
min-width: 150px;
max-height: 40px;
margin: 0 2px;
text-align: center;
font-size: 12pt;
}

View File

@@ -5,7 +5,7 @@
:url="uploadUrl"
:label="title"
:headers="headers"
accept=".csv,.xlsx"
accept=".xlsx"
v-on:rejected="onRejected"
v-on:uploaded="onUploadFinished"
v-on:failed="onFailed"
@@ -15,7 +15,7 @@
</template>
<script setup lang="ts">
import { createUploaderComponent, useQuasar } from 'quasar';
import { createUploaderComponent, useQuasar } from 'quasar';
import { useAuthStore } from 'src/stores/useAuthStore';
import { ref } from 'vue';
const $q=useQuasar();
@@ -30,7 +30,7 @@ import { ref } from 'vue';
// https://quasar.dev/quasar-plugins/notify#Installation
$q.notify({
type: 'negative',
message: `CSVおよびExcelファイルを選択してください。`
message: `Excelファイルを選択してください。`
})
}
@@ -40,7 +40,7 @@ import { ref } from 'vue';
function onUploadFinished({xhr}:{xhr:XMLHttpRequest}){
let msg="ファイルのアップロードが完了しました。";
if(xhr && xhr.response){
msg=`${msg} (${xhr.responseText})`;
msg=`${msg} (${xhr.responseText})`;
}
$q.notify({
type: 'positive',
@@ -52,14 +52,28 @@ import { ref } from 'vue';
}, 2000);
}
/**
* 例外発生時、responseからエラー情報を取得する
* @param xhr
*/
function getResponseError(xhr:XMLHttpRequest){
try{
const resp = JSON.parse(xhr.responseText);
return 'detail' in resp ? resp.detail:'';
}catch(err){
return xhr.responseText;
}
}
/**
*
* @param info ファイルアップロード失敗時の処理
*/
function onFailed({files,xhr}:{files: readonly any[],xhr:any}){
function onFailed({files,xhr}:{files: readonly any[],xhr:XMLHttpRequest}){
let msg ="ファイルアップロードが失敗しました。";
if(xhr && xhr.status){
msg=`${msg} (${xhr.status }:${xhr.statusText})`
const detail = getResponseError(xhr);
msg=`${msg} (${xhr.status }:${detail})`
}
$q.notify({
type:"negative",
@@ -74,7 +88,7 @@ import { ref } from 'vue';
const headers = ref([{name:"Authorization",value:'Bearer ' + authStore.token}]);
const props = withDefaults(defineProps<Props>(), {
title:"設計書から導入する(csv or excel)",
title:"設計書から導入する(Excel)",
uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1`
});

View File

@@ -2,13 +2,30 @@
<div class="q-mx-md" style="max-width: 600px;">
<!-- <q-card> -->
<div class="q-mb-md">
<q-input ref="inputRef" outlined dense debounce="200" @update:model-value="updateSharedText"
v-model="sharedText" :readonly="!canInput">
<q-input ref="inputRef" v-if="!optionsRef|| optionsRef.length===0"
outlined dense debounce="200" @update:model-value="updateSharedText"
v-model="sharedText" :readonly="!canInputFlag" autogrow>
<template v-slot:append>
<q-btn flat round padding="none" icon="cancel" @click="clearSharedText" color="grey-6" />
</template>
</q-input>
<q-select v-if="optionsRef && optionsRef.length>0"
:model-value="sharedText"
:options="optionsRef"
clearable
value-key="index"
outlined
dense
use-input
hide-selected
input-debounce="10"
fill-input
@input-value="setValue"
@clear="sharedText=null"
hide-dropdown-icon
:readonly="!canInputFlag"
>
</q-select>
</div>
<div class="row q-gutter-sm">
@@ -34,18 +51,12 @@
</template>
<script lang="ts">
import { ref, inject, watchEffect, defineComponent } from 'vue';
import { ref, inject, watchEffect, defineComponent,PropType } from 'vue';
import FieldAdd from './FieldAdd.vue';
import VariableAdd from './VariableAdd.vue';
// import FunctionAdd from './FunctionAdd.vue';
import ShowDialog from '../ShowDialog.vue';
type ButtonConfig = {
label: string;
color: string;
type: string;
editable: boolean;
};
import { IButtonConfig } from 'src/types/ComponentTypes';
export default defineComponent({
name: 'DynamicItemInput',
@@ -56,18 +67,21 @@ export default defineComponent({
ShowDialog
},
props: {
// canInput: {
// type: Boolean,
// default: false
// },
canInput: {
type: Boolean,
default: false
},
appId: {
type: String,
},
selectedObject: {
default: {}
},
options:{
type:Array as PropType< string[]>
},
buttonsConfig: {
type: Array as () => ButtonConfig[],
type: Array as PropType<IButtonConfig[]>,
default: () => [
{ label: 'フィールド', color: 'primary', type: 'FieldAdd' }
]
@@ -77,65 +91,71 @@ export default defineComponent({
const filter = ref('');
const dialogVisible = ref(false);
const currentDialogName = ref('');
const selectedObjectRef = ref(props.selectedObject);
const currentComponent = ref('FieldAdd');
const sharedText = ref(props.selectedObject?.sharedText ?? '');
const inputRef = ref();
const canInput = ref(true);
const canInputFlag = ref(props.canInput);
const editable = ref(false);
const openDialog = (button: ButtonConfig) => {
const openDialog = (button: IButtonConfig) => {
currentDialogName.value = button.label;
currentComponent.value = button.type;
dialogVisible.value = true;
editable.value = button.editable ?? true;
editable.value = canInputFlag.value;
};
const closeDialog = () => {
dialogVisible.value = false;
};
const handleSelect = (value) => {
// 获取当前光标位置
// const cursorPosition = inputRef.value.getNativeElement().selectionStart;
// if (cursorPosition === undefined || cursorPosition === 0) {
sharedText.value = `${value._t}`;
// } else {
// const textBefore = sharedText.value.substring(0, cursorPosition);
// const textAfter = sharedText.value.substring(cursorPosition);
// sharedText.value = `${textBefore}${value._t}${textAfter}`;
// }
const handleSelect = (value:any) => {
if (value && value._t && (value._t as string).length > 0) {
canInput.value = editable.value;
canInputFlag.value = editable.value;
}
emit('update:selectedObject', { sharedText: sharedText.value, ...value });
selectedObjectRef.value={ sharedText: value._t, ...value };
sharedText.value = `${value._t}`;
// emit('update:selectedObject', { sharedText: sharedText.value, ...value });
dialogVisible.value = false;
};
const clearSharedText = () => {
sharedText.value = '';
canInput.value = true;
emit('update:selectedObject', {});
selectedObjectRef.value={};
canInputFlag.value = true;
// emit('update:selectedObject', {});
}
const updateSharedText = (value) => {
const updateSharedText = (value:string) => {
sharedText.value = value;
emit('update:selectedObject', { ...props.selectedObject, sharedText: value });
selectedObjectRef.value= { sharedText: value,objectType:'text' }
// emit('update:selectedObject', { ...props.selectedObject, sharedText: value,objectType:'text' });
}
const setValue=(value:string)=>{
sharedText.value = value;
if(selectedObjectRef.value.sharedText!==value){
selectedObjectRef.value= { sharedText: value,objectType:'text' }
}
}
const optionsRef=ref(props.options);
return {
filter,
dialogVisible,
currentDialogName,
currentComponent,
canInput,
canInputFlag,
openDialog,
closeDialog,
handleSelect,
clearSharedText,
updateSharedText,
setValue,
sharedText,
inputRef
inputRef,
optionsRef,
selectedObjectRef
};
}
});
</script>
</script>

View File

@@ -19,6 +19,7 @@
</q-item-section>
</q-item>
<q-separator
class="q-my-sm"
v-if="isSeparator"
inset
/>

View File

@@ -1,18 +1,24 @@
<template>
<div class="q-pa-md">
<q-table flat bordered :loading="!isLoaded" row-key="name" :selection="type" :selected="modelValue"
@update:selected="$emit('update:modelValue', $event)" :filter="filter" :columns="columns" :rows="rows" />
@update:selected="$emit('update:modelValue', $event)"
:filter="filter"
:columns="columns"
:rows="rows"
:pagination="pagination"
style="max-height: 55vh;"/>
</div>
</template>
<script lang="ts">
import { useAsyncState } from '@vueuse/core';
import { api } from 'boot/axios';
import { computed } from 'vue';
import { computed ,Prop,PropType,ref} from 'vue';
import {IField} from 'src/types/ComponentTypes';
export default {
name: 'FieldList',
props: {
fields: Array,
fields: Array as PropType<IField[]>,
name: String,
type: String,
appId: Number,
@@ -30,27 +36,32 @@ export default {
{ name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true }
]
const { state : rows, isReady: isLoaded, isLoading } = useAsyncState((args) => {
if (props.fields && Object.keys(props.fields).length > 0) {
return props.fields.map(f => ({ name: f.label, objectType: 'field', ...f }));
return props.fields.map(f => ({ name: f.label, ...f ,objectType: 'field'}));
} else {
return api.get('api/v1/appfields', {
params: {
app: props.appId
}
}).then(res => {
console.log(res);
return Object.values(res.data.properties).map(f => ({ name: f.label, objectType: 'field', ...f }));
const fields = res.data.properties;
return Object.values(fields).map((f:any) => ({ name: f.label, objectType: 'field', ...f }));
});
}
}, [{ name: '', objectType: '', type: '', code: '', label: '' }])
return {
columns,
rows,
// selected: ref([]),
isLoaded
isLoaded,
pagination: ref({
rowsPerPage: 25,
sortBy: 'name',
descending: false,
page: 1,
})
}
},

View File

@@ -3,7 +3,7 @@
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table flat bordered v-else row-key="name" :selection="type" v-model:selected="selected" :columns="columns"
<q-table flat bordered v-else row-key="id" :selection="type" v-model:selected="selected" :columns="columns"
:rows="rows" :pagination="pageSetting" :filter="filter" style="max-height: 55vh;"/>
</div>
</template>
@@ -26,7 +26,7 @@ export default {
default: false,
},
selectedFields:{
type:Array,
type:Array ,
default:()=>[]
},
fieldTypes:{
@@ -37,6 +37,10 @@ export default {
updateSelectFields: {
type: Function
},
blackListLabel: {
type:Array,
default:()=>[]
}
},
setup(props) {
const isLoaded = ref(false);
@@ -44,16 +48,16 @@ export default {
{ name: 'name', required: true, label: 'フィールド名', align: 'left', field: row => row.name, sortable: true },
{ name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true }
]
];
const pageSetting = ref({
sortBy: 'desc',
sortBy: 'name',
descending: false,
page: 1,
rowsPerPage: props.not_page ? 0 : 5
rowsPerPage: props.not_page ? 0 : 25
// rowsNumber: xx if getting data from a server
});
const rows = reactive([]);
const selected = ref(props.selectedFields && props.selectedFields.length>0?props.selectedFields:[]);
const selected = ref((props.selectedFields && props.selectedFields.length>0)?props.selectedFields:[]);
onMounted(async () => {
const url = props.fieldTypes.includes('SPACER')?'api/v1/allfields':'api/v1/appfields';
@@ -62,16 +66,25 @@ export default {
app: props.appId
}
});
let fields = res.data.properties;
Object.keys(fields).forEach((key) => {
const fld = fields[key];
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
rows.push({ name: fld.label || fld.code, ...fld });
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){
rows.push({ name: fld.label || fld.code, ...fld });
let fields = Object.values(res.data.properties);
for (const index in fields) {
const fld = fields[index]
if(props.blackListLabel.length > 0){
if(!props.blackListLabel.find(blackListItem => blackListItem === fld.label)){
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}
}
} else {
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}
}
});
}
isLoaded.value = true;
});

View File

@@ -11,7 +11,7 @@
<q-card-section class="q-mt-md" :style="sectionStyle">
<slot></slot>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-card-actions v-if="!disableBtn" align="right" class="text-primary">
<q-btn flat label="確定" v-close-popup @click="CloseDialogue('OK')" />
<q-btn flat label="キャンセル" v-close-popup @click="CloseDialogue('Cancel')" />
</q-card-actions>
@@ -29,7 +29,11 @@ export default {
width:String,
height:String,
minWidth:String,
minHeight:String
minHeight:String,
disableBtn:{
type: Boolean,
default: false
}
},
emits: [
'close'

View File

@@ -0,0 +1,36 @@
<template>
<q-table :rows="rows" :columns="columns" row-key="id" :filter="props.filter" :loading="loading"
:pagination="pagination" selection="single" v-model:selected="selected"></q-table>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { api } from 'boot/axios';
const props = defineProps<{filter:string}>()
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'firstName', label: '氏名', field: 'firstName', align: 'left', sortable: true },
{ name: 'lastName', label: '苗字', field: 'lastName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 10 });
const rows = ref([]);
const loading = ref(false);
const selected = ref([]);
defineExpose({
selected
})
const getUsers = async (filter = () => true) => {
loading.value = true;
const result = await api.get(`api/v1/users`);
rows.value = result.data.map((item) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active }
}).filter(filter);
loading.value = false;
}
onMounted(async () => {
await getUsers();
})
</script>

View File

@@ -8,8 +8,8 @@
class="q-mr-sm">
</q-icon>
<div class="no-wrap"
:class="selectedEvent && prop.node.eventId === selectedEvent.eventId ? 'selected-node' : ''">{{
prop.node.label }}</div>
:class="selectedEvent && prop.node.eventId === selectedEvent.eventId ? 'selected-node' : ''">{{prop.node.label }}
</div>
<q-space></q-space>
<!-- <q-icon v-if="prop.node.hasFlow" name="delete" color="negative" size="16px" class="q-mr-sm"></q-icon> -->
</div>
@@ -27,14 +27,14 @@
<template v-slot:header-DELETABLE="prop">
<div class="row col items-start event-node" @click="onSelected(prop.node)">
<q-icon v-if="prop.node.eventId" name="play_circle" :color="prop.node.hasFlow ? 'green' : 'grey'" size="16px" class="q-mr-sm" />
<div class="no-wrap" :class="selectedEvent && prop.node.eventId === selectedEvent.eventId ? 'selected-node' : ''" >{{ prop.node.label }}</div>
<div class="no-wrap" :class="selectedEvent && prop.node.eventId === selectedEvent.eventId ? 'selected-node' : ''" >{{ prop.node.label}}</div>
<q-space></q-space>
<q-icon name="delete_forever" color="negative" size="16px" @click="deleteEvent(prop.node)"></q-icon>
</div>
</template>
</q-tree>
<show-dialog v-model:visible="showDialog" name="フィールド選択" @close="closeDg">
<field-select ref="appDg" name="フィールド" type="single" :appId="store.appInfo?.appId"></field-select>
<field-select ref="appDg" name="フィールド" type="single" :fieldTypes="fieldTypes" :appId="store.appInfo?.appId"></field-select>
</show-dialog>
</template>
@@ -42,8 +42,8 @@
import { QTree, useQuasar } from 'quasar';
import { ActionFlow, RootAction } from 'src/types/ActionTypes';
import { useFlowEditorStore } from 'stores/flowEditor';
import { defineComponent, ref } from 'vue';
import { IKintoneEvent, IKintoneEventGroup, IKintoneEventNode } from '../../types/KintoneEvents';
import { defineComponent, ref,watchEffect } from 'vue';
import { IKintoneEvent, IKintoneEventGroup, IKintoneEventNode, kintoneEvent } from '../../types/KintoneEvents';
import FieldSelect from '../FieldSelect.vue';
import ShowDialog from '../ShowDialog.vue';
export default defineComponent({
@@ -58,12 +58,25 @@ export default defineComponent({
const store = useFlowEditorStore();
const showDialog = ref(false);
const tree = ref<QTree>();
const fieldTypes=[
'RADIO_BUTTON',
'DROP_DOWN',
'CHECK_BOX',
'MULTI_SELECT',
'USER_SELECT',
'GROUP_SELECT',
'ORGANIZATION_SELECT',
'DATE',
'DATETIME',
'TIME',
'SINGLE_LINE_TEXT',
'NUMBER'];
// const eventTree=ref(kintoneEvents);
// const selectedFlow = store.currentFlow;
// const expanded=ref();
const selectedEvent = ref<IKintoneEvent | null>(null);
const selectedChangeEvent = ref<IKintoneEventGroup | null>(null);
const selectedEvent = ref<IKintoneEvent | undefined>(store.selectedEvent);
const selectedChangeEvent = ref<IKintoneEventGroup | undefined>(undefined);
const isFieldChange = (node: IKintoneEventNode) => {
return node.header == 'EVENT' && node.eventId.indexOf(".change.") > -1;
}
@@ -76,12 +89,12 @@ export default defineComponent({
if (store.eventTree.findEventById(eventid)) {
return;
}
selectedChangeEvent.value?.events.push({
eventId: eventid,
label: field.name,
parentId: selectedChangeEvent.value.eventId,
header: 'DELETABLE'
});
selectedChangeEvent.value?.events.push(new kintoneEvent(
field.name,
eventid,
selectedChangeEvent.value.eventId,
'DELETABLE'
));
tree.value?.expanded?.push(selectedChangeEvent.value.eventId);
tree.value?.expandAll();
}
@@ -136,6 +149,9 @@ export default defineComponent({
selectedEvent.value.flowData = flow;
}
};
watchEffect(()=>{
store.setCurrentEvent(selectedEvent.value);
});
return {
// eventTree,
// expanded,
@@ -148,7 +164,8 @@ export default defineComponent({
addChangeEvent,
deleteEvent,
closeDg,
store
store,
fieldTypes
}
}
});

View File

@@ -1,152 +1,165 @@
<template>
<div class="q-my-md" v-bind="$attrs">
<q-card flat>
<q-card-section class="q-pa-none q-my-sm q-mr-md">
<!-- <div class=" q-my-none ">App Field Select</div> -->
<div class="row q-mb-xs">
<div class="text-primary q-mb-xs text-caption">{{ $props.displayName }}</div>
</div>
<div class="row">
<div class="col">
<div class="q-mb-xs">{{ selectedField.app?.name || '未選択' }}</div>
</div>
<div class="col-1">
<q-btn round flat size="sm" color="primary" icon="search" @click="showDg" />
</div>
</div>
</q-card-section>
<q-separator />
<q-card-section class="q-pa-none q-ma-none">
<div style="">
<div v-if="selectedField.fields && selectedField.fields.length > 0">
<q-list bordered>
<q-virtual-scroll style="max-height: 160px;" :items="selectedField.fields" separator
v-slot="{ item, index }">
<q-item :key="index" dense clickable>
<q-item-section>
<q-item-label>
{{ item.label }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-btn round flat size="sm" icon="clear" @click="removeField(index)" />
</q-item-section>
</q-item>
</q-virtual-scroll>
</q-list>
</div>
<!-- <div v-else class="row q-mt-lg">
</div> -->
</div>
<!-- <q-separator /> -->
</q-card-section>
<q-card-section class="q-px-none q-py-xs" v-if="selectedField.fields && selectedField.fields.length === 0">
<div class="row">
<div class="text-grey text-caption"> {{ $props.placeholder }}</div>
<!-- <q-btn flat color="grey" label="clear" @click="clear" /> -->
</div>
</q-card-section>
</q-card>
<div class="q-my-md" v-bind="$attrs">
<q-field v-model="selectedField" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label
:rules="rulesExp" lazy-rules="ondemand" @clear="clear" ref="fieldRef">
<template v-slot:control>
{{ isSelected ? selectedField.app?.name : "(未選択)" }}
</template>
<template v-slot:hint v-if="!isSelected">
{{ placeholder }}
</template>
<template v-slot:append>
<q-icon name="search" class="cursor-pointer" color="primary" @click="showDg" />
</template>
</q-field>
<div v-if="selectedField.fields && selectedField.fields.length > 0">
<q-list bordered>
<q-virtual-scroll style="max-height: 160px;" :items="selectedField.fields" separator v-slot="{ item, index }">
<q-item :key="index" dense clickable>
<q-item-section>
<q-item-label>
{{ item.label }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-btn round flat size="sm" icon="clear" @click="removeField(index)" />
</q-item-section>
</q-item>
</q-virtual-scroll>
</q-list>
</div>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeAFBox">
<AppFieldSelectBox v-model:selectedField="selectedField" :selectType="selectType" ref="afBox"/>
<AppFieldSelectBox v-model:selectedField="selectedField" :selectType="selectType" ref="afBox"
:fieldTypes="fieldTypes" />
</show-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watchEffect } from 'vue';
import { computed, defineComponent, ref, watchEffect } from 'vue';
import AppFieldSelectBox from '../AppFieldSelectBox.vue';
import ShowDialog from '../ShowDialog.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
export interface IApp {
id: string,
name: string
id: string,
name: string
}
export interface IField {
name: string,
code: string,
type: string
name: string,
code: string,
type: string,
label?: string
}
export interface IAppFields {
app?: IApp,
fields: IField[]
app?: IApp,
fields: IField[]
}
export default defineComponent({
inheritAttrs: false,
name: 'AppFieldSelect',
components: {
ShowDialog,
AppFieldSelectBox
inheritAttrs: false,
name: 'AppFieldSelect2',
components: {
ShowDialog,
AppFieldSelectBox
},
props: {
displayName: {
type: String,
default: '',
},
props: {
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
modelValue: {
type: Object,
default: null
},
selectType: {
type: String,
default: 'single'
}
name: {
type: String,
default: '',
},
setup(props, { emit }) {
const show = ref(false);
const afBox = ref();
const selectedField = ref<IAppFields>({
app: undefined,
fields: []
});
if (props.modelValue && 'app' in props.modelValue && 'fields' in props.modelValue) {
selectedField.value = props.modelValue as IAppFields;
}
const store = useFlowEditorStore();
const clear = () => {
selectedField.value = {
fields: []
};
}
const removeField = (index: number) => {
selectedField.value.fields.splice(index, 1);
}
const closeAFBox = (val: string) => {
if (val == 'OK') {
console.log(afBox.value);
selectedField.value = afBox.value.selField;
}
};
watchEffect(() => {
emit('update:modelValue', selectedField.value);
});
return {
store,
afBox,
show,
showDg: () => { show.value = true },
selectedField,
clear,
removeField,
closeAFBox,
};
placeholder: {
type: String,
default: '',
},
modelValue: {
type: Object,
default: null
},
selectType: {
type: String,
default: 'single'
},
fieldTypes: {
type: Array,
default: () => []
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const show = ref(false);
const afBox = ref();
const fieldRef = ref();
const selectedField = ref<IAppFields>({
app: undefined,
fields: []
});
if (props.modelValue && 'app' in props.modelValue && 'fields' in props.modelValue) {
selectedField.value = props.modelValue as IAppFields;
}
const store = useFlowEditorStore();
const clear = () => {
selectedField.value = {
fields: []
};
}
const removeField = (index: number) => {
selectedField.value.fields.splice(index, 1);
}
const closeAFBox = (val: string) => {
if (val == 'OK') {
console.log(afBox.value);
selectedField.value = afBox.value.selField;
fieldRef.value.validate();
}
};
const isSelected = computed(() => {
return !!selectedField.value.app
});
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required ? [((val: any) => (val && val.app && val.fields && val.fields.length > 0) || errmsg)] : [];
const rulesExp = [...requiredExp, ...customExp];
watchEffect(() => {
emit('update:modelValue', selectedField.value);
});
return {
store,
afBox,
show,
showDg: () => { show.value = true },
selectedField,
clear,
removeField,
closeAFBox,
isSelected,
rulesExp,
fieldRef
};
}
});
</script>

View File

@@ -1,14 +1,18 @@
<template>
<div>
<q-field :label="displayName" labelColor="primary" stack-label>
<div v-bind="$attrs">
<q-field :label="displayName" labelColor="primary" stack-label
:rules="rulesExp"
lazy-rules="ondemand"
v-model="selectedApp"
ref="fieldRef">
<template v-slot:control>
<q-card flat class="full-width">
<q-card-actions vertical>
<q-btn color="grey-3" text-color="black" @click="() => { dgIsShow = true }">アプリ選択</q-btn>
</q-card-actions>
<q-card-section class="text-caption">
<div v-if="selectedField.app.name">
{{ selectedField.app.name }}
<div v-if="selectedApp.app.name">
{{ selectedApp.app.name }}
</div>
<div v-else>{{ placeholder }}</div>
</q-card-section>
@@ -43,10 +47,6 @@ export default defineComponent({
AppSelectBox
},
props: {
context: {
type: Array<Props>,
default: '',
},
displayName: {
type: String,
default: '',
@@ -62,31 +62,50 @@ export default defineComponent({
modelValue: {
type: Object,
default: null
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const appDg = ref()
const appDg = ref();
const fieldRef=ref();
const dgIsShow = ref(false)
const selectedField = props.modelValue && props.modelValue.app ? props.modelValue : reactive({app:{}});
const selectedApp = props.modelValue && props.modelValue.app ? props.modelValue : reactive({app:{}});
const closeDg = (state: string) => {
dgIsShow.value = false;
if (state == 'OK') {
selectedField.app = appDg.value.selected[0];
selectedApp.app = appDg.value.selected[0];
fieldRef.value.validate();
}
};
console.log(selectedField);
//ルール設定
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}を選択してください。`;
const requiredExp = props.required ? [((val: any) => (!!val && !!val.app && !!val.app.name) || errmsg)] : [];
const rulesExp = [...requiredExp, ...customExp];
watchEffect(() => {
emit('update:modelValue', selectedField);
emit('update:modelValue', selectedApp);
});
return {
filter: ref(''),
dgIsShow,
appDg,
fieldRef,
closeDg,
selectedField
selectedApp,
rulesExp
};
}
});

View File

@@ -0,0 +1,133 @@
<template>
<div v-bind="$attrs">
<q-field :label="displayName" labelColor="primary" stack-label lazy-rules="ondemand" ref="fieldRef">
<template v-slot:control>
<q-card flat class="full-width">
<q-card-actions vertical>
<q-btn color="grey-3" text-color="black" @click="() => { dgIsShow = true }">クリックで設定</q-btn>
</q-card-actions>
<q-card-section class="text-caption">
<div v-if="data.dropDownApp?.name">
{{ `${data.sourceApp?.name} -> ${data.dropDownApp?.name}` }}
</div>
<div v-else>{{ placeholder }}</div>
</q-card-section>
</q-card>
</template>
</q-field>
</div>
<ShowDialog v-model:visible="dgIsShow" name="ドロップダウン階層化設定" @close="closeDg" min-width="50vw" min-height="20vh" disableBtn>
<template v-slot:toolbar>
<q-btn flat round dense icon="more_vert" >
<q-menu auto-close anchor="bottom start">
<q-list>
<q-item clickable @click="copySetting()">
<q-item-section avatar><q-icon name="content_copy" ></q-icon></q-item-section>
<q-item-section >コピー</q-item-section>
</q-item>
<q-item clickable @click="pasteSetting()">
<q-item-section avatar><q-icon name="content_paste" ></q-icon></q-item-section>
<q-item-section >貼り付け</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</template>
<div class="q-mb-md q-ml-md q-mr-md">
<CascadingDropDownBox v-model:model-value="data" :finishDialogHandler="finishDialogHandler" />
</div>
</ShowDialog>
</template>
<script lang="ts">
import { defineComponent, ref, watchEffect } from 'vue';
import ShowDialog from '../ShowDialog.vue';
import CascadingDropDownBox from '../CascadingDropDownBox.vue';
export default defineComponent({
inheritAttrs: false,
name: 'CascadingDropDown',
components: {
ShowDialog,
CascadingDropDownBox
},
props: {
displayName: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
modelValue: {
type: Object,
default: () => { ({}) }
},
},
setup(props, { emit }) {
const dgIsShow = ref(false);
// const data = ref(props.modelValue);
const data = ref({
sourceApp: props.modelValue.sourceApp ?? { appFilter: '', showSelectApp: false },
dropDownApp: props.modelValue.dropDownApp,
fieldList: props.modelValue.fieldList ?? [],
});
const closeDg = (state: string) => {
dgIsShow.value = false;
};
const finishDialogHandler = (boxData) => {
data.value = boxData
dgIsShow.value = false
emit('update:modelValue', data.value);
}
//設定をコピーする
const copySetting=()=>{
if (navigator.clipboard) {
const jsonData= JSON.stringify(data.value);
navigator.clipboard.writeText(jsonData).then(() => {
console.log('Text successfully copied to clipboard');
},
(err) => {
console.error('Error in copying text: ', err);
});
} else {
console.log('Clipboard API not available');
}
};
//設定を貼り付ける
const pasteSetting=async ()=>{
try {
const text = await navigator.clipboard.readText();
console.log('Text from clipboard:', text);
const jsonData=JSON.parse(text);
if('sourceApp' in jsonData && 'dropDownApp' in jsonData && 'fieldList' in jsonData){
const {sourceApp,dropDownApp, fieldList}=jsonData;
data.value.sourceApp=sourceApp;
data.value.dropDownApp=dropDownApp;
data.value.fieldList=fieldList;
}
} catch (err) {
console.error('Failed to read text from clipboard: ', err);
throw err;
}
}
// watchEffect(() => {
// emit('update:modelValue', data.value);
// });
return {
dgIsShow,
closeDg,
data,
finishDialogHandler,
copySetting,
pasteSetting
};
}
});
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="" v-bind="$attrs">
<q-field v-model="color" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label :bottom-slots="!isSelected" >
<q-field v-model="color" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label :bottom-slots="!isSelected" :rules="rulesExp">
<template v-slot:control>
<q-chip text-color="black" color="white" v-if="isSelected">
<div class="row">
@@ -57,17 +57,34 @@ export default defineComponent({
type: String,
default: null
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const color = ref(props.modelValue??"");
const isSelected = computed(()=>props.modelValue && props.modelValue!=="");
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg ),"anyColor"]:[];
const rulesExp=[...requiredExp,...customExp];
watchEffect(()=>{
emit('update:modelValue', color.value);
});
return {
color,
isSelected
isSelected,
rulesExp
};
}
});

View File

@@ -18,39 +18,11 @@
</div>
</template>
<script lang="ts">
import { ConditionNode, ConditionTree, Operator, OperatorListItem } from 'app/src/types/Conditions';
import { computed, defineComponent, provide, reactive, ref, watchEffect } from 'vue';
import ConditionEditor from '../ConditionEditor/ConditionEditor.vue';
type Props = {
props?: {
name: string;
modelValue?: {
app: {
id: string;
name: string;
},
fields: {
type: string;
label: string;
code: string;
}[]
}
}
};
type InputConfg = {
canInput: boolean;
buttonsConfig: {
label: string;
color: string;
type: string;
}[]
};
import { IActionProperty } from 'src/types/ActionTypes';
export default defineComponent({
name: 'FieldInput',
@@ -60,7 +32,7 @@ export default defineComponent({
},
props: {
context: {
type: Array<Props>,
type: Array<IActionProperty>,
default: '',
},
displayName: {
@@ -87,6 +59,10 @@ export default defineComponent({
type: String,
default: 'field'
},
connectProps:{
type:Object,
default:()=>({})
},
onlySourceSelect: {
type: Boolean,
default: false
@@ -115,7 +91,10 @@ export default defineComponent({
},
setup(props, { emit }) {
const source = props.context.find(element => element?.props?.name === 'sources')
let source = reactive(props.connectProps["source"]);
if(!source){
source = props.context.find(element => element.props.name === 'sources');
}
if (source) {
if (props.sourceType === 'field') {
@@ -157,7 +136,8 @@ export default defineComponent({
const isSetted = ref(props.modelValue && props.modelValue !== '');
const conditionString = computed(() => {
return tree.buildConditionString(tree.root);
const condiStr= tree.buildConditionString(tree.root);
return condiStr==='()'?'(条件なし)':condiStr;
});
const showDg = () => {

View File

@@ -1,6 +1,10 @@
<template>
<div>
<q-field :label="displayName" labelColor="primary" stack-label>
<div class="q-my-md" v-bind="$attrs">
<q-field :label="displayName" labelColor="primary" stack-label
v-model="mappingProps"
:rules="rulesExp"
ref="fieldRef"
>
<template v-slot:control>
<q-card flat class="full-width">
<q-card-actions vertical>
@@ -16,64 +20,77 @@
</q-card>
</template>
</q-field>
<show-dialog v-model:visible="dgIsShow" name="データマッピング" @close="closeDg" min-width="50vw" min-height="60vh">
<div class="q-mx-md">
<show-dialog v-model:visible="dgIsShow" name="データマッピング" @close="closeDg" min-width="55vw" min-height="60vh">
<div class="">
<div class="row q-col-gutter-x-xs flex-center">
<div class="col-6">
<div class="col-5">
<div class="q-mx-xs">ソース</div>
</div>
<!-- <div class="col-1">
</div> -->
<div class="col-6">
<div class="q-mx-xs">目標</div>
<div class="col-5">
<div class="row justify-between q-mr-md">
<div class="">{{ sourceApp?.name }}</div>
<q-btn outline color="primary" size="xs" label="最新のフィールドを取得する"
@click="() => updateFields(sourceAppId!)" />
</div>
</div>
<div class="col-1 q-pl-sm">
キー
</div>
<!-- <div class="col-1"><q-btn flat round dense icon="add" size="sm" @click="addMappingObject" /> -->
<!-- </div> -->
</div>
<q-virtual-scroll style="max-height: 75vh;" :items="mappingProps" separator v-slot="{ item, index }">
<q-virtual-scroll style="max-height: 60vh;" :items="mappingProps.data" separator v-slot="{ item, index }">
<!-- <div class="q-my-sm" v-for="(item, index) in mappingProps" :key="item.id"> -->
<div class="row q-my-md q-col-gutter-x-md flex-center">
<div class="col-6">
<ConditionObject :config="config" v-model="item.from" />
<div class="row q-pa-sm q-col-gutter-x-md flex-center">
<div class="col-5">
<ConditionObject :config="config" v-model="item.from" :disabled="item.disabled"
:label="item.disabled ? '「Lookup」によってロックされる' : undefined" />
</div>
<!-- <div class="col-1">
</div> -->
<div class="col-6">
<q-field v-model="item.vName" type="text" outlined dense>
<div class="col-5">
<q-field v-model="item.vName" type="text" outlined dense :disable="item.disabled" >
<!-- <template v-slot:append>
<q-icon name="search" class="cursor-pointer"
@click="() => { mappingProps[index].to.isDialogVisible = true }" />
</template> -->
<template v-slot:control>
<div class="self-center full-width no-outline" tabindex="0"
<div class="self-center full-width no-outline" tabindex="0"
v-if="item.to.app?.name && item.to.fields?.length > 0 && item.to.fields[0].label">
{{ `${item.to.fields[0].label}` }}
<q-tooltip>
<span class="text-red" v-if="item.to.fields[0].required">*</span>
<q-tooltip class="bg-yellow-2 text-black shadow-4" >
<div>アプリ : {{ item.to.app.name }}</div>
<div>フィールドのコード : {{ item.to.fields[0].code }}</div>
<div>フィールドのタイプ : {{ item.to.fields[0].type }}</div>
<div>フィールド : {{ item.to.fields[0] }}</div>
<div v-if="item.to.fields[0].required">必須項目</div>
<!-- <div>フィールド : {{ item.to.fields[0] }}</div>
<div>フィールド : {{ item.isKey }}</div> -->
</q-tooltip>
</div>
</template>
</q-field>
</div>
<!-- <div class="col-1">
<q-btn flat round dense icon="delete" size="sm" @click="() => deleteMappingObject(index)" />
</div> -->
<div class="col-1">
<q-checkbox size="sm" v-model="item.isKey" :disable="item.disabled" />
<!-- <q-btn flat round dense icon="delete" size="sm" @click="() => deleteMappingObject(index)" /> -->
</div>
</div>
<show-dialog v-model:visible="mappingProps[index].to.isDialogVisible" name="フィールド一覧"
<show-dialog v-model:visible="mappingProps.data[index].to.isDialogVisible" name="フィールド一覧"
@close="closeToDg" ref="fieldDlg">
<FieldSelect v-if="onlySourceSelect" ref="fieldDlg" name="フィールド" :appId="sourceAppId" not_page
:selectedFields="mappingProps[index].to.fields"
:updateSelects="(fields) => { mappingProps[index].to.fields = fields; mappingProps[index].to.app = sourceApp }">
:selectedFields="mappingProps.data[index].to.fields"
:updateSelects="(fields) => { mappingProps.data[index].to.fields = fields; mappingProps.data[index].to.app = sourceApp }">
</FieldSelect>
<AppFieldSelectBox v-else v-model:selectedField="mappingProps[index].to" />
<AppFieldSelectBox v-else v-model:selectedField="mappingProps.data[index].to" />
</show-dialog>
<!-- </div> -->
</q-virtual-scroll>
<div class="q-mt-lg q-ml-md row ">
<q-checkbox size="sm" v-model="mappingProps.createWithNull" label="キーが存在しない場合は新規に作成され、存在する場合はデータが更新されます。" />
</div>
</div>
</show-dialog>
</div>
@@ -86,10 +103,10 @@ import ConditionObject from '../ConditionEditor/ConditionObject.vue';
import ShowDialog from '../ShowDialog.vue';
import AppFieldSelectBox from '../AppFieldSelectBox.vue';
import FieldSelect from '../FieldSelect.vue';
import IAppFields from './AppFieldSelect.vue';
import { IApp, IField } from './AppFieldSelect.vue';
import { api } from 'boot/axios';
type Props = {
type ContextProps = {
props?: {
name: string;
modelValue?: {
@@ -100,14 +117,25 @@ type Props = {
}
}
};
type ValueType = {
id: string;
from: object;
to: typeof IAppFields & {
isDialogVisible: boolean;
};
interface IMappingSetting {
data: IMappingValueType[];
createWithNull: boolean;
}
interface IMappingValueType {
id: string;
from: { sharedText?: string };
to: {
app?: IApp,
fields: IField[],
isDialogVisible: boolean;
};
isKey: boolean;
disabled: boolean;
}
const blackListLabelName = ['レコード番号', '作業者', '更新者', '更新日時', '作成日時', '作成者']
export default defineComponent({
name: 'DataMapping',
@@ -120,7 +148,7 @@ export default defineComponent({
},
props: {
context: {
type: Array<Props>,
type: Array<ContextProps>,
default: '',
},
displayName: {
@@ -132,7 +160,7 @@ export default defineComponent({
default: '',
},
modelValue: {
type: Object as () => ValueType[],
type: Object as () => IMappingSetting,
},
placeholder: {
type: String,
@@ -141,73 +169,114 @@ export default defineComponent({
onlySourceSelect: {
type: Boolean,
default: false
},
fieldTypes:{
type:Array,
default:()=>[]
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const fieldRef=ref();
const source = props.context.find(element => element?.props?.name === 'sources')
const sourceApp = computed(() => source?.props?.modelValue?.app);
const sourceAppId = computed(() => sourceApp.value?.id);
//ルール設定
const checkMapping = (val:IMappingSetting)=>{
if(!val || !val.data){
return false;
}
console.log(val);
const mappingDatas = val.data.filter(item=>item.from?.sharedText && item.to.fields?.length > 0);
return mappingDatas.length>0;
}
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}を選択してください。`;
const requiredExp = props.required ? [((val: any) => checkMapping(val) || errmsg)] : [];
const rulesExp = [...requiredExp, ...customExp];
// const mappingProps = ref(props.modelValue?.data ?? []);
// const createWithNull = ref(props.modelValue?.createWithNull ?? false);
const mappingProps = reactive<IMappingSetting>({
data:props.modelValue?.data ?? [],
createWithNull:props.modelValue?.createWithNull ?? false
});
const closeDg = () => {
emit('update:modelValue', mappingProps.value
);
fieldRef.value.validate();
emit('update:modelValue',mappingProps);
}
const closeToDg = () => {
emit('update:modelValue', mappingProps.value
);
emit('update:modelValue',mappingProps);
}
const mappingProps = computed(() => props.modelValue ?? []);
watch(() => sourceAppId.value, async (newId, oldId) => {
// 外部ソースコンポーネントの appid をリッスンし、変更されたときに現在のコンポーネントを更新します
watch(() => sourceAppId.value, async (newId,) => {
if (!newId) return;
const a = await api.get('api/v1/appfields', {
updateFields(newId)
})
const updateFields = async (sourceAppId: string) => {
const ktAppFields = await api.get('api/v1/appfields', {
params: {
app: newId
app: sourceAppId
}
}).then(res => {
return Object.values(res.data.properties)
// kintoneのデフォルトの非表示フィールドフィルタリング
.filter(f => !blackListLabelName.find(label => f.label === label))
.map(f => ({ name: f.label, objectType: 'field', ...f }))
.map(f => {
// 更新前の値を求める
const beforeData = mappingProps.data.find(m => m.to.fields[0].code === f.code)
return {
id: uuidv4(),
from: {},
from: beforeData?.from ?? {}, // 以前のデータを入力します
to: {
app: sourceApp.value,
fields: [f],
isDialogVisible: false
}
},
isKey: beforeData?.isKey ?? false, // 以前のデータを入力します
disabled: false
}
})
})
const modelValue = props.modelValue ?? [];
if (modelValue.length === 0 || newId !== oldId) {
emit('update:modelValue', a);
return;
// 「ルックアップ」によってロックされているフィールドを検索する
const lookupFixedField = ktAppFields
.filter(field => field.to.fields[0].lookup !== undefined)
.flatMap(field => field.to.fields[0].lookup.fieldMappings.map((m) => m.field))
// 「ルックアップ」でロックされたビューコンポーネントを非対話型に設定します
if (lookupFixedField.length > 0) {
ktAppFields.filter(f => lookupFixedField.includes(f.to.fields[0].code)).forEach(f => f.disabled = true)
}
const modelValueFieldNames = modelValue.map(item => item.to.fields[0].name);
const newFields = a.filter(field => !modelValueFieldNames.includes(field.to.fields[0].name));
const updatedModelValue = [...modelValue, ...newFields];
emit('update:modelValue', updatedModelValue);
})
console.log(mappingProps.value);
// const deleteMappingObject = (index: number) => mappingProps.length === 1
// ? mappingProps.splice(0, mappingProps.length, defaultMappingProp())
// : mappingProps.splice(index, 1);
mappingProps.data = ktAppFields
}
const mappingObjectsInputDisplay = computed(() =>
(mappingProps.value && Array.isArray(mappingProps.value)) ?
mappingProps.value
(mappingProps.data && Array.isArray(mappingProps.data)) ?
mappingProps.data
.filter(item => item.from?.sharedText && item.to.fields?.length > 0)
.map(item => {
return `field(${item.to.app?.id},${item.to.fields[0].label}) = ${item.from.sharedText} `;
@@ -215,36 +284,39 @@ export default defineComponent({
: []
);
const btnDisable = computed(() => props.onlySourceSelect ? !(source?.props?.modelValue?.app?.id) : false);
//集計処理方法
watchEffect(() => {
emit('update:modelValue', mappingProps.value);
emit('update:modelValue', mappingProps);
});
return {
uuidv4,
dgIsShow: ref(false),
fieldRef,
closeDg,
toDgIsShow: ref(false),
closeToDg,
mappingProps,
updateFields,
// addMappingObject: () => mappingProps.push(defaultMappingProp()),
// deleteMappingObject,
mappingObjectsInputDisplay,
sourceApp,
sourceAppId,
btnDisable,
rulesExp,
checkMapping,
config: {
canInput: false,
buttonsConfig: [
{ label: '変数', color: 'green', type: 'VariableAdd',editable:false },
{ label: 'フィールド', color: 'primary', type: 'FieldAdd' },
{ label: '変数', color: 'green', type: 'VariableAdd', editable: false },
]
}
};
},
});
</script>
<style lang="scss"></style>

View File

@@ -1,6 +1,11 @@
<template>
<div>
<q-field :label="displayName" labelColor="primary" stack-label>
<q-field :label="displayName" labelColor="primary" stack-label
v-model="processingProps"
:rules="rulesExp"
lazy-rules="ondemand"
ref="fieldRef"
>
<template v-slot:control>
<q-card flat class="full-width">
<q-card-actions vertical>
@@ -40,7 +45,7 @@
<div class="col-5">
<ConditionObject v-model="item.field" />
</div>
<div class="col-2">
<div class="col-2 q-pa-sm">
<q-select v-model="item.logicalOperator" :options="logicalOperators" outlined dense></q-select>
</div>
<div class="col-4">
@@ -78,14 +83,8 @@ type Props = {
type ProcessingObjectType = {
field?: {
name: string | {
name: string;
};
objectType: string;
type: string;
code: string;
label: string;
noLabel: boolean;
sharedText: string;
objectType: 'field';
};
logicalOperator?: string;
vName?: string;
@@ -126,9 +125,19 @@ export default defineComponent({
type: String,
default: '',
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
}
},
setup(props, { emit }) {
const fieldRef=ref();
const source = props.context.find(element => element?.props?.name === 'sources')
if (source) {
@@ -145,34 +154,55 @@ export default defineComponent({
const actionName = props.context.find(element => element?.props?.name === 'displayName')
const processingProps: ValueType = props.modelValue && props.modelValue.vars
? props.modelValue
? reactive(props.modelValue)
: reactive({
name: '',
actionName: actionName?.props?.modelValue as string,
displayName: '結果(戻り値)',
vars: [{ id: uuidv4() }]
vars: [
{
id: uuidv4(),
field:{
objectType:'field',
sharedText:''
}
}]
});
const closeDg = () => {
emit('update:modelValue', processingProps
);
fieldRef.value.validate();
emit('update:modelValue', processingProps);
}
const processingObjects = processingProps.vars;
const deleteProcessingObject = (index: number) => processingObjects.length === 1
? processingObjects.splice(0, processingObjects.length, { id: uuidv4() })
: processingObjects.splice(index, 1);
const deleteProcessingObject = (index: number) => {
if(processingObjects.length >0){
processingObjects.splice(index, 1);
}
if(processingObjects.length===0){
addProcessingObject();
}
}
const processingObjectsInputDisplay = computed(() =>
processingObjects ?
processingObjects
.filter(item => item.field && item.logicalOperator && item.vName)
.map(item => {
return`var(${processingProps.name}.${item.vName}) = ${item.field.sharedText}`
return`var(${processingProps.name}.${item.vName}) = ${item.field?.sharedText}`
})
: []
);
const addProcessingObject=()=>{
processingObjects.push({
id: uuidv4(),
field:{
objectType:'field',
sharedText:''
}
});
}
//集計処理方法
const logicalOperators = ref([
{
@@ -204,6 +234,24 @@ export default defineComponent({
"label": "最初の値"
}
]);
const checkInput=(val:ValueType)=>{
if(!val){
return false;
}
if(!val.name){
return "集計結果の変数名を入力してください";
}
if(!val.vars || val.vars.length==0){
return "集計処理を設定してください";
}
if(val.vars.some((x)=>!x.vName)){
return "集計結果変数名を入力してください";
}
return true;
}
const customExp = props.rules === undefined ? [] : eval(props.rules);
const requiredExp = props.required ? [(val: any) => checkInput(val)] : [];
const rulesExp = [...requiredExp, ...customExp];
watchEffect(() => {
emit('update:modelValue', processingProps);
@@ -214,10 +262,12 @@ export default defineComponent({
closeDg,
processingObjects,
processingProps,
addProcessingObject: () => processingObjects.push({ id: uuidv4() }),
addProcessingObject,
deleteProcessingObject,
logicalOperators,
processingObjectsInputDisplay,
rulesExp,
fieldRef
};
},
});

View File

@@ -1,6 +1,6 @@
<template>
<div v-bind="$attrs">
<q-input v-model="selectedDate" :label="displayName" :placeholder="placeholder" label-color="primary" mask="date" :rules="['date']" stack-label>
<q-input v-model="selectedDate" :label="displayName" :placeholder="placeholder" label-color="primary" mask="date" :rules="rulesExp" stack-label>
<template v-slot:append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
@@ -43,16 +43,32 @@ export default defineComponent({
type: String,
default: '',
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const selectedDate = ref(props.modelValue);
const customExp = props.rules === undefined ? [] : eval(props.rules);
const requiredExp = props.required?[((val:any)=>!!val||`${props.displayName}が必須です。`),'date']:['date'];
const rulesExp=[...requiredExp,...customExp];
watchEffect(() => {
emit('update:modelValue', selectedDate.value);
});
return {
selectedDate
selectedDate,
rulesExp
};
}
});

View File

@@ -1,6 +1,9 @@
<template>
<div v-bind="$attrs">
<q-input :label="displayName" v-model="inputValue" label-color="primary" :placeholder="placeholder" stack-label>
<q-input :label="displayName" v-model="inputValue" label-color="primary"
:placeholder="placeholder"
:rules="rulesExp"
stack-label>
<template v-slot:append>
<q-btn round dense flat icon="add" @click="addButtonEvent()" />
</template>
@@ -40,12 +43,29 @@ export default defineComponent({
connectProps:{
type:Object,
default:undefined
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props , { emit }) {
const inputValue = ref(props.modelValue);
const store = useFlowEditorStore();
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}を入力してください。`;
const requiredExp = props.required ? [((val: any) => !!val || errmsg)] : [];
const rulesExp = [...requiredExp, ...customExp];
const addButtonEvent=()=>{
const eventId =store.currentFlow?.getRoot()?.name;
if(eventId===undefined){return;}
@@ -61,22 +81,22 @@ export default defineComponent({
if(store.eventTree.findEventById(addEventId)){
return;
}
customEvents.events.push({
eventId: addEventId,
label: displayName,
parentId: customButtonId,
header: 'DELETABLE'
});
customEvents.events.push(new kintoneEvent(
displayName,
addEventId,
customButtonId,
'DELETABLE'
));
}
}
watchEffect(() => {
emit('update:modelValue', inputValue.value);
});
return {
inputValue,
addButtonEvent
addButtonEvent,
rulesExp
};
},
});

View File

@@ -1,7 +1,9 @@
<template>
<div v-bind="$attrs">
<q-field v-model="selectedField" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label
:bottom-slots="!isSelected">
:bottom-slots="!isSelected"
:rules="rulesExp"
>
<template v-slot:control>
<q-chip color="primary" text-color="white" v-if="isSelected">
{{ selectedField.name }}
@@ -15,8 +17,15 @@
<q-icon name="search" class="cursor-pointer" color="primary" @click="showDg" />
</template>
</q-field>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg" widht="400px">
<field-select ref="appDg" name="フィールド" :type="selectType" :appId="store.appInfo?.appId" :fieldTypes="fieldTypes"></field-select>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg" min-width="400px">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<field-select ref="appDg" name="フィールド" :type="selectType" :appId="store.appInfo?.appId" :selectedFields="selectedFields" :fieldTypes="fieldTypes" :filter="filter"></field-select>
</show-dialog>
</div>
</template>
@@ -67,16 +76,35 @@ export default defineComponent({
type: Object,
default: null
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const appDg = ref();
const show = ref(false);
const selectedField = ref(props.modelValue);
const selectedFields =computed(()=>!selectedField.value?[]: [selectedField.value]);
const store = useFlowEditorStore();
const isSelected = computed(() => {
return selectedField.value !== null && typeof selectedField.value === 'object' && ('name' in selectedField.value)
});
//ルール設定
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required ? [((val: any) => (!!val && typeof val==='object' && !!val.name) || errmsg)] : [];
const rulesExp = [...requiredExp, ...customExp];
const showDg = () => {
show.value = true;
@@ -99,7 +127,10 @@ export default defineComponent({
showDg,
closeDg,
selectedField,
isSelected
isSelected,
filter:ref(''),
selectedFields,
rulesExp
};
}
});

View File

@@ -14,7 +14,6 @@
</template>
<script lang="ts">
import { kMaxLength } from 'buffer';
import { defineComponent, ref, watchEffect, computed } from 'vue';
export default defineComponent({
@@ -50,8 +49,16 @@ export default defineComponent({
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
modelValue: {
// type: Any,
type: null as any,
default: '',
},
},
@@ -75,8 +82,11 @@ export default defineComponent({
},
});
// const inputValue = ref(props.modelValue);
const rulesExp = props.rules === undefined ? null : eval(props.rules);
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[];
const rulesExp=[...requiredExp,...customExp];
// const finalValue = computed(() => {
// return props.name !== 'verName' ? inputValue.value : {
// name: inputValue.value,

View File

@@ -1,7 +1,10 @@
<template>
<div v-bind="$attrs">
<q-input :label="displayName" label-color="primary" v-model="inputValue" :placeholder="placeholder" autogrow
stack-label />
<q-input :label="displayName" label-color="primary" v-model="inputValue"
:placeholder="placeholder"
:rules="rulesExp"
autogrow
stack-label />
</div>
</template>
@@ -32,17 +35,34 @@ export default defineComponent({
type: String,
default: '',
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
},
setup(props, { emit }) {
const inputValue = ref(props.modelValue);
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[];
const rulesExp=[...requiredExp,...customExp];
watchEffect(() => {
emit('update:modelValue', inputValue.value);
});
return {
inputValue,
rulesExp
};
},
});

View File

@@ -49,6 +49,14 @@ export default defineComponent({
type:String,
default:undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
modelValue: {
type: [Number , String],
default: undefined
@@ -57,23 +65,10 @@ export default defineComponent({
setup(props, { emit }) {
const numValue = ref(props.modelValue);
const rulesExp = props.rules===undefined?null : eval(props.rules);
const isError = computed(()=>{
const val = numValue.value;
if (val === undefined) {
return false;
}
const numVal = typeof val === "string" ? parseInt(val) : val;
// Ensure parsed value is a valid number
if (isNaN(numVal)) {
return true;
}
// Check against min and max boundaries, if defined
if ((props.min !== undefined && numVal < props.min) || (props.max !== undefined && numVal > props.max)) {
return true;
}
return false;
});
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[];
const rulesExp=[...requiredExp,...customExp];
watchEffect(()=>{
emit("update:modelValue",numValue.value);

View File

@@ -24,6 +24,7 @@ import NumInput from './NumInput.vue';
import DataProcessing from './DataProcessing.vue';
import DataMapping from './DataMapping.vue';
import AppSelect from './AppSelect.vue';
import CascadingDropDown from './CascadingDropDown.vue';
import { IActionNode,IActionProperty,IProp } from 'src/types/ActionTypes';
export default defineComponent({
@@ -41,7 +42,8 @@ export default defineComponent({
NumInput,
DataProcessing,
DataMapping,
AppSelect
AppSelect,
CascadingDropDown
},
props: {
nodeProps: {

View File

@@ -11,6 +11,7 @@
elevated
overlay
>
<q-form @submit="save" autocomplete="off" class="full-height">
<q-card class="column" style="max-width: 300px;min-height: 100%">
<q-card-section>
<div class="text-h6">{{ actionNode?.subTitle }}設定</div>
@@ -21,16 +22,17 @@
<q-card-actions align="right" class="bg-white text-teal">
<q-btn flat label="キャンセル" @click="cancel" outline dense padding="none sm" color="primary"/>
<q-btn flat label="更新" @click="save" outline dense padding="none sm" color="primary" />
<q-btn flat label="更新" type="submit" outline dense padding="none sm" color="primary" />
</q-card-actions>
</q-card>
</q-form>
</q-drawer>
</div>
</template>
<script lang="ts">
import { ref,defineComponent, PropType ,watchEffect} from 'vue'
import PropertyList from 'components/right/PropertyList.vue';
import { IActionNode } from 'src/types/ActionTypes';
import { ref,defineComponent, PropType ,watchEffect} from 'vue'
import PropertyList from 'components/right/PropertyList.vue';
import { IActionNode, IActionProperty } from 'src/types/ActionTypes';
export default defineComponent({
name: 'PropertyPanel',
components: {
@@ -47,14 +49,28 @@ import { IActionNode } from 'src/types/ActionTypes';
}
},
emits: [
'update:drawerRight'
'update:drawerRight',
'saveActionProps'
],
setup(props,{emit}) {
const showPanel =ref(props.drawerRight);
const actionProps =ref(props.actionNode?.actionProps);
const showPanel =ref(props.drawerRight);
const cloneProps = (actionProps:IActionProperty[]):IActionProperty[]|null=>{
if(!actionProps){
return null;
}
const json=JSON.stringify(actionProps);
return JSON.parse(json);
}
const actionProps =ref(cloneProps(props.actionNode?.actionProps));
watchEffect(() => {
if(showPanel.value!==undefined){
showPanel.value = props.drawerRight;
}
showPanel.value = props.drawerRight;
actionProps.value= props.actionNode?.actionProps;
actionProps.value= cloneProps(props.actionNode?.actionProps);
});
const cancel = async() =>{
@@ -64,7 +80,8 @@ import { IActionNode } from 'src/types/ActionTypes';
const save = async () =>{
showPanel.value=false;
emit('update:drawerRight',false )
emit('saveActionProps', actionProps.value);
emit('update:drawerRight',false );
}
return {

View File

@@ -1,6 +1,9 @@
<template>
<div v-bind="$attrs">
<q-select v-model="selectedValue" :use-chips="multiple" :label="displayName" label-color="primary" :options="options" stack-label
<q-select v-model="selectedValue" :use-chips="multiple" :label="displayName" label-color="primary"
:options="options"
stack-label
:rules="rulesExp"
:multiple="multiple"/>
</div>
</template>
@@ -32,6 +35,19 @@ export default defineComponent({
type: [Array,String],
default: null,
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
},
setup(props, { emit }) {
const selectedValue = ref(props.modelValue);
@@ -41,10 +57,14 @@ export default defineComponent({
watchEffect(() => {
emit('update:modelValue', selectedValue.value);
});
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}を選択してください。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[];
const rulesExp=[...requiredExp,...customExp];
return {
selectedValue,
multiple
multiple,
rulesExp
};
},
});

View File

@@ -2,40 +2,28 @@
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-btn flat dense round icon="menu" aria-label="Menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
{{ productName }}
<q-badge align="top" outline>V{{ version }}</q-badge>
</q-toolbar-title>
<domain-selector></domain-selector>
<q-btn flat round dense icon="logout" @click="authStore.logout()"/>
<q-btn flat round dense icon="logout" @click="authStore.logout()" />
</q-toolbar>
</q-header>
<q-drawer
:model-value="authStore.toggleLeftDrawer"
:show-if-above="false"
bordered
>
<q-drawer :model-value="authStore.LeftDrawer" :show-if-above="false" bordered>
<q-list>
<q-item-label
header
>
関連リンク
<q-item-label header>
メニュー
</q-item-label>
<EssentialLink
v-for="link in essentialLinks"
:key="link.title"
v-bind="link"
/>
<EssentialLink v-for="link in essentialLinks" :key="link.title" v-bind="link" />
<div v-if="isAdmin()">
<EssentialLink v-for="link in adminLinks" :key="link.title" v-bind="link" />
</div>
<EssentialLink v-for="link in domainLinks" :key="link.title" v-bind="link" />
</q-list>
</q-drawer>
@@ -46,7 +34,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onMounted } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import DomainSelector from 'components/DomainSelector.vue';
import { useAuthStore } from 'stores/useAuthStore';
@@ -54,107 +42,90 @@ import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore();
const essentialLinks: EssentialLinkProps[] = [
{
{
title: 'ホーム',
caption: 'home',
caption: '設計書から導入する',
icon: 'home',
link: '/',
target:'_self'
target: '_self'
},
{
title: 'フローエディター',
caption: 'flowChart',
caption: 'イベントを設定する',
icon: 'account_tree',
link: '/#/FlowChart',
target:'_self'
target: '_self'
},
// {
// title: '条件エディター',
// caption: 'condition',
// icon: 'tune',
// link: '/#/condition',
// target:'_self'
// },
{
title: '条件エディター',
caption: 'condition',
icon: 'tune',
link: '/#/condition',
target:'_self'
title: '',
isSeparator: true
},
{
title:'',
isSeparator:true
},
{
title:'Kintone ポータル',
caption:'Kintone',
icon:'cloud_queue',
link:'https://mfu07rkgnb7c.cybozu.com/k/#/portal'
},
{
title:'CUSTOMINE',
caption:'gusuku',
link:'https://app-customine.gusuku.io/drive.html',
icon:'settings_suggest'
},
{
title:'Kintone API ドキュメント',
caption:'Kintone API',
link:'https://cybozu.dev/ja/kintone/docs/',
icon:'help_outline'
},
{
title:'',
isSeparator:true
},
{
title: 'Docs',
caption: 'quasar.dev',
icon: 'school',
link: 'https://quasar.dev'
},
{
title: 'Icons',
caption: 'Material Icons',
icon: 'insert_emoticon',
link: 'https://fonts.google.com/icons?selected=Material+Icons:insert_emoticon:'
},
{
title: 'Github',
caption: 'github.com/quasarframework',
icon: 'code',
link: 'https://github.com/quasarframework'
},
{
title: 'Discord Chat Channel',
caption: 'chat.quasar.dev',
icon: 'chat',
link: 'https://chat.quasar.dev'
},
{
title: 'Forum',
caption: 'forum.quasar.dev',
icon: 'record_voice_over',
link: 'https://forum.quasar.dev'
},
{
title: 'Twitter',
caption: '@quasarframework',
icon: 'rss_feed',
link: 'https://twitter.quasar.dev'
},
{
title: 'Facebook',
caption: '@QuasarFramework',
icon: 'public',
link: 'https://facebook.quasar.dev'
},
{
title: 'Quasar Awesome',
caption: 'Community Quasar projects',
icon: 'favorite',
link: 'https://awesome.quasar.dev'
}
// {
// title:'Kintone ポータル',
// caption:'Kintone',
// icon:'cloud_queue',
// link:'https://mfu07rkgnb7c.cybozu.com/k/#/portal'
// },
// {
// title:'CUSTOMINE',
// caption:'gusuku',
// link:'https://app-customine.gusuku.io/drive.html',
// icon:'settings_suggest'
// },
// {
// title:'Kintone API ドキュメント',
// caption:'Kintone API',
// link:'https://cybozu.dev/ja/kintone/docs/',
// icon:'help_outline'
// },
];
const domainLinks: EssentialLinkProps[] = [
{
title: 'ドメイン管理',
caption: 'kintoneのドメイン設定',
icon: 'domain',
link: '/#/domain',
target: '_self'
},
// {
// title: 'ドメイン適用',
// caption: 'ユーザー使用可能なドメインの設定',
// icon: 'assignment_ind',
// link: '/#/userDomain',
// target: '_self'
// },
];
const adminLinks: EssentialLinkProps[] = [
{
title: 'ユーザー管理',
caption: 'ユーザーを管理する',
icon: 'manage_accounts',
link: '/#/user',
target: '_self'
},
]
const version = process.env.version;
const productName = process.env.productName;
onMounted(() => {
authStore.toggleLeftMenu();
});
function toggleLeftDrawer() {
authStore.toggleLeftMenu();
}
function isAdmin(){
const permission = authStore.permissions;
return permission === 'admin'
}
</script>

View File

@@ -16,7 +16,27 @@
<div class="flex-center fixed-bottom bg-grey-3 q-pa-md row ">
<q-btn color="secondary" glossy label="デプロイ" @click="onDeploy" icon="sync" :loading="deployLoading" />
<q-space></q-space>
<q-btn color="primary" label="保存" @click="onSaveFlow" icon="save" :loading="saveLoading" />
<q-btn-dropdown color="primary" label="保存" icon="save" :loading="saveLoading" >
<q-list>
<q-item clickable v-close-popup @click="onSaveFlow">
<q-item-section avatar >
<q-icon name="save" color="primary"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>選択中フローの保存</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="onSaveAllFlow">
<q-item-section avatar>
<q-icon name="collections_bookmark" color="accent"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>一括保存</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</q-drawer>
</div>
@@ -31,7 +51,7 @@
@deleteNode="onDeleteNode" @deleteAllNextNodes="onDeleteAllNextNodes" @copyFlow="onCopyFlow"></node-item>
</div>
</div>
<PropertyPanel :actionNode="store.activeNode" v-model:drawerRight="drawerRight"></PropertyPanel>
<PropertyPanel :actionNode="store.activeNode" v-model:drawerRight="drawerRight" @save-action-props="onSaveActionProps"></PropertyPanel>
</q-layout>
<ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg" min-width="500px" min-height="500px">
<template v-slot:toolbar>
@@ -41,7 +61,7 @@
</template>
</q-input>
</template>
<action-select ref="appDg" name="model" :filter="filter" type="single"></action-select>
<action-select ref="appDg" name="model" :filter="filter" type="single" @clearFilter="onClearFilter" ></action-select>
</ShowDialog>
</q-page>
@@ -79,10 +99,7 @@ const showAddAction = ref(false);
const drawerRight = ref(false);
const filter=ref("");
const model = ref("");
const addActionNode = (action: IActionNode) => {
// refFlow.value?.actionNodes.push(action);
store.currentFlow?.actionNodes.push(action);
}
const rootNode = computed(()=>{
return store.currentFlow?.getRoot();
});
@@ -193,13 +210,24 @@ const onDeploy = async () => {
return;
}
const onSaveActionProps=(props:IActionProperty[])=>{
if(store.activeNode){
store.activeNode.actionProps=props;
$q.notify({
type: 'positive',
caption: "通知",
message: `${store.activeNode?.subTitle}の属性を設定しました。(保存はされていません)`
});
}
};
const onSaveFlow = async () => {
const targetFlow = store.selectedFlow;
if (targetFlow === undefined) {
$q.notify({
type: 'negative',
caption: "エラー",
message: `編集中のフローがありません。`
caption: 'エラー',
message: `選択中のフローがありません。`
});
return;
}
@@ -221,7 +249,44 @@ const onSaveFlow = async () => {
message: `${targetFlow.getRoot()?.subTitle}のフローの設定の保存が失敗しました。`
})
}
}
/**
* すべてフローの設定を保存する
*/
const onSaveAllFlow= async ()=>{
try{
const targetFlows = store.eventTree.findAllFlows();
if (!targetFlows || targetFlows.length === 0 ) {
$q.notify({
type: 'negative',
caption: 'エラー',
message: `設定されたフローがありません。`
});
return;
}
saveLoading.value = true;
for(const flow of targetFlows ){
const isNew = flow.id === '';
if(isNew && flow.actionNodes.length===1){
continue;
}
await store.saveFlow(flow);
}
$q.notify({
type: 'positive',
caption: "通知",
message: `すべてのフロー設定を保存しました。`
});
saveLoading.value = false;
}catch (error) {
console.error(error);
saveLoading.value = false;
$q.notify({
type: 'negative',
caption: "エラー",
message: `フローの設定の保存が失敗しました。`
});
}
}
const fetchData = async () => {
@@ -241,6 +306,10 @@ const fetchData = async () => {
}
}
const onClearFilter=()=>{
filter.value='';
}
onMounted(() => {
authStore.toggleLeftMenu();
fetchData();

View File

@@ -1,54 +1,95 @@
<template>
<div class="q-pa-md">
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" selection="single" :filter="filter"
:loading="loading" v-model:selected="selected">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="domain" label="ドメイン管理" />
</q-breadcrumbs>
</div>
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" />
<q-btn class="q-ml-sm" color="primary" :disable="loading" label="編集" @click="editRow" />
<q-btn class="q-ml-sm" color="primary" :disable="loading" label="削除" @click="removeRow" />
<q-space />
<q-input borderless dense debounce="300" color="primary" v-model="filter">
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editRow(p.row)" />
<q-btn flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
</q-td>
</template>
</q-table>
<q-dialog :model-value="show" persistent>
<q-card style="min-width: 400px">
<q-card-section>
<div class="text-h6">Kintone Account</div>
</q-card-section>
<q-card style="min-width: 36em">
<q-form class="q-gutter-md" @submit="onSubmit" autocomplete="off">
<q-card-section>
<div class="text-h6 q-ma-sm">Kintone Account</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-form class="q-gutter-md">
<q-input filled v-model="tenantid" label="Tenant" hint="Tenant ID" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" />
<q-card-section class="q-pt-none q-mt-none">
<div class="q-gutter-lg">
<q-input filled v-model="name" label="Your name *" hint="Kintone envirment name" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" />
<q-input filled type="url" v-model="url" label="Kintone url" hint="Kintone domain address" lazy-rules
:rules="[val => val && val.length > 0, isDomain || 'Please type something']" />
<q-input filled v-model="tenantid" label="テナントID" hint="テナントIDを入力してください。" lazy-rules
:rules="[val => val && val.length > 0 || 'テナントIDを入力してください。']" />
<q-input filled v-model="kintoneuser" label="Login user " hint="Kintone user name" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" />
<q-input filled v-model="name" label="環境名 *" hint="kintoneの環境名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'kintoneの環境名を入力してください。']" />
<q-input v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'" hint="Password with toggle"
label="User password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer" @click="isPwd = !isPwd" />
</template>
</q-input>
</q-form>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn label="Save" type="submit" color="primary" @click="onSubmit" />
<q-btn label="Cancel" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
<q-input filled type="url" v-model="url" label="Kintone url" hint="KintoneのURLを入力してください" lazy-rules
:rules="[val => val && val.length > 0, isDomain || 'KintoneのURLを入力してください']" />
<q-input filled v-model="kintoneuser" label="ログイン名 *" hint="Kintoneのログイン名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'Kintoneのログイン名を入力してください']" autocomplete="new-password" />
<q-input v-if="isCreate" v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'"
hint="パスワード" label="パスワード" :disable="!isCreate" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<div class="q-gutter-y-md" v-if="!isCreate">
<q-separator />
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>パスワードリセット</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="resetPsw" @update:model-value="updateResetPsw" />
</q-item-section>
</q-item>
<q-input v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'" hint="パスワードを入力してください"
label="パスワード" :disable="!resetPsw" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<!-- <q-btn label="asdf"/> -->
</div>
</div>
</q-card-section>
<q-card-actions align="right" class="text-primary q-mb-md q-mx-sm">
<q-btn label="保存" type="submit" color="primary" />
<q-btn label="キャンセル" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
@@ -56,7 +97,7 @@
<q-dialog v-model="confirm" persistent>
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="confirm" color="primary" text-color="white" />
<q-icon name="warning" color="warning" size="2em" />
<span class="q-ml-sm">削除してもよろしいですか</span>
</q-card-section>
@@ -73,66 +114,68 @@
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue';
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore();
const columns = [
{ name: 'id' },
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{
name: 'tenantid',
required: true,
label: 'Tenant',
align: 'left',
label: 'テナントID',
field: row => row.tenantid,
format: val => `${val}`,
align: 'left',
sortable: true
},
{ name: 'name', align: 'center', label: 'Name', field: 'name', sortable: true },
{ name: 'url', align: 'left', label: 'URL', field: 'url', sortable: true },
{ name: 'user', label: 'Account', field: 'user' },
{ name: 'password', label: 'Password', field: 'password' }
{ name: 'name', label: '環境名', field: 'name', align: 'left', sortable: true },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true },
{ name: 'user', label: 'ログイン名', field: 'user', align: 'left', },
{ name: 'actions', label: '操作', field: 'actions' }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false);
const filter = ref('');
const rows = ref([]);
const show = ref(false);
const confirm = ref(false);
const selected = ref([]);
const tenantid = ref('');
const resetPsw = ref(false);
const tenantid = ref(authStore.currentDomain.id);
const name = ref('');
const url = ref('');
const isPwd = ref(true);
const kintoneuser = ref('');
const kintonepwd = ref('');
const kintonepwdBK = ref('');
const isCreate = ref(true);
let editId = ref(0);
const getDomain = async () => {
loading.value = true;
const result= await api.get(`api/domains/1`);
rows.value= result.data.map((item)=>{
const userId = authStore.userId;
const result = await api.get(`api/domain?userId=${userId}`);
rows.value = result.data.map((item) => {
return { id: item.id, tenantid: item.tenantid, name: item.name, url: item.url, user: item.kintoneuser, password: item.kintonepwd }
});
loading.value = false;
}
onMounted(async () => {
await getDomain();
await getDomain();
})
// emulate fetching data from server
const addRow = () => {
editId.value
// editId.value
onReset();
show.value = true;
}
const removeRow = () => {
//loading.value = true
const removeRow = (row) => {
confirm.value = true;
let row = JSON.parse(JSON.stringify(selected.value[0]));
if (selected.value.length === 0) {
return;
}
editId.value = row.id;
}
@@ -141,14 +184,11 @@ const deleteDomain = () => {
getDomain();
})
editId.value = 0;
selected.value = [];
};
const editRow = () => {
if (selected.value.length === 0) {
return;
}
let row = JSON.parse(JSON.stringify(selected.value[0]));
const editRow = (row) => {
isCreate.value = false
editId.value = row.id;
tenantid.value = row.tenantid;
name.value = row.name;
@@ -158,6 +198,16 @@ const editRow = () => {
isPwd.value = true;
show.value = true;
};
const updateResetPsw = (value: boolean) => {
if (value === true) {
kintonepwd.value = ''
isPwd.value = true
} else {
kintonepwd.value = kintonepwdBK.value
}
}
const closeDg = () => {
show.value = false;
onReset();
@@ -171,7 +221,7 @@ const onSubmit = () => {
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value
'kintonepwd': isCreate.value || resetPsw.value ? kintonepwd.value : ''
}).then(() => {
getDomain();
closeDg();
@@ -192,7 +242,7 @@ const onSubmit = () => {
onReset();
})
}
selected.value = [];
}
const onReset = () => {
@@ -202,5 +252,7 @@ const onReset = () => {
kintonepwd.value = '';
isPwd.value = true;
editId.value = 0;
isCreate.value = true;
resetPsw.value = false
}
</script>

View File

@@ -1,127 +1,43 @@
<!-- <template>
<div class="q-pa-md" style="max-width: 400px">
<q-form
@submit="onSubmit"
@reset="onReset"
class="q-gutter-md"
>
<q-input
filled
v-model="name"
label="Your name *"
hint="Kintone envirment name"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<q-input
filled type="url"
v-model="url"
label="Kintone url"
hint="Kintone domain address"
lazy-rules
:rules="[ val => val && val.length > 0,isDomain || 'Please type something']"
/>
<q-input
filled
v-model="username"
label="Login user "
hint="Kintone user name"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<q-input v-model="password" filled :type="isPwd ? 'password' : 'text'" hint="Password with toggle" label="User password">
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
<q-toggle v-model="accept" label="Active Domain" />
<div>
<q-btn label="Submit" type="submit" color="primary"/>
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</div>
</template>
<script>
import { useQuasar } from 'quasar'
import { ref } from 'vue'
export default {
setup () {
const $q = useQuasar()
const name = ref(null)
const age = ref(null)
const accept = ref(false)
const isPwd =ref(true)
return {
name,
age,
accept,
isPwd,
isDomain(val) {
const domainPattern = /^https?\/\/:([a-zA-Z] +\.){1}([a-zA-Z]+)\.([a-zA-Z]+)$/;
return (domainPattern.test(val) || '無効なURL')
},
onSubmit () {
if (accept.value !== true) {
$q.notify({
color: 'red-5',
textColor: 'white',
icon: 'warning',
message: 'You need to accept the license and terms first'
})
}
else {
$q.notify({
color: 'green-4',
textColor: 'white',
icon: 'cloud_done',
message: 'Submitted'
})
}
},
onReset () {
name.value = null
age.value = null
accept.value = false
}
}
}
}
</script> -->
<template>
<div class="q-pa-md">
<q-table grid grid-header title="Domain" selection="single" :rows="rows" :columns="columns" v-model:selected="selected" row-key="name" :filter="filter" hide-header>
<div class="q-pa-lg">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="assignment_ind" label="ドメイン適用" />
</q-breadcrumbs>
</div>
<q-table grid grid-header title="Domain" selection="single" :rows="rows" :columns="columns" row-key="name"
:filter="userDomainTableFilter" virtual-scroll v-model:pagination="pagination">
<template v-slot:top>
<div class="q-pa-md q-gutter-sm">
<q-btn color="primary" label="追加" @click="newDomain()" dense />
</div>
<q-btn class="q-mx-none" color="primary" label="追加" @click="clickAddDomain()" />
<q-space />
<q-input borderless dense debounce="300" v-model="filter" placeholder="Search">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
<div class="row q-gutter-md">
<q-item v-if="authStore.permissions === 'admin'" tag="label" dense @click="clickSwitchUser()">
<q-item-section>
<q-item-label>適用するユーザ : </q-item-label>
</q-item-section>
<q-item-section avatar>
{{ currentUserName }}
</q-item-section>
</q-item>
<q-input borderless dense filled debounce="300" v-model="userDomainTableFilter" placeholder="Search">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</div>
</template>
<template v-slot:header>
<div style="height: 1dvh">
</div>
</template>
<template v-slot:item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
<div class="q-pa-sm">
<q-card>
<q-card-section>
<div class="q-table__grid-item-row">
@@ -130,40 +46,73 @@ export default {
</div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">URL</div>
<div class="q-table__grid-item-value">{{ props.row.url }}</div>
<div class="q-table__grid-item-value" style="width: 22rem;">{{ props.row.url }}</div>
</div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Account</div>
<div class="q-table__grid-item-value">{{ props.row.kintoneuser }}</div>
</div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-value">{{isActive(props.row.id) }}</div>
</div>
</q-card-section>
<q-separator />
<q-card-actions align="right">
<q-btn flat @click = "activeDomain(props.row.id)">有効</q-btn>
<q-btn flat @click = "deleteConfirm(props.row)">削除</q-btn>
<div style="width: 98%;">
<div class="row items-center justify-between">
<div class="q-table__grid-item-value"
:class="isActive(props.row.id) ? 'text-positive' : 'text-negative'">{{
isActive(props.row.id)?'既定':'' }}</div>
<div class="col-auto">
<q-btn v-if="!isActive(props.row.id)" flat
@click="activeDomain(props.row.id)">既定にする</q-btn>
<q-btn flat @click="clickDeleteConfirm(props.row)">削除</q-btn>
</div>
</div>
</div>
</q-card-actions>
</q-card>
</div>
</template>
</q-table>
<show-dialog v-model:visible="show" name="ドメイン" @close="closeDg" width="350px">
<domain-select ref="domainDg" name="ドメイン" type="multiple"></domain-select>
<show-dialog v-model:visible="showAddDomainDg" name="ドメイン" @close="addUserDomainFinished">
<domain-select ref="addDomainRef" name="ドメイン" type="multiple"></domain-select>
</show-dialog>
<q-dialog v-model="confirm" persistent>
<show-dialog v-model:visible="showSwitchUserDd" name="ドメイン" minWidth="35vw" @close="switchUserFinished">
<template v-slot:toolbar>
<q-input dense placeholder="検索" v-model="switchUserFilter">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<div class="q-gutter-md">
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>他のユーザーを選択する</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="useOtherUser" />
</q-item-section>
</q-item>
<div v-if="useOtherUser">
<user-list ref="switchUserRef" name="ドメイン" :filter="switchUserFilter" />
</div>
</div>
</show-dialog>
<q-dialog v-model="showDeleteConfirm" persistent>
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="confirm" color="primary" text-color="white" />
<span class="q-ml-sm">削除してもよろしいですか</span>
<div class="q-ma-sm q-mt-md">
<q-icon name="warning" color="warning" size="2em" />
<span class="q-ml-sm">削除してもよろしいですか</span>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" v-close-popup @click = "deleteDomain()"/>
<q-btn flat label="OK" color="primary" v-close-popup @click="deleteDomainFinished()" />
</q-card-actions>
</q-card>
</q-dialog>
@@ -171,100 +120,117 @@ export default {
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar'
import { ref, onMounted, reactive } from 'vue'
import ShowDialog from 'components/ShowDialog.vue';
import DomainSelect from 'components/DomainSelect.vue';
import { ref, onMounted, computed } from 'vue'
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import ShowDialog from 'components/ShowDialog.vue';
import DomainSelect from 'components/DomainSelect.vue';
import UserList from 'components/UserList.vue';
const authStore = useAuthStore();
import { api } from 'boot/axios';
import { domain } from 'process';
const $q = useQuasar()
const domainDg = ref();
const selected = ref([])
const show = ref(false);
const confirm = ref(false)
let editId = ref(0);
let activedomainid = ref(0);
const pagination = ref({ sortBy: 'id', rowsPerPage: 0 });
const rows = ref([] as any[]);
const columns = [
{ name: 'id'},
{name: 'name',required: true,label: 'Name',align: 'left',field: 'name',sortable: true},
{ name: 'id' },
{ name: 'name', required: true, label: 'Name', align: 'left', field: 'name', sortable: true },
{ name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true },
{ name: 'kintonepwd' },
{ name: 'active', field: 'active'}
]
{ name: 'active', field: 'active' }
];
const userDomainTableFilter = ref();
const rows = ref([] as any[]);
const currentUserName = ref('');
const useOtherUser = ref(false);
const otherUserId = ref('');
const isActive = (id:number) =>{
if(id == activedomainid.value)
return "Active";
else
return "Inactive";
}
let editId = ref(0);
const newDomain = () => {
const showAddDomainDg = ref(false);
const addDomainRef = ref();
const clickAddDomain = () => {
editId.value = 0;
show.value = true;
showAddDomainDg.value = true;
};
const activeDomain = (id:number) => {
api.put(`api/activedomain/`+ id).then(() =>{
getDomain();
})
const addUserDomainFinished = (val: string) => {
if (val == 'OK') {
let dodmainids = [];
let domains = JSON.parse(JSON.stringify(addDomainRef.value.selected));
for (var key in domains) {
dodmainids.push(domains[key].id);
}
api.post(`api/domain/${useOtherUser.value ? otherUserId.value : authStore.userId}`, dodmainids)
.then(() => { getDomain(useOtherUser.value ? otherUserId.value : undefined); });
}
};
const deleteConfirm = (row:object) => {
confirm.value = true;
const showDeleteConfirm = ref(false);
const clickDeleteConfirm = (row: any) => {
showDeleteConfirm.value = true;
editId.value = row.id;
};
const deleteDomain = () => {
api.delete(`api/domain/`+ editId.value+'/1').then(() =>{
getDomain();
})
const deleteDomainFinished = () => {
api.delete(`api/domain/${editId.value}/${useOtherUser.value ? otherUserId.value : authStore.userId}`).then(() => {
getDomain(useOtherUser.value ? otherUserId.value : undefined);
})
editId.value = 0;
};
const closeDg = (val:string) => {
if (val == 'OK') {
let dodmainids =[];
let domains = JSON.parse(JSON.stringify(domainDg.value.selected));
for(var key in domains)
{
dodmainids.push(domains[key].id);
}
api.post(`api/domain`, dodmainids).then(() =>{getDomain();});
}
const activeDomain = (id: number) => {
api.put(`api/activedomain/${id}${useOtherUser.value ? `?userId=${otherUserId.value}` : ''}`)
.then(() => { getDomain(useOtherUser.value ? otherUserId.value : undefined); })
};
const getDomain = async () => {
const resp = await api.get(`api/activedomain`);
activedomainid.value = resp.data.id;
const domainResult = await api.get(`api/domain`);
const domains = domainResult.data as any[];
rows.value=domains.map((item)=>{
return { id:item.id,name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd}
});
let activeDomainId = ref(0);
const isActive = computed(() => (id: number) => {
return id == activeDomainId.value;
});
const showSwitchUserDd = ref(false);
const switchUserRef = ref();
const switchUserFilter = ref('')
const clickSwitchUser = () => {
showSwitchUserDd.value = true;
useOtherUser.value = false;
};
const switchUserFinished = async (val: string) => {
if (val == 'OK') {
if (useOtherUser.value) {
const user = switchUserRef.value.selected[0]
currentUserName.value = user.email;
otherUserId.value = user.id
await getDomain(user.id)
} else {
currentUserName.value = authStore.userInfo.email
await getDomain();
}
}
};
const getDomain = async (userId? : string) => {
const resp = await api.get(`api/activedomain${useOtherUser.value ? `?userId=${otherUserId.value}` : ''}`);
activeDomainId.value = resp?.data?.id;
const domainResult = userId ? await api.get(`api/domain?userId=${userId}`) : await api.get(`api/domain`);
const domains = domainResult.data as any[];
rows.value = domains.map((item) => {
return { id: item.id, name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd }
});
}
onMounted(async () => {
currentUserName.value = authStore.userInfo.email
await getDomain();
})
const isDomain = (val) =>{
// const domainPattern = /^https\/\/:([a-zA-Z] +\.){1}([a-zA-Z]+)\.([a-zA-Z]+)$/;
// return (domainPattern.test(val) || '無効なURL')
return true;
};
</script>

View File

@@ -0,0 +1,307 @@
<template>
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="manage_accounts" label="ユーザー管理" />
</q-breadcrumbs>
</div>
<q-table title="ユーザーリスト" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading"
:pagination="pagination" >
<template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell-status="props">
<q-td :props="props">
<div class="row">
<div v-if="props.row.isActive">
<q-chip square color="positive" text-color="white" icon="done" label="使用可能" size="sm" />
</div>
<div v-else>
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
</div>
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
</div>
</q-td>
</template>
<template v-slot:header-cell-status="p">
<q-th :props="p">
<div class="row items-center">
<label class="q-mr-md">{{ p.col.label }}</label>
<q-select v-model="statusFilter" :options="options" @update:model-value="updateStatusFilter" borderless
dense options-dense style="font-size: 12px; padding-top: 1px;" />
</div>
</q-th>
</template>
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editRow(p.row)" />
<q-btn flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
</q-td>
</template>
</q-table>
<q-dialog :model-value="show" persistent>
<q-card style="min-width: 36em">
<q-form class="q-gutter-md" @submit="onSubmit" autocomplete="off">
<q-card-section>
<div class="text-h6 q-ma-sm">K-True Account</div>
</q-card-section>
<q-card-section class="q-pt-none q-mt-none">
<div class="q-gutter-lg">
<q-input filled v-model="firstName" label="氏名 *" hint="ユーザーの氏名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'ユーザーの氏名を入力してください。']" />
<q-input filled v-model="lastName" label="苗字 *" hint="ユーザーの苗字を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'ユーザーの苗字を入力してください']" />
<q-input filled type="email" v-model="email" label="電子メール *" hint="電子メール、ログインとしても使用" lazy-rules
:rules="[val => val && val.length > 0 || '電子メールを入力してください']" autocomplete="new-password" />
<q-input v-if="isCreate" v-model="pwd" filled :type="isPwd ? 'password' : 'text'" hint="パスワード"
label="パスワード" :disable="!isCreate" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>システム管理者</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="isSuperuser" />
</q-item-section>
</q-item>
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>使用可能</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="isActive" />
</q-item-section>
</q-item>
<div class="q-gutter-y-md" v-if="!isCreate">
<q-separator />
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>パスワードリセット</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="resetPsw" />
</q-item-section>
</q-item>
<q-input v-model="pwd" filled :type="isPwd ? 'password' : 'text'" hint="パスワードを入力してください" label="パスワード"
:disable="!resetPsw" lazy-rules :rules="[val => val && val.length > 0 || 'Please type something']"
autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<!-- <q-btn label="asdf"/> -->
</div>
</div>
</q-card-section>
<q-card-actions align="right" class="text-primary q-mb-md q-mx-sm">
<q-btn label="保存" type="submit" color="primary" />
<q-btn label="キャンセル" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="confirm" persistent>
<q-card>
<q-card-section class="row items-center">
<q-icon name="warning" color="warning" size="2em" />
<span class="q-ml-sm">削除してもよろしいですか</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" v-close-popup @click="deleteUser()" />
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { api } from 'boot/axios';
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'firstName', label: '氏名', field: 'firstName', align: 'left', sortable: true },
{ name: 'lastName', label: '苗字', field: 'lastName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' },
{ name: 'actions', label: '操作', field: 'actions' }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false);
const filter = ref('');
const statusFilter = ref('全データ');
const rows = ref([]);
const show = ref(false);
const confirm = ref(false);
const resetPsw = ref(false);
const firstName = ref('');
const lastName = ref('');
const email = ref('');
const isSuperuser = ref(false);
const isActive = ref(true);
const isPwd = ref(true);
const pwd = ref('');
const isCreate = ref(true);
let editId = ref(0);
const getUsers = async (filter = () => true) => {
loading.value = true;
const result = await api.get(`api/v1/users`);
rows.value = result.data.map((item) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active }
}).filter(filter);
loading.value = false;
}
const updateStatusFilter = (status) => {
switch (status) {
case 'システム管理者のみ':
getUsers((row) => row.isSuperuser)
break;
case '使用可能':
getUsers((row) => row.isActive)
break;
case '使用不可':
getUsers((row) => !row.isActive)
break;
default:
getUsers()
break;
}
}
onMounted(async () => {
await getUsers();
})
const options = ['全データ', 'システム管理者のみ', '使用可能', '使用不可']
// emulate fetching data from server
const addRow = () => {
// editId.value
onReset();
show.value = true;
}
const removeRow = (row) => {
confirm.value = true;
editId.value = row.id;
}
const deleteUser = () => {
api.delete(`api/v1/users/${editId.value}`).then(() => {
getUsers();
})
editId.value = 0;
};
const editRow = (row) => {
isCreate.value = false
editId.value = row.id;
firstName.value = row.firstName;
lastName.value = row.lastName;
email.value = row.email;
pwd.value = row.password;
isSuperuser.value = row.isSuperuser;
isActive.value = row.isActive;
isPwd.value = true;
show.value = true;
};
const closeDg = () => {
show.value = false;
onReset();
}
const onSubmit = () => {
if (editId.value !== 0) {
api.put(`api/v1/users/${editId.value}`, {
'first_name': firstName.value,
'last_name': lastName.value,
'is_superuser': isSuperuser.value,
'is_active': isActive.value,
'email': email.value,
...(isCreate.value || resetPsw.value ? { password: pwd.value } : {})
}).then(() => {
getUsers();
closeDg();
onReset();
})
}
else {
api.post(`api/v1/users`, {
'id': 0,
'first_name': firstName.value,
'last_name': lastName.value,
'is_superuser': isSuperuser.value,
'is_active': isActive.value,
'email': email.value,
'password': pwd.value
}).then(() => {
getUsers();
closeDg();
onReset();
})
}
}
const onReset = () => {
firstName.value = '';
lastName.value = '';
email.value = '';
pwd.value = '';
isActive.value = true;
isSuperuser.value = false;
isPwd.value = true;
editId.value = 0;
isCreate.value = true;
resetPsw.value = false
}
</script>

View File

@@ -25,7 +25,8 @@ const routes: RouteRecordRaw[] = [
// { path: 'FlowChart', component: () => import('pages/FlowChart.vue') },
{ path: 'right', component: () => import('pages/testRight.vue') },
{ path: 'domain', component: () => import('pages/TenantDomain.vue') },
{ path: 'userdomain', component: () => import('pages/UserDomain.vue')},
// { path: 'userdomain', component: () => import('pages/UserDomain.vue')},
{ path: 'user', component: () => import('pages/UserManagement.vue')},
{ path: 'condition', component: () => import('pages/conditionPage.vue') }
],
},

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia';
import { AppInfo, IActionFlow, IActionNode } from 'src/types/ActionTypes';
import { IKintoneEvent, KintoneEventManager } from 'src/types/KintoneEvents';
import { IKintoneEvent, KintoneEventManager, kintoneEvent } from 'src/types/KintoneEvents';
import { FlowCtrl } from '../control/flowctrl';
export interface FlowEditorState {
@@ -11,7 +11,7 @@ export interface FlowEditorState {
activeNode: IActionNode | undefined;
eventTree: KintoneEventManager;
selectedEvent: IKintoneEvent | undefined;
expandedScreen: any[];
expandedScreen: string[];
}
const flowCtrl = new FlowCtrl();
const eventTree = new KintoneEventManager();
@@ -62,10 +62,17 @@ export const useFlowEditorStore = defineStore('flowEditor', {
},
selectFlow(flow: IActionFlow | undefined) {
this.selectedFlow = flow;
if(flow!==undefined){
const eventId = flow.getRoot()?.name;
this.selectedEvent=this.eventTree.findEventById(eventId) as IKintoneEvent;
}
},
setActiveNode(node: IActionNode) {
this.activeNode = node;
},
setCurrentEvent(event:IKintoneEvent | undefined){
this.selectedEvent=event;
},
setApp(app: AppInfo) {
this.appInfo = app;
},
@@ -81,20 +88,34 @@ export const useFlowEditorStore = defineStore('flowEditor', {
if (actionFlows === undefined || actionFlows.length === 0) {
this.flows = [];
this.selectedFlow = undefined;
this.expandedScreen =[];
return;
}
this.setFlows(actionFlows);
if (actionFlows && actionFlows.length > 0) {
this.selectFlow(actionFlows[0]);
}
const expandNames = actionFlows.map((flow) => flow.getRoot()?.title);
const expandEventIds = actionFlows.map((flow) => flow.getRoot()?.name);
const expandScreens:string[]=[];
expandEventIds.forEach((eventid)=>{
const eventNode=this.eventTree.findEventById(eventid||'');
if(eventNode){
expandScreens.push(eventNode.parentId);
if(eventNode.header==='DELETABLE'){
const groupEvent = this.eventTree.findEventById(eventNode.parentId);
if(groupEvent){
expandScreens.push(groupEvent.parentId);
}
}
}
});
// const expandName =actionFlows[0].getRoot()?.title;
this.expandedScreen = expandNames;
this.expandedScreen = expandScreens;
},
/**
* フローをDBに保存及び更新する
*/
async saveFlow(flow: IActionFlow) {
async saveFlow(flow: IActionFlow):Promise<boolean> {
const root = flow.getRoot();
const isNew = flow.id === '';
const jsonData = {
@@ -108,7 +129,14 @@ export const useFlowEditorStore = defineStore('flowEditor', {
if (isNew) {
return await flowCtrl.SaveFlow(jsonData);
} else {
return await flowCtrl.UpdateFlow(jsonData);
if(flow.actionNodes.length>1){
return await flowCtrl.UpdateFlow(jsonData);
}else{
const eventId = flow.getRoot()?.name||'';
const eventNode = eventTree.findEventById(eventId) as kintoneEvent;
eventNode.flowData=undefined;
return await flowCtrl.DeleteFlow(flow.id);
}
}
},

View File

@@ -1,6 +1,7 @@
import { store } from 'quasar/wrappers'
import { createPinia } from 'pinia'
import { Router } from 'vue-router';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
/*
* When adding new properties to stores, you should also
@@ -23,10 +24,11 @@ declare module 'pinia' {
*/
export default store((/* { ssrContext } */) => {
const pinia = createPinia()
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
})
return pinia;
});

View File

@@ -1,91 +1,118 @@
import { defineStore } from 'pinia';
import { api } from 'boot/axios';
import {router} from 'src/router';
import {IDomainInfo} from '../types/ActionTypes';
export interface IUserState{
token?:string;
returnUrl:string;
currentDomain:IDomainInfo;
LeftDrawer:boolean;
import { router } from 'src/router';
import { IDomainInfo } from '../types/ActionTypes';
import { jwtDecode } from 'jwt-decode';
interface UserInfo {
firstName: string;
lastName: string;
email: string;
}
export const useAuthStore = defineStore({
id: 'auth',
state: ():IUserState =>{
const token=localStorage.getItem('token')||'';
if(token!==''){
api.defaults.headers["Authorization"]='Bearer ' + token;
export interface IUserState {
token?: string;
returnUrl: string;
currentDomain: IDomainInfo;
LeftDrawer: boolean;
userId?: string;
userInfo: UserInfo;
permissions: 'admin' | 'user';
}
export const useAuthStore = defineStore('auth', {
state: (): IUserState => ({
token: '',
returnUrl: '',
LeftDrawer: false,
currentDomain: {} as IDomainInfo,
userId: '',
userInfo: {} as UserInfo,
permissions: 'user',
}),
getters: {
toggleLeftDrawer(): boolean {
return this.LeftDrawer;
},
},
actions: {
toggleLeftMenu() {
this.LeftDrawer = !this.LeftDrawer;
},
async login(username: string, password: string) {
const params = new URLSearchParams();
params.append('username', username);
params.append('password', password);
try {
const result = await api.post(`api/token`, params);
console.info(result);
this.token = result.data.access_token;
const tokenJson = jwtDecode(result.data.access_token);
this.userId = tokenJson.sub;
this.permissions = (tokenJson as any).permissions ?? 'user';
api.defaults.headers['Authorization'] = 'Bearer ' + this.token;
this.currentDomain = await this.getCurrentDomain();
this.userInfo = await this.getUserInfo();
router.push(this.returnUrl || '/');
return true;
} catch (e) {
console.error(e);
return false;
}
},
async getCurrentDomain(): Promise<IDomainInfo> {
const activedomain = (await api.get(`api/activedomain`))?.data;
return {
token,
returnUrl: '',
LeftDrawer:false,
currentDomain: JSON.parse(localStorage.getItem('currentDomain')||"{}")
}
id: activedomain?.id,
domainName: activedomain?.name,
kintoneUrl: activedomain?.url,
};
},
getters:{
toggleLeftDrawer():boolean{
return this.LeftDrawer;
async getUserDomains(): Promise<IDomainInfo[]> {
const resp = await api.get(`api/domain`);
const domains = resp.data as any[];
return domains.map((data) => ({
id: data.id,
domainName: data.name,
kintoneUrl: data.url,
}));
},
async getUserInfo():Promise<UserInfo>{
const resp = (await api.get(`api/v1/users/me`)).data;
return {
firstName: resp.first_name,
lastName: resp.last_name,
email: resp.email,
}
},
actions: {
toggleLeftMenu(){
this.LeftDrawer=!this.LeftDrawer;
},
async login(username:string, password:string) {
const params = new URLSearchParams();
params.append('username', username);
params.append('password', password);
try{
const result = await api.post(`api/token`,params);
console.info(result);
this.token =result.data.access_token;
localStorage.setItem('token', result.data.access_token);
api.defaults.headers["Authorization"]='Bearer ' + this.token;
this.currentDomain=await this.getCurrentDomain();
localStorage.setItem('currentDomain',JSON.stringify(this.currentDomain));
this.router.push(this.returnUrl || '/');
return true;
}catch(e)
{
console.info(e);
return false;
logout() {
this.token = '';
this.currentDomain = {} as IDomainInfo; // 清空当前域
router.push('/login');
},
async setCurrentDomain(domain: IDomainInfo) {
if (domain.id === this.currentDomain.id) {
return;
}
await api.put(`api/activedomain/${domain.id}`);
this.currentDomain = domain;
},
},
persist: {
afterRestore: (ctx) => {
api.defaults.headers['Authorization'] = 'Bearer ' + ctx.store.token;
//axios例外キャプチャー
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
// 認証エラーの場合再ログインする
console.error('(; ゚Д゚)/認証エラー(401)', error);
ctx.store.logout();
}
},
async getCurrentDomain():Promise<IDomainInfo>{
const activedomain = await api.get(`api/activedomain`);
return {
id:activedomain.data.id,
domainName:activedomain.data.name,
kintoneUrl:activedomain.data.url
}
},
async getUserDomains():Promise<IDomainInfo[]>{
const resp = await api.get(`api/domain`);
const domains =resp.data as any[];
return domains.map(data=>{
return {
id:data.id,
domainName:data.name,
kintoneUrl:data.url
}
});
},
logout() {
this.token = undefined;
localStorage.removeItem('token');
localStorage.removeItem('currentDomain');
router.push('/login');
},
async setCurrentDomain(domain:IDomainInfo){
if(domain.id===this.currentDomain.id){
return;
}
await api.put(`api/activedomain/${domain.id}`);
this.currentDomain=domain;
localStorage.setItem('currentDomain',JSON.stringify(this.currentDomain));
return Promise.reject(error);
}
}
);
},
},
});

View File

@@ -292,6 +292,11 @@ export class ActionFlow implements IActionFlow {
if (!targetNode) {
return false;
}
if(targetNode.isRoot){
this.actionNodes=[targetNode];
targetNode.nextNodeIds.clear();
return;
}
if (targetNode.nextNodeIds.size == 0) {
return false;
}
@@ -312,9 +317,9 @@ export class ActionFlow implements IActionFlow {
if (!targetNode) {
return
}
if (targetNode.nextNodeIds.size == 0) {
return
}
// if (targetNode.nextNodeIds.size == 0) {
// return
// }
for (const [, id] of targetNode.nextNodeIds) {
this.removeAll(id);
}
@@ -460,7 +465,7 @@ export class ActionFlow implements IActionFlow {
if(prevNode.varName.modelValue ==='object'){
console.log(prevNode);
}
varNames.unshift({

View File

@@ -0,0 +1,50 @@
export interface IApp {
id: string,
name: string
}
export interface IField {
label?:string;
code:string;
type?:string;
required?:boolean;
options?:string;
}
/**
* 選択されたフィールド
*/
export interface ISelectedField extends IField{
objectType:'Field'|'RefField';
}
export interface IAppFields {
app?: IApp,
name?:string;
fields: IField[]
}
/**
* 条件式の入力ボタンの属性定義
*/
export interface IButtonConfig{
label: string;
color: string;
type: 'FieldAdd' | 'VariableAdd' | 'FunctionAdd';
};
/**
* 条件入力項目の属性
*/
export interface IDynamicInputConfig{
canInput: boolean;
buttonsConfig: IButtonConfig[];
}
/**
* 条件式入力項目の属性
*/
export interface ICoditionConfig{
left:IDynamicInputConfig,
right:IDynamicInputConfig
}

View File

@@ -200,12 +200,13 @@ export class ConditionTree {
return conditionString;
} else {
const condNode=node as ConditionNode;
if (condNode.object && condNode.operator ) {
if (condNode.object && condNode.object.sharedText && condNode.operator ) {
// let value=condNode.value;
// if(value && typeof value ==='object' && ('label' in value)){
// value =condNode.value.label;
// }
return `${condNode.object.sharedText} ${typeof condNode.operator === 'object' ? condNode.operator.label : condNode.operator} ${condNode.value.sharedText}`;
const rightVal = condNode.value.sharedText || '""';
return `${condNode.object.sharedText} ${typeof condNode.operator === 'object' ? condNode.operator.label : condNode.operator} ${rightVal}`;
// return `${typeof condNode.object.name === 'object' ? condNode.object.name.name : condNode.object.name} ${typeof condNode.operator === 'object' ? condNode.operator.label : condNode.operator} '${value}'`;
} else {
return '';
@@ -219,7 +220,7 @@ export class ConditionTree {
if(node.type !== NodeType.Root){
conditionString = '(';
}
const groupNode = node as GroupNode;
for (let i = 0; i < groupNode.children.length; i++) {
const childConditionString = this.buildConditionQueryString(groupNode.children[i]);

View File

@@ -24,11 +24,12 @@ export class kintoneEvent implements IKintoneEvent {
}
flowData?: IActionFlow | undefined;
label: string;
header = 'EVENT';
constructor(label: string, eventId: string, parentId: string) {
header :string;
constructor(label: string, eventId: string, parentId: string,header?:string) {
this.eventId = eventId;
this.label = label;
this.parentId = parentId;
this.header=header?header:'EVENT';
}
}
@@ -97,16 +98,9 @@ export class KintoneEventManager {
const eventNode = this.findEventById(groupId);
if (eventNode && (eventNode.header === 'EVENTGROUP' || eventNode.header === 'CHANGE')) {
const groupEvent = eventNode as kintoneEventGroup;
const newEvent = {
label: flow.getRoot()?.subTitle || '',
eventId: eventId,
parentId: groupId,
header: 'DELETABLE',
hasFlow: true,
flowData: flow,
};
const label=flow.getRoot()?.subTitle || '';
const newEvent = new kintoneEvent(label,eventId,groupId,'DELETABLE');
newEvent.flowData=flow;
groupEvent.events.push(newEvent);
}
}
@@ -138,6 +132,31 @@ export class KintoneEventManager {
return null;
}
public findAllFlows():IActionFlow[]{
const flows:IActionFlow[]=[];
for (const screen of this.screens) {
for (const event of screen.events) {
if (event.header === "EVENT") {
const eventNode = event as IKintoneEvent;
if(eventNode.flowData!==undefined){
flows.push(eventNode.flowData);
}
}else if (event.header === 'EVENTGROUP' || event.header === 'CHANGE') {
const eventGroup = event as IKintoneEventGroup;
eventGroup.events.forEach((ev) => {
if (ev.header === "EVENT" || ev.header === "DELETABLE") {
const eventNode = ev as IKintoneEvent;
if(eventNode.flowData!==undefined){
flows.push(eventNode.flowData);
}
}
});
}
}
}
return flows;
}
public findScreen(eventId: string): IKintoneEventGroup | undefined {
return this.screens.find((screen) => screen.eventId == eventId);
}
@@ -194,7 +213,7 @@ export class KintoneEventManager {
),
new kintoneEventGroup(
'app.record.create.show.customButtonClick',
'ボタンをクリックした',
'ボタンをクリックしたとき',
[],
'app.record.create'
),
@@ -222,7 +241,7 @@ export class KintoneEventManager {
),
new kintoneEventGroup(
'app.record.detail.show.customButtonClick',
'ボタンをクリックした',
'ボタンをクリックしたとき',
[],
'app.record.detail'
),
@@ -256,7 +275,7 @@ export class KintoneEventManager {
),
new kintoneEventGroup(
'app.record.edit.show.customButtonClick',
'ボタンをクリックした',
'ボタンをクリックしたとき',
[],
'app.record.edit'
),
@@ -278,7 +297,7 @@ export class KintoneEventManager {
'app.record.index'
),
new kintoneEvent(
'インライン編集の保存をクリックしたとき',
'インライン編集の保存をクリックしたとき',
'app.record.index.edit.submit',
'app.record.index'
),
@@ -287,15 +306,15 @@ export class KintoneEventManager {
'app.record.index.edit.submit.success',
'app.record.index'
),
new kintoneEventForChange(
'app.record.index.edit.change',
'インライン編集のフィールド値を変更したとき',
[],
'app.record.index'
),
// new kintoneEventForChange(
// 'app.record.index.edit.change',
// 'インライン編集のフィールド値を変更したとき',
// [],
// 'app.record.index'
// ),
new kintoneEventGroup(
'app.record.detail.show.customButtonClick',
'ボタンをクリックした',
'app.record.index.show.customButtonClick',
'ボタンをクリックしたとき',
[],
'app.record.index'
),

View File

@@ -283,6 +283,11 @@
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz"
integrity sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==
"@types/web-bluetooth@^0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
"@typescript-eslint/eslint-plugin@^5.10.0":
version "5.61.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz"
@@ -419,6 +424,11 @@
resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz"
integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
"@vue/devtools-api@^6.6.3":
version "6.6.3"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.3.tgz#b23a588154cba8986bba82b6e1d0248bde3fd1a0"
integrity sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==
"@vue/reactivity-transform@3.3.4":
version "3.3.4"
resolved "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz"
@@ -467,6 +477,28 @@
resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz"
integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==
"@vueuse/core@^10.9.0":
version "10.11.1"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.11.1.tgz#15d2c0b6448d2212235b23a7ba29c27173e0c2c6"
integrity sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==
dependencies:
"@types/web-bluetooth" "^0.0.20"
"@vueuse/metadata" "10.11.1"
"@vueuse/shared" "10.11.1"
vue-demi ">=0.14.8"
"@vueuse/metadata@10.11.1":
version "10.11.1"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.11.1.tgz#209db7bb5915aa172a87510b6de2ca01cadbd2a7"
integrity sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==
"@vueuse/shared@10.11.1":
version "10.11.1"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.11.1.tgz#62b84e3118ae6e1f3ff38f4fbe71b0c5d0f10938"
integrity sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==
dependencies:
vue-demi ">=0.14.8"
accepts@~1.3.5, accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz"
@@ -1830,6 +1862,11 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
jwt-decode@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
kind-of@^6.0.2:
version "6.0.3"
resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz"
@@ -2212,13 +2249,18 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pinia@^2.1.6:
version "2.1.6"
resolved "https://registry.npmjs.org/pinia/-/pinia-2.1.6.tgz"
integrity sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==
pinia-plugin-persistedstate@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz#66780602aecd6c7b152dd7e3ddc249a1f7a13fe5"
integrity sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==
pinia@^2.1.7:
version "2.2.1"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.2.1.tgz#7cf860f6a23981c23e58605cee45496ce46d15d1"
integrity sha512-ltEU3xwiz5ojVMizdP93AHi84Rtfz0+yKd8ud75hr9LVyWX2alxp7vLbY1kFm7MXFmHHr/9B08Xf8Jj6IHTEiQ==
dependencies:
"@vue/devtools-api" "^6.5.0"
vue-demi ">=0.14.5"
"@vue/devtools-api" "^6.6.3"
vue-demi "^0.14.10"
postcss-selector-parser@^6.0.9:
version "6.0.13"
@@ -2792,10 +2834,10 @@ vite@^2.9.13:
optionalDependencies:
fsevents "~2.3.2"
vue-demi@>=0.14.5:
version "0.14.6"
resolved "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz"
integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==
vue-demi@>=0.14.8, vue-demi@^0.14.10:
version "0.14.10"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
vue-eslint-parser@^9.3.0:
version "9.3.1"

View File

@@ -26,15 +26,13 @@
"sass": "^1.69.5",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-checker": "^0.6.4",
"vite-plugin-lib-inject-css": "^2.1.1"
"vite-plugin-checker": "^0.6.4"
},
"dependencies": {
"@kintone/rest-api-client": "^5.5.2",
"@popperjs/core": "^2.11.8",
"@types/bootstrap": "^5.2.10",
"bootstrap": "^5.3.3",
"jquery": "^3.7.1",
"vite-plugin-css-injected-by-js": "^3.5.1"
"jquery": "^3.7.1"
}
}

View File

@@ -70,7 +70,10 @@
| placeholder | 対象項目を選択してください| 入力フィールドに表示されるプレースホルダーのテキストです。この場合は設定されていません。 |
| hint | 説明文| 長い説明文を設定することが可能です。markdown形式サポート予定、現在HTML可能 |
| selectType |`single` or `multiple`| フィールド選択・他のアプリのフィールド選択の選択モードを設定する |
| required | boolean | 必須チェックするかどうか |
| requiredMessage| string | 必須チェック時のエラーメッセージ。未設定の場合「XXXX」が必須です。になります |
| rules |"[val=>val<=100 && val>=1 \|\| '1-100の範囲内の数値を入力してください']"| 必須チェック以外のルールを設定する |
| fieldTypes |["SINGLE_LINE_TEXT","MULTI_LINE_TEXT","NUMBER"]| FieldInput,AppFieldSelectのみ使用可能。 |
### 使用可能なコンポーネント
@@ -78,14 +81,50 @@
|-----|------------------|------------------|-----------------------------------------|
| 1 | テキストボックス | InputText | 一行のテキスト入力が可能なフィールドです。 |
| 2 | テキストボックス(改行可能) | MuiltInputText | 複数行のテキスト入力が可能なテキストエリアです。 |
| 3 | 日付 | DatePicker | 日付を選択するためのカレンダーコンポーネントです。 |
| 4 | フィールド選択 | FieldInput | システムのフィールドを選択するための入力コンポーネントです。 |
| 5 | 選択リスト | SelectBox | 複数のオプションから選択するためのドロップダウンリストです。 |
| 6 | 条件式設定 | ConditionInput | 条件式やロジックを入力するためのコンポーネントです。 |
| 7 | イベント設定 |EventSetter | ボタンイベント設定のコンポーネントです。 |
| 8 | 色選択 | ColorPicker | 色を設定する(追加予定中) |
| 9 | 他のアプリのフィールド選択 | AppFieldPicker | 他のアプリのフィールドを選択する(追加予定中) |
| 10 |ユーザー選択 | UserPicker | ユーザーを選択する(追加予定中) |
| 3 | 数値入力 | NumInput | 数値のみ入力可能フィールド。 |
| 4 | 日付 | DatePicker | 日付を選択するためのカレンダーコンポーネントです。 |
| 5 | フィールド選択 | FieldInput | システムのフィールドを選択するための入力コンポーネントです。 |
| 6 | 選択リスト | SelectBox | 複数のオプションから選択するためのドロップダウンリストです。 |
| 7 | 条件式設定 | ConditionInput | 条件式やロジックを入力するためのコンポーネントです。 |
| 8 | イベント設定 |EventSetter | ボタンイベント設定のコンポーネントです。 |
| 9 | 選択 | ColorPicker | 色を設定する |
| 10 | 他のアプリのフィールド選択 | AppFieldSelect | 他のアプリのフィールドを選択する |
| 11 | アプリ選択 | AppSelect | アプリを選択する |
### フィールド選択コンポーネントのfieldTypes属性を使用可能フィールド種別
| 番号 | 項目タイプ名 | 種別タイプ |
|------|-----------------------|-------------------|
| 1 | カテゴリー | CATEGORY |
| 2 | 作成日時 | CREATED_TIME |
| 3 | 作成者 | CREATOR |
| 4 | 更新者 | MODIFIER |
| 5 | レコード番号 | RECORD_NUMBER |
| 6 | 更新日時 | UPDATED_TIME |
| 7 | 計算 | CALC |
| 8 | チェックボックス | CHECK_BOX |
| 9 | 日付 | DATE |
| 10 | 日時 | DATETIME |
| 11 | ドロップダウン | DROP_DOWN |
| 12 | 添付ファイル | FILE |
| 13 | グループ | GROUP |
| 14 | グループ選択 | GROUP_SELECT |
| 15 | リンク | LINK |
| 16 | 文字列 (複数行) | MULTI_LINE_TEXT |
| 17 | 複数選択 | MULTI_SELECT |
| 18 | 数値 | NUMBER |
| 19 | 組織選択 | ORGANIZATION_SELECT |
| 20 | ラジオボタン | RADIO_BUTTON |
| 21 | 関連レコード一覧 | REFERENCE_TABLE |
| 22 | リッチエディター | RICH_TEXT |
| 23 | 文字列 (1行) | SINGLE_LINE_TEXT |
| 24 | ステータス | STATUS |
| 25 | 作業者 | STATUS_ASSIGNEE |
| 26 | テーブル | SUBTABLE |
| 27 | 時刻 | TIME |
| 28 | ユーザー選択 | USER_SELECT |
| 29 | スペース | SPACER |
| 30 | ルックアップ | lookup |
## 2.アクションアドインの開発
@@ -270,7 +309,7 @@ npm run build:dev
- Azure App Service 拡張機能でデプロイが完了したことを確認します。
- ka-addin の URL にアクセスしてアプリケーションが正常に動作しているか確認します。
3. **ローカルでプラグインをテストする**
3. **ローカルでプラグインをテストする(ZCCの導入ため、廃止する)**
1. kintone-addinsをPreviewで起動する
```bash
yarn build:dev
@@ -278,7 +317,7 @@ yarn preview
#またはyarn devは yarn build:dev + yarn preview と同じです
yarn dev
```
2. **ngrokをインストールする**
2. **ngrokをインストールする(ZCCの導入ため、廃止する)**
1. [ngrok の公式ウェブサイト](https://ngrok.com/)にアクセスします。
2. 「Sign up」をクリックしてアカウントを登録するか、既存のアカウントにログインします。
3. 登録またはログイン後、ダッシュボードに進み、ダウンロードリンクが表示されます。操作システムWindows、macOS、Linuxに応じて、適切なバージョンを選択してダウンロードします。
@@ -292,4 +331,10 @@ yarn dev
6. ngrok を起動する
```bash
ngrok https http://localhost:4173/
```
```
3. kintone-addinsをビルドする
```bash
yarn build:dev #開発モード
#またはyarn devは yarn build:dev + yarn preview と同じです
yarn build #本番リリースモード
```

View File

@@ -1 +1,20 @@
@import 'bootstrap/scss/bootstrap';
.modal-backdrop {
--bs-backdrop-zindex: 1050;
--bs-backdrop-bg: #000;
--bs-backdrop-opacity: .5;
position: fixed;
top: 0;
left: 0;
z-index: var(--bs-backdrop-zindex);
width: 100vw;
height: 100vh;
background-color: var(--bs-backdrop-bg)
}
.modal-backdrop.fade {
opacity: 0
}
.modal-backdrop.show {
opacity: var(--bs-backdrop-opacity)
}

View File

@@ -9,14 +9,14 @@ import {
import { actionAddins } from ".";
import type { Record} from "@kintone/rest-api-client/lib/src/client/types";
import { KintoneRestAPIClient} from "@kintone/rest-api-client";
import { KintoneAllRecordsError, KintoneRestAPIClient} from "@kintone/rest-api-client";
import "./auto-lookup.scss";
import "bootstrap/js/dist/modal";
// import "bootstrap/js/dist/spinner";
import {Modal} from "bootstrap"
import $ from "jquery";
interface Props {
interface IAutoLookUpProps {
displayName: string;
lookupField: LookupField;
condition: Condition;
@@ -53,7 +53,7 @@ interface Field {
noLabel: boolean;
required: boolean;
lookup: Lookup;
}
}
interface Lookup {
relatedApp: RelatedApp;
@@ -84,11 +84,11 @@ interface App {
export class AutoLookUpAction implements IAction {
name: string;
actionProps: IActionProperty[];
props: Props;
props: IAutoLookUpProps;
constructor() {
this.name = "ルックアップ更新";
this.actionProps = [];
this.props = {} as Props;
this.props = {} as IAutoLookUpProps;
this.register();
}
@@ -96,15 +96,15 @@ export class AutoLookUpAction implements IAction {
* アクセスのメインの処理関数
*/
async process(
prop: IActionNode,
actionNode: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = prop.actionProps;
this.actionProps = actionNode.actionProps;
this.props = {
...prop.ActionValue,
condition: JSON.parse((prop.ActionValue as any).condition),
} as Props;
...actionNode.ActionValue,
condition: JSON.parse((actionNode.ActionValue as any).condition),
} as IAutoLookUpProps;
// console.log(context);
let result = {
@@ -129,19 +129,16 @@ export class AutoLookUpAction implements IAction {
}
const updateRecords = this.convertForLookup(targetRecords,lookUpField,key);
console.log("updateRecords", updateRecords);
this.showSpinnerModel(this.props.lookupField.app);
await this.updateLookupTarget(updateRecords);
this.showResult(this.props.lookupField.app,updateRecords.length);
this.showSpinnerModel(this.props.lookupField.app,lookUpField);
const updateResult = await this.updateLookupTarget(updateRecords);
if(updateResult){
this.showResult(this.props.lookupField.app,lookUpField,updateRecords.length);
}
} catch (error) {
console.error("ルックアップ更新中例外が発生しました。", error);
if(error instanceof Error){
event.error = error.message;
}else{
event.error = "ルックアップ更新中例外が発生しました。";
}
this.closeDialog();
context.errors.handleError(error,actionNode,"ルックアップ更新中例外が発生しました");
result.canNext = false;
}
console.log("autoLookupProps", this.props);
return result;
}
@@ -153,12 +150,17 @@ export class AutoLookUpAction implements IAction {
* @returns
*/
makeQuery=(lookUpField:Field,key:any)=>{
let query ="";
if(typeof key==='number'){
return `${lookUpField.code} = ${key}`
query = `${lookUpField.code} = ${key}`
}
if(typeof key==='string'){
return `${lookUpField.code} = "${key}"`
query = `${lookUpField.code} = "${key}"`
}
if(this.props.condition.queryString!==''){
query = `${query} and (${this.props.condition.queryString})`
}
return query;
}
/**
@@ -192,28 +194,41 @@ export class AutoLookUpAction implements IAction {
* ルックアップ先を更新する
* @param updateRecords
*/
updateLookupTarget = async (updateRecords:Array<any>)=>{
updateLookupTarget = async (updateRecords:Array<any>):Promise<boolean>=>{
if (updateRecords && updateRecords.length > 0) {
const client=new KintoneRestAPIClient();
client.record.updateAllRecords({
app:this.props.lookupField.app.id,
records:updateRecords
});
try{
const client=new KintoneRestAPIClient();
const result = await client.record.updateAllRecords({
app:this.props.lookupField.app.id,
records:updateRecords
});
return true;
}catch(error ){
if(error instanceof KintoneAllRecordsError){
this.showError(this.props.lookupField.app,
this.props.lookupField.fields[0],
error as KintoneAllRecordsError,updateRecords.length);
return false;
}else{
throw error;
}
}
// await kintone.api(kintone.api.url("/k/v1/records.json", true), "PUT", {
// app: this.props.lookupField.app.id,
// records: updateRecords
// });
}
return false;
}
/**
* 更新中のダイアログ表示
* @param app
*/
showSpinnerModel = (app:App) => {
showSpinnerModel = (app:App,lookup:Field) => {
let dialog = $("#alcLookupModal");
if(dialog.length===0){
const modalHTML = `
const modalHTML = `<div class="bs-scope">
<div class="modal" id="alcLookupModal" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog-centered">
<div class="modal-dialog modal-content">
@@ -222,7 +237,7 @@ export class AutoLookUpAction implements IAction {
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row" id="app${app.id}">
<div class="row" id="app${app.id}_${lookup.code}">
<div class="spinner-border text-secondary col-1 " role="alert"></div>
<div class="col">${app.name}</div>
</div>
@@ -230,25 +245,24 @@ export class AutoLookUpAction implements IAction {
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>`;
dialog = $(modalHTML).appendTo("body");
</div></div></div></div>`;
$(modalHTML).appendTo("body");
dialog = $("#alcLookupModal");
dialog.get()[0].addEventListener('hidden.bs.modal',(ev)=>{
Modal.getOrCreateInstance(dialog.get()[0]).dispose();
$("#alcLookupModal").remove();
$("#alcLookupModal").parent().remove();
});
}else{
const dialogBody=$("#alcLookupModal .modal-body");
const htmlrow=`
<div class="row" id="app${app.id}">
<div class="row" id="app${app.id}_${lookup.code}">
<div class="spinner-border text-secondary col-1 " role="alert">
</div>
<div class="col">${app.name}</div>
<div>`;
dialogBody.append(htmlrow);
}
Modal.getOrCreateInstance(dialog.get()[0]).show();
Modal.getOrCreateInstance(dialog.get()[0]).show();
}
/**
@@ -256,14 +270,39 @@ export class AutoLookUpAction implements IAction {
* @param app  更新先アプリ情報
* @param count 更新件数
*/
showResult=(app:App,count:number)=>{
const dialogBody=$(`#alcLookupModal .modal-body #app${app.id}`);
showResult=(app:App,lookup:Field,count:number)=>{
const dialogBody=$(`#alcLookupModal .modal-body #app${app.id}_${lookup.code}`);
const html=` <div class="col-1 text-success">✔</div>
<div class="col">${app.name}</div>
<div class="col">更新件数:${count}件</div>`;
dialogBody.html(html);
}
/**
* 更新結果を表示する
* @param app  更新先アプリ情報
* @param count 更新件数
*/
showError=(app:App,lookup:Field,error:KintoneAllRecordsError,allCount:Number)=>{
const message=error.error.message;
const proRecords = error.numOfProcessedRecords;
const allRecords=error.numOfAllRecords;
const dialogBody=$(`#alcLookupModal .modal-body #app${app.id}_${lookup.code}`);
const html=`<div class="col-1 text-danger">✖</div>
<div class="col">${app.name}</div>
<div class="col">更新件数:${proRecords}/${allRecords}</div>
<div class="row text-danger">${message}<div>`;
dialogBody.html(html);
}
/**
* ダイアログ画面を閉じる
*/
closeDialog=()=>{
const dialog = $("#alcLookupModal");
Modal.getOrCreateInstance(dialog.get()[0]).dispose();
$("#alcLookupModal").parent().remove();
}
register(): void {
actionAddins[this.name] = this;
}

View File

@@ -65,8 +65,7 @@ export class AutoNumbering implements IAction{
}
return result;
}catch(error){
console.error(error);
event.error="処理中異常が発生しました。";
context.errors.handleError(error,actionNode);
return {
canNext:false,
result:false

View File

@@ -0,0 +1,47 @@
// @import 'bootstrap/scss/bootstrap';
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/root";
.bs-scope{
// Required
@import "bootstrap/scss/utilities";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
@import "bootstrap/scss/images";
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
// @import "bootstrap/scss/tables";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
// @import "bootstrap/scss/button-group";
// @import "bootstrap/scss/nav";
// @import "bootstrap/scss/navbar"; // Requires nav
@import "bootstrap/scss/card";
// @import "bootstrap/scss/breadcrumb";
// @import "bootstrap/scss/accordion";
// @import "bootstrap/scss/pagination";
// @import "bootstrap/scss/badge";
// @import "bootstrap/scss/alert";
// @import "bootstrap/scss/progress";
// @import "bootstrap/scss/list-group";
@import "bootstrap/scss/close";
// @import "bootstrap/scss/toasts";
@import "bootstrap/scss/modal"; // Requires transitions
// @import "bootstrap/scss/tooltip";
@import "bootstrap/scss/popover";
// @import "bootstrap/scss/carousel";
@import "bootstrap/scss/spinners";
@import "bootstrap/scss/offcanvas"; // Requires transitions
// @import "bootstrap/scss/placeholders";
// Helpers
// @import "bootstrap/scss/helpers";
// Utilities
@import "bootstrap/scss/utilities/api";
}

View File

@@ -1,7 +1,7 @@
import { actionAddins } from ".";
import $ from 'jquery';
import { IAction, IActionProperty, IActionNode, IActionResult } from "../types/ActionTypes";
import { IAction, IActionProperty, IActionNode, IActionResult, IContext } from "../types/ActionTypes";
import "./button-add.css";
/**
@@ -43,7 +43,7 @@ export class ButtonAddAction implements IAction {
* @param event
* @returns
*/
async process(actionNode: IActionNode, event: any): Promise<IActionResult> {
async process(actionNode: IActionNode, event: any,context:IContext): Promise<IActionResult> {
let result = {
canNext: true,
result: false
@@ -75,8 +75,7 @@ export class ButtonAddAction implements IAction {
});
return result;
} catch (error) {
event.error = error;
console.error(error);
context.errors.handleError(error,actionNode);
result.canNext = false;
return result;
}

View File

@@ -0,0 +1,18 @@
.alc-loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
display: flex;
z-index: 9999;
}
.alc-loading > div {
margin: auto;
}
.alc-dnone{
display: none;
}

View File

@@ -0,0 +1,80 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
} from "../types/ActionTypes";
import { actionAddins } from ".";
// import { KintoneRestAPIClient } from "@kintone/rest-api-client";
// import { getPageState } from "../util/url";
import { Snipper } from '../util/ui-helper';
import { DropDownManager,ICascadingDropDown, IFieldList} from '../types/CascadingDropDownManager'
import "./cascading-dropdown.scss";
// import { Record as KTRecord } from "@kintone/rest-api-client/lib/src/client/types";
// 階層化ドロップダウンメニューのプロパティインターフェース
interface ICascadingDropDownProps {
displayName: string;
cascadingDropDown: ICascadingDropDown;
}
/**
* 階層化ドロップダウンのクラス実装
*/
export class CascadingDropDownAction implements IAction {
name: string;
actionProps: IActionProperty[];
props: ICascadingDropDownProps;
constructor() {
this.name = "階層化ドロップダウン";
this.actionProps = [];
this.props = {} as ICascadingDropDownProps;
this.register();
}
/**
* アクションのプロセス実行
* @param actionNode
* @param event
* @param context
* @returns
*/
async process(
actionNode: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = actionNode.actionProps;
this.props = actionNode.ActionValue as ICascadingDropDownProps;
const result: IActionResult = { canNext: true, result: "" };
const snipper = new Snipper("body");
const dropDownManager= new DropDownManager(this.props.cascadingDropDown,event);
try {
if (!this.props) return result;
const appId = this.props.cascadingDropDown.dropDownApp.id;
//snipper表示
snipper.showSpinner();
await dropDownManager.handlePageState(appId);
snipper.hideSpinner();
return result;
} catch (error) {
console.error(
"CascadingDropDownAction プロセス中にエラーが発生しました:",
error
);
context.errors.handleError(error, actionNode);
return { canNext: false, result: "" };
}finally{
snipper.removeSpinner();
}
}
register(): void {
actionAddins[this.name] = this;
}
}
new CascadingDropDownAction();

View File

@@ -63,8 +63,7 @@ export class ConditionAction implements IAction{
}
return result;
}catch(error){
event.error=error;
console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}

View File

@@ -1,5 +1,5 @@
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField} from "../types/ActionTypes";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext} from "../types/ActionTypes";
/**
* アクションの属性定義
*/
@@ -32,7 +32,7 @@ export class StrCountCheckAciton implements IAction{
* @param event
* @returns
*/
async process(actionNode:IActionNode,event:any):Promise<IActionResult> {
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:true,
result:false
@@ -54,15 +54,16 @@ export class StrCountCheckAciton implements IAction{
}else if(maxLength < value.length){
record[this.props.field.code].error = this.props.message;
}else{
result= {
canNext:true,
result:true
}
record[this.props.field.code].error = null;
}
result= {
canNext:true,
result:true
}
return result;
}catch(error){
event.error=error;
console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}

View File

@@ -4,13 +4,14 @@ import {
IActionNode,
IActionProperty,
IContext,
IField,
} from "../types/ActionTypes";
import { actionAddins } from ".";
interface Props {
interface ICurrentFieldGetProps {
displayName: string;
field: Field;
field: IField;
verName: VerName;
}
@@ -18,49 +19,43 @@ interface VerName {
name: string;
}
interface Field {
name: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
}
export class CurrentFieldGetAction implements IAction {
name: string;
actionProps: IActionProperty[];
currentFieldGetProps: Props;
props: ICurrentFieldGetProps;
constructor() {
this.name = "フィールドの値を取得する";
this.actionProps = [];
this.currentFieldGetProps = {} as Props;
this.props = {} as ICurrentFieldGetProps;
this.register();
}
async process(
prop: IActionNode,
actionNode: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.currentFieldGetProps = prop.ActionValue as Props;
this.actionProps = prop.actionProps;
this.props = actionNode.ActionValue as ICurrentFieldGetProps;
this.actionProps = actionNode.actionProps;
let result = {
canNext: true,
result: "",
result: '',
} as IActionResult;
try {
context.variables[this.currentFieldGetProps.verName.name] = context.record[this.currentFieldGetProps.field.code].value;
} catch (error) {
console.error("CurrentFieldGetAction error", error);
const record = event.record;
if(!(this.props.field.code in record)){
throw new Error(`フィールド[${this.props.field.code}]が見つかりませんでした。`);
}
//変数設定
if(this.props.verName && this.props.verName.name!==''){
context.variables[this.props.verName.name]=record[this.props.field.code].value;
}
}
catch (error) {
context.errors.handleError(error,actionNode);
result.canNext = false;
}
return result;

View File

@@ -1,166 +0,0 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
} from "../types/ActionTypes";
import { actionAddins } from ".";
interface Props {
displayName: string;
sources: Sources;
dataMapping: DataMapping[];
}
interface DataMapping {
id: string;
from: From;
to: To;
}
interface To {
app: App;
fields: Field[];
isDialogVisible: boolean;
}
interface Field {
name: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
}
interface From {
sharedText: string;
_t: string;
id: string;
objectType: string;
name: Name;
actionName: string;
displayName: string;
}
interface Name {
name: string;
}
interface Sources {
app: App;
}
interface App {
id: string;
name: string;
description: string;
createdate: string;
}
export class DataMappingAction implements IAction {
name: string;
actionProps: IActionProperty[];
dataMappingProps: Props;
constructor() {
this.name = "データマッピング";
this.actionProps = [];
this.dataMappingProps = {} as Props;
this.register();
}
async process(
prop: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = prop.actionProps;
this.dataMappingProps = prop.ActionValue as Props;
console.log(prop.ActionValue);
// this.initTypedActionProps();
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
const record = this.dataMappingProps.dataMapping
.filter(
(item) =>
item.from.objectType === "variable" &&
item.from.name.name &&
item.to.app &&
item.to.fields &&
item.to.fields.length > 0
)
.reduce((accumulator, item) => {
return {...accumulator, [item.to.fields[0].code]: {
value: getValueByPath(context.variables, item.from.name.name),
}};
}, {});
if (record && Object.keys(record).length > 0) {
await kintone.api(kintone.api.url("/k/v1/record.json", true), "POST", {
app: this.dataMappingProps.sources.app.id,
record: record,
});
}
} catch (error) {
console.error("DataMappingAction error", error);
result.canNext = false;
}
console.log("dataMappingProps", this.dataMappingProps);
return result;
}
register(): void {
actionAddins[this.name] = this;
}
}
new DataMappingAction();
const getValueByPath = (obj: any, path: string) => {
return path.split(".").reduce((o, k) => (o || {})[k], obj);
};
type Resp = { records: RespRecordType[] };
type RespRecordType = {
[key: string]: {
type: string;
value: any;
};
};
type Result = {
type: string;
value: any[];
};
const selectData = async (appid: string, field: string): Promise<Result> => {
return kintone
.api(kintone.api.url("/k/v1/records", true), "GET", {
app: appid ?? kintone.app.getId(),
fields: [field],
})
.then((resp: Resp) => {
const result: Result = { type: "", value: [] };
resp.records.forEach((element) => {
for (const [key, value] of Object.entries(element)) {
if (result.type === "") {
result.type = value.type;
}
result.value.push(value.value);
}
});
return result;
});
};

View File

@@ -6,67 +6,19 @@ import {
IContext,
} from "../types/ActionTypes";
import { actionAddins } from ".";
import {KintoneRestAPIClient} from '@kintone/rest-api-client';
import { Aggregator,Operator} from '../util/aggregates';
import { FieldForm } from "../types/FieldLayout";
interface Props {
interface IDataProcessingProps {
displayName: string;
sources: Sources;
condition: string;
conditionO: Condition;
verName: VerName;
verName?: VerName;
}
interface Condition {
queryString: string;
index: number;
type: string;
children: Child[];
parent: null;
logicalOperator: string;
}
interface Child {
index: number;
type: string;
parent: string;
object: Object;
operator: ChildOperator;
value: Value;
}
interface Value {
sharedText: string;
_t: string;
objectType: string;
actionName: string;
displayName: string;
name: Name;
}
interface Name {
name: string;
}
interface ChildOperator {
label: string;
value: string;
}
interface Object {
sharedText: string;
_t: string;
name: string;
objectType: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
}
interface VerName {
@@ -78,170 +30,26 @@ interface VerName {
interface Var {
id: string;
field: Field2;
logicalOperator: LogicalOperator;
field: FieldForm;
logicalOperator: CalcOperator;
vName: string;
}
interface LogicalOperator {
operator: string;
interface CalcOperator {
operator: Operator;
label: string;
}
interface Field2 {
sharedText: string;
_t: string;
name: string;
objectType: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
}
interface Sources {
app: App;
fields: Field[];
}
interface Field {
name: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
fields: FieldForm[];
}
interface App {
id: string;
name: string;
description: string;
createdate: string;
name?: string;
}
export class DataProcessingAction implements IAction {
name: string;
actionProps: IActionProperty[];
dataProcessingProps: Props | null;
constructor() {
this.name = "データ処理";
this.actionProps = [];
this.dataProcessingProps = null;
this.register();
}
async process(
nodes: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = nodes.actionProps;
this.dataProcessingProps = nodes.ActionValue as Props;
this.dataProcessingProps.conditionO = JSON.parse(
this.dataProcessingProps.condition
);
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
if (!this.dataProcessingProps) {
return result;
}
const data = await selectData(
varGet(
this.dataProcessingProps.conditionO.queryString,
context.variables
)
);
console.log("data ", data);
context.variables[this.dataProcessingProps.verName.name] =
this.dataProcessingProps.verName.vars.reduce((acc, f) => {
const v = calc(f, data);
if (v) {
acc[f.vName] = calc(f, data);
}
return acc;
}, {} as AnyObject);
console.log("context ", context);
return result;
} catch (error) {
console.error(error);
event.error = error;
return result;
}
}
register(): void {
actionAddins[this.name] = this;
}
}
new DataProcessingAction();
const varGet = (str: string, vars: any) => {
console.log(str);
const regex = /var\((.*?)\)/g;
let match;
while ((match = regex.exec(str)) !== null) {
const varName = match[1];
if (varName in vars) {
str = str.replace(match[0], vars[varName]);
} else {
throw new Error(`変数${varName}が見つかりません`);
}
}
console.log(str);
return str;
};
const selectData = async (query?: string) => {
return kintone
.api(kintone.api.url("/k/v1/records", true), "GET", {
app: kintone.app.getId(),
query: query,
})
.then((resp: Resp) => {
const result: Result = {};
resp.records.forEach((element) => {
for (const [key, value] of Object.entries(element)) {
if (!result[key]) {
result[key] = { type: value.type, value: [] };
}
result[key].value.push(value.value);
}
});
return result;
});
};
type Resp = { records: RespRecordType[] };
type RespRecordType = {
[key: string]: {
type: string;
value: any;
};
};
type Result = {
[key: string]: {
type: string;
@@ -253,122 +61,120 @@ type AnyObject = {
[key: string]: any;
};
const ERROR_TYPE = "ERROR_TYPE";
export class DataProcessingAction implements IAction {
name: string;
actionProps: IActionProperty[];
props: IDataProcessingProps ;
const calc = (f: Var, result: Result) => {
const type = typeCheck(f.field.type);
if (!type) {
return ERROR_TYPE;
constructor() {
this.name = "データ処理";
this.actionProps = [];
this.props = {
displayName:'',
condition:'',
sources:{
app:{
id:""
},
fields:[]
},
};
this.register();
}
const fun =
calcFunc[
`${type}_${Operator[f.logicalOperator.operator as keyof typeof Operator]}`
];
if (!fun) {
return ERROR_TYPE;
}
const values = result[f.field.code].value;
if (!values) {
return null;
}
return fun(values);
};
async process(
actionNode: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = actionNode.actionProps;
const typeCheck = (type: string) => {
switch (type) {
case "RECORD_NUMBER":
case "NUMBER":
return CalcType.NUMBER;
case "SINGLE_LINE_TEXT":
case "MULTI_LINE_TEXT":
case "RICH_TEXT":
return CalcType.STRING;
case "DATE":
return CalcType.DATE;
case "TIME":
return CalcType.TIME;
case "DATETIME":
case "UPDATED_TIME":
return CalcType.DATETIME;
default:
return null;
}
};
this.props = actionNode.ActionValue as IDataProcessingProps;
const condition = JSON.parse(this.props.condition) as Condition;
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
if (!this.props) {
return result;
}
enum Operator {
SUM = "SUM",
AVG = "AVG",
MAX = "MAX",
MIN = "MIN",
COUNT = "COUNT",
FIRST = "FIRST",
const query = this.getQuery(condition.queryString,context.variables);
const data = await this.selectData(query);
console.log("data ", data);
if(this.props.verName){
const varValues= this.props.verName.vars.reduce((acc, f) => {
const datas=data[f.field.code].value;
const agg = new Aggregator(datas,f.field);
const result = agg.calculate(f.logicalOperator.operator)
acc[f.vName]=result;
return acc;
}, {} as AnyObject);
context.variables[this.props.verName.name]=varValues;
console.log("context ", context);
}
return result;
} catch (error) {
context.errors.handleError(error,actionNode);
result.canNext = false;
return result;
}
}
/**
*
* @param str
* @param vars
* @returns
*/
getQuery = (str: string, vars: any) => {
console.log(str);
const regex = /var\((.*?)\)/g;
let match;
while ((match = regex.exec(str)) !== null) {
const varName = match[1];
if (varName in vars) {
str = str.replace(match[0], vars[varName]);
} else {
throw new Error(`変数${varName}が見つかりません`);
}
}
console.log(str);
return str;
};
/**
* データを取得する
* @param query
* @returns
*/
selectData = async ( query?: string) => {
const api = new KintoneRestAPIClient();
const fields = this.props.sources.fields.map((field)=>field.code);
const resp = await api.record.getAllRecords({
app: this.props.sources.app.id,
fields:fields,
condition:query
});
const result: Result = {};
resp.forEach((element) => {
for (const [key, value] of Object.entries(element)) {
if (!result[key]) {
result[key] = { type: value.type, value: [] };
}
result[key].value.push(value.value);
}
});
return result;
};
register(): void {
actionAddins[this.name] = this;
}
}
enum CalcType {
NUMBER = "number",
STRING = "string",
DATE = "date",
TIME = "time",
DATETIME = "datetime",
}
const calcFunc: Record<string, (value: string[]) => string | null> = {
[`${CalcType.NUMBER}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.STRING}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.DATE}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.TIME}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.DATETIME}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.NUMBER}_${Operator.SUM}`]: (value: string[]) =>
value.reduce((acc, v) => acc + Number(v), 0).toString(),
[`${CalcType.NUMBER}_${Operator.AVG}`]: (value: string[]) =>
(value.reduce((acc, v) => acc + Number(v), 0) / value.length).toString(),
[`${CalcType.NUMBER}_${Operator.MAX}`]: (value: string[]) =>
Math.max(...value.map(Number)).toString(),
[`${CalcType.NUMBER}_${Operator.MIN}`]: (value: string[]) =>
Math.min(...value.map(Number)).toString(),
[`${CalcType.STRING}_${Operator.SUM}`]: (value: string[]) => value.join(" "),
[`${CalcType.DATE}_${Operator.MAX}`]: (value: string[]) =>
value.reduce((maxDate, currentDate) =>
maxDate > currentDate ? maxDate : currentDate
),
[`${CalcType.DATE}_${Operator.MIN}`]: (value: string[]) =>
value.reduce((minDate, currentDate) =>
minDate < currentDate ? minDate : currentDate
),
[`${CalcType.TIME}_${Operator.MAX}`]: (value: string[]) =>
value.reduce((maxTime, currentTime) =>
maxTime > currentTime ? maxTime : currentTime
),
[`${CalcType.TIME}_${Operator.MIN}`]: (value: string[]) =>
value.reduce((minTime, currentTime) =>
minTime < currentTime ? minTime : currentTime
),
[`${CalcType.DATETIME}_${Operator.MAX}`]: (value: string[]) =>
value.reduce((maxDateTime, currentDateTime) =>
new Date(maxDateTime) > new Date(currentDateTime)
? maxDateTime
: currentDateTime
),
[`${CalcType.DATETIME}_${Operator.MIN}`]: (value: string[]) =>
value.reduce((minDateTime, currentDateTime) =>
new Date(minDateTime) < new Date(currentDateTime)
? minDateTime
: currentDateTime
),
[`${CalcType.STRING}_${Operator.FIRST}`]: (value: string[]) => {
return value[0];
},
};
new DataProcessingAction();

View File

@@ -0,0 +1,334 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
} from "../types/ActionTypes";
import { actionAddins } from ".";
import { KintoneRestAPIClient } from "@kintone/rest-api-client";
import { Lookup } from "@kintone/rest-api-client/lib/src/KintoneFields/types/property";
import { FieldForm, FieldType } from "../types/FieldLayout";
interface Props {
displayName: string;
sources: Sources;
dataMapping: DataMapping;
}
interface DataMapping {
data: Mapping[];
createWithNull: boolean;
}
interface Mapping {
id: string;
from: From;
to: To;
isKey: boolean;
}
interface To {
app: App;
fields:FieldForm[];
isDialogVisible: boolean;
}
interface From {
sharedText: string;
id: string;
objectType: 'variable'|'field'|'text';
}
interface IVar extends From{
name:{
name:string;
}
}
interface IFromField extends From,FieldForm{
}
interface Sources {
app: App;
}
interface App {
id: string;
name: string;
description: string;
createdate: string;
}
export class DataUpdateAction implements IAction {
name: string;
actionProps: IActionProperty[];
dataMappingProps: Props;
constructor() {
this.name = "データ更新";
this.actionProps = [];
this.dataMappingProps = {} as Props;
this.register();
}
async process(
actionNode: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = actionNode.actionProps;
this.dataMappingProps = actionNode.ActionValue as Props;
console.log(context);
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
const lookupFixedFieldCodes = await getLookupFixedFieldCodes(
this.dataMappingProps.sources.app.id
);
// createWithNull が有効な場合は、4 番目のパラメーターを true にして doUpdate 関数ブランチを実行します。
if (this.dataMappingProps.dataMapping.createWithNull) {
await doUpdate(
this.dataMappingProps.dataMapping.data,
this.dataMappingProps.sources.app.id,
context,
true, // キーがない場合、またはキーでターゲットが見つからない場合に、マッピング条件によって新しいレコードを作成するかどうかを決定するために使用されます。
lookupFixedFieldCodes
);
} else if (
// キーがないと更新対象を取得できないため、この時点でのみ更新が行われます。 doUpdate 関数の 4 番目のパラメーターは false です。
this.dataMappingProps.dataMapping.data
.map((m) => m.isKey)
.find((isKey) => isKey === true)
) {
await doUpdate(
this.dataMappingProps.dataMapping.data,
this.dataMappingProps.sources.app.id,
context,
false,
lookupFixedFieldCodes
);
} else {
await doCreate(
this.dataMappingProps.dataMapping.data,
this.dataMappingProps.sources.app.id,
context,
lookupFixedFieldCodes
);
}
} catch (error) {
context.errors.handleError(error,actionNode);
result.canNext = false;
}
console.log("dataMappingProps", this.dataMappingProps);
return result;
}
register(): void {
actionAddins[this.name] = this;
}
}
new DataUpdateAction();
const getContextVarByPath = (obj: any, path: string) => {
return path.split(".").reduce((o, k) => (o || {})[k], obj);
};
interface UpdateRecord {
id: string;
record: {
[key: string]: {
value: any;
};
};
}
const client = new KintoneRestAPIClient();
const getFromValue=(item:Mapping,context:IContext)=>{
if (item.from.objectType === "variable") {
const rfrom =item.from as IVar;
return getContextVarByPath(context.variables,rfrom.name.name);
}else if(item.from.objectType === "field"){
const field = item.from as IFromField;
return context.record[field.code].value;
}
else {
return item.from.sharedText;
}
}
const doUpdate = async (
mappingData: Mapping[],
appId: string,
context: IContext,
needCreate: boolean,
lookupFixedFieldCodes: string[]
) => {
const targetField = await findUpdateField(mappingData, appId, context);
console.log(targetField);
if (targetField.records.length === 0 && needCreate) {
await doCreate(mappingData, appId, context, lookupFixedFieldCodes);
} else {
// マッピングデータを単純なオブジェクトに処理し、ソース値が変数の場合は変数を置き換えます。
const mappingRules = mappingData
.filter(
(m) =>
Object.keys(m.from).length > 0 &&
!lookupFixedFieldCodes.includes(m.to.fields[0].code)
)
.map((m) => {
if (m.from.objectType === "variable") {
const rfrom =m.from as IVar;
return {
value: getContextVarByPath(context.variables,rfrom.name.name),
code: m.to.fields[0].code,
};
}else if(m.from.objectType === "field"){
const field = m.from as IFromField;
return {
value: context.record[field.code].value,
code: m.to.fields[0].code,
}
}
else {
return {
value: m.from.sharedText,
code: m.to.fields[0].code,
};
}
});
const updateRecords: UpdateRecord[] = targetField.records.map(
(targetRecord) => {
const updateRecord: UpdateRecord["record"] = {};
// マッピング内のルールにヒットしたフィールドのみが更新されます。
for (const mapping of mappingRules) {
if (targetRecord[mapping.code]) {
updateRecord[mapping.code] = {
value: mapping.value,
};
}
}
return {
id: targetRecord.$id.value as string,
record: updateRecord,
};
}
);
console.log(updateRecords);
await client.record.updateRecords({
app: appId,
records: updateRecords,
});
}
};
const makeQuery=(field:FieldForm,key:any)=>{
if(field.type===FieldType.NUMBER || field.type===FieldType.RECORD_NUMBER){
return `${field.code} = ${Number(key)}`
}
if(typeof key==='string'){
return `${field.code} = "${key}"`
}
}
const findUpdateField = async (
mappingData: Mapping[],
appId: string,
context: IContext
) => {
const queryStr = mappingData
.filter((m) => m.to.app && m.to.fields && m.to.fields.length > 0 && m.isKey)
.map((m) => {
if (m.from.objectType === "variable") {
const vfrom = m.from as IVar;
return makeQuery(m.to.fields[0],getContextVarByPath(context.variables , vfrom.name.name));
}
else if(m.from.objectType === "field"){
const field = m.from as IFromField;
return makeQuery(m.to.fields[0],context.record[field.code].value);
}
else{
return makeQuery(m.to.fields[0],m.from.sharedText);
}
})
.join("&");
// 検索条件が空の場合は全レコードを返すため、検索対象が見つからない場合は検索は行われません。
if (queryStr.length === 0) {
return {
records: [],
};
} else {
return await client.record.getRecords({
app: appId,
// query: undefined
query: queryStr,
});
}
};
const doCreate = async (
mappingData: Mapping[],
appId: string,
context: IContext,
lookupFixedFieldCodes: string[]
) => {
const filterHandler = (item:Mapping)=>{
if(!item.to.fields || item.to.fields.length===0){
return false;
}
if(item.from.objectType === "variable" && (item.from as IVar).name.name ){
return true;
}
if(item.from.objectType === "field" && (item.from as IFromField).code){
return true;
}
if(item.from.objectType === "text" && item.from.sharedText!==null){
return true;
}
return false;
}
const record = mappingData
.filter(filterHandler)
.filter((item) => !lookupFixedFieldCodes.includes(item.to.fields[0].code))
.reduce((accumulator, item) => {
return {
...accumulator,
[item.to.fields[0].code]: {
value: getFromValue(item,context),
},
};
}, {});
if (record && Object.keys(record).length > 0) {
console.log(record);
await client.record.addRecord({
app:appId,
record:record
});
// await kintone.api(kintone.api.url("/k/v1/record.json", true), "POST", {
// app: appId,
// record: record,
// });
}
};
const getLookupFixedFieldCodes = async (appId: string) => {
return await client.app
.getFormFields({ app: appId })
.then((resp) =>
Object.values(resp.properties)
.filter((f) => (f as Lookup).lookup !== undefined)
.flatMap((f) => (f as Lookup).lookup.fieldMappings.map((m) => m.field))
);
};

View File

@@ -0,0 +1,89 @@
import { actionAddins } from ".";
import { IAction, IActionResult, IActionNode, IActionProperty, IField ,IContext, IVarName} from "../types/ActionTypes";
/**
* アクションの属性定義
*/
interface IDateSpecifiedProps {
verNameGet:string;
newYear:number;
newMonth:number;
newDay:number;
verName:IVarName;
}
/**
* 日付指定アクション
*/
export class DateSpecifiedAction implements IAction {
name: string;
actionProps: IActionProperty[];
props: IDateSpecifiedProps;
constructor() {
this.name = "日付指定";
this.actionProps = [];
this.props = {
verNameGet:'',
newYear:0,
newMonth:0,
newDay:0,
verName:{name:''}
}
this.register();
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode: IActionNode, event: any,context:IContext): Promise<IActionResult> {
let result = {
canNext: true,
result: false
};
try {
//属性設定を取得する
this.actionProps = actionNode.actionProps;
if (!('verName' in actionNode.ActionValue) && !('verNameGet' in actionNode.ActionValue) ) {
return result
}
this.props = actionNode.ActionValue as IDateSpecifiedProps;
////////////////////////////////////////////////////////////////////////////////////////////////
//本番コード開始:
//取得変数の値を呼び出して代入する:
const getContextVarByPath = (obj: any, path: string) => {
return path.split(".").reduce((o, k) => (o || {})[k], obj);
};
let verNameGetValue = getContextVarByPath(context.variables,this.props.verNameGet);
////////////////////////////////////////////////////////////////////////////////////////////////
//取得変数の値Dateオブジェクトに変換
let dateObj = new Date(verNameGetValue);
if(verNameGetValue === undefined || verNameGetValue === null || verNameGetValue === '' || isNaN(dateObj.getDate())){
throw new Error("Invalid time value");
}
// 年の設定newYearが設定されていない場合は、元の値を使用
dateObj.setFullYear(this.props.newYear >=1900 && this.props.newYear <=9999 ? this.props.newYear : dateObj.getFullYear());
// 月の設定newMonthが設定されていない場合は、元の値を使用// 月は0始まりなので、12月は11。
dateObj.setMonth(this.props.newMonth >=1 && this.props.newMonth <=12 ? this.props.newMonth-1 : dateObj.getMonth());
// 日の設定newDayが設定されていない場合は、元の値を使用
dateObj.setDate(this.props.newDay >=1 && this.props.newDay <=31 ?this.props.newDay : dateObj.getDate());
// 変数に新しい値を設定
if(this.props.verName && this.props.verName.name!==''){
context.variables[this.props.verName.name]=dateObj.toISOString();
}
////////////////////////////////////////////////////////////////////////////////////////////////
result = {
canNext:true,
result:true
}
return result;
}catch(error){
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}
};
register(): void {
actionAddins[this.name] = this;
}
}
new DateSpecifiedAction();

View File

@@ -0,0 +1,202 @@
import { actionAddins } from ".";
import { IAction, IActionResult, IActionNode, IActionProperty, IField ,IContext, IVarName} from "../types/ActionTypes";
/**
* アクションの属性定義
*/
interface IDateTimeCalcProps{
verNameGet:string;
calcOption:string;
verName:IVarName;
year:string;
month:string;
date:string;
hour:string;
minute:string;
second:string;
}
/**
*
*/
export class DateTimeCalcAction implements IAction{
name: string;
actionProps: IActionProperty[];
props:IDateTimeCalcProps;
constructor(){
this.name="日時を加算/減算する";// DBに登録したアクション名
this.actionProps=[];
//プロパティ属性の初期化
this.props={
verNameGet:'',
calcOption:'',
verName:{name:''},
year:"0",
month:"0",
date:"0",
hour:"0",
minute:"0",
second:"0"
}
//アクションを登録する
this.register();
}
/**
* 基準日となる変数の値が、日付・日時の形式であるか、判断する
* @param {string} dateValue
* @returns {boolean}
*/
isDateValue(dateValue :string){
let date;
//正規表現チェック
let singleDigitMonth = dateValue.match(/(\d{4})-(\d{1})-(\d{1})$/);//4桁の数字-1桁の数字-2桁の数字
let twoDigitMonth = dateValue.match(/(\d{4})-(\d{2})-(\d{2})$/);//4桁の数字-2桁の数字-2桁の数字
let singleDigitDate = dateValue.match(/(\d{4})-(\d{2})-(\d{1})$/);//4桁の数字-2桁の数字-1桁の数字
let twoDigitDate = dateValue.match(/(\d{4})-(\d{1})-(\d{2})$/);//4桁の数字-1桁の数字-2桁の数字
let dateTimeMilliSecond = dateValue.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).(\d{2,3})Z$/);//時刻入りのUTCの日付形式(ミリ秒)
let dateTime = dateValue.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/);//時刻入りのUTCの日付形式
//date型に変換
date = new Date(dateValue);
//date型変換できたか確認
if(date !== undefined && !isNaN(date.getDate())){
//正規表現チェック確認
if(twoDigitMonth === null && singleDigitMonth === null && singleDigitDate === null && twoDigitDate === null && dateTime === null && dateTimeMilliSecond === null){
throw new Error("計算の基準日となる値が、適切な日付・日時の形式ではありません。「日時を加算/減算する」コンポーネントの処理を中断しました。");
}
}
return true;
}
/**
* 値を数値に変換する
* @param {any} context
* @param {string} calcValue,calcOption
* @returns {number}
*/
valueToNumber(context :any,calcValue :string,calcOption :string): number{
const getContextVarByPath = (obj: any, path: string) => {
return path.split(".").reduce((o, k) => (o || {})[k], obj);
};
//計算値が変数の場合は、変数の値を取得
if(calcOption === "変数" && isNaN(Number(calcValue))){
calcValue = getContextVarByPath (context.variables,calcValue);
}
//数値型に変換
let number = Number(calcValue);
//有限数かどうか判定
if(!isFinite(number)){
throw new Error("計算値が、数値ではありません。「日時を加算/減算する」コンポーネントの処理を中断しました。");
}
return number;
}
/**
* 日付・日時を加算・減算する
* @param {any} dateValue
* @param {number} year month day hour minute second
* @returns {string}
*/
calcDate(dateValue:any,year:number,month:number,date:number,hour:number,minute:number,second:number):string{
let calcResult;
//フィールドの値文字列をdate型に変換する
dateValue = new Date(dateValue);
// 年を計算
dateValue.setFullYear(dateValue.getFullYear()+year);
//月を計算
dateValue.setMonth(dateValue.getMonth()+month);
//日を計算
dateValue.setDate(dateValue.getDate()+date);
//時間を計算
dateValue.setHours(dateValue.getHours()+hour);
//分を計算
dateValue.setMinutes(dateValue.getMinutes()+minute);
//秒を計算
dateValue.setSeconds(dateValue.getSeconds()+second);
//UTC形式に変換
calcResult = dateValue.toISOString();
return calcResult;
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:true,
result:false
};
try{
//属性設定を取得する
this.actionProps = actionNode.actionProps;
this.props = actionNode.ActionValue as IDateTimeCalcProps;
const getContextVarByPath = (obj: any, path: string) => {
return path.split(".").reduce((o, k) => (o || {})[k], obj);
};
//基準日となる変数の値取得
const dateValue = getContextVarByPath (context.variables,this.props.verNameGet);
//基準日となる変数の値が空の場合、処理を終了する
if(!dateValue){
throw new Error("基準値となる変数の値が空、または存在しません。「日時を加算/減算する」コンポーネントの処理を中断しました。");
}
let checkDateValue;
//基準値となる変数の値、日時、日付形式か確認する
checkDateValue = this.isDateValue(dateValue);
if(checkDateValue){
//計算値の入力方法を取得する
let calcOptions = this.props.calcOption;
//計算値を数値型に変換する
let year = this.valueToNumber(context,this.props.year,calcOptions);
let month = this.valueToNumber(context,this.props.month,calcOptions);
let date = this.valueToNumber(context,this.props.date,calcOptions);
let hour = this.valueToNumber(context,this.props.hour,calcOptions);
let minute = this.valueToNumber(context,this.props.minute,calcOptions);
let second = this.valueToNumber(context,this.props.second,calcOptions);
//計算結果の日付を格納する変数
let calculatedDate;
//日付を加算、減算する
calculatedDate = this.calcDate(dateValue,year,month,date,hour,minute,second);
//計算結果を変数に代入する
context.variables[this.props.verName.name] = calculatedDate;
}
result= {
canNext:true,
result:true
}
return result;
}catch(error){
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}
}
register(): void {
actionAddins[this.name]=this;
}
}
new DateTimeCalcAction();

View File

@@ -52,8 +52,7 @@ export class DatetimeGetterAction implements IAction {
return result;
} catch (error) {
event.error = error;
console.error(error);
context.errors.handleError(error,actionNode);
result.canNext = false;
return result;
}

View File

@@ -0,0 +1,77 @@
import { actionAddins } from ".";
import { IAction, IActionResult, IActionNode, IActionProperty, IField ,IContext, IVarName} from "../types/ActionTypes";
/**
* アクションの属性定義
*/
interface IEndOfMonthProps {
verNameGet:string;
verName:IVarName;
}
/**
* 月末算出アクション
*/
export class EndOfMonthAction implements IAction {
name: string;
actionProps: IActionProperty[];
props: IEndOfMonthProps;
constructor() {
this.name = "月末算出";
this.actionProps = [];
this.props = {
verNameGet:'',
verName:{name:''}
}
this.register();
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode: IActionNode, event: any,context:IContext): Promise<IActionResult> {
let result = {
canNext: true,
result: false
};
try {
//属性設定を取得する
this.actionProps = actionNode.actionProps;
if (!('verName' in actionNode.ActionValue) && !('verNameGet' in actionNode.ActionValue) ) {
return result
}
this.props = actionNode.ActionValue as IEndOfMonthProps;
////////////////////////////////////////////////////////////////////////////////////////////////
//本番コード開始:
//取得変数の値を呼び出して代入する:
const getContextVarByPath = (obj: any, path: string) => {
return path.split(".").reduce((o, k) => (o || {})[k], obj);
};
let verNameGetValue = getContextVarByPath(context.variables,this.props.verNameGet);
////////////////////////////////////////////////////////////////////////////////////////////////
//取得変数の値Dateオブジェクトに変換
let dateObj = new Date(verNameGetValue);
// 月末を計算
let year = dateObj.getFullYear();
let month = dateObj.getMonth() + 1; //月は0から始まるため、1を足す
let lastDayOfMonth = new Date(year, month, 0); // 翌月の0日目は今月の月末
if(this.props.verName && this.props.verName.name!==''){
context.variables[this.props.verName.name]=lastDayOfMonth.toISOString();
}
////////////////////////////////////////////////////////////////////////////////////////////////
result = {
canNext:true,
result:true
}
return result;
}catch(error){
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}
};
register(): void {
actionAddins[this.name] = this;
}
}
new EndOfMonthAction();

View File

@@ -30,30 +30,26 @@ export class ErrorShowAction implements IAction {
* @param event
* @returns
*/
async process(actionNode: IActionNode, event: any, context: IContext): Promise<IActionResult> { //异步处理某函数下的xx属性
async process(actionNode: IActionNode, event: any, context: IContext): Promise<IActionResult> {
let result = {
canNext: true,
result: false
};
try { //尝试执行以下代码部分
try {
this.actionProps = actionNode.actionProps;
if (!('message' in actionNode.ActionValue) && !('condition' in actionNode.ActionValue)) { //如果message以及condition两者都不存在的情况下return
return result
}
this.props = actionNode.ActionValue as IErrorShowProps;
const conditionResult = this.getConditionResult(context);
console.log("条件結果:",conditionResult);
if (conditionResult) {
event.error = this.props.message;
} else {
result = {
canNext: false,
result: true
}
result.canNext=false;
}
return result;
} catch (error) {
event.error = error;
console.error(error);
context.errors.handleError(error,actionNode);
result.canNext = false;
return result;
}

View File

@@ -0,0 +1,112 @@
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes";
import { ConditionTree } from '../types/Conditions';
/**
* アクションの属性定義
*/
interface IDisableProps{
//対象フィールド
field:IField;
//編集可/不可設定
editable:'編集可'|'編集不可'|'';
condition:string;
}
/**
* 編集可/不可アクション
*/
export class FieldDisableAction implements IAction{
name: string;
actionProps: IActionProperty[];
props:IDisableProps;
constructor(){
this.name="編集可/不可";
this.actionProps=[];
this.props={
field:{code:''},
editable:'',
condition:''
}
//アクションを登録する
this.register();
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:true,
result:false
};
try{
//属性設定を取得する
this.actionProps=actionNode.actionProps;
if (!('field' in actionNode.ActionValue) && !('editable' in actionNode.ActionValue)) {
return result
}
this.props = actionNode.ActionValue as IDisableProps;
//条件式の計算結果を取得
const conditionResult = this.getConditionResult(context);
const record = event.record;
if(!(this.props.field.code in record)){
throw new Error(`フィールド「${this.props.field.code}」が見つかりません。`);
}
if(conditionResult){
if(this.props.editable==='編集可'){
record[this.props.field.code].disabled=false;
}else if (this.props.editable==='編集不可'){
record[this.props.field.code].disabled=true;
}
}
result= {
canNext:true,
result:true
}
return result;
}catch(error){
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}
}
/**
*
* @param context 条件式を実行する
* @returns
*/
getConditionResult(context:any):boolean{
const tree =this.getCondition(this.props.condition);
if(!tree){
//条件を設定されていません
return true;
}
return tree.evaluate(tree.root,context);
}
/**
* @param condition 条件式ツリーを取得する
* @returns
*/
getCondition(condition:string):ConditionTree|null{
try{
const tree = new ConditionTree();
tree.fromJson(condition);
if(tree.getConditions(tree.root).length>0){
return tree;
}else{
return null;
}
}catch(error){
return null;
}
}
register(): void {
actionAddins[this.name]=this;
}
}
new FieldDisableAction();

View File

@@ -61,8 +61,7 @@ export class FieldShownAction implements IAction{
}
return result;
}catch(error){
event.error=error;
console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}

View File

@@ -0,0 +1,321 @@
import { actionAddins } from ".";
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IField,
IContext,
IVarName,
} from "../types/ActionTypes";
import { ConditionTree } from "../types/Conditions";
/**
* アクションの属性定義
*/
interface IConversionProps {
field: IField;
conversion: string;
verName: IVarName;
}
const fullWidthToHalfWidthMap: { [key: string]: string } = {
: "ガ",
: "ギ",
: "グ",
: "ゲ",
: "ゴ",
: "ザ",
: "ジ",
: "ズ",
: "ゼ",
: "ゾ",
: "ダ",
: "ヂ",
: "ヅ",
: "デ",
: "ド",
: "バ",
: "ビ",
: "ブ",
: "ベ",
: "ボ",
: "パ",
: "ピ",
: "プ",
: "ペ",
: "ポ",
: "ヴ",
: "ヷ",
: "ヺ",
: "ア",
: "イ",
: "ウ",
: "エ",
: "オ",
: "カ",
: "キ",
: "ク",
: "ケ",
: "コ",
: "サ",
: "シ",
: "ス",
: "セ",
: "ソ",
: "タ",
: "チ",
: "ツ",
: "テ",
: "ト",
: "ナ",
: "ニ",
: "ヌ",
: "ネ",
: "ノ",
: "ハ",
: "ヒ",
: "フ",
: "ヘ",
: "ホ",
: "マ",
: "ミ",
: "ム",
: "メ",
: "モ",
: "ヤ",
: "ユ",
: "ヨ",
: "ラ",
: "リ",
: "ル",
: "レ",
: "ロ",
: "ワ",
: "ヲ",
: "ン",
: "ァ",
: "ィ",
: "ゥ",
: "ェ",
: "ォ",
: "ッ",
: "ャ",
: "ュ",
: "ョ",
"。": "。",
"、": "、",
: "ー",
"「": "「",
"」": "」",
"・": "・",
" ": " ", // 全角スペースも半角スペースに変換
};
const halfWidthToFullWidthMap: { [key: string]: string } = {
: "ガ",
: "ギ",
: "グ",
: "ゲ",
: "ゴ",
: "ザ",
: "ジ",
: "ズ",
: "ゼ",
ソ: "ゾ",
: "ダ",
: "ヂ",
: "ヅ",
: "デ",
: "ド",
: "バ",
: "ビ",
: "ブ",
: "ベ",
: "ボ",
: "パ",
: "ピ",
: "プ",
: "ペ",
: "ポ",
: "ヴ",
: "ヷ",
: "ヺ",
: "ア",
: "イ",
: "ウ",
: "エ",
: "オ",
: "カ",
: "キ",
: "ク",
: "ケ",
: "コ",
: "サ",
: "シ",
: "ス",
: "セ",
ソ: "ソ",
: "タ",
: "チ",
: "ツ",
: "テ",
: "ト",
: "ナ",
: "ニ",
: "ヌ",
: "ネ",
: "",
: "ハ",
: "ヒ",
: "フ",
: "ヘ",
: "ホ",
: "マ",
: "ミ",
: "ム",
: "メ",
: "モ",
: "ヤ",
: "ユ",
: "ヨ",
: "ラ",
: "リ",
: "ル",
: "レ",
: "ロ",
: "ワ",
: "ヲ",
: "ン",
: "ァ",
: "ィ",
: "ゥ",
: "ェ",
: "ォ",
: "ッ",
: "ャ",
: "ュ",
: "ョ",
"。": "。",
"、": "、",
: "ー",
"「": "「",
"」": "」",
"・": "・",
" ": " ",
};
/**
* 全角/半角変換アクション
*/
export class FullHalfConversionAction implements IAction {
name: string;
actionProps: IActionProperty[];
props: IConversionProps;
constructor() {
this.name = "全角/半角変換";
this.actionProps = [];
this.props = {
field: { code: "" },
conversion: "",
verName: { name: "" },
};
this.register();
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(
actionNode: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
let result = {
canNext: true,
result: false,
};
try {
//属性設定を取得する
this.actionProps = actionNode.actionProps;
if (
!("field" in actionNode.ActionValue) &&
!("conversion" in actionNode.ActionValue) &&
!("verName" in actionNode.ActionValue)
) {
return result;
}
this.props = actionNode.ActionValue as IConversionProps;
//条件式の計算結果を取得
const record = event.record;
if (!(this.props.field.code in record)) {
throw new Error(
`フィールド「${this.props.field.code}」が見つかりません。`
);
}
//
const value = record[this.props.field.code]?.value;
//条件分岐
//未入力時は何も処理をせず終了
if (value === undefined || value === "") {
record[this.props.field.code].error = null;
}
if (this.props.conversion === "全角") {
context.variables[this.props.verName.name] = this.toFullWidth(value);
}
if (this.props.conversion === "半角") {
context.variables[this.props.verName.name] = this.toHalfWidth(value);
} else {
record[this.props.field.code].error = null;
}
//resultプロパティ指定
result = {
canNext: true,
result: true,
};
return result;
//例外処理
} catch (error) {
context.errors.handleError(error, actionNode);
result.canNext = false;
return result;
}
}
// 半角から全角に変換
toFullWidth(str: string): string {
//半角の英数字と記号を検索し、半角から全角に変換(半角コードに 0xFEE0 を足して全角にする)
let retStr = str.replace(/[A-Za-z0-9!-~]/g, function (s) {
return String.fromCharCode(s.charCodeAt(0) + 0xfee0);
//半角カタカナなどを検索し、全角に変換するためのマッピングオブジェクトhalfWidthToFullWidthMapで全角文字に変換
});
retStr = retStr.replace(/[\uFF61-\uFF9F ]/g, function (s) {
return halfWidthToFullWidthMap[s] || s;
});
return retStr;
}
toHalfWidth(str: string): string {
//全角の英数字や記号を検索し、全角から半角に変換します(全角コードから 0xFEE0 を引いて半角にする)
let retStr = str.replace(/[-]/g, function (s) {
return String.fromCharCode(s.charCodeAt(0) - 0xfee0);
//全角片仮名記号スペースなどを検索し、半角に変換するためのマッピングオブジェクト( halfWidthToFullWidthMap )で半角文字に変換
});
retStr = retStr.replace(/[ガ-ヴァ-ン。、ー「」・ ]/g, function (s) {
return fullWidthToHalfWidthMap[s] || s;
});
return retStr;
}
register(): void {
actionAddins[this.name] = this;
}
}
new FullHalfConversionAction();

View File

@@ -1,2 +1,3 @@
import { IAction } from "../types/ActionTypes";
import './bootstrap.scss'
export const actionAddins :Record<string,IAction>={};

View File

@@ -1,5 +1,6 @@
import { each } from "jquery";
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes";
import { ConditionTree } from '../types/Conditions';
@@ -41,16 +42,20 @@ export class InsertValueAction implements IAction{
* @param {string} inputValue - 挿入する値
* @return {boolean} -入力値が有効な日付形式の場合はtrueを返し、そうでない場合は例外を発生させる
*/
checkInputBlank(fieldType :string | undefined,inputValue :string,fieldCode :string,fieldRequired :boolean | undefined,event :any): boolean{
//正規表現チェック
let blankCheck= inputValue.match(/^(\s| )+?$/);//半角スペース・タブ文字・改行・改ページ・全角スペース
checkInputValueBlank(fieldType :string | undefined,inputValue :string,fieldCode :string,fieldRequired :boolean | undefined,event :any): boolean{
if(blankCheck !== null){
let valueHasBlank;
//正規表現チェック
valueHasBlank = inputValue.match(/^(\s| )*$/);//値が半角スペース・タブ文字・改行・改ページ・全角スペースのみであるか
//値に空白文字が入っている、nullのときは、エラーチェックする
if(valueHasBlank !== null || inputValue === null || inputValue === ''){
//空白文字を空白文字が非対応のフィールドに挿入しようとしている場合、例外を発生させる
if(fieldType === "NUMBER" || fieldType === "DATE" || fieldType === "DATETIME" || fieldType === "TIME" || fieldType === "USER_SELECT"
|| fieldType === "ORGANIZATION_SELECT" || fieldType === "GROUP_SELECT" || fieldType === "RADIO_BUTTON" || fieldType === "DROP_DOWN" || fieldType === "CHECK_BOX" || fieldType === "MULTI_SELECT"){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドには、空白文字は挿入できません。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"フィールドには、空白文字は挿入できません。「値を挿入する」コンポーネントの処理を中断しました。");
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"に挿入しようとした、値は空白文字です。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"に挿入しようとした、値は空白文字です。「値を挿入する」コンポーネントの処理を中断しました。");
//空白文字を必須項目フィールドに挿入しようとしている場合、例外を発生させる
}else if(fieldRequired){
@@ -62,6 +67,40 @@ export class InsertValueAction implements IAction{
return true;
}
/**
* 空白文字の変数を非対応のフィールドに挿入しようとしていないか、必須項目フィールドに挿入しようとしていないかチェックする
* @param {string} inputValue - 挿入する値
* @return {boolean} -入力値が有効な日付形式の場合はtrueを返し、そうでない場合は例外を発生させる
*/
checkVariableValueBlank(fieldType :string | undefined,inputValueArray :any,fieldCode :string,fieldRequired :boolean | undefined,event :any): boolean{
let variableHasBlank;
//正規表現チェック
for(let i =0;i<inputValueArray.length;i++){
//配列の要素にnullがないか、空白文字が値に含まれていないかチェックする
if (typeof inputValueArray[i] === "string"){
variableHasBlank = inputValueArray[i].match(/^(\s| )*$/);//値が半角スペース・タブ文字・改行・改ページ・全角スペースのみであるか
}
}
//変数の値に空白文字が入っている、配列に要素がないときは、エラーチェックする
if(variableHasBlank !== null && variableHasBlank !== undefined && variableHasBlank !== "" && inputValueArray.length === 0){
//空白文字を空白文字が非対応のフィールドに挿入しようとしている場合、例外を発生させる
if(fieldType === "NUMBER" || fieldType === "DATE" || fieldType === "DATETIME" || fieldType === "TIME" || fieldType === "USER_SELECT"
|| fieldType === "ORGANIZATION_SELECT" || fieldType === "GROUP_SELECT" || fieldType === "RADIO_BUTTON" || fieldType === "DROP_DOWN" || fieldType === "CHECK_BOX" || fieldType === "MULTI_SELECT"){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"に挿入しようとした、変数の値は空白文字です。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"に挿入しようとした、変数の値は空白文字です。「値を挿入する」コンポーネントの処理を中断しました。");
//空白文字を必須項目フィールドに挿入しようとしている場合、例外を発生させる
}else if(fieldRequired){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドは必須項目であり、空白・空白文字の値の変数は、挿入できません。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"フィールドは必須項目であり、空白・空白文字の値の変数は、挿入できません。「値を挿入する」コンポーネントの処理を中断しました。");
}
}
return true;
}
/**
* 入力値が半角数字かチェックする関数
* @param {string} inputValue - 挿入する値
@@ -70,7 +109,7 @@ export class InsertValueAction implements IAction{
checkInputNumber(inputValue :string,fieldCode :string,event :any): boolean{
let inputNumberValue = Number(inputValue);//数値型に変換
//有限数かどうか判定s
//有限数かどうか判定
if(!isFinite(inputNumberValue)){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な日付形式です。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした値は、有効な数値ではありません。「値を挿入する」コンポーネントの処理を中断しました。");
@@ -88,7 +127,8 @@ export class InsertValueAction implements IAction{
let singleDigitMonthDay = inputValue.match(/(\d{4})-(\d{1})-(\d{1})$/);//4桁の数字-1桁の数字-2桁の数字
let singleDigitMonth = inputValue.match(/(\d{4})-(\d{1})-(\d{2})$/);//4桁の数字-1桁の数字-2桁の数字
let singleDigitDay = inputValue.match(/(\d{4})-(\d{2})-(\d{1})$/);//4桁の数字-2桁の数字-1桁の数字
let dateTime = inputValue.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).000Z/);//時刻入りのUTCの日付形式
let dateTimeMilliSecond = inputValue.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).(\d{2,3})Z$/);//時刻入りのUTCの日付形式(ミリ秒)
let dateTime = inputValue.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/);//時刻入りのUTCの日付形式
let date;
//date型に変換
date = new Date(inputValue);
@@ -96,7 +136,7 @@ export class InsertValueAction implements IAction{
//date型変換できたか確認
if(date !== undefined && !isNaN(date.getDate())){
//正規表現チェック確認
if(twoDigitMonthDay === null && singleDigitMonth === null && singleDigitDay === null && singleDigitMonthDay === null && dateTime === null){
if(twoDigitMonthDay === null && singleDigitMonth === null && singleDigitDay === null && singleDigitMonthDay === null && dateTime === null && dateTimeMilliSecond === null){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な日付形式です。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な日付形式です。「値を挿入する」コンポーネントの処理を中断しました。");
}
@@ -110,13 +150,13 @@ export class InsertValueAction implements IAction{
*/
checkInputTime(inputValue :string,fieldCode :string,event :any): boolean{
//正規表現チェック
let timeFormat =inputValue.match(/^([0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/);
let timeFormat =inputValue.match(/^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/);
//正規表現チェック確認
if(timeFormat === null){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な時刻形式です。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な時刻形式です。「値を挿入する」コンポーネントの処理を中断しました。");
}
//正規表現チェック確認
if(timeFormat === null){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な時刻形式です。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な時刻形式です。「値を挿入する」コンポーネントの処理を中断しました。「12桁 : 2桁」の値を指定してください。");
}
return true;
}
@@ -141,7 +181,26 @@ export class InsertValueAction implements IAction{
return dateTime;
}
//日付フィールドの場合、時刻なしの日付形式変換
//日付フィールドの場合、時刻なしの日付形式変換
//UTCの時刻を挿入したい場合、JSTに変換する
let dateTimeMilliSecond = inputValue.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).(\d{2,3})Z$/);//時刻入りのUTCの日付形式(ミリ秒)
let dateTimeNotIncludingMilliSeconds = inputValue.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/);//時刻入りのUTCの日付形式
if(dateTimeMilliSecond !== null || dateTimeNotIncludingMilliSeconds !== null){
//JSTに変換
let jstDate=date.toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" });
console.log(jstDate);
let dateArray=jstDate.match(/(\d{4})\/(\d{1,2})\/(\d{1,2})/);//4桁の数字-12桁の数字-12桁の数字
if(dateArray !== null){
let yearIndex = 1;
let monthIndex = 2;
let dayIndex = 3;
let dateFormatted=`${dateArray[yearIndex]}-${dateArray[monthIndex]}-${dateArray[dayIndex]}`
return dateFormatted;
}
}
//UTC時刻でない値を挿入したい場合、年、月、日を抽出し、月-年-日の形式変換
let dateArray=inputValue.match(/(\d{4})-(\d{1,2})-(\d{1,2})$/);//4桁の数字-12桁の数字-12桁の数字
if(dateArray !== null){
let yearIndex = 1;
@@ -183,19 +242,27 @@ export class InsertValueAction implements IAction{
* @param {string} inputValue - 挿入する値
* @return {string | boolean} 入力値が登録されているユーザー情報から見つかった場合、trueを返し、見つからなかった場合、falseを返す
*/
async setInputUser(inputValue :string): Promise<string | boolean>{
async setInputUser(inputValue :any): Promise<any | boolean>{
//ユーザー名を格納する変数
let usersName;
const usersInfoColumnIndex=0;
let usersName = [];
try{
//APIでユーザー情報を取得する
const resp =await kintone.api(kintone.api.url('/v1/users', true), 'GET', {codes:[inputValue ]})
const resp =await kintone.api(kintone.api.url('/v1/users', true), 'GET', {codes: inputValue.join(',')})
let usersInfo = resp.users;
//入力されたログイン名(メールアドレス)がユーザー情報に登録されている場合、そのユーザー名を取得する
if (resp.users[usersInfoColumnIndex].code === inputValue) {
usersName=resp.users[usersInfoColumnIndex].name;
if (usersInfo.length !== inputValue.length) {
throw new Error();
}
//入力されたログイン名がユーザー情報に登録されている場合、そのユーザー名を取得する
for (let indexUsersInfo in usersInfo) {
for(let indexInputUser in inputValue){
if(usersInfo[indexUsersInfo].code === inputValue[indexInputUser]){
usersName.push(usersInfo[indexUsersInfo].name);
}
}
}
//ユーザー名が取得できた場合、ログイン名とユーザー名をフィールドにセットする
@@ -205,7 +272,6 @@ export class InsertValueAction implements IAction{
}catch{
return false;
}
return usersName;
}
@@ -271,6 +337,70 @@ export class InsertValueAction implements IAction{
return groupsName;
}
/**
* ユーザーオブジェクトを挿入する場合、挿入先フィールドによって、適切なオブジェクトの値を取得し、セットする
* @param {string} inputValue -入力された値
* @param {string} objectValue -オブジェクト変数
* @param {string} fieldType -挿入先フィールドタイプ
* @param {string} fieldCode -挿入先フィールドタイプ
* @return {string} -挿入先フィールドによって、ログインユーザーオブジェクトの何の値を返すか、変わる
*/
setValueOfUserObject(inputValue :any,objectValue :any,fieldType : any,fieldCode : any): any{
//変数の値
let variableValue = [];
//ユーザーオブジェクト挿入できないフィールド(日付、数値)を選択している場合、エラーを出す
if(fieldType === "NUMBER" || fieldType === "DATE" || fieldType === "DATETIME" || fieldType === "TIME"){
throw new Error("「"+fieldCode+"」"+"フィールドには、ユーザー情報を挿入できません。処理を中断しました。");
}
//ユーザー選択フィールドに挿入時、ユーザーオブジェクトのcodeを代入する
if(fieldType === 'USER_SELECT'){
//変数の値取得
if(!Array.isArray(objectValue)){
variableValue.push(objectValue.code);
}else{
for(let i=0;i<objectValue.length;i++){
variableValue.push(objectValue[i].code);
}
}
}else{
//ユーザー選択フィールド以外に挿入時、ユーザーオブジェクトの値の指定があれば、その値を代入する
if(inputValue.includes('.name') || inputValue.includes('.email') || inputValue.includes('.employeeNumber')
|| inputValue.includes('.extensionNumber') || inputValue.includes('.id') || inputValue.includes('.isGuest')|| inputValue.includes('.timezone')
|| inputValue.includes('.language') || inputValue.includes('.mobilePhone') || inputValue.includes('.phone') || inputValue.includes('.url')){
//値を挿入する変数名取得
let objectPropertyName = inputValue.split('.')[1];
if(!Array.isArray(objectValue)){
variableValue.push(objectValue[objectPropertyName]);
}else{
for(let i=0;i<objectValue.length;i++){
variableValue.push(objectValue[i][objectPropertyName]);
}
}
//ユーザー選択フィールド以外に挿入時、ユーザーオブジェクトの値の指定がなければ、codeを代入する
}else{
if(!Array.isArray(objectValue)){
variableValue.push(objectValue.code);
}else{
for(let i=0;i<objectValue.length;i++){
variableValue.push(objectValue[i].code);
}
}
}
}
if(variableValue === undefined){
throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした変数は、無効な入力形式です。");
}
return variableValue;
}
/**
@@ -293,18 +423,9 @@ export class InsertValueAction implements IAction{
}
const fieldColumnIndex=1;
const valueColumnIndex=3;
//プロパティで選択されたフィールド
const field=this.actionProps[fieldColumnIndex].props.modelValue.type;
//プロパティの挿入する値
const value=this.actionProps[valueColumnIndex].props.modelValue;
//条件式の結果を取得
const conditionResult = this.getConditionResult(context);
if(!conditionResult){
return result;
}
//プロパティの値を挿入するフィールドが未選択の場合、例外を発生させる
if(field === null){
@@ -319,15 +440,10 @@ export class InsertValueAction implements IAction{
throw new Error("「値を挿入する」コンポーネントで、選択されたフィールドは、値を挿入するコンポーネントでは非対応のフィールドのため、処理を中断しました。");
}
//プロパティの挿入する値が未入力の場合、例外を発生させる
if(value === ""){
throw new Error("「値を挿入する」コンポーネントで、フィールドに挿入する値が指定されていなかったため、処理が中断されました。");
}
//既定のプロパティのインターフェースへ変換する
this.props = actionNode.ActionValue as IInsertValueProps;
//挿入する値を取得
//プロパティの挿入する値を取得
let fieldValue = this.props.value;
//フィールドの種類を取得
const fieldType = this.props.field.type;
@@ -340,42 +456,127 @@ export class InsertValueAction implements IAction{
//ラジオボタン・チェックボックス・複数選択・ドロップダウンの選択肢を取得
let fieldOptions =this.props.field.options;
//変数の値を格納する
let variableValue;
let variableValue :any;
//挿入する値を格納する
let fieldValueArray = [];
let notInputError;
//contextに存在する変数名を格納する
let contextHasVariablesNames;
//変数の場合、値が取得できるかチェック
if(insertValueType === "変数" && conditionResult){
variableValue = context.variables[fieldValue];
if(insertValueType === "変数"){
if(variableValue === undefined){
//変数の値を呼び出して代入する
const getContextVarByPath = (obj: any, path: string) => {
return path.split(".").reduce((o, k) => (o || {})[k], obj);
};
//contextに存在する変数名を全て取得する
contextHasVariablesNames = Object.keys(context.variables);
//contextに変数が1つも存在しない場合、エラーを出す
if(contextHasVariablesNames.length === 0){
throw new Error("「"+fieldCode+"」"+"フィールドに挿入しようとした変数は、存在しないため、処理を中断しました。");
}
let inputVariablesName;
//入力値がオブジェクト変数の変数名であり、プロパティの指定がある場合、変数名のみ取得
if (fieldValue.includes('.')) {
// '.'より前の文字を抽出
inputVariablesName = fieldValue.split('.')[0];
} else {
// '.'が含まれていない場合、そのまま返す
inputVariablesName = fieldValue;
}
let inputVariablesExist;
//入力された変数名が、contextの変数に存在する場合、inputVariablesExistにtrueに代入する
for(let i=0;i< contextHasVariablesNames.length;i++){
if(inputVariablesName === contextHasVariablesNames[i]){
inputVariablesExist = true;
}
}
//入力された変数名が、contextの変数に存在しない場合、エラーを表示する
if(!inputVariablesExist){
throw new Error("「"+fieldCode+"」"+"フィールドに挿入しようとした変数は、存在しないため、処理を中断しました。");
}
if(inputVariablesName){
//入力された変数名の値を取得
variableValue = getContextVarByPath(context.variables,inputVariablesName)
}
//文字列型のフィールド以外に、空白文字の変数の値を挿入する場合、エラーを出す
if(variableValue === "" || variableValue === null){
if(fieldType === "NUMBER" || fieldType === "DATE" || fieldType === "DATETIME" || fieldType === "TIME" || fieldType === "USER_SELECT"
|| fieldType === "ORGANIZATION_SELECT" || fieldType === "GROUP_SELECT" || fieldType === "RADIO_BUTTON" || fieldType === "DROP_DOWN" || fieldType === "CHECK_BOX" || fieldType === "MULTI_SELECT"){
throw new Error("「"+fieldCode+"」"+"フィールドに挿入しようとした変数は、値がnullのため、処理を中断しました。");
}
}
//変数がオブジェクトの場合、プロパティを取得し、何のオブジェクトか判断する
if(typeof variableValue === 'object'){
let objectProperties=[]
if(variableValue.length > 0){
objectProperties=Object.keys(variableValue[0]);
}else{
objectProperties = Object.keys(variableValue);
}
//(ログインユーザー・値取得のコンポーネントからの)ユーザーオブジェクトを挿入時、挿入先フィールドによって、値を指定する
if(objectProperties.includes('code') && objectProperties.includes('name')){
variableValue=this.setValueOfUserObject(fieldValue,variableValue,fieldType,fieldCode);
//ユーザーオブジェクトから取得した値を、fieldValueArrayに代入
for (const value of variableValue) {
fieldValueArray.push(value);
}
}
}else{
//オブジェクト変数でない場合、変数をfieldValueArrayに代入
variableValue = context.variables[fieldValue]
fieldValueArray[0] = variableValue;
}
//変数がfieldValueArrayに代入できなかった場合、エラーを出す
if(fieldValueArray === undefined){
throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした変数は、無効な入力形式です。");
}
fieldValue = variableValue;
//変数の値にエラー(空文字・空白文字の混入)がないことをチェックする
notInputError=this.checkVariableValueBlank(fieldType,fieldValueArray,fieldCode,fieldRequired,event);
//手入力の値を挿入する場合、挿入する値をfieldArrayに代入
}else{
fieldValueArray.push(fieldValue);
//入力エラー(空白文字の混入)がないことをチェック
notInputError=this.checkInputValueBlank(fieldType,fieldValue,fieldCode,fieldRequired,event);
}
//入力値チェック後、形式変換、型変換した値を格納する変数
let correctFormattedValue;
//入力値チェック後、形式変換、型変換した値を格納する配列
let correctValues :string[] = [];
//入力エラー(空白文字の混入)がないことをチェック
let notInputError=this.checkInputBlank(fieldType,fieldValue,fieldCode,fieldRequired,event);
//形式変換、型変換した値を格納する変数
let correctFormattedValue = undefined;
//形式変換、型変換した値を格納する配列
let correctValues :any[] = [];
//条件式の結果がtrue、入力エラー空白文字の混入がない場合、挿入する値をフィールドタイプ別にチェックする
if(conditionResult && notInputError){
if(notInputError){
//文字列型のフィールドに挿入しようとしている値が適切の場合、correctFormattedValueに代入する
if(fieldType === "SINGLE_LINE_TEXT" || fieldType === "MULTI_LINE_TEXT" || fieldType === "RICH_TEXT" || fieldType === "LINK" ){
correctFormattedValue = fieldValue;
correctFormattedValue = fieldValueArray.join(',');
//数値型のフィールドに挿入しようとしている値が適切の場合、数値型に型変換してcorrectFormattedValueに代入する
}else if(fieldType === "NUMBER" ){
if(this.checkInputNumber(fieldValue,fieldCode,event)){//入力値チェック
correctFormattedValue = Number(fieldValue);//型変換
if(this.checkInputNumber(fieldValueArray[0],fieldCode,event)){//入力値チェック
correctFormattedValue = Number(fieldValueArray[0]);//型変換
}
//日付・日時型のフィールドに挿入しようとしている値が適切の場合、指定の日付・日時に形式変換してcorrectFormattedValueに代入する
}else if(fieldType === "DATE" || fieldType === "DATETIME" ){
if(this.checkInputDate(fieldValue,fieldCode,event)){//入力値チェック
let formattedDate = this.changeDateFormat(fieldValue,fieldType,fieldCode,event)
if(this.checkInputDate(fieldValueArray[0],fieldCode,event)){//入力値チェック
let formattedDate = this.changeDateFormat(fieldValueArray[0],fieldType,fieldCode,event)
if(formattedDate){
correctFormattedValue = formattedDate
}
@@ -383,33 +584,35 @@ export class InsertValueAction implements IAction{
//時刻フィールドに挿入しようとしている値が適切の場合、correctFormattedValueに代入する
}else if(fieldType === "TIME"){
if(this.checkInputTime(fieldValue,fieldCode,event)){//入力値チェック
correctFormattedValue = fieldValue;
if(this.checkInputTime(fieldValueArray[0],fieldCode,event)){//入力値チェック
correctFormattedValue = fieldValueArray[0];
}
//ラジオボタン・ドロップダウンのフィールドの選択肢と入力値が一致した場合、correctFormattedValueに代入する
}else if(fieldType === "RADIO_BUTTON" || fieldType === "DROP_DOWN"){
if(this.checkInputOption(fieldValue,fieldOptions,fieldCode,event)){//入力値チェック
correctFormattedValue = fieldValue;
if(this.checkInputOption(fieldValueArray[0],fieldOptions,fieldCode,event)){//入力値チェック
correctFormattedValue = fieldValueArray[0];
}
//チェックボックス・複数選択のフィールドの選択肢と入力値が一致した場合、correctValuesの配列に代入する
}else if(fieldType === "CHECK_BOX" || fieldType === "MULTI_SELECT" ){
if(this.checkInputOption(fieldValue,fieldOptions,fieldCode,event)){//入力値チェック
correctValues[0] = fieldValue;
if(this.checkInputOption(fieldValueArray[0],fieldOptions,fieldCode,event)){//入力値チェック
correctValues[0] = fieldValueArray[0];
}
//ユーザー情報フィードに挿入しようとした値が適切な場合、correctFormattedValueに代入する
//ユーザー情報フィードに挿入しようとした値が適切な場合、correctValues(配列)に代入する
}else if(fieldType === "USER_SELECT"){
//挿入する値がユーザー情報から見つかれば、ユーザー名を格納
let users=await this.setInputUser(fieldValue);
let usersName=await this.setInputUser(fieldValueArray);
//ユーザー名が格納できている場合、ログイン名とユーザー名をcorrectFormattedValueに代入する
if(!users){
//ユーザー名が格納できている場合、ログイン名とユーザー名をcorrectValues配列に代入する
if(!usersName){
event.record[fieldCode]['error']="ユーザー選択に、挿入しようとしたユーザー情報は見つかりませんでした。「値を挿入する」コンポーネントの処理を中断しました。";
throw new Error("ユーザー選択に、挿入しようとしたユーザー情報は見つかりませんでした。「値を挿入する」コンポーネントの処理を中断しました。");
}else{
correctFormattedValue=[{ code: fieldValue, name: users }];
}
for(let indexInputUser in fieldValueArray){
correctValues.push({ code: fieldValueArray[indexInputUser], name: usersName[indexInputUser] });
}
//組織情報フィードに挿入しようとした値が適切な場合、correctFormattedValueに代入する
@@ -440,16 +643,64 @@ export class InsertValueAction implements IAction{
}
}
//条件式の結果がtrueかつ挿入する値が変換できた場合、フィールドラジオボタン・ドロップダウン・チェックボックス・複数選択・文字列一行・文字列複数行・リッチエディタ・数値・日付・日時・時刻にセット
if(conditionResult && (correctFormattedValue || correctValues)){
//条件式の結果がtureかつ、値を正しい形式に変換できた場合、フィールドに値をセットする
if(correctFormattedValue){
event.record[fieldCode].value = correctFormattedValue;
//条件式の結果がtureかつ、値を正しい形式配列に変換できた場合、フィールドに値配列をセットする
}else if(correctValues.length > 0){
event.record[fieldCode].value = correctValues;
const conditionResult = this.getConditionResult(context);
//保存成功イベントの場合、kintone async/await による非同期処理でフィールドに値を挿入する
if(!event.type.includes('success')){
//条件式の結果がtrueかつ挿入する値が変換できた場合、フィールドラジオボタン・ドロップダウン・チェックボックス・複数選択・文字列一行・文字列複数行・リッチエディタ・数値・日付・日時・時刻にセット
if(conditionResult){
//値を正しい形式に変換できた場合、フィールドに値をセットする
if(correctFormattedValue !== undefined){
event.record[fieldCode].value = correctFormattedValue;
//値を正しい形式(配列)に変換できた場合、フィールドに値(配列)をセットする
}else{
event.record[fieldCode].value = correctValues;
}
}
}
}else{
//挿入する値を挿入先フィールドに値をセットし、kintoneAPIによってレコード更新を行う
async function setUpdateData(conditionResult:boolean,fieldCode:string,event:any,correctFormattedValue :any,correctValues :any) {
//値を正しい形式に変換できた場合、フィールドに値をセットする
if(correctFormattedValue !== undefined){
event.record[fieldCode].value = correctFormattedValue;
//値を正しい形式(配列)に変換できた場合、フィールドに値(配列)をセットする
}else{
event.record[fieldCode].value = correctValues;
}
// 条件が真の場合の処理、kintoneAPIによる非同期処理レコード更新
if(conditionResult){
if(correctFormattedValue !== undefined){
await updateData(fieldCode,event,correctFormattedValue);
}else{
await updateData(fieldCode,event,correctValues);
}
}
}
//kintone async/await による非同期処理(レコード更新)
async function updateData(fieldCode:string,event:any,insertValue:any) {
try{
var updatedRecord = {
app: event.appId,
id: event.recordId,
record: {[fieldCode]:{"value":insertValue}}
};
//APIでユーザー情報を取得する
const resp =await kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', updatedRecord)
}catch{
return false;
}
}
// 関数の呼び出し
setUpdateData(conditionResult,fieldCode,event,correctFormattedValue,correctValues);
};
result= {
canNext:true,
@@ -457,13 +708,10 @@ export class InsertValueAction implements IAction{
}
return result;
}catch(error:any){
event.record;
event.error=error.message;
console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=true;//次のノードは処理を続ける
return result;
}
}
/**

View File

@@ -61,7 +61,7 @@ export class LoginUserGetterAction implements IAction{
return result;
//////////////////////////////////////////////////////////////////////////////////////
}catch(error:any){;
event.error=error.message;
context.errors.handleError(error,actionNode);
return {
canNext:false,
result:false

View File

@@ -1,5 +1,5 @@
import { actionAddins } from ".";
import { IAction, IActionResult, IActionNode, IActionProperty, IField } from "../types/ActionTypes";
import { IAction, IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes";
/**
* アクションの属性定義
*/
@@ -32,7 +32,7 @@ export class MailCheckAction implements IAction {
* @param event
* @returns
*/
async process(actionNode: IActionNode, event: any): Promise<IActionResult> {
async process(actionNode: IActionNode, event: any,context:IContext): Promise<IActionResult> {
let result = {
canNext: true,
result: false
@@ -46,18 +46,29 @@ export class MailCheckAction implements IAction {
return result
}
this.props = actionNode.ActionValue as IMailCheckProps;
//条件式の計算結果を取得
const record = event.record;
//対象フィールドの存在チェック
if(!(this.props.field.code in record)){
throw new Error(`フィールド[${this.props.field.code}]が見つかりませんでした。`);
}
const value = record[this.props.field.code].value;
if (this.props.emailCheck === '厳格') {
if (!/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(value)) {
if (!/^[a-zA-Z0-9_-¥.]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(value)) {
record[this.props.field.code].error = this.props.message;
}
else {
record[this.props.field.code].error = null;
result.result=true;
}
} else if (this.props.emailCheck === 'ゆるめ') {
if (!/^[^@]+@[^@]+$/.test(value)) {
record[this.props.field.code].error = this.props.message;
}
else {
record[this.props.field.code].error = null;
result.result=true;
}
} else {
result = {
canNext: true,
@@ -66,8 +77,7 @@ export class MailCheckAction implements IAction {
}
return result;
} catch (error) {
event.error = error;
console.error(error);
context.errors.handleError(error,actionNode);
result.canNext = false;
return result;
}

View File

@@ -1,6 +1,6 @@
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField } from "../types/ActionTypes";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes";
interface IMustInputProps{
field:IField;
@@ -22,25 +22,33 @@ export class MustInputAction implements IAction{
this.register();
}
async process(actionNode:IActionNode,event:any):Promise<IActionResult> {
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:true,
result:false
};
this.actionProps=actionNode.actionProps;
if (!('field' in actionNode.ActionValue) && !('message' in actionNode.ActionValue)) {
return result
}
this.props = actionNode.ActionValue as IMustInputProps;
const record = event.record;
const value = record[this.props.field.code]?.value;
if(value===undefined || value===''){
record[this.props.field.code].error=this.props.message;
return result;
}
result= {
canNext:true,
result:true
try{
this.actionProps=actionNode.actionProps;
if (!('field' in actionNode.ActionValue) && !('message' in actionNode.ActionValue)) {
return result
}
this.props = actionNode.ActionValue as IMustInputProps;
const record = event.record;
if(!(this.props.field.code in record)){
throw new Error(`フィールド[${this.props.field.code}]が見つかりませんでした。`);
}
const value = record[this.props.field.code].value;
if(value===undefined || value===''){
record[this.props.field.code].error=this.props.message;
return result;
}
result= {
canNext:true,
result:true
}
}catch(error){
context.errors.handleError(error,actionNode);
result.canNext=false;
}
return result;
}

View File

@@ -1,5 +1,5 @@
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField} from "../types/ActionTypes";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext} from "../types/ActionTypes";
/**
* アクションの属性定義
*/
@@ -32,7 +32,7 @@ export class RegularCheckAction implements IAction{
* @param event
* @returns
*/
async process(actionNode:IActionNode,event:any):Promise<IActionResult> {
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:true,
result:false
@@ -49,17 +49,18 @@ export class RegularCheckAction implements IAction{
const value = record[this.props.field.code].value;
const regex = new RegExp(this.props.regExpression);
if(!regex.test(value)){
record[this.props.field.code].error=this.props.message;
}else{
result= {
canNext:true,
result:true
}
record[this.props.field.code].error=this.props.message;
}
else {
record[this.props.field.code].error = null;
}
result= {
canNext:true,
result:true
}
return result;
}catch(error){
event.error=error;
console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}

View File

@@ -159,32 +159,32 @@ export class StringJoinAction implements IAction{
if (!event.type.includes('success')){
//保存先フィールドに値セット:
record[this.props.saveField.code].value=saveValue;
window.alert("文字結合行いました。"+this.props.joinField1.name+":"+joinValue1+","+this.props.joinField2.name+":"+joinValue2+"。");
//window.alert("文字結合行いました。"+this.props.joinField1.name+":"+joinValue1+","+this.props.joinField2.name+":"+joinValue2+"。");
}else{
const params={
"app":event.appId,
"id":event.recordId,
"record":{[this.props.saveField.code]:{"value":saveValue}}
};
return await kintone.api(kintone.api.url('/k/v1/record',true),'PUT',params).then((resp) => {
//kintone保存先フィールド存在確認
record[this.props.saveField.code].value=saveValue;
if (event.type.includes('index')){
window.alert("文字結合行いました。"+this.props.joinField1.name+":"+joinValue1+","+this.props.joinField2.name+":"+joinValue2+"。一覧画面更新成功後自動リロードしません。必要に応じて手動リロードください。");
}else{
window.alert("文字結合行いました。"+this.props.joinField1.name+":"+joinValue1+","+this.props.joinField2.name+":"+joinValue2+"。");
}
//一覧画面更新成功後リロード:
// if (event.type.includes('index')){
// event.url = location.href.endsWith('/') || location.href.endsWith('&') ?
// location.href.slice(0, -1) :
// location.href + (location.href.includes('?') ? '&' : '/');
// }
}).catch((error) => {
event.error = 'エラーが発生しました。結合しません。システム管理者へお問合せください';
window.alert(event.error+"error message"+error);
return event;
});
//kintone async/await による非同期処理(成功イベントREST API処理時)
async function updateRecord(fieldCode:string,event:any) {
return new Promise((resolve, reject) => {
var updatedRecord = {
app: event.appId,
id: event.recordId,
record: {[fieldCode]:{"value":saveValue}}
};
kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', updatedRecord, (resp) => {
resolve(resp);
}, (error) => {
reject(error);
});
});
}
//kintone保存先フィールド存在確認
record[this.props.saveField.code].value=saveValue;
//kintone async/await による非同期処理:
await updateRecord(this.props.saveField.code,event);
//一覧画面更新成功後手動リロードください:
if (event.type.includes('index')){
//window.alert("文字結合行いました。"+this.props.joinField1.name+":"+joinValue1+","+this.props.joinField2.name+":"+joinValue2+"。一覧画面更新成功後自動リロードしません。必要に応じて手動リロードください。");
window.alert("文字結合には、一覧画面更新成功後自動リロードしません。必要に応じて手動リロードください。");
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
result= {

View File

@@ -0,0 +1,238 @@
import { Record } from "@kintone/rest-api-client/lib/src/client/types";
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes";
import { ConditionTree } from '../types/Conditions';
type TextStyle = "太字" | "斜体" | "下線" | "打ち消し線";
type StyleRigion = "書式変更フィールド" | "行全体"|"";
/**
* アクションの属性定義
*/
interface IStyleFieldProps{
field:IField;
fontColor:string;
bgColor:string;
fontStyle:TextStyle[];
allRow:StyleRigion;
condition:string;
}
/**
* 条件書式表示アクション
*/
export class StyleFieldAction implements IAction{
name: string;
actionProps: IActionProperty[];
props:IStyleFieldProps;
constructor(){
this.name="条件書式表示";
this.actionProps=[];
this.props={
field:{code:''},
fontColor:'',
bgColor:'',
fontStyle:[],
allRow:'',
condition:''
}
//アクションを登録する
this.register();
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:true,
result:false
};
try{
//属性設定を取得する
this.actionProps=actionNode.actionProps;
if (!('field' in actionNode.ActionValue) && !('allRow' in actionNode.ActionValue)) {
return result
}
this.props = actionNode.ActionValue as IStyleFieldProps;
//書式設定
console.log(event.type)
if (event.type === "app.record.index.show") {
this.setStyleForView(event, this.props, context);
} else if (event.type === "app.record.detail.show") {
this.setStyleForDetail(event, this.props, context);
} else if (
event.type.includes("app.record.create.change.") ||
event.type.includes("app.record.edit.change.")
) {
this.setStyleForEdit(event, this.props, context);
}
result= {
canNext:true,
result:true
}
return result;
}catch(error){
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}
}
setStyleForEdit(event:any,props:IStyleFieldProps,context:IContext){
const recordTable = document.getElementById('record-gaia');
if (recordTable) {
Array.from(
recordTable.getElementsByTagName("span")
)
.filter((span) => span.innerText === props.field.code)
.map((span) =>
span.parentElement?.nextElementSibling
?.querySelector(".input-text-outer-cybozu")
?.firstElementChild as HTMLElement | null
)
.filter((inputElement): inputElement is HTMLElement => !!inputElement)
.forEach((inputElement) => {
const conditionResult = this.getConditionResult(
this.getCondition(props.condition),
context
);
if (conditionResult) {
this.setFieldStyle(props, inputElement);
} else {
inputElement.removeAttribute('style');
}
});
}
}
/**
* 詳細表示時のスタイル設定
* @param event
* @param props
* @param context
* @returns
*/
setStyleForDetail(event:any,props:IStyleFieldProps,context:IContext){
const elem = kintone.app.record.getFieldElement(props.field.code);
if(!elem){
return;
}
const tree = this.getCondition(props.condition);
const conditionResult = this.getConditionResult(tree,context);
if(conditionResult){
this.setFieldStyle(props,elem);
}
}
/**
* 一覧表示時の書式設定
* @param event
* @param props
* @param context
* @returns
*/
setStyleForView(event:any,props:IStyleFieldProps,context:IContext){
const records:Record[] = event.records;
const cells = kintone.app.getFieldElements(props.field.code);
if(!cells){
return;
}
let elem :HTMLElement|null;
const conditionTree = this.getCondition(props.condition);
records.forEach((record:Record,index:number) => {
const currContext:IContext = {
variables:context.variables,
record:record,
errors:context.errors
}
const conditionResult = this.getConditionResult(conditionTree,currContext);
if(conditionResult){
elem = cells[index];
if(props.allRow==="行全体"){
elem = cells[index].parentElement;
}
if(elem){
this.setFieldStyle(props,elem);
}
}
});
}
/**
*
* @param props HtmlElement書式設定
*/
setFieldStyle(props:IStyleFieldProps,elem:HTMLElement){
if(props.fontColor){
elem.style.color=props.fontColor;
}
if(props.bgColor){
elem.style.backgroundColor=props.bgColor;
}
if(props.fontStyle.length>0){
if(props.fontStyle.includes("斜体")){
elem.style.fontStyle="italic";
}
if(props.fontStyle.includes("太字")){
elem.style.fontWeight = "bold";
}
let textDecoration="";
if(props.fontStyle.includes("下線")){
textDecoration="underline";
}
if(props.fontStyle.includes("打ち消し線")){
textDecoration = textDecoration? textDecoration + " line-through":"line-through";
}
if(textDecoration){
elem.style.textDecoration=textDecoration;
}
}
}
/**
* 条件式を実行する
* @param tree 条件式オブジェクト
* @param context
* @returns
*/
getConditionResult(tree:ConditionTree|null, context:any):boolean{
if(!tree){
//条件を設定されていません
return true;
}
return tree.evaluate(tree.root,context);
}
/**
* @param condition 条件式ツリーを取得する
* @returns
*/
getCondition(condition:string):ConditionTree|null{
try{
const tree = new ConditionTree();
tree.fromJson(condition);
if(tree.getConditions(tree.root).length>0){
return tree;
}else{
return null;
}
}catch(error){
return null;
}
}
register(): void {
actionAddins[this.name]=this;
}
}
new StyleFieldAction();

View File

@@ -1,12 +1,10 @@
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes";
//クラス名を設計書に揃える
/**
* アクションの属性定義
*/
interface FullWidthProps{
//checkOption:Array<string>,
field:IField
}
/**
@@ -20,7 +18,6 @@ export class FullWidthAction implements IAction{
this.name="全角チェック"; /* pgadminのnameと同様 */
this.actionProps=[];
this.props={
//checkOption:[],
field:{code:''}
}
//アクションを登録する
@@ -46,19 +43,24 @@ export class FullWidthAction implements IAction{
this.props = actionNode.ActionValue as FullWidthProps;
//条件式の計算結果を取得
const record = event.record;
if(!(this.props.field.code in record)){
throw new Error(`フィールド「${this.props.field.code}」が見つかりません。`);
}
const value = record[this.props.field.code]?.value;
//条件分岐
//未入力時は何も処理をせず終了
if(value===undefined || value===''){
return result;
record[this.props.field.code].error=null;
}
//半角が含まれていた場合resultがfalse
if(!this.containsFullWidthChars(value)){
if(!this.containsFullWidthChars(value) && !(value === undefined || value ==='')){
//エラー時に出力される文字設定
record[this.props.field.code].error="半角が含まれています";
//次の処理を中止する値設定
result.canNext=false;
return result;
}
else{
record[this.props.field.code].error=null;
}
//resultプロパティ指定
result= {
@@ -69,8 +71,9 @@ export class FullWidthAction implements IAction{
//例外処理
}catch(error){
event.error=error;
console.error(error);
// event.error=error;
// console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}
@@ -79,6 +82,7 @@ export class FullWidthAction implements IAction{
containsFullWidthChars(text: string): boolean {
// 半角英数字カナ記号を除外
//全角かどうか
const checkRegex="^[^\x01-\x7E\uFF61-\uFF9F]+$";
//正規表現オブジェクト生成

View File

@@ -43,11 +43,14 @@ export class HalfWidthAction implements IAction{
this.props = actionNode.ActionValue as HalfWidthProps;
//条件式の計算結果を取得
const record = event.record;
if(!(this.props.field.code in record)){
throw new Error(`フィールド「${this.props.field.code}」が見つかりません。`);
}
const value = record[this.props.field.code]?.value;
//条件分岐
//未入力時は何も処理をせず終了
if(value===undefined || value===''){
return result;
record[this.props.field.code].error=null;
}
//全角が含まれていた場合保存処理中止(エラー処理)
if(!this.containsHalfWidthChars(value)){
@@ -55,7 +58,9 @@ export class HalfWidthAction implements IAction{
record[this.props.field.code].error="全角が含まれています";
//次の処理を中止する値設定
result.canNext=false;
return result;
}
else{
record[this.props.field.code].error=null;
}
//半角の場合問題なく実行
//resultプロパティ指定
@@ -66,8 +71,9 @@ export class HalfWidthAction implements IAction{
return result;
//例外処理
}catch(error){
event.error=error;
console.error(error);
// event.error=error;
// console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}

View File

@@ -56,8 +56,9 @@ export class GetValueAciton implements IAction{
return result;
}catch(error){
event.error=error;
console.error(error);
// event.error=error;
// console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=false;
return result;
}

View File

@@ -42,7 +42,12 @@ $(function (){
const flow=ActionFlow.fromJSON(flowinfo.content);
if(flow!==undefined){
const process = new ActionProcess(event.type,flow,event);
process.exec();
process.exec().then((res)=>{
const record = event.record;
kintone.app.record.set({record});
}).catch((err)=>{
console.error(err);
});
}
return event;
});
@@ -58,7 +63,7 @@ $(function (){
await process.exec();
}
const record = event.record;
kintone.app.record.set({record})
kintone.app.record.set({record});
});
});
}

View File

@@ -1,4 +1,3 @@
/**
* アプリ情報
*/
@@ -67,7 +66,8 @@ export interface IActionResult{
*/
export interface IContext{
record:any,
variables:any
variables:any,
errors:ErrorManager
}
/**
@@ -84,13 +84,13 @@ export interface IAction{
register():void;
}
export interface IField{
name?:string;
code:string;
type?:string;
required?:boolean;
options?:string;
label?: string;
}
//変数のインターフェース
export interface IVarName{
@@ -98,6 +98,14 @@ export interface IVarName{
fields?:IVarName[];
}
/**
* ユーザー、グループ、組織などオブジェクト類型
*/
export interface ICodeValue {
code: string;
name?: string;
}
/**
* アクションのプロパティ定義に基づいたクラス
*/
@@ -275,3 +283,46 @@ export class ActionFlow implements IActionFlow {
}
}
export interface IActionError{
action: IActionNode,
error:string
}
export class ErrorManager{
private errors:IActionError[]=[];
public get hasError():boolean{
return this.errors.length>0;
}
public handleError(err:any,action:IActionNode,defaultMessage?:string){
const defMsg = defaultMessage ? defaultMessage : '';
let error = err instanceof Error? `${defMsg} ${err.message}` : defMsg ;
console.error(`${action.name}」処理中例外発生しました。`,err);
this.errors.push({error,action});
}
public setEvent(event:any){
const messages = this.errors.map((err)=>{
return `${err.action.name}」処理中例外発生しました。\n詳細:${err.error}`;
});
event.error=messages.join('\n');
}
public showError(){
const msg =this.errors.map((err)=>{
return `${err.action.name}」処理中例外発生しました。\n詳細:${err.error}`;
});
window.alert(msg);
// const html=
// `<div id="myalert" class="notifier-cybozu notifier-error-cybozu" style="left: 50%; top: 0px;">
// <div class="notifier-header-cybozu"><div class="notifier-title-cybozu">エラー</div>
// <ul class="notifier-body-cybozu"><li>${msg}</li></ul></div>
// <a class="notifier-remove-cybozu" href="javascript:void(0)"></a></div>`
// const alert = $(html).appendTo("body");
// alert.children("#myalert .notifier-remove-cybozu").on("click",(ev)=>{
// $("myalert").slideUp().remove();
// });
}
}

View File

@@ -0,0 +1,353 @@
import {IField,IActionNode} from "../types/ActionTypes";
import { KintoneRestAPIClient } from "@kintone/rest-api-client";
import { getPageState } from "../util/url";
import $ from 'jquery';
import { Record as KTRecord } from "@kintone/rest-api-client/lib/src/client/types";
// 階層化ドロップダウンメニューの設定インターフェース
export interface ICascadingDropDown {
sourceApp: IApp;
dropDownApp: IApp;
fieldList: IFieldList[];
}
// アプリケーションインターフェース
export interface IApp {
name: string;
id: string;
}
// フィールドリストインターフェース
export interface IFieldList {
source: IField;
dropDown: IField;
}
// ドロップダウンメニューの辞書タイプ、ドロップダウンオプションの検索を高速化するために使用
type DropdownDictionary = Record<string, string[]>;
// ドロップダウンメニューの処理クラス
export class DropDownManager {
private dictionary: DropdownDictionary = {};
private state: Record<string, string> = {};
private selects: Map<string, HTMLElement> = new Map();
private columnMap: Map<HTMLElement, IFieldList> = new Map();
private columMapValueArray: IFieldList[] = [];
private columMapArray: [HTMLElement, IFieldList][] = [];
private props :ICascadingDropDown;
private event:Event;
constructor(props: ICascadingDropDown,event:any){
this.props=props;
this.event=event;
}
// 初期化メソッド
async init(): Promise<void> {
try {
const client = new KintoneRestAPIClient();
const sourceAppId = this.props.sourceApp.id;
const fields = this.props.fieldList.map((f) => f.source.code);
const records = await client.record.getAllRecords({
app: sourceAppId,
fields,
});
this.dictionary =this.buildDropdownDictionary(records, this.props.fieldList);
// const hash = await this.calculateHash(records, this.actionNode);
// const storageKey = `dropdown_dictionary::${this.props.dropDownApp.id}_${hash}`;
// const lsDictionary = this.getFromLocalStorage(storageKey);
// this.dictionary =
// lsDictionary || this.buildDropdownDictionary(records, this.props.fieldList);
// if (!lsDictionary) {
// this.saveToLocalStorage(storageKey, this.dictionary);
// }
} catch (error) {
console.error(
"階層化ドロップダウンの初期化中にエラーが発生しました:",
error
);
throw error;
}
}
// Web Crypto APIを使用してハッシュ値を計算
private async calculateHash(
records: KTRecord[],
actionNode: IActionNode
): Promise<string> {
const str = JSON.stringify(records) + JSON.stringify(actionNode);
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return hashHex;
}
/**
* ドロップダウンメニューの辞書を構築
* @param records データソースのレコード
* @param fieldList データソースの階層フィールド
* @returns
*/
private buildDropdownDictionary(records: KTRecord[],fieldList: IFieldList[]): DropdownDictionary {
const tempDictionary: Record<string, Set<string>> = { "0_TOP": new Set() };
const fieldCodeToIndexMap = new Map(
fieldList.map((field, index) => [field.source.code, index])
);
records
.map((record) =>
Object.entries(record)
.map(([fieldCode, fieldData]) => ({
fieldCode,
value: fieldData.value,
index: fieldCodeToIndexMap.get(fieldCode),
}))
.filter((item) => item.index !== undefined)
.sort((a, b) => a.index! - b.index!)
)
.forEach((recordArray) => {
recordArray.forEach((item, index, array) => {
const { value, fieldCode } = item;
if (!value) return;
const v = value as string;
if (index === 0) {
tempDictionary["0_TOP"].add(v);
} else {
const previousItem = array[index - 1];
const previousKey = `${previousItem.index}_${previousItem.value}`;
tempDictionary[previousKey] =
tempDictionary[previousKey] || new Set();
tempDictionary[previousKey].add(v);
}
});
});
const dictionary: DropdownDictionary = {};
for (const [key, set] of Object.entries(tempDictionary)) {
dictionary[key] = Array.from(set).sort();
}
return dictionary;
}
// ローカルストレージから辞書を取得
private getFromLocalStorage(key: string): DropdownDictionary | null {
const data = localStorage.getItem(key);
this.clearUpDictionary(key);
return data ? JSON.parse(data) : null;
}
// 古い辞書をクリア
private clearUpDictionary(key: string): void {
Object.keys(localStorage)
.filter((k) => k.startsWith("dropdown_dictionary::") && !k.endsWith(key))
.forEach((k) => localStorage.removeItem(k));
}
// ローカルストレージに辞書を保存
private saveToLocalStorage(key: string, data: DropdownDictionary): void {
localStorage.setItem(key, JSON.stringify(data));
}
// ページの状態を処理
async handlePageState(appId: string): Promise<void> {
const currentState = getPageState(
window.location.href.replace("#", "?"),
appId
);
switch (currentState.type) {
case "app":
case "edit":
case "show":
if (currentState.type === "show" && currentState.mode !== "edit") break;
await this.init();
this.setupCascadingDropDown(currentState.type);
break;
}
}
// 階層化ドロップダウンを設定する
setupCascadingDropDown(pageType: string): void {
const tableElement = document.getElementById(
pageType === "app" ? "view-list-data-gaia" : "record-gaia"
);
if (!tableElement) {
console.error("ルート要素が見つかりません");
return;
}
if(pageType==="app"){
this.initDropdownContainerForList();
}else{
this.initDropdownContainerforDetail(tableElement);
}
this.renderDropdownContainer();
}
/**
* ドロップダウンメニューコンテナを初期化(一覧編集画面)
* @param tableElement
*/
private initDropdownContainerForList(){
const fieldLists = this.props.fieldList;
fieldLists.forEach((fld,index,array)=>{
const elems = kintone.app.getFieldElements(fld.dropDown.code);
if(elems){
const editElem = $(elems).filter(".recordlist-editcell-gaia").get(0);
if(editElem!==undefined){
this.columnMap.set(editElem, fld);
}
}
});
this.columMapArray = Array.from(this.columnMap.entries());
this.columMapValueArray = Array.from(this.columnMap.values());
}
// ドロップダウンメニューコンテナを初期化(明細編集画面)
private initDropdownContainerforDetail(tableElement: HTMLElement): void {
const fieldList = this.props.fieldList;
let headerCells = $(tableElement).find(".control-gaia");
for (const field of fieldList) {
const cell = headerCells.has(`div.control-label-gaia span:contains("${field.dropDown.label}")`).get(0);
if(cell!==undefined){
const valueElem = $(cell).find(".control-value-gaia").get(0);
if(valueElem!==undefined){
this.columnMap.set(cell, field);
}
}
}
this.columMapArray = Array.from(this.columnMap.entries());
this.columMapValueArray = Array.from(this.columnMap.values());
}
// ドロップダウンメニューをレンダリング
private renderDropdownContainer(): void {
this.columnMap.forEach((field, cell) => {
// const cell = cells[columnIndex];
if (!cell) return;
const input = cell.querySelector<HTMLInputElement>("input");
if (!input) return;
this.createSelect(input, field.dropDown.code);
});
}
// ドロップダウンメニューを作成
private createSelect(input: HTMLInputElement, fieldCode: string): void {
const div = document.createElement("div");
div.className = "bs-scope";
div.style.margin = "0.12rem";
const select = document.createElement("select");
select.className = "custom-dropdown form-select";
select.dataset.field = fieldCode;
select.addEventListener("change", (event) =>{
this.selectorChangeHandle(fieldCode, event);
input.value=this.state[fieldCode];
});
div.appendChild(select);
this.updateOptions(fieldCode, select, input.value);
input.parentNode?.insertBefore(div, input.nextSibling);
input.style.display = "none";
this.selects.set(fieldCode, div);
}
// ドロップダウンメニューのオプションを更新
private updateOptions(
fieldCode: string,
initSelect?: HTMLSelectElement | undefined | null,
value?: string
): void {
let select = initSelect;
if (!initSelect) {
select = this.selects
.get(fieldCode)
?.querySelector<HTMLSelectElement>("select");
if (!select) {
console.error(
`フィールド ${fieldCode} のドロップダウンメニュー要素が見つかりません`
);
return;
}
} else {
this.state[fieldCode] = value!;
}
const field = this.props.fieldList.find((f) => f.dropDown.code === fieldCode);
if (!field) {
console.error(`フィールド ${fieldCode} の設定が見つかりません`);
throw new Error(`フィールド ${fieldCode} の設定が見つかりません`);
}
const level = this.getLevel(fieldCode);
const previousValue = this.getPreviousValueFromState(fieldCode);
const options = this.getOptions(level, previousValue);
if (!select) {
return;
}
select.innerHTML = '<option value="">選択してください</option>';
options.forEach((option) => {
const optionElement = document.createElement("option");
optionElement.value = option;
optionElement.textContent = option;
select?.appendChild(optionElement);
});
select.value = value ?? this.state[fieldCode] ?? "";
select.disabled = level > 0 && !previousValue;
}
// ドロップダウンメニューが変更された後にトリガーされる関数
private selectorChangeHandle=(fieldCode: string, event: Event)=> {
const select = event.target as HTMLSelectElement;
this.state[fieldCode] = select.value;
const currentLevel = this.getLevel(fieldCode);
this.columMapArray
.filter((_, arrayIndex) => arrayIndex > currentLevel)
.forEach(([_, field]) => {
const fieldCode = field.dropDown.code;
this.updateOptions(fieldCode);
const inputElem = this.selects.get(fieldCode)?.previousElementSibling as HTMLInputElement;
if(inputElem){
inputElem.value="";
}
delete this.state[fieldCode];
});
}
// フィールドのレベルを取得
private getLevel(fieldCode: string): number {
return this.columMapValueArray.findIndex(
(field) => field.dropDown.code === fieldCode
);
}
// 前のレベルのフィールドの値を取得
private getPreviousValueFromState(fieldCode: string): string {
const currentIndex = this.getLevel(fieldCode);
if (currentIndex <= 0) return "";
const previousField = this.columMapValueArray[currentIndex - 1];
return this.state[previousField.dropDown.code] ?? "";
}
// 指定された階層と値のドロップダウンメニューオプションを取得
private getOptions(level: number, value: string): string[] {
const key = level === 0 ? "0_TOP" : this.buildKey(level - 1, value);
return this.dictionary[key] || [];
}
// ドロップダウンメニューのキーを構築
private buildKey(level: number, value: string): string {
return `${level}_${value}`;
}
}

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