Merge branch 'development'
This commit is contained in:
commit
5e74817f59
16 changed files with 475 additions and 199 deletions
|
|
@ -10,9 +10,18 @@ const props = withDefaults(
|
|||
defineProps<{
|
||||
open: boolean
|
||||
error: {
|
||||
externalFileExist?: boolean
|
||||
fileExist?: boolean
|
||||
fileName2Long?: boolean
|
||||
}
|
||||
disableFields?: (
|
||||
| 'file'
|
||||
| 'title'
|
||||
| 'description'
|
||||
| 'keyword'
|
||||
| 'category'
|
||||
| 'author'
|
||||
)[]
|
||||
mode: 'create' | 'edit'
|
||||
fileNameLabel?: string
|
||||
title?: string
|
||||
|
|
@ -63,6 +72,25 @@ function reset() {
|
|||
}
|
||||
|
||||
function submit() {
|
||||
if (props.mode === 'edit') {
|
||||
return emit('submit', {
|
||||
mode: props.mode,
|
||||
file: props.disableFields?.includes('file') ? undefined : file.value,
|
||||
title: props.disableFields?.includes('title') ? undefined : props.title,
|
||||
description: props.disableFields?.includes('description')
|
||||
? undefined
|
||||
: props.description ?? '',
|
||||
keyword: props.disableFields?.includes('keyword')
|
||||
? undefined
|
||||
: props.keyword,
|
||||
category: props.disableFields?.includes('category')
|
||||
? undefined
|
||||
: props.category,
|
||||
author: props.disableFields?.includes('author')
|
||||
? undefined
|
||||
: props.author,
|
||||
})
|
||||
}
|
||||
emit('submit', {
|
||||
mode: props.mode,
|
||||
file: file.value,
|
||||
|
|
@ -146,12 +174,13 @@ const file = ref<File | undefined>()
|
|||
/>
|
||||
</q-toolbar>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<section class="q-pb-xs">
|
||||
<span class="text-weight-bold q-mb-sm block">อัปโหลดไฟล์</span>
|
||||
<q-file
|
||||
dense
|
||||
outlined
|
||||
v-model="file"
|
||||
:disable="disableFields?.includes('file')"
|
||||
@update:model-value="(v) => $emit('filechange', v.name)"
|
||||
:label="
|
||||
mode === 'edit' && fileNameLabel && !file?.name
|
||||
|
|
@ -160,13 +189,19 @@ const file = ref<File | undefined>()
|
|||
? undefined
|
||||
: 'เลือกไฟล์'
|
||||
"
|
||||
:error="!!error.fileExist || !!error.fileName2Long"
|
||||
:error="
|
||||
!!error.externalFileExist ||
|
||||
!!error.fileExist ||
|
||||
!!error.fileName2Long
|
||||
"
|
||||
:error-message="
|
||||
error.fileExist
|
||||
? 'พบไฟล์ชื่อซ้ำในระบบ ไฟล์ชื่อนี้ภายในระบบจะถูกเขียนทับ'
|
||||
: error.fileName2Long
|
||||
? 'ไม่สามารถเพิ่มไฟล์ที่ชื่อยาวเกิน 85 ตัวอักษรได้'
|
||||
: ''
|
||||
error.externalFileExist
|
||||
? 'พบไฟล์ชื่อซ้ำในระบบอื่น'
|
||||
: error.fileExist
|
||||
? 'พบไฟล์ชื่อซ้ำในระบบ ไฟล์ชื่อนี้ภายในระบบจะถูกเขียนทับ'
|
||||
: error.fileName2Long
|
||||
? 'ไม่สามารถเพิ่มไฟล์ที่ชื่อยาวเกิน 85 ตัวอักษรได้'
|
||||
: ''
|
||||
"
|
||||
id="inputFile"
|
||||
:rules="[
|
||||
|
|
@ -179,13 +214,14 @@ const file = ref<File | undefined>()
|
|||
</q-file>
|
||||
</section>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<section class="q-pb-xs">
|
||||
<span class="text-weight-bold">ชื่อเรื่อง</span>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
class="q-my-sm"
|
||||
class="q-mt-sm"
|
||||
placeholder="กรอกชื่อเรื่อง"
|
||||
:disable="disableFields?.includes('title')"
|
||||
:model-value="title"
|
||||
:rules="[
|
||||
(v) =>
|
||||
|
|
@ -196,7 +232,7 @@ const file = ref<File | undefined>()
|
|||
/>
|
||||
</section>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<section class="q-pb-lg">
|
||||
<span class="text-weight-bold">รายละเอียดของเอกสาร</span>
|
||||
<q-input
|
||||
outlined
|
||||
|
|
@ -204,63 +240,21 @@ const file = ref<File | undefined>()
|
|||
class="q-mt-sm no-resize"
|
||||
type="textarea"
|
||||
placeholder="กรอกรายละเอียด"
|
||||
:disable="disableFields?.includes('description')"
|
||||
:model-value="description"
|
||||
@update:model-value="(v) => $emit('update:description', v)"
|
||||
id="inputDescription"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<span class="text-weight-bold">กลุ่ม/หมวดหมู่</span>
|
||||
<div class="q-mt-md">
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
:model-value="category"
|
||||
label="กดปุ่มEnterเพื่อเพิ่ม"
|
||||
use-input
|
||||
use-chips
|
||||
hide-dropdown-icon
|
||||
multiple
|
||||
input-debounce="0"
|
||||
@new-value="createCategory"
|
||||
:options="filterDataCategory"
|
||||
@filter="filterCategory"
|
||||
@update:model-value="(v) => $emit('update:category', v)"
|
||||
data-testid="filterDataCategory"
|
||||
>
|
||||
</q-select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<span class="text-weight-bold">คำสำคัญ</span>
|
||||
<div class="q-mt-md">
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
label="กดปุ่มEnterเพื่อเพิ่ม"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
:model-value="props.keyword"
|
||||
@update:model-value="(v) => $emit('update:keyword', v)"
|
||||
@new-value="createKeyword"
|
||||
data-testid="filterDataKeyWord"
|
||||
>
|
||||
</q-select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<section class="q-pb-xs">
|
||||
<span class="text-weight-bold">เจ้าของผลงาน</span>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
class="q-my-sm"
|
||||
class="q-mt-sm"
|
||||
placeholder="เจ้าของผลงาน"
|
||||
:disable="disableFields?.includes('author')"
|
||||
:model-value="author"
|
||||
:rules="[
|
||||
(v) =>
|
||||
|
|
@ -271,6 +265,50 @@ const file = ref<File | undefined>()
|
|||
/>
|
||||
</section>
|
||||
|
||||
<section class="q-pb-md">
|
||||
<span class="text-weight-bold">กลุ่ม/หมวดหมู่</span>
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
:model-value="category"
|
||||
label="กดปุ่มEnterเพื่อเพิ่ม"
|
||||
use-input
|
||||
use-chips
|
||||
hide-dropdown-icon
|
||||
multiple
|
||||
class="q-my-sm"
|
||||
input-debounce="0"
|
||||
@new-value="createCategory"
|
||||
:disable="disableFields?.includes('category')"
|
||||
:options="filterDataCategory"
|
||||
@filter="filterCategory"
|
||||
@update:model-value="(v) => $emit('update:category', v)"
|
||||
data-testid="filterDataCategory"
|
||||
>
|
||||
</q-select>
|
||||
</section>
|
||||
|
||||
<section class="q-pb-lg">
|
||||
<span class="text-weight-bold">คำสำคัญ</span>
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
label="กดปุ่มEnterเพื่อเพิ่ม"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
class="q-my-sm"
|
||||
input-debounce="0"
|
||||
:disable="disableFields?.includes('keyword')"
|
||||
:model-value="props.keyword"
|
||||
@update:model-value="(v) => $emit('update:keyword', v)"
|
||||
@new-value="createKeyword"
|
||||
data-testid="filterDataKeyWord"
|
||||
>
|
||||
</q-select>
|
||||
</section>
|
||||
|
||||
<section :style="{ display: 'flex', gap: '.5rem' }">
|
||||
<q-btn
|
||||
label="บันทึก"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import UploadExistDialog from './UploadExistDialog.vue'
|
|||
|
||||
const storageStore = useStorage()
|
||||
const { file, currentInfo } = storeToRefs(storageStore)
|
||||
const { createFile, updateFile } = storageStore
|
||||
const { createFile, updateFile, goto } = storageStore
|
||||
|
||||
const fileFormState = ref<boolean>(false)
|
||||
const fileFormPath = ref<string>('')
|
||||
|
|
@ -18,10 +18,16 @@ const fileFormData = ref<{
|
|||
keyword?: string[]
|
||||
category?: string[]
|
||||
author?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}>({})
|
||||
const fileFormType = ref<'edit' | 'create'>('create')
|
||||
const fileFormError = ref<{ fileExist?: boolean; fileName2Long?: boolean }>({})
|
||||
const fileFormError = ref<{
|
||||
externalFileExist?: boolean
|
||||
fileExist?: boolean
|
||||
fileName2Long?: boolean
|
||||
}>({})
|
||||
const fileExistNotification = ref<boolean>(false)
|
||||
const errorState = ref<'fileExist' | 'externalFileExist'>('fileExist')
|
||||
const fileFormComponent = ref<InstanceType<typeof FileForm>>()
|
||||
const fileNameLabel = ref<string>()
|
||||
|
||||
|
|
@ -38,6 +44,7 @@ function triggerFileEdit(
|
|||
keyword: string[]
|
||||
category: string[]
|
||||
author: string
|
||||
metadata?: Record<string, unknown>
|
||||
},
|
||||
pathname: string,
|
||||
file?: string,
|
||||
|
|
@ -46,13 +53,18 @@ function triggerFileEdit(
|
|||
fileFormType.value = 'edit'
|
||||
fileFormPath.value = pathname
|
||||
fileFormData.value = {
|
||||
title: value.title,
|
||||
title: (value.metadata?.subject as string) ?? value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
author: value.author,
|
||||
author: (value.metadata?.author as string) ?? value.author,
|
||||
metadata: value.metadata,
|
||||
}
|
||||
fileNameLabel.value = file
|
||||
|
||||
const arr = pathname.split('/')
|
||||
arr[arr.length - 1] = ''
|
||||
goto(arr.join('/'))
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
@ -62,6 +74,18 @@ defineExpose({
|
|||
|
||||
const currentParam = ref<Parameters<typeof submitFileForm>[0]>()
|
||||
|
||||
function checkFileExistMetadata(name: string) {
|
||||
const fileInfo = file.value[currentInfo.value.path].find(
|
||||
(v) => v.fileName === name,
|
||||
)
|
||||
|
||||
if (fileInfo?.metadata && Object.keys(fileInfo?.metadata).length > 0) {
|
||||
return Boolean(true)
|
||||
}
|
||||
|
||||
return Boolean(false)
|
||||
}
|
||||
|
||||
function checkFileExist(name: string, except?: string) {
|
||||
return Boolean(
|
||||
file.value[currentInfo.value.path].find(
|
||||
|
|
@ -88,6 +112,16 @@ async function submitFileForm(
|
|||
) {
|
||||
currentParam.value = value
|
||||
|
||||
const fileInfo = file.value[currentInfo.value.path]?.find(
|
||||
(v) => v.fileName === value.file?.name,
|
||||
)
|
||||
|
||||
if (fileInfo?.metadata && Object.keys(fileInfo.metadata).length > 0) {
|
||||
fileExistNotification.value = true
|
||||
errorState.value = 'externalFileExist'
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
value.file &&
|
||||
file.value[currentInfo.value.path].find(
|
||||
|
|
@ -96,6 +130,7 @@ async function submitFileForm(
|
|||
!force
|
||||
) {
|
||||
fileExistNotification.value = true
|
||||
errorState.value = 'fileExist'
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +168,11 @@ async function submitFileForm(
|
|||
:mode="fileFormType"
|
||||
:error="fileFormError"
|
||||
:fileNameLabel="fileNameLabel"
|
||||
:disableFields="
|
||||
fileFormData.metadata && Object.keys(fileFormData.metadata).length > 0
|
||||
? ['file', 'title', 'author']
|
||||
: []
|
||||
"
|
||||
v-model:open="fileFormState"
|
||||
v-model:title="fileFormData.title"
|
||||
v-model:description="fileFormData.description"
|
||||
|
|
@ -142,6 +182,7 @@ async function submitFileForm(
|
|||
@reset="() => (fileFormError = {})"
|
||||
@filechange="
|
||||
(name: string) => {
|
||||
fileFormError.externalFileExist = checkFileExistMetadata(name)
|
||||
;(fileFormError.fileExist = checkFileExist(name, fileNameLabel)),
|
||||
(fileFormError.fileName2Long = checkFileName2Long(
|
||||
name,
|
||||
|
|
@ -153,6 +194,7 @@ async function submitFileForm(
|
|||
/>
|
||||
<upload-exist-dialog
|
||||
v-model:notification="fileExistNotification"
|
||||
v-model:errorState="errorState"
|
||||
@confirm="() => currentParam && submitFileForm(currentParam, true)"
|
||||
@cancel="() => (currentParam = undefined)"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ function triggerFileDelete(pathname: string) {
|
|||
>
|
||||
<file-item-action
|
||||
:nameId="value.pathname"
|
||||
:have-meta="false"
|
||||
@delete="() => triggerFolderDelete(value.pathname)"
|
||||
@edit="
|
||||
() =>
|
||||
|
|
@ -238,6 +239,9 @@ function triggerFileDelete(pathname: string) {
|
|||
>
|
||||
<file-item-action
|
||||
:nameId="value.pathname"
|
||||
:have-meta="
|
||||
!!(value.metadata && Object.keys(value.metadata).length > 0)
|
||||
"
|
||||
@edit="
|
||||
() =>
|
||||
fileFormComponent?.triggerFileEdit(
|
||||
|
|
@ -247,6 +251,7 @@ function triggerFileDelete(pathname: string) {
|
|||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
author: value.author,
|
||||
metadata: value.metadata,
|
||||
},
|
||||
value.pathname,
|
||||
value.fileName,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ defineEmits(['edit', 'delete'])
|
|||
|
||||
const props = defineProps<{
|
||||
nameId: string
|
||||
haveMeta: boolean
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
|
|
@ -39,6 +40,7 @@ const open = ref(false)
|
|||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="!haveMeta"
|
||||
clickable
|
||||
@click.stop="
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,9 @@ onMounted(() => {
|
|||
>
|
||||
<file-item-action
|
||||
:nameId="value.pathname"
|
||||
:have-meta="
|
||||
!!(value.metadata && Object.keys(value.metadata).length > 0)
|
||||
"
|
||||
@edit="
|
||||
() =>
|
||||
fileFormComponent?.triggerFileEdit(
|
||||
|
|
@ -235,6 +238,7 @@ onMounted(() => {
|
|||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
author: value.author,
|
||||
metadata: value.metadata,
|
||||
},
|
||||
value.pathname,
|
||||
value.fileName,
|
||||
|
|
@ -353,8 +357,21 @@ onMounted(() => {
|
|||
dense
|
||||
icon="mdi-trash-can-outline"
|
||||
@click="() => triggerFileDelete(actionData.row.pathname)"
|
||||
v-if="
|
||||
!(
|
||||
actionData.row.metadata &&
|
||||
Object.keys(actionData.row.metadata).length > 0
|
||||
)
|
||||
"
|
||||
id="listViewFileDelete"
|
||||
/>
|
||||
<div
|
||||
style="width: 64px"
|
||||
v-if="
|
||||
actionData.row.metadata &&
|
||||
Object.keys(actionData.row.metadata).length > 0
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -379,7 +379,7 @@ const onRowClick = ((_, row) => {
|
|||
fileFormComponent?.triggerFileEdit(
|
||||
data.row,
|
||||
data.row.pathname,
|
||||
data.row.filename,
|
||||
data.row.fileName,
|
||||
)
|
||||
"
|
||||
id="listViewFileEdit"
|
||||
|
|
@ -391,8 +391,20 @@ const onRowClick = ((_, row) => {
|
|||
color="negative"
|
||||
icon="mdi-trash-can-outline"
|
||||
@click.stop="() => triggerFileDelete(data.row.pathname)"
|
||||
v-if="
|
||||
!(
|
||||
data.row.metadata &&
|
||||
Object.keys(data.row.metadata).length > 0
|
||||
)
|
||||
"
|
||||
data-testid="listViewFileDelete"
|
||||
/>
|
||||
<div
|
||||
style="width: 64px"
|
||||
v-if="
|
||||
data.row.metadata && Object.keys(data.row.metadata).length > 0
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
notification: boolean
|
||||
errorState: 'fileExist' | 'externalFileExist'
|
||||
}>()
|
||||
|
||||
defineEmits(['update:notification', 'confirm', 'cancel'])
|
||||
|
|
@ -22,11 +23,19 @@ defineEmits(['update:notification', 'confirm', 'cancel'])
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="q-my-none">ยืนยันการเพิ่มข้อมูล</h6>
|
||||
<h6 class="q-my-none">
|
||||
{{
|
||||
errorState === 'externalFileExist'
|
||||
? 'พบไฟล์ชื่อช้ำในระบบอื่น'
|
||||
: 'ยืนยันการเพิ่มข้อมูล'
|
||||
}}
|
||||
</h6>
|
||||
<p class="q-my-none">
|
||||
พบไฟล์ชื่อซ้ำในระบบ หากดำเนินการต่อ
|
||||
ไฟล์ที่มีอยู่จะถูกแทนที่ด้วยไฟล์ใหม่
|
||||
ต้องการยืนยันการอัปโหลดไฟล์นี้หรือไม่
|
||||
{{
|
||||
errorState === 'externalFileExist'
|
||||
? 'พบไฟล์ชื่อช้ำในระบบอื่น จะไม่สามารถอัปโหลดไฟล์นี้ได้'
|
||||
: 'ไฟล์ที่มีอยู่จะถูกแทนที่ด้วยไฟล์ใหม่ต้องการยืนยันการอัปโหลดไฟล์นี้หรือไม่'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -44,7 +53,12 @@ defineEmits(['update:notification', 'confirm', 'cancel'])
|
|||
label="ดำเนินการต่อ"
|
||||
v-close-popup
|
||||
color="warning"
|
||||
@click="() => $emit('confirm')"
|
||||
@click="
|
||||
() =>
|
||||
errorState === 'externalFileExist'
|
||||
? $emit('cancel')
|
||||
: $emit('confirm')
|
||||
"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
|
|
|||
|
|
@ -132,7 +132,9 @@ async function downloadSubmit(path: string | undefined) {
|
|||
<span>ชื่อเรื่อง</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
<span class="text-grey">{{ fileInfo?.metadata.subject ?? fileInfo?.title }}</span>
|
||||
<span class="text-grey">{{
|
||||
fileInfo?.metadata?.subject ?? fileInfo?.title
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
|
|
@ -150,7 +152,9 @@ async function downloadSubmit(path: string | undefined) {
|
|||
<span>เจ้าของผลงาน</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
<span class="text-grey">{{ fileInfo?.metadata.author ?? fileInfo?.author }}</span>
|
||||
<span class="text-grey">{{
|
||||
fileInfo?.metadata?.author ?? fileInfo?.author
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
|
|
|
|||
|
|
@ -12,6 +12,21 @@ import { useFileInfoStore } from '@/stores/file-info-data'
|
|||
import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue'
|
||||
import useStorage from '@/stores/storage'
|
||||
|
||||
interface SearchInfo {
|
||||
field: string
|
||||
value: string
|
||||
exact?: boolean
|
||||
}
|
||||
interface SearchOperator {
|
||||
AND?: (SearchInfo | SearchOperator)[]
|
||||
OR?: (SearchInfo | SearchOperator)[]
|
||||
}
|
||||
interface Search extends SearchOperator {
|
||||
recursive?: boolean
|
||||
path?: string[]
|
||||
exact?: boolean
|
||||
}
|
||||
|
||||
const loaderStore = useLoader()
|
||||
const storageStore = useStorage()
|
||||
const { currentInfo } = storeToRefs(storageStore)
|
||||
|
|
@ -26,21 +41,12 @@ const {
|
|||
advSearchDataRow,
|
||||
} = storeToRefs(useSearchDataStore())
|
||||
const { getFoundFile } = useSearchDataStore()
|
||||
const submitSearchData = ref<{
|
||||
AND: {
|
||||
field: string
|
||||
value: string
|
||||
exact?: boolean
|
||||
}[]
|
||||
OR: {
|
||||
field: string
|
||||
value: string
|
||||
exact?: boolean
|
||||
}[]
|
||||
}>({
|
||||
|
||||
const submitSearchData = ref<Search>({
|
||||
AND: [],
|
||||
OR: [],
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'admin' | 'user'
|
||||
}>()
|
||||
|
|
@ -50,55 +56,50 @@ async function submitSearch() {
|
|||
if (searchData.value.value.trim() !== '') {
|
||||
submitSearchData.value = { AND: [], OR: [] }
|
||||
if (props.mode === 'admin') {
|
||||
submitSearchData.value.exact = true
|
||||
submitSearchData.value.recursive = true
|
||||
submitSearchData.value.path = currentInfo.value.path
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
|
||||
optionsField.value.forEach((option) => {
|
||||
submitSearchData.value.OR.push({
|
||||
submitSearchData.value.OR?.push({
|
||||
field: option.value,
|
||||
value: searchData.value.value,
|
||||
exact: true,
|
||||
})
|
||||
})
|
||||
submitSearchData.value.OR.push({
|
||||
submitSearchData.value.OR?.push({
|
||||
field: 'fileName',
|
||||
value: searchData.value.value,
|
||||
exact: true,
|
||||
})
|
||||
submitSearchData.value.OR.push({
|
||||
submitSearchData.value.OR?.push({
|
||||
field: 'metadata.author',
|
||||
value: searchData.value.value,
|
||||
exact: true,
|
||||
})
|
||||
submitSearchData.value.OR.push({
|
||||
submitSearchData.value.OR?.push({
|
||||
field: 'metadata.subject',
|
||||
value: searchData.value.value,
|
||||
exact: true,
|
||||
})
|
||||
submitSearchData.value.OR.push({
|
||||
submitSearchData.value.OR?.push({
|
||||
field: 'fileType',
|
||||
value: mime.getType(searchData.value.value) || '',
|
||||
exact: true,
|
||||
})
|
||||
if (currentInfo.value.path !== '/') {
|
||||
submitSearchData.value.AND.push({
|
||||
field: 'path',
|
||||
value: currentInfo.value.path,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
submitSearchData.value.recursive = true
|
||||
submitSearchData.value.path = []
|
||||
if (searchData.value.field == 'title') {
|
||||
submitSearchData.value.OR.push({
|
||||
submitSearchData.value.OR?.push({
|
||||
field: 'metadata.subject',
|
||||
value: searchData.value.value,
|
||||
exact: isExact.value,
|
||||
})
|
||||
}
|
||||
if (searchData.value.field == 'author') {
|
||||
submitSearchData.value.OR.push({
|
||||
submitSearchData.value.OR?.push({
|
||||
field: 'metadata.author',
|
||||
value: searchData.value.value,
|
||||
exact: isExact.value,
|
||||
})
|
||||
}
|
||||
submitSearchData.value.OR.push({
|
||||
submitSearchData.value.OR?.push({
|
||||
field: searchData.value.field,
|
||||
value: searchData.value.value,
|
||||
exact: isExact.value,
|
||||
|
|
@ -111,31 +112,56 @@ async function submitSearch() {
|
|||
(d: { field: string; value: string; op: string; exact: boolean }) => {
|
||||
if (d.field && d.value.trim() !== '') {
|
||||
const op = d.op === 'AND' ? 'AND' : 'OR'
|
||||
if (d.field == 'title') {
|
||||
submitSearchData.value[op].push({
|
||||
field: 'metadata.subject',
|
||||
if (
|
||||
(op === 'AND' && d.field === 'title') ||
|
||||
d.field === 'author'
|
||||
) {
|
||||
console.log('and title author')
|
||||
if (d.field === 'title') {
|
||||
submitSearchData.value[op]?.push({
|
||||
OR: [
|
||||
{
|
||||
field: d.field,
|
||||
value: d.value,
|
||||
exact: d.exact,
|
||||
},
|
||||
{
|
||||
field: 'metadata.subject',
|
||||
value: d.value,
|
||||
exact: d.exact,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
if (d.field === 'author') {
|
||||
submitSearchData.value[op]?.push({
|
||||
OR: [
|
||||
{
|
||||
field: d.field,
|
||||
value: d.value,
|
||||
exact: d.exact,
|
||||
},
|
||||
{
|
||||
field: 'metadata.author',
|
||||
value: d.value,
|
||||
exact: d.exact,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
submitSearchData.value[op]?.push({
|
||||
field: d.field,
|
||||
value: d.value,
|
||||
exact: d.exact,
|
||||
})
|
||||
}
|
||||
if (d.field == 'author') {
|
||||
submitSearchData.value[op].push({
|
||||
field: 'metadata.author',
|
||||
value: d.value,
|
||||
exact: d.exact,
|
||||
})
|
||||
}
|
||||
submitSearchData.value[op].push({
|
||||
field: d.field,
|
||||
value: d.value,
|
||||
exact: d.exact,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
if (advField.keyword.length > 0) {
|
||||
for (let i = 0; i < advField.keyword.length; i++) {
|
||||
submitSearchData.value.AND.push({
|
||||
submitSearchData.value.AND?.push({
|
||||
field: 'keyword',
|
||||
value: advField.keyword[i],
|
||||
exact: true,
|
||||
|
|
@ -143,7 +169,7 @@ async function submitSearch() {
|
|||
}
|
||||
}
|
||||
if (advField.description.trim() !== '') {
|
||||
submitSearchData.value.AND.push({
|
||||
submitSearchData.value.AND?.push({
|
||||
field: 'description',
|
||||
value: advField.description,
|
||||
exact: true,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export const useFileInfoStore = defineStore('info', () => {
|
|||
|
||||
if (extension) return extension
|
||||
if (fileName && fileName.includes('.')) {
|
||||
return fileName.substring(fileName.lastIndexOf('.'))
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1)
|
||||
}
|
||||
|
||||
return 'ไม่ทราบประเภท'
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export interface StorageFile {
|
|||
title: string
|
||||
description: string
|
||||
author: string
|
||||
metadata: Record<string, unknown>
|
||||
metadata?: Record<string, unknown>
|
||||
category: string[]
|
||||
keyword: string[]
|
||||
updatedAt: string
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { Body, Controller, Post, Route, Security, SuccessResponse, Tags } from "tsoa";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
import esClient from "../elasticsearch";
|
||||
import { Search } from "../interfaces/search";
|
||||
import { Search, SearchInfo, SearchOperator, SearchOptions } from "../interfaces/search";
|
||||
import { StorageFile } from "../interfaces/storage-fs";
|
||||
import { QueryDslQueryContainer } from "@elastic/elasticsearch/lib/api/types";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
|
||||
const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX;
|
||||
|
||||
|
|
@ -14,22 +16,57 @@ export class SearchController extends Controller {
|
|||
@Tags("Search")
|
||||
@Security("bearerAuth")
|
||||
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
|
||||
public async searchFile(@Body() search: Search): Promise<StorageFile[]> {
|
||||
public async searchFile(
|
||||
@Body()
|
||||
body: {
|
||||
AND?: (
|
||||
| SearchInfo
|
||||
| {
|
||||
AND?: unknown;
|
||||
OR?: unknown;
|
||||
}
|
||||
)[];
|
||||
OR?: (
|
||||
| SearchInfo
|
||||
| {
|
||||
AND?: unknown;
|
||||
OR?: unknown;
|
||||
}
|
||||
)[];
|
||||
} & SearchOptions,
|
||||
): Promise<StorageFile[]> {
|
||||
const search = body as Search;
|
||||
const type = ["match", "match_phrase"] as const;
|
||||
|
||||
const searchMapCallback = (v: SearchInfo | SearchOperator): QueryDslQueryContainer => {
|
||||
if ("field" in v && "value" in v) {
|
||||
return { [type[search.exact || v.exact ? 1 : 0]]: { [v.field]: v.value } };
|
||||
}
|
||||
|
||||
if ("AND" in v || "OR" in v) {
|
||||
return {
|
||||
bool: { must: v.AND?.map(searchMapCallback), should: v.OR?.map(searchMapCallback) },
|
||||
};
|
||||
}
|
||||
|
||||
throw new HttpError(
|
||||
HttpStatusCode.UNPROCESSABLE_ENTITY,
|
||||
"ข้อมูลค้นหาไม่ถูกต้อง กรุณาตรวจสอบอีกครั้ง",
|
||||
);
|
||||
};
|
||||
|
||||
const result = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
|
||||
index: DEFAULT_INDEX,
|
||||
query: {
|
||||
bool: {
|
||||
must: search.AND?.map((v) => ({
|
||||
[type[search.exact || v.exact ? 1 : 0]]: { [v.field]: v.value },
|
||||
})),
|
||||
should: search.OR?.map((v) => ({
|
||||
[type[search.exact || v.exact ? 1 : 0]]: { [v.field]: v.value },
|
||||
})),
|
||||
must_not: {
|
||||
match: { hidden: true },
|
||||
},
|
||||
must: search.AND?.map(searchMapCallback),
|
||||
should: search.OR?.map(searchMapCallback),
|
||||
must_not: { match: { hidden: true } },
|
||||
},
|
||||
},
|
||||
post_filter: {
|
||||
[["match", "prefix"][search.recursive ? 1 : 0]]: {
|
||||
path: `${search.path?.join("/")}/`.replace(/^\//, "") || "",
|
||||
},
|
||||
},
|
||||
size: 10000,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,34 @@
|
|||
export interface Search {
|
||||
AND?: {
|
||||
field: string;
|
||||
value: string;
|
||||
exact?: boolean;
|
||||
}[];
|
||||
OR?: {
|
||||
field: string;
|
||||
value: string;
|
||||
exact?: boolean;
|
||||
}[];
|
||||
import { QueryDslQueryContainer } from "@elastic/elasticsearch/lib/api/types";
|
||||
|
||||
export interface SearchOptions {
|
||||
recursive?: boolean;
|
||||
path?: string[];
|
||||
exact?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchInfo {
|
||||
field: string;
|
||||
value: string;
|
||||
exact?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchOperator {
|
||||
AND?: (SearchInfo | SearchOperator)[];
|
||||
OR?: (SearchInfo | SearchOperator)[];
|
||||
}
|
||||
|
||||
export type Search = SearchOperator & SearchOptions;
|
||||
|
||||
export function mapCallback(exact: boolean) {
|
||||
const type = ["match", "match_phrase"] as const;
|
||||
return (v: SearchInfo | SearchOperator): QueryDslQueryContainer => {
|
||||
return "field" in v && "value" in v
|
||||
? { [type[exact || v.exact ? 1 : 0]]: { [v.field]: v.value } }
|
||||
: {
|
||||
bool: {
|
||||
must: v.AND?.map(mapCallback(exact)),
|
||||
should: v.OR?.map(mapCallback(exact)),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."
|
|||
// for failed queue that will come later
|
||||
const cachedBuffer: Record<string, Buffer> = {};
|
||||
const cachedMetadata: Record<string, { size: number; type: string }> = {};
|
||||
const cachedRecord: Record<string, StorageFile> = {};
|
||||
let errorKey: string[] = [];
|
||||
|
||||
export async function handler(key: string, event: string): Promise<boolean> {
|
||||
console.info(`[AMQ] Messages received - key: ${key}, event: ${event}`);
|
||||
|
|
@ -61,15 +63,19 @@ export async function handler(key: string, event: string): Promise<boolean> {
|
|||
|
||||
const rec = await popInfo(pathname);
|
||||
|
||||
console.info(`[AMQ] Key: ${key} - ${JSON.stringify(rec, null, 2) ?? "Not Found."}`);
|
||||
if (rec) cachedRecord[key] = rec;
|
||||
|
||||
const result = rec
|
||||
? await handleFoundRecord(rec, cachedBuffer[key], cachedMetadata[key])
|
||||
: await handleNotFoundRecord(pathname, cachedBuffer[key], cachedMetadata[key]);
|
||||
console.info(`[AMQ] Key: ${key} - ${JSON.stringify(cachedRecord[key] ?? "Not Found", null, 2)}`);
|
||||
|
||||
const result = cachedRecord[key]
|
||||
? await handleFoundRecord(cachedRecord[key], cachedBuffer[key], cachedMetadata[key], key)
|
||||
: await handleNotFoundRecord(pathname, cachedBuffer[key], cachedMetadata[key], key);
|
||||
|
||||
if (result) {
|
||||
delete cachedBuffer[key];
|
||||
delete cachedMetadata[key];
|
||||
delete cachedRecord[key];
|
||||
errorKey = errorKey.filter((v) => v !== key);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -96,7 +102,7 @@ async function popInfo(pathname: string) {
|
|||
return result.hits.hits[0]._source;
|
||||
}
|
||||
|
||||
if (result && result.hits.hits.length === 0) console.info("Index Not Found");
|
||||
if (result && result.hits.hits.length === 0) console.info("[AMQ] Index Not Found");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -126,10 +132,11 @@ async function handleNotFoundRecord(
|
|||
pathname: string,
|
||||
buffer: Buffer,
|
||||
stat: { size: number; type: string },
|
||||
key: string,
|
||||
) {
|
||||
const path = stripLeadingSlash(pathname.split("/").slice(0, -1).join("/") + "/");
|
||||
const filename = pathname.split("/").at(-1);
|
||||
const base64 = Buffer.from(buffer).toString("base64");
|
||||
const base64 = errorKey.includes(key) ? "" : Buffer.from(buffer).toString("base64");
|
||||
|
||||
const metadata = {
|
||||
pathname,
|
||||
|
|
@ -158,7 +165,12 @@ async function handleNotFoundRecord(
|
|||
document: { data: base64, ...metadata },
|
||||
refresh: "wait_for",
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
.catch((e) => {
|
||||
if (e.meta.statusCode >= 400 && e.meta.statusCode < 500) {
|
||||
errorKey.push(key);
|
||||
}
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
if (!result) return false;
|
||||
|
||||
|
|
@ -173,6 +185,7 @@ async function handleFoundRecord(
|
|||
metadata: StorageFile,
|
||||
buffer: Buffer,
|
||||
stat: { size: number; type: string },
|
||||
key: string,
|
||||
) {
|
||||
metadata.fileSize = stat.size;
|
||||
metadata.fileType = stat.type;
|
||||
|
|
@ -182,10 +195,19 @@ async function handleFoundRecord(
|
|||
.index({
|
||||
pipeline: "attachment",
|
||||
index: DEFAULT_INDEX!,
|
||||
document: { data: Buffer.from(buffer).toString("base64"), ...metadata },
|
||||
document: {
|
||||
data: errorKey.includes(key) ? "" : Buffer.from(buffer).toString("base64"),
|
||||
...metadata,
|
||||
},
|
||||
refresh: "wait_for",
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
.catch((e) => {
|
||||
if (e.meta.statusCode >= 400 && e.meta.statusCode < 500) {
|
||||
errorKey.push(key);
|
||||
console.log(cachedRecord);
|
||||
}
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
if (!result) return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,11 +45,21 @@ const models: TsoaRoute.Models = {
|
|||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"Search": {
|
||||
"SearchInfo": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"AND": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"exact":{"dataType":"boolean"},"value":{"dataType":"string","required":true},"field":{"dataType":"string","required":true}}}},
|
||||
"OR": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"exact":{"dataType":"boolean"},"value":{"dataType":"string","required":true},"field":{"dataType":"string","required":true}}}},
|
||||
"field": {"dataType":"string","required":true},
|
||||
"value": {"dataType":"string","required":true},
|
||||
"exact": {"dataType":"boolean"},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"SearchOptions": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"recursive": {"dataType":"boolean"},
|
||||
"path": {"dataType":"array","array":{"dataType":"string"}},
|
||||
"exact": {"dataType":"boolean"},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
|
@ -175,7 +185,7 @@ export function RegisterRoutes(app: Router) {
|
|||
|
||||
function SearchController_searchFile(request: any, response: any, next: any) {
|
||||
const args = {
|
||||
search: {"in":"body","name":"search","required":true,"ref":"Search"},
|
||||
body: {"in":"body","name":"body","required":true,"dataType":"intersection","subSchemas":[{"dataType":"nestedObjectLiteral","nestedProperties":{"OR":{"dataType":"array","array":{"dataType":"union","subSchemas":[{"ref":"SearchInfo"},{"dataType":"nestedObjectLiteral","nestedProperties":{"OR":{"dataType":"any"},"AND":{"dataType":"any"}}}]}},"AND":{"dataType":"array","array":{"dataType":"union","subSchemas":[{"ref":"SearchInfo"},{"dataType":"nestedObjectLiteral","nestedProperties":{"OR":{"dataType":"any"},"AND":{"dataType":"any"}}}]}}}},{"ref":"SearchOptions"}]},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
|
@ -274,7 +284,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/storage/folder/size',
|
||||
authenticateMiddleware([{"bearerAuth":["management-role","admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":[]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(StorageController)),
|
||||
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.folderSize)),
|
||||
|
||||
|
|
|
|||
|
|
@ -110,47 +110,33 @@
|
|||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Search": {
|
||||
"SearchInfo": {
|
||||
"properties": {
|
||||
"AND": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"exact": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
"field"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"OR": {
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"exact": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"field",
|
||||
"value"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SearchOptions": {
|
||||
"properties": {
|
||||
"recursive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"exact": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
"field"
|
||||
],
|
||||
"type": "object"
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
|
@ -608,7 +594,50 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Search"
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"OR": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SearchInfo"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"OR": {},
|
||||
"AND": {}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"AND": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SearchInfo"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"OR": {},
|
||||
"AND": {}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SearchOptions"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -847,10 +876,7 @@
|
|||
],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"management-role",
|
||||
"admin"
|
||||
]
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue