Compare commits

..

36 Commits

Author SHA1 Message Date
Tian Dai
50a4349a84 Merge branch 'dev' into feature-auto-lookup
# Conflicts:
#	frontend/src/components/AppFieldSelectBox.vue
#	frontend/src/components/right/AppFieldSelect.vue
#	plugin/kintone-addins/package.json
#	plugin/kintone-addins/src/actions/auto-lookup.ts
#	plugin/kintone-addins/src/types/action-process.ts
#	plugin/kintone-addins/vite.config.js
2024-07-08 15:21:39 +09:00
Mouriya
eb50f72df9 Merge commit '3540becf6f657ee6ab820f18c63b3b59c4f54c25' into feature-auto-lookup 2024-07-02 12:17:07 +09:00
Mouriya
3540becf6f vite-plugin-css-injected-by-jsはメンテナンスされていないため、通常のソースマップ生成に影響を与えるので、vite-plugin-lib-injected-cssを使用してください 2024-07-02 12:15:18 +09:00
Mouriya
5ca16d7b45 自動検索プラグインを追加 2024-07-01 16:20:58 +09:00
Mouriya
9d2217f8e1 ログの削除 2024-07-01 04:08:28 +09:00
Mouriya
704aab9265 スペルミスの修正 2024-07-01 04:07:53 +09:00
Mouriya
046ef4cb9f APPフィールドコンポーネントのバグ修正 2024-07-01 03:53:49 +09:00
Mouriya
d2f271e3cd エントリーを追加するときにデータを統合する 2024-06-17 15:22:10 +09:00
Mouriya
5fdb23d6d5 フィールドの生のjsonをビューに直接表示する。 2024-06-17 15:20:32 +09:00
Mouriya
734adf9a1e 空リストの修正 2024-06-17 15:19:36 +09:00
Mouriya
0e8d1957a3 ユーザーが手動でフィールドリストを追加または削除する必要はなくなり、プログラムが自動的にアプリのすべてのフィールドに対してリストを作成します。 2024-06-17 03:18:52 +09:00
Mouriya
e2a625ba12 入力ボックスの入力可否の設定を削除し、特定のボタン入力後に入力ボックスの入力可否に変更する。 2024-06-17 03:16:26 +09:00
Mouriya
eb89b3f8a6 Merge commit '26a685b872daa65a069ad8c39ef4b973c8e25022' into feature-data-mapping 2024-06-17 00:48:01 +09:00
Mouriya
343f97234a バグ修正:json文字列フェッチの問題 2024-06-10 12:34:01 +09:00
Mouriya
04f28a3be5 新しい統合されたフィールド入力、すべての関連部品の一致修正. フィールドの値を取得する に新しいアクションを追加 2024-06-10 06:10:32 +09:00
Tian Dai
63099eda8b Merge branch 'plugin-infar-vite' of ssh.dev.azure.com:v3/alicorn-dev/KintoneAppBuilder/KintoneAppBuilder into feature-data-mapping 2024-06-04 17:49:32 +09:00
Tian Dai
65d89b0462 Windowsのオペレーティングシステムの権限問題のため、Viteの自動URLオープン機能を無効にしました 2024-06-04 17:48:39 +09:00
Tian Dai
c6ded099fa vite devとvite serverはエイリアスではなく、置き換え可能で、問題を修正しました。 2024-06-04 17:41:09 +09:00
Tian Dai
c072233593 Merge branch 'plugin-infar-vite' into feature-data-mapping
# Conflicts:
#	plugin/kintone-addins/package.json
#	plugin/kintone-addins/vite.config.js
2024-06-04 17:21:43 +09:00
Tian Dai
4e296c1555 npm-run-all2を全オペレーティング・システムと依存性管理のサポートに置き換える。ラン・スクリプトをより組み合わせ可能なものに再編成する。 プロジェクトの dotenv サポートを提供する。 2024-06-04 16:16:36 +09:00
016fcaab29 feat:データマッピングアクション変更 2024-06-03 17:39:39 +09:00
Mouriya
bebc1ec9fa フィックス 2024-06-03 08:10:23 +09:00
Mouriya
f71c3d2123 挿入専用のデータ・マッピング・プラグインを追加 2024-06-03 08:08:06 +09:00
Mouriya
d79ce8d06b データ収集プラグインの追加 2024-06-03 07:08:04 +09:00
Mouriya
fc9c3a5e81 条件演算子をカスタマイズし、kintoneクエリ文字列に変換することができます。 2024-06-03 07:07:20 +09:00
Mouriya
6df72a1ae3 Merge commit '372dbe50f7254ed3142a85dac58e624fce695aa7' into feature-data-mapping 2024-06-03 02:16:50 +09:00
Mouriya
372dbe50f7 HTTPサーバーのホットリロード機能を追加。 2024-06-03 02:08:37 +09:00
Mouriya
68fde6d490 Merge commit 'b25c17ab53a848f5692f113c6c3c1177e310d87f' into feature-data-mapping 2024-06-03 00:41:01 +09:00
Mouriya
c398dee21e 誤ったmodelValueの使用法の修正 2024-05-27 19:52:16 +09:00
Mouriya
f2ab310b6d Merge commit 'a6cf95b76d56841e7a5d545c8f0835e44cf4dafc' into feature-data-mapping 2024-05-27 19:27:44 +09:00
Mouriya
ca0f24465b 外部アプリセレクタコンポーネントがある場合、このコンポーネントはアプリを選択せずにフィールドを選択できる。 2024-05-27 19:04:21 +09:00
Mouriya
3cc4b65460 アプリ選択コンポーネントがある場合、アプリまたはフィールドが選択されるまで、それらを無効にすることができる。 2024-05-27 19:03:08 +09:00
Mouriya
a6cf95b76d 新しいアプリ選択コンポーネント 2024-05-27 18:45:34 +09:00
Mouriya
484ab9fdae ファイル名とコンポーネント名を変更し、コンポーネントとして使用できるようにする。 2024-05-27 18:44:36 +09:00
Mouriya
78bba2502f データ・マッピング・コンポーネントの追加 2024-05-25 07:01:58 +09:00
Mouriya
c78b3cb5c0 AppFieldSelectコンポーネントは、パブリック部分をAppFieldSelectBoxとして抽出する。 2024-05-25 07:01:38 +09:00
180 changed files with 3749 additions and 18237 deletions

4
.gitignore vendored
View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,58 +1,40 @@
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from datetime import timedelta from datetime import timedelta
from app.db.session import get_db
from app.core import security
from app.core.auth import authenticate_user, sign_up_new_user from app.core.auth import authenticate_user, sign_up_new_user
from app.core import security,tenantCacheService
from app.core.dbmanager import get_db
from sqlalchemy.orm import Session
auth_router = r = APIRouter() auth_router = r = APIRouter()
@r.post("/token") @r.post("/token")
async def login(request: Request,db:Session= Depends(get_db) ,form_data: OAuth2PasswordRequestForm = Depends()): async def login(
if not db : db=Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
):
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="abcIncorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta( access_token_expires = timedelta(
minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES
) )
if user.is_superuser: if user.is_superuser:
roles = "super" permissions = "admin"
permissions = "ALL"
else: else:
roles = ";".join(role.name for role in user.roles) permissions = "user"
perlst = [perm.privilege for role in user.roles for perm in role.permissions]
permissions =";".join(list(set(perlst)))
access_token = security.create_access_token( access_token = security.create_access_token(
data={"sub": user.id,"roles":roles,"permissions": permissions,"tenant":user.tenantid,}, data={"sub": user.id, "permissions": permissions},
expires_delta=access_token_expires, expires_delta=access_token_expires,
) )
request.state.user = user.id
return JSONResponse( return {"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name}
status_code=200,
content={"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name}
)
@r.post("/signup") @r.post("/signup")
async def signup( async def signup(

View File

@@ -8,26 +8,25 @@ import deepdiff
import app.core.config as config import app.core.config as config
import os import os
from pathlib import Path from pathlib import Path
from app.core.dbmanager import get_db from app.db.session import SessionLocal
from app.db.crud import get_flows_by_app,get_kintoneformat from app.db.crud import get_flows_by_app,get_activedomain,get_kintoneformat
from app.core.auth import get_current_active_user,get_current_user from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException from app.core.apiexception import APIException
from app.db.cruddb import domainService,appService
kinton_router = r = APIRouter() kinton_router = r = APIRouter()
def getkintoneenv(user = Depends(get_current_user),db = Depends(get_db)): def getkintoneenv(user = Depends(get_current_user)):
#db = SessionLocal() db = SessionLocal()
domain = domainService.get_default_domain(db,user.id) #get_activedomain(db, user.id) domain = get_activedomain(db, user.id)
#db.close() db.close()
kintoneevn = config.KINTONE_ENV(domain) kintoneevn = config.KINTONE_ENV(domain)
return kintoneevn return kintoneevn
def getkintoneformat(db,user = Depends(get_current_user)): def getkintoneformat():
#db = SessionLocal() db = SessionLocal()
formats = get_kintoneformat(db) formats = get_kintoneformat(db)
#db.close() db.close()
return formats return formats
@@ -157,10 +156,10 @@ def getsettingfromexcel(df):
des = df.iloc[2,2] des = df.iloc[2,2]
return {"name":appname,"description":des} return {"name":appname,"description":des}
def getsettingfromkintone(app:str,env:config.KINTONE_ENV): def getsettingfromkintone(app:str,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"app":app} 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) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
@@ -172,69 +171,68 @@ def analysesettings(excel,kintone):
updatesettings[key] = excel[key] updatesettings[key] = excel[key]
return updatesettings return updatesettings
def createkintoneapp(name:str,env:config.KINTONE_ENV): def createkintoneapp(name:str,c:config.KINTONE_ENV):
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} 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)) r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json() return r.json()
def updateappsettingstokintone(app:str,updates:dict,env:config.KINTONE_ENV): def updateappsettingstokintone(app:str,updates:dict,c:config.KINTONE_ENV):
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"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/settings.json" url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/settings.json"
data = {"app":app} data = {"app":app}
data.update(updates) data.update(updates)
r = httpx.put(url,headers=headers,data=json.dumps(data)) r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json() return r.json()
def addfieldstokintone(app:str,fields:dict,env:config.KINTONE_ENV,revision:str = None): def addfieldstokintone(app:str,fields:dict,c:config.KINTONE_ENV,revision:str = None):
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"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json" url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
if revision != None: if revision != None:
data = {"app":app,"revision":revision,"properties":fields} data = {"app":app,"revision":revision,"properties":fields}
else: else:
data = {"app":app,"properties":fields} data = {"app":app,"properties":fields}
r = httpx.post(url,headers=headers,data=json.dumps(data)) r = httpx.post(url,headers=headers,data=json.dumps(data))
r.raise_for_status()
return r.json() return r.json()
def updatefieldstokintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV): def updatefieldstokintone(app:str,revision:str,fields:dict,c:config.KINTONE_ENV):
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"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json" url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
data = {"app":app,"properties":fields} data = {"app":app,"properties":fields}
r = httpx.put(url,headers=headers,data=json.dumps(data)) r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json() return r.json()
def deletefieldsfromkintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV): def deletefieldsfromkintone(app:str,revision:str,fields:dict,c:config.KINTONE_ENV):
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"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json" url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
params = {"app":app,"revision":revision,"fields":fields} params = {"app":app,"revision":revision,"fields":fields}
#r = httpx.delete(url,headers=headers,content=json.dumps(params)) #r = httpx.delete(url,headers=headers,content=json.dumps(params))
r = httpx.request(method="DELETE",url=url,headers=headers,content=json.dumps(params)) r = httpx.request(method="DELETE",url=url,headers=headers,content=json.dumps(params))
return r.json() return r.json()
def deoployappfromkintone(app:str,revision:str,env:config.KINTONE_ENV): def deoployappfromkintone(app:str,revision:str,c:config.KINTONE_ENV):
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"}
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":[{"app":app,"revision":revision}],"revert": False} data = {"apps":[{"app":app,"revision":revision}],"revert": False}
r = httpx.post(url,headers=headers,data=json.dumps(data)) r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json return r.json
# 既定項目に含めるアプリのフィールドのみ取得する # 既定項目に含めるアプリのフィールドのみ取得する
# スペース、枠線、ラベルを含まない # スペース、枠線、ラベルを含まない
def getfieldsfromkintone(app:str,env:config.KINTONE_ENV): def getfieldsfromkintone(app:str,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"app":app} params = {"app":app}
url = f"{env.BASE_URL}{config.API_V1_STR}/app/form/fields.json" url = f"{c.BASE_URL}{config.API_V1_STR}/app/form/fields.json"
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
# フォームに配置するフィールドのみ取得する # フォームに配置するフィールドのみ取得する
# スペース、枠線、ラベルも含める # スペース、枠線、ラベルも含める
def getformfromkintone(app:str,env:config.KINTONE_ENV): def getformfromkintone(app:str,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"app":app} params = {"app":app}
url = f"{env.BASE_URL}{config.API_V1_STR}/form.json" url = f"{c.BASE_URL}{config.API_V1_STR}/form.json"
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
@@ -287,10 +285,10 @@ def analysefields(excel,kintone):
return {"update":updatefields,"add":addfields,"del":delfields} return {"update":updatefields,"add":addfields,"del":delfields}
def getprocessfromkintone(app:str,env:config.KINTONE_ENV): def getprocessfromkintone(app:str,c:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
params = {"app":app} 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) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
@@ -375,87 +373,47 @@ def getkintoneorgs(c:config.KINTONE_ENV):
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
def uploadkintonefiles(file,env:config.KINTONE_ENV): def uploadkintonefiles(file,c:config.KINTONE_ENV):
if (file.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"): if (file.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
return {'fileKey':file} return {'fileKey':file}
upload_files = {'file': open(file,'rb')} upload_files = {'file': open(file,'rb')}
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
data ={'name':'file','filename':os.path.basename(file)} data ={'name':'file','filename':os.path.basename(file)}
url = f"{env.BASE_URL}/k/v1/file.json" url = f"{c.BASE_URL}/k/v1/file.json"
r = httpx.post(url,headers=headers,data=data,files=upload_files) r = httpx.post(url,headers=headers,data=data,files=upload_files)
#{"name":data['filename'],'fileKey':r['fileKey']}
return r.json() return r.json()
def updateappjscss(app,uploads,env:config.KINTONE_ENV): def updateappjscss(app,uploads,c:config.KINTONE_ENV):
dsjs = [] dsjs = []
dscss = [] dscss = []
#mobile側
mbjs = []
mbcss = []
customize = getappcustomize(app, env)
current_js = customize['desktop'].get('js', [])
current_css = customize['desktop'].get('css', [])
current_mobile_js = customize['mobile'].get('js', [])
current_mobile_css = customize['mobile'].get('css', [])
current_js = [item for item in current_js if not (item.get('type') == 'URL' and item.get('url', '').endswith('alc_runtime.js'))]
for upload in uploads: for upload in uploads:
for key in upload: for key in upload:
filename = os.path.basename(key)
if key.endswith('.js'): if key.endswith('.js'):
existing_js = next((item for item in current_js if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
if item.get('type') == 'FILE' and item['file']['name'] == filename dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
), None)
if existing_js:
current_js = [item for item in current_js if item.get('type') == 'URL' or item['file'].get('name') != filename]
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
else: else:
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"): dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
else:
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
elif key.endswith('.css'): elif key.endswith('.css'):
existing_css = next((item for item in current_css dscss.append({'type':'FILE','file':{'fileKey':upload[key]}})
if item.get('type') == 'FILE' and item['file']['name'] == filename
), None)
if existing_css:
current_css = [item for item in current_css if item.get('type') == 'URL' or item['file'].get('name') != filename]
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
else:
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
#現在のJSとCSSがdsjsに追加する
dsjs.extend(current_js)
dscss.extend(current_css)
mbjs.extend(current_mobile_js)
mbcss.extend(current_mobile_css)
ds ={'js':dsjs,'css':dscss} ds ={'js':dsjs,'css':dscss}
mb ={'js':mbjs,'css':mbcss} mb ={'js':[],'css':[]}
data = {'app':app,'scope':'ALL','desktop':ds,'mobile':mb,'revision':customize["revision"]} data = {'app':app,'scope':'ALL','desktop':ds,'mobile':mb}
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"}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/customize.json" url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
print(json.dumps(data)) print(data)
r = httpx.put(url,headers=headers,data=json.dumps(data)) r = httpx.put(url,headers=headers,data=json.dumps(data))
return r.json() return r.json()
#kintone カスタマイズ情報
def getappcustomize(app,env:config.KINTONE_ENV):
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
params = {"app":app}
r = httpx.get(url,headers=headers,params=params)
return r.json()
def getTempPath(filename): def getTempPath(filename):
scriptdir = Path(__file__).resolve().parent scriptdir = Path(__file__).resolve().parent
rootdir = scriptdir.parent.parent.parent.parent rootdir = scriptdir.parent.parent.parent.parent
fpath = os.path.join(rootdir,"Temp",filename) fpath = os.path.join(rootdir,"Temp",filename)
return fpath return fpath
def createappjs(domain_url,app,db): def createappjs(domainid,app):
#db = SessionLocal() db = SessionLocal()
flows = appService.get_flow(db,domain_url,app) #get_flows_by_app(db,domain_url,app) flows = get_flows_by_app(db,domainid,app)
#db.close() db.close()
content={} content={}
for flow in flows: for flow in flows:
content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content} content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content}
@@ -522,7 +480,7 @@ async def upload(request:Request,files:t.List[UploadFile] = File(...)):
return {"files": [file.filename for file in files]} return {"files": [file.filename for file in files]}
@r.post("/updatejscss") @r.post("/updatejscss")
async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env:config.KINTONE_ENV = Depends(getkintoneenv)): async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env = Depends(getkintoneenv)):
try: try:
jscs=[] jscs=[]
for file in files: for file in files:
@@ -543,47 +501,35 @@ async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env
raise APIException('kintone:updatejscss',request.url._url, f"Error occurred while update js/css {file.filename} is not an Excel file",e) raise APIException('kintone:updatejscss',request.url._url, f"Error occurred while update js/css {file.filename} is not an Excel file",e)
@r.get("/app") @r.get("/app")
async def app(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)): async def app(request:Request,app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try: try:
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{env.BASE_URL}{config.API_V1_STR}/app.json" url = f"{c.BASE_URL}{config.API_V1_STR}/app.json"
params ={"id":app} params ={"id":app}
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
except Exception as e: except Exception as e:
raise APIException('kintone:app',request.url._url, f"Error occurred while get app({env.DOMAIN_NAME}->{app}):",e) raise APIException('kintone:app',request.url._url, f"Error occurred while get app({c.DOMAIN_NAME}->{app}):",e)
@r.get("/allapps") @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: try:
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{env.BASE_URL}{config.API_V1_STR}/apps.json" url = f"{c.BASE_URL}{config.API_V1_STR}/apps.json"
offset = 0 r = httpx.get(url,headers=headers)
limit = 100 return r.json()
all_apps = []
while True:
r = httpx.get(f"{url}?limit={limit}&offset={offset}", headers=headers)
json_data = r.json()
apps = json_data.get("apps",[])
all_apps.extend(apps)
if len(apps)<limit:
break
offset += limit
return {"apps": all_apps}
except Exception as e: except Exception as e:
raise APIException('kintone:allapps', request.url._url, f"Error occurred while get allapps({env.DOMAIN_NAME}):", e) raise APIException('kintone:allapps',request.url._url, f"Error occurred while get allapps({c.DOMAIN_NAME}):",e)
@r.get("/appfields") @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: try:
return getfieldsfromkintone(app,env) return getfieldsfromkintone(app,env)
except Exception as e: except Exception as e:
raise APIException('kintone:appfields',request.url._url, f"Error occurred while get app fileds({env.DOMAIN_NAME}->{app}):",e) raise APIException('kintone:appfields',request.url._url, f"Error occurred while get app fileds({env.DOMAIN_NAME}->{app}):",e)
@r.get("/allfields") @r.get("/allfields")
async def allfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)): async def allfields(request:Request,app:str,env = Depends(getkintoneenv)):
try: try:
field_resp = getfieldsfromkintone(app,env) field_resp = getfieldsfromkintone(app,env)
form_resp = getformfromkintone(app,env) form_resp = getformfromkintone(app,env)
@@ -592,44 +538,44 @@ async def allfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(get
raise APIException('kintone:allfields',request.url._url, f"Error occurred while get form fileds({env.DOMAIN_NAME}->{app}):",e) raise APIException('kintone:allfields',request.url._url, f"Error occurred while get form fileds({env.DOMAIN_NAME}->{app}):",e)
@r.get("/appprocess") @r.get("/appprocess")
async def appprocess(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)): async def appprocess(request:Request,app:str,env = Depends(getkintoneenv)):
try: try:
return getprocessfromkintone(app,env) return getprocessfromkintone(app,env)
except Exception as e: except Exception as e:
raise APIException('kintone:appprocess',request.url._url, f"Error occurred while get app process({env.DOMAIN_NAME}->{app}):",e) raise APIException('kintone:appprocess',request.url._url, f"Error occurred while get app process({env.DOMAIN_NAME}->{app}):",e)
@r.get("/alljscss") @r.get("/alljscss")
async def alljscs(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)): async def alljscs(request:Request,app:str,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try: try:
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
url = f"{env.BASE_URL}{config.API_V1_STR}/app/customize.json" url = f"{c.BASE_URL}{config.API_V1_STR}/app/customize.json"
params = {"app":app} params = {"app":app}
r = httpx.get(url,headers=headers,params=params) r = httpx.get(url,headers=headers,params=params)
return r.json() return r.json()
except Exception as e: 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_NAME}->{app}):",e)
@r.post("/createapp",) @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: 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} 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)) r = httpx.post(url,headers=headers,data=json.dumps(data))
result = r.json() result = r.json()
if result.get("app") != None: if result.get("app") != None:
url = f"{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} data = {"apps":[result],"revert": False}
r = httpx.post(url,headers=headers,data=json.dumps(data)) r = httpx.post(url,headers=headers,data=json.dumps(data))
return r.json return r.json
except Exception as e: except Exception as e:
raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({env.DOMAIN_NAME}->{name}):",e) raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({c.DOMAIN_NAME}->{name}):",e)
@r.post("/createappfromexcel",) @r.post("/createappfromexcel",)
async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv),db = Depends(get_db)): async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv)):
try: try:
mapping = getkintoneformat(db)[format] mapping = getkintoneformat()[format]
except Exception as e: except Exception as e:
raise APIException('kintone:createappfromexcel',request.url._url, f"Error occurred while get kintone format:",e) raise APIException('kintone:createappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
@@ -666,9 +612,9 @@ async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...
@r.post("/updateappfromexcel") @r.post("/updateappfromexcel")
async def updateappfromexcel(request:Request,app:str,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv),db = Depends(get_db)): async def updateappfromexcel(request:Request,app:str,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv)):
try: try:
mapping = getkintoneformat(db)[format] mapping = getkintoneformat()[format]
except Exception as e: except Exception as e:
raise APIException('kintone:updateappfromexcel',request.url._url, f"Error occurred while get kintone format:",e) raise APIException('kintone:updateappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
@@ -758,17 +704,16 @@ async def updateprocessfromexcel(request:Request,app:str,env = Depends(getkinton
@r.post("/createjstokintone",) @r.post("/createjstokintone",)
async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv),db = Depends(get_db)): async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
try: try:
jscs=[] jscs=[]
files=[] files=[]
files.append(createappjs(env.BASE_URL, app, db)) files.append(createappjs(env.DOMAIN_ID, app))
files.append(getTempPath('alc_runtime.js')) files.append(getTempPath('alc_runtime.js'))
files.append(getTempPath('alc_runtime.css')) files.append(getTempPath('alc_runtime.css'))
for file in files: for file in files:
upload = uploadkintonefiles(file,env) upload = uploadkintonefiles(file,env)
if upload.get('fileKey') != None: if upload.get('fileKey') != None:
print(upload)
jscs.append({ file :upload['fileKey']}) jscs.append({ file :upload['fileKey']})
appjscs = updateappjscss(app,jscs,env) appjscs = updateappjscss(app,jscs,env)
if appjscs.get("revision") != None: if appjscs.get("revision") != None:

View File

@@ -1,152 +1,14 @@
from http import HTTPStatus from fastapi import Request,Depends, APIRouter, UploadFile,HTTPException,File
from fastapi import Query, Request,Depends, APIRouter, UploadFile,HTTPException,File from app.db import Base,engine
from fastapi.responses import JSONResponse from app.db.session import get_db
# from app.core.operation import log_operation
# from app.db import Base,engine
from app.core.dbmanager import get_db
from app.db.crud import * from app.db.crud import *
from app.db.schemas import * from app.db.schemas import *
from typing import List, Optional from typing import List
from app.core.auth import get_current_active_user,get_current_user from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException from app.core.apiexception import APIException
from app.core.common import ApiReturnModel,ApiReturnPage
#from fastapi_pagination import Page
from app.db.cruddb import domainService,appService
import httpx
import app.core.config as config
from app.core import domainCacheService,tenantCacheService
platform_router = r = APIRouter() platform_router = r = APIRouter()
@r.get(
"/test",
response_model_exclude_none=True,
)
async def test(
request: Request,
user = Depends(get_current_active_user),
db=Depends(get_db),
):
domainService.select(db,{"tenantid":1,"name":["b","c"]})
@r.get(
"/apps",tags=["App"],
response_model=ApiReturnModel[List[AppList]|None],
response_model_exclude_none=True,
)
async def apps_list(
request: Request,
user = Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domain = domainService.get_default_domain(db,user.id) #get_activedomain(db, user.id)
if not domain:
return ApiReturnModel(data = None)
filtered_apps = []
platformapps = appService.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}
for papp in platformapps:
if papp.appid in kintone_apps_dict:
papp.appname = kintone_apps_dict[papp.appid]["name"]
filtered_apps.append(papp)
return ApiReturnModel(data = filtered_apps)
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get apps:",e)
@r.post("/apps", tags=["App"],
response_model=ApiReturnModel[AppList|None],
response_model_exclude_none=True)
async def apps_update(
request: Request,
app: VersionUpdate,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data =appService.update_appversion(db, domainurl,app,user.id))
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get create app :",e)
@r.delete("/apps/{appid}",tags=["App"],
response_model=ApiReturnModel[AppList|None],
response_model_exclude_none=True
)
async def apps_delete(
request: Request,
appid: str,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data =appService.delete_app(db, domainurl,appid))
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while delete app({appid}):",e)
@r.get(
"/appversions/{appid}",tags=["App"],
response_model=ApiReturnPage[AppVersion|None],
response_model_exclude_none=True,
)
async def appversions_list(
request: Request,
appid: str,
user = Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domainurl:
return ApiReturnPage(data = None)
return appService.get_appversions(db,domainurl,appid)
except Exception as e:
raise APIException('platform:appversions',request.url._url,f"Error occurred while get app({appid}) version :",e)
@r.put(
"/appversions/{appid}/{version}",tags=["App"],
response_model=ApiReturnModel[AppList|None],
response_model_exclude_none=True
)
async def appversions_change(
request: Request,
appid: str,
version: int,
user = Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domainurl:
ApiReturnModel(data = None)
return ApiReturnModel(data = appService.change_appversion(db, domainurl,appid,version,user.id))
except Exception as e:
raise APIException('platform:appversions',request.url._url,f"Error occurred while change app version:",e)
@r.get( @r.get(
"/appsettings/{id}", "/appsettings/{id}",
response_model=App, response_model=App,
@@ -237,177 +99,127 @@ async def action_data(
raise APIException('platform:actions',request.url._url,f"Error occurred while get actions:",e) raise APIException('platform:actions',request.url._url,f"Error occurred while get actions:",e)
@r.get( @r.get(
"/flow/{appid}",tags=["App"], "/flow/{flowid}",
response_model=ApiReturnModel[List[Flow]|None], response_model=Flow,
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def flow_details( async def flow_details(
request: Request, request: Request,
appid: str, flowid: str,
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) app = get_flow(db, flowid)
if not domainurl: return app
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.get_flow(db, domainurl, appid))
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e)
@r.get( @r.get(
"/flows/{appid}", tags=["App"], "/flows/{appid}",
response_model=List[Flow|None], response_model=List[Flow],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def flow_list( async def flow_list(
request: Request, request: Request,
appid: str, appid: str,
user=Depends(get_current_active_user), user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id) domain = get_activedomain(db, user.id)
if not domainurl: print("domain=>",domain)
return [] flows = get_flows_by_app(db, domain.id, appid)
#flows = get_flows_by_app(db, domainurl, appid)
flows = appService.get_flow(db,domainurl,appid)
return flows return flows
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
@r.post("/flow", tags=["App"], @r.post("/flow", response_model=Flow, response_model_exclude_none=True)
response_model=ApiReturnModel[Flow|None],
response_model_exclude_none=True)
async def flow_create( async def flow_create(
request: Request, request: Request,
flow: FlowIn, flow: FlowBase,
user=Depends(get_current_active_user), user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id) domain = get_activedomain(db, user.id)
if not domainurl: return create_flow(db, domain.id, flow)
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.create_flow(db, domainurl, flow,user.id))
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
@r.put( @r.put(
"/flow", tags=["App"], "/flow/{flowid}", response_model=Flow, response_model_exclude_none=True
response_model=ApiReturnModel[Flow|None],
response_model_exclude_none=True
) )
async def flow_edit( async def flow_edit(
request: Request, request: Request,
flow: FlowIn, flow: FlowBase,
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) return edit_flow(db, flow)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.edit_flow(db,domainurl, flow,user.id))
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
@r.delete( @r.delete(
"/flow/{flowid}", tags=["App"], "/flow/{flowid}", response_model=Flow, response_model_exclude_none=True
response_model=ApiReturnModel[Flow|None],
response_model_exclude_none=True
) )
async def flow_delete( async def flow_delete(
request: Request, request: Request,
flowid: str, flowid: str,
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) return delete_flow(db, flowid)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.delete_flow(db, flowid))
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while delete flow:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while delete flow:",e)
@r.get( @r.get(
"/domains",tags=["Domain"], "/domains/{tenantid}",
response_model=ApiReturnPage[Domain], response_model=List[Domain],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def domain_list( async def domain_details(
request: Request, request: Request,
user=Depends(get_current_active_user), tenantid:str,
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
if user.is_superuser: domains = get_domains(db,tenantid)
domains = domainService.get_domains(db) return domains
else:
domains = domainService.get_domains_by_manage(db,user.id)
return domains
except Exception as e: except Exception as e:
raise APIException('platform:domains',request.url._url,f"Error occurred while get domains:",e) raise APIException('platform:domains',request.url._url,f"Error occurred while get domains:",e)
@r.get( @r.post("/domain", response_model=Domain, response_model_exclude_none=True)
"/domain/{domain_id}",tags=["Domain"],
response_model=ApiReturnModel[Domain|None],
response_model_exclude_none=True,
)
async def domain_detail(
request: Request,
domain_id:int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return ApiReturnModel(data = domainService.get(db,domain_id))
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while get domain detail:",e)
@r.post("/domain", tags=["Domain"],
response_model=ApiReturnModel[Domain],
response_model_exclude_none=True)
async def domain_create( async def domain_create(
request: Request, request: Request,
domain: DomainIn, domain: DomainBase,
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return ApiReturnModel(data = domainService.create_domain(db, domain,user.id)) return create_domain(db, domain)
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e) raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e)
@r.put( @r.put(
"/domain", tags=["Domain"], "/domain", response_model=Domain, response_model_exclude_none=True
response_model=ApiReturnModel[Domain|None],
response_model_exclude_none=True
) )
async def domain_edit( async def domain_edit(
request: Request, request: Request,
domain: DomainIn, domain: DomainBase,
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domain = domainService.edit_domain(db, domain,user.id) return edit_domain(db, domain)
if domain :
domainCacheService.clear_default_domainurl()
return ApiReturnModel(data = domain)
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e) raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e)
@r.delete( @r.delete(
"/domain/{id}",tags=["Domain"], "/domain/{id}", response_model=Domain, response_model_exclude_none=True
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
) )
async def domain_delete( async def domain_delete(
request: Request, request: Request,
@@ -415,9 +227,9 @@ async def domain_delete(
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return ApiReturnModel(data = domainService.delete_domain(db,id)) return delete_domain(db,id)
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain({id}):",e) raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain:",e)
@r.get( @r.get(
"/domain", "/domain",
@@ -426,164 +238,77 @@ async def domain_delete(
) )
async def userdomain_details( async def userdomain_details(
request: Request, request: Request,
userId: Optional[int] = Query(None, alias="userId"), user=Depends(get_current_user),
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domains = get_domain(db, userId if userId is not None else user.id) domains = get_domain(db, user.id)
return domains return domains
except Exception as e: except Exception as e:
raise APIException('platform:userdomain',request.url._url,f"Error occurred while get user({user.id}) domain:",e) raise APIException('platform:domain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
@r.post( @r.post(
"/userdomain",tags=["Domain"], "/domain/{userid}",
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def create_userdomain( async def create_userdomain(
request: Request, request: Request,
userdomain:UserDomainParam, userid: int,
user=Depends(get_current_active_user), domainids:list,
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
userid = userdomain.userid domain = add_userdomain(db, userid,domainids)
domainid = userdomain.domainid return domain
if user.is_superuser:
domain = domainService.add_userdomain(db,user.id,userid,domainid)
else:
domain = domainService.add_userdomain_by_owner(db,user.id,userid,domainid)
return ApiReturnModel(data = domain)
except Exception as e: except Exception as e:
raise APIException('platform:userdomain',request.url._url,f"Error occurred while add user({userid}) domain({domainid}):",e) raise APIException('platform:domain',request.url._url,f"Error occurred while add user({userid}) domain:",e)
@r.delete( @r.delete(
"/domain/{domainid}/{userid}",tags=["Domain"], "/domain/{domainid}/{userid}", response_model_exclude_none=True
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
) )
async def delete_userdomain( async def userdomain_delete(
request: Request, request: Request,
domainid:int, domainid:int,
userid: int, userid: int,
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return ApiReturnModel(data = domainService.delete_userdomain(db,userid,domainid)) return delete_userdomain(db, userid,domainid)
except Exception as e: except Exception as e:
raise APIException('platform:userdomain',request.url._url,f"Error occurred while delete user({userid}) domain({domainid}):",e) raise APIException('platform:delete',request.url._url,f"Error occurred while delete user({userid}) domain:",e)
@r.get( @r.get(
"/managedomainuser/{domainid}",tags=["Domain"], "/activedomain",
response_model=ApiReturnPage[UserOut|None], response_model=Domain,
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def get_managedomainuser( async def get_useractivedomain(
request: Request, request: Request,
domainid:int, user=Depends(get_current_user),
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return domainService.get_managedomain_users(db,domainid) domain = get_activedomain(db, user.id)
return domain
except Exception as e: except Exception as e:
raise APIException('platform:managedomain',request.url._url,f"Error occurred while get managedomain({user.id}) user:",e) raise APIException('platform:activedomain',request.url._url,f"Error occurred while get user({user.id}) activedomain:",e)
@r.post(
"/managedomain",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def create_managedomain(
request: Request,
userdomain:UserDomainParam,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
userid = userdomain.userid
domainid = userdomain.domainid
if user.is_superuser:
domain = domainService.add_managedomain(db,user.id,userid,domainid)
else:
domain = domainService.add_managedomain_by_owner(db,user.id,userid,domainid)
return ApiReturnModel(data = domain)
except Exception as e:
raise APIException('platform:managedomain',request.url._url,f"Error occurred while add manage({userid}) domain({domainid}):",e)
@r.delete(
"/managedomain/{domainid}/{userid}",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def delete_managedomain(
request: Request,
domainid:int,
userid: int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return ApiReturnModel(data = domainService.delete_managedomain(db,userid,domainid))
except Exception as e:
raise APIException('platform:managedomain',request.url._url,f"Error occurred while delete managedomain({userid}) domain({domainid}):",e)
@r.get(
"/defaultdomain",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def get_defaultuserdomain(
request: Request,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return ApiReturnModel(data =domainService.get_default_domain(db,user.id))
except Exception as e:
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while get user({user.id}) defaultdomain:",e)
@r.put( @r.put(
"/defaultdomain/{domainid}",tags=["Domain"], "/activedomain/{domainid}",
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def set_defualtuserdomain( async def update_activeuserdomain(
request: Request, request: Request,
domainid:int, domainid:int,
user=Depends(get_current_active_user), user=Depends(get_current_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domain = domainCacheService.set_default_domain(db,user.id,domainid) domain = active_userdomain(db, user.id,domainid)
return ApiReturnModel(data= domain) return domain
except Exception as e: except Exception as e:
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while update user({user.id}) defaultdomain:",e) raise APIException('platform:activedomain',request.url._url,f"Error occurred while update user({user.id}) activedomain:",e)
@r.get(
"/domainshareduser/{domainid}",tags=["Domain"],
response_model=ApiReturnPage[UserOut|None],
response_model_exclude_none=True,
)
async def get_domainshareduser(
request: Request,
domainid:int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return domainService.get_shareddomain_users(db,domainid)
except Exception as e:
raise APIException('platform:sharedomain',request.url._url,f"Error occurred while get user({user.id}) sharedomain:",e)
@r.get( @r.get(
"/events", "/events",

View File

@@ -1,187 +1,107 @@
from fastapi import APIRouter, Request, Depends, Response, Security, encoders from fastapi import APIRouter, Request, Depends, Response, encoders
import typing as t import typing as t
from app.core.common import ApiReturnModel,ApiReturnPage
from app.core.apiexception import APIException from app.db.session import get_db
from app.core.dbmanager import get_db
from app.db.crud import ( from app.db.crud import (
get_allusers,
get_users, get_users,
get_user, get_user,
create_user, create_user,
delete_user, delete_user,
edit_user, edit_user,
assign_userrole,
get_roles,
) )
from app.db.schemas import UserCreate, UserEdit, User, UserOut,RoleBase,AssignUserRoles,Permission from app.db.schemas import UserCreate, UserEdit, User, UserOut
from app.core.auth import get_current_user,get_current_active_user, get_current_active_superuser from app.core.auth import get_current_active_user, get_current_active_superuser
from app.db.cruddb import userService
from app.core import tenantCacheService
users_router = r = APIRouter() users_router = r = APIRouter()
@r.get( @r.get(
"/users",tags=["User"], "/users",
response_model=ApiReturnPage[User], response_model=t.List[User],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def users_list( async def users_list(
request: Request, response: Response,
db=Depends(get_db), db=Depends(get_db),
current_user=Depends(get_current_active_user), current_user=Depends(get_current_active_superuser),
): ):
try: """
if current_user.is_superuser: Get all users
users = userService.get_users(db) """
else: users = get_users(db)
users = userService.get_users_not_admin(db) # This is necessary for react-admin to work
return users response.headers["Content-Range"] = f"0-9/{len(users)}"
except Exception as e: return users
raise APIException('user:users',request.url._url,f"Error occurred while get user list",e)
@r.get("/users/me", tags=["User"],
response_model=ApiReturnModel[User], @r.get("/users/me", response_model=User, response_model_exclude_none=True)
response_model_exclude_none=True,
)
async def user_me(current_user=Depends(get_current_active_user)): async def user_me(current_user=Depends(get_current_active_user)):
return ApiReturnModel(data = current_user) """
Get own user
"""
return current_user
@r.get( @r.get(
"/users/{user_id}",tags=["User"], "/users/{user_id}",
response_model=ApiReturnModel[User|None], response_model=User,
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def user_details( async def user_details(
request: Request, request: Request,
user_id: int, user_id: int,
db=Depends(get_db), db=Depends(get_db),
current_user=Depends(get_current_active_user), current_user=Depends(get_current_active_superuser),
): ):
try: """
user = userService.get(db, user_id) Get any user details
if user: """
if user.is_superuser and not current_user.is_superuser: user = get_user(db, user_id)
user = None return user
return ApiReturnModel(data = user) # return encoders.jsonable_encoder(
except Exception as e: # user, skip_defaults=True, exclude_none=True,
raise APIException('user:users',request.url._url,f"Error occurred while get user({user_id}) detail:",e) # )
@r.post("/users", tags=["User"], @r.post("/users", response_model=User, response_model_exclude_none=True)
response_model=ApiReturnModel[User|None],
response_model_exclude_none=True,
)
async def user_create( async def user_create(
request: Request, request: Request,
user: UserCreate, user: UserCreate,
db=Depends(get_db), db=Depends(get_db),
current_user=Depends(get_current_active_user), current_user=Depends(get_current_active_superuser),
): ):
try: """
if user.is_superuser and not current_user.is_superuser: Create a new user
return ApiReturnModel(data = None) """
return ApiReturnModel(data =userService.create_user(db, user,current_user.id)) return create_user(db, user)
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while create user({user.email}):",e)
@r.put( @r.put(
"/users/{user_id}", tags=["User"], "/users/{user_id}", response_model=User, response_model_exclude_none=True
response_model=ApiReturnModel[User|None],
response_model_exclude_none=True,
) )
async def user_edit( async def user_edit(
request: Request, request: Request,
user_id: int, user_id: int,
user: UserEdit, user: UserEdit,
db=Depends(get_db), db=Depends(get_db),
current_user=Depends(get_current_active_user), current_user=Depends(get_current_active_superuser),
): ):
try: """
if user.is_superuser and not current_user.is_superuser: Update existing user
return ApiReturnModel(data = None) """
return ApiReturnModel(data = userService.edit_user(db,user_id,user,current_user.id)) return edit_user(db, user_id, user)
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while edit user({user_id}):",e)
@r.delete( @r.delete(
"/users/{user_id}", tags=["User"], "/users/{user_id}", response_model=User, response_model_exclude_none=True
response_model=ApiReturnModel[UserOut|None],
response_model_exclude_none=True
) )
async def user_delete( async def user_delete(
request: Request, request: Request,
user_id: int, user_id: int,
db=Depends(get_db), db=Depends(get_db),
current_user=Depends(get_current_active_user), current_user=Depends(get_current_active_superuser),
): ):
try: """
user = userService.get(db,user_id) Delete existing user
if user.is_superuser and not current_user.is_superuser: """
return ApiReturnModel(data = None) return delete_user(db, user_id)
return ApiReturnModel(data = userService.delete_user(db, user_id))
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while delete user({user_id}):",e)
@r.post("/userrole",tags=["User"],
response_model=ApiReturnModel[User],
response_model_exclude_none=True,)
async def assign_role(
request: Request,
userroles:AssignUserRoles,
db=Depends(get_db)
):
try:
return ApiReturnModel(data = userService.assign_userrole(db,userroles.userid,userroles.roleids))
except Exception as e:
raise APIException('user:userrole',request.url._url,f"Error occurred while assign user({userroles.userid}) roles({userroles.roleids}):",e)
@r.get(
"/roles",tags=["User"],
response_model=ApiReturnModel[t.List[RoleBase]|None],
response_model_exclude_none=True,
)
async def roles_list(
request: Request,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
#current_user=Security(get_current_active_user, scopes=["role_list"]),
):
try:
if current_user.is_superuser:
roles = userService.get_roles(db)
else:
if len(current_user.roles)>0:
roles = userService.get_roles_by_level(db,current_user.roles)
else:
roles = []
return ApiReturnModel(data = roles)
except Exception as e:
raise APIException('user:roles',request.url._url,f"Error occurred while get roles:",e)
@r.get(
"/userpermssions",tags=["User"],
response_model=ApiReturnModel[t.List[Permission]|None],
response_model_exclude_none=True,
)
async def permssions_list(
request: Request,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
#current_user=Security(get_current_active_user, scopes=["role_list"]),
):
try:
if current_user.is_superuser:
permissions = userService.get_permissions(db)
else:
if len(current_user.roles)>0:
permissions = userService.get_user_permissions(db,current_user.id)
else:
permissions = []
return ApiReturnModel(data = permissions)
except Exception as e:
raise APIException('user:userpermssions',request.url._url,f"Error occurred while get user({current_user.id}) permissions:",e)

View File

@@ -1,2 +0,0 @@
from app.core.cache import domainCacheService
from app.core.cache import tenantCacheService

View File

@@ -1,40 +1,26 @@
from fastapi import HTTPException, status,Depends from fastapi import HTTPException, status
import httpx
from app.db.schemas import ErrorCreate from app.db.schemas import ErrorCreate
from app.core.dbmanager import get_log_db from app.db.session import SessionLocal
from app.db.crud import create_log from app.db.crud import create_log
class APIException(Exception): class APIException(Exception):
def __init__(self, location: str, title: str, content: str, e: Exception):
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 += str(e.detail)
else:
self.detail = str(e)
self.status_code = 500
content += str(e)
if len(content) > 5000:
content = content[:5000]
self.error = ErrorCreate(location=location, title=title, content=content)
super().__init__(self.error)
def writedblog(exc: APIException,): def __init__(self,location:str,title:str,content:str,e:Exception):
#db = SessionLocal() if(str(e) == ''):
db = get_log_db() content += e.detail
self.detail = e.detail
self.status_code = e.status_code
else:
self.detail = str(e)
content += str(e)
self.status_code = 500
if(len(content) > 5000):
content =content[0:5000]
self.error = ErrorCreate(location=location,title=title,content=content)
def writedblog(exc: APIException):
db = SessionLocal()
try: try:
create_log(db,exc.error) create_log(db,exc.error)
finally: finally:
db.close() db.close()

View File

@@ -1,16 +1,14 @@
from fastapi.security import SecurityScopes
import jwt import jwt
from fastapi import Depends, HTTPException, Request, Security, status from fastapi import Depends, HTTPException, status
from jwt import PyJWTError from jwt import PyJWTError
from app.db import models, schemas from app.db import models, schemas, session
from app.db.crud import get_user_by_email, create_user,get_user from app.db.crud import get_user_by_email, create_user,get_user
from app.core import security from app.core import security
from app.db.cruddb import userService
from app.core.dbmanager import get_db
async def get_current_user(request: Request,security_scopes: SecurityScopes,
db=Depends(get_db), token: str = Depends(security.oauth2_scheme) async def get_current_user(
db=Depends(session.get_db), token: str = Depends(security.oauth2_scheme)
): ):
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@@ -24,25 +22,13 @@ async def get_current_user(request: Request,security_scopes: SecurityScopes,
id: int = payload.get("sub") id: int = payload.get("sub")
if id is None: if id is None:
raise credentials_exception raise credentials_exception
tenant:str = payload.get("tenant")
if tenant is None:
raise credentials_exception
permissions: str = payload.get("permissions") permissions: str = payload.get("permissions")
if not permissions =="ALL":
for scope in security_scopes.scopes:
if scope not in permissions.split(";"):
raise HTTPException(
status_code=403, detail="The user doesn't have enough privileges"
)
token_data = schemas.TokenData(id = id, permissions=permissions) token_data = schemas.TokenData(id = id, permissions=permissions)
except PyJWTError: except PyJWTError:
raise credentials_exception raise credentials_exception
user = userService.get_user(db, token_data.id) user = get_user(db, token_data.id)
if user is None: if user is None:
raise credentials_exception raise credentials_exception
request.state.user = user.id
return user return user
async def get_current_active_user( async def get_current_active_user(
@@ -64,11 +50,11 @@ async def get_current_active_superuser(
def authenticate_user(db, email: str, password: str): def authenticate_user(db, email: str, password: str):
user = userService.get_user_by_email(db,email) #get_user_by_email(db, email) user = get_user_by_email(db, email)
if not user: if not user:
return None return False
if not security.verify_password(password, user.hashed_password): if not security.verify_password(password, user.hashed_password):
return None return False
return user return user

View File

@@ -1,72 +0,0 @@
import time
from typing import Any
from sqlalchemy.orm import Session
from app.db.cruddb import domainService,tenantService
from app.db.session import Database
class MemoryCache:
def __init__(self, max_cache_size: int = 100, ttl: int = 60):
self.cache = {}
self.max_cache_size = max_cache_size
self.ttl = ttl
def get(self, key: str) -> Any:
item = self.cache.get(key)
if item:
if time.time() - item['timestamp'] > self.ttl:
self.cache.pop(key)
return None
return item['value']
return None
def set(self, key: str, value: Any) -> None:
if len(self.cache) >= self.max_cache_size:
self.cache.pop(next(iter(self.cache)))
self.cache[key] = {'value': value, 'timestamp': time.time()}
# def clear(self,key) -> None:
# self.cache.pop(key,None)
def clear(self) -> None:
self.cache.clear()
class domainCache:
def __init__(self):
self.memoryCache = MemoryCache(max_cache_size=50, ttl=120)
def set_default_domain(self, db: Session,userid: int,domainid:str):
domain = domainService.set_default_domain(db,userid,domainid)
if domain:
self.memoryCache.set(f"DOMAIN_{userid}",domain.url)
return domain
def get_default_domainurl(self,db: Session, userid: int):
if not self.memoryCache.get(f"DOMAIN_{userid}"):
domain = domainService.get_default_domain(db,userid)
if domain:
self.memoryCache.set(f"DOMAIN_{userid}",domain.url)
return self.memoryCache.get(f"DOMAIN_{userid}")
def clear_default_domainurl(self):
self.memoryCache.clear()
domainCacheService =domainCache()
class tenantCache:
def __init__(self):
self.memoryCache = MemoryCache(max_cache_size=50, ttl=120)
def get_tenant_db(self,db: Session, tenantid: str):
if not self.memoryCache.get(f"TENANT_{tenantid}"):
tenant = tenantService.get_tenant(db,tenantid)
if tenant:
database = Database(tenant.db)
self.memoryCache.set(f"TENANT_{tenantid}",database)
return self.memoryCache.get(f"TENANT_{tenantid}")
tenantCacheService =tenantCache()

View File

@@ -1,49 +0,0 @@
import math
from fastapi import Query
from fastapi_pagination.bases import AbstractPage,AbstractParams,RawParams
from pydantic import BaseModel
from typing import Any, Generic, List, Type,TypeVar,Generic,Sequence
from fastapi_pagination import Page,utils
T = TypeVar('T')
class ApiReturnModel(BaseModel,Generic[T]):
code:int = 0
msg:str ="OK"
data:T
class ApiReturnError(BaseModel):
code:int = -1
msg:str =""
class Params(BaseModel, AbstractParams):
page:int = Query(1,get=1, description="Page number")
size:int = Query(20,get=0, le=100,description="Page size")
def to_raw_params(self) -> RawParams:
return RawParams(
limit=self.size,
offset=self.size*(self.page-1)
)
class ApiReturnPage(AbstractPage[T],Generic[T]):
code:int =0
msg:str ="OK"
data:Sequence[T]
total:int
page:int
size:int
# next:str
# previous:str
total_pages:int
__params_type__ =Params
@classmethod
def create(cls,items:Sequence[T],params:Params,**kwargs: Any) -> Type[Page[T]]:
total = kwargs.get('total', 0)
total_pages = math.ceil(total/params.size)
return utils.create_pydantic_model(cls,data=items,total=total,page=params.page,size=params.size,total_pages=total_pages)

View File

@@ -1,13 +1,11 @@
import os import os
import base64 import base64
PROJECT_NAME = "KintoneAppBuilder" PROJECT_NAME = "KintoneAppBuilder"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/dev" #SQLALCHEMY_DATABASE_URI = "postgres://maxz64:m@xz1205@alicornkintone.postgres.database.azure.com/postgres"
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://kabAdmin:P%40ssw0rd!@kintonetooldb.postgres.database.azure.com/dev_v2" SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/postgres"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/test" #SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/unittest"
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@ktune-prod-db.postgres.database.azure.com/postgres"
API_V1_STR = "/k/v1" API_V1_STR = "/k/v1"
API_V1_AUTH_KEY = "X-Cybozu-Authorization" API_V1_AUTH_KEY = "X-Cybozu-Authorization"
@@ -15,13 +13,12 @@ API_V1_AUTH_KEY = "X-Cybozu-Authorization"
DEPLOY_MODE = "PROD" #DEV,PROD DEPLOY_MODE = "PROD" #DEV,PROD
DEPLOY_JS_URL = "https://ka-addin.azurewebsites.net/alc_runtime.js" DEPLOY_JS_URL = "https://ka-addin.azurewebsites.net/alc_runtime.js"
#DEPLOY_JS_URL = "https://ce1c-133-139-70-194.ngrok-free.app/alc_runtime.js"
KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"] KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"]
KINTONE_FIELD_PROPERTY=['label','code','type','required','unique','maxValue','minValue','maxLength','minLength','defaultValue','defaultNowValue','options','expression','hideExpression','digit','protocol','displayScale','unit','unitPosition'] KINTONE_FIELD_PROPERTY=['label','code','type','required','unique','maxValue','minValue','maxLength','minLength','defaultValue','defaultNowValue','options','expression','hideExpression','digit','protocol','displayScale','unit','unitPosition']
KINTONE_PSW_CRYPTO_KEY=bytes.fromhex("53 6c 93 bd 48 ad b5 c0 93 df a1 27 25 a1 a3 32 a2 03 3b a0 27 1f 51 dc 20 0e 6c d7 be fc fb ea")
class KINTONE_ENV: class KINTONE_ENV:
BASE_URL = "" BASE_URL = ""
@@ -39,4 +36,4 @@ class KINTONE_ENV:
self.DOMAIN_ID=domain.id self.DOMAIN_ID=domain.id
self.BASE_URL = domain.url self.BASE_URL = domain.url
self.KINTONE_USER = domain.kintoneuser self.KINTONE_USER = domain.kintoneuser
self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.decrypt_kintonepwd()}","utf-8")) self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.kintonepwd}","utf-8"))

View File

@@ -1,20 +0,0 @@
from fastapi import Depends,Request
from app.db.session import get_tenant_db
from app.core import tenantCacheService
from app.db.session import tenantdb
def get_db(request: Request,tenant:str = "1",tenantdb = Depends(get_tenant_db)):
database = tenantCacheService.get_tenant_db(tenantdb,tenant)
try:
db = database.get_db()
request.state.tenant = tenant
request.state.db = db
yield db
finally:
db.close()
def get_log_db():
db = tenantdb.get_db()
return db

View File

@@ -1,75 +0,0 @@
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from sqlalchemy.orm import Session
from app.db.models import OperationLog,User
from app.core.apiexception import APIException
from app.core.dbmanager import get_log_db
from app.db.crud import create_log
import json
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.method in ("POST", "PUT", "PATCH","DELETE"):
try:
request.state.body = await request.json()
except json.JSONDecodeError:
request.state.body = await request.body()
else:
request.state.body = None
try:
response = await call_next(request)
state = request.state
except Exception as e:
await self.log_error(request, e)
response = JSONResponse(
content={"detail": "Internal Server Error"},
status_code=500
)
if hasattr(request.state, "user") and hasattr(request.state, "tenant"):
await self.log_request(request, response,state)
return response
async def log_request(self, request: Request, response,state):
try:
headers = dict(request.headers)
route = request.scope.get("route")
if route:
path_template = route.path
else:
path_template = request.url.path
db_operation = OperationLog(tenantid =request.state.tenant,
clientip = request.client.host if request.client else None,
useragent =headers.get("user-agent", ""),
userid = request.state.user,
operation = request.method,
function = path_template,
parameters = str({"path": request.path_params,"query": dict(request.query_params),"body": request.state.body}),
response = f"status_code:{response.status_code }" )
db = request.state.db
if db:
await self.write_log_to_db(db_operation,db)
except Exception as e:
print(f"Logging failed: {str(e)}")
async def log_error(self, request: Request, e: Exception):
exc = APIException('operation:dispatch',request.url._url,f"Error occurred while writting operation log:",e)
db = get_log_db()
try:
create_log(db,exc.error)
finally:
db.close()
async def write_log_to_db(self, db_operation,db):
db.add(db_operation)
db.commit()

View File

@@ -1,11 +1,7 @@
import jwt import jwt
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext from passlib.context import CryptContext
from datetime import datetime, timedelta,timezone from datetime import datetime, timedelta
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
import os
import base64
from app.core import config
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
@@ -27,38 +23,9 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
def create_access_token(*, data: dict, expires_delta: timedelta = None): def create_access_token(*, data: dict, expires_delta: timedelta = None):
to_encode = data.copy() to_encode = data.copy()
if expires_delta: if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta expire = datetime.utcnow() + expires_delta
else: else:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt return encoded_jwt
def chacha20Encrypt(plaintext:str, key=config.KINTONE_PSW_CRYPTO_KEY):
if plaintext is None or plaintext == '':
return None
nonce = os.urandom(16)
algorithm = algorithms.ChaCha20(key, nonce)
cipher = Cipher(algorithm, mode=None)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext.encode('utf-8')) + encryptor.finalize()
return base64.b64encode(nonce +'𒀸'.encode('utf-8')+ ciphertext).decode('utf-8')
def chacha20Decrypt(encoded_str:str, key=config.KINTONE_PSW_CRYPTO_KEY):
try:
decoded_data = base64.b64decode(encoded_str)
if len(decoded_data) < 18:
return encoded_str
special_char = decoded_data[16:20]
if special_char != '𒀸'.encode('utf-8'):
return encoded_str
nonce = decoded_data[:16]
ciphertext = decoded_data[20:]
except Exception as e:
print(f"An error occurred: {e}")
return encoded_str
algorithm = algorithms.ChaCha20(key, nonce)
cipher = Cipher(algorithm, mode=None)
decryptor = cipher.decryptor()
plaintext_bytes = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext_bytes.decode('utf-8')

View File

@@ -1,11 +1,10 @@
from datetime import datetime
from fastapi import HTTPException, status from fastapi import HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import and_ from sqlalchemy import and_
import typing as t import typing as t
from . import models, schemas from . import models, schemas
from app.core.security import chacha20Decrypt, get_password_hash from app.core.security import get_password_hash
def get_user(db: Session, user_id: int): def get_user(db: Session, user_id: int):
@@ -19,15 +18,10 @@ def get_user_by_email(db: Session, email: str) -> schemas.UserBase:
return db.query(models.User).filter(models.User.email == email).first() return db.query(models.User).filter(models.User.email == email).first()
def get_allusers(
db: Session
) -> t.List[schemas.UserOut]:
return db.query(models.User).all()
def get_users( def get_users(
db: Session db: Session, skip: int = 0, limit: int = 100
) -> t.List[schemas.UserOut]: ) -> t.List[schemas.UserOut]:
return db.query(models.User).filter(models.User.is_superuser == False) return db.query(models.User).offset(skip).limit(limit).all()
def create_user(db: Session, user: schemas.UserCreate): def create_user(db: Session, user: schemas.UserCreate):
@@ -76,80 +70,6 @@ def edit_user(
return db_user return db_user
def get_roles(
db: Session
) -> t.List[schemas.RoleBase]:
return db.query(models.Role).all()
def assign_userrole( db: Session, user_id: int, roles: t.List[int]):
db_user = db.query(models.User).get(user_id)
if db_user:
for role in db_user.roles:
db_user.roles.remove(role)
for roleid in roles:
role = db.query(models.Role).get(roleid)
if role:
db_user.roles.append(role)
db.commit()
db.refresh(db_user)
return db_user
def get_apps(
db: Session,
domainurl:str
) -> t.List[schemas.AppList]:
return db.query(models.App).filter(models.App.domainurl == domainurl).all()
def update_appversion(db: Session, appedit: schemas.AppVersion,userid:int):
db_app = db.query(models.App).filter(and_(models.App.domainurl == appedit.domainurl,models.App.appid == appedit.appid)).first()
if not db_app:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="User not found")
db_app.version = db_app.version + 1
appversion = models.AppVersion(
domainurl = appedit.domainurl,
appid=appedit.appid,
appname=db_app.appname,
version = db_app.version,
versionname = appedit.versionname,
comment = appedit.comment,
updateuserid = userid,
createuserid = userid
)
db.add(appversion)
db.add(db_app)
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == appedit.domainurl,models.App.appid == appedit.appid))
for flow in flows:
db_flowhistory = models.FlowHistory(
flowid = flow.flowid,
appid = flow.appid,
eventid = flow.eventid,
domainurl = flow.domainurl,
name = flow.name,
content = flow.content,
createuser = userid,
version = db_app.version,
updateuserid = userid,
createuserid = userid
)
db.add(db_flowhistory)
db.commit()
db.refresh(db_app)
return db_app
def delete_apps(db: Session, domainurl: str,appid: str ):
db_app = db.query(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid ==appid)).first()
if not db_app:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="App not found")
db.delete(db_app)
db_flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid ==appid))
for flow in db_flows:
db.delete(flow)
db.commit()
return db_app
def get_appsetting(db: Session, id: int): def get_appsetting(db: Session, id: int):
app = db.query(models.AppSetting).get(id) app = db.query(models.AppSetting).get(id)
if not app: if not app:
@@ -205,28 +125,16 @@ def get_actions(db: Session):
return actions return actions
def create_flow(db: Session, domainurl: str, flow: schemas.FlowIn,userid:int): def create_flow(db: Session, domainid: int, flow: schemas.FlowBase):
db_flow = models.Flow( db_flow = models.Flow(
flowid=flow.flowid, flowid=flow.flowid,
appid=flow.appid, appid=flow.appid,
eventid=flow.eventid, eventid=flow.eventid,
domainurl=domainurl, domainid=domainid,
name=flow.name, name=flow.name,
content=flow.content, content=flow.content
createuserid = userid,
updateuserid = userid
) )
db.add(db_flow) db.add(db_flow)
db_app = db.query(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid)).first()
if not db_app:
db_app = models.App(
domainurl = domainurl,
appid=flow.appid,
appname=flow.appname,
version = 0,
createuserid= userid,
updateuserid = userid
)
db.commit() db.commit()
db.refresh(db_flow) db.refresh(db_flow)
return db_flow return db_flow
@@ -241,20 +149,16 @@ def delete_flow(db: Session, flowid: str):
def edit_flow( def edit_flow(
db: Session, domainurl: str, flow: schemas.FlowIn,userid:int db: Session, flow: schemas.FlowBase
) -> schemas.Flow: ) -> schemas.Flow:
db_flow = get_flow(db, flow.flowid) db_flow = get_flow(db, flow.flowid)
if not db_flow: if not db_flow:
#見つからない時新規作成 raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Flow not found")
return create_flow(db,domainurl,flow,userid) update_data = flow.dict(exclude_unset=True)
db_flow.appid =flow.appid for key, value in update_data.items():
db_flow.eventid=flow.eventid setattr(db_flow, key, value)
db_flow.domainurl=domainurl
db_flow.name=flow.name
db_flow.content=flow.content
db_flow.updateuserid = userid
db.add(db_flow) db.add(db_flow)
db.commit() db.commit()
db.refresh(db_flow) db.refresh(db_flow)
@@ -269,149 +173,103 @@ def get_flows(db: Session, flowid: str):
def get_flow(db: Session, flowid: str): def get_flow(db: Session, flowid: str):
flow = db.query(models.Flow).filter(models.Flow.flowid == flowid).first() flow = db.query(models.Flow).filter(models.Flow.flowid == flowid).first()
# if not flow: if not flow:
# raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
return flow return flow
def get_flows_by_app(db: Session,domainurl: str, appid: str): def get_flows_by_app(db: Session, domainid: int, appid: str):
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid == appid)).all() flows = db.query(models.Flow).filter(and_(models.Flow.domainid == domainid,models.Flow.appid == appid)).all()
if not flows: if not flows:
raise Exception("Data not found") raise Exception("Data not found")
return flows return flows
def create_domain(db: Session, domain: schemas.DomainIn,userid:int): def create_domain(db: Session, domain: schemas.DomainBase):
domain.encrypt_kintonepwd()
db_domain = models.Domain( db_domain = models.Domain(
tenantid = domain.tenantid, tenantid = domain.tenantid,
name=domain.name, name=domain.name,
url=domain.url, url=domain.url,
is_active=domain.is_active,
kintoneuser=domain.kintoneuser, kintoneuser=domain.kintoneuser,
kintonepwd=domain.kintonepwd, kintonepwd=domain.kintonepwd
createuserid = userid,
updateuserid = userid,
ownerid = domain.ownerid
) )
db.add(db_domain) db.add(db_domain)
#add_userdomain(db,userid,db_domain.id)
db.commit() db.commit()
db.refresh(db_domain) db.refresh(db_domain)
return db_domain return db_domain
def delete_domain(db: Session,id: int): def delete_domain(db: Session,id: int):
db_domain = db.query(models.Domain).get(id) db_domain = db.query(models.Domain).get(id)
#if not db_domain: if not db_domain:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found") raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
if db_domain: db.delete(db_domain)
db.delete(db_domain) db.commit()
db.commit() return db_domain
return True
def edit_domain( def edit_domain(
db: Session, domain: schemas.DomainIn,userid:int db: Session, domain: schemas.DomainBase
) -> schemas.Domain: ) -> schemas.Domain:
if domain.kintonepwd != "":
domain.encrypt_kintonepwd()
db_domain = db.query(models.Domain).get(domain.id) db_domain = db.query(models.Domain).get(domain.id)
if not db_domain: if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found") raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db_domain.tenantid = domain.tenantid update_data = domain.dict(exclude_unset=True)
db_domain.name=domain.name
db_domain.url=domain.url for key, value in update_data.items():
if db_domain.is_active == True and domain.is_active == False: if(key != "id"):
db_userdomains = db.query(models.UserDomain).filter(and_(models.UserDomain.domainid == db_domain.id,models.UserDomain.active == True)).all() setattr(db_domain, key, value)
for userdomain in db_userdomains:
userdomain.active = False
db.add(userdomain)
db_domain.is_active=domain.is_active
db_domain.kintoneuser=domain.kintoneuser
if domain.kintonepwd != "":
db_domain.kintonepwd = domain.kintonepwd
db_domain.updateuserid = userid
db_domain.ownerid = domain.ownerid
db.add(db_domain) db.add(db_domain)
db.commit() db.commit()
db.refresh(db_domain) db.refresh(db_domain)
return db_domain return db_domain
def add_userdomain(db: Session, userid:int,domainids:list):
def add_admindomain(db: Session,userid:int,domainid:int): for domainid in domainids:
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.is_active)).first() db_domain = models.UserDomain(
if db_domain: userid = userid,
user_domain = models.UserDomain(userid = userid, domainid = domainid ) domainid = domainid
db.add(user_domain) )
db.commit() db.add(db_domain)
return db_domain
def add_userdomain(db: Session,ownerid:int, userid:int,domainid:int):
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.ownerid == ownerid,models.Domain.is_active)).first()
if db_domain:
user_domain = models.UserDomain(userid = userid, domainid = domainid )
db.add(user_domain)
db.commit()
return db_domain
def add_userdomains(db: Session, userid:int,domainids:list[str]):
dbCommits = list(map(lambda domainid: models.UserDomain(userid = userid, domainid = domainid ), domainids))
db.bulk_save_objects(dbCommits)
db.commit() db.commit()
return dbCommits db.refresh(db_domain)
return db_domain
def delete_userdomain(db: Session, userid: int,domainid: int): def delete_userdomain(db: Session, userid: int,domainid: int):
db_domain = db.query(models.UserDomain).filter(and_(models.UserDomain.userid == userid,models.UserDomain.domainid == domainid)).first() db_domain = db.query(models.UserDomain).filter(and_(models.UserDomain.userid == userid,models.UserDomain.domainid == domainid)).first()
#if not db_domain: if not db_domain:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found") raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
if db_domain: db.delete(db_domain)
db.delete(db_domain) db.commit()
db.commit()
return True
def active_userdomain(db: Session, userid: int,domainid: int):
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.is_active)).first()
if db_domain:
db_userdomains = db.query(models.UserDomain).filter(models.UserDomain.userid == userid).all()
# if not db_userdomains:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
for domain in db_userdomains:
if domain.domainid == domainid:
domain.active = True
else:
domain.active = False
db.add(domain)
db.commit()
return db_domain return db_domain
def active_userdomain(db: Session, userid: int,domainid: int):
db_userdomains = db.query(models.UserDomain).filter(models.UserDomain.userid == userid).all()
if not db_userdomains:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
for domain in db_userdomains:
if domain.domainid == domainid:
domain.active = True
else:
domain.active = False
db.add(domain)
db.commit()
return db_userdomains
def get_activedomain(db: Session, userid: int): def get_activedomain(db: Session, userid: int):
# user_domains = (db.query(models.Domain,models.UserDomain.active) 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()
# .join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ) if not db_domain:
# .filter(models.UserDomain.userid == userid) raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
# .all())
db_domain=(db.query(models.Domain).filter(models.Domain.is_active)
.join(models.UserDomain,models.UserDomain.domainid == models.Domain.id).filter(and_(models.UserDomain.active,models.UserDomain.userid == userid)).first())
# if len(user_domains)==1:
# db_domain = user_domains[0][0];
# else:
# db_domain = next((domain for domain,active in user_domains if active),None)
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
return db_domain return db_domain
def get_domain(db: Session, userid: str): def get_domain(db: Session, userid: str):
domains = db.query(models.Domain).join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ).filter(models.UserDomain.userid == userid).all() domains = db.query(models.Domain).join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ).filter(models.UserDomain.userid == userid).all()
# if not domains: if not domains:
# raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
return domains return domains
def get_alldomains(db: Session): def get_domains(db: Session,tenantid:str):
domains = db.query(models.Domain).all() domains = db.query(models.Domain).filter(models.Domain.tenantid == tenantid ).all()
return domains if not domains:
raise HTTPException(status_code=404, detail="Data not found")
def get_domains(db: Session,userid:int):
domains = db.query(models.Domain).filter(models.Domain.ownerid == userid ).all()
return domains return domains
def get_events(db: Session): def get_events(db: Session):
@@ -420,35 +278,9 @@ def get_events(db: Session):
raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
return events return events
def get_category(db:Session):
categorys=db.query(models.Category).all()
return categorys
def get_eventactions(db: Session,eventid: str): def get_eventactions(db: Session,eventid: str):
#eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid == models.Action.id ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all() #eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid == models.Action.id ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all()
#category = get_category(db) eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid != models.Action.id and models.EventAction.eventid == eventid ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all()
blackactions = (
db.query(models.EventAction.actionid)
.filter(models.EventAction.eventid == eventid)
.subquery()
)
eveactions = (
db.query(
models.Action.id,
models.Action.name,
models.Action.title,
models.Action.subtitle,
models.Action.outputpoints,
models.Action.property,
models.Action.categoryid,
models.Action.nosort,
models.Category.categoryname)
.join(models.Category,models.Category.id == models.Action.categoryid)
.filter(models.Action.id.notin_(blackactions))
.order_by(models.Category.nosort,models.Action.nosort)
.all()
)
if not eveactions: if not eveactions:
raise HTTPException(status_code=404, detail="Data not found") raise HTTPException(status_code=404, detail="Data not found")
return eveactions return eveactions

View File

@@ -1,4 +0,0 @@
from app.db.cruddb.dbuser import userService
from app.db.cruddb.dbdomain import domainService
from app.db.cruddb.dbapp import appService
from app.db.cruddb.dbtenant import tenantService

View File

@@ -1,104 +0,0 @@
from sqlalchemy import asc, desc, select
from sqlalchemy.orm import Session
from sqlalchemy.orm.query import Query
from typing import Type, List, Optional
from app.core.common import ApiReturnPage
from sqlalchemy import and_ ,or_
from pydantic import BaseModel
from app.db import models
class crudbase:
def __init__(self, model: Type[models.Base]):
self.model = model
def _apply_filters(self, query: Query, filters: dict) -> Query:
and_conditions = []
or_conditions = []
for column_name, value in filters.items():
column = getattr(self.model, column_name, None)
if column:
if isinstance(value, dict):
if 'operator' in value:
operator = value['operator']
filter_value = value['value']
if operator == '!=':
and_conditions.append(column != filter_value)
elif operator == 'like':
and_conditions.append(column.like(f"%{filter_value}%"))
elif operator == '=':
and_conditions.append(column == filter_value)
elif operator == '>':
and_conditions.append(column > filter_value)
elif operator == '>=':
and_conditions.append(column >= filter_value)
elif operator == '<':
and_conditions.append(column < filter_value)
elif operator == '<=':
and_conditions.append(column <= filter_value)
elif operator == 'in':
if isinstance(filter_value, list):
or_conditions.append(column.in_(filter_value))
else:
and_conditions.append(column == filter_value)
else:
and_conditions.append(column == value)
else:
and_conditions.append(column == value)
if and_conditions:
query = query.where(and_(*and_conditions))
if or_conditions:
query = query.where(or_(*or_conditions))
return query
def _apply_sorting(self, query: Query, sort_by: Optional[str], sort_order: Optional[str]) -> Query:
if sort_by:
column = getattr(self.model, sort_by, None)
if column:
if sort_order == "desc":
query = query.order_by(desc(column))
else:
query = query.order_by(asc(column))
return query
def get_all(self) -> Query:
return select(self.model)
def get(self, db: Session, item_id: int) -> Optional[models.Base]:
return db.execute(select(self.model).filter(self.model.id == item_id)).scalar_one_or_none()
def create(self, db: Session, obj_in: BaseModel) -> models.Base:
db_obj = self.model(**obj_in.model_dump())
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(self, db: Session, item_id: int, obj_in: BaseModel) -> Optional[models.Base]:
db_obj = self.get(db,item_id)
if db_obj:
for key, value in obj_in.model_dump(exclude_unset=True).items():
setattr(db_obj, key, value)
db.commit()
db.refresh(db_obj)
return db_obj
return None
def delete(self, db: Session, item_id: int) -> Optional[models.Base]:
db_obj = self.get(db,item_id)
if db_obj:
db.delete(db_obj)
db.commit()
return db_obj
return None
def get_by_conditions(self, filters: Optional[dict] = None, sort_by: Optional[str] = None,
sort_order: Optional[str] = "asc") -> Query:
query = select(self.model)
if filters:
query = self._apply_filters(query, filters)
if sort_by:
query = self._apply_sorting(query, sort_by, sort_order)
print(str(query))
return query

View File

@@ -1,216 +0,0 @@
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import select,and_
import typing as t
from app.db.cruddb.crudbase import crudbase
from fastapi_pagination.ext.sqlalchemy import paginate
from app.core.common import ApiReturnPage
from app.db import models, schemas
from app.core.security import chacha20Decrypt, get_password_hash
class dbflowhistory(crudbase):
def __init__(self):
super().__init__(model=models.FlowHistory)
def get_flows_by_appid_version(self,db: Session,domainurl:str,appid:str,version:int):
return db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid, "version":version})).scalars().all()
dbflowhistory = dbflowhistory()
class dbflow(crudbase):
def __init__(self):
super().__init__(model=models.Flow)
def get_domain_apps(self):
return None
def get_flow_by_flowid(self,db: Session,flowid:str):
return db.execute(super().get_by_conditions({"flowid":flowid})).scalars().first()
def get_flows_by_appid(self,db: Session,domainurl:str,appid:str):
return db.execute(select(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid == appid))).scalars().all()
def create_flow(self,db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
db_flow = models.Flow(
flowid=flow.flowid,
appid=flow.appid,
eventid=flow.eventid,
domainurl=domainurl,
name=flow.name,
content=flow.content,
createuserid = userid,
updateuserid = userid
)
db.add(db_flow)
db_app = db.execute(select(models.App).where(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid))).scalars().first()
if not db_app:
db_app = models.App(
domainurl = domainurl,
appid=flow.appid,
appname=flow.appname,
version = 0,
createuserid= userid,
updateuserid = userid
)
db.add(db_app)
db.commit()
db.refresh(db_flow)
return db_flow
dbflow = dbflow()
class dbappversion(crudbase):
def __init__(self):
super().__init__(model=models.AppVersion)
def get_appversions(self,domainurl:str,appid:str):
return super().get_by_conditions({"domainurl":domainurl,"appid":appid})
def get_app_by_version(self,db: Session,domainurl:str,appid:str,version:int):
return db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid,"version":version})).scalars().first()
def get_app_latestversion(self,db: Session,domainurl:str,appid:str):
appversion = db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid},"version","desc")).scalars().first()
if appversion:
return appversion.version
else:
return 0
dbappversion = dbappversion()
class dbapp(crudbase):
def __init__(self):
super().__init__(model=models.App)
def get_app(self,db: Session,domainurl:str,appid:str):
return db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid})).scalars().first()
def get_apps(self,db: Session,domainurl:str):
return db.execute(super().get_by_conditions({"domainurl":domainurl})).scalars().all()
def update_appversion(self,db: Session,domainurl, newversion: schemas.VersionUpdate,userid:int):
db_app = self.get_app(db,domainurl,newversion.appid)
if db_app:
db_app.version = dbappversion.get_app_latestversion(db,domainurl,newversion.appid)+1
db_app.updateuserid = userid,
db_app.versionname = newversion.versionname
db_app.is_saved = False
appversion = models.AppVersion(
domainurl = db_app.domainurl,
appid=db_app.appid,
appname=db_app.appname,
version = db_app.version,
versionname = newversion.versionname,
comment = newversion.comment,
updateuserid = userid,
createuserid = userid
)
db.add(appversion)
db.add(db_app)
flows = dbflow.get_flows_by_appid(db,domainurl,newversion.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,
version = db_app.version,
updateuserid = userid,
createuserid = userid
)
db.add(db_flowhistory)
db.add(db_flowhistory)
db.commit()
db.refresh(db_app)
return db_app
return None
def change_appversion(self,db: Session, domainurl:str,appid:str,version:int,userid:int):
db_app = self.get_app(db, domainurl, appid)
if not db_app:
return None
db_appversion = dbappversion.get_app_by_version(db, domainurl, appid, version)
if not db_appversion:
return None
db_app.version = version
db_app.versionname = db_appversion.versionname
db_app.updateuserid = userid
db_app.is_saved = False
db.add(db_app)
flows = dbflow.get_flows_by_appid(db, domainurl, appid)
for flow in flows:
db.delete(flow)
db.flush()
flowhistorys = dbflowhistory.get_flows_by_appid_version(db, domainurl, appid, version)
for flow in flowhistorys:
db_flow = models.Flow(
flowid = flow.flowid,
appid = flow.appid,
eventid = flow.eventid,
domainurl = flow.domainurl,
name = flow.name,
content = flow.content,
updateuserid = userid,
createuserid = userid
)
db.add(db_flow)
db.commit()
db.refresh(db_app)
return db_app
def delete_app(self,db: Session, domainurl: str,appid: str ):
db_app =self.get_app(db,domainurl,appid)
if db_app:
flows = dbflow.get_flows_by_appid(db,domainurl,appid)
for flow in flows:
db.delete(flow)
db.delete(db_app)
db.commit()
return db_app
return None
def get_appversions(self,db: Session, domainurl:str,appid:str):
return paginate(db,dbappversion.get_appversions(domainurl,appid))
def get_flow(self,db: Session, domainurl: str, appid:str):
return dbflow.get_flows_by_appid(db,domainurl,appid)
def create_flow(self,db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
return dbflow.create_flow(db,domainurl,flow,userid)
def edit_flow(self,db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
db_flow = dbflow.get_flow_by_flowid(db, flow.flowid)
if not db_flow:
return dbflow.create_flow(db,domainurl,flow,userid)
db_flow.appid =flow.appid
db_flow.eventid=flow.eventid
db_flow.domainurl=domainurl
db_flow.name=flow.name
db_flow.content=flow.content
db_flow.updateuserid = userid
db.add(db_flow)
db_app = self.get_app(db, domainurl, flow.appid)
if db_app and db_app.version > 0:
db_app.is_saved = True
db.add(db_app)
db.commit()
db.refresh(db_flow)
return db_flow
def delete_flow(self,db: Session, flowid: str):
db_flow = dbflow.get_flow_by_flowid(db,flowid)
if db_flow:
return dbflow.delete(db,db_flow.id)
return None
appService = dbapp()

View File

@@ -1,218 +0,0 @@
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import select,and_
import typing as t
from app.db.cruddb.crudbase import crudbase
from fastapi_pagination.ext.sqlalchemy import paginate
from app.core.common import ApiReturnPage
from app.db import models, schemas
from app.core.security import chacha20Decrypt, get_password_hash
class dbuserdomain(crudbase):
def __init__(self):
super().__init__(model=models.UserDomain)
def get_userdomain(self,db: Session,userid:int,domainid:int):
return db.execute(super().get_by_conditions({"userid":userid,"domainid":domainid})).scalars().first()
def get_userdomain_by_domainid(self,db: Session,ownerid:int,domainid:int):
return super().get_by_conditions({"domainid":domainid})
def get_default_domains(self,db: Session,domainid:int):
return db.execute(super().get_by_conditions({"domainid":domainid,"is_default":True})).scalars().all()
def get_user_default_domain(self,db: Session,userid:int):
return db.execute(super().get_by_conditions({"userid":userid,"is_default":True})).scalars().first()
dbuserdomain = dbuserdomain()
class dbmanagedomain(crudbase):
def __init__(self):
super().__init__(model=models.ManageDomain)
def get_managedomain(self,db: Session,userid:int,domainid:int):
return db.execute(super().get_by_conditions({"userid":userid,"domainid":domainid})).scalars().first()
def get_managedomain_by_domain(self,db: Session,domainid:int):
return db.execute(super().get_by_conditions({"domainid":domainid})).scalars().all()
dbmanagedomain = dbmanagedomain()
class dbdomain(crudbase):
def __init__(self):
super().__init__(model=models.Domain)
def get_domains(self,db: Session)-> ApiReturnPage[models.Base]:
return paginate(db,super().get_all())
def get_domains_by_manage(self,db: Session,userid:int)-> ApiReturnPage[models.Base]:
query = select(models.Domain).join(models.ManageDomain,models.ManageDomain.domainid == models.Domain.id).where(models.ManageDomain.userid == userid)
return paginate(db,query)
def get_domains_by_owner(self,db: Session,ownerid:int)-> ApiReturnPage[models.Base]:
return paginate(db,super().get_by_conditions({"ownerid":ownerid}))
def create_domain(self,db: Session, domain: schemas.DomainIn,userid:int):
#db_domain = super().get_by_conditions(db,{"url":domain.url,"kintoneuser":domain.kintoneuser,"onwerid":userid}).first()
#if not db_domain:
domain.encrypt_kintonepwd()
db_domain = models.Domain(
tenantid = domain.tenantid,
name = domain.name,
url = domain.url,
kintoneuser = domain.kintoneuser,
kintonepwd = domain.kintonepwd,
is_active = domain.is_active,
createuserid = userid,
updateuserid = userid,
ownerid = userid
)
db.add(db_domain)
db.flush()
user_domain = models.UserDomain(userid = userid, domainid = db_domain.id ,createuserid = userid,updateuserid = userid)
db.add(user_domain)
manage_domain = models.ManageDomain(userid = userid, domainid = db_domain.id ,createuserid = userid,updateuserid = userid)
db.add(manage_domain)
db.commit()
db.refresh(db_domain)
return db_domain
def delete_domain(self,db: Session,id: int):
db_managedomains = dbmanagedomain.get_managedomain_by_domain(db,id)
for manage in db_managedomains:
db.delete(manage)
return super().delete(db,id)
def edit_domain(self,db: Session, domain: schemas.DomainIn,userid:int) -> schemas.DomainOut:
db_domain = super().get(db,domain.id)
if db_domain:
db_domain.tenantid = domain.tenantid
db_domain.name=domain.name
db_domain.url=domain.url
if db_domain.is_active == True and domain.is_active == False:
db_userdomains = dbuserdomain.get_default_domains(db,domain.id)
for userdomain in db_userdomains:
userdomain.is_default = False
db.add(userdomain)
db_domain.is_active=domain.is_active
db_domain.kintoneuser=domain.kintoneuser
if domain.kintonepwd != "" and domain.kintonepwd != None:
domain.encrypt_kintonepwd()
db_domain.kintonepwd = domain.kintonepwd
db_domain.updateuserid = userid
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
return None
def add_userdomain(self,db: Session,ownerid:int,userid:int,domainid:int) -> schemas.DomainOut:
db_domain = super().get(db,domainid)
if db_domain and db_domain.is_active:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if not db_userdomain:
user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid = ownerid,updateuserid = ownerid)
db.add(user_domain)
db.commit()
return db_domain
return None
def add_userdomain_by_owner(self,db: Session,ownerid:int, userid:int,domainid:int) -> schemas.DomainOut:
db_domain = db.execute(super().get_by_conditions({"id":domainid,"is_active":True})).scalars().first()
if db_domain:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if not db_userdomain:
user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid =ownerid,updateuserid = ownerid)
db.add(user_domain)
db.commit()
return db_domain
return None
def delete_userdomain(self,db: Session, userid: int,domainid: int) -> schemas.DomainOut:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if db_userdomain:
domain = db_userdomain.domain
db.delete(db_userdomain)
db.commit()
return domain
return None
def get_default_domain(self,db: Session, userid: int) -> schemas.DomainOut:
userdomain = dbuserdomain.get_user_default_domain(db,userid)
if userdomain:
return userdomain.domain
else:
return None
def set_default_domain(self,db: Session, userid: int,domainid: int):
db_domain =db.execute(super().get_by_conditions({"id":domainid,"is_active":True})).scalars().first()
if db_domain:
db_default_domain = dbuserdomain.get_user_default_domain(db,userid)
db_userdomain =dbuserdomain.get_userdomain(db,userid,domainid)
if db_default_domain:
if db_default_domain.domainid != domainid:
db_default_domain.is_default = False
db_default_domain.updateuserid = userid
db.add(db_default_domain)
else:
return db_domain
if db_userdomain:
db_userdomain.is_default = True
db_userdomain.updateuserid = userid
db.add(db_userdomain)
else:
db_userdomain = dbuserdomain.create(db,schemas.UserDomainIn(domainid=domainid,userid=userid,is_default = True))
db.add(db_userdomain)
db.commit()
return db_domain
else:
return None
def get_shareddomain_users(self,db: Session,domainid: int) -> ApiReturnPage[models.Base]:
users = select(models.User).join(models.UserDomain,models.UserDomain.userid == models.User.id).filter(models.UserDomain.domainid ==domainid)
return paginate(db,users)
def add_managedomain(self,db: Session,ownerid:int,userid:int,domainid:int) -> schemas.DomainOut:
db_domain = self.get(db,domainid)
if db_domain:
db_managedomain = dbmanagedomain.get_managedomain(db,userid,domainid)
if not db_managedomain:
manage_domain = models.ManageDomain(userid = userid, domainid = domainid ,createuserid = ownerid,updateuserid = ownerid)
db.add(manage_domain)
db.commit()
return db_domain
return None
def add_managedomain_by_owner(self,db: Session,ownerid:int, userid:int,domainid:int) -> schemas.DomainOut:
db_domain = db.execute(super().get_by_conditions({"id":domainid,"ownerid":ownerid,})).scalars().first()
if db_domain:
db_managedomain = dbmanagedomain.get_managedomain(db,userid,domainid)
if not db_managedomain:
manage_domain = models.ManageDomain(userid = userid, domainid = domainid ,createuserid =ownerid,updateuserid = ownerid)
db.add(manage_domain)
db.commit()
return db_domain
return None
def delete_managedomain(self,db: Session, userid: int,domainid: int) -> schemas.DomainOut:
db_managedomain = dbmanagedomain.get_managedomain(db,userid,domainid)
if db_managedomain:
domain = db_managedomain.domain
if domain.ownerid != userid:
db.delete(db_managedomain)
db.commit()
return domain
return None
def get_managedomain_users(self,db: Session,domainid: int) -> ApiReturnPage[models.Base]:
users = select(models.User).join(models.ManageDomain,models.ManageDomain.userid == models.User.id).where(models.ManageDomain.domainid ==domainid)
return paginate(db,users)
domainService = dbdomain()

View File

@@ -1,13 +0,0 @@
from app.db.cruddb.crudbase import crudbase
from app.db import models, schemas
from sqlalchemy.orm import Session
class dbtenant(crudbase):
def __init__(self):
super().__init__(model=models.Tenant)
def get_tenant(sefl,db:Session,tenantid: str):
tenant = db.execute(super().get_by_conditions({"tenantid":tenantid})).scalars().first()
return tenant
tenantService = dbtenant()

View File

@@ -1,98 +0,0 @@
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import and_
import typing as t
from app.db.cruddb.crudbase import crudbase
from fastapi_pagination.ext.sqlalchemy import paginate
from app.core.common import ApiReturnPage
from app.db import models, schemas
from app.core.security import chacha20Decrypt, get_password_hash
class dbpermission(crudbase):
def __init__(self):
super().__init__(model=models.Permission)
dbpermission = dbpermission()
class dbrole(crudbase):
def __init__(self):
super().__init__(model=models.Role)
dbrole = dbrole()
class dbuser(crudbase):
def __init__(self):
super().__init__(model=models.User)
def get_user(self,db: Session, user_id: int) -> schemas.User:
return super().get(db,user_id)
def get_user_by_email(self,db: Session, email: str) -> schemas.User:
return db.execute(super().get_by_conditions({"email":email})).scalars().first()
def get_users(self,db: Session) -> ApiReturnPage[models.Base]:
return paginate(db,super().get_all())
def get_users_not_admin(self,db: Session) -> ApiReturnPage[models.Base]:
return paginate(db,super().get_by_conditions({"is_superuser":False}))
def create_user(self,db: Session, user: schemas.UserCreate,userid:int):
hashed_password = get_password_hash(user.password)
user.hashed_password = hashed_password
user.createuserid = userid
user.updateuserid = userid
del user.password
return super().create(db,user)
def delete_user(self,db: Session, user_id: int):
return super().delete(db,user_id)
def edit_user(self,db: Session, user_id:int,user: schemas.UserEdit,userid: int) -> schemas.User:
if not user.password is None and user.password != "":
user.hashed_password = get_password_hash(user.password)
del user.password
user.updateuserid = userid
return super().update(db,user_id,user)
def get_roles(self,db: Session) -> t.List[schemas.RoleBase]:
return db.execute(dbrole.get_all()).scalars().all()
#return dbrole.get_all().all()
def get_roles_by_level(self,db: Session,roles:t.List[models.Role]) -> t.List[schemas.RoleBase]:
level = 99999
for role in roles:
if role.level < level:
level = role.level
return db.execute(dbrole.get_by_conditions({"level":{"operator":">","value":level}})).scalars().all()
def assign_userrole(self,db: Session, user_id: int, roles: t.List[int]):
db_user = super().get(db,user_id)
if db_user:
for role in db_user.roles:
if role.id not in roles:
db_user.roles.remove(role)
for roleid in roles:
role = dbrole.get(db,roleid)
if role not in db_user.roles:
db_user.roles.append(role)
db.commit()
db.refresh(db_user)
return db_user
def get_permissions(self,db: Session) -> t.List[schemas.Permission]:
return db.execute(dbpermission.get_all()).scalars().all()
def get_user_permissions(self,db: Session,user_id: int) -> t.List[schemas.Permission]:
permissions =[]
db_user = super().get(db,user_id)
if db_user:
for role in db_user.roles:
permissions += role.permissions
return list(set(permissions))
userService = dbuser()

View File

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

View File

@@ -2,81 +2,46 @@ from pydantic import BaseModel
from datetime import datetime from datetime import datetime
import typing as t import typing as t
from app.core.security import chacha20Decrypt, chacha20Encrypt
class Base(BaseModel): class Base(BaseModel):
create_time: datetime create_time: datetime
update_time: datetime update_time: datetime
class Permission(BaseModel):
id: int
menu:str
function:str
link:str
privilege:str
class RoleBase(BaseModel):
id: int
name:str
description:str
level:int
class RoleWithPermission(RoleBase):
permissions:t.List[Permission] = []
class AssignUserRoles(BaseModel):
userid:int
roleids:t.List[int]
class UserBase(BaseModel): class UserBase(BaseModel):
email: str email: str
is_active: bool = True is_active: bool = True
is_superuser: bool = False is_superuser: bool = False
first_name: str = None first_name: str = None
last_name: str = None last_name: str = None
roles:t.List[RoleBase] = []
class UserOut(BaseModel):
id: int class UserOut(UserBase):
email: str pass
is_active: bool = True
is_superuser: bool = False
first_name: str = None
last_name: str = None
class UserCreate(UserBase): class UserCreate(UserBase):
email:str email:str
password: str password: str
hashed_password :str = None
first_name: str first_name: str
last_name: str last_name: str
is_active:bool is_active:bool
is_superuser:bool is_superuser:bool
tenantid:t.Optional[str] = "1"
createuserid:t.Optional[int] = None
updateuserid:t.Optional[int] = None
class ConfigDict: class Config:
orm_mode = True orm_mode = True
class UserEdit(UserBase): class UserEdit(UserBase):
password: t.Optional[str] = None password: t.Optional[str] = None
hashed_password :str = None
updateuserid:t.Optional[int] = None
class ConfigDict: class Config:
orm_mode = True orm_mode = True
class User(UserBase): class User(UserBase):
id: int id: int
class ConfigDict: class Config:
orm_mode = True orm_mode = True
@@ -84,32 +49,6 @@ class Token(BaseModel):
access_token: str access_token: str
token_type: str token_type: str
class AppList(Base):
domainurl: str
appname: str
appid:str
version:int
is_saved:bool
versionname: t.Optional[str] = None
updateuser: UserOut
createuser: UserOut
class AppVersion(Base):
domainurl: str
appname: str
versionname: str
comment:str
appid:str
version:t.Optional[int] = None
updateuser: UserOut
createuser: UserOut
class VersionUpdate(BaseModel):
appid:str
versionname: str
comment:str
class TokenData(BaseModel): class TokenData(BaseModel):
id:int = 0 id:int = 0
@@ -128,7 +67,7 @@ class AppBase(BaseModel):
class App(AppBase): class App(AppBase):
id: int id: int
class ConfigDict: class Config:
orm_mode = True orm_mode = True
@@ -139,7 +78,7 @@ class Kintone(BaseModel):
desc: str = None desc: str = None
content: str = None content: str = None
class ConfigDict: class Config:
orm_mode = True orm_mode = True
class Action(BaseModel): class Action(BaseModel):
@@ -149,17 +88,13 @@ class Action(BaseModel):
subtitle: str = None subtitle: str = None
outputpoints: str = None outputpoints: str = None
property: str = None property: str = None
categoryid: int = None
nosort: int class Config:
categoryname : str =None
class ConfigDict:
orm_mode = True orm_mode = True
class FlowIn(BaseModel): class FlowBase(BaseModel):
flowid: str flowid: str
# domainurl:str
appid: str appid: str
appname:str
eventid: str eventid: str
name: str = None name: str = None
content: str = None content: str = None
@@ -169,55 +104,20 @@ class Flow(Base):
flowid: str flowid: str
appid: str appid: str
eventid: str eventid: str
domainurl: str domainid: int
name: str = None name: str = None
content: str = None content: str = None
class ConfigDict: class Config:
orm_mode = True orm_mode = True
class DomainIn(BaseModel): class DomainBase(BaseModel):
id: int id: int
tenantid: str tenantid: str
name: str name: str
url: str url: str
kintoneuser: str kintoneuser: str
kintonepwd: t.Optional[str] = None kintonepwd: str
is_active: bool
createuserid:t.Optional[int] = None
updateuserid:t.Optional[int] = None
ownerid:t.Optional[int] = None
def encrypt_kintonepwd(self):
encrypted_pwd = chacha20Encrypt(self.kintonepwd)
self.kintonepwd = encrypted_pwd
class DomainOut(BaseModel):
id: int
tenantid: str
name: str
url: str
kintoneuser: str
is_active: bool
ownerid:int
class ConfigDict:
orm_mode = True
class UserDomainParam(BaseModel):
userid:int
domainid:int
class UserDomain(BaseModel):
id: int
is_default: bool
domain:DomainOut
user:UserOut
class UserDomainIn(BaseModel):
is_default: bool
domainid:int
userid:int
class Domain(Base): class Domain(Base):
id: int id: int
@@ -225,13 +125,10 @@ class Domain(Base):
name: str name: str
url: str url: str
kintoneuser: str kintoneuser: str
is_active: bool kintonepwd: str
updateuser:UserOut
owner:UserOut
class ConfigDict:
orm_mode = True
class Config:
orm_mode = True
class Event(Base): class Event(Base):
id: int id: int
@@ -242,7 +139,7 @@ class Event(Base):
mobile: bool mobile: bool
eventgroup: bool eventgroup: bool
class ConfigDict: class Config:
orm_mode = True orm_mode = True
class ErrorCreate(BaseModel): class ErrorCreate(BaseModel):

View File

@@ -1,37 +1,21 @@
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base, Session from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core import config from app.core import config
engine = create_engine(
config.SQLALCHEMY_DATABASE_URI,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base() Base = declarative_base()
# engine = create_engine(
# config.SQLALCHEMY_DATABASE_URI,
# )
# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Database: # Dependency
def __init__(self, database_url: str): def get_db():
self.database_url = database_url db = SessionLocal()
self.engine = create_engine(self.database_url)
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
self.Base = declarative_base()
def get_db(self):
db =self.SessionLocal()
return db
tenantdb = Database(config.SQLALCHEMY_DATABASE_URI)
def get_tenant_db():
db = tenantdb.get_db()
try: try:
yield db yield db
finally: finally:
db.close() db.close()
def get_user_db(database_url: str):
database = Database(database_url)
db = database.get_db()
return db

View File

@@ -1,6 +1,5 @@
import os import os
from fastapi import FastAPI, Depends from fastapi import FastAPI, Depends
from fastapi_pagination import add_pagination
from starlette.requests import Request from starlette.requests import Request
import uvicorn import uvicorn
from app.api.api_v1.routers.kintone import kinton_router from app.api.api_v1.routers.kintone import kinton_router
@@ -8,29 +7,21 @@ from app.api.api_v1.routers.users import users_router
from app.api.api_v1.routers.auth import auth_router from app.api.api_v1.routers.auth import auth_router
from app.api.api_v1.routers.platform import platform_router from app.api.api_v1.routers.platform import platform_router
from app.core import config from app.core import config
#from app.db import Base,engine from app.db import Base,engine
from app.core.auth import get_current_active_user from app.core.auth import get_current_active_user
from app.core.celery_app import celery_app from app.core.celery_app import celery_app
from app import tasks from app import tasks
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import logging import logging
from app.core.apiexception import APIException, writedblog from app.core.apiexception import APIException, writedblog
from app.core.common import ApiReturnError
from app.db.crud import create_log from app.db.crud import create_log
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import asyncio import asyncio
from contextlib import asynccontextmanager
from app.core.operation import LoggingMiddleware
#Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
@asynccontextmanager
async def lifespan(app: FastAPI):
startup_event()
yield
app = FastAPI( app = FastAPI(
title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api",lifespan=lifespan title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api"
) )
origins = [ origins = [
@@ -45,10 +36,6 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
app.add_middleware(LoggingMiddleware)
add_pagination(app)
# @app.middleware("http") # @app.middleware("http")
# async def db_session_middleware(request: Request, call_next): # async def db_session_middleware(request: Request, call_next):
# request.state.db = SessionLocal() # request.state.db = SessionLocal()
@@ -56,11 +43,12 @@ add_pagination(app)
# request.state.db.close() # request.state.db.close()
# return response # return response
def startup_event(): @app.on_event("startup")
async def startup_event():
log_dir="log" log_dir="log"
if not os.path.exists(log_dir): if not os.path.exists(log_dir):
os.makedirs(log_dir) os.makedirs(log_dir)
logger = logging.getLogger("uvicorn.access") logger = logging.getLogger("uvicorn.access")
handler = logging.handlers.RotatingFileHandler(f"{log_dir}/api.log",mode="a",maxBytes = 100*1024, backupCount = 3) handler = logging.handlers.RotatingFileHandler(f"{log_dir}/api.log",mode="a",maxBytes = 100*1024, backupCount = 3)
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
@@ -72,7 +60,7 @@ async def api_exception_handler(request: Request, exc: APIException):
loop.run_in_executor(None,writedblog,exc) loop.run_in_executor(None,writedblog,exc)
return JSONResponse( return JSONResponse(
status_code=exc.status_code, status_code=exc.status_code,
content= ApiReturnError(msg = f"{exc.detail}").model_dump(), content={"detail": f"{exc.detail}"},
) )
@app.get("/api/v1") @app.get("/api/v1")

View File

@@ -1,252 +0,0 @@
import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
import typing as t
from app.core import config, security
from app.core.dbmanager import get_db
from app.db import models,schemas
from app.main import app
from app.core import security
import jwt
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://kabAdmin:P%40ssw0rd!@kintonetooldb.postgres.database.azure.com/test"
engine = create_engine(SQLALCHEMY_DATABASE_URI,echo=True)
test_session_maker = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="session")
def test_db():
connection = engine.connect()
transaction = connection.begin()
test_session = test_session_maker(bind=connection)
yield test_session
test_session.close()
transaction.rollback()
#transaction.commit()
connection.close()
@pytest.fixture(scope="session")
def test_client(test_db):
def get_test_db():
try:
yield test_db
finally:
test_db.close()
app.dependency_overrides[get_db] = get_test_db
with TestClient(app) as test_client:
yield test_client
@pytest.fixture(scope="session")
def test_tenant_id():
return "1"
@pytest.fixture(scope="session")
def test_user(test_db,test_tenant_id):
password ="test"
user = models.User(
email = "test@test.com",
first_name = "test",
last_name = "abc",
hashed_password = security.get_password_hash(password),
is_active = True,
is_superuser = False,
tenantid = test_tenant_id
)
test_db.add(user)
test_db.commit()
test_db.refresh(user)
dicUser = user.__dict__
dicUser["password"] = password
return dicUser
@pytest.fixture(scope="session")
def password():
return "password"
@pytest.fixture(scope="session")
def user(test_db,password,test_tenant_id):
user = models.User(
email = "user@test.com",
first_name = "user",
last_name = "abc",
hashed_password = security.get_password_hash(password),
is_active = True,
is_superuser = False,
tenantid = test_tenant_id
)
test_db.add(user)
test_db.commit()
test_db.refresh(user)
return user.__dict__
@pytest.fixture(scope="session")
def admin(test_db,password,test_tenant_id):
user = models.User(
email = "admin@test.com",
first_name = "admin",
last_name = "abc",
hashed_password = security.get_password_hash(password),
is_active = True,
is_superuser = True,
tenantid =test_tenant_id
)
test_db.add(user)
test_db.commit()
test_db.refresh(user)
return user.__dict__
@pytest.fixture(scope="session")
def login_user(test_db,test_client,user,password):
# test_db.add(user)
# test_db.commit()
#test_db.refresh(user)
response = test_client.post("/api/token", data={"username": user["email"], "password":password })
return response.json()["access_token"]
@pytest.fixture(scope="session")
def login_admin(test_db,test_client,admin,password):
# test_db.add(admin)
# test_db.commit()
#test_db.refresh(admin)
response = test_client.post("/api/token", data={"username": admin["email"], "password":password })
return response.json()["access_token"]
@pytest.fixture(scope="session")
def login_user_id(login_user):
payload = jwt.decode(login_user, security.SECRET_KEY, algorithms=[security.ALGORITHM])
id = payload.get("sub")
return id
@pytest.fixture(scope="session")
def login_admin_id(login_admin):
payload = jwt.decode(login_admin, security.SECRET_KEY, algorithms=[security.ALGORITHM])
id = payload.get("sub")
return id
@pytest.fixture(scope="session")
def test_role(test_db):
role = models.Role(
name = "test",
description = "test",
level = 1
)
test_db.add(role)
test_db.commit()
test_db.refresh(role)
return role.__dict__
@pytest.fixture(scope="session")
def test_domain(test_db,login_user_id):
domain = models.Domain(
tenantid = "1",
name = "テスト環境",
url = "https://mfu07rkgnb7c.cybozu.com",
kintoneuser = "MXZ",
kintonepwd = security.chacha20Encrypt("maxz1205"),
is_active = True,
createuserid =login_user_id,
updateuserid =login_user_id,
ownerid = login_user_id
)
test_db.add(domain)
test_db.flush()
user_domain = models.UserDomain(userid = login_user_id, domainid = domain.id ,createuserid = login_user_id,updateuserid = login_user_id)
test_db.add(user_domain)
manage_domain = models.ManageDomain(userid = login_user_id, domainid = domain.id ,createuserid = login_user_id,updateuserid = login_user_id)
test_db.add(manage_domain)
test_db.commit()
test_db.refresh(domain)
return domain
@pytest.fixture(scope="session")
def test_app_id():
return "132"
# @pytest.fixture
# def test_password() -> str:
# return "securepassword"
# def get_password_hash() -> str:
# """
# Password hashing can be expensive so a mock will be much faster
# """
# return "supersecrethash"
# @pytest.fixture
# def test_user(test_db) -> models.User:
# """
# Make a test user in the database
# """
# user = models.User(
# email="fake@email.com",
# hashed_password=get_password_hash(),
# is_active=True,
# )
# test_db.add(user)
# test_db.commit()
# return user
# @pytest.fixture
# def test_superuser(test_db) -> models.User:
# """
# Superuser for testing
# """
# user = models.User(
# email="fakeadmin@email.com",
# hashed_password=get_password_hash(),
# is_superuser=True,
# )
# test_db.add(user)
# test_db.commit()
# return user
# def verify_password_mock(first: str, second: str) -> bool:
# return True
# @pytest.fixture
# def user_token_headers(
# client: TestClient, test_user, test_password, monkeypatch
# ) -> t.Dict[str, str]:
# monkeypatch.setattr(security, "verify_password", verify_password_mock)
# login_data = {
# "username": test_user.email,
# "password": test_password,
# }
# r = client.post("/api/token", data=login_data)
# tokens = r.json()
# a_token = tokens["access_token"]
# headers = {"Authorization": f"Bearer {a_token}"}
# return headers
# @pytest.fixture
# def superuser_token_headers(
# client: TestClient, test_superuser, test_password, monkeypatch
# ) -> t.Dict[str, str]:
# monkeypatch.setattr(security, "verify_password", verify_password_mock)
# login_data = {
# "username": test_superuser.email,
# "password": test_password,
# }
# r = client.post("/api/token", data=login_data)
# tokens = r.json()
# a_token = tokens["access_token"]
# headers = {"Authorization": f"Bearer {a_token}"}
# return headers

View File

@@ -1,5 +0,0 @@
[pytest]
log_cli = 1
log_cli_level = CRITICAL
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format=%Y-%m-%d %H:%M:%S

View File

@@ -1,11 +0,0 @@
import logging
def test_usr_login(test_client,test_user):
response = test_client.post("/api/token", data={"username": test_user["email"], "password": test_user["password"]})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "access_token" in response.json()
assert "token_type" in response.json()
assert response.json()["user_name"] == test_user["first_name"]+ " " + test_user["last_name"]

View File

@@ -1,175 +0,0 @@
import logging
def test_get_domains(test_client,test_domain,login_user):
response = test_client.get("/api/domains",headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert len(data["data"]) == 1
assert data["data"][0]["name"] == test_domain.name
def test_create_domain(test_client, login_user,login_user_id):
create_domain ={
"id": 0,
"tenantid": "1",
"name": "abc",
"url": "efg",
"kintoneuser": "eee",
"kintonepwd": "fff",
"is_active": True,
}
response = test_client.post("/api/domain", json=create_domain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == create_domain["name"]
assert data["data"]["url"] == create_domain["url"]
assert data["data"]["kintoneuser"] == create_domain["kintoneuser"]
assert data["data"]["is_active"] == create_domain["is_active"]
assert data["data"]["owner"]["id"] == login_user_id
def test_get_managedomainuser(test_client,test_domain,login_user,login_user_id):
response = test_client.get("/api/managedomainuser/" + str(test_domain.id),headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert len(data["data"]) == 1
assert data["data"][0]["id"] == login_user_id
def test_add_delete_userdomain(test_client,test_domain,test_user,login_user,login_user_id):
userdomain ={
"userid":test_user["id"],
"domainid":test_domain.id
}
response = test_client.post("/api/userdomain" , json=userdomain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
response = test_client.delete(f"/api/domain/{test_domain.id}/{test_user["id"]}" , headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
def test_add_delete_managedomain(test_client,test_domain,test_user,login_user,login_user_id):
userdomain ={
"userid":test_user["id"],
"domainid":test_domain.id
}
response = test_client.post("/api/managedomain" , json=userdomain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
response = test_client.delete(f"/api/managedomain/{test_domain.id}/{test_user["id"]}" , headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
def test_delete_domain(test_client, login_user,login_user_id):
delete_domain ={
"id": 0,
"tenantid": "1",
"name": "delete",
"url": "delete",
"kintoneuser": "delete",
"kintonepwd": "delete",
"is_active": True,
}
response = test_client.post("/api/domain", json=delete_domain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
id = data["data"]["id"]
response = test_client.delete(f"/api/domain/{id}/{login_user_id}",headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
response = test_client.delete(f"/api/domain/{id}",headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert response.json()["data"]["name"] == delete_domain["name"]
response = test_client.get(f"/api/domain/{id}", headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
assert "data" not in response.json()
def test_set_defaultuserdomain(test_client, test_domain,login_user):
response = test_client.put("/api/defaultdomain/"+str(test_domain.id), headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
assert data["data"]["kintoneuser"] == test_domain.kintoneuser
assert data["data"]["is_active"] == test_domain.is_active
def test_get_defaultuserdomain(test_client, test_domain,login_user):
response = test_client.get("/api/defaultdomain", headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
assert data["data"]["kintoneuser"] == test_domain.kintoneuser
assert data["data"]["is_active"] == test_domain.is_active
def test_get_domainshareduser(test_client, test_domain,login_user,login_user_id):
response = test_client.get("/api/domainshareduser/"+str(test_domain.id), headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert len(data["data"]) == 1
assert data["data"][0]["id"] == login_user_id
def test_edit_domain(test_client, test_domain, login_user):
update_domain ={
"id": test_domain.id,
"tenantid": "1",
"name": "テスト環境abc",
"url": test_domain.url,
"kintoneuser": test_domain.kintoneuser,
"is_active": True
}
response = test_client.put("/api/domain", json=update_domain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == update_domain["name"]
assert data["data"]["url"] == update_domain["url"]
assert data["data"]["kintoneuser"] == update_domain["kintoneuser"]
assert data["data"]["is_active"] == update_domain["is_active"]

View File

@@ -1,6 +1,4 @@
def test_read_main(client):
response = client.get("/api/v1")
def test_read_main(test_client):
response = test_client.get("/api/v1")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"message": "success"} assert response.json() == {"message": "Hello World"}

View File

@@ -1,156 +0,0 @@
import logging
def test_users_list(test_client,login_user):
response = test_client.get("/api/v1/users", headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert len(data["data"]) == 2
def test_users_list_for_admin(test_client,login_admin):
response = test_client.get("/api/v1/users", headers={"Authorization": "Bearer " + login_admin})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert len(data["data"]) == 3
def test_user_create(test_client,login_user):
user_data = {
"email": "newuser1@example.com",
"password": "password123",
"first_name": "New",
"last_name": "User",
"is_active": True,
"is_superuser": False
}
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["id"] > 0
assert data["data"]["email"] == user_data["email"]
assert data["data"]["first_name"] == user_data["first_name"]
assert data["data"]["last_name"] == user_data["last_name"]
assert data["data"]["is_active"] == user_data["is_active"]
assert data["data"]["is_superuser"] == user_data["is_superuser"]
def test_admin_create(test_client,login_user):
user_data = {
"email": "newuser2@example.com",
"password": "password123",
"first_name": "New",
"last_name": "User",
"is_active": True,
"is_superuser": True
}
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" not in data
def test_admin_create_for_admin(test_client,login_admin):
user_data = {
"email": "admin@example.com",
"password": "password123",
"first_name": "New",
"last_name": "User",
"is_active": True,
"is_superuser": True
}
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_admin})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["id"] > 0
assert data["data"]["email"] == user_data["email"]
assert data["data"]["first_name"] == user_data["first_name"]
assert data["data"]["last_name"] == user_data["last_name"]
assert data["data"]["is_active"] == user_data["is_active"]
assert data["data"]["is_superuser"] == user_data["is_superuser"]
def test_user_details(test_client,login_user_id, login_user,user):
id = login_user_id
response = test_client.get("/api/v1/users/"+ str(id), headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert data["data"]["email"] == user["email"]
assert data["data"]["first_name"] == user["first_name"]
assert data["data"]["last_name"] == user["last_name"]
assert data["data"]["is_active"] == user["is_active"]
assert data["data"]["is_superuser"] == user["is_superuser"]
assert data["data"]["id"] == id
def test_user_edit(test_client, login_user_id,login_user,user):
id = login_user_id
user_data = {
"email": user["email"],
"first_name": "Updated",
"last_name": "test",
"is_active": True,
"is_superuser": False
}
response = test_client.put("/api/v1/users/" + str(id), json=user_data, headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert data["data"]["email"] == user["email"]
assert data["data"]["first_name"] == user_data["first_name"]
assert data["data"]["last_name"] == user_data["last_name"]
assert data["data"]["is_active"] == user["is_active"]
assert data["data"]["id"] == id
def test_user_delete(test_client, login_user):
user_data = {
"email": "delete@example.com",
"password": "password123",
"first_name": "delete",
"last_name": "User",
"is_active": True,
"is_superuser": False
}
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
id = data["data"]["id"]
response = test_client.delete("/api/v1/users/"+ str(id),headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
assert response.json()["data"]["email"] == "delete@example.com"
response = test_client.get("/api/v1/users/"+ str(id), headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
assert "data" not in response.json()
def test_role_assign(test_client, login_user_id,login_user,test_role):
userroles ={
"userid":login_user_id,
"roleids":[test_role["id"]]
}
response = test_client.post("/api/v1/userrole", json=userroles, headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
response = test_client.get("/api/v1/users/"+ str(login_user_id), headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert len(data["data"]["roles"]) == 1
def test_roles_get(test_client,login_user):
response = test_client.get("/api/v1/roles", headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert len(data["data"]) == 0
def test_roles_admin_get(test_client,login_admin):
response = test_client.get("/api/v1/roles", headers={"Authorization": "Bearer " + login_admin})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert len(data["data"]) == 1

View File

@@ -1,121 +0,0 @@
import logging
def test_create_flow(test_client,test_domain,test_app_id,login_user):
test_flow={
"flowid": "73e82bee-76a2-4347-a069-e21bf5e21111",
"appid": test_app_id,
"appname": "test_app",
"eventid": "a",
"name": "保存をクリックしたとき",
"content": ""
}
response = test_client.post("/api/flow", json=test_flow,headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert data["data"] is not None
assert data["data"]["domainurl"] == test_domain.url
assert data["data"]["flowid"] == test_flow["flowid"]
assert data["data"]["appid"] == test_flow["appid"]
assert data["data"]["eventid"] == test_flow["eventid"]
assert data["data"]["content"] == test_flow["content"]
def test_delete_flow(test_client,test_domain,test_app_id,login_user):
response = test_client.delete("/api/flow/73e82bee-76a2-4347-a069-e21bf5e21111",headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
def test_edit_flow(test_client,test_domain,test_app_id,login_user):
test_flow={
"flowid": "73e82bee-76a2-4347-a069-e21bf5e21111",
"appid": test_app_id,
"appname": "test_app_new",
"eventid": "abc",
"name": "保存をクリックしたとき",
"content": ""
}
response = test_client.put("/api/flow", json=test_flow,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["domainurl"] == test_domain.url
assert data["data"]["flowid"] == test_flow["flowid"]
assert data["data"]["appid"] == test_flow["appid"]
assert data["data"]["eventid"] == test_flow["eventid"]
assert data["data"]["content"] == test_flow["content"]
def test_appversions_update(test_client,test_domain,test_app_id,login_user):
app_version ={
"versionname": "version1",
"comment": "save version1",
"appid": test_app_id
}
response = test_client.post("/api/apps", json=app_version,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["domainurl"] == test_domain.url
assert data["data"]["version"] == 1
assert data["data"]["appid"] == app_version["appid"]
assert data["data"]["versionname"] == app_version["versionname"]
assert data["data"]["is_saved"] == False
def test_apps_list(test_client,login_user):
response = test_client.get("/api/apps", headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert data["data"] is not None
assert len(data["data"]) == 1
def test_appversions_list(test_client,test_domain,test_app_id,login_user):
response = test_client.get("/api/appversions/" + test_app_id , headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert data["data"] is not None
assert len(data["data"]) == 1
assert "versionname" in data["data"][0]
def test_appversions_change(test_client,test_domain,test_app_id,login_user):
app_version ={
"versionname": "version2",
"comment": "test",
"appid": test_app_id
}
response = test_client.post("/api/apps", json=app_version,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["version"] == 2
assert data["data"]["versionname"] == app_version["versionname"]
assert data["data"]["is_saved"] == False
response = test_client.put("/api/appversions/" + test_app_id +"/1", headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["domainurl"] == test_domain.url
assert data["data"]["version"] == 1
assert data["data"]["appid"] == test_app_id
def test_delete_app(test_client,test_app_id,login_user):
response = test_client.delete("/api/apps/"+ test_app_id, headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None

View File

@@ -1,9 +0,0 @@
import logging
def test_get_allapps(test_client,test_domain,login_user):
response = test_client.get("/api/v1/allapps", headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "apps" in data
assert data["apps"] is not None
assert len(data["apps"]) > 0

169
backend/conftest.py Normal file
View File

@@ -0,0 +1,169 @@
import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from sqlalchemy_utils import database_exists, create_database, drop_database
from fastapi.testclient import TestClient
import typing as t
from app.core import config, security
from app.db.session import Base, get_db
from app.db import models
from app.main import app
def get_test_db_url() -> str:
return f"{config.SQLALCHEMY_DATABASE_URI}_test"
@pytest.fixture
def test_db():
"""
Modify the db session to automatically roll back after each test.
This is to avoid tests affecting the database state of other tests.
"""
# Connect to the test database
engine = create_engine(
get_test_db_url(),
)
connection = engine.connect()
trans = connection.begin()
# Run a parent transaction that can roll back all changes
test_session_maker = sessionmaker(
autocommit=False, autoflush=False, bind=engine
)
test_session = test_session_maker()
test_session.begin_nested()
@event.listens_for(test_session, "after_transaction_end")
def restart_savepoint(s, transaction):
if transaction.nested and not transaction._parent.nested:
s.expire_all()
s.begin_nested()
yield test_session
# Roll back the parent transaction after the test is complete
test_session.close()
trans.rollback()
connection.close()
@pytest.fixture(scope="session", autouse=True)
def create_test_db():
"""
Create a test database and use it for the whole test session.
"""
test_db_url = get_test_db_url()
# Create the test database
assert not database_exists(
test_db_url
), "Test database already exists. Aborting tests."
create_database(test_db_url)
test_engine = create_engine(test_db_url)
Base.metadata.create_all(test_engine)
# Run the tests
yield
# Drop the test database
drop_database(test_db_url)
@pytest.fixture
def client(test_db):
"""
Get a TestClient instance that reads/write to the test database.
"""
def get_test_db():
yield test_db
app.dependency_overrides[get_db] = get_test_db
yield TestClient(app)
@pytest.fixture
def test_password() -> str:
return "securepassword"
def get_password_hash() -> str:
"""
Password hashing can be expensive so a mock will be much faster
"""
return "supersecrethash"
@pytest.fixture
def test_user(test_db) -> models.User:
"""
Make a test user in the database
"""
user = models.User(
email="fake@email.com",
hashed_password=get_password_hash(),
is_active=True,
)
test_db.add(user)
test_db.commit()
return user
@pytest.fixture
def test_superuser(test_db) -> models.User:
"""
Superuser for testing
"""
user = models.User(
email="fakeadmin@email.com",
hashed_password=get_password_hash(),
is_superuser=True,
)
test_db.add(user)
test_db.commit()
return user
def verify_password_mock(first: str, second: str) -> bool:
return True
@pytest.fixture
def user_token_headers(
client: TestClient, test_user, test_password, monkeypatch
) -> t.Dict[str, str]:
monkeypatch.setattr(security, "verify_password", verify_password_mock)
login_data = {
"username": test_user.email,
"password": test_password,
}
r = client.post("/api/token", data=login_data)
tokens = r.json()
a_token = tokens["access_token"]
headers = {"Authorization": f"Bearer {a_token}"}
return headers
@pytest.fixture
def superuser_token_headers(
client: TestClient, test_superuser, test_password, monkeypatch
) -> t.Dict[str, str]:
monkeypatch.setattr(security, "verify_password", verify_password_mock)
login_data = {
"username": test_superuser.email,
"password": test_password,
}
r = client.post("/api/token", data=login_data)
tokens = r.json()
a_token = tokens["access_token"]
headers = {"Authorization": f"Bearer {a_token}"}
return headers

View File

@@ -29,11 +29,3 @@ python -m venv env
```bash ```bash
uvicorn app.main:app --reload uvicorn app.main:app --reload
``` ```
# ZCC対応
1. ENV環境中Pythone現在使用している証明書(cacert.pem)のパス確認
```
python -m certifi
# C:\Projects\AI-IOT\AppBuilderforkintone\backend\env\Scripts\python.exe: No module named certifi
```
2. 上記のコマンドを実行すると、証明書までのパスが出てくるので、どこかにメモしてください。次のコマンドで使います。

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,211 +0,0 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" version="24.9.2">
<diagram name="ページ1" id="oKiyF3b1qzm0IX1SNAWJ">
<mxGraphModel dx="1395" dy="1078" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Clf12EPbcqlM1huzJpPN-73" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d0cee2;strokeColor=#56517e;" vertex="1" parent="1">
<mxGeometry x="92" y="645" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-71" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="110" y="605" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-53" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffcccc;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="140" y="570" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-52" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#cdeb8b;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="170" y="530" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-51" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffcc99;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="210" y="490" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-49" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="90" y="20" width="1080" height="290" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-1" target="Clf12EPbcqlM1huzJpPN-9">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-1" value="タスク開始" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="120" y="215" width="90" height="50" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-45" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;curved=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-8" target="Clf12EPbcqlM1huzJpPN-9">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-47" value="DBからクロールのタスクを取得する" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Clf12EPbcqlM1huzJpPN-45">
<mxGeometry x="0.2449" y="53" relative="1" as="geometry">
<mxPoint x="17" y="-44" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-79" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-8" target="Clf12EPbcqlM1huzJpPN-78">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-8" value="DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#cce5ff;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="570" y="-110" width="100" height="70" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-9" value="&lt;div style=&quot;background-color: rgb(255, 255, 254); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; font-size: 14px; line-height: 19px; white-space: pre;&quot;&gt;&lt;span style=&quot;&quot;&gt;タスクスケジューラ&lt;/span&gt;&lt;/div&gt;" style="whiteSpace=wrap;html=1;rounded=1;fontColor=default;" vertex="1" parent="1">
<mxGeometry x="260" y="217.5" width="140" height="45" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;curved=1;startArrow=block;startFill=1;endArrow=none;endFill=0;dashed=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-15" target="Clf12EPbcqlM1huzJpPN-9">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-18" value="タスクの割り当て" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Clf12EPbcqlM1huzJpPN-17">
<mxGeometry x="0.04" relative="1" as="geometry">
<mxPoint x="-18" y="30" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;curved=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-15" target="Clf12EPbcqlM1huzJpPN-31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-15" value="&lt;b&gt;RabbitMQ&lt;/b&gt;" style="whiteSpace=wrap;html=1;rounded=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="510" y="380" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-22" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-19" target="Clf12EPbcqlM1huzJpPN-21">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-19" value="MQコマンドの受信" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="250" y="560" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;curved=1;dashed=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-15" target="Clf12EPbcqlM1huzJpPN-19">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-24" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-21" target="Clf12EPbcqlM1huzJpPN-23">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-21" value="データ収集" style="whiteSpace=wrap;html=1;rounded=0;" vertex="1" parent="1">
<mxGeometry x="490" y="560" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;curved=1;dashed=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-23" target="Clf12EPbcqlM1huzJpPN-15">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-23" value="結果をMQメッセージを変換" style="whiteSpace=wrap;html=1;rounded=0;" vertex="1" parent="1">
<mxGeometry x="740" y="560" width="170" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;curved=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-26" target="Clf12EPbcqlM1huzJpPN-31">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="980" y="390" />
<mxPoint x="980" y="350" />
<mxPoint x="745" y="350" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-26" value="データ収集結果&lt;br&gt;一時保存" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#eeeeee;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="970" y="390" width="100" height="70" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-27" value="タスクコマンド" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="1">
<mxGeometry x="340" y="450" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-23" target="Clf12EPbcqlM1huzJpPN-26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-29" value="コンテンツなど大きい情報を一時保存" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="950" y="500" width="230" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-30" value="収集結果通知MQ" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="740" y="450" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-37" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-31" target="Clf12EPbcqlM1huzJpPN-36">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-31" value="メッセージキューから&lt;div&gt;データ受信&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="677" y="212.5" width="150" height="50" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-34" value="MQのキーで収集結果を取得" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="760" y="350" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-39" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-36" target="Clf12EPbcqlM1huzJpPN-38">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-36" value="&lt;div style=&quot;background-color: rgb(255, 255, 254); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; white-space: pre;&quot;&gt;&lt;span&gt;データクリーニング&lt;br&gt;と重複排除&lt;/span&gt;&lt;/div&gt;" style="whiteSpace=wrap;html=1;rounded=1;fontColor=default;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="890" y="207.5" width="148" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-43" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-38" target="Clf12EPbcqlM1huzJpPN-42">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-38" value="データベースに保存" style="whiteSpace=wrap;html=1;fontSize=11;rounded=1;" vertex="1" parent="1">
<mxGeometry x="904" y="60" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-44" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-42" target="Clf12EPbcqlM1huzJpPN-31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-42" value="収集結果からサブ項目を抽出" style="whiteSpace=wrap;html=1;fontSize=11;rounded=1;" vertex="1" parent="1">
<mxGeometry x="692" y="60" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-46" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;curved=1;dashed=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-38" target="Clf12EPbcqlM1huzJpPN-8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-48" value="クロール結果をDBに登録する" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Clf12EPbcqlM1huzJpPN-46">
<mxGeometry x="0.1149" y="13" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-50" value="&lt;b&gt;メインプログラム&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="540" y="20" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-54" value="&lt;b&gt;クローラー サブプログラム&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="485" y="490" width="185" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-55" value="&lt;b&gt;SharePointクローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="220" y="640" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-60" value="MQ情報" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;align=center;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" vertex="1" parent="1">
<mxGeometry x="1024" y="630" width="160" height="236" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-61" value="MQキー" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="26" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-70" value="クローラーID" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="56" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-62" value="分類" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="86" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-63" value="タイトル" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="116" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-64" value="URL" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="146" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-65" value="サブ項目" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="176" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-66" value="コンテンツ" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="206" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-67" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-60" target="Clf12EPbcqlM1huzJpPN-23">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="930" y="840" as="sourcePoint" />
<mxPoint x="980" y="790" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-68" value="&lt;b&gt;OneNodeクローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="180" y="680" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-69" value="&lt;b&gt;EIM クローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="150" y="720" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-72" value="&lt;b&gt;Coredasuクローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="120" y="755" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-74" value="&lt;b&gt;その他クローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="92" y="795" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-75" value="PostGre SQL" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="585" y="-40" width="90" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-76" value="再帰的な処理" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1030" y="140" width="80" height="50" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-77" value="NEO4J" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" vertex="1" parent="1">
<mxGeometry x="570" y="-390" width="100" height="80" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-78" value="データ抽出" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="560" y="-250" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-80" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-78" target="Clf12EPbcqlM1huzJpPN-77">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

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

View File

@@ -37,8 +37,7 @@ module.exports = configure(function (/* ctx */) {
// https://v2.quasar.dev/quasar-cli-vite/boot-files // https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [ boot: [
'axios', 'axios',
'error-handler', 'error-handler'
'permissions'
], ],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
@@ -105,7 +104,7 @@ module.exports = configure(function (/* ctx */) {
config: {}, config: {},
// iconSet: 'material-icons', // Quasar icon set // iconSet: 'material-icons', // Quasar icon set
lang: 'ja', // Quasar language pack // lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact // For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples), // (like functional components as one of the examples),
@@ -116,8 +115,7 @@ module.exports = configure(function (/* ctx */) {
// Quasar plugins // Quasar plugins
plugins: [ plugins: [
'Notify', 'Notify'
'Dialog'
] ]
}, },

View File

@@ -1,8 +1,6 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance, AxiosResponse } from 'axios'; import axios, { AxiosInstance } from 'axios';
import {router} from 'src/router'; import {router} from 'src/router';
import { IResponse } from 'src/types/BaseTypes';
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
@@ -17,10 +15,30 @@ declare module '@vue/runtime-core' {
// good idea to move this instance creation inside of the // good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually // "export default () => {}" function below (which runs individually
// for each client) // for each client)
const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL }); const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL });
const token=localStorage.getItem('token')||'';
if(token!==''){
api.defaults.headers["Authorization"]='Bearer ' + token;
}
//axios例外キャプチャー
api.interceptors.response.use(
(response)=>response,
(error)=>{
if (error.response && error.response.status === 401) {
// 認証エラーの場合再ログインする
console.error('(; ゚Д゚)/認証エラー(401)', error);
localStorage.removeItem('token');
router.replace({
path:"/login",
query:{redirect:router.currentRoute.value.fullPath}
});
}
return Promise.reject(error);
}
)
export default boot(({ app }) => { export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api // for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios; app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form) // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file // so you won't necessarily have to import axios in each vue file

View File

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

View File

@@ -1,75 +0,0 @@
// src/boot/permissions.ts
import { boot } from 'quasar/wrappers';
import { useAuthStore } from 'src/stores/useAuthStore';
import { DirectiveBinding } from 'vue';
export const MenuMapping = {
home: null,
app: null,
version: null,
user: 'user',
role: 'role',
domain: null,
userDomain: null,
};
export const Actions = {
user: {
show: 'user_list',
add: 'user_add',
edit: 'user_edit',
delete: 'user_delete',
},
role: {
show: 'role_list',
},
domain: {
show: 'domain_list',
add: 'domain_add',
edit: 'domain_edit',
delete: 'domain_delete',
grantUse: {
list: 'domain_grant_use_list',
edit: 'domain_grant_use_edit',
},
grantManage: {
list: 'domain_grant_manage_list',
edit: 'domain_grant_manage_edit',
},
},
};
const store = useAuthStore();
export default boot(({ app }) => {
app.directive('permissions', {
mounted(el: HTMLElement, binding: DirectiveBinding) {
if (!hasPermission(binding.value)) {
hideElement(el);
}
},
});
app.config.globalProperties.$hasPermission = hasPermission;
});
function hasPermission(value: any) {
if (!value || store.isSuperAdmin) {
return true;
}
if (typeof value === 'string') {
return store.permissions[value];
} else if (typeof value === 'object') {
return Object.values(value).some((permission: any) => store.permissions[permission]);
} else if (Array.isArray(value)) {
return value.some((permission) => store.permissions[permission]);
}
}
function hideElement(el: HTMLElement) {
if (el.parentNode) {
el.parentNode.removeChild(el);
} else {
el.style.display = 'none';
}
}

View File

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

View File

@@ -33,7 +33,7 @@
<div class="row"> <div class="row">
<field-select ref="fieldDlg" name="フィールド" :type="selectType" :updateSelectFields="updateSelectFields" <field-select ref="fieldDlg" name="フィールド" :type="selectType" :updateSelectFields="updateSelectFields"
:appId="selField?.app?.id" not_page :filter="fieldFilter" :appId="selField?.app?.id" not_page :filter="fieldFilter"
:selectedFields="selField.fields" :fieldTypes="fieldTypes"></field-select> :selectedFields="selField.fields"></field-select>
</div> </div>
</div> </div>
</div> </div>
@@ -92,10 +92,7 @@ export default defineComponent({
type: String, type: String,
default: 'single' default: 'single'
}, },
fieldTypes:{
type:Array,
default:()=>[]
}
}, },
setup(props, { emit }) { setup(props, { emit }) {
const showSelectApp = ref(false); const showSelectApp = ref(false);
@@ -108,7 +105,7 @@ export default defineComponent({
const updateSelectApp = (newAppinfo: IApp) => { const updateSelectApp = (newAppinfo: IApp) => {
selField.app = newAppinfo selField.app = newAppinfo
} }
const updateSelectFields = (newFields: IField[]) => { const updateSelectFields = (newFields: IField[]) => {
selField.fields = newFields selField.fields = newFields
} }

View File

@@ -21,7 +21,7 @@
</div> </div>
</template> </template>
</q-field> </q-field>
<q-field stack-label full-width label="アプリ説明"> <q-field stack-label full-width label="アプリ説明">
<template v-slot:control> <template v-slot:control>
<div class="self-center full-width no-outline" tabindex="0"> <div class="self-center full-width no-outline" tabindex="0">
{{ appinfo?.description }} {{ appinfo?.description }}
@@ -46,9 +46,9 @@ export default defineComponent({
const { app } = toRefs(props); const { app } = toRefs(props);
const authStore = useAuthStore(); const authStore = useAuthStore();
const appinfo = ref<AppInfo>({ const appinfo = ref<AppInfo>({
appId: '', appId: "",
name: '', name: "",
description: '' description: ""
}); });
const link= ref(`${authStore.currentDomain.kintoneUrl}/k/${app.value}`); const link= ref(`${authStore.currentDomain.kintoneUrl}/k/${app.value}`);
const getAppInfo = async (appId:string|undefined) => { const getAppInfo = async (appId:string|undefined) => {
@@ -56,7 +56,7 @@ export default defineComponent({
return; return;
} }
let result : any ={appId:'',name:''}; let result : any ={appId:"",name:""};
let retry =0; let retry =0;
while(retry<=3 && result && result.appId!==appId){ while(retry<=3 && result && result.appId!==appId){
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));

View File

@@ -1,58 +1,64 @@
<template> <template>
<detail-field-table <div class="q-px-xs">
detailField="description" <div v-if="!isLoaded" class="spinner flex flex-center">
:name="name" <q-spinner color="primary" size="3em" />
:type="type" </div>
:filter="filter" <q-table v-else class="app-table" :selection="type" row-key="id" v-model:selected="selected" flat bordered
:columns="columns" virtual-scroll :columns="columns" :rows="rows" :pagination="pagination" :rows-per-page-options="[0]"
:fetchData="fetchApps" :filter="filter" style="max-height: 65vh;">
@update:selected="(item) => { selected = item }" <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> </template>
<script lang="ts"> <script lang="ts">
import { ref, PropType } from 'vue'; import { ref, onMounted, reactive, watchEffect } from 'vue'
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import DetailFieldTable from './dialog/DetailFieldTable.vue';
interface IAppDisplay {
id: string;
name: string;
description: string;
createdate: string;
}
export default { export default {
name: 'AppSelectBox', name: 'AppSelectBox',
components: {
DetailFieldTable
},
props: { props: {
name: String, name: String,
type: String, type: String,
filter: String, filter: String,
filterInitRowsFunc: { updateSelectApp: {
type: Function as PropType<(app: IAppDisplay) => boolean>, type: Function
} }
}, },
setup(props) { setup(props) {
const selected = ref<IAppDisplay[]>([]);
const columns = [ const columns = [
{ name: 'id', required: true, label: 'ID', align: 'left', field: 'id', sortable: true, sort: (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10) }, { name: 'id', required: true, label: 'ID', align: 'left', field: 'id', sortable: true },
{ name: 'name', label: 'アプリ名', field: 'name', sortable: true, align: 'left' }, { name: 'name', label: 'アプリ名', field: 'name', sortable: true, align: 'left' },
{ name: 'description', label: '概要', field: 'description', align: 'left', sortable: false }, { name: 'description', label: '概要', field: 'description', align: 'left', sortable: false },
{ name: 'createdate', label: '作成日時', field: 'createdate', align: 'left' } { name: 'createdate', label: '作成日時', field: 'createdate', align: 'left' }
]; ]
const isLoaded = ref(false);
const rows: any[] = reactive([]);
const selected = ref([])
const fetchApps = async () => { watchEffect(()=>{
const res = await api.get('api/v1/allapps'); if (selected.value && selected.value[0] && props.updateSelectApp) {
return res.data.apps.map((item: any) => ({ props.updateSelectApp(selected.value[0])
id: item.appId, }
name: item.name, });
description: item.description, onMounted(() => {
createdate: dateFormat(item.createdAt) api.get('api/v1/allapps').then(res => {
})).filter(app => !props.filterInitRowsFunc || props.filterInitRowsFunc(app)); 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 dateFormat = (dateStr: string) => {
const date = new Date(dateStr); const date = new Date(dateStr);
@@ -64,13 +70,31 @@ export default {
const minutes = pad(date.getMinutes()); const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds()); const seconds = pad(date.getSeconds());
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
}; }
return { return {
columns, columns,
fetchApps, rows,
selected selected,
}; isLoaded,
} pagination: ref({
}; rowsPerPage: 10
</script> })
}
},
}
</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

@@ -42,9 +42,9 @@ import { useQuasar } from 'quasar';
} }
}, },
emits:[ emits:[
'closed', "closed",
'update:conditionTree', "update:conditionTree",
'update:show' "update:show"
], ],
setup(props,context) { setup(props,context) {
const appDg = ref(); const appDg = ref();
@@ -52,17 +52,17 @@ import { useQuasar } from 'quasar';
const tree = ref(props.conditionTree); const tree = ref(props.conditionTree);
const closeDg = (val:string) => { const closeDg = (val:string) => {
if (val == 'OK') { if (val == 'OK') {
// if(tree.value.root.children.length===0){ if(tree.value.root.children.length===0){
// $q.notify({ $q.notify({
// type: 'negative', type: 'negative',
// message: `条件式を設定してください。` message: `条件式を設定してください。`
// }); });
// } }
context.emit('update:conditionTree',tree.value); context.emit("update:conditionTree",tree.value);
} }
showflg.value=false; showflg.value=false;
context.emit('update:show',false); context.emit("update:show",false);
context.emit('closed',val); context.emit("closed",val);
}; };
const showflg =ref(props.show); const showflg =ref(props.show);
//条件式をコピーする //条件式をコピーする

View File

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

View File

@@ -68,20 +68,18 @@
<div @click.stop @keypress.stop v-else > <div @click.stop @keypress.stop v-else >
<div class="row no-wrap items-center q-my-xs"> <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"/> <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> <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" <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"/> --> <!-- <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)" <!-- <q-input v-if="!prop.node.object || !('options' in prop.node.object)"
v-model="prop.node.value" v-model="prop.node.value"
class="condition-value" :outlined="true" :dense="true" ></q-input> --> class="condition-value" :outlined="true" :dense="true" ></q-input> -->
<!-- <q-select v-if="prop.node.object && ('options' in prop.node.object)" <q-select v-if="prop.node.object && ('options' in prop.node.object)"
v-model="prop.node.value" v-model="prop.node.value"
:options="objectValueOptions(prop.node.object.options)" :options="objectValueOptions(prop.node.object.options)"
clearable clearable
value-key="index" 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-btn flat round dense icon="more_horiz" size="sm" >
<q-menu auto-close anchor="top right"> <q-menu auto-close anchor="top right">
<q-list> <q-list>
@@ -120,7 +118,6 @@ import { finished } from 'stream';
import { defineComponent,ref,reactive, computed, inject } from 'vue'; import { defineComponent,ref,reactive, computed, inject } from 'vue';
import { INode,ConditionTree,GroupNode,ConditionNode, LogicalOperator,Operator,NodeType } from '../../types/Conditions'; import { INode,ConditionTree,GroupNode,ConditionNode, LogicalOperator,Operator,NodeType } from '../../types/Conditions';
import ConditionObject from './ConditionObject.vue'; import ConditionObject from './ConditionObject.vue';
import { IDynamicInputConfig } from 'src/types/ComponentTypes';
export default defineComponent( { export default defineComponent( {
name: 'NodeCondition', name: 'NodeCondition',
components: { components: {
@@ -148,18 +145,17 @@ export default defineComponent( {
return opts; return opts;
}); });
const operatorSet = inject<Array<any>>('Operator') const operator = inject('Operator')
const operators = ref(operatorSet ? operatorSet : Object.values(Operator)); const operators =computed(()=>{
return operator ? operator : Object.values(Operator);
});
const tree = reactive(props.conditionTree); const tree = reactive(props.conditionTree);
const conditionString = computed(()=>{ const conditionString = computed(()=>{
return tree.buildConditionString(tree.root); return tree.buildConditionString(tree.root);
}); });
const objectValueOptions=(options:any):any[]|null=>{ const objectValueOptions=(options:any):any[]=>{
if(!options){
return null;
}
const opts:any[] =[]; const opts:any[] =[];
Object.keys(options).forEach((key) => Object.keys(options).forEach((key) =>
{ {
@@ -217,7 +213,7 @@ export default defineComponent( {
const canMerge =(node:INode)=>{ const canMerge =(node:INode)=>{
const checkedIndexs:number[] = ticked.value; const checkedIndexs:number[] = ticked.value;
const findNode = checkedIndexs.find(index=>node.index===index); const findNode = checkedIndexs.find(index=>node.index===index);
console.log('findNode=>',findNode!==undefined,findNode); console.log("findNode=>",findNode!==undefined,findNode);
return findNode!==undefined; return findNode!==undefined;
} }
//グループ化解散 //グループ化解散
@@ -226,14 +222,13 @@ export default defineComponent( {
ticked.value=[]; ticked.value=[];
} }
const expanded=computed(()=>tree.getGroups(tree.root)); const expanded=computed(()=>tree.getGroups(tree.root));
// addCondition(tree.root); // addCondition(tree.root);
const leftDynamicItemConfig = inject<IDynamicInputConfig>('leftDynamicItemConfig');
const rightDynamicItemConfig = inject<IDynamicInputConfig>('rightDynamicItemConfig');
return { return {
leftDynamicItemConfig, leftDynamicItemConfig :inject('leftDynamicItemConfig'),
rightDynamicItemConfig, rightDynamicItemConfig:inject('rightDynamicItemConfig'),
showingCondition, showingCondition,
conditionString, conditionString,
tree, tree,
@@ -265,12 +260,10 @@ export default defineComponent( {
max-height: 40px; max-height: 40px;
margin: 0 2px; margin: 0 2px;
} }
.operator{ .operator{
min-width: 150px; min-width: 150px;
max-height: 40px; max-height: 40px;
margin: 0 2px; margin: 0 2px;
text-align: center; text-align: center;
font-size: 12pt; font-size: 12pt;
} }

View File

@@ -1,105 +1,84 @@
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<q-uploader <q-uploader
style="max-width: 400px" style="max-width: 400px"
:url="uploadUrl" :url="uploadUrl"
:label="title" :label="title"
:headers="headers" :headers="headers"
accept=".xlsx" accept=".csv,.xlsx"
v-on:rejected="onRejected" v-on:rejected="onRejected"
v-on:uploaded="onUploadFinished" v-on:uploaded="onUploadFinished"
v-on:failed="onFailed" v-on:failed="onFailed"
field-name="files" field-name="files"
></q-uploader> ></q-uploader>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useQuasar } from 'quasar'; import { createUploaderComponent, useQuasar } from 'quasar';
import { useAuthStore } from 'src/stores/useAuthStore'; import { useAuthStore } from 'src/stores/useAuthStore';
import { ref } from 'vue'; import { ref } from 'vue';
const $q=useQuasar();
const authStore = useAuthStore();
const emit =defineEmits(['uploaded']);
/**
* ファイルアップロードを拒否する時の処理
* @param rejectedEntries
*/
function onRejected (rejectedEntries:any) {
// Notify plugin needs to be installed
// https://quasar.dev/quasar-plugins/notify#Installation
$q.notify({
type: 'negative',
message: `CSVおよびExcelファイルを選択してください。`
})
}
const $q = useQuasar(); /**
const authStore = useAuthStore(); * ファイルアップロード成功時の処理
const emit = defineEmits(['uploaded']); */
function onUploadFinished({xhr}:{xhr:XMLHttpRequest}){
let msg="ファイルのアップロードが完了しました。";
if(xhr && xhr.response){
msg=`${msg} (${xhr.responseText})`;
}
$q.notify({
type: 'positive',
caption:"通知",
message: msg
});
setTimeout(() => {
emit('uploaded',xhr.responseText);
}, 2000);
}
interface Props { /**
title?: string; *
uploadUrl?: string; * @param info ファイルアップロード失敗時の処理
} */
function onFailed({files,xhr}:{files: readonly any[],xhr:any}){
let msg ="ファイルアップロードが失敗しました。";
if(xhr && xhr.status){
msg=`${msg} (${xhr.status }:${xhr.statusText})`
}
$q.notify({
type:"negative",
message:msg
});
}
const headers = ref([ interface Props {
{ name: 'Authorization', value: 'Bearer ' + authStore.token }, title: string;
]); uploadUrl:string;
}
const props = withDefaults(defineProps<Props>(), { const headers = ref([{name:"Authorization",value:'Bearer ' + authStore.token}]);
title: '設計書から導入する(Excel)', const props = withDefaults(defineProps<Props>(), {
uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1`, title:"設計書から導入する(csv or excel)",
}); uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1`
/** });
* ファイルアップロードを拒否する時の処理
* @param rejectedEntries
*/
function onRejected(rejectedEntries: any) {
// Notify plugin needs to be installed
// https://quasar.dev/quasar-plugins/notify#Installation
$q.notify({
type: 'negative',
message: 'Excelファイルを選択してください。',
});
}
/**
* ファイルアップロード成功時の処理
*/
function onUploadFinished({ xhr }: { xhr: XMLHttpRequest }) {
let msg = 'ファイルのアップロードが完了しました。';
if (xhr && xhr.response) {
msg = `${msg} (${xhr.responseText})`;
}
$q.notify({
type: 'positive',
caption: '通知',
message: msg,
});
setTimeout(() => {
emit('uploaded', xhr.responseText);
}, 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;
}) {
let msg = 'ファイルアップロードが失敗しました。';
if (xhr && xhr.status) {
const detail = getResponseError(xhr);
msg = `${msg} (${xhr.status}:${detail})`;
}
$q.notify({
type: 'negative',
message: msg,
});
}
</script> </script>
<style lang="scss"></style> <style lang="scss">
</style>

View File

@@ -1,78 +1,37 @@
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<q-table :loading="loading" :title="name+'一覧'" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows"> <q-table :title="name+'一覧'" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" />
<template v-slot:body-cell-name="p">
<q-td class="content-box flex justify-between items-center" :props="p">
{{ p.row.name }}
<q-badge v-if="!p.row.domainActive" color="grey">未启用</q-badge>
<q-badge v-if="p.row.id == currentDomainId" color="primary">現在</q-badge>
</q-td>
</template>
</q-table>
</div> </div>
</template> </template>
<script> <script>
import { ref,onMounted,reactive, computed } from 'vue' import { ref,onMounted,reactive } from 'vue'
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { useAuthStore } from 'src/stores/useAuthStore';
export default { export default {
name: 'DomainSelect', name: 'DomainSelect',
props: { props: {
name: String, name: String,
type: String, type: String
filterInitRowsFunc: {
type: Function,
},
}, },
setup(props) { setup() {
const authStore = useAuthStore(); const columns = [
const currentDomainId = computed(() => authStore.currentDomain.id); { name: 'id'},
const loading = ref(true); { name: 'tenantid', required: true,label: 'テナント',align: 'left',field: 'tenantid',sortable: true},
const inactiveRowClass = (row) => row.domainActive ? '' : 'inactive-row'; { name: 'name', align: 'center', label: 'ドメイン', field: 'name', sortable: true },
{ name: 'url', label: 'URL', field: 'url', sortable: true },
const columns = [ { name: 'kintoneuser', label: 'アカウント', field: 'kintoneuser' }
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true, classes: inactiveRowClass }, ]
{ name: 'name', align: 'left', label: 'ドメイン', field: 'name', sortable: true, classes: inactiveRowClass }, const rows = reactive([])
{ name: 'url', label: 'URL', field: 'url',align: 'left', sortable: true, classes: inactiveRowClass }, onMounted( () => {
{ name: 'user', label: 'アカウント', field: 'user',align: 'left', classes: inactiveRowClass }, api.get(`api/domains/1`).then(res =>{
{ name: 'owner', label: '所有者', field: row => row.owner.fullName, align: 'left', classes: inactiveRowClass }, res.data.forEach((item) =>
] {
const rows = reactive([]); rows.push({id:item.id,tenantid:item.tenantid,name:item.name,url:item.url,kintoneuser:item.kintoneuser});
}
onMounted(() => { )
loading.value = true; });
api.get('api/domains').then(res =>{
res.data.data.forEach((data) => {
const item = {
id: data.id,
tenantid: data.tenantid,
domainActive: data.is_active,
name: data.name,
url: data.url,
user: data.kintoneuser,
owner: {
id: data.owner.id,
firstName: data.owner.first_name,
lastName: data.owner.last_name,
fullNameSearch: (data.owner.last_name + data.owner.first_name).toLowerCase(),
fullName: data.owner.last_name + ' ' + data.owner.first_name,
email: data.owner.email,
isActive: data.owner.is_active,
isSuperuser: data.owner.is_superuser,
}
}
if (props.filterInitRowsFunc && !props.filterInitRowsFunc(item)) {
return;
}
rows.push(item);
})
loading.value = false;
}); });
});
return { return {
loading,
currentDomainId,
columns, columns,
rows, rows,
selected: ref([]), selected: ref([]),
@@ -81,11 +40,3 @@ export default {
} }
</script> </script>
<style lang="scss">
.q-table td.inactive-row {
color: #aaa;
}
.q-table .content-box {
box-sizing: content-box;
}
</style>

View File

@@ -1,32 +1,43 @@
<template> <template>
<q-btn-dropdown <q-btn-dropdown
class="customized-disabled-btn" color="primay"
push push
flat flat
no-caps no-caps
icon="share" icon="share"
size="md" size="md"
:label="userStore.currentDomain.domainName" :label="userStore.currentDomain.domainName"
:disable-dropdown="true"
dropdown-icon="none"
:disable="true"
> >
<q-list>
<q-item v-for="domain in domains" :key="domain.domainName"
clickable v-close-popup @click="onItemClick(domain)">
<q-item-section side>
<q-icon name="share" size="sm" color="orange" text-color="white"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>{{domain.domainName}}</q-item-label>
<q-item-label caption>{{domain.kintoneUrl}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown> </q-btn-dropdown>
</template>
<script setup lang="ts">
import { useAuthStore } from 'stores/useAuthStore';
</template>
<script setup lang="ts" >
import { IDomainInfo } from 'src/types/ActionTypes';
import { useAuthStore,IUserState } from 'stores/useAuthStore';
import { ref } from 'vue';
const userStore = useAuthStore(); const userStore = useAuthStore();
const domains = ref<IDomainInfo[]>([]);
(async ()=>{
domains.value = await userStore.getUserDomains();
})();
const onItemClick=(domain:IDomainInfo)=>{
console.log(domain);
userStore.setCurrentDomain(domain);
}
</script> </script>
<style lang="scss"> <style lang="scss">
.q-btn.disabled.customized-disabled-btn {
opacity: 1 !important;
cursor: default !important;
.q-icon.q-btn-dropdown__arrow {
display: none;
}
* {
cursor: default !important;
}
}
</style> </style>

View File

@@ -2,30 +2,13 @@
<div class="q-mx-md" style="max-width: 600px;"> <div class="q-mx-md" style="max-width: 600px;">
<!-- <q-card> --> <!-- <q-card> -->
<div class="q-mb-md"> <div class="q-mb-md">
<q-input ref="inputRef" v-if="!optionsRef|| optionsRef.length===0" <q-input ref="inputRef" outlined dense debounce="200" @update:model-value="updateSharedText"
outlined dense debounce="200" @update:model-value="updateSharedText" v-model="sharedText" :readonly="!canInput">
v-model="sharedText" :readonly="!canInputFlag" autogrow>
<template v-slot:append> <template v-slot:append>
<q-btn flat round padding="none" icon="cancel" @click="clearSharedText" color="grey-6" /> <q-btn flat round padding="none" icon="cancel" @click="clearSharedText" color="grey-6" />
</template> </template>
</q-input> </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>
<div class="row q-gutter-sm"> <div class="row q-gutter-sm">
@@ -51,12 +34,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, inject, watchEffect, defineComponent,PropType } from 'vue'; import { ref, inject, watchEffect, defineComponent } from 'vue';
import FieldAdd from './FieldAdd.vue'; import FieldAdd from './FieldAdd.vue';
import VariableAdd from './VariableAdd.vue'; import VariableAdd from './VariableAdd.vue';
// import FunctionAdd from './FunctionAdd.vue'; // import FunctionAdd from './FunctionAdd.vue';
import ShowDialog from '../ShowDialog.vue'; import ShowDialog from '../ShowDialog.vue';
import { IButtonConfig } from 'src/types/ComponentTypes';
type ButtonConfig = {
label: string;
color: string;
type: string;
editable: boolean;
};
export default defineComponent({ export default defineComponent({
name: 'DynamicItemInput', name: 'DynamicItemInput',
@@ -67,21 +56,18 @@ export default defineComponent({
ShowDialog ShowDialog
}, },
props: { props: {
canInput: { // canInput: {
type: Boolean, // type: Boolean,
default: false // default: false
}, // },
appId: { appId: {
type: String, type: String,
}, },
selectedObject: { selectedObject: {
default: {} default: {}
}, },
options:{
type:Array as PropType< string[]>
},
buttonsConfig: { buttonsConfig: {
type: Array as PropType<IButtonConfig[]>, type: Array as () => ButtonConfig[],
default: () => [ default: () => [
{ label: 'フィールド', color: 'primary', type: 'FieldAdd' } { label: 'フィールド', color: 'primary', type: 'FieldAdd' }
] ]
@@ -91,71 +77,65 @@ export default defineComponent({
const filter = ref(''); const filter = ref('');
const dialogVisible = ref(false); const dialogVisible = ref(false);
const currentDialogName = ref(''); const currentDialogName = ref('');
const selectedObjectRef = ref(props.selectedObject);
const currentComponent = ref('FieldAdd'); const currentComponent = ref('FieldAdd');
const sharedText = ref(props.selectedObject?.sharedText ?? ''); const sharedText = ref(props.selectedObject?.sharedText ?? '');
const inputRef = ref(); const inputRef = ref();
const canInputFlag = ref(props.canInput); const canInput = ref(true);
const editable = ref(false); const editable = ref(false);
const openDialog = (button: IButtonConfig) => { const openDialog = (button: ButtonConfig) => {
currentDialogName.value = button.label; currentDialogName.value = button.label;
currentComponent.value = button.type; currentComponent.value = button.type;
dialogVisible.value = true; dialogVisible.value = true;
editable.value = canInputFlag.value; editable.value = button.editable ?? true;
}; };
const closeDialog = () => { const closeDialog = () => {
dialogVisible.value = false; dialogVisible.value = false;
}; };
const handleSelect = (value:any) => { const handleSelect = (value) => {
// 获取当前光标位置
if (value && value._t && (value._t as string).length > 0) { // const cursorPosition = inputRef.value.getNativeElement().selectionStart;
canInputFlag.value = editable.value; // if (cursorPosition === undefined || cursorPosition === 0) {
}
selectedObjectRef.value={ sharedText: value._t, ...value };
sharedText.value = `${value._t}`; sharedText.value = `${value._t}`;
// emit('update:selectedObject', { sharedText: sharedText.value, ...value }); // } else {
// const textBefore = sharedText.value.substring(0, cursorPosition);
// const textAfter = sharedText.value.substring(cursorPosition);
// sharedText.value = `${textBefore}${value._t}${textAfter}`;
// }
if (value && value._t && (value._t as string).length > 0) {
canInput.value = editable.value;
}
emit('update:selectedObject', { sharedText: sharedText.value, ...value });
dialogVisible.value = false; dialogVisible.value = false;
}; };
const clearSharedText = () => { const clearSharedText = () => {
sharedText.value = ''; sharedText.value = '';
selectedObjectRef.value={}; canInput.value = true;
canInputFlag.value = true; emit('update:selectedObject', {});
// emit('update:selectedObject', {});
} }
const updateSharedText = (value:string) => { const updateSharedText = (value) => {
sharedText.value = value; sharedText.value = value;
selectedObjectRef.value= { sharedText: value,objectType:'text' } emit('update:selectedObject', { ...props.selectedObject, sharedText: value });
// 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 { return {
filter, filter,
dialogVisible, dialogVisible,
currentDialogName, currentDialogName,
currentComponent, currentComponent,
canInputFlag, canInput,
openDialog, openDialog,
closeDialog, closeDialog,
handleSelect, handleSelect,
clearSharedText, clearSharedText,
updateSharedText, updateSharedText,
setValue,
sharedText, sharedText,
inputRef, inputRef
optionsRef,
selectedObjectRef
}; };
} }
}); });
</script> </script>

View File

@@ -1,11 +1,9 @@
<template> <template>
<q-item <q-item
v-permissions="permission"
clickable clickable
tag="a" tag="a"
:target="target?target:'_blank'" :target="target?target:'_blank'"
:href="link" :href="link"
:disable="disable"
v-if="!isSeparator" v-if="!isSeparator"
> >
<q-item-section <q-item-section
@@ -21,7 +19,6 @@
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator <q-separator
class="q-my-sm"
v-if="isSeparator" v-if="isSeparator"
inset inset
/> />
@@ -35,8 +32,6 @@ export interface EssentialLinkProps {
icon?: string; icon?: string;
isSeparator?: boolean; isSeparator?: boolean;
target?:string; target?:string;
disable?:boolean;
permission?: string|null;
} }
withDefaults(defineProps<EssentialLinkProps>(), { withDefaults(defineProps<EssentialLinkProps>(), {
caption: '', caption: '',

View File

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

View File

@@ -3,7 +3,7 @@
<div v-if="!isLoaded" class="spinner flex flex-center"> <div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" /> <q-spinner color="primary" size="3em" />
</div> </div>
<q-table flat bordered v-else row-key="id" :selection="type" v-model:selected="selected" :columns="columns" <q-table flat bordered v-else row-key="name" :selection="type" v-model:selected="selected" :columns="columns"
:rows="rows" :pagination="pageSetting" :filter="filter" style="max-height: 55vh;"/> :rows="rows" :pagination="pageSetting" :filter="filter" style="max-height: 55vh;"/>
</div> </div>
</template> </template>
@@ -26,7 +26,7 @@ export default {
default: false, default: false,
}, },
selectedFields:{ selectedFields:{
type:Array , type:Array,
default:()=>[] default:()=>[]
}, },
fieldTypes:{ fieldTypes:{
@@ -37,10 +37,6 @@ export default {
updateSelectFields: { updateSelectFields: {
type: Function type: Function
}, },
blackListLabel: {
type:Array,
default:()=>[]
}
}, },
setup(props) { setup(props) {
const isLoaded = ref(false); const isLoaded = ref(false);
@@ -48,16 +44,16 @@ export default {
{ name: 'name', required: true, label: 'フィールド名', align: 'left', field: row => row.name, sortable: true }, { name: 'name', required: true, label: 'フィールド名', align: 'left', field: row => row.name, sortable: true },
{ name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true }, { name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true } { name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true }
]; ]
const pageSetting = ref({ const pageSetting = ref({
sortBy: 'name', sortBy: 'desc',
descending: false, descending: false,
page: 1, page: 1,
rowsPerPage: props.not_page ? 0 : 25 rowsPerPage: props.not_page ? 0 : 5
// rowsNumber: xx if getting data from a server // rowsNumber: xx if getting data from a server
}); });
const rows = reactive([]); const rows = reactive([]);
const selected = ref((props.selectedFields && props.selectedFields.length>0)?props.selectedFields:[]); const selected = ref(props.selectedFields && props.selectedFields.length>0?props.selectedFields:[]);
onMounted(async () => { onMounted(async () => {
const url = props.fieldTypes.includes('SPACER')?'api/v1/allfields':'api/v1/appfields'; const url = props.fieldTypes.includes('SPACER')?'api/v1/allfields':'api/v1/appfields';
@@ -66,25 +62,16 @@ export default {
app: props.appId app: props.appId
} }
}); });
let fields = Object.values(res.data.properties); let fields = res.data.properties;
for (const index in fields) { Object.keys(fields).forEach((key) => {
const fld = fields[index] const fld = fields[key];
if(props.blackListLabel.length > 0){ if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
if(!props.blackListLabel.find(blackListItem => blackListItem === fld.label)){ rows.push({ name: fld.label || fld.code, ...fld });
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){ }else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){
rows.push({id:index, name: fld.label || fld.code, ...fld }); rows.push({ name: fld.label || fld.code, ...fld });
}else if(props.fieldTypes.includes('lookup') && ('lookup' in fld)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}
}
} else {
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}else if(props.fieldTypes.includes('lookup') && ('lookup' in fld)){
rows.push({id:index, name: fld.label || fld.code, ...fld });
}
} }
}
});
isLoaded.value = true; isLoaded.value = true;
}); });

View File

@@ -1,24 +0,0 @@
<template>
<q-badge class="q-mr-xs" v-if="isOwner" color="secondary">所有者</q-badge>
<!-- <q-badge v-else-if="isManager" color="primary">管理者</q-badge> -->
<q-badge v-if="isSelf" color="purple">自分</q-badge>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore();
interface Props {
user: { id: number };
domain: IDomainOwnerDisplay;
}
const props = defineProps<Props>();
const isSelf = computed(() => props.user.id === (Number)(authStore.userId));
const isOwner = computed(() => props.user.id === props.domain.owner.id);
const isManager = computed(() => props.user.id === props.domain.owner.id); // TODO
</script>

View File

@@ -1,276 +0,0 @@
<template>
<q-dialog :auto-close="false" :model-value="visible" persistent bordered>
<q-card class="dialog-content" >
<q-toolbar class="bg-grey-4">
<q-toolbar-title>{{ dialogTitle }}</q-toolbar-title>
<q-btn flat round dense icon="close" @click="close" />
</q-toolbar>
<q-card-section class="q-mx-md " >
<q-select
v-permissions="props.actionPermissions.edit"
class="q-mt-md"
:disable="loading||!domain.domainActive"
filled
dense
v-model="canSharedUserFilter"
use-input
input-debounce="0"
:options="canSharedUserFilteredOptions"
clearable
:placeholder="canSharedUserFilter ? '' : domain.domainActive ? '権限を付与するユーザーを選択' : 'ドメインが無効なため、権限を付与できません'"
@filter="filterFn">
<template v-slot:selected-item="scope">
<span v-if="canSharedUserFilter">
{{ canSharedUserFilter.fullName }} {{ canSharedUserFilter.email }}
<role-label :domain="domain" :user="scope.opt"></role-label>
</span>
</template>
<template v-slot:after>
<q-btn :disable="!canSharedUserFilter" :loading="addLoading" label="付与" color="primary" @click="shareTo(canSharedUserFilter as IUserDisplayWithShareRole)" />
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>{{scope.opt.id}}</q-item-section>
<q-item-section>{{scope.opt.fullName}}</q-item-section>
<q-item-section>{{scope.opt.email}}</q-item-section>
<q-item-section side>
<div style="width: 6.5em;">
<role-label :domain="domain" :user="scope.opt"></role-label>
</div>
</q-item-section>
</q-item>
</template>
</q-select>
<sharing-user-list class="q-mt-md" style="height: 330px" :users="sharedUsers" :loading="loading" :title="userListTitle">
<template v-slot:body-cell-role="{ row }">
<q-td auto-width>
<role-label :domain="domain" :user="row"></role-label>
</q-td>
</template>
<template v-slot:actions="{ row }">
<q-btn v-permissions="props.actionPermissions.edit" round title="解除" flat color="primary" :disable="isActionDisable && isActionDisable(row)" padding="xs" size="1em" :loading="row.isRemoving" icon="person_off" @click="removeShareTo(row)" />
</template>
</sharing-user-list>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="確定" @click="checkClose" />
<q-btn flat label="キャンセル" @click="close" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { IUser, IUserDisplay } from '../../types/UserTypes';
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import SharingUserList from 'components/ShareDomain/SharingUserList.vue';
import RoleLabel from 'components/ShareDomain/RoleLabel.vue';
import { Dialog } from 'quasar'
const authStore = useAuthStore();
interface Props {
modelValue: boolean;
domain: IDomainOwnerDisplay;
dialogTitle: string;
userListTitle: string;
isActionDisable?: (user: IUserDisplay) => boolean;
shareApi: (user: IUserDisplay, domain: IDomainOwnerDisplay) => Promise<any>;
removeSharedApi: (user: IUserDisplay, domain: IDomainOwnerDisplay) => Promise<any>;
getSharedApi: (domain: IDomainOwnerDisplay) => Promise<any>;
actionPermissions: { 'list': string, 'edit': string };
}
interface IUserDisplayWithShareRole extends IUserDisplay {
isRemoving: boolean;
role: number;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
}>();
const addLoading = ref(false);
const loading = ref(true);
const visible = ref(props.modelValue);
const allUsers = ref<IUserDisplayWithShareRole[]>([]);
const sharedUsers = ref<IUserDisplayWithShareRole[]>([]);
const sharedUsersIdSet = new Set<number>();
const canSharedUsers = ref<IUserDisplayWithShareRole[]>([]);
const canSharedUserFilter = ref<IUserDisplayWithShareRole>();
const canSharedUserFilteredOptions = ref<IUserDisplayWithShareRole[]>([]);
const filterFn = (val:string, update: (cb: () => void) => void) => {
update(() => {
if (val === '') {
canSharedUserFilteredOptions.value = canSharedUsers.value;
return;
}
const needle = val.toLowerCase();
canSharedUserFilteredOptions.value = canSharedUsers.value.filter(v =>
v.email.toLowerCase().indexOf(needle) > -1 || v.fullNameSearch.toLowerCase().indexOf(needle) > -1);
})
}
watch(
() => props.modelValue,
async (newValue) => {
visible.value = newValue;
sharedUsers.value = [];
canSharedUserFilter.value = undefined
loading.value = false;
addLoading.value = false;
if (newValue) {
if (Object.keys(allUsers.value).length == 0) {
await getUsers();
}
await loadShared();
}
}
);
watch(
() => visible.value,
(newValue) => {
emit('update:modelValue', newValue);
}
);
const checkClose = () => {
if (!canSharedUserFilter.value) {
close();
return;
}
Dialog.create({
title: '注意',
message: '選択済だがまだ付与未完了のユーザーがあります。<br>必要な操作を選んでください。',
html: true,
persistent: true,
ok: {
color: 'primary',
label: '付与'
},
cancel: '直接閉じる',
}).onCancel(() => {
close();
}).onOk(() => {
shareTo(canSharedUserFilter.value as IUserDisplayWithShareRole);
});
};
const close = () => {
emit('close');
};
const shareTo = async (user: IUserDisplayWithShareRole) => {
addLoading.value = true;
loading.value = true;
await props.shareApi(user, props.domain);
await loadShared();
canSharedUserFilter.value = undefined;
loading.value = false;
addLoading.value = false;
}
const removeShareTo = async (user: IUserDisplayWithShareRole) => {
loading.value = true;
user.isRemoving = true;
await props.removeSharedApi(user, props.domain);
if (isCurrentDomain()) {
await authStore.loadCurrentDomain();
}
await loadShared();
loading.value = false;
};
const isCurrentDomain = () => {
return props.domain.id === authStore.currentDomain.id;
}
const loadShared = async () => {
loading.value = true;
sharedUsersIdSet.clear();
const { data } = await props.getSharedApi(props.domain);
sharedUsers.value = data.data.reduce((arr: IUserDisplayWithShareRole[], item: IUser) => {
const val = itemToDisplay(item);
if(!sharedUsersIdSet.has(val.id)) {
sharedUsersIdSet.add(val.id);
// for sort
if (isOwner(val.id)) {
val.role = 2;
} else if (isManager(val.id)) {
val.role = 1;
} else {
val.role = 0;
}
arr.push(val);
}
return arr;
}, []).sort((a: IUserDisplayWithShareRole, b: IUserDisplayWithShareRole) => b.role - a.role);
canSharedUsers.value = allUsers.value.filter((item) => !sharedUsersIdSet.has(item.id));
canSharedUserFilteredOptions.value = canSharedUsers.value;
loading.value = false;
}
function isOwner(userId: number) {
return userId === props.domain?.owner?.id
}
function isManager(userId: number) {
return false // TODO
}
const getUsers = async () => {
loading.value = true;
const result = await api.get('api/v1/users');
allUsers.value = result.data.data.map(itemToDisplay);
loading.value = false;
}
const itemToDisplay = (item: IUser) => {
return {
id: item.id,
firstName: item.first_name,
lastName: item.last_name,
fullNameSearch: (item.last_name + item.first_name).toLowerCase(),
fullName: item.last_name + ' ' + item.first_name,
email: item.email,
isSuperuser: item.is_superuser,
isActive: item.is_active,
role: 0,
} as IUserDisplayWithShareRole
}
</script>
<style lang="scss">
.dialog-content {
width: 700px !important;
max-width: 80vw !important;
max-height: 80vh;
.q-select {
min-width: 0 !important;
}
}
</style>

View File

@@ -1,63 +0,0 @@
<template>
<share-domain-dialog
:dialogTitle="`「${domain.name}」の接続先管理権限設定`"
userListTitle="接続先管理権限を持つユーザー"
:domain="domain"
:share-api="shareApi"
:remove-shared-api="removeSharedApi"
:get-shared-api="getSharedApi"
:is-action-disable="(row) => row.id === authStore.userId"
:model-value="modelValue"
:action-permissions="Actions.domain.grantManage"
@update:modelValue="updateModelValue"
@close="close"
/>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
import { IUserDisplay } from '../../types/UserTypes';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { api } from 'boot/axios';
import { Actions } from 'boot/permissions';
import { useAuthStore } from 'src/stores/useAuthStore';
import { IResponse } from 'src/types/BaseTypes';
const authStore = useAuthStore();
interface Props {
modelValue: boolean;
domain: IDomainOwnerDisplay;
}
const props = defineProps<Props>();
async function shareApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
return api.post('api/managedomain', {
userid: user.id,
domainid: domain.id,
});
}
async function removeSharedApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
return api.delete<IResponse>(`api/managedomain/${domain.id}/${user.id}`);
}
async function getSharedApi(domain: IDomainOwnerDisplay) {
return api.get<IResponse>(`/api/managedomainuser/${domain.id}`);
}
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
}>();
const updateModelValue = (value: boolean) => {
emit('update:modelValue', value);
};
const close = () => {
emit('close');
};
</script>

View File

@@ -1,58 +0,0 @@
<template>
<share-domain-dialog
:dialogTitle="`「${domain.name}」のドメイン利用権限設定`"
userListTitle="ドメイン利用権限を持つユーザー"
:domain="domain"
:share-api="shareApi"
:remove-shared-api="removeSharedApi"
:get-shared-api="getSharedApi"
:model-value="modelValue"
:action-permissions="Actions.domain.grantUse"
@update:modelValue="updateModelValue"
@close="close"
/>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
import { IUserDisplay } from '../../types/UserTypes';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { api } from 'boot/axios';
import { Actions } from 'boot/permissions';
interface Props {
modelValue: boolean;
domain: IDomainOwnerDisplay;
}
const props = defineProps<Props>();
async function shareApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
return api.post('api/userdomain', {
userid: user.id,
domainid: domain.id,
});
}
async function removeSharedApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
return api.delete(`api/domain/${domain.id}/${user.id}`);
}
async function getSharedApi(domain: IDomainOwnerDisplay) {
return api.get(`/api/domainshareduser/${domain.id}`);
}
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
}>();
const updateModelValue = (value: boolean) => {
emit('update:modelValue', value);
};
const close = () => {
emit('close');
};
</script>

View File

@@ -1,53 +0,0 @@
<template>
<q-table :rows="users" :filter="filter" dense :columns="columns" row-key="id" :loading="loading" :pagination="pagination">
<template v-slot:top>
<div class="h6 text-weight-bold">{{props.title}}</div>
<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-for="col in columns" :key="col.name" v-slot:[`body-cell-${col.name}`]="props">
<slot :name="`body-cell-${col.name}`" :row="props.row" :column="props.col">
<!-- 默认内容 -->
<q-td v-if="col.name !== 'actions'" :props="props" >
<span>{{ props.row[col.name] }}</span>
</q-td>
<!-- actions -->
<q-td v-else auto-width :props="props">
<slot name="actions" :row="props.row"></slot>
</q-td>
</slot>
</template>
</q-table>
</template>
<script setup lang="ts">
import { ref, PropType } from 'vue';
import { IUserDisplay } from '../../types/UserTypes';
const props = defineProps({
users: {
type: Array as PropType<IUserDisplay[]>,
required: true,
},
loading: {
type: Boolean,
default: false,
},
title: String
});
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'fullName', label: '名前', field: 'fullName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'role', label: '', field: 'role', align: 'left', sortable: false },
{ name: 'actions', label: '', field: 'actions', sortable: false },
];
const filter = ref('');
const pagination = ref({ rowsPerPage: 10 });
</script>

View File

@@ -4,16 +4,16 @@
<q-card class="" style="min-width: 40vw; max-width: 80vw; max-height: 95vh;" :style="cardStyle"> <q-card class="" style="min-width: 40vw; max-width: 80vw; max-height: 95vh;" :style="cardStyle">
<q-toolbar class="bg-grey-4"> <q-toolbar class="bg-grey-4">
<q-toolbar-title>{{ name }}</q-toolbar-title> <q-toolbar-title>{{ name }}</q-toolbar-title>
<q-space v-if="$slots.toolbar"></q-space> <q-space></q-space>
<slot name="toolbar"></slot> <slot name="toolbar"></slot>
<q-btn flat round dense icon="close" @click="CloseDialogue('Cancel')" /> <q-btn flat round dense icon="close" @click="CloseDialogue('Cancel')" />
</q-toolbar> </q-toolbar>
<q-card-section class="q-mt-md" :style="sectionStyle"> <q-card-section class="q-mt-md" :style="sectionStyle">
<slot></slot> <slot></slot>
</q-card-section> </q-card-section>
<q-card-actions v-if="!disableBtn" align="right" class="text-primary"> <q-card-actions align="right" class="text-primary">
<q-btn flat :label="okBtnLabel || '確定'" :loading="okBtnLoading" :v-close-popup="okBtnAutoClose" @click="CloseDialogue('OK')" /> <q-btn flat label="確定" v-close-popup @click="CloseDialogue('OK')" />
<q-btn flat label="キャンセル" :disable="okBtnLoading" v-close-popup @click="CloseDialogue('Cancel')" /> <q-btn flat label="キャンセル" v-close-popup @click="CloseDialogue('Cancel')" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
@@ -29,21 +29,10 @@ export default {
width:String, width:String,
height:String, height:String,
minWidth:String, minWidth:String,
minHeight:String, minHeight:String
okBtnLabel:String,
okBtnLoading:Boolean,
okBtnAutoClose:{
type: Boolean,
default: true
},
disableBtn:{
type: Boolean,
default: false
}
}, },
emits: [ emits: [
'close', 'close'
'update:visible'
], ],
setup(props, context) { setup(props, context) {
const CloseDialogue = (val) => { const CloseDialogue = (val) => {

View File

@@ -1,92 +0,0 @@
<template>
<q-btn v-if="hasPermission()" flat padding="xs" round size="1em" icon="more_vert" class="action-menu">
<q-menu :max-width="maxWidth">
<q-list dense :style="{ 'min-width': minWidth }">
<template v-for="(item, index) in actions" :key="index" >
<q-item v-if="isAction(item)" v-permissions="item.permission" :disable="isFunction(item.disable) ? item.disable(row) : item.disable"
:class="item.class" clickable v-close-popup @click="item.action(row)">
<q-item-section side style="color: inherit;">
<q-icon size="1.2em" :name="item.icon" />
</q-item-section>
<q-item-section>{{ item.label }}</q-item-section>
<q-tooltip v-if="item.tooltip && !isFunction(item.tooltip) || (isFunction(item.tooltip) && item.tooltip(row))" :delay="500" self="center middle">
{{ isFunction(item.tooltip) ? item.tooltip(row) : item.tooltip }}
</q-tooltip>
</q-item>
<q-separator v-else />
</template>
</q-list>
</q-menu>
</q-btn>
</template>
<script lang="ts">
import { PropType, getCurrentInstance } from 'vue';
import { IDomainOwnerDisplay } from '../types/DomainTypes';
interface Action {
label: string;
icon?: string;
tooltip?: string|((row: IDomainOwnerDisplay) => string);
disable?: boolean|((row: IDomainOwnerDisplay) => boolean);
permission?: string|object;
action: (row: any) => void|Promise<void>;
class?: string;
}
interface Separator {
separator: boolean;
}
type MenuItem = Action | Separator;
export default {
name: 'TableActionMenu',
props: {
row: {
type: Object as PropType<IDomainOwnerDisplay>,
required: true
},
maxWidth: {
type: String,
default: '150px'
},
minWidth: {
type: String,
default: '100px'
},
actions: {
type: Array as PropType<MenuItem[]>,
required: true
}
},
methods: {
isAction(item: MenuItem): item is Action {
return !('separator' in item);
},
isFunction(item: any): item is ((row: IDomainOwnerDisplay) => boolean|string) {
return typeof item === 'function';
},
hasPermission() {
const proxy = getCurrentInstance()?.proxy;
if (!proxy) {
return false;
}
for (const item of this.actions) {
if (this.isAction(item) && proxy.$hasPermission(item.permission)) {
return true;
}
}
}
}
};
</script>
<style lang="scss" scoped>
.q-table tr > td:last-child .action-menu {
opacity: 0.25 !important;
}
.q-table tr:hover > td:last-child .action-menu:not([disabled]) {
opacity: 1 !important;
}
</style>

View File

@@ -1,74 +0,0 @@
<template>
<q-card :class="['domain-card', item.id == activeId ? 'default': '']">
<q-card-section>
<div class="row no-wrap">
<div class="col">
<div class="text-h6 ellipsis">{{ item.name }}</div>
<div class="text-subtitle2">{{ item.url }}</div>
</div>
<div class="col-auto">
<!-- <q-badge color="secondary" text-color="white" align="middle" class="q-mb-xs" label="他人の所有" /> -->
<q-chip v-if="!isOwnerFunc(item.owner.id)" square color="secondary" text-color="white" icon="people" label="他人の所有" size="sm" />
<q-chip v-else square color="purple" text-color="white" icon="people" label="自分" size="sm" />
<div class="text-right">
<!-- icon="add_moderator" -->
<!-- <q-chip square color="primary" text-color="white" label="管理者" size="sm" /> -->
</div>
</div>
</div>
</q-card-section>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-grey-7 text-caption text-weight-medium">
アカウント
</div>
<div class="smaller-font-size">{{ item.user }}</div>
</div>
<div class="col-auto">
<div class="text-grey-7 text-caption text-weight-medium">
所有者
</div>
<div class="smaller-font-size">{{ !isOwnerFunc(item.owner.id) ? item.owner.fullName : '自分' }}</div>
</div>
</div>
</q-card-section>
<q-separator v-if="$slots.actions" />
<slot name="actions" :item="item"></slot>
</q-card>
</template>
<script setup lang="ts">
import { defineProps, computed } from 'vue';
import { IDomainOwnerDisplay } from 'src/types/DomainTypes';
import { useAuthStore } from 'stores/useAuthStore';
const props = defineProps<{
item: IDomainOwnerDisplay;
activeId: number;
}>();
const authStore = useAuthStore();
const isOwnerFunc = computed(() => (ownerId: string) => {
return ownerId == authStore.userId;
});
</script>
<style lang="scss" scoped>
.domain-card.default {
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, 0.2),
0 10px 14px 1px rgba(0, 0, 0, 0.14),
0 4px 18px 3px rgba(0, 0, 0, 0.12),
inset 0 0 0px 2px #1976D2;
}
.domain-card {
width: 22rem;
word-break: break-word;
.smaller-font-size {
font-size: 13px;
}
}
</style>

View File

@@ -1,40 +0,0 @@
<template>
<q-btn flat no-caps dense icon="account_circle" :label="userInfo.fullName">
<q-menu class="bar-user-menu">
<div class="row no-wrap q-px-md q-pt-sm ">
<div class="column items-center justify-center">
<q-icon name="account_circle" color="grey" size="3em" />
</div>
<div class="column q-ml-sm overflow-hidden">
<div class="text-subtitle1 ellipsis full-width">{{ userInfo.fullName }}</div>
<div class="text-grey-7 ellipsis text-caption q-mb-sm full-width">{{ userInfo.email }}</div>
</div>
</div>
<div class="row q-pb-sm q-px-md">
<q-chip v-if="authStore.isSuperAdmin" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
<q-chip v-else v-for="(item) in roles" square class="role-label" color="primary" text-color="white" :key="item.id" :label="item.name" size="sm" />
</div>
<div class="row q-pb-sm q-px-md">
<q-btn outline color="negative" icon="logout" label="Logout" @click="authStore.logout()" class="full-width" size="sm" v-close-popup />
</div>
</q-menu>
</q-btn>
</template>
<script setup lang="ts">
import { useAuthStore } from 'stores/useAuthStore';
import { computed } from 'vue';
const authStore = useAuthStore();
const userInfo = computed(() => authStore.userInfo);
const roles = computed(() => authStore.roles);
</script>
<style lang="scss" >
.bar-user-menu {
max-width: 230px !important;
}
.role-label {
margin: 2px;
}
</style>

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: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
{ name: 'firstName', label: '苗字', field: 'firstName', 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.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,105 +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;" @update:selected="emitSelected">
<template v-for="col in columns" :key="col.name" v-slot:[`body-cell-${col.name}`]="props">
<q-td :props="props">
<!-- 使用动态插槽名称 -->
<slot v-if="col.name !== detailField" :name="`body-cell-${col.name}`" :row="props.row" :column="props.col">
<!-- 默认内容 -->
<span>{{ props.row[col.name] }}</span>
</slot>
<q-scroll-area v-else class="description-cell">
<div v-html="props.row[detailField]"></div>
</q-scroll-area>
</q-td>
</template>
</q-table>
</div>
</template>
<script lang="ts">
import { ref, onMounted, reactive, PropType } from 'vue'
interface IRow {
[key: string]: any;
}
export default {
name: 'DetailFieldTable',
props: {
name: String,
type: String,
filter: String,
detailField: {
type: String,
required: true
},
columns: {
type: Array as PropType<any[]>,
required: true
},
fetchData: {
type: Function as PropType<() => Promise<IRow[]>>,
required: true
},
sortBy: {
type: String,
required: false
},
sortDesc: {
type: Boolean,
required: false
}
},
emits: ['update:selected'],
setup(props, { emit }) {
const isLoaded = ref(false);
const rows = reactive<IRow[]>([]);
const selected = ref([]);
onMounted(async () => {
const data = await props.fetchData();
rows.push(...data);
isLoaded.value = true;
});
const emitSelected = (selectedItems: any[]) => {
emit('update:selected', selectedItems);
};
return {
rows,
selected,
isLoaded,
pagination: ref({
sortBy: props.sortBy || undefined,
descending: props.sortDesc || undefined,
rowsPerPage: 10
}),
emitSelected
};
}
};
</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,71 +0,0 @@
<template>
<detail-field-table
detailField="description"
:name="name"
:type="type"
:filter="filter"
:columns="columns"
:fetchData="fetchUsers"
@update:selected="(item) => { selected = item }">
<template v-slot:body-cell-status="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>
</template>
</detail-field-table>
</template>
<script lang="ts">
import { ref, PropType } from 'vue';
import { IUser } from 'src/types/UserTypes';
import { api } from 'boot/axios';
import DetailFieldTable from './DetailFieldTable.vue';
export default {
name: 'UserSelectBox',
components: {
DetailFieldTable
},
props: {
name: String,
type: String,
filter: String,
filterInitRowsFunc: {
type: Function as PropType<(user: IUser) => boolean>,
}
},
setup(props) {
const selected = ref<IUser[]>([]);
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' }
];
const fetchUsers = async () => {
const result = await api.get('api/v1/users');
return result.data.data.map((item: any) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active, roles: item.roles.map(role => role.id) }
}).filter(user => !props.filterInitRowsFunc || props.filterInitRowsFunc(user));
};
return {
columns,
fetchUsers,
selected
};
}
};
</script>

View File

@@ -1,100 +0,0 @@
<template>
<detail-field-table
detailField="comment"
type="single"
:columns="columns"
sortBy="id"
:sortDesc="true"
:fetchData="fetchVersionHistory"
@update:selected="(item) => { selected = item }"
>
<template v-slot:body-cell-id="p">
<div class="flex justify-between">
<span>{{ p.row.id }}</span>
<q-badge v-if="p.row.isActive" color="primary">現在</q-badge>
</div>
</template>
</detail-field-table>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import { IAppDisplay, IAppVersion, IAppVersionDisplay } from 'src/types/AppTypes';
import { date } from 'quasar';
import { api } from 'boot/axios';
import DetailFieldTable from './DetailFieldTable.vue';
import { IUser, IUserDisplay } from 'src/types/UserTypes';
interface IVersionDisplay extends IAppVersionDisplay {
isActive : boolean
}
export default defineComponent({
name: 'VersionHistory',
components: {
DetailFieldTable
},
props: {
app: {
type: Object as PropType<IAppDisplay>,
required: true,
},
},
setup(props, { emit }) {
const selected = ref<IVersionDisplay[]>();
const columns = [
{ name: 'id', label: 'バージョン', field: 'version', align: 'left', sortable: true },
{ name: 'name', label: 'バージョン名', field: 'name', align: 'left', sortable: true },
{ name: 'comment', label: 'コメント', field: 'comment', align: 'left', sortable: true },
// { name: 'creator', label: '作成者', field: (row: IVersionDisplay) => row.creator.fullName, align: 'left', sortable: true },
// { name: 'createTime', label: '作成日時', field: 'createTime', align: 'left', sortable: true },
// { name: 'updater', label: '更新者', field: (row: IVersionDisplay) => row.updater.fullName, align: 'left', sortable: true },
// { name: 'updateTime', label: '更新日時', field: 'updateTime', align: 'left', sortable: true },
];
const formatDate = (dateStr: string) => {
return date.formatDate(dateStr, 'YYYY/MM/DD HH:mm:ss');
};
const toUserDisplay = (user: IUser) => {
return {
id: user.id,
firstName: user.first_name,
lastName: user.last_name,
fullNameSearch: (user.last_name + user.first_name).toLowerCase(),
fullName: user.last_name + ' ' + user.first_name,
email: user.email,
isActive: user.is_active,
isSuperuser: user.is_superuser,
}
}
const fetchVersionHistory = async () => {
const { data } = await api.get(`api/appversions/${props.app.id}`);
return data.data.reduce((arr: IVersionDisplay[], item: any) => {
const val = {
id: item.version,
isActive: item.version === props.app.version,
version: item.version,
appid: item.appid,
name: item.versionname,
comment: item.comment,
// updater: toUserDisplay(item.updateuser),
// updateTime: formatDate(item.updatetime),
// creator: toUserDisplay(item.createuser),
// createTime: formatDate(item.createtime),
} as IVersionDisplay;
arr.push(val);
return arr;
}, []);
}
return {
fetchVersionHistory,
columns,
selected,
};
},
});
</script>

View File

@@ -1,61 +0,0 @@
<template>
<q-input
ref="nameRef"
v-model="versionInfo.name"
filled
autofocus
label="バージョン名"
:rules="[
val => !!val || 'バージョン名を入力してください。',
(val) => !val || val.length <= 80 || '80字以内で入力ください'
]"
/>
<q-input
ref="commentRef"
v-model="versionInfo.comment"
filled
type="textarea"
:rules="[(val) => !val || val.length <= 300 || '300字以内で入力ください']"
label="説明"
/>
</template>
<script setup lang="ts">
import { ref, watch, defineProps, defineEmits } from 'vue';
import { QInput } from 'quasar';
import { IVersionSubmit } from 'src/types/AppTypes';
const nameRef = ref();
const commentRef = ref();
const isValid = () => {
const nameHasError = nameRef.value?.hasError ?? false;
const commentHasError = commentRef.value?.hasError ?? false;
return !nameHasError && !commentHasError;
};
const props = defineProps<{
modelValue: IVersionSubmit;
}>();
const defaultTitle = `${new Date().toLocaleString()}`;
const versionInfo = ref({
...props.modelValue,
name: props.modelValue.name || defaultTitle,
comment: props.modelValue.comment || '',
});
const emit = defineEmits(['update:modelValue']);
defineExpose({
isValid
})
watch(
versionInfo,
() => {
emit('update:modelValue', { ...versionInfo.value });
},
{ immediate: true, deep: true }
);
</script>

View File

@@ -44,7 +44,7 @@ import { useAuthStore } from 'src/stores/useAuthStore';
export default defineComponent({ export default defineComponent({
name: 'AppSelector', name: 'AppSelector',
emits:[ emits:[
'appSelected' "appSelected"
], ],
components:{ components:{
AppSelectBox, AppSelectBox,
@@ -59,7 +59,7 @@ export default defineComponent({
const closeDg=(val :any)=>{ const closeDg=(val :any)=>{
showSelectApp.value=false; showSelectApp.value=false;
console.log('Dialog closed->',val); console.log("Dialog closed->",val);
if (val == 'OK') { if (val == 'OK') {
const data = appDg.value.selected[0]; const data = appDg.value.selected[0];
console.log(data); console.log(data);

View File

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

View File

@@ -92,11 +92,11 @@ export default defineComponent({
}, },
emits: [ emits: [
'addNode', 'addNode',
'nodeSelected', "nodeSelected",
'nodeEdit', "nodeEdit",
'deleteNode', "deleteNode",
'deleteAllNextNodes', "deleteAllNextNodes",
'copyFlow' "copyFlow"
], ],
setup(props, context) { setup(props, context) {
const store = useFlowEditorStore(); const store = useFlowEditorStore();
@@ -204,7 +204,7 @@ export default defineComponent({
* 変数名取得 * 変数名取得
*/ */
const varName =(node:IActionNode)=>{ const varName =(node:IActionNode)=>{
const prop = node.actionProps.find((prop) => prop.props.name === 'verName'); const prop = node.actionProps.find((prop) => prop.props.name === "verName");
return prop?.props.modelValue.name; return prop?.props.modelValue.name;
}; };
const copyFlow=()=>{ const copyFlow=()=>{

View File

@@ -31,11 +31,11 @@
import { ref, defineComponent, computed, PropType } from 'vue'; import { ref, defineComponent, computed, PropType } from 'vue';
import { IActionNode, ActionNode, ActionFlow, RootAction } from '../../types/ActionTypes'; import { IActionNode, ActionNode, ActionFlow, RootAction } from '../../types/ActionTypes';
export enum Direction { export enum Direction {
Default = 'None', Default = "None",
Left = 'LEFT', Left = "LEFT",
Right = 'RIGHT', Right = "RIGHT",
LeftNotNext = 'LEFTNOTNEXT', LeftNotNext = "LEFTNOTNEXT",
RightNotNext = 'RIGHTNOTNEXT', RightNotNext = "RIGHTNOTNEXT",
} }
export default defineComponent({ export default defineComponent({
name: 'NodeLine', name: 'NodeLine',

View File

@@ -1,33 +0,0 @@
<template>
<q-table :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-name="p">
<q-td class="flex justify-between items-center" :props="p">
{{ p.row.name }}
<q-badge v-if="!p.row.domainActive" color="grey">未启用</q-badge>
<q-badge v-if="p.row.id == currendDomainId" color="primary">現在</q-badge>
</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="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>
</template>

View File

@@ -55,7 +55,7 @@ export default defineComponent({
}); });
const connectProps=(props:IProp)=>{ const connectProps=(props:IProp)=>{
const connProps:any={}; const connProps:any={};
if(props && 'connectProps' in props && props.connectProps!=undefined){ if(props && "connectProps" in props && props.connectProps!=undefined){
for(let connProp of props.connectProps){ for(let connProp of props.connectProps){
let targetProp = componentData.value.find((prop)=>prop.props.name===connProp.propName); let targetProp = componentData.value.find((prop)=>prop.props.name===connProp.propName);
if(targetProp){ if(targetProp){

View File

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

View File

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

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,6 +1,6 @@
<template> <template>
<div class="" v-bind="$attrs"> <div class="" v-bind="$attrs">
<q-field v-model="color" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label :bottom-slots="!isSelected" :rules="rulesExp"> <q-field v-model="color" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label :bottom-slots="!isSelected" >
<template v-slot:control> <template v-slot:control>
<q-chip text-color="black" color="white" v-if="isSelected"> <q-chip text-color="black" color="white" v-if="isSelected">
<div class="row"> <div class="row">
@@ -57,34 +57,17 @@ export default defineComponent({
type: String, type: String,
default: null default: null
}, },
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
}
}, },
setup(props, { emit }) { setup(props, { emit }) {
const color = ref(props.modelValue??''); const color = ref(props.modelValue??"");
const isSelected = computed(()=>props.modelValue && props.modelValue!==''); const isSelected = computed(()=>props.modelValue && props.modelValue!=="");
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg ),'anyColor']:[];
const rulesExp=[...requiredExp,...customExp];
watchEffect(()=>{ watchEffect(()=>{
emit('update:modelValue', color.value); emit('update:modelValue', color.value);
}); });
return { return {
color, color,
isSelected, isSelected
rulesExp
}; };
} }
}); });

View File

@@ -18,11 +18,39 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ConditionNode, ConditionTree, Operator, OperatorListItem } from 'app/src/types/Conditions'; import { ConditionNode, ConditionTree, Operator, OperatorListItem } from 'app/src/types/Conditions';
import { computed, defineComponent, provide, reactive, ref, watchEffect } from 'vue'; import { computed, defineComponent, provide, reactive, ref, watchEffect } from 'vue';
import ConditionEditor from '../ConditionEditor/ConditionEditor.vue'; import ConditionEditor from '../ConditionEditor/ConditionEditor.vue';
import { IActionProperty } from 'src/types/ActionTypes';
type Props = {
props?: {
name: string;
modelValue?: {
app: {
id: string;
name: string;
},
fields: {
type: string;
label: string;
code: string;
}[]
}
}
};
type InputConfg = {
canInput: boolean;
buttonsConfig: {
label: string;
color: string;
type: string;
}[]
};
export default defineComponent({ export default defineComponent({
name: 'FieldInput', name: 'FieldInput',
@@ -32,7 +60,7 @@ export default defineComponent({
}, },
props: { props: {
context: { context: {
type: Array<IActionProperty>, type: Array<Props>,
default: '', default: '',
}, },
displayName: { displayName: {
@@ -59,10 +87,6 @@ export default defineComponent({
type: String, type: String,
default: 'field' default: 'field'
}, },
connectProps:{
type:Object,
default:()=>({})
},
onlySourceSelect: { onlySourceSelect: {
type: Boolean, type: Boolean,
default: false default: false
@@ -91,10 +115,7 @@ export default defineComponent({
}, },
setup(props, { emit }) { setup(props, { emit }) {
let source = reactive(props.connectProps['source']); const source = props.context.find(element => element?.props?.name === 'sources')
if(!source){
source = props.context.find(element => element.props.name === 'sources');
}
if (source) { if (source) {
if (props.sourceType === 'field') { if (props.sourceType === 'field') {
@@ -136,8 +157,7 @@ export default defineComponent({
const isSetted = ref(props.modelValue && props.modelValue !== ''); const isSetted = ref(props.modelValue && props.modelValue !== '');
const conditionString = computed(() => { const conditionString = computed(() => {
const condiStr= tree.buildConditionString(tree.root); return tree.buildConditionString(tree.root);
return condiStr==='()'?'(条件なし)':condiStr;
}); });
const showDg = () => { const showDg = () => {

View File

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

View File

@@ -1,11 +1,6 @@
<template> <template>
<div> <div>
<q-field :label="displayName" labelColor="primary" stack-label <q-field :label="displayName" labelColor="primary" stack-label>
v-model="processingProps"
:rules="rulesExp"
lazy-rules="ondemand"
ref="fieldRef"
>
<template v-slot:control> <template v-slot:control>
<q-card flat class="full-width"> <q-card flat class="full-width">
<q-card-actions vertical> <q-card-actions vertical>
@@ -45,7 +40,7 @@
<div class="col-5"> <div class="col-5">
<ConditionObject v-model="item.field" /> <ConditionObject v-model="item.field" />
</div> </div>
<div class="col-2 q-pa-sm"> <div class="col-2">
<q-select v-model="item.logicalOperator" :options="logicalOperators" outlined dense></q-select> <q-select v-model="item.logicalOperator" :options="logicalOperators" outlined dense></q-select>
</div> </div>
<div class="col-4"> <div class="col-4">
@@ -83,8 +78,14 @@ type Props = {
type ProcessingObjectType = { type ProcessingObjectType = {
field?: { field?: {
sharedText: string; name: string | {
objectType: 'field'; name: string;
};
objectType: string;
type: string;
code: string;
label: string;
noLabel: boolean;
}; };
logicalOperator?: string; logicalOperator?: string;
vName?: string; vName?: string;
@@ -125,19 +126,9 @@ export default defineComponent({
type: String, type: String,
default: '', default: '',
}, },
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
}
}, },
setup(props, { emit }) { setup(props, { emit }) {
const fieldRef=ref();
const source = props.context.find(element => element?.props?.name === 'sources') const source = props.context.find(element => element?.props?.name === 'sources')
if (source) { if (source) {
@@ -154,104 +145,65 @@ export default defineComponent({
const actionName = props.context.find(element => element?.props?.name === 'displayName') const actionName = props.context.find(element => element?.props?.name === 'displayName')
const processingProps: ValueType = props.modelValue && props.modelValue.vars const processingProps: ValueType = props.modelValue && props.modelValue.vars
? reactive(props.modelValue) ? props.modelValue
: reactive({ : reactive({
name: '', name: '',
actionName: actionName?.props?.modelValue as string, actionName: actionName?.props?.modelValue as string,
displayName: '結果(戻り値)', displayName: '結果(戻り値)',
vars: [ vars: [{ id: uuidv4() }]
{
id: uuidv4(),
field:{
objectType:'field',
sharedText:''
}
}]
}); });
const closeDg = () => { const closeDg = () => {
fieldRef.value.validate(); emit('update:modelValue', processingProps
emit('update:modelValue', processingProps); );
} }
const processingObjects = processingProps.vars; const processingObjects = processingProps.vars;
const deleteProcessingObject = (index: number) => { const deleteProcessingObject = (index: number) => processingObjects.length === 1
if(processingObjects.length >0){ ? processingObjects.splice(0, processingObjects.length, { id: uuidv4() })
processingObjects.splice(index, 1); : processingObjects.splice(index, 1);
}
if(processingObjects.length===0){
addProcessingObject();
}
}
const processingObjectsInputDisplay = computed(() => const processingObjectsInputDisplay = computed(() =>
processingObjects ? processingObjects ?
processingObjects processingObjects
.filter(item => item.field && item.logicalOperator && item.vName) .filter(item => item.field && item.logicalOperator && item.vName)
.map(item => { .map(item => {
return`var(${processingProps.name}.${item.vName}) = ${item.field?.sharedText}` return`var(${processingProps.name}.${item.vName}) = ${item.field.sharedText}`
}) })
: [] : []
); );
const addProcessingObject=()=>{
processingObjects.push({
id: uuidv4(),
field:{
objectType:'field',
sharedText:''
}
});
}
//集計処理方法 //集計処理方法
const logicalOperators = ref([ const logicalOperators = ref([
{ {
'operator': '', "operator": "",
'label': 'なし' "label": "なし"
}, },
{ {
'operator': 'SUM', "operator": "SUM",
'label': '合計' "label": "合計"
}, },
{ {
'operator': 'AVG', "operator": "AVG",
'label': '平均' "label": "平均"
}, },
{ {
'operator': 'MAX', "operator": "MAX",
'label': '最大値' "label": "最大値"
}, },
{ {
'operator': 'MIN', "operator": "MIN",
'label': '最小値' "label": "最小値"
}, },
{ {
'operator': 'COUNT', "operator": "COUNT",
'label': 'カウント' "label": "カウント"
}, },
{ {
'operator': 'FIRST', "operator": "FIRST",
'label': '最初の値' "label": "最初の値"
} }
]); ]);
const checkInput=(val:ValueType)=>{
if(!val){
return false;
}
if(!val.name){
return '集計結果の変数名を入力してください';
}
if(!val.vars || val.vars.length==0){
return '集計処理を設定してください';
}
if(val.vars.some((x)=>!x.vName)){
return '集計結果変数名を入力してください';
}
return true;
}
const customExp = props.rules === undefined ? [] : eval(props.rules);
const requiredExp = props.required ? [(val: any) => checkInput(val)] : [];
const rulesExp = [...requiredExp, ...customExp];
watchEffect(() => { watchEffect(() => {
emit('update:modelValue', processingProps); emit('update:modelValue', processingProps);
@@ -262,12 +214,10 @@ export default defineComponent({
closeDg, closeDg,
processingObjects, processingObjects,
processingProps, processingProps,
addProcessingObject, addProcessingObject: () => processingObjects.push({ id: uuidv4() }),
deleteProcessingObject, deleteProcessingObject,
logicalOperators, logicalOperators,
processingObjectsInputDisplay, processingObjectsInputDisplay,
rulesExp,
fieldRef
}; };
}, },
}); });

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
<template> <template>
<div v-bind="$attrs"> <div v-bind="$attrs">
<q-field v-model="selectedField" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label <q-field v-model="selectedField" :label="displayName" labelColor="primary" :clearable="isSelected" stack-label
:bottom-slots="!isSelected" :bottom-slots="!isSelected">
:rules="rulesExp"
>
<template v-slot:control> <template v-slot:control>
<q-chip color="primary" text-color="white" v-if="isSelected"> <q-chip color="primary" text-color="white" v-if="isSelected">
{{ selectedField.name }} {{ selectedField.name }}
@@ -17,15 +15,8 @@
<q-icon name="search" class="cursor-pointer" color="primary" @click="showDg" /> <q-icon name="search" class="cursor-pointer" color="primary" @click="showDg" />
</template> </template>
</q-field> </q-field>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg" min-width="400px"> <show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg" widht="400px">
<template v-slot:toolbar> <field-select ref="appDg" name="フィールド" :type="selectType" :appId="store.appInfo?.appId" :fieldTypes="fieldTypes"></field-select>
<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> </show-dialog>
</div> </div>
</template> </template>
@@ -76,35 +67,16 @@ export default defineComponent({
type: Object, type: Object,
default: null default: null
}, },
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required: {
type: Boolean,
default: false
},
requiredMessage: {
type: String,
default: ''
}
}, },
setup(props, { emit }) { setup(props, { emit }) {
const appDg = ref(); const appDg = ref();
const show = ref(false); const show = ref(false);
const selectedField = ref(props.modelValue); const selectedField = ref(props.modelValue);
const selectedFields =computed(()=>!selectedField.value?[]: [selectedField.value]);
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const isSelected = computed(() => { const isSelected = computed(() => {
return selectedField.value !== null && typeof selectedField.value === 'object' && ('name' in selectedField.value) return selectedField.value !== null && typeof selectedField.value === 'object' && ('name' in selectedField.value)
}); });
//ルール設定
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required ? [((val: any) => (!!val && typeof val==='object' && !!val.name) || errmsg)] : [];
const rulesExp = [...requiredExp, ...customExp];
const showDg = () => { const showDg = () => {
show.value = true; show.value = true;
@@ -127,10 +99,7 @@ export default defineComponent({
showDg, showDg,
closeDg, closeDg,
selectedField, selectedField,
isSelected, isSelected
filter:ref(''),
selectedFields,
rulesExp
}; };
} }
}); });

View File

@@ -14,6 +14,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { kMaxLength } from 'buffer';
import { defineComponent, ref, watchEffect, computed } from 'vue'; import { defineComponent, ref, watchEffect, computed } from 'vue';
export default defineComponent({ export default defineComponent({
@@ -49,16 +50,8 @@ export default defineComponent({
type: String, type: String,
default: undefined default: undefined
}, },
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
modelValue: { modelValue: {
type: null as any, // type: Any,
default: '', default: '',
}, },
}, },
@@ -82,11 +75,8 @@ export default defineComponent({
}, },
}); });
// const inputValue = ref(props.modelValue); // const inputValue = ref(props.modelValue);
const rulesExp = props.rules === undefined ? null : eval(props.rules);
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[];
const rulesExp=[...requiredExp,...customExp];
// const finalValue = computed(() => { // const finalValue = computed(() => {
// return props.name !== 'verName' ? inputValue.value : { // return props.name !== 'verName' ? inputValue.value : {
// name: inputValue.value, // name: inputValue.value,

View File

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

View File

@@ -49,14 +49,6 @@ export default defineComponent({
type:String, type:String,
default:undefined default:undefined
}, },
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
modelValue: { modelValue: {
type: [Number , String], type: [Number , String],
default: undefined default: undefined
@@ -65,13 +57,26 @@ export default defineComponent({
setup(props, { emit }) { setup(props, { emit }) {
const numValue = ref(props.modelValue); const numValue = ref(props.modelValue);
const customExp = props.rules === undefined ? [] : eval(props.rules); const rulesExp = props.rules===undefined?null : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`; const isError = computed(()=>{
const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[]; const val = numValue.value;
const rulesExp=[...requiredExp,...customExp]; if (val === undefined) {
return false;
}
const numVal = typeof val === "string" ? parseInt(val) : val;
// Ensure parsed value is a valid number
if (isNaN(numVal)) {
return true;
}
// Check against min and max boundaries, if defined
if ((props.min !== undefined && numVal < props.min) || (props.max !== undefined && numVal > props.max)) {
return true;
}
return false;
});
watchEffect(()=>{ watchEffect(()=>{
emit('update:modelValue',numValue.value); emit("update:modelValue",numValue.value);
}); });
return { return {
numValue, numValue,

View File

@@ -24,7 +24,6 @@ import NumInput from './NumInput.vue';
import DataProcessing from './DataProcessing.vue'; import DataProcessing from './DataProcessing.vue';
import DataMapping from './DataMapping.vue'; import DataMapping from './DataMapping.vue';
import AppSelect from './AppSelect.vue'; import AppSelect from './AppSelect.vue';
import CascadingDropDown from './CascadingDropDown.vue';
import { IActionNode,IActionProperty,IProp } from 'src/types/ActionTypes'; import { IActionNode,IActionProperty,IProp } from 'src/types/ActionTypes';
export default defineComponent({ export default defineComponent({
@@ -42,8 +41,7 @@ export default defineComponent({
NumInput, NumInput,
DataProcessing, DataProcessing,
DataMapping, DataMapping,
AppSelect, AppSelect
CascadingDropDown
}, },
props: { props: {
nodeProps: { nodeProps: {
@@ -59,7 +57,7 @@ export default defineComponent({
const properties=ref(props.nodeProps); const properties=ref(props.nodeProps);
const connectProps=(props:IProp)=>{ const connectProps=(props:IProp)=>{
const connProps:any={context:properties}; const connProps:any={context:properties};
if(props && 'connectProps' in props && props.connectProps!=undefined){ if(props && "connectProps" in props && props.connectProps!=undefined){
for(let connProp of props.connectProps){ for(let connProp of props.connectProps){
let targetProp = properties.value.find((prop)=>prop.props.name===connProp.propName); let targetProp = properties.value.find((prop)=>prop.props.name===connProp.propName);
if(targetProp){ if(targetProp){

View File

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

View File

@@ -1,9 +1,6 @@
<template> <template>
<div v-bind="$attrs"> <div v-bind="$attrs">
<q-select v-model="selectedValue" :use-chips="multiple" :label="displayName" label-color="primary" <q-select v-model="selectedValue" :use-chips="multiple" :label="displayName" label-color="primary" :options="options" stack-label
:options="options"
stack-label
:rules="rulesExp"
:multiple="multiple"/> :multiple="multiple"/>
</div> </div>
</template> </template>
@@ -35,19 +32,6 @@ export default defineComponent({
type: [Array,String], type: [Array,String],
default: null, default: null,
}, },
//例:[val=>!!val ||'入力してください']
rules: {
type: String,
default: undefined
},
required:{
type:Boolean,
default:false
},
requiredMessage: {
type: String,
default: ''
},
}, },
setup(props, { emit }) { setup(props, { emit }) {
const selectedValue = ref(props.modelValue); const selectedValue = ref(props.modelValue);
@@ -57,14 +41,10 @@ export default defineComponent({
watchEffect(() => { watchEffect(() => {
emit('update:modelValue', selectedValue.value); emit('update:modelValue', selectedValue.value);
}); });
const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}を選択してください。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg )]:[];
const rulesExp=[...requiredExp,...customExp];
return { return {
selectedValue, selectedValue,
multiple, multiple
rulesExp
}; };
}, },
}); });

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