Compare commits

...

409 Commits

Author SHA1 Message Date
xiaozhe.ma
1626091e36 backend変更マージ 2024-11-23 18:36:01 +09:00
fa1d3b01b0 app&appversion&flowhistory&role&permission 2024-11-22 15:19:49 +09: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
xiaozhe.ma
6fff3ec006 条件設定関連No511,512障害修正 2024-07-18 17:40:17 +09:00
Yukina Mori
64e72a66d5 Merged PR 52: fix: 値挿入のコンポーネント BUG523 [値挿入]日時に挿入できない BUG517 [値挿入]対象イベント編集成功後に挿入されない の修正
fix: 値挿入のコンポーネント BUG523 [値挿入]日時に挿入できない BUG517 [値挿入]対象イベント編集成功後に挿入されない の修正

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

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

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

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

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

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

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

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

Related work items: #241
2024-07-05 08:37:54 +00:00
832d46d360 feat:lookup同期アクション 2024-07-05 17:20:51 +09:00
Yu Wang
c8f9cbda9a Merged PR 41: feat:#262ログインユーザー取得コンポーネントの追加
feat:#262ログインユーザー取得コンポーネントの追加

Related work items: #262
2024-07-05 00:11:36 +00:00
王玉
c1c265c73e feat:#262ログインユーザー取得コンポーネントの追加 2024-07-04 19:48:37 +09:00
e4800d2937 bugfix:Jsファイルでデプロイ時の不具合修正 2024-06-28 02:42:30 +09:00
550e59b4db ZCC対応の改修 2024-06-28 01:09:24 +09:00
Kanaru Tsuda
26a685b872 Merged PR 39: feat 236(全角チェック),284(半角チェック)
コンポーネントの改修

Related work items: #236, #284
2024-06-13 06:32:02 +00:00
Shohtetsu Ma
8514adf15e Merged PR 40: 変数定義の型はstring->IVarNameへ変更
変数定義の型を変更しました。
今後変数はオブジェクト型を対応可能のため、インターフェースを変更しました
下記のアクションを対応しました。
・auto-numbering.ts
・datetime-getter.ts
・value-getter.ts
・condition-action.ts
2024-06-13 06:31:23 +00:00
kanarutsuda
5f2059fd6a feat 236,284 全半角の改修コミット 2024-06-13 15:20:04 +09:00
504a76b4ac feat:変数定義類型の変更対応 2024-06-13 15:16:44 +09:00
kanarutsuda
80694ee49c feat 236,284 全半角のチェック コミット 2024-06-13 15:12:30 +09:00
kanarutsuda
493b9ca0e9 Merge branch 'dev' into feature/validation-half-width 2024-06-13 15:04:20 +09:00
kanarutsuda
55bbf50656 feat 236,284
全半角の改修コミット
2024-06-13 14:51:28 +09:00
4b27504b99 bugfix:フィールド表示障害修正 2024-06-13 13:53:43 +09:00
kanarutsuda
1d248bde43 半角チェック文字コード修正変更 2024-06-12 17:34:13 +09:00
Kanaru Tsuda
5cad10575f Merged PR 38: 半角チェック、全角チェックファイル名変更
それぞれfull widthcheck,half widthcheckをfull-widthcheck,half-widthcheckに変更しました

Related work items: #380
2024-06-12 07:39:58 +00:00
kanarutsuda
3b56c78bf1 ファイル名変更(削除) 2024-06-12 16:37:14 +09:00
kanarutsuda
6ab668f86a ファイル名の変更 2024-06-12 16:13:32 +09:00
kanarutsuda
7b1daaab33 ファイル名の変更 2024-06-12 15:55:38 +09:00
kanarutsuda
ef47912c37 feat: #380 半角チェック
指定したフィールドが半角かどうか判定する(全角が1文字でも含まれていたらエラー表示)
2024-06-12 15:25:50 +09:00
Shohtetsu Ma
140c48bcb7 Merged PR 36: 属性UIの新機能追加
1.フィールド選択UIのフィールド種別指定追加
2.スペースにボタン配置可能にする
3.選択肢UIの複数選択可能にする
4.スペース表示するため、バックエンドの改修
5.ボタンクリックイベントと項目値変更イベントのノード削除機能追加

Related work items: #241, #316, #344
2024-06-11 10:32:54 +00:00
ba0b96146e merge:DEVブランチとマージする 2024-06-11 19:22:05 +09:00
53aa5dff88 env:環境ファイル戻す 2024-06-11 18:21:18 +09:00
e52b02ec7f bugfix:アプリフィールド選択のフィールド種別指定修正 2024-06-11 18:10:04 +09:00
47dbaaf87d 環境変数戻す 2024-06-11 17:16:51 +09:00
478c751ea7 ボタン配置改修 2024-06-11 15:14:31 +09:00
4ee72a8a75 ボタン配置改修 2024-06-11 15:11:24 +09:00
Yukina Mori
e2db112080 Merged PR 33: fix: #234 フィールドに値を挿入するコンポーネントのinsert-value.tsの修正
fix: #234 フィールドに値を挿入するコンポーネントのinsert-value.tsの修正

コンポーネントの動作確認が取れていなかったため、insert-value.tsを修正いたしました。

Related work items: #234
2024-06-11 05:05:41 +00:00
Moriyukina2
3c0d572a0e fix: #234 フィールドに値を挿入するコンポーネントのinsert-value.tsのコメントの修正 2024-06-11 13:58:19 +09:00
Moriyukina2
c225ddd39d fix: #234 フィールドに値を挿入するコンポーネントのinsert-value.tsの修正
コンポーネントの動作確認が取れていなかったため、insert-value.tsを修正いたしました。
2024-06-11 13:19:17 +09:00
0e9b0ea693 feat:タブ対応するためにボタン配置対応 2024-06-11 09:57:52 +09:00
36f225a5b6 Merge remote-tracking branch 'origin/feature-delete-event' into feature/button-on-space 2024-06-11 02:24:10 +09:00
9496128e02 Merge branch 'feature-data-processing' into feature/button-on-space 2024-06-10 11:41:52 +09:00
612962cc83 merge with dev 2024-06-10 11:33:42 +09:00
52514b7197 feat:ボタンをスペースに配置 2024-06-10 11:24:10 +09:00
Kanaru Tsuda
234e55bc01 Merged PR 32: 全角チェック
指定したフィールドが全角かどうかをチェックし、全角でなければエラーを出す。
2024-06-05 06:05:07 +00:00
kanarutsuda
f4a1bc3e58 Merge remote-tracking branch 'origin/dev' into feature/full-width-check 2024-06-05 13:26:29 +09:00
Kanaru Tsuda
192174b2ca Deleted fullwidth-check.ts 2024-06-04 05:45:02 +00:00
kanarutsuda
544370688e 全角チェックのアップデート 2024-06-04 14:35:11 +09:00
Yu Wang
6a6e772e32 Merged PR 29: feat:#239文字結合コンポーネントの追加
文字結合コンポーネントの追加

Related work items: #239
2024-06-03 09:48:31 +00:00
tenraku ou
f4500a09bc Merged PR 28: 追加 フィールドの値を取得する
追加 フィールドの値を取得する

Related work items: #287
2024-06-03 09:46:32 +00:00
takuto
e7c3d3c8ad merge:action-processの整備 2024-06-03 18:30:30 +09:00
王玉
91bd72f7e0 feat:#239文字結合コンポーネントの追加,action-process onflict改修 2024-06-03 17:59:34 +09:00
王玉
2e69dc4dcf feat:#239文字結合コンポーネントの追加 2024-06-03 17:39:54 +09:00
tenraku ou
535049a188 Updated action-process.ts 2024-06-03 07:41:33 +00:00
tenraku ou
5bde55e5fc Updated value-getter.ts 2024-06-03 07:40:41 +00:00
tenraku ou
c1cad3d7a9 Renamed get-value.ts to value-getter.ts 2024-06-03 07:40:09 +00:00
tenraku ou
c378bfe20c Deleted quasar-project 2024-06-03 07:26:53 +00:00
Yukina Mori
0e0d028c24 Merged PR 26: 修正版 feat: #234 フィールドに値を挿入するコンポーネントの追加
feat: #234 フィールドに値を挿入するコンポーネントの追加

フィールドに値を挿入するコンポーネントの新規追加です。

Related work items: #234
2024-06-03 04:57:23 +00:00
Moriyukina2
3b6eed32ec feat: #234 フィールドに値を挿入するコンポーネントの追加
フィールドに値を挿入するコンポーネントの新規追加です。
2024-06-03 13:45:41 +09:00
Takuto Yoshida(タクト)
f62a7c3389 Merged PR 24: feat:現在時刻の取得
feat:現在時刻の取得

Related work items: #237
2024-06-03 03:35:41 +00:00
takuto
a161d8e2c8 feat:現在時刻の取得 2024-06-03 03:35:14 +09:00
wtl
b97888fca9 文字数チェック属性名変更 2024-05-30 15:28:55 +09:00
wtl
371ec3a133 メールチェックインターフェース更新 2024-05-30 15:14:54 +09:00
tenraku ou
711b9afaea Merged PR 23: メールアドレスチェック 文字数チェック 正規表現チェック追加
1.メールアドレスチェック email-check addin
2.文字数チェック counter-check addin
3.正規表現チェック追加 regular-check addin

Related work items: #247, #248, #250
2024-05-30 04:55:09 +00:00
wtl
00227ca713 update console delete 2024-05-29 10:14:52 +09:00
wtl
842edd6f1f update console.log delete 2024-05-29 10:05:47 +09:00
wtl
cd0c3197fa add get-value 2024-05-29 09:53:38 +09:00
wtl
7f35a91765 scriptsファイル削除環境変更 2024-05-28 10:20:22 +09:00
c44b42f498 不要ファイル削除 2024-05-28 09:44:36 +09:00
cff6ee5478 feat:メールアドレスチェック 2024-05-27 21:49:52 +09:00
wtl
40b21604f9 環境変更 2024-05-27 18:04:47 +09:00
wtl
98fcd2eb47 文字数チェック追加 2024-05-27 17:24:03 +09:00
Mouriya
c3b560dbc9 条件付きコンポーネントは'source'でappidを受け取ることができる。 2024-05-25 04:15:09 +09:00
53aadfcaaa feat:データ集計処理作成 2024-05-24 09:20:19 +09:00
wtl
dda9b7adad addNewEmailCheck 2024-05-21 16:12:40 +09:00
Mouriya
7fb3d08ccb 細部の問題の修正 2024-05-20 03:38:27 +09:00
Mouriya
cf4209333d データ処理書き込み完了 2024-05-17 23:32:14 +09:00
Mouriya
61ac281134 verNameのラッピング・オブジェクト 2024-05-17 14:41:15 +09:00
kanarutsuda
22e9094d4c feat全角チェックのソースマージ 2024-05-15 16:19:47 +09:00
kanarutsuda
a13721f63e feat:全角チェックアクション追加 2024-05-15 16:06:42 +09:00
kanarutsuda
05b9a0ce1b feat:全角入力チェックアクション追加 2024-05-15 12:14:28 +09:00
Mouriya
b25c17ab53 cssとjsを1つのファイルにまとめるためのviteプラグインを追加。 2024-05-13 16:11:52 +09:00
Mouriya
64d2cadd82 2つのデータ計算コンポーネントを追加する 2024-05-13 06:56:44 +09:00
Mouriya
371e2ee073 add vueuse dependencies 2024-05-13 06:56:03 +09:00
Mouriya
a7078b54c5 イベントに削除関数を追加し、即座に適用する 2024-05-13 01:16:09 +09:00
wtl
5663c313ea Merge branch 'dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into tenraku 2024-05-10 17:00:19 +09:00
wtl
7da9b81319 fork 2024-05-10 16:58:06 +09:00
c426bbf793 feat: numInput属性UI追加
inputText,numInputにrules設定追加、入力ルール設定可能
2024-05-09 10:49:05 +09:00
329debaab8 Merge branch 'mvp_step2_dev' into feature-colorpicker 2024-05-07 11:56:20 +09:00
Mouriya
994a0174f5 カラーピッカーと数字入力ボックスの追加 2024-05-06 20:58:06 +09:00
2846297112 アプリからフィールド選択の複数選択・単一選択対応 2024-04-30 12:54:20 +09:00
5cf60ddfdc Merge branch 'mvp_step2_dev' into feature-appfieldselect 2024-04-30 12:09:38 +09:00
0de33f04bc アプリからフィールド選択UI追加 2024-04-25 09:46:34 +09:00
472353632c 長い説明文追加 2024-04-22 22:45:03 +09:00
Mouriya
1a48fb5b20 update 2024-04-22 22:05:39 +09:00
99d3a01991 条件式のバッグ修正 2024-04-18 18:55:59 +09:00
wtl
da24972482 正規表現アクション実装 2024-04-15 18:08:22 +09:00
ecb90e7120 ダイアログに検索追加 2024-04-15 16:54:33 +09:00
wtl
784cb7a473 newActionRegularCheck 2024-03-29 17:04:39 +09:00
5349c46225 不要ファイル削除 2024-03-29 10:52:29 +09:00
3be4402239 アクションフローBugFix 2024-03-27 02:01:12 +09:00
4c482ea289 条件アクションの障害修正 2024-03-26 17:08:26 +09:00
44a73478a7 フローエディタの左パネル表示・非表示機能追加 2024-03-12 18:04:50 +09:00
bceac2f172 config変更 2024-03-04 12:53:23 +09:00
98842db343 Merge branch 'mvp_step2_dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp_step2_dev 2024-03-01 22:49:41 +09:00
03904a4e35 ボタンクリックイベント対応 2024-03-01 22:47:37 +09:00
09b3c8df47 eventaction blacklist 2024-02-28 18:53:40 +09:00
26761f6d39 add eventgroup->event 2024-02-28 18:32:57 +09:00
72608a8ffd アクションフローの不具合改修 2024-02-26 12:20:31 +09:00
d1ec123c8b ActionFlow障害修正 2024-02-25 02:52:06 +09:00
4102ff5522 変数設定を追加 2024-02-21 16:28:43 +09:00
08e857884b Merge branch 'mvp_step2_dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp_step2_dev 2024-02-17 23:35:20 +09:00
c0db2d230b 条件式比較バグ修正 2024-02-16 14:09:43 +09:00
2b9b772b39 設計書取込機能追加 2024-02-13 14:34:53 +09:00
c46e8a7047 Merge branch 'mvp_step2_back' into mvp_step2_dev 2024-02-13 14:25:49 +09:00
6e6350d6ce 設計書取込機能追加 2024-02-13 14:25:09 +09:00
1e7d553bd6 kintone excel new format 2024-02-06 17:37:55 +09:00
35ae2539cb kintone excel new format 2024-02-06 17:37:29 +09:00
wtl
a614d754f4 ErrorShowNewVer 2024-02-06 15:33:47 +09:00
46a6ba534e Merge branch 'mvp_step2_dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp_step2_dev 2024-02-03 11:56:13 +09:00
8fecde4c42 left menu 修正 2024-02-03 11:55:44 +09:00
wtl
3e73799532 New Plagin Error Show 2024-02-02 12:52:00 +09:00
3159366560 Actionアドインの開発手順作成 2024-02-01 10:58:39 +09:00
5176cff2bd action開発手順作成 2024-01-31 15:34:29 +09:00
978aa723ae action開発手順作成 2024-01-31 15:33:18 +09:00
926c338f73 Merge branch 'mvp_step2_dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp_step2_dev 2024-01-31 05:37:04 +09:00
6de60c82ba 条件エディタ実装 2024-01-31 05:22:09 +09:00
6ed17a50e5 token 15m->48h 2024-01-30 12:36:33 +09:00
5cd6d02f6e 条件エディタ追加 2024-01-22 10:52:55 +09:00
276e5e9122 condition test 2023-12-25 17:11:11 +09:00
6e75a2a524 condtion tree 2023-12-25 17:07:40 +09:00
ea6e603036 update APIException 2023-11-22 14:35:37 +09:00
edad30e264 exception の変更 2023-11-21 21:50:50 +09:00
58616100f4 Merge branch 'mvp_step2_dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp_step2_dev 2023-11-21 21:22:47 +09:00
359558bad3 add dev build mode 2023-11-21 21:22:30 +09:00
6a6554ed1f add APIException 2023-11-21 21:20:02 +09:00
e20625abdb domain切替バグ修正 2023-11-20 01:50:02 +09:00
f83dd693d5 log bugfix 2023-11-19 13:37:55 +09:00
a464297511 add api.log&errorlog->db 2023-11-19 13:24:29 +09:00
991c8e8083 add api.log&errorlog->db 2023-11-19 13:24:08 +09:00
9ea183ff2d 処理中表示追加 2023-11-15 03:13:24 +09:00
34d368b730 ダイアログ表示時snipper追加 2023-11-15 00:18:06 +09:00
17760a6926 FlowChart のレイアウト修正 2023-11-14 09:11:46 +09:00
8b9cfa34c7 Merge branch 'mvp_step2_dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp_step2_dev 2023-11-13 22:04:27 +09:00
5fb8fe53bb backend bug fix 2023-11-13 22:03:45 +09:00
5cb9375db3 add domainid->flow 2023-11-13 18:02:19 +09:00
55181f2c57 ユーザー追加API Bug fix 2023-11-10 02:34:50 +09:00
4577df371a Token無効の際再ログイン対応 2023-11-09 13:47:21 +09:00
0f154832a5 前端APIのURL参数化対応およびバックエンドのBugFix 2023-11-08 15:44:42 +09:00
5951fcc108 depoly bug fix 2023-11-04 22:54:21 +09:00
7966217ac2 add js dev model 2023-11-04 22:40:40 +09:00
64851bd51d added deploy DEV mode 2023-11-04 22:35:23 +09:00
10e584d2ac bugfix 2023-11-04 22:22:26 +09:00
b97a728624 kintone ENV bugfix 2023-11-04 22:16:23 +09:00
5a875e6853 domain bug fix 2023-11-04 22:11:32 +09:00
57a4823f61 Merge branch 'mvp_feature3_dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp_feature3_dev 2023-11-04 17:17:08 +09:00
761eb4c13e API token 2023-11-04 17:16:57 +09:00
354fc6868d remove id from localStorage 2023-11-04 17:15:17 +09:00
2538e4526f first&last name 2023-11-04 17:13:44 +09:00
26890f5b35 Merge branch 'mvp_feature3_dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp_feature3_dev 2023-11-04 15:15:44 +09:00
086b5e2621 Azure env 2023-11-04 15:15:35 +09:00
617b060869 login user 2023-11-04 15:12:27 +09:00
a782e92bd6 自動採番障害対応 2023-11-01 22:59:23 +09:00
f60f97380f domain 2023-11-01 22:56:47 +09:00
cfc416fd14 自動採番アクション追加・ドメイン追加 2023-11-01 10:47:24 +09:00
df593d2ffe modified ER 2023-10-31 03:42:25 +09:00
9cd4c8a5ab event&eventaction 2023-10-29 17:16:50 +09:00
maxiaozhe
ead6658455 floweditor 修正 2023-10-26 09:15:34 +09:00
f6d677b51f Kintone plugin 実装 2023-10-24 09:08:45 +09:00
25f05ab018 Kintoneがわファイルアップロード 2023-10-20 12:28:07 +09:00
178cf33949 deploy機能実装 2023-10-16 17:13:14 +09:00
b54c0f8022 Merge branch 'mvp_step2_dev' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp_step2_dev 2023-10-16 14:02:16 +09:00
c5cc3c1a24 UI修正 2023-10-16 14:02:01 +09:00
0b414fbfbe フロー保存の実装 2023-10-16 13:38:51 +09:00
981d7a5062 bugfix 2023-10-15 23:35:06 +09:00
33fc0b74ef createjstokintone add 2023-10-15 23:34:12 +09:00
286acc4584 upload kintone jscss file 2023-10-15 19:30:35 +09:00
cdfb1d4310 Merge branch 'maxz-real-impl' into mvp_step2_dev 2023-10-15 18:17:32 +09:00
6844652b5d ER図変更 2023-10-12 16:29:51 +09:00
9e72acf84b DB設計図追加 2023-10-12 14:10:20 +09:00
52f4af759e floweditorの動き修正 2023-10-12 09:23:19 +09:00
76457b6667 UserDomain Add 2023-10-09 15:55:58 +09:00
e1f2afa942 sqlserver->postgresql 2023-09-30 15:16:05 +09:00
8d5dff60f1 Merge branch 'maxz-real-impl' into mvp_step2_dev 2023-09-30 13:33:39 +09:00
461cd26690 add app dialog 2023-09-30 13:12:08 +09:00
4c6b2ea844 DB連動実装 2023-09-27 14:35:24 +09:00
418f45f997 Merge branch 'maxz-pinia' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into maxz-pinia 2023-09-24 00:12:49 +09:00
51ebe99d1c FlowEditorPage2 2023-09-24 00:12:42 +09:00
2f1f8a60fc Merge branch 'maxz-pinia' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into maxz-pinia 2023-09-23 23:56:01 +09:00
64795a80c7 actiontypes bug fix 2023-09-23 23:55:52 +09:00
94a17073dd flow add&update 2023-09-23 14:53:48 +00:00
7f7d625fdd backend merge 2023-09-23 06:38:00 +00:00
6902079866 add right panel with pinia 2023-09-23 15:19:53 +09:00
6aa057e590 flow api add 2023-09-21 07:22:12 +00:00
2721cd60d1 action bugfix 2023-09-18 02:54:53 +00:00
1f8d079d4d action bugfix 2023-09-18 02:54:35 +00:00
dt
f34dec1054 Merge branch 'daitian' of https://alicorn-dev@dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into daitian-pinia 2023-09-17 23:40:51 +08:00
dt
01b64f1aba Change App Selection Component 2023-09-17 23:34:24 +08:00
dt
3367ada343 Merge branch 'daitian' of https://alicorn-dev@dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into daitian-pinia 2023-09-17 21:46:21 +08:00
dt
f4ea3eaccb Merge branch 'maxz-new-step' of https://alicorn-dev@dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into daitian 2023-09-17 21:45:34 +08:00
dt
4adb8401d6 add pinia 2023-09-17 21:44:28 +08:00
df59bff6ae ActionFlow bug fix 2023-09-17 20:21:39 +09:00
3ae685a0e2 action add subtitle 2023-09-17 08:54:54 +00:00
dt
fce56e43c3 add pinia 2023-09-16 11:07:32 +08:00
dt
42618602f4 Replace ItemSelector with ActionSelec 2023-09-16 10:48:08 +08:00
c1e50736e8 action table add 2023-09-16 01:52:15 +00:00
dt
e02131846b selected change 2023-09-15 07:02:29 +08:00
dt
2c3b27d9de style change 2023-09-15 04:04:48 +08:00
dt
6ccc833f7d Remove unnecessary components, add an action bar. 2023-09-15 03:56:17 +08:00
dt
a0ecc2eee3 flow editor assembly and modification 2023-09-13 16:13:30 +08:00
59e6d33656 右側プロパティ開くとののバグfix 2023-09-11 23:14:05 +09:00
b641c729c2 bug fix 2023-09-11 22:16:14 +09:00
142cdcda38 プロパティ属性設定連動実装 2023-09-10 01:15:40 +09:00
fc2669dabf Merge remote-tracking branch 'origin/fang' into mvp_step2_dev 2023-09-09 01:25:06 +09:00
8e095b51e3 FlowChart削除メニュー追加 2023-09-08 21:17:20 +09:00
ff03490209 UI美化 2023-09-08 20:04:34 +09:00
40cd9998d0 FlowEditor初期合体 2023-09-08 14:28:45 +09:00
973ba159b4 flowChart初期実装 2023-09-08 13:31:41 +09:00
063a5af822 add right drawer 2023-09-08 03:52:18 +00:00
dt
6a06c71104 add flow editor left component 2023-09-07 07:54:53 +08:00
cccff1d16d fang create 2023-09-06 13:02:40 +00:00
dt
100d8de54f fix extra code after merge 2023-09-06 00:16:47 +08:00
dt
7c667660c0 Merge branch 'dt' into daitian
# Conflicts:
#	frontend/src/router/routes.ts
2023-09-05 23:56:39 +08:00
daitian
4eb56372a5 add FlowEditorPage 2023-09-02 08:14:24 +08:00
daitian
16edd398be add node v20, pnpm support 2023-09-02 05:40:53 +08:00
daitian
4e08159e6d add pnpm-lock file ignore 2023-09-02 05:14:34 +08:00
7a9718a6fa flow function add 2023-08-09 13:36:09 +00:00
f597f7aa5a Merge branch 'mvp-step1' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp-step1 2023-08-02 11:35:51 +00:00
0ec2b22754 App,Field,Action Select Dialogue 2023-08-02 11:35:32 +00:00
a04f7b1bd5 Updated README.md 2023-07-31 08:51:25 +00:00
2240603c2c Renamed VUE3.0コーディングルール.md to VUE3.0-coding-rule.md 2023-07-31 08:50:35 +00:00
d9a7532805 Updated README.md 2023-07-30 18:22:40 +00:00
e59f9b802b Renamed VUE3.0编程规范.md to VUE3.0コーディングルール.md 2023-07-30 18:21:51 +00:00
ad1c330231 Renamed vue3.0编程概要.md to vue3.0概要.md 2023-07-30 18:21:30 +00:00
a1905a1274 VUEコーディングルール追加 2023-07-31 03:14:31 +09:00
e515f99a44 slot test 2023-07-30 10:02:21 +00:00
d42fac9a7d component added 2023-07-30 12:35:35 +09:00
da3df6f0a7 Merge branch 'mvp-step1' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into mvp-step1 2023-07-29 22:42:09 +09:00
0bf3a1b2c8 document 追加 2023-07-29 22:41:41 +09:00
b63999c7f9 Add Dialogue 2023-07-29 13:40:36 +00:00
772ab3c6a5 Add Dialogue 2023-07-29 13:40:04 +00:00
e3c66a5bc4 document追加 2023-07-28 15:02:11 +09:00
9e510b0183 CORS追加 2023-07-26 23:51:44 +09:00
187 changed files with 27286 additions and 489 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/

4
backend/.gitignore vendored
View File

@@ -56,6 +56,7 @@ coverage.xml
# Django stuff: # Django stuff:
*.log *.log
*.log.*
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
@@ -125,4 +126,5 @@ cython_debug/
# VS Code settings # VS Code settings
.vscode/ .vscode/
*.lock *.lock
Temp/

View File

@@ -1,5 +1,5 @@
FROM python:3.8 FROM python:3.11.4
RUN mkdir /app RUN mkdir /app
WORKDIR /app WORKDIR /app

104
backend/Temp/alc_runtime.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -25,22 +25,27 @@ async def login(
minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES
) )
if user.is_superuser: if user.is_superuser:
permissions = "admin" roles = "super"
permissions = "ALL"
else: else:
permissions = "user" roles = ";".join(role.name for role in user.roles)
perlst = [perm.privilege for role in user.roles for perm in role.permissions]
permissions =";".join(list(set(perlst)))
access_token = security.create_access_token( access_token = security.create_access_token(
data={"sub": user.email, "permissions": permissions}, data={"sub": user.id, "roles":roles,"permissions": permissions ,},
expires_delta=access_token_expires, expires_delta=access_token_expires,
) )
return {"access_token": access_token, "token_type": "bearer"} return {"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name}
@r.post("/signup") @r.post("/signup")
async def signup( async def signup(
firstname:str, lastname:str,
db=Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends() db=Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
): ):
user = sign_up_new_user(db, form_data.username, form_data.password) user = sign_up_new_user(db, form_data.username, form_data.password,firstname,lastname)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=status.HTTP_409_CONFLICT,
@@ -56,8 +61,8 @@ async def signup(
else: else:
permissions = "user" permissions = "user"
access_token = security.create_access_token( access_token = security.create_access_token(
data={"sub": user.email, "permissions": permissions}, data={"sub": user.id, "permissions": permissions},
expires_delta=access_token_expires, expires_delta=access_token_expires,
) )
return {"access_token": access_token, "token_type": "bearer"} return {"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name}

View File

@@ -5,33 +5,150 @@ import pandas as pd
import json import json
import httpx import httpx
import deepdiff import deepdiff
import app.core.config as c import app.core.config as config
import os
from pathlib import Path
from app.db.session import SessionLocal
from app.db.crud import get_flows_by_app,get_activedomain,get_kintoneformat
from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
kinton_router = r = APIRouter() kinton_router = r = APIRouter()
def getfieldsfromexcel(df): def getkintoneenv(user = Depends(get_current_user)):
db = SessionLocal()
domain = get_activedomain(db, user.id)
db.close()
kintoneevn = config.KINTONE_ENV(domain)
return kintoneevn
def getkintoneformat():
db = SessionLocal()
formats = get_kintoneformat(db)
db.close()
return formats
def createkintonefields(property,value,trueformat):
p = []
if(property=="options"):
o=[]
for v in value.split(','):
o.append(f"\"{v.split('|')[0]}\":{{\"label\":\"{v.split('|')[0]}\",\"index\":\"{v.split('|')[1]}\"}}")
p.append(f"\"options\":{{{','.join(o)}}}")
elif(property =="expression"):
p.append(f"\"hideExpression\":true")
p.append(f"\"expression\":\"{value.split(':')[1]}\"")
elif(property =="required" or property =="unique" or property =="defaultNowValue" or property =="hideExpression" or property =="digit"):
if str(value) == trueformat:
p.append(f"\"{property}\":true")
else:
p.append(f"\"{property}\":false")
elif(property =="protocol"):
if(value == "メールアドレス"):
p.append("\"protocol\":\"MAIL\"")
elif(value == "Webサイト"):
p.append("\"protocol\":\"WEB\"")
elif(value == "電話番号"):
p.append("\"protocol\":\"CALL\"")
else:
p.append(f"\"{property}\":\"{value}\"")
return p
def getfieldsfromexcel(df,mapping):
startrow = mapping.startrow
startcolumn = mapping.startcolumn
typecolumn = mapping.typecolumn
codecolumn = mapping.codecolumn
property = mapping.field.split(",")
trueformat = mapping.trueformat
appname = df.iloc[0,2] appname = df.iloc[0,2]
col=[] col=[]
for row in range(5,len(df)): for row in range(startrow,len(df)):
if pd.isna(df.iloc[row,1]): if pd.isna(df.iloc[row,startcolumn]):
break break
if not df.iloc[row,3] in c.KINTONE_FIELD_TYPE: if not df.iloc[row,typecolumn] in config.KINTONE_FIELD_TYPE:
continue continue
p=[] p=[]
for column in range(1,7): for column in range(startcolumn,startcolumn + len(property)):
if(not pd.isna(df.iloc[row,column])): if(not pd.isna(df.iloc[row,column])):
if(property[column-1]=="options"): propertyname =property[column-1]
o=[] if(propertyname.find("[") == 0):
for v in df.iloc[row,column].split(','): continue
o.append(f"\"{v.split('|')[0]}\":{{\"label\":\"{v.split('|')[0]}\",\"index\":\"{v.split('|')[1]}\"}}") elif (propertyname =="remark"):
p.append(f"\"{property[column-1]}\":{{{','.join(o)}}}") if (df.iloc[row,column].find("|") !=-1):
elif(property[column-1]=="required"): propertyname = "options"
p.append(f"\"{property[column-1]}\":{df.iloc[row,column]}") p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
if (df.iloc[row,column] == "メールアドレス" or df.iloc[row,column] == "Webサイト" or df.iloc[row,column] == "電話番号"):
propertyname = "protocol"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
if (df.iloc[row,column].find("桁区切り") !=-1):
propertyname = "digit"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
if (df.iloc[row,column].find("前単位") !=-1):
propertyname = "unitPosition"
p = p + createkintonefields(propertyname, "BEFORE",trueformat)
if (df.iloc[row,column].find("後単位") !=-1):
propertyname = "unitPosition"
p = p + createkintonefields(propertyname, "AFTER",trueformat)
if (df.iloc[row,column].find("単位「") !=-1):
propertyname = "unit"
ids = df.iloc[row,column].index("単位「")
ide = df.iloc[row,column].index("")
unit = df.iloc[row,column][ids+3:ide]
p = p + createkintonefields(propertyname, unit,trueformat)
else:
continue
elif(propertyname =="mixValue"):
if(df.iloc[row,column].find("レコード登録時の日") != -1):
propertyname = "defaultNowValue"
df.iloc[row,column] = trueformat
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
elif(df.iloc[row,column].find("計:") != -1):
propertyname = "expression"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
elif(df.iloc[row,column] !=""):
propertyname = "defaultValue"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
else:
continue
elif(propertyname=="max" or propertyname == "min"):
if(df.iloc[row,typecolumn] == "NUMBER"):
propertyname = property[column-1] + "Value"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
else:
propertyname = property[column-1] + "Length"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
else: else:
p.append(f"\"{property[column-1]}\":\"{df.iloc[row,column]}\"") p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
col.append(f"\"{df.iloc[row,2]}\":{{{','.join(p)}}}")
fields = ",".join(col).replace("False","false").replace("True","true") # if(propertyname=="options"):
# o=[]
# for v in df.iloc[row,column].split(','):
# o.append(f"\"{v.split('|')[0]}\":{{\"label\":\"{v.split('|')[0]}\",\"index\":\"{v.split('|')[1]}\"}}")
# p.append(f"\"options\":{{{','.join(o)}}}")
# elif(propertyname=="expression"):
# p.append(f"\"hideExpression\":true")
# p.append(f"\"expression\":{df.iloc[row,column].split(':')[1]}")
# elif(propertyname=="required" or propertyname =="unique" or propertyname=="defaultNowValue" or propertyname=="hideExpression" or propertyname=="digit"):
# if (df.iloc[row,column] == trueformat):
# p.append(f"\"{propertyname}\":true")
# else:
# p.append(f"\"{propertyname}\":false")
# elif(propertyname =="protocol"):
# if(df.iloc[row,column] == "メールアドレス"):
# p.append("\"protocol\":\"MAIL\"")
# elif(df.iloc[row,column] == "Webサイト"):
# p.append("\"protocol\":\"WEB\"")
# elif(df.iloc[row,column] == "電話番号"):
# p.append("\"protocol\":\"CALL\"")
# else:
# p.append(f"\"{propertyname}\":\"{df.iloc[row,column]}\"")
col.append(f"\"{df.iloc[row,codecolumn]}\":{{{','.join(p)}}}")
fields = ",".join(col).replace("\\", "\\\\")
return json.loads(f"{{{fields}}}") return json.loads(f"{{{fields}}}")
def getsettingfromexcel(df): def getsettingfromexcel(df):
@@ -39,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): def getsettingfromkintone(app:str,env:config.KINTONE_ENV):
headers={c.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}{c.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()
@@ -54,60 +171,101 @@ def analysesettings(excel,kintone):
updatesettings[key] = excel[key] updatesettings[key] = excel[key]
return updatesettings return updatesettings
def createkintoneapp(name:str): def createkintoneapp(name:str,env:config.KINTONE_ENV):
headers={c.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}{c.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): def updateappsettingstokintone(app:str,updates:dict,env:config.KINTONE_ENV):
headers={c.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}{c.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,revision:str = None): def addfieldstokintone(app:str,fields:dict,env:config.KINTONE_ENV,revision:str = None):
headers={c.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}{c.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): def updatefieldstokintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV):
headers={c.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}{c.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): def deletefieldsfromkintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV):
headers={c.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}{c.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): def deoployappfromkintone(app:str,revision:str,env:config.KINTONE_ENV):
headers={c.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}{c.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): # 既定項目に含めるアプリのフィールドのみ取得する
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} # スペース、枠線、ラベルを含まない
def getfieldsfromkintone(app:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params = {"app":app} params = {"app":app}
url = f"{c.BASE_URL}{c.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,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params = {"app":app}
url = f"{env.BASE_URL}{config.API_V1_STR}/form.json"
r = httpx.get(url,headers=headers,params=params)
return r.json()
def merge_kintone_fields(fields_response: dict, form_response: dict) -> dict:
fields_properties = fields_response.get('properties', {})
form_properties = form_response.get('properties', [])
merged_properties = {k: v for k, v in fields_properties.items()}
for index, form_field in enumerate(form_properties):
code = form_field.get('code')
if code:
if code and code not in merged_properties:
merged_properties[code] = form_field
else:
element_id = form_field.get('elementId')
if element_id:
key = element_id
form_field['code']=element_id
form_field['label']=form_field.get('type')
# else:
# key = f"{form_field.get('type')}_{index}"
merged_properties[key] = form_field
merged_response = {
'revision': fields_response.get('revision', ''),
'properties': merged_properties
}
return merged_response
def analysefields(excel,kintone): def analysefields(excel,kintone):
updatefields={} updatefields={}
addfields={} addfields={}
@@ -116,22 +274,22 @@ def analysefields(excel,kintone):
adds = excel.keys() - kintone.keys() adds = excel.keys() - kintone.keys()
dels = kintone.keys() - excel.keys() dels = kintone.keys() - excel.keys()
for key in updates: for key in updates:
for p in property: for p in config.KINTONE_FIELD_PROPERTY:
if excel[key].get(p) != None and kintone[key][p] != excel[key][p]: if excel[key].get(p) != None and kintone[key].get(p) != None and kintone[key][p] != excel[key][p]:
updatefields[key] = excel[key] updatefields[key] = excel[key]
break break
for key in adds: for key in adds:
addfields[key] = excel[key] addfields[key] = excel[key]
for key in dels: for key in dels:
if kintone[key]["type"] in c.KINTONE_FIELD_TYPE: if kintone[key]["type"] in config.KINTONE_FIELD_TYPE:
delfields.append(key) delfields.append(key)
return {"update":updatefields,"add":addfields,"del":delfields} return {"update":updatefields,"add":addfields,"del":delfields}
def getprocessfromkintone(app:str): def getprocessfromkintone(app:str,env:config.KINTONE_ENV):
headers={c.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}{c.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()
@@ -195,27 +353,121 @@ def analysprocess(excel,kintone):
# return True # return True
return diff return diff
def updateprocesstokintone(app:str,process:dict): def updateprocesstokintone(app:str,process:dict,c:config.KINTONE_ENV):
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/status.json" url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/status.json"
data = {"app":app,"enable":True} data = {"app":app,"enable":True}
data.update(process) data.update(process)
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 getkintoneusers(): def getkintoneusers(c:config.KINTONE_ENV):
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}/v1/users.json" url = f"{c.BASE_URL}/v1/users.json"
r = httpx.get(url,headers=headers) r = httpx.get(url,headers=headers)
return r.json() return r.json()
def getkintoneorgs(): def getkintoneorgs(c:config.KINTONE_ENV):
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"code":c.KINTONE_USER} params = {"code":c.KINTONE_USER}
url = f"{c.BASE_URL}/v1/user/organizations.json" url = f"{c.BASE_URL}/v1/user/organizations.json"
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,env:config.KINTONE_ENV):
if (file.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
return {'fileKey':file}
upload_files = {'file': open(file,'rb')}
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
data ={'name':'file','filename':os.path.basename(file)}
url = f"{env.BASE_URL}/k/v1/file.json"
r = httpx.post(url,headers=headers,data=data,files=upload_files)
#{"name":data['filename'],'fileKey':r['fileKey']}
return r.json()
def updateappjscss(app,uploads,env:config.KINTONE_ENV):
dsjs = []
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 key in upload:
filename = os.path.basename(key)
if key.endswith('.js'):
existing_js = next((item for item in current_js
if item.get('type') == 'FILE' and item['file']['name'] == filename
), None)
if existing_js:
current_js = [item for item in current_js if item.get('type') == 'URL' or item['file'].get('name') != filename]
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
else:
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
else:
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
elif key.endswith('.css'):
existing_css = next((item for item in current_css
if item.get('type') == 'FILE' and item['file']['name'] == filename
), None)
if existing_css:
current_css = [item for item in current_css if item.get('type') == 'URL' or item['file'].get('name') != filename]
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
else:
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
#現在のJSとCSSがdsjsに追加する
dsjs.extend(current_js)
dscss.extend(current_css)
mbjs.extend(current_mobile_js)
mbcss.extend(current_mobile_css)
ds ={'js':dsjs,'css':dscss}
mb ={'js':mbjs,'css':mbcss}
data = {'app':app,'scope':'ALL','desktop':ds,'mobile':mb,'revision':customize["revision"]}
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
print(json.dumps(data))
r = httpx.put(url,headers=headers,data=json.dumps(data))
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):
scriptdir = Path(__file__).resolve().parent
rootdir = scriptdir.parent.parent.parent.parent
fpath = os.path.join(rootdir,"Temp",filename)
return fpath
def createappjs(domain_url,app):
db = SessionLocal()
flows = get_flows_by_app(db,domain_url,app)
db.close()
content={}
for flow in flows:
content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content}
js = 'const alcflow=' + json.dumps(content)
# scriptdir = Path(__file__).resolve().parent
# rootdir = scriptdir.parent.parent.parent.parent
# fpath = os.path.join(rootdir,"Temp",f"alc_setting_{app}.js")
fpath = getTempPath(f"alc_setting_{app}.js")
print(fpath)
with open(fpath,'w') as file:
file.write(js)
return fpath
@r.post("/test",) @r.post("/test",)
async def test(file:UploadFile= File(...),app:str=None): async def test(file:UploadFile= File(...),app:str=None):
if file.filename.endswith('.xlsx'): if file.filename.endswith('.xlsx'):
@@ -233,15 +485,26 @@ async def test(file:UploadFile= File(...),app:str=None):
# kintone = getfieldsfromkintone(app) # kintone = getfieldsfromkintone(app)
# fields = analysefields(excel,kintone["properties"]) # fields = analysefields(excel,kintone["properties"])
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}") raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}")
else: else:
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file") raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file")
return test return test
@r.post("/upload",) @r.post("/download",)
async def upload(files:t.List[UploadFile] = File(...)): async def download(request:Request,key,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"fileKey":key}
url = f"{c.BASE_URL}/k/v1/file.json"
r = httpx.get(url,headers=headers,params=params)
return r.json()
except Exception as e:
raise APIException('kintone:upload',request.url._url,f"Error occurred while download file.json:",e)
@r.post("/upload")
async def upload(request:Request,files:t.List[UploadFile] = File(...)):
dataframes = [] dataframes = []
for file in files: for file in files:
if file.filename.endswith('.xlsx'): if file.filename.endswith('.xlsx'):
@@ -251,61 +514,124 @@ async def upload(files:t.List[UploadFile] = File(...)):
print(df) print(df)
dataframes.append(df) dataframes.append(df)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}") raise APIException('kintone:upload',request.url._url,f"Error occurred while uploading file {file.filename}:",e)
else: else:
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file") raise APIException('kintone:upload',request.url._url, f"File {file.filename} is not an Excel file",e)
return {"files": [file.filename for file in files]} return {"files": [file.filename for file in files]}
@r.get("/allapps",) @r.post("/updatejscss")
async def allapps(): async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env:config.KINTONE_ENV = Depends(getkintoneenv)):
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} try:
url = f"{c.BASE_URL}{c.API_V1_STR}/apps.json" jscs=[]
r = httpx.get(url,headers=headers) for file in files:
return r.json() fbytes = file.file.read()
fname = file.filename
fpath = '{}\\{}'.format('Temp',fname)
fout = open(fpath,'wb')
fout.write(fbytes)
fout.close()
upload = uploadkintonefiles(fpath,env)
if upload.get('fileKey') != None:
jscs.append({ file.filename:upload['fileKey']})
appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None:
deoployappfromkintone(app,appjscs["revision"],env)
return appjscs
except Exception as 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(app:str): async def app(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} try:
url = f"{c.BASE_URL}{c.API_V1_STR}/app.json" headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params ={"id":app} url = f"{env.BASE_URL}{config.API_V1_STR}/app.json"
r = httpx.get(url,headers=headers,params=params) params ={"id":app}
return r.json() r = httpx.get(url,headers=headers,params=params)
return r.json()
except Exception as e:
raise APIException('kintone:app',request.url._url, f"Error occurred while get app({env.DOMAIN_NAME}->{app}):",e)
@r.get("/allapps")
async def allapps(request:Request,env:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{env.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
return {"apps": all_apps}
except Exception as 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(app:str): async def appfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
return getfieldsfromkintone(app) try:
return getfieldsfromkintone(app,env)
except Exception as e:
raise APIException('kintone:appfields',request.url._url, f"Error occurred while get app fileds({env.DOMAIN_NAME}->{app}):",e)
@r.get("/allfields")
async def allfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
try:
field_resp = getfieldsfromkintone(app,env)
form_resp = getformfromkintone(app,env)
return merge_kintone_fields(field_resp,form_resp)
except Exception as 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(app:str): async def appprocess(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
return getprocessfromkintone(app) try:
return getprocessfromkintone(app,env)
except Exception as e:
raise APIException('kintone:appprocess',request.url._url, f"Error occurred while get app process({env.DOMAIN_NAME}->{app}):",e)
@r.get("/alljscs") @r.get("/alljscss")
async def alljscs(app:str): async def alljscs(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} try:
url = f"{c.BASE_URL}{c.API_V1_STR}/app/customize.json" headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params = {"app":app} url = f"{env.BASE_URL}{config.API_V1_STR}/app/customize.json"
r = httpx.get(url,headers=headers,params=params) params = {"app":app}
return r.json() r = httpx.get(url,headers=headers,params=params)
return r.json()
except Exception as 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(name:str): async def createapp(request:Request,name:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} try:
data = {"name":name} headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app.json" data = {"name":name}
r = httpx.post(url,headers=headers,data=json.dumps(data)) url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app.json"
result = r.json()
if result.get("app") != None:
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/deploy.json"
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 result = r.json()
if result.get("app") != None:
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json"
data = {"apps":[result],"revert": False}
r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json
except Exception as e:
raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({env.DOMAIN_NAME}->{name}):",e)
property=["label","code","type","required","defaultValue","options"]
@r.post("/createappfromexcel",) @r.post("/createappfromexcel",)
async def createappfromexcel(files:t.List[UploadFile] = File(...)): async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv)):
try:
mapping = getkintoneformat()[format]
except Exception as e:
raise APIException('kintone:createappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
for file in files: for file in files:
if file.filename.endswith('.xlsx'): if file.filename.endswith('.xlsx'):
try: try:
@@ -315,87 +641,90 @@ async def createappfromexcel(files:t.List[UploadFile] = File(...)):
appname = df.iloc[0,2] appname = df.iloc[0,2]
desc = df.iloc[2,2] desc = df.iloc[2,2]
result = {"app":0,"revision":0,"msg":""} result = {"app":0,"revision":0,"msg":""}
fields = getfieldsfromexcel(df) fields = getfieldsfromexcel(df,mapping)
users = getkintoneusers() users = getkintoneusers(env)
orgs = getkintoneorgs() orgs = getkintoneorgs(env)
processes = getprocessfromexcel(df,users["users"], orgs["organizationTitles"]) processes = getprocessfromexcel(df,users["users"], orgs["organizationTitles"])
app = createkintoneapp(appname) app = createkintoneapp(appname,env)
if app.get("app") != None: if app.get("app") != None:
result["app"] = app["app"] result["app"] = app["app"]
app = updateappsettingstokintone(result["app"],{"description":desc}) app = updateappsettingstokintone(result["app"],{"description":desc},env)
if app.get("revision") != None: if app.get("revision") != None:
result["revision"] = app["revision"] result["revision"] = app["revision"]
app = addfieldstokintone(result["app"],fields) app = addfieldstokintone(result["app"],fields,env)
if len(processes)> 0: if len(processes)> 0:
app = updateprocesstokintone(result["app"],processes) app = updateprocesstokintone(result["app"],processes,env)
if app.get("revision") != None: if app.get("revision") != None:
result["revision"] = app["revision"] result["revision"] = app["revision"]
deoployappfromkintone(result["app"],result["revision"]) deoployappfromkintone(result["app"],result["revision"],env)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}") raise APIException('kintone:createappfromexcel',request.url._url, f"Error occurred while parsing file ({env.DOMAIN_NAME}->{file.filename}):",e)
else: else:
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file") raise APIException('kintone:createappfromexcel',request.url._url, f"File {file.filename} is not an Excel file",e)
return result return result
@r.post("/updateappfromexcel",) @r.post("/updateappfromexcel")
async def updateappfromexcel(app:str,files:t.List[UploadFile] = File(...)): async def updateappfromexcel(request:Request,app:str,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv)):
try:
mapping = getkintoneformat()[format]
except Exception as e:
raise APIException('kintone:updateappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
for file in files: for file in files:
if file.filename.endswith('.xlsx'): if file.filename.endswith('.xlsx'):
try: try:
content = await file.read() content = await file.read()
df = pd.read_excel(BytesIO(content)) df = pd.read_excel(BytesIO(content))
excel = getsettingfromexcel(df) excel = getsettingfromexcel(df)
kintone= getsettingfromkintone(app) kintone= getsettingfromkintone(app,env)
settings = analysesettings(excel,kintone) settings = analysesettings(excel,kintone)
excel = getfieldsfromexcel(df) excel = getfieldsfromexcel(df,mapping)
kintone = getfieldsfromkintone(app) kintone = getfieldsfromkintone(app,env)
users = getkintoneusers() users = getkintoneusers(env)
orgs = getkintoneorgs() orgs = getkintoneorgs(env)
exp = getprocessfromexcel(df,users["users"], orgs["organizationTitles"]) exp = getprocessfromexcel(df,users["users"], orgs["organizationTitles"])
#exp = getprocessfromexcel(df) #exp = getprocessfromexcel(df)
kinp = getprocessfromkintone(app) kinp = getprocessfromkintone(app,env)
process = analysprocess(exp,kinp) process = analysprocess(exp,kinp)
revision = kintone["revision"] revision = kintone["revision"]
fields = analysefields(excel,kintone["properties"]) fields = analysefields(excel,kintone["properties"])
result = {"app":app,"revision":revision,"msg":"No Update"} result = {"app":app,"revision":revision,"msg":"No Update"}
deploy = False deploy = False
if len(fields["update"]) > 0: if len(fields["update"]) > 0:
result = updatefieldstokintone(app,revision,fields["update"]) result = updatefieldstokintone(app,revision,fields["update"],env)
revision = result["revision"] revision = result["revision"]
deploy = True deploy = True
if len(fields["add"]) > 0: if len(fields["add"]) > 0:
result = addfieldstokintone(app,fields["add"],revision) result = addfieldstokintone(app,fields["add"],env,revision)
revision = result["revision"] revision = result["revision"]
deploy = True deploy = True
if len(fields["del"]) > 0: if len(fields["del"]) > 0:
result = deletefieldsfromkintone(app,revision,fields["del"]) result = deletefieldsfromkintone(app,revision,fields["del"],env)
revision = result["revision"] revision = result["revision"]
deploy = True deploy = True
if len(settings) > 0: if len(settings) > 0:
result = updateappsettingstokintone(app,settings) result = updateappsettingstokintone(app,settings,env)
revision = result["revision"] revision = result["revision"]
deploy = True deploy = True
if len(process)>0: if len(process)>0:
result = updateprocesstokintone(app,exp) result = updateprocesstokintone(app,exp,env)
revision = result["revision"] revision = result["revision"]
deploy = True deploy = True
if deploy: if deploy:
result = deoployappfromkintone(app,revision) result = deoployappfromkintone(app,revision,env)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}") raise APIException('kintone:updateappfromexcel',request.url._url, f"Error occurred while parsing file ({env.DOMAIN_NAME}->{file.filename}):",e)
else: else:
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file") raise APIException('kintone:updateappfromexcel',request.url._url, f"File {file.filename} is not an Excel file",e)
return result return result
@r.post("/updateprocessfromexcel",) @r.post("/updateprocessfromexcel",)
async def updateprocessfromexcel(app:str): async def updateprocessfromexcel(request:Request,app:str,env = Depends(getkintoneenv)):
try: try:
excel = getprocessfromexcel() excel = getprocessfromexcel()
kintone = getprocessfromkintone(app) kintone = getprocessfromkintone(app,env)
revision = kintone["revision"] revision = kintone["revision"]
#fields = analysefields(excel,kintone["properties"]) #fields = analysefields(excel,kintone["properties"])
result = {"app":app,"revision":revision,"msg":"No Update"} result = {"app":app,"revision":revision,"msg":"No Update"}
@@ -416,14 +745,33 @@ async def updateprocessfromexcel(app:str):
# result = updateappsettingstokintone(app,settings) # result = updateappsettingstokintone(app,settings)
# revision = result["revision"] # revision = result["revision"]
# deploy = True # deploy = True
result = updateprocesstokintone(app,excel) result = updateprocesstokintone(app,excel,env)
revision = result["revision"] revision = result["revision"]
deploy = True deploy = True
if deploy: if deploy:
result = deoployappfromkintone(app,revision) result = deoployappfromkintone(app,revision,env)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error occurred : {str(e)}") raise APIException('kintone:updateprocessfromexcel',request.url._url, f"Error occurred while update process ({env.DOMAIN_NAME}->{app}):",e)
return result return result
@r.post("/createjstokintone",)
async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
try:
jscs=[]
files=[]
files.append(createappjs(env.BASE_URL, app))
files.append(getTempPath('alc_runtime.js'))
files.append(getTempPath('alc_runtime.css'))
for file in files:
upload = uploadkintonefiles(file,env)
if upload.get('fileKey') != None:
print(upload)
jscs.append({ file :upload['fileKey']})
appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None:
deoployappfromkintone(app,appjscs["revision"],env)
return appjscs
except Exception as e:
raise APIException('kintone:createjstokintone',request.url._url, f"Error occurred while create js ({env.DOMAIN_NAME}->{app}):",e)

