Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3
This commit is contained in:
@@ -15,6 +15,7 @@ from app.db.cruddb import domainService,appService
|
||||
|
||||
import httpx
|
||||
import app.core.config as config
|
||||
from app.core import domainCacheService
|
||||
|
||||
platform_router = r = APIRouter()
|
||||
|
||||
@@ -82,10 +83,10 @@ async def apps_update(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.get_default_domain(db, user.id)
|
||||
if not domain:
|
||||
domainurl = domainCacheService.get_default_domainurl(db,user.id)
|
||||
if not domainurl:
|
||||
return ApiReturnModel(data = None)
|
||||
return ApiReturnModel(data =appService.update_appversion(db, domain.url,app,user.id))
|
||||
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)
|
||||
|
||||
@@ -100,10 +101,10 @@ async def apps_delete(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id)
|
||||
if not domain:
|
||||
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, domain.url,appid))
|
||||
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)
|
||||
|
||||
@@ -119,10 +120,10 @@ async def appversions_list(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id)
|
||||
if not domain:
|
||||
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,domain.url,appid)
|
||||
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)
|
||||
|
||||
@@ -139,10 +140,10 @@ async def appversions_change(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id)
|
||||
if not domain:
|
||||
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, domain.url,appid,version,user.id))
|
||||
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)
|
||||
|
||||
@@ -237,7 +238,7 @@ async def action_data(
|
||||
|
||||
@r.get(
|
||||
"/flow/{appid}",tags=["App"],
|
||||
response_model=ApiReturnModel[List[Flow|None]],
|
||||
response_model=ApiReturnModel[Flow|None],
|
||||
response_model_exclude_none=True,
|
||||
)
|
||||
async def flow_details(
|
||||
@@ -247,10 +248,10 @@ async def flow_details(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.get_default_domain(db, user.id)
|
||||
if not domain:
|
||||
domainurl = domainCacheService.get_default_domainurl(db,user.id)
|
||||
if not domainurl:
|
||||
return ApiReturnModel(data = None)
|
||||
return ApiReturnModel(data = appService.get_flow(db, domain.url, appid,user.id))
|
||||
return ApiReturnModel(data = appService.get_flow(db, domainurl, appid,user.id))
|
||||
except Exception as e:
|
||||
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e)
|
||||
|
||||
@@ -266,11 +267,10 @@ async def flow_list(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id)
|
||||
if not domain:
|
||||
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
|
||||
if not domainurl:
|
||||
return []
|
||||
print("domain=>",domain)
|
||||
flows = get_flows_by_app(db, domain.url, appid)
|
||||
flows = get_flows_by_app(db, domainurl, appid)
|
||||
return flows
|
||||
except Exception as e:
|
||||
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
|
||||
@@ -286,10 +286,10 @@ async def flow_create(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id)
|
||||
if not domain:
|
||||
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
|
||||
if not domainurl:
|
||||
return ApiReturnModel(data = None)
|
||||
return ApiReturnModel(data = appService.create_flow(db, domain.url, flow,user.id))
|
||||
return ApiReturnModel(data = appService.create_flow(db, domainurl, flow,user.id))
|
||||
except Exception as e:
|
||||
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
|
||||
|
||||
@@ -306,10 +306,10 @@ async def flow_edit(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.get_default_domain(db, user.id)
|
||||
if not domain:
|
||||
domainurl = domainCacheService.get_default_domainurl(db,user.id)
|
||||
if not domainurl:
|
||||
return ApiReturnModel(data = None)
|
||||
return ApiReturnModel(data = appService.edit_flow(db,domain.url, flow,user.id))
|
||||
return ApiReturnModel(data = appService.edit_flow(db,domainurl, flow,user.id))
|
||||
except Exception as e:
|
||||
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
|
||||
|
||||
@@ -326,8 +326,8 @@ async def flow_delete(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.get_default_domain(db, user.id)
|
||||
if not domain:
|
||||
domainurl = domainCacheService.get_default_domainurl(db,user.id)
|
||||
if not domainurl:
|
||||
return ApiReturnModel(data = None)
|
||||
return ApiReturnModel(data = appService.delete_flow(db, flowid))
|
||||
except Exception as e:
|
||||
@@ -395,7 +395,10 @@ async def domain_edit(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
return ApiReturnModel(data = domainService.edit_domain(db, domain,user.id))
|
||||
domain = domainService.edit_domain(db, domain,user.id)
|
||||
if domain :
|
||||
domainCacheService.clear_default_domainurl()
|
||||
return ApiReturnModel(data = domain)
|
||||
except Exception as e:
|
||||
raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e)
|
||||
|
||||
@@ -482,7 +485,7 @@ async def get_defaultuserdomain(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
return ApiReturnModel(data =domainService.get_default_domain(db, user.id))
|
||||
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)
|
||||
|
||||
@@ -498,7 +501,7 @@ async def set_defualtuserdomain(
|
||||
db=Depends(get_db),
|
||||
):
|
||||
try:
|
||||
domain = domainService.set_default_domain(db,user.id,domainid)
|
||||
domain = domainCacheService.set_default_domain(db,user.id,domainid)
|
||||
return ApiReturnModel(data= domain)
|
||||
except Exception as e:
|
||||
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while update user({user.id}) defaultdomain:",e)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from app.core.cache import domainCacheService
|
||||
55
backend/app/core/cache.py
Normal file
55
backend/app/core/cache.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import time
|
||||
from typing import Any
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.cruddb import domainService
|
||||
|
||||
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()
|
||||
@@ -47,7 +47,7 @@ class dbflow(crudbase):
|
||||
updateuserid = userid
|
||||
)
|
||||
db.add(db_flow)
|
||||
db_app = db.execute(select(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid))).scalars().first()
|
||||
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,
|
||||
|
||||
@@ -57,8 +57,9 @@ def test_delete_domain(test_client, login_user):
|
||||
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})
|
||||
assert response.status_code == 200
|
||||
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
|
||||
@@ -99,8 +100,9 @@ def test_edit_domain(test_client, test_domain, login_user):
|
||||
"is_active": True
|
||||
}
|
||||
response = test_client.put("/api/domain", json=update_domain,headers={"Authorization": "Bearer " + login_user})
|
||||
assert response.status_code == 200
|
||||
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"]
|
||||
|
||||
@@ -37,8 +37,9 @@ def test_edit_flow(test_client,test_domain,test_app_id,login_user):
|
||||
"content": ""
|
||||
}
|
||||
response = test_client.put("/api/flow", json=test_flow,headers={"Authorization": "Bearer " + login_user})
|
||||
assert response.status_code == 200
|
||||
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
|
||||
|
||||
@@ -8,65 +8,23 @@
|
||||
size="md"
|
||||
:label="userStore.currentDomain.domainName"
|
||||
:disable-dropdown="true"
|
||||
dropdown-icon='none'
|
||||
dropdown-icon="none"
|
||||
:disable="true"
|
||||
>
|
||||
<q-list>
|
||||
<q-item :active="isCurrentDomain(domain)" active-class="active-domain-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="isCurrentDomain(domain) ? 'orange': ''" text-color="white"></q-icon>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{domain.domainName}}</q-item-label>
|
||||
<q-item-label caption>{{domain.kintoneUrl}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
|
||||
</template>
|
||||
<script setup lang="ts" >
|
||||
import { IDomainInfo } from 'src/types/DomainTypes';
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from 'stores/useAuthStore';
|
||||
import { useDomainStore } from 'src/stores/useDomainStore';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const userStore = useAuthStore();
|
||||
const domainStore = useDomainStore();
|
||||
const route = useRoute()
|
||||
const domains = computed(() => domainStore.userDomains);
|
||||
|
||||
(async ()=>{
|
||||
await domainStore.loadUserDomains();
|
||||
})();
|
||||
|
||||
const isUnclickable = computed(()=>{
|
||||
return route.path.startsWith('/FlowChart/') || domains.value === undefined || domains.value.length === 0;
|
||||
});
|
||||
|
||||
const isCurrentDomain=(domain:IDomainInfo)=>{
|
||||
return domain.id === userStore.currentDomain.id;
|
||||
}
|
||||
|
||||
const onItemClick=(domain:IDomainInfo)=>{
|
||||
console.log(domain);
|
||||
userStore.setCurrentDomain(domain);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.q-btn.disabled.customized-disabled-btn {
|
||||
opacity: 1 !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.q-item.active-domain-item {
|
||||
color: inherit;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.q-btn.disabled.customized-disabled-btn * {
|
||||
cursor: default !important;
|
||||
* {
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<q-btn flat padding="xs" round size="1em" icon="more_vert" class="action-menu">
|
||||
<q-menu>
|
||||
<q-list dense :style="'min-width:' + minWidth ">
|
||||
<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)" :class="item.class" clickable v-close-popup @click="item.action(row)">
|
||||
<q-item-section side style="color: inherit;">
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { PropType, Ref } from 'vue';
|
||||
import { IDomainOwnerDisplay } from '../types/DomainTypes';
|
||||
|
||||
interface Action {
|
||||
@@ -38,6 +38,10 @@ export default {
|
||||
type: Object as PropType<IDomainOwnerDisplay>,
|
||||
required: true
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: '150px'
|
||||
},
|
||||
minWidth: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
@@ -56,10 +60,10 @@ export default {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.q-table tr > td:last-child .action-menu {
|
||||
opacity: 0.25;
|
||||
opacity: 0.25 !important;
|
||||
}
|
||||
|
||||
.q-table tr:hover > td:last-child .action-menu {
|
||||
opacity: 1;
|
||||
.q-table tr:hover > td:last-child .action-menu:not([disabled]) {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
filled
|
||||
autofocus
|
||||
label="バージョン名"
|
||||
:rules="[(val) => !val || val.length <= 30 || '30字以内で入力ください']"
|
||||
:rules="[(val) => !val || val.length <= 80 || '80字以内で入力ください']"
|
||||
/>
|
||||
<q-input
|
||||
v-model="versionInfo.comment"
|
||||
filled
|
||||
type="textarea"
|
||||
:rules="[(val) => !val || val.length <= 80 || '80字以内で入力ください']"
|
||||
:rules="[(val) => !val || val.length <= 300 || '300字以内で入力ください']"
|
||||
label="説明"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -40,8 +40,7 @@ import { IActionNode, IActionProperty } from 'src/types/ActionTypes';
|
||||
},
|
||||
props: {
|
||||
actionNode:{
|
||||
type:Object as PropType<IActionNode>,
|
||||
required:true
|
||||
type:Object as PropType<IActionNode>
|
||||
},
|
||||
drawerRight:{
|
||||
type:Boolean,
|
||||
@@ -55,7 +54,7 @@ import { IActionNode, IActionProperty } from 'src/types/ActionTypes';
|
||||
setup(props,{emit}) {
|
||||
const showPanel =ref(props.drawerRight);
|
||||
|
||||
const cloneProps = (actionProps:IActionProperty[]):IActionProperty[]|null=>{
|
||||
const cloneProps = (actionProps:IActionProperty[]|undefined):IActionProperty[]|null=>{
|
||||
if(!actionProps){
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<q-breadcrumbs-el icon="widgets" label="アプリ管理" />
|
||||
</q-breadcrumbs>
|
||||
</div>
|
||||
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
|
||||
<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="showAddAppDialog" />
|
||||
@@ -46,12 +46,6 @@
|
||||
<app-select-box ref="appDialog" name="アプリ" type="single" :filter="dgFilter" :filterInitRowsFunc="filterInitRows" />
|
||||
</show-dialog>
|
||||
|
||||
|
||||
<show-dialog v-model:visible="showVersionHistory" :name="targetRow?.name + 'のバージョン履歴'" @close="closeHistoryDg" min-width="30vw" :ok-btn-auto-close="false" :ok-btn-loading="isAdding"
|
||||
ok-btn-label="選択">
|
||||
<version-history ref="versionDialog" :app="targetRow" />
|
||||
</show-dialog>
|
||||
|
||||
<q-dialog v-model="deleteDialog" persistent>
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
@@ -69,20 +63,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, reactive } from 'vue';
|
||||
import { useQuasar } from 'quasar'
|
||||
import { api } from 'boot/axios';
|
||||
import { useAuthStore } from 'stores/useAuthStore';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useAppStore } from 'stores/useAppStore';
|
||||
import { useFlowEditorStore } from 'stores/flowEditor';
|
||||
import { router } from 'src/router';
|
||||
import { date } from 'quasar'
|
||||
import { IManagedApp, IAppDisplay, IAppVersion } from 'src/types/AppTypes';
|
||||
import { IAppDisplay } from 'src/types/AppTypes';
|
||||
import ShowDialog from 'src/components/ShowDialog.vue';
|
||||
import AppSelectBox from 'src/components/AppSelectBox.vue';
|
||||
import TableActionMenu from 'components/TableActionMenu.vue';
|
||||
import VersionHistory from 'components/dialog/VersionHistory.vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const appStore = useAppStore();
|
||||
const numberStringSorting = (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10);
|
||||
|
||||
const columns = [
|
||||
@@ -99,57 +89,35 @@ const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
|
||||
const loading = ref(false);
|
||||
const filter = ref('');
|
||||
const dgFilter = ref('');
|
||||
const rows = ref<IAppDisplay[]>([]);
|
||||
const rows = computed(() => appStore.apps);
|
||||
const targetRow = ref<IAppDisplay>();
|
||||
const rowIds = new Set<string>();
|
||||
|
||||
const $q = useQuasar()
|
||||
const store = useFlowEditorStore();
|
||||
const appDialog = ref();
|
||||
const versionDialog = ref();
|
||||
const showSelectApp=ref(false);
|
||||
const showVersionHistory=ref(false);
|
||||
const isAdding = ref(false);
|
||||
const deleteDialog = ref(false);
|
||||
const deleteUserLoading = ref(false);
|
||||
|
||||
const actionList = [
|
||||
{ label: 'フローの編集', icon: 'account_tree', action: toEditFlowPage },
|
||||
{ label: 'バージョンの切替', icon: 'history', action: showHistory },
|
||||
{ label: 'バージョンの切替', icon: 'history', action: toVersionHistoryPage },
|
||||
{ separator: true },
|
||||
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
|
||||
];
|
||||
|
||||
const getApps = async () => {
|
||||
loading.value = true;
|
||||
rowIds.clear();
|
||||
try {
|
||||
const { data } = await api.get('api/apps');
|
||||
rows.value = data.data.map((item: IManagedApp) => {
|
||||
rowIds.add(item.appid);
|
||||
return appToAppDisplay(item)
|
||||
}).sort((a: IAppDisplay, b: IAppDisplay) => a.sortId - b.sortId); // set default order
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
icon: 'error',
|
||||
color: 'negative',
|
||||
message: 'アプリ一覧の読み込みに失敗しました'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
await appStore.loadApps();
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getApps();
|
||||
});
|
||||
|
||||
watch(() => authStore.currentDomain.id, async () => {
|
||||
await getApps();
|
||||
});
|
||||
|
||||
const filterInitRows = (row: {id: string}) => {
|
||||
return !rowIds.has(row.id);
|
||||
return !appStore.rowIds.has(row.id);
|
||||
}
|
||||
|
||||
const showAddAppDialog = () => {
|
||||
@@ -173,59 +141,17 @@ function removeRow(app:IAppDisplay) {
|
||||
}
|
||||
|
||||
const deleteApp = async () => {
|
||||
deleteUserLoading.value = true;
|
||||
try {
|
||||
await api.delete(`api/apps/${targetRow.value?.id}`);
|
||||
if (targetRow.value?.id) {
|
||||
deleteUserLoading.value = true;
|
||||
await appStore.deleteApp(targetRow.value)
|
||||
await getApps();
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
icon: 'error',
|
||||
color: 'negative',
|
||||
message: 'アプリの削除に失敗しました'
|
||||
});
|
||||
} finally {
|
||||
deleteUserLoading.value = false;
|
||||
deleteDialog.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showHistory(app:IAppDisplay) {
|
||||
targetRow.value = app;
|
||||
showVersionHistory.value = true;
|
||||
dgFilter.value = ''
|
||||
}
|
||||
|
||||
const closeHistoryDg = async (val: 'OK'|'Cancel') => {
|
||||
showVersionHistory.value = true;
|
||||
if (val == 'OK' && versionDialog.value.selected[0]) {
|
||||
isAdding.value = true;
|
||||
await api.put(`api/appversions/${targetRow.value?.id}/${versionDialog.value.selected[0].id}`)
|
||||
await getApps();
|
||||
}
|
||||
showVersionHistory.value = false;
|
||||
isAdding.value = false;
|
||||
}
|
||||
|
||||
const appToAppDisplay = (app: IManagedApp) => {
|
||||
const user = app.updateuser;
|
||||
return {
|
||||
id: app.appid,
|
||||
sortId: parseInt(app.appid, 10),
|
||||
name: app.appname,
|
||||
url: `${app.domainurl}/k/${app.appid}`,
|
||||
version: app.version,
|
||||
updateTime:date.formatDate(app.update_time, 'YYYY/MM/DD HH:mm'),
|
||||
updateUser: {
|
||||
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,
|
||||
isSuperuser: user.is_superuser,
|
||||
isActive: user.is_active,
|
||||
}
|
||||
} as IAppDisplay
|
||||
async function toVersionHistoryPage(app:IAppDisplay) {
|
||||
await router.push('/app/version/' + app.id);
|
||||
}
|
||||
|
||||
async function toEditFlowPage(app:IAppDisplay) {
|
||||
|
||||
227
frontend/src/pages/AppVersionManagement.vue
Normal file
227
frontend/src/pages/AppVersionManagement.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-sm row items-start">
|
||||
<q-breadcrumbs>
|
||||
<q-breadcrumbs-el icon="widgets" label="アプリ管理" to="/app" />
|
||||
<q-breadcrumbs-el :label="app?.name" />
|
||||
</q-breadcrumbs>
|
||||
</div>
|
||||
|
||||
<q-table card-class="bg-grey-1" :rows="[app]" :columns="appColumns" class="q-mt-sm q-mb-lg" :loading="appLoading" hide-bottom>
|
||||
<template v-slot:body-cell-name="prop">
|
||||
<q-td :props="prop">
|
||||
<q-btn flat dense :label="prop.row.name" @click="toEditFlowPage(prop.row)" ></q-btn>
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-url="prop">
|
||||
<q-td :props="prop">
|
||||
<a :href="prop.row.url" target="_blank" :title="prop.row.name" >
|
||||
{{ prop.row.url }}
|
||||
</a>
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-actions="p">
|
||||
<q-td :props="p">
|
||||
<table-action-menu :row="p.row" :actions="appActionList" />
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-table class="q-ma-md" flat bordered :rows="rows" title="バージョン履歴" :columns="columns" :loading="versionLoading" :pagination="pagination" :filter="filter" >
|
||||
<template v-slot:top-right>
|
||||
<q-input borderless dense filled clearable debounce="300" v-model="filter" placeholder="検索">
|
||||
<template v-slot:append>
|
||||
<q-icon name="search"/>
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
<template v-slot:body-cell-id="p">
|
||||
<q-td :props="p">
|
||||
<div class="flex justify-between">
|
||||
<span>{{ p.row.id }}</span>
|
||||
<q-badge v-if="p.row.id === app.version" color="primary">現在</q-badge>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-comment="p">
|
||||
<q-td :props="p">
|
||||
<q-scroll-area class="description-cell">
|
||||
<div v-html="p.row['comment']"></div>
|
||||
</q-scroll-area>
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-actions="p">
|
||||
<q-td :props="p">
|
||||
<span v-if="p.row.id === app.version"></span>
|
||||
<table-action-menu v-else :row="p.row" minWidth="140px" :actions="actionList" />
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<q-dialog v-model="confirmDialog" persistent>
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
<q-icon name="warning" color="warning" size="2em" />
|
||||
<span class="q-ml-sm">削除してもよろしいですか?</span>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Cancel" color="primary" v-close-popup />
|
||||
<q-btn flat label="OK" color="primary" :loading="deleteUserLoading" @click="deleteApp" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from 'quasar';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useAppStore } from 'stores/useAppStore';
|
||||
import { useFlowEditorStore } from 'stores/flowEditor';
|
||||
import { router } from 'src/router';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { IAppDisplay, IAppVersionDisplay } from 'src/types/AppTypes';
|
||||
import TableActionMenu from 'components/TableActionMenu.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const route = useRoute()
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const appLoading = ref(false);
|
||||
const app = ref<IAppDisplay>({} as IAppDisplay);
|
||||
const appColumns = [
|
||||
{ name: 'id', label: 'ID', field: 'id', align: 'left', classes: 'q-td--no-hover' },
|
||||
{ name: 'name', label: 'アプリ名', field: 'name', align: 'left', classes: 'q-td--no-hover' },
|
||||
{ name: 'url', label: 'URL', field: 'url', align: 'left', classes: 'q-td--no-hover' },
|
||||
{ name: 'updateUser', label: '最後更新者', field: (row: IAppDisplay) => row?.updateUser?.fullName, align: 'left', classes: 'q-td--no-hover'},
|
||||
{ name: 'updateTime', label: '最後更新日', field: 'updateTime', align: 'left', classes: 'q-td--no-hover'},
|
||||
{ name: 'version', label: 'バージョン', field: 'version', align: 'left', classes: 'q-td--no-hover' },
|
||||
{ name: 'actions', label: '', field: 'actions', classes: 'q-td--no-hover' }
|
||||
];
|
||||
const appActionList = ref([
|
||||
{ label: '設定', icon: 'account_tree', action: toEditFlowPage },
|
||||
]);
|
||||
|
||||
const rows = ref<IAppVersionDisplay[]>([]);
|
||||
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 },
|
||||
{ name: 'actions', label: '', field: 'actions' }
|
||||
];
|
||||
|
||||
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
|
||||
const filter = ref('');
|
||||
const versionLoading = ref(false);
|
||||
|
||||
const target = ref<IAppVersionDisplay>();
|
||||
const store = useFlowEditorStore();
|
||||
const confirmDialog = ref(false);
|
||||
const deleteUserLoading = ref(false);
|
||||
|
||||
|
||||
|
||||
const actionList = ref([
|
||||
{ label: 'プル', icon: 'flag', action: changeVersion },
|
||||
// { label: 'プレビュー', icon: 'visibility', action: toVersionHistoryPage },
|
||||
// { separator: true },
|
||||
// { label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
|
||||
]);
|
||||
|
||||
const getApps = async (suppressLoading: false) => {
|
||||
if (!suppressLoading) {
|
||||
appLoading.value = true;
|
||||
}
|
||||
await appStore.loadApps();
|
||||
appLoading.value = false;
|
||||
}
|
||||
|
||||
const getAppById = () => {
|
||||
let res = appStore.getAppById(route.params.id as string);
|
||||
if (res != null) {
|
||||
app.value = res;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const getVersions = async () => {
|
||||
versionLoading.value = true;
|
||||
rows.value = await appStore.getVersionsByAppId(app.value);
|
||||
versionLoading.value = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
let isSuccess = getAppById();
|
||||
if (!isSuccess) {
|
||||
await getApps();
|
||||
isSuccess = getAppById();
|
||||
if (!isSuccess) {
|
||||
$q.notify({
|
||||
icon: 'error',
|
||||
color: 'negative',
|
||||
message: 'バージョン一覧の読み込みに失敗しました'
|
||||
})
|
||||
await router.push('/app');
|
||||
}
|
||||
}
|
||||
await getVersions();
|
||||
});
|
||||
|
||||
async function changeVersion(version: IAppVersionDisplay) {
|
||||
// TODO
|
||||
versionLoading.value = true;
|
||||
target.value = version;
|
||||
await appStore.changeVersion(app.value, version);
|
||||
await getApps(true);
|
||||
getAppById();
|
||||
versionLoading.value = false;
|
||||
}
|
||||
|
||||
// const deleteApp = async () => {
|
||||
// if (target.value?.id) {
|
||||
// deleteUserLoading.value = true;
|
||||
// await appStore.deleteApp(targetRow.value)
|
||||
// await getApps();
|
||||
// deleteUserLoading.value = false;
|
||||
// confirmDialog.value = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
async function toEditFlowPage(app:IAppDisplay) {
|
||||
store.setApp({
|
||||
appId: app.id,
|
||||
name: app.name
|
||||
});
|
||||
store.selectFlow(undefined);
|
||||
try {
|
||||
await router.push('/FlowChart/' + app.id);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.description-cell {
|
||||
height: 50px;
|
||||
min-width: 300px;
|
||||
max-height: 50px;
|
||||
white-space: break-spaces;
|
||||
|
||||
.q-scrollarea__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -142,13 +142,11 @@
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { api } from 'boot/axios';
|
||||
import { useAuthStore } from 'stores/useAuthStore';
|
||||
import { useDomainStore } from 'stores/useDomainStore';
|
||||
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
|
||||
import TableActionMenu from 'components/TableActionMenu.vue';
|
||||
import { IDomain, IDomainDisplay, IDomainOwnerDisplay, IDomainSubmit } from '../types/DomainTypes';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const domainStore = useDomainStore();
|
||||
const inactiveRowClass = (row: IDomainOwnerDisplay) => row.domainActive ? '' : 'inactive-row';
|
||||
|
||||
const columns = [
|
||||
@@ -329,7 +327,6 @@ const onSubmit = () => {
|
||||
await authStore.setCurrentDomain();
|
||||
}
|
||||
getDomain();
|
||||
domainStore.loadUserDomains();
|
||||
closeDg();
|
||||
onReset();
|
||||
addEditLoading.value = false;
|
||||
|
||||
@@ -28,6 +28,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{ path: 'userdomain', component: () => import('pages/UserDomain.vue')},
|
||||
{ path: 'user', component: () => import('pages/UserManagement.vue')},
|
||||
{ path: 'app', component: () => import('pages/AppManagement.vue')},
|
||||
{ path: 'app/version/:id', component: () => import('pages/AppVersionManagement.vue')},
|
||||
{ path: 'condition', component: () => import('pages/conditionPage.vue') }
|
||||
],
|
||||
},
|
||||
|
||||
106
frontend/src/stores/useAppStore.ts
Normal file
106
frontend/src/stores/useAppStore.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { api } from 'boot/axios';
|
||||
import { IAppDisplay, IAppVersion, IAppVersionDisplay, IManagedApp } from 'src/types/AppTypes';
|
||||
import { IUser } from 'src/types/UserTypes';
|
||||
import { date, Notify } from 'quasar'
|
||||
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
apps: [] as IAppDisplay[],
|
||||
rowIds: new Set<string>(),
|
||||
}),
|
||||
actions: {
|
||||
async loadApps() {
|
||||
this.reset();
|
||||
try {
|
||||
const { data } = await api.get('api/apps');
|
||||
this.apps = data.data.map((item: IManagedApp) => {
|
||||
this.rowIds.add(item.appid);
|
||||
return appToAppDisplay(item)
|
||||
}).sort((a: IAppDisplay, b: IAppDisplay) => a.sortId - b.sortId); // set default order
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
icon: 'error',
|
||||
color: 'negative',
|
||||
message: 'アプリ一覧の読み込みに失敗しました'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
getAppById(id: string) {
|
||||
if (!this.rowIds.has(id)) {
|
||||
return null;
|
||||
}
|
||||
return this.apps.find((item: IAppDisplay) => item.id === id);
|
||||
},
|
||||
|
||||
async getVersionsByAppId(app: IAppDisplay) {
|
||||
const { data } = await api.get(`api/appversions/${app.id}`);
|
||||
return data.data.map((item: IAppVersion) => versionToVersionDisplay(item));
|
||||
},
|
||||
|
||||
async changeVersion(app: IAppDisplay, version: IAppVersionDisplay) {
|
||||
await api.put(`api/appversions/${app.id}/${version.id}`);
|
||||
},
|
||||
|
||||
async deleteApp(app: IAppDisplay) {
|
||||
try {
|
||||
await api.delete(`api/apps/${app.id}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Notify.create({
|
||||
icon: 'error',
|
||||
color: 'negative',
|
||||
message: 'アプリの削除に失敗しました'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.apps = [];
|
||||
this.rowIds.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function versionToVersionDisplay(item: IAppVersion) {
|
||||
return {
|
||||
id: item.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 IAppVersionDisplay;
|
||||
}
|
||||
|
||||
function appToAppDisplay(app: IManagedApp) {
|
||||
return {
|
||||
id: app.appid,
|
||||
sortId: parseInt(app.appid, 10),
|
||||
name: app.appname,
|
||||
url: `${app.domainurl}/k/${app.appid}`,
|
||||
version: app.version,
|
||||
updateTime: date.formatDate(app.update_time, 'YYYY/MM/DD HH:mm'),
|
||||
updateUser: userToUserDisplay(app.updateuser)
|
||||
} as IAppDisplay
|
||||
}
|
||||
|
||||
function userToUserDisplay(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,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { api } from 'boot/axios';
|
||||
import { router } from 'src/router';
|
||||
import { IDomainInfo } from '../types/DomainTypes';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { useAppStore } from './useAppStore';
|
||||
|
||||
interface UserInfo {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
@@ -87,6 +89,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
logout() {
|
||||
this.token = '';
|
||||
this.currentDomain = {} as IDomainInfo; // 清空当前域
|
||||
useAppStore().reset();
|
||||
router.push('/login');
|
||||
},
|
||||
async setCurrentDomain(domain?: IDomainInfo) {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { api } from 'boot/axios';
|
||||
import { IDomainInfo, IDomain } from '../types/DomainTypes';
|
||||
|
||||
export const useDomainStore = defineStore('domain', {
|
||||
state: () => ({
|
||||
userDomains: [] as IDomainInfo[],
|
||||
}),
|
||||
actions: {
|
||||
async loadUserDomains(): Promise<IDomainInfo[]> {
|
||||
const resp = await api.get(`api/domain`);
|
||||
const domains = resp.data as IDomain[];
|
||||
this.userDomains = domains
|
||||
.filter(data => data.is_active)
|
||||
.map((data) => ({
|
||||
id: data.id,
|
||||
domainName: data.name,
|
||||
kintoneUrl: data.url,
|
||||
}));
|
||||
return this.userDomains;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -4,18 +4,7 @@ export interface IManagedApp {
|
||||
appid: string;
|
||||
appname: string;
|
||||
domainurl: string;
|
||||
version: string;
|
||||
user: IUser;
|
||||
updateuser: IUser;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
export interface IManagedApp {
|
||||
appid: string;
|
||||
appname: string;
|
||||
domainurl: string;
|
||||
version: string;
|
||||
version: number;
|
||||
user: IUser;
|
||||
updateuser: IUser;
|
||||
create_time: string;
|
||||
@@ -29,7 +18,7 @@ export interface IAppDisplay{
|
||||
url:string;
|
||||
updateUser: IUserDisplay;
|
||||
updateTime:string;
|
||||
version:string;
|
||||
version:number;
|
||||
}
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
||||
Reference in New Issue
Block a user