Merged PR 80: BUG582:UIメッセージの改善と統一

下記の箇所修正しました。
1.画面中UIメッセージの改善と統一(アクションの説明はDBの設定のため、含まれておりません)
2.kintoneドメイン登録、ユーザー使用可能なkintoneドメイン設定画面追加

Related work items: #582
This commit is contained in:
Shohtetsu Ma
2024-08-09 07:47:11 +00:00
committed by Takuto Yoshida(タクト)
14 changed files with 337 additions and 367 deletions

View File

@@ -254,7 +254,7 @@ async def userdomain_details(
async def create_userdomain( async def create_userdomain(
request: Request, request: Request,
userid: int, userid: int,
domainids:list, domainids:List[int] ,
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:

View File

@@ -35,6 +35,8 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
return encoded_jwt return encoded_jwt
def chacha20Encrypt(plaintext:str, key=config.KINTONE_PSW_CRYPTO_KEY): def chacha20Encrypt(plaintext:str, key=config.KINTONE_PSW_CRYPTO_KEY):
if plaintext is None or plaintext == '':
return None
nonce = os.urandom(16) nonce = os.urandom(16)
algorithm = algorithms.ChaCha20(key, nonce) algorithm = algorithms.ChaCha20(key, nonce)
cipher = Cipher(algorithm, mode=None) cipher = Cipher(algorithm, mode=None)

View File

@@ -216,24 +216,19 @@ def edit_domain(
update_data = domain.dict(exclude_unset=True) update_data = domain.dict(exclude_unset=True)
for key, value in update_data.items(): for key, value in update_data.items():
if(key != "id"): if key != "id" and not (key == "kintonepwd" and (value is None or value == "")):
setattr(db_domain, key, value) setattr(db_domain, key, value)
print(str(db_domain))
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_userdomain(db: Session, userid:int,domainids:list[str]):
for domainid in domainids: dbCommits = list(map(lambda domainid: models.UserDomain(userid = userid, domainid = domainid ), domainids))
db_domain = models.UserDomain( db.bulk_save_objects(dbCommits)
userid = userid,
domainid = domainid
)
db.add(db_domain)
db.commit() db.commit()
db.refresh(db_domain) return dbCommits
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()

View File

@@ -12,14 +12,15 @@
"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

@@ -2,6 +2,7 @@ import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import {router} from 'src/router'; import {router} from 'src/router';
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$axios: AxiosInstance; $axios: AxiosInstance;
@@ -15,30 +16,10 @@ 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

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

View File

@@ -5,7 +5,7 @@
:url="uploadUrl" :url="uploadUrl"
:label="title" :label="title"
:headers="headers" :headers="headers"
accept=".csv,.xlsx" accept=".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"
@@ -30,7 +30,7 @@ import { ref } from 'vue';
// https://quasar.dev/quasar-plugins/notify#Installation // https://quasar.dev/quasar-plugins/notify#Installation
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: `CSVおよびExcelファイルを選択してください。` message: `Excelファイルを選択してください。`
}) })
} }
@@ -88,7 +88,7 @@ import { ref } from 'vue';
const headers = ref([{name:"Authorization",value:'Bearer ' + authStore.token}]); const headers = ref([{name:"Authorization",value:'Bearer ' + authStore.token}]);
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
title:"設計書から導入する(csv or excel)", title:"設計書から導入する(Excel)",
uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1` uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1`
}); });

View File

@@ -20,7 +20,7 @@
</q-header> </q-header>
<q-drawer <q-drawer
:model-value="authStore.toggleLeftDrawer" :model-value="authStore.LeftDrawer"
:show-if-above="false" :show-if-above="false"
bordered bordered
> >
@@ -28,7 +28,7 @@
<q-item-label <q-item-label
header header
> >
関連リンク メニュー
</q-item-label> </q-item-label>
<EssentialLink <EssentialLink
@@ -46,7 +46,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { onMounted } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue'; import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import DomainSelector from 'components/DomainSelector.vue'; import DomainSelector from 'components/DomainSelector.vue';
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
@@ -56,14 +56,14 @@ const authStore = useAuthStore();
const essentialLinks: EssentialLinkProps[] = [ const essentialLinks: EssentialLinkProps[] = [
{ {
title: 'ホーム', title: 'ホーム',
caption: 'home', caption: '設計書から導入する',
icon: 'home', icon: 'home',
link: '/', link: '/',
target:'_self' target:'_self'
}, },
{ {
title: 'フローエディター', title: 'フローエディター',
caption: 'flowChart', caption: 'イベントを設定する',
icon: 'account_tree', icon: 'account_tree',
link: '/#/FlowChart', link: '/#/FlowChart',
target:'_self' target:'_self'
@@ -153,6 +153,9 @@ const essentialLinks: EssentialLinkProps[] = [
const version = process.env.version; const version = process.env.version;
const productName = process.env.productName; const productName = process.env.productName;
onMounted(() => {
authStore.toggleLeftMenu();
});
function toggleLeftDrawer() { function toggleLeftDrawer() {
authStore.toggleLeftMenu(); authStore.toggleLeftMenu();

View File

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

View File

@@ -1,127 +1,21 @@
<!-- <template>
<div class="q-pa-md" style="max-width: 400px">
<q-form
@submit="onSubmit"
@reset="onReset"
class="q-gutter-md"
>
<q-input
filled
v-model="name"
label="Your name *"
hint="Kintone envirment name"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<q-input
filled type="url"
v-model="url"
label="Kintone url"
hint="Kintone domain address"
lazy-rules
:rules="[ val => val && val.length > 0,isDomain || 'Please type something']"
/>
<q-input
filled
v-model="username"
label="Login user "
hint="Kintone user name"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<q-input v-model="password" filled :type="isPwd ? 'password' : 'text'" hint="Password with toggle" label="User password">
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
<q-toggle v-model="accept" label="Active Domain" />
<div>
<q-btn label="Submit" type="submit" color="primary"/>
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</div>
</template>
<script>
import { useQuasar } from 'quasar'
import { ref } from 'vue'
export default {
setup () {
const $q = useQuasar()
const name = ref(null)
const age = ref(null)
const accept = ref(false)
const isPwd =ref(true)
return {
name,
age,
accept,
isPwd,
isDomain(val) {
const domainPattern = /^https?\/\/:([a-zA-Z] +\.){1}([a-zA-Z]+)\.([a-zA-Z]+)$/;
return (domainPattern.test(val) || '無効なURL')
},
onSubmit () {
if (accept.value !== true) {
$q.notify({
color: 'red-5',
textColor: 'white',
icon: 'warning',
message: 'You need to accept the license and terms first'
})
}
else {
$q.notify({
color: 'green-4',
textColor: 'white',
icon: 'cloud_done',
message: 'Submitted'
})
}
},
onReset () {
name.value = null
age.value = null
accept.value = false
}
}
}
}
</script> -->
<template> <template>
<div class="q-pa-md"> <div class="q-pa-lg">
<q-table grid grid-header title="Domain" selection="single" :rows="rows" :columns="columns" v-model:selected="selected" row-key="name" :filter="filter" hide-header> <q-table grid grid-header title="Domain" selection="single" :rows="rows" :columns="columns"
v-model:selected="selected" row-key="name" :filter="filter" hide-header>
<template v-slot:top> <template v-slot:top>
<div class="q-pa-md q-gutter-sm"> <div class="q-gutter-sm">
<q-btn color="primary" label="追加" @click="newDomain()" dense /> <q-btn class="q-mx-none" color="primary" label="追加" @click="newDomain()" />
</div> </div>
<q-space /> <q-space />
<q-input borderless dense debounce="300" v-model="filter" placeholder="Search"> <q-input borderless dense filled debounce="300" v-model="filter" placeholder="Search">
<template v-slot:append> <template v-slot:append>
<q-icon name="search" /> <q-icon name="search" />
</template> </template>
</q-input> </q-input>
</template> </template>
<template v-slot:item="props"> <template v-slot:item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4"> <div class="q-pa-sm">
<q-card> <q-card>
<q-card-section> <q-card-section>
<div class="q-table__grid-item-row"> <div class="q-table__grid-item-row">
@@ -130,40 +24,49 @@ export default {
</div> </div>
<div class="q-table__grid-item-row"> <div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">URL</div> <div class="q-table__grid-item-title">URL</div>
<div class="q-table__grid-item-value">{{ props.row.url }}</div> <div class="q-table__grid-item-value" style="width: 22rem;">{{ props.row.url }}</div>
</div> </div>
<div class="q-table__grid-item-row"> <div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Account</div> <div class="q-table__grid-item-title">Account</div>
<div class="q-table__grid-item-value">{{ props.row.kintoneuser }}</div> <div class="q-table__grid-item-value">{{ props.row.kintoneuser }}</div>
</div> </div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-value">{{isActive(props.row.id) }}</div>
</div>
</q-card-section> </q-card-section>
<q-separator /> <q-separator />
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat @click = "activeDomain(props.row.id)">有効</q-btn> <div style="width: 98%;">
<q-btn flat @click = "deleteConfirm(props.row)">削除</q-btn> <div class="row items-center justify-between">
<div class="q-table__grid-item-value"
:class="isActive(props.row.id) === 'Active' ? 'text-positive' : 'text-negative'">{{
isActive(props.row.id) }}</div>
<div class="col-auto">
<q-btn v-if="isActive(props.row.id) !== 'Active'" flat
@click="activeDomain(props.row.id)">有効</q-btn>
<q-btn flat @click="deleteConfirm(props.row)">削除</q-btn>
</div>
</div>
</div>
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</div> </div>
</template> </template>
</q-table> </q-table>
<show-dialog v-model:visible="show" name="ドメイン" @close="closeDg" width="350px"> <show-dialog v-model:visible="show" name="ドメイン" @close="closeDg">
<domain-select ref="domainDg" name="ドメイン" type="multiple"></domain-select> <domain-select ref="domainDg" name="ドメイン" type="multiple"></domain-select>
</show-dialog> </show-dialog>
<q-dialog v-model="confirm" persistent> <q-dialog v-model="confirm" persistent>
<q-card> <q-card>
<q-card-section class="row items-center"> <q-card-section class="row items-center">
<q-avatar icon="confirm" color="primary" text-color="white" /> <div class="q-ma-sm q-mt-md">
<span class="q-ml-sm">削除してもよろしいですか</span> <q-icon name="warning" color="warning" size="2em" />
<span class="q-ml-sm">削除してもよろしいですか</span>
</div>
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup /> <q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" v-close-popup @click = "deleteDomain()"/> <q-btn flat label="OK" color="primary" v-close-popup @click="deleteDomain()" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
@@ -171,40 +74,36 @@ export default {
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useQuasar } from 'quasar' import { ref, onMounted } from 'vue'
import { ref, onMounted, reactive } from 'vue'
import ShowDialog from 'components/ShowDialog.vue'; import ShowDialog from 'components/ShowDialog.vue';
import DomainSelect from 'components/DomainSelect.vue'; import DomainSelect from 'components/DomainSelect.vue';
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore(); const authStore = useAuthStore();
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { domain } from 'process';
const $q = useQuasar()
const domainDg = ref(); const domainDg = ref();
const selected = ref([]) const selected = ref([])
const show = ref(false); const show = ref(false);
const confirm = ref(false) const confirm = ref(false)
const filter = ref()
let editId = ref(0); let editId = ref(0);
let activedomainid = ref(0); let activedomainid = ref(0);
const columns = [ const columns = [
{ name: 'id'}, { name: 'id' },
{name: 'name',required: true,label: 'Name',align: 'left',field: 'name',sortable: true}, { name: 'name', required: true, label: 'Name', align: 'left', field: 'name', sortable: true },
{ name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true }, { name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true }, { name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true },
{ name: 'kintonepwd' }, { name: 'kintonepwd' },
{ name: 'active', field: 'active'} { name: 'active', field: 'active' }
] ]
const rows = ref([] as any[]); const rows = ref([] as any[]);
const isActive = (id:number) =>{ const isActive = (id: number) => {
if(id == activedomainid.value) if (id == activedomainid.value)
return "Active"; return "Active";
else else
return "Inactive"; return "Inactive";
@@ -216,55 +115,48 @@ const newDomain = () => {
}; };
const activeDomain = (id:number) => { const activeDomain = (id: number) => {
api.put(`api/activedomain/`+ id).then(() =>{ api.put(`api/activedomain/` + id).then(() => {
getDomain(); getDomain();
}) })
}; };
const deleteConfirm = (row:object) => { const deleteConfirm = (row: object) => {
confirm.value = true; confirm.value = true;
editId.value = row.id; editId.value = row.id;
}; };
const deleteDomain = () => { const deleteDomain = () => {
api.delete(`api/domain/`+ editId.value+'/1').then(() =>{ console.log(authStore.getCurrentDomain);
getDomain();
}) api.delete(`api/domain/${editId.value}/${authStore.userId}`).then(() => {
getDomain();
})
editId.value = 0; editId.value = 0;
}; };
const closeDg = (val:string) => { const closeDg = (val: string) => {
if (val == 'OK') { if (val == 'OK') {
let dodmainids =[]; let dodmainids = [];
let domains = JSON.parse(JSON.stringify(domainDg.value.selected)); let domains = JSON.parse(JSON.stringify(domainDg.value.selected));
for(var key in domains) for (var key in domains) {
{
dodmainids.push(domains[key].id); dodmainids.push(domains[key].id);
} }
api.post(`api/domain`, dodmainids).then(() =>{getDomain();}); api.post(`api/domain/${authStore.userId}`, dodmainids).then(() => { getDomain(); });
} }
}; };
const getDomain = async () => { const getDomain = async () => {
const resp = await api.get(`api/activedomain`); const resp = await api.get(`api/activedomain`);
activedomainid.value = resp.data.id; activedomainid.value = resp.data.id;
const domainResult = await api.get(`api/domain`); const domainResult = await api.get(`api/domain`);
const domains = domainResult.data as any[]; const domains = domainResult.data as any[];
rows.value=domains.map((item)=>{ rows.value = domains.map((item) => {
return { id:item.id,name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd} return { id: item.id, name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd }
}); });
} }
onMounted(async () => { onMounted(async () => {
await getDomain(); await getDomain();
}) })
const isDomain = (val) =>{
// const domainPattern = /^https\/\/:([a-zA-Z] +\.){1}([a-zA-Z]+)\.([a-zA-Z]+)$/;
// return (domainPattern.test(val) || '無効なURL')
return true;
};
</script> </script>

View File

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

View File

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

View File

@@ -194,7 +194,7 @@ export class KintoneEventManager {
), ),
new kintoneEventGroup( new kintoneEventGroup(
'app.record.create.show.customButtonClick', 'app.record.create.show.customButtonClick',
'ボタンをクリックした', 'ボタンをクリックしたとき',
[], [],
'app.record.create' 'app.record.create'
), ),
@@ -222,7 +222,7 @@ export class KintoneEventManager {
), ),
new kintoneEventGroup( new kintoneEventGroup(
'app.record.detail.show.customButtonClick', 'app.record.detail.show.customButtonClick',
'ボタンをクリックした', 'ボタンをクリックしたとき',
[], [],
'app.record.detail' 'app.record.detail'
), ),
@@ -256,7 +256,7 @@ export class KintoneEventManager {
), ),
new kintoneEventGroup( new kintoneEventGroup(
'app.record.edit.show.customButtonClick', 'app.record.edit.show.customButtonClick',
'ボタンをクリックした', 'ボタンをクリックしたとき',
[], [],
'app.record.edit' 'app.record.edit'
), ),
@@ -278,7 +278,7 @@ export class KintoneEventManager {
'app.record.index' 'app.record.index'
), ),
new kintoneEvent( new kintoneEvent(
'インライン編集の保存をクリックしたとき', 'インライン編集の保存をクリックしたとき',
'app.record.index.edit.submit', 'app.record.index.edit.submit',
'app.record.index' 'app.record.index'
), ),
@@ -295,7 +295,7 @@ export class KintoneEventManager {
// ), // ),
new kintoneEventGroup( new kintoneEventGroup(
'app.record.index.show.customButtonClick', 'app.record.index.show.customButtonClick',
'ボタンをクリックした', 'ボタンをクリックしたとき',
[], [],
'app.record.index' 'app.record.index'
), ),

View File

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