View File

@@ -1,11 +1,88 @@
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 AppBase, AppEdit, App,Kintone from app.db.schemas import *
from typing import List, Optional
from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
import httpx
import app.core.config as config
platform_router = r = APIRouter() 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_active_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_active_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.delete(
"/apps/{domainurl}/{appid}", response_model_exclude_none=True
)
async def apps_delete(
request: Request,
domainurl:str,
appid: str,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return delete_apps(db, domainurl,appid)
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while delete apps({domainurl}:{appid}):",e)
@r.get( @r.get(
"/appsettings/{id}", "/appsettings/{id}",
response_model=App, response_model=App,
@@ -16,9 +93,11 @@ async def appsetting_details(
id: int, id: int,
db=Depends(get_db), db=Depends(get_db),
): ):
app = get_appsetting(db, id) try:
return app app = get_appsetting(db, id)
return app
except Exception as e:
raise APIException('platform:appsettings',request.url._url,f"Error occurred while get app setting:",e)
@r.post("/appsettings", response_model=App, response_model_exclude_none=True) @r.post("/appsettings", response_model=App, response_model_exclude_none=True)
async def appsetting_create( async def appsetting_create(
@@ -26,7 +105,10 @@ async def appsetting_create(
app: AppBase, app: AppBase,
db=Depends(get_db), db=Depends(get_db),
): ):
return create_appsetting(db, app) try:
return create_appsetting(db, app)
except Exception as e:
raise APIException('platform:appsettings',request.url._url,f"Error occurred while get create app setting:",e)
@r.put( @r.put(
@@ -38,7 +120,10 @@ async def appsetting_edit(
app: AppEdit, app: AppEdit,
db=Depends(get_db), db=Depends(get_db),
): ):
return edit_appsetting(db, id, app) try:
return edit_appsetting(db, id, app)
except Exception as e:
raise APIException('platform:appsettings',request.url._url,f"Error occurred while edit app setting:",e)
@r.delete( @r.delete(
@@ -49,8 +134,10 @@ async def appsettings_delete(
id: int, id: int,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
return delete_appsetting(db, id) return delete_appsetting(db, id)
except Exception as e:
raise APIException('platform:appsettings',request.url._url,f"Error occurred while delete app setting:",e)
@r.get( @r.get(
@@ -63,5 +150,281 @@ async def kintone_data(
type: int, type: int,
db=Depends(get_db), db=Depends(get_db),
): ):
kintone = get_kintones(db, type) try:
return kintone kintone = get_kintones(db, type)
return kintone
except Exception as e:
raise APIException('platform:kintone',request.url._url,f"Error occurred while get kintone env:",e)
@r.get(
"/actions",
response_model=t.List[Action],
response_model_exclude={"id"},
response_model_exclude_none=True,
)
async def action_data(
request: Request,
db=Depends(get_db),
):
try:
actions = get_actions(db)
return actions
except Exception as e:
raise APIException('platform:actions',request.url._url,f"Error occurred while get actions:",e)
@r.get(
"/flow/{flowid}",
response_model=Flow,
response_model_exclude_none=True,
)
async def flow_details(
request: Request,
flowid: str,
db=Depends(get_db),
):
try:
app = get_flow(db, flowid)
return app
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e)
@r.get(
"/flows/{appid}",
response_model=List[Flow],
response_model_exclude_none=True,
)
async def flow_list(
request: Request,
appid: str,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
print("domain=>",domain)
flows = get_flows_by_app(db, domain.url, appid)
return flows
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
@r.post("/flow", response_model=Flow, response_model_exclude_none=True)
async def flow_create(
request: Request,
flow: FlowIn,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
return create_flow(db, domain.url, flow)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
@r.put(
"/flow/{flowid}", response_model=Flow, response_model_exclude_none=True
)
async def flow_edit(
request: Request,
flowid: str,
flow: FlowIn,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
return edit_flow(db,domain.url, flow,user.id)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
@r.delete(
"/flow/{flowid}", response_model=Flow, response_model_exclude_none=True
)
async def flow_delete(
request: Request,
flowid: str,
db=Depends(get_db),
):
try:
return delete_flow(db, flowid)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while delete flow:",e)
@r.get(
"/domains/{tenantid}",
response_model=List[Domain],
response_model_exclude_none=True,
)
async def domain_details(
request: Request,
tenantid:str,
db=Depends(get_db),
):
try:
domains = get_domains(db,tenantid)
return domains
except Exception as e:
raise APIException('platform:domains',request.url._url,f"Error occurred while get domains:",e)
@r.post("/domain", response_model=Domain, response_model_exclude_none=True)
async def domain_create(
request: Request,
domain: DomainBase,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return create_domain(db, domain,user.id)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e)
@r.put(
"/domain", response_model=Domain, response_model_exclude_none=True
)
async def domain_edit(
request: Request,
domain: DomainBase,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return edit_domain(db, domain,user.id)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e)
@r.delete(
"/domain/{id}", response_model=Domain, response_model_exclude_none=True
)
async def domain_delete(
request: Request,
id: int,
db=Depends(get_db),
):
try:
return delete_domain(db,id)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain:",e)
@r.get(
"/domain",
# response_model=List[Domain],
response_model_exclude_none=True,
)
async def userdomain_details(
request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domains = get_domain(db, userId if userId is not None else user.id)
return domains
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
@r.post(
"/domain/{userid}",
response_model_exclude_none=True,
)
async def create_userdomain(
request: Request,
userid: int,
domainids:List[int] ,
db=Depends(get_db),
):
try:
domain = add_userdomain(db, userid,domainids)
return domain
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while add user({userid}) domain:",e)
@r.delete(
"/domain/{domainid}/{userid}", response_model_exclude_none=True
)
async def userdomain_delete(
request: Request,
domainid:int,
userid: int,
db=Depends(get_db),
):
try:
return delete_userdomain(db, userid,domainid)
except Exception as e:
raise APIException('platform:delete',request.url._url,f"Error occurred while delete user({userid}) domain:",e)
@r.get(
"/activedomain",
response_model=Domain,
response_model_exclude_none=True,
)
async def get_useractivedomain(
request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
# 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
except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while get user({user.id}) activedomain:",e)
@r.put(
"/activedomain/{domainid}",
response_model_exclude_none=True,
)
async def update_activeuserdomain(
request: Request,
domainid:int,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domain = active_userdomain(db, userId if userId is not None else user.id,domainid)
return domain
except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while update user({user.id}) activedomain:",e)
@r.get(
"/events",
response_model=t.List[Event],
response_model_exclude={"id"},
response_model_exclude_none=True,
)
async def event_data(
request: Request,
db=Depends(get_db),
):
try:
events = get_events(db)
return events
except Exception as e:
raise APIException('platform:events',request.url._url,f"Error occurred while get events:",e)
@r.get(
"/eventactions/{eventid}",
response_model=t.List[Action],
response_model_exclude={"id"},
response_model_exclude_none=True,
)
async def eventactions_data(
request: Request,
eventid: str,
db=Depends(get_db),
):
try:
actions = get_eventactions(db,eventid)
return actions
except Exception as e:
raise APIException('platform:eventactions',request.url._url,f"Error occurred while get eventactions:",e)

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Request, Depends, Response, encoders from fastapi import APIRouter, Request, Depends, Response, Security, encoders
import typing as t import typing as t
from app.db.session import get_db from app.db.session import get_db
@@ -8,9 +8,11 @@ from app.db.crud import (
create_user, create_user,
delete_user, delete_user,
edit_user, edit_user,
assign_userrole,
get_roles,
) )
from app.db.schemas import UserCreate, UserEdit, User, UserOut from app.db.schemas import UserCreate, UserEdit, User, UserOut,Role
from app.core.auth import get_current_active_user, get_current_active_superuser from app.core.auth import get_current_user,get_current_active_user, get_current_active_superuser
users_router = r = APIRouter() users_router = r = APIRouter()
@@ -23,14 +25,14 @@ users_router = r = APIRouter()
async def users_list( async def users_list(
response: Response, response: Response,
db=Depends(get_db), db=Depends(get_db),
current_user=Depends(get_current_active_superuser), current_user=Depends(get_current_active_user),
): ):
""" """
Get all users Get all users
""" """
users = get_users(db) users = get_users(db,current_user.is_superuser)
# This is necessary for react-admin to work # This is necessary for react-admin to work
response.headers["Content-Range"] = f"0-9/{len(users)}" #response.headers["Content-Range"] = f"0-9/{len(users)}"
return users return users
@@ -105,3 +107,30 @@ async def user_delete(
Delete existing user Delete existing user
""" """
return delete_user(db, user_id) return delete_user(db, user_id)
@r.post("/userrole",
response_model=User,
response_model_exclude_none=True,)
async def assign_role(
request: Request,
userid:int,
roles:t.List[int],
db=Depends(get_db)
):
return assign_userrole(db,userid,roles)
@r.get(
"/roles",
response_model=t.List[Role],
response_model_exclude_none=True,
)
async def roles_list(
response: Response,
db=Depends(get_db),
current_user=Security(get_current_active_user, scopes=["role_list"]),
):
roles = get_roles(db)
return roles

View File

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

View File

@@ -1,13 +1,14 @@
from fastapi.security import SecurityScopes
import jwt import jwt
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, Request, Security, status
from jwt import PyJWTError from jwt import PyJWTError
from app.db import models, schemas, session from app.db import models, schemas, session
from app.db.crud import get_user_by_email, create_user from app.db.crud import get_user_by_email, create_user,get_user
from app.core import security from app.core import security
async def get_current_user( async def get_current_user(security_scopes: SecurityScopes,
db=Depends(session.get_db), token: str = Depends(security.oauth2_scheme) db=Depends(session.get_db), token: str = Depends(security.oauth2_scheme)
): ):
credentials_exception = HTTPException( credentials_exception = HTTPException(
@@ -16,17 +17,25 @@ async def get_current_user(
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try: try:
payload = jwt.decode( payload = jwt.decode(
token, security.SECRET_KEY, algorithms=[security.ALGORITHM] token, security.SECRET_KEY, algorithms=[security.ALGORITHM]
) )
email: str = payload.get("sub") id: int = payload.get("sub")
if email is None: if id is None:
raise credentials_exception raise credentials_exception
permissions: str = payload.get("permissions") permissions: str = payload.get("permissions")
token_data = schemas.TokenData(email=email, permissions=permissions) if not permissions =="ALL":
for scope in security_scopes.scopes:
if scope not in permissions.split(";"):
raise HTTPException(
status_code=403, detail="The user doesn't have enough privileges"
)
token_data = schemas.TokenData(id = id, permissions=permissions)
except PyJWTError: except PyJWTError:
raise credentials_exception raise credentials_exception
user = get_user_by_email(db, token_data.email) user = get_user(db, token_data.id)
if user is None: if user is None:
raise credentials_exception raise credentials_exception
return user return user
@@ -58,7 +67,7 @@ def authenticate_user(db, email: str, password: str):
return user return user
def sign_up_new_user(db, email: str, password: str): def sign_up_new_user(db, email: str, password: str, firstname: str,lastname: str):
user = get_user_by_email(db, email) user = get_user_by_email(db, email)
if user: if user:
return False # User already exists return False # User already exists
@@ -67,6 +76,8 @@ def sign_up_new_user(db, email: str, password: str):
schemas.UserCreate( schemas.UserCreate(
email=email, email=email,
password=password, password=password,
first_name = firstname,
last_name = lastname,
is_active=True, is_active=True,
is_superuser=False, is_superuser=False,
), ),

View File

@@ -1,18 +1,42 @@
import os import os
import base64
PROJECT_NAME = "KintoneAppBuilder" PROJECT_NAME = "KintoneAppBuilder"
SQLALCHEMY_DATABASE_URI = "mssql+pymssql://maxz64@maxzdb:m@xz1205@maxzdb.database.windows.net/alloc" #SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/dev"
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://kabAdmin:P%40ssw0rd!@kintonetooldb.postgres.database.azure.com/dev_v2"
BASE_URL = "https://mfu07rkgnb7c.cybozu.com" #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"
API_V1_AUTH_VALUE = "TVhaOm1heHoxMjA1" DEPLOY_MODE = "PROD" #DEV,PROD
KINTONE_USER = "MXZ" DEPLOY_JS_URL = "https://ka-addin.azurewebsites.net/alc_runtime.js"
KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"] KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"]
KINTONE_FIELD_PROPERTY=['label','code','type','required','unique','maxValue','minValue','maxLength','minLength','defaultValue','defaultNowValue','options','expression','hideExpression','digit','protocol','displayScale','unit','unitPosition']
KINTONE_PSW_CRYPTO_KEY=bytes.fromhex("53 6c 93 bd 48 ad b5 c0 93 df a1 27 25 a1 a3 32 a2 03 3b a0 27 1f 51 dc 20 0e 6c d7 be fc fb ea")
class KINTONE_ENV:
BASE_URL = ""
API_V1_AUTH_VALUE = ""
KINTONE_USER = ""
DOMAIN_ID = ""
DOMAIN_NAME =""
def __init__(self,domain) -> None:
self.DOMAIN_NAME=domain.name
self.DOMAIN_ID=domain.id
self.BASE_URL = domain.url
self.KINTONE_USER = domain.kintoneuser
self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.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")
@@ -9,7 +13,7 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = "alicorns" SECRET_KEY = "alicorns"
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 ACCESS_TOKEN_EXPIRE_MINUTES = 2880
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
@@ -25,7 +29,36 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
if expires_delta: if expires_delta:
expire = datetime.utcnow() + expires_delta expire = datetime.utcnow() + expires_delta
else: else:
expire = datetime.utcnow() + timedelta(minutes=15) expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
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

@@ -1,9 +1,11 @@
import datetime
from fastapi import HTTPException, status from fastapi import HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
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):
@@ -18,9 +20,12 @@ def get_user_by_email(db: Session, email: str) -> schemas.UserBase:
def get_users( def get_users(
db: Session, skip: int = 0, limit: int = 100 db: Session, super:bool
) -> t.List[schemas.UserOut]: ) -> t.List[schemas.UserOut]:
return db.query(models.User).offset(skip).limit(limit).all() if super:
return db.query(models.User).all()
else:
return db.query(models.User).filter(models.User.is_superuser == False)
def create_user(db: Session, user: schemas.UserCreate): def create_user(db: Session, user: schemas.UserCreate):
@@ -69,6 +74,80 @@ def edit_user(
return db_user return db_user
def get_roles(
db: Session
) -> t.List[schemas.Role]:
return db.query(models.Role).all()
def assign_userrole( db: Session, user_id: int, roles: t.List[int]):
db_user = db.query(models.User).get(user_id)
if db_user:
for role in db_user.roles:
db_user.roles.remove(role)
for roleid in roles:
role = db.query(models.Role).get(roleid)
if role:
db_user.roles.append(role)
db.commit()
db.refresh(db_user)
return db_user
def get_apps(
db: Session,
domainurl:str
) -> t.List[schemas.AppList]:
return db.query(models.App).filter(models.App.domainurl == domainurl).all()
def update_appversion(db: Session, appedit: schemas.AppVersion,userid:int):
db_app = db.query(models.App).filter(and_(models.App.domainurl == appedit.domainurl,models.App.appid == appedit.appid)).first()
if not db_app:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="User not found")
db_app.version = db_app.version + 1
appversion = models.AppVersion(
domainurl = appedit.domainurl,
appid=appedit.appid,
appname=db_app.appname,
version = db_app.version,
versionname = appedit.versionname,
comment = appedit.comment,
updateuserid = userid,
createuserid = userid
)
db.add(appversion)
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 = db_app.version,
updateuserid = userid,
createuserid = userid
)
db.add(db_flowhistory)
db.commit()
db.refresh(db_app)
return db_app
def delete_apps(db: Session, domainurl: str,appid: str ):
db_app = db.query(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid ==appid)).first()
if not db_app:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="App not found")
db.delete(db_app)
db_flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid ==appid))
for flow in db_flows:
db.delete(flow)
db.commit()
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)
if not app: if not app:
@@ -115,4 +194,247 @@ def get_kintones(db: Session, type: int):
kintones = db.query(models.Kintone).filter(models.Kintone.type == type).all() kintones = db.query(models.Kintone).filter(models.Kintone.type == type).all()
if not kintones: if not kintones:
raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
return kintones return kintones
def get_actions(db: Session):
actions = db.query(models.Action).all()
if not actions:
raise HTTPException(status_code=404, detail="Data not found")
return actions
def create_flow(db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
db_flow = models.Flow(
flowid=flow.flowid,
appid=flow.appid,
eventid=flow.eventid,
domainurl=domainurl,
name=flow.name,
content=flow.content,
createuserid = userid,
updateuserid = userid
)
db.add(db_flow)
db_app = db.query(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid)).first()
if not db_app:
db_app = models.App(
domainurl = domainurl,
appid=flow.appid,
appname=flow.appname,
version = 0,
createuserid= userid,
updateuserid = userid
)
db.commit()
db.refresh(db_flow)
return db_flow
def delete_flow(db: Session, flowid: str):
flow = get_flow(db, flowid)
if not flow:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Flow not found")
db.delete(flow)
db.commit()
return flow
def edit_flow(
db: Session, domainurl: str, flow: schemas.FlowIn,userid:int
) -> schemas.Flow:
db_flow = get_flow(db, flow.flowid)
if not db_flow:
#見つからない時新規作成
return create_flow(db,domainurl,flow,userid)
db_flow.appid =flow.appid,
db_flow.eventid=flow.eventid,
db_flow.domainurl=domainurl,
db_flow.name=flow.name,
db_flow.content=flow.content,
db_flow.updateuserid = userid,
db_flow.update_time = datetime.now
db.add(db_flow)
db.commit()
db.refresh(db_flow)
return db_flow
def get_flows(db: Session, flowid: str):
flows = db.query(models.Flow).all()
if not flows:
raise HTTPException(status_code=404, detail="Data not found")
return flows
def get_flow(db: Session, flowid: str):
flow = db.query(models.Flow).filter(models.Flow.flowid == flowid).first()
# if not flow:
# raise HTTPException(status_code=404, detail="Data not found")
return flow
def get_flows_by_app(db: Session,domainurl: str, appid: str):
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid == appid)).all()
if not flows:
raise Exception("Data not found")
return flows
def create_domain(db: Session, domain: schemas.DomainBase,userid:int):
domain.encrypt_kintonepwd()
db_domain = models.Domain(
tenantid = domain.tenantid,
name=domain.name,
url=domain.url,
kintoneuser=domain.kintoneuser,
kintonepwd=domain.kintonepwd,
createuserid = userid,
updateuserid = userid
)
db.add(db_domain)
db.flush()
add_userdomain(db,userid,db_domain.id)
db.commit()
db.refresh(db_domain)
return db_domain
def delete_domain(db: Session,id: int):
db_domain = db.query(models.Domain).get(id)
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db.delete(db_domain)
db.commit()
return db_domain
def edit_domain(
db: Session, domain: schemas.DomainBase,userid:int
) -> schemas.Domain:
domain.encrypt_kintonepwd()
db_domain = db.query(models.Domain).get(domain.id)
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db_domain.tenantid = domain.tenantid
db_domain.name=domain.name
db_domain.url=domain.url
db_domain.kintoneuser=domain.kintoneuser
db_domain.kintonepwd = domain.kintonepwd
db_domain.updateuserid = userid
db_domain.update_time = datetime.now
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
def add_userdomain(db: Session, userid:int,domainid:int):
user_domain = models.UserDomain(userid = userid, domainid = domainid )
db.add(user_domain)
return user_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()
return dbCommits
def delete_userdomain(db: Session, userid: int,domainid: int):
db_domain = db.query(models.UserDomain).filter(and_(models.UserDomain.userid == userid,models.UserDomain.domainid == domainid)).first()
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db.delete(db_domain)
db.commit()
return db_domain
def active_userdomain(db: Session, userid: int,domainid: int):
db_userdomains = db.query(models.UserDomain).filter(models.UserDomain.userid == userid).all()
if not db_userdomains:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
for domain in db_userdomains:
if domain.domainid == domainid:
domain.active = True
else:
domain.active = False
db.add(domain)
db.commit()
return db_userdomains
def get_activedomain(db: Session, userid: int)-> t.Optional[models.Domain]:
user_domains = (db.query(models.Domain,models.UserDomain.active)
.join(models.UserDomain,models.UserDomain.domainid == models.Domain.id )
.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
def get_domain(db: Session, userid: str):
domains = db.query(models.Domain).join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ).filter(models.UserDomain.userid == userid).all()
# if not domains:
# raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
return domains
def get_domains(db: Session,tenantid:str):
domains = db.query(models.Domain).filter(models.Domain.tenantid == tenantid ).all()
if not domains:
raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
return domains
def get_events(db: Session):
events = db.query(models.Event).all()
if not events:
raise HTTPException(status_code=404, detail="Data not found")
return events
def get_category(db:Session):
categorys=db.query(models.Category).all()
return categorys
def get_eventactions(db: Session,eventid: str):
#eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid == models.Action.id ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all()
#category = get_category(db)
blackactions = (
db.query(models.EventAction.actionid)
.filter(models.EventAction.eventid == eventid)
.subquery()
)
eveactions = (
db.query(
models.Action.id,
models.Action.name,
models.Action.title,
models.Action.subtitle,
models.Action.outputpoints,
models.Action.property,
models.Action.categoryid,
models.Action.nosort,
models.Category.categoryname)
.join(models.Category,models.Category.id == models.Action.categoryid)
.filter(models.Action.id.notin_(blackactions))
.order_by(models.Category.nosort,models.Action.nosort)
.all()
)
if not eveactions:
raise HTTPException(status_code=404, detail="Data not found")
return eveactions
def create_log(db: Session, error:schemas.ErrorCreate):
db_log = models.ErrorLog(title=error.title,location=error.location,content=error.content)
db.add(db_log)
db.commit()
db.refresh(db_log)
return db_log
def get_kintoneformat(db: Session):
formats = db.query(models.KintoneFormat).order_by(models.KintoneFormat.id).all()
return formats

View File

@@ -1,31 +1,221 @@
from sqlalchemy import Boolean, Column, Integer, String from sqlalchemy import Boolean, Column, Integer, String, DateTime,ForeignKey,Table
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.session import Base
from app.core.security import chacha20Decrypt
from .session import Base @as_declarative()
class Base:
id = Column(Integer, primary_key=True, index=True)
create_time = Column(DateTime, default=datetime.now)
update_time = Column(DateTime, default=datetime.now, onupdate=datetime.now)
userrole = Table(
"userrole",
Base.metadata,
Column("userid",Integer,ForeignKey("user.id")),
Column("roleid",Integer,ForeignKey("role.id")),
)
rolepermission = Table(
"rolepermission",
Base.metadata,
Column("roleid",Integer,ForeignKey("role.id")),
Column("permissionid",Integer,ForeignKey("permission.id")),
)
class User(Base): class User(Base):
__tablename__ = "user" __tablename__ = "user"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(50), unique=True, index=True, nullable=False) email = Column(String(50), unique=True, index=True, nullable=False)
first_name = Column(String(100)) first_name = Column(String(100))
last_name = Column(String(100)) last_name = Column(String(100))
hashed_password = Column(String(200), nullable=False) hashed_password = Column(String(200), nullable=False)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False) is_superuser = Column(Boolean, default=False)
roles = relationship("Role",secondary=userrole,back_populates="users")
class Role(Base):
__tablename__ = "role"
name = Column(String(100))
description = Column(String(255))
users = relationship("User",secondary=userrole,back_populates="roles")
permissions = relationship("Permission",secondary=rolepermission,back_populates="roles")
class Permission(Base):
__tablename__ = "permission"
menu = Column(String(100))
function = Column(String(255))
privilege = Column(String(100))
roles = relationship("Role",secondary=rolepermission,back_populates="permissions")
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)
createuserid = Column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid])
class AppVersion(Base):
__tablename__ = "appversion"
domainurl = Column(String(200), nullable=False)
appname = Column(String(200), nullable=False)
appid = Column(String(100), index=True, nullable=False)
version = Column(Integer)
versionname = Column(String(200), nullable=False)
comment = Column(String(200), nullable=False)
createuserid = Column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid])
class AppSetting(Base): class AppSetting(Base):
__tablename__ = "appsetting" __tablename__ = "appsetting"
id = Column(Integer, primary_key=True, index=True)
appid = Column(String(100), index=True, nullable=False) appid = Column(String(100), index=True, nullable=False)
setting = Column(String(1000)) setting = Column(String(1000))
class Kintone(Base): class Kintone(Base):
__tablename__ = "kintone" __tablename__ = "kintone"
id = Column(Integer, primary_key=True, index=True)
type = Column(Integer, index=True, nullable=False) type = Column(Integer, index=True, nullable=False)
name = Column(String(100), nullable=False) name = Column(String(100), nullable=False)
desc = Column(String(500)) desc = Column(String)
content = Column(String(2000)) content = Column(String)
class Action(Base):
__tablename__ = "action"
name = Column(String(100), index=True, nullable=False)
title = Column(String(200))
subtitle = Column(String(500))
outputpoints = Column(String)
property = Column(String)
categoryid = Column(Integer,ForeignKey("category.id"))
nosort = Column(Integer)
class Flow(Base):
__tablename__ = "flow"
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)
createuserid = Column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid])
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)
version = Column(Integer)
createuserid = Column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid])
class Tenant(Base):
__tablename__ = "tenant"
tenantid = Column(String(100), index=True, nullable=False)
name = Column(String(200))
licence = Column(String(200))
startdate = Column(DateTime)
enddate = Column(DateTime)
class Domain(Base):
__tablename__ = "domain"
tenantid = Column(String(100), index=True, nullable=False)
name = Column(String(100), nullable=False)
url = Column(String(200), nullable=False)
kintoneuser = Column(String(100), nullable=False)
kintonepwd = Column(String(100), nullable=False)
def decrypt_kintonepwd(self):
decrypted_pwd = chacha20Decrypt(self.kintonepwd)
return decrypted_pwd
createuserid = Column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid])
class UserDomain(Base):
__tablename__ = "userdomain"
userid = Column(Integer,ForeignKey("user.id"))
domainid = Column(Integer,ForeignKey("domain.id"))
active = Column(Boolean, default=False)
class Event(Base):
__tablename__ = "event"
category = Column(String(100), nullable=False)
type = Column(String(100), nullable=False)
eventid= Column(String(100), nullable=False)
function = Column(String(500), nullable=False)
mobile = Column(Boolean, default=False)
eventgroup = Column(Boolean, default=False)
class EventAction(Base):
__tablename__ = "eventaction"
eventid = Column(String(100),ForeignKey("event.eventid"))
actionid = Column(Integer,ForeignKey("action.id"))
class ErrorLog(Base):
__tablename__ = "errorlog"
title = Column(String(50))
location = Column(String(500))
content = Column(String(5000))
class OperationLog(Base):
__tablename__ = "operationlog"
tenantid = Column(String(100))
domainurl = Column(String(200))
userid = Column(Integer,ForeignKey("user.id"))
operation = Column(String(200))
function = Column(String(200))
detail = Column(String(200))
user = relationship('User')
class KintoneFormat(Base):
__tablename__ = "kintoneformat"
name = Column(String(50))
startrow =Column(Integer)
startcolumn =Column(Integer)
typecolumn =Column(Integer)
codecolumn =Column(Integer)
field = Column(String(5000))
trueformat = Column(String(10))
class Category(Base):
__tablename__ = "category"
categoryname = Column(String(20))
nosort = Column(Integer)

View File

@@ -1,37 +1,61 @@
from pydantic import BaseModel from pydantic import BaseModel
from datetime import datetime
import typing as t import typing as t
from app.core.security import chacha20Decrypt, chacha20Encrypt
class Base(BaseModel):
create_time: datetime
update_time: datetime
class Permission(BaseModel):
id: int
menu:str
function:str
privilege:str
class Role(BaseModel):
id: int
name:str
description:str
permissions:t.List[Permission] = []
class UserBase(BaseModel): class UserBase(BaseModel):
email: str email: str
is_active: bool = True is_active: bool = True
is_superuser: bool = False is_superuser: bool = False
first_name: str = None first_name: str = None
last_name: str = None last_name: str = None
roles:t.List[Role] = []
class UserOut(UserBase): class UserOut(UserBase):
pass pass
class UserCreate(UserBase): class UserCreate(UserBase):
email:str
password: str password: str
first_name: str
last_name: str
is_active: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
@@ -39,8 +63,23 @@ class Token(BaseModel):
access_token: str access_token: str
token_type: str token_type: str
class AppList(Base):
domainurl: str
appname: str
appid:str
updateuser: UserOut
version:int
class AppVersion(BaseModel):
domainurl: str
appname: str
versionname: str
comment:str
appid:str
class TokenData(BaseModel): class TokenData(BaseModel):
id:int = 0
email: str = None email: str = None
permissions: str = "user" permissions: str = "user"
@@ -56,7 +95,7 @@ class AppBase(BaseModel):
class App(AppBase): class App(AppBase):
id: int id: int
class Config: class ConfigDict:
orm_mode = True orm_mode = True
@@ -67,5 +106,78 @@ 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):
id: int
name: str = None
title: str = None
subtitle: str = None
outputpoints: str = None
property: str = None
categoryid: int = None
nosort: int
categoryname : str =None
class ConfigDict:
orm_mode = True
class FlowIn(BaseModel):
flowid: str
# domainurl:str
appid: str
appname:str
eventid: str
name: str = None
content: str = None
class Flow(Base):
id: int
flowid: str
appid: str
eventid: str
domainurl: str
name: str = None
content: str = None
class ConfigDict:
orm_mode = True
class DomainBase(BaseModel):
id: int
tenantid: str
name: str
url: str
kintoneuser: str
kintonepwd: str
def encrypt_kintonepwd(self):
encrypted_pwd = chacha20Encrypt(self.kintonepwd)
self.kintonepwd = encrypted_pwd
class Domain(Base):
id: int
tenantid: str
name: str
url: str
kintoneuser: str
kintonepwd: str
class ConfigDict:
orm_mode = True
class Event(Base):
id: int
category: str
type: str
eventid: str
function: str
mobile: bool
eventgroup: bool
class ConfigDict:
orm_mode = True
class ErrorCreate(BaseModel):
title:str
location:str
content:str

View File

@@ -1,3 +1,4 @@
import os
from fastapi import FastAPI, Depends from fastapi import FastAPI, Depends
from starlette.requests import Request from starlette.requests import Request
import uvicorn import uvicorn
@@ -10,7 +11,12 @@ from app.db import Base,engine
from app.core.auth import get_current_active_user from app.core.auth import get_current_active_user
from app.core.celery_app import celery_app from app.core.celery_app import celery_app
from app import tasks from app import tasks
from fastapi.middleware.cors import CORSMiddleware
import logging
from app.core.apiexception import APIException, writedblog
from app.db.crud import create_log
from fastapi.responses import JSONResponse
import asyncio
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
@@ -18,6 +24,17 @@ app = FastAPI(
title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api" title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api"
) )
origins = [
"*"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# @app.middleware("http") # @app.middleware("http")
# async def db_session_middleware(request: Request, call_next): # async def db_session_middleware(request: Request, call_next):
@@ -26,6 +43,25 @@ app = FastAPI(
# request.state.db.close() # request.state.db.close()
# return response # return response
@app.on_event("startup")
async def startup_event():
log_dir="log"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
logger = logging.getLogger("uvicorn.access")
handler = logging.handlers.RotatingFileHandler(f"{log_dir}/api.log",mode="a",maxBytes = 100*1024, backupCount = 3)
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(handler)
@app.exception_handler(APIException)
async def api_exception_handler(request: Request, exc: APIException):
loop = asyncio.get_event_loop()
loop.run_in_executor(None,writedblog,exc)
return JSONResponse(
status_code=exc.status_code,
content={"detail": f"{exc.detail}"},
)
@app.get("/api/v1") @app.get("/api/v1")
async def root(): async def root():

View File

@@ -24,4 +24,8 @@ python -m venv env
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
4. backend 起動
```bash
uvicorn app.main:app --reload
```

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,147 @@
<mxfile host="app.diagrams.net" modified="2024-02-21T05:42:02.026Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" etag="T2S5cjvthSOlO5DmGw-C" version="23.1.5" type="device">
<diagram id="Z6uZM46JtkVaKDzPjE9h" name="サイトマップ">
<mxGraphModel dx="1434" dy="820" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Gi77RX5G2m4J9-6cMje4-14" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-1" target="Gi77RX5G2m4J9-6cMje4-13" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-1" value="テナント登録" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.login;" parent="1" vertex="1">
<mxGeometry x="60" y="50" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-2" value="Admin Login" style="html=1;whiteSpace=wrap;strokeColor=#2D7600;fillColor=#60a917;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.login;" parent="1" vertex="1">
<mxGeometry x="60" y="270" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-5" target="Gi77RX5G2m4J9-6cMje4-7" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-5" target="Gi77RX5G2m4J9-6cMje4-7" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-5" target="Gi77RX5G2m4J9-6cMje4-11" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-5" value="Home" style="html=1;whiteSpace=wrap;strokeColor=#2D7600;fillColor=#60a917;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="240" y="270" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-2" target="Gi77RX5G2m4J9-6cMje4-5" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-7" target="Gi77RX5G2m4J9-6cMje4-41" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-7" value="ユーザー登録" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="440" y="220" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-11" value="ドメイン登録" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="440" y="340" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-13" value="テナント管理者作成" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.login;" parent="1" vertex="1">
<mxGeometry x="240" y="50" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-15" value="ライセンス情報" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1">
<mxGeometry x="480" y="10" width="90" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-16" value="Adminユーザー" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1">
<mxGeometry x="480" y="90" width="90" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-13" target="Gi77RX5G2m4J9-6cMje4-15" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-13" target="Gi77RX5G2m4J9-6cMje4-16" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-19" value="テナントDB&lt;br&gt;作成" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1">
<mxGeometry x="550" y="50" width="90" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-20" target="Gi77RX5G2m4J9-6cMje4-21" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-20" value="Login" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;sketch=0;shape=mxgraph.sitemap.login;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="50" y="610" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-24" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-21" target="Gi77RX5G2m4J9-6cMje4-25" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="430" y="645" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-21" value="Home" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="230" y="610" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-27" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-25" target="Gi77RX5G2m4J9-6cMje4-26" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-25" value="アプリ一覧" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
<mxGeometry x="440" y="610" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-29" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-26" target="Gi77RX5G2m4J9-6cMje4-28" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-26" value="フロー一覧" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
<mxGeometry x="620" y="610" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-40" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-28" target="Gi77RX5G2m4J9-6cMje4-39" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-28" value="フローエディタ" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
<mxGeometry x="800" y="610" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-33" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-30" target="Gi77RX5G2m4J9-6cMje4-32" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-30" value="設計書取込" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
<mxGeometry x="440" y="715" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-21" target="Gi77RX5G2m4J9-6cMje4-30" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-32" value="取込結果表示" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
<mxGeometry x="620" y="715" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-37" value="設計書ダウロード" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
<mxGeometry x="440" y="825" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-38" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-21" target="Gi77RX5G2m4J9-6cMje4-37" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-39" value="フロー履歴管理" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
<mxGeometry x="980" y="610" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-41" value="ALC設定" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="620" y="220" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-43" value="管理ドメイン設定" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="800" y="220" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-44" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-41" target="Gi77RX5G2m4J9-6cMje4-43" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-45" value="プロファイル" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="440" y="935" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-46" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-21" target="Gi77RX5G2m4J9-6cMje4-45" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-50" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-47" target="Gi77RX5G2m4J9-6cMje4-49" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-47" value="プロファイル" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="440" y="450" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-48" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-5" target="Gi77RX5G2m4J9-6cMje4-47" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-49" value="ライセンス情報" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="620" y="450" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-51" value="ライセンス情報" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
<mxGeometry x="620" y="935" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="Gi77RX5G2m4J9-6cMje4-52" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-45" target="Gi77RX5G2m4J9-6cMje4-51" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +1,6 @@
KAB_BACKEND_URL="http://127.0.0.1:8000/api/v1/" #開発環境
#KAB_BACKEND_URL="https://kab-backend.azurewebsites.net/"
#単体テスト環境
#KAB_BACKEND_URL="https://kab-backend-unittest.azurewebsites.net/"
#ローカル開発環境
KAB_BACKEND_URL="http://127.0.0.1:8000/"

View File

@@ -1,2 +1,2 @@
VUE_BACKEND_URL="http://localhost:8000/api/" #KAB_BACKEND_URL="https://kab-backend.azurewebsites.net/"
KAB_BACKEND_URL="http://127.0.0.1:8000/"

3
frontend/.gitignore vendored
View File

@@ -35,3 +35,6 @@ yarn-error.log*
# local .env files # local .env files
.env.local* .env.local*
# pnpm
pnpm-lock.yaml

View File

@@ -47,3 +47,7 @@ quasar build
### Customize the configuration ### Customize the configuration
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js). See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
## VUE3.0编程规范
1. [VUE3.0编程概要](./VUE3.0概要.md)
2. [VUE3.0编程规范](./VUE3.0-coding-rule.md)

View File

@@ -0,0 +1,241 @@
以下是一份 Vue 3 和 TypeScript 的编程规范:
**1. 组件定义**
- 推荐使用 `defineComponent` 来定义组件,以获取 TypeScript 的类型支持。
```typescript
import { defineComponent } from 'vue'
export default defineComponent({
// 组件选项
})
```
**2. 数据定义**
- 使用 `reactive` 定义响应式对象。
- 使用 `ref` 定义响应式单值。
- 使用 `computed` 定义计算属性。
```typescript
import { reactive, ref, computed } from 'vue'
const state = reactive({
count: 0,
message: 'Hello Vue 3'
})
const count = ref(0)
const doubledCount = computed(() => state.count * 2)
```
**3. 生命周期钩子**
- 使用 `onMounted``onUpdated` 等生命周期钩子,而不是 `beforeCreate``created``beforeMount``mounted``beforeUpdate``updated``beforeUnmount``unmounted`
```typescript
import { onMounted } from 'vue'
onMounted(() => {
console.log('Component is mounted.')
})
```
**4. 组件通信**
- 使用 `props``emit` 实现父子组件通信。
- 使用 `provide``inject` 实现祖先和后代组件通信。
- 不再推荐使用 `event bus` 进行任意组件间的通信,可以使用 Vuex 或者全局 `provide`/`inject` 替代。
```typescript
// 父组件
<ChildComponent @my-event="handleEvent" />
// 子组件
this.$emit('my-event', eventData)
```
**5. 异步处理**
- 使用 `async/await` 进行异步处理。
- 使用 `Suspense``async setup()` 处理异步依赖。
```typescript
import { ref, onMounted } from 'vue'
import axios from 'axios'
onMounted(async () => {
const response = await axios.get('https://api.example.com/data')
data.value = response.data
})
```
**6. Vue Router 和 Vuex**
- 使用 Vue Router 4 和 Vuex 4它们是为 Vue 3 重新设计的。
- 使用 `useRouter``useRoute` 钩子函数在组件中使用 router。
- 使用 `useStore` 钩子函数在组件中使用 Vuex store。
```typescript
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
const store = useStore()
const router = useRouter()
```
**7. 组件模板**
- 使用 `v-model` 代替 `.sync` 修饰符进行双向绑定。
- 使用 `v-for``:key` 渲染列表。
- 使用 `v-if``v-else``v-else-if` 进行条件渲染。
- 使用 `v-on` 或者 `@` 监听事件。
- 使用 `v-bind` 或者 `:` 绑定属性。
```html
<template>
<div v-if="condition">If block</div>
<div v-else>Else block</div>
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
<button @click="handleClick">Click me</button>
</template>
```
8. **使用类型注解和接口**
在 TypeScript 中,尽可能使用类型注解和接口来提供更完善的类型信息和类型检查。这将有助于发现和预防错误。
```typescript
interface User {
name: string;
age: number;
}
const user: User = {
name: 'John',
age: 30
}
```
9. **模块化和组件化**
尽可能将功能和逻辑模块化和组件化,使得代码更易于理解和维护。特别是使用 Composition API 时,可以将公共的逻辑封装成 composable 函数。
下面是一个使用 `Suspense` 和 `axios` 的示例,这个示例将会从一个 JSON Placeholder API 获取数据:
首先,我们创建一个 composable 函数,用于获取数据:
```typescript
import { ref } from 'vue'
import axios from 'axios'
export function useAsyncData(url: string) {
const data = ref(null)
const error = ref(null)
const fetchData = async () => {
try {
const response = await axios.get(url)
data.value = response.data
} catch (e) {
error.value = e
}
}
return { data, error, fetchData }
}
```
然后,我们创建一个 Vue 组件来使用这个函数:
```js
<template>
<Suspense>
<template #default>
<div v-if="error">{{ error.message }}</div>
<div v-else>{{ data }}</div>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue'
import { useAsyncData } from './composables/useAsyncData'
export default defineComponent({
setup() {
const { data, error, fetchData } = useAsyncData('https://jsonplaceholder.typicode.com/posts/1')
onMounted(fetchData)
return { data, error }
},
})
</script>
<style scoped>
/* 在这里添加 CSS 样式 */
</style>
```
在这个示例中,我们在 `useAsyncData` 函数中获取数据。当 `onMounted` 钩子函数被调用时(也就是当组件挂载完成后),我们开始获取数据。这个过程是异步的,因此我们在 `Suspense` 组件的 `#fallback` 插槽中显示一个 "Loading..." 的提示。一旦数据加载完成,`Suspense` 组件的 `#default` 插槽就会被渲染,并显示获取到的数据。
10. **良好的代码风格**
遵循一致的代码风格和代码质量规则,例如使用 ESLint 和 Prettier 来检查和格式化代码。
11. **单元测试和端到端测试**
对关键的组件和函数编写单元测试,对用户的主要操作路径编写端到端测试,确保功能的正确性。
12. **注释和文档**
对复杂的逻辑和函数编写注释,提供必要的项目文档,帮助其他开发者理解和使用你的代码。
13. **以下是 Vue 2 中被废弃或改变的部分API以及在 Vue 3 中的替代方案:**
| Vue 2.x | Vue 3.0 | 说明 |
| --- | --- | --- |
| `Vue.set` / `this.$set` | 响应式数据现在是默认的,无需使用这些方法 | Vue 3 的响应式系统是从头开始构建的,所有的对象和数组都是响应式的 |
| `Vue.delete` / `this.$delete` | 无需使用这些方法 | 在 Vue 3 中,你只需要使用 `delete` 操作符即可 |
| `filters` | 无对应项 | Vue 3 不再支持过滤器,建议使用计算属性或方法替代 |
| `Vue.observable` | `reactive` | 用 `reactive` 替代 `Vue.observable`,实现数据的响应式 |
| `Vue.prototype` | `app.config.globalProperties` | 在 Vue 3 中,全局 API 已经改变,`Vue.prototype` 被 `app.config.globalProperties` 替代 |
| `Vue.component`, `Vue.directive`, `Vue.mixin`, `Vue.use` | `app.component`, `app.directive`, `app.mixin`, `app.use` | 全局注册的 API 改变,如 `Vue.component` 变为 `app.component` |
| `beforeDestroy` 和 `destroyed` 生命周期钩子 | `beforeUnmount` 和 `unmounted` | 生命周期钩子名字变化,`beforeDestroy` 和 `destroyed` 分别改为 `beforeUnmount` 和 `unmounted` |
| `$on`, `$off`, `$once` | 无对应项 | Event Bus方法被移除需要用户自行实现或者使用第三方库 |
| `v-model` 在自定义组件上使用 | 需要明确的 `modelValue` 和 `update:modelValue` | Vue 3 对 `v-model` 的改动使其在自定义组件上更具灵活性 |
| `functional` 选项 | 无对应项 | Vue 3 不再支持函数式组件的写法,而是推荐使用 `render` 函数或 `setup` 函数 |
| 异步组件的 `functional` 写法 | `defineAsyncComponent` | 异步组件的创建方式更改,通过 `defineAsyncComponent` 方法创建 |
| `destroyed` 和 `beforeDestroy` 钩子函数 | `unmounted` 和 `beforeUnmount` | 生命周期钩子的名称已改为更直观的名称,以更好地表示其在组件实例生命周期中的角色 |
| `Vue.extend` | `defineComponent` | Vue 3 使用 `defineComponent` 方法定义组件,有更好的类型推断 |
注意Vue 3 对于 Options API 和 Composition API 提供了完全的支持,你可以在一个组件中混合使用这两种 API。不过为了代码的一致性和可读性建议在一个项目中选择一种 API 并坚持使用。
- **以下是 Vue 3.0 中的 Composition API 函数的基本说明,以及与 Options API 的对比**
| Composition API | 说明 | 对应的 Options API |
| --------------- | ---- | ------------------ |
| `setup` | `setup` 是一个新引入的组件选项,用于使用 Composition API。它是组件内部使用 Composition API 的入口。| 无 |
| `ref` | `ref` 函数用于创建一个响应式的数据。它接收一个参数,返回一个响应式的 Ref 对象。| `data` |
| `reactive` | `reactive` 函数用于创建一个响应式的对象。它接收一个普通对象,返回一个响应式的对象。| `data` |
| `computed` | `computed` 函数用于创建一个计算属性。它接收一个 getter 函数或者一个具有 getter 和 setter 的对象,返回一个响应式的 Ref 对象。| `computed` |
| `watch` | `watch` 函数用于响应式地跟踪和触发副作用。它接收一个响应式的源和一个执行副作用的回调函数。| `watch` |
| `watchEffect` | `watchEffect` 函数用于立即执行传入的一个函数,并响应式地追踪其依赖,并在其依赖变更时重新运行该函数。 | 无 |
| `onMounted` | `onMounted` 函数在组件被挂载时调用。它接收一个在组件挂载后执行的回调函数。 | `mounted` |
| `onUnmounted` | `onUnmounted` 函数在组件被卸载时调用。它接收一个在组件卸载后执行的回调函数。 | `beforeDestroy`/`unmounted` |
| `onUpdated` | `onUpdated` 在组件更新后调用。它接收一个在组件更新后执行的回调函数。| `updated` |
| `provide` | `provide` 函数用于在组件上定义一个可以被后代组件注入的值。它接收一个提供的键和值。 | 有,与 `provide/inject` 相似但是属性而不是函数 |
| `inject` | `inject` 函数用于在组件中注入一个由祖先组件提供的值。它接收一个注入的键。 | 有,但与 `provide/inject` 相似但是属性而不是函数 |
值得注意的是,虽然一些 Composition API 函数与 Options API 的某些选项有相似之处,但它们的工作方式和使用方式可能有所不同。在实际使用中,你需要根据具体的使用场景和需求选择合适的 API。

View File

@@ -1,8 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="ja-jp">
<head> <head>
<title><%= productName %></title> <title><%= productName %></title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="description" content="<%= productDescription %>"> <meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">

View File

@@ -10,13 +10,16 @@
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"axios": "^1.4.0", "axios": "^1.4.0",
"pinia": "^2.1.6",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"uuid": "^9.0.0",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-router": "^4.0.0" "vue-router": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@quasar/app-vite": "^1.3.0", "@quasar/app-vite": "^1.3.0",
"@types/node": "^12.20.21", "@types/node": "^12.20.21",
"@types/uuid": "^9.0.3",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0", "@typescript-eslint/parser": "^5.10.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
@@ -28,8 +31,9 @@
"typescript": "^4.5.4" "typescript": "^4.5.4"
}, },
"engines": { "engines": {
"node": "^18 || ^16 || ^14.19", "node": "^20 ||^18 || ^16 || ^14.19",
"npm": ">= 6.13.4", "npm": ">= 6.13.4",
"pnpm": ">=8.6.0",
"yarn": ">= 1.21.1" "yarn": ">= 1.21.1"
} }
}, },
@@ -544,6 +548,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/uuid": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz",
"integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"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",
@@ -4070,6 +4080,56 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pinia": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.6.tgz",
"integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==",
"dependencies": {
"@vue/devtools-api": "^6.5.0",
"vue-demi": ">=0.14.5"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.3.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.25", "version": "8.4.25",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz",
@@ -4946,7 +5006,7 @@
"version": "4.9.5", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true, "devOptional": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -5045,6 +5105,14 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@@ -1,8 +1,8 @@
{ {
"name": "kintone-app-builder", "name": "k-tune",
"version": "0.0.1", "version": "0.2.0",
"description": "Kintoneアプリの自動生成とデプロイを支援ツールです", "description": "Kintoneアプリの自動生成とデプロイを支援ツールです",
"productName": "Kintone App Builder", "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": {
@@ -10,18 +10,26 @@
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore", "format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test": "echo \"No test specified\" && exit 0", "test": "echo \"No test specified\" && exit 0",
"dev": "quasar dev", "dev": "quasar dev",
"build": "quasar build" "dev:local": "set \"LOCAL=true\" && quasar dev",
"build": "set \"SOURCE_MAP=false\" && 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",
"axios": "^1.4.0", "axios": "^1.4.0",
"jwt-decode": "^4.0.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"uuid": "^9.0.0",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-router": "^4.0.0" "vue-router": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@quasar/app-vite": "^1.3.0", "@quasar/app-vite": "^1.3.0",
"@types/node": "^12.20.21", "@types/node": "^12.20.21",
"@types/uuid": "^9.0.3",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0", "@typescript-eslint/parser": "^5.10.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
@@ -33,8 +41,9 @@
"typescript": "^4.5.4" "typescript": "^4.5.4"
}, },
"engines": { "engines": {
"node": "^18 || ^16 || ^14.19", "node": "^20 ||^18 || ^16 || ^14.19",
"npm": ">= 6.13.4", "npm": ">= 6.13.4",
"yarn": ">= 1.21.1" "yarn": ">= 1.21.1",
"pnpm": ">=8.6.0"
} }
} }

View File

@@ -0,0 +1,8 @@
<configuration>
<system.webServer>
<staticContent>
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
</staticContent>
</system.webServer>
</configuration>

View File

@@ -10,10 +10,14 @@
const { configure } = require('quasar/wrappers'); const { configure } = require('quasar/wrappers');
const dotenv = require('dotenv').config().parsed; const envPath = process.env.LOCAL==='true'?'.env.development':'.env';
const package = require('./package.json'); const dotenv = require('dotenv').config({path:envPath}).parsed;
console.log('dotenv=>',dotenv);
// const package = require('./package.json');
const { Notify } = require('quasar'); const { Notify } = require('quasar');
const version = package.version; const version = process.env.npm_package_version;
const productName=process.env.npm_package_productName;
// console.log(process.env);
module.exports = configure(function (/* ctx */) { module.exports = configure(function (/* ctx */) {
return { return {
eslint: { eslint: {
@@ -32,7 +36,8 @@ module.exports = configure(function (/* ctx */) {
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files // https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [ boot: [
'axios' 'axios',
'error-handler'
], ],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
@@ -49,7 +54,6 @@ module.exports = configure(function (/* ctx */) {
// 'themify', // 'themify',
// 'line-awesome', // 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it 'roboto-font', // optional, you are not bound to it
'material-icons', // optional, you are not bound to it 'material-icons', // optional, you are not bound to it
], ],
@@ -60,6 +64,7 @@ module.exports = configure(function (/* ctx */) {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'], browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
node: 'node16' node: 'node16'
}, },
sourcemap:process.env.SOURCE_MAP === 'true',
vueRouterMode: 'hash', // available values: 'hash', 'history' vueRouterMode: 'hash', // available values: 'hash', 'history'
// vueRouterBase, // vueRouterBase,
@@ -70,7 +75,7 @@ module.exports = configure(function (/* ctx */) {
// publicPath: '/', // publicPath: '/',
// analyze: true, // analyze: true,
env: { ...dotenv, version }, env: { ...dotenv, version ,productName},
// rawDefine: {} // rawDefine: {}
// ignorePublicFolder: true, // ignorePublicFolder: true,
// minify: false, // minify: false,
@@ -89,6 +94,7 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: { devServer: {
// https: true // https: true
port:9001,
open: true, // opens browser window automatically open: true, // opens browser window automatically
env: { ...dotenv }, env: { ...dotenv },
}, },

View File

@@ -1,5 +1,7 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import {router} from 'src/router';
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
@@ -14,11 +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 });
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

@@ -0,0 +1,21 @@
// src/boot/error-handler.ts
import { boot } from 'quasar/wrappers';
import { Router } from 'vue-router';
import { App } from 'vue';
export default boot(({ app, router }: { app: App<Element>; router: Router }) => {
document.documentElement.lang="ja-JP";
app.config.errorHandler = (err: any, instance: any, info: string) => {
if (err.response && err.response.status === 401) {
// 認証エラーの場合再ログインする
console.error('(; ゚Д゚)/認証エラー(401)', err, info);
localStorage.removeItem('token');
router.replace({
path:"/login",
query:{redirect:router.currentRoute.value.fullPath}
});
} else {
console.error('(; ゚Д゚)例外:', err, info);
}
};
});

View File

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

View File

@@ -0,0 +1,83 @@
<template>
<div id="action1">
<q-card class="my-card">
<q-card-section class="bg-primary text-white">
<div class="text-h6">Our Changing Planet</div>
<div class="text-subtitle2">by John Doe</div>
</q-card-section>
<q-separator />
<q-card-actions align="right">
<q-btn flat>設定</q-btn>
<q-btn class="del" flat @click="clickdel">削除</q-btn>
</q-card-actions>
</q-card>
<div class="next" style="display: table;width: 100%;height:40px;" @mouseenter="showAdd = true" @mouseleave="()=>{if(!showMenu) showAdd = false;}">
<div aria-hidden="false" style="display: table-row;">
<div
style="display: table-cell;background: url(&quot;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEzIDMwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyMCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPi5zdDB7ZmlsbDpub25lO3N0cm9rZTojNUI1QjVDO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1taXRlcmxpbWl0OjEwO30NCgkuc3Qxe2ZpbGw6IzVCNUI1Qzt9PC9zdHlsZT4NCiA8cmVjdCB4PSI5IiB3aWR0aD0iMiIgaGVpZ2h0PSIzNy45NzQiIGZpbGw9IiM1MTUxNTEiIHN0cm9rZS13aWR0aD0iNC41MDYyIi8+DQogPHBvbHlnb24gdHJhbnNmb3JtPSJtYXRyaXgoMS4xNzg1IDAgMCAxLjE3ODUgLS42MDY5MiAyMy41NDQpIiBwb2ludHM9IjEuOTI4IDQuMDY1IDguOTk5IDExLjEzNiAxNi4wNzIgNC4wNjUgMTcuNDg2IDUuNDc5IDguOTk5IDEzLjk2NCAwLjUxNSA1LjQ3OSIgZmlsbD0iIzUxNTE1MSIvPg0KPC9zdmc+DQo=&quot;) center center no-repeat;">
</div>
</div>
<div v-if="showAdd" style="display:table-row;height:inherit;position:absolute;left:50%;">
<div style="display:table-cell;">
<q-btn round size="xs" color="primary" label="+">
<q-menu v-model="showMenu">
<q-list style="min-width: 100px">
<q-item clickable v-close-popup>
<q-item-section @click="clickadd">New tab</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { ref,watch } from 'vue'
export default {
emits: [
'addaction'
],
setup(props,context) {
const showAdd = ref(false)
const showMenu = ref(false)
watch(showMenu,(newVal) =>{
console.log('3');
if(!newVal)
{
showAdd.value = false;
}
});
const clickadd = () => {
console.log('3');
context.emit('addaction');
//let oDiv1 = pdiv;
// let oDiv1 = document.getElementById('action1');
// let oDiv2 = document.createElement('div');
// if (oDiv1 !== null) {
// oDiv2.innerHTML = oDiv1?.innerHTML;
// oDiv1?.after(oDiv2);
// let oAdd = oDiv2.getElementsByClassName('next')[0];
// oAdd.addEventListener('mouseenter', mouseenter);
// oAdd.addEventListener('mouseleave', mouseleave);
// let oDel = oDiv2.getElementsByClassName('del')[0];
// oDel.addEventListener('click', clickdel);
// }
};
const clickdel = (event: Event) => {
let oBtn = event.target as Element;
oBtn.parentElement?.parentElement?.parentElement?.parentElement?.remove();
};
// window.clickadd = clickadd;
// window.clickdel = clickdel;
// window.mouseenter = mouseenter;
// window.mouseleave = mouseleave;
return {clickadd, clickdel, showAdd, showMenu }
}
}
</script>

View File

@@ -0,0 +1,131 @@
<template>
<div class="q-mx-md q-mb-lg">
<div class="q-mb-xs q-ml-md text-primary">アプリ選択</div>
<div class="q-pa-md row" style="border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 4px;">
<div v-if="selField?.app && !showSelectApp">{{ selField?.app?.name }}</div>
<q-space />
<div>
<q-btn outline dense label="選 択" padding="none sm" color="primary" @click="() => {
showSelectApp = true;
}"></q-btn>
</div>
</div>
</div>
<div v-if="!showSelectApp && selField?.app?.name">
<div>
<div class="row q-mb-md">
<!-- <div class="col"> -->
<div class="q-mb-xs q-ml-md text-primary">フィールド選択</div>
<!-- </div> -->
<q-space />
<!-- <div class="col"> -->
<div class="q-mr-md">
<q-input dense debounce="300" v-model="fieldFilter" placeholder="フィールド検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</div>
</div>
<div class="row">
<field-select ref="fieldDlg" name="フィールド" :type="selectType" :updateSelectFields="updateSelectFields"
:appId="selField?.app?.id" not_page :filter="fieldFilter"
:selectedFields="selField.fields" :fieldTypes="fieldTypes"></field-select>
</div>
</div>
</div>
<div style="min-width: 45vw;" v-else>
</div>
<show-dialog v-model:visible="showSelectApp" name="アプリ選択">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<AppSelectBox ref="appDlg" name="アプリ" type="single" :filter="filter"
:updateSelectApp="updateSelectApp"></AppSelectBox>
</show-dialog>
</template>
<script lang="ts">
import { defineComponent, ref, watchEffect, computed, reactive } from 'vue';
import ShowDialog from './ShowDialog.vue';
import FieldSelect from './FieldSelect.vue';
import AppSelectBox from './AppSelectBox.vue';
interface IApp {
id: string,
name: string
}
interface IField {
name: string,
code: string,
type: string
}
interface IAppFields {
app?: IApp,
fields: IField[]
}
export default defineComponent({
inheritAttrs: false,
name: 'AppFieldSelectBox',
components: {
ShowDialog,
FieldSelect,
AppSelectBox,
},
props: {
selectedField: {
type: Object,
required: true
},
selectType: {
type: String,
default: 'single'
},
fieldTypes:{
type:Array,
default:()=>[]
}
},
setup(props, { emit }) {
const showSelectApp = ref(false);
const selField = reactive(props.selectedField);
const isSelected = computed(() => {
return selField !== null && typeof selField === 'object' && ('app' in selField)
});
const updateSelectApp = (newAppinfo: IApp) => {
selField.app = newAppinfo
}
const updateSelectFields = (newFields: IField[]) => {
selField.fields = newFields
}
watchEffect(() => {
emit('update:modelValue', selField);
});
return {
showSelectApp,
isSelected,
updateSelectApp,
filter: ref(),
updateSelectFields,
fieldFilter: ref(),
selField
};
}
});
</script>

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 }}
@@ -36,7 +36,7 @@
import { AppInfo, AppSeed } from './models'; import { AppInfo, AppSeed } from './models';
import { ref, defineComponent, watch, onMounted , toRefs } from 'vue'; import { ref, defineComponent, watch, onMounted , toRefs } from 'vue';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { promises } from 'dns'; import { useAuthStore } from 'src/stores/useAuthStore';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -44,12 +44,13 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const { app } = toRefs(props); const { app } = toRefs(props);
const authStore = useAuthStore();
const appinfo = ref<AppInfo>({ const appinfo = ref<AppInfo>({
appId: "", appId: "",
name: "", name: "",
description: "" description: ""
}); });
const link= ref('https://mfu07rkgnb7c.cybozu.com/k/' + app.value); const link= ref(`${authStore.currentDomain.kintoneUrl}/k/${app.value}`);
const getAppInfo = async (appId:string|undefined) => { const getAppInfo = async (appId:string|undefined) => {
if(!appId){ if(!appId){
return; return;
@@ -59,7 +60,7 @@ export default defineComponent({
let retry =0; let retry =0;
while(retry<=3 && result && result.appId!==appId){ while(retry<=3 && result && result.appId!==appId){
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
const response = await api.get('app', { const response = await api.get('api/v1/app', {
params:{ params:{
app: appId app: appId
} }
@@ -73,7 +74,7 @@ export default defineComponent({
watch(app, async (newApp) => { watch(app, async (newApp) => {
appinfo.value = await getAppInfo(newApp); appinfo.value = await getAppInfo(newApp);
link.value = 'https://mfu07rkgnb7c.cybozu.com/k/' + newApp; link.value = `${authStore.currentDomain.kintoneUrl}/k/${newApp}`;
}, { immediate: true }); }, { immediate: true });
const linkClick=(ev : MouseEvent)=>{ const linkClick=(ev : MouseEvent)=>{
@@ -82,7 +83,7 @@ export default defineComponent({
}; };
onMounted(async ()=>{ onMounted(async ()=>{
appinfo.value = await getAppInfo(app.value); appinfo.value = await getAppInfo(app.value);
link.value = 'https://mfu07rkgnb7c.cybozu.com/k/' + app.value; link.value = `${authStore.currentDomain.kintoneUrl}/k/${app.value}`;
}); });
return { return {

View File

@@ -0,0 +1,100 @@
<template>
<div class="q-px-xs">
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table v-else class="app-table" :selection="type" row-key="id" v-model:selected="selected" flat bordered
virtual-scroll :columns="columns" :rows="rows" :pagination="pagination" :rows-per-page-options="[0]"
:filter="filter" style="max-height: 65vh;">
<template v-slot:body-cell-description="props">
<q-td :props="props">
<q-scroll-area class="description-cell">
<div v-html="props.row.description"></div>
</q-scroll-area>
</q-td>
</template>
</q-table>
</div>
</template>
<script lang="ts">
import { ref, onMounted, reactive, watchEffect } from 'vue'
import { api } from 'boot/axios';
export default {
name: 'AppSelectBox',
props: {
name: String,
type: String,
filter: String,
updateSelectApp: {
type: Function
}
},
setup(props) {
const columns = [
{ name: 'id', required: true, label: 'ID', align: 'left', field: 'id', sortable: true },
{ name: 'name', label: 'アプリ名', field: 'name', sortable: true, align: 'left' },
{ name: 'description', label: '概要', field: 'description', align: 'left', sortable: false },
{ name: 'createdate', label: '作成日時', field: 'createdate', align: 'left' }
]
const isLoaded = ref(false);
const rows: any[] = reactive([]);
const selected = ref([])
watchEffect(()=>{
if (selected.value && selected.value[0] && props.updateSelectApp) {
props.updateSelectApp(selected.value[0])
}
});
onMounted(() => {
api.get('api/v1/allapps').then(res => {
res.data.apps.forEach((item: any) => {
rows.push({
id: item.appId,
name: item.name,
description: item.description,
createdate: dateFormat(item.createdAt)
});
});
isLoaded.value = true;
});
});
const dateFormat = (dateStr: string) => {
const date = new Date(dateStr);
const pad = (num: number) => num.toString().padStart(2, '0');
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
}
return {
columns,
rows,
selected,
isLoaded,
pagination: ref({
rowsPerPage: 10
})
}
},
}
</script>
<style lang="scss">
.description-cell {
height: 60px;
width: 300px;
max-height: 60px;
max-width: 300px;
white-space: break-spaces;
}
.spinner {
min-height: 300px;
min-width: 400px;
}
</style>

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

@@ -0,0 +1,107 @@
<template>
<show-dialog v-model:visible="showflg" name="条件エディタ" @close="closeDg" min-width="50vw" min-height="60vh">
<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="copyCondition()">
<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="pasteCondition()">
<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>
<NodeCondition v-model:conditionTree="tree"></NodeCondition>
</show-dialog>
</template>
<script lang="ts">
import { defineComponent, ref ,watchEffect} from 'vue';
import ShowDialog from '../../components/ShowDialog.vue';
import NodeCondition from './NodeCondition.vue';
import { ConditionTree } from '../../types/Conditions';
import { useQuasar } from 'quasar';
export default defineComponent({
name: 'ConditionObject',
components: {
ShowDialog,
NodeCondition,
},
props: {
conditionTree: {
type: ConditionTree,
default: null
},
show:{
type:Boolean,
default:false
}
},
emits:[
"closed",
"update:conditionTree",
"update:show"
],
setup(props,context) {
const appDg = ref();
const $q=useQuasar();
const tree = ref(props.conditionTree);
const closeDg = (val:string) => {
if (val == 'OK') {
// if(tree.value.root.children.length===0){
// $q.notify({
// type: 'negative',
// message: `条件式を設定してください。`
// });
// }
context.emit("update:conditionTree",tree.value);
}
showflg.value=false;
context.emit("update:show",false);
context.emit("closed",val);
};
const showflg =ref(props.show);
//条件式をコピーする
const copyCondition=()=>{
if (navigator.clipboard) {
const jsonData=tree.value.toJson();
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 pasteCondition=async ()=>{
try {
const text = await navigator.clipboard.readText();
console.log('Text from clipboard:', text);
tree.value.fromJson(text);
} catch (err) {
console.error('Failed to read text from clipboard: ', err);
throw err;
}
}
watchEffect(() => {
showflg.value=props.show;
});
return {
tree,
appDg,
closeDg,
showflg,
copyCondition,
pasteCondition
};
}
});
</script>

View File

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

View File

@@ -0,0 +1,277 @@
<template>
<!-- <q-toolbar class="bg-grey-3" flat dense round icon="menu" aria-label="Menu" @click.stop>
<q-toolbar-title>条件エディタ</q-toolbar-title>
<q-space></q-space>
<q-btn flat round dense icon="info" color="blue" @click="showingCondition=!showingCondition"></q-btn>
</q-toolbar> -->
<div class="q-pa-md">
<q-tree :nodes="[tree.root]" node-key="index" children-key="children"
tick-strategy="strict" v-model:ticked="ticked" :expanded="expanded" default-expand-all dense color="primary" >
<template v-slot:header-root="prop">
<!-- root -->
<div class="row items-center" @click.stop>
<q-select v-model="prop.node.logicalOperator" :options="logicalOperators" filled outlined dense></q-select>
<q-btn flat round dense icon="more_horiz" size="sm" >
<q-menu auto-close anchor="top right">
<q-list>
<q-item clickable @click="addGroup(prop.node, LogicalOperator.AND)">
<q-item-section avatar><q-icon name="playlist_add" ></q-icon></q-item-section>
<q-item-section>グループの追加</q-item-section>
</q-item>
<q-item clickable @click="addCondition(prop.node)">
<q-item-section avatar><q-icon name="add_circle_outline" ></q-icon></q-item-section>
<q-item-section >条件式の追加</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
</template>
<template v-slot:header-generic="prop">
<!-- logic group -->
<div v-if="prop.node.type !== NodeType.Condition" class="row items-center" @click.stop>
<q-select v-model="prop.node.logicalOperator" :options="logicalOperators" :outlined="true" :filled="true" :dense="true"></q-select>
<q-btn flat round dense icon="more_horiz" size="sm" >
<q-menu auto-close anchor="top right">
<q-list>
<q-item clickable @click="moveUp(prop.node)">
<q-item-section avatar><q-icon name="arrow_upward" ></q-icon></q-item-section>
<q-item-section >一つ上に移動</q-item-section>
</q-item>
<q-item clickable @click="moveDown(prop.node)">
<q-item-section avatar><q-icon name="arrow_downward" ></q-icon></q-item-section>
<q-item-section >一つ下に移動</q-item-section>
</q-item>
<q-separator inset/>
<q-item clickable @click="addGroup(prop.node, LogicalOperator.AND)">
<q-item-section avatar><q-icon name="playlist_add" ></q-icon></q-item-section>
<q-item-section >グループ追加</q-item-section>
</q-item>
<q-item clickable @click="addCondition(prop.node)">
<q-item-section avatar><q-icon name="add_circle_outline" ></q-icon></q-item-section>
<q-item-section >条件式追加</q-item-section>
</q-item>
<q-separator inset/>
<q-item clickable @click="splitGroup(prop.node)">
<q-item-section avatar><q-icon name="playlist_remove" color="negative"></q-icon></q-item-section>
<q-item-section >グループ化解除</q-item-section>
</q-item>
<q-item clickable @click="removeNode(prop.node)">
<q-item-section avatar><q-icon name="delete" color="negative"></q-icon></q-item-section>
<q-item-section >削除</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
<!-- condition -->
<div @click.stop @keypress.stop v-else >
<div class="row no-wrap items-center q-my-xs">
<ConditionObject v-bind="prop.node" v-model="prop.node.object" :config="leftDynamicItemConfig" class="col-4"/>
<q-select v-model="prop.node.operator" :options="operators" class="operator" :outlined="true" :dense="true"></q-select>
<ConditionObject v-bind="prop.node" v-model="prop.node.value" :config="rightDynamicItemConfig" class="col-4"
:options="objectValueOptions(prop.node?.object?.options)"
/>
<!-- <ConditionObject v-bind="prop.node" v-model="prop.node.object" class="col-4"/> -->
<!-- <q-input v-if="!prop.node.object || !('options' in prop.node.object)"
v-model="prop.node.value"
class="condition-value" :outlined="true" :dense="true" ></q-input> -->
<!-- <q-select v-if="prop.node.object && ('options' in prop.node.object)"
v-model="prop.node.value"
:options="objectValueOptions(prop.node.object.options)"
clearable
value-key="index"
class="condition-value" :outlined="true" :dense="true" ></q-select> -->
<q-btn flat round dense icon="more_horiz" size="sm" >
<q-menu auto-close anchor="top right">
<q-list>
<q-item clickable @click="moveUp(prop.node)">
<q-item-section avatar><q-icon name="arrow_upward" ></q-icon></q-item-section>
<q-item-section >一つ上に移動</q-item-section>
</q-item>
<q-item clickable @click="moveDown(prop.node)">
<q-item-section avatar><q-icon name="arrow_downward" ></q-icon></q-item-section>
<q-item-section >一つ下に移動</q-item-section>
</q-item>
<q-separator inset/>
<q-item clickable @click="groupMerge(prop.node)" v-if="canMerge(prop.node)">
<q-item-section avatar><q-icon name="playlist_add"></q-icon></q-item-section>
<q-item-section >グループ化</q-item-section>
</q-item>
<q-separator inset/>
<q-item clickable @click="removeNode(prop.node)">
<q-item-section avatar><q-icon name="delete" color="negative"></q-icon></q-item-section>
<q-item-section>削除</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
</div>
</template>
</q-tree>
<q-tooltip anchor="center middle" v-model="showingCondition" no-parent-event>
import { finished } from 'stream';
{{ conditionString }}
</q-tooltip>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive, computed, inject } from 'vue';
import { INode,ConditionTree,GroupNode,ConditionNode, LogicalOperator,Operator,NodeType } from '../../types/Conditions';
import ConditionObject from './ConditionObject.vue';
import { IDynamicInputConfig } from 'src/types/ComponentTypes';
export default defineComponent( {
name: 'NodeCondition',
components: {
ConditionObject
},
props:{
conditionTree: {
type: ConditionTree,
default: null
},
show:{
type:Boolean,
default:false
}
},
setup(props) {
const ticked= ref([]);
const showingCondition=ref(false);
const logicalOperators = computed(()=>{
const opts=[];
for(const op in LogicalOperator){
opts.push(LogicalOperator[op as keyof typeof LogicalOperator]);
}
return opts;
});
const operatorSet = inject<Array<any>>('Operator')
const operators = ref(operatorSet ? operatorSet : Object.values(Operator));
const tree = reactive(props.conditionTree);
const conditionString = computed(()=>{
return tree.buildConditionString(tree.root);
});
const objectValueOptions=(options:any):any[]|null=>{
if(!options){
return null;
}
const opts:any[] =[];
Object.keys(options).forEach((key) =>
{
const opt=options[key];
opts.push(opt);
});
return opts;
};
const addGroup = (parent:GroupNode, logicOp:LogicalOperator) => {
if(!parent){
parent=tree.root;
}
tree.addNode(parent,new GroupNode(logicOp,parent));
};
const addCondition = (parent:GroupNode) => {
const newNode = new ConditionNode({},Operator.Equal,'',parent);
tree.addNode(parent,newNode);
};
const removeNode = (node:INode) => {
tree.removeNode(node);
};
const moveUp =(node:INode)=>{
tree.moveNode(node,'up');
}
const moveDown =(node:INode)=>{
tree.moveNode(node,'down');
}
const getConditionJson=()=>{
return tree.toJson();
}
//JsonからConditionTreeのインスタンスを作成
const LoadCondition=()=>{
tree.fromJson(conditionString.value);
}
//グループ化
const groupMerge=(node:INode)=>{
const checkedNodes:INode[]=[];
const checkedIndexs:number[] = ticked.value;
checkedIndexs.forEach(index => {
const node = tree.findByIndex(index);
if(node){
checkedNodes.push(node);
}
});
tree.createGroupNode(node,checkedNodes,LogicalOperator.AND);
ticked.value=[];
}
//グループ化可能かをチェックする
const canMerge =(node:INode)=>{
const checkedIndexs:number[] = ticked.value;
const findNode = checkedIndexs.find(index=>node.index===index);
console.log("findNode=>",findNode!==undefined,findNode);
return findNode!==undefined;
}
//グループ化解散
const splitGroup=(node:INode)=>{
tree.dissolveGroupNode(node as GroupNode);
ticked.value=[];
}
const expanded=computed(()=>tree.getGroups(tree.root));
// addCondition(tree.root);
const leftDynamicItemConfig = inject<IDynamicInputConfig>('leftDynamicItemConfig');
const rightDynamicItemConfig = inject<IDynamicInputConfig>('rightDynamicItemConfig');
return {
leftDynamicItemConfig,
rightDynamicItemConfig,
showingCondition,
conditionString,
tree,
ticked,
logicalOperators,
operators,
addGroup,
addCondition,
removeNode,
moveUp,
moveDown,
LogicalOperator,
Operator,
NodeType,
getConditionJson,
LoadCondition,
objectValueOptions,
expanded,
canMerge,
groupMerge,
splitGroup
};
},
});
</script>
<style lang="scss">
.condition-value{
min-width: 200px;
max-height: 40px;
margin: 0 2px;
}
.operator{
min-width: 150px;
max-height: 40px;
margin: 0 2px;
text-align: center;
font-size: 12pt;
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="q-gutter-y-md" style="max-width: 600px;">
<q-card >
<q-tabs
v-model="tab"
dense
class="text-grey"
active-color="white"
active-bg-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
>
<q-tab name="fields" label="フィールド"></q-tab>
<q-tab name="vars" label="変数"></q-tab>
</q-tabs>
<q-separator></q-separator>
<q-tab-panels v-model="tab" animated>
<q-tab-panel name="fields">
<field-list v-model="selected" type="single" :filter="filter" :appId="sourceApp ? sourceApp :appId " :fields="sourceFields"></field-list>
</q-tab-panel>
<q-tab-panel name="vars" >
<variable-list v-model="selected" type="single" :vars="vars"></variable-list>
</q-tab-panel>
</q-tab-panels>
</q-card>
</div>
</template>
<script lang="ts">
import { ref, onMounted, reactive, inject } from 'vue'
import FieldList from './FieldList.vue';
import VariableList from './VariableList.vue';
export default {
name: 'ConditionObjects',
components:{
FieldList,
VariableList
},
props: {
name: String,
type: String,
appId: Number,
vars: Array,
filter:String
},
setup(props) {
const selected = ref([]);
console.log(selected);
return {
sourceFields : inject('sourceFields'),
sourceApp : inject('sourceApp'),
tab: ref('fields'),
selected
}
},
}
</script>

View File

@@ -4,7 +4,8 @@
style="max-width: 400px" style="max-width: 400px"
:url="uploadUrl" :url="uploadUrl"
:label="title" :label="title"
accept=".csv,.xlsx" :headers="headers"
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"
@@ -14,8 +15,11 @@
</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 { ref } from 'vue';
const $q=useQuasar(); const $q=useQuasar();
const authStore = useAuthStore();
const emit =defineEmits(['uploaded']); const emit =defineEmits(['uploaded']);
/** /**
* ファイルアップロードを拒否する時の処理 * ファイルアップロードを拒否する時の処理
@@ -26,7 +30,7 @@
// 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ファイルを選択してください。`
}) })
} }
@@ -36,7 +40,7 @@
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',
@@ -48,14 +52,28 @@
}, 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",
@@ -67,9 +85,12 @@
title: string; title: string;
uploadUrl:string; uploadUrl:string;
} }
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}createappfromexcel` uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1`
}); });
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -0,0 +1,42 @@
<template>
<div class="q-pa-md">
<q-table :title="name+'一覧'" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" />
</div>
</template>
<script>
import { ref,onMounted,reactive } from 'vue'
import { api } from 'boot/axios';
export default {
name: 'DomainSelect',
props: {
name: String,
type: String
},
setup() {
const columns = [
{ name: 'id'},
{ name: 'tenantid', required: true,label: 'テナント',align: 'left',field: 'tenantid',sortable: true},
{ name: 'name', align: 'center', label: 'ドメイン', field: 'name', sortable: true },
{ name: 'url', label: 'URL', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'アカウント', field: 'kintoneuser' }
]
const rows = reactive([])
onMounted( () => {
api.get(`api/domains/1`).then(res =>{
res.data.forEach((item) =>
{
rows.push({id:item.id,tenantid:item.tenantid,name:item.name,url:item.url,kintoneuser:item.kintoneuser});
}
)
});
});
return {
columns,
rows,
selected: ref([]),
}
},
}
</script>

View File

@@ -0,0 +1,60 @@
<template>
<q-btn-dropdown
class="customized-disabled-btn"
push
flat
no-caps
icon="share"
size="md"
:label="userStore.currentDomain.domainName"
:disable-dropdown="isUnclickable"
:dropdown-icon="isUnclickable ? 'none' : ''"
:disable="isUnclickable"
>
<q-list>
<q-item v-for="domain in domains" :key="domain.domainName"
clickable v-close-popup @click="onItemClick(domain)">
<q-item-section side>
<q-icon name="share" size="sm" color="orange" text-color="white"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>{{domain.domainName}}</q-item-label>
<q-item-label caption>{{domain.kintoneUrl}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</template>
<script setup lang="ts" >
import { IDomainInfo } from 'src/types/ActionTypes';
import { useAuthStore,IUserState } from 'stores/useAuthStore';
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
const userStore = useAuthStore();
const route = useRoute()
const domains = ref<IDomainInfo[]>([]);
(async ()=>{
domains.value = await userStore.getUserDomains();
})();
const isUnclickable = computed(()=>{
return route.path.startsWith('/FlowChart/') || domains.value === undefined || domains.value.length === 0;
});
const onItemClick=(domain:IDomainInfo)=>{
console.log(domain);
userStore.setCurrentDomain(domain);
}
</script>
<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>

View File

@@ -0,0 +1,161 @@
<template>
<div class="q-mx-md" style="max-width: 600px;">
<!-- <q-card> -->
<div class="q-mb-md">
<q-input ref="inputRef" v-if="!optionsRef|| optionsRef.length===0"
outlined dense debounce="200" @update:model-value="updateSharedText"
v-model="sharedText" :readonly="!canInputFlag" autogrow>
<template v-slot:append>
<q-btn flat round padding="none" icon="cancel" @click="clearSharedText" color="grey-6" />
</template>
</q-input>
<q-select v-if="optionsRef && optionsRef.length>0"
:model-value="sharedText"
:options="optionsRef"
clearable
value-key="index"
outlined
dense
use-input
hide-selected
input-debounce="10"
fill-input
@input-value="setValue"
@clear="sharedText=null"
hide-dropdown-icon
:readonly="!canInputFlag"
>
</q-select>
</div>
<div class="row q-gutter-sm">
<q-btn v-for="button in buttonsConfig" :key="button.type" :color="button.color" @mousedown.prevent
@click="openDialog(button)" size="sm">
{{ button.label }}
</q-btn>
</div>
<show-dialog v-model:visible="dialogVisible" :name="currentDialogName" @close="closeDialog" min-width="400px">
<template v-slot:toolbar>
<q-input dense debounce="200" v-model="filter" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<!-- asdf -->
<component :is="currentComponent" @select="handleSelect" :filter="filter" :appId="appId" />
</show-dialog>
<!-- </q-card> -->
</div>
</template>
<script lang="ts">
import { ref, inject, watchEffect, defineComponent,PropType } from 'vue';
import FieldAdd from './FieldAdd.vue';
import VariableAdd from './VariableAdd.vue';
// import FunctionAdd from './FunctionAdd.vue';
import ShowDialog from '../ShowDialog.vue';
import { IButtonConfig } from 'src/types/ComponentTypes';
export default defineComponent({
name: 'DynamicItemInput',
components: {
FieldAdd,
VariableAdd,
// FunctionAdd,
ShowDialog
},
props: {
canInput: {
type: Boolean,
default: false
},
appId: {
type: String,
},
selectedObject: {
default: {}
},
options:{
type:Array as PropType< string[]>
},
buttonsConfig: {
type: Array as PropType<IButtonConfig[]>,
default: () => [
{ label: 'フィールド', color: 'primary', type: 'FieldAdd' }
]
}
},
setup(props, { emit }) {
const filter = ref('');
const dialogVisible = ref(false);
const currentDialogName = ref('');
const selectedObjectRef = ref(props.selectedObject);
const currentComponent = ref('FieldAdd');
const sharedText = ref(props.selectedObject?.sharedText ?? '');
const inputRef = ref();
const canInputFlag = ref(props.canInput);
const editable = ref(false);
const openDialog = (button: IButtonConfig) => {
currentDialogName.value = button.label;
currentComponent.value = button.type;
dialogVisible.value = true;
editable.value = canInputFlag.value;
};
const closeDialog = () => {
dialogVisible.value = false;
};
const handleSelect = (value:any) => {
if (value && value._t && (value._t as string).length > 0) {
canInputFlag.value = editable.value;
}
selectedObjectRef.value={ sharedText: value._t, ...value };
sharedText.value = `${value._t}`;
// emit('update:selectedObject', { sharedText: sharedText.value, ...value });
dialogVisible.value = false;
};
const clearSharedText = () => {
sharedText.value = '';
selectedObjectRef.value={};
canInputFlag.value = true;
// emit('update:selectedObject', {});
}
const updateSharedText = (value:string) => {
sharedText.value = value;
selectedObjectRef.value= { sharedText: value,objectType:'text' }
// emit('update:selectedObject', { ...props.selectedObject, sharedText: value,objectType:'text' });
}
const setValue=(value:string)=>{
sharedText.value = value;
if(selectedObjectRef.value.sharedText!==value){
selectedObjectRef.value= { sharedText: value,objectType:'text' }
}
}
const optionsRef=ref(props.options);
return {
filter,
dialogVisible,
currentDialogName,
currentComponent,
canInputFlag,
openDialog,
closeDialog,
handleSelect,
clearSharedText,
updateSharedText,
setValue,
sharedText,
inputRef,
optionsRef,
selectedObjectRef
};
}
});
</script>

View File

@@ -0,0 +1,41 @@
<template>
<field-list v-model="selected" type="single" :filter="filter" :appId="sourceApp ? sourceApp : appId"
:fields="sourceFields" @update:modelValue="handleSelect" />
</template>
<script lang="ts">
import { computed, inject, ref } from 'vue';
import FieldList from '../FieldList.vue';
export default {
name: 'FieldAdd',
components: {
FieldList,
},
props: {
appId: Number,
filter: String
},
setup(props, { emit }) {
const sourceFields = inject<Array<unknown>>('sourceFields')
const sourceApp = inject<number>('sourceApp')
const appId = computed(() => {
if (sourceFields || sourceApp) {
return sourceApp.value
} else {
return props.appId
}
});
return {
sourceFields,
sourceApp,
selected: ref([]),
handleSelect: (newSelection: any[]) => {
if (newSelection.length > 0) {
const v = newSelection[0]
emit('select', { _t: `field(${appId.value},${v.name})`, ...v }); // 假设您只需要选择的第一个字段的名称
}
}
}
},
}
</script>

View File

@@ -0,0 +1,42 @@
<template>
<variable-list v-model="selected" type="single" :vars="vars" :filter="filter" @update:modelValue="handleSelect" />
</template>
<script lang="ts">
import { ref } from 'vue';
import VariableList from '../VariableList.vue';
import { useFlowEditorStore } from 'src/stores/flowEditor';
import { IActionVariable } from 'src/types/ActionTypes';
export default {
name: 'VariableAdd',
components: {
VariableList,
},
props: {
appId: Number,
filter: String
},
setup(props, { emit }) {
const store = useFlowEditorStore();
let vars: IActionVariable[] = [];
console.log(store.currentFlow !== undefined && store.activeNode !== undefined);
if (store.currentFlow !== undefined && store.activeNode !== undefined) {
vars = store.currentFlow.getVarNames(store.activeNode);
}
return {
vars,
selected: ref([]),
handleSelect: (newSelection: any[]) => {
if (newSelection.length > 0) {
const v = newSelection[0];
let name = v.name
if (typeof name === 'object') {
name = name.name
}
emit('select', { _t: `var(${name})`, ...v }); // 假设您只需要选择的第一个字段的名称
}
}
}
},
}
</script>

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

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

View File

@@ -0,0 +1,107 @@
<template>
<div class="q-px-md" style=" min-width: 50vw; max-width: 85vw;">
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table flat bordered v-else row-key="id" :selection="type" v-model:selected="selected" :columns="columns"
:rows="rows" :pagination="pageSetting" :filter="filter" style="max-height: 55vh;"/>
</div>
</template>
<script>
import { ref, onMounted, reactive, watchEffect } from 'vue'
import { api } from 'boot/axios';
export default {
name: 'fieldSelect',
props: {
name: String,
type: {
type: String,
default: 'single'
},
appId: Number,
not_page: {
type: Boolean,
default: false,
},
selectedFields:{
type:Array ,
default:()=>[]
},
fieldTypes:{
type:Array,
default:()=>[]
},
filter: String,
updateSelectFields: {
type: Function
},
blackListLabel: {
type:Array,
default:()=>[]
}
},
setup(props) {
const isLoaded = ref(false);
const columns = [
{ name: 'name', required: true, label: 'フィールド名', align: 'left', field: row => row.name, sortable: true },
{ name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true }
];
const pageSetting = ref({
sortBy: 'name',
descending: false,
page: 1,
rowsPerPage: props.not_page ? 0 : 25
// rowsNumber: xx if getting data from a server
});
const rows = reactive([]);
const selected = ref((props.selectedFields && props.selectedFields.length>0)?props.selectedFields:[]);
onMounted(async () => {
const url = props.fieldTypes.includes('SPACER')?'api/v1/allfields':'api/v1/appfields';
const res = await api.get(url, {
params: {
app: props.appId
}
});
let fields = Object.values(res.data.properties);
for (const index in fields) {
const fld = fields[index]
if(props.blackListLabel.length > 0){
if(!props.blackListLabel.find(blackListItem => blackListItem === fld.label)){
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}
}
} else {
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}
}
}
isLoaded.value = true;
});
watchEffect(()=>{
if (selected.value && selected.value[0] && props.updateSelectFields) {
props.updateSelectFields(selected)
}
});
return {
columns,
rows,
selected,
isLoaded,
pageSetting
}
},
}
</script>

View File

@@ -1,9 +0,0 @@
export interface Rule{
id:number;
name:string;
condtion:CondtionTree
}
export interface CondtionTree{
}

View File

@@ -0,0 +1,64 @@
<template>
<!-- <div class="q-pa-md q-gutter-sm" > -->
<q-dialog :model-value="visible" persistent bordered >
<q-card class="" style="min-width: 40vw; max-width: 80vw; max-height: 95vh;" :style="cardStyle">
<q-toolbar class="bg-grey-4">
<q-toolbar-title>{{ name }}</q-toolbar-title>
<q-space></q-space>
<slot name="toolbar"></slot>
<q-btn flat round dense icon="close" @click="CloseDialogue('Cancel')" />
</q-toolbar>
<q-card-section class="q-mt-md" :style="sectionStyle">
<slot></slot>
</q-card-section>
<q-card-actions v-if="!disableBtn" align="right" class="text-primary">
<q-btn flat label="確定" v-close-popup @click="CloseDialogue('OK')" />
<q-btn flat label="キャンセル" v-close-popup @click="CloseDialogue('Cancel')" />
</q-card-actions>
</q-card>
</q-dialog>
<!-- </div> -->
</template>
<script>
import {computed} from 'vue'
export default {
name: 'ShowDialog',
props: {
name:String,
visible: Boolean,
width:String,
height:String,
minWidth:String,
minHeight:String,
disableBtn:{
type: Boolean,
default: false
}
},
emits: [
'close'
],
setup(props, context) {
const CloseDialogue = (val) => {
context.emit('update:visible', false);
context.emit('close', val);
}
const cardStyle = computed(() => ({
minWidth: props.minWidth,
width: props.width
}));
const sectionStyle = computed(() => ({
height: props.height,
minHeight: props.minHeight
}));
return {
CloseDialogue,
cardStyle,
sectionStyle
}
},
}
</script>

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,61 @@
<template>
<div class="q-pa-md">
<q-table flat bordered row-key="id" :selection="type" :selected="modelValue" :filter="filter"
@update:selected="$emit('update:modelValue', $event)" :columns="columns" :rows="rows" />
</div>
</template>
<script lang="ts">
import { PropType, reactive } from 'vue';
import { IActionVariable } from '../types/ActionTypes';
import { v4 as uuidv4 } from 'uuid';
export default {
name: 'VariableList',
props: {
name: String,
type: String,
vars: {
type: Array as PropType<IActionVariable[]>,
reqired: true,
default: () => []
},
modelValue: Array,
filter: String
},
emits: [
'update:modelValue'
],
setup(props) {
const variableName = (field) => {
const name = field.name;
return name.name;
}
const columns = [
{ name: 'actionName', label: 'アクション名', align: 'left', field: 'actionName', sortable: true },
{ name: 'displayName', label: '変数表示名', align: 'left', field: 'displayName', sortable: true },
{ name: 'name', label: '変数名', align: 'left', field: variableName, required: true, sortable: true }
];
const rows = props.vars.flatMap((v) => {
if (v.name.vars && v.name.vars.length > 0) {
return v.name.vars
.filter(o => o.vName && o.logicalOperator && o.field)
.map(o => ({
id: uuidv4(),
objectType: 'variable',
name: { name: `${v.name.name}.${o.vName}` },
actionName: v.name.actionName,
displayName: v.name.displayName
}));
} else {
return [{ objectType: 'variable', ...v }];
}
});
return {
columns,
rows: reactive(rows)
}
}
}
</script>

View File

@@ -0,0 +1,29 @@
<template>
<div class="q-py-md">
<q-tree
no-connectors
selected-color="primary"
default-expand-all
:nodes="LeftDataBus.root"
v-model:selected="flowNames1"
node-key="label"
>
</q-tree>
</div>
</template>
<script setup lang="ts">
import {
LeftDataBus,
setControlPanelE,
} from 'components/flowEditor/left/DataBus';
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor';
// 应该在page中用网络请求获取值并初始化组件
// 然后在page中执行setControlPane设置databus
const store = useFlowEditorStore();
const { flowNames1 } = storeToRefs(store);
setControlPanelE();
</script>

View File

@@ -0,0 +1,72 @@
import { reactive } from 'vue'
export const LeftDataBus = reactive<LeftData>({})
const defaultData = {
root: [
{
label: 'レコードを追加画面',
children: [
{
label: '追加画面表示した時',
header: 'rg',
value: '1-1',
group: 'g1',
children: []
},
{
label: '保存をクリックした時',
header: 'rg',
value: '1-2',
group: 'g1',
children: []
},
{
label: '保存成功した時',
header: 'rg',
value: '1-3',
group: 'g1',
children: []
},
]
},
{
label: 'レコード編集画面',
},
{
label: 'レコード詳細画面',
},
{
label: 'レコード一覧画面',
},
],
data: new Map([['g1', '1-1']])
}
export const setControlPanel = (rootData: LeftData) => {
const { root: dr, data: dd } = defaultData
LeftDataBus.title = rootData.title
LeftDataBus.root = rootData.root ?? dr
LeftDataBus.data = rootData.data ?? dd
}
export const setControlPanelE = () => {
const { root: dr, data: dd } = defaultData
// LeftDataBus.title = rootData.title
LeftDataBus.root = dr
LeftDataBus.data = dd
}
export interface LeftData {
title?: string
root?: ControlPanelData[]
data?: Map<string, string>
}
export interface ControlPanelData {
label: string,
header?: string,
value?: string,
group?: string,
children?: ControlPanelData[]
}

View File

@@ -0,0 +1,42 @@
<template>
<div
class="row"
style="
border-radius: 2px;
box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 0px 1px inset,
rgba(0, 0, 0, 0.3) 0px 0px 0px 1px;
"
>
<q-icon
class="self-center q-ma-sm"
name="widgets"
color="grey-9"
style="font-size: 2em"
/>
<div class="col-7 self-center ellipsis">
{{ actName }}
</div>
<div class="self-center">
<q-btn
outline
dense
label="変 更"
padding="none sm"
color="primary"
></q-btn>
</div>
</div>
</template>
<script>
import { computed } from 'vue';
export default {
props: ['actName'],
setup(props) {
const actName = computed(() => props.actName);
},
};
</script>

View File

@@ -0,0 +1,94 @@
<template>
<div class="row app-box">
<q-icon
class="self-center q-ma-sm"
name="widgets"
color="grey-9"
style="font-size: 2em"
/>
<div class="col-7 self-center ellipsis">
<a :href="!store.appInfo?'':`${authStore.currentDomain.kintoneUrl}/k/${store.appInfo?.appId}`" target="_blank" title="Kiontoneへ">
{{ store.appInfo?.name }}
</a>
</div>
<div class="self-center">
<q-btn
outline
dense
label="変 更"
padding="none sm"
color="primary"
@click="showAppDialog"
></q-btn>
</div>
</div>
<ShowDialog v-model:visible="showSelectApp" name="アプリ選択" @close="closeDg" min-width="50vw" min-height="50vh" >
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<AppSelectBox ref="appDg" name="アプリ" type="single" :filter="filter"></AppSelectBox>
</ShowDialog>
</template>
<script lang="ts">
import { defineComponent,ref } from 'vue';
import {AppInfo} from '../../types/ActionTypes'
import ShowDialog from '../../components/ShowDialog.vue';
import AppSelectBox from '../../components/AppSelectBox.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'src/stores/useAuthStore';
export default defineComponent({
name: 'AppSelector',
emits:[
"appSelected"
],
components:{
AppSelectBox,
ShowDialog
},
setup(props, context) {
const store = useFlowEditorStore();
const authStore=useAuthStore();
const appDg = ref();
const showSelectApp=ref(false);
const closeDg=(val :any)=>{
showSelectApp.value=false;
console.log("Dialog closed->",val);
if (val == 'OK') {
const data = appDg.value.selected[0];
console.log(data);
const appInfo={
appId:data.id ,
name:data.name
};
store.setApp(appInfo);
store.loadFlow();
}
}
const showAppDialog=()=>{
showSelectApp.value=true;
}
return {
store,
authStore,
showSelectApp,
showAppDialog,
closeDg,
appDg,
filter:ref('')
}
}
});
</script>
<style lang="scss">
.app-box{
border-radius: 2px;
box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 0px 1px inset,rgba(0, 0, 0, 0.3) 0px 0px 0px 1px;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<!-- <div class="q-pa-md q-gutter-sm"> -->
<q-tree :nodes="store.eventTree.screens" node-key="eventId" children-key="events" no-connectors
v-model:expanded="store.expandedScreen" :dense="true" :ref="tree">
<template v-slot:header-EVENT="prop">
<div :ref="prop.node.eventId" class="row col items-center no-wrap 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>
<div class="no-wrap" :class="getSelectedClass(prop.node)">{{ prop.node.label }}</div>
<q-space></q-space>
<!-- <q-icon v-if="prop.node.hasFlow" name="delete" color="negative" size="16px" class="q-mr-sm"></q-icon> -->
</div>
</template>
<template v-slot:header-CHANGE="prop">
<div class="row col items-start no-wrap event-node">
<div class="no-wrap" :class="getSelectedClass(prop.node)">{{ prop.node.label }}</div>
<q-space></q-space>
<q-icon name="add_circle" color="primary" size="16px" class="q-mr-sm"
@click="addChangeEvent(prop.node)"></q-icon>
</div>
</template>
<template v-slot:header-DELETABLE="prop">
<div class="row col items-start event-node" @click="onSelected(prop.node)">
<q-icon v-if="prop.node.eventId" name="play_circle" :color="prop.node.hasFlow ? 'green' : 'grey'" size="16px" class="q-mr-sm" />
<div class="no-wrap" :class="getSelectedClass(prop.node)">{{ prop.node.label }}</div>
<q-space></q-space>
<q-icon name="delete_forever" color="negative" size="16px" @click="deleteEvent(prop.node)"></q-icon>
</div>
</template>
</q-tree>
<show-dialog v-model:visible="showDialog" name="フィールド選択" @close="closeDg">
<field-select ref="appDg" name="フィールド" type="single" :fieldTypes="fieldTypes" :appId="store.appInfo?.appId"></field-select>
</show-dialog>
</template>
<script lang="ts">
import { QTree, useQuasar } from 'quasar';
import { ActionFlow, RootAction } from 'src/types/ActionTypes';
import { useFlowEditorStore } from 'stores/flowEditor';
import { defineComponent, ref, watchEffect } from 'vue';
import { IKintoneEvent, IKintoneEventGroup, IKintoneEventNode, kintoneEvent } from '../../types/KintoneEvents';
import FieldSelect from '../FieldSelect.vue';
import ShowDialog from '../ShowDialog.vue';
export default defineComponent({
name: 'EventTree',
components: {
ShowDialog,
FieldSelect,
},
setup(props, context) {
const $q = useQuasar();
const appDg = ref();
const store = useFlowEditorStore();
const showDialog = ref(false);
const tree = ref<QTree>();
const fieldTypes=[
'RADIO_BUTTON',
'DROP_DOWN',
'CHECK_BOX',
'MULTI_SELECT',
'USER_SELECT',
'GROUP_SELECT',
'ORGANIZATION_SELECT',
'DATE',
'DATETIME',
'TIME',
'SINGLE_LINE_TEXT',
'NUMBER'];
// const eventTree=ref(kintoneEvents);
// const selectedFlow = store.currentFlow;
// const expanded=ref();
const selectedEvent = ref<IKintoneEvent | undefined>(store.selectedEvent);
const selectedChangeEvent = ref<IKintoneEventGroup | undefined>(undefined);
const isFieldChange = (node: IKintoneEventNode) => {
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) => {
if (val == 'OK') {
if (!selectedChangeEvent.value) { return; }
const field = appDg.value.selected[0];
const eventid = `${selectedChangeEvent.value.eventId}.${field.code}`;
if (store.eventTree.findEventById(eventid)) {
return;
}
selectedChangeEvent.value?.events.push(new kintoneEvent(
field.name,
eventid,
selectedChangeEvent.value.eventId,
'DELETABLE'
));
tree.value?.expanded?.push(selectedChangeEvent.value.eventId);
tree.value?.expandAll();
}
};
const addChangeEvent = (node: IKintoneEventGroup) => {
if (store.appInfo === undefined) {
return;
}
selectedChangeEvent.value = node;
showDialog.value = true;
}
const deleteEvent = (node: IKintoneEvent) => {
if (!node.eventId) {
return;
}
store.deleteEvent(node);
store.selectFlow(undefined)
$q.notify({
type: 'positive',
caption: "通知",
message: `イベント ${node.label} 削除`
})
}
const onSelected = (node: IKintoneEvent) => {
if (!node.eventId) {
return;
}
selectedEvent.value = node;
if (store.appInfo === undefined) {
return;
}
const screen = store.eventTree.findEventById(node.parentId);
let flow = store.findFlowByEventId(node.eventId);
let screenName = screen !== null ? screen.label : '';
let nodeLabel = node.label;
// if(isFieldChange(node)){
// screenName=nodeLabel;
// nodeLabel=`${node.label}の値を変更したとき`;
// }
if (flow !== undefined && flow !== null) {
store.selectFlow(flow);
} else {
const root = new RootAction(node.eventId, screenName, nodeLabel)
const flow = new ActionFlow(root);
store.flows?.push(flow);
store.selectFlow(flow);
selectedEvent.value.flowData = flow;
}
};
watchEffect(()=>{
store.setCurrentEvent(selectedEvent.value);
});
return {
// eventTree,
// expanded,
appDg,
tree,
showDialog,
isFieldChange,
getSelectedClass,
onSelected,
selectedEvent,
addChangeEvent,
deleteEvent,
closeDg,
store,
fieldTypes
}
}
});
</script>
<style lang="scss">
.nowrap {
flex-wrap: nowarp;
text-wrap: nowarp;
}
.event-node {
cursor: pointer;
}
.selected-node {
color: $primary;
font-weight: bolder;
}
.event-node:hover {
background-color: $light-blue-1;
}
.delete-btn {
margin-right: 5px;
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<div class="row justify-center no-wrap" >
<div class="row">
<q-card class="action-node" :class="nodeStyle" :square="false" @click="onNodeClick" >
<q-toolbar class="col" >
<div class="text-subtitle2">{{ node.subTitle }}</div>
<q-space></q-space>
<q-btn flat round dense icon="more_horiz" size="sm" >
<q-menu auto-close anchor="top right">
<q-list>
<q-item clickable v-if="isRoot" @click="copyFlow">
<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 v-if="!isRoot" @click="onEditNode">
<q-item-section avatar><q-icon name="edit" ></q-icon></q-item-section>
<q-item-section >編集する</q-item-section>
</q-item>
<q-item clickable v-if="!isRoot" @click="onDeleteNode">
<q-item-section avatar><q-icon name="delete" ></q-icon></q-item-section>
<q-item-section>削除する</q-item-section>
</q-item>
<q-item clickable @click="onDeleteAllNode">
<q-item-section avatar><q-icon name="delete_sweep" ></q-icon></q-item-section>
<q-item-section >以下すべて削除する</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</q-toolbar>
<q-separator />
<q-card-section class="action-title">
<div class="row">
<span class="text-h7">{{ node.title }}</span>
<q-space></q-space>
<q-chip color="info" text-color="white" size="0.70rem" v-if="varName(node)" clickable>{{ varName(node) }}</q-chip>
</div>
</q-card-section>
<template v-if="hasBranch">
<q-separator />
<q-card-actions align="around">
<q-btn flat v-for="(point, index) in node.outputPoints" :key="index">
{{ point }}
</q-btn>
</q-card-actions>
</template>
</q-card>
</div>
</div>
<template v-if="hasBranch">
<node-line :action-node="node" @addNode="addNode" :left-columns="leftColumns" :right-columns="rightColumns"></node-line>
<div class="row justify-center no-wrap" >
<div v-for="(point, index) in node.outputPoints" :key="index" class="column" style="min-width: 300px;">
<div class="justify-center" >
<node-item v-if="nextNode(point)!==undefined" :key="nextNode(point).id" :isSelected="nextNode(point) === store.activeNode"
:actionNode="nextNode(point)" @addNode="addNodeFromItem" @nodeSelected="onNodeSelected" @nodeEdit="onNodeEdit"
@deleteNode="onDeleteNodeFromItem" @deleteAllNextNodes="onDeleteAllNextNodes" ></node-item>
</div>
</div>
</div>
</template>
<template v-if="!hasBranch">
<div class="row justify-center no-wrap" >
<node-line :action-node="node" @addNode="addNode" ></node-line>
</div>
<div>
<node-item v-if="nextNode('')!==undefined" :key="nextNode('').id" :isSelected="nextNode('') === store.activeNode"
:actionNode="nextNode('')" @addNode="addNodeFromItem" @nodeSelected="onNodeSelected" @nodeEdit="onNodeEdit"
@deleteNode="onDeleteNodeFromItem" @deleteAllNextNodes="onDeleteAllNextNodes" ></node-item>
</div>
</template>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from 'vue';
import { IActionNode, IActionProperty } from '../../types/ActionTypes';
import NodeLine, { Direction } from '../main/NodeLine.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
export default defineComponent({
name: 'NodeItem',
components: {
NodeLine
},
props: {
actionNode: {
type: Object as () => IActionNode,
required: true
},
isSelected: {
type: Boolean
}
},
emits: [
'addNode',
"nodeSelected",
"nodeEdit",
"deleteNode",
"deleteAllNextNodes",
"copyFlow"
],
setup(props, context) {
const store = useFlowEditorStore();
const hasBranch = computed(() => props.actionNode.outputPoints.length > 0);
const nodeStyle = computed(() => {
return {
'root-node': props.actionNode.isRoot,
'text-white': props.actionNode.isRoot,
'selected': props.isSelected && !props.actionNode.isRoot
};
});
const nextNode=(point:string)=>{
const nextId= props.actionNode.nextNodeIds.get(point);
if(!nextId) return undefined;
return store.currentFlow?.findNodeById(nextId);
}
/**
* アクションノード追加イベントを
* @param point 入力ポイント
*/
const addNode = (point: string) => {
context.emit('addNode', props.actionNode, point);
}
/**
* アクションノード追加イベントを
* @param point 入力ポイント
*/
const addNodeFromItem = (node:IActionNode,point: string) => {
context.emit('addNode', node, point);
}
const leftColumns=computed(()=>{
if(!props.actionNode.outputPoints || props.actionNode.outputPoints.length<2){
return 1;
}
const leftNode = nextNode(props.actionNode.outputPoints[0]);
if(leftNode){
return store.currentFlow?.getColumns(leftNode);
}else{
return 1;
}
});
const rightColumns=computed(()=>{
if(!props.actionNode.outputPoints || props.actionNode.outputPoints.length<2){
return 1;
}
const rightNode = nextNode(props.actionNode.outputPoints[1]);
if(rightNode){
return store.currentFlow?.getColumns(rightNode);
}else{
return 1;
}
});
/**
* ノード選択状態
*/
const onNodeClick = () => {
context.emit('nodeSelected', props.actionNode);
}
const onNodeSelected = (node: IActionNode) => {
context.emit('nodeSelected', node);
}
const onEditNode=()=>{
context.emit('nodeEdit', props.actionNode);
}
const onNodeEdit=(node:IActionNode)=>{
context.emit('nodeEdit', node);
}
/**
* ノードを削除する
*/
const onDeleteNode=()=>{
context.emit('deleteNode', props.actionNode);
}
/**
* ノードを削除する
*/
const onDeleteNodeFromItem=(node:IActionNode)=>{
context.emit('deleteNode', node);
}
/**
* ノードの以下すべて削除する
*/
const onDeleteAllNode=()=>{
context.emit('deleteAllNextNodes', props.actionNode);
};
/**
* ノードの以下すべて削除する
*/
const onDeleteAllNextNodes=(node:IActionNode)=>{
context.emit('deleteAllNextNodes', node);
};
/**
* 変数名取得
*/
const varName =(node:IActionNode)=>{
const prop = node.actionProps.find((prop) => prop.props.name === "verName");
return prop?.props.modelValue.name;
};
const copyFlow=()=>{
context.emit('copyFlow', props.actionNode);
}
return {
store,
node: props.actionNode,
nextNode,
isRoot: props.actionNode.isRoot,
hasBranch,
nodeStyle,
// getMode,
addNode,
addNodeFromItem,
onNodeClick,
onNodeSelected,
onEditNode,
onNodeEdit,
onDeleteNode,
onDeleteNodeFromItem,
onDeleteAllNode,
onDeleteAllNextNodes,
copyFlow,
varName,
leftColumns,
rightColumns
}
}
});
</script>
<style lang="scss">
.action-node {
min-width: 280px !important;
}
.action-title{
max-width: 280px !important;
overflow-wrap: anywhere;
}
.line {
height: 20px;
}
.line:after {
content: '';
background-color: $blue-7;
display: block;
width: 3px;
}
.add-icon {
font-size: 2em;
color: $blue-7;
}
.root-node {
background-color: $blue-7;
border-radius: 20px;
}
.action-node:not(.root-node):hover{
background-color: $light-blue-1;
}
.selected{
background-color: $yellow-1;
}
</style>

View File

@@ -0,0 +1,168 @@
<template>
<div class="row justify-center">
<svg class="node-line" style="width:100%" :viewBox="viewBox()">
<template v-if="!node.outputPoints || node.outputPoints.length===0" >
<polyline :points="points(getMode('')).linePoints" class="line" ></polyline>
<text class="add-icon"
@click="addNode(node,'')"
:x="points(getMode('')).iconPoint.x"
:y="points(getMode('')).iconPoint.y"
font-family="Arial" font-size="25"
text-anchor="middle" dy=".3em" style="cursor: pointer;" >
</text>
</template>
<template v-for="(point, index) in node.outputPoints" :key="index" >
<polyline :points="points(getMode(point)).linePoints" class="line" ></polyline>
<text class="add-icon"
@click="addNode(node,point)"
:x="points(getMode(point)).iconPoint.x"
:y="points(getMode(point)).iconPoint.y"
font-family="Arial" font-size="25"
text-anchor="middle" dy=".3em" style="cursor: pointer;" >
</text>
</template>
</svg>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, computed, PropType } from 'vue';
import { IActionNode, ActionNode, ActionFlow, RootAction } from '../../types/ActionTypes';
export enum Direction {
Default = "None",
Left = "LEFT",
Right = "RIGHT",
LeftNotNext = "LEFTNOTNEXT",
RightNotNext = "RIGHTNOTNEXT",
}
export default defineComponent({
name: 'NodeLine',
props: {
actionNode: {
type: Object as PropType<IActionNode>,
required: true
},
leftColumns:{
type:Number,
required:false
},
rightColumns:{
type:Number,
required:false
}
},
emits: ['addNode'],
setup(props,context) {
const hasBranch = computed(() => props.actionNode.outputPoints.length > 0);
const getMode = (point: string):Direction => {
if (point === '' || props.actionNode.outputPoints.length === 0) {
return Direction.Default;
}
if (point === props.actionNode.outputPoints[0]) {
if (props.actionNode.nextNodeIds.get(point)) {
return Direction.Left;
} else {
return Direction.LeftNotNext;
}
} else {
if (props.actionNode.nextNodeIds.get(point)) {
return Direction.Right;
} else {
return Direction.RightNotNext;
}
}
}
const points = (mode:Direction) => {
let startX ,endX;
const leftColumn=props.leftColumns?props.leftColumns:1;
const rightColumn=props.rightColumns?props.rightColumns:1;
switch (mode) {
case Direction.Left:
startX = leftColumn*300/2.0;
endX = ((leftColumn+rightColumn)/2.0 - 0.25)*300;
return {
linePoints: `${startX}, 60, ${startX}, 40, ${endX}, 40, ${endX}, 0`,
iconPoint: { x: endX, y: 20 }
};
case Direction.Right:
startX = ((leftColumn+rightColumn)/2.0 + 0.25)*300;
endX = (leftColumn+(rightColumn/2.0))*300;
return {
linePoints: `${startX}, 0, ${startX}, 40, ${endX}, 40, ${endX}, 60`,
iconPoint: { x: startX, y: 20 }
};
case Direction.LeftNotNext:
startX = ((leftColumn+rightColumn)/2.0 - 0.25)*300;
return {
linePoints: `${startX}, 0, ${startX}, 40`,
iconPoint: { x: startX, y: 20 }
};
case Direction.RightNotNext:
startX = ((leftColumn+rightColumn)/2.0 + 0.25)*300;
return {
linePoints: `${startX}, 0, ${startX}, 40`,
iconPoint: { x: startX, y: 20 }
};
default:
return {
linePoints: '150, 0, 150, 60',
iconPoint: { x: 150, y: 30 }
};
}
};
const addNode=(prveNode:IActionNode,point:string)=>{
context.emit('addNode',point);
}
const viewBox=()=>{
let columns=0;
if(props.leftColumns!==undefined) columns+=props.leftColumns;
if(props.rightColumns!==undefined) columns+=props.rightColumns;
if(columns===0) columns=1;
const width= columns*300;
return `0 0 ${width} 60`;
};
return {
node: props.actionNode,
getMode,
hasBranch,
points,
addNode,
viewBox
}
}
});
</script>
<style lang="scss">
.node-line {
height: 60px;
width: 240px;
}
.line {
stroke: $blue-7;
fill: none;
stroke-width: 2;
}
.add-icon {
stroke: $blue-8;
fill: $blue-8;
font-family: Arial;
pointer-events: all;
font-size: 2.0em;
}
.add-icon:hover{
stroke: $blue-8;
fill:$blue-8;
font-weight: bold;
font-size: 2.4em;
}
</style>

View File

@@ -32,4 +32,3 @@ export interface AppInfo {
creator?:User; creator?:User;
modifier?:User; modifier?:User;
} }

View File

@@ -0,0 +1,75 @@
<template>
<div>
<div v-for="(item, index) in componentData" :key="index">
<component :is="item.component" v-bind="item.props" :connectProps="connectProps" v-model="item.props.modelValue"></component>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent,computed } from 'vue';
import InputText from '../right/InputText.vue';
import SelectBox from '../right/SelectBox.vue';
import DatePicker from '../right/DatePicker.vue';
import FieldInput from '../right/FieldInput.vue';
import EventSetter from '../right/EventSetter.vue';
import { IActionProperty, IProp } from 'src/types/ActionTypes';
export default defineComponent({
name: 'ActionProperty',
components: {
InputText,
SelectBox,
DatePicker,
FieldInput,
EventSetter
},
props: {
jsonData: {
type: Object,
required: true,
},
jsonValue:{
type: Object,
required: false,
}
},
setup(props){
const componentData=computed<Array<IActionProperty>>(()=>{
return props.jsonData.elements.map((element: any) => {
if(props.jsonValue != undefined )
{
if(props.jsonValue.hasOwnProperty(element.props.name))
{
element.props.modelValue = props.jsonValue[element.props.name];
}
else
{
element.props.modelValue = '';
}
}
return {
component: element.component,
props: element.props,
};
});
});
const connectProps=(props:IProp)=>{
const connProps:any={};
if(props && "connectProps" in props && props.connectProps!=undefined){
for(let connProp of props.connectProps){
let targetProp = componentData.value.find((prop)=>prop.props.name===connProp.propName);
if(targetProp){
connProps[connProp.key]=targetProp;
}
}
}
return connProps;
}
return{
componentData,
connectProps
}
}
});
</script>

View File

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

View File

@@ -0,0 +1,112 @@
<template>
<div v-bind="$attrs">
<q-field :label="displayName" labelColor="primary" stack-label
:rules="rulesExp"
lazy-rules="ondemand"
v-model="selectedApp"
ref="fieldRef">
<template v-slot:control>
<q-card flat class="full-width">
<q-card-actions vertical>
<q-btn color="grey-3" text-color="black" @click="() => { dgIsShow = true }">アプリ選択</q-btn>
</q-card-actions>
<q-card-section class="text-caption">
<div v-if="selectedApp.app.name">
{{ selectedApp.app.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="50vh">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<AppSelectBox ref="appDg" name="アプリ" type="single" :filter="filter"></AppSelectBox>
</ShowDialog>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref, watchEffect } from 'vue';
import ShowDialog from '../ShowDialog.vue';
import AppSelectBox from '../AppSelectBox.vue';
export default defineComponent({
inheritAttrs: false,
name: 'AppSelect',
components: {
ShowDialog,
AppSelectBox
},
props: {
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
modelValue: {
type: Object,
default: null
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const appDg = ref();
const fieldRef=ref();
const dgIsShow = ref(false)
const selectedApp = props.modelValue && props.modelValue.app ? props.modelValue : reactive({app:{}});
const closeDg = (state: string) => {
dgIsShow.value = false;
if (state == 'OK') {
selectedApp.app = appDg.value.selected[0];
fieldRef.value.validate();
}
};
//ルール設定
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}を選択してください。`;
const requiredExp = props.required ? [((val: any) => (!!val && !!val.app && !!val.app.name) || errmsg)] : [];
const rulesExp = [...requiredExp, ...customExp];
watchEffect(() => {
emit('update:modelValue', selectedApp);
});
return {
filter: ref(''),
dgIsShow,
appDg,
fieldRef,
closeDg,
selectedApp,
rulesExp
};
}
});
</script>

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

@@ -0,0 +1,91 @@
<template>
<div class="" v-bind="$attrs">
<q-field v-model="color" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label :bottom-slots="!isSelected" :rules="rulesExp">
<template v-slot:control>
<q-chip text-color="black" color="white" v-if="isSelected">
<div class="row">
<div class="col-4">
<q-avatar class="shadow-1" :style="{ background: color }" size="xs"></q-avatar>
</div>
<div class="col">
{{ color }}
</div>
</div>
</q-chip>
</template>
<template v-slot:append>
<q-icon name="colorize" class="cursor-pointer" color="primary" >
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-color no-header default-view="palette" v-model="color" />
</q-popup-proxy>
</q-icon>
</template>
<template v-slot:hint>
{{ placeholder }}
</template>
</q-field>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref,watchEffect } from 'vue';
export default defineComponent({
inheritAttrs:false,
name: 'ColorPicker',
components: {
},
props: {
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
hint: {
type: String,
default: '',
},
modelValue: {
type: String,
default: null
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const color = ref(props.modelValue??"");
const isSelected = computed(()=>props.modelValue && props.modelValue!=="");
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg ),"anyColor"]:[];
const rulesExp=[...requiredExp,...customExp];
watchEffect(()=>{
emit('update:modelValue', color.value);
});
return {
color,
isSelected,
rulesExp
};
}
});
</script>

View File

@@ -0,0 +1,174 @@
<template>
<div v-bind="$attrs">
<q-field v-model="tree" :label="displayName" labelColor="primary" stack-label>
<template v-slot:control>
<q-card flat class="full-width">
<q-card-actions vertical>
<q-btn color="grey-3" text-color="black" :disable="btnDisable" @click="showDg()">クリックで設定{{ isSetted ?
'設定済み' : '未設定' }}</q-btn>
</q-card-actions>
<q-card-section class="text-caption">
<div v-if="!isSetted">{{ placeholder }}</div>
<div v-else>{{ conditionString }}</div>
</q-card-section>
</q-card>
</template>
</q-field>
<condition-editor v-model:show="show" v-model:conditionTree="tree" @closed="onClosed"></condition-editor>
</div>
</template>
<script lang="ts">
import { ConditionNode, ConditionTree, Operator, OperatorListItem } from 'app/src/types/Conditions';
import { computed, defineComponent, provide, reactive, ref, watchEffect } from 'vue';
import ConditionEditor from '../ConditionEditor/ConditionEditor.vue';
import { IActionProperty } from 'src/types/ActionTypes';
export default defineComponent({
name: 'FieldInput',
inheritAttrs: false,
components: {
ConditionEditor
},
props: {
context: {
type: Array<IActionProperty>,
default: '',
},
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
hint: {
type: String,
default: '',
},
modelValue: {
type: String,
default: null
},
sourceType: {
type: String,
default: 'field'
},
connectProps:{
type:Object,
default:()=>({})
},
onlySourceSelect: {
type: Boolean,
default: false
},
operatorList: {
type: Array,
},
inputConfig: {
type: Object,
default: () => ({
left: {
canInput: false,
buttonsConfig: [
{ label: 'フィールド', color: 'primary', type: 'FieldAdd' },
{ label: '変数', color: 'green', type: 'VariableAdd' },
]
},
right: {
canInput: true,
buttonsConfig: [
{ label: '変数', color: 'green', type: 'VariableAdd' },
]
},
})
}
},
setup(props, { emit }) {
let source = reactive(props.connectProps["source"]);
if(!source){
source = props.context.find(element => element.props.name === 'sources');
}
if (source) {
if (props.sourceType === 'field') {
provide('sourceFields', computed(() => source.props?.modelValue?.fields ?? []));
} else if (props.sourceType === 'app') {
provide('sourceApp', computed(() => source.props?.modelValue?.app?.id));
}
}
provide('leftDynamicItemConfig', props.inputConfig.left);
provide('rightDynamicItemConfig', props.inputConfig.right);
provide('Operator', props.operatorList);
const btnDisable = computed(() => {
const onlySourceSelect = props.onlySourceSelect;
if (!onlySourceSelect) {
return false;
}
if (props.sourceType === 'field') {
return source?.props?.modelValue?.fields?.length ?? 0 > 0;
} else if (props.sourceType === 'app') {
return source?.props?.modelValue?.app?.id ? false : true
}
return true;
})
const appDg = ref();
const show = ref(false);
const tree = reactive(new ConditionTree());
if (props.modelValue && props.modelValue !== '') {
tree.fromJson(props.modelValue);
} else {
const newNode = new ConditionNode({}, (props.operatorList && props.operatorList.length > 0) ? props.operatorList[0] as OperatorListItem : Operator.Equal, '', tree.root);
tree.addNode(tree.root, newNode);
}
const isSetted = ref(props.modelValue && props.modelValue !== '');
const conditionString = computed(() => {
const condiStr= tree.buildConditionString(tree.root);
return condiStr==='()'?'(条件なし)':condiStr;
});
const showDg = () => {
show.value = true;
};
const onClosed = (val: string) => {
if (val == 'OK') {
isSetted.value = true;
tree.setQuery(tree.buildConditionQueryString(tree.root));
const conditionJson = tree.toJson();
emit('update:modelValue', conditionJson);
}
};
watchEffect(() => {
tree.setQuery(tree.buildConditionQueryString(tree.root));
const conditionJson = tree.toJson();
emit('update:modelValue', conditionJson);
});
return {
appDg,
isSetted,
show,
showDg,
onClosed,
tree,
conditionString,
btnDisable
};
}
});
</script>

View File

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

View File

@@ -0,0 +1,275 @@
<template>
<div>
<q-field :label="displayName" labelColor="primary" stack-label
v-model="processingProps"
:rules="rulesExp"
lazy-rules="ondemand"
ref="fieldRef"
>
<template v-slot:control>
<q-card flat class="full-width">
<q-card-actions vertical>
<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="processingObjectsInputDisplay && processingObjectsInputDisplay.length>0">
<div v-for="(item) in processingObjectsInputDisplay" :key="item">{{ item }}</div>
</div>
<div v-else>{{ placeholder }}</div>
</q-card-section>
</q-card>
</template>
</q-field>
<show-dialog v-model:visible="dgIsShow" name="集計処理" @close="closeDg" min-width="50vw" min-height="60vh">
<div class="q-mx-md q-mb-md">
<q-input v-model="processingProps.name" type="text" label-color="primary" label="集計結果の変数名"
placeholder="集計結果を格納する変数名を入力してください" stack-label />
</div>
<div class="q-mx-md">
<div class="row q-col-gutter-x-xs flex-center">
<div class="col-5">
<div class="q-mx-xs">データソース</div>
</div>
<div class="col-2">
<div class="q-mx-xs">集計計算</div>
</div>
<div class="col-4">
<div class="q-mx-xs">集計結果変数名</div>
</div>
<div class="col-1"><q-btn flat round dense icon="add" size="sm" @click="addProcessingObject" />
</div>
</div>
<div class="q-my-sm" v-for="(item, index) in processingObjects" :key="item.id">
<div class="row q-col-gutter-x-xs flex-center">
<div class="col-5">
<ConditionObject v-model="item.field" />
</div>
<div class="col-2 q-pa-sm">
<q-select v-model="item.logicalOperator" :options="logicalOperators" outlined dense></q-select>
</div>
<div class="col-4">
<q-input v-model="item.vName" type="text" outlined dense />
</div>
<div class="col-1">
<q-btn flat round dense icon="delete" size="sm" @click="() => deleteProcessingObject(index)" />
</div>
</div>
</div>
</div>
</show-dialog>
</div>
</template>
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { computed, defineComponent, provide, reactive, ref, watchEffect } from 'vue';
import ConditionObject from '../ConditionEditor/ConditionObject.vue';
import ShowDialog from '../ShowDialog.vue';
type Props = {
props?: {
name: string;
modelValue?: {
fields: {
type: string;
label: string;
code: string;
}[]
} | string
}
};
type ProcessingObjectType = {
field?: {
sharedText: string;
objectType: 'field';
};
logicalOperator?: string;
vName?: string;
id: string;
}
type ValueType = {
name: string;
actionName: string,
displayName: string,
vars: ProcessingObjectType[];
}
export default defineComponent({
name: 'DataProcessing',
inheritAttrs: false,
components: {
ShowDialog,
ConditionObject,
},
props: {
context: {
type: Array<Props>,
default: '',
},
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
modelValue: {
type: Object as () => ValueType,
},
placeholder: {
type: String,
default: '',
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
}
},
setup(props, { emit }) {
const fieldRef=ref();
const source = props.context.find(element => element?.props?.name === 'sources')
if (source) {
provide('sourceFields', computed(() => {
const modelValue = source.props?.modelValue;
if (modelValue && typeof modelValue !== 'string') {
return modelValue.fields;
}
return null;
}));
provide('sourceApp', computed(() => source.props?.modelValue?.app?.id));
}
const actionName = props.context.find(element => element?.props?.name === 'displayName')
const processingProps: ValueType = props.modelValue && props.modelValue.vars
? reactive(props.modelValue)
: reactive({
name: '',
actionName: actionName?.props?.modelValue as string,
displayName: '結果(戻り値)',
vars: [
{
id: uuidv4(),
field:{
objectType:'field',
sharedText:''
}
}]
});
const closeDg = () => {
fieldRef.value.validate();
emit('update:modelValue', processingProps);
}
const processingObjects = processingProps.vars;
const deleteProcessingObject = (index: number) => {
if(processingObjects.length >0){
processingObjects.splice(index, 1);
}
if(processingObjects.length===0){
addProcessingObject();
}
}
const processingObjectsInputDisplay = computed(() =>
processingObjects ?
processingObjects
.filter(item => item.field && item.logicalOperator && item.vName)
.map(item => {
return`var(${processingProps.name}.${item.vName}) = ${item.field?.sharedText}`
})
: []
);
const addProcessingObject=()=>{
processingObjects.push({
id: uuidv4(),
field:{
objectType:'field',
sharedText:''
}
});
}
//集計処理方法
const logicalOperators = ref([
{
"operator": "",
"label": "なし"
},
{
"operator": "SUM",
"label": "合計"
},
{
"operator": "AVG",
"label": "平均"
},
{
"operator": "MAX",
"label": "最大値"
},
{
"operator": "MIN",
"label": "最小値"
},
{
"operator": "COUNT",
"label": "カウント"
},
{
"operator": "FIRST",
"label": "最初の値"
}
]);
const checkInput=(val:ValueType)=>{
if(!val){
return false;
}
if(!val.name){
return "集計結果の変数名を入力してください";
}
if(!val.vars || val.vars.length==0){
return "集計処理を設定してください";
}
if(val.vars.some((x)=>!x.vName)){
return "集計結果変数名を入力してください";
}
return true;
}
const customExp = props.rules === undefined ? [] : eval(props.rules);
const requiredExp = props.required ? [(val: any) => checkInput(val)] : [];
const rulesExp = [...requiredExp, ...customExp];
watchEffect(() => {
emit('update:modelValue', processingProps);
});
return {
uuidv4,
dgIsShow: ref(false),
closeDg,
processingObjects,
processingProps,
addProcessingObject,
deleteProcessingObject,
logicalOperators,
processingObjectsInputDisplay,
rulesExp,
fieldRef
};
},
});
</script>
<style lang="scss"></style>

View File

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

View File

@@ -0,0 +1,103 @@
<template>
<div v-bind="$attrs">
<q-input :label="displayName" v-model="inputValue" label-color="primary"
:placeholder="placeholder"
:rules="rulesExp"
stack-label>
<template v-slot:append>
<q-btn round dense flat icon="add" @click="addButtonEvent()" />
</template>
</q-input>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,watchEffect } from 'vue';
import { useFlowEditorStore } from '../../stores/flowEditor';
import { IKintoneEventGroup,kintoneEvent } from 'src/types/KintoneEvents';
export default defineComponent({
name: 'EventSetter',
inheritAttrs:false,
props: {
displayName:{
type: String,
default: '',
},
name:{
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
hint:{
type: String,
default: '',
},
modelValue: {
type: String,
default: '',
},
connectProps:{
type:Object,
default:undefined
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props , { emit }) {
const inputValue = ref(props.modelValue);
const store = useFlowEditorStore();
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}を入力してください。`;
const requiredExp = props.required ? [((val: any) => !!val || errmsg)] : [];
const rulesExp = [...requiredExp, ...customExp];
const addButtonEvent=()=>{
const eventId =store.currentFlow?.getRoot()?.name;
if(eventId===undefined){return;}
let displayName = inputValue.value;
if(props.connectProps!==undefined && "displayName" in props.connectProps){
displayName =props.connectProps["displayName"].props.modelValue;
}
const customButtonId=`${eventId}.customButtonClick`;
const findedEvent = store.eventTree.findEventById(customButtonId);
if(findedEvent && "events" in findedEvent){
const customEvents = findedEvent as IKintoneEventGroup;
const addEventId = customButtonId+"." + inputValue.value;
if(store.eventTree.findEventById(addEventId)){
return;
}
customEvents.events.push(new kintoneEvent(
displayName,
addEventId,
customButtonId,
'DELETABLE'
));
}
}
watchEffect(() => {
emit('update:modelValue', inputValue.value);
});
return {
inputValue,
addButtonEvent,
rulesExp
};
},
});
</script>

View File

@@ -0,0 +1,137 @@
<template>
<div v-bind="$attrs">
<q-field v-model="selectedField" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label
:bottom-slots="!isSelected"
:rules="rulesExp"
>
<template v-slot:control>
<q-chip color="primary" text-color="white" v-if="isSelected">
{{ selectedField.name }}
</q-chip>
</template>
<template v-slot:hint v-if="!isSelected">
{{ placeholder }}
</template>
<template v-slot:append>
<q-icon name="search" class="cursor-pointer" color="primary" @click="showDg" />
</template>
</q-field>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg" min-width="400px">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<field-select ref="appDg" name="フィールド" :type="selectType" :appId="store.appInfo?.appId" :selectedFields="selectedFields" :fieldTypes="fieldTypes" :filter="filter"></field-select>
</show-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watchEffect, computed } from 'vue';
import ShowDialog from '../ShowDialog.vue';
import FieldSelect from '../FieldSelect.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
interface IField {
name: string,
code: string,
type: string
}
export default defineComponent({
name: 'FieldInput',
inheritAttrs:false,
components: {
ShowDialog,
FieldSelect,
},
props: {
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
selectType:{
type:String,
default:'single'
},
fieldTypes:{
type:Array,
default:()=>[]
},
hint: {
type: String,
default: '',
},
modelValue: {
type: Object,
default: null
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
},
requiredMessage: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const appDg = ref();
const show = ref(false);
const selectedField = ref(props.modelValue);
const selectedFields =computed(()=>!selectedField.value?[]: [selectedField.value]);
const store = useFlowEditorStore();
const isSelected = computed(() => {
return selectedField.value !== null && typeof selectedField.value === 'object' && ('name' in selectedField.value)
});
//ルール設定
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required ? [((val: any) => (!!val && typeof val==='object' && !!val.name) || errmsg)] : [];
const rulesExp = [...requiredExp, ...customExp];
const showDg = () => {
show.value = true;
};
const closeDg = (val: string) => {
if (val == 'OK') {
selectedField.value = appDg.value.selected[0];
}
};
watchEffect(() => {
emit('update:modelValue', selectedField.value);
});
return {
store,
appDg,
show,
showDg,
closeDg,
selectedField,
isSelected,
filter:ref(''),
selectedFields,
rulesExp
};
}
});
</script>

View File

@@ -0,0 +1,113 @@
<template>
<div v-bind="$attrs">
<q-input :label="displayName" v-model="inputValue" label-color="primary" :placeholder="placeholder" stack-label
:rules="rulesExp" :maxlength="maxLength">
<template v-slot:append v-if="hint !== ''">
<q-icon name="help" size="22px" color="blue-8">
<q-tooltip class="bg-yellow-2 text-black shadow-4" anchor="bottom right">
<div class="hint-text" v-html="hint" />
</q-tooltip>
</q-icon>
</template>
</q-input>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watchEffect, computed } from 'vue';
export default defineComponent({
name: 'InputText',
inheritAttrs: false,
props: {
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
fieldTypes:{
type:Array,
default:()=>[]
},
hint: {
type: String,
default: '',
},
maxLength: {
type: Number,
default: undefined
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
modelValue: {
type: null as any,
default: '',
},
},
setup(props, { emit }) {
const inputValue = computed({
get: () => {
if (props.modelValue !== null && typeof props.modelValue === 'object' && 'name' in props.modelValue) {
return props.modelValue.name;
} else {
return props.modelValue;
}
},
set: (val) => {
if (props.name === 'verName') {
// return props.modelValue.name;
emit('update:modelValue', { name: val });
} else {
emit('update:modelValue', val);
}
},
});
// 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];
// const finalValue = computed(() => {
// return props.name !== 'verName' ? inputValue.value : {
// name: inputValue.value,
// };
// });
// watchEffect(() => {
// emit('update:modelValue', finalValue);
// });
return {
inputValue,
showhint: ref(false),
rulesExp
};
},
});
</script>
<style lang="scss">
.hint-text {
white-space: always;
max-width: 450px;
font-size: 1.2em;
}
</style>

View File

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

View File

@@ -0,0 +1,82 @@
<template>
<div class="" v-bind="$attrs">
<q-input v-model.number="numValue" type="number" :label="displayName" label-color="primary" stack-label bottom-slots
:min="min"
:max="max"
:rules="rulesExp"
>
<template v-slot:hint>
{{ placeholder }}
</template>
</q-input>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, watchEffect } from 'vue';
export default defineComponent({
name: 'NumInput',
inheritAttrs:false,
components: {
},
props: {
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
hint: {
type: String,
default: '',
},
min:{
type:Number,
default:undefined
},
max:{
type:Number,
default:undefined
},
//[val=>!!val ||'数値を入力してください',val=>val<=100 && val>=1 || '1-100の範囲内の数値を入力してください']
rules:{
type:String,
default:undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
modelValue: {
type: [Number , String],
default: undefined
},
},
setup(props, { emit }) {
const numValue = ref(props.modelValue);
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[];
const rulesExp=[...requiredExp,...customExp];
watchEffect(()=>{
emit("update:modelValue",numValue.value);
});
return {
numValue,
rulesExp
};
}
});
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div>
<div v-for="(item, index) in properties" :key="index" >
<component :is="item.component" v-bind="item.props" :context="properties" :connectProps="connectProps(item.props)" v-model="item.props.modelValue"></component>
</div>
</div>
</template>
<script lang="ts">
/**
* プロパティ属性設定生成する
*/
import { PropType, defineComponent,ref } from 'vue';
import InputText from '../right/InputText.vue';
import SelectBox from '../right/SelectBox.vue';
import DatePicker from '../right/DatePicker.vue';
import FieldInput from '../right/FieldInput.vue';
import AppFieldSelect from './AppFieldSelect.vue';
import MuiltInputText from '../right/MuiltInputText.vue';
import ConditionInput from '../right/ConditionInput.vue';
import EventSetter from '../right/EventSetter.vue';
import ColorPicker from './ColorPicker.vue';
import NumInput from './NumInput.vue';
import DataProcessing from './DataProcessing.vue';
import DataMapping from './DataMapping.vue';
import AppSelect from './AppSelect.vue';
import CascadingDropDown from './CascadingDropDown.vue';
import { IActionNode,IActionProperty,IProp } from 'src/types/ActionTypes';
export default defineComponent({
name: 'PropertyList',
components: {
InputText,
SelectBox,
DatePicker,
FieldInput,
AppFieldSelect,
MuiltInputText,
ConditionInput,
EventSetter,
ColorPicker,
NumInput,
DataProcessing,
DataMapping,
AppSelect,
CascadingDropDown
},
props: {
nodeProps: {
type: Object as PropType<Array<IActionProperty>>,
required: true,
},
jsonValue:{
type: Object,
required: false,
}
},
setup(props, context) {
const properties=ref(props.nodeProps);
const connectProps=(props:IProp)=>{
const connProps:any={context:properties};
if(props && "connectProps" in props && props.connectProps!=undefined){
for(let connProp of props.connectProps){
let targetProp = properties.value.find((prop)=>prop.props.name===connProp.propName);
if(targetProp){
connProps[connProp.key]=targetProp;
}
}
}
return connProps;
}
return {
properties,
connectProps
}
}
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,97 @@
<template>
<div class="q-pa-md q-gutter-sm">
<q-drawer
side="right"
:show-if-above="false"
bordered
:width="301"
:breakpoint="500"
class="bg-grey-3"
:model-value="showPanel"
elevated
overlay
>
<q-form @submit="save" autocomplete="off" class="full-height">
<q-card class="column" style="max-width: 300px;min-height: 100%">
<q-card-section>
<div class="text-h6">{{ actionNode?.subTitle }}設定</div>
</q-card-section>
<q-card-section class="col q-pt-none">
<property-list :node-props="actionProps" v-if="showPanel" ></property-list>
</q-card-section>
<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="更新" type="submit" outline dense padding="none sm" color="primary" />
</q-card-actions>
</q-card>
</q-form>
</q-drawer>
</div>
</template>
<script lang="ts">
import { ref,defineComponent, PropType ,watchEffect} from 'vue'
import PropertyList from 'components/right/PropertyList.vue';
import { IActionNode, IActionProperty } from 'src/types/ActionTypes';
export default defineComponent({
name: 'PropertyPanel',
components: {
PropertyList
},
props: {
actionNode:{
type:Object as PropType<IActionNode>,
required:true
},
drawerRight:{
type:Boolean,
required:true
}
},
emits: [
'update:drawerRight',
'saveActionProps'
],
setup(props,{emit}) {
const showPanel =ref(props.drawerRight);
const cloneProps = (actionProps:IActionProperty[]):IActionProperty[]|null=>{
if(!actionProps){
return null;
}
const json=JSON.stringify(actionProps);
return JSON.parse(json);
}
const actionProps =ref(cloneProps(props.actionNode?.actionProps));
watchEffect(() => {
if(showPanel.value!==undefined){
showPanel.value = props.drawerRight;
}
showPanel.value = props.drawerRight;
actionProps.value= cloneProps(props.actionNode?.actionProps);
});
const cancel = async() =>{
showPanel.value = false;
emit('update:drawerRight',false )
}
const save = async () =>{
showPanel.value=false;
emit('saveActionProps', actionProps.value);
emit('update:drawerRight',false );
}
return {
cancel,
save,
actionProps,
showPanel
}
}
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div v-bind="$attrs">
<q-select v-model="selectedValue" :use-chips="multiple" :label="displayName" label-color="primary"
:options="options"
stack-label
:rules="rulesExp"
:multiple="multiple"/>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,watchEffect,computed } from 'vue';
export default defineComponent({
name: 'SelectBox',
inheritAttrs:false,
props: {
displayName:{
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
options: {
type: Array,
required: true,
},
selectType:{
type:String,
default:'',
},
modelValue: {
type: [Array,String],
default: null,
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
},
setup(props, { emit }) {
const selectedValue = ref(props.modelValue);
const multiple = computed(()=>{
return props.selectType==='multiple'
});
watchEffect(() => {
emit('update:modelValue', selectedValue.value);
});
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}を選択してください。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[];
const rulesExp=[...requiredExp,...customExp];
return {
selectedValue,
multiple,
rulesExp
};
},
});
</script>

View File

@@ -0,0 +1,22 @@
import { api } from 'boot/axios';
export class Auth
{
async login(user:string,pwd:string):Promise<boolean>
{
const params = new URLSearchParams();
params.append('username', user);
params.append('password', pwd);
try{
const result = await api.post(`api/token`,params);
console.info(result);
localStorage.setItem('Token', result.data.access_token);
return true;
}catch(e)
{
console.info(e);
return false;
}
}
}

View File

@@ -0,0 +1,59 @@
import { api } from 'boot/axios';
import { ActionFlow } from 'src/types/ActionTypes';
export class FlowCtrl {
async getFlows(appId: string): Promise<ActionFlow[]> {
const flows: ActionFlow[] = [];
try {
const result = await api.get(`api/flows/${appId}`);
//console.info(result.data);
if (!result.data || !Array.isArray(result.data)) {
return [];
}
for (const flow of result.data) {
flows.push(ActionFlow.fromJSON(flow.content));
}
return flows;
} catch (error) {
console.error(error);
return flows;
}
}
async SaveFlow(jsonData: any): Promise<boolean> {
const result = await api.post('api/flow', jsonData);
console.info(result.data);
return true;
}
/**
* フローを更新する
* @param jsonData
* @returns
*/
async UpdateFlow(jsonData: any): Promise<boolean> {
const result = await api.put('api/flow/' + jsonData.flowid, jsonData);
console.info(result.data);
return true;
}
/**
* フローを消去する
* @param flowId
* @returns
*/
async DeleteFlow(flowId: string): Promise<boolean> {
const result = await api.delete('api/flow/' + flowId);
console.info(result.data);
return true;
}
/**
* デプロイ
* @param appid
* @returns
*/
async depoly(appid: string): Promise<boolean> {
const result = await api.post(`api/v1/createjstokintone?app=${appid}`);
console.info(result.data);
return true;
}
}

View File

@@ -1 +1,25 @@
// app global css in SCSS form // app global css in SCSS form
::-webkit-scrollbar {
height: 12px;
width: 14px;
background: transparent;
z-index: 12;
overflow: visible;
}
::-webkit-scrollbar-thumb {
width: 10px;
background-color: #c1c1c1;
border-radius: 10px;
z-index: 12;
border: 4px solid rgba(0, 0, 0, 0);
background-clip: padding-box;
transition: background-color .32s ease-in-out;
margin: 4px;
min-height: 32px;
min-width: 32px;
}
::-webkit-scrollbar-thumb:hover {
background: #c1c1c1;
}

View File

@@ -2,38 +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>
Kintone App Builder {{ productName }}
<q-badge align="top" outline>V{{ env.version }}</q-badge> <q-badge align="top" outline>V{{ version }}</q-badge>
</q-toolbar-title> </q-toolbar-title>
<domain-selector></domain-selector>
<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>
v-model="leftDrawerOpen"
:show-if-above="false"
bordered
>
<q-list> <q-list>
<q-item-label <q-item-label header>
header メニュー
>
Essential Links
</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>
@@ -44,105 +34,105 @@
</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 { useAuthStore } from 'stores/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: 'rule', caption: 'アプリを管理する',
icon: 'rule', icon: 'widgets',
link: '/#/ruleEditor', link: '/#/app',
target:'_self' target: '_self'
}, },
// {
// title: '条件エディター',
// caption: 'condition',
// icon: 'tune',
// link: '/#/condition',
// target:'_self'
// },
{ {
title:'', title: '',
isSeparator:true isSeparator: true
}, },
{ // {
title:'Kintone ポータル', // title:'Kintone ポータル',
caption:'Kintone', // caption:'Kintone',
icon:'cloud_queue', // icon:'cloud_queue',
link:'https://mfu07rkgnb7c.cybozu.com/k/#/portal' // link:'https://mfu07rkgnb7c.cybozu.com/k/#/portal'
}, // },
{ // {
title:'CUSTOMINE', // title:'CUSTOMINE',
caption:'gusuku', // caption:'gusuku',
link:'https://app-customine.gusuku.io/drive.html', // link:'https://app-customine.gusuku.io/drive.html',
icon:'settings_suggest' // icon:'settings_suggest'
}, // },
{ // {
title:'Kintone API ドキュメント', // title:'Kintone API ドキュメント',
caption:'Kintone API', // caption:'Kintone API',
link:'https://cybozu.dev/ja/kintone/docs/', // link:'https://cybozu.dev/ja/kintone/docs/',
icon:'help_outline' // 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 leftDrawerOpen = ref(false) const domainLinks: EssentialLinkProps[] = [
{
title: 'ドメイン管理',
caption: 'kintoneのドメイン設定',
icon: 'domain',
link: '/#/domain',
target: '_self'
},
// {
// title: 'ドメイン適用',
// caption: 'ユーザー使用可能なドメインの設定',
// icon: 'assignment_ind',
// link: '/#/userDomain',
// target: '_self'
// },
];
const env=process.env; const adminLinks: EssentialLinkProps[] = [
{
title: 'ユーザー管理',
caption: 'ユーザーを管理する',
icon: 'manage_accounts',
link: '/#/user',
target: '_self'
},
]
const version = process.env.version;
const productName = process.env.productName;
onMounted(() => {
authStore.toggleLeftMenu();
});
function toggleLeftDrawer() { function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value 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.updateuser.first_name} ${item.updateuser.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

@@ -0,0 +1,364 @@
<template>
<q-page>
<q-layout container class="absolute-full shadow-2 rounded-borders">
<div class="q-pa-sm q-gutter-sm ">
<q-drawer side="left" :overlay="true" bordered v-model="drawerLeft" :show-if-above="false" elevated>
<div class="flex-center absolute-full" style="padding:15px">
<q-scroll-area class="fit" :horizontal-thumb-style="{ opacity: '0' }">
<EventTree />
</q-scroll-area>
</div>
<div class="flex-center fixed-bottom bg-grey-3 q-pa-md row ">
<q-btn color="secondary" glossy label="デプロイ" @click="onDeploy" icon="sync" :loading="deployLoading" />
<q-space></q-space>
<q-btn-dropdown color="primary" label="保存" icon="save" :loading="saveLoading" >
<q-list>
<q-item clickable v-close-popup @click="onSaveFlow">
<q-item-section avatar >
<q-icon name="save" color="primary"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>選択中フローの保存</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="onSaveAllFlow">
<q-item-section avatar>
<q-icon name="collections_bookmark" color="accent"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>一括保存</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</q-drawer>
</div>
<q-btn flat dense round
:icon="drawerLeft?'keyboard_double_arrow_left':'keyboard_double_arrow_right'"
:style="{'left': fixedLeftPosition}"
@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="flowchart" v-if="store.currentFlow" :style="[drawerLeft?{paddingLeft:'300px'}:{}]">
<node-item v-if="rootNode!==undefined" :key="rootNode.id" :isSelected="rootNode === store.activeNode"
:actionNode="rootNode" @addNode="addNode" @nodeSelected="onNodeSelected" @nodeEdit="onNodeEdit"
@deleteNode="onDeleteNode" @deleteAllNextNodes="onDeleteAllNextNodes" @copyFlow="onCopyFlow"></node-item>
</div>
</div>
<PropertyPanel :actionNode="store.activeNode" v-model:drawerRight="drawerRight" @save-action-props="onSaveActionProps"></PropertyPanel>
</q-layout>
<ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg" min-width="500px" min-height="500px">
<template v-slot:toolbar>
<q-input dense debounce="200" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<action-select ref="appDg" name="model" :filter="filter" type="single" @clearFilter="onClearFilter" ></action-select>
</ShowDialog>
<q-inner-loading
:showing="initLoading"
color="primary"
label="読み込み中..."
/>
</q-page>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty } from 'src/types/ActionTypes';
import { IManagedApp } from 'src/types/AppTypes';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'stores/useAuthStore';
import { api } from 'boot/axios';
import NodeItem from 'src/components/main/NodeItem.vue';
import ShowDialog from 'components/ShowDialog.vue';
import ActionSelect from 'components/ActionSelect.vue';
import PropertyPanel from 'components/right/PropertyPanel.vue';
import EventTree from 'components/left/EventTree.vue';
import { FlowCtrl } from '../control/flowctrl';
import { useQuasar } from 'quasar';
const deployLoading = ref(false);
const saveLoading = ref(false);
const initLoading = ref(true);
const drawerLeft = ref(false);
const $q = useQuasar();
const store = useFlowEditorStore();
const authStore = useAuthStore();
const route = useRoute()
const appDg = ref();
const prevNodeIfo = ref({
prevNode: {} as IActionNode,
inputPoint: ""
});
// const refFlow = ref<ActionFlow|null>(null);
const showAddAction = ref(false);
const drawerRight = ref(false);
const filter=ref("");
const model = ref("");
const rootNode = computed(()=>{
return store.currentFlow?.getRoot();
});
const minPanelWidth=computed(()=>{
const root = store.currentFlow?.getRoot();
if(store.currentFlow && root){
return store.currentFlow?.getColumns(root) * 300 + 'px';
}else{
return "300px";
}
});
const fixedLeftPosition = computed(()=>{
return drawerLeft.value?"300px":"0px";
});
const addNode = (node: IActionNode, inputPoint: string) => {
if (drawerRight.value) {
drawerRight.value = false;
}
showAddAction.value = true;
prevNodeIfo.value.prevNode = node;
prevNodeIfo.value.inputPoint = inputPoint;
}
const onNodeSelected = (node: IActionNode) => {
//右パネルが開いている場合、自動閉じる
if (drawerRight.value && store.activeNode?.id !== node.id) {
drawerRight.value = false;
}
store.setActiveNode(node);
}
const onNodeEdit = (node: IActionNode) => {
store.setActiveNode(node);
drawerRight.value = true;
}
const onDeleteNode = (node: IActionNode) => {
if (!store.currentFlow) return;
//右パネルが開いている場合、自動閉じる
if (drawerRight.value && store.activeNode?.id === node.id) {
drawerRight.value = false;
}
store.currentFlow?.removeNode(node);
}
const onDeleteAllNextNodes = (node: IActionNode) => {
if (!store.currentFlow) return;
//右パネルが開いている場合、自動閉じる
if (drawerRight.value) {
drawerRight.value = false;
}
store.currentFlow?.removeAllNext(node.id);
}
const closeDg = (val: any) => {
console.log("Dialog closed->", val);
if (val == 'OK') {
const data = appDg.value.selected[0];
const actionProps = JSON.parse(data.property);
const outputPoint = JSON.parse(data.outputPoints);
const action = new ActionNode(data.name, data.desc, "", outputPoint, actionProps);
store.currentFlow?.addNode(action, prevNodeIfo.value.prevNode, prevNodeIfo.value.inputPoint);
}
}
/*
*フローのデータをコピーする
*/
const onCopyFlow = () => {
if (navigator.clipboard) {
const jsonData =JSON.stringify(store.currentFlow) ;
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 onDeploy = async () => {
if (store.appInfo === undefined || store.flows?.length === 0) {
$q.notify({
type: 'negative',
caption: "エラー",
message: `設定されたフローがありません。`
});
return;
}
try {
deployLoading.value = true;
await store.deploy();
deployLoading.value = false;
$q.notify({
type: 'positive',
caption: "通知",
message: `デプロイを成功しました。`
});
} catch (error) {
console.error(error);
deployLoading.value = false;
$q.notify({
type: 'negative',
caption: "エラー",
message: `デプロイが失敗しました。`
})
}
return;
}
const onSaveActionProps=(props:IActionProperty[])=>{
if(store.activeNode){
store.activeNode.actionProps=props;
$q.notify({
type: 'positive',
caption: "通知",
message: `${store.activeNode?.subTitle}の属性を設定しました。(保存はされていません)`
});
}
};
const onSaveFlow = async () => {
const targetFlow = store.selectedFlow;
if (targetFlow === undefined) {
$q.notify({
type: 'negative',
caption: 'エラー',
message: `選択中のフローがありません。`
});
return;
}
try {
saveLoading.value = true;
await store.saveFlow(targetFlow);
saveLoading.value = false;
$q.notify({
type: 'positive',
caption: "通知",
message: `${targetFlow.getRoot()?.subTitle}のフロー設定を保存しました。`
});
} catch (error) {
console.error(error);
saveLoading.value = false;
$q.notify({
type: 'negative',
caption: "エラー",
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 () => {
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;
}
const fetchAppById = async(id: string) => {
const result = await api.get('api/apps');
return result.data.find((item: IManagedApp) => item.appid === id ) as IManagedApp;
}
const onClearFilter=()=>{
filter.value='';
}
onMounted(() => {
authStore.setLeftMenu(false);
fetchData();
});
</script>
<style lang="scss">
.flowchart {
padding-top: 10px;
}
.flow-toolbar {
opacity: 50%;
}
.event-tree .q-drawer {
top: 50px;
z-index: 999;
}
.expand{
position: fixed;
left: 0px;
top: 50%;
z-index: 9999;
}
</style>

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