Compare commits

...

100 Commits

Author SHA1 Message Date
Mouriya
3540becf6f vite-plugin-css-injected-by-jsはメンテナンスされていないため、通常のソースマップ生成に影響を与えるので、vite-plugin-lib-injected-cssを使用してください 2024-07-02 12:15:18 +09:00
Tian Dai
63099eda8b Merge branch 'plugin-infar-vite' of ssh.dev.azure.com:v3/alicorn-dev/KintoneAppBuilder/KintoneAppBuilder into feature-data-mapping 2024-06-04 17:49:32 +09:00
Tian Dai
65d89b0462 Windowsのオペレーティングシステムの権限問題のため、Viteの自動URLオープン機能を無効にしました 2024-06-04 17:48:39 +09:00
Tian Dai
c6ded099fa vite devとvite serverはエイリアスではなく、置き換え可能で、問題を修正しました。 2024-06-04 17:41:09 +09:00
Tian Dai
c072233593 Merge branch 'plugin-infar-vite' into feature-data-mapping
# Conflicts:
#	plugin/kintone-addins/package.json
#	plugin/kintone-addins/vite.config.js
2024-06-04 17:21:43 +09:00
Tian Dai
4e296c1555 npm-run-all2を全オペレーティング・システムと依存性管理のサポートに置き換える。ラン・スクリプトをより組み合わせ可能なものに再編成する。 プロジェクトの dotenv サポートを提供する。 2024-06-04 16:16:36 +09:00
016fcaab29 feat:データマッピングアクション変更 2024-06-03 17:39:39 +09:00
Mouriya
bebc1ec9fa フィックス 2024-06-03 08:10:23 +09:00
Mouriya
f71c3d2123 挿入専用のデータ・マッピング・プラグインを追加 2024-06-03 08:08:06 +09:00
Mouriya
d79ce8d06b データ収集プラグインの追加 2024-06-03 07:08:04 +09:00
Mouriya
fc9c3a5e81 条件演算子をカスタマイズし、kintoneクエリ文字列に変換することができます。 2024-06-03 07:07:20 +09:00
Mouriya
6df72a1ae3 Merge commit '372dbe50f7254ed3142a85dac58e624fce695aa7' into feature-data-mapping 2024-06-03 02:16:50 +09:00
Mouriya
372dbe50f7 HTTPサーバーのホットリロード機能を追加。 2024-06-03 02:08:37 +09:00
Mouriya
68fde6d490 Merge commit 'b25c17ab53a848f5692f113c6c3c1177e310d87f' into feature-data-mapping 2024-06-03 00:41:01 +09:00
Mouriya
c398dee21e 誤ったmodelValueの使用法の修正 2024-05-27 19:52:16 +09:00
Mouriya
f2ab310b6d Merge commit 'a6cf95b76d56841e7a5d545c8f0835e44cf4dafc' into feature-data-mapping 2024-05-27 19:27:44 +09:00
Mouriya
ca0f24465b 外部アプリセレクタコンポーネントがある場合、このコンポーネントはアプリを選択せずにフィールドを選択できる。 2024-05-27 19:04:21 +09:00
Mouriya
3cc4b65460 アプリ選択コンポーネントがある場合、アプリまたはフィールドが選択されるまで、それらを無効にすることができる。 2024-05-27 19:03:08 +09:00
Mouriya
a6cf95b76d 新しいアプリ選択コンポーネント 2024-05-27 18:45:34 +09:00
Mouriya
484ab9fdae ファイル名とコンポーネント名を変更し、コンポーネントとして使用できるようにする。 2024-05-27 18:44:36 +09:00
Mouriya
78bba2502f データ・マッピング・コンポーネントの追加 2024-05-25 07:01:58 +09:00
Mouriya
c78b3cb5c0 AppFieldSelectコンポーネントは、パブリック部分をAppFieldSelectBoxとして抽出する。 2024-05-25 07:01:38 +09:00
Mouriya
c3b560dbc9 条件付きコンポーネントは'source'でappidを受け取ることができる。 2024-05-25 04:15:09 +09:00
53aadfcaaa feat:データ集計処理作成 2024-05-24 09:20:19 +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
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
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
ecb90e7120 ダイアログに検索追加 2024-04-15 16:54:33 +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
a782e92bd6 自動採番障害対応 2023-11-01 22:59:23 +09:00
cfc416fd14 自動採番アクション追加・ドメイン追加 2023-11-01 10:47:24 +09:00
df593d2ffe modified ER 2023-10-31 03:42:25 +09:00
110 changed files with 10626 additions and 3116 deletions

1
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

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

File diff suppressed because one or more lines are too long

View File

@@ -7,9 +7,11 @@ import httpx
import deepdiff import deepdiff
import app.core.config as config import app.core.config as config
import os import os
from pathlib import Path
from app.db.session import SessionLocal from app.db.session import SessionLocal
from app.db.crud import get_flows,get_activedomain 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.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
kinton_router = r = APIRouter() kinton_router = r = APIRouter()
@@ -21,28 +23,132 @@ def getkintoneenv(user = Depends(get_current_user)):
return kintoneevn return kintoneevn
def getfieldsfromexcel(df): 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 config.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: else:
p.append(f"\"{property[column-1]}\":\"{df.iloc[row,column]}\"") continue
col.append(f"\"{df.iloc[row,2]}\":{{{','.join(p)}}}") elif(propertyname =="mixValue"):
fields = ",".join(col).replace("False","false").replace("True","true") 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:
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
# 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):
@@ -127,14 +233,14 @@ 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}
@@ -206,7 +312,7 @@ def analysprocess(excel,kintone):
# return True # return True
return diff return diff
def updateprocesstokintone(app:str,process:dict,c:config.KINTONE_ENV=Depends(getkintoneenv)): def updateprocesstokintone(app:str,process:dict,c:config.KINTONE_ENV):
headers={config.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}{config.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}
@@ -214,20 +320,22 @@ def updateprocesstokintone(app:str,process:dict,c:config.KINTONE_ENV=Depends(get
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(c:config.KINTONE_ENV=Depends(getkintoneenv)): def getkintoneusers(c:config.KINTONE_ENV):
headers={config.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(c:config.KINTONE_ENV=Depends(getkintoneenv)): def getkintoneorgs(c:config.KINTONE_ENV):
headers={config.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,c:config.KINTONE_ENV=Depends(getkintoneenv)): def uploadkintonefiles(file,c:config.KINTONE_ENV):
if (file.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
return {'fileKey':file}
upload_files = {'file': open(file,'rb')} upload_files = {'file': open(file,'rb')}
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
data ={'name':'file','filename':os.path.basename(file)} data ={'name':'file','filename':os.path.basename(file)}
@@ -235,12 +343,15 @@ def uploadkintonefiles(file,c:config.KINTONE_ENV=Depends(getkintoneenv)):
r = httpx.post(url,headers=headers,data=data,files=upload_files) r = httpx.post(url,headers=headers,data=data,files=upload_files)
return r.json() return r.json()
def updateappjscss(app,uploads,c:config.KINTONE_ENV=Depends(getkintoneenv)): def updateappjscss(app,uploads,c:config.KINTONE_ENV):
dsjs = [] dsjs = []
dscss = [] dscss = []
for upload in uploads: for upload in uploads:
for key in upload: for key in upload:
if key.endswith('.js'): if key.endswith('.js'):
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
else:
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}}) dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
elif key.endswith('.css'): elif key.endswith('.css'):
dscss.append({'type':'FILE','file':{'fileKey':upload[key]}}) dscss.append({'type':'FILE','file':{'fileKey':upload[key]}})
@@ -253,21 +364,23 @@ def updateappjscss(app,uploads,c:config.KINTONE_ENV=Depends(getkintoneenv)):
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 createappjs(app): def createappjs(domainid,app):
db = SessionLocal() db = SessionLocal()
flows = get_flows(db,app) flows = get_flows_by_app(db,domainid,app)
db.close() db.close()
content={} content={}
for flow in flows: for flow in flows:
content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content} content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content}
js = 'const flow=' + json.dumps(content) js = 'const alcflow=' + json.dumps(content)
fpath = '{}\\alc_setting_{}.js'.format('Temp',app) scriptdir = Path(__file__).resolve().parent
file = open(fpath,'w',encoding="utf-8") rootdir = scriptdir.parent.parent.parent.parent
fpath = os.path.join(rootdir,"Temp",f"alc_setting_{app}.js")
print(rootdir)
print(fpath)
with open(fpath,'w') as file:
file.write(js) file.write(js)
file.close()
return fpath 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'):
@@ -285,7 +398,7 @@ 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")
@@ -293,15 +406,18 @@ async def test(file:UploadFile= File(...),app:str=None):
@r.post("/download",) @r.post("/download",)
async def download(key,c:config.KINTONE_ENV=Depends(getkintoneenv)): async def download(request:Request,key,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"fileKey":key} params = {"fileKey":key}
url = f"{c.BASE_URL}/k/v1/file.json" url = f"{c.BASE_URL}/k/v1/file.json"
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
except Exception as e:
raise APIException('kintone:upload',request.url._url,f"Error occurred while download file.json:",e)
@r.post("/upload") @r.post("/upload")
async def upload(files:t.List[UploadFile] = File(...)): 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'):
@@ -311,14 +427,14 @@ 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.post("/updatejscss") @r.post("/updatejscss")
async def jscss(app:str,files:t.List[UploadFile] = File(...),env = Depends(getkintoneenv)): async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env = Depends(getkintoneenv)):
try: try:
jscs=[] jscs=[]
for file in files: for file in files:
@@ -328,42 +444,65 @@ async def jscss(app:str,files:t.List[UploadFile] = File(...),env = Depends(getki
fout = open(fpath,'wb') fout = open(fpath,'wb')
fout.write(fbytes) fout.write(fbytes)
fout.close() fout.close()
upload = uploadkintonefiles(fpath) upload = uploadkintonefiles(fpath,env)
if upload.get('fileKey') != None: if upload.get('fileKey') != None:
jscs.append({ file.filename:upload['fileKey']}) jscs.append({ file.filename:upload['fileKey']})
appjscs = updateappjscss(app,jscs) appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None: if appjscs.get("revision") != None:
deoployappfromkintone(app,appjscs["revision"],env) deoployappfromkintone(app,appjscs["revision"],env)
return appjscs return appjscs
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error occurred while update file {file.filename}: {str(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")
async def app(request:Request,app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}{config.API_V1_STR}/app.json"
params ={"id":app}
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({c.DOMAIN_NAM}->{app}):",e)
@r.get("/allapps") @r.get("/allapps")
async def allapps(c:config.KINTONE_ENV=Depends(getkintoneenv)): async def allapps(request:Request,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.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}{config.API_V1_STR}/apps.json" url = f"{c.BASE_URL}{config.API_V1_STR}/apps.json"
r = httpx.get(url,headers=headers) r = httpx.get(url,headers=headers)
return r.json() return r.json()
except Exception as e:
raise APIException('kintone:allapps',request.url._url, f"Error occurred while get allapps({c.DOMAIN_NAM}):",e)
@r.get("/appfields") @r.get("/appfields")
async def appfields(app:str,env = Depends(getkintoneenv)): async def appfields(request:Request,app:str,env = Depends(getkintoneenv)):
try:
return getfieldsfromkintone(app,env) return getfieldsfromkintone(app,env)
except Exception as e:
raise APIException('kintone:appfields',request.url._url, f"Error occurred while get app fileds({env.DOMAIN_NAM}->{app}):",e)
@r.get("/appprocess") @r.get("/appprocess")
async def appprocess(app:str,env = Depends(getkintoneenv)): async def appprocess(request:Request,app:str,env = Depends(getkintoneenv)):
try:
return getprocessfromkintone(app,env) return getprocessfromkintone(app,env)
except Exception as e:
raise APIException('kintone:appprocess',request.url._url, f"Error occurred while get app process({env.DOMAIN_NAM}->{app}):",e)
@r.get("/alljscss") @r.get("/alljscss")
async def alljscs(app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)): async def alljscs(request:Request,app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.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}{config.API_V1_STR}/app/customize.json" url = f"{c.BASE_URL}{config.API_V1_STR}/app/customize.json"
params = {"app":app} params = {"app":app}
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
except Exception as e:
raise APIException('kintone:alljscss',request.url._url, f"Error occurred while get app js/css({c.DOMAIN_NAM}->{app}):",e)
@r.post("/createapp",) @r.post("/createapp",)
async def createapp(name:str,c:config.KINTONE_ENV=Depends(getkintoneenv)): async def createapp(request:Request,name:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.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"}
data = {"name":name} data = {"name":name}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app.json" url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app.json"
@@ -374,11 +513,17 @@ async def createapp(name:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
data = {"apps":[result],"revert": False} data = {"apps":[result],"revert": False}
r = httpx.post(url,headers=headers,data=json.dumps(data)) r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json return r.json
except Exception as e:
raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({c.DOMAIN_NAM}->{name}):",e)
property=["label","code","type","required","defaultValue","options"]
@r.post("/createappfromexcel",) @r.post("/createappfromexcel",)
async def createappfromexcel(files:t.List[UploadFile] = File(...),env = Depends(getkintoneenv)): 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:
@@ -388,9 +533,9 @@ async def createappfromexcel(files:t.List[UploadFile] = File(...),env = Depends(
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,env) app = createkintoneapp(appname,env)
if app.get("app") != None: if app.get("app") != None:
@@ -398,22 +543,26 @@ async def createappfromexcel(files:t.List[UploadFile] = File(...),env = Depends(
app = updateappsettingstokintone(result["app"],{"description":desc},env) 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"],env,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"],env) 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(...),env = Depends(getkintoneenv)): 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:
@@ -422,10 +571,10 @@ async def updateappfromexcel(app:str,files:t.List[UploadFile] = File(...),env =
excel = getsettingfromexcel(df) excel = getsettingfromexcel(df)
kintone= getsettingfromkintone(app,env) kintone= getsettingfromkintone(app,env)
settings = analysesettings(excel,kintone) settings = analysesettings(excel,kintone)
excel = getfieldsfromexcel(df) excel = getfieldsfromexcel(df,mapping)
kintone = getfieldsfromkintone(app,env) 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,env) kinp = getprocessfromkintone(app,env)
@@ -451,20 +600,19 @@ async def updateappfromexcel(app:str,files:t.List[UploadFile] = File(...),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,env) 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,env = Depends(getkintoneenv)): async def updateprocessfromexcel(request:Request,app:str,env = Depends(getkintoneenv)):
try: try:
excel = getprocessfromexcel() excel = getprocessfromexcel()
@@ -489,32 +637,31 @@ async def updateprocessfromexcel(app:str,env = Depends(getkintoneenv)):
# 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,env) 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_NAM}->{app}):",e)
return result return result
@r.post("/createjstokintone",) @r.post("/createjstokintone",)
async def createjstokintone(app:str,env = Depends(getkintoneenv)): async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
try: try:
jscs=[] jscs=[]
files=[] files=[]
files.append(createappjs(app)) files.append(createappjs(env.DOMAIN_ID, app))
files.append('Temp\\alc_runtime.js') files.append('Temp\\alc_runtime.js')
for file in files: for file in files:
upload = uploadkintonefiles(file) upload = uploadkintonefiles(file,env)
if upload.get('fileKey') != None: if upload.get('fileKey') != None:
jscs.append({ app + '.js':upload['fileKey']}) jscs.append({ file :upload['fileKey']})
appjscs = updateappjscss(app,jscs) appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None: if appjscs.get("revision") != None:
deoployappfromkintone(app,appjscs["revision"],env) deoployappfromkintone(app,appjscs["revision"],env)
return appjscs return appjscs
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error occurred : {str(e)}") raise APIException('kintone:createjstokintone',request.url._url, f"Error occurred while create js ({env.DOMAIN_NAM}->{app}):",e)

View File

@@ -5,6 +5,7 @@ from app.db.crud import *
from app.db.schemas import * from app.db.schemas import *
from typing import List from typing import List
from app.core.auth import get_current_active_user,get_current_user from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
platform_router = r = APIRouter() platform_router = r = APIRouter()
@@ -18,9 +19,11 @@ async def appsetting_details(
id: int, id: int,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
app = get_appsetting(db, id) app = get_appsetting(db, id)
return app 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(
@@ -28,7 +31,10 @@ async def appsetting_create(
app: AppBase, app: AppBase,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
return create_appsetting(db, app) 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(
@@ -40,7 +46,10 @@ async def appsetting_edit(
app: AppEdit, app: AppEdit,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
return edit_appsetting(db, id, app) 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(
@@ -51,8 +60,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(
@@ -65,8 +76,11 @@ async def kintone_data(
type: int, type: int,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
kintone = get_kintones(db, type) kintone = get_kintones(db, type)
return kintone return kintone
except Exception as e:
raise APIException('platform:kintone',request.url._url,f"Error occurred while get kintone env:",e)
@r.get( @r.get(
"/actions", "/actions",
@@ -78,8 +92,11 @@ async def action_data(
request: Request, request: Request,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
actions = get_actions(db) actions = get_actions(db)
return actions return actions
except Exception as e:
raise APIException('platform:actions',request.url._url,f"Error occurred while get actions:",e)
@r.get( @r.get(
"/flow/{flowid}", "/flow/{flowid}",
@@ -91,8 +108,11 @@ async def flow_details(
flowid: str, flowid: str,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
app = get_flow(db, flowid) app = get_flow(db, flowid)
return app return app
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e)
@r.get( @r.get(
@@ -103,19 +123,30 @@ async def flow_details(
async def flow_list( async def flow_list(
request: Request, request: Request,
appid: str, appid: str,
user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
flows = get_flows_by_app(db, appid) try:
domain = get_activedomain(db, user.id)
print("domain=>",domain)
flows = get_flows_by_app(db, domain.id, appid)
return flows 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) @r.post("/flow", response_model=Flow, response_model_exclude_none=True)
async def flow_create( async def flow_create(
request: Request, request: Request,
flow: FlowBase, flow: FlowBase,
user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
return create_flow(db, flow) try:
domain = get_activedomain(db, user.id)
return create_flow(db, domain.id, flow)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
@r.put( @r.put(
@@ -126,7 +157,10 @@ async def flow_edit(
flow: FlowBase, flow: FlowBase,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
return edit_flow(db, flow) return edit_flow(db, flow)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
@r.delete( @r.delete(
@@ -137,8 +171,10 @@ async def flow_delete(
flowid: str, flowid: str,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
return delete_flow(db, flowid) 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( @r.get(
"/domains/{tenantid}", "/domains/{tenantid}",
@@ -150,8 +186,11 @@ async def domain_details(
tenantid:str, tenantid:str,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
domains = get_domains(db,tenantid) domains = get_domains(db,tenantid)
return domains 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) @r.post("/domain", response_model=Domain, response_model_exclude_none=True)
async def domain_create( async def domain_create(
@@ -159,7 +198,10 @@ async def domain_create(
domain: DomainBase, domain: DomainBase,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
return create_domain(db, domain) return create_domain(db, domain)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e)
@r.put( @r.put(
@@ -170,7 +212,10 @@ async def domain_edit(
domain: DomainBase, domain: DomainBase,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
return edit_domain(db, domain) return edit_domain(db, domain)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e)
@r.delete( @r.delete(
@@ -181,8 +226,10 @@ async def domain_delete(
id: int, id: int,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
return delete_domain(db,id) 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( @r.get(
"/domain", "/domain",
@@ -194,8 +241,11 @@ async def userdomain_details(
user=Depends(get_current_user), user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try:
domains = get_domain(db, user.id) domains = get_domain(db, user.id)
return domains 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( @r.post(
"/domain/{userid}", "/domain/{userid}",
@@ -207,8 +257,11 @@ async def create_userdomain(
domainids:list, domainids:list,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
domain = add_userdomain(db, userid,domainids) domain = add_userdomain(db, userid,domainids)
return domain return domain
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while add user({userid}) domain:",e)
@r.delete( @r.delete(
"/domain/{domainid}/{userid}", response_model_exclude_none=True "/domain/{domainid}/{userid}", response_model_exclude_none=True
@@ -219,7 +272,10 @@ async def userdomain_delete(
userid: int, userid: int,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
return delete_userdomain(db, userid,domainid) 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( @r.get(
@@ -232,8 +288,11 @@ async def get_useractivedomain(
user=Depends(get_current_user), user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try:
domain = get_activedomain(db, user.id) domain = get_activedomain(db, user.id)
return domain 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( @r.put(
"/activedomain/{domainid}", "/activedomain/{domainid}",
@@ -245,9 +304,11 @@ async def update_activeuserdomain(
user=Depends(get_current_user), user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try:
domain = active_userdomain(db, user.id,domainid) domain = active_userdomain(db, user.id,domainid)
return domain 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( @r.get(
"/events", "/events",
@@ -259,8 +320,11 @@ async def event_data(
request: Request, request: Request,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
events = get_events(db) events = get_events(db)
return events return events
except Exception as e:
raise APIException('platform:events',request.url._url,f"Error occurred while get events:",e)
@r.get( @r.get(
@@ -274,5 +338,8 @@ async def eventactions_data(
eventid: str, eventid: str,
db=Depends(get_db), db=Depends(get_db),
): ):
try:
actions = get_eventactions(db,eventid) actions = get_eventactions(db,eventid)
return actions return actions
except Exception as e:
raise APIException('platform:eventactions',request.url._url,f"Error occurred while get eventactions:",e)

View File

@@ -0,0 +1,26 @@
from fastapi import HTTPException, status
from app.db.schemas import ErrorCreate
from app.db.session import SessionLocal
from app.db.crud import create_log
class APIException(Exception):
def __init__(self,location:str,title:str,content:str,e:Exception):
if(str(e) == ''):
content += e.detail
self.detail = e.detail
self.status_code = e.status_code
else:
self.detail = str(e)
content += str(e)
self.status_code = 500
if(len(content) > 5000):
content =content[0:5000]
self.error = ErrorCreate(location=location,title=title,content=content)
def writedblog(exc: APIException):
db = SessionLocal()
try:
create_log(db,exc.error)
finally:
db.close()

View File

@@ -67,8 +67,8 @@ def sign_up_new_user(db, email: str, password: str, firstname: str,lastname: str
schemas.UserCreate( schemas.UserCreate(
email=email, email=email,
password=password, password=password,
firstname = firstname, first_name = firstname,
lastname = lastname, last_name = lastname,
is_active=True, is_active=True,
is_superuser=False, is_superuser=False,
), ),

View File

@@ -3,15 +3,22 @@ import base64
PROJECT_NAME = "KintoneAppBuilder" PROJECT_NAME = "KintoneAppBuilder"
SQLALCHEMY_DATABASE_URI = "postgres://maxz64:m@xz1205@alicornkintone.postgres.database.azure.com/postgres" #SQLALCHEMY_DATABASE_URI = "postgres://maxz64:m@xz1205@alicornkintone.postgres.database.azure.com/postgres"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/postgres" SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/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"
DEPLOY_MODE = "DEV" #DEV,PROD
DEPLOY_JS_URL = "https://ka-addin.azurewebsites.net/alc_runtime.js"
#DEPLOY_JS_URL = "https://e84c-133-139-70-142.ngrok-free.app/alc_runtime.js"
KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"] KINTONE_FIELD_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']
class KINTONE_ENV: class KINTONE_ENV:
BASE_URL = "" BASE_URL = ""
@@ -20,7 +27,13 @@ class KINTONE_ENV:
KINTONE_USER = "" KINTONE_USER = ""
DOMAIN_ID = ""
DOMAIN_NAME =""
def __init__(self,domain) -> None: def __init__(self,domain) -> None:
self.DOMAIN_NAME=domain.name
self.DOMAIN_ID=domain.id
self.BASE_URL = domain.url self.BASE_URL = domain.url
self.KINTONE_USER = domain.kintoneuser self.KINTONE_USER = domain.kintoneuser
self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.kintonepwd}","utf-8")) self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.kintonepwd}","utf-8"))

View File

@@ -9,7 +9,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 +25,7 @@ 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

View File

@@ -125,11 +125,12 @@ def get_actions(db: Session):
return actions return actions
def create_flow(db: Session, flow: schemas.FlowBase): def create_flow(db: Session, domainid: int, flow: schemas.FlowBase):
db_flow = models.Flow( db_flow = models.Flow(
flowid=flow.flowid, flowid=flow.flowid,
appid=flow.appid, appid=flow.appid,
eventid=flow.eventid, eventid=flow.eventid,
domainid=domainid,
name=flow.name, name=flow.name,
content=flow.content content=flow.content
) )
@@ -176,10 +177,10 @@ def get_flow(db: Session, flowid: str):
raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
return flow return flow
def get_flows_by_app(db: Session, appid: str): def get_flows_by_app(db: Session, domainid: int, appid: str):
flows = db.query(models.Flow).filter(models.Flow.appid == appid).all() flows = db.query(models.Flow).filter(and_(models.Flow.domainid == domainid,models.Flow.appid == appid)).all()
if not flows: if not flows:
raise HTTPException(status_code=404, detail="Data not found") raise Exception("Data not found")
return flows return flows
def create_domain(db: Session, domain: schemas.DomainBase): def create_domain(db: Session, domain: schemas.DomainBase):
@@ -278,7 +279,20 @@ def get_events(db: Session):
return events return events
def get_eventactions(db: Session,eventid: str): def get_eventactions(db: Session,eventid: str):
eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid == models.Action.id ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all() #eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid == models.Action.id ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all()
eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid != models.Action.id and models.EventAction.eventid == eventid ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all()
if not eveactions: if not eveactions:
raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
return eveactions return eveactions
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

@@ -47,6 +47,7 @@ class Flow(Base):
flowid = Column(String(100), index=True, nullable=False) flowid = Column(String(100), index=True, nullable=False)
appid = Column(String(100), index=True, nullable=False) appid = Column(String(100), index=True, nullable=False)
eventid = Column(String(100), index=True, nullable=False) eventid = Column(String(100), index=True, nullable=False)
domainid = Column(Integer,ForeignKey("domain.id"))
name = Column(String(200)) name = Column(String(200))
content = Column(String) content = Column(String)
@@ -84,9 +85,29 @@ class Event(Base):
eventid= Column(String(100), nullable=False) eventid= Column(String(100), nullable=False)
function = Column(String(500), nullable=False) function = Column(String(500), nullable=False)
mobile = Column(Boolean, default=False) mobile = Column(Boolean, default=False)
eventgroup = Column(Boolean, default=False)
class EventAction(Base): class EventAction(Base):
__tablename__ = "eventaction" __tablename__ = "eventaction"
eventid = Column(Integer,ForeignKey("event.id")) eventid = Column(Integer,ForeignKey("event.id"))
actionid = Column(Integer,ForeignKey("action.id")) actionid = Column(Integer,ForeignKey("action.id"))
class ErrorLog(Base):
__tablename__ = "errorlog"
title = Column(String(50))
location = Column(String(500))
content = Column(String(5000))
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))

View File

@@ -20,9 +20,12 @@ class UserOut(UserBase):
class UserCreate(UserBase): class UserCreate(UserBase):
email:str
password: str password: str
first_name: str first_name: str
last_name: str last_name: str
is_active:bool
is_superuser:bool
class Config: class Config:
orm_mode = True orm_mode = True
@@ -101,6 +104,7 @@ class Flow(Base):
flowid: str flowid: str
appid: str appid: str
eventid: str eventid: str
domainid: int
name: str = None name: str = None
content: str = None content: str = None
@@ -133,6 +137,12 @@ class Event(Base):
eventid: str eventid: str
function: str function: str
mobile: bool mobile: bool
eventgroup: bool
class Config: class Config:
orm_mode = True 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
@@ -11,6 +12,11 @@ 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 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)
@@ -19,9 +25,7 @@ app = FastAPI(
) )
origins = [ origins = [
"http://localhost:9000", "*"
"http://localhost",
"http://localhost:8080",
] ]
app.add_middleware( app.add_middleware(
@@ -39,6 +43,25 @@ app.add_middleware(
# 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

@@ -1,6 +1,6 @@
<mxfile host="Electron" modified="2023-10-12T07:28:40.725Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/21.1.2 Chrome/106.0.5249.199 Electron/21.4.3 Safari/537.36" etag="uS9Bk4R-Yu3zzAy4CElf" version="21.1.2" type="device" pages="3"> <mxfile host="65bd71144e" pages="3">
<diagram name="Page-1" id="efa7a0a1-bf9b-a30e-e6df-94a7791c09e9"> <diagram name="Page-1" id="efa7a0a1-bf9b-a30e-e6df-94a7791c09e9">
<mxGraphModel dx="1418" dy="828" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="826" pageHeight="1169" background="none" math="0" shadow="0"> <mxGraphModel dx="1073" dy="518" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="826" pageHeight="1169" background="none" math="0" shadow="0">
<root> <root>
<mxCell id="0"/> <mxCell id="0"/>
<mxCell id="1" parent="0"/> <mxCell id="1" parent="0"/>
@@ -129,7 +129,7 @@
</mxGraphModel> </mxGraphModel>
</diagram> </diagram>
<diagram id="lCZzTbn_7m8qK95pbvvS" name="ER図"> <diagram id="lCZzTbn_7m8qK95pbvvS" name="ER図">
<mxGraphModel dx="2261" dy="884" 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"> <mxGraphModel dx="1900" dy="518" 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> <root>
<mxCell id="0"/> <mxCell id="0"/>
<mxCell id="1" parent="0"/> <mxCell id="1" parent="0"/>
@@ -427,7 +427,7 @@
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="GH2g80-cBXHe62XdPV4N-1" value="Event" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1"> <mxCell id="GH2g80-cBXHe62XdPV4N-1" value="Event" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
<mxGeometry x="140" y="470" width="180" height="150" as="geometry"> <mxGeometry x="130" y="480" width="180" height="150" as="geometry">
<mxRectangle x="40" y="390" width="70" height="30" as="alternateBounds"/> <mxRectangle x="40" y="390" width="70" height="30" as="alternateBounds"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
@@ -643,7 +643,7 @@
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="AJLbZMHdl7kzV18ppMoM-40" value="Action" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1"> <mxCell id="AJLbZMHdl7kzV18ppMoM-40" value="Action" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
<mxGeometry x="140" y="680" width="180" height="150" as="geometry" /> <mxGeometry x="130" y="720" width="180" height="150" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="AJLbZMHdl7kzV18ppMoM-41" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="AJLbZMHdl7kzV18ppMoM-40" vertex="1"> <mxCell id="AJLbZMHdl7kzV18ppMoM-41" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="AJLbZMHdl7kzV18ppMoM-40" vertex="1">
<mxGeometry y="30" width="180" height="30" as="geometry"/> <mxGeometry y="30" width="180" height="30" as="geometry"/>
@@ -777,55 +777,55 @@
<mxPoint x="370" y="370" as="targetPoint"/> <mxPoint x="370" y="370" as="targetPoint"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-1" value="EventAction" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1"> <mxCell id="dFNVox72oq8u18PzFUgJ-1" value="EventAction" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
<mxGeometry x="-110" y="570" width="180" height="120" as="geometry"/> <mxGeometry x="-110" y="570" width="180" height="120" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-2" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" vertex="1" parent="dFNVox72oq8u18PzFUgJ-1"> <mxCell id="dFNVox72oq8u18PzFUgJ-2" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="dFNVox72oq8u18PzFUgJ-1" vertex="1">
<mxGeometry y="30" width="180" height="30" as="geometry"/> <mxGeometry y="30" width="180" height="30" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-3" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="dFNVox72oq8u18PzFUgJ-2"> <mxCell id="dFNVox72oq8u18PzFUgJ-3" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="dFNVox72oq8u18PzFUgJ-2" vertex="1">
<mxGeometry width="30" height="30" as="geometry"> <mxGeometry width="30" height="30" as="geometry">
<mxRectangle width="30" height="30" as="alternateBounds"/> <mxRectangle width="30" height="30" as="alternateBounds"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-4" value="id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="dFNVox72oq8u18PzFUgJ-2"> <mxCell id="dFNVox72oq8u18PzFUgJ-4" value="id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" parent="dFNVox72oq8u18PzFUgJ-2" vertex="1">
<mxGeometry x="30" width="150" height="30" as="geometry"> <mxGeometry x="30" width="150" height="30" as="geometry">
<mxRectangle width="150" height="30" as="alternateBounds"/> <mxRectangle width="150" height="30" as="alternateBounds"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-5" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="dFNVox72oq8u18PzFUgJ-1"> <mxCell id="dFNVox72oq8u18PzFUgJ-5" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="dFNVox72oq8u18PzFUgJ-1" vertex="1">
<mxGeometry y="60" width="180" height="30" as="geometry"/> <mxGeometry y="60" width="180" height="30" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-6" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="dFNVox72oq8u18PzFUgJ-5"> <mxCell id="dFNVox72oq8u18PzFUgJ-6" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="dFNVox72oq8u18PzFUgJ-5" vertex="1">
<mxGeometry width="30" height="30" as="geometry"> <mxGeometry width="30" height="30" as="geometry">
<mxRectangle width="30" height="30" as="alternateBounds"/> <mxRectangle width="30" height="30" as="alternateBounds"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-7" value="event_id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="dFNVox72oq8u18PzFUgJ-5"> <mxCell id="dFNVox72oq8u18PzFUgJ-7" value="event_id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="dFNVox72oq8u18PzFUgJ-5" vertex="1">
<mxGeometry x="30" width="150" height="30" as="geometry"> <mxGeometry x="30" width="150" height="30" as="geometry">
<mxRectangle width="150" height="30" as="alternateBounds"/> <mxRectangle width="150" height="30" as="alternateBounds"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-8" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="dFNVox72oq8u18PzFUgJ-1"> <mxCell id="dFNVox72oq8u18PzFUgJ-8" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="dFNVox72oq8u18PzFUgJ-1" vertex="1">
<mxGeometry y="90" width="180" height="30" as="geometry"/> <mxGeometry y="90" width="180" height="30" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-9" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="dFNVox72oq8u18PzFUgJ-8"> <mxCell id="dFNVox72oq8u18PzFUgJ-9" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="dFNVox72oq8u18PzFUgJ-8" vertex="1">
<mxGeometry width="30" height="30" as="geometry"> <mxGeometry width="30" height="30" as="geometry">
<mxRectangle width="30" height="30" as="alternateBounds"/> <mxRectangle width="30" height="30" as="alternateBounds"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-10" value="action_id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="dFNVox72oq8u18PzFUgJ-8"> <mxCell id="dFNVox72oq8u18PzFUgJ-10" value="action_id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="dFNVox72oq8u18PzFUgJ-8" vertex="1">
<mxGeometry x="30" width="150" height="30" as="geometry"> <mxGeometry x="30" width="150" height="30" as="geometry">
<mxRectangle width="150" height="30" as="alternateBounds"/> <mxRectangle width="150" height="30" as="alternateBounds"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-16" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERmandOne;startArrow=ERmandOne;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;curved=1;" edge="1" parent="1" source="dFNVox72oq8u18PzFUgJ-5" target="GH2g80-cBXHe62XdPV4N-2"> <mxCell id="dFNVox72oq8u18PzFUgJ-16" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERmandOne;startArrow=ERmandOne;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;curved=1;" parent="1" source="dFNVox72oq8u18PzFUgJ-5" target="GH2g80-cBXHe62XdPV4N-2" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry"> <mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="290" y="800" as="sourcePoint"/> <mxPoint x="290" y="800" as="sourcePoint"/>
<mxPoint x="390" y="700" as="targetPoint"/> <mxPoint x="390" y="700" as="targetPoint"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="dFNVox72oq8u18PzFUgJ-17" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERmandOne;startArrow=ERmandOne;rounded=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="AJLbZMHdl7kzV18ppMoM-41" target="dFNVox72oq8u18PzFUgJ-8"> <mxCell id="dFNVox72oq8u18PzFUgJ-17" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERmandOne;startArrow=ERmandOne;rounded=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="AJLbZMHdl7kzV18ppMoM-41" target="dFNVox72oq8u18PzFUgJ-8" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry"> <mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="-100" y="900" as="sourcePoint"/> <mxPoint x="-100" y="900" as="sourcePoint"/>
<mxPoint y="800" as="targetPoint"/> <mxPoint y="800" as="targetPoint"/>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

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>

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +1,3 @@
KAB_BACKEND_URL="http://127.0.0.1:8000/api/v1/" KAB_BACKEND_URL="https://kab-backend.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/"

View File

@@ -2,7 +2,6 @@
<html lang="ja-jp"> <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

@@ -1,8 +1,8 @@
{ {
"name": "kintone-app-builder", "name": "kintone-automate",
"version": "0.2.0", "version": "0.2.0",
"description": "Kintoneアプリの自動生成とデプロイを支援ツールです", "description": "Kintoneアプリの自動生成とデプロイを支援ツールです",
"productName": "Kintone App Builder", "productName": "kintone Automate",
"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,12 +10,16 @@
"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",
"pinia": "^2.1.6", "pinia": "^2.1.7",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vue": "^3.0.0", "vue": "^3.0.0",

View File

@@ -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,

View File

@@ -1,5 +1,6 @@
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 {
@@ -15,7 +16,26 @@ declare module '@vue/runtime-core' {
// "export default () => {}" function below (which runs individually // "export default () => {}" function below (which runs individually
// for each client) // for each client)
const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL }); const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL });
const token=localStorage.getItem('token')||'';
if(token!==''){
api.defaults.headers["Authorization"]='Bearer ' + token;
}
//axios例外キャプチャー
api.interceptors.response.use(
(response)=>response,
(error)=>{
if (error.response && error.response.status === 401) {
// 認証エラーの場合再ログインする
console.error('(; ゚Д゚)/認証エラー(401)', error);
localStorage.removeItem('token');
router.replace({
path:"/login",
query:{redirect:router.currentRoute.value.fullPath}
});
}
return Promise.reject(error);
}
)
export default boot(({ app }) => { export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api // for use inside Vue files (Options API) through this.$axios and this.$api

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

@@ -1,12 +1,17 @@
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<q-table row-key="name" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" <div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table v-else row-key="index" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows"
class="action-table" class="action-table"
flat bordered flat bordered
virtual-scroll virtual-scroll
:pagination="pagination" :pagination="pagination"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
/> :filter="filter"
>
</q-table>
</div> </div>
</template> </template>
<script> <script>
@@ -17,23 +22,24 @@ export default {
name: 'actionSelect', name: 'actionSelect',
props: { props: {
name: String, name: String,
type: String type: String,
filter:String
}, },
setup() { setup(props) {
const isLoaded=ref(false);
const columns = [ const columns = [
{ name: 'name', required: true,label: 'アクション名',align: 'left',field: 'name',sortable: true}, { name: 'name', required: true,label: 'アクション名',align: 'left',field: 'name',sortable: true},
{ name: 'desc', align: 'left', label: '説明', field: 'desc', sortable: true }, { name: 'desc', align: 'left', label: '説明', field: 'desc', sortable: true },
// { name: 'content', label: '内容', field: 'content', sortable: true } // { name: 'content', label: '内容', field: 'content', sortable: true }
] ];
const rows = reactive([]) const rows = reactive([])
onMounted(async () => { onMounted(async () => {
await api.get('http://127.0.0.1:8000/api/kintone/1').then(res =>{ const res =await api.get('api/actions');
res.data.forEach((item) => res.data.forEach((item,index) =>
{ {
rows.push({name:item.name,desc:item.desc,content:item.content}); rows.push({index,name:item.name,desc:item.title,outputPoints:item.outputpoints,property:item.property});
}
)
}); });
isLoaded.value=true;
}); });
return { return {
columns, columns,
@@ -41,7 +47,8 @@ export default {
selected: ref([]), selected: ref([]),
pagination:ref({ pagination:ref({
rowsPerPage:0 rowsPerPage:0
}) }),
isLoaded,
} }
}, },
@@ -49,6 +56,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.action-table{ .action-table{
height: 100%; min-height: 10vh;
max-height: 68vh;
} }
</style> </style>

View File

@@ -0,0 +1,152 @@
<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" :updateSelects="updateItems"
:appId="selField.app?.id" not_page :filter="fieldFilter"
:selFields="selField.fields"></field-select>
</div>
</div>
</div>
<div style="min-width: 45vw;" v-else>
</div>
<show-dialog v-model:visible="showSelectApp" name="アプリ選択" @close="closeAppDlg">
<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"
:updateExternalSelectAppInfo="updateExternalSelectAppInfo"></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 { useFlowEditorStore } from 'stores/flowEditor';
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'
},
},
setup(props, { emit }) {
const appDlg = ref();
const fieldDlg = ref();
const showSelectApp = ref(false);
const selField = reactive(props.selectedField);
console.log(props.selectedField);
const store = useFlowEditorStore();
const isSelected = computed(() => {
return selField !== null && typeof selField === 'object' && ('app' in selField)
});
const closeAppDlg = (val: string) => {
if (val == 'OK') {
selField.app = appDlg.value.selected[0];
selField.fields = [];
showSelectApp.value = false;
}
};
const closeFieldDialog = (val: string) => {
if (val == 'OK') {
selField.fields = fieldDlg.value.selected;
}
};
const updateExternalSelectAppInfo = (newAppinfo: IApp) => {
selField.app = newAppinfo
}
const updateItems = (newFields: IField[]) => {
selField.fields = newFields
}
watchEffect(() => {
emit('update:modelValue', selField);
});
return {
appDlg,
fieldDlg,
closeAppDlg,
closeFieldDialog,
showSelectApp,
isSelected,
updateExternalSelectAppInfo,
filter: ref(),
updateItems,
fieldFilter: ref(),
selField
};
}
});
</script>

View File

@@ -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

@@ -1,41 +0,0 @@
<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: 'AppSelect',
props: {
name: String,
type: String
},
setup() {
const columns = [
{ name: 'id', required: true,label: 'アプリID',align: 'left',field: 'id',sortable: true},
{ name: 'name', align: 'center', label: 'アプリ名', field: 'name', sortable: true },
{ name: 'creator', label: '作成者', field: 'creator', sortable: true },
{ name: 'createdate', label: '作成日時', field: 'createdate' }
]
const rows = reactive([])
onMounted( () => {
api.get('allapps').then(res =>{
res.data.apps.forEach((item) =>
{
rows.push({id:item.appId,name:item.name,creator:item.creator.name,createdate:item.createdAt});
}
)
});
});
return {
columns,
rows,
selected: ref([]),
}
},
}
</script>

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,
updateExternalSelectAppInfo: {
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.updateExternalSelectAppInfo) {
props.updateExternalSelectAppInfo(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,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,96 @@
<template>
<q-field v-model="selectedObject" labelColor="primary" class="condition-object"
:clearable="isSelected" stack-label :dense="true" :outlined="true" >
<template v-slot:control >
<q-chip color="primary" text-color="white" v-if="isSelected && selectedObject.objectType==='field'" :dense="true" class="selected-obj">
{{ selectedObject.name }}
</q-chip>
<q-chip color="info" text-color="white" v-if="isSelected && selectedObject.objectType==='variable'" :dense="true" class="selected-obj">
{{ selectedObject.name.name }}
</q-chip>
</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>
</show-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, ref ,watchEffect,computed} from 'vue';
import ShowDialog from '../ShowDialog.vue';
import ConditionObjects from '../ConditionObjects.vue';
import { useFlowEditorStore } from '../../stores/flowEditor';
import {IActionFlow,IActionNode,IActionVariable} from '../../types/ActionTypes';
export default defineComponent({
name: 'ConditionObject',
components: {
ShowDialog,
ConditionObjects
},
props: {
modelValue: {
type: Object,
default: null
},
},
setup(props, { emit }) {
const appDg = ref();
const show = ref(false);
const selectedObject = ref(props.modelValue);
const store = useFlowEditorStore();
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];
}
};
watchEffect(() => {
emit('update:modelValue', selectedObject.value);
});
return {
store,
appDg,
show,
showDg,
closeDg,
selectedObject,
vars:reactive(vars),
isSelected,
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,266 @@
<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" class="col-4"></ConditionObject>
<q-select v-model="prop.node.operator" :options="operators" class="operator" :outlined="true" :dense="true"></q-select>
<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';
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 operator = inject('Operator')
const operators =computed(()=>{
return operator ? operator : Object.values(Operator);
});
const tree = reactive(props.conditionTree);
const conditionString = computed(()=>{
return tree.buildConditionString(tree.root);
});
const objectValueOptions=(options:any):any[]=>{
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);
return {
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,6 +4,7 @@
style="max-width: 400px" style="max-width: 400px"
:url="uploadUrl" :url="uploadUrl"
:label="title" :label="title"
:headers="headers"
accept=".csv,.xlsx" accept=".csv,.xlsx"
v-on:rejected="onRejected" v-on:rejected="onRejected"
v-on:uploaded="onUploadFinished" v-on:uploaded="onUploadFinished"
@@ -15,7 +16,10 @@
</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']);
/** /**
* ファイルアップロードを拒否する時の処理 * ファイルアップロードを拒否する時の処理
@@ -67,9 +71,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:"設計書から導入する(csv or 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

@@ -23,7 +23,7 @@ export default {
] ]
const rows = reactive([]) const rows = reactive([])
onMounted( () => { onMounted( () => {
api.get(`http://127.0.0.1:8000/api/domains/testtenant`).then(res =>{ api.get(`api/domains/1`).then(res =>{
res.data.forEach((item) => res.data.forEach((item) =>
{ {
rows.push({id:item.id,tenantid:item.tenantid,name:item.name,url:item.url,kintoneuser:item.kintoneuser}); rows.push({id:item.id,tenantid:item.tenantid,name:item.name,url:item.url,kintoneuser:item.kintoneuser});

View File

@@ -0,0 +1,43 @@
<template>
<q-btn-dropdown
color="primay"
push
flat
no-caps
icon="share"
size="md"
:label="userStore.currentDomain.domainName"
>
<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 } from 'vue';
const userStore = useAuthStore();
const domains = ref<IDomainInfo[]>([]);
(async ()=>{
domains.value = await userStore.getUserDomains();
})();
const onItemClick=(domain:IDomainInfo)=>{
console.log(domain);
userStore.setCurrentDomain(domain);
}
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,58 @@
<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" />
</div>
</template>
<script lang="ts">
import { useAsyncState } from '@vueuse/core';
import { api } from 'boot/axios';
import { computed } from 'vue';
export default {
name: 'FieldList',
props: {
fields: Array,
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) {
return props.fields.map(f => ({ name: f.label, objectType: 'field', ...f }));
} else {
return api.get('api/v1/appfields', {
params: {
app: props.appId
}
}).then(res => {
console.log(res);
return Object.values(res.data.properties).map(f => ({ name: f.label, objectType: 'field', ...f }));
});
}
}, [{ name: '', objectType: '', type: '', code: '', label: '' }])
return {
columns,
rows,
// selected: ref([]),
isLoaded
}
},
}
</script>

View File

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

View File

@@ -1,29 +1,38 @@
<template> <template>
<div class="q-pa-md q-gutter-sm"> <!-- <div class="q-pa-md q-gutter-sm" > -->
<q-dialog :model-value="visible" persistent> <q-dialog :model-value="visible" persistent bordered >
<q-card style="min-width: 350px"> <q-card 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> <q-card-section>
<div class="text-h6">{{ name }}選択</div> <!-- <div class="text-h6">{{ name }}</div> -->
</q-card-section> </q-card-section>
<q-card-section class="q-pt-none" :style="sectionStyle">
<q-card-section class="q-pt-none">
<slot></slot> <slot></slot>
</q-card-section> </q-card-section>
<q-card-actions align="right" class="text-primary"> <q-card-actions align="right" class="text-primary q-mt-lg">
<q-btn flat label="確定" v-close-popup @click="CloseDialogue('OK')" /> <q-btn flat label="確定" v-close-popup @click="CloseDialogue('OK')" />
<q-btn flat label="キャンセル" v-close-popup @click="CloseDialogue('Cancel')" /> <q-btn flat label="キャンセル" v-close-popup @click="CloseDialogue('Cancel')" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
</div> <!-- </div> -->
</template> </template>
<script> <script>
import {computed} from 'vue'
export default { export default {
name: 'ShowDialog', name: 'ShowDialog',
props: { props: {
name:String, name:String,
visible: Boolean, visible: Boolean,
width:String,
height:String,
minWidth:String,
minHeight:String
}, },
emits: [ emits: [
'close' 'close'
@@ -34,8 +43,20 @@ export default {
context.emit('close', val); context.emit('close', val);
} }
const cardStyle = computed(() => ({
minWidth: props.minWidth,
width: props.width
}));
const sectionStyle = computed(() => ({
height: props.height,
minHeight: props.minHeight
}));
return { return {
CloseDialogue CloseDialogue,
cardStyle,
sectionStyle
} }
}, },
} }

View File

@@ -0,0 +1,60 @@
<template>
<div class="q-pa-md">
<q-table flat bordered row-key="id" :selection="type" :selected="modelValue"
@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
},
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

@@ -7,7 +7,7 @@
style="font-size: 2em" style="font-size: 2em"
/> />
<div class="col-7 self-center ellipsis"> <div class="col-7 self-center ellipsis">
<a :href="!store.appInfo?'':`https://mfu07rkgnb7c.cybozu.com/k/${store.appInfo?.appId}`" target="_blank" title="Kiontoneへ"> <a :href="!store.appInfo?'':`${authStore.currentDomain.kintoneUrl}/k/${store.appInfo?.appId}`" target="_blank" title="Kiontoneへ">
{{ store.appInfo?.name }} {{ store.appInfo?.name }}
</a> </a>
</div> </div>
@@ -22,8 +22,15 @@
></q-btn> ></q-btn>
</div> </div>
</div> </div>
<ShowDialog v-model:visible="showSelectApp" name="アプリ" @close="closeDg"> <ShowDialog v-model:visible="showSelectApp" name="アプリ選択" @close="closeDg" min-width="50vw" min-height="50vh" >
<AppSelect ref="appDg" name="アプリ" type="single"></AppSelect> <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> </ShowDialog>
</template> </template>
@@ -31,20 +38,22 @@
import { defineComponent,ref } from 'vue'; import { defineComponent,ref } from 'vue';
import {AppInfo} from '../../types/ActionTypes' import {AppInfo} from '../../types/ActionTypes'
import ShowDialog from '../../components/ShowDialog.vue'; import ShowDialog from '../../components/ShowDialog.vue';
import AppSelect from '../../components/AppSelect.vue'; import AppSelectBox from '../../components/AppSelectBox.vue';
import { useFlowEditorStore } from 'stores/flowEditor'; import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'src/stores/useAuthStore';
export default defineComponent({ export default defineComponent({
name: 'AppSelector', name: 'AppSelector',
emits:[ emits:[
"appSelected" "appSelected"
], ],
components:{ components:{
AppSelect, AppSelectBox,
ShowDialog ShowDialog
}, },
setup(props, context) { setup(props, context) {
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const authStore=useAuthStore();
const appDg = ref(); const appDg = ref();
const showSelectApp=ref(false); const showSelectApp=ref(false);
@@ -67,10 +76,12 @@ export default defineComponent({
} }
return { return {
store, store,
authStore,
showSelectApp, showSelectApp,
showAppDialog, showAppDialog,
closeDg, closeDg,
appDg appDg,
filter:ref('')
} }
} }
}); });

View File

@@ -1,43 +1,117 @@
<template> <template>
<!-- <div class="q-pa-md q-gutter-sm"> --> <!-- <div class="q-pa-md q-gutter-sm"> -->
<q-tree <q-tree :nodes="store.eventTree.screens" node-key="eventId" children-key="events" no-connectors
:nodes="store.eventTree.screens" v-model:expanded="store.expandedScreen" :dense="true" :ref="tree">
node-key="label" <template v-slot:header-EVENT="prop">
children-key="events" <div :ref="prop.node.eventId" class="row col items-center no-wrap event-node">
no-connectors <q-icon v-if="prop.node.eventId" name="play_circle" :color="prop.node.hasFlow ? 'green' : 'grey'" size="16px"
v-model:expanded="store.expandedScreen" class="q-mr-sm">
:dense="true"
>
<template v-slot:default-header="prop">
<div class="row col items-start 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> </q-icon>
<div class="no-wrap" :class="selectedEvent && prop.node.eventId===selectedEvent.eventId?'selected-node':''">{{ prop.node.label }}</div> <div class="no-wrap" @click="onSelected(prop.node)"
:class="selectedEvent && prop.node.eventId === selectedEvent.eventId ? 'selected-node' : ''">{{
prop.node.label }}</div>
<q-space></q-space>
<!-- <q-icon v-if="prop.node.hasFlow" name="delete" color="negative" size="16px" class="q-mr-sm"></q-icon> -->
</div>
</template>
<template v-slot:header-CHANGE="prop">
<div class="row col items-center no-wrap event-node">
<div class="no-wrap">{{ 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-center event-node">
<div class="row col items-center" @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>{{ prop.node.label }}</div>
</div>
<div>
<q-btn class="q-mr-sm delete-btn" flat fab-mini icon="delete_forever" padding="none" color="negative"
@click="deleteEvent(prop.node)"></q-btn>
</div>
</div> </div>
</template> </template>
</q-tree> </q-tree>
<!-- </div> --> <show-dialog v-model:visible="showDialog" name="フィールド選択" @close="closeDg">
<field-select ref="appDg" name="フィールド" type="single" :appId="store.appInfo?.appId"></field-select>
</show-dialog>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, ref } from 'vue'; import { QTree, useQuasar } from 'quasar';
import { IKintoneEvent } from '../../types/KintoneEvents'; import { ActionFlow, RootAction } from 'src/types/ActionTypes';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor'; import { useFlowEditorStore } from 'stores/flowEditor';
import { ActionFlow, ActionNode, RootAction } from 'src/types/ActionTypes'; import { defineComponent, ref } from 'vue';
import { S } from 'app/dist/spa/assets/QTable.50486f7c'; import { IKintoneEvent, IKintoneEventGroup, IKintoneEventNode } from '../../types/KintoneEvents';
import FieldSelect from '../FieldSelect.vue';
import ShowDialog from '../ShowDialog.vue';
export default defineComponent({ export default defineComponent({
name: 'EventTree', name: 'EventTree',
components: {
ShowDialog,
FieldSelect,
},
setup(props, context) { setup(props, context) {
const $q = useQuasar();
const appDg = ref();
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const showDialog = ref(false);
const tree = ref<QTree>();
// const eventTree=ref(kintoneEvents); // const eventTree=ref(kintoneEvents);
// const selectedFlow = store.currentFlow; // const selectedFlow = store.currentFlow;
// const expanded=ref(); // const expanded=ref();
const selectedEvent = ref<IKintoneEvent | null>(null); const selectedEvent = ref<IKintoneEvent | null>(null);
const selectedChangeEvent = ref<IKintoneEventGroup | null>(null);
const isFieldChange = (node: IKintoneEventNode) => {
return node.header == 'EVENT' && node.eventId.indexOf(".change.") > -1;
}
//フィールド値変更イベント追加
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({
eventId: eventid,
label: field.name,
parentId: selectedChangeEvent.value.eventId,
header: '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) => { const onSelected = (node: IKintoneEvent) => {
if (!node.eventId) { if (!node.eventId) {
return; return;
@@ -46,24 +120,38 @@ export default defineComponent({
if (store.appInfo === undefined) { if (store.appInfo === undefined) {
return; return;
} }
const screen = store.eventTree.findScreen(node.eventId); const screen = store.eventTree.findEventById(node.parentId);
let flow = store.findFlowByEventId(node.eventId); let flow = store.findFlowByEventId(node.eventId);
const screenName=screen!==null?screen.label:""; let screenName = screen !== null ? screen.label : "";
let nodeLabel = node.label;
// if(isFieldChange(node)){
// screenName=nodeLabel;
// nodeLabel=`${node.label}の値を変更したとき`;
// }
if (flow !== undefined && flow !== null) { if (flow !== undefined && flow !== null) {
store.selectFlow(flow); store.selectFlow(flow);
} else { } else {
const root = new RootAction(node.eventId,screenName,node.label) const root = new RootAction(node.eventId, screenName, nodeLabel)
const flow = new ActionFlow(root); const flow = new ActionFlow(root);
store.flows?.push(flow); store.flows?.push(flow);
store.selectFlow(flow); store.selectFlow(flow);
selectedEvent.value.flowData = flow; selectedEvent.value.flowData = flow;
} }
} };
return { return {
// eventTree, // eventTree,
// expanded, // expanded,
appDg,
tree,
showDialog,
isFieldChange,
onSelected, onSelected,
selectedEvent, selectedEvent,
addChangeEvent,
deleteEvent,
closeDg,
store store
} }
} }
@@ -74,14 +162,21 @@ export default defineComponent({
flex-wrap: nowarp; flex-wrap: nowarp;
text-wrap: nowarp; text-wrap: nowarp;
} }
.event-node { .event-node {
cursor: pointer; cursor: pointer;
} }
.selected-node { .selected-node {
color: $primary; color: $primary;
font-weight: bolder; font-weight: bolder;
} }
.event-node:hover { .event-node:hover {
background-color: $light-blue-1; background-color: $light-blue-1;
} }
.delete-btn {
margin-right: 5px;
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="row justify-center" :style="{ marginLeft: node.inputPoint !== '' ? '240px' : '' }" > <div class="row justify-center no-wrap" >
<div class="row"> <div class="row">
<q-card class="action-node" :class="nodeStyle" :square="false" @click="onNodeClick" > <q-card class="action-node" :class="nodeStyle" :square="false" @click="onNodeClick" >
<q-toolbar class="col" > <q-toolbar class="col" >
@@ -8,6 +8,10 @@
<q-btn flat round dense icon="more_horiz" size="sm" > <q-btn flat round dense icon="more_horiz" size="sm" >
<q-menu auto-close anchor="top right"> <q-menu auto-close anchor="top right">
<q-list> <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 clickable v-if="!isRoot" @click="onEditNode">
<q-item-section avatar><q-icon name="edit" ></q-icon></q-item-section> <q-item-section avatar><q-icon name="edit" ></q-icon></q-item-section>
<q-item-section >編集する</q-item-section> <q-item-section >編集する</q-item-section>
@@ -25,8 +29,12 @@
</q-btn> </q-btn>
</q-toolbar> </q-toolbar>
<q-separator /> <q-separator />
<q-card-section> <q-card-section class="action-title">
<div class="text-h7">{{ node.title }}</div> <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> </q-card-section>
<template v-if="hasBranch"> <template v-if="hasBranch">
<q-separator /> <q-separator />
@@ -40,23 +48,34 @@
</div> </div>
</div> </div>
<template v-if="hasBranch"> <template v-if="hasBranch">
<div class="row justify-center" :style="{ marginLeft: node.inputPoint !== '' ? '240px' : '' }"> <node-line :action-node="node" @addNode="addNode" :left-columns="leftColumns" :right-columns="rightColumns"></node-line>
<div v-for="(point, index) in node.outputPoints" :key="index"> <div class="row justify-center no-wrap" >
<node-line :action-node="node" :mode="getMode(point)" @addNode="addNode" :input-point="point"></node-line> <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>
</div> </div>
</template> </template>
<template v-if="!hasBranch"> <template v-if="!hasBranch">
<div class="row justify-center" :style="{ marginLeft: node.inputPoint !== '' ? '240px' : '' }"> <div class="row justify-center no-wrap" >
<node-line :action-node="node" :mode="getMode('')" @addNode="addNode" input-point=""></node-line> <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> </div>
</template> </template>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, ref } from 'vue'; import { defineComponent, computed, ref } from 'vue';
import { IActionNode } from '../../types/ActionTypes'; import { IActionNode, IActionProperty } from '../../types/ActionTypes';
import NodeLine, { Direction } from '../main/NodeLine.vue'; import NodeLine, { Direction } from '../main/NodeLine.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
export default defineComponent({ export default defineComponent({
name: 'NodeItem', name: 'NodeItem',
components: { components: {
@@ -77,8 +96,10 @@ export default defineComponent({
"nodeEdit", "nodeEdit",
"deleteNode", "deleteNode",
"deleteAllNextNodes", "deleteAllNextNodes",
"copyFlow"
], ],
setup(props, context) { setup(props, context) {
const store = useFlowEditorStore();
const hasBranch = computed(() => props.actionNode.outputPoints.length > 0); const hasBranch = computed(() => props.actionNode.outputPoints.length > 0);
const nodeStyle = computed(() => { const nodeStyle = computed(() => {
return { return {
@@ -87,23 +108,11 @@ export default defineComponent({
'selected': props.isSelected && !props.actionNode.isRoot 'selected': props.isSelected && !props.actionNode.isRoot
}; };
}); });
const getMode = (point: string) => {
if (point === '' || props.actionNode.outputPoints.length === 0) { const nextNode=(point:string)=>{
return Direction.Default; const nextId= props.actionNode.nextNodeIds.get(point);
} if(!nextId) return undefined;
if (point === props.actionNode.outputPoints[0]) { return store.currentFlow?.findNodeById(nextId);
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;
}
}
} }
/** /**
* アクションノード追加イベントを * アクションノード追加イベントを
@@ -112,6 +121,38 @@ export default defineComponent({
const addNode = (point: string) => { const addNode = (point: string) => {
context.emit('addNode', props.actionNode, point); 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;
}
});
/** /**
* ノード選択状態 * ノード選択状態
*/ */
@@ -119,9 +160,20 @@ export default defineComponent({
context.emit('nodeSelected', props.actionNode); context.emit('nodeSelected', props.actionNode);
} }
const onNodeSelected = (node: IActionNode) => {
context.emit('nodeSelected', node);
}
const onEditNode=()=>{ const onEditNode=()=>{
context.emit('nodeEdit', props.actionNode); context.emit('nodeEdit', props.actionNode);
} }
const onNodeEdit=(node:IActionNode)=>{
context.emit('nodeEdit', node);
}
/** /**
* ノードを削除する * ノードを削除する
*/ */
@@ -129,30 +181,68 @@ export default defineComponent({
context.emit('deleteNode', props.actionNode); context.emit('deleteNode', props.actionNode);
} }
/**
* ノードを削除する
*/
const onDeleteNodeFromItem=(node:IActionNode)=>{
context.emit('deleteNode', node);
}
/** /**
* ノードの以下すべて削除する * ノードの以下すべて削除する
*/ */
const onDeleteAllNode=()=>{ const onDeleteAllNode=()=>{
context.emit('deleteAllNextNodes', props.actionNode); 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 { return {
store,
node: props.actionNode, node: props.actionNode,
nextNode,
isRoot: props.actionNode.isRoot, isRoot: props.actionNode.isRoot,
hasBranch, hasBranch,
nodeStyle, nodeStyle,
getMode, // getMode,
addNode, addNode,
addNodeFromItem,
onNodeClick, onNodeClick,
onNodeSelected,
onEditNode, onEditNode,
onNodeEdit,
onDeleteNode, onDeleteNode,
onDeleteAllNode onDeleteNodeFromItem,
onDeleteAllNode,
onDeleteAllNextNodes,
copyFlow,
varName,
leftColumns,
rightColumns
} }
} }
}); });
</script> </script>
<style lang="scss"> <style lang="scss">
.action-node { .action-node {
min-width: 300px !important; min-width: 280px !important;
}
.action-title{
max-width: 280px !important;
overflow-wrap: anywhere;
} }
.line { .line {

View File

@@ -1,11 +1,28 @@
<template> <template>
<div> <div class="row justify-center">
<svg class="node-line"> <svg class="node-line" style="width:100%" :viewBox="viewBox()">
<polyline :points="points.linePoints" class="line" ></polyline> <template v-if="!node.outputPoints || node.outputPoints.length===0" >
<text class="add-icon" @click="addNode(node)" :x="points.iconPoint.x" :y="points.iconPoint.y" font-family="Arial" font-size="25" <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-anchor="middle" dy=".3em" style="cursor: pointer;" >
</text> </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> </svg>
</div> </div>
</template> </template>
@@ -27,55 +44,97 @@ export default defineComponent({
type: Object as PropType<IActionNode>, type: Object as PropType<IActionNode>,
required: true required: true
}, },
mode: { leftColumns:{
type: String as PropType<Direction>, type:Number,
required: true required:false
}, },
inputPoint:{ rightColumns:{
type:String type:Number,
required:false
} }
}, },
emits: ['addNode'], emits: ['addNode'],
setup(props,context) { setup(props,context) {
const hasBranch = computed(() => props.actionNode.outputPoints.length > 0); const hasBranch = computed(() => props.actionNode.outputPoints.length > 0);
const points = computed(() => { const getMode = (point: string):Direction => {
switch (props.mode) { 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: case Direction.Left:
startX = leftColumn*300/2.0;
endX = ((leftColumn+rightColumn)/2.0 - 0.25)*300;
return { return {
linePoints: '180, 0, 180, 40, 120, 40, 120, 60', linePoints: `${startX}, 60, ${startX}, 40, ${endX}, 40, ${endX}, 0`,
iconPoint: { x: 180, y: 20 } iconPoint: { x: endX, y: 20 }
}; };
case Direction.Right: case Direction.Right:
startX = ((leftColumn+rightColumn)/2.0 + 0.25)*300;
endX = (leftColumn+(rightColumn/2.0))*300;
return { return {
linePoints: '60, 0, 60, 40, 120, 40, 120, 60', linePoints: `${startX}, 0, ${startX}, 40, ${endX}, 40, ${endX}, 60`,
iconPoint: { x: 60, y: 20 } iconPoint: { x: startX, y: 20 }
}; };
case Direction.LeftNotNext: case Direction.LeftNotNext:
startX = ((leftColumn+rightColumn)/2.0 - 0.25)*300;
return { return {
linePoints: '180, 0, 180, 40', linePoints: `${startX}, 0, ${startX}, 40`,
iconPoint: { x: 180, y: 20 } iconPoint: { x: startX, y: 20 }
}; };
case Direction.RightNotNext: case Direction.RightNotNext:
startX = ((leftColumn+rightColumn)/2.0 + 0.25)*300;
return { return {
linePoints: '60, 0, 60, 40', linePoints: `${startX}, 0, ${startX}, 40`,
iconPoint: { x: 60, y: 30 } iconPoint: { x: startX, y: 20 }
}; };
default: default:
return { return {
linePoints: '120, 0, 120, 60', linePoints: '150, 0, 150, 60',
iconPoint: { x: 120, y: 30 } iconPoint: { x: 150, y: 30 }
}; };
} }
}); };
const addNode=(prveNode:IActionNode)=>{
context.emit('addNode',props.inputPoint); 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 { return {
node: props.actionNode, node: props.actionNode,
getMode,
hasBranch, hasBranch,
points, points,
addNode addNode,
viewBox
} }
} }
}); });

View File

@@ -1,25 +1,27 @@
<template> <template>
<div> <div>
<div v-for="(item, index) in componentData" :key="index"> <div v-for="(item, index) in componentData" :key="index">
<component :is="item.component" v-bind="item.props" v-model="item.props.modelValue"></component> <component :is="item.component" v-bind="item.props" :connectProps="connectProps" v-model="item.props.modelValue"></component>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent,computed } from 'vue';
import InputText from '../right/InputText.vue'; import InputText from '../right/InputText.vue';
import SelectBox from '../right/SelectBox.vue'; import SelectBox from '../right/SelectBox.vue';
import DatePicker from '../right/DatePicker.vue'; import DatePicker from '../right/DatePicker.vue';
import FieldInput from '../right/FieldInput.vue'; import FieldInput from '../right/FieldInput.vue';
import EventSetter from '../right/EventSetter.vue';
import { IActionProperty, IProp } from 'src/types/ActionTypes';
export default defineComponent({ export default defineComponent({
name: 'ActionProperty', name: 'ActionProperty',
components: { components: {
InputText, InputText,
SelectBox, SelectBox,
DatePicker, DatePicker,
FieldInput FieldInput,
EventSetter
}, },
props: { props: {
jsonData: { jsonData: {
@@ -31,27 +33,43 @@ export default defineComponent({
required: false, required: false,
} }
}, },
computed: { setup(props){
componentData() { const componentData=computed<Array<IActionProperty>>(()=>{
return this.jsonData.elements.map((element: any) => { return props.jsonData.elements.map((element: any) => {
if(this.jsonValue != undefined ) if(props.jsonValue != undefined )
{ {
if(this.jsonValue.hasOwnProperty(element.props.name)) if(props.jsonValue.hasOwnProperty(element.props.name))
{ {
element.props.modelValue = this.jsonValue[element.props.name]; element.props.modelValue = props.jsonValue[element.props.name];
} }
else else
{ {
element.props.modelValue = ''; element.props.modelValue = '';
} }
} }
return { return {
component: element.component, component: element.component,
props: element.props, 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> </script>

View File

@@ -0,0 +1,140 @@
<template>
<div class="q-my-md" v-bind="$attrs">
<q-card flat>
<q-card-section class="q-pa-none q-my-sm q-mr-md">
<!-- <div class=" q-my-none ">App Field Select</div> -->
<div class="row q-mb-xs">
<div class="text-primary q-mb-xs text-caption">{{ $props.displayName }}</div>
</div>
<div class="row">
<div class="col">
<div class="q-mb-xs">{{ selectedField.app?.name || '未選択' }}</div>
</div>
<div class="col-1">
<q-btn round flat size="sm" color="primary" icon="search" @click="showDg" />
</div>
</div>
</q-card-section>
<q-separator />
<q-card-section class="q-pa-none q-ma-none">
<div style="">
<div v-if="selectedField.fields && selectedField.fields.length > 0">
<q-list bordered>
<q-virtual-scroll style="max-height: 160px;" :items="selectedField.fields" separator
v-slot="{ item, index }">
<q-item :key="index" dense clickable>
<q-item-section>
<q-item-label>
{{ item.label }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-btn round flat size="sm" icon="clear" @click="removeField(index)" />
</q-item-section>
</q-item>
</q-virtual-scroll>
</q-list>
</div>
<!-- <div v-else class="row q-mt-lg">
</div> -->
</div>
<!-- <q-separator /> -->
</q-card-section>
<q-card-section class="q-px-none q-py-xs" v-if="selectedField.fields && selectedField.fields.length === 0">
<div class="row">
<div class="text-grey text-caption"> {{ $props.placeholder }}</div>
<!-- <q-btn flat color="grey" label="clear" @click="clear" /> -->
</div>
</q-card-section>
</q-card>
</div>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeFieldDialog" ref="fieldDlg">
<AppFieldSelectBox v-model:selectedField="selectedField" :selectType="selectType" />
</show-dialog>
</template>
<script lang="ts">
import { 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
}
export interface IAppFields {
app?: IApp,
fields: IField[]
}
export default defineComponent({
inheritAttrs: false,
name: 'AppFieldSelect',
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'
}
},
setup(props, { emit }) {
const show = ref(false);
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);
}
watchEffect(() => {
emit('update:modelValue', selectedField.value);
});
return {
store,
show,
showDg: () => { show.value = true },
selectedField,
clear,
removeField,
};
}
});
</script>

View File

@@ -0,0 +1,93 @@
<template>
<div>
<q-field :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" @click="() => { dgIsShow = true }">アプリ選択</q-btn>
</q-card-actions>
<q-card-section class="text-caption">
<div v-if="selectedField.app.name">
{{ selectedField.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: {
context: {
type: Array<Props>,
default: '',
},
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
modelValue: {
type: Object,
default: null
}
},
setup(props, { emit }) {
const appDg = ref()
const dgIsShow = ref(false)
const selectedField = props.modelValue && props.modelValue.app ? props.modelValue : reactive({app:{}});
const closeDg = (state: string) => {
dgIsShow.value = false;
if (state == 'OK') {
selectedField.app = appDg.value.selected[0];
}
};
console.log(selectedField);
watchEffect(() => {
emit('update:modelValue', selectedField);
});
return {
filter: ref(''),
dgIsShow,
appDg,
closeDg,
selectedField
};
}
});
</script>

View File

@@ -0,0 +1,74 @@
<template>
<div class="" v-bind="$attrs">
<q-field v-model="color" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label :bottom-slots="!isSelected" >
<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
},
},
setup(props, { emit }) {
const color = ref(props.modelValue??"");
const isSelected = computed(()=>props.modelValue && props.modelValue!=="");
watchEffect(()=>{
emit('update:modelValue', color.value);
});
return {
color,
isSelected
};
}
});
</script>

View File

@@ -0,0 +1,165 @@
<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';
type Props = {
props?: {
name: string;
modelValue?: {
app: {
id: string;
name: string;
},
fields: {
type: string;
label: string;
code: string;
}[]
}
}
};
export default defineComponent({
name: 'FieldInput',
inheritAttrs: false,
components: {
ConditionEditor
},
props: {
context: {
type: Array<Props>,
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'
},
onlySourceSelect: {
type: Boolean,
default: false
},
operatorList: {
type: Array,
}
},
setup(props, { emit }) {
const 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('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(() => {
return tree.buildConditionString(tree.root);
});
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,202 @@
<template>
<div>
<q-field :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="() => { 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="50vw" min-height="60vh">
<div class="q-mx-md">
<div class="row q-col-gutter-x-xs flex-center">
<div class="col-5">
<div class="q-mx-xs">From</div>
</div>
<div class="col-1">
</div>
<div class="col-5">
<div class="q-mx-xs">To</div>
</div>
<div class="col-1"><q-btn flat round dense icon="add" size="sm" @click="addMappingObject" />
</div>
</div>
<div class="q-my-sm" v-for="(item, index) in mappingProps" :key="item.id">
<div class="row q-col-gutter-x-xs flex-center">
<div class="col-5">
<ConditionObject v-model="item.from" />
</div>
<div class="col-1">
</div>
<div class="col-5">
<q-field v-model="item.vName" type="text" outlined dense>
<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.app?.name} : ${item.to.fields[0].label}` }}
</div>
</template>
</q-field>
</div>
<div class="col-1">
<q-btn flat round dense icon="delete" size="sm" @click="() => deleteMappingObject(index)" />
</div>
</div>
<show-dialog v-model:visible="mappingProps[index].to.isDialogVisible" name="フィールド一覧"
@close="closeToDg" ref="fieldDlg">
<FieldSelect v-if="onlySourceSelect" ref="fieldDlg" name="フィールド" :appId="sourceAppId" not_page
:selectedFields="mappingProps[index].to.fields"
:updateSelects="(fields) => { mappingProps[index].to.fields = fields; mappingProps[index].to.app = sourceApp }">
</FieldSelect>
<AppFieldSelectBox v-else v-model:selectedField="mappingProps[index].to" />
</show-dialog>
</div>
</div>
</show-dialog>
</div>
</template>
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { computed, defineComponent, 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 IAppFields from './AppFieldSelect.vue';
type Props = {
props?: {
name: string;
modelValue?: {
app: {
id: string;
name: string;
}
}
}
};
type ValueType = {
id: string;
from: object;
to: typeof IAppFields & {
isDialogVisible: boolean;
};
}
const defaultMappingProp = () => ({ id: uuidv4(), to: { app: {}, fields: [], isDialogVisible: false } });
export default defineComponent({
name: 'DataMapping',
inheritAttrs: false,
components: {
ShowDialog,
ConditionObject,
AppFieldSelectBox,
FieldSelect
},
props: {
context: {
type: Array<Props>,
default: '',
},
displayName: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
modelValue: {
type: Object as () => ValueType[],
},
placeholder: {
type: String,
default: '',
},
onlySourceSelect: {
type: Boolean,
default: false
}
},
setup(props, { emit }) {
const closeDg = () => {
emit('update:modelValue', mappingProps
);
}
const closeToDg = () => {
emit('update:modelValue', mappingProps
);
}
const mappingProps: ValueType[] = props.modelValue
? props.modelValue
: reactive([defaultMappingProp()]);
const deleteMappingObject = (index: number) => mappingProps.length === 1
? mappingProps.splice(0, mappingProps.length, defaultMappingProp())
: mappingProps.splice(index, 1);
const mappingObjectsInputDisplay = computed(() =>
mappingProps ?
mappingProps
.filter(item => item.from?.name && item.to.fields?.length > 0)
.map(item => {
const name = typeof item.from?.name === 'string'
? item.from.name
: item.from?.name.name;
return `[${name}] - (${item.to.app?.name} : ${item.to.fields[0].label})`;
})
: []
);
const source = props.context.find(element => element?.props?.name === 'sources')
const sourceApp = computed(() => source?.props?.modelValue?.app);
const sourceAppId = computed(() => sourceApp.value?.id);
const btnDisable = computed(() => props.onlySourceSelect ? !(source?.props?.modelValue?.app?.id) : false);
//集計処理方法
watchEffect(() => {
emit('update:modelValue', mappingProps);
});
return {
uuidv4,
dgIsShow: ref(false),
closeDg,
toDgIsShow: ref(false),
closeToDg,
mappingProps,
addMappingObject: () => mappingProps.push(defaultMappingProp()),
deleteMappingObject,
mappingObjectsInputDisplay,
sourceApp,
sourceAppId,
btnDisable
};
},
});
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,229 @@
<template>
<div>
<q-field :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" @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-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?: {
name: string | {
name: string;
};
objectType: string;
type: string;
code: string;
label: string;
noLabel: boolean;
};
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: '',
},
},
setup(props, { emit }) {
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;
}));
}
const actionName = props.context.find(element => element?.props?.name === 'displayName')
const processingProps: ValueType = props.modelValue && props.modelValue.vars
? props.modelValue
: reactive({
name: '',
actionName: actionName?.props?.modelValue as string,
displayName: '結果(戻り値)',
vars: [{ id: uuidv4() }]
});
const closeDg = () => {
emit('update:modelValue', processingProps
);
}
const processingObjects = processingProps.vars;
const deleteProcessingObject = (index: number) => processingObjects.length === 1
? processingObjects.splice(0, processingObjects.length, { id: uuidv4() })
: processingObjects.splice(index, 1);
const processingObjectsInputDisplay = computed(() =>
processingObjects ?
processingObjects
.filter(item => item.field && item.logicalOperator && item.vName)
.map(item => {
const name = typeof item.field?.name === 'string'
? item.field.name
: item.field?.name.name;
return item.logicalOperator.operator!==''?
`${processingProps.name}.${item.vName} = ${item.logicalOperator.operator}(${name})`
:`${processingProps.name}.${item.vName} = ${name}`
})
: []
);
//集計処理方法
const logicalOperators = ref([
{
"operator": "",
"label": "なし"
},
{
"operator": "SUM",
"label": "合計"
},
{
"operator": "AVG",
"label": "平均"
},
{
"operator": "MAX",
"label": "最大値"
},
{
"operator": "MIN",
"label": "最小値"
},
{
"operator": "COUNT",
"label": "カウント"
},
{
"operator": "FIRST",
"label": "最初の値"
}
]);
watchEffect(() => {
emit('update:modelValue', processingProps);
});
return {
uuidv4,
dgIsShow: ref(false),
closeDg,
processingObjects,
processingProps,
addProcessingObject: () => processingObjects.push({ id: uuidv4() }),
deleteProcessingObject,
logicalOperators,
processingObjectsInputDisplay,
};
},
});
</script>
<style lang="scss"></style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div v-bind="$attrs">
<q-input v-model="selectedDate" :label="displayName" :placeholder="placeholder" mask="date" :rules="['date']"> <q-input v-model="selectedDate" :label="displayName" :placeholder="placeholder" label-color="primary" mask="date" :rules="['date']" stack-label>
<template v-slot:append> <template v-slot:append>
<q-icon name="event" class="cursor-pointer"> <q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-popup-proxy cover transition-show="scale" transition-hide="scale">
@@ -13,6 +13,7 @@
</q-icon> </q-icon>
</template> </template>
</q-input> </q-input>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -20,15 +21,24 @@ import { defineComponent, ref ,watchEffect} from 'vue';
export default defineComponent({ export default defineComponent({
name: 'DatePicker', name: 'DatePicker',
inheritAttrs:false,
props: { props: {
displayName:{ displayName:{
type: String, type: String,
default: '', default: '',
}, },
name:{
type: String,
default: '',
},
placeholder: { placeholder: {
type: String, type: String,
default: '', default: '',
}, },
hint:{
type: String,
default: '',
},
modelValue: { modelValue: {
type: String, type: String,
default: '', default: '',

View File

@@ -0,0 +1,83 @@
<template>
<div v-bind="$attrs">
<q-input :label="displayName" v-model="inputValue" label-color="primary" :placeholder="placeholder" 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
}
},
setup(props , { emit }) {
const inputValue = ref(props.modelValue);
const store = useFlowEditorStore();
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)
);
}
}
watchEffect(() => {
emit('update:modelValue', inputValue.value);
});
return {
inputValue,
addButtonEvent
};
},
});
</script>

View File

@@ -1,21 +1,42 @@
<template> <template>
<q-input v-model="selectedField" :label="displayName" labelColor="" :placeholder="placeholder" clearable > <div v-bind="$attrs">
<template v-slot:append> <q-field v-model="selectedField" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label
<q-icon name="search" class="cursor-pointer" @click="showDg"/> :bottom-slots="!isSelected">
<template v-slot:control>
<q-chip color="primary" text-color="white" v-if="isSelected">
{{ selectedField.name }}
</q-chip>
</template> </template>
</q-input> <!-- <template v-slot:hint v-if="isSelected">
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg"> <div> 項目コード<q-chip size="sm" outline color="secondary" text-color="white">{{selectedField.code}}</q-chip></div>
</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" widht="400px">
<field-select ref="appDg" name="フィールド" type="single" :appId="store.appInfo?.appId"></field-select> <field-select ref="appDg" name="フィールド" type="single" :appId="store.appInfo?.appId"></field-select>
</show-dialog> </show-dialog>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref ,watchEffect} from 'vue'; import { defineComponent, ref, watchEffect, computed } from 'vue';
import ShowDialog from '../ShowDialog.vue'; import ShowDialog from '../ShowDialog.vue';
import FieldSelect from '../FieldSelect.vue'; import FieldSelect from '../FieldSelect.vue';
import { useFlowEditorStore } from 'stores/flowEditor'; import { useFlowEditorStore } from 'stores/flowEditor';
interface IField {
name: string,
code: string,
type: string
}
export default defineComponent({ export default defineComponent({
name: 'FieldInput', name: 'FieldInput',
inheritAttrs:false,
components: { components: {
ShowDialog, ShowDialog,
FieldSelect, FieldSelect,
@@ -25,14 +46,22 @@ export default defineComponent({
type: String, type: String,
default: '', default: '',
}, },
name: {
type: String,
default: '',
},
placeholder: { placeholder: {
type: String, type: String,
default: '', default: '',
}, },
modelValue: { hint: {
type: String, type: String,
default: '', default: '',
}, },
modelValue: {
type: Object,
default: null
},
}, },
setup(props, { emit }) { setup(props, { emit }) {
@@ -40,6 +69,9 @@ export default defineComponent({
const show = ref(false); const show = ref(false);
const selectedField = ref(props.modelValue); const selectedField = ref(props.modelValue);
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const isSelected = computed(() => {
return selectedField.value !== null && typeof selectedField.value === 'object' && ('name' in selectedField.value)
});
const showDg = () => { const showDg = () => {
show.value = true; show.value = true;
@@ -47,7 +79,7 @@ export default defineComponent({
const closeDg = (val: string) => { const closeDg = (val: string) => {
if (val == 'OK') { if (val == 'OK') {
selectedField.value = appDg.value.selected[0].code; selectedField.value = appDg.value.selected[0];
} }
}; };
@@ -62,6 +94,7 @@ export default defineComponent({
showDg, showDg,
closeDg, closeDg,
selectedField, selectedField,
isSelected
}; };
} }
}); });

View File

@@ -1,37 +1,99 @@
<template> <template>
<q-input :label="displayName" :placeholder="placeholder" v-model="inputValue"/> <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> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent,ref,watchEffect } from 'vue'; import { kMaxLength } from 'buffer';
import { defineComponent, ref, watchEffect, computed } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'InputText', name: 'InputText',
inheritAttrs: false,
props: { props: {
displayName: { displayName: {
type: String, type: String,
default: '', default: '',
}, },
name: {
type: String,
default: '',
},
placeholder: { placeholder: {
type: String, type: String,
default: '', default: '',
}, },
modelValue: { hint: {
type: String, type: String,
default: '', default: '',
}, },
maxLength: {
type: Number,
default: undefined
},
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
modelValue: {
// type: Any,
default: '',
},
}, },
setup(props, { emit }) { setup(props, { emit }) {
const inputValue = ref(props.modelValue); const inputValue = computed({
get: () => {
watchEffect(() => { if (props.modelValue !== null && typeof props.modelValue === 'object' && 'name' in props.modelValue) {
emit('update:modelValue', inputValue.value); 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 rulesExp = props.rules === undefined ? null : eval(props.rules);
// const finalValue = computed(() => {
// return props.name !== 'verName' ? inputValue.value : {
// name: inputValue.value,
// };
// });
// watchEffect(() => {
// emit('update:modelValue', finalValue);
// });
return { return {
inputValue, inputValue,
showhint: ref(false),
rulesExp
}; };
}, },
}); });
</script> </script>
<style lang="scss">
.hint-text {
white-space: always;
max-width: 450px;
font-size: 1.2em;
}
</style>

View File

@@ -1,5 +1,8 @@
<template> <template>
<q-input :label="displayName" :placeholder="placeholder" v-model="inputValue" autogrow /> <div v-bind="$attrs">
<q-input :label="displayName" label-color="primary" v-model="inputValue" :placeholder="placeholder" autogrow
stack-label />
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -7,15 +10,24 @@ import { defineComponent,ref,watchEffect } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'MuiltInputText', name: 'MuiltInputText',
inheritAttrs: false,
props: { props: {
displayName: { displayName: {
type: String, type: String,
default: '', default: '',
}, },
name: {
type: String,
default: '',
},
placeholder: { placeholder: {
type: String, type: String,
default: '', default: '',
}, },
hint: {
type: String,
default: '',
},
modelValue: { modelValue: {
type: String, type: String,
default: '', default: '',

View File

@@ -0,0 +1,87 @@
<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
},
modelValue: {
type: [Number , String],
default: undefined
},
},
setup(props, { emit }) {
const numValue = ref(props.modelValue);
const rulesExp = props.rules===undefined?null : eval(props.rules);
const isError = computed(()=>{
const val = numValue.value;
if (val === undefined) {
return false;
}
const numVal = typeof val === "string" ? parseInt(val) : val;
// Ensure parsed value is a valid number
if (isNaN(numVal)) {
return true;
}
// Check against min and max boundaries, if defined
if ((props.min !== undefined && numVal < props.min) || (props.max !== undefined && numVal > props.max)) {
return true;
}
return false;
});
watchEffect(()=>{
emit("update:modelValue",numValue.value);
});
return {
numValue,
rulesExp
};
}
});
</script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div v-for="(item, index) in properties" :key="index" > <div v-for="(item, index) in properties" :key="index" >
<component :is="item.component" v-bind="item.props" v-model="item.props.modelValue"></component> <component :is="item.component" v-bind="item.props" :context="properties" :connectProps="connectProps(item.props)" v-model="item.props.modelValue"></component>
</div> </div>
</div> </div>
</template> </template>
@@ -15,8 +15,16 @@ import InputText from '../right/InputText.vue';
import SelectBox from '../right/SelectBox.vue'; import SelectBox from '../right/SelectBox.vue';
import DatePicker from '../right/DatePicker.vue'; import DatePicker from '../right/DatePicker.vue';
import FieldInput from '../right/FieldInput.vue'; import FieldInput from '../right/FieldInput.vue';
import AppFieldSelect from './AppFieldSelect.vue';
import MuiltInputText from '../right/MuiltInputText.vue'; import MuiltInputText from '../right/MuiltInputText.vue';
import { IActionNode,IActionProperty } from 'src/types/ActionTypes'; 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 { IActionNode,IActionProperty,IProp } from 'src/types/ActionTypes';
export default defineComponent({ export default defineComponent({
name: 'PropertyList', name: 'PropertyList',
@@ -25,7 +33,15 @@ export default defineComponent({
SelectBox, SelectBox,
DatePicker, DatePicker,
FieldInput, FieldInput,
MuiltInputText AppFieldSelect,
MuiltInputText,
ConditionInput,
EventSetter,
ColorPicker,
NumInput,
DataProcessing,
DataMapping,
AppSelect
}, },
props: { props: {
nodeProps: { nodeProps: {
@@ -38,10 +54,25 @@ export default defineComponent({
} }
}, },
setup(props, context) { setup(props, context) {
const properties=ref(props.nodeProps) 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 { return {
properties properties,
connectProps
} }
} }
}); });
</script> </script>
<style lang="scss">
</style>

View File

@@ -11,9 +11,9 @@
elevated elevated
overlay overlay
> >
<q-card class="column full-height" style="width: 300px"> <q-card class="column" style="max-width: 300px;min-height: 100%">
<q-card-section> <q-card-section>
<div class="text-h6">プロパティ</div> <div class="text-h6">{{ actionNode?.subTitle }}設定</div>
</q-card-section> </q-card-section>
<q-card-section class="col q-pt-none"> <q-card-section class="col q-pt-none">
<property-list :node-props="actionProps" v-if="showPanel" ></property-list> <property-list :node-props="actionProps" v-if="showPanel" ></property-list>
@@ -51,10 +51,10 @@ import { IActionNode } from 'src/types/ActionTypes';
], ],
setup(props,{emit}) { setup(props,{emit}) {
const showPanel =ref(props.drawerRight); const showPanel =ref(props.drawerRight);
const actionProps =ref(props.actionNode.actionProps); const actionProps =ref(props.actionNode?.actionProps);
watchEffect(() => { watchEffect(() => {
showPanel.value = props.drawerRight; showPanel.value = props.drawerRight;
actionProps.value= props.actionNode.actionProps; actionProps.value= props.actionNode?.actionProps;
}); });
const cancel = async() =>{ const cancel = async() =>{

View File

@@ -1,12 +1,16 @@
<template> <template>
<q-select v-model="selectedValue" :label="displayName" :options="options"/> <div v-bind="$attrs">
<q-select v-model="selectedValue" :use-chips="multiple" :label="displayName" label-color="primary" :options="options" stack-label
:multiple="multiple"/>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent,ref,watchEffect } from 'vue'; import { defineComponent,ref,watchEffect,computed } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'SelectBox', name: 'SelectBox',
inheritAttrs:false,
props: { props: {
displayName:{ displayName:{
type: String, type: String,
@@ -20,20 +24,27 @@ export default defineComponent({
type: Array, type: Array,
required: true, required: true,
}, },
modelValue: { selectType:{
type:String, type:String,
default:'', default:'',
}, },
modelValue: {
type: Object,
default: null,
},
}, },
setup(props, { emit }) { setup(props, { emit }) {
const selectedValue = ref(props.modelValue); const selectedValue = ref(props.modelValue);
const multiple = computed(()=>{
return props.selectType==='multiple'
});
watchEffect(() => { watchEffect(() => {
emit('update:modelValue', selectedValue.value); emit('update:modelValue', selectedValue.value);
}); });
return { return {
selectedValue selectedValue,
multiple
}; };
}, },
}); });

View File

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

View File

@@ -1,14 +1,11 @@
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { ActionFlow } from 'src/types/ActionTypes'; import { ActionFlow } from 'src/types/ActionTypes';
export class FlowCtrl export class FlowCtrl {
{ async getFlows(appId: string): Promise<ActionFlow[]> {
async getFlows(appId:string):Promise<ActionFlow[]>
{
const flows: ActionFlow[] = []; const flows: ActionFlow[] = [];
try { try {
const result = await api.get(`http://127.0.0.1:8000/api/flows/${appId}`); const result = await api.get(`api/flows/${appId}`);
//console.info(result.data); //console.info(result.data);
if (!result.data || !Array.isArray(result.data)) { if (!result.data || !Array.isArray(result.data)) {
return []; return [];
@@ -24,10 +21,9 @@ export class FlowCtrl
} }
} }
async SaveFlow(jsonData:any):Promise<boolean> async SaveFlow(jsonData: any): Promise<boolean> {
{ const result = await api.post('api/flow', jsonData);
const result = await api.post('http://127.0.0.1:8000/api/flow',jsonData); console.info(result.data);
console.info(result.data)
return true; return true;
} }
/** /**
@@ -35,10 +31,19 @@ export class FlowCtrl
* @param jsonData * @param jsonData
* @returns * @returns
*/ */
async UpdateFlow(jsonData:any):Promise<boolean> async UpdateFlow(jsonData: any): Promise<boolean> {
{ const result = await api.put('api/flow/' + jsonData.flowid, jsonData);
const result = await api.put('http://127.0.0.1:8000/api/flow/' + jsonData.flowid,jsonData); console.info(result.data);
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; return true;
} }
/** /**
@@ -46,12 +51,9 @@ export class FlowCtrl
* @param appid * @param appid
* @returns * @returns
*/ */
async depoly(appid:string):Promise<boolean> async depoly(appid: string): Promise<boolean> {
{ const result = await api.post(`api/v1/createjstokintone?app=${appid}`);
const result = await api.post(`http://127.0.0.1:8000/api/v1/createjstokintone?app=${appid}`);
console.info(result.data); console.info(result.data);
return true; 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

@@ -11,24 +11,16 @@
@click="toggleLeftDrawer" @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 color="blue" size="sm" @click="authStore.userdomain()">
{{ authStore.domain }}
</q-btn>
<q-chip>
{{ authStore.name }}
</q-chip>
<q-btn flat round dense icon="logout" @click="authStore.logout()"/> <q-btn flat round dense icon="logout" @click="authStore.logout()"/>
</q-toolbar> </q-toolbar>
</q-header> </q-header>
<q-drawer <q-drawer
v-model="leftDrawerOpen" :model-value="authStore.toggleLeftDrawer"
:show-if-above="false" :show-if-above="false"
bordered bordered
> >
@@ -36,7 +28,7 @@
<q-item-label <q-item-label
header header
> >
Essential Links 関連リンク
</q-item-label> </q-item-label>
<EssentialLink <EssentialLink
@@ -56,6 +48,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } 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'; import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore(); const authStore = useAuthStore();
@@ -72,14 +65,14 @@ const essentialLinks: EssentialLinkProps[] = [
title: 'フローエディター', title: 'フローエディター',
caption: 'flowChart', caption: 'flowChart',
icon: 'account_tree', icon: 'account_tree',
link: '/#/flowEditor2', link: '/#/FlowChart',
target:'_self' target:'_self'
}, },
{ {
title: 'FlowEditor', title: '条件エディター',
caption: 'FlowEditor', caption: 'condition',
icon: 'account_tree', icon: 'tune',
link: '/#/flowEditor', link: '/#/condition',
target:'_self' target:'_self'
}, },
{ {
@@ -158,11 +151,10 @@ const essentialLinks: EssentialLinkProps[] = [
} }
]; ];
const leftDrawerOpen = ref(false) const version = process.env.version;
const productName = process.env.productName;
const env=process.env;
function toggleLeftDrawer() { function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value authStore.toggleLeftMenu();
} }
</script> </script>

View File

@@ -1,47 +1,50 @@
<template> <template>
<q-page> <q-page>
<div class="q-pa-sm q-gutter-sm event-tree "> <q-layout container class="absolute-full shadow-2 rounded-borders">
<q-drawer <div class="q-pa-sm q-gutter-sm ">
side="left" <q-drawer side="left" :overlay="true" bordered v-model="drawerLeft" :show-if-above="false" elevated>
overlay <div class="flex-center fixed-top app-selector">
bordered
v-model="drawerLeft"
:show-if-above="false"
elevated
>
<div class="" style="padding:10px">
<div class="flex-center " >
<AppSelector /> <AppSelector />
</div> </div>
<div class="flex-center">
<div class="flex-center absolute-full" style="padding-top:65px;padding-left:15px;padding-right:15px;">
<q-scroll-area class="fit" :horizontal-thumb-style="{ opacity: '0' }">
<EventTree /> <EventTree />
</q-scroll-area>
</div> </div>
<div class="flex-center fixed-bottom bg-grey-3 q-pa-md row "> <div class="flex-center fixed-bottom bg-grey-3 q-pa-md row ">
<q-btn color="secondary" glossy label="デプロイ" @click="onDeploy" icon="sync"/> <q-btn color="secondary" glossy label="デプロイ" @click="onDeploy" icon="sync" :loading="deployLoading" />
<q-space></q-space> <q-space></q-space>
<q-btn color="primary" label="保存" @click="onSaveFlow" icon="save" /> <q-btn color="primary" label="保存" @click="onSaveFlow" icon="save" :loading="saveLoading" />
</div>
</div> </div>
</q-drawer> </q-drawer>
</div> </div>
<div class="q-pa-md q-gutter-sm"> <q-btn flat dense round
<div class="flowchart" v-if="store.currentFlow"> :icon="drawerLeft?'keyboard_double_arrow_left':'keyboard_double_arrow_right'"
<node-item v-for="(node,) in store.currentFlow.actionNodes" :key="node.id" :style="[drawerLeft?{'left':'300px'}:{'left':'0px'}]"
:isSelected="node===state.activeNode" :actionNode="node" @click="drawerLeft=!drawerLeft" class="expand" />
@addNode="addNode" <div class="q-pa-md q-gutter-sm" :style="{minWidth: minPanelWidth}">
@nodeSelected="onNodeSelected" <div class="flowchart" v-if="store.currentFlow" :style="[drawerLeft?{paddingLeft:'300px'}:{}]">
@nodeEdit="onNodeEdit" <node-item v-if="rootNode!==undefined" :key="rootNode.id" :isSelected="rootNode === store.activeNode"
@deleteNode="onDeleteNode" :actionNode="rootNode" @addNode="addNode" @nodeSelected="onNodeSelected" @nodeEdit="onNodeEdit"
@deleteAllNextNodes="onDeleteAllNextNodes" @deleteNode="onDeleteNode" @deleteAllNextNodes="onDeleteAllNextNodes" @copyFlow="onCopyFlow"></node-item>
></node-item>
</div> </div>
</div> </div>
</q-page> <PropertyPanel :actionNode="store.activeNode" v-model:drawerRight="drawerRight"></PropertyPanel>
<PropertyPanel :actionNode="state.activeNode" v-model:drawerRight="drawerRight"></PropertyPanel> </q-layout>
<ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg"> <ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg" min-width="500px" min-height="500px">
<action-select ref="appDg" name="model" type="single"></action-select> <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"></action-select>
</ShowDialog> </ShowDialog>
</q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -49,6 +52,8 @@ import {ref,reactive,computed,onMounted} from 'vue';
import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty } from 'src/types/ActionTypes'; import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty } from 'src/types/ActionTypes';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor'; import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'stores/useAuthStore';
import NodeItem from 'src/components/main/NodeItem.vue'; import NodeItem from 'src/components/main/NodeItem.vue';
import ShowDialog from 'components/ShowDialog.vue'; import ShowDialog from 'components/ShowDialog.vue';
import ActionSelect from 'components/ActionSelect.vue'; import ActionSelect from 'components/ActionSelect.vue';
@@ -57,15 +62,13 @@ import AppSelector from 'components/left/AppSelector.vue';
import EventTree from 'components/left/EventTree.vue'; import EventTree from 'components/left/EventTree.vue';
import { FlowCtrl } from '../control/flowctrl'; import { FlowCtrl } from '../control/flowctrl';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
const drawerLeft = ref(true); const deployLoading = ref(false);
const saveLoading = ref(false);
const drawerLeft = ref(false);
const $q = useQuasar(); const $q = useQuasar();
const store = useFlowEditorStore(); const store = useFlowEditorStore();
// ref関数を使ってtemplateとバインド const authStore = useAuthStore();
const state=reactive({
activeNode:{
id:""
},
})
const appDg = ref(); const appDg = ref();
const prevNodeIfo = ref({ const prevNodeIfo = ref({
prevNode: {} as IActionNode, prevNode: {} as IActionNode,
@@ -74,13 +77,28 @@ const prevNodeIfo=ref({
// const refFlow = ref<ActionFlow|null>(null); // const refFlow = ref<ActionFlow|null>(null);
const showAddAction = ref(false); const showAddAction = ref(false);
const drawerRight = ref(false); const drawerRight = ref(false);
const filter=ref("");
const model = ref(""); const model = ref("");
const addActionNode = (action: IActionNode) => { const addActionNode = (action: IActionNode) => {
// refFlow.value?.actionNodes.push(action); // refFlow.value?.actionNodes.push(action);
store.currentFlow?.actionNodes.push(action); store.currentFlow?.actionNodes.push(action);
} }
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 addNode = (node: IActionNode, inputPoint: string) => { const addNode = (node: IActionNode, inputPoint: string) => {
if (drawerRight.value) {
drawerRight.value = false;
}
showAddAction.value = true; showAddAction.value = true;
prevNodeIfo.value.prevNode = node; prevNodeIfo.value.prevNode = node;
prevNodeIfo.value.inputPoint = inputPoint; prevNodeIfo.value.inputPoint = inputPoint;
@@ -88,35 +106,60 @@ const addNode=(node:IActionNode,inputPoint:string)=>{
const onNodeSelected = (node: IActionNode) => { const onNodeSelected = (node: IActionNode) => {
//右パネルが開いている場合、自動閉じる //右パネルが開いている場合、自動閉じる
if(drawerRight.value && state.activeNode.id!==node.id){ if (drawerRight.value && store.activeNode?.id !== node.id) {
drawerRight.value = false; drawerRight.value = false;
} }
state.activeNode = node; store.setActiveNode(node);
} }
const onNodeEdit = (node: IActionNode) => { const onNodeEdit = (node: IActionNode) => {
state.activeNode = node; store.setActiveNode(node);
drawerRight.value = true; drawerRight.value = true;
} }
const onDeleteNode = (node: IActionNode) => { const onDeleteNode = (node: IActionNode) => {
if (!store.currentFlow) return; if (!store.currentFlow) return;
//右パネルが開いている場合、自動閉じる
if (drawerRight.value && store.activeNode?.id === node.id) {
drawerRight.value = false;
}
store.currentFlow?.removeNode(node); store.currentFlow?.removeNode(node);
} }
const onDeleteAllNextNodes = (node: IActionNode) => { const onDeleteAllNextNodes = (node: IActionNode) => {
if (!store.currentFlow) return; if (!store.currentFlow) return;
//右パネルが開いている場合、自動閉じる
if (drawerRight.value) {
drawerRight.value = false;
}
store.currentFlow?.removeAllNext(node.id); store.currentFlow?.removeAllNext(node.id);
} }
const closeDg = (val: any) => { const closeDg = (val: any) => {
console.log("Dialog closed->", val); console.log("Dialog closed->", val);
if (val == 'OK') { if (val == 'OK') {
const data = appDg.value.selected[0]; const data = appDg.value.selected[0];
const actionProps=JSON.parse(data.content); const actionProps = JSON.parse(data.property);
const action = new ActionNode(data.name,data.desc,"",[],actionProps); 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); 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');
}
};
/** /**
* デプロイ * デプロイ
*/ */
@@ -130,13 +173,17 @@ const onDeploy= async ()=>{
return; return;
} }
try { try {
deployLoading.value = true;
await store.deploy(); await store.deploy();
deployLoading.value = false;
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
caption: "通知", caption: "通知",
message: `デプロイを成功しました。` message: `デプロイを成功しました。`
}); });
} catch (error) { } catch (error) {
console.error(error);
deployLoading.value = false;
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: "エラー", caption: "エラー",
@@ -157,13 +204,17 @@ const onSaveFlow = async ()=>{
return; return;
} }
try { try {
saveLoading.value = true;
await store.saveFlow(targetFlow); await store.saveFlow(targetFlow);
saveLoading.value = false;
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
caption: "通知", caption: "通知",
message: `${targetFlow.getRoot()?.subTitle}のフロー設定を保存しました。` message: `${targetFlow.getRoot()?.subTitle}のフロー設定を保存しました。`
}); });
} catch (error) { } catch (error) {
console.error(error);
saveLoading.value = false;
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: "エラー", caption: "エラー",
@@ -174,8 +225,9 @@ const onSaveFlow = async ()=>{
} }
const fetchData = async () => { const fetchData = async () => {
const flowCtrl = new FlowCtrl(); drawerLeft.value = true;
if (store.appInfo === undefined) return; if (store.appInfo === undefined) return;
const flowCtrl = new FlowCtrl();
const actionFlows = await flowCtrl.getFlows(store.appInfo?.appId); const actionFlows = await flowCtrl.getFlows(store.appInfo?.appId);
if (actionFlows && actionFlows.length > 0) { if (actionFlows && actionFlows.length > 0) {
store.setFlows(actionFlows); store.setFlows(actionFlows);
@@ -185,28 +237,38 @@ const fetchData = async ()=>{
} }
const root = actionFlows[0].getRoot(); const root = actionFlows[0].getRoot();
if (root) { if (root) {
state.activeNode=root; store.setActiveNode(root);
} }
} }
onMounted(() => { onMounted(() => {
authStore.toggleLeftMenu();
fetchData(); fetchData();
}); });
</script> </script>
<style lang="scss"> <style lang="scss">
.app-selector {
padding: 15px;
z-index: 999;
}
.flowchart { .flowchart {
padding-top: 10px; padding-top: 10px;
} }
.flow-toolbar { .flow-toolbar {
opacity: 50%; opacity: 50%;
} }
.flow-container{
height: 91.5dvb;
overflow: hidden;
}
.event-tree .q-drawer { .event-tree .q-drawer {
top: 50px; top: 50px;
z-index: 999; z-index: 999;
} }
.expand{
position: fixed;
left: 0px;
top: 50%;
z-index: 9999;
}
</style> </style>

View File

@@ -13,7 +13,7 @@
</div> </div>
</q-page> </q-page>
<PropertyPanel :actionNode="state.activeNode" v-model:drawerRight="drawerRight"></PropertyPanel> <PropertyPanel :actionNode="state.activeNode" v-model:drawerRight="drawerRight"></PropertyPanel>
<show-dialog v-model:visible="showAddAction" name="アクション" @close="closeDg"> <show-dialog v-model:visible="showAddAction" name="アクション" @close="closeDg" width="350px">
<action-select ref="appDg" name="アクション" type="single"></action-select> <action-select ref="appDg" name="アクション" type="single"></action-select>
</show-dialog> </show-dialog>
</template> </template>

View File

@@ -1,117 +0,0 @@
<template>
<div >
<div class="q-ma-md">
<div class="q-gutter-xs row items-start">
<q-btn
size="md"
@click="drawerLeft = !drawerLeft"
icon="keyboard_double_arrow_right"
round
/>
<q-space />
<q-btn
color="white"
size="sm"
text-color="black"
label="キャンセル"
dense
/>
<q-btn
class="q-px-sm"
color="primary"
size="sm"
label="保存する"
@click="save()"
dense
/>
</div>
</div>
<q-layout
container
class="flow-container shadow-2 rounded-borders"
>
<q-drawer side="left" overlay bordered v-model="drawerLeft">
<div class="q-pa-sm fixed-right">
<q-btn
flat
round
color="primary"
icon="close"
@click="drawerLeft = !drawerLeft"
/>
</div>
<div class="q-mt-lg q-pa-sm">
<q-card-section>
<div class="flex-center">
<ItemSelector />
</div>
</q-card-section>
</div>
<q-separator />
<div class="q-mt-md q-pa-sm">
<q-card-section>
<ControlPanel />
</q-card-section>
</div>
<q-separator />
<q-card-actions align="right">
<div class="q-pa-sm">
<q-btn
flat
color="primary"
size="md"
@click="drawerLeft = !drawerLeft"
label="ジャンプ"
dense
/>
</div>
</q-card-actions>
</q-drawer>
<FlowChartTest />
</q-layout>
</div>
</template>
<script setup lang="ts">
import FlowChartTest from 'pages/FlowChartTest.vue';
import ControlPanel from 'components/flowEditor/left/ControlPanelC.vue';
import ItemSelector from 'components/flowEditor/left/ItemSelector.vue';
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor';
import { FlowCtrl } from '../control/flowctrl'
const flowCtrl = new FlowCtrl();
const actName = ref('勤怠管理 - 4');
const drawerLeft = ref(false);
const store = useFlowEditorStore();
const { flowNames1 } = storeToRefs(store);
let isNew = ref(true);
const save = () =>{
if(isNew.value)
{
flowCtrl.SaveFlow({appid:'1',flowid:'flow123',eventid:'event123',name:'test',content:'[]'});
isNew.value = false;
}
else
{
flowCtrl.UpdateFlow({appid:'1',flowid:'flow123',eventid:'event123',name:'test',content:'[{"a":"b"}]'});
}
}
</script>
<style lang="scss">
.flow-toolbar{
opacity: 50%;
}
.flow-container{
height: calc(91.5dvb - 50px);
overflow: hidden;
}
</style>

View File

@@ -4,10 +4,9 @@
<q-page class="window-height window-width row justify-center items-center"> <q-page class="window-height window-width row justify-center items-center">
<div class="column q-pa-lg"> <div class="column q-pa-lg">
<div class="row"> <div class="row">
<q-card square class="shadow-24" style="width:400px;height:540px;"> <q-card :square="false" class="shadow-24" style="width:400px;height:540px;">
<q-card-section class="bg-primary"> <q-card-section class="bg-primary">
<h4 class="text-h5 text-white q-my-md">{{ title}}</h4> <h4 class="text-h5 text-white q-my-md">{{ title}}</h4>
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<q-form class="q-px-sm q-pt-xl" ref="loginForm"> <q-form class="q-px-sm q-pt-xl" ref="loginForm">
@@ -31,8 +30,13 @@
</q-card-section> </q-card-section>
<q-card-actions class="q-px-lg"> <q-card-actions class="q-px-lg">
<q-btn unelevated size="lg" color="secondary" @click="submit" class="full-width text-white" <q-btn :loading="loading" unelevated size="lg" color="secondary" @click="submit" class="full-width text-white"
:label="btnLabel" /> label="ログイン" >
<template v-slot:loading>
<q-spinner class="on-left" />
ログイン中...
</template>
</q-btn>
</q-card-actions> </q-card-actions>
</q-card> </q-card>
@@ -52,13 +56,13 @@ import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore(); const authStore = useAuthStore();
const $q = useQuasar() const $q = useQuasar()
const loginForm = ref(null); const loginForm = ref(null);
const loading = ref(false);
let title = ref('ログイン'); let title = ref('ログイン');
let email = ref(''); let email = ref('');
let password = ref(''); let password = ref('');
let visibility = ref(false); let visibility = ref(false);
let passwordFieldType = ref('password'); let passwordFieldType = ref('password');
let visibilityIcon = ref('visibility'); let visibilityIcon = ref('visibility');
let btnLabel = ref('ログイン');
const required = (val:string) => { const required = (val:string) => {
return (val && val.length > 0 || '必須項目') return (val && val.length > 0 || '必須項目')
} }
@@ -74,26 +78,34 @@ import { useAuthStore } from 'stores/useAuthStore';
passwordFieldType.value = visibility.value ? 'text' : 'password' passwordFieldType.value = visibility.value ? 'text' : 'password'
visibilityIcon.value = visibility.value ? 'visibility_off' : 'visibility' visibilityIcon.value = visibility.value ? 'visibility_off' : 'visibility'
} }
const submit = () =>{ const submit = async () =>{
authStore.login(email.value,password.value).then((result)=>{ loading.value=true;
if(result) try {
{ const result = await authStore.login(email.value,password.value);
loading.value=false;
if(result){
$q.notify({ $q.notify({
icon: 'done', icon: 'done',
color: 'positive', color: 'positive',
message: 'ログイン成功' message: 'ログイン成功'
}) });
} }
else else{
{
$q.notify({ $q.notify({
icon: 'error', icon: 'error',
color: 'negative', color: 'negative',
message: 'ログイン失敗' message: 'ログイン失敗'
})
}
}); });
}
}catch (error) {
console.error(error);
loading.value=false;
$q.notify({
icon: 'error',
color: 'negative',
message: 'ログイン失敗'
});
}
} }

View File

@@ -22,9 +22,9 @@
<div class="q-pa-md"> <div class="q-pa-md">
<q-select v-model="model" :options="options" label="Standard"/> <q-select v-model="model" :options="options" label="Standard"/>
<q-btn :label="model+'選択'" color="primary" @click="showDg()" /> <q-btn :label="model+'選択'" color="primary" @click="showDg()" />
<show-dialog v-model:visible="show" :name="model" @close="closeDg"> <show-dialog v-model:visible="show" :name="model" @close="closeDg" width="400px">
<template v-if="model=='アプリ'"> <template v-if="model=='アプリ'">
<app-select ref="appDg" :name="model" type="single"></app-select> <app-select-box ref="appDg" :name="model" type="single"></app-select-box>
</template> </template>
<template v-if="model=='フィールド'"> <template v-if="model=='フィールド'">
<field-select ref="appDg" :name="model" type="multiple" :appId="1"></field-select> <field-select ref="appDg" :name="model" type="multiple" :appId="1"></field-select>
@@ -42,7 +42,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ShowDialog from 'components/ShowDialog.vue'; import ShowDialog from 'components/ShowDialog.vue';
import AppSelect from 'components/AppSelect.vue'; import AppSelectBox from 'components/AppSelectBox.vue';
import FieldSelect from 'components/FieldSelect.vue'; import FieldSelect from 'components/FieldSelect.vue';
import ActionSelect from 'components/ActionSelect.vue'; import ActionSelect from 'components/ActionSelect.vue';
import { ref } from 'vue' import { ref } from 'vue'

View File

@@ -1,15 +1,7 @@
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<q-table <q-table title="Treats" :rows="rows" :columns="columns" row-key="id" selection="single" :filter="filter"
title="Treats" :loading="loading" v-model:selected="selected">
:rows="rows"
:columns="columns"
row-key="id"
selection="single"
:filter="filter"
:loading="loading"
v-model:selected="selected"
>
<template v-slot:top> <template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" /> <q-btn color="primary" :disable="loading" label="新規" @click="addRow" />
@@ -80,7 +72,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'; import { ref, onMounted, reactive } from 'vue';
import { useQuasar } from 'quasar'
import { api } from 'boot/axios'; import { api } from 'boot/axios';
const columns = [ const columns = [
@@ -98,43 +89,37 @@ const columns = [
{ name: 'url', align: 'left', label: 'URL', field: 'url', sortable: true }, { name: 'url', align: 'left', label: 'URL', field: 'url', sortable: true },
{ name: 'user', label: 'Account', field: 'user' }, { name: 'user', label: 'Account', field: 'user' },
{ name: 'password', label: 'Password', field: 'password' } { name: 'password', label: 'Password', field: 'password' }
] ];
const loading = ref(false) const loading = ref(false);
const filter = ref('') const filter = ref('');
const rows = reactive([]) const rows = ref([]);
const show = ref(false); const show = ref(false);
const confirm = ref(false); const confirm = ref(false);
const selected = ref([]) const selected = ref([]);
const tenantid = ref('') const tenantid = ref('');
const name = ref('') const name = ref('');
const url =ref('') const url = ref('');
const isPwd =ref(true) const isPwd = ref(true);
const kintoneuser =ref('') const kintoneuser = ref('');
const kintonepwd =ref('') const kintonepwd = ref('');
const $q = useQuasar()
let editId = ref(0); let editId = ref(0);
const getDomain = () => { const getDomain = async () => {
loading.value = true; loading.value = true;
api.get(`http://127.0.0.1:8000/api/domains/testtenant`).then(res => { const result= await api.get(`api/domains/1`);
rows.length = 0; rows.value= result.data.map((item)=>{
res.data.forEach((item) => { return { id: item.id, tenantid: item.tenantid, name: item.name, url: item.url, user: item.kintoneuser, password: item.kintonepwd }
rows.push({ id:item.id,tenantid: item.tenantid,name: item.name, url: item.url, user: item.kintoneuser, password: item.kintonepwd }); });
loading.value = false;
} }
)
}).finally(()=>{ loading.value = false; });
} onMounted(async () => {
onMounted(() => { await getDomain();
getDomain();
}) })
// emulate fetching data from server // emulate fetching data from server
const addRow = () => { const addRow = () => {
editId.value editId.value
@@ -145,15 +130,14 @@ onMounted(() => {
//loading.value = true //loading.value = true
confirm.value = true; confirm.value = true;
let row = JSON.parse(JSON.stringify(selected.value[0])); let row = JSON.parse(JSON.stringify(selected.value[0]));
if(selected.value.length === 0) if (selected.value.length === 0) {
{
return; return;
} }
editId.value = row.id; editId.value = row.id;
} }
const deleteDomain = () => { const deleteDomain = () => {
api.delete(`http://127.0.0.1:8000/api/domain/`+ editId.value).then(() =>{ api.delete(`api/domain/${editId.value}`).then(() => {
getDomain(); getDomain();
}) })
editId.value = 0; editId.value = 0;
@@ -161,8 +145,7 @@ onMounted(() => {
}; };
const editRow = () => { const editRow = () => {
if(selected.value.length === 0) if (selected.value.length === 0) {
{
return; return;
} }
let row = JSON.parse(JSON.stringify(selected.value[0])); let row = JSON.parse(JSON.stringify(selected.value[0]));
@@ -181,9 +164,8 @@ onMounted(() => {
} }
const onSubmit = () => { const onSubmit = () => {
if(editId.value !== 0) if (editId.value !== 0) {
{ api.put(`api/domain`, {
api.put(`http://127.0.0.1:8000/api/domain`,{
'id': editId.value, 'id': editId.value,
'tenantid': tenantid.value, 'tenantid': tenantid.value,
'name': name.value, 'name': name.value,
@@ -196,9 +178,8 @@ onMounted(() => {
onReset(); onReset();
}) })
} }
else else {
{ api.post(`api/domain`, {
api.post(`http://127.0.0.1:8000/api/domain`,{
'id': 0, 'id': 0,
'tenantid': tenantid.value, 'tenantid': tenantid.value,
'name': name.value, 'name': name.value,

View File

@@ -150,7 +150,7 @@ export default {
</template> </template>
</q-table> </q-table>
<show-dialog v-model:visible="show" name="ドメイン" @close="closeDg"> <show-dialog v-model:visible="show" name="ドメイン" @close="closeDg" width="350px">
<domain-select ref="domainDg" name="ドメイン" type="multiple"></domain-select> <domain-select ref="domainDg" name="ドメイン" type="multiple"></domain-select>
</show-dialog> </show-dialog>
@@ -179,6 +179,7 @@ import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore(); const authStore = useAuthStore();
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { domain } from 'process';
const $q = useQuasar() const $q = useQuasar()
@@ -193,21 +194,14 @@ let activedomainid = ref(0);
const columns = [ const columns = [
{ name: 'id'}, { name: 'id'},
{ {name: 'name',required: true,label: 'Name',align: 'left',field: 'name',sortable: true},
name: 'name',
required: true,
label: 'Name',
align: 'left',
field: row => row.name,
sortable: true
},
{ name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true }, { name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true }, { name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true },
{ name: 'kintonepwd' }, { name: 'kintonepwd' },
{ name: 'active', field: 'active'} { name: 'active', field: 'active'}
] ]
const rows = reactive([]) const rows = ref([] as any[]);
const isActive = (id:number) =>{ const isActive = (id:number) =>{
if(id == activedomainid.value) if(id == activedomainid.value)
@@ -223,7 +217,7 @@ const newDomain = () => {
const activeDomain = (id:number) => { const activeDomain = (id:number) => {
api.put(`http://127.0.0.1:8000/api/activedomain/`+ id).then(() =>{ api.put(`api/activedomain/`+ id).then(() =>{
getDomain(); getDomain();
}) })
}; };
@@ -234,7 +228,7 @@ const deleteConfirm = (row:object) => {
}; };
const deleteDomain = () => { const deleteDomain = () => {
api.delete(`http://127.0.0.1:8000/api/domain/`+ editId.value+'/1').then(() =>{ api.delete(`api/domain/`+ editId.value+'/1').then(() =>{
getDomain(); getDomain();
}) })
editId.value = 0; editId.value = 0;
@@ -248,25 +242,21 @@ const closeDg = (val:string) => {
{ {
dodmainids.push(domains[key].id); dodmainids.push(domains[key].id);
} }
api.post(`http://127.0.0.1:8000/api/domain`, dodmainids).then(() =>{getDomain();}); api.post(`api/domain`, dodmainids).then(() =>{getDomain();});
} }
}; };
const getDomain = () => { const getDomain = async () => {
api.get(`http://127.0.0.1:8000/api/activedomain`).then(res => { const resp = await api.get(`api/activedomain`);
activedomainid.value = res.data.id; activedomainid.value = resp.data.id;
authStore.changedomain(res.data.name); const domainResult = await api.get(`api/domain`);
}); const domains = domainResult.data as any[];
api.get(`http://127.0.0.1:8000/api/domain`).then(res => { rows.value=domains.map((item)=>{
rows.length = 0; return { id:item.id,name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd}
res.data.forEach((item) => {
rows.push({ id:item.id,name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd});
}
)
}); });
} }
onMounted(() => { onMounted(async () => {
getDomain(); await getDomain();
}) })
const isDomain = (val) =>{ const isDomain = (val) =>{

View File

@@ -0,0 +1,39 @@
<template>
<q-page>
<div class="flowchart">
<q-btn @click="showCondition()" class="q-mt-md" color="primary" icon="mdi-plus">条件エディタ表示</q-btn>
</div>
<condition-editor v-model:show="show" v-model:conditionTree="tree"></condition-editor>
<q-code>{{conditionString}}</q-code>
</q-page>
</template>
<script setup lang="ts">
import {ref,reactive,computed} from 'vue';
import ConditionEditor from '../components/ConditionEditor/ConditionEditor.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
import { ConditionTree,GroupNode,ConditionNode,LogicalOperator,Operator } from 'app/src/types/Conditions';
const store = useFlowEditorStore();
const tree = reactive(new ConditionTree());
const newNode = new ConditionNode({},Operator.Equal,'',tree.root);
tree.addNode(tree.root,newNode);
const show =ref(false);
const showCondition=()=>{
show.value=true;
}
const conditionString = computed(()=>{
return tree.buildConditionString(tree.root);
});
store.setApp({
appId:'146',
name:'トリトン管理部日報'
});
</script>
<style lang="scss">
.flowchart {
padding-top: 10px;
}
</style>

View File

@@ -4,7 +4,7 @@
<div class="q-pa-md column content-center items-center"> <div class="q-pa-md column content-center items-center">
<div> <div>
<q-btn label="アクション選択" color="primary" @click="showDg()" v-if="addshow" /> <q-btn label="アクション選択" color="primary" @click="showDg()" v-if="addshow" />
<show-dialog v-model:visible="show" name="アクション" @close="closeDg"> <show-dialog v-model:visible="show" name="アクション" @close="closeDg" width="350px">
<action-select ref="appDg" name="アクション" type="single"></action-select> <action-select ref="appDg" name="アクション" type="single"></action-select>
</show-dialog> </show-dialog>
</div> </div>

View File

@@ -17,31 +17,11 @@ import { useAuthStore } from 'stores/useAuthStore';
* with the Router instance. * with the Router instance.
*/ */
// export default route(function (/* { store, ssrContext } */) {
// const createHistory = process.env.SERVER
// ? createMemoryHistory
// : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
// const Router = createRouter({
// scrollBehavior: () => ({ left: 0, top: 0 }),
// routes,
// // Leave this as is and make changes in quasar.conf.js instead!
// // quasar.conf.js -> build -> vueRouterMode
// // quasar.conf.js -> build -> publicPath
// history: createHistory(process.env.VUE_ROUTER_BASE),
// });
// return Router;
// });
const createHistory = process.env.SERVER const createHistory = process.env.SERVER
? createMemoryHistory ? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory); : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
export const Router = createRouter({ const routerInstance = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
routes, routes,
@@ -52,10 +32,8 @@ export const Router = createRouter({
}); });
export default route(function (/* { store, ssrContext } */) { export default route(function (/* { store, ssrContext } */) {
return Router;
});
Router.beforeEach(async (to) => { routerInstance.beforeEach(async (to) => {
// clear alert on route change // clear alert on route change
//const alertStore = useAlertStore(); //const alertStore = useAlertStore();
//alertStore.clear(); //alertStore.clear();
@@ -70,4 +48,29 @@ Router.beforeEach(async (to) => {
return '/login'; return '/login';
} }
}); });
return routerInstance;
});
export const router = routerInstance;
// const createHistory = process.env.SERVER
// ? createMemoryHistory
// : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
// export const Router = createRouter({
// scrollBehavior: () => ({ left: 0, top: 0 }),
// routes,
// // Leave this as is and make changes in quasar.conf.js instead!
// // quasar.conf.js -> build -> vueRouterMode
// // quasar.conf.js -> build -> publicPath
// history: createHistory(process.env.VUE_ROUTER_BASE),
// });
// export default route(function (/* { store, ssrContext } */) {
// return Router;
// });

View File

@@ -5,7 +5,13 @@ const routes: RouteRecordRaw[] = [
path: '/login', path: '/login',
component: () => import('pages/LoginPage.vue') component: () => import('pages/LoginPage.vue')
}, },
{
path:'/FlowChart',
component:()=>import('layouts/MainLayout.vue'),
children:[
{path:'',component:()=>import('pages/FlowChart.vue')}
]
},
{ {
path: '/', path: '/',
component: () => import('layouts/MainLayout.vue'), component: () => import('layouts/MainLayout.vue'),
@@ -14,13 +20,13 @@ const routes: RouteRecordRaw[] = [
{ path: 'ruleEditor', component: () => import('pages/RuleEditor.vue') }, { path: 'ruleEditor', component: () => import('pages/RuleEditor.vue') },
{ path: 'test', component: () => import('pages/testQursar.vue') }, { path: 'test', component: () => import('pages/testQursar.vue') },
{ path: 'flow', component: () => import('pages/testFlow.vue') }, { path: 'flow', component: () => import('pages/testFlow.vue') },
{ path: 'flowchart', component: () => import('pages/FlowChartTest.vue') }, { path: 'FlowChartTest', component: () => import('pages/FlowChartTest.vue') },
{ path: 'flowEditor', component: () => import('pages/FlowEditorPage.vue') }, { path: 'flowEditor', component: () => import('pages/FlowEditorPage.vue') },
{ path: 'flowEditor2', component: () => import('pages/FlowChart.vue') }, // { path: 'FlowChart', component: () => import('pages/FlowChart.vue') },
{ path: 'flowChart2', component: () => import('pages/FlowEditorPage2.vue') },
{ path: 'right', component: () => import('pages/testRight.vue') }, { path: 'right', component: () => import('pages/testRight.vue') },
{ path: 'domain', component: () => import('pages/TenantDomain.vue') }, { path: 'domain', component: () => import('pages/TenantDomain.vue') },
{ path: 'userdomain', component: () => import('pages/UserDomain.vue') } { path: 'userdomain', component: () => import('pages/UserDomain.vue')},
{ path: 'condition', component: () => import('pages/conditionPage.vue') }
], ],
}, },
// Always leave this as last one, // Always leave this as last one,

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { AppInfo ,IActionFlow} from 'src/types/ActionTypes'; import { AppInfo, IActionFlow, IActionNode } from 'src/types/ActionTypes';
import { kintoneEvents,IKintoneEvent,KintoneEventManager } from 'src/types/KintoneEvents'; import { IKintoneEvent, KintoneEventManager } from 'src/types/KintoneEvents';
import { FlowCtrl } from '../control/flowctrl'; import { FlowCtrl } from '../control/flowctrl';
export interface FlowEditorState { export interface FlowEditorState {
@@ -8,20 +8,23 @@ export interface FlowEditorState{
appInfo?: AppInfo; appInfo?: AppInfo;
flows?: IActionFlow[]; flows?: IActionFlow[];
selectedFlow?: IActionFlow | undefined; selectedFlow?: IActionFlow | undefined;
activeNode: IActionNode | undefined;
eventTree: KintoneEventManager; eventTree: KintoneEventManager;
selectedEvent: IKintoneEvent | undefined; selectedEvent: IKintoneEvent | undefined;
expandedScreen: any[]; expandedScreen: any[];
} }
const flowCtrl = new FlowCtrl(); const flowCtrl = new FlowCtrl();
export const useFlowEditorStore = defineStore("flowEditor",{ const eventTree = new KintoneEventManager();
export const useFlowEditorStore = defineStore('flowEditor', {
state: (): FlowEditorState => ({ state: (): FlowEditorState => ({
flowNames1: '', flowNames1: '',
appInfo: undefined, appInfo: undefined,
flows: [], flows: [],
selectedFlow: undefined, selectedFlow: undefined,
eventTree:kintoneEvents, activeNode: undefined,
eventTree: eventTree,
selectedEvent: undefined, selectedEvent: undefined,
expandedScreen:[] expandedScreen: [],
}), }),
getters: { getters: {
/** /**
@@ -40,19 +43,29 @@ export const useFlowEditorStore = defineStore("flowEditor",{
return (eventId: string) => { return (eventId: string) => {
return state.flows?.find((flow) => { return state.flows?.find((flow) => {
const root = flow.getRoot(); const root = flow.getRoot();
return root?.name===eventId return root?.name === eventId;
}); });
} };
} },
findEventById(state) {
return (eventId: string) => {
return state.eventTree.findEventById(eventId);
};
},
}, },
actions: { actions: {
setFlows(flows: IActionFlow[]) { setFlows(flows: IActionFlow[]) {
this.flows = flows; this.flows = flows;
}, },
selectFlow(flow:IActionFlow){ selectFlow(flow: IActionFlow | undefined) {
this.selectedFlow = flow; this.selectedFlow = flow;
}, },
setActiveNode(node: IActionNode) {
this.activeNode = node;
},
setApp(app: AppInfo) { setApp(app: AppInfo) {
this.appInfo = app; this.appInfo = app;
}, },
@@ -74,7 +87,7 @@ export const useFlowEditorStore = defineStore("flowEditor",{
if (actionFlows && actionFlows.length > 0) { if (actionFlows && actionFlows.length > 0) {
this.selectFlow(actionFlows[0]); this.selectFlow(actionFlows[0]);
} }
const expandNames = actionFlows.map(flow=>flow.getRoot()?.title); const expandNames = actionFlows.map((flow) => flow.getRoot()?.title);
// const expandName =actionFlows[0].getRoot()?.title; // const expandName =actionFlows[0].getRoot()?.title;
this.expandedScreen = expandNames; this.expandedScreen = expandNames;
}, },
@@ -89,8 +102,8 @@ export const useFlowEditorStore = defineStore("flowEditor",{
appid: this.appInfo?.appId, appid: this.appInfo?.appId,
eventid: root?.name, eventid: root?.name,
name: root?.subTitle, name: root?.subTitle,
content: JSON.stringify(flow) content: JSON.stringify(flow),
} };
if (isNew) { if (isNew) {
return await flowCtrl.SaveFlow(jsonData); return await flowCtrl.SaveFlow(jsonData);
@@ -98,6 +111,24 @@ export const useFlowEditorStore = defineStore("flowEditor",{
return await flowCtrl.UpdateFlow(jsonData); return await flowCtrl.UpdateFlow(jsonData);
} }
}, },
deleteEvent(event: IKintoneEvent) {
const store = useFlowEditorStore();
if (event.flowData) {
const flow = event.flowData;
if (flow.id === '') {
return;
}
flowCtrl.DeleteFlow(flow.id)
eventTree.deleteEvent(event, store);
if(this.flows){
this.flows = this.flows.filter((f) => f.id !== flow.id);
}
} else {
eventTree.deleteEvent(event, store);
}
},
/** /**
* デプロイする * デプロイする
*/ */
@@ -106,7 +137,6 @@ export const useFlowEditorStore = defineStore("flowEditor",{
return false; return false;
} }
return await flowCtrl.depoly(this.appInfo?.appId); return await flowCtrl.depoly(this.appInfo?.appId);
} },
},
}
}); });

View File

@@ -1,40 +1,52 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { Router } from '../router'; import {router} from 'src/router';
import {IDomainInfo} from '../types/ActionTypes';
export interface IUserState{
token?:string;
returnUrl:string;
currentDomain:IDomainInfo;
LeftDrawer:boolean;
}
export const useAuthStore = defineStore({ export const useAuthStore = defineStore({
id: 'auth', id: 'auth',
state: () =>{ state: ():IUserState =>{
const token=localStorage.getItem('token'); const token=localStorage.getItem('token')||'';
if(token && token!==''){ if(token!==''){
api.defaults.headers["Authorization"]='Bearer ' + token; api.defaults.headers["Authorization"]='Bearer ' + token;
} }
return { return {
token: token, token,
name:localStorage.getItem('name'), returnUrl: '',
domain:localStorage.getItem('domain'), LeftDrawer:false,
returnUrl: '' currentDomain: JSON.parse(localStorage.getItem('currentDomain')||"{}")
}
},
getters:{
toggleLeftDrawer():boolean{
return this.LeftDrawer;
} }
}, },
actions: { actions: {
toggleLeftMenu(){
this.LeftDrawer=!this.LeftDrawer;
},
async login(username:string, password:string) { async login(username:string, password:string) {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('username', username); params.append('username', username);
params.append('password', password); params.append('password', password);
try{ try{
const result = await api.post(`http://127.0.0.1:8000/api/token`,params); const result = await api.post(`api/token`,params);
console.info(result); console.info(result);
this.token =result.data.access_token; this.token =result.data.access_token;
this.name = result.data.user_name;
localStorage.setItem('token', result.data.access_token); localStorage.setItem('token', result.data.access_token);
localStorage.setItem('name', result.data.user_name);
// const config = {headers:{Authorization : 'Bearer ' + this.token}};
api.defaults.headers["Authorization"]='Bearer ' + this.token; api.defaults.headers["Authorization"]='Bearer ' + this.token;
const activedomain = await api.get(`http://127.0.0.1:8000/api/activedomain`); this.currentDomain=await this.getCurrentDomain();
this.domain = activedomain.data.name; localStorage.setItem('currentDomain',JSON.stringify(this.currentDomain));
localStorage.setItem('domain', activedomain.data.name); this.router.push(this.returnUrl || '/');
Router.push(this.returnUrl || '/');
return true; return true;
}catch(e) }catch(e)
{ {
@@ -42,19 +54,38 @@ export const useAuthStore = defineStore({
return false; return false;
} }
}, },
async getCurrentDomain():Promise<IDomainInfo>{
const activedomain = await api.get(`api/activedomain`);
return {
id:activedomain.data.id,
domainName:activedomain.data.name,
kintoneUrl:activedomain.data.url
}
},
async getUserDomains():Promise<IDomainInfo[]>{
const resp = await api.get(`api/domain`);
const domains =resp.data as any[];
return domains.map(data=>{
return {
id:data.id,
domainName:data.name,
kintoneUrl:data.url
}
});
},
logout() { logout() {
this.token = null; this.token = undefined;
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('name'); localStorage.removeItem('currentDomain');
localStorage.removeItem('domain'); router.push('/login');
Router.push('/login');
}, },
userdomain() { async setCurrentDomain(domain:IDomainInfo){
Router.push('/userdomain'); if(domain.id===this.currentDomain.id){
}, return;
changedomain(domain:string){ }
this.domain = domain; await api.put(`api/activedomain/${domain.id}`);
localStorage.setItem('domain', domain); this.currentDomain=domain;
localStorage.setItem('currentDomain',JSON.stringify(this.currentDomain));
} }
} }
}); });

View File

@@ -1,4 +1,10 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
export interface IDomainInfo{
id:number;
domainName:string;
kintoneUrl:string;
}
/** /**
* アプリ情報 * アプリ情報
*/ */
@@ -10,18 +16,44 @@ export interface AppInfo {
} }
/** /**
* アクションのプロパティ定義 * 属性項目情報
*/ */
export interface IActionProperty { export interface IProp{
component: string;
props: {
//プロパティ名 //プロパティ名
name: string; name: string;
//プロパティ表示名 //プロパティ表示名
displayName: string; displayName: string;
placeholder: string; placeholder: string;
//入力提示・説明
hint:string;
//関連属性リスト
connectProps:[{key:string,propName:string}]|undefined;
//プロパティ設定値 //プロパティ設定値
modelValue: any; modelValue: any;
}
/**
* アクションのプロパティ定義
*/
export interface IActionProperty {
component: string;
props: IProp;
}
/**
* 変数オブジェクト
*/
export interface IActionVariable{
actionName:string;
displayName:string;
name: {
name:string;
actionName:string;
displayName:string;
vars : {
vName:string;
logicalOperator:string;
field: object;
}[]
}; };
} }
/** /**
@@ -32,6 +64,7 @@ export interface IActionNode {
//アクション名 //アクション名
name: string; name: string;
title: string; title: string;
varName:IProp|undefined;
subTitle: string; subTitle: string;
inputPoint: string; inputPoint: string;
//出力ポイント(条件分岐以外未使用) //出力ポイント(条件分岐以外未使用)
@@ -54,10 +87,12 @@ export interface IActionFlow {
addNode(newNode: IActionNode, prevNode?: IActionNode, inputPoint?: string): IActionNode; addNode(newNode: IActionNode, prevNode?: IActionNode, inputPoint?: string): IActionNode;
removeNode(targetNode: IActionNode): boolean; removeNode(targetNode: IActionNode): boolean;
removeAllNext(targetNodeId: string): void; removeAllNext(targetNodeId: string): void;
getVarNames(currentNode:IActionNode):IActionVariable[];
findNodeById(id: string): IActionNode | undefined; findNodeById(id: string): IActionNode | undefined;
toJSON(): any; toJSON(): any;
getRoot(): IActionNode | undefined; getRoot(): IActionNode | undefined;
createNewId(): string; createNewId(): string;
getColumns(node:IActionNode):number;
} }
/** /**
@@ -65,18 +100,10 @@ export interface IActionFlow {
*/ */
class ActionProperty implements IActionProperty { class ActionProperty implements IActionProperty {
component: string; component: string;
props: { props: IProp;
// プロパティ名
name: string;
// プロパティ表示名
displayName: string;
placeholder: string;
// プロパティ設定値
modelValue: any;
};
static defaultProperty(): IActionProperty { static defaultProperty(): IActionProperty {
return new ActionProperty('InputText', 'displayName', '表示名', '表示を入力してください', ''); return new ActionProperty('InputText', 'displayName', '表示名', '表示を入力してください', '','');
}; };
constructor( constructor(
@@ -84,6 +111,7 @@ class ActionProperty implements IActionProperty {
name: string, name: string,
displayName: string, displayName: string,
placeholder: string, placeholder: string,
hint:string,
modelValue: any modelValue: any
) { ) {
this.component = component; this.component = component;
@@ -91,6 +119,8 @@ class ActionProperty implements IActionProperty {
name: name, name: name,
displayName: displayName, displayName: displayName,
placeholder: placeholder, placeholder: placeholder,
hint:hint,
connectProps:undefined,
modelValue: modelValue modelValue: modelValue
}; };
} }
@@ -110,6 +140,13 @@ export class ActionNode implements IActionNode {
get subTitle(): string { get subTitle(): string {
return this.name; return this.name;
}; };
//変数名
get varName():IProp|undefined{
const prop = this.actionProps.find((prop) => prop.props.name === "verName");
return prop?.props;
}
inputPoint: string; inputPoint: string;
//出力ポイント(条件分岐以外未使用) //出力ポイント(条件分岐以外未使用)
outputPoints: Array<string>; outputPoints: Array<string>;
@@ -157,6 +194,7 @@ export class RootAction implements IActionNode {
title: string; title: string;
subTitle: string; subTitle: string;
inputPoint: string; inputPoint: string;
varName: IProp | undefined=undefined;
//出力ポイント(条件分岐以外未使用) //出力ポイント(条件分岐以外未使用)
outputPoints: Array<string>; outputPoints: Array<string>;
isRoot: boolean; isRoot: boolean;
@@ -214,7 +252,7 @@ export class ActionFlow implements IActionFlow {
newNode.inputPoint = inputPoint; newNode.inputPoint = inputPoint;
} }
if (prevNode !== undefined) { if (prevNode !== undefined) {
this.connectNodes(prevNode, newNode, inputPoint || ''); this.resetNodeRelation(prevNode, newNode, inputPoint || '');
} else { } else {
prevNode = this.actionNodes[this.actionNodes.length - 1]; prevNode = this.actionNodes[this.actionNodes.length - 1];
this.connectNodes(prevNode, newNode, inputPoint || ''); this.connectNodes(prevNode, newNode, inputPoint || '');
@@ -258,9 +296,29 @@ export class ActionFlow implements IActionFlow {
return false; return false;
} }
for (const [, id] of targetNode.nextNodeIds) { for (const [, id] of targetNode.nextNodeIds) {
this.removeAllNext(id); this.removeAll(id);
this.removeFromActionNodes(id);
} }
targetNode.nextNodeIds.clear();
}
/***
* 目標ノードの次のノードを全部削除する
*/
removeAll(targetNodeId: string) {
if (!targetNodeId || targetNodeId === '') {
return;
}
const targetNode = this.findNodeById(targetNodeId);
if (!targetNode) {
return
}
if (targetNode.nextNodeIds.size == 0) {
return
}
for (const [, id] of targetNode.nextNodeIds) {
this.removeAll(id);
}
this.removeNode(targetNode);
} }
// 断开与前一个节点的连接 // 断开与前一个节点的连接
@@ -308,8 +366,7 @@ export class ActionFlow implements IActionFlow {
if (!nextNodeId) return; if (!nextNodeId) return;
const nextNode = this.findNodeById(nextNodeId); const nextNode = this.findNodeById(nextNodeId);
if (!nextNode) return; if (!nextNode) return;
nextNode.prevNodeId = prevNode.id; this.connectNodes(prevNode,nextNode,targetNode.inputPoint || '');
prevNode.nextNodeIds.set(targetNode.inputPoint || '', nextNodeId);
return; return;
} }
//二つ以上の場合 //二つ以上の場合
@@ -343,15 +400,16 @@ export class ActionFlow implements IActionFlow {
/** /**
* *
* @param prevNode ノードの接続をリセットする * @param prevNode ノード挿入時の接続をリセットする
* @param newNode * @param newNode
* @param inputPoint * @param inputPoint
*/ */
resetNodeRelation(prevNode: IActionNode, newNode: IActionNode, inputPoint?: string) { resetNodeRelation(prevNode: IActionNode, newNode: IActionNode, inputPoint?: string) {
// //元の次のノードを取得
const originalNextNodeId = prevNode.nextNodeIds.get(inputPoint || '');
prevNode.nextNodeIds.set(inputPoint || '', newNode.id); prevNode.nextNodeIds.set(inputPoint || '', newNode.id);
newNode.prevNodeId = prevNode.id; newNode.prevNodeId = prevNode.id;
const originalNextNodeId = prevNode.nextNodeIds.get(inputPoint || ''); newNode.inputPoint=inputPoint||'';
this.setNewNodeNextId(newNode, originalNextNodeId, inputPoint); this.setNewNodeNextId(newNode, originalNextNodeId, inputPoint);
} }
@@ -363,15 +421,18 @@ export class ActionFlow implements IActionFlow {
*/ */
private setNewNodeNextId(newNode: IActionNode, originalNextNodeId: string | undefined, inputPoint?: string) { private setNewNodeNextId(newNode: IActionNode, originalNextNodeId: string | undefined, inputPoint?: string) {
// 元の接続ノードが存在する // 元の接続ノードが存在する
if (originalNextNodeId) { if (!originalNextNodeId) {return;}
const originNextNode = this.findNodeById(originalNextNodeId);
if (!originNextNode) {return;}
// 新しいノードの outputPoints に該当 inputPointが存在するか場合をチェックする // 新しいノードの outputPoints に該当 inputPointが存在するか場合をチェックする
if (newNode.outputPoints.includes(inputPoint || '')) { if (newNode.outputPoints.includes(inputPoint || '')) {
newNode.nextNodeIds.set(inputPoint || '', originalNextNodeId); newNode.nextNodeIds.set(inputPoint || '', originalNextNodeId);
originNextNode.prevNodeId=newNode.id;
} else { } else {
// inputPointが存在しない場合、outputPointのポイントの任意ポートを選択する // inputPointが存在しない場合、outputPointのポイントの任意ポートを選択する
const alternativeOutputPoint = newNode.outputPoints.length > 0 ? newNode.outputPoints[0] : ''; const alternativeOutputPoint = newNode.outputPoints.length > 0 ? newNode.outputPoints[0] : '';
newNode.nextNodeIds.set(alternativeOutputPoint, originalNextNodeId); newNode.nextNodeIds.set(alternativeOutputPoint, originalNextNodeId);
} originNextNode.prevNodeId=newNode.id;
} }
} }
@@ -382,6 +443,42 @@ export class ActionFlow implements IActionFlow {
return this.actionNodes.find((node) => node.id === id); return this.actionNodes.find((node) => node.id === id);
} }
getVarNames(currentNode:IActionNode):IActionVariable[]{
let varNames:IActionVariable[]=[];
if(currentNode.prevNodeId!==undefined){
const prevNode=this.findNodeById(currentNode.prevNodeId);
if(prevNode!==undefined){
varNames = this.getPrevVarNames(prevNode);
}
}
return varNames;
}
getPrevVarNames(prevNode:IActionNode):IActionVariable[]{
let varNames:IActionVariable[]=[];
if(prevNode.varName!==undefined && prevNode.varName.modelValue){
if(prevNode.varName.modelValue ==='object'){
console.log(prevNode);
}
varNames.unshift({
actionName:prevNode.name,
displayName:prevNode.varName.displayName,
name:prevNode.varName.modelValue
});
}
if(prevNode.prevNodeId!==undefined){
const prevPrevNode=this.findNodeById(prevNode.prevNodeId);
if(prevPrevNode!==undefined){
const prevVars = this.getPrevVarNames(prevPrevNode);
varNames=[...prevVars,...varNames];
}
}
return varNames;
}
toJSON() { toJSON() {
return { return {
id: this.id, id: this.id,
@@ -389,12 +486,27 @@ export class ActionFlow implements IActionFlow {
const { nextNodeIds, ...rest } = node; const { nextNodeIds, ...rest } = node;
return { return {
...rest, ...rest,
nextNodeIds: Array.from(nextNodeIds.entries()) nextNodeIds: Object.fromEntries(nextNodeIds)
}; };
}) })
}; };
} }
getColumns(node:IActionNode):number{
let result= 1;
if(node.outputPoints && node.outputPoints.length>1){
result += node.outputPoints.length -1;
}
let nextNode;
for (const [key, id] of node.nextNodeIds) {
nextNode=this.findNodeById(id);
if(nextNode){
result +=this.getColumns(nextNode)-1;
}
}
return result;
}
getRoot(): IActionNode | undefined { getRoot(): IActionNode | undefined {
return this.actionNodes.find(node => node.isRoot) return this.actionNodes.find(node => node.isRoot)
} }
@@ -408,9 +520,9 @@ export class ActionFlow implements IActionFlow {
const parsedObject = JSON.parse(json); const parsedObject = JSON.parse(json);
const actionNodes = parsedObject.actionNodes.map((node: any) => { const actionNodes = parsedObject.actionNodes.map((node: any) => {
const nodeClass = !node.isRoot ? new ActionNode(node.name, node.title, node.inputPoint, node.outputPoint, node.actionProps) const nodeClass = !node.isRoot ? new ActionNode(node.name, node.title, node.inputPoint, node.outputPoints, node.actionProps)
: new RootAction(node.name, node.title, node.subTitle); : new RootAction(node.name, node.title, node.subTitle);
nodeClass.nextNodeIds = new Map(node.nextNodeIds); nodeClass.nextNodeIds = new Map<string,string>(Object.entries(node.nextNodeIds));
nodeClass.prevNodeId = node.prevNodeId; nodeClass.prevNodeId = node.prevNodeId;
nodeClass.id = node.id; nodeClass.id = node.id;
return nodeClass; return nodeClass;

View File

@@ -0,0 +1,383 @@
//ノード種別
export enum NodeType{
Root = 'root',
LogicGroup ='logicgroup',
Condition ='condition'
}
//ロジックオペレーター
export enum LogicalOperator{
AND = 'AND',
OR = 'OR'
}
//条件オペレーター
export enum Operator{
Equal = '=',
NotEqual='!=',
Greater = '>',
GreaterOrEqual = '>=',
Less = '<',
LessOrEqual = '<=',
Contains = 'contains',
NotContains = 'not contains',
StartWith = 'start With',
EndWith = 'end with',
NotStartWith = 'not start with',
NotEndWith = 'not end with'
}
// INode
export interface INode {
index:number;
type: NodeType;
header:string;
parent: INode | null;
logicalOperator:LogicalOperator
}
// ロジックノード
export class GroupNode implements INode {
index:number;
type: NodeType;
children: INode[];
parent: INode | null;
logicalOperator: LogicalOperator;
get label():string{
return this.logicalOperator;
}
get header():string{
return this.type===NodeType.Root?'root':'generic';
}
get expanded():boolean{
return this.children.length>0;
}
constructor(logicOp:LogicalOperator, parent: INode | null) {
this.index=0;
this.type = parent==null?NodeType.Root: NodeType.LogicGroup;
this.logicalOperator = logicOp;
this.parent=parent;
this.children=[];
}
static fromJSON(json: any, parent: INode | null = null): GroupNode {
const node = new GroupNode(json.logicalOperator, parent);
node.index=json.index;
node.children = json.children.map((childJson: any) => {
return childJson.type === NodeType.LogicGroup
? GroupNode.fromJSON(childJson, node)
: ConditionNode.fromJSON(childJson, node);
});
return node;
}
}
export type OperatorListItem = {
label: string;
value: string;
}
// 条件式ノード
export class ConditionNode implements INode {
index: number;
type: NodeType;
parent:INode;
get logicalOperator(): LogicalOperator{
return this.parent.logicalOperator;
};
object: any; // 比較元
operator: Operator | OperatorListItem; // 比較子
value: any;
get header():string{
return 'generic';
}
constructor(object: any, operator: Operator | OperatorListItem, value: any, parent: GroupNode) {
this.index=0;
this.type = NodeType.Condition;
this.object = object;
this.operator = operator;
this.value = value;
this.parent=parent;
}
static fromJSON(json: any, parent: GroupNode): ConditionNode {
const node= new ConditionNode(
json.object,
json.operator,
json.value,
parent
);
node.index=json.index;
return node;
}
}
// 条件式の管理クラス
export class ConditionTree {
root: GroupNode;
maxIndex:number;
queryString:string;
constructor() {
this.maxIndex=0;
this.root = new GroupNode(LogicalOperator.AND, null);
this.queryString='';
}
// ノード追加
addNode(parent: GroupNode, node: INode): void {
this.maxIndex++;
node.index=this.maxIndex;
parent.children.push(node);
}
// ノード削除
removeNode(node: INode): void {
if (node.parent === null) {
throw new Error('ルートノード削除できません');
} else {
const parent = node.parent as GroupNode;
const index = parent.children.indexOf(node);
if (index > -1) {
parent.children.splice(index, 1);
}
}
}
findByIndex(index:number):INode|undefined{
return this.findChildren(this.root,index);
}
findChildren(parent: GroupNode, index: number): INode | undefined {
if (parent.index === index) {
return parent;
}
for (const node of parent.children) {
if (node.index === index) {
return node;
}
if (node.type !== NodeType.Condition) {
const foundNode = this.findChildren(node as GroupNode, index);
if (foundNode) {
return foundNode;
}
}
}
return undefined;
}
getMaxIndex(node:INode):number{
let maxIndex:number=node.index;
if(node.type!==NodeType.Condition){
const groupNode = node as GroupNode;
groupNode.children.forEach((child)=>{
const childMax = this.getMaxIndex(child);
if(childMax>maxIndex){
maxIndex=childMax;
}
});
}
return maxIndex;
}
//条件式を表示する
buildConditionString(node:INode){
if (node.type !== NodeType.Condition) {
let conditionString = '(';
const groupNode = node as GroupNode;
for (let i = 0; i < groupNode.children.length; i++) {
const childConditionString = this.buildConditionString(groupNode.children[i]);
if (childConditionString !== '') {
conditionString += childConditionString;
if (i < groupNode.children.length - 1) {
conditionString += ` ${groupNode.logicalOperator} `;
}
}
}
conditionString += ')';
return conditionString;
} else {
const condNode=node as ConditionNode;
if (condNode.object && condNode.operator ) {
let value=condNode.value;
if(value && typeof value ==='object' && ('label' in value)){
value =condNode.value.label;
}
return `${typeof condNode.object.name === 'object' ? condNode.object.name.name : condNode.object.name} ${typeof condNode.operator === 'object' ? condNode.operator.label : condNode.operator} '${value}'`;
} else {
return '';
}
}
}
buildConditionQueryString(node:INode){
if (node.type !== NodeType.Condition) {
let conditionString = '';
if(node.type !== NodeType.Root){
conditionString = '(';
}
const groupNode = node as GroupNode;
for (let i = 0; i < groupNode.children.length; i++) {
const childConditionString = this.buildConditionQueryString(groupNode.children[i]);
if (childConditionString !== '') {
conditionString += childConditionString;
if (i < groupNode.children.length - 1) {
conditionString += ` ${groupNode.logicalOperator.toLowerCase()} `;
}
}
}
if(node.type !== NodeType.Root){
conditionString += ')';
}
return conditionString;
} else {
const condNode=node as ConditionNode;
if (condNode.object && condNode.operator ) {
let value=condNode.value;
if(value && typeof value ==='object' && ('label' in value)){
value =condNode.value.label;
}
return `${condNode.object.code} ${typeof condNode.operator === 'object' ? condNode.operator.value : condNode.operator} "${value}"`;
} else {
return '';
}
}
}
/**
*
* @param node ノード移動
* @param direction
* @returns
*/
moveNode(node:INode, direction: 'up' | 'down'): void {
if (!node || !node.parent) {
return;
}
const parent = node.parent as GroupNode;
const currentIndex = parent.children.findIndex(child => child === node);
if (currentIndex === -1) {
return;
}
const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
// 範囲外のインデックスの処理
if (newIndex >= 0 && newIndex < parent.children.length) {
// ノードの位置を入れ替える
[parent.children[currentIndex], parent.children[newIndex]] = [parent.children[newIndex], parent.children[currentIndex]];
return; // 範囲外なら移動しない
}else if(newIndex<0 && parent.parent){
this.removeNode(node);
const parentIndex = (parent.parent as GroupNode).children.findIndex(child => child === parent);
(parent.parent as GroupNode).children.splice(parentIndex, 0, node);
node.parent = parent.parent;
}
}
getGroups(parent:GroupNode):number[]{
const groups:number[]=[];
groups.push(parent.index);
parent.children.forEach((node)=>{
if(node.type!==NodeType.Condition){
groups.push(...this.getGroups(node as GroupNode));
}
});
return groups;
}
/**
* 条件ノードをグループ化
* @param nodes 結合ノードを選択する
* @param logicOp
* @returns
*/
createGroupNode(firstNode:INode,nodes: INode[], logicOp: LogicalOperator): GroupNode | null {
if (nodes.length === 0) {
return null;
}
// 最初のノードの親ノードを取得
const parent = firstNode.parent as GroupNode;
if (!parent) {
throw new Error('ルートノードをグループ化できません');
}
// 親ノードを取得
const filteredNodes = nodes.filter(node => node.parent === parent);
// 新しいグループノードを作成
const newGroup = new GroupNode(logicOp, parent);
this.maxIndex++;
newGroup.index = this.maxIndex;
// 新しいグループノードの挿入位置を取得
let firstNodeIndex = parent.children.length;
if (filteredNodes.length > 0) {
firstNodeIndex = parent.children.indexOf(filteredNodes[0]);
}
filteredNodes.forEach(node => {
// 元の親ノードから削除する
const nodeIndex = parent.children.indexOf(node);
parent.children.splice(nodeIndex, 1);
// 新しいグループに追加
node.parent = newGroup;
newGroup.children.push(node);
});
// 新しいGroupNodeを挿入する
parent.children.splice(firstNodeIndex, 0, newGroup);
return newGroup;
}
/**
* GroupNodeを解散する
* @param groupNode 対象グループノード
*/
dissolveGroupNode(groupNode: GroupNode): void {
if (groupNode.parent === null || groupNode.type !== NodeType.LogicGroup) {
throw new Error('ルートノードと非グループノードを解散することができません');
}
// 親ノードを取得
const parent = groupNode.parent as GroupNode;
const groupIndex = parent.children.indexOf(groupNode);
// 子ノードをリセットする
groupNode.children.forEach(child => {
child.parent = parent;
parent.children.splice(groupIndex, 0, child);
});
//グループノードを削除する
parent.children.splice(groupIndex + groupNode.children.length, 1);
}
// Jsonから復元
fromJson(jsonString: string): INode {
const json = JSON.parse(jsonString);
this.root = GroupNode.fromJSON(json) as GroupNode;
this.maxIndex=this.getMaxIndex(this.root);
return this.root;
}
toJson():string{
return JSON.stringify({queryString :this.queryString, ...this.root}, (key, value) => {
if (key === 'parent') {
return value ? value.type : null;
}
return value;
});
}
setQuery(queryString:string){
this.queryString=queryString;
}
}

View File

@@ -1,112 +1,341 @@
import { publicDecrypt } from 'crypto'; import { useFlowEditorStore } from 'src/stores/flowEditor';
import { IActionFlow } from './ActionTypes'; import { IActionFlow } from './ActionTypes';
export interface TreeNode { export interface IKintoneEventNode {
label: string; label: string;
header: string;
eventId: string;
parentId: string;
} }
export interface IKintoneEvent extends TreeNode { export interface IKintoneEvent extends IKintoneEventNode {
eventId: string;
hasFlow: boolean; hasFlow: boolean;
flowData?: IActionFlow; flowData?: IActionFlow;
} }
export interface IKintoneScreen extends TreeNode { export interface IKintoneEventGroup extends IKintoneEventNode {
label: string; events: IKintoneEventNode[];
events: IKintoneEvent[];
} }
export class kintoneEvent implements IKintoneEvent { export class kintoneEvent implements IKintoneEvent {
eventId: string; eventId: string;
parentId: string;
get hasFlow(): boolean { get hasFlow(): boolean {
return this.flowData!==undefined && this.flowData.actionNodes.length>1 return this.flowData !== undefined && this.flowData.actionNodes.length > 1;
}; }
flowData?: IActionFlow | undefined; flowData?: IActionFlow | undefined;
label: string; label: string;
constructor({eventId,label}:{eventId:string,label:string}){ header = 'EVENT';
constructor(label: string, eventId: string, parentId: string) {
this.eventId = eventId; this.eventId = eventId;
this.label = label; this.label = label;
this.parentId = parentId;
}
} }
export class kintoneEventGroup implements IKintoneEventGroup {
eventId: string;
parentId: string;
label: string;
events: IKintoneEventNode[];
get header(): string {
return 'EVENTGROUP';
}
constructor(
eventId: string,
label: string,
events: IKintoneEventNode[],
parentId: string
) {
this.eventId = eventId;
this.label = label;
this.events = events;
this.parentId = parentId;
}
}
export class kintoneEventForChange implements IKintoneEventGroup {
eventId: string;
parentId: string;
label: string;
events: IKintoneEventNode[];
get header(): string {
return 'CHANGE';
}
constructor(
eventId: string,
label: string,
events: IKintoneEventNode[],
parentId: string
) {
this.eventId = eventId;
this.label = label;
this.events = events;
this.parentId = parentId;
}
} }
export class KintoneEventManager { export class KintoneEventManager {
public screens: IKintoneScreen[]; public screens: IKintoneEventGroup[];
constructor(screens: IKintoneScreen[]) { constructor() {
this.screens = screens; this.screens = this.getKintoneEvents();
} }
public bindFlows(flows: IActionFlow[]) { public bindFlows(flows: IActionFlow[]) {
for (const screen of this.screens) { this.screens = this.getKintoneEvents();
screen.events.forEach((ev)=>ev.flowData=undefined);
}
for (const flow of flows) { for (const flow of flows) {
const eventId = flow.getRoot()?.name; const eventId = flow.getRoot()?.name;
if (eventId !== undefined) { if (eventId !== undefined) {
const event = this.findEventById(eventId); const eventNode = this.findEventById(eventId);
if(event!==null){ if (eventNode !== null && eventNode.header === 'EVENT') {
const event = eventNode as kintoneEvent;
event.flowData = flow; event.flowData = flow;
} else {
//EventGroupのIDを取得
const lastIndex = eventId.lastIndexOf('.');
const groupId = eventId.substring(0, lastIndex);
const eventNode = this.findEventById(groupId);
if (
eventNode &&
(eventNode.header === 'EVENTGROUP' || eventNode.header === 'CHANGE')
) {
const groupEvent = eventNode as kintoneEventGroup;
const newEvent = {
label: flow.getRoot()?.subTitle || '',
eventId: eventId,
parentId: groupId,
header: 'DELETABLE',
hasFlow: true,
flowData: flow,
};
groupEvent.events.push(newEvent);
}
} }
} }
} }
} }
public findEventById(eventId: string): IKintoneEvent | null { public findEventById(eventId: string): IKintoneEventNode | null {
const screen = this.findScreen(eventId);
if (screen) {
return screen;
}
for (const screen of this.screens) { for (const screen of this.screens) {
for (const event of screen.events) { for (const event of screen.events) {
if (event.eventId === eventId) { if (event.eventId === eventId) {
return event; return event;
} }
if (event.header === 'EVENTGROUP' || event.header === 'CHANGE') {
const eventGroup = event as IKintoneEventGroup;
const targetEvent = eventGroup.events.find((ev) => {
return ev.eventId === eventId;
});
if (targetEvent) {
return targetEvent;
}
}
} }
} }
return null; return null;
} }
public findScreen(eventId:string):IKintoneScreen|null{ public findScreen(eventId: string): IKintoneEventGroup | undefined {
for (const screen of this.screens) { return this.screens.find((screen) => screen.eventId == eventId);
if(screen.events.some((ev:IKintoneEvent)=>ev.eventId===eventId)){
return screen;
} }
public deleteEvent(
event: kintoneEvent,
store: ReturnType<typeof useFlowEditorStore>
) {
if (event.header !== 'DELETABLE') {
return;
} }
return null;
const parent = store.findEventById(event.parentId);
if (parent?.header !== 'CHANGE' && parent?.header !== 'EVENTGROUP') {
return;
}
const realParent = parent as kintoneEventForChange;
const index = realParent.events.findIndex(
(e) => e.eventId === event.eventId
);
if (index !== -1) {
realParent.events.splice(index, 1);
} }
} }
export const kintoneEvents:KintoneEventManager = new KintoneEventManager([ public getKintoneEvents(): IKintoneEventGroup[] {
{ return [
label:'レコード追加画面', new kintoneEventGroup(
events:[ 'app.record.create',
new kintoneEvent({label:'レコード追加画面を表示した後',eventId:'app.record.create.show'}), 'レコード追加画面',
new kintoneEvent({label:'保存をクリックしたとき',eventId:'app.record.create.submit'}), [
new kintoneEvent({label:'保存が成功したとき',eventId:'app.record.create.submit.success'}), new kintoneEvent(
new kintoneEvent({label:'フィールドの値を変更したとき',eventId:'app.record.create.change'}), 'レコード追加画面を表示した後',
] 'app.record.create.show',
}, 'app.record.create'
{ ),
label:'レコード詳細画面', new kintoneEvent(
events:[ '保存をクリックしたとき',
new kintoneEvent({label:'レコード詳細画面を表示した後',eventId:'app.record.detail.show'}), 'app.record.create.submit',
new kintoneEvent({label:'レコードを削除するとき',eventId:'app.record.detail.delete.submit'}), 'app.record.create'
new kintoneEvent({label:'プロセス管理のアクションを実行したとき',eventId:'app.record.detail.process.proceed'}), ),
] new kintoneEvent(
}, '保存が成功したとき',
{ 'app.record.create.submit.success',
label:'レコード編集画面', 'app.record.create'
events:[new kintoneEvent({label:'レコード編集画面を表示した後',eventId:'app.record.edit.show'}), ),
new kintoneEvent({label:'保存をクリックしたとき',eventId:'app.record.edit.submit'}), new kintoneEventForChange(
new kintoneEvent({label:'保存が成功したとき',eventId:'app.record.edit.submit.success'}), 'app.record.create.change',
new kintoneEvent({label:'フィールドの値を変更したとき',eventId:'app.record.edit.change'}), 'フィールドの値を変更したとき',
] [],
}, 'app.record.create'
{ ),
label:'レコード一覧画面', new kintoneEventGroup(
events:[ 'app.record.create.show.customButtonClick',
new kintoneEvent({label:'一覧画面を表示した後', eventId:'app.record.index.show'}), 'ボタンをクリックした時',
new kintoneEvent({label:'インライン編集を開始したとき',eventId:'app.record.index.edit.show'}), [],
new kintoneEvent({label:'インライン編集のフィールド値を変更したとき', eventId:'app.record.index.edit.change'}), 'app.record.create'
new kintoneEvent({label:'インライン編集の【保存】をクリックしたとき',eventId:'app.record.index.edit.submit'}), ),
new kintoneEvent({label:'インライン編集の保存が成功したとき', eventId:'app.record.index.edit.submit.success'}), ],
] ''
),
new kintoneEventGroup(
'app.record.detail',
'レコード詳細画面',
[
new kintoneEvent(
'レコード詳細画面を表示した後',
'app.record.detail.show',
'app.record.detail'
),
new kintoneEvent(
'レコードを削除するとき',
'app.record.detail.delete.submit',
'app.record.detail'
),
new kintoneEvent(
'プロセス管理のアクションを実行したとき',
'app.record.detail.process.proceed',
'app.record.detail'
),
new kintoneEventGroup(
'app.record.detail.show.customButtonClick',
'ボタンをクリックした時',
[],
'app.record.detail'
),
],
''
),
new kintoneEventGroup(
'app.record.edit',
'レコード編集画面',
[
new kintoneEvent(
'レコード編集画面を表示した後',
'app.record.edit.show',
'app.record.edit'
),
new kintoneEvent(
'保存をクリックしたとき',
'app.record.edit.submit',
'app.record.edit'
),
new kintoneEvent(
'保存が成功したとき',
'app.record.edit.submit.success',
'app.record.edit'
),
new kintoneEventForChange(
'app.record.edit.change',
'フィールドの値を変更したとき',
[],
'app.record.edit'
),
new kintoneEventGroup(
'app.record.edit.show.customButtonClick',
'ボタンをクリックした時',
[],
'app.record.edit'
),
],
''
),
new kintoneEventGroup(
'app.record.index',
'レコード一覧画面',
[
new kintoneEvent(
'一覧画面を表示した後',
'app.record.index.show',
'app.record.index'
),
new kintoneEvent(
'インライン編集を開始したとき',
'app.record.index.edit.show',
'app.record.index'
),
new kintoneEvent(
'インライン編集の【保存】をクリックしたとき',
'app.record.index.edit.submit',
'app.record.index'
),
new kintoneEvent(
'インライン編集の保存が成功したとき',
'app.record.index.edit.submit.success',
'app.record.index'
),
new kintoneEventForChange(
'app.record.index.edit.change',
'インライン編集のフィールド値を変更したとき',
[],
'app.record.index'
),
new kintoneEventGroup(
'app.record.detail.show.customButtonClick',
'ボタンをクリックした時',
[],
'app.record.index'
),
],
''
),
];
} }
]); }
// export const kintoneEvents:KintoneEventManager = new KintoneEventManager([
// new kintoneEventGroup("app.record.create","レコード追加画面",[
// new kintoneEvent('レコード追加画面を表示した後','app.record.create.show',"app.record.create"),
// new kintoneEvent('保存をクリックしたとき','app.record.create.submit',"app.record.create"),
// new kintoneEvent('保存が成功したとき','app.record.create.submit.success',"app.record.create"),
// new kintoneEventForChange('app.record.create.change','フィールドの値を変更したとき',[],"app.record.create"),
// new kintoneEventGroup('app.record.create.customButtonClick','ボタンをクリックした時',[],"app.record.create")
// ],""),
// new kintoneEventGroup("app.record.detail","レコード詳細画面",[
// new kintoneEvent('レコード詳細画面を表示した後','app.record.detail.show',"app.record.detail"),
// new kintoneEvent('レコードを削除するとき','app.record.detail.delete.submit',"app.record.detail"),
// new kintoneEvent('プロセス管理のアクションを実行したとき','app.record.detail.process.proceed',"app.record.detail"),
// new kintoneEventGroup('app.record.detail.customButtonClick','ボタンをクリックした時',[],"app.record.detail"),
// ],""),
// new kintoneEventGroup("app.record.edit","レコード編集画面",[
// new kintoneEvent('レコード編集画面を表示した後','app.record.edit.show',"app.record.edit"),
// new kintoneEvent('保存をクリックしたとき','app.record.edit.submit',"app.record.edit"),
// new kintoneEvent('保存が成功したとき','app.record.edit.submit.success',"app.record.edit"),
// new kintoneEventForChange('app.record.edit.change','フィールドの値を変更したとき',[],"app.record.edit"),
// new kintoneEventGroup('app.record.edit.customButtonClick','ボタンをクリックした時',[],"app.record.edit"),
// ],""),
// new kintoneEventGroup("app.record.index","レコード一覧画面",[
// new kintoneEvent('一覧画面を表示した後', 'app.record.index.show',"app.record.index"),
// new kintoneEvent('インライン編集を開始したとき','app.record.index.edit.show',"app.record.index"),
// new kintoneEvent('インライン編集の【保存】をクリックしたとき','app.record.index.edit.submit',"app.record.index"),
// new kintoneEvent('インライン編集の保存が成功したとき', 'app.record.index.edit.submit.success',"app.record.index"),
// new kintoneEventForChange('app.record.index.edit.change','インライン編集のフィールド値を変更したとき' ,[],"app.record.index"),
// new kintoneEventGroup('app.record.detail.customButtonClick','ボタンをクリックした時',[],"app.record.index"),
// ],"")
// ]);

View File

@@ -0,0 +1,2 @@
VITE_SOURCE_MAP = inline
VITE_PORT = 4173

View File

@@ -0,0 +1,2 @@
VITE_SOURCE_MAP = false
VITE_PORT = 4173

View File

@@ -12,6 +12,8 @@
}, },
"devDependencies": { "devDependencies": {
"@types/jquery": "^3.5.24", "@types/jquery": "^3.5.24",
"@types/node": "^20.8.9",
"sass": "^1.69.5",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.4.5" "vite": "^4.4.5"
} }
@@ -377,12 +379,82 @@
"@types/sizzle": "*" "@types/sizzle": "*"
} }
}, },
"node_modules/@types/node": {
"version": "20.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
"integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/sizzle": { "node_modules/@types/sizzle": {
"version": "2.3.5", "version": "2.3.5",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz",
"integrity": "sha512-tAe4Q+OLFOA/AMD+0lq8ovp8t3ysxAOeaScnfNdZpUxaGl51ZMDEITxkvFl1STudQ58mz6gzVGl9VhMKhwRnZQ==", "integrity": "sha512-tAe4Q+OLFOA/AMD+0lq8ovp8t3ysxAOeaScnfNdZpUxaGl51ZMDEITxkvFl1STudQ58mz6gzVGl9VhMKhwRnZQ==",
"dev": true "dev": true
}, },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.18.20", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
@@ -420,6 +492,18 @@
"@esbuild/win32-x64": "0.18.20" "@esbuild/win32-x64": "0.18.20"
} }
}, },
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -434,6 +518,66 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/immutable": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz",
"integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==",
"dev": true
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/jquery": { "node_modules/jquery": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
@@ -457,12 +601,33 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true "dev": true
}, },
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -491,6 +656,18 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.29.4", "version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
@@ -507,6 +684,23 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/sass": {
"version": "1.69.7",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz",
"integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@@ -516,6 +710,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
@@ -529,6 +735,12 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",

View File

@@ -4,14 +4,28 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "run-p watch server ngrok",
"build": "tsc && vite build && xcopy dist\\*.js ..\\..\\backend\\Temp\\ /E /I /Y", "watch": "vite build --watch --mode dev",
"preview": "vite preview" "server": "vite dev --mode dev",
"ngrok": "ngrok http 4173",
"build": "run-s b:production copy:windows",
"build:dev": "run-s b:dev copy:windows",
"build:linux": "run-s b:production copy:linux",
"build:linux-dev": "run-s b:dev copy:linux",
"b:production": "vite build --mode production",
"b:dev": "vite build --mode dev",
"copy:windows": "xcopy dist\\*.js ..\\..\\backend\\Temp\\ /E /I /Y",
"copy:linux": "cp -ur dist/*.js ../../backend/Temp"
}, },
"devDependencies": { "devDependencies": {
"@types/jquery": "^3.5.24", "@types/jquery": "^3.5.24",
"@types/node": "^20.8.9",
"npm-run-all2": "^6.2.0",
"sass": "^1.69.5",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.4.5" "vite": "^4.4.5",
"vite-plugin-checker": "^0.6.4",
"vite-plugin-lib-inject-css": "^2.1.1"
}, },
"dependencies": { "dependencies": {
"jquery": "^3.7.1" "jquery": "^3.7.1"

View File

@@ -0,0 +1,295 @@
# kintone自動化開発ツールのアクションのアドイン開発手順
## 1. アクションの登録
アクションプラグインをシステムに登録するためには、以下の情報をデータベースの`action`表に挿入する必要があります。
|列名 | 項目 | 説明 |
|----- |-------------|-------------------------------------------|
|name | 名前 | アクションプラグイン名(ユニークな名前が必要) |
|title |タイトル | タイトル 20文字以内 |
|subtitle|サブタイトル | サブタイトル |
|outputpoint|出力ポイント | 出力値に分岐がある場合の接続点 |
|property|プロパティ | アクションプラグインの属性json形式 |
### 登録の例
以下は「表示/非表示」アクションプラグインを登録する例です。
- name: "表示/非表示"
- title: "指定項目の表示・非表示を設定する"
- subtitle: "表示/非表示"
- outputpoint: "[]"
- property:
```json
[
{
"component": "FieldInput",
"props": {
"displayName": "フィールド",
"modelValue": {},
"name": "field",
"placeholder": "対象項目を選択してください"
}
},
{
"component": "SelectBox",
"props": {
"displayName": "表示/非表示",
"options": ["表示", "非表示"],
"modelValue": "",
"name": "show",
"placeholder": ""
}
},
{
"component": "ConditionInput",
"props": {
"displayName": "条件",
"modelValue": "",
"name": "condition",
"placeholder": "条件式を設定してください"
}
}
]
```
### プロパティ属性設定画面
![プロパティ属性設定画面](../../document/action-property.png)
### 属性UIコンポーネントの共通属性
| 属性 | 設定値の例 | 説明 |
|-------------|--------------------|----------------------------------------------------------------------------|
| component | InputText | コンポーネントの種類を示しており、この場合は選択リストを意味します。<br>使用可能なコンポーネントを参照|
| displayName | 表示/非表示 | ユーザーに対して表示されるコンポーネントの名前です。 |
| options | ["表示", "非表示"] | ユーザーが選択できるオプションの配列です。<br>SelectBoxのみ使用可能 |
| modelValue | 空文字 | コンポーネントの初期値を設定します。<br>初期設定ないの場合は空文字で設定する。
| name | field | 属性の設定値の名前です。 |
| placeholder | 対象項目を選択してください| 入力フィールドに表示されるプレースホルダーのテキストです。この場合は設定されていません。 |
| hint | 説明文| 長い説明文を設定することが可能です。markdown形式サポート予定、現在HTML可能 |
| selectType |`single` or `multiple`| フィールド選択・他のアプリのフィールド選択の選択モードを設定する |
### 使用可能なコンポーネント
| No. | コンポーネント名 | コンポーネントタイプ | 説明 |
|-----|------------------|------------------|-----------------------------------------|
| 1 | テキストボックス | InputText | 一行のテキスト入力が可能なフィールドです。 |
| 2 | テキストボックス(改行可能) | MuiltInputText | 複数行のテキスト入力が可能なテキストエリアです。 |
| 3 | 日付 | DatePicker | 日付を選択するためのカレンダーコンポーネントです。 |
| 4 | フィールド選択 | FieldInput | システムのフィールドを選択するための入力コンポーネントです。 |
| 5 | 選択リスト | SelectBox | 複数のオプションから選択するためのドロップダウンリストです。 |
| 6 | 条件式設定 | ConditionInput | 条件式やロジックを入力するためのコンポーネントです。 |
| 7 | イベント設定 |EventSetter | ボタンイベント設定のコンポーネントです。 |
| 8 | 色選択 | ColorPicker | 色を設定する(追加予定中) |
| 9 | 他のアプリのフィールド選択 | AppFieldPicker | 他のアプリのフィールドを選択する(追加予定中) |
| 10 |ユーザー選択 | UserPicker | ユーザーを選択する(追加予定中) |
## 2.アクションアドインの開発
### 1. Action pluginファイルの追加
アクションプラグインを作成するためには、以下のディレクトリ構造に`TypeScript`ファイルを追加します。
```
KintoneAppBuilder
└─ plugin
└─ kintone-addins
└─ src
└─ actions
└─ your-action.ts // ここにアクションプラグインのtsファイルを追加
```
### 2. アクションクラスの実装手順
`IAction` インターフェースに従ってアクションクラスを実装します。
```typescript
/**
* アクションのインターフェース
*/
export interface IAction{
// アクションのユニークな名前
name:string;
//属性設定情報
actionProps: Array<IActionProperty>;
//アクションのプロセス実行関数
process(prop:IActionNode,event:any,context:IContext):Promise<IActionResult>;
//アクションの登録関数
register():void;
}
```
#### サンプルコード
```ts
// アクションの属性定義
interface IShownProps{
field:IField;
show:string;
condition:string;
}
// 表示/非表示アクション
export class FieldShownAction implements IAction{
name: string;
actionProps: IActionProperty[];
props:IShownProps;
constructor(){
this.name="表示/非表示"; // DBに登録したアクション名一致する必要があり
this.actionProps=[];
//プロパティ属性の初期化
this.props={
field:{code:''},
show:'',
condition:''
}
//アクションの自動登録
this.register();
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
// ... (アクション処理の実装)
}
register(): void {
actionAddins[this.name]=this;
}
}
new FieldShownAction();
```
アクションプラグインを実装するには、`IAction`インターフェースの定義に従って、必要なメソッドとプロパティをクラスに実装します。
以下に、`IAction`インターフェースを用いて`表示/非表示`アクションを実装する手順を説明します。
1. **アクションの属性定義**
2. **アクションクラスの作成**:
- `IAction`インターフェースを実装する新しいクラス`FieldShownAction`を作成します。
3. **コンストラクタの定義**:
- アクション名や初期プロパティを設定します。
- このクラスのインスタンスが作成された際に、自動的にアクションが登録されるように、コンストラクタ内で`register`メソッドを呼び出します。
4. **プロセス実行関数の実装** (`process`):
- `process`メソッドは、アクションの主要なロジックを含み、アクションの実行時に呼び出されます。
- * 以下は`process`関数のパラメータとその用途を説明します。
| パラメータ名 | 型 | 用途 |
|----------|----------------|------------------------------------------------------------------------------------------------|
| actionNode | `IActionNode` | Kintone自動化ツールのアクションの設定やプロパティ情報を保持します。 |
| event |kintoneのイベント情報| レコードやエラー制御で使用します |
| context | `IContext` | 現在のレコード情報や変数など、実行に必要なデータへのアクセスを提供します。 |
- このメソッド内で、アクションに必要な処理を行います。
- 1. アクションプロパティの取得:
`Kitone自動化ツール`を設定したプロパティの値を取得する
```ts
//プロパティ設定を取得する
this.actionProps=actionNode.actionProps;
//プロパティ設定のデータ型は必要な情報が含めますか
if (!('field' in actionNode.ActionValue) && !('show' in actionNode.ActionValue)) {
return result
}
//既定のプロパティのインターフェースへ変換する
this.props = actionNode.ActionValue as IShownProps;
```
- 2. 条件式の評価
getConditionResult関数を呼び出して条件式を評価します。この関数は、現在のコンテキストに基づいて条件式が真か偽かを返します。
```ts
//条件式の計算結果を取得
const conditionResult = this.getConditionResult(context);
/**
*
* @param context 条件式を実行する
* @returns
*/
getConditionResult(context:any):boolean{
//プロパティ`condition`から条件ツリーを取得する
const tree =this.getCondition(this.props.condition);
if(!tree){
//条件を設定されていません
return true;
}
return tree.evaluate(tree.root,context);
}
```
- 3. Kintone APIを使用して、フィールドの表示非表示の制御
```ts
//条件式の計算結果を取得
const conditionResult = this.getConditionResult(context);
if(conditionResult){
if(this.props.show==='表示'){
kintone.app.record.setFieldShown(this.props.field.code,true);
}else if (this.props.show==='非表示'){
kintone.app.record.setFieldShown(this.props.field.code,false);
}
}
```
5. **登録関数の実装** (`register`):
- アクションをアドインシステムに登録するための`register`メソッドを実装します。
6. **アクションプロセス`ActionProcess`に参照追加**
```ts
import { actionAddins } from "../actions";
import '../actions/must-input';
import '../actions/auto-numbering';
import '../actions/field-shown';
import '../actions/your-action'; //ここに新規のアクションの参照を追加する
...
```
### 3. デプロイ
1. **プロジェクトをビルドする**
- 本番環境にデプロイする場合
```bash
cd plug\kintone-addins\
npm install
npm run build
```
- 開発環境にデプロイする場合(ソースマップ出力ます)
```bash
cd plug\kintone-addins\
npm install
npm run build:dev
```
2. **Azureにデプロイする**
- Azure 拡張機能のインストール:
VSCode の拡張機能ペインで`Azure Tools`を検索し、インストールします。
- Azure にログイン:
- Azure Account 拡張機能を使用して Azure にログインします。
- Azure へのデプロイ:
- 「Deploy to Web App」オプションを使用し、デプロイするファイルやフォルダを指定します。
- デプロイの確認:
- Azure App Service 拡張機能でデプロイが完了したことを確認します。
- ka-addin の URL にアクセスしてアプリケーションが正常に動作しているか確認します。
3. **ローカルでプラグインをテストする**
1. kintone-addinsをPreviewで起動する
```bash
yarn build:dev
yarn preview
#またはyarn devは yarn build:dev + yarn preview と同じです
yarn dev
```
2. **ngrokをインストールする**
1. [ngrok の公式ウェブサイト](https://ngrok.com/)にアクセスします。
2. 「Sign up」をクリックしてアカウントを登録するか、既存のアカウントにログインします。
3. 登録またはログイン後、ダッシュボードに進み、ダウンロードリンクが表示されます。操作システムWindows、macOS、Linuxに応じて、適切なバージョンを選択してダウンロードします。
4. ダウンロード後、`.zip` ファイルを解凍します。
5. ngrok を設定する
1. ngrok ダッシュボードにログインし、ホームページで認証トークンを見つけます。
2. ターミナル(またはコマンドプロンプト)を開き、以下のコマンドを実行して認証トークンを追加します:
```bash
ngrok config add-authtoken <認証トークン>
```
6. ngrok を起動する
```bash
ngrok https http://localhost:4173/
```

View File

@@ -0,0 +1,24 @@
.alc-button-normal {
display: inline-block;
box-sizing: border-box;
padding: 0 16px;
margin-left: 16px;
margin-top: 8px;
min-width: 100px;
outline: none;
border: 1px solid #e3e7e8;
background-color: #f7f9fa;
box-shadow: 1px 1px 1px #fff inset;
color: #3498db;
text-align: center;
line-height: 32px;
}
.alc-button-normal:hover {
background-color: #c8d6dd;
box-shadow: none;
cursor: pointer;
}
.alc-button-normal:active {
color: #f7f9fa;
background-color: #54b8eb;
}

View File

@@ -0,0 +1,149 @@
import { actionAddins } from ".";
import { IField, IAction,IActionResult, IActionNode, IActionProperty, IContext } from "../types/ActionTypes";
import { Formatter } from "../util/format";
import "./auto-numbering.css";
declare global {
interface Window { $format: any; }
}
interface IAutoNumberingProps{
//文書番号を格納する
field:IField;
format:string;
prefix:string;
suffix:string;
verName:string;
}
export class AutoNumbering implements IAction{
name: string;
actionProps: IActionProperty[];
props:IAutoNumberingProps;
constructor(){
this.name="自動採番する";
this.actionProps=[];
this.register();
this.props={
field:{code:''},
format:'',
prefix:'',
suffix:'',
verName:''
}
globalThis.window.$format=this.format;
this.register();
}
/**
* アクションの処理を実装する
* @param actionNode アクションノード
* @param event Kintoneのイベント
* @param context コンテキスト(レコード、変数情報を持っている)
* @returns
*/
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:false,
result:false
};
try{
this.actionProps=actionNode.actionProps;
if (!('field' in actionNode.ActionValue) && !('format' in actionNode.ActionValue)) {
return result
}
this.props = actionNode.ActionValue as IAutoNumberingProps;
const record = event.record;
const docNum = await this.createNumber(this.props);
record[this.props.field.code].value=docNum;
//変数設定
if(this.props.verName){
context.variables[this.props.verName]=docNum;
}
result= {
canNext:true,
result:true
}
return result;
}catch(error){
console.error(error);
event.error="処理中異常が発生しました。";
return {
canNext:false,
result:false
}
}
}
execTemplate(template:string):string{
if(template===undefined) return '';
const regex = /\$\{([^}]+)\}/g;
return template.replace(regex,(match,expr)=>{
return this.execEval(match,expr);
});
}
execEval(match:string,expr:string):string{
console.log(match);
// @ts-ignore
return eval(expr);
}
format(pattern:string):string{
const now=new Date();
return Formatter.dateFormat(now, pattern);
}
async createNumber(props:IAutoNumberingProps){
let number :string='';
let prefix:string='';
let suffix:string='';
let no=1;
try{
no = await this.fetchNo();
}catch(error){
console.log(error);
}
if(props.format!==undefined && props.format!==''){
number=Formatter.numberFormat(no, props.format);
}else{
number=no.toString(10);
}
if(props.prefix!==undefined && props.prefix!==''){
prefix=this.execTemplate(props.prefix);
}
if(props.suffix!==undefined && props.suffix!==''){
suffix=this.execTemplate(props.suffix);
}
return `${prefix}${number}${suffix}`;
}
async fetchNo():Promise<number>{
let recNo=1;
return await new kintone.Promise<number>((resolve,reject)=>{
const appurl = kintone.api.url('/k/v1/records',true);
const params={
app:kintone.app.getId(),
fields:['$id'],
query:'limit 1'
};
return kintone.api(appurl,'GET',params).then((resp)=>{
if(resp.records[0]!==null){
recNo = parseInt(resp.records[0].$id.value,10)+1;
}
resolve(recNo);
}).catch((error)=>{
reject(error);
});
});
}
register(): void {
actionAddins[this.name]=this;
}
}
new AutoNumbering();

View File

@@ -0,0 +1,86 @@
import { actionAddins } from ".";
import $ from 'jquery';
import { IAction, IActionProperty, IActionNode, IActionResult } from "../types/ActionTypes";
import "./button-add.css";
/**
* ボタン配置属性定義
*/
interface IButtonAddProps {
//ボタン表示名
buttonName: string;
//配置位置
position: string;
//イベント名
eventName:string
}
export class ButtonAddAction implements IAction {
name: string;
actionProps: IActionProperty[];
props: IButtonAddProps;
constructor() {
this.name = "ボタンの配置";
this.actionProps = [];
this.props = {
buttonName: '',
position: '',
eventName:''
}
this.register();
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode: IActionNode, event: any): Promise<IActionResult> {
let result = {
canNext: true,
result: false
};
try {
this.actionProps = actionNode.actionProps;
if (!('buttonName' in actionNode.ActionValue) && !('position' in actionNode.ActionValue)) {
return result
}
this.props = actionNode.ActionValue as IButtonAddProps;
//ボタンを配置する
const menuSpace = kintone.app.record.getHeaderMenuSpaceElement();
if(!menuSpace) return result;
if($("style#alc-button-add").length===0){
const css=`
`;
const style = $("<style id='alc-button-add'>/<style>");
style.text(css);
$("head").append(style);
}
const button =$(`<button id='${this.props.eventName}' class='alc-button-normal' >${this.props.buttonName}</button>`);
if(this.props.position==="一番左に追加する"){
$(menuSpace).prepend(button);
}else{
$(menuSpace).append(button);
}
const clickEventName = `${event.type}.customButtonClick.${this.props.eventName}`;
button.on("click",()=>{
$(document).trigger(clickEventName,event);
});
return result;
} catch (error) {
event.error = error;
console.error(error);
result.canNext = false;
return result;
}
}
register(): void {
actionAddins[this.name] = this;
}
}
new ButtonAddAction();

View File

@@ -0,0 +1,109 @@
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IContext } from "../types/ActionTypes";
import { ConditionTree } from '../types/Conditions';
/**
* アクションの属性定義
*/
interface ICondition{
condition:string;
verName:string;
}
/**
* 条件分岐アクション
*/
export class ConditionAction implements IAction{
name: string;
actionProps: IActionProperty[];
props:ICondition;
constructor(){
this.name="条件式";
this.actionProps=[];
this.props={
condition:'',
verName:''
}
//アクションを登録する
this.register();
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:true,
result:''
};
try{
//属性設定を取得する
this.actionProps=actionNode.actionProps;
if (!('condition' in actionNode.ActionValue) && !('verName' in actionNode.ActionValue)) {
return result
}
this.props = actionNode.ActionValue as ICondition;
//条件式の計算結果を取得
const conditionResult = this.getConditionResult(context);
console.log("条件計算結果:",conditionResult);
if(conditionResult){
result= {
canNext:true,
result:'はい'
}
}else{
result= {
canNext:true,
result:'いいえ'
}
}
if(this.props.verName){
context.variables[this.props.verName]=result.result;
}
return result;
}catch(error){
event.error=error;
console.error(error);
result.canNext=false;
return result;
}
}
/**
*
* @param context 条件式を実行する
* @returns
*/
getConditionResult(context:any):boolean{
const tree =this.getCondition(this.props.condition);
if(!tree){
//条件を設定されていません
return true;
}
return tree.evaluate(tree.root,context);
}
/**
* @param condition 条件式ツリーを取得する
* @returns
*/
getCondition(condition:string):ConditionTree|null{
try{
const tree = new ConditionTree();
tree.fromJson(condition);
if(tree.getConditions(tree.root).length>0){
return tree;
}else{
return null;
}
}catch(error){
return null;
}
}
register(): void {
actionAddins[this.name]=this;
}
}
new ConditionAction();

View File

@@ -0,0 +1,182 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
} from "../types/ActionTypes";
import { actionAddins } from ".";
export type IApp = {
id: string;
name: string;
};
export type IField = {
name: string;
code: string;
type: string;
};
export type IAppFields = {
app?: IApp;
fields: IField[];
};
type ValueType = {
id: string;
from: {
objectType: "variable" | "field";
name: { name: string };
code: string;
};
to: IAppFields & {
isDialogVisible: boolean;
};
};
type Props = { app: IApp; field: ValueType[] };
export class DataMappingAction implements IAction {
name: string;
actionProps: IActionProperty[];
dataMappingProps: Props;
constructor() {
this.name = "DataMapping";
this.actionProps = [];
this.dataMappingProps = {} as Props;
this.register();
}
async process(
prop: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.initActionProps(prop);
this.initTypedActionProps();
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
for (const item of this.dataMappingProps.field) {
if (item.from.objectType === "variable") {
if (
item.from.name.name &&
item.to.app &&
item.to.fields &&
item.to.fields.length > 0
) {
const value = getValueByPath(
context.variables,
item.from.name.name
);
if (value) {
await kintone.api(
kintone.api.url("/k/v1/record.json", true),
"POST",
{
app: item.to.app.id,
record: {
[item.to.fields[0].code]: {
value: value,
},
},
}
);
}
}
} else if (item.from.objectType === "field") {
if (
item.from.code &&
item.to.app &&
item.to.fields &&
item.to.fields.length > 0
) {
const value = await selectData(
item.to.app.id,
item.to.fields[0].code
);
if (value && value.type === context.record[item.from.code].type) {
await kintone.api(
kintone.api.url("/k/v1/records.json", true),
"POST",
{
app: item.to.app.id,
records: value.value.map((v) => ({
[item.to.fields[0].code]: {
value: v,
},
})),
}
);
}
}
}
}
} catch (error) {
console.error("DataMappingAction error", error);
result.canNext = false;
}
console.log("dataMappingProps", this.dataMappingProps);
return result;
}
private initActionProps(nodes: IActionNode) {
this.actionProps = nodes.actionProps;
}
private initTypedActionProps() {
for (const action of this.actionProps) {
if (action.component === "DataMapping") {
this.dataMappingProps.field = action.props.modelValue as ValueType[];
} else if (action.component === "AppSelect") {
this.dataMappingProps.app = action.props.modelValue.app as IApp;
}
}
}
register(): void {
actionAddins[this.name] = this;
}
}
new DataMappingAction();
const getValueByPath = (obj: any, path: string) => {
return path.split(".").reduce((o, k) => (o || {})[k], obj);
};
type Resp = { records: RespRecordType[] };
type RespRecordType = {
[key: string]: {
type: string;
value: any;
};
};
type Result = {
type: string;
value: any[];
};
const selectData = async (appid: string, field: string): Promise<Result> => {
return kintone
.api(kintone.api.url("/k/v1/records", true), "GET", {
app: appid ?? kintone.app.getId(),
fields: [field],
})
.then((resp: Resp) => {
const result: Result = { type: "", value: [] };
resp.records.forEach((element) => {
for (const [key, value] of Object.entries(element)) {
if (result.type === "") {
result.type = value.type;
}
result.value.push(value.value);
}
});
return result;
});
};

View File

@@ -0,0 +1,280 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
} from "../types/ActionTypes";
import { actionAddins } from ".";
type DataProcessingProps = {
app: {
id: string;
name: string;
};
conditionsQuery: string;
propcessing: {
varRootName: string;
fields: Field[];
};
};
type Field = {
name: string;
code: string;
type: string;
varName: string;
operator: string;
};
export class DataProcessingAction implements IAction {
name: string;
actionProps: IActionProperty[];
dataProcessingProps: DataProcessingProps | null;
constructor() {
this.name = "データ処理";
this.actionProps = [];
this.dataProcessingProps = null;
this.register();
}
async process(
nodes: IActionNode,event: any,context: IContext
): Promise<IActionResult> {
this.initActionProps(nodes);
this.initTypedActionProps();
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
if (!this.dataProcessingProps) {
return result;
}
const data = await selectData(this.dataProcessingProps.conditionsQuery);
console.log("data ", data);
context.variables[this.dataProcessingProps.propcessing.varRootName] =
this.dataProcessingProps.propcessing.fields.reduce((acc, f) => {
const v = calc(f, data);
if (v) {
acc[f.varName] = calc(f, data);
}
return acc;
}, {} as Var);
console.log("context ", context);
return result;
} catch (error) {
console.error(error);
event.error=error;
return result;
}
}
register(): void {
actionAddins[this.name] = this;
}
private initActionProps(nodes: IActionNode) {
this.actionProps = nodes.actionProps;
}
private initTypedActionProps() {
this.dataProcessingProps = {
app: {
id: "",
name: "",
},
conditionsQuery: "",
propcessing: {
varRootName: "",
fields: [],
},
};
for (const action of this.actionProps) {
if (action.component === "AppFieldSelect") {
this.dataProcessingProps.app.id = action.props.modelValue.app.id;
this.dataProcessingProps.app.name = action.props.modelValue.app.name;
} else if (action.component === "DataProcessing") {
this.dataProcessingProps.propcessing.varRootName =
action.props.modelValue.name;
for (const f of action.props.modelValue.vars) {
this.dataProcessingProps.propcessing.fields.push({
name: f.field.name,
code: f.field.code,
type: f.field.type,
varName: f.vName,
operator: f.logicalOperator.operator,
});
}
} else if (action.component === "ConditionInput") {
this.dataProcessingProps.conditionsQuery = JSON.parse(
action.props.modelValue
).queryString;
}
}
}
}
new DataProcessingAction();
const selectData = async (query?: string) => {
return kintone
.api(kintone.api.url("/k/v1/records", true), "GET", {
app: kintone.app.getId(),
query: query,
})
.then((resp: Resp) => {
const result: Result = {};
resp.records.forEach((element) => {
for (const [key, value] of Object.entries(element)) {
if (!result[key]) {
result[key] = { type: value.type, value: [] };
}
result[key].value.push(value.value);
}
});
return result;
});
};
type Resp = { records: RespRecordType[] };
type RespRecordType = {
[key: string]: {
type: string;
value: any;
};
};
type Result = {
[key: string]: {
type: string;
value: any[];
};
};
type Var = {
[key: string]: any;
};
const ERROR_TYPE = "ERROR_TYPE";
const calc = (field: Field, result: Result) => {
const type = typeCheck(field.type);
if (!type) {
return ERROR_TYPE;
}
const fun =
calcFunc[`${type}_${Operator[field.operator as keyof typeof Operator]}`];
if (!fun) {
return ERROR_TYPE;
}
const values = result[field.code].value;
if (!values) {
return null;
}
return fun(values);
};
const typeCheck = (type: string) => {
switch (type) {
case "RECORD_NUMBER":
case "NUMBER":
return CalcType.NUMBER;
case "SINGLE_LINE_TEXT":
case "MULTI_LINE_TEXT":
case "RICH_TEXT":
return CalcType.STRING;
case "DATE":
return CalcType.DATE;
case "TIME":
return CalcType.TIME;
case "DATETIME":
case "UPDATED_TIME":
return CalcType.DATETIME;
default:
return null;
}
};
enum Operator {
SUM = "SUM",
AVG = "AVG",
MAX = "MAX",
MIN = "MIN",
COUNT = "COUNT",
FIRST = "FIRST"
}
enum CalcType {
NUMBER = "number",
STRING = "string",
DATE = "date",
TIME = "time",
DATETIME = "datetime",
}
const calcFunc: Record<string, (value: string[]) => string | null> = {
[`${CalcType.NUMBER}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.STRING}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.DATE}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.TIME}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.DATETIME}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.NUMBER}_${Operator.SUM}`]: (value: string[]) =>
value.reduce((acc, v) => acc + Number(v), 0).toString(),
[`${CalcType.NUMBER}_${Operator.AVG}`]: (value: string[]) =>
(value.reduce((acc, v) => acc + Number(v), 0) / value.length).toString(),
[`${CalcType.NUMBER}_${Operator.MAX}`]: (value: string[]) =>
Math.max(...value.map(Number)).toString(),
[`${CalcType.NUMBER}_${Operator.MIN}`]: (value: string[]) =>
Math.min(...value.map(Number)).toString(),
[`${CalcType.STRING}_${Operator.SUM}`]: (value: string[]) => value.join(" "),
[`${CalcType.DATE}_${Operator.MAX}`]: (value: string[]) =>
value.reduce((maxDate, currentDate) =>
maxDate > currentDate ? maxDate : currentDate
),
[`${CalcType.DATE}_${Operator.MIN}`]: (value: string[]) =>
value.reduce((minDate, currentDate) =>
minDate < currentDate ? minDate : currentDate
),
[`${CalcType.TIME}_${Operator.MAX}`]: (value: string[]) =>
value.reduce((maxTime, currentTime) =>
maxTime > currentTime ? maxTime : currentTime
),
[`${CalcType.TIME}_${Operator.MIN}`]: (value: string[]) =>
value.reduce((minTime, currentTime) =>
minTime < currentTime ? minTime : currentTime
),
[`${CalcType.DATETIME}_${Operator.MAX}`]: (value: string[]) =>
value.reduce((maxDateTime, currentDateTime) =>
new Date(maxDateTime) > new Date(currentDateTime)
? maxDateTime
: currentDateTime
),
[`${CalcType.DATETIME}_${Operator.MIN}`]: (value: string[]) =>
value.reduce((minDateTime, currentDateTime) =>
new Date(minDateTime) < new Date(currentDateTime)
? minDateTime
: currentDateTime
),
[`${CalcType.STRING}_${Operator.FIRST}`]:(value: string[])=>{
return value[0];
}
};

View File

@@ -0,0 +1,99 @@
import { actionAddins } from ".";
import { IAction, IActionProperty, IActionNode, IContext, IActionResult } from "../types/ActionTypes";
import { ConditionTree } from '../types/Conditions';
/**
* アクションの属性定義
*/
interface IErrorShowProps {
message: string;
condition: string;
}
export class ErrorShowAction implements IAction {
name: string;
actionProps: IActionProperty[]; //调用从import导入关于显示类的定义
props: IErrorShowProps;//从上方的interface 定义这个props所需要接受的属性
constructor() { //解构函数定义需要的类名
this.name = "エラー表示";
this.actionProps = [];
this.props = {
message: '',
condition: ''
}
this.register(); //重置以上注册表
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode: IActionNode, event: any, context: IContext): Promise<IActionResult> { //异步处理某函数下的xx属性
let result = {
canNext: true,
result: false
};
try { //尝试执行以下代码部分
this.actionProps = actionNode.actionProps;
if (!('message' in actionNode.ActionValue) && !('condition' in actionNode.ActionValue)) { //如果message以及condition两者都不存在的情况下return
return result
}
this.props = actionNode.ActionValue as IErrorShowProps;
const conditionResult = this.getConditionResult(context);
if (conditionResult) {
event.error = this.props.message;
} else {
result = {
canNext: false,
result: true
}
}
return result;
} catch (error) {
event.error = error;
console.error(error);
result.canNext = false;
return result;
}
}
/**
*
* @param context 条件式を実行する
* @returns
*/
getConditionResult(context: any): boolean {
const tree = this.getCondition(this.props.condition);
if (!tree) {
//条件を設定されていません
return true;
}
return tree.evaluate(tree.root, context);
}
/**
* @param condition 条件式ツリーを取得する
* @returns
*/
getCondition(condition: string): ConditionTree | null {
try {
const tree = new ConditionTree();
tree.fromJson(condition);
if (tree.getConditions(tree.root).length > 0) {
return tree;
} else {
return null;
}
} catch (error) {
return null;
}
}
register(): void {
actionAddins[this.name] = this;
}
}
new ErrorShowAction();

View File

@@ -0,0 +1,108 @@
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes";
import { ConditionTree } from '../types/Conditions';
/**
* アクションの属性定義
*/
interface IShownProps{
field:IField;
show:string;
condition:string;
}
/**
* 表示/非表示アクション
*/
export class FieldShownAction implements IAction{
name: string;
actionProps: IActionProperty[];
props:IShownProps;
constructor(){
this.name="表示/非表示";
this.actionProps=[];
this.props={
field:{code:''},
show:'',
condition:''
}
//アクションを登録する
this.register();
}
/**
* アクションの実行を呼び出す
* @param actionNode
* @param event
* @returns
*/
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:true,
result:false
};
try{
//属性設定を取得する
this.actionProps=actionNode.actionProps;
if (!('field' in actionNode.ActionValue) && !('show' in actionNode.ActionValue)) {
return result
}
this.props = actionNode.ActionValue as IShownProps;
//条件式の計算結果を取得
const conditionResult = this.getConditionResult(context);
if(conditionResult){
if(this.props.show==='表示'){
kintone.app.record.setFieldShown(this.props.field.code,true);
}else if (this.props.show==='非表示'){
kintone.app.record.setFieldShown(this.props.field.code,false);
}
}
result= {
canNext:true,
result:true
}
return result;
}catch(error){
event.error=error;
console.error(error);
result.canNext=false;
return result;
}
}
/**
*
* @param context 条件式を実行する
* @returns
*/
getConditionResult(context:any):boolean{
const tree =this.getCondition(this.props.condition);
if(!tree){
//条件を設定されていません
return true;
}
return tree.evaluate(tree.root,context);
}
/**
* @param condition 条件式ツリーを取得する
* @returns
*/
getCondition(condition:string):ConditionTree|null{
try{
const tree = new ConditionTree();
tree.fromJson(condition);
if(tree.getConditions(tree.root).length>0){
return tree;
}else{
return null;
}
}catch(error){
return null;
}
}
register(): void {
actionAddins[this.name]=this;
}
}
new FieldShownAction();

View File

@@ -1,9 +1,9 @@
import { actionAddins } from "."; import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty } from "../types/ActionTypes"; import { IAction,IActionResult, IActionNode, IActionProperty, IField } from "../types/ActionTypes";
interface IMustInputProps{ interface IMustInputProps{
field:string; field:IField;
message:string; message:string;
} }
@@ -16,13 +16,13 @@ export class MustInputAction implements IAction{
this.actionProps=[]; this.actionProps=[];
this.register(); this.register();
this.props={ this.props={
field:'', field:{code:''},
message:'' message:''
} }
this.register(); this.register();
} }
process(actionNode:IActionNode,event:any):IActionResult { async process(actionNode:IActionNode,event:any):Promise<IActionResult> {
let result={ let result={
canNext:true, canNext:true,
result:false result:false
@@ -33,9 +33,9 @@ export class MustInputAction implements IAction{
} }
this.props = actionNode.ActionValue as IMustInputProps; this.props = actionNode.ActionValue as IMustInputProps;
const record = event.record; const record = event.record;
const value = record[this.props.field]?.value; const value = record[this.props.field.code]?.value;
if(value===undefined || value===''){ if(value===undefined || value===''){
record[this.props.field].error=this.props.message; record[this.props.field.code].error=this.props.message;
return result; return result;
} }
result= { result= {

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