Compare commits

...

147 Commits

Author SHA1 Message Date
xue jiahao
2b804d7608 Add save version dialog 2024-11-22 15:10:43 +08:00
xiaozhe.ma
4563274789 backend bug fix 2024-11-20 15:09:45 +09:00
xue jiahao
3b9f08b43d Merged PR 6: [bugfix] id format error when saving flow
[bugfix] id format error when saving flow
2024-11-19 04:03:07 +00:00
xue jiahao
4c8cc1def9 [bugfix] id format error when saving flow 2024-11-19 11:25:55 +08:00
xue jiahao
7284f982a3 Merged PR 5: some fix for apps management page
1. 修改了 /apps 下的时间列格式
2. 修复了 /apps 下切换 domain 时更新 table
3. 修复了 /apps 下的 id 排序(使用数值,而非字符串字典序)
4. /flowChart 添加 id,从而在页面上支持刷新
5. /flowChart 添加了返回按钮

---

# 更新:
1. /flowChart 更新了面包屑导航
2. /flowChart 下禁止切换 domain

![image (7).png](https://dev.azure.com/alicorn-dev/96136197-fa1c-44c2-b522-b9ab8b541f34/_apis/git/repositories/11e363ac-4aa8-4076-9a9a-eaac160866ff/pullRequests/5/attachments/image%20%287%29.png)

Related work items: #63, #64
2024-11-19 01:00:14 +00:00
xue jiahao
ed27a18d25 [UI] fix loading behavious in /flowChart 2024-11-18 23:35:24 +08:00
xue jiahao
40074fb162 [UI] prevent change domain in /chartflow and loading 2024-11-18 23:25:45 +08:00
xue jiahao
96ec2a059e [UI] use breadcrumbs for flowchart 2024-11-18 23:25:35 +08:00
xue jiahao
d833ebb086 [feature] Add id field in FlowChart page 2024-11-18 18:47:25 +08:00
xue jiahao
f26ef1dd42 [feature] Update UI in flowchart and add return btn 2024-11-18 16:27:01 +08:00
xue jiahao
7ac722081e [bugfix] Improve App management page
1. reload apps when change domain
2. fix date format
3. fix order
2024-11-18 16:20:09 +08:00
xiaozhe.ma
fa120d2ce9 V2アプリ一覧バッグ修正 2024-11-17 18:41:40 +09:00
hsuehchiahao
1f0b05ee13 Merged PR 3: アプリ管理 page & some fix
![image.png](https://dev.azure.com/alicorn-dev/96136197-fa1c-44c2-b522-b9ab8b541f34/_apis/git/repositories/11e363ac-4aa8-4076-9a9a-eaac160866ff/pullRequests/3/attachments/image.png)

---

Another commit is some refactoring and bugfix:

![prev.gif](https://dev.azure.com/alicorn-dev/96136197-fa1c-44c2-b522-b9ab8b541f34/_apis/git/repositories/11e363ac-4aa8-4076-9a9a-eaac160866ff/pullRequests/3/attachments/prev.gif)
2024-11-11 11:20:36 +00:00
hsuehchiahao
c2c6dee8c5 Merged PR 4: show domain page for all user
show domain page for all user

1. show ドメイン管理
2. hide ドメイン適用

admin
![image.png](https://dev.azure.com/alicorn-dev/96136197-fa1c-44c2-b522-b9ab8b541f34/_apis/git/repositories/11e363ac-4aa8-4076-9a9a-eaac160866ff/pullRequests/4/attachments/image.png)

user
![image (2).png](https://dev.azure.com/alicorn-dev/96136197-fa1c-44c2-b522-b9ab8b541f34/_apis/git/repositories/11e363ac-4aa8-4076-9a9a-eaac160866ff/pullRequests/4/attachments/image%20%282%29.png)
2024-11-11 11:18:51 +00:00
xue jiahao
43ad0f5dd8 show domain page for all user
1. show ドメイン管理
2. hide ドメイン適用
2024-11-11 17:50:13 +08:00
xue jiahao
a3375c4526 some refactoring and make highlighter change when app changed 2024-11-11 15:26:52 +08:00
xue jiahao
1028327a37 add app management page 2024-11-11 15:26:52 +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
102 changed files with 9517 additions and 2171 deletions

4
.gitignore vendored
View File

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

File diff suppressed because one or more lines are too long

View File

@@ -156,10 +156,10 @@ def getsettingfromexcel(df):
des = df.iloc[2,2] des = df.iloc[2,2]
return {"name":appname,"description":des} return {"name":appname,"description":des}
def getsettingfromkintone(app:str,c:config.KINTONE_ENV): def getsettingfromkintone(app:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params = {"app":app} params = {"app":app}
url = f"{c.BASE_URL}{config.API_V1_STR}/app/settings.json" url = f"{env.BASE_URL}{config.API_V1_STR}/app/settings.json"
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
@@ -171,68 +171,69 @@ def analysesettings(excel,kintone):
updatesettings[key] = excel[key] updatesettings[key] = excel[key]
return updatesettings return updatesettings
def createkintoneapp(name:str,c:config.KINTONE_ENV): def createkintoneapp(name:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
data = {"name":name} data = {"name":name}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app.json" url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app.json"
r = httpx.post(url,headers=headers,data=json.dumps(data)) r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json() return r.json()
def updateappsettingstokintone(app:str,updates:dict,c:config.KINTONE_ENV): def updateappsettingstokintone(app:str,updates:dict,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/settings.json" url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/settings.json"
data = {"app":app} data = {"app":app}
data.update(updates) data.update(updates)
r = httpx.put(url,headers=headers,data=json.dumps(data)) r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json() return r.json()
def addfieldstokintone(app:str,fields:dict,c:config.KINTONE_ENV,revision:str = None): def addfieldstokintone(app:str,fields:dict,env:config.KINTONE_ENV,revision:str = None):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json" url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
if revision != None: if revision != None:
data = {"app":app,"revision":revision,"properties":fields} data = {"app":app,"revision":revision,"properties":fields}
else: else:
data = {"app":app,"properties":fields} data = {"app":app,"properties":fields}
r = httpx.post(url,headers=headers,data=json.dumps(data)) r = httpx.post(url,headers=headers,data=json.dumps(data))
r.raise_for_status()
return r.json() return r.json()
def updatefieldstokintone(app:str,revision:str,fields:dict,c:config.KINTONE_ENV): def updatefieldstokintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json" url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
data = {"app":app,"properties":fields} data = {"app":app,"properties":fields}
r = httpx.put(url,headers=headers,data=json.dumps(data)) r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json() return r.json()
def deletefieldsfromkintone(app:str,revision:str,fields:dict,c:config.KINTONE_ENV): def deletefieldsfromkintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json" url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
params = {"app":app,"revision":revision,"fields":fields} params = {"app":app,"revision":revision,"fields":fields}
#r = httpx.delete(url,headers=headers,content=json.dumps(params)) #r = httpx.delete(url,headers=headers,content=json.dumps(params))
r = httpx.request(method="DELETE",url=url,headers=headers,content=json.dumps(params)) r = httpx.request(method="DELETE",url=url,headers=headers,content=json.dumps(params))
return r.json() return r.json()
def deoployappfromkintone(app:str,revision:str,c:config.KINTONE_ENV): def deoployappfromkintone(app:str,revision:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json" url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json"
data = {"apps":[{"app":app,"revision":revision}],"revert": False} data = {"apps":[{"app":app,"revision":revision}],"revert": False}
r = httpx.post(url,headers=headers,data=json.dumps(data)) r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json return r.json
# 既定項目に含めるアプリのフィールドのみ取得する # 既定項目に含めるアプリのフィールドのみ取得する
# スペース、枠線、ラベルを含まない # スペース、枠線、ラベルを含まない
def getfieldsfromkintone(app:str,c:config.KINTONE_ENV): def getfieldsfromkintone(app:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params = {"app":app} params = {"app":app}
url = f"{c.BASE_URL}{config.API_V1_STR}/app/form/fields.json" url = f"{env.BASE_URL}{config.API_V1_STR}/app/form/fields.json"
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
# フォームに配置するフィールドのみ取得する # フォームに配置するフィールドのみ取得する
# スペース、枠線、ラベルも含める # スペース、枠線、ラベルも含める
def getformfromkintone(app:str,c:config.KINTONE_ENV): def getformfromkintone(app:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params = {"app":app} params = {"app":app}
url = f"{c.BASE_URL}{config.API_V1_STR}/form.json" url = f"{env.BASE_URL}{config.API_V1_STR}/form.json"
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
@@ -285,10 +286,10 @@ def analysefields(excel,kintone):
return {"update":updatefields,"add":addfields,"del":delfields} return {"update":updatefields,"add":addfields,"del":delfields}
def getprocessfromkintone(app:str,c:config.KINTONE_ENV): def getprocessfromkintone(app:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params = {"app":app} params = {"app":app}
url = f"{c.BASE_URL}{config.API_V1_STR}/app/status.json" url = f"{env.BASE_URL}{config.API_V1_STR}/app/status.json"
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
@@ -373,46 +374,86 @@ def getkintoneorgs(c:config.KINTONE_ENV):
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
def uploadkintonefiles(file,c:config.KINTONE_ENV): def uploadkintonefiles(file,env:config.KINTONE_ENV):
if (file.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"): if (file.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
return {'fileKey':file} return {'fileKey':file}
upload_files = {'file': open(file,'rb')} upload_files = {'file': open(file,'rb')}
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
data ={'name':'file','filename':os.path.basename(file)} data ={'name':'file','filename':os.path.basename(file)}
url = f"{c.BASE_URL}/k/v1/file.json" url = f"{env.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() return r.json()
def updateappjscss(app,uploads,c:config.KINTONE_ENV): def updateappjscss(app,uploads,env:config.KINTONE_ENV):
dsjs = [] dsjs = []
dscss = [] dscss = []
#mobile側
mbjs = []
mbcss = []
customize = getappcustomize(app, env)
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 upload in uploads:
for key in upload: for key in upload:
filename = os.path.basename(key)
if key.endswith('.js'): if key.endswith('.js'):
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"): existing_js = next((item for item in current_js
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL}) if item.get('type') == 'FILE' and item['file']['name'] == filename
else: ), 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]}}) 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'): 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} ds ={'js':dsjs,'css':dscss}
mb ={'js':[],'css':[]} mb ={'js':mbjs,'css':mbcss}
data = {'app':app,'scope':'ALL','desktop':ds,'mobile':mb} 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"} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/customize.json" url = f"{env.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)) r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json() return r.json()
#kintone カスタマイズ情報
def getappcustomize(app,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{env.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): def getTempPath(filename):
scriptdir = Path(__file__).resolve().parent scriptdir = Path(__file__).resolve().parent
rootdir = scriptdir.parent.parent.parent.parent rootdir = scriptdir.parent.parent.parent.parent
fpath = os.path.join(rootdir,"Temp",filename) fpath = os.path.join(rootdir,"Temp",filename)
return fpath return fpath
def createappjs(domainid,app): def createappjs(domain_url,app):
db = SessionLocal() db = SessionLocal()
flows = get_flows_by_app(db,domainid,app) flows = get_flows_by_app(db,domain_url,app)
db.close() db.close()
content={} content={}
for flow in flows: for flow in flows:
@@ -480,7 +521,7 @@ async def upload(request:Request,files:t.List[UploadFile] = File(...)):
return {"files": [file.filename for file in files]} return {"files": [file.filename for file in files]}
@r.post("/updatejscss") @r.post("/updatejscss")
async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env = Depends(getkintoneenv)): async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env:config.KINTONE_ENV = Depends(getkintoneenv)):
try: try:
jscs=[] jscs=[]
for file in files: for file in files:
@@ -501,35 +542,47 @@ async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env
raise APIException('kintone:updatejscss',request.url._url, f"Error occurred while update js/css {file.filename} is not an Excel file",e) raise APIException('kintone:updatejscss',request.url._url, f"Error occurred while update js/css {file.filename} is not an Excel file",e)
@r.get("/app") @r.get("/app")
async def app(request:Request,app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)): async def app(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
try: try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}{config.API_V1_STR}/app.json" url = f"{env.BASE_URL}{config.API_V1_STR}/app.json"
params ={"id":app} params ={"id":app}
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
except Exception as e: except Exception as e:
raise APIException('kintone:app',request.url._url, f"Error occurred while get app({c.DOMAIN_NAME}->{app}):",e) raise APIException('kintone:app',request.url._url, f"Error occurred while get app({env.DOMAIN_NAME}->{app}):",e)
@r.get("/allapps") @r.get("/allapps")
async def allapps(request:Request,c:config.KINTONE_ENV=Depends(getkintoneenv)): async def allapps(request:Request,env:config.KINTONE_ENV=Depends(getkintoneenv)):
try: try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}{config.API_V1_STR}/apps.json" url = f"{env.BASE_URL}{config.API_V1_STR}/apps.json"
r = httpx.get(url,headers=headers) offset = 0
return r.json() 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: 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({env.DOMAIN_NAME}):", e)
@r.get("/appfields") @r.get("/appfields")
async def appfields(request:Request,app:str,env = Depends(getkintoneenv)): async def appfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
try: try:
return getfieldsfromkintone(app,env) return getfieldsfromkintone(app,env)
except Exception as e: except Exception as e:
raise APIException('kintone:appfields',request.url._url, f"Error occurred while get app fileds({env.DOMAIN_NAME}->{app}):",e) raise APIException('kintone:appfields',request.url._url, f"Error occurred while get app fileds({env.DOMAIN_NAME}->{app}):",e)
@r.get("/allfields") @r.get("/allfields")
async def allfields(request:Request,app:str,env = Depends(getkintoneenv)): async def allfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
try: try:
field_resp = getfieldsfromkintone(app,env) field_resp = getfieldsfromkintone(app,env)
form_resp = getformfromkintone(app,env) form_resp = getformfromkintone(app,env)
@@ -538,38 +591,38 @@ async def allfields(request:Request,app:str,env = Depends(getkintoneenv)):
raise APIException('kintone:allfields',request.url._url, f"Error occurred while get form fileds({env.DOMAIN_NAME}->{app}):",e) raise APIException('kintone:allfields',request.url._url, f"Error occurred while get form fileds({env.DOMAIN_NAME}->{app}):",e)
@r.get("/appprocess") @r.get("/appprocess")
async def appprocess(request:Request,app:str,env = Depends(getkintoneenv)): async def appprocess(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
try: try:
return getprocessfromkintone(app,env) return getprocessfromkintone(app,env)
except Exception as e: except Exception as e:
raise APIException('kintone:appprocess',request.url._url, f"Error occurred while get app process({env.DOMAIN_NAME}->{app}):",e) raise APIException('kintone:appprocess',request.url._url, f"Error occurred while get app process({env.DOMAIN_NAME}->{app}):",e)
@r.get("/alljscss") @r.get("/alljscss")
async def alljscs(request:Request,app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)): async def alljscs(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
try: try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}{config.API_V1_STR}/app/customize.json" url = f"{env.BASE_URL}{config.API_V1_STR}/app/customize.json"
params = {"app":app} params = {"app":app}
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
except Exception as e: except Exception as e:
raise APIException('kintone:alljscss',request.url._url, f"Error occurred while get app js/css({c.DOMAIN_NAME}->{app}):",e) raise APIException('kintone:alljscss',request.url._url, f"Error occurred while get app js/css({env.DOMAIN_NAME}->{app}):",e)
@r.post("/createapp",) @r.post("/createapp",)
async def createapp(request:Request,name:str,c:config.KINTONE_ENV=Depends(getkintoneenv)): async def createapp(request:Request,name:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
try: try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
data = {"name":name} data = {"name":name}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app.json" url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app.json"
r = httpx.post(url,headers=headers,data=json.dumps(data)) r = httpx.post(url,headers=headers,data=json.dumps(data))
result = r.json() result = r.json()
if result.get("app") != None: if result.get("app") != None:
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json" url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json"
data = {"apps":[result],"revert": False} data = {"apps":[result],"revert": False}
r = httpx.post(url,headers=headers,data=json.dumps(data)) r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json return r.json
except Exception as e: except Exception as e:
raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({c.DOMAIN_NAME}->{name}):",e) raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({env.DOMAIN_NAME}->{name}):",e)
@r.post("/createappfromexcel",) @r.post("/createappfromexcel",)
@@ -708,12 +761,13 @@ async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Dep
try: try:
jscs=[] jscs=[]
files=[] files=[]
files.append(createappjs(env.DOMAIN_ID, app)) files.append(createappjs(env.BASE_URL, app))
files.append(getTempPath('alc_runtime.js')) files.append(getTempPath('alc_runtime.js'))
files.append(getTempPath('alc_runtime.css')) files.append(getTempPath('alc_runtime.css'))
for file in files: for file in files:
upload = uploadkintonefiles(file,env) upload = uploadkintonefiles(file,env)
if upload.get('fileKey') != None: if upload.get('fileKey') != None:
print(upload)
jscs.append({ file :upload['fileKey']}) jscs.append({ file :upload['fileKey']})
appjscs = updateappjscss(app,jscs,env) appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None: if appjscs.get("revision") != None:

View File

@@ -1,14 +1,74 @@
from fastapi import Request,Depends, APIRouter, UploadFile,HTTPException,File from http import HTTPStatus
from fastapi import Query, Request,Depends, APIRouter, UploadFile,HTTPException,File
from fastapi.responses import JSONResponse
# from app.core.operation import log_operation
from app.db import Base,engine from app.db import Base,engine
from app.db.session import get_db from app.db.session import get_db
from app.db.crud import * from app.db.crud import *
from app.db.schemas 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.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException from app.core.apiexception import APIException
import httpx
import app.core.config as config
platform_router = r = APIRouter() 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:
domain = get_activedomain(db, user.id)
platformapps = get_apps(db,domain.url)
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
kintone_apps_dict = {app['appId']: app for app in all_apps}
filtered_apps = []
for papp in platformapps:
if papp.appid in kintone_apps_dict:
papp.appname = kintone_apps_dict[papp.appid]["name"]
filtered_apps.append(papp)
return filtered_apps
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( @r.get(
"/appsettings/{id}", "/appsettings/{id}",
response_model=App, response_model=App,
@@ -129,7 +189,7 @@ async def flow_list(
try: try:
domain = get_activedomain(db, user.id) domain = get_activedomain(db, user.id)
print("domain=>",domain) print("domain=>",domain)
flows = get_flows_by_app(db, domain.id, appid) flows = get_flows_by_app(db, domain.url, appid)
return flows return flows
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
@@ -144,7 +204,7 @@ async def flow_create(
): ):
try: try:
domain = get_activedomain(db, user.id) domain = get_activedomain(db, user.id)
return create_flow(db, domain.id, flow) return create_flow(db, domain.url, flow)
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
@@ -154,11 +214,14 @@ async def flow_create(
) )
async def flow_edit( async def flow_edit(
request: Request, request: Request,
flowid: str,
flow: FlowBase, flow: FlowBase,
user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return edit_flow(db, flow) domain = get_activedomain(db, user.id)
return edit_flow(db,domain.url, flow)
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
@@ -196,10 +259,11 @@ async def domain_details(
async def domain_create( async def domain_create(
request: Request, request: Request,
domain: DomainBase, domain: DomainBase,
user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return create_domain(db, domain) return create_domain(db, domain,user.id)
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e) raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e)
@@ -233,16 +297,17 @@ async def domain_delete(
@r.get( @r.get(
"/domain", "/domain",
response_model=List[Domain], # response_model=List[Domain],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def userdomain_details( async def userdomain_details(
request: Request, request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user), user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domains = get_domain(db, user.id) domains = get_domain(db, userId if userId is not None else user.id)
return domains return domains
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while get user({user.id}) domain:",e) raise APIException('platform:domain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
@@ -254,7 +319,7 @@ async def userdomain_details(
async def create_userdomain( async def create_userdomain(
request: Request, request: Request,
userid: int, userid: int,
domainids:list, domainids:List[int] ,
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
@@ -285,11 +350,15 @@ async def userdomain_delete(
) )
async def get_useractivedomain( async def get_useractivedomain(
request: Request, request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user), user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: 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)
if domain is None:
return JSONResponse(content=None,status_code=HTTPStatus.OK)
return domain return domain
except Exception as e: except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while get user({user.id}) activedomain:",e) raise APIException('platform:activedomain',request.url._url,f"Error occurred while get user({user.id}) activedomain:",e)
@@ -301,11 +370,12 @@ async def get_useractivedomain(
async def update_activeuserdomain( async def update_activeuserdomain(
request: Request, request: Request,
domainid:int, domainid:int,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user), user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domain = active_userdomain(db, user.id,domainid) domain = active_userdomain(db, userId if userId is not None else user.id,domainid)
return domain return domain
except Exception as e: except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while update user({user.id}) activedomain:",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 from fastapi import HTTPException, status
import httpx
from app.db.schemas import ErrorCreate from app.db.schemas import ErrorCreate
from app.db.session import SessionLocal from app.db.session import SessionLocal
from app.db.crud import create_log from app.db.crud import create_log
class APIException(Exception): class APIException(Exception):
def __init__(self, location: str, title: str, content: str, e: Exception):
def __init__(self,location:str,title:str,content:str,e:Exception): self.detail = str(e)
if(str(e) == ''): self.status_code = 500
content += e.detail if isinstance(e,httpx.HTTPStatusError):
self.detail = e.detail try:
self.status_code = e.status_code error_response = e.response.json()
else: self.detail = error_response.get('message', self.detail)
self.detail = str(e) self.status_code = e.response.status_code
content += str(e) content += self.detail
self.status_code = 500 except ValueError:
if(len(content) > 5000): pass
content =content[0:5000] elif hasattr(e, 'detail'):
self.error = ErrorCreate(location=location,title=title,content=content) 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): def writedblog(exc: APIException):
db = SessionLocal() db = SessionLocal()

View File

@@ -1,11 +1,13 @@
import os import os
import base64 import base64
PROJECT_NAME = "KintoneAppBuilder" 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/dev"
SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/postgres" 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/unittest" #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_STR = "/k/v1"
API_V1_AUTH_KEY = "X-Cybozu-Authorization" API_V1_AUTH_KEY = "X-Cybozu-Authorization"
@@ -18,6 +20,8 @@ KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","U
KINTONE_FIELD_PROPERTY=['label','code','type','required','unique','maxValue','minValue','maxLength','minLength','defaultValue','defaultNowValue','options','expression','hideExpression','digit','protocol','displayScale','unit','unitPosition'] 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: class KINTONE_ENV:
BASE_URL = "" BASE_URL = ""
@@ -35,4 +39,4 @@ class KINTONE_ENV:
self.DOMAIN_ID=domain.id self.DOMAIN_ID=domain.id
self.BASE_URL = domain.url self.BASE_URL = domain.url
self.KINTONE_USER = domain.kintoneuser 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 fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext from passlib.context import CryptContext
from datetime import datetime, timedelta 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") 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}) to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt 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 import typing as t
from . import models, schemas 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): def get_user(db: Session, user_id: int):
@@ -69,6 +69,47 @@ def edit_user(
db.refresh(db_user) db.refresh(db_user)
return db_user return db_user
def get_apps(
db: Session,
domain_url:str
) -> t.List[schemas.AppList]:
return db.query(models.App).filter(models.App.domainurl == domain_url).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): def get_appsetting(db: Session, id: int):
app = db.query(models.AppSetting).get(id) app = db.query(models.AppSetting).get(id)
@@ -125,12 +166,12 @@ def get_actions(db: Session):
return actions 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( db_flow = models.Flow(
flowid=flow.flowid, flowid=flow.flowid,
appid=flow.appid, appid=flow.appid,
eventid=flow.eventid, eventid=flow.eventid,
domainid=domainid, domainurl=domainurl,
name=flow.name, name=flow.name,
content=flow.content content=flow.content
) )
@@ -149,15 +190,17 @@ def delete_flow(db: Session, flowid: str):
def edit_flow( def edit_flow(
db: Session, flow: schemas.FlowBase db: Session, domainurl: str, flow: schemas.FlowBase
) -> schemas.Flow: ) -> schemas.Flow:
db_flow = get_flow(db, flow.flowid) db_flow = get_flow(db, flow.flowid)
if not db_flow: if not db_flow:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Flow not found") #見つからない時新規作成
return create_flow(db,domainurl,flow)
update_data = flow.dict(exclude_unset=True) update_data = flow.dict(exclude_unset=True)
for key, value in update_data.items(): for key, value in update_data.items():
setattr(db_flow, key, value) setattr(db_flow, key, value)
db.add(db_flow) db.add(db_flow)
db.commit() db.commit()
@@ -173,17 +216,18 @@ def get_flows(db: Session, flowid: str):
def get_flow(db: Session, flowid: str): def get_flow(db: Session, flowid: str):
flow = db.query(models.Flow).filter(models.Flow.flowid == flowid).first() flow = db.query(models.Flow).filter(models.Flow.flowid == flowid).first()
if not flow: # if not flow:
raise HTTPException(status_code=404, detail="Data not found") # raise HTTPException(status_code=404, detail="Data not found")
return flow return flow
def get_flows_by_app(db: Session, domainid: int, appid: str): def get_flows_by_app(db: Session,domainurl: str, appid: str):
flows = db.query(models.Flow).filter(and_(models.Flow.domainid == domainid,models.Flow.appid == appid)).all() flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid == appid)).all()
if not flows: if not flows:
raise Exception("Data not found") raise Exception("Data not found")
return flows return flows
def create_domain(db: Session, domain: schemas.DomainBase): def create_domain(db: Session, domain: schemas.DomainBase,userid:int):
domain.encrypt_kintonepwd()
db_domain = models.Domain( db_domain = models.Domain(
tenantid = domain.tenantid, tenantid = domain.tenantid,
name=domain.name, name=domain.name,
@@ -192,6 +236,8 @@ def create_domain(db: Session, domain: schemas.DomainBase):
kintonepwd=domain.kintonepwd kintonepwd=domain.kintonepwd
) )
db.add(db_domain) db.add(db_domain)
db.flush()
add_userdomain(db,userid,db_domain.id)
db.commit() db.commit()
db.refresh(db_domain) db.refresh(db_domain)
return db_domain return db_domain
@@ -208,30 +254,32 @@ def delete_domain(db: Session,id: int):
def edit_domain( def edit_domain(
db: Session, domain: schemas.DomainBase db: Session, domain: schemas.DomainBase
) -> schemas.Domain: ) -> schemas.Domain:
domain.encrypt_kintonepwd()
db_domain = db.query(models.Domain).get(domain.id) db_domain = db.query(models.Domain).get(domain.id)
if not db_domain: if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found") raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
update_data = domain.dict(exclude_unset=True) update_data = domain.dict(exclude_unset=True)
for key, value in update_data.items(): for key, value in update_data.items():
if(key != "id"): if key != "id" and not (key == "kintonepwd" and (value is None or value == "")):
setattr(db_domain, key, value) setattr(db_domain, key, value)
print(str(db_domain))
db.add(db_domain) db.add(db_domain)
db.commit() db.commit()
db.refresh(db_domain) db.refresh(db_domain)
return db_domain return db_domain
def add_userdomain(db: Session, userid:int,domainids:list):
for domainid in domainids: def add_userdomain(db: Session, userid:int,domainid:int):
db_domain = models.UserDomain( user_domain = models.UserDomain(userid = userid, domainid = domainid )
userid = userid, db.add(user_domain)
domainid = domainid return user_domain
)
db.add(db_domain) def add_userdomains(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.commit()
db.refresh(db_domain) return dbCommits
return db_domain
def delete_userdomain(db: Session, userid: int,domainid: int): 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() db_domain = db.query(models.UserDomain).filter(and_(models.UserDomain.userid == userid,models.UserDomain.domainid == domainid)).first()
@@ -254,22 +302,35 @@ def active_userdomain(db: Session, userid: int,domainid: int):
db.commit() db.commit()
return db_userdomains return db_userdomains
def get_activedomain(db: Session, userid: int): def get_activedomain(db: Session, userid: int)-> t.Optional[models.Domain]:
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() user_domains = (db.query(models.Domain,models.UserDomain.active)
if not db_domain: .join(models.UserDomain,models.UserDomain.domainid == models.Domain.id )
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found") .filter(models.UserDomain.userid == userid)
.all())
db_domain=None
if len(user_domains)==1:
db_domain = user_domains[0][0];
else:
db_domain = next((domain for domain,active in user_domains if active),None)
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
return db_domain return db_domain
def get_domain(db: Session, userid: str): 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() domains = db.query(models.Domain).join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ).filter(models.UserDomain.userid == userid).all()
if not domains: # if not domains:
raise HTTPException(status_code=404, detail="Data not found") # raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
return domains return domains
def get_domains(db: Session,tenantid:str): def get_domains(db: Session,tenantid:str):
domains = db.query(models.Domain).filter(models.Domain.tenantid == tenantid ).all() domains = db.query(models.Domain).filter(models.Domain.tenantid == tenantid ).all()
if not domains: if not domains:
raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
return domains return domains
def get_events(db: Session): def get_events(db: Session):
@@ -278,9 +339,35 @@ def get_events(db: Session):
raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
return events return events
def get_category(db:Session):
categorys=db.query(models.Category).all()
return categorys
def get_eventactions(db: Session,eventid: str): 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 ).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: if not eveactions:
raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
return eveactions return eveactions

View File

@@ -1,7 +1,10 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime,ForeignKey from sqlalchemy import Boolean, Column, Integer, String, DateTime,ForeignKey
from sqlalchemy.ext.declarative import as_declarative from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
from app.core.security import chacha20Decrypt
@as_declarative() @as_declarative()
class Base: class Base:
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
@@ -18,6 +21,16 @@ class User(Base):
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False) 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): class AppSetting(Base):
__tablename__ = "appsetting" __tablename__ = "appsetting"
@@ -40,6 +53,8 @@ class Action(Base):
subtitle = Column(String(500)) subtitle = Column(String(500))
outputpoints = Column(String) outputpoints = Column(String)
property = Column(String) property = Column(String)
categoryid = Column(Integer,ForeignKey("category.id"))
nosort = Column(Integer)
class Flow(Base): class Flow(Base):
__tablename__ = "flow" __tablename__ = "flow"
@@ -47,10 +62,22 @@ class Flow(Base):
flowid = Column(String(100), index=True, nullable=False) flowid = Column(String(100), index=True, nullable=False)
appid = Column(String(100), index=True, nullable=False) appid = Column(String(100), index=True, nullable=False)
eventid = 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)) name = Column(String(200))
content = Column(String) 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): class Tenant(Base):
__tablename__ = "tenant" __tablename__ = "tenant"
@@ -68,6 +95,9 @@ class Domain(Base):
url = Column(String(200), nullable=False) url = Column(String(200), nullable=False)
kintoneuser = Column(String(100), nullable=False) kintoneuser = Column(String(100), nullable=False)
kintonepwd = 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): class UserDomain(Base):
@@ -90,7 +120,7 @@ class Event(Base):
class EventAction(Base): class EventAction(Base):
__tablename__ = "eventaction" __tablename__ = "eventaction"
eventid = Column(Integer,ForeignKey("event.id")) eventid = Column(String(100),ForeignKey("event.eventid"))
actionid = Column(Integer,ForeignKey("action.id")) actionid = Column(Integer,ForeignKey("action.id"))
@@ -101,6 +131,17 @@ class ErrorLog(Base):
location = Column(String(500)) location = Column(String(500))
content = Column(String(5000)) 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): class KintoneFormat(Base):
__tablename__ = "kintoneformat" __tablename__ = "kintoneformat"
@@ -110,4 +151,10 @@ class KintoneFormat(Base):
typecolumn =Column(Integer) typecolumn =Column(Integer)
codecolumn =Column(Integer) codecolumn =Column(Integer)
field = Column(String(5000)) 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 from datetime import datetime
import typing as t import typing as t
from app.core.security import chacha20Decrypt, chacha20Encrypt
class Base(BaseModel): class Base(BaseModel):
create_time: datetime create_time: datetime
@@ -27,21 +28,21 @@ class UserCreate(UserBase):
is_active:bool is_active:bool
is_superuser:bool is_superuser:bool
class Config: class ConfigDict:
orm_mode = True orm_mode = True
class UserEdit(UserBase): class UserEdit(UserBase):
password: t.Optional[str] = None password: t.Optional[str] = None
class Config: class ConfigDict:
orm_mode = True orm_mode = True
class User(UserBase): class User(UserBase):
id: int id: int
class Config: class ConfigDict:
orm_mode = True orm_mode = True
@@ -49,6 +50,17 @@ class Token(BaseModel):
access_token: str access_token: str
token_type: 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): class TokenData(BaseModel):
id:int = 0 id:int = 0
@@ -67,7 +79,7 @@ class AppBase(BaseModel):
class App(AppBase): class App(AppBase):
id: int id: int
class Config: class ConfigDict:
orm_mode = True orm_mode = True
@@ -78,7 +90,7 @@ class Kintone(BaseModel):
desc: str = None desc: str = None
content: str = None content: str = None
class Config: class ConfigDict:
orm_mode = True orm_mode = True
class Action(BaseModel): class Action(BaseModel):
@@ -88,8 +100,10 @@ class Action(BaseModel):
subtitle: str = None subtitle: str = None
outputpoints: str = None outputpoints: str = None
property: str = None property: str = None
categoryid: int = None
class Config: nosort: int
categoryname : str =None
class ConfigDict:
orm_mode = True orm_mode = True
class FlowBase(BaseModel): class FlowBase(BaseModel):
@@ -104,11 +118,11 @@ class Flow(Base):
flowid: str flowid: str
appid: str appid: str
eventid: str eventid: str
domainid: int domainurl: str
name: str = None name: str = None
content: str = None content: str = None
class Config: class ConfigDict:
orm_mode = True orm_mode = True
class DomainBase(BaseModel): class DomainBase(BaseModel):
@@ -119,6 +133,10 @@ class DomainBase(BaseModel):
kintoneuser: str kintoneuser: str
kintonepwd: str kintonepwd: str
def encrypt_kintonepwd(self):
encrypted_pwd = chacha20Encrypt(self.kintonepwd)
self.kintonepwd = encrypted_pwd
class Domain(Base): class Domain(Base):
id: int id: int
tenantid: str tenantid: str
@@ -126,8 +144,7 @@ class Domain(Base):
url: str url: str
kintoneuser: str kintoneuser: str
kintonepwd: str kintonepwd: str
class ConfigDict:
class Config:
orm_mode = True orm_mode = True
class Event(Base): class Event(Base):
@@ -139,7 +156,7 @@ class Event(Base):
mobile: bool mobile: bool
eventgroup: bool eventgroup: bool
class Config: class ConfigDict:
orm_mode = True orm_mode = True
class ErrorCreate(BaseModel): 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", "version": "0.2.0",
"description": "Kintoneアプリの自動生成とデプロイを支援ツールです", "description": "Kintoneアプリの自動生成とデプロイを支援ツールです",
"productName": "kintone Automate", "productName": "k-tune | kintoneジェネレーター",
"author": "maxiaozhe@alicorns.co.jp <maxiaozhe@alicorns.co.jp>", "author": "maxiaozhe@alicorns.co.jp <maxiaozhe@alicorns.co.jp>",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -12,14 +12,15 @@
"dev": "quasar dev", "dev": "quasar dev",
"dev:local": "set \"LOCAL=true\" && quasar dev", "dev:local": "set \"LOCAL=true\" && quasar dev",
"build": "set \"SOURCE_MAP=false\" && quasar build", "build": "set \"SOURCE_MAP=false\" && quasar build",
"build:dev":"set \"SOURCE_MAP=true\" && quasar build" "build:dev": "set \"SOURCE_MAP=true\" && quasar build"
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"jwt-decode": "^4.0.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vue": "^3.0.0", "vue": "^3.0.0",

View File

@@ -2,6 +2,7 @@ import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import {router} from 'src/router'; import {router} from 'src/router';
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$axios: AxiosInstance; $axios: AxiosInstance;
@@ -15,30 +16,10 @@ declare module '@vue/runtime-core' {
// good idea to move this instance creation inside of the // good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually // "export default () => {}" function below (which runs individually
// for each client) // for each client)
const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL }); 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 }) => { export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api // for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios; app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form) // ^ ^ ^ 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 // 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"> <div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" /> <q-spinner color="primary" size="3em" />
</div> </div>
<q-table v-else row-key="index" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" <q-splitter
class="action-table" v-model="splitterModel"
flat bordered style="height: 100%"
virtual-scroll before-class="tab"
:pagination="pagination" unit="px"
:rows-per-page-options="[0]" v-else
:filter="filter"
> >
</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> </div>
</template> </template>
<script> <script>
import { ref,onMounted,reactive } from 'vue' import { ref,onMounted,reactive,watchEffect,computed,watch } from 'vue'
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { useFlowEditorStore } from 'stores/flowEditor';
export default { export default {
name: 'actionSelect', name: 'actionSelect',
@@ -25,30 +51,74 @@ export default {
type: String, type: String,
filter:String filter:String
}, },
setup(props) { emits:[
"clearFilter"
],
setup(props,{emit}) {
const isLoaded=ref(false); const isLoaded=ref(false);
const columns = [ const columns = [
{ name: 'name', required: true,label: 'アクション名',align: 'left',field: 'name',sortable: true}, { name: 'name', required: true,label: 'アクション名',align: 'left',field: 'name',sortable: true},
{ name: 'desc', align: 'left', label: '説明', field: 'desc', sortable: true }, { name: 'desc', align: 'left', label: '説明', field: 'desc', sortable: true },
// { name: 'content', label: '内容', field: 'content', 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 () => { onMounted(async () => {
const res =await api.get('api/actions'); let eventId='';
res.data.forEach((item,index) => if(store.selectedEvent ){
{ eventId=store.selectedEvent.header!=='DELETABLE'? store.selectedEvent.eventId : store.selectedEvent.parentId;
rows.push({index,name:item.name,desc:item.title,outputPoints:item.outputpoints,property:item.property}); }
}); 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; 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 { return {
columns, columns,
rows,
selected: ref([]), selected: ref([]),
pagination:ref({ pagination:ref({
rowsPerPage:0 rowsPerPage:0
}), }),
isLoaded, isLoaded,
tab,
actionData,
categorys,
splitterModel: ref(150),
actionForTab
} }
}, },
@@ -58,5 +128,6 @@ export default {
.action-table{ .action-table{
min-height: 10vh; min-height: 10vh;
max-height: 68vh; max-height: 68vh;
min-width: 550px;
} }
</style> </style>

View File

@@ -21,7 +21,7 @@
</div> </div>
</template> </template>
</q-field> </q-field>
<q-field stack-label full-width label="アプリ説明"> <q-field stack-label full-width label="アプリ説明">
<template v-slot:control> <template v-slot:control>
<div class="self-center full-width no-outline" tabindex="0"> <div class="self-center full-width no-outline" tabindex="0">
{{ appinfo?.description }} {{ 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 tree = ref(props.conditionTree);
const closeDg = (val:string) => { const closeDg = (val:string) => {
if (val == 'OK') { if (val == 'OK') {
if(tree.value.root.children.length===0){ // if(tree.value.root.children.length===0){
$q.notify({ // $q.notify({
type: 'negative', // type: 'negative',
message: `条件式を設定してください。` // message: `条件式を設定してください。`
}); // });
} // }
context.emit("update:conditionTree",tree.value); context.emit("update:conditionTree",tree.value);
} }
showflg.value=false; showflg.value=false;

View File

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

View File

@@ -1,12 +1,15 @@
<template> <template>
<q-btn-dropdown <q-btn-dropdown
color="primay" class="customized-disabled-btn"
push push
flat flat
no-caps no-caps
icon="share" icon="share"
size="md" size="md"
:label="userStore.currentDomain.domainName" :label="userStore.currentDomain.domainName"
:disable-dropdown="isUnclickable"
:dropdown-icon="isUnclickable ? 'none' : ''"
:disable="isUnclickable"
> >
<q-list> <q-list>
<q-item v-for="domain in domains" :key="domain.domainName" <q-item v-for="domain in domains" :key="domain.domainName"
@@ -26,18 +29,32 @@
<script setup lang="ts" > <script setup lang="ts" >
import { IDomainInfo } from 'src/types/ActionTypes'; import { IDomainInfo } from 'src/types/ActionTypes';
import { useAuthStore,IUserState } from 'stores/useAuthStore'; import { useAuthStore,IUserState } from 'stores/useAuthStore';
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
const userStore = useAuthStore(); const userStore = useAuthStore();
const route = useRoute()
const domains = ref<IDomainInfo[]>([]); const domains = ref<IDomainInfo[]>([]);
(async ()=>{ (async ()=>{
domains.value = await userStore.getUserDomains(); domains.value = await userStore.getUserDomains();
})(); })();
const isUnclickable = computed(()=>{
return route.path.startsWith('/FlowChart/') || domains.value === undefined || domains.value.length === 0;
});
const onItemClick=(domain:IDomainInfo)=>{ const onItemClick=(domain:IDomainInfo)=>{
console.log(domain); console.log(domain);
userStore.setCurrentDomain(domain); userStore.setCurrentDomain(domain);
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.q-btn.disabled.customized-disabled-btn {
opacity: 1 !important;
cursor: default !important;
}
.q-btn.disabled.customized-disabled-btn * {
cursor: default !important;
}
</style> </style>

View File

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

View File

@@ -1,18 +1,24 @@
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<q-table flat bordered :loading="!isLoaded" row-key="name" :selection="type" :selected="modelValue" <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> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { useAsyncState } from '@vueuse/core'; import { useAsyncState } from '@vueuse/core';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { computed } from 'vue'; import { computed ,Prop,PropType,ref} from 'vue';
import {IField} from 'src/types/ComponentTypes';
export default { export default {
name: 'FieldList', name: 'FieldList',
props: { props: {
fields: Array, fields: Array as PropType<IField[]>,
name: String, name: String,
type: String, type: String,
appId: Number, appId: Number,
@@ -30,27 +36,32 @@ export default {
{ name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true }, { name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true } { name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true }
] ]
const { state : rows, isReady: isLoaded, isLoading } = useAsyncState((args) => { const { state : rows, isReady: isLoaded, isLoading } = useAsyncState((args) => {
if (props.fields && Object.keys(props.fields).length > 0) { 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 { } else {
return api.get('api/v1/appfields', { return api.get('api/v1/appfields', {
params: { params: {
app: props.appId app: props.appId
} }
}).then(res => { }).then(res => {
console.log(res); const fields = res.data.properties;
return Object.values(res.data.properties).map(f => ({ name: f.label, objectType: 'field', ...f })); return Object.values(fields).map((f:any) => ({ name: f.label, objectType: 'field', ...f }));
}); });
} }
}, [{ name: '', objectType: '', type: '', code: '', label: '' }]) }, [{ name: '', objectType: '', type: '', code: '', label: '' }])
return { return {
columns, columns,
rows, rows,
// selected: ref([]), // 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"> <div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" /> <q-spinner color="primary" size="3em" />
</div> </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;"/> :rows="rows" :pagination="pageSetting" :filter="filter" style="max-height: 55vh;"/>
</div> </div>
</template> </template>
@@ -26,7 +26,7 @@ export default {
default: false, default: false,
}, },
selectedFields:{ selectedFields:{
type:Array, type:Array ,
default:()=>[] default:()=>[]
}, },
fieldTypes:{ fieldTypes:{
@@ -37,6 +37,10 @@ export default {
updateSelectFields: { updateSelectFields: {
type: Function type: Function
}, },
blackListLabel: {
type:Array,
default:()=>[]
}
}, },
setup(props) { setup(props) {
const isLoaded = ref(false); const isLoaded = ref(false);
@@ -44,16 +48,16 @@ export default {
{ name: 'name', required: true, label: 'フィールド名', align: 'left', field: row => row.name, sortable: true }, { name: 'name', required: true, label: 'フィールド名', align: 'left', field: row => row.name, sortable: true },
{ name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true }, { name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true } { name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true }
] ];
const pageSetting = ref({ const pageSetting = ref({
sortBy: 'desc', sortBy: 'name',
descending: false, descending: false,
page: 1, page: 1,
rowsPerPage: props.not_page ? 0 : 5 rowsPerPage: props.not_page ? 0 : 25
// rowsNumber: xx if getting data from a server // rowsNumber: xx if getting data from a server
}); });
const rows = reactive([]); 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 () => { onMounted(async () => {
const url = props.fieldTypes.includes('SPACER')?'api/v1/allfields':'api/v1/appfields'; const url = props.fieldTypes.includes('SPACER')?'api/v1/allfields':'api/v1/appfields';
@@ -62,16 +66,25 @@ export default {
app: props.appId app: props.appId
} }
}); });
let fields = res.data.properties; let fields = Object.values(res.data.properties);
Object.keys(fields).forEach((key) => { for (const index in fields) {
const fld = fields[key]; const fld = fields[index]
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){ if(props.blackListLabel.length > 0){
rows.push({ name: fld.label || fld.code, ...fld }); if(!props.blackListLabel.find(blackListItem => blackListItem === fld.label)){
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){ if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
rows.push({ name: fld.label || fld.code, ...fld }); 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; isLoaded.value = true;
}); });

View File

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

@@ -0,0 +1,39 @@
<template>
<q-input
v-model="versionInfo.name"
filled
label="バージョン名"
:rules="[(val) => val.length <= 20 || '20字以内で入力ください']"
/>
<q-input
v-model="versionInfo.desc"
filled
type="textarea"
:rules="[(val) => val.length <= 80 || '80字以内で入力ください']"
label="説明"
/>
</template>
<script setup lang="ts">
import { ref, watch, defineProps, defineEmits } from 'vue';
import { QInput } from 'quasar';
import { IVersionInfo } from 'src/types/AppTypes';
const props = defineProps<{
modelValue: IVersionInfo;
}>();
const versionInfo = ref({
...props.modelValue,
name: props.modelValue.name || `新バージョン ${new Date().toLocaleString()}`,
});
const emit = defineEmits(['update:modelValue']);
watch(
versionInfo,
() => {
emit('update:modelValue', { ...versionInfo.value });
},
{ immediate: true, deep: true }
);
</script>

View File

@@ -7,18 +7,14 @@
<q-icon v-if="prop.node.eventId" name="play_circle" :color="prop.node.hasFlow ? 'green' : 'grey'" size="16px" <q-icon v-if="prop.node.eventId" name="play_circle" :color="prop.node.hasFlow ? 'green' : 'grey'" size="16px"
class="q-mr-sm"> class="q-mr-sm">
</q-icon> </q-icon>
<div class="no-wrap" <div class="no-wrap" :class="getSelectedClass(prop.node)">{{ prop.node.label }}</div>
:class="selectedEvent && prop.node.eventId === selectedEvent.eventId ? 'selected-node' : ''">{{
prop.node.label }}</div>
<q-space></q-space> <q-space></q-space>
<!-- <q-icon v-if="prop.node.hasFlow" name="delete" color="negative" size="16px" class="q-mr-sm"></q-icon> --> <!-- <q-icon v-if="prop.node.hasFlow" name="delete" color="negative" size="16px" class="q-mr-sm"></q-icon> -->
</div> </div>
</template> </template>
<template v-slot:header-CHANGE="prop"> <template v-slot:header-CHANGE="prop">
<div class="row col items-start no-wrap event-node"> <div class="row col items-start no-wrap event-node">
<div class="no-wrap" <div class="no-wrap" :class="getSelectedClass(prop.node)">{{ prop.node.label }}</div>
:class="selectedEvent && prop.node.eventId === selectedEvent.eventId ? 'selected-node' : ''"
>{{ prop.node.label }}</div>
<q-space></q-space> <q-space></q-space>
<q-icon name="add_circle" color="primary" size="16px" class="q-mr-sm" <q-icon name="add_circle" color="primary" size="16px" class="q-mr-sm"
@click="addChangeEvent(prop.node)"></q-icon> @click="addChangeEvent(prop.node)"></q-icon>
@@ -27,14 +23,14 @@
<template v-slot:header-DELETABLE="prop"> <template v-slot:header-DELETABLE="prop">
<div class="row col items-start event-node" @click="onSelected(prop.node)"> <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" /> <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="getSelectedClass(prop.node)">{{ prop.node.label }}</div>
<q-space></q-space> <q-space></q-space>
<q-icon name="delete_forever" color="negative" size="16px" @click="deleteEvent(prop.node)"></q-icon> <q-icon name="delete_forever" color="negative" size="16px" @click="deleteEvent(prop.node)"></q-icon>
</div> </div>
</template> </template>
</q-tree> </q-tree>
<show-dialog v-model:visible="showDialog" name="フィールド選択" @close="closeDg"> <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> </show-dialog>
</template> </template>
@@ -42,8 +38,8 @@
import { QTree, useQuasar } from 'quasar'; import { QTree, useQuasar } from 'quasar';
import { ActionFlow, RootAction } from 'src/types/ActionTypes'; import { ActionFlow, RootAction } from 'src/types/ActionTypes';
import { useFlowEditorStore } from 'stores/flowEditor'; import { useFlowEditorStore } from 'stores/flowEditor';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref, watchEffect } from 'vue';
import { IKintoneEvent, IKintoneEventGroup, IKintoneEventNode } from '../../types/KintoneEvents'; import { IKintoneEvent, IKintoneEventGroup, IKintoneEventNode, kintoneEvent } from '../../types/KintoneEvents';
import FieldSelect from '../FieldSelect.vue'; import FieldSelect from '../FieldSelect.vue';
import ShowDialog from '../ShowDialog.vue'; import ShowDialog from '../ShowDialog.vue';
export default defineComponent({ export default defineComponent({
@@ -58,15 +54,33 @@ export default defineComponent({
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const showDialog = ref(false); const showDialog = ref(false);
const tree = ref<QTree>(); 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 eventTree=ref(kintoneEvents);
// const selectedFlow = store.currentFlow; // const selectedFlow = store.currentFlow;
// const expanded=ref(); // const expanded=ref();
const selectedEvent = ref<IKintoneEvent | null>(null); const selectedEvent = ref<IKintoneEvent | undefined>(store.selectedEvent);
const selectedChangeEvent = ref<IKintoneEventGroup | null>(null); const selectedChangeEvent = ref<IKintoneEventGroup | undefined>(undefined);
const isFieldChange = (node: IKintoneEventNode) => { const isFieldChange = (node: IKintoneEventNode) => {
return node.header == 'EVENT' && node.eventId.indexOf(".change.") > -1; return node.header == 'EVENT' && node.eventId.indexOf(".change.") > -1;
} }
const getSelectedClass = (node: IKintoneEventNode) => {
return store.selectedEvent && node.eventId === store.selectedEvent.eventId ? 'selected-node' : '';
};
//フィールド値変更イベント追加 //フィールド値変更イベント追加
const closeDg = (val: string) => { const closeDg = (val: string) => {
if (val == 'OK') { if (val == 'OK') {
@@ -76,12 +90,12 @@ export default defineComponent({
if (store.eventTree.findEventById(eventid)) { if (store.eventTree.findEventById(eventid)) {
return; return;
} }
selectedChangeEvent.value?.events.push({ selectedChangeEvent.value?.events.push(new kintoneEvent(
eventId: eventid, field.name,
label: field.name, eventid,
parentId: selectedChangeEvent.value.eventId, selectedChangeEvent.value.eventId,
header: 'DELETABLE' 'DELETABLE'
}); ));
tree.value?.expanded?.push(selectedChangeEvent.value.eventId); tree.value?.expanded?.push(selectedChangeEvent.value.eventId);
tree.value?.expandAll(); tree.value?.expandAll();
} }
@@ -119,7 +133,7 @@ export default defineComponent({
const screen = store.eventTree.findEventById(node.parentId); const screen = store.eventTree.findEventById(node.parentId);
let flow = store.findFlowByEventId(node.eventId); let flow = store.findFlowByEventId(node.eventId);
let screenName = screen !== null ? screen.label : ""; let screenName = screen !== null ? screen.label : '';
let nodeLabel = node.label; let nodeLabel = node.label;
// if(isFieldChange(node)){ // if(isFieldChange(node)){
// screenName=nodeLabel; // screenName=nodeLabel;
@@ -136,6 +150,9 @@ export default defineComponent({
selectedEvent.value.flowData = flow; selectedEvent.value.flowData = flow;
} }
}; };
watchEffect(()=>{
store.setCurrentEvent(selectedEvent.value);
});
return { return {
// eventTree, // eventTree,
// expanded, // expanded,
@@ -143,12 +160,14 @@ export default defineComponent({
tree, tree,
showDialog, showDialog,
isFieldChange, isFieldChange,
getSelectedClass,
onSelected, onSelected,
selectedEvent, selectedEvent,
addChangeEvent, addChangeEvent,
deleteEvent, deleteEvent,
closeDg, closeDg,
store store,
fieldTypes
} }
} }
}); });

View File

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

View File

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

View File

@@ -23,9 +23,6 @@ import { ConditionNode, ConditionTree, Operator, OperatorListItem } from 'app/sr
import { computed, defineComponent, provide, reactive, ref, watchEffect } from 'vue'; import { computed, defineComponent, provide, reactive, ref, watchEffect } from 'vue';
import ConditionEditor from '../ConditionEditor/ConditionEditor.vue'; import ConditionEditor from '../ConditionEditor/ConditionEditor.vue';
import { IActionProperty } from 'src/types/ActionTypes'; import { IActionProperty } from 'src/types/ActionTypes';
import { } from 'src/types/';
export default defineComponent({ export default defineComponent({
name: 'FieldInput', name: 'FieldInput',
@@ -62,6 +59,10 @@ export default defineComponent({
type: String, type: String,
default: 'field' default: 'field'
}, },
connectProps:{
type:Object,
default:()=>({})
},
onlySourceSelect: { onlySourceSelect: {
type: Boolean, type: Boolean,
default: false default: false
@@ -90,7 +91,10 @@ export default defineComponent({
}, },
setup(props, { emit }) { 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 (source) {
if (props.sourceType === 'field') { if (props.sourceType === 'field') {
@@ -132,7 +136,8 @@ export default defineComponent({
const isSetted = ref(props.modelValue && props.modelValue !== ''); const isSetted = ref(props.modelValue && props.modelValue !== '');
const conditionString = computed(() => { const conditionString = computed(() => {
return tree.buildConditionString(tree.root); const condiStr= tree.buildConditionString(tree.root);
return condiStr==='()'?'(条件なし)':condiStr;
}); });
const showDg = () => { const showDg = () => {

View File

@@ -1,6 +1,10 @@
<template> <template>
<div> <div class="q-my-md" v-bind="$attrs">
<q-field :label="displayName" labelColor="primary" stack-label> <q-field :label="displayName" labelColor="primary" stack-label
v-model="mappingProps"
:rules="rulesExp"
ref="fieldRef"
>
<template v-slot:control> <template v-slot:control>
<q-card flat class="full-width"> <q-card flat class="full-width">
<q-card-actions vertical> <q-card-actions vertical>
@@ -17,7 +21,6 @@
</template> </template>
</q-field> </q-field>
<show-dialog v-model:visible="dgIsShow" name="データマッピング" @close="closeDg" min-width="55vw" min-height="60vh"> <show-dialog v-model:visible="dgIsShow" name="データマッピング" @close="closeDg" min-width="55vw" min-height="60vh">
<div class=""> <div class="">
<div class="row q-col-gutter-x-xs flex-center"> <div class="row q-col-gutter-x-xs flex-center">
<div class="col-5"> <div class="col-5">
@@ -36,7 +39,7 @@
キー キー
</div> </div>
</div> </div>
<q-virtual-scroll style="max-height: 60vh;" :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="q-my-sm" v-for="(item, index) in mappingProps" :key="item.id"> -->
<div class="row q-pa-sm q-col-gutter-x-md flex-center"> <div class="row q-pa-sm q-col-gutter-x-md flex-center">
<div class="col-5"> <div class="col-5">
@@ -74,19 +77,19 @@
</div> </div>
</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"> @close="closeToDg" ref="fieldDlg">
<FieldSelect v-if="onlySourceSelect" ref="fieldDlg" name="フィールド" :appId="sourceAppId" not_page <FieldSelect v-if="onlySourceSelect" ref="fieldDlg" name="フィールド" :appId="sourceAppId" not_page
:selectedFields="mappingProps[index].to.fields" :selectedFields="mappingProps.data[index].to.fields"
:updateSelects="(fields) => { mappingProps[index].to.fields = fields; mappingProps[index].to.app = sourceApp }"> :updateSelects="(fields) => { mappingProps.data[index].to.fields = fields; mappingProps.data[index].to.app = sourceApp }">
</FieldSelect> </FieldSelect>
<AppFieldSelectBox v-else v-model:selectedField="mappingProps[index].to" /> <AppFieldSelectBox v-else v-model:selectedField="mappingProps.data[index].to" />
</show-dialog> </show-dialog>
<!-- </div> --> <!-- </div> -->
</q-virtual-scroll> </q-virtual-scroll>
<div class="q-mt-lg q-ml-md row "> <div class="q-mt-lg q-ml-md row ">
<q-checkbox size="sm" v-model="createWithNull" label="キーが存在しない場合は新規に作成され、存在する場合はデータが更新されます。" /> <q-checkbox size="sm" v-model="mappingProps.createWithNull" label="キーが存在しない場合は新規に作成され、存在する場合はデータが更新されます。" />
</div> </div>
</div> </div>
</show-dialog> </show-dialog>
@@ -115,12 +118,12 @@ type ContextProps = {
} }
}; };
type CurrentModelValueType = { interface IMappingSetting {
data: MappingValueType[]; data: IMappingValueType[];
createWithNull: boolean; createWithNull: boolean;
} }
type MappingValueType = { interface IMappingValueType {
id: string; id: string;
from: { sharedText?: string }; from: { sharedText?: string };
to: { to: {
@@ -157,7 +160,7 @@ export default defineComponent({
default: '', default: '',
}, },
modelValue: { modelValue: {
type: Object as () => CurrentModelValueType, type: Object as () => IMappingSetting,
}, },
placeholder: { placeholder: {
type: String, type: String,
@@ -171,27 +174,60 @@ export default defineComponent({
type:Array, type:Array,
default:()=>[] default:()=>[]
}, },
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
},
requiredMessage: {
type: String,
default: ''
}
}, },
setup(props, { emit }) { setup(props, { emit }) {
const fieldRef=ref();
const source = props.context.find(element => element?.props?.name === 'sources') const source = props.context.find(element => element?.props?.name === 'sources')
const sourceApp = computed(() => source?.props?.modelValue?.app); const sourceApp = computed(() => source?.props?.modelValue?.app);
const sourceAppId = computed(() => sourceApp.value?.id); 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 = () => { const closeDg = () => {
emit('update:modelValue', { data: mappingProps.value, createWithNull: createWithNull.value }); fieldRef.value.validate();
emit('update:modelValue',mappingProps);
} }
const closeToDg = () => { const closeToDg = () => {
emit('update:modelValue', { data: mappingProps.value, createWithNull: createWithNull.value }); emit('update:modelValue',mappingProps);
} }
const mappingProps = ref(props.modelValue?.data ?? []);
const createWithNull = ref(props.modelValue?.createWithNull ?? false)
// 外部ソースコンポーネントの appid をリッスンし、変更されたときに現在のコンポーネントを更新します // 外部ソースコンポーネントの appid をリッスンし、変更されたときに現在のコンポーネントを更新します
watch(() => sourceAppId.value, async (newId,) => { watch(() => sourceAppId.value, async (newId,) => {
if (!newId) return; if (!newId) return;
@@ -210,7 +246,7 @@ export default defineComponent({
.map(f => ({ name: f.label, objectType: 'field', ...f })) .map(f => ({ name: f.label, objectType: 'field', ...f }))
.map(f => { .map(f => {
// 更新前の値を求める // 更新前の値を求める
const beforeData = mappingProps.value.find(m => m.to.fields[0].code === f.code) const beforeData = mappingProps.data.find(m => m.to.fields[0].code === f.code)
return { return {
id: uuidv4(), id: uuidv4(),
from: beforeData?.from ?? {}, // 以前のデータを入力します from: beforeData?.from ?? {}, // 以前のデータを入力します
@@ -235,12 +271,12 @@ export default defineComponent({
ktAppFields.filter(f => lookupFixedField.includes(f.to.fields[0].code)).forEach(f => f.disabled = true) ktAppFields.filter(f => lookupFixedField.includes(f.to.fields[0].code)).forEach(f => f.disabled = true)
} }
mappingProps.value = ktAppFields mappingProps.data = ktAppFields
} }
const mappingObjectsInputDisplay = computed(() => const mappingObjectsInputDisplay = computed(() =>
(mappingProps.value && Array.isArray(mappingProps.value)) ? (mappingProps.data && Array.isArray(mappingProps.data)) ?
mappingProps.value mappingProps.data
.filter(item => item.from?.sharedText && item.to.fields?.length > 0) .filter(item => item.from?.sharedText && item.to.fields?.length > 0)
.map(item => { .map(item => {
return `field(${item.to.app?.id},${item.to.fields[0].label}) = ${item.from.sharedText} `; return `field(${item.to.app?.id},${item.to.fields[0].label}) = ${item.from.sharedText} `;
@@ -251,17 +287,17 @@ export default defineComponent({
const btnDisable = computed(() => props.onlySourceSelect ? !(source?.props?.modelValue?.app?.id) : false); const btnDisable = computed(() => props.onlySourceSelect ? !(source?.props?.modelValue?.app?.id) : false);
watchEffect(() => { watchEffect(() => {
emit('update:modelValue', { data: mappingProps.value, createWithNull: createWithNull.value }); emit('update:modelValue', mappingProps);
}); });
return { return {
uuidv4, uuidv4,
dgIsShow: ref(false), dgIsShow: ref(false),
fieldRef,
closeDg, closeDg,
toDgIsShow: ref(false), toDgIsShow: ref(false),
closeToDg, closeToDg,
mappingProps, mappingProps,
createWithNull,
updateFields, updateFields,
// addMappingObject: () => mappingProps.push(defaultMappingProp()), // addMappingObject: () => mappingProps.push(defaultMappingProp()),
// deleteMappingObject, // deleteMappingObject,
@@ -269,6 +305,8 @@ export default defineComponent({
sourceApp, sourceApp,
sourceAppId, sourceAppId,
btnDisable, btnDisable,
rulesExp,
checkMapping,
config: { config: {
canInput: false, canInput: false,
buttonsConfig: [ buttonsConfig: [

View File

@@ -1,6 +1,11 @@
<template> <template>
<div> <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> <template v-slot:control>
<q-card flat class="full-width"> <q-card flat class="full-width">
<q-card-actions vertical> <q-card-actions vertical>
@@ -120,9 +125,19 @@ export default defineComponent({
type: String, type: String,
default: '', default: '',
}, },
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
}
}, },
setup(props, { emit }) { setup(props, { emit }) {
const fieldRef=ref();
const source = props.context.find(element => element?.props?.name === 'sources') const source = props.context.find(element => element?.props?.name === 'sources')
if (source) { if (source) {
@@ -155,6 +170,7 @@ export default defineComponent({
}); });
const closeDg = () => { const closeDg = () => {
fieldRef.value.validate();
emit('update:modelValue', processingProps); emit('update:modelValue', processingProps);
} }
@@ -218,6 +234,24 @@ export default defineComponent({
"label": "最初の値" "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(() => { watchEffect(() => {
emit('update:modelValue', processingProps); emit('update:modelValue', processingProps);
@@ -232,6 +266,8 @@ export default defineComponent({
deleteProcessingObject, deleteProcessingObject,
logicalOperators, logicalOperators,
processingObjectsInputDisplay, processingObjectsInputDisplay,
rulesExp,
fieldRef
}; };
}, },
}); });

View File

@@ -1,6 +1,6 @@
<template> <template>
<div v-bind="$attrs"> <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> <template v-slot:append>
<q-icon name="event" class="cursor-pointer"> <q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-popup-proxy cover transition-show="scale" transition-hide="scale">
@@ -43,16 +43,32 @@ export default defineComponent({
type: String, type: String,
default: '', default: '',
}, },
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
}
}, },
setup(props, { emit }) { setup(props, { emit }) {
const selectedDate = ref(props.modelValue); 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(() => { watchEffect(() => {
emit('update:modelValue', selectedDate.value); emit('update:modelValue', selectedDate.value);
}); });
return { return {
selectedDate selectedDate,
rulesExp
}; };
} }
}); });

View File

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

View File

@@ -1,7 +1,9 @@
<template> <template>
<div v-bind="$attrs"> <div v-bind="$attrs">
<q-field v-model="selectedField" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label <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> <template v-slot:control>
<q-chip color="primary" text-color="white" v-if="isSelected"> <q-chip color="primary" text-color="white" v-if="isSelected">
{{ selectedField.name }} {{ selectedField.name }}
@@ -23,7 +25,7 @@
</template> </template>
</q-input> </q-input>
</template> </template>
<field-select ref="appDg" name="フィールド" :type="selectType" :appId="store.appInfo?.appId" :fieldTypes="fieldTypes" :filter="filter"></field-select> <field-select ref="appDg" name="フィールド" :type="selectType" :appId="store.appInfo?.appId" :selectedFields="selectedFields" :fieldTypes="fieldTypes" :filter="filter"></field-select>
</show-dialog> </show-dialog>
</div> </div>
</template> </template>
@@ -74,16 +76,35 @@ export default defineComponent({
type: Object, type: Object,
default: null default: null
}, },
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
},
requiredMessage: {
type: String,
default: ''
}
}, },
setup(props, { emit }) { setup(props, { emit }) {
const appDg = ref(); const appDg = ref();
const show = ref(false); const show = ref(false);
const selectedField = ref(props.modelValue); const selectedField = ref(props.modelValue);
const selectedFields =computed(()=>!selectedField.value?[]: [selectedField.value]);
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const isSelected = computed(() => { const isSelected = computed(() => {
return selectedField.value !== null && typeof selectedField.value === 'object' && ('name' in selectedField.value) 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 = () => { const showDg = () => {
show.value = true; show.value = true;
@@ -108,6 +129,8 @@ export default defineComponent({
selectedField, selectedField,
isSelected, isSelected,
filter:ref(''), filter:ref(''),
selectedFields,
rulesExp
}; };
} }
}); });

View File

@@ -14,7 +14,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { kMaxLength } from 'buffer';
import { defineComponent, ref, watchEffect, computed } from 'vue'; import { defineComponent, ref, watchEffect, computed } from 'vue';
export default defineComponent({ export default defineComponent({
@@ -50,8 +49,16 @@ export default defineComponent({
type: String, type: String,
default: undefined default: undefined
}, },
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
modelValue: { modelValue: {
// type: Any, type: null as any,
default: '', default: '',
}, },
}, },
@@ -75,8 +82,11 @@ export default defineComponent({
}, },
}); });
// const inputValue = ref(props.modelValue); // 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(() => { // const finalValue = computed(() => {
// return props.name !== 'verName' ? inputValue.value : { // return props.name !== 'verName' ? inputValue.value : {
// name: inputValue.value, // name: inputValue.value,

View File

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

View File

@@ -49,6 +49,14 @@ export default defineComponent({
type:String, type:String,
default:undefined default:undefined
}, },
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
modelValue: { modelValue: {
type: [Number , String], type: [Number , String],
default: undefined default: undefined
@@ -57,23 +65,10 @@ export default defineComponent({
setup(props, { emit }) { setup(props, { emit }) {
const numValue = ref(props.modelValue); const numValue = ref(props.modelValue);
const rulesExp = props.rules===undefined?null : eval(props.rules); const customExp = props.rules === undefined ? [] : eval(props.rules);
const isError = computed(()=>{ const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const val = numValue.value; const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[];
if (val === undefined) { const rulesExp=[...requiredExp,...customExp];
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;
});
watchEffect(()=>{ watchEffect(()=>{
emit("update:modelValue",numValue.value); emit("update:modelValue",numValue.value);

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
<template> <template>
<div v-bind="$attrs"> <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"/> :multiple="multiple"/>
</div> </div>
</template> </template>
@@ -32,6 +35,19 @@ export default defineComponent({
type: [Array,String], type: [Array,String],
default: null, default: null,
}, },
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
}, },
setup(props, { emit }) { setup(props, { emit }) {
const selectedValue = ref(props.modelValue); const selectedValue = ref(props.modelValue);
@@ -41,10 +57,14 @@ export default defineComponent({
watchEffect(() => { watchEffect(() => {
emit('update:modelValue', selectedValue.value); 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 { return {
selectedValue, selectedValue,
multiple multiple,
rulesExp
}; };
}, },
}); });

View File

@@ -2,40 +2,28 @@
<q-layout view="lHh Lpr lFf"> <q-layout view="lHh Lpr lFf">
<q-header elevated> <q-header elevated>
<q-toolbar> <q-toolbar>
<q-btn <q-btn flat dense round icon="menu" aria-label="Menu" @click="toggleLeftDrawer" />
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-toolbar-title> <q-toolbar-title>
{{ productName }} {{ productName }}
<q-badge align="top" outline>V{{ version }}</q-badge> <q-badge align="top" outline>V{{ version }}</q-badge>
</q-toolbar-title> </q-toolbar-title>
<domain-selector></domain-selector> <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-toolbar>
</q-header> </q-header>
<q-drawer <q-drawer :model-value="authStore.LeftDrawer" :show-if-above="false" bordered>
:model-value="authStore.toggleLeftDrawer"
:show-if-above="false"
bordered
>
<q-list> <q-list>
<q-item-label <q-item-label header>
header メニュー
>
関連リンク
</q-item-label> </q-item-label>
<EssentialLink <EssentialLink v-for="link in essentialLinks" :key="link.title" v-bind="link" />
v-for="link in essentialLinks" <div v-if="isAdmin()">
:key="link.title" <EssentialLink v-for="link in adminLinks" :key="link.title" v-bind="link" />
v-bind="link" </div>
/> <EssentialLink v-for="link in domainLinks" :key="link.title" v-bind="link" />
</q-list> </q-list>
</q-drawer> </q-drawer>
@@ -46,7 +34,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { onMounted } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue'; import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import DomainSelector from 'components/DomainSelector.vue'; import DomainSelector from 'components/DomainSelector.vue';
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
@@ -54,107 +42,97 @@ import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore(); const authStore = useAuthStore();
const essentialLinks: EssentialLinkProps[] = [ const essentialLinks: EssentialLinkProps[] = [
{ {
title: 'ホーム', title: 'ホーム',
caption: 'home', caption: '設計書から導入する',
icon: 'home', icon: 'home',
link: '/', link: '/',
target:'_self' target: '_self'
}, },
// {
// title: 'フローエディター',
// caption: 'イベントを設定する',
// icon: 'account_tree',
// link: '/#/FlowChart',
// target: '_self'
// },
{ {
title: 'フローエディター', title: 'アプリ管理',
caption: 'flowChart', caption: 'アプリを管理する',
icon: 'account_tree', icon: 'widgets',
link: '/#/FlowChart', link: '/#/app',
target:'_self' target: '_self'
}, },
// {
// title: '条件エディター',
// caption: 'condition',
// icon: 'tune',
// link: '/#/condition',
// target:'_self'
// },
{ {
title: '条件エディター', title: '',
caption: 'condition', isSeparator: true
icon: 'tune',
link: '/#/condition',
target:'_self'
}, },
{ // {
title:'', // title:'Kintone ポータル',
isSeparator:true // caption:'Kintone',
}, // icon:'cloud_queue',
{ // link:'https://mfu07rkgnb7c.cybozu.com/k/#/portal'
title:'Kintone ポータル', // },
caption:'Kintone', // {
icon:'cloud_queue', // title:'CUSTOMINE',
link:'https://mfu07rkgnb7c.cybozu.com/k/#/portal' // caption:'gusuku',
}, // link:'https://app-customine.gusuku.io/drive.html',
{ // icon:'settings_suggest'
title:'CUSTOMINE', // },
caption:'gusuku', // {
link:'https://app-customine.gusuku.io/drive.html', // title:'Kintone API ドキュメント',
icon:'settings_suggest' // caption:'Kintone API',
}, // link:'https://cybozu.dev/ja/kintone/docs/',
{ // icon:'help_outline'
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'
}
]; ];
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 version = process.env.version;
const productName = process.env.productName; const productName = process.env.productName;
onMounted(() => {
authStore.toggleLeftMenu();
});
function toggleLeftDrawer() { function toggleLeftDrawer() {
authStore.toggleLeftMenu(); authStore.toggleLeftMenu();
} }
function isAdmin(){
const permission = authStore.permissions;
return permission === 'admin'
}
</script> </script>

View File

@@ -0,0 +1,123 @@
<template>
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="widgets" 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 disabled 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-url="prop">
<q-td :props="prop">
<a :href="prop.row.url" target="_blank" :title="prop.row.name" >
{{ prop.row.url }}
</a>
</q-td>
</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="editFlow(p.row)" />
<q-btn disabled flat color="primary" padding="xs" size="1em" icon="history" @click="showHistory(p.row)" />
<q-btn disabled flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
</q-td>
</template>
</q-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, reactive } from 'vue';
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import { useFlowEditorStore } from 'stores/flowEditor';
import { router } from 'src/router';
import { date } from 'quasar'
import { IManagedApp } from 'src/types/AppTypes';
interface IAppDisplay{
id:string;
name:string;
url:string;
user:string;
version:string;
updatetime:string;
}
const authStore = useAuthStore();
const numberStringSorting = (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10);
const columns = [
{ name: 'id', label: 'アプリID', field: 'id', align: 'left', sortable: true, sort: numberStringSorting },
{ 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', sortable: true},
{ name: 'updatetime', label: '最後更新日', field: 'updatetime', align: 'left', sortable: true},
{ name: 'version', label: 'バージョン', field: 'version', align: 'left', sortable: true},
{ name: 'actions', label: '操作', field: 'actions' }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false);
const filter = ref('');
const rows = ref<IAppDisplay[]>([]);
const store = useFlowEditorStore();
const getApps = async () => {
loading.value = true;
const result = await api.get('api/apps');
rows.value = result.data.map((item: IManagedApp) => {
return {
id: item.appid,
name: item.appname,
url: `${item.domainurl}/k/${item.appid}`,
user: `${item.user.first_name} ${item.user.last_name}` ,
updatetime:date.formatDate(item.update_time, 'YYYY/MM/DD HH:mm'),
version: Number(item.version)
}
}).sort((a: IAppDisplay, b: IAppDisplay) => numberStringSorting(a.id, b.id)); // set default order
loading.value = false;
}
onMounted(async () => {
authStore.setLeftMenu(false);
await getApps();
});
watch(() => authStore.currentDomain.id, async () => {
await getApps();
});
const addRow = () => {
return
}
const removeRow = (app:IAppDisplay) => {
return
}
const showHistory = (app:IAppDisplay) => {
return
}
const editFlow = (app:IAppDisplay) => {
store.setApp({
appId: app.id,
name: app.name
});
store.selectFlow(undefined);
router.push('/FlowChart/' + app.id);
};
</script>

View File

@@ -3,11 +3,7 @@
<q-layout container class="absolute-full shadow-2 rounded-borders"> <q-layout container class="absolute-full shadow-2 rounded-borders">
<div class="q-pa-sm q-gutter-sm "> <div class="q-pa-sm q-gutter-sm ">
<q-drawer side="left" :overlay="true" bordered v-model="drawerLeft" :show-if-above="false" elevated> <q-drawer side="left" :overlay="true" bordered v-model="drawerLeft" :show-if-above="false" elevated>
<div class="flex-center fixed-top app-selector"> <div class="flex-center absolute-full" style="padding:15px">
<AppSelector />
</div>
<div class="flex-center absolute-full" style="padding-top:65px;padding-left:15px;padding-right:15px;">
<q-scroll-area class="fit" :horizontal-thumb-style="{ opacity: '0' }"> <q-scroll-area class="fit" :horizontal-thumb-style="{ opacity: '0' }">
<EventTree /> <EventTree />
</q-scroll-area> </q-scroll-area>
@@ -16,14 +12,59 @@
<div class="flex-center fixed-bottom bg-grey-3 q-pa-md row "> <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-btn color="secondary" glossy label="デプロイ" @click="onDeploy" icon="sync" :loading="deployLoading" />
<q-space></q-space> <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="onSaveVersion">
<q-item-section avatar >
<q-icon name="history"></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="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> </div>
</q-drawer> </q-drawer>
</div> </div>
<q-btn flat dense round <q-btn flat dense round
:icon="drawerLeft?'keyboard_double_arrow_left':'keyboard_double_arrow_right'" :icon="drawerLeft?'keyboard_double_arrow_left':'keyboard_double_arrow_right'"
:style="[drawerLeft?{'left':'300px'}:{'left':'0px'}]" :style="{'left': fixedLeftPosition}"
@click="drawerLeft=!drawerLeft" class="expand" /> @click="drawerLeft=!drawerLeft" class="expand" />
<q-breadcrumbs v-if="store.appInfo" class="fixed q-pl-md"
:style="{'left': fixedLeftPosition}">
<q-breadcrumbs-el icon="widgets" label="アプリ管理" to="/app" />
<q-breadcrumbs-el>
<template v-slot>
<a class="full-width" :href="!store.appInfo?'':`${authStore.currentDomain.kintoneUrl}/k/${store.appInfo?.appId}`" target="_blank" title="Kiontoneへ">
{{ store.appInfo?.name }}
<q-icon
class="q-ma-xs"
name="open_in_new"
color="grey-9"
/>
</a>
</template>
</q-breadcrumbs-el>
</q-breadcrumbs>
<div class="q-pa-md q-gutter-sm" :style="{minWidth: minPanelWidth}"> <div class="q-pa-md q-gutter-sm" :style="{minWidth: minPanelWidth}">
<div class="flowchart" v-if="store.currentFlow" :style="[drawerLeft?{paddingLeft:'300px'}:{}]"> <div class="flowchart" v-if="store.currentFlow" :style="[drawerLeft?{paddingLeft:'300px'}:{}]">
<node-item v-if="rootNode!==undefined" :key="rootNode.id" :isSelected="rootNode === store.activeNode" <node-item v-if="rootNode!==undefined" :key="rootNode.id" :isSelected="rootNode === store.activeNode"
@@ -31,7 +72,7 @@
@deleteNode="onDeleteNode" @deleteAllNextNodes="onDeleteAllNextNodes" @copyFlow="onCopyFlow"></node-item> @deleteNode="onDeleteNode" @deleteAllNextNodes="onDeleteAllNextNodes" @copyFlow="onCopyFlow"></node-item>
</div> </div>
</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> </q-layout>
<ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg" min-width="500px" min-height="500px"> <ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg" min-width="500px" min-height="500px">
<template v-slot:toolbar> <template v-slot:toolbar>
@@ -41,33 +82,48 @@
</template> </template>
</q-input> </q-input>
</template> </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> </ShowDialog>
<!-- save version dialog -->
<ShowDialog v-model:visible="saveVersionAction" name="新バージョン保存" @close="closeSaveVersionDg" min-width="500px">
<version-input v-model="versionInfo" />
</ShowDialog>
<q-inner-loading
:showing="initLoading"
color="primary"
label="読み込み中..."
/>
</q-page> </q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty } from 'src/types/ActionTypes'; import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty } from 'src/types/ActionTypes';
import { IManagedApp, IVersionInfo } from 'src/types/AppTypes';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor'; import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
import { api } from 'boot/axios';
import NodeItem from 'src/components/main/NodeItem.vue'; import NodeItem from 'src/components/main/NodeItem.vue';
import ShowDialog from 'components/ShowDialog.vue'; import ShowDialog from 'components/ShowDialog.vue';
import ActionSelect from 'components/ActionSelect.vue'; import ActionSelect from 'components/ActionSelect.vue';
import PropertyPanel from 'components/right/PropertyPanel.vue'; import PropertyPanel from 'components/right/PropertyPanel.vue';
import AppSelector from 'components/left/AppSelector.vue';
import EventTree from 'components/left/EventTree.vue'; import EventTree from 'components/left/EventTree.vue';
import VersionInput from 'components/dialog/VersionInput.vue';
import { FlowCtrl } from '../control/flowctrl'; import { FlowCtrl } from '../control/flowctrl';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
const deployLoading = ref(false); const deployLoading = ref(false);
const saveLoading = ref(false); const saveLoading = ref(false);
const initLoading = ref(true);
const drawerLeft = ref(false); const drawerLeft = ref(false);
const versionInfo = ref<IVersionInfo>();
const $q = useQuasar(); const $q = useQuasar();
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const route = useRoute()
const appDg = ref(); const appDg = ref();
const prevNodeIfo = ref({ const prevNodeIfo = ref({
@@ -76,13 +132,11 @@ const prevNodeIfo = ref({
}); });
// const refFlow = ref<ActionFlow|null>(null); // const refFlow = ref<ActionFlow|null>(null);
const showAddAction = ref(false); const showAddAction = ref(false);
const saveVersionAction = ref(false);
const drawerRight = ref(false); const drawerRight = ref(false);
const filter=ref(""); const filter=ref("");
const model = ref(""); const model = ref("");
const addActionNode = (action: IActionNode) => {
// refFlow.value?.actionNodes.push(action);
store.currentFlow?.actionNodes.push(action);
}
const rootNode = computed(()=>{ const rootNode = computed(()=>{
return store.currentFlow?.getRoot(); return store.currentFlow?.getRoot();
}); });
@@ -94,6 +148,9 @@ const minPanelWidth=computed(()=>{
return "300px"; return "300px";
} }
}); });
const fixedLeftPosition = computed(()=>{
return drawerLeft.value?"300px":"0px";
});
const addNode = (node: IActionNode, inputPoint: string) => { const addNode = (node: IActionNode, inputPoint: string) => {
if (drawerRight.value) { if (drawerRight.value) {
@@ -136,7 +193,7 @@ const onDeleteAllNextNodes = (node: IActionNode) => {
} }
const closeDg = (val: any) => { const closeDg = (val: any) => {
console.log("Dialog closed->", val); console.log("Dialog closed->", val);
if (val == 'OK') { if (val == 'OK' && appDg?.value?.selected?.length > 0) {
const data = appDg.value.selected[0]; const data = appDg.value.selected[0];
const actionProps = JSON.parse(data.property); const actionProps = JSON.parse(data.property);
const outputPoint = JSON.parse(data.outputPoints); const outputPoint = JSON.parse(data.outputPoints);
@@ -193,13 +250,38 @@ const onDeploy = async () => {
return; return;
} }
const onSaveActionProps=(props:IActionProperty[])=>{
if(store.activeNode){
store.activeNode.actionProps=props;
$q.notify({
type: 'positive',
caption: "通知",
message: `${store.activeNode?.subTitle}の属性を設定しました。(保存はされていません)`
});
}
};
const onSaveVersion = async () => {
versionInfo.value = {
id: '1' // TODO
}
saveVersionAction.value = true;
// await onSaveAllFlow();
}
const closeSaveVersionDg = (val: 'OK'|'CANCEL') => {
if (val == 'OK') {
console.log(versionInfo.value);
}
}
const onSaveFlow = async () => { const onSaveFlow = async () => {
const targetFlow = store.selectedFlow; const targetFlow = store.selectedFlow;
if (targetFlow === undefined) { if (targetFlow === undefined) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: "エラー", caption: 'エラー',
message: `編集中のフローがありません。` message: `選択中のフローがありません。`
}); });
return; return;
} }
@@ -221,38 +303,76 @@ const onSaveFlow = async () => {
message: `${targetFlow.getRoot()?.subTitle}のフローの設定の保存が失敗しました。` 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 () => { const fetchData = async () => {
initLoading.value = true;
if (store.appInfo === undefined && route?.params?.id !== undefined) {
const { appid, appname } = await fetchAppById(route.params.id as string);
store.setApp({
appId: appid,
name: appname
});
};
await store.loadFlow();
initLoading.value = false
drawerLeft.value = true; drawerLeft.value = true;
if (store.appInfo === undefined) return; }
const flowCtrl = new FlowCtrl();
const actionFlows = await flowCtrl.getFlows(store.appInfo?.appId); const fetchAppById = async(id: string) => {
if (actionFlows && actionFlows.length > 0) { const result = await api.get('api/apps');
store.setFlows(actionFlows); return result.data.find((item: IManagedApp) => item.appid === id ) as IManagedApp;
} }
if (actionFlows && actionFlows.length == 1) {
store.selectFlow(actionFlows[0]); const onClearFilter=()=>{
} filter.value='';
const root = actionFlows[0].getRoot();
if (root) {
store.setActiveNode(root);
}
} }
onMounted(() => { onMounted(() => {
authStore.toggleLeftMenu(); authStore.setLeftMenu(false);
fetchData(); fetchData();
}); });
</script> </script>
<style lang="scss"> <style lang="scss">
.app-selector {
padding: 15px;
z-index: 999;
}
.flowchart { .flowchart {
padding-top: 10px; padding-top: 10px;
} }

View File

@@ -1,54 +1,95 @@
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" selection="single" :filter="filter" <div class="q-gutter-sm row items-start">
:loading="loading" v-model:selected="selected"> <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> <template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" /> <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-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> <template v-slot:append>
<q-icon name="search" /> <q-icon name="search" />
</template> </template>
</q-input> </q-input>
</template> </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-table>
<q-dialog :model-value="show" persistent> <q-dialog :model-value="show" persistent>
<q-card style="min-width: 400px"> <q-card style="min-width: 36em">
<q-card-section> <q-form class="q-gutter-md" @submit="onSubmit" autocomplete="off">
<div class="text-h6">Kintone Account</div> <q-card-section>
</q-card-section> <div class="text-h6 q-ma-sm">Kintone Account</div>
</q-card-section>
<q-card-section class="q-pt-none"> <q-card-section class="q-pt-none q-mt-none">
<q-form class="q-gutter-md"> <div class="q-gutter-lg">
<q-input filled v-model="tenantid" label="Tenant" hint="Tenant ID" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" />
<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 <q-input filled v-model="tenantid" label="テナントID" hint="テナントIDを入力してください。" lazy-rules
:rules="[val => val && val.length > 0, isDomain || 'Please type something']" /> :rules="[val => val && val.length > 0 || 'テナントIDを入力してください。']" />
<q-input filled v-model="kintoneuser" label="Login user " hint="Kintone user name" lazy-rules <q-input filled v-model="name" label="環境名 *" hint="kintoneの環境名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" /> :rules="[val => val && val.length > 0 || 'kintoneの環境名を入力してください。']" />
<q-input v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'" hint="Password with toggle" <q-input filled type="url" v-model="url" label="Kintone url" hint="KintoneのURLを入力してください" lazy-rules
label="User password"> :rules="[val => val && val.length > 0, isDomain || 'KintoneのURLを入力してください']" />
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer" @click="isPwd = !isPwd" /> <q-input filled v-model="kintoneuser" label="ログイン名 *" hint="Kintoneのログイン名を入力してください" lazy-rules
</template> :rules="[val => val && val.length > 0 || 'Kintoneのログイン名を入力してください']" autocomplete="new-password" />
</q-input>
</q-form> <q-input v-if="isCreate" v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'"
</q-card-section> hint="パスワード" label="パスワード" :disable="!isCreate" lazy-rules
<q-card-actions align="right" class="text-primary"> :rules="[val => val && val.length > 0 || 'Please type something']" autocomplete="new-password">
<q-btn label="Save" type="submit" color="primary" @click="onSubmit" /> <template v-slot:append>
<q-btn label="Cancel" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" /> <q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
</q-card-actions> @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-card>
</q-dialog> </q-dialog>
@@ -56,7 +97,7 @@
<q-dialog v-model="confirm" persistent> <q-dialog v-model="confirm" persistent>
<q-card> <q-card>
<q-card-section class="row items-center"> <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> <span class="q-ml-sm">削除してもよろしいですか</span>
</q-card-section> </q-card-section>
@@ -73,66 +114,68 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'; import { ref, onMounted, reactive } from 'vue';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore();
const columns = [ const columns = [
{ name: 'id' }, { name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ {
name: 'tenantid', name: 'tenantid',
required: true, required: true,
label: 'Tenant', label: 'テナントID',
align: 'left',
field: row => row.tenantid, field: row => row.tenantid,
format: val => `${val}`, format: val => `${val}`,
align: 'left',
sortable: true sortable: true
}, },
{ name: 'name', align: 'center', label: 'Name', field: 'name', sortable: true }, { name: 'name', label: '環境名', field: 'name', align: 'left', sortable: true },
{ name: 'url', align: 'left', label: 'URL', field: 'url', sortable: true }, { name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true },
{ name: 'user', label: 'Account', field: 'user' }, { name: 'user', label: 'ログイン名', field: 'user', align: 'left', },
{ name: 'password', label: 'Password', field: 'password' } { name: 'actions', label: '操作', field: 'actions' }
]; ];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false); const loading = ref(false);
const filter = ref(''); const filter = ref('');
const rows = ref([]); const rows = ref([]);
const show = ref(false); const show = ref(false);
const confirm = ref(false); const confirm = ref(false);
const selected = ref([]); const resetPsw = ref(false);
const tenantid = ref('');
const tenantid = ref(authStore.currentDomain.id);
const name = ref(''); const name = ref('');
const url = ref(''); const url = ref('');
const isPwd = ref(true); const isPwd = ref(true);
const kintoneuser = ref(''); const kintoneuser = ref('');
const kintonepwd = ref(''); const kintonepwd = ref('');
const kintonepwdBK = ref('');
const isCreate = ref(true);
let editId = ref(0); let editId = ref(0);
const getDomain = async () => { const getDomain = async () => {
loading.value = true; loading.value = true;
const result= await api.get(`api/domains/1`); const userId = authStore.userId;
rows.value= result.data.map((item)=>{ 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 } return { id: item.id, tenantid: item.tenantid, name: item.name, url: item.url, user: item.kintoneuser, password: item.kintonepwd }
}); });
loading.value = false; loading.value = false;
} }
onMounted(async () => { onMounted(async () => {
await getDomain(); await getDomain();
}) })
// emulate fetching data from server // emulate fetching data from server
const addRow = () => { const addRow = () => {
editId.value // editId.value
onReset();
show.value = true; show.value = true;
} }
const removeRow = () => { const removeRow = (row) => {
//loading.value = true
confirm.value = true; confirm.value = true;
let row = JSON.parse(JSON.stringify(selected.value[0]));
if (selected.value.length === 0) {
return;
}
editId.value = row.id; editId.value = row.id;
} }
@@ -141,14 +184,11 @@ const deleteDomain = () => {
getDomain(); getDomain();
}) })
editId.value = 0; editId.value = 0;
selected.value = [];
}; };
const editRow = () => { const editRow = (row) => {
if (selected.value.length === 0) { isCreate.value = false
return;
}
let row = JSON.parse(JSON.stringify(selected.value[0]));
editId.value = row.id; editId.value = row.id;
tenantid.value = row.tenantid; tenantid.value = row.tenantid;
name.value = row.name; name.value = row.name;
@@ -158,6 +198,16 @@ const editRow = () => {
isPwd.value = true; isPwd.value = true;
show.value = true; show.value = true;
}; };
const updateResetPsw = (value: boolean) => {
if (value === true) {
kintonepwd.value = ''
isPwd.value = true
} else {
kintonepwd.value = kintonepwdBK.value
}
}
const closeDg = () => { const closeDg = () => {
show.value = false; show.value = false;
onReset(); onReset();
@@ -171,7 +221,7 @@ const onSubmit = () => {
'name': name.value, 'name': name.value,
'url': url.value, 'url': url.value,
'kintoneuser': kintoneuser.value, 'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value 'kintonepwd': isCreate.value || resetPsw.value ? kintonepwd.value : ''
}).then(() => { }).then(() => {
getDomain(); getDomain();
closeDg(); closeDg();
@@ -192,7 +242,7 @@ const onSubmit = () => {
onReset(); onReset();
}) })
} }
selected.value = [];
} }
const onReset = () => { const onReset = () => {
@@ -202,5 +252,7 @@ const onReset = () => {
kintonepwd.value = ''; kintonepwd.value = '';
isPwd.value = true; isPwd.value = true;
editId.value = 0; editId.value = 0;
isCreate.value = true;
resetPsw.value = false
} }
</script> </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> <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> <template v-slot:top>
<div class="q-pa-md q-gutter-sm">
<q-btn color="primary" label="追加" @click="newDomain()" dense /> <q-btn class="q-mx-none" color="primary" label="追加" @click="clickAddDomain()" />
</div>
<q-space /> <q-space />
<q-input borderless dense debounce="300" v-model="filter" placeholder="Search"> <div class="row q-gutter-md">
<template v-slot:append> <q-item v-if="authStore.permissions === 'admin'" tag="label" dense @click="clickSwitchUser()">
<q-icon name="search" /> <q-item-section>
</template> <q-item-label>適用するユーザ : </q-item-label>
</q-input> </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>
<template v-slot:header>
<div style="height: 1dvh">
</div>
</template>
<template v-slot:item="props"> <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>
<q-card-section> <q-card-section>
<div class="q-table__grid-item-row"> <div class="q-table__grid-item-row">
@@ -130,40 +46,73 @@ export default {
</div> </div>
<div class="q-table__grid-item-row"> <div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">URL</div> <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>
<div class="q-table__grid-item-row"> <div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Account</div> <div class="q-table__grid-item-title">Account</div>
<div class="q-table__grid-item-value">{{ props.row.kintoneuser }}</div> <div class="q-table__grid-item-value">{{ props.row.kintoneuser }}</div>
</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-card-section>
<q-separator /> <q-separator />
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat @click = "activeDomain(props.row.id)">有効</q-btn> <div style="width: 98%;">
<q-btn flat @click = "deleteConfirm(props.row)">削除</q-btn> <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-actions>
</q-card> </q-card>
</div> </div>
</template> </template>
</q-table> </q-table>
<show-dialog v-model:visible="show" name="ドメイン" @close="closeDg" width="350px"> <show-dialog v-model:visible="showAddDomainDg" name="ドメイン" @close="addUserDomainFinished">
<domain-select ref="domainDg" name="ドメイン" type="multiple"></domain-select> <domain-select ref="addDomainRef" name="ドメイン" type="multiple"></domain-select>
</show-dialog> </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>
<q-card-section class="row items-center"> <q-card-section class="row items-center">
<q-avatar icon="confirm" color="primary" text-color="white" /> <div class="q-ma-sm q-mt-md">
<span class="q-ml-sm">削除してもよろしいですか</span> <q-icon name="warning" color="warning" size="2em" />
<span class="q-ml-sm">削除してもよろしいですか</span>
</div>
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup /> <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-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
@@ -171,100 +120,117 @@ export default {
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useQuasar } from 'quasar' import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, reactive } from 'vue' import { api } from 'boot/axios';
import ShowDialog from 'components/ShowDialog.vue';
import DomainSelect from 'components/DomainSelect.vue';
import { useAuthStore } from 'stores/useAuthStore'; 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(); const authStore = useAuthStore();
import { api } from 'boot/axios'; const pagination = ref({ sortBy: 'id', rowsPerPage: 0 });
import { domain } from 'process'; const rows = ref([] as any[]);
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 columns = [ const columns = [
{ name: 'id'}, { name: 'id' },
{name: 'name',required: true,label: 'Name',align: 'left',field: 'name',sortable: true}, { name: 'name', required: true, label: 'Name', align: 'left', field: 'name', sortable: true },
{ name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true }, { name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true }, { name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true },
{ name: 'kintonepwd' }, { 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) =>{ let editId = ref(0);
if(id == activedomainid.value)
return "Active";
else
return "Inactive";
}
const newDomain = () => { const showAddDomainDg = ref(false);
const addDomainRef = ref();
const clickAddDomain = () => {
editId.value = 0; editId.value = 0;
show.value = true; showAddDomainDg.value = true;
}; };
const addUserDomainFinished = (val: string) => {
const activeDomain = (id:number) => { if (val == 'OK') {
api.put(`api/activedomain/`+ id).then(() =>{ let dodmainids = [];
getDomain(); 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) => { const showDeleteConfirm = ref(false);
confirm.value = true;
const clickDeleteConfirm = (row: any) => {
showDeleteConfirm.value = true;
editId.value = row.id; editId.value = row.id;
}; };
const deleteDomain = () => { const deleteDomainFinished = () => {
api.delete(`api/domain/`+ editId.value+'/1').then(() =>{ api.delete(`api/domain/${editId.value}/${useOtherUser.value ? otherUserId.value : authStore.userId}`).then(() => {
getDomain(); getDomain(useOtherUser.value ? otherUserId.value : undefined);
}) })
editId.value = 0; editId.value = 0;
}; };
const closeDg = (val:string) => { const activeDomain = (id: number) => {
if (val == 'OK') { api.put(`api/activedomain/${id}${useOtherUser.value ? `?userId=${otherUserId.value}` : ''}`)
let dodmainids =[]; .then(() => { getDomain(useOtherUser.value ? otherUserId.value : undefined); })
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 getDomain = async () => {
const resp = await api.get(`api/activedomain`); let activeDomainId = ref(0);
activedomainid.value = resp.data.id;
const domainResult = await api.get(`api/domain`); const isActive = computed(() => (id: number) => {
const domains = domainResult.data as any[]; return id == activeDomainId.value;
rows.value=domains.map((item)=>{ });
return { id:item.id,name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd}
});
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 () => { onMounted(async () => {
currentUserName.value = authStore.userInfo.email
await getDomain(); 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> </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

@@ -6,7 +6,7 @@ const routes: RouteRecordRaw[] = [
component: () => import('pages/LoginPage.vue') component: () => import('pages/LoginPage.vue')
}, },
{ {
path:'/FlowChart', path:'/FlowChart/:id',
component:()=>import('layouts/MainLayout.vue'), component:()=>import('layouts/MainLayout.vue'),
children:[ children:[
{path:'',component:()=>import('pages/FlowChart.vue')} {path:'',component:()=>import('pages/FlowChart.vue')}
@@ -25,7 +25,9 @@ const routes: RouteRecordRaw[] = [
// { path: 'FlowChart', component: () => import('pages/FlowChart.vue') }, // { path: 'FlowChart', component: () => import('pages/FlowChart.vue') },
{ path: 'right', component: () => import('pages/testRight.vue') }, { path: 'right', component: () => import('pages/testRight.vue') },
{ path: 'domain', component: () => import('pages/TenantDomain.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: 'app', component: () => import('pages/AppManagement.vue')},
{ path: 'condition', component: () => import('pages/conditionPage.vue') } { path: 'condition', component: () => import('pages/conditionPage.vue') }
], ],
}, },

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { AppInfo, IActionFlow, IActionNode } from 'src/types/ActionTypes'; 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'; import { FlowCtrl } from '../control/flowctrl';
export interface FlowEditorState { export interface FlowEditorState {
@@ -11,7 +11,7 @@ export interface FlowEditorState {
activeNode: IActionNode | undefined; activeNode: IActionNode | undefined;
eventTree: KintoneEventManager; eventTree: KintoneEventManager;
selectedEvent: IKintoneEvent | undefined; selectedEvent: IKintoneEvent | undefined;
expandedScreen: any[]; expandedScreen: string[];
} }
const flowCtrl = new FlowCtrl(); const flowCtrl = new FlowCtrl();
const eventTree = new KintoneEventManager(); const eventTree = new KintoneEventManager();
@@ -62,10 +62,19 @@ export const useFlowEditorStore = defineStore('flowEditor', {
}, },
selectFlow(flow: IActionFlow | undefined) { selectFlow(flow: IActionFlow | undefined) {
this.selectedFlow = flow; this.selectedFlow = flow;
if(flow!==undefined){
const eventId = flow.getRoot()?.name;
this.selectedEvent = this.eventTree.findEventById(eventId) as IKintoneEvent;
} else {
this.selectedEvent = undefined;
}
}, },
setActiveNode(node: IActionNode) { setActiveNode(node: IActionNode) {
this.activeNode = node; this.activeNode = node;
}, },
setCurrentEvent(event:IKintoneEvent | undefined){
this.selectedEvent=event;
},
setApp(app: AppInfo) { setApp(app: AppInfo) {
this.appInfo = app; this.appInfo = app;
}, },
@@ -79,22 +88,41 @@ export const useFlowEditorStore = defineStore('flowEditor', {
//eventTreeにバンドする //eventTreeにバンドする
this.eventTree.bindFlows(actionFlows); this.eventTree.bindFlows(actionFlows);
if (actionFlows === undefined || actionFlows.length === 0) { if (actionFlows === undefined || actionFlows.length === 0) {
this.flows = []; this.setFlows([]);
this.selectedFlow = undefined; this.selectFlow(undefined);
this.expandedScreen =[];
return; return;
} }
this.setFlows(actionFlows); this.setFlows(actionFlows);
if (actionFlows && actionFlows.length > 0) { if (actionFlows && actionFlows.length > 0) {
this.selectFlow(actionFlows[0]); this.selectFlow(actionFlows[0]);
} }
const expandNames = actionFlows.map((flow) => flow.getRoot()?.title); const root = actionFlows[0].getRoot();
if (root) {
this.setActiveNode(root);
}
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; // const expandName =actionFlows[0].getRoot()?.title;
this.expandedScreen = expandNames; this.expandedScreen = expandScreens;
}, },
/** /**
* フローをDBに保存及び更新する * フローをDBに保存及び更新する
*/ */
async saveFlow(flow: IActionFlow) { async saveFlow(flow: IActionFlow):Promise<boolean> {
const root = flow.getRoot(); const root = flow.getRoot();
const isNew = flow.id === ''; const isNew = flow.id === '';
const jsonData = { const jsonData = {
@@ -108,7 +136,14 @@ export const useFlowEditorStore = defineStore('flowEditor', {
if (isNew) { if (isNew) {
return await flowCtrl.SaveFlow(jsonData); return await flowCtrl.SaveFlow(jsonData);
} else { } 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 { store } from 'quasar/wrappers'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { Router } from 'vue-router'; import { Router } from 'vue-router';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
/* /*
* When adding new properties to stores, you should also * When adding new properties to stores, you should also
@@ -23,10 +24,11 @@ declare module 'pinia' {
*/ */
export default store((/* { ssrContext } */) => { export default store((/* { ssrContext } */) => {
const pinia = createPinia() const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
// You can add Pinia plugins here // You can add Pinia plugins here
// pinia.use(SomePiniaPlugin) // pinia.use(SomePiniaPlugin)
return pinia return pinia;
}) });

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
interface IUser {
first_name: string;
last_name: string;
email: string;
}
export interface IManagedApp {
appid: string;
appname: string;
domainurl: string;
version: string;
user: IUser;
update_time: string;
}
export interface IVersionInfo {
id: string;
name?: string;
desc?: string;
}

View File

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

View File

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

View File

@@ -283,6 +283,11 @@
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz" resolved "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz"
integrity sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug== 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": "@typescript-eslint/eslint-plugin@^5.10.0":
version "5.61.0" version "5.61.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz" 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" resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz"
integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q== 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": "@vue/reactivity-transform@3.3.4":
version "3.3.4" version "3.3.4"
resolved "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz" 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" resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz"
integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ== 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: accepts@~1.3.5, accepts@~1.3.8:
version "1.3.8" version "1.3.8"
resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz"
@@ -1830,6 +1862,11 @@ jsonfile@^6.0.1:
optionalDependencies: optionalDependencies:
graceful-fs "^4.1.6" 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: kind-of@^6.0.2:
version "6.0.3" version "6.0.3"
resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" 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" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pinia@^2.1.6: pinia-plugin-persistedstate@^3.2.1:
version "2.1.6" version "3.2.1"
resolved "https://registry.npmjs.org/pinia/-/pinia-2.1.6.tgz" resolved "https://registry.yarnpkg.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz#66780602aecd6c7b152dd7e3ddc249a1f7a13fe5"
integrity sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ== 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: dependencies:
"@vue/devtools-api" "^6.5.0" "@vue/devtools-api" "^6.6.3"
vue-demi ">=0.14.5" vue-demi "^0.14.10"
postcss-selector-parser@^6.0.9: postcss-selector-parser@^6.0.9:
version "6.0.13" version "6.0.13"
@@ -2792,10 +2834,10 @@ vite@^2.9.13:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
vue-demi@>=0.14.5: vue-demi@>=0.14.8, vue-demi@^0.14.10:
version "0.14.6" version "0.14.10"
resolved "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz" resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w== integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
vue-eslint-parser@^9.3.0: vue-eslint-parser@^9.3.0:
version "9.3.1" version "9.3.1"

View File

@@ -70,7 +70,10 @@
| placeholder | 対象項目を選択してください| 入力フィールドに表示されるプレースホルダーのテキストです。この場合は設定されていません。 | | placeholder | 対象項目を選択してください| 入力フィールドに表示されるプレースホルダーのテキストです。この場合は設定されていません。 |
| hint | 説明文| 長い説明文を設定することが可能です。markdown形式サポート予定、現在HTML可能 | | hint | 説明文| 長い説明文を設定することが可能です。markdown形式サポート予定、現在HTML可能 |
| selectType |`single` or `multiple`| フィールド選択・他のアプリのフィールド選択の選択モードを設定する | | 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 | 一行のテキスト入力が可能なフィールドです。 | | 1 | テキストボックス | InputText | 一行のテキスト入力が可能なフィールドです。 |
| 2 | テキストボックス(改行可能) | MuiltInputText | 複数行のテキスト入力が可能なテキストエリアです。 | | 2 | テキストボックス(改行可能) | MuiltInputText | 複数行のテキスト入力が可能なテキストエリアです。 |
| 3 | 日付 | DatePicker | 日付を選択するためのカレンダーコンポーネントです。 | | 3 | 数値入力 | NumInput | 数値のみ入力可能フィールド。 |
| 4 | フィールド選択 | FieldInput | システムのフィールドを選択するための入力コンポーネントです。 | | 4 | 日付 | DatePicker | 日付を選択するためのカレンダーコンポーネントです。 |
| 5 | 選択リスト | SelectBox | 複数のオプションから選択するためのドロップダウンリストです。 | | 5 | フィールド選択 | FieldInput | システムのフィールドを選択するための入力コンポーネントです。 |
| 6 | 条件式設定 | ConditionInput | 条件式やロジックを入力するためのコンポーネントです。 | | 6 | 選択リスト | SelectBox | 複数のオプションから選択するためのドロップダウンリストです。 |
| 7 | イベント設定 |EventSetter | ボタンイベント設定のコンポーネントです。 | | 7 | 条件式設定 | ConditionInput | 条件式やロジックを入力するためのコンポーネントです。 |
| 8 | 色選択 | ColorPicker | 色を設定する(追加予定中) | | 8 | イベント設定 |EventSetter | ボタンイベント設定のコンポーネントです。 |
| 9 | 他のアプリのフィールド選択 | AppFieldPicker | 他のアプリのフィールドを選択する(追加予定中) | | 9 | 選択 | ColorPicker | 色を設定する |
| 10 |ユーザー選択 | UserPicker | ユーザーを選択する(追加予定中) | | 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.アクションアドインの開発 ## 2.アクションアドインの開発
@@ -270,7 +309,7 @@ npm run build:dev
- Azure App Service 拡張機能でデプロイが完了したことを確認します。 - Azure App Service 拡張機能でデプロイが完了したことを確認します。
- ka-addin の URL にアクセスしてアプリケーションが正常に動作しているか確認します。 - ka-addin の URL にアクセスしてアプリケーションが正常に動作しているか確認します。
3. **ローカルでプラグインをテストする** 3. **ローカルでプラグインをテストする(ZCCの導入ため、廃止する)**
1. kintone-addinsをPreviewで起動する 1. kintone-addinsをPreviewで起動する
```bash ```bash
yarn build:dev yarn build:dev
@@ -278,7 +317,7 @@ yarn preview
#またはyarn devは yarn build:dev + yarn preview と同じです #またはyarn devは yarn build:dev + yarn preview と同じです
yarn dev yarn dev
``` ```
2. **ngrokをインストールする** 2. **ngrokをインストールする(ZCCの導入ため、廃止する)**
1. [ngrok の公式ウェブサイト](https://ngrok.com/)にアクセスします。 1. [ngrok の公式ウェブサイト](https://ngrok.com/)にアクセスします。
2. 「Sign up」をクリックしてアカウントを登録するか、既存のアカウントにログインします。 2. 「Sign up」をクリックしてアカウントを登録するか、既存のアカウントにログインします。
3. 登録またはログイン後、ダッシュボードに進み、ダウンロードリンクが表示されます。操作システムWindows、macOS、Linuxに応じて、適切なバージョンを選択してダウンロードします。 3. 登録またはログイン後、ダッシュボードに進み、ダウンロードリンクが表示されます。操作システムWindows、macOS、Linuxに応じて、適切なバージョンを選択してダウンロードします。
@@ -292,4 +331,10 @@ yarn dev
6. ngrok を起動する 6. ngrok を起動する
```bash ```bash
ngrok https http://localhost:4173/ 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 { actionAddins } from ".";
import type { Record} from "@kintone/rest-api-client/lib/src/client/types"; 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 "./auto-lookup.scss";
import "bootstrap/js/dist/modal"; import "bootstrap/js/dist/modal";
// import "bootstrap/js/dist/spinner"; // import "bootstrap/js/dist/spinner";
import {Modal} from "bootstrap" import {Modal} from "bootstrap"
import $ from "jquery"; import $ from "jquery";
interface Props { interface IAutoLookUpProps {
displayName: string; displayName: string;
lookupField: LookupField; lookupField: LookupField;
condition: Condition; condition: Condition;
@@ -84,11 +84,11 @@ interface App {
export class AutoLookUpAction implements IAction { export class AutoLookUpAction implements IAction {
name: string; name: string;
actionProps: IActionProperty[]; actionProps: IActionProperty[];
props: Props; props: IAutoLookUpProps;
constructor() { constructor() {
this.name = "ルックアップ更新"; this.name = "ルックアップ更新";
this.actionProps = []; this.actionProps = [];
this.props = {} as Props; this.props = {} as IAutoLookUpProps;
this.register(); this.register();
} }
@@ -96,15 +96,15 @@ export class AutoLookUpAction implements IAction {
* アクセスのメインの処理関数 * アクセスのメインの処理関数
*/ */
async process( async process(
prop: IActionNode, actionNode: IActionNode,
event: any, event: any,
context: IContext context: IContext
): Promise<IActionResult> { ): Promise<IActionResult> {
this.actionProps = prop.actionProps; this.actionProps = actionNode.actionProps;
this.props = { this.props = {
...prop.ActionValue, ...actionNode.ActionValue,
condition: JSON.parse((prop.ActionValue as any).condition), condition: JSON.parse((actionNode.ActionValue as any).condition),
} as Props; } as IAutoLookUpProps;
// console.log(context); // console.log(context);
let result = { let result = {
@@ -129,19 +129,16 @@ export class AutoLookUpAction implements IAction {
} }
const updateRecords = this.convertForLookup(targetRecords,lookUpField,key); const updateRecords = this.convertForLookup(targetRecords,lookUpField,key);
console.log("updateRecords", updateRecords); console.log("updateRecords", updateRecords);
this.showSpinnerModel(this.props.lookupField.app); this.showSpinnerModel(this.props.lookupField.app,lookUpField);
await this.updateLookupTarget(updateRecords); const updateResult = await this.updateLookupTarget(updateRecords);
this.showResult(this.props.lookupField.app,updateRecords.length); if(updateResult){
this.showResult(this.props.lookupField.app,lookUpField,updateRecords.length);
}
} catch (error) { } catch (error) {
console.error("ルックアップ更新中例外が発生しました。", error); this.closeDialog();
if(error instanceof Error){ context.errors.handleError(error,actionNode,"ルックアップ更新中例外が発生しました");
event.error = error.message;
}else{
event.error = "ルックアップ更新中例外が発生しました。";
}
result.canNext = false; result.canNext = false;
} }
console.log("autoLookupProps", this.props);
return result; return result;
} }
@@ -153,12 +150,17 @@ export class AutoLookUpAction implements IAction {
* @returns * @returns
*/ */
makeQuery=(lookUpField:Field,key:any)=>{ makeQuery=(lookUpField:Field,key:any)=>{
let query ="";
if(typeof key==='number'){ if(typeof key==='number'){
return `${lookUpField.code} = ${key}` query = `${lookUpField.code} = ${key}`
} }
if(typeof key==='string'){ 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 * @param updateRecords
*/ */
updateLookupTarget = async (updateRecords:Array<any>)=>{ updateLookupTarget = async (updateRecords:Array<any>):Promise<boolean>=>{
if (updateRecords && updateRecords.length > 0) { if (updateRecords && updateRecords.length > 0) {
const client=new KintoneRestAPIClient(); try{
client.record.updateAllRecords({ const client=new KintoneRestAPIClient();
app:this.props.lookupField.app.id, const result = await client.record.updateAllRecords({
records:updateRecords 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", { // await kintone.api(kintone.api.url("/k/v1/records.json", true), "PUT", {
// app: this.props.lookupField.app.id, // app: this.props.lookupField.app.id,
// records: updateRecords // records: updateRecords
// }); // });
} }
return false;
} }
/** /**
* 更新中のダイアログ表示 * 更新中のダイアログ表示
* @param app * @param app
*/ */
showSpinnerModel = (app:App) => { showSpinnerModel = (app:App,lookup:Field) => {
let dialog = $("#alcLookupModal"); let dialog = $("#alcLookupModal");
if(dialog.length===0){ 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" id="alcLookupModal" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog-centered"> <div class="modal-dialog-centered">
<div class="modal-dialog modal-content"> <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> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <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="spinner-border text-secondary col-1 " role="alert"></div>
<div class="col">${app.name}</div> <div class="col">${app.name}</div>
</div> </div>
@@ -230,25 +245,24 @@ export class AutoLookUpAction implements IAction {
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button> <button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div> </div>
</div> </div></div></div></div>`;
</div> $(modalHTML).appendTo("body");
</div>`; dialog = $("#alcLookupModal");
dialog = $(modalHTML).appendTo("body");
dialog.get()[0].addEventListener('hidden.bs.modal',(ev)=>{ dialog.get()[0].addEventListener('hidden.bs.modal',(ev)=>{
Modal.getOrCreateInstance(dialog.get()[0]).dispose(); Modal.getOrCreateInstance(dialog.get()[0]).dispose();
$("#alcLookupModal").remove(); $("#alcLookupModal").parent().remove();
}); });
}else{ }else{
const dialogBody=$("#alcLookupModal .modal-body"); const dialogBody=$("#alcLookupModal .modal-body");
const htmlrow=` 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 class="spinner-border text-secondary col-1 " role="alert">
</div> </div>
<div class="col">${app.name}</div> <div class="col">${app.name}</div>
<div>`; <div>`;
dialogBody.append(htmlrow); 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 app  更新先アプリ情報
* @param count 更新件数 * @param count 更新件数
*/ */
showResult=(app:App,count:number)=>{ showResult=(app:App,lookup:Field,count:number)=>{
const dialogBody=$(`#alcLookupModal .modal-body #app${app.id}`); const dialogBody=$(`#alcLookupModal .modal-body #app${app.id}_${lookup.code}`);
const html=` <div class="col-1 text-success">✔</div> const html=` <div class="col-1 text-success">✔</div>
<div class="col">${app.name}</div> <div class="col">${app.name}</div>
<div class="col">更新件数:${count}件</div>`; <div class="col">更新件数:${count}件</div>`;
dialogBody.html(html); 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 { register(): void {
actionAddins[this.name] = this; actionAddins[this.name] = this;
} }

View File

@@ -65,8 +65,7 @@ export class AutoNumbering implements IAction{
} }
return result; return result;
}catch(error){ }catch(error){
console.error(error); context.errors.handleError(error,actionNode);
event.error="処理中異常が発生しました。";
return { return {
canNext:false, canNext:false,
result: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 { actionAddins } from ".";
import $ from 'jquery'; import $ from 'jquery';
import { IAction, IActionProperty, IActionNode, IActionResult } from "../types/ActionTypes"; import { IAction, IActionProperty, IActionNode, IActionResult, IContext } from "../types/ActionTypes";
import "./button-add.css"; import "./button-add.css";
/** /**
@@ -43,7 +43,7 @@ export class ButtonAddAction implements IAction {
* @param event * @param event
* @returns * @returns
*/ */
async process(actionNode: IActionNode, event: any): Promise<IActionResult> { async process(actionNode: IActionNode, event: any,context:IContext): Promise<IActionResult> {
let result = { let result = {
canNext: true, canNext: true,
result: false result: false
@@ -75,8 +75,7 @@ export class ButtonAddAction implements IAction {
}); });
return result; return result;
} catch (error) { } catch (error) {
event.error = error; context.errors.handleError(error,actionNode);
console.error(error);
result.canNext = false; result.canNext = false;
return result; 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; return result;
}catch(error){ }catch(error){
event.error=error; context.errors.handleError(error,actionNode);
console.error(error);
result.canNext=false; result.canNext=false;
return result; return result;
} }

View File

@@ -1,5 +1,5 @@
import { actionAddins } from "."; 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 * @param event
* @returns * @returns
*/ */
async process(actionNode:IActionNode,event:any):Promise<IActionResult> { async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={ let result={
canNext:true, canNext:true,
result:false result:false
@@ -54,15 +54,16 @@ export class StrCountCheckAciton implements IAction{
}else if(maxLength < value.length){ }else if(maxLength < value.length){
record[this.props.field.code].error = this.props.message; record[this.props.field.code].error = this.props.message;
}else{ }else{
result= { record[this.props.field.code].error = null;
canNext:true, }
result:true result= {
} canNext:true,
result:true
} }
return result; return result;
}catch(error){ }catch(error){
event.error=error; context.errors.handleError(error,actionNode);
console.error(error);
result.canNext=false; result.canNext=false;
return result; return result;
} }

View File

@@ -4,13 +4,14 @@ import {
IActionNode, IActionNode,
IActionProperty, IActionProperty,
IContext, IContext,
IField,
} from "../types/ActionTypes"; } from "../types/ActionTypes";
import { actionAddins } from "."; import { actionAddins } from ".";
interface Props { interface ICurrentFieldGetProps {
displayName: string; displayName: string;
field: Field; field: IField;
verName: VerName; verName: VerName;
} }
@@ -18,49 +19,43 @@ interface VerName {
name: string; 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 { export class CurrentFieldGetAction implements IAction {
name: string; name: string;
actionProps: IActionProperty[]; actionProps: IActionProperty[];
currentFieldGetProps: Props; props: ICurrentFieldGetProps;
constructor() { constructor() {
this.name = "フィールドの値を取得する"; this.name = "フィールドの値を取得する";
this.actionProps = []; this.actionProps = [];
this.currentFieldGetProps = {} as Props; this.props = {} as ICurrentFieldGetProps;
this.register(); this.register();
} }
async process( async process(
prop: IActionNode, actionNode: IActionNode,
event: any, event: any,
context: IContext context: IContext
): Promise<IActionResult> { ): Promise<IActionResult> {
this.currentFieldGetProps = prop.ActionValue as Props; this.props = actionNode.ActionValue as ICurrentFieldGetProps;
this.actionProps = prop.actionProps; this.actionProps = actionNode.actionProps;
let result = { let result = {
canNext: true, canNext: true,
result: "", result: '',
} as IActionResult; } as IActionResult;
try { try {
context.variables[this.currentFieldGetProps.verName.name] = context.record[this.currentFieldGetProps.field.code].value; const record = event.record;
} catch (error) { if(!(this.props.field.code in record)){
console.error("CurrentFieldGetAction error", error); 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; result.canNext = false;
} }
return result; return result;

View File

@@ -83,13 +83,13 @@ export class DataProcessingAction implements IAction {
} }
async process( async process(
nodes: IActionNode, actionNode: IActionNode,
event: any, event: any,
context: IContext context: IContext
): Promise<IActionResult> { ): Promise<IActionResult> {
this.actionProps = nodes.actionProps; this.actionProps = actionNode.actionProps;
this.props = nodes.ActionValue as IDataProcessingProps; this.props = actionNode.ActionValue as IDataProcessingProps;
const condition = JSON.parse(this.props.condition) as Condition; const condition = JSON.parse(this.props.condition) as Condition;
let result = { let result = {
canNext: true, canNext: true,
@@ -118,12 +118,7 @@ export class DataProcessingAction implements IAction {
} }
return result; return result;
} catch (error) { } catch (error) {
console.error("データ集計中例外が発生しました。", error); context.errors.handleError(error,actionNode);
if(error instanceof Error){
event.error = `データ集計中例外が発生しました。 ${error.message}`;
}else{
event.error = "データ集計中例外が発生しました。";
}
result.canNext = false; result.canNext = false;
return result; return result;
} }

View File

@@ -74,12 +74,12 @@ export class DataUpdateAction implements IAction {
} }
async process( async process(
prop: IActionNode, actionNode: IActionNode,
event: any, event: any,
context: IContext context: IContext
): Promise<IActionResult> { ): Promise<IActionResult> {
this.actionProps = prop.actionProps; this.actionProps = actionNode.actionProps;
this.dataMappingProps = prop.ActionValue as Props; this.dataMappingProps = actionNode.ActionValue as Props;
console.log(context); console.log(context);
let result = { let result = {
canNext: true, canNext: true,
@@ -121,7 +121,7 @@ export class DataUpdateAction implements IAction {
); );
} }
} catch (error) { } catch (error) {
console.error("DataMappingAction error", error); context.errors.handleError(error,actionNode);
result.canNext = false; result.canNext = false;
} }
console.log("dataMappingProps", this.dataMappingProps); console.log("dataMappingProps", this.dataMappingProps);

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

View File

@@ -1,5 +1,6 @@
import { each } from "jquery";
import { actionAddins } from "."; import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes"; import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes";
import { ConditionTree } from '../types/Conditions'; import { ConditionTree } from '../types/Conditions';
@@ -41,16 +42,20 @@ export class InsertValueAction implements IAction{
* @param {string} inputValue - 挿入する値 * @param {string} inputValue - 挿入する値
* @return {boolean} -入力値が有効な日付形式の場合はtrueを返し、そうでない場合は例外を発生させる * @return {boolean} -入力値が有効な日付形式の場合はtrueを返し、そうでない場合は例外を発生させる
*/ */
checkInputBlank(fieldType :string | undefined,inputValue :string,fieldCode :string,fieldRequired :boolean | undefined,event :any): boolean{ checkInputValueBlank(fieldType :string | undefined,inputValue :string,fieldCode :string,fieldRequired :boolean | undefined,event :any): boolean{
//正規表現チェック
let blankCheck= inputValue.match(/^(\s| )+?$/);//半角スペース・タブ文字・改行・改ページ・全角スペース
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" 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"){ || fieldType === "ORGANIZATION_SELECT" || fieldType === "GROUP_SELECT" || fieldType === "RADIO_BUTTON" || fieldType === "DROP_DOWN" || fieldType === "CHECK_BOX" || fieldType === "MULTI_SELECT"){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドには、空白文字は挿入できません。"; //レコードにエラーを表示 event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"に挿入しようとした、値は空白文字です。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"フィールドには、空白文字は挿入できません。「値を挿入する」コンポーネントの処理を中断しました。"); throw new Error("「"+fieldCode+"」"+"に挿入しようとした、値は空白文字です。「値を挿入する」コンポーネントの処理を中断しました。");
//空白文字を必須項目フィールドに挿入しようとしている場合、例外を発生させる //空白文字を必須項目フィールドに挿入しようとしている場合、例外を発生させる
}else if(fieldRequired){ }else if(fieldRequired){
@@ -62,6 +67,40 @@ export class InsertValueAction implements IAction{
return true; 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 - 挿入する値 * @param {string} inputValue - 挿入する値
@@ -70,7 +109,7 @@ export class InsertValueAction implements IAction{
checkInputNumber(inputValue :string,fieldCode :string,event :any): boolean{ checkInputNumber(inputValue :string,fieldCode :string,event :any): boolean{
let inputNumberValue = Number(inputValue);//数値型に変換 let inputNumberValue = Number(inputValue);//数値型に変換
//有限数かどうか判定s //有限数かどうか判定
if(!isFinite(inputNumberValue)){ if(!isFinite(inputNumberValue)){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な日付形式です。"; //レコードにエラーを表示 event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な日付形式です。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした値は、有効な数値ではありません。「値を挿入する」コンポーネントの処理を中断しました。"); throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした値は、有効な数値ではありません。「値を挿入する」コンポーネントの処理を中断しました。");
@@ -111,13 +150,13 @@ export class InsertValueAction implements IAction{
*/ */
checkInputTime(inputValue :string,fieldCode :string,event :any): boolean{ 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){ if(timeFormat === null){
event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な時刻形式です。"; //レコードにエラーを表示 event.record[fieldCode]['error'] = "「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な時刻形式です。"; //レコードにエラーを表示
throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な時刻形式です。「値を挿入する」コンポーネントの処理を中断しました。"); throw new Error("「"+fieldCode+"」"+"フィールドに入れようとした値は、無効な時刻形式です。「値を挿入する」コンポーネントの処理を中断しました。「12桁 : 2桁」の値を指定してください。");
} }
return true; return true;
} }
@@ -142,7 +181,26 @@ export class InsertValueAction implements IAction{
return dateTime; 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桁の数字 let dateArray=inputValue.match(/(\d{4})-(\d{1,2})-(\d{1,2})$/);//4桁の数字-12桁の数字-12桁の数字
if(dateArray !== null){ if(dateArray !== null){
let yearIndex = 1; let yearIndex = 1;
@@ -184,19 +242,27 @@ export class InsertValueAction implements IAction{
* @param {string} inputValue - 挿入する値 * @param {string} inputValue - 挿入する値
* @return {string | boolean} 入力値が登録されているユーザー情報から見つかった場合、trueを返し、見つからなかった場合、falseを返す * @return {string | boolean} 入力値が登録されているユーザー情報から見つかった場合、trueを返し、見つからなかった場合、falseを返す
*/ */
async setInputUser(inputValue :string): Promise<string | boolean>{ async setInputUser(inputValue :any): Promise<any | boolean>{
//ユーザー名を格納する変数 //ユーザー名を格納する変数
let usersName; let usersName = [];
const usersInfoColumnIndex=0;
try{ try{
//APIでユーザー情報を取得する //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 (usersInfo.length !== inputValue.length) {
if (resp.users[usersInfoColumnIndex].code === inputValue) { throw new Error();
usersName=resp.users[usersInfoColumnIndex].name; }
//入力されたログイン名がユーザー情報に登録されている場合、そのユーザー名を取得する
for (let indexUsersInfo in usersInfo) {
for(let indexInputUser in inputValue){
if(usersInfo[indexUsersInfo].code === inputValue[indexInputUser]){
usersName.push(usersInfo[indexUsersInfo].name);
}
}
} }
//ユーザー名が取得できた場合、ログイン名とユーザー名をフィールドにセットする //ユーザー名が取得できた場合、ログイン名とユーザー名をフィールドにセットする
@@ -206,7 +272,6 @@ export class InsertValueAction implements IAction{
}catch{ }catch{
return false; return false;
} }
return usersName; return usersName;
} }
@@ -272,6 +337,70 @@ export class InsertValueAction implements IAction{
return groupsName; 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;
}
/** /**
@@ -294,18 +423,9 @@ export class InsertValueAction implements IAction{
} }
const fieldColumnIndex=1; const fieldColumnIndex=1;
const valueColumnIndex=3;
//プロパティで選択されたフィールド //プロパティで選択されたフィールド
const field=this.actionProps[fieldColumnIndex].props.modelValue.type; 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){ if(field === null){
@@ -320,15 +440,10 @@ export class InsertValueAction implements IAction{
throw new Error("「値を挿入する」コンポーネントで、選択されたフィールドは、値を挿入するコンポーネントでは非対応のフィールドのため、処理を中断しました。"); throw new Error("「値を挿入する」コンポーネントで、選択されたフィールドは、値を挿入するコンポーネントでは非対応のフィールドのため、処理を中断しました。");
} }
//プロパティの挿入する値が未入力の場合、例外を発生させる
if(value === ""){
throw new Error("「値を挿入する」コンポーネントで、フィールドに挿入する値が指定されていなかったため、処理が中断されました。");
}
//既定のプロパティのインターフェースへ変換する //既定のプロパティのインターフェースへ変換する
this.props = actionNode.ActionValue as IInsertValueProps; this.props = actionNode.ActionValue as IInsertValueProps;
//挿入する値を取得 //プロパティの挿入する値を取得
let fieldValue = this.props.value; let fieldValue = this.props.value;
//フィールドの種類を取得 //フィールドの種類を取得
const fieldType = this.props.field.type; const fieldType = this.props.field.type;
@@ -341,42 +456,127 @@ export class InsertValueAction implements IAction{
//ラジオボタン・チェックボックス・複数選択・ドロップダウンの選択肢を取得 //ラジオボタン・チェックボックス・複数選択・ドロップダウンの選択肢を取得
let fieldOptions =this.props.field.options; let fieldOptions =this.props.field.options;
//変数の値を格納する //変数の値を格納する
let variableValue; let variableValue :any;
//挿入する値を格納する
let fieldValueArray = [];
let notInputError;
//contextに存在する変数名を格納する
let contextHasVariablesNames;
//変数の場合、値が取得できるかチェック //変数の場合、値が取得できるかチェック
if(insertValueType === "変数" && conditionResult){ if(insertValueType === "変数"){
variableValue = context.variables[fieldValue];
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+"」"+"フィールドに入れようとした変数は、無効な入力形式です。"); 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 correctFormattedValue = undefined;
//入力値チェック後、形式変換、型変換した値を格納する配列 //形式変換、型変換した値を格納する配列
let correctValues :string[] = []; let correctValues :any[] = [];
//入力エラー(空白文字の混入)がないことをチェック
let notInputError=this.checkInputBlank(fieldType,fieldValue,fieldCode,fieldRequired,event);
//条件式の結果がtrue、入力エラー空白文字の混入がない場合、挿入する値をフィールドタイプ別にチェックする //条件式の結果がtrue、入力エラー空白文字の混入がない場合、挿入する値をフィールドタイプ別にチェックする
if(conditionResult && notInputError){ if(notInputError){
//文字列型のフィールドに挿入しようとしている値が適切の場合、correctFormattedValueに代入する //文字列型のフィールドに挿入しようとしている値が適切の場合、correctFormattedValueに代入する
if(fieldType === "SINGLE_LINE_TEXT" || fieldType === "MULTI_LINE_TEXT" || fieldType === "RICH_TEXT" || fieldType === "LINK" ){ if(fieldType === "SINGLE_LINE_TEXT" || fieldType === "MULTI_LINE_TEXT" || fieldType === "RICH_TEXT" || fieldType === "LINK" ){
correctFormattedValue = fieldValue; correctFormattedValue = fieldValueArray.join(',');
//数値型のフィールドに挿入しようとしている値が適切の場合、数値型に型変換してcorrectFormattedValueに代入する //数値型のフィールドに挿入しようとしている値が適切の場合、数値型に型変換してcorrectFormattedValueに代入する
}else if(fieldType === "NUMBER" ){ }else if(fieldType === "NUMBER" ){
if(this.checkInputNumber(fieldValue,fieldCode,event)){//入力値チェック if(this.checkInputNumber(fieldValueArray[0],fieldCode,event)){//入力値チェック
correctFormattedValue = Number(fieldValue);//型変換 correctFormattedValue = Number(fieldValueArray[0]);//型変換
} }
//日付・日時型のフィールドに挿入しようとしている値が適切の場合、指定の日付・日時に形式変換してcorrectFormattedValueに代入する //日付・日時型のフィールドに挿入しようとしている値が適切の場合、指定の日付・日時に形式変換してcorrectFormattedValueに代入する
}else if(fieldType === "DATE" || fieldType === "DATETIME" ){ }else if(fieldType === "DATE" || fieldType === "DATETIME" ){
if(this.checkInputDate(fieldValue,fieldCode,event)){//入力値チェック if(this.checkInputDate(fieldValueArray[0],fieldCode,event)){//入力値チェック
let formattedDate = this.changeDateFormat(fieldValue,fieldType,fieldCode,event) let formattedDate = this.changeDateFormat(fieldValueArray[0],fieldType,fieldCode,event)
if(formattedDate){ if(formattedDate){
correctFormattedValue = formattedDate correctFormattedValue = formattedDate
} }
@@ -384,33 +584,35 @@ export class InsertValueAction implements IAction{
//時刻フィールドに挿入しようとしている値が適切の場合、correctFormattedValueに代入する //時刻フィールドに挿入しようとしている値が適切の場合、correctFormattedValueに代入する
}else if(fieldType === "TIME"){ }else if(fieldType === "TIME"){
if(this.checkInputTime(fieldValue,fieldCode,event)){//入力値チェック if(this.checkInputTime(fieldValueArray[0],fieldCode,event)){//入力値チェック
correctFormattedValue = fieldValue; correctFormattedValue = fieldValueArray[0];
} }
//ラジオボタン・ドロップダウンのフィールドの選択肢と入力値が一致した場合、correctFormattedValueに代入する //ラジオボタン・ドロップダウンのフィールドの選択肢と入力値が一致した場合、correctFormattedValueに代入する
}else if(fieldType === "RADIO_BUTTON" || fieldType === "DROP_DOWN"){ }else if(fieldType === "RADIO_BUTTON" || fieldType === "DROP_DOWN"){
if(this.checkInputOption(fieldValue,fieldOptions,fieldCode,event)){//入力値チェック if(this.checkInputOption(fieldValueArray[0],fieldOptions,fieldCode,event)){//入力値チェック
correctFormattedValue = fieldValue; correctFormattedValue = fieldValueArray[0];
} }
//チェックボックス・複数選択のフィールドの選択肢と入力値が一致した場合、correctValuesの配列に代入する //チェックボックス・複数選択のフィールドの選択肢と入力値が一致した場合、correctValuesの配列に代入する
}else if(fieldType === "CHECK_BOX" || fieldType === "MULTI_SELECT" ){ }else if(fieldType === "CHECK_BOX" || fieldType === "MULTI_SELECT" ){
if(this.checkInputOption(fieldValue,fieldOptions,fieldCode,event)){//入力値チェック if(this.checkInputOption(fieldValueArray[0],fieldOptions,fieldCode,event)){//入力値チェック
correctValues[0] = fieldValue; correctValues[0] = fieldValueArray[0];
} }
//ユーザー情報フィードに挿入しようとした値が適切な場合、correctFormattedValueに代入する //ユーザー情報フィードに挿入しようとした値が適切な場合、correctValues(配列)に代入する
}else if(fieldType === "USER_SELECT"){ }else if(fieldType === "USER_SELECT"){
//挿入する値がユーザー情報から見つかれば、ユーザー名を格納 //挿入する値がユーザー情報から見つかれば、ユーザー名を格納
let users=await this.setInputUser(fieldValue); let usersName=await this.setInputUser(fieldValueArray);
//ユーザー名が格納できている場合、ログイン名とユーザー名をcorrectFormattedValueに代入する //ユーザー名が格納できている場合、ログイン名とユーザー名をcorrectValues配列に代入する
if(!users){ if(!usersName){
event.record[fieldCode]['error']="ユーザー選択に、挿入しようとしたユーザー情報は見つかりませんでした。「値を挿入する」コンポーネントの処理を中断しました。"; event.record[fieldCode]['error']="ユーザー選択に、挿入しようとしたユーザー情報は見つかりませんでした。「値を挿入する」コンポーネントの処理を中断しました。";
throw new Error("ユーザー選択に、挿入しようとしたユーザー情報は見つかりませんでした。「値を挿入する」コンポーネントの処理を中断しました。"); throw new Error("ユーザー選択に、挿入しようとしたユーザー情報は見つかりませんでした。「値を挿入する」コンポーネントの処理を中断しました。");
}else{ }
correctFormattedValue=[{ code: fieldValue, name: users }];
for(let indexInputUser in fieldValueArray){
correctValues.push({ code: fieldValueArray[indexInputUser], name: usersName[indexInputUser] });
} }
//組織情報フィードに挿入しようとした値が適切な場合、correctFormattedValueに代入する //組織情報フィードに挿入しようとした値が適切な場合、correctFormattedValueに代入する
@@ -441,50 +643,63 @@ export class InsertValueAction implements IAction{
} }
} }
const conditionResult = this.getConditionResult(context);
//保存成功イベントの場合、kintone async/await による非同期処理でフィールドに値を挿入する //保存成功イベントの場合、kintone async/await による非同期処理でフィールドに値を挿入する
if(!event.type.includes('success')){ if(!event.type.includes('success')){
//条件式の結果がtrueかつ挿入する値が変換できた場合、フィールドラジオボタン・ドロップダウン・チェックボックス・複数選択・文字列一行・文字列複数行・リッチエディタ・数値・日付・日時・時刻にセット //条件式の結果がtrueかつ挿入する値が変換できた場合、フィールドラジオボタン・ドロップダウン・チェックボックス・複数選択・文字列一行・文字列複数行・リッチエディタ・数値・日付・日時・時刻にセット
if(conditionResult && (correctFormattedValue || correctValues)){ if(conditionResult){
//条件式の結果がtureかつ、値を正しい形式に変換できた場合、フィールドに値をセットする //値を正しい形式に変換できた場合、フィールドに値をセットする
if(correctFormattedValue){ if(correctFormattedValue !== undefined){
event.record[fieldCode].value = correctFormattedValue; event.record[fieldCode].value = correctFormattedValue;
//条件式の結果がtureかつ、値を正しい形式(配列)に変換できた場合、フィールドに値(配列)をセットする //値を正しい形式(配列)に変換できた場合、フィールドに値(配列)をセットする
}else if(correctValues.length > 0){ }else{
event.record[fieldCode].value = correctValues; event.record[fieldCode].value = correctValues;
} }
} }
}else{ }else{
//kintone async/await による非同期処理(保存成功時イベントREST API処理時)
async function updateRecord(fieldCode:string,event:any,insertValue:any) { //挿入する値を挿入先フィールドに値をセットし、kintoneAPIによってレコード更新を行う
return new Promise((resolve, reject) => { 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 = { var updatedRecord = {
app: event.appId, app: event.appId,
id: event.recordId, id: event.recordId,
record: {[fieldCode]:{"value":insertValue}} record: {[fieldCode]:{"value":insertValue}}
}; };
kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', updatedRecord, (resp) => { //APIでユーザー情報を取得する
resolve(resp); const resp =await kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', updatedRecord)
}, (error) => {
reject(error);
});
});
}
//条件式の結果がtrueかつ挿入する値が変換できた場合、フィールドラジオボタン・ドロップダウン・チェックボックス・複数選択・文字列一行・文字列複数行・リッチエディタ・数値・日付・日時・時刻にセット }catch{
if(conditionResult && (correctFormattedValue || correctValues)){ return false;
//条件式の結果がtureかつ、値を正しい形式に変換できた場合、フィールドに値をセットする
if(correctFormattedValue){
event.record[fieldCode].value = correctFormattedValue;
//条件式の結果がtureかつ、値を正しい形式配列に変換できた場合、フィールドに値配列をセットする
}else if(correctValues.length > 0){
event.record[fieldCode].value = correctValues;
} }
} }
//kintone async/await による非同期処理(レコード更新) // 関数の呼び出し
await updateRecord(fieldCode,event,correctFormattedValue); setUpdateData(conditionResult,fieldCode,event,correctFormattedValue,correctValues);
}; };
result= { result= {
@@ -493,13 +708,10 @@ export class InsertValueAction implements IAction{
} }
return result; return result;
}catch(error:any){ }catch(error:any){
event.record; context.errors.handleError(error,actionNode);
event.error=error.message;
console.error(error);
result.canNext=true;//次のノードは処理を続ける result.canNext=true;//次のノードは処理を続ける
return result; return result;
} }
} }
/** /**

View File

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

View File

@@ -1,5 +1,5 @@
import { actionAddins } from "."; 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 * @param event
* @returns * @returns
*/ */
async process(actionNode: IActionNode, event: any): Promise<IActionResult> { async process(actionNode: IActionNode, event: any,context:IContext): Promise<IActionResult> {
let result = { let result = {
canNext: true, canNext: true,
result: false result: false
@@ -46,8 +46,11 @@ export class MailCheckAction implements IAction {
return result return result
} }
this.props = actionNode.ActionValue as IMailCheckProps; this.props = actionNode.ActionValue as IMailCheckProps;
//条件式の計算結果を取得
const record = event.record; 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; const value = record[this.props.field.code].value;
if (this.props.emailCheck === '厳格') { if (this.props.emailCheck === '厳格') {
@@ -56,13 +59,15 @@ export class MailCheckAction implements IAction {
} }
else { else {
record[this.props.field.code].error = null; record[this.props.field.code].error = null;
result.result=true;
} }
} else if (this.props.emailCheck === 'ゆるめ') { } else if (this.props.emailCheck === 'ゆるめ') {
if (!/^[^@]+@[^@]+$/.test(value)) { if (!/^[^@]+@[^@]+$/.test(value)) {
record[this.props.field.code].error = this.props.message; record[this.props.field.code].error = this.props.message;
} }
else { else {
record[this.props.field.code].error = null; record[this.props.field.code].error = null;
result.result=true;
} }
} else { } else {
result = { result = {
@@ -72,8 +77,7 @@ export class MailCheckAction implements IAction {
} }
return result; return result;
} catch (error) { } catch (error) {
event.error = error; context.errors.handleError(error,actionNode);
console.error(error);
result.canNext = false; result.canNext = false;
return result; return result;
} }

View File

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

View File

@@ -1,5 +1,5 @@
import { actionAddins } from "."; 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 * @param event
* @returns * @returns
*/ */
async process(actionNode:IActionNode,event:any):Promise<IActionResult> { async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={ let result={
canNext:true, canNext:true,
result:false result:false
@@ -49,17 +49,18 @@ export class RegularCheckAction implements IAction{
const value = record[this.props.field.code].value; const value = record[this.props.field.code].value;
const regex = new RegExp(this.props.regExpression); const regex = new RegExp(this.props.regExpression);
if(!regex.test(value)){ if(!regex.test(value)){
record[this.props.field.code].error=this.props.message; record[this.props.field.code].error=this.props.message;
}else{ }
result= { else {
canNext:true, record[this.props.field.code].error = null;
result:true }
} result= {
canNext:true,
result:true
} }
return result; return result;
}catch(error){ }catch(error){
event.error=error; context.errors.handleError(error,actionNode);
console.error(error);
result.canNext=false; result.canNext=false;
return result; return 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

@@ -43,6 +43,9 @@ export class FullWidthAction implements IAction{
this.props = actionNode.ActionValue as FullWidthProps; this.props = actionNode.ActionValue as FullWidthProps;
//条件式の計算結果を取得 //条件式の計算結果を取得
const record = event.record; 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; const value = record[this.props.field.code]?.value;
//条件分岐 //条件分岐
//未入力時は何も処理をせず終了 //未入力時は何も処理をせず終了
@@ -68,8 +71,9 @@ export class FullWidthAction implements IAction{
//例外処理 //例外処理
}catch(error){ }catch(error){
event.error=error; // event.error=error;
console.error(error); // console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=false; result.canNext=false;
return result; return result;
} }

View File

@@ -43,6 +43,9 @@ export class HalfWidthAction implements IAction{
this.props = actionNode.ActionValue as HalfWidthProps; this.props = actionNode.ActionValue as HalfWidthProps;
//条件式の計算結果を取得 //条件式の計算結果を取得
const record = event.record; 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; const value = record[this.props.field.code]?.value;
//条件分岐 //条件分岐
//未入力時は何も処理をせず終了 //未入力時は何も処理をせず終了
@@ -68,8 +71,9 @@ export class HalfWidthAction implements IAction{
return result; return result;
//例外処理 //例外処理
}catch(error){ }catch(error){
event.error=error; // event.error=error;
console.error(error); // console.error(error);
context.errors.handleError(error,actionNode);
result.canNext=false; result.canNext=false;
return result; return result;
} }

View File

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

View File

@@ -1,4 +1,3 @@
/** /**
* アプリ情報 * アプリ情報
*/ */
@@ -67,7 +66,8 @@ export interface IActionResult{
*/ */
export interface IContext{ export interface IContext{
record:any, record:any,
variables:any variables:any,
errors:ErrorManager
} }
/** /**
@@ -84,13 +84,13 @@ export interface IAction{
register():void; register():void;
} }
export interface IField{ export interface IField{
name?:string; name?:string;
code:string; code:string;
type?:string; type?:string;
required?:boolean; required?:boolean;
options?:string; options?:string;
label?: string;
} }
//変数のインターフェース //変数のインターフェース
export interface IVarName{ export interface IVarName{
@@ -98,6 +98,14 @@ export interface IVarName{
fields?: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}`;
}
}

View File

@@ -1,4 +1,4 @@
import { IContext } from "./ActionTypes"; import { IContext,ICodeValue } from "./ActionTypes";
//ノード種別 //ノード種別
export enum NodeType{ export enum NodeType{
@@ -315,7 +315,38 @@ export class ConditionTree {
throw new Error('Unsupported node type'); throw new Error('Unsupported node type');
} }
} }
getCodeValue(value: any): any {
if (value && typeof value === 'object' && 'code' in value) {
return value.code;
} else {
return value;
}
}
/**
* ICodeValue型のcodeに変換する
* @param value
* @returns
*/
convertCodeValue(value:any):any{
if (Array.isArray(value)) {
return value.map(v => this.getCodeValue(v));
} else {
return this.getCodeValue(value);
}
}
/**
*
* @param vars 変数値を取得する
* @param path
* @returns
*/
getContextVarByPath (vars: {[key:string]:any}, path: string){
return path.split(".").reduce((o, k) => (o || {})[k], vars);
};
/** /**
* Condition objectの値を取得する * Condition objectの値を取得する
* @param object * @param object
@@ -331,10 +362,13 @@ export class ConditionTree {
if(fieldValue.type==='NUMBER' && fieldValue.value!==undefined){ if(fieldValue.type==='NUMBER' && fieldValue.value!==undefined){
return Number(fieldValue.value); return Number(fieldValue.value);
}else{ }else{
return fieldValue.value; return this.convertCodeValue(fieldValue.value);
} }
}else if(object.objectType==='variable'){ }else if(object.objectType==='variable'){
return context.variables[object.name].value; const varValue = this.getContextVarByPath(context.variables,object.name.name);
return this.convertCodeValue(varValue);
}else if(object.objectType==='text'){
return object.sharedText;
} }
} }
@@ -343,10 +377,13 @@ export class ConditionTree {
* @param object * @param object
* @returns * @returns
*/ */
getConditionValue(object:any){ getConditionValue(object:any,context:IContext){
if(!object || typeof object!=="object"){ if(!object || typeof object!=="object"){
return object; return object;
} }
if("objectType" in object){
return this.getObjectValue(object,context);
}
if("sharedText" in object){ if("sharedText" in object){
return object.sharedText; return object.sharedText;
} }
@@ -369,8 +406,16 @@ export class ConditionTree {
*/ */
evaluateCondition(condition: ConditionNode, context: IContext): boolean { evaluateCondition(condition: ConditionNode, context: IContext): boolean {
const { object, operator, value } = condition; const { object, operator, value } = condition;
const targetValue = this.getObjectValue(object,context); const targetObject = this.getObjectValue(object,context);
const conditionValue = this.getConditionValue(value); const conditionObject = this.getConditionValue(value,context);
const isArrayTarget = Array.isArray(targetObject);
const isArrayValue = Array.isArray(conditionObject);
//配列の比較
if((isArrayTarget && targetObject.length > 1)||(isArrayValue && conditionObject.length>1)){
return this.compareArray(operator,targetObject,conditionObject)
}
const targetValue = isArrayTarget?targetObject[0]:targetObject;
const conditionValue = isArrayValue?conditionObject[0]:conditionObject;
switch (operator) { switch (operator) {
case Operator.Equal: case Operator.Equal:
case Operator.NotEqual: case Operator.NotEqual:
@@ -427,7 +472,6 @@ export class ConditionTree {
else if (typeof targetValue === 'string') { else if (typeof targetValue === 'string') {
value = String(value); value = String(value);
} }
switch (operator) { switch (operator) {
case Operator.Equal: case Operator.Equal:
return targetValue === value; return targetValue === value;
@@ -446,6 +490,40 @@ export class ConditionTree {
} }
} }
/**
* 配列値の比較
* @param operator
* @param targetValue
* @param value
*/
/**
* 配列値の比較
* @param operator
* @param targetValue
* @param value
*/
compareArray(operator: Operator, targetValue: any, value: any): boolean {
const isArrayTarget = Array.isArray(targetValue);
const isArrayValue = Array.isArray(value);
const sortArray = (arr: any[]) => arr.slice().sort();
targetValue = isArrayTarget?targetValue:[targetValue];
value= isArrayValue?value:[value];
const targetArray=sortArray(targetValue);
const valueArray=sortArray(value);
switch (operator) {
case Operator.Equal:
return targetArray.length===valueArray.length && targetArray.every((v, i) => v === valueArray[i]);
case Operator.NotEqual:
return targetArray.length!==valueArray.length || !targetArray.every((v, i) => v === valueArray[i]);
case Operator.Contains:
return valueArray.length>0 && valueArray.every(v => targetArray.includes(v));
case Operator.NotContains:
return valueArray.length===0 || !valueArray.every(v => targetArray.includes(v));
default:
return false;
}
}
/** /**
* 含む計算 * 含む計算
* @param targetValue * @param targetValue
@@ -482,6 +560,4 @@ export class ConditionTree {
} }
return false; return false;
} }
} }

View File

@@ -18,23 +18,36 @@ import '../actions/value-getter';
import '../actions/string-join'; import '../actions/string-join';
import '../actions/validation-fullwidth'; import '../actions/validation-fullwidth';
import '../actions/validation-halfwidth'; import '../actions/validation-halfwidth';
import '../actions/half-full-conversion';
import '../actions/login-user-getter'; import '../actions/login-user-getter';
import '../actions/auto-lookup'; import '../actions/auto-lookup';
import '../actions/field-disable';
import { ActionFlow,IActionFlow, IActionResult,IContext } from "./ActionTypes"; import '../actions/style-field';
import '../actions/datetime-calc';
import '../actions/end-of-month';
import '../actions/date-specified';
import '../actions/cascading-dropdown';
import { ActionFlow,ErrorManager,IActionFlow, IActionResult,IContext } from "./ActionTypes";
const ShowErrorEvents:string[] = [
"app.record.create.submit.success",
"app.record.edit.submit.success",
"app.record.index.edit.submit.success",
];
export class ActionProcess{ export class ActionProcess{
eventId:string; eventId:string;
flow:IActionFlow; flow:IActionFlow;
event:any; event:any;
context:IContext; context:IContext;
constructor(eventId:string,flow:ActionFlow,event:any){ constructor(eventId:string,flow:ActionFlow,event:any){
this.eventId=eventId; this.eventId=eventId;
this.flow=flow; this.flow=flow;
this.event=event; this.event=event;
const errormanager = new ErrorManager();
this.context={ this.context={
record:this.event.record, record:this.event.record,
variables:{} variables:{},
errors:errormanager
}; };
} }
async exec(){ async exec(){
@@ -59,9 +72,14 @@ export class ActionProcess{
nextInput = result.result||''; nextInput = result.result||'';
} }
id=nextAction.nextNodeIds.get(nextInput); id=nextAction.nextNodeIds.get(nextInput);
if(id===undefined) return; if(id===undefined) break;
nextAction = this.flow.findNodeById(id); nextAction = this.flow.findNodeById(id);
} }
if(this.context.errors.hasError){
this.context.errors.setEvent(this.event);
if(ShowErrorEvents.includes(this.event.type)){
this.context.errors.showError();
}
}
} }
} }

View File

@@ -0,0 +1,37 @@
import $ from 'jquery';
/**
* 画面処理中のLoadding表示
*/
export class Snipper {
private spinnerElement: JQuery<HTMLElement>;;
private container :JQuery<HTMLElement>;
constructor(selector: string) {
this.container = $(selector??'body');
this.spinnerElement=this.createSpinner();
}
createSpinner() {
const html =[
'<div class="bs-scope alc-loading alc-dnone">',
'<div class="spinner-border" style="width: 3rem; height: 3rem;" role="status">',
'<span class="visually-hidden"></span>',
'</div></div>'
].join("");
const spinner = $(html);
this.container.append(spinner);
return spinner;
}
removeSpinner() {
this.spinnerElement.remove();
}
showSpinner() {
this.spinnerElement.removeClass('alc-dnone');
}
hideSpinner() {
this.spinnerElement.addClass('alc-dnone');
}
}

View File

@@ -0,0 +1,86 @@
/**
* ページの種類を表す型
*/
export type PageType = 'app' | 'show' | 'edit' | 'other';
/**
* showページのモードを表す型
*/
export type ShowPageMode = 'show' | 'edit' | null;
/**
* ページの状態を表すインターフェース
*/
export interface PageState {
type: PageType;
mode: ShowPageMode;
}
/**
* URLの特定のセグメントを取得する
* @param url 分析対象のURL
* @param index 取得したいセグメントのインデックス(負の数は末尾からのインデックス)
*/
const getSegment = (url: URL, index: number): string => {
const segments = url.pathname.split('/').filter(Boolean);
return segments.at(index) ?? '';
};
/**
* URLとアプリIDに基づいて現在のページの状態を判断する
* @param url 分析対象のURL
* @param appId 確認するアプリID
*/
export const getPageState = (url: string, appId: string): PageState => {
const parsedUrl = new URL(url);
const lastSegment = getSegment(parsedUrl, -1);
const secondLastSegment = getSegment(parsedUrl, -2);
if (lastSegment === appId && /^\d+$/.test(appId)) {
return { type: 'app', mode: null };
}
if (lastSegment.toLowerCase() === 'show' && secondLastSegment === appId && /^\d+$/.test(appId)) {
const mode = parsedUrl.searchParams.get('mode');
return {
type: 'show',
mode: (mode === 'show' || mode === 'edit') ? mode : null
};
}
if (lastSegment.toLowerCase() === 'edit' && secondLastSegment === appId && /^\d+$/.test(appId)) {
return { type: 'edit', mode: null };
}
return { type: 'other', mode: null };
};
/**
* 現在のページの状態を判断する現在のURLを使用
* @param appId 確認するアプリID
*/
export const getCurrentPageState = (appId: string): PageState =>
getPageState(window.location.href, appId);
/**
* URLを正規化する末尾のスラッシュを削除
* @param url 正規化対象のURL
*/
export const normalizeUrl = (url: string): string => {
const parsedUrl = new URL(url);
parsedUrl.pathname = parsedUrl.pathname.replace(/\/$/, '');
return parsedUrl.toString();
};
/**
* URLを安全に解析する
* @param url 解析対象のURL文字列
*/
export const safeParseUrl = (url: string): URL | null => {
try {
return new URL(url);
} catch {
return null;
}
};

View File

@@ -0,0 +1,693 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 15.6
-- Dumped by pg_dump version 15.4
-- Started on 2024-08-21 09:37:58
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
ALTER TABLE IF EXISTS ONLY public.action DROP CONSTRAINT IF EXISTS fk_categoryid;
ALTER TABLE IF EXISTS ONLY public.eventaction DROP CONSTRAINT IF EXISTS eventaction_eventid_fkey;
ALTER TABLE IF EXISTS ONLY public.eventaction DROP CONSTRAINT IF EXISTS eventaction_actionid_fkey;
DROP INDEX IF EXISTS public.ix_eventaction_id;
DROP INDEX IF EXISTS public.ix_action_name;
DROP INDEX IF EXISTS public.ix_action_id;
ALTER TABLE IF EXISTS ONLY public.action DROP CONSTRAINT IF EXISTS name;
ALTER TABLE IF EXISTS ONLY public.eventaction DROP CONSTRAINT IF EXISTS eventaction_pkey;
ALTER TABLE IF EXISTS ONLY public.category DROP CONSTRAINT IF EXISTS category_pkey;
ALTER TABLE IF EXISTS ONLY public.action DROP CONSTRAINT IF EXISTS action_pkey;
ALTER TABLE IF EXISTS public.eventaction ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.action ALTER COLUMN id DROP DEFAULT;
DROP SEQUENCE IF EXISTS public.eventaction_id_seq;
DROP TABLE IF EXISTS public.eventaction;
DROP TABLE IF EXISTS public.category;
DROP SEQUENCE IF EXISTS public.action_id_seq;
DROP TABLE IF EXISTS public.action;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- TOC entry 214 (class 1259 OID 27560)
-- Name: action; Type: TABLE; Schema: public; Owner: kabAdmin
--
CREATE TABLE public.action (
id integer NOT NULL,
create_time timestamp without time zone,
update_time timestamp without time zone,
name character varying(100) NOT NULL,
title character varying(200),
subtitle character varying(500),
outputpoints character varying,
property character varying,
nosort integer,
categoryid integer
);
ALTER TABLE public.action OWNER TO "kabAdmin";
--
-- TOC entry 215 (class 1259 OID 27565)
-- Name: action_id_seq; Type: SEQUENCE; Schema: public; Owner: kabAdmin
--
CREATE SEQUENCE public.action_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.action_id_seq OWNER TO "kabAdmin";
--
-- TOC entry 3995 (class 0 OID 0)
-- Dependencies: 215
-- Name: action_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: kabAdmin
--
ALTER SEQUENCE public.action_id_seq OWNED BY public.action.id;
--
-- TOC entry 240 (class 1259 OID 27752)
-- Name: category; Type: TABLE; Schema: public; Owner: kabAdmin
--
CREATE TABLE public.category (
id integer NOT NULL,
categoryname character varying(20) NOT NULL,
nosort integer,
create_time timestamp without time zone,
update_time timestamp without time zone
);
ALTER TABLE public.category OWNER TO "kabAdmin";
--
-- TOC entry 224 (class 1259 OID 27591)
-- Name: eventaction; Type: TABLE; Schema: public; Owner: kabAdmin
--
CREATE TABLE public.eventaction (
id integer NOT NULL,
create_time timestamp without time zone,
update_time timestamp without time zone,
eventid character varying(100),
actionid integer
);
ALTER TABLE public.eventaction OWNER TO "kabAdmin";
--
-- TOC entry 225 (class 1259 OID 27594)
-- Name: eventaction_id_seq; Type: SEQUENCE; Schema: public; Owner: kabAdmin
--
CREATE SEQUENCE public.eventaction_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.eventaction_id_seq OWNER TO "kabAdmin";
--
-- TOC entry 3996 (class 0 OID 0)
-- Dependencies: 225
-- Name: eventaction_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: kabAdmin
--
ALTER SEQUENCE public.eventaction_id_seq OWNED BY public.eventaction.id;
--
-- TOC entry 3827 (class 2604 OID 27627)
-- Name: action id; Type: DEFAULT; Schema: public; Owner: kabAdmin
--
ALTER TABLE ONLY public.action ALTER COLUMN id SET DEFAULT nextval('public.action_id_seq'::regclass);
--
-- TOC entry 3828 (class 2604 OID 27632)
-- Name: eventaction id; Type: DEFAULT; Schema: public; Owner: kabAdmin
--
ALTER TABLE ONLY public.eventaction ALTER COLUMN id SET DEFAULT nextval('public.eventaction_id_seq'::regclass);
--
-- TOC entry 3985 (class 0 OID 27560)
-- Dependencies: 214
-- Data for Name: action; Type: TABLE DATA; Schema: public; Owner: kabAdmin
--
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (5, '2024-01-21 15:33:50.958114', '2024-01-21 15:33:50.958114', 'エラー表示', '指定した条件でエラーメッセージを表示する', 'エラー表示', '[]', '[{"component": "ConditionInput","props": {"displayName": "条件", "modelValue":"","name": "condition","placeholder": "条件を選択または入力してください"}},{"component":"MuiltInputText","props":{"displayName":"エラーメッセージ","modelValue":"","name":"message","placeholder":"エラーメッセージを入力してください"}}]', 1, 2);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (3, '2024-01-21 15:33:50.958114', '2024-01-21 15:33:50.958114', '表示/非表示', '要素の表示/非表示を設定する', '表示/非表示', '[]', '[{"component":"FieldInput","props":{"displayName":"フィールド","modelValue":{},"name":"field","placeholder":"対象項目を選択してください"}},{"component":"SelectBox","props":{"displayName":"表示/非表示","options":["表示","非表示"],"modelValue":"","name":"show","placeholder":""}},{"component":"ConditionInput","props":{"displayName":"条件","modelValue":"","name":"condition","placeholder":"条件式を設定してください"}}]', 2, 2);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (4, '2024-01-21 15:33:50.958114', '2024-01-21 15:33:50.958114', '条件式', '指定した条件でフローを分岐させる', '条件式', '["はい","いいえ"]', '[{"component":"ConditionInput","props":{"displayName":"条件","modelValue":"","name":"condition","placeholder":"条件式を設定してください"}},{"component":"InputText","props":{"displayName":"結果(戻り値)","modelValue":"","name":"verName","placeholder":"変数名を入力してください"}}]', 3, 3);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (7, '2024-03-04 15:37:50.958114', '2024-03-04 15:37:50.958114', '空白除去', 'フィールドの空白文字を除去する', '空白除去', '[]', '[
{
"component": "FieldInput",
"props": {
"displayName": "フィールド",
"modelValue": {},
"name": "field",
"placeholder": "対象項目を選択してください"
}
},
{
"component": "SelectBox",
"props": {
"displayName": "空白種類",
"modelValue": "",
"name": "removeBlank",
"placeholder": "空白種類を選択してください",
"options": [
"半角・全角空白除去",
"全角空白除去",
"半角空白除去"
]
}
},
{
"component": "SelectBox",
"props": {
"displayName": "空白除去方法",
"modelValue": "",
"name": "removeBlank2",
"placeholder": "空白除去方法を選択してください",
"options": [
"前後空白除去",
"前空白除去",
"後空白除去"
]
}
}
]', 1, 4);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (11, '2024-04-30 15:33:50.958114', '2024-04-30 15:33:50.958114', 'メールアドレスチェック', 'フィールドの値がメールアドレスかチェックする', 'メールアドレスチェック', '[]', '[{"component":"FieldInput","props":{"displayName":"フィールド","modelValue":{},"name":"field","placeholder":"対象項目を選択してください"}},{"component":"SelectBox","props":{"displayName":"チェックする強さを選択してください","modelValue":"","name":"emailCheck","placeholder":"チェックする強さを選択してください","options":["厳格","ゆるめ"]}},{"component":"MuiltInputText","props":{"displayName":"エラーメッセージ","modelValue":"","name":"message","placeholder":"エラーメッセージを入力してください"}}]
', 4, 1);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (19, '2024-05-27 18:59:42', '2024-05-27 18:59:42', '現在日時', '現在日時を取得する', '現在日時', '[]', '[{"component":"InputText","props":{"displayName":"変数","modelValue":"","name":"verName","placeholder":""}}]', 1, 3);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (10, '2024-04-22 19:50:37', '2024-04-22 19:50:41', '属性UIテスト用', '属性UIテスト用', '属性UIテスト用', '[]', '[
{
"component": "InputText",
"props": {
"displayName": "文字入力",
"modelValue": "",
"name": "str",
"placeholder": "文字を入力してください",
"maxLength":20,
"hint":"文字列入力<br>入力ルール指定可能。ルールの設定例:[val=>!!val||''必須入力です'']",
"rules":"[val=>!!val||''必須入力です'']"
}
},
{
"component": "AppFieldSelect",
"props": {
"displayName": "フィールド選択(複数)",
"modelValue": {},
"name": "selectFields",
"placeholder": "アプリ選択後、フィールドを選んでください",
"selectType":"multiple"
}
},
{
"component": "AppFieldSelect",
"props": {
"displayName": "フィールド選択(単一)",
"modelValue": {},
"name": "selectField",
"placeholder": "アプリ選択後、フィールドを選んでください",
"selectType":"single"
}
},
{
"component": "ColorPicker",
"props": {
"displayName": "色選択",
"modelValue": "",
"name": "color",
"placeholder": "カラーを選択してください"
}
},
{
"component": "NumInput",
"props": {
"displayName": "数値入力フィールド",
"modelValue": "",
"name": "num",
"max":100,
"min":0,
"placeholder": "数値を入力してください",
"rules":"[val=>!!val ||''数値を入力してください'',val=>val<=100 && val>=1 || ''1-100の範囲内の数値を入力してください'']"
}
},
{
"component": "SelectBox",
"props": {
"displayName": "選択肢(複数)",
"modelValue": null,
"name": "options",
"placeholder": "オプションを選択する",
"selectType":"multiple",
"options":["全角記号および句読点","ひらがな","カタカナ","全角英数字","常用漢字","拡張漢字"]
}
},
{
"component": "FieldInput",
"props": {
"displayName": "文字入力フィールド選択",
"modelValue": {},
"name": "selectFields",
"placeholder": "アプリ選択後、フィールドを選んでください",
"selectType":"multiple",
"fieldTypes":["SINGLE_LINE_TEXT","MULTI_LINE_TEXT"],
"hint":"単一行、複数行文字のみ選択してください"
}
},
{
"component": "AppFieldSelect",
"props": {
"displayName": "フィールド選択(複数)",
"modelValue": {},
"name": "selectFields",
"placeholder": "単一行、複数行文字のみ選択してください",
"fieldTypes":["SINGLE_LINE_TEXT","MULTI_LINE_TEXT"],
"selectType":"multiple"
}
}
]', 10, 5);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (1, '2024-01-21 15:33:50.958114', '2024-01-21 15:33:50.958114', '必須チェック', 'フィールドの値が入力されているかチェックする', '必須チェック', '[]', '[{"component":"FieldInput","props":{"displayName":"フィールド","modelValue":{},"name":"field","placeholder":"必須項目を選択してください"}},{"component":"MuiltInputText","props":{"displayName":"エラーメッセージ","modelValue":"","name":"message","placeholder":"エラーメッセージを入力してください"}}]', 1, 1);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (12, '2024-05-07 15:33:50.958114', '2024-05-07 15:33:50.958114', '全角チェック', 'フィールドの値がすべて全角かチェックする', '全角チェック', '[]', '[
{
"component": "FieldInput",
"props": {
"displayName": "フィールド",
"modelValue": {},
"name": "field",
"placeholder": "対象項目を選択してください"
}
}
]
', 2, 1);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (6, '2024-02-29 15:33:50.958114', '2024-02-29 15:33:50.958114', 'ボタンの配置', 'ボタンをメニュー/スペースに配置する', 'ボタンの配置', '[]', '[
{
"component": "InputText",
"props": {
"displayName": "ボタン名",
"modelValue": "",
"name": "buttonName",
"placeholder": "ボタンのラベルを入力してください"
}
},
{
"component": "FieldInput",
"props": {
"displayName": "配置スペースを選択",
"modelValue": {},
"name": "space",
"fieldTypes":["SPACER"],
"placeholder": "ボタンを配置するスペースを選択"
}
},
{
"component": "SelectBox",
"props": {
"displayName": "追加位置",
"modelValue": "",
"name": "position",
"options": [
"一番右に追加する",
"一番左に追加する"
],
"placeholder": "追加位置を選択してください"
}
},
{
"component": "EventSetter",
"props": {
"displayName": "イベント名",
"modelValue": "",
"name": "eventName",
"connectProps": [
{
"key": "displayName",
"propName": "buttonName"
}
],
"placeholder": "イベント名を入力してください"
}
}
]', 4, 2);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (18, '2024-01-21 15:33:50.958114', '2024-01-21 15:33:50.958114', '値を取得する', 'フィールドの値を取得する', '値を取得する', '[]', '[{"component":"FieldInput","props":{"displayName":"フィールド","placeholder":"フィールドを選択してください","name":"field"}},{"component":"InputText","props":{"displayName":"結果(戻り値)","modelValue":"","name":"verName","placeholder":"変数名を入力してください"}}]', 3, 4);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (13, '2024-05-10 10:08:34.990297', '2024-05-10 10:08:34.990297', '文字結合', '2つのフィールドの文字を結合する', '文字結合', '[]', '[{"component":"FieldInput","props":{"displayName":"保存先フィールド","modelValue":{},"name":"saveField","placeholder":"","hint":"対象:文字列(1行),文字列(複数行),リッチエディターのいずれか。<br>未選択/不存在/型合わない時エラー表示。"}},{"component":"FieldInput","props":{"displayName":"結合元フィールド1","modelValue":{},"name":"joinField1","placeholder":"","hint":"対象外:ラベル,関連レコード一覧,スペース,罫線,グループ,テーブル,イベント的な取得対象外(例:新規追加成功前のレコード番号取得)。<br>未選択/不存在/対象外時エラー表示。"}},{"component":"FieldInput","props":{"displayName":"結合元フィールド2","modelValue":{},"name":"joinField2","placeholder":"","hint":"対象外:ラベル,関連レコード一覧,スペース,罫線,グループ,テーブル,イベント的な取得対象外(例:新規追加成功前のレコード番号取得)。<br>未選択/不存在/対象外時エラー表示。"}},{"component":"InputText","props":{"displayName":"区切り文字", "modelValue":"","name":"delimiter","placeholder":""}}]', 2, 4);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (16, '2024-05-13 04:08:44', '2024-05-13 04:08:48', 'データ処理', '指定したアプリのデータを指定した条件で集計する', 'データ処理', '[]', '[
{
"component": "AppFieldSelect",
"props": {
"displayName": "データソース",
"modelValue": {},
"name": "sources",
"placeholder": "データソースを選択してください",
"selectType": "multiple",
"fieldTypes":["CALC","CATEGORY","CREATED_TIME","DATE","DATETIME","NUMBER","SINGLE_LINE_TEXT","TIME"]
}
},
{
"component": "ConditionInput",
"props": {
"displayName": "抽出条件",
"modelValue": "",
"name": "condition",
"placeholder": "データ抽出条件を設定してください",
"sourceType": "app",
"operatorList": [
{
"label": "=",
"value": "="
},
{
"label": "!=",
"value": "!="
},
{
"label": ">",
"value": ">"
},
{
"label": ">=",
"value": ">="
},
{
"label": "<",
"value": "<"
},
{
"label": "<=",
"value": "<="
},
{
"label": "contains",
"value": "like"
}
]
}
},
{
"component": "DataProcessing",
"props": {
"displayName": "集計処理",
"modelValue": "",
"name": "verName",
"placeholder": "データのマッピング・集計処理を設定してください"
}
}
]', 5, 4);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (22, '2024-06-07 10:34:50.958114', '2024-06-07 10:34:50.958114', '半角チェック', 'フィールドの値がすべて半角かチェックする', '半角チェック', '[]', '[
{
"component": "FieldInput",
"props": {
"displayName": "フィールド",
"modelValue": {},
"name": "field",
"placeholder": "対象項目を選択してください"
}
}
]
', 3, 1);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (14, '2024-05-10 13:00:23.958114', '2024-05-10 13:00:23.958114', '文字数チェック', 'フィールドの値が指定された文字数かチェックする', '文字数チェック', '[]', '[{"component":"FieldInput","props":{"displayName":"フィールド","placeholder":"フィールドを選択してください","name":"field"}},{"component":"NumInput","props":{"displayName":"文字数チェック","name":"maxLength","placeholder":"制限したい文字数を入力してください"}},{"component":"MuiltInputText","props":{"displayName":"エラーメッセージ","modelValue":"","name":"message","placeholder":"エラーメッセージを入力してください"}}]', 6, 1);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (8, '2024-01-21 15:33:50.958114', '2024-01-21 15:33:50.958114', '正規表現チェック', 'フィールドの値が指定した正規表現かチェックする', '正規表現チェック', '[]', '[{"component":"FieldInput","props":{"displayName":"フィールド","placeholder":"フィールドを選択してください","name":"field"}},{"component":"InputText","props":{"displayName":"正規表現","name":"regExpression","placeholder":"正規表現を入力してください"}},{"component":"MuiltInputText","props":{"displayName":"エラーメッセージ","modelValue":"","name":"message","placeholder":"エラーメッセージを入力してください"}}]', 5, 1);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (23, '2024-06-07 15:37:50.958114', '2024-06-07 15:37:50.958114', 'ログインユーザー取得', 'ログインユーザーのデータを取得する', 'ログインユーザー取得', '[]', '[
{
"component":"InputText",
"props":{
"displayName":"変数にセット:",
"modelValue":"",
"name":"verName",
"placeholder":"セット先の変数名を入力してください"
}
}
]', 2, 3);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (15, '2024-05-13 01:17:24', '2024-05-13 01:17:28', 'データ更新', '指定のアプリのレコードを追加または更新する', 'データ更新', '[]', '[
{
"component": "AppSelect",
"props": {
"displayName": "連携先アプリ選択",
"modelValue": {},
"name": "sources",
"placeholder": "データ連携先のアプリを選択してください"
}
},
{
"component": "DataMapping",
"props": {
"displayName": "マッピング設定",
"modelValue": "",
"name": "dataMapping",
"placeholder": "データ連携のマッピングを設定してください",
"onlySourceSelect": false
}
}
]', 6, 4);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (9, '2024-04-08 11:21:50.958114', '2024-04-08 11:21:50.958114', '値を挿入する', 'フィールドに値を挿入する', '値を挿入する', '[]', '[{"component": "FieldInput","props": {"displayName": "フィールド選択","modelValue": {},"name": "field","placeholder": "値を挿入するフィールドを選択してください"}},{"component": "ConditionInput","props": {"displayName": "条件","modelValue": "","name": "condition","placeholder": "条件式を設定してください"}},{"component": "InputText","props": {"displayName": "挿入する値、変数","modelValue": "","name": "value","placeholder": "変数は、変数名を直接入力ください"}},{"component":"SelectBox","props":{"displayName":"値は手入力/変数","options":["手入力","変数"],"modelValue":"","name":"show","placeholder":""}}]', 4, 4);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (24, '2024-07-23 15:33:50.958114', '2024-07-23 15:33:50.958114', '編集可/不可', 'フィールドの編集可/不可を設定する', '編集可/不可', '[]', '[
{
"component": "FieldInput",
"props": {
"displayName": "フィールド",
"modelValue": {},
"name": "field",
"fieldTypes":[
"CHECK_BOX",
"DATE",
"DATETIME",
"DROP_DOWN",
"FILE",
"GROUP_SELECT",
"LINK",
"MULTI_LINE_TEXT",
"MULTI_SELECT",
"NUMBER",
"ORGANIZATION_SELECT",
"RADIO_BUTTON",
"RICH_TEXT",
"SINGLE_LINE_TEXT",
"TIME",
"USER_SELECT"
],
"placeholder": "対象項目を選択してください"
}
},
{
"component": "SelectBox",
"props": {
"displayName": "編集可/不可",
"options": [
"編集可",
"編集不可"
],
"modelValue": "",
"name": "editable",
"placeholder": ""
}
},
{
"component": "ConditionInput",
"props": {
"displayName": "条件",
"modelValue": "",
"name": "condition",
"placeholder": "条件式を設定してください"
}
}
]', 3, 2);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (21, '2024-01-21 15:33:50.958114', '2024-01-21 15:33:50.958114', 'ルックアップ更新', '更新されたときにルックアップ先を同期する', 'ルックアップ更新', '[]', '[
{
"component": "AppFieldSelect",
"props": {
"displayName": "更新先選択",
"fieldTypes": [
"lookup"
],
"hint": "更新先のルックアップフィールドを選択する",
"modelValue": {},
"name": "lookupField",
"placeholder": ""
}
},
{
"component": "ConditionInput",
"props": {
"displayName": "更新条件",
"modelValue": "",
"name": "condition",
"placeholder": "条件式を設定してください"
}
}
]', 8, 4);
INSERT INTO public.action (id, create_time, update_time, name, title, subtitle, outputpoints, property, nosort, categoryid) VALUES (2, '2024-01-21 15:33:50.958114', '2024-01-21 15:33:50.958114', '自動採番する', 'レコードIDを元に指定のフォーマットに変換する', '自動採番する', '[]', '[{"component":"FieldInput","props":{"displayName":"採番項目","modelValue":{},"name":"field","placeholder":"採番項目を選択してください"}},{"component":"InputText","props":{"displayName":"フォーマット","modelValue":"","name":"format","placeholder":"数値書式文字列を指定します","hint":"数値書式文字列記入例:<br>ゼロ埋めと桁揃えを指定することができます。<br>使用例:\"000000\" - 整数部が6桁に満たない場合に先頭をゼロで埋めて6桁にする。"}},{"component":"InputText","props":{"displayName":"前につける文字列","modelValue":"","name":"prefix","placeholder":"前につける文字列を入力してください","hint":"固定文字やJavascriptの式を`${}`内に記述することが可能です。<br>使用可能日付フォーマット関数:$format(''フォーマット式'')。<br>使用例:${$format(''yyyyMMdd'')} --現在の日付をyyyyMMdd形式を表記する"}},{"component":"InputText","props":{"displayName":"後ろにつける文字列","modelValue":"","name":"suffix","placeholder":"後ろにつける文字列を入力してください","hint":"固定文字やJavascriptの式を`${}`内に記述することが可能です。<br>使用可能日付フォーマット関数:$format(''フォーマット式'')。<br>使用例:${$format(''yyyyMMdd'')} --現在の日付をyyyyMMdd形式を表記する"}},{"component":"InputText","props":{"displayName":"結果(戻り値)","modelValue":"","name":"verName","placeholder":"変数名を入力してください","hint":"採番された番号の値を指定の変数名に格納する"}}]', 7, 4);
--
-- TOC entry 3989 (class 0 OID 27752)
-- Dependencies: 240
-- Data for Name: category; Type: TABLE DATA; Schema: public; Owner: kabAdmin
--
INSERT INTO public.category (id, categoryname, nosort, create_time, update_time) VALUES (1, '入力チェック', 1, '2024-08-19 00:00:00', '2024-08-19 00:00:00');
INSERT INTO public.category (id, categoryname, nosort, create_time, update_time) VALUES (2, '表示・動作', 2, '2024-08-19 00:00:00', '2024-08-19 00:00:00');
INSERT INTO public.category (id, categoryname, nosort, create_time, update_time) VALUES (3, 'ユーティリティ', 4, '2024-08-19 00:00:00', '2024-08-19 00:00:00');
INSERT INTO public.category (id, categoryname, nosort, create_time, update_time) VALUES (4, 'データ操作', 3, '2024-08-19 00:00:00', '2024-08-19 00:00:00');
INSERT INTO public.category (id, categoryname, nosort, create_time, update_time) VALUES (5, 'テスト', 5, '2024-08-19 00:00:00', '2024-08-19 00:00:00');
--
-- TOC entry 3987 (class 0 OID 27591)
-- Dependencies: 224
-- Data for Name: eventaction; Type: TABLE DATA; Schema: public; Owner: kabAdmin
--
INSERT INTO public.eventaction (id, create_time, update_time, eventid, actionid) VALUES (1, '2024-06-24 00:00:00', '2024-06-24 00:00:00', 'app.record.create.show', 1);
INSERT INTO public.eventaction (id, create_time, update_time, eventid, actionid) VALUES (2, '2024-06-24 00:00:00', '2024-06-24 00:00:00', 'app.record.create.submit.success', 1);
INSERT INTO public.eventaction (id, create_time, update_time, eventid, actionid) VALUES (3, '2024-06-24 00:00:00', '2024-06-24 00:00:00', 'app.record.detail.show', 1);
INSERT INTO public.eventaction (id, create_time, update_time, eventid, actionid) VALUES (4, '2024-06-24 00:00:00', '2024-06-24 00:00:00', 'app.record.detail.process.proceed', 1);
INSERT INTO public.eventaction (id, create_time, update_time, eventid, actionid) VALUES (5, '2024-06-24 00:00:00', '2024-06-24 00:00:00', 'app.record.edit.show', 1);
INSERT INTO public.eventaction (id, create_time, update_time, eventid, actionid) VALUES (6, '2024-06-24 00:00:00', '2024-06-24 00:00:00', 'app.record.edit.submit.success', 1);
INSERT INTO public.eventaction (id, create_time, update_time, eventid, actionid) VALUES (7, '2024-06-24 00:00:00', '2024-06-24 00:00:00', 'app.record.index.show', 1);
INSERT INTO public.eventaction (id, create_time, update_time, eventid, actionid) VALUES (8, '2024-06-24 00:00:00', '2024-06-24 00:00:00', 'app.record.index.edit.show', 1);
INSERT INTO public.eventaction (id, create_time, update_time, eventid, actionid) VALUES (9, '2024-06-24 00:00:00', '2024-06-24 00:00:00', 'app.record.index.edit.submit.success', 1);
--
-- TOC entry 3997 (class 0 OID 0)
-- Dependencies: 215
-- Name: action_id_seq; Type: SEQUENCE SET; Schema: public; Owner: kabAdmin
--
SELECT pg_catalog.setval('public.action_id_seq', 23, true);
--
-- TOC entry 3998 (class 0 OID 0)
-- Dependencies: 225
-- Name: eventaction_id_seq; Type: SEQUENCE SET; Schema: public; Owner: kabAdmin
--
SELECT pg_catalog.setval('public.eventaction_id_seq', 1, false);
--
-- TOC entry 3830 (class 2606 OID 27662)
-- Name: action action_pkey; Type: CONSTRAINT; Schema: public; Owner: kabAdmin
--
ALTER TABLE ONLY public.action
ADD CONSTRAINT action_pkey PRIMARY KEY (id);
--
-- TOC entry 3839 (class 2606 OID 27756)
-- Name: category category_pkey; Type: CONSTRAINT; Schema: public; Owner: kabAdmin
--
ALTER TABLE ONLY public.category
ADD CONSTRAINT category_pkey PRIMARY KEY (id);
--
-- TOC entry 3836 (class 2606 OID 27672)
-- Name: eventaction eventaction_pkey; Type: CONSTRAINT; Schema: public; Owner: kabAdmin
--
ALTER TABLE ONLY public.eventaction
ADD CONSTRAINT eventaction_pkey PRIMARY KEY (id);
--
-- TOC entry 3834 (class 2606 OID 27682)
-- Name: action name; Type: CONSTRAINT; Schema: public; Owner: kabAdmin
--
ALTER TABLE ONLY public.action
ADD CONSTRAINT name UNIQUE (name);
--
-- TOC entry 3831 (class 1259 OID 27689)
-- Name: ix_action_id; Type: INDEX; Schema: public; Owner: kabAdmin
--
CREATE INDEX ix_action_id ON public.action USING btree (id);
--
-- TOC entry 3832 (class 1259 OID 27690)
-- Name: ix_action_name; Type: INDEX; Schema: public; Owner: kabAdmin
--
CREATE INDEX ix_action_name ON public.action USING btree (name);
--
-- TOC entry 3837 (class 1259 OID 27697)
-- Name: ix_eventaction_id; Type: INDEX; Schema: public; Owner: kabAdmin
--
CREATE INDEX ix_eventaction_id ON public.eventaction USING btree (id);
--
-- TOC entry 3841 (class 2606 OID 27710)
-- Name: eventaction eventaction_actionid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: kabAdmin
--
ALTER TABLE ONLY public.eventaction
ADD CONSTRAINT eventaction_actionid_fkey FOREIGN KEY (actionid) REFERENCES public.action(id);
--
-- TOC entry 3842 (class 2606 OID 27715)
-- Name: eventaction eventaction_eventid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: kabAdmin
--
ALTER TABLE ONLY public.eventaction
ADD CONSTRAINT eventaction_eventid_fkey FOREIGN KEY (eventid) REFERENCES public.event(eventid);
--
-- TOC entry 3840 (class 2606 OID 27762)
-- Name: action fk_categoryid; Type: FK CONSTRAINT; Schema: public; Owner: kabAdmin
--
ALTER TABLE ONLY public.action
ADD CONSTRAINT fk_categoryid FOREIGN KEY (categoryid) REFERENCES public.category(id) NOT VALID;
-- Completed on 2024-08-21 09:38:00
--
-- PostgreSQL database dump complete
--

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