Compare commits

...

41 Commits

Author SHA1 Message Date
58e22dc55f bugfix 2024-02-07 18:06:22 +09:00
f861955b51 bugfix 2024-02-07 16:52:34 +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
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
77 changed files with 4664 additions and 1721 deletions

View File

@@ -1,5 +1,5 @@
FROM python:3.8
FROM python:3.11.4
RUN mkdir /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 app.core.config as config
import os
from pathlib import Path
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.apiexception import APIException
kinton_router = r = APIRouter()
@@ -21,28 +23,134 @@ def getkintoneenv(user = Depends(get_current_user)):
return kintoneevn
def getfieldsfromexcel(df):
def getkintoneformat():
db = SessionLocal()
formats = get_kintoneformat(db)
db.close()
return formats
def createkintonefields(property,value,trueformat):
if(type(value) == str):
value = value.replace("\"","⊡⊡")
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]
col=[]
for row in range(5,len(df)):
if pd.isna(df.iloc[row,1]):
for row in range(startrow,len(df)):
if pd.isna(df.iloc[row,startcolumn]):
break
if not df.iloc[row,3] in config.KINTONE_FIELD_TYPE:
if not df.iloc[row,typecolumn] in config.KINTONE_FIELD_TYPE:
continue
p=[]
for column in range(1,7):
for column in range(startcolumn,startcolumn + len(property)):
if(not pd.isna(df.iloc[row,column])):
if(property[column-1]=="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"\"{property[column-1]}\":{{{','.join(o)}}}")
elif(property[column-1]=="required"):
p.append(f"\"{property[column-1]}\":{df.iloc[row,column]}")
propertyname =property[column-1]
if(propertyname.find("[") == 0):
continue
elif (propertyname =="remark"):
if (df.iloc[row,column].find("|") !=-1):
propertyname = "options"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
if (df.iloc[row,column] == "メールアドレス" or df.iloc[row,column] == "Webサイト" or df.iloc[row,column] == "電話番号"):
propertyname = "protocol"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
if (df.iloc[row,column].find("桁区切り") !=-1):
propertyname = "digit"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
if (df.iloc[row,column].find("前単位") !=-1):
propertyname = "unitPosition"
p = p + createkintonefields(propertyname, "BEFORE",trueformat)
if (df.iloc[row,column].find("後単位") !=-1):
propertyname = "unitPosition"
p = p + createkintonefields(propertyname, "AFTER",trueformat)
if (df.iloc[row,column].find("単位「") !=-1):
propertyname = "unit"
ids = df.iloc[row,column].index("単位「")
ide = df.iloc[row,column].index("")
unit = df.iloc[row,column][ids+3:ide]
p = p + createkintonefields(propertyname, unit,trueformat)
else:
continue
elif(propertyname =="mixValue"):
if(df.iloc[row,column].find("レコード登録時の日") != -1):
propertyname = "defaultNowValue"
df.iloc[row,column] = trueformat
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
elif(df.iloc[row,column].find("計:") != -1):
propertyname = "expression"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
elif(df.iloc[row,column] !=""):
propertyname = "defaultValue"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
else:
continue
elif(propertyname=="max" or propertyname == "min"):
if(df.iloc[row,typecolumn] == "NUMBER"):
propertyname = property[column-1] + "Value"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
else:
propertyname = property[column-1] + "Length"
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
else:
p.append(f"\"{property[column-1]}\":\"{df.iloc[row,column]}\"")
col.append(f"\"{df.iloc[row,2]}\":{{{','.join(p)}}}")
fields = ",".join(col).replace("False","false").replace("True","true")
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("\\", "\\\\").replace("⊡⊡","\\\"")
return json.loads(f"{{{fields}}}")
def getsettingfromexcel(df):
@@ -127,14 +235,14 @@ def analysefields(excel,kintone):
adds = excel.keys() - kintone.keys()
dels = kintone.keys() - excel.keys()
for key in updates:
for p in property:
if excel[key].get(p) != None and kintone[key][p] != excel[key][p]:
for p in config.KINTONE_FIELD_PROPERTY:
if excel[key].get(p) != None and kintone[key].get(p) != None and kintone[key][p] != excel[key][p]:
updatefields[key] = excel[key]
break
for key in adds:
addfields[key] = excel[key]
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)
return {"update":updatefields,"add":addfields,"del":delfields}
@@ -206,7 +314,7 @@ def analysprocess(excel,kintone):
# return True
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"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/status.json"
data = {"app":app,"enable":True}
@@ -214,20 +322,22 @@ def updateprocesstokintone(app:str,process:dict,c:config.KINTONE_ENV=Depends(get
r = httpx.put(url,headers=headers,data=json.dumps(data))
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}
url = f"{c.BASE_URL}/v1/users.json"
r = httpx.get(url,headers=headers)
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}
params = {"code":c.KINTONE_USER}
url = f"{c.BASE_URL}/v1/user/organizations.json"
r = httpx.get(url,headers=headers,params=params)
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')}
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
data ={'name':'file','filename':os.path.basename(file)}
@@ -235,13 +345,16 @@ def uploadkintonefiles(file,c:config.KINTONE_ENV=Depends(getkintoneenv)):
r = httpx.post(url,headers=headers,data=data,files=upload_files)
return r.json()
def updateappjscss(app,uploads,c:config.KINTONE_ENV=Depends(getkintoneenv)):
def updateappjscss(app,uploads,c:config.KINTONE_ENV):
dsjs = []
dscss = []
for upload in uploads:
for key in upload:
if key.endswith('.js'):
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
else:
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
elif key.endswith('.css'):
dscss.append({'type':'FILE','file':{'fileKey':upload[key]}})
ds ={'js':dsjs,'css':dscss}
@@ -253,21 +366,23 @@ def updateappjscss(app,uploads,c:config.KINTONE_ENV=Depends(getkintoneenv)):
r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json()
def createappjs(app):
def createappjs(domainid,app):
db = SessionLocal()
flows = get_flows(db,app)
flows = get_flows_by_app(db,domainid,app)
db.close()
content={}
for flow in flows:
content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content}
js = 'const flow=' + json.dumps(content)
fpath = '{}\\alc_setting_{}.js'.format('Temp',app)
file = open(fpath,'w',encoding="utf-8")
file.write(js)
file.close()
js = 'const alcflow=' + json.dumps(content)
scriptdir = Path(__file__).resolve().parent
rootdir = scriptdir.parent.parent.parent.parent
fpath = os.path.join(rootdir,"Temp",f"alc_setting_{app}.js")
print(rootdir)
print(fpath)
with open(fpath,'w') as file:
file.write(js)
return fpath
@r.post("/test",)
async def test(file:UploadFile= File(...),app:str=None):
if file.filename.endswith('.xlsx'):
@@ -285,7 +400,7 @@ async def test(file:UploadFile= File(...),app:str=None):
# kintone = getfieldsfromkintone(app)
# fields = analysefields(excel,kintone["properties"])
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:
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file")
@@ -293,15 +408,18 @@ async def test(file:UploadFile= File(...),app:str=None):
@r.post("/download",)
async def download(key,c:config.KINTONE_ENV=Depends(getkintoneenv)):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"fileKey":key}
url = f"{c.BASE_URL}/k/v1/file.json"
r = httpx.get(url,headers=headers,params=params)
return r.json()
async def download(request:Request,key,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"fileKey":key}
url = f"{c.BASE_URL}/k/v1/file.json"
r = httpx.get(url,headers=headers,params=params)
return r.json()
except Exception as e:
raise APIException('kintone:upload',request.url._url,f"Error occurred while download file.json:",e)
@r.post("/upload")
async def upload(files:t.List[UploadFile] = File(...)):
async def upload(request:Request,files:t.List[UploadFile] = File(...)):
dataframes = []
for file in files:
if file.filename.endswith('.xlsx'):
@@ -311,14 +429,14 @@ async def upload(files:t.List[UploadFile] = File(...)):
print(df)
dataframes.append(df)
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:
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]}
@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:
jscs=[]
for file in files:
@@ -328,57 +446,86 @@ async def jscss(app:str,files:t.List[UploadFile] = File(...),env = Depends(getki
fout = open(fpath,'wb')
fout.write(fbytes)
fout.close()
upload = uploadkintonefiles(fpath)
upload = uploadkintonefiles(fpath,env)
if upload.get('fileKey') != None:
jscs.append({ file.filename:upload['fileKey']})
appjscs = updateappjscss(app,jscs)
appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None:
deoployappfromkintone(app,appjscs["revision"],env)
return appjscs
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")
async def allapps(c:config.KINTONE_ENV=Depends(getkintoneenv)):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}{config.API_V1_STR}/apps.json"
r = httpx.get(url,headers=headers)
return r.json()
async def allapps(request:Request,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}{config.API_V1_STR}/apps.json"
r = httpx.get(url,headers=headers)
return r.json()
except Exception as e:
raise APIException('kintone:allapps',request.url._url, f"Error occurred while get allapps({c.DOMAIN_NAM}):",e)
@r.get("/appfields")
async def appfields(app:str,env = Depends(getkintoneenv)):
return getfieldsfromkintone(app,env)
async def appfields(request:Request,app:str,env = Depends(getkintoneenv)):
try:
return getfieldsfromkintone(app,env)
except Exception as e:
raise APIException('kintone:appfields',request.url._url, f"Error occurred while get app fileds({env.DOMAIN_NAM}->{app}):",e)
@r.get("/appprocess")
async def appprocess(app:str,env = Depends(getkintoneenv)):
return getprocessfromkintone(app,env)
async def appprocess(request:Request,app:str,env = Depends(getkintoneenv)):
try:
return getprocessfromkintone(app,env)
except Exception as e:
raise APIException('kintone:appprocess',request.url._url, f"Error occurred while get app process({env.DOMAIN_NAM}->{app}):",e)
@r.get("/alljscss")
async def alljscs(app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{c.BASE_URL}{config.API_V1_STR}/app/customize.json"
params = {"app":app}
r = httpx.get(url,headers=headers,params=params)
return r.json()
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}
url = f"{c.BASE_URL}{config.API_V1_STR}/app/customize.json"
params = {"app":app}
r = httpx.get(url,headers=headers,params=params)
return r.json()
except Exception as e:
raise APIException('kintone:alljscss',request.url._url, f"Error occurred while get app js/css({c.DOMAIN_NAM}->{app}):",e)
@r.post("/createapp",)
async def createapp(name:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
data = {"name":name}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app.json"
r = httpx.post(url,headers=headers,data=json.dumps(data))
result = r.json()
if result.get("app") != None:
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json"
data = {"apps":[result],"revert": False}
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"}
data = {"name":name}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app.json"
r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json
result = r.json()
if result.get("app") != None:
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json"
data = {"apps":[result],"revert": False}
r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json
except Exception as e:
raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({c.DOMAIN_NAM}->{name}):",e)
property=["label","code","type","required","defaultValue","options"]
@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:
if file.filename.endswith('.xlsx'):
try:
@@ -388,9 +535,9 @@ async def createappfromexcel(files:t.List[UploadFile] = File(...),env = Depends(
appname = df.iloc[0,2]
desc = df.iloc[2,2]
result = {"app":0,"revision":0,"msg":""}
fields = getfieldsfromexcel(df)
users = getkintoneusers()
orgs = getkintoneorgs()
fields = getfieldsfromexcel(df,mapping)
users = getkintoneusers(env)
orgs = getkintoneorgs(env)
processes = getprocessfromexcel(df,users["users"], orgs["organizationTitles"])
app = createkintoneapp(appname,env)
if app.get("app") != None:
@@ -398,22 +545,26 @@ async def createappfromexcel(files:t.List[UploadFile] = File(...),env = Depends(
app = updateappsettingstokintone(result["app"],{"description":desc},env)
if app.get("revision") != None:
result["revision"] = app["revision"]
app = addfieldstokintone(result["app"],env,fields)
if len(processes)> 0:
app = updateprocesstokintone(result["app"],processes)
app = addfieldstokintone(result["app"],fields,env)
if len(processes["states"])> 0:
app = updateprocesstokintone(result["app"],processes,env)
if app.get("revision") != None:
result["revision"] = app["revision"]
deoployappfromkintone(result["app"],result["revision"],env)
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:
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
@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:
if file.filename.endswith('.xlsx'):
try:
@@ -422,10 +573,10 @@ async def updateappfromexcel(app:str,files:t.List[UploadFile] = File(...),env =
excel = getsettingfromexcel(df)
kintone= getsettingfromkintone(app,env)
settings = analysesettings(excel,kintone)
excel = getfieldsfromexcel(df)
excel = getfieldsfromexcel(df,mapping)
kintone = getfieldsfromkintone(app,env)
users = getkintoneusers()
orgs = getkintoneorgs()
users = getkintoneusers(env)
orgs = getkintoneorgs(env)
exp = getprocessfromexcel(df,users["users"], orgs["organizationTitles"])
#exp = getprocessfromexcel(df)
kinp = getprocessfromkintone(app,env)
@@ -451,20 +602,19 @@ async def updateappfromexcel(app:str,files:t.List[UploadFile] = File(...),env =
revision = result["revision"]
deploy = True
if len(process)>0:
result = updateprocesstokintone(app,exp)
result = updateprocesstokintone(app,exp,env)
revision = result["revision"]
deploy = True
if deploy:
result = deoployappfromkintone(app,revision,env)
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:
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
@r.post("/updateprocessfromexcel",)
async def updateprocessfromexcel(app:str,env = Depends(getkintoneenv)):
async def updateprocessfromexcel(request:Request,app:str,env = Depends(getkintoneenv)):
try:
excel = getprocessfromexcel()
@@ -489,32 +639,31 @@ async def updateprocessfromexcel(app:str,env = Depends(getkintoneenv)):
# result = updateappsettingstokintone(app,settings)
# revision = result["revision"]
# deploy = True
result = updateprocesstokintone(app,excel)
result = updateprocesstokintone(app,excel,env)
revision = result["revision"]
deploy = True
if deploy:
result = deoployappfromkintone(app,revision,env)
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
@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:
jscs=[]
files=[]
files.append(createappjs(app))
files.append(createappjs(env.DOMAIN_ID, app))
files.append('Temp\\alc_runtime.js')
for file in files:
upload = uploadkintonefiles(file)
upload = uploadkintonefiles(file,env)
if upload.get('fileKey') != None:
jscs.append({ app + '.js':upload['fileKey']})
appjscs = updateappjscss(app,jscs)
jscs.append({ file :upload['fileKey']})
appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None:
deoployappfromkintone(app,appjscs["revision"],env)
return appjscs
except Exception as e:
raise 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 typing import List
from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
platform_router = r = APIRouter()
@@ -18,9 +19,11 @@ async def appsetting_details(
id: int,
db=Depends(get_db),
):
app = get_appsetting(db, id)
return app
try:
app = get_appsetting(db, id)
return app
except Exception as e:
raise APIException('platform:appsettings',request.url._url,f"Error occurred while get app setting:",e)
@r.post("/appsettings", response_model=App, response_model_exclude_none=True)
async def appsetting_create(
@@ -28,7 +31,10 @@ async def appsetting_create(
app: AppBase,
db=Depends(get_db),
):
return create_appsetting(db, app)
try:
return create_appsetting(db, app)
except Exception as e:
raise APIException('platform:appsettings',request.url._url,f"Error occurred while get create app setting:",e)
@r.put(
@@ -40,7 +46,10 @@ async def appsetting_edit(
app: AppEdit,
db=Depends(get_db),
):
return edit_appsetting(db, id, app)
try:
return edit_appsetting(db, id, app)
except Exception as e:
raise APIException('platform:appsettings',request.url._url,f"Error occurred while edit app setting:",e)
@r.delete(
@@ -51,8 +60,10 @@ async def appsettings_delete(
id: int,
db=Depends(get_db),
):
return delete_appsetting(db, id)
try:
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(
@@ -65,8 +76,11 @@ async def kintone_data(
type: int,
db=Depends(get_db),
):
kintone = get_kintones(db, type)
return kintone
try:
kintone = get_kintones(db, type)
return kintone
except Exception as e:
raise APIException('platform:kintone',request.url._url,f"Error occurred while get kintone env:",e)
@r.get(
"/actions",
@@ -78,8 +92,11 @@ async def action_data(
request: Request,
db=Depends(get_db),
):
actions = get_actions(db)
return actions
try:
actions = get_actions(db)
return actions
except Exception as e:
raise APIException('platform:actions',request.url._url,f"Error occurred while get actions:",e)
@r.get(
"/flow/{flowid}",
@@ -91,8 +108,11 @@ async def flow_details(
flowid: str,
db=Depends(get_db),
):
app = get_flow(db, flowid)
return app
try:
app = get_flow(db, flowid)
return app
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e)
@r.get(
@@ -103,19 +123,30 @@ async def flow_details(
async def flow_list(
request: Request,
appid: str,
user=Depends(get_current_user),
db=Depends(get_db),
):
flows = get_flows_by_app(db, appid)
return flows
try:
domain = get_activedomain(db, user.id)
print("domain=>",domain)
flows = get_flows_by_app(db, domain.id, appid)
return flows
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
@r.post("/flow", response_model=Flow, response_model_exclude_none=True)
async def flow_create(
request: Request,
flow: FlowBase,
user=Depends(get_current_user),
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(
@@ -126,7 +157,10 @@ async def flow_edit(
flow: FlowBase,
db=Depends(get_db),
):
return edit_flow(db, flow)
try:
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(
@@ -137,8 +171,10 @@ async def flow_delete(
flowid: str,
db=Depends(get_db),
):
return delete_flow(db, flowid)
try:
return delete_flow(db, flowid)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while delete flow:",e)
@r.get(
"/domains/{tenantid}",
@@ -150,8 +186,11 @@ async def domain_details(
tenantid:str,
db=Depends(get_db),
):
domains = get_domains(db,tenantid)
return domains
try:
domains = get_domains(db,tenantid)
return domains
except Exception as e:
raise APIException('platform:domains',request.url._url,f"Error occurred while get domains:",e)
@r.post("/domain", response_model=Domain, response_model_exclude_none=True)
async def domain_create(
@@ -159,7 +198,10 @@ async def domain_create(
domain: DomainBase,
db=Depends(get_db),
):
return create_domain(db, domain)
try:
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(
@@ -170,7 +212,10 @@ async def domain_edit(
domain: DomainBase,
db=Depends(get_db),
):
return edit_domain(db, domain)
try:
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(
@@ -181,8 +226,10 @@ async def domain_delete(
id: int,
db=Depends(get_db),
):
return delete_domain(db,id)
try:
return delete_domain(db,id)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain:",e)
@r.get(
"/domain",
@@ -194,8 +241,11 @@ async def userdomain_details(
user=Depends(get_current_user),
db=Depends(get_db),
):
domains = get_domain(db, user.id)
return domains
try:
domains = get_domain(db, user.id)
return domains
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
@r.post(
"/domain/{userid}",
@@ -207,8 +257,11 @@ async def create_userdomain(
domainids:list,
db=Depends(get_db),
):
domain = add_userdomain(db, userid,domainids)
return domain
try:
domain = add_userdomain(db, userid,domainids)
return domain
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while add user({userid}) domain:",e)
@r.delete(
"/domain/{domainid}/{userid}", response_model_exclude_none=True
@@ -219,7 +272,10 @@ async def userdomain_delete(
userid: int,
db=Depends(get_db),
):
return delete_userdomain(db, userid,domainid)
try:
return delete_userdomain(db, userid,domainid)
except Exception as e:
raise APIException('platform:delete',request.url._url,f"Error occurred while delete user({userid}) domain:",e)
@r.get(
@@ -232,8 +288,11 @@ async def get_useractivedomain(
user=Depends(get_current_user),
db=Depends(get_db),
):
domain = get_activedomain(db, user.id)
return domain
try:
domain = get_activedomain(db, user.id)
return domain
except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while get user({user.id}) activedomain:",e)
@r.put(
"/activedomain/{domainid}",
@@ -245,9 +304,11 @@ async def update_activeuserdomain(
user=Depends(get_current_user),
db=Depends(get_db),
):
domain = active_userdomain(db, user.id,domainid)
return domain
try:
domain = active_userdomain(db, user.id,domainid)
return domain
except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while update user({user.id}) activedomain:",e)
@r.get(
"/events",
@@ -259,8 +320,11 @@ async def event_data(
request: Request,
db=Depends(get_db),
):
events = get_events(db)
return events
try:
events = get_events(db)
return events
except Exception as e:
raise APIException('platform:events',request.url._url,f"Error occurred while get events:",e)
@r.get(
@@ -274,5 +338,8 @@ async def eventactions_data(
eventid: str,
db=Depends(get_db),
):
actions = get_eventactions(db,eventid)
return actions
try:
actions = get_eventactions(db,eventid)
return actions
except Exception as e:
raise APIException('platform:eventactions',request.url._url,f"Error occurred while get eventactions:",e)

View File

@@ -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(
email=email,
password=password,
firstname = firstname,
lastname = lastname,
first_name = firstname,
last_name = lastname,
is_active=True,
is_superuser=False,
),

View File

@@ -3,15 +3,21 @@ import base64
PROJECT_NAME = "KintoneAppBuilder"
SQLALCHEMY_DATABASE_URI = "postgres://maxz64:m@xz1205@alicornkintone.postgres.database.azure.com/postgres"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/postgres"
#SQLALCHEMY_DATABASE_URI = "postgres://maxz64:m@xz1205@alicornkintone.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_AUTH_KEY = "X-Cybozu-Authorization"
DEPLOY_MODE = "DEV" #DEV,PROD
DEPLOY_JS_URL = "https://ka-addin.azurewebsites.net/alc_runtime.js"
KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"]
KINTONE_FIELD_PROPERTY=['label','code','type','required','unique','maxValue','minValue','maxLength','minLength','defaultValue','defaultNowValue','options','expression','hideExpression','digit','protocol','displayScale','unit','unitPosition']
class KINTONE_ENV:
BASE_URL = ""
@@ -19,8 +25,14 @@ class KINTONE_ENV:
API_V1_AUTH_VALUE = ""
KINTONE_USER = ""
DOMAIN_ID = ""
DOMAIN_NAME =""
def __init__(self,domain) -> None:
self.DOMAIN_NAME=domain.name
self.DOMAIN_ID=domain.id
self.BASE_URL = domain.url
self.KINTONE_USER = domain.kintoneuser
self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.kintonepwd}","utf-8"))

View File

@@ -25,7 +25,7 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
expire = datetime.utcnow() + timedelta(minutes=2880)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

View File

@@ -125,11 +125,12 @@ def get_actions(db: Session):
return actions
def create_flow(db: Session, flow: schemas.FlowBase):
def create_flow(db: Session, domainid: int, flow: schemas.FlowBase):
db_flow = models.Flow(
flowid=flow.flowid,
appid=flow.appid,
eventid=flow.eventid,
domainid=domainid,
name=flow.name,
content=flow.content
)
@@ -176,10 +177,10 @@ def get_flow(db: Session, flowid: str):
raise HTTPException(status_code=404, detail="Data not found")
return flow
def get_flows_by_app(db: Session, appid: str):
flows = db.query(models.Flow).filter(models.Flow.appid == appid).all()
def get_flows_by_app(db: Session, domainid: int, appid: str):
flows = db.query(models.Flow).filter(and_(models.Flow.domainid == domainid,models.Flow.appid == appid)).all()
if not flows:
raise HTTPException(status_code=404, detail="Data not found")
raise Exception("Data not found")
return flows
def create_domain(db: Session, domain: schemas.DomainBase):
@@ -281,4 +282,16 @@ 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()
if not eveactions:
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)
appid = 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))
content = Column(String)
@@ -89,4 +90,23 @@ class EventAction(Base):
__tablename__ = "eventaction"
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):
email:str
password: str
first_name: str
last_name: str
is_active:bool
is_superuser:bool
class Config:
orm_mode = True
@@ -101,6 +104,7 @@ class Flow(Base):
flowid: str
appid: str
eventid: str
domainid: int
name: str = None
content: str = None
@@ -135,4 +139,9 @@ class Event(Base):
mobile: bool
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 starlette.requests import Request
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 import tasks
from fastapi.middleware.cors import CORSMiddleware
import logging
from app.core.apiexception import APIException, writedblog
from app.db.crud import create_log
from fastapi.responses import JSONResponse
import asyncio
Base.metadata.create_all(bind=engine)
@@ -19,9 +25,7 @@ app = FastAPI(
)
origins = [
"http://localhost:9000",
"http://localhost",
"http://localhost:8080",
"*"
]
app.add_middleware(
@@ -39,6 +43,25 @@ app.add_middleware(
# request.state.db.close()
# 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")
async def root():

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

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

@@ -1,8 +1,8 @@
{
"name": "kintone-app-builder",
"name": "kintone-automate",
"version": "0.2.0",
"description": "Kintoneアプリの自動生成とデプロイを支援ツールです",
"productName": "Kintone App Builder",
"productName": "Kintone Automate",
"author": "maxiaozhe@alicorns.co.jp <maxiaozhe@alicorns.co.jp>",
"private": true,
"scripts": {
@@ -10,7 +10,10 @@
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test": "echo \"No test specified\" && exit 0",
"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": {
"@quasar/extras": "^1.16.4",

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 dotenv = require('dotenv').config().parsed;
const package = require('./package.json');
const envPath = process.env.LOCAL==='true'?'.env.development':'.env';
const dotenv = require('dotenv').config({path:envPath}).parsed;
console.log('dotenv=>',dotenv);
// const package = require('./package.json');
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 */) {
return {
eslint: {
@@ -49,7 +53,6 @@ module.exports = configure(function (/* ctx */) {
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it
'material-icons', // optional, you are not bound to it
],
@@ -60,6 +63,7 @@ module.exports = configure(function (/* ctx */) {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
node: 'node16'
},
sourcemap:process.env.SOURCE_MAP === 'true',
vueRouterMode: 'hash', // available values: 'hash', 'history'
// vueRouterBase,
@@ -70,7 +74,7 @@ module.exports = configure(function (/* ctx */) {
// publicPath: '/',
// analyze: true,
env: { ...dotenv, version },
env: { ...dotenv, version ,productName},
// rawDefine: {}
// ignorePublicFolder: true,
// minify: false,

View File

@@ -1,5 +1,6 @@
import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios';
import {router} from 'src/router';
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
@@ -15,7 +16,28 @@ declare module '@vue/runtime-core' {
// "export default () => {}" function below (which runs individually
// for each client)
const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL });
const token=localStorage.getItem('token')||'';
if(token!==''){
api.defaults.headers["Authorization"]='Bearer ' + token;
}
api.interceptors.response.use(
(response)=>response,
(error)=>{
const orgReq=error.config;
if(error.response && error.response.status===401){
console.error("401エラー");
localStorage.removeItem('token');
router.replace({
path:"/login",
query:{redirect:router.currentRoute.value.fullPath}
});
// router.push({
// path:"/login",
// query:{redirect:router.currentRoute.value.fullPath}
// });
}
}
)
export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api

View File

@@ -1,6 +1,9 @@
<template>
<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="name" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows"
class="action-table"
flat bordered
virtual-scroll
@@ -20,20 +23,20 @@ export default {
type: String
},
setup() {
const columns = [
{ name: 'name', required: true,label: 'アクション名',align: 'left',field: 'name',sortable: true},
{ name: 'desc', align: 'left', label: '説明', field: 'desc', sortable: true },
// { name: 'content', label: '内容', field: 'content', sortable: true }
]
const rows = reactive([])
const isLoaded=ref(false);
const columns = [
{ name: 'name', required: true,label: 'アクション名',align: 'left',field: 'name',sortable: true},
{ name: 'desc', align: 'left', label: '説明', field: 'desc', sortable: true },
// { name: 'content', label: '内容', field: 'content', sortable: true }
];
const rows = reactive([])
onMounted(async () => {
await api.get('http://127.0.0.1:8000/api/kintone/1').then(res =>{
res.data.forEach((item) =>
const res =await api.get('api/actions');
res.data.forEach((item) =>
{
rows.push({name:item.name,desc:item.desc,content:item.content});
}
)
});
rows.push({name:item.name,desc:item.title,outputPoints:item.outputpoints,property:item.property});
});
isLoaded.value=true;
});
return {
columns,
@@ -41,7 +44,8 @@ export default {
selected: ref([]),
pagination:ref({
rowsPerPage:0
})
}),
isLoaded,
}
},

View File

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

View File

@@ -1,11 +1,23 @@
<template>
<div class="q-pa-md">
<q-table :title="name+'一覧'" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" />
<div class="q-pa-md" >
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table v-else :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" >
<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>
<script lang="ts">
import { ref,onMounted,reactive } from 'vue'
import { api } from 'boot/axios';
import { LeftDataBus } from './flowEditor/left/DataBus';
export default {
name: 'AppSelect',
@@ -15,27 +27,58 @@ export default {
},
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' }
{ 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 rows = reactive([])
const isLoaded=ref(false);
const rows :any[]= 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});
}
)
});
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: ref([]),
isLoaded
}
},
}
</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" width="60vw" 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,79 @@
<template>
<q-field v-model="selectedField" 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" :dense="true" class="selected-obj">
{{ selectedField.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" widht="400px">
<condition-objects ref="appDg" name="フィールド" type="single" :appId="store.appInfo?.appId"></condition-objects>
</show-dialog>
</template>
<script lang="ts">
import { defineComponent, ref ,watchEffect,computed} from 'vue';
import ShowDialog from '../ShowDialog.vue';
import ConditionObjects from '../ConditionObjects.vue';
import { useFlowEditorStore } from '../../stores/flowEditor';
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 selectedField = ref(props.modelValue);
const store = useFlowEditorStore();
const isSelected = computed(()=>{
return selectedField.value!==null && typeof selectedField.value === 'object' && ('name' in selectedField.value)
});
const showDg = () => {
show.value = true;
};
const closeDg = (val:string) => {
if (val == 'OK') {
selectedField.value = appDg.value.selected[0];
}
};
watchEffect(() => {
emit('update:modelValue', selectedField.value);
});
return {
store,
appDg,
show,
showDg,
closeDg,
selectedField,
isSelected
};
}
});
</script>
<style lang="scss">
.condition-object{
min-width: 200px;
max-height: 40px;
padding: 2px;
}
.selected-obj{
margin: 0px;
}
</style>

View File

@@ -0,0 +1,273 @@
<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">
<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-btn @click="addCondition(tree.root)" class="q-mt-md" color="primary" icon="mdi-plus">Add Condition</q-btn> -->
<!-- <q-btn @click="getConditionString()" class="q-mt-md" color="primary" icon="mdi-plus">Show Condtion</q-btn>
<q-btn @click="getConditionJson()" class="q-mt-md" color="primary" icon="mdi-plus">Show Condtion data</q-btn>
<q-btn @click="LoadCondition()" class="q-mt-md" color="primary" icon="mdi-plus">Load Condition</q-btn> -->
<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 } 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 operators =computed(()=>{
const opts=[];
for(const op in Operator){
opts.push(Operator[op as keyof typeof Operator]);
}
return opts;
});
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;
padding: 2px;
}
.operator{
min-width: 150px;
max-height: 40px;
padding: 2px;
text-align: center;
font-size: 12pt;
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="q-pa-md">
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table v-else row-key="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: 'ConditionObjects',
props: {
name: String,
type: String,
appId:Number
},
setup(props) {
const isLoaded=ref(false);
const columns = [
{ name: 'name', required: true,label: 'フィールド名',align: 'left',field: row=>row.name,sortable: true},
{ name: 'code', label: 'フィールドコード', align: 'left',field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left',field: 'type', sortable: true }
]
const rows = reactive([])
onMounted( async () => {
const res = await api.get('api/v1/appfields', {
params:{
app: props.appId
}
});
let fields = res.data.properties;
console.log(fields);
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:fld.label,objectType:'field',...fld});
});
isLoaded.value=true;
});
return {
columns,
rows,
selected: ref([]),
isLoaded
}
},
}
</script>

View File

@@ -4,6 +4,7 @@
style="max-width: 400px"
:url="uploadUrl"
:label="title"
:headers="headers"
accept=".csv,.xlsx"
v-on:rejected="onRejected"
v-on:uploaded="onUploadFinished"
@@ -15,7 +16,10 @@
</template>
<script setup lang="ts">
import { createUploaderComponent, useQuasar } from 'quasar';
import { useAuthStore } from 'src/stores/useAuthStore';
import { ref } from 'vue';
const $q=useQuasar();
const authStore = useAuthStore();
const emit =defineEmits(['uploaded']);
/**
* ファイルアップロードを拒否する時の処理
@@ -67,9 +71,12 @@
title: string;
uploadUrl:string;
}
const headers = ref([{name:"Authorization",value:'Bearer ' + authStore.token}]);
const props = withDefaults(defineProps<Props>(), {
title:"設計書から導入する(csv or excel)",
uploadUrl: `${process.env.KAB_BACKEND_URL}createappfromexcel`
uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel`
});
</script>
<style lang="scss">

View File

@@ -23,7 +23,7 @@ export default {
]
const rows = reactive([])
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) =>
{
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

@@ -1,6 +1,9 @@
<template>
<div class="q-pa-md">
<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 v-else row-key="name" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" />
</div>
</template>
<script>
@@ -15,32 +18,36 @@ export default {
appId:Number
},
setup(props) {
const columns = [
{ name: 'name', required: true,label: 'フィールド名',align: 'left',field: row=>row.name,sortable: true},
{ name: 'code', label: 'フィールドコード', align: 'left',field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left',field: 'type', sortable: true }
]
const rows = reactive([])
onMounted( () => {
api.get('appfields', {
params:{
app: props.appId
}
}).then(res =>{
let fields = res.data.properties;
console.log(fields);
Object.keys(fields).forEach((key) =>
{
rows.push({name:fields[key].label,code:fields[key].code,type:fields[key].type});
}
)
});
});
return {
columns,
rows,
selected: ref([]),
}
const isLoaded=ref(false);
const columns = [
{ name: 'name', required: true,label: 'フィールド',align: 'left',field: row=>row.name,sortable: true},
{ name: 'code', label: 'フィールドコード', align: 'left',field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left',field: 'type', sortable: true }
]
const rows = reactive([])
onMounted( async () => {
const res = await api.get('api/v1/appfields', {
params:{
app: props.appId
}
});
let fields = res.data.properties;
console.log(fields);
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:fld.label,...fld});
});
isLoaded.value=true;
});
return {
columns,
rows,
selected: ref([]),
isLoaded
}
},
}

View File

@@ -1,12 +1,17 @@
<template>
<div class="q-pa-md q-gutter-sm">
<q-dialog :model-value="visible" persistent>
<q-card style="min-width: 350px">
<!-- <div class="q-pa-md q-gutter-sm" > -->
<q-dialog :model-value="visible" persistent bordered>
<q-card :style="{minWidth : width}" >
<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>
<div class="text-h6">{{ name }}選択</div>
<!-- <div class="text-h6">{{ name }}</div> -->
</q-card-section>
<q-card-section class="q-pt-none">
<q-card-section class="q-pt-none" :style="{...(height? {minHeight:height}:{}) }">
<slot></slot>
</q-card-section>
<q-card-actions align="right" class="text-primary">
@@ -15,7 +20,7 @@
</q-card-actions>
</q-card>
</q-dialog>
</div>
<!-- </div> -->
</template>
<script>
@@ -24,6 +29,8 @@ export default {
props: {
name:String,
visible: Boolean,
width:String,
height:String
},
emits: [
'close'

View File

@@ -7,7 +7,7 @@
style="font-size: 2em"
/>
<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 }}
</a>
</div>
@@ -22,7 +22,7 @@
></q-btn>
</div>
</div>
<ShowDialog v-model:visible="showSelectApp" name="アプリ" @close="closeDg">
<ShowDialog v-model:visible="showSelectApp" name="アプリ選択" @close="closeDg" width="600px" >
<AppSelect ref="appDg" name="アプリ" type="single"></AppSelect>
</ShowDialog>
</template>
@@ -33,6 +33,7 @@ import {AppInfo} from '../../types/ActionTypes'
import ShowDialog from '../../components/ShowDialog.vue';
import AppSelect from '../../components/AppSelect.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'src/stores/useAuthStore';
export default defineComponent({
name: 'AppSelector',
emits:[
@@ -45,6 +46,7 @@ export default defineComponent({
setup(props, context) {
const store = useFlowEditorStore();
const authStore=useAuthStore();
const appDg = ref();
const showSelectApp=ref(false);
@@ -67,6 +69,7 @@ export default defineComponent({
}
return {
store,
authStore,
showSelectApp,
showAppDialog,
closeDg,

View File

@@ -28,7 +28,6 @@ import { IKintoneEvent } from '../../types/KintoneEvents';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor';
import { ActionFlow, ActionNode, RootAction } from 'src/types/ActionTypes';
import { S } from 'app/dist/spa/assets/QTable.50486f7c';
export default defineComponent({
name: 'EventTree',
setup(props, context) {

View File

@@ -26,7 +26,11 @@
</q-toolbar>
<q-separator />
<q-card-section>
<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>
<template v-if="hasBranch">
<q-separator />
@@ -134,7 +138,14 @@ export default defineComponent({
*/
const onDeleteAllNode=()=>{
context.emit('deleteAllNextNodes', props.actionNode);
}
};
/**
* 変数名取得
*/
const varName =(node:IActionNode)=>{
const prop = node.actionProps.find((prop) => prop.props.name === "verName");
return prop?.props.modelValue;
};
return {
node: props.actionNode,
isRoot: props.actionNode.isRoot,
@@ -145,7 +156,8 @@ export default defineComponent({
onNodeClick,
onEditNode,
onDeleteNode,
onDeleteAllNode
onDeleteAllNode,
varName
}
}
});

View File

@@ -0,0 +1,95 @@
<template>
<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" @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>
</template>
<script lang="ts">
import { defineComponent, ref ,watchEffect,computed,reactive} from 'vue';
import { ConditionTree,GroupNode,ConditionNode,LogicalOperator,Operator } from 'app/src/types/Conditions';
import ConditionEditor from '../ConditionEditor/ConditionEditor.vue'
export default defineComponent({
name: 'FieldInput',
components: {
ConditionEditor
},
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 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({},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') {
const conditionJson = tree.toJson();
isSetted.value=true;
emit('update:modelValue', conditionJson);
}
};
watchEffect(() => {
const conditionJson = tree.toJson();
emit('update:modelValue', conditionJson);
});
return {
appDg,
isSetted,
show,
showDg,
onClosed,
tree,
conditionString
};
}
});
</script>

View File

@@ -1,6 +1,6 @@
<template>
<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>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
@@ -25,10 +25,18 @@ export default defineComponent({
type: String,
default: '',
},
name:{
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
hint:{
type: String,
default: '',
},
modelValue: {
type: String,
default: '',

View File

@@ -1,19 +1,37 @@
<template>
<q-input v-model="selectedField" :label="displayName" labelColor="" :placeholder="placeholder" clearable >
<template v-slot:append>
<q-field v-model="selectedField" :label="displayName" labelColor="primary"
:clearable="isSelected" stack-label :bottom-slots="!isSelected" >
<template v-slot:control >
<q-chip color="primary" text-color="white" v-if="isSelected">
{{ selectedField.name }}
</q-chip>
</template>
<!-- <template v-slot:hint v-if="isSelected">
<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" @click="showDg"/>
</template>
</q-input>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg">
</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>
</show-dialog>
</template>
<script lang="ts">
import { defineComponent, ref ,watchEffect} from 'vue';
import { defineComponent, ref ,watchEffect,computed} from 'vue';
import ShowDialog from '../ShowDialog.vue';
import FieldSelect from '../FieldSelect.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
interface IField{
name:string,
code:string,
type:string
}
export default defineComponent({
name: 'FieldInput',
components: {
@@ -25,14 +43,22 @@ export default defineComponent({
type: String,
default: '',
},
name:{
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
modelValue: {
hint:{
type: String,
default: '',
},
modelValue: {
type: Object,
default: null
},
},
setup(props, { emit }) {
@@ -40,6 +66,9 @@ export default defineComponent({
const show = ref(false);
const selectedField = ref(props.modelValue);
const store = useFlowEditorStore();
const isSelected = computed(()=>{
return selectedField.value!==null && typeof selectedField.value === 'object' && ('name' in selectedField.value)
});
const showDg = () => {
show.value = true;
@@ -47,7 +76,7 @@ export default defineComponent({
const closeDg = (val:string) => {
if (val == 'OK') {
selectedField.value = appDg.value.selected[0].code;
selectedField.value = appDg.value.selected[0];
}
};
@@ -62,6 +91,7 @@ export default defineComponent({
showDg,
closeDg,
selectedField,
isSelected
};
}
});

View File

@@ -1,5 +1,5 @@
<template>
<q-input :label="displayName" :placeholder="placeholder" v-model="inputValue"/>
<q-input :label="displayName" v-model="inputValue" label-color="primary" :placeholder="placeholder" stack-label/>
</template>
<script lang="ts">
@@ -12,17 +12,25 @@ export default defineComponent({
type: String,
default: '',
},
name:{
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
hint:{
type: String,
default: '',
},
modelValue: {
type: String,
default: '',
},
},
setup(props, { emit }) {
setup(props , { emit }) {
const inputValue = ref(props.modelValue);
watchEffect(() => {

View File

@@ -1,5 +1,5 @@
<template>
<q-input :label="displayName" :placeholder="placeholder" v-model="inputValue" autogrow />
<q-input :label="displayName" label-color="primary" v-model="inputValue" :placeholder="placeholder" autogrow stack-label/>
</template>
<script lang="ts">
@@ -12,10 +12,18 @@ export default defineComponent({
type: String,
default: '',
},
name:{
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
hint:{
type: String,
default: '',
},
modelValue: {
type: String,
default: '',

View File

@@ -1,6 +1,6 @@
<template>
<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>
</div>
</div>
@@ -16,6 +16,7 @@ import SelectBox from '../right/SelectBox.vue';
import DatePicker from '../right/DatePicker.vue';
import FieldInput from '../right/FieldInput.vue';
import MuiltInputText from '../right/MuiltInputText.vue';
import ConditionInput from '../right/ConditionInput.vue';
import { IActionNode,IActionProperty } from 'src/types/ActionTypes';
export default defineComponent({
@@ -25,7 +26,8 @@ export default defineComponent({
SelectBox,
DatePicker,
FieldInput,
MuiltInputText
MuiltInputText,
ConditionInput
},
props: {
nodeProps: {
@@ -45,3 +47,5 @@ export default defineComponent({
}
});
</script>
<style lang="scss">
</style>

View File

@@ -13,7 +13,7 @@
>
<q-card class="column full-height" style="width: 300px">
<q-card-section>
<div class="text-h6">プロパティ</div>
<div class="text-h6">{{ actionNode.subTitle }}設定</div>
</q-card-section>
<q-card-section class="col q-pt-none">
<property-list :node-props="actionProps" v-if="showPanel" ></property-list>

View File

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

View File

@@ -8,7 +8,7 @@ export class FlowCtrl
{
const flows:ActionFlow[]=[];
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);
if(!result.data || !Array.isArray(result.data)){
return [];
@@ -26,7 +26,7 @@ export class FlowCtrl
async SaveFlow(jsonData:any):Promise<boolean>
{
const result = await api.post('http://127.0.0.1:8000/api/flow',jsonData);
const result = await api.post('api/flow',jsonData);
console.info(result.data)
return true;
}
@@ -37,7 +37,7 @@ export class FlowCtrl
*/
async UpdateFlow(jsonData:any):Promise<boolean>
{
const result = await api.put('http://127.0.0.1:8000/api/flow/' + jsonData.flowid,jsonData);
const result = await api.put('api/flow/' + jsonData.flowid,jsonData);
console.info(result.data)
return true;
}
@@ -48,7 +48,7 @@ export class FlowCtrl
*/
async depoly(appid:string):Promise<boolean>
{
const result = await api.post(`http://127.0.0.1:8000/api/v1/createjstokintone?app=${appid}`);
const result = await api.post(`api/v1/createjstokintone?app=${appid}`);
console.info(result.data);
return true;
}

View File

@@ -11,20 +11,12 @@
@click="toggleLeftDrawer"
/>
<q-toolbar-title>
Kintone App Builder
<q-badge align="top" outline>V{{ env.version }}</q-badge>
{{ productName }}
<q-badge align="top" outline>V{{ version }}</q-badge>
</q-toolbar-title>
<q-btn color="blue" size="sm" @click="authStore.userdomain()">
{{ authStore.domain }}
</q-btn>
<q-chip>
{{ authStore.name }}
</q-chip>
<domain-selector></domain-selector>
<q-btn flat round dense icon="logout" @click="authStore.logout()"/>
</q-toolbar>
</q-header>
<q-drawer
@@ -36,7 +28,7 @@
<q-item-label
header
>
Essential Links
関連リンク
</q-item-label>
<EssentialLink
@@ -56,6 +48,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import DomainSelector from 'components/DomainSelector.vue';
import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore();
@@ -72,14 +65,14 @@ const essentialLinks: EssentialLinkProps[] = [
title: 'フローエディター',
caption: 'flowChart',
icon: 'account_tree',
link: '/#/flowEditor2',
link: '/#/FlowChart',
target:'_self'
},
{
title: 'FlowEditor',
caption: 'FlowEditor',
icon: 'account_tree',
link: '/#/flowEditor',
title: '条件エディター',
caption: 'condition',
icon: 'tune',
link: '/#/condition',
target:'_self'
},
{
@@ -159,8 +152,8 @@ const essentialLinks: EssentialLinkProps[] = [
];
const leftDrawerOpen = ref(false)
const env=process.env;
const version = process.env.version;
const productName = process.env.productName;
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value

View File

@@ -1,46 +1,54 @@
<template>
<q-page>
<div class="q-pa-sm q-gutter-sm event-tree ">
<q-drawer
side="left"
overlay
bordered
v-model="drawerLeft"
:show-if-above="false"
elevated
>
<div class="" style="padding:10px">
<div class="flex-center " >
<AppSelector />
</div>
<div class="flex-center">
<EventTree />
</div>
<div class="flex-center fixed-bottom bg-grey-3 q-pa-md row ">
<q-btn color="secondary" glossy label="デプロイ" @click="onDeploy" icon="sync"/>
<q-space></q-space>
<q-btn color="primary" label="保存" @click="onSaveFlow" icon="save" />
</div>
</div>
</q-drawer>
</div>
<div class="q-pa-md q-gutter-sm">
<div class="flowchart" v-if="store.currentFlow">
<node-item v-for="(node,) in store.currentFlow.actionNodes" :key="node.id"
:isSelected="node===state.activeNode" :actionNode="node"
@addNode="addNode"
@nodeSelected="onNodeSelected"
@nodeEdit="onNodeEdit"
@deleteNode="onDeleteNode"
@deleteAllNextNodes="onDeleteAllNextNodes"
></node-item>
<q-layout
container
class="absolute-full shadow-2 rounded-borders"
>
<div class="q-pa-sm q-gutter-sm ">
<q-drawer
side="left"
:overlay="true"
bordered
v-model="drawerLeft"
:show-if-above="false"
elevated
>
<div class="flex-center fixed-top app-selector" >
<AppSelector />
</div>
<div class="flex-center absolute-full" style="padding-top:65px;padding-left:15px;padding-right:15px;">
<q-scroll-area class="fit" :horizontal-thumb-style="{ opacity: '0' }">
<EventTree />
</q-scroll-area>
</div>
<div class="flex-center fixed-bottom bg-grey-3 q-pa-md row ">
<q-btn color="secondary" glossy label="デプロイ" @click="onDeploy" icon="sync" :loading="deployLoading" />
<q-space></q-space>
<q-btn color="primary" label="保存" @click="onSaveFlow" icon="save" :loading="saveLoading"/>
</div>
</q-drawer>
</div>
</div>
<div class="q-pa-md q-gutter-sm">
<div class="flowchart" v-if="store.currentFlow">
<node-item v-for="(node,) in store.currentFlow.actionNodes" :key="node.id"
:isSelected="node===state.activeNode" :actionNode="node"
@addNode="addNode"
@nodeSelected="onNodeSelected"
@nodeEdit="onNodeEdit"
@deleteNode="onDeleteNode"
@deleteAllNextNodes="onDeleteAllNextNodes"
></node-item>
</div>
</div>
<PropertyPanel :actionNode="state.activeNode" v-model:drawerRight="drawerRight"></PropertyPanel>
</q-layout>
<ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg" width="350px">
<action-select ref="appDg" name="model" type="single"></action-select>
</ShowDialog>
</q-page>
<PropertyPanel :actionNode="state.activeNode" v-model:drawerRight="drawerRight"></PropertyPanel>
<ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg">
<action-select ref="appDg" name="model" type="single"></action-select>
</ShowDialog>
</template>
@@ -57,7 +65,10 @@ import AppSelector from 'components/left/AppSelector.vue';
import EventTree from 'components/left/EventTree.vue';
import {FlowCtrl } from '../control/flowctrl';
import { useQuasar } from 'quasar';
const drawerLeft = ref(true);
const deployLoading = ref(false);
const saveLoading = ref(false);
const drawerLeft = ref(false);
const $q=useQuasar();
const store = useFlowEditorStore();
// ref関数を使ってtemplateとバインド
@@ -81,6 +92,9 @@ const addActionNode=(action:IActionNode)=>{
}
const addNode=(node:IActionNode,inputPoint:string)=>{
if(drawerRight.value){
drawerRight.value=false;
}
showAddAction.value=true;
prevNodeIfo.value.prevNode=node;
prevNodeIfo.value.inputPoint=inputPoint;
@@ -101,19 +115,28 @@ const onNodeEdit=(node:IActionNode)=>{
const onDeleteNode=(node:IActionNode)=>{
if(!store.currentFlow) return;
//右パネルが開いている場合、自動閉じる
if(drawerRight.value && state.activeNode.id===node.id){
drawerRight.value=false;
}
store.currentFlow?.removeNode(node);
}
const onDeleteAllNextNodes=(node:IActionNode)=>{
if(!store.currentFlow) return;
//右パネルが開いている場合、自動閉じる
if(drawerRight.value){
drawerRight.value=false;
}
store.currentFlow?.removeAllNext(node.id);
}
const closeDg=(val :any)=>{
console.log("Dialog closed->",val);
if (val == 'OK') {
const data = appDg.value.selected[0];
const actionProps=JSON.parse(data.content);
const action = new ActionNode(data.name,data.desc,"",[],actionProps);
const actionProps=JSON.parse(data.property);
const outputPoint =JSON.parse(data.outputPoints);
const action = new ActionNode(data.name,data.desc,"",outputPoint,actionProps);
store.currentFlow?.addNode(action, prevNodeIfo.value.prevNode,prevNodeIfo.value.inputPoint);
}
}
@@ -130,13 +153,17 @@ const onDeploy= async ()=>{
return;
}
try{
deployLoading.value=true;
await store.deploy();
deployLoading.value=false;
$q.notify({
type: 'positive',
caption:"通知",
message: `デプロイを成功しました。`
});
}catch(error){
console.error(error);
deployLoading.value=false;
$q.notify({
type: 'negative',
caption:"エラー",
@@ -157,13 +184,17 @@ const onSaveFlow = async ()=>{
return;
}
try{
saveLoading.value=true;
await store.saveFlow(targetFlow);
saveLoading.value=false;
$q.notify({
type: 'positive',
caption:"通知",
message: `${targetFlow.getRoot()?.subTitle}のフロー設定を保存しました。`
});
}catch(error){
console.error(error);
saveLoading.value=false;
$q.notify({
type: 'negative',
caption:"エラー",
@@ -174,8 +205,9 @@ const onSaveFlow = async ()=>{
}
const fetchData = async ()=>{
const flowCtrl = new FlowCtrl();
drawerLeft.value=true;
if(store.appInfo===undefined) return;
const flowCtrl = new FlowCtrl();
const actionFlows = await flowCtrl.getFlows(store.appInfo?.appId);
if(actionFlows && actionFlows.length>0){
store.setFlows(actionFlows);
@@ -195,16 +227,18 @@ onMounted(() => {
</script>
<style lang="scss">
.app-selector{
padding:15px;
z-index: 999;
}
.flowchart{
padding-top: 10px;
}
.flow-toolbar{
opacity: 50%;
}
.flow-container{
height: 91.5dvb;
overflow: hidden;
}
.event-tree .q-drawer {
top:50px;
z-index: 999;

View File

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

View File

@@ -22,7 +22,7 @@
<div class="q-pa-md">
<q-select v-model="model" :options="options" label="Standard"/>
<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=='アプリ'">
<app-select ref="appDg" :name="model" type="single"></app-select>
</template>

View File

@@ -1,15 +1,7 @@
<template>
<div class="q-pa-md">
<q-table
title="Treats"
:rows="rows"
:columns="columns"
row-key="id"
selection="single"
:filter="filter"
:loading="loading"
v-model:selected="selected"
>
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" selection="single" :filter="filter"
:loading="loading" v-model:selected="selected">
<template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" />
@@ -54,8 +46,8 @@
</q-form>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn label="Save" type="submit" color="primary" @click="onSubmit"/>
<q-btn label="Cancel" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()"/>
<q-btn label="Save" type="submit" color="primary" @click="onSubmit" />
<q-btn label="Cancel" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
</q-card>
@@ -70,7 +62,7 @@
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" v-close-popup @click = "deleteDomain()"/>
<q-btn flat label="OK" color="primary" v-close-popup @click="deleteDomain()" />
</q-card-actions>
</q-card>
</q-dialog>
@@ -79,12 +71,11 @@
</template>
<script setup lang="ts">
import { ref,onMounted, reactive} from 'vue';
import { useQuasar } from 'quasar'
import { ref, onMounted, reactive } from 'vue';
import { api } from 'boot/axios';
const columns = [
{ name: 'id'},
{ name: 'id' },
{
name: 'tenantid',
required: true,
@@ -95,131 +86,121 @@ const columns = [
sortable: true
},
{ name: 'name', align: 'center', label: 'Name', field: 'name', sortable: true },
{ 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: 'password', label: 'Password', field: 'password' }
]
];
const loading = ref(false)
const filter = ref('')
const rows = reactive([])
const show = ref(false);
const confirm = ref(false);
const selected = ref([])
const tenantid = ref('')
const name = ref('')
const url =ref('')
const isPwd =ref(true)
const kintoneuser =ref('')
const kintonepwd =ref('')
const loading = ref(false);
const filter = ref('');
const rows = ref([]);
const show = ref(false);
const confirm = ref(false);
const selected = ref([]);
const tenantid = ref('');
const name = ref('');
const url = ref('');
const isPwd = ref(true);
const kintoneuser = ref('');
const kintonepwd = ref('');
const $q = useQuasar()
let editId = ref(0);
const getDomain = () => {
loading.value = true;
api.get(`http://127.0.0.1:8000/api/domains/testtenant`).then(res => {
rows.length = 0;
res.data.forEach((item) => {
rows.push({ id:item.id,tenantid: item.tenantid,name: item.name, url: item.url, user: item.kintoneuser, password: item.kintonepwd });
}
)
}).finally(()=>{ loading.value = false; });
let editId = ref(0);
const getDomain = async () => {
loading.value = true;
const result= await api.get(`api/domains/1`);
rows.value= result.data.map((item)=>{
return { id: item.id, tenantid: item.tenantid, name: item.name, url: item.url, user: item.kintoneuser, password: item.kintonepwd }
});
loading.value = false;
}
onMounted(() => {
getDomain();
onMounted(async () => {
await getDomain();
})
// emulate fetching data from server
const addRow = () => {
editId.value
show.value = true;
}
const removeRow = () => {
//loading.value = true
confirm.value = true;
let row = JSON.parse(JSON.stringify(selected.value[0]));
if (selected.value.length === 0) {
return;
}
editId.value = row.id;
}
// emulate fetching data from server
const addRow = () => {
editId.value
show.value = true;
}
const deleteDomain = () => {
api.delete(`api/domain/${editId.value}`).then(() => {
getDomain();
})
editId.value = 0;
selected.value = [];
};
const removeRow = () => {
//loading.value = true
confirm.value = true;
let row = JSON.parse(JSON.stringify(selected.value[0]));
if(selected.value.length === 0)
{
return;
}
editId.value = row.id;
}
const editRow = () => {
if (selected.value.length === 0) {
return;
}
let row = JSON.parse(JSON.stringify(selected.value[0]));
editId.value = row.id;
tenantid.value = row.tenantid;
name.value = row.name;
url.value = row.url;
kintoneuser.value = row.user;
kintonepwd.value = row.password;
isPwd.value = true;
show.value = true;
};
const closeDg = () => {
show.value = false;
onReset();
}
const deleteDomain = () => {
api.delete(`http://127.0.0.1:8000/api/domain/`+ editId.value).then(() =>{
getDomain();
})
editId.value = 0;
selected.value=[];
};
const editRow = () => {
if(selected.value.length === 0)
{
return;
}
let row = JSON.parse(JSON.stringify(selected.value[0]));
editId.value = row.id;
tenantid.value = row.tenantid;
name.value = row.name;
url.value = row.url;
kintoneuser.value = row.user;
kintonepwd.value = row.password;
isPwd.value = true;
show.value = true;
};
const closeDg = () => {
show.value = false;
const onSubmit = () => {
if (editId.value !== 0) {
api.put(`api/domain`, {
'id': editId.value,
'tenantid': tenantid.value,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value
}).then(() => {
getDomain();
closeDg();
onReset();
}
})
}
else {
api.post(`api/domain`, {
'id': 0,
'tenantid': tenantid.value,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value
}).then(() => {
getDomain();
closeDg();
onReset();
})
}
selected.value = [];
}
const onSubmit = () => {
if(editId.value !== 0)
{
api.put(`http://127.0.0.1:8000/api/domain`,{
'id': editId.value,
'tenantid': tenantid.value,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value
}).then(() =>{
getDomain();
closeDg();
onReset();
})
}
else
{
api.post(`http://127.0.0.1:8000/api/domain`,{
'id': 0,
'tenantid': tenantid.value,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value
}).then(() =>{
getDomain();
closeDg();
onReset();
})
}
selected.value=[];
}
const onReset = () => {
name.value = '';
url.value = '';
kintoneuser.value = '';
kintonepwd.value ='';
isPwd.value = true;
editId.value = 0;
}
const onReset = () => {
name.value = '';
url.value = '';
kintoneuser.value = '';
kintonepwd.value = '';
isPwd.value = true;
editId.value = 0;
}
</script>

View File

@@ -150,7 +150,7 @@ export default {
</template>
</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>
</show-dialog>
@@ -179,6 +179,7 @@ import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore();
import { api } from 'boot/axios';
import { domain } from 'process';
const $q = useQuasar()
@@ -193,21 +194,14 @@ let activedomainid = ref(0);
const columns = [
{ name: 'id'},
{
name: 'name',
required: true,
label: 'Name',
align: 'left',
field: row => row.name,
sortable: true
},
{name: 'name',required: true,label: 'Name',align: 'left',field: 'name',sortable: true},
{ name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true },
{ name: 'kintonepwd' },
{ name: 'active', field: 'active'}
]
const rows = reactive([])
const rows = ref([] as any[]);
const isActive = (id:number) =>{
if(id == activedomainid.value)
@@ -223,7 +217,7 @@ const newDomain = () => {
const activeDomain = (id:number) => {
api.put(`http://127.0.0.1:8000/api/activedomain/`+ id).then(() =>{
api.put(`api/activedomain/`+ id).then(() =>{
getDomain();
})
};
@@ -234,7 +228,7 @@ const deleteConfirm = (row:object) => {
};
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();
})
editId.value = 0;
@@ -248,25 +242,21 @@ const closeDg = (val:string) => {
{
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 = () => {
api.get(`http://127.0.0.1:8000/api/activedomain`).then(res => {
activedomainid.value = res.data.id;
authStore.changedomain(res.data.name);
});
api.get(`http://127.0.0.1:8000/api/domain`).then(res => {
rows.length = 0;
res.data.forEach((item) => {
rows.push({ id:item.id,name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd});
}
)
});
const getDomain = async () => {
const resp = await api.get(`api/activedomain`);
activedomainid.value = resp.data.id;
const domainResult = await api.get(`api/domain`);
const domains = domainResult.data as any[];
rows.value=domains.map((item)=>{
return { id:item.id,name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd}
});
}
onMounted(() => {
getDomain();
onMounted(async () => {
await getDomain();
})
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>
<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>
</show-dialog>
</div>

View File

@@ -17,57 +17,60 @@ import { useAuthStore } from 'stores/useAuthStore';
* 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
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
export const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
const routerInstance = 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;
});
Router.beforeEach(async (to) => {
// clear alert on route change
//const alertStore = useAlertStore();
//alertStore.clear();
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login'];
const authRequired = !publicPages.includes(to.path);
const authStore = useAuthStore();
if (authRequired && !authStore.token) {
authStore.returnUrl = to.fullPath;
return '/login';
}
// 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 } */) {
routerInstance.beforeEach(async (to) => {
// clear alert on route change
//const alertStore = useAlertStore();
//alertStore.clear();
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login'];
const authRequired = !publicPages.includes(to.path);
const authStore = useAuthStore();
if (authRequired && !authStore.token) {
authStore.returnUrl = to.fullPath;
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

@@ -14,13 +14,13 @@ const routes: RouteRecordRaw[] = [
{ path: 'ruleEditor', component: () => import('pages/RuleEditor.vue') },
{ path: 'test', component: () => import('pages/testQursar.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: 'flowEditor2', component: () => import('pages/FlowChart.vue') },
{ path: 'flowChart2', component: () => import('pages/FlowEditorPage2.vue') },
{ path: 'FlowChart', component: () => import('pages/FlowChart.vue') },
{ path: 'right', component: () => import('pages/testRight.vue') },
{ path: 'domain', component: () => import('pages/TenantDomain.vue') },
{ path: 'userdomain', component: () => import('pages/UserDomain.vue') }
{ path: 'userdomain', component: () => import('pages/UserDomain.vue')},
{ path: 'condition', component: () => import('pages/conditionPage.vue') }
],
},
// Always leave this as last one,

View File

@@ -1,20 +1,26 @@
import { defineStore } from 'pinia';
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;
}
export const useAuthStore = defineStore({
id: 'auth',
state: () =>{
const token=localStorage.getItem('token');
if(token && token!==''){
state: ():IUserState =>{
const token=localStorage.getItem('token')||'';
if(token!==''){
api.defaults.headers["Authorization"]='Bearer ' + token;
}
return {
token: token,
name:localStorage.getItem('name'),
domain:localStorage.getItem('domain'),
returnUrl: ''
token,
returnUrl: '',
currentDomain: JSON.parse(localStorage.getItem('currentDomain')||"{}")
}
},
actions: {
@@ -23,18 +29,14 @@ export const useAuthStore = defineStore({
params.append('username', username);
params.append('password', password);
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);
this.token =result.data.access_token;
this.name = result.data.user_name;
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;
const activedomain = await api.get(`http://127.0.0.1:8000/api/activedomain`);
this.domain = activedomain.data.name;
localStorage.setItem('domain', activedomain.data.name);
Router.push(this.returnUrl || '/');
this.currentDomain=await this.getCurrentDomain();
localStorage.setItem('currentDomain',JSON.stringify(this.currentDomain));
this.router.push(this.returnUrl || '/');
return true;
}catch(e)
{
@@ -42,19 +44,38 @@ export const useAuthStore = defineStore({
return false;
}
},
logout() {
this.token = null;
localStorage.removeItem('token');
localStorage.removeItem('name');
localStorage.removeItem('domain');
Router.push('/login');
async getCurrentDomain():Promise<IDomainInfo>{
const activedomain = await api.get(`api/activedomain`);
return {
id:activedomain.data.id,
domainName:activedomain.data.name,
kintoneUrl:activedomain.data.url
}
},
userdomain() {
Router.push('/userdomain');
},
changedomain(domain:string){
this.domain = domain;
localStorage.setItem('domain', domain);
}
async getUserDomains():Promise<IDomainInfo[]>{
const resp = await api.get(`api/domain`);
const domains =resp.data as any[];
return domains.map(data=>{
return {
id:data.id,
domainName:data.name,
kintoneUrl:data.url
}
});
},
logout() {
this.token = undefined;
localStorage.removeItem('token');
localStorage.removeItem('currentDomain');
router.push('/login');
},
async setCurrentDomain(domain:IDomainInfo){
if(domain.id===this.currentDomain.id){
return;
}
await api.put(`api/activedomain/${domain.id}`);
this.currentDomain=domain;
localStorage.setItem('currentDomain',JSON.stringify(this.currentDomain));
}
}
});

View File

@@ -1,4 +1,10 @@
import { v4 as uuidv4 } from 'uuid';
export interface IDomainInfo{
id:number;
domainName:string;
kintoneUrl:string;
}
/**
* アプリ情報
*/
@@ -20,6 +26,8 @@ export interface IActionProperty {
//プロパティ表示名
displayName: string;
placeholder: string;
//入力提示・説明
hint:string;
//プロパティ設定値
modelValue: any;
};
@@ -71,12 +79,13 @@ class ActionProperty implements IActionProperty {
// プロパティ表示名
displayName: string;
placeholder: string;
hint:string;
// プロパティ設定値
modelValue: any;
};
static defaultProperty(): IActionProperty {
return new ActionProperty('InputText', 'displayName', '表示名', '表示を入力してください', '');
return new ActionProperty('InputText', 'displayName', '表示名', '表示を入力してください', '','');
};
constructor(
@@ -84,6 +93,7 @@ class ActionProperty implements IActionProperty {
name: string,
displayName: string,
placeholder: string,
hint:string,
modelValue: any
) {
this.component = component;
@@ -91,6 +101,7 @@ class ActionProperty implements IActionProperty {
name: name,
displayName: displayName,
placeholder: placeholder,
hint:hint,
modelValue: modelValue
};
}

View File

@@ -0,0 +1,336 @@
//ノード種別
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 class ConditionNode implements INode {
index: number;
type: NodeType;
parent:INode;
get logicalOperator(): LogicalOperator{
return this.parent.logicalOperator;
};
object: any; // 比較元
operator: Operator; // 比較子
value: any;
get header():string{
return 'generic';
}
constructor(object: any, operator: Operator, 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;
constructor() {
this.maxIndex=0;
this.root = new GroupNode(LogicalOperator.AND, null);
}
// ノード追加
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 `${condNode.object.name} ${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(this.root, (key, value) => {
if (key === 'parent') {
return value ? value.type : null;
}
return value;
});
}
}

View File

@@ -12,6 +12,8 @@
},
"devDependencies": {
"@types/jquery": "^3.5.24",
"@types/node": "^20.8.9",
"sass": "^1.69.5",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
@@ -377,12 +379,82 @@
"@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": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz",
"integrity": "sha512-tAe4Q+OLFOA/AMD+0lq8ovp8t3ysxAOeaScnfNdZpUxaGl51ZMDEITxkvFl1STudQ58mz6gzVGl9VhMKhwRnZQ==",
"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": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
@@ -420,6 +492,18 @@
"@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": {
"version": "2.3.3",
"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_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": {
"version": "3.7.1",
"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_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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"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": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -491,6 +656,18 @@
"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": {
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
@@ -507,6 +684,23 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@@ -516,6 +710,18 @@
"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": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
@@ -529,6 +735,12 @@
"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": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",

View File

@@ -6,10 +6,13 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build && xcopy dist\\*.js ..\\..\\backend\\Temp\\ /E /I /Y",
"build:dev":"tsc && set \"SOURCE_MAP=true\" && vite build && xcopy dist\\*.js ..\\..\\backend\\Temp\\ /E /I /Y",
"preview": "vite preview"
},
"devDependencies": {
"@types/jquery": "^3.5.24",
"@types/node": "^20.8.9",
"sass": "^1.69.5",
"typescript": "^5.0.2",
"vite": "^4.4.5"
},

View File

@@ -0,0 +1,266 @@
# 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 | 対象項目を選択してください| 入力フィールドに表示されるプレースホルダーのテキストです。この場合は設定されていません。 |
### 使用可能なコンポーネント
| No. | コンポーネント名 | コンポーネントタイプ | 説明 |
|-----|------------------|------------------|-----------------------------------------|
| 1 | テキストボックス | InputText | 一行のテキスト入力が可能なフィールドです。 |
| 2 | テキストボックス(改行可能) | MuiltInputText | 複数行のテキスト入力が可能なテキストエリアです。 |
| 3 | 日付 | DatePicker | 日付を選択するためのカレンダーコンポーネントです。 |
| 4 | フィールド選択 | FieldInput | システムのフィールドを選択するための入力コンポーネントです。 |
| 5 | 選択リスト | SelectBox | 複数のオプションから選択するためのドロップダウンリストです。 |
| 6 | 条件式設定 | ConditionInput | 条件式やロジックを入力するためのコンポーネントです。 |
| 7 | 色選択 | ColorPicker | 色を設定する(追加予定中) |
| 8 | 他のアプリのフィールド選択 | AppFieldPicker | 他のアプリのフィールドを選択する(追加予定中) |
| 9 |ユーザー選択 | 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 にアクセスしてアプリケーションが正常に動作しているか確認します。

View File

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

View File

@@ -15,12 +15,12 @@ declare const alcflow : {
$(function (){
const events=Object.keys(alcflow);
kintone.events.on(events,(event:any)=>{
kintone.events.on(events,async (event:any)=>{
const flowinfo = alcflow[event.type];
const flow=ActionFlow.fromJSON(flowinfo.content);
if(flow!==undefined){
const process = new ActionProcess(event.type,flow,event);
process.exec();
await process.exec();
}
return event;
});

View File

@@ -61,13 +61,36 @@ export interface IActionResult{
result?:any;
}
/**
* コンテキスト
* レコードとフローの変数を持つ
*/
export interface IContext{
record:any,
variables:any
}
/**
* アクションのインターフェース
*/
export interface IAction{
//アクションの名前(ユーニック名が必要)
name:string;
//属性設定情報
actionProps: Array<IActionProperty>;
process(prop:IActionNode,event:any):IActionResult;
//アクションのプロセス実行関数
process(prop:IActionNode,event:any,context:IContext):Promise<IActionResult>;
//アクションの登録関数
register():void;
}
export interface IField{
name?:string;
code:string;
type?:string;
}
/**
* アクションのプロパティ定義に基づいたクラス
*/

View File

@@ -0,0 +1,473 @@
import { IContext } from "./ActionTypes";
//ノード種別
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 class ConditionNode implements INode {
index: number;
type: NodeType;
parent:INode;
get logicalOperator(): LogicalOperator{
return this.parent.logicalOperator;
};
object: any; // 比較元
operator: Operator; // 比較子
value: any;
get header():string{
return 'generic';
}
constructor(object: any, operator: Operator, 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;
constructor() {
this.maxIndex=0;
this.root = new GroupNode(LogicalOperator.AND, null);
}
/**
* ノード追加
* @param parent
* @param node
*/
addNode(parent: GroupNode, node: INode): void {
this.maxIndex++;
node.index=this.maxIndex;
parent.children.push(node);
}
/**
* ノード削除
* @param 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);
}
}
}
/**
* 条件ツリーからインディクス値で条件ノードを検索する
* @param index
* @returns
*/
findByIndex(index:number):INode|undefined{
return this.findChildren(this.root,index);
}
/**
* 指定のノードグループからインディクス値で条件ノードを検索する
* @param parent
* @param index
* @returns
*/
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;
}
/**
*
* @param node 最大のインディクス値を取得する
* @returns
*/
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;
}
/**
* 条件式を表示する
* @param node 条件ノード
* @returns
*/
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 `${condNode.object.name} ${condNode.operator} '${value}'`;
} else {
return '';
}
}
}
/**
* すべて条件式を取得する
*/
getConditions(parent:GroupNode):ConditionNode[]{
const condiNodes:ConditionNode[]=[];
parent.children.forEach((node)=>{
if(node.type===NodeType.Condition){
condiNodes.push(node as ConditionNode);
}else{
condiNodes.push(...this.getConditions(node as GroupNode));
}
});
return condiNodes;
}
/**
*
* @param parent すべて条件グループを取得する
* @returns
*/
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;
}
/**
* Jsonから復元
* @param jsonString
* @returns
*/
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;
}
/**
* JSON文字列に変換する
* @returns
*/
toJson():string{
return JSON.stringify(this.root, (key, value) => {
if (key === 'parent') {
return value ? value.type : null;
}
return value;
});
}
/**
* 条件式を計算する
* @param node
* @param context
* @returns
*/
evaluate(node: INode, context: any): boolean {
if (node.type === NodeType.Condition) {
return this.evaluateCondition(node as ConditionNode, context);
} else if (node.type === NodeType.LogicGroup || node.type === NodeType.Root) {
const groupNode = node as GroupNode;
const results = groupNode.children.map(child => this.evaluate(child, context));
if (groupNode.logicalOperator === LogicalOperator.AND) {
return results.every(result => result);
} else if (groupNode.logicalOperator === LogicalOperator.OR) {
return results.some(result => result);
} else {
throw new Error('Unsupported logical operator');
}
} else {
throw new Error('Unsupported node type');
}
}
/**
* Condition objectの値を取得する
* @param object
* @param context
* @returns
*/
getObjectValue(object:any,context:IContext){
if(!object || typeof object!=="object" || !("objectType" in object)){
return object;
}
if(object.objectType==='field'){
return context.record[object.code].value;
}else if(object.objectType==='var'){
return context.variables[object.varName].value;
}
}
/**
* 比較オブジェクトの値を取得
* @param object
* @returns
*/
getConditionValue(object:any){
if(!object || typeof object!=="object"){
return object;
}
if("label" in object){
return object.label;
}
if("value" in object){
return object.value;
}
if("name" in object){
return object.name;
}
}
/**
* 条件式を計算する
* @param condition
* @param context
* @returns
*/
evaluateCondition(condition: ConditionNode, context: IContext): boolean {
const { object, operator, value } = condition;
const targetValue = this.getObjectValue(object,context);
const conditionValue = this.getConditionValue(value);
switch (operator) {
case Operator.Equal:
case Operator.NotEqual:
case Operator.Greater:
case Operator.GreaterOrEqual:
case Operator.Less:
case Operator.LessOrEqual:
return this.compare(operator,targetValue, conditionValue);
case Operator.Contains:
return this.contains(targetValue,conditionValue);
case Operator.NotContains:
return !this.contains(targetValue,conditionValue);
case Operator.StartWith:
return this.startWith(targetValue,conditionValue);
case Operator.NotStartWith:
return !this.startWith(targetValue,conditionValue);
case Operator.EndWith:
return this.endsWith(targetValue,conditionValue);
case Operator.NotEndWith:
return this.endsWith(targetValue,conditionValue);
default:
throw new Error('Unsupported operator');
}
}
/**
* 比較を実行する
* @param operator
* @param targetValue
* @param value
* @returns
*/
compare(operator: Operator, targetValue: any, value: any): boolean {
// targetValue は日期时value も日期に変換して比較する
if (targetValue instanceof Date) {
const dateValue = new Date(value);
if (!isNaN(dateValue.getTime())) {
value = dateValue;
}
}
//targetValueは数値時value を数値に変換する
else if (typeof targetValue === 'number') {
const numberValue = Number(value);
if (!isNaN(numberValue)) {
value = numberValue;
}
}
else if (typeof targetValue === 'string') {
value = String(value);
}
switch (operator) {
case Operator.Equal:
return targetValue === value;
case Operator.NotEqual:
return targetValue !== value;
case Operator.Greater:
return targetValue > value;
case Operator.GreaterOrEqual:
return targetValue >= value;
case Operator.Less:
return targetValue < value;
case Operator.LessOrEqual:
return targetValue <= value;
default:
throw new Error('Unsupported operator for comparison');
}
}
/**
* 含む計算
* @param targetValue
* @param value
* @returns
*/
contains(targetValue: any, value: any): boolean {
if (typeof targetValue === 'string' && typeof value === 'string') {
return targetValue.includes(value);
}
return false;
}
/**
* StartWith計算
* @param targetValue
* @param value
* @returns
*/
startWith(targetValue: any, value: any): boolean {
if (typeof targetValue === 'string' && typeof value === 'string') {
return targetValue.startsWith(value);
}
return false;
}
/**
* EndsWith計算
* @param targetValue
* @param value
* @returns
*/
endsWith(targetValue: any, value: any): boolean {
if (typeof targetValue === 'string' && typeof value === 'string') {
return targetValue.endsWith(value);
}
return false;
}
}

View File

@@ -1,18 +1,25 @@
import { actionAddins } from "../actions";
import '../actions/must-input';
import { ActionFlow,IActionFlow, IActionResult } from "./ActionTypes";
import '../actions/auto-numbering';
import '../actions/field-shown';
import { ActionFlow,IActionFlow, IActionResult,IContext } from "./ActionTypes";
export class ActionProcess{
eventId:string;
flow:IActionFlow;
event:any;
context:IContext;
constructor(eventId:string,flow:ActionFlow,event:any){
this.eventId=eventId;
this.flow=flow;
this.event=event;
this.context={
record:this.event.record,
variables:{}
};
}
exec(){
async exec(){
const root = this.flow.getRoot();
if(root===undefined || root.nextNodeIds.size===0){
return;
@@ -26,7 +33,7 @@ export class ActionProcess{
while(nextAction!==undefined && result.canNext){
const action = actionAddins[nextAction.name];
if(action!==undefined){
result = action.process(nextAction,this.event)
result = await action.process(nextAction,this.event,this.context);
}
const nextInput = nextAction.outputPoints!==undefined?result.result||'':'';
id=nextAction.nextNodeIds.get(nextInput);

View File

@@ -0,0 +1,71 @@
export class Formatter{
static numberFormat(num:number,format:string):string{
let integerPart = Math.floor(Math.abs(num)).toString();
let fractionPart = Math.abs(num).toString().split('.')[1] || '';
let isNegative = num < 0;
let isPercent = format.includes('%');
// %
if (isPercent) {
num *= 100;
integerPart = Math.floor(Math.abs(num)).toString();
fractionPart = Math.abs(num).toString().split('.')[1] || '';
}
// 小数と整数部分の処理
let [integerFormat, fractionFormat] = format.split('.');
integerPart = integerFormat ? integerPart.padStart(integerFormat.replace(/[^0]/g, '').length, '0') : integerPart;
fractionPart = fractionPart.padEnd(fractionFormat ? fractionFormat.length : 0, '0');
// カマ区切
if (/,/.test(integerFormat)) {
const parts = [];
while (integerPart.length) {
parts.unshift(integerPart.slice(-3));
integerPart = integerPart.slice(0, -3);
}
integerPart = parts.join(',');
}
// すべて組合わせ
let result = fractionFormat ? `${integerPart}.${fractionPart}` : integerPart;
result = isNegative ? `-${result}` : result;
return isPercent ? `${result}%` : result;
}
static dateFormat(date: Date, format: string): string {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const second = date.getSeconds();
const millisecond = date.getMilliseconds();
const timeZoneOffset = -date.getTimezoneOffset() / 60;
const replacements = {
'yyyy': year.toString(),
'yy': year.toString().slice(-2),
'MM': month.toString().padStart(2, '0'),
'M': month.toString(),
'dd': day.toString().padStart(2, '0'),
'd': day.toString(),
'HH': hour.toString().padStart(2, '0'),
'H': hour.toString(),
'hh': (hour > 12 ? hour - 12 : hour).toString().padStart(2, '0'),
'h': (hour > 12 ? hour - 12 : hour).toString(),
'mm': minute.toString().padStart(2, '0'),
'm': minute.toString(),
'ss': second.toString().padStart(2, '0'),
's': second.toString(),
'fff': millisecond.toString().padStart(3, '0'),
'zzz': (timeZoneOffset >= 0 ? '+' : '-') + Math.abs(timeZoneOffset).toString().padStart(2, '0') + ':00'
};
return format.replace(/yyyy|yy|MM|M|dd|d|HH|H|hh|h|mm|m|ss|s|fff|zzz/g, (match) => {
return replacements[match as keyof typeof replacements] || match;
});
}
}

View File

@@ -1,13 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"lib": ["esnext", "dom"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
@@ -17,7 +17,8 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
},
"include": ["src"]
}

View File

@@ -1,14 +1,16 @@
// vite.config.js
import { defineConfig } from 'vite'
const sourcemap = process.env.SOURCE_MAP==='true';
export default defineConfig({
build: {
rollupOptions: {
input: 'src/index.ts', // entry file
output:{
entryFileNames:'alc_runtime.js'
entryFileNames:'alc_runtime.js',
// assetFileNames:'alc_kintone_style.css'
}
},
sourcemap:true
sourcemap:sourcemap
}
})

View File

@@ -2,131 +2,68 @@
# yarn lockfile v1
"@esbuild/android-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
"@esbuild/android-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
"@esbuild/android-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
"@esbuild/darwin-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
"@esbuild/darwin-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
"@esbuild/freebsd-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
"@esbuild/freebsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
"@esbuild/linux-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
"@esbuild/linux-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
"@esbuild/linux-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
"@esbuild/linux-loong64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
"@esbuild/linux-mips64el@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
"@esbuild/linux-ppc64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
"@esbuild/linux-riscv64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
"@esbuild/linux-s390x@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
"@esbuild/linux-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
"@esbuild/netbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
"@esbuild/openbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
"@esbuild/sunos-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
"@esbuild/win32-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
"@esbuild/win32-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
"@esbuild/win32-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
"@types/jquery@^3.5.24":
version "3.5.25"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.25.tgz#c817c71d414855f7d71f46da39f43e6b9579b0b9"
integrity sha512-gykx2c+OZf5nx2tv/5fDQqmvGgTiXshELy5jf9IgXPtVfSBl57IUYByN4osbwMXwJijWGOEYQABzGaFZE79A0Q==
version "3.5.24"
resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.24.tgz"
integrity sha512-V/TG69ge5amcr8Ap7vY3SObqKfZlV7ttqcYnNcYnndI77ySIRi05+3GjvfwRtE2qalAC2ySLIL1ker512sI20g==
dependencies:
"@types/sizzle" "*"
"@types/node@^20.8.9", "@types/node@>= 14":
version "20.11.0"
resolved "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz"
integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==
dependencies:
undici-types "~5.26.4"
"@types/sizzle@*":
version "2.3.5"
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.5.tgz#d93dd29cdcd5801d90be968073b09a6b370780e4"
resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz"
integrity sha512-tAe4Q+OLFOA/AMD+0lq8ovp8t3ysxAOeaScnfNdZpUxaGl51ZMDEITxkvFl1STudQ58mz6gzVGl9VhMKhwRnZQ==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
"chokidar@>=3.0.0 <4.0.0":
version "3.5.3"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
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"
optionalDependencies:
fsevents "~2.3.2"
esbuild@^0.18.10:
version "0.18.20"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz"
integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==
optionalDependencies:
"@esbuild/android-arm" "0.18.20"
@@ -152,55 +89,131 @@ esbuild@^0.18.10:
"@esbuild/win32-ia32" "0.18.20"
"@esbuild/win32-x64" "0.18.20"
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
immutable@^4.0.0:
version "4.3.4"
resolved "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz"
integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
jquery@^3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"
resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz"
integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==
nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.1"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
postcss@^8.4.27:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
rollup@^3.27.1:
version "3.29.4"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981"
resolved "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz"
integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==
optionalDependencies:
fsevents "~2.3.2"
source-map-js@^1.0.2:
sass@*, sass@^1.69.5:
version "1.69.7"
resolved "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz"
integrity sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
source-map-js@^1.0.2, "source-map-js@>=0.6.2 <2.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
to-regex-range@^5.0.1:
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==
dependencies:
is-number "^7.0.0"
typescript@^5.0.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
vite@^4.4.5:
version "4.5.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26"
resolved "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz"
integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==
dependencies:
esbuild "^0.18.10"

View File

@@ -1,20 +1,33 @@
[
{
"component": "FieldInput",
"props": {
"displayName": "フィールド",
"modelValue": "",
"name": "field",
"placeholder": "必須項目を選択してください"
}
},
{
"component": "MuiltInputText",
"props": {
"displayName": "エラーメッセージ",
"modelValue": "",
"name": "message",
"placeholder": "エラーメッセージを入力してください"
}
{
"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": "対象項目を選択してください"
}
}
]

59
sample2.json Normal file
View File

@@ -0,0 +1,59 @@
{
"index": 0,
"type": "root",
"children": [
{
"index": 1,
"type": "condition",
"parent": "root",
"logicalOperator": "AND",
"object": {
"label": "Field 1",
"value": "field1"
},
"operator": "=",
"value": "1"
},
{
"index": 2,
"type": "condition",
"parent": "root",
"logicalOperator": "AND",
"object": {
"label": "Field 1",
"value": "field1"
},
"operator": "=",
"value": "2"
},
{
"index": 3,
"type": "condition",
"parent": "root",
"logicalOperator": "AND",
"object": {
"label": "Field 1",
"value": "field1"
},
"operator": {
"label": ">",
"value": "Greater"
},
"value": "3"
},
{
"index": 4,
"type": "condition",
"parent": "root",
"logicalOperator": "AND",
"object": {
"label": "Field 1",
"value": "field1"
},
"operator": "=",
"value": "4"
}
],
"parent": null,
"logicalOperator": "AND"
}