Compare commits

..

2 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
147 changed files with 3669 additions and 18075 deletions

4
.gitignore vendored
View File

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

1
backend/.gitignore vendored
View File

@@ -56,7 +56,6 @@ coverage.xml
# Django stuff:
*.log
*.log.*
local_settings.py
db.sqlite3
db.sqlite3-journal

File diff suppressed because one or more lines are too long

View File

@@ -31,6 +31,8 @@ def getkintoneformat():
def createkintonefields(property,value,trueformat):
if(type(value) == str):
value = value.replace("\"","⊡⊡")
p = []
if(property=="options"):
o=[]
@@ -148,7 +150,7 @@ def getfieldsfromexcel(df,mapping):
col.append(f"\"{df.iloc[row,codecolumn]}\":{{{','.join(p)}}}")
fields = ",".join(col).replace("\\", "\\\\")
fields = ",".join(col).replace("\\", "\\\\").replace("⊡⊡","\\\"")
return json.loads(f"{{{fields}}}")
def getsettingfromexcel(df):
@@ -156,10 +158,10 @@ def getsettingfromexcel(df):
des = df.iloc[2,2]
return {"name":appname,"description":des}
def getsettingfromkintone(app:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
def getsettingfromkintone(app:str,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"app":app}
url = f"{env.BASE_URL}{config.API_V1_STR}/app/settings.json"
url = f"{c.BASE_URL}{config.API_V1_STR}/app/settings.json"
r = httpx.get(url,headers=headers,params=params)
return r.json()
@@ -171,100 +173,59 @@ def analysesettings(excel,kintone):
updatesettings[key] = excel[key]
return updatesettings
def createkintoneapp(name:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
def createkintoneapp(name:str,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
data = {"name":name}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app.json"
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()
def updateappsettingstokintone(app:str,updates:dict,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/settings.json"
def updateappsettingstokintone(app:str,updates: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/settings.json"
data = {"app":app}
data.update(updates)
r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json()
def addfieldstokintone(app:str,fields:dict,env:config.KINTONE_ENV,revision:str = None):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
def addfieldstokintone(app:str,fields:dict,c:config.KINTONE_ENV,revision:str = None):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
if revision != None:
data = {"app":app,"revision":revision,"properties":fields}
else:
data = {"app":app,"properties":fields}
r = httpx.post(url,headers=headers,data=json.dumps(data))
r.raise_for_status()
return r.json()
def updatefieldstokintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
def updatefieldstokintone(app:str,revision:str,fields:dict,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
data = {"app":app,"properties":fields}
r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json()
def deletefieldsfromkintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
def deletefieldsfromkintone(app:str,revision:str,fields:dict,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
params = {"app":app,"revision":revision,"fields":fields}
#r = httpx.delete(url,headers=headers,content=json.dumps(params))
r = httpx.request(method="DELETE",url=url,headers=headers,content=json.dumps(params))
return r.json()
def deoployappfromkintone(app:str,revision:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json"
def deoployappfromkintone(app:str,revision:str,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/deploy.json"
data = {"apps":[{"app":app,"revision":revision}],"revert": False}
r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json
# 既定項目に含めるアプリのフィールドのみ取得する
# スペース、枠線、ラベルを含まない
def getfieldsfromkintone(app:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params = {"app":app}
url = f"{env.BASE_URL}{config.API_V1_STR}/app/form/fields.json"
r = httpx.get(url,headers=headers,params=params)
return r.json()
# フォームに配置するフィールドのみ取得する
# スペース、枠線、ラベルも含める
def getformfromkintone(app:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
params = {"app":app}
url = f"{env.BASE_URL}{config.API_V1_STR}/form.json"
r = httpx.get(url,headers=headers,params=params)
return r.json()
def merge_kintone_fields(fields_response: dict, form_response: dict) -> dict:
fields_properties = fields_response.get('properties', {})
form_properties = form_response.get('properties', [])
merged_properties = {k: v for k, v in fields_properties.items()}
for index, form_field in enumerate(form_properties):
code = form_field.get('code')
if code:
if code and code not in merged_properties:
merged_properties[code] = form_field
else:
element_id = form_field.get('elementId')
if element_id:
key = element_id
form_field['code']=element_id
form_field['label']=form_field.get('type')
# else:
# key = f"{form_field.get('type')}_{index}"
merged_properties[key] = form_field
merged_response = {
'revision': fields_response.get('revision', ''),
'properties': merged_properties
}
return merged_response
def getfieldsfromkintone(app:str,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"app":app}
url = f"{c.BASE_URL}{config.API_V1_STR}/app/form/fields.json"
r = httpx.get(url,headers=headers,params=params)
return r.json()
def analysefields(excel,kintone):
updatefields={}
@@ -286,10 +247,10 @@ def analysefields(excel,kintone):
return {"update":updatefields,"add":addfields,"del":delfields}
def getprocessfromkintone(app:str,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
def getprocessfromkintone(app:str,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"app":app}
url = f"{env.BASE_URL}{config.API_V1_STR}/app/status.json"
url = f"{c.BASE_URL}{config.API_V1_STR}/app/status.json"
r = httpx.get(url,headers=headers,params=params)
return r.json()
@@ -374,95 +335,49 @@ def getkintoneorgs(c:config.KINTONE_ENV):
r = httpx.get(url,headers=headers,params=params)
return r.json()
def uploadkintonefiles(file,env:config.KINTONE_ENV):
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:env.API_V1_AUTH_VALUE}
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
data ={'name':'file','filename':os.path.basename(file)}
url = f"{env.BASE_URL}/k/v1/file.json"
r = httpx.post(url,headers=headers,data=data,files=upload_files)
#{"name":data['filename'],'fileKey':r['fileKey']}
url = f"{c.BASE_URL}/k/v1/file.json"
r = httpx.post(url,headers=headers,data=data,files=upload_files)
return r.json()
def updateappjscss(app,uploads,env:config.KINTONE_ENV):
def updateappjscss(app,uploads,c:config.KINTONE_ENV):
dsjs = []
dscss = []
#mobile側
mbjs = []
mbcss = []
customize = getappcustomize(app, env)
current_js = customize['desktop'].get('js', [])
current_css = customize['desktop'].get('css', [])
current_mobile_js = customize['mobile'].get('js', [])
current_mobile_css = customize['mobile'].get('css', [])
current_js = [item for item in current_js if not (item.get('type') == 'URL' and item.get('url', '').endswith('alc_runtime.js'))]
for upload in uploads:
for key in upload:
filename = os.path.basename(key)
if key.endswith('.js'):
existing_js = next((item for item in current_js
if item.get('type') == 'FILE' and item['file']['name'] == filename
), None)
if existing_js:
current_js = [item for item in current_js if item.get('type') == 'URL' or item['file'].get('name') != filename]
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
else:
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
else:
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
elif key.endswith('.css'):
existing_css = next((item for item in current_css
if item.get('type') == 'FILE' and item['file']['name'] == filename
), None)
if existing_css:
current_css = [item for item in current_css if item.get('type') == 'URL' or item['file'].get('name') != filename]
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
else:
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
#現在のJSとCSSがdsjsに追加する
dsjs.extend(current_js)
dscss.extend(current_css)
mbjs.extend(current_mobile_js)
mbcss.extend(current_mobile_css)
dscss.append({'type':'FILE','file':{'fileKey':upload[key]}})
ds ={'js':dsjs,'css':dscss}
mb ={'js':mbjs,'css':mbcss}
data = {'app':app,'scope':'ALL','desktop':ds,'mobile':mb,'revision':customize["revision"]}
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
print(json.dumps(data))
mb ={'js':[],'css':[]}
data = {'app':app,'scope':'ALL','desktop':ds,'mobile':mb}
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
print(data)
r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json()
#kintone カスタマイズ情報
def getappcustomize(app,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
params = {"app":app}
r = httpx.get(url,headers=headers,params=params)
return r.json()
def getTempPath(filename):
scriptdir = Path(__file__).resolve().parent
rootdir = scriptdir.parent.parent.parent.parent
fpath = os.path.join(rootdir,"Temp",filename)
return fpath
def createappjs(domain_url,app):
def createappjs(domainid,app):
db = SessionLocal()
flows = get_flows_by_app(db,domain_url,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 alcflow=' + json.dumps(content)
# scriptdir = Path(__file__).resolve().parent
# rootdir = scriptdir.parent.parent.parent.parent
# fpath = os.path.join(rootdir,"Temp",f"alc_setting_{app}.js")
fpath = getTempPath(f"alc_setting_{app}.js")
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)
@@ -521,7 +436,7 @@ async def upload(request:Request,files:t.List[UploadFile] = File(...)):
return {"files": [file.filename for file in files]}
@r.post("/updatejscss")
async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env:config.KINTONE_ENV = Depends(getkintoneenv)):
async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env = Depends(getkintoneenv)):
try:
jscs=[]
for file in files:
@@ -542,87 +457,66 @@ async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env
raise APIException('kintone:updatejscss',request.url._url, f"Error occurred while update js/css {file.filename} is not an Excel file",e)
@r.get("/app")
async def app(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
async def app(request:Request,app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{env.BASE_URL}{config.API_V1_STR}/app.json"
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({env.DOMAIN_NAME}->{app}):",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(request:Request,env:config.KINTONE_ENV=Depends(getkintoneenv)):
async def allapps(request:Request,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{env.BASE_URL}{config.API_V1_STR}/apps.json"
offset = 0
limit = 100
all_apps = []
while True:
r = httpx.get(f"{url}?limit={limit}&offset={offset}", headers=headers)
json_data = r.json()
apps = json_data.get("apps",[])
all_apps.extend(apps)
if len(apps)<limit:
break
offset += limit
return {"apps": all_apps}
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({env.DOMAIN_NAME}):", e)
raise APIException('kintone:allapps',request.url._url, f"Error occurred while get allapps({c.DOMAIN_NAM}):",e)
@r.get("/appfields")
async def appfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
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_NAME}->{app}):",e)
@r.get("/allfields")
async def allfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
try:
field_resp = getfieldsfromkintone(app,env)
form_resp = getformfromkintone(app,env)
return merge_kintone_fields(field_resp,form_resp)
except Exception as e:
raise APIException('kintone:allfields',request.url._url, f"Error occurred while get form fileds({env.DOMAIN_NAME}->{app}):",e)
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(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
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_NAME}->{app}):",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(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
async def alljscs(request:Request,app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{env.BASE_URL}{config.API_V1_STR}/app/customize.json"
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({env.DOMAIN_NAME}->{app}):",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(request:Request,name:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
async def createapp(request:Request,name:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
data = {"name":name}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app.json"
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"{env.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json"
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({env.DOMAIN_NAME}->{name}):",e)
raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({c.DOMAIN_NAM}->{name}):",e)
@r.post("/createappfromexcel",)
@@ -652,7 +546,7 @@ async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...
if app.get("revision") != None:
result["revision"] = app["revision"]
app = addfieldstokintone(result["app"],fields,env)
if len(processes)> 0:
if len(processes["states"])> 0:
app = updateprocesstokintone(result["app"],processes,env)
if app.get("revision") != None:
result["revision"] = app["revision"]
@@ -751,7 +645,7 @@ async def updateprocessfromexcel(request:Request,app:str,env = Depends(getkinton
if deploy:
result = deoployappfromkintone(app,revision,env)
except Exception as e:
raise APIException('kintone:updateprocessfromexcel',request.url._url, f"Error occurred while update process ({env.DOMAIN_NAME}->{app}):",e)
raise APIException('kintone:updateprocessfromexcel',request.url._url, f"Error occurred while update process ({env.DOMAIN_NAM}->{app}):",e)
return result
@@ -761,17 +655,15 @@ async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Dep
try:
jscs=[]
files=[]
files.append(createappjs(env.BASE_URL, app))
files.append(getTempPath('alc_runtime.js'))
files.append(getTempPath('alc_runtime.css'))
files.append(createappjs(env.DOMAIN_ID, app))
files.append('Temp\\alc_runtime.js')
for file in files:
upload = uploadkintonefiles(file,env)
if upload.get('fileKey') != None:
print(upload)
jscs.append({ file :upload['fileKey']})
appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None:
deoployappfromkintone(app,appjscs["revision"],env)
return appjscs
except Exception as e:
raise APIException('kintone:createjstokintone',request.url._url, f"Error occurred while create js ({env.DOMAIN_NAME}->{app}):",e)
raise APIException('kintone:createjstokintone',request.url._url, f"Error occurred while create js ({env.DOMAIN_NAM}->{app}):",e)

View File

@@ -1,74 +1,14 @@
from http import HTTPStatus
from fastapi import Query, Request,Depends, APIRouter, UploadFile,HTTPException,File
from fastapi.responses import JSONResponse
# from app.core.operation import log_operation
from fastapi import Request,Depends, APIRouter, UploadFile,HTTPException,File
from app.db import Base,engine
from app.db.session import get_db
from app.db.crud import *
from app.db.schemas import *
from typing import List, Optional
from typing import List
from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
import httpx
import app.core.config as config
platform_router = r = APIRouter()
@r.get(
"/apps",
response_model=List[AppList],
response_model_exclude_none=True,
)
async def apps_list(
request: Request,
user = Depends(get_current_user),
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
platformapps = get_apps(db,domain.url)
kintoneevn = config.KINTONE_ENV(domain)
headers={config.API_V1_AUTH_KEY:kintoneevn.API_V1_AUTH_VALUE}
url = f"{kintoneevn.BASE_URL}{config.API_V1_STR}/apps.json"
offset = 0
limit = 100
all_apps = []
while True:
r = httpx.get(f"{url}?limit={limit}&offset={offset}", headers=headers)
json_data = r.json()
apps = json_data.get("apps",[])
all_apps.extend(apps)
if len(apps)<limit:
break
offset += limit
kintone_apps_dict = {app['appId']: app for app in all_apps}
filtered_apps = []
for papp in platformapps:
if papp.appid in kintone_apps_dict:
papp.appname = kintone_apps_dict[papp.appid]["name"]
filtered_apps.append(papp)
return filtered_apps
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get apps:",e)
@r.post("/apps", response_model=AppList, response_model_exclude_none=True)
async def apps_update(
request: Request,
app: AppVersion,
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
return update_appversion(db, app,user.id)
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get create app :",e)
@r.get(
"/appsettings/{id}",
response_model=App,
@@ -189,7 +129,7 @@ async def flow_list(
try:
domain = get_activedomain(db, user.id)
print("domain=>",domain)
flows = get_flows_by_app(db, domain.url, appid)
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)
@@ -204,7 +144,7 @@ async def flow_create(
):
try:
domain = get_activedomain(db, user.id)
return create_flow(db, domain.url, flow)
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)
@@ -214,14 +154,11 @@ async def flow_create(
)
async def flow_edit(
request: Request,
flowid: str,
flow: FlowBase,
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
return edit_flow(db,domain.url, flow)
return edit_flow(db, flow)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
@@ -259,11 +196,10 @@ async def domain_details(
async def domain_create(
request: Request,
domain: DomainBase,
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
return create_domain(db, domain,user.id)
return create_domain(db, domain)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e)
@@ -297,17 +233,16 @@ async def domain_delete(
@r.get(
"/domain",
# response_model=List[Domain],
response_model=List[Domain],
response_model_exclude_none=True,
)
async def userdomain_details(
request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domains = get_domain(db, userId if userId is not None else user.id)
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)
@@ -319,7 +254,7 @@ async def userdomain_details(
async def create_userdomain(
request: Request,
userid: int,
domainids:List[int] ,
domainids:list,
db=Depends(get_db),
):
try:
@@ -350,15 +285,11 @@ async def userdomain_delete(
)
async def get_useractivedomain(
request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
# domain = get_activedomain(db, user.id)
domain = get_activedomain(db, userId if userId is not None else user.id)
if domain is None:
return JSONResponse(content=None,status_code=HTTPStatus.OK)
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)
@@ -370,12 +301,11 @@ async def get_useractivedomain(
async def update_activeuserdomain(
request: Request,
domainid:int,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domain = active_userdomain(db, userId if userId is not None else user.id,domainid)
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)

View File

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

View File

@@ -1,18 +1,16 @@
import os
import base64
PROJECT_NAME = "KintoneAppBuilder"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/dev"
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://kabAdmin:P%40ssw0rd!@kintonetooldb.postgres.database.azure.com/dev_v2"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/test"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@ktune-prod-db.postgres.database.azure.com/postgres"
#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 = "PROD" #DEV,PROD
DEPLOY_MODE = "DEV" #DEV,PROD
DEPLOY_JS_URL = "https://ka-addin.azurewebsites.net/alc_runtime.js"
@@ -20,10 +18,8 @@ KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","U
KINTONE_FIELD_PROPERTY=['label','code','type','required','unique','maxValue','minValue','maxLength','minLength','defaultValue','defaultNowValue','options','expression','hideExpression','digit','protocol','displayScale','unit','unitPosition']
KINTONE_PSW_CRYPTO_KEY=bytes.fromhex("53 6c 93 bd 48 ad b5 c0 93 df a1 27 25 a1 a3 32 a2 03 3b a0 27 1f 51 dc 20 0e 6c d7 be fc fb ea")
class KINTONE_ENV:
BASE_URL = ""
API_V1_AUTH_VALUE = ""
@@ -39,4 +35,4 @@ class KINTONE_ENV:
self.DOMAIN_ID=domain.id
self.BASE_URL = domain.url
self.KINTONE_USER = domain.kintoneuser
self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.decrypt_kintonepwd()}","utf-8"))
self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.kintonepwd}","utf-8"))

View File

@@ -2,10 +2,6 @@ import jwt
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from datetime import datetime, timedelta
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
import os
import base64
from app.core import config
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
@@ -13,7 +9,7 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = "alicorns"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 2880
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def get_password_hash(password: str) -> str:
@@ -29,36 +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=ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.utcnow() + timedelta(minutes=2880)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def chacha20Encrypt(plaintext:str, key=config.KINTONE_PSW_CRYPTO_KEY):
if plaintext is None or plaintext == '':
return None
nonce = os.urandom(16)
algorithm = algorithms.ChaCha20(key, nonce)
cipher = Cipher(algorithm, mode=None)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext.encode('utf-8')) + encryptor.finalize()
return base64.b64encode(nonce +'𒀸'.encode('utf-8')+ ciphertext).decode('utf-8')
def chacha20Decrypt(encoded_str:str, key=config.KINTONE_PSW_CRYPTO_KEY):
try:
decoded_data = base64.b64decode(encoded_str)
if len(decoded_data) < 18:
return encoded_str
special_char = decoded_data[16:20]
if special_char != '𒀸'.encode('utf-8'):
return encoded_str
nonce = decoded_data[:16]
ciphertext = decoded_data[20:]
except Exception as e:
print(f"An error occurred: {e}")
return encoded_str
algorithm = algorithms.ChaCha20(key, nonce)
cipher = Cipher(algorithm, mode=None)
decryptor = cipher.decryptor()
plaintext_bytes = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext_bytes.decode('utf-8')

View File

@@ -4,7 +4,7 @@ from sqlalchemy import and_
import typing as t
from . import models, schemas
from app.core.security import chacha20Decrypt, get_password_hash
from app.core.security import get_password_hash
def get_user(db: Session, user_id: int):
@@ -69,47 +69,6 @@ def edit_user(
db.refresh(db_user)
return db_user
def get_apps(
db: Session,
domain_url:str
) -> t.List[schemas.AppList]:
return db.query(models.App).filter(models.App.domainurl == domain_url).all()
def update_appversion(db: Session, appedit: schemas.AppVersion,userid:int):
app = db.query(models.App).filter(and_(models.App.domainurl == appedit.domainurl,models.App.appid == appedit.appid)).first()
if app:
app.version = app.version + 1
db_app = app
appver = app.version
else:
appver = 1
db_app = models.App(
domainurl = appedit.domainurl,
appid=appedit.appid,
appname=appedit.appname,
version = 1,
updateuser= userid
)
db.add(db_app)
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == appedit.domainurl,models.App.appid == appedit.appid))
for flow in flows:
db_flowhistory = models.FlowHistory(
flowid = flow.flowid,
appid = flow.appid,
eventid = flow.eventid,
domainurl = flow.domainurl,
name = flow.name,
content = flow.content,
createuser = userid,
version = appver
)
db.add(db_flowhistory)
db.commit()
db.refresh(db_app)
return db_app
def get_appsetting(db: Session, id: int):
app = db.query(models.AppSetting).get(id)
@@ -166,12 +125,12 @@ def get_actions(db: Session):
return actions
def create_flow(db: Session, domainurl: str, 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,
domainurl=domainurl,
domainid=domainid,
name=flow.name,
content=flow.content
)
@@ -190,17 +149,15 @@ def delete_flow(db: Session, flowid: str):
def edit_flow(
db: Session, domainurl: str, flow: schemas.FlowBase
db: Session, flow: schemas.FlowBase
) -> schemas.Flow:
db_flow = get_flow(db, flow.flowid)
if not db_flow:
#見つからない時新規作成
return create_flow(db,domainurl,flow)
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Flow not found")
update_data = flow.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(db_flow, key, value)
setattr(db_flow, key, value)
db.add(db_flow)
db.commit()
@@ -216,18 +173,17 @@ def get_flows(db: Session, flowid: str):
def get_flow(db: Session, flowid: str):
flow = db.query(models.Flow).filter(models.Flow.flowid == flowid).first()
# if not flow:
# raise HTTPException(status_code=404, detail="Data not found")
if not flow:
raise HTTPException(status_code=404, detail="Data not found")
return flow
def get_flows_by_app(db: Session,domainurl: str, appid: str):
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid == appid)).all()
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 Exception("Data not found")
return flows
def create_domain(db: Session, domain: schemas.DomainBase,userid:int):
domain.encrypt_kintonepwd()
def create_domain(db: Session, domain: schemas.DomainBase):
db_domain = models.Domain(
tenantid = domain.tenantid,
name=domain.name,
@@ -236,8 +192,6 @@ def create_domain(db: Session, domain: schemas.DomainBase,userid:int):
kintonepwd=domain.kintonepwd
)
db.add(db_domain)
db.flush()
add_userdomain(db,userid,db_domain.id)
db.commit()
db.refresh(db_domain)
return db_domain
@@ -254,32 +208,30 @@ def delete_domain(db: Session,id: int):
def edit_domain(
db: Session, domain: schemas.DomainBase
) -> schemas.Domain:
domain.encrypt_kintonepwd()
db_domain = db.query(models.Domain).get(domain.id)
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
update_data = domain.dict(exclude_unset=True)
for key, value in update_data.items():
if key != "id" and not (key == "kintonepwd" and (value is None or value == "")):
setattr(db_domain, key, value)
print(str(db_domain))
if(key != "id"):
setattr(db_domain, key, value)
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
def add_userdomain(db: Session, userid:int,domainid:int):
user_domain = models.UserDomain(userid = userid, domainid = domainid )
db.add(user_domain)
return user_domain
def add_userdomains(db: Session, userid:int,domainids:list[str]):
dbCommits = list(map(lambda domainid: models.UserDomain(userid = userid, domainid = domainid ), domainids))
db.bulk_save_objects(dbCommits)
def add_userdomain(db: Session, userid:int,domainids:list):
for domainid in domainids:
db_domain = models.UserDomain(
userid = userid,
domainid = domainid
)
db.add(db_domain)
db.commit()
return dbCommits
db.refresh(db_domain)
return db_domain
def delete_userdomain(db: Session, userid: int,domainid: int):
db_domain = db.query(models.UserDomain).filter(and_(models.UserDomain.userid == userid,models.UserDomain.domainid == domainid)).first()
@@ -302,35 +254,22 @@ def active_userdomain(db: Session, userid: int,domainid: int):
db.commit()
return db_userdomains
def get_activedomain(db: Session, userid: int)-> t.Optional[models.Domain]:
user_domains = (db.query(models.Domain,models.UserDomain.active)
.join(models.UserDomain,models.UserDomain.domainid == models.Domain.id )
.filter(models.UserDomain.userid == userid)
.all())
db_domain=None
if len(user_domains)==1:
db_domain = user_domains[0][0];
else:
db_domain = next((domain for domain,active in user_domains if active),None)
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
def get_activedomain(db: Session, userid: int):
db_domain = db.query(models.Domain).join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ).filter(and_(models.UserDomain.userid == userid,models.UserDomain.active == True)).first()
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
return db_domain
def get_domain(db: Session, userid: str):
domains = db.query(models.Domain).join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ).filter(models.UserDomain.userid == userid).all()
# if not domains:
# raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
if not domains:
raise HTTPException(status_code=404, detail="Data not found")
return domains
def get_domains(db: Session,tenantid:str):
domains = db.query(models.Domain).filter(models.Domain.tenantid == tenantid ).all()
if not domains:
raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
return domains
def get_events(db: Session):
@@ -339,35 +278,8 @@ def get_events(db: Session):
raise HTTPException(status_code=404, detail="Data not found")
return events
def get_category(db:Session):
categorys=db.query(models.Category).all()
return categorys
def get_eventactions(db: Session,eventid: str):
#eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid == models.Action.id ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all()
#category = get_category(db)
blackactions = (
db.query(models.EventAction.actionid)
.filter(models.EventAction.eventid == eventid)
.subquery()
)
eveactions = (
db.query(
models.Action.id,
models.Action.name,
models.Action.title,
models.Action.subtitle,
models.Action.outputpoints,
models.Action.property,
models.Action.categoryid,
models.Action.nosort,
models.Category.categoryname)
.join(models.Category,models.Category.id == models.Action.categoryid)
.filter(models.Action.id.notin_(blackactions))
.order_by(models.Category.nosort,models.Action.nosort)
.all()
)
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

View File

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

View File

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

View File

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

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

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

View File

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

View File

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

View File

@@ -36,8 +36,7 @@ module.exports = configure(function (/* ctx */) {
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [
'axios',
'error-handler'
'axios'
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
@@ -94,7 +93,6 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
// https: true
port:9001,
open: true, // opens browser window automatically
env: { ...dotenv },
},

View File

@@ -2,7 +2,6 @@ import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios';
import {router} from 'src/router';
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$axios: AxiosInstance;
@@ -16,10 +15,32 @@ declare module '@vue/runtime-core' {
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL });
const token=localStorage.getItem('token')||'';
if(token!==''){
api.defaults.headers["Authorization"]='Bearer ' + token;
}
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
app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,84 @@
<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 :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 lang="ts">
import { ref,onMounted,reactive } from 'vue'
import { api } from 'boot/axios';
import { LeftDataBus } from './flowEditor/left/DataBus';
export default {
name: 'AppSelect',
props: {
name: String,
type: String
},
setup() {
const columns = [
{ name: 'id', required: true,label: 'ID',align: 'left',field: 'id',sortable: true},
{ name: 'name', label: 'アプリ名', field: 'name', sortable: true,align:'left' },
{ name: 'description', label: '概要', field: 'description',align:'left', sortable: false },
{ name: 'createdate', label: '作成日時', field: 'createdate',align:'left'}
]
const isLoaded=ref(false);
const rows :any[]= reactive([]);
onMounted( () => {
api.get('api/v1/allapps').then(res =>{
res.data.apps.forEach((item:any) =>
{
rows.push({
id:item.appId,
name:item.name,
description:item.description,
createdate:dateFormat(item.createdAt)});
});
isLoaded.value=true;
});
});
const dateFormat=(dateStr:string)=>{
const date = new Date(dateStr);
const pad = (num:number) => num.toString().padStart(2, '0');
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
}
return {
columns,
rows,
selected: 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

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

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<show-dialog v-model:visible="showflg" name="条件エディタ" @close="closeDg" min-width="50vw" min-height="60vh">
<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">
@@ -52,12 +52,12 @@ import { useQuasar } from 'quasar';
const tree = ref(props.conditionTree);
const closeDg = (val:string) => {
if (val == 'OK') {
// if(tree.value.root.children.length===0){
// $q.notify({
// type: 'negative',
// message: `条件式を設定してください。`
// });
// }
if(tree.value.root.children.length===0){
$q.notify({
type: 'negative',
message: `条件式を設定してください。`
});
}
context.emit("update:conditionTree",tree.value);
}
showflg.value=false;

View File

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

View File

@@ -66,22 +66,18 @@
</div>
<!-- condition -->
<div @click.stop @keypress.stop v-else >
<div class="row no-wrap items-center q-my-xs">
<ConditionObject v-bind="prop.node" v-model="prop.node.object" :config="leftDynamicItemConfig" class="col-4"/>
<q-select v-model="prop.node.operator" :options="operators" class="operator" :outlined="true" :dense="true"></q-select>
<ConditionObject v-bind="prop.node" v-model="prop.node.value" :config="rightDynamicItemConfig" class="col-4"
:options="objectValueOptions(prop.node?.object?.options)"
/>
<!-- <ConditionObject v-bind="prop.node" v-model="prop.node.object" class="col-4"/> -->
<!-- <q-input v-if="!prop.node.object || !('options' in prop.node.object)"
<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)"
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> -->
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>
@@ -110,17 +106,20 @@
</div>
</template>
</q-tree>
<q-tooltip anchor="center middle" v-model="showingCondition" no-parent-event>
<!-- <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, inject } from 'vue';
import { defineComponent,ref,reactive, computed } from 'vue';
import { INode,ConditionTree,GroupNode,ConditionNode, LogicalOperator,Operator,NodeType } from '../../types/Conditions';
import ConditionObject from './ConditionObject.vue';
import { IDynamicInputConfig } from 'src/types/ComponentTypes';
export default defineComponent( {
name: 'NodeCondition',
components: {
@@ -148,18 +147,20 @@ export default defineComponent( {
return opts;
});
const operatorSet = inject<Array<any>>('Operator')
const operators = ref(operatorSet ? operatorSet : Object.values(Operator));
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[]|null=>{
if(!options){
return null;
}
const objectValueOptions=(options:any):any[]=>{
const opts:any[] =[];
Object.keys(options).forEach((key) =>
{
@@ -226,14 +227,11 @@ export default defineComponent( {
ticked.value=[];
}
const expanded=computed(()=>tree.getGroups(tree.root));
// addCondition(tree.root);
const leftDynamicItemConfig = inject<IDynamicInputConfig>('leftDynamicItemConfig');
const rightDynamicItemConfig = inject<IDynamicInputConfig>('rightDynamicItemConfig');
return {
leftDynamicItemConfig,
rightDynamicItemConfig,
showingCondition,
conditionString,
tree,
@@ -263,14 +261,12 @@ export default defineComponent( {
.condition-value{
min-width: 200px;
max-height: 40px;
margin: 0 2px;
padding: 2px;
}
.operator{
min-width: 150px;
max-height: 40px;
margin: 0 2px;
padding: 2px;
text-align: center;
font-size: 12pt;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,20 @@
<template>
<!-- <div class="q-pa-md q-gutter-sm" > -->
<q-dialog :model-value="visible" persistent bordered >
<q-card class="" style="min-width: 40vw; max-width: 80vw; max-height: 95vh;" :style="cardStyle">
<q-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 class="q-mt-md" :style="sectionStyle">
<q-card-section>
<!-- <div class="text-h6">{{ name }}</div> -->
</q-card-section>
<q-card-section class="q-pt-none" :style="{...(height? {minHeight:height}:{}) }">
<slot></slot>
</q-card-section>
<q-card-actions v-if="!disableBtn" align="right" class="text-primary">
<q-card-actions align="right" class="text-primary">
<q-btn flat label="確定" v-close-popup @click="CloseDialogue('OK')" />
<q-btn flat label="キャンセル" v-close-popup @click="CloseDialogue('Cancel')" />
</q-card-actions>
@@ -20,20 +23,14 @@
<!-- </div> -->
</template>
<script>
import {computed} from 'vue'
export default {
name: 'ShowDialog',
props: {
name:String,
visible: Boolean,
width:String,
height:String,
minWidth:String,
minHeight:String,
disableBtn:{
type: Boolean,
default: false
}
height:String
},
emits: [
'close'
@@ -44,20 +41,8 @@ export default {
context.emit('close', val);
}
const cardStyle = computed(() => ({
minWidth: props.minWidth,
width: props.width
}));
const sectionStyle = computed(() => ({
height: props.height,
minHeight: props.minHeight
}));
return {
CloseDialogue,
cardStyle,
sectionStyle
CloseDialogue
}
},
}

View File

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

View File

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

View File

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

View File

@@ -22,15 +22,8 @@
></q-btn>
</div>
</div>
<ShowDialog v-model:visible="showSelectApp" name="アプリ選択" @close="closeDg" min-width="50vw" min-height="50vh" >
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<AppSelectBox ref="appDg" name="アプリ" type="single" :filter="filter"></AppSelectBox>
<ShowDialog v-model:visible="showSelectApp" name="アプリ選択" @close="closeDg" width="600px" >
<AppSelect ref="appDg" name="アプリ" type="single"></AppSelect>
</ShowDialog>
</template>
@@ -38,7 +31,7 @@
import { defineComponent,ref } from 'vue';
import {AppInfo} from '../../types/ActionTypes'
import ShowDialog from '../../components/ShowDialog.vue';
import AppSelectBox from '../../components/AppSelectBox.vue';
import AppSelect from '../../components/AppSelect.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'src/stores/useAuthStore';
export default defineComponent({
@@ -47,7 +40,7 @@ export default defineComponent({
"appSelected"
],
components:{
AppSelectBox,
AppSelect,
ShowDialog
},
setup(props, context) {
@@ -80,8 +73,7 @@ export default defineComponent({
showSelectApp,
showAppDialog,
closeDg,
appDg,
filter:ref('')
appDg
}
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div v-for="(item, index) in properties" :key="index" >
<component :is="item.component" v-bind="item.props" :context="properties" :connectProps="connectProps(item.props)" v-model="item.props.modelValue"></component>
<component :is="item.component" v-bind="item.props" v-model="item.props.modelValue"></component>
</div>
</div>
</template>
@@ -15,17 +15,9 @@ import InputText from '../right/InputText.vue';
import SelectBox from '../right/SelectBox.vue';
import DatePicker from '../right/DatePicker.vue';
import FieldInput from '../right/FieldInput.vue';
import AppFieldSelect from './AppFieldSelect.vue';
import MuiltInputText from '../right/MuiltInputText.vue';
import ConditionInput from '../right/ConditionInput.vue';
import EventSetter from '../right/EventSetter.vue';
import ColorPicker from './ColorPicker.vue';
import NumInput from './NumInput.vue';
import DataProcessing from './DataProcessing.vue';
import DataMapping from './DataMapping.vue';
import AppSelect from './AppSelect.vue';
import CascadingDropDown from './CascadingDropDown.vue';
import { IActionNode,IActionProperty,IProp } from 'src/types/ActionTypes';
import { IActionNode,IActionProperty } from 'src/types/ActionTypes';
export default defineComponent({
name: 'PropertyList',
@@ -34,16 +26,8 @@ export default defineComponent({
SelectBox,
DatePicker,
FieldInput,
AppFieldSelect,
MuiltInputText,
ConditionInput,
EventSetter,
ColorPicker,
NumInput,
DataProcessing,
DataMapping,
AppSelect,
CascadingDropDown
ConditionInput
},
props: {
nodeProps: {
@@ -56,22 +40,9 @@ export default defineComponent({
}
},
setup(props, context) {
const properties=ref(props.nodeProps);
const connectProps=(props:IProp)=>{
const connProps:any={context:properties};
if(props && "connectProps" in props && props.connectProps!=undefined){
for(let connProp of props.connectProps){
let targetProp = properties.value.find((prop)=>prop.props.name===connProp.propName);
if(targetProp){
connProps[connProp.key]=targetProp;
}
}
}
return connProps;
}
const properties=ref(props.nodeProps)
return {
properties,
connectProps
properties
}
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,13 +5,7 @@ const routes: RouteRecordRaw[] = [
path: '/login',
component: () => import('pages/LoginPage.vue')
},
{
path:'/FlowChart/:id',
component:()=>import('layouts/MainLayout.vue'),
children:[
{path:'',component:()=>import('pages/FlowChart.vue')}
]
},
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
@@ -22,12 +16,10 @@ const routes: RouteRecordRaw[] = [
{ path: 'flow', component: () => import('pages/testFlow.vue') },
{ path: 'FlowChartTest', component: () => import('pages/FlowChartTest.vue') },
{ path: 'flowEditor', component: () => import('pages/FlowEditorPage.vue') },
// { path: 'FlowChart', component: () => import('pages/FlowChart.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: 'user', component: () => import('pages/UserManagement.vue')},
{ path: 'app', component: () => import('pages/AppManagement.vue')},
{ path: 'userdomain', component: () => import('pages/UserDomain.vue')},
{ path: 'condition', component: () => import('pages/conditionPage.vue') }
],
},

View File

@@ -1,177 +1,112 @@
import { defineStore } from 'pinia';
import { AppInfo, IActionFlow, IActionNode } from 'src/types/ActionTypes';
import { IKintoneEvent, KintoneEventManager, kintoneEvent } from 'src/types/KintoneEvents';
import { FlowCtrl } from '../control/flowctrl';
import { AppInfo ,IActionFlow} from 'src/types/ActionTypes';
import { kintoneEvents,IKintoneEvent,KintoneEventManager } from 'src/types/KintoneEvents';
import {FlowCtrl } from '../control/flowctrl';
export interface FlowEditorState {
flowNames1: string;
appInfo?: AppInfo;
flows?: IActionFlow[];
selectedFlow?: IActionFlow | undefined;
activeNode: IActionNode | undefined;
eventTree: KintoneEventManager;
selectedEvent: IKintoneEvent | undefined;
expandedScreen: string[];
export interface FlowEditorState{
flowNames1:string;
appInfo?:AppInfo;
flows?:IActionFlow[];
selectedFlow?:IActionFlow|undefined;
eventTree:KintoneEventManager;
selectedEvent:IKintoneEvent|undefined;
expandedScreen:any[];
}
const flowCtrl = new FlowCtrl();
const eventTree = new KintoneEventManager();
export const useFlowEditorStore = defineStore('flowEditor', {
state: (): FlowEditorState => ({
flowNames1: '',
appInfo: undefined,
flows: [],
selectedFlow: undefined,
activeNode: undefined,
eventTree: eventTree,
selectedEvent: undefined,
expandedScreen: [],
}),
const flowCtrl=new FlowCtrl();
export const useFlowEditorStore = defineStore("flowEditor",{
state: ():FlowEditorState => ({
flowNames1: '',
appInfo:undefined,
flows:[],
selectedFlow:undefined,
eventTree:kintoneEvents,
selectedEvent:undefined,
expandedScreen:[]
}),
getters: {
/**
*
* @returns 現在編集しているフロー
*/
currentFlow(): IActionFlow | undefined {
return this.selectedFlow;
currentFlow():IActionFlow|undefined{
return this.selectedFlow;
},
/**
* KintoneイベントIDから、バンドしているフローを検索する
* @param state
* @returns
*/
findFlowByEventId(state) {
return (eventId: string) => {
return state.flows?.find((flow) => {
const root = flow.getRoot();
return root?.name === eventId;
});
};
},
findEventById(state) {
return (eventId: string) => {
return state.eventTree.findEventById(eventId);
};
},
findFlowByEventId(state){
return (eventId:string)=>{
return state.flows?.find((flow)=>{
const root=flow.getRoot();
return root?.name===eventId
});
}
}
},
actions: {
setFlows(flows: IActionFlow[]) {
this.flows = flows;
setFlows(flows:IActionFlow[]){
this.flows=flows;
},
selectFlow(flow: IActionFlow | undefined) {
this.selectedFlow = flow;
if(flow!==undefined){
const eventId = flow.getRoot()?.name;
this.selectedEvent = this.eventTree.findEventById(eventId) as IKintoneEvent;
} else {
this.selectedEvent = undefined;
}
selectFlow(flow:IActionFlow){
this.selectedFlow=flow;
},
setActiveNode(node: IActionNode) {
this.activeNode = node;
},
setCurrentEvent(event:IKintoneEvent | undefined){
this.selectedEvent=event;
},
setApp(app: AppInfo) {
this.appInfo = app;
setApp(app:AppInfo){
this.appInfo=app;
},
/**
* DBからフルーを保存する
* @returns
*/
async loadFlow() {
if (this.appInfo === undefined) return;
const actionFlows = await flowCtrl.getFlows(this.appInfo?.appId);
//eventTreeにバンドする
this.eventTree.bindFlows(actionFlows);
if (actionFlows === undefined || actionFlows.length === 0) {
this.setFlows([]);
this.selectFlow(undefined);
this.expandedScreen =[];
return;
}
this.setFlows(actionFlows);
if (actionFlows && actionFlows.length > 0) {
this.selectFlow(actionFlows[0]);
}
const root = actionFlows[0].getRoot();
if (root) {
this.setActiveNode(root);
}
const expandEventIds = actionFlows.map((flow) => flow.getRoot()?.name);
const expandScreens:string[]=[];
expandEventIds.forEach((eventid)=>{
const eventNode=this.eventTree.findEventById(eventid||'');
if(eventNode){
expandScreens.push(eventNode.parentId);
if(eventNode.header==='DELETABLE'){
const groupEvent = this.eventTree.findEventById(eventNode.parentId);
if(groupEvent){
expandScreens.push(groupEvent.parentId);
}
}
async loadFlow(){
if(this.appInfo===undefined) return;
const actionFlows = await flowCtrl.getFlows(this.appInfo?.appId);
//eventTreeにバンドする
this.eventTree.bindFlows(actionFlows);
if(actionFlows===undefined || actionFlows.length===0){
this.flows=[];
this.selectedFlow=undefined;
return;
}
});
// const expandName =actionFlows[0].getRoot()?.title;
this.expandedScreen = expandScreens;
this.setFlows(actionFlows);
if(actionFlows && actionFlows.length>0){
this.selectFlow(actionFlows[0]);
}
const expandNames = actionFlows.map(flow=>flow.getRoot()?.title);
// const expandName =actionFlows[0].getRoot()?.title;
this.expandedScreen=expandNames;
},
/**
* フローをDBに保存及び更新する
*/
async saveFlow(flow: IActionFlow):Promise<boolean> {
const root = flow.getRoot();
const isNew = flow.id === '';
const jsonData = {
flowid: isNew ? flow.createNewId() : flow.id,
async saveFlow(flow:IActionFlow){
const root=flow.getRoot();
const isNew = flow.id==='';
const jsonData={
flowid: isNew ? flow.createNewId():flow.id,
appid: this.appInfo?.appId,
eventid: root?.name,
name: root?.subTitle,
content: JSON.stringify(flow),
};
content: JSON.stringify(flow)
}
if (isNew) {
if(isNew){
return await flowCtrl.SaveFlow(jsonData);
} else {
if(flow.actionNodes.length>1){
return await flowCtrl.UpdateFlow(jsonData);
}else{
const eventId = flow.getRoot()?.name||'';
const eventNode = eventTree.findEventById(eventId) as kintoneEvent;
eventNode.flowData=undefined;
return await flowCtrl.DeleteFlow(flow.id);
}
}
},
async deleteEvent(event: IKintoneEvent) {
const store = useFlowEditorStore();
if (event.flowData) {
const flow = event.flowData;
if (flow.id !== '') {
await flowCtrl.DeleteFlow(flow.id)
if (this.flows) {
this.flows = this.flows.filter((f) => f.id !== flow.id);
}
}
eventTree.deleteEvent(event, store);
}
else {
eventTree.deleteEvent(event, store);
}else{
return await flowCtrl.UpdateFlow(jsonData);
}
},
/**
* デプロイする
*/
async deploy(): Promise<boolean> {
if (this.appInfo === undefined) {
async deploy():Promise<boolean>{
if(this.appInfo===undefined){
return false;
}
return await flowCtrl.depoly(this.appInfo?.appId);
},
},
}
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -74,11 +74,6 @@ export class GroupNode implements INode {
}
export type OperatorListItem = {
label: string;
value: string;
}
// 条件式ノード
export class ConditionNode implements INode {
index: number;
@@ -88,13 +83,13 @@ export class ConditionNode implements INode {
return this.parent.logicalOperator;
};
object: any; // 比較元
operator: Operator | OperatorListItem; // 比較子
operator: Operator; // 比較子
value: any;
get header():string{
return 'generic';
}
constructor(object: any, operator: Operator | OperatorListItem, value: any, parent: GroupNode) {
constructor(object: any, operator: Operator, value: any, parent: GroupNode) {
this.index=0;
this.type = NodeType.Condition;
this.object = object;
@@ -118,12 +113,10 @@ export class ConditionNode implements INode {
export class ConditionTree {
root: GroupNode;
maxIndex:number;
queryString:string;
constructor() {
this.maxIndex=0;
this.root = new GroupNode(LogicalOperator.AND, null);
this.queryString='';
}
// ノード追加
@@ -198,57 +191,19 @@ export class ConditionTree {
}
conditionString += ')';
return conditionString;
} else {
const condNode=node as ConditionNode;
if (condNode.object && condNode.object.sharedText && condNode.operator ) {
// let value=condNode.value;
// if(value && typeof value ==='object' && ('label' in value)){
// value =condNode.value.label;
// }
const rightVal = condNode.value.sharedText || '""';
return `${condNode.object.sharedText} ${typeof condNode.operator === 'object' ? condNode.operator.label : condNode.operator} ${rightVal}`;
// return `${typeof condNode.object.name === 'object' ? condNode.object.name.name : condNode.object.name} ${typeof condNode.operator === 'object' ? condNode.operator.label : condNode.operator} '${value}'`;
} else {
return '';
}
}
}
buildConditionQueryString(node:INode){
if (node.type !== NodeType.Condition) {
let conditionString = '';
if(node.type !== NodeType.Root){
conditionString = '(';
}
const groupNode = node as GroupNode;
for (let i = 0; i < groupNode.children.length; i++) {
const childConditionString = this.buildConditionQueryString(groupNode.children[i]);
if (childConditionString !== '') {
conditionString += childConditionString;
if (i < groupNode.children.length - 1) {
conditionString += ` ${groupNode.logicalOperator.toLowerCase()} `;
}
}
}
if(node.type !== NodeType.Root){
conditionString += ')';
}
return conditionString;
} else {
const condNode=node as ConditionNode;
if (condNode.object && condNode.operator ) {
if (!condNode.object.code || !condNode.value.sharedText){
return '';
let value=condNode.value;
if(value && typeof value ==='object' && ('label' in value)){
value =condNode.value.label;
}
return `${condNode.object.code} ${typeof condNode.operator === 'object' ? condNode.operator.value : condNode.operator} "${condNode.value.sharedText}"`;
return `${condNode.object.name} ${condNode.operator} '${value}'`;
} else {
return '';
}
}
}
/**
*
* @param node ノード移動
@@ -370,7 +325,7 @@ export class ConditionTree {
}
toJson():string{
return JSON.stringify({queryString :this.queryString, ...this.root}, (key, value) => {
return JSON.stringify(this.root, (key, value) => {
if (key === 'parent') {
return value ? value.type : null;
}
@@ -378,7 +333,4 @@ export class ConditionTree {
});
}
setQuery(queryString:string){
this.queryString=queryString;
}
}

View File

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

View File

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

6
node_modules/.package-lock.json generated vendored
View File

@@ -1,6 +0,0 @@
{
"name": "App Builder for kintone",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

15
node_modules/.yarn-integrity generated vendored
View File

@@ -1,15 +0,0 @@
{
"systemParams": "win32-x64-108",
"modulesFolders": [
"node_modules"
],
"flags": [],
"linkedModules": [
"@quasar\\quasar-ui-qactivity",
"docs"
],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}

View File

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

View File

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

View File

@@ -8,8 +8,7 @@
"name": "kintone-addins",
"version": "0.0.0",
"dependencies": {
"jquery": "^3.7.1",
"yarn": "^1.22.22"
"jquery": "^3.7.1"
},
"devDependencies": {
"@types/jquery": "^3.5.24",
@@ -796,19 +795,6 @@
"optional": true
}
}
},
"node_modules/yarn": {
"version": "1.22.22",
"resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz",
"integrity": "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==",
"hasInstallScript": true,
"bin": {
"yarn": "bin/yarn.js",
"yarnpkg": "bin/yarn.js"
},
"engines": {
"node": ">=4.0.0"
}
}
}
}

View File

@@ -4,35 +4,19 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "run-p watch server ngrok",
"watch": "vite build --watch --mode dev",
"server": "vite dev --mode dev",
"ngrok": "ngrok http 4173",
"build": "run-s b:production copyjs:windows copycss:windows",
"build:dev": "run-s b:dev copyjs:windows copycss:windows",
"build:linux": "run-s b:production copyjs:linux copycss:linux",
"build:linux-dev": "run-s b:dev copy:linux",
"b:production": "tsc & vite build --mode production",
"b:dev": "tsc & vite build --mode dev",
"copyjs:windows": "xcopy dist\\*.js ..\\..\\backend\\Temp\\ /E /I /Y",
"copyjs:linux": "cp -ur dist/*.js ../../backend/Temp",
"copycss:windows": "xcopy dist\\*.css ..\\..\\backend\\Temp\\ /E /I /Y",
"copycss:linux": "cp -ur dist/*.css ../../backend/Temp"
"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",
"npm-run-all2": "^6.2.0",
"sass": "^1.69.5",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-checker": "^0.6.4"
"vite": "^4.4.5"
},
"dependencies": {
"@kintone/rest-api-client": "^5.5.2",
"@popperjs/core": "^2.11.8",
"@types/bootstrap": "^5.2.10",
"bootstrap": "^5.3.3",
"jquery": "^3.7.1"
}
}

View File

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

View File

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

View File

@@ -1,311 +0,0 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
IField,
} from "../types/ActionTypes";
import { actionAddins } from ".";
import type { Record} from "@kintone/rest-api-client/lib/src/client/types";
import { KintoneAllRecordsError, KintoneRestAPIClient} from "@kintone/rest-api-client";
import "./auto-lookup.scss";
import "bootstrap/js/dist/modal";
// import "bootstrap/js/dist/spinner";
import {Modal} from "bootstrap"
import $ from "jquery";
interface IAutoLookUpProps {
displayName: string;
lookupField: LookupField;
condition: Condition;
}
interface Condition {
queryString: string;
index: number;
type: string;
children: Child[];
parent: null;
logicalOperator: string;
}
interface Child {
index: number;
type: string;
parent: string;
object: any;
operator: string;
value: string;
}
interface LookupField {
app: App;
fields: Field[];
}
interface Field {
name: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
lookup: Lookup;
}
interface Lookup {
relatedApp: RelatedApp;
relatedKeyField: string;
fieldMappings: FieldMapping[];
lookupPickerFields: any[];
filterCond: string;
sort: string;
}
interface FieldMapping {
field: string;
relatedField: string;
}
interface RelatedApp {
app: string;
code: string;
}
interface App {
id: string;
name: string;
description: string;
createdate: string;
}
export class AutoLookUpAction implements IAction {
name: string;
actionProps: IActionProperty[];
props: IAutoLookUpProps;
constructor() {
this.name = "ルックアップ更新";
this.actionProps = [];
this.props = {} as IAutoLookUpProps;
this.register();
}
/***
* アクセスのメインの処理関数
*/
async process(
actionNode: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = actionNode.actionProps;
this.props = {
...actionNode.ActionValue,
condition: JSON.parse((actionNode.ActionValue as any).condition),
} as IAutoLookUpProps;
// console.log(context);
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
const lookUpFields = this.props.lookupField.fields.filter(
(f) => f.lookup && f.lookup.relatedApp.app === String(kintone.app.getId())
);
if (!lookUpFields || lookUpFields.length===0) {
throw new Error(
`ルックアップの設定は不正です。${this.props.lookupField.fields[0].label} `
);
}
const lookUpField = this.props.lookupField.fields[0];
const key = event.record[lookUpField.lookup.relatedKeyField].value;
const targetRecords = await this.getUpdateRecords(lookUpField, key);
//更新対象がない時にスキップ
if(targetRecords.length===0){
return result;
}
const updateRecords = this.convertForLookup(targetRecords,lookUpField,key);
console.log("updateRecords", updateRecords);
this.showSpinnerModel(this.props.lookupField.app,lookUpField);
const updateResult = await this.updateLookupTarget(updateRecords);
if(updateResult){
this.showResult(this.props.lookupField.app,lookUpField,updateRecords.length);
}
} catch (error) {
this.closeDialog();
context.errors.handleError(error,actionNode,"ルックアップ更新中例外が発生しました");
result.canNext = false;
}
return result;
}
/**
* REST API用クエリ作成
* TODO:共通関数として作成
* @param lookUpField
* @param key
* @returns
*/
makeQuery=(lookUpField:Field,key:any)=>{
let query ="";
if(typeof key==='number'){
query = `${lookUpField.code} = ${key}`
}
if(typeof key==='string'){
query = `${lookUpField.code} = "${key}"`
}
if(this.props.condition.queryString!==''){
query = `${query} and (${this.props.condition.queryString})`
}
return query;
}
/**
* 更新対象のレコードを取得する
*/
getUpdateRecords = async (lookUpField:Field,key:any):Promise< Record[]>=>{
const client=new KintoneRestAPIClient();
const resp = await client.record.getAllRecords({
app:this.props.lookupField.app.id,
fields:["$id"],
condition:this.makeQuery(lookUpField,key)
});
return resp;
}
/**
* ルックアップ更新用レコードに変換する
* @param targetRecords 更新対象レコード
* @param lookUpField ルックアップフィールド
* @param key ルックアップフィールドの値
* @returns
*/
convertForLookup = (targetRecords:Record[],lookUpField:Field,key:any):Array<any>=>{
return targetRecords.map((r) => ({
id: Number(r["$id"].value),
record: { [lookUpField.code]: { value: key } },
}));
}
/**
* ルックアップ先を更新する
* @param updateRecords
*/
updateLookupTarget = async (updateRecords:Array<any>):Promise<boolean>=>{
if (updateRecords && updateRecords.length > 0) {
try{
const client=new KintoneRestAPIClient();
const result = await client.record.updateAllRecords({
app:this.props.lookupField.app.id,
records:updateRecords
});
return true;
}catch(error ){
if(error instanceof KintoneAllRecordsError){
this.showError(this.props.lookupField.app,
this.props.lookupField.fields[0],
error as KintoneAllRecordsError,updateRecords.length);
return false;
}else{
throw error;
}
}
// await kintone.api(kintone.api.url("/k/v1/records.json", true), "PUT", {
// app: this.props.lookupField.app.id,
// records: updateRecords
// });
}
return false;
}
/**
* 更新中のダイアログ表示
* @param app
*/
showSpinnerModel = (app:App,lookup:Field) => {
let dialog = $("#alcLookupModal");
if(dialog.length===0){
const modalHTML = `<div class="bs-scope">
<div class="modal" id="alcLookupModal" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog-centered">
<div class="modal-dialog modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="alcLookupModalLabel">ルックアップ同期処理</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row" id="app${app.id}_${lookup.code}">
<div class="spinner-border text-secondary col-1 " role="alert"></div>
<div class="col">${app.name}</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div></div></div></div>`;
$(modalHTML).appendTo("body");
dialog = $("#alcLookupModal");
dialog.get()[0].addEventListener('hidden.bs.modal',(ev)=>{
Modal.getOrCreateInstance(dialog.get()[0]).dispose();
$("#alcLookupModal").parent().remove();
});
}else{
const dialogBody=$("#alcLookupModal .modal-body");
const htmlrow=`
<div class="row" id="app${app.id}_${lookup.code}">
<div class="spinner-border text-secondary col-1 " role="alert">
</div>
<div class="col">${app.name}</div>
<div>`;
dialogBody.append(htmlrow);
}
Modal.getOrCreateInstance(dialog.get()[0]).show();
}
/**
* 更新結果を表示する
* @param app  更新先アプリ情報
* @param count 更新件数
*/
showResult=(app:App,lookup:Field,count:number)=>{
const dialogBody=$(`#alcLookupModal .modal-body #app${app.id}_${lookup.code}`);
const html=` <div class="col-1 text-success">✔</div>
<div class="col">${app.name}</div>
<div class="col">更新件数:${count}件</div>`;
dialogBody.html(html);
}
/**
* 更新結果を表示する
* @param app  更新先アプリ情報
* @param count 更新件数
*/
showError=(app:App,lookup:Field,error:KintoneAllRecordsError,allCount:Number)=>{
const message=error.error.message;
const proRecords = error.numOfProcessedRecords;
const allRecords=error.numOfAllRecords;
const dialogBody=$(`#alcLookupModal .modal-body #app${app.id}_${lookup.code}`);
const html=`<div class="col-1 text-danger">✖</div>
<div class="col">${app.name}</div>
<div class="col">更新件数:${proRecords}/${allRecords}</div>
<div class="row text-danger">${message}<div>`;
dialogBody.html(html);
}
/**
* ダイアログ画面を閉じる
*/
closeDialog=()=>{
const dialog = $("#alcLookupModal");
Modal.getOrCreateInstance(dialog.get()[0]).dispose();
$("#alcLookupModal").parent().remove();
}
register(): void {
actionAddins[this.name] = this;
}
}
new AutoLookUpAction();

View File

@@ -1,19 +1,20 @@
import { actionAddins } from ".";
import { IField, IAction,IActionResult, IActionNode, IActionProperty, IContext, IVarName } from "../types/ActionTypes";
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:IVarName;
verName:string;
}
export class AutoNumbering implements IAction{
@@ -29,18 +30,12 @@ export class AutoNumbering implements IAction{
format:'',
prefix:'',
suffix:'',
verName:{name:''}
verName:''
}
globalThis.window.$format=this.format;
this.register();
}
/**
* アクションの処理を実装する
* @param actionNode アクションノード
* @param event Kintoneのイベント
* @param context コンテキスト(レコード、変数情報を持っている)
* @returns
*/
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:false,
@@ -56,8 +51,8 @@ export class AutoNumbering implements IAction{
const docNum = await this.createNumber(this.props);
record[this.props.field.code].value=docNum;
//変数設定
if(this.props.verName && this.props.verName.name!==''){
context.variables[this.props.verName.name]=docNum;
if(this.props.verName){
context.variables[this.props.verName]=docNum;
}
result= {
canNext:true,
@@ -65,7 +60,8 @@ export class AutoNumbering implements IAction{
}
return result;
}catch(error){
context.errors.handleError(error,actionNode);
console.error(error);
event.error="処理中異常が発生しました。";
return {
canNext:false,
result:false
@@ -83,7 +79,6 @@ export class AutoNumbering implements IAction{
execEval(match:string,expr:string):string{
console.log(match);
// @ts-ignore
return eval(expr);
}

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