Add VersionHistory page

This commit is contained in:
xue jiahao
2024-12-10 17:15:55 +08:00
parent f33fd0c64b
commit 76643d280a
9 changed files with 368 additions and 109 deletions

View File

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

View File

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

View File

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

View File

@@ -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,45 +89,27 @@ 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 () => {
@@ -145,7 +117,7 @@ onMounted(async () => {
});
const filterInitRows = (row: {id: string}) => {
return !rowIds.has(row.id);
return !appStore.rowIds.has(row.id);
}
const showAddAppDialog = () => {
@@ -169,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) {

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

View File

@@ -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') }
],
},

View 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,
}
}

View File

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

View File

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