Merge branch 'development'

This commit is contained in:
Methapon2001 2024-02-09 09:39:14 +07:00
commit 5e74817f59
No known key found for this signature in database
GPG key ID: 849924FEF46BD132
16 changed files with 475 additions and 199 deletions

View file

@ -10,9 +10,18 @@ const props = withDefaults(
defineProps<{ defineProps<{
open: boolean open: boolean
error: { error: {
externalFileExist?: boolean
fileExist?: boolean fileExist?: boolean
fileName2Long?: boolean fileName2Long?: boolean
} }
disableFields?: (
| 'file'
| 'title'
| 'description'
| 'keyword'
| 'category'
| 'author'
)[]
mode: 'create' | 'edit' mode: 'create' | 'edit'
fileNameLabel?: string fileNameLabel?: string
title?: string title?: string
@ -63,6 +72,25 @@ function reset() {
} }
function submit() { 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', { emit('submit', {
mode: props.mode, mode: props.mode,
file: file.value, file: file.value,
@ -146,12 +174,13 @@ const file = ref<File | undefined>()
/> />
</q-toolbar> </q-toolbar>
<section class="q-mb-md"> <section class="q-pb-xs">
<span class="text-weight-bold q-mb-sm block">ปโหลดไฟล</span> <span class="text-weight-bold q-mb-sm block">ปโหลดไฟล</span>
<q-file <q-file
dense dense
outlined outlined
v-model="file" v-model="file"
:disable="disableFields?.includes('file')"
@update:model-value="(v) => $emit('filechange', v.name)" @update:model-value="(v) => $emit('filechange', v.name)"
:label=" :label="
mode === 'edit' && fileNameLabel && !file?.name mode === 'edit' && fileNameLabel && !file?.name
@ -160,13 +189,19 @@ const file = ref<File | undefined>()
? undefined ? undefined
: 'เลือกไฟล์' : 'เลือกไฟล์'
" "
:error="!!error.fileExist || !!error.fileName2Long" :error="
!!error.externalFileExist ||
!!error.fileExist ||
!!error.fileName2Long
"
:error-message=" :error-message="
error.fileExist error.externalFileExist
? 'พบไฟล์ชื่อซ้ำในระบบ ไฟล์ชื่อนี้ภายในระบบจะถูกเขียนทับ' ? 'พบไฟล์ชื่อซ้ำในระบบอื่น'
: error.fileName2Long : error.fileExist
? 'ไม่สามารถเพิ่มไฟล์ที่ชื่อยาวเกิน 85 ตัวอักษรได้' ? 'พบไฟล์ชื่อซ้ำในระบบ ไฟล์ชื่อนี้ภายในระบบจะถูกเขียนทับ'
: '' : error.fileName2Long
? 'ไม่สามารถเพิ่มไฟล์ที่ชื่อยาวเกิน 85 ตัวอักษรได้'
: ''
" "
id="inputFile" id="inputFile"
:rules="[ :rules="[
@ -179,13 +214,14 @@ const file = ref<File | undefined>()
</q-file> </q-file>
</section> </section>
<section class="q-mb-md"> <section class="q-pb-xs">
<span class="text-weight-bold">อเรอง</span> <span class="text-weight-bold">อเรอง</span>
<q-input <q-input
outlined outlined
dense dense
class="q-my-sm" class="q-mt-sm"
placeholder="กรอกชื่อเรื่อง" placeholder="กรอกชื่อเรื่อง"
:disable="disableFields?.includes('title')"
:model-value="title" :model-value="title"
:rules="[ :rules="[
(v) => (v) =>
@ -196,7 +232,7 @@ const file = ref<File | undefined>()
/> />
</section> </section>
<section class="q-mb-md"> <section class="q-pb-lg">
<span class="text-weight-bold">รายละเอยดของเอกสาร</span> <span class="text-weight-bold">รายละเอยดของเอกสาร</span>
<q-input <q-input
outlined outlined
@ -204,63 +240,21 @@ const file = ref<File | undefined>()
class="q-mt-sm no-resize" class="q-mt-sm no-resize"
type="textarea" type="textarea"
placeholder="กรอกรายละเอียด" placeholder="กรอกรายละเอียด"
:disable="disableFields?.includes('description')"
:model-value="description" :model-value="description"
@update:model-value="(v) => $emit('update:description', v)" @update:model-value="(v) => $emit('update:description', v)"
id="inputDescription" id="inputDescription"
/> />
</section> </section>
<section class="q-mb-md"> <section class="q-pb-xs">
<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">
<span class="text-weight-bold">เจาของผลงาน</span> <span class="text-weight-bold">เจาของผลงาน</span>
<q-input <q-input
outlined outlined
dense dense
class="q-my-sm" class="q-mt-sm"
placeholder="เจ้าของผลงาน" placeholder="เจ้าของผลงาน"
:disable="disableFields?.includes('author')"
:model-value="author" :model-value="author"
:rules="[ :rules="[
(v) => (v) =>
@ -271,6 +265,50 @@ const file = ref<File | undefined>()
/> />
</section> </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' }"> <section :style="{ display: 'flex', gap: '.5rem' }">
<q-btn <q-btn
label="บันทึก" label="บันทึก"

View file

@ -7,7 +7,7 @@ import UploadExistDialog from './UploadExistDialog.vue'
const storageStore = useStorage() const storageStore = useStorage()
const { file, currentInfo } = storeToRefs(storageStore) const { file, currentInfo } = storeToRefs(storageStore)
const { createFile, updateFile } = storageStore const { createFile, updateFile, goto } = storageStore
const fileFormState = ref<boolean>(false) const fileFormState = ref<boolean>(false)
const fileFormPath = ref<string>('') const fileFormPath = ref<string>('')
@ -18,10 +18,16 @@ const fileFormData = ref<{
keyword?: string[] keyword?: string[]
category?: string[] category?: string[]
author?: string author?: string
metadata?: Record<string, unknown>
}>({}) }>({})
const fileFormType = ref<'edit' | 'create'>('create') 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 fileExistNotification = ref<boolean>(false)
const errorState = ref<'fileExist' | 'externalFileExist'>('fileExist')
const fileFormComponent = ref<InstanceType<typeof FileForm>>() const fileFormComponent = ref<InstanceType<typeof FileForm>>()
const fileNameLabel = ref<string>() const fileNameLabel = ref<string>()
@ -38,6 +44,7 @@ function triggerFileEdit(
keyword: string[] keyword: string[]
category: string[] category: string[]
author: string author: string
metadata?: Record<string, unknown>
}, },
pathname: string, pathname: string,
file?: string, file?: string,
@ -46,13 +53,18 @@ function triggerFileEdit(
fileFormType.value = 'edit' fileFormType.value = 'edit'
fileFormPath.value = pathname fileFormPath.value = pathname
fileFormData.value = { fileFormData.value = {
title: value.title, title: (value.metadata?.subject as string) ?? value.title,
description: value.description, description: value.description,
keyword: value.keyword, keyword: value.keyword,
category: value.category, category: value.category,
author: value.author, author: (value.metadata?.author as string) ?? value.author,
metadata: value.metadata,
} }
fileNameLabel.value = file fileNameLabel.value = file
const arr = pathname.split('/')
arr[arr.length - 1] = ''
goto(arr.join('/'))
} }
defineExpose({ defineExpose({
@ -62,6 +74,18 @@ defineExpose({
const currentParam = ref<Parameters<typeof submitFileForm>[0]>() 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) { function checkFileExist(name: string, except?: string) {
return Boolean( return Boolean(
file.value[currentInfo.value.path].find( file.value[currentInfo.value.path].find(
@ -88,6 +112,16 @@ async function submitFileForm(
) { ) {
currentParam.value = value 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 ( if (
value.file && value.file &&
file.value[currentInfo.value.path].find( file.value[currentInfo.value.path].find(
@ -96,6 +130,7 @@ async function submitFileForm(
!force !force
) { ) {
fileExistNotification.value = true fileExistNotification.value = true
errorState.value = 'fileExist'
return return
} }
@ -133,6 +168,11 @@ async function submitFileForm(
:mode="fileFormType" :mode="fileFormType"
:error="fileFormError" :error="fileFormError"
:fileNameLabel="fileNameLabel" :fileNameLabel="fileNameLabel"
:disableFields="
fileFormData.metadata && Object.keys(fileFormData.metadata).length > 0
? ['file', 'title', 'author']
: []
"
v-model:open="fileFormState" v-model:open="fileFormState"
v-model:title="fileFormData.title" v-model:title="fileFormData.title"
v-model:description="fileFormData.description" v-model:description="fileFormData.description"
@ -142,6 +182,7 @@ async function submitFileForm(
@reset="() => (fileFormError = {})" @reset="() => (fileFormError = {})"
@filechange=" @filechange="
(name: string) => { (name: string) => {
fileFormError.externalFileExist = checkFileExistMetadata(name)
;(fileFormError.fileExist = checkFileExist(name, fileNameLabel)), ;(fileFormError.fileExist = checkFileExist(name, fileNameLabel)),
(fileFormError.fileName2Long = checkFileName2Long( (fileFormError.fileName2Long = checkFileName2Long(
name, name,
@ -153,6 +194,7 @@ async function submitFileForm(
/> />
<upload-exist-dialog <upload-exist-dialog
v-model:notification="fileExistNotification" v-model:notification="fileExistNotification"
v-model:errorState="errorState"
@confirm="() => currentParam && submitFileForm(currentParam, true)" @confirm="() => currentParam && submitFileForm(currentParam, true)"
@cancel="() => (currentParam = undefined)" @cancel="() => (currentParam = undefined)"
/> />

View file

@ -117,6 +117,7 @@ function triggerFileDelete(pathname: string) {
> >
<file-item-action <file-item-action
:nameId="value.pathname" :nameId="value.pathname"
:have-meta="false"
@delete="() => triggerFolderDelete(value.pathname)" @delete="() => triggerFolderDelete(value.pathname)"
@edit=" @edit="
() => () =>
@ -238,6 +239,9 @@ function triggerFileDelete(pathname: string) {
> >
<file-item-action <file-item-action
:nameId="value.pathname" :nameId="value.pathname"
:have-meta="
!!(value.metadata && Object.keys(value.metadata).length > 0)
"
@edit=" @edit="
() => () =>
fileFormComponent?.triggerFileEdit( fileFormComponent?.triggerFileEdit(
@ -247,6 +251,7 @@ function triggerFileDelete(pathname: string) {
keyword: value.keyword, keyword: value.keyword,
category: value.category, category: value.category,
author: value.author, author: value.author,
metadata: value.metadata,
}, },
value.pathname, value.pathname,
value.fileName, value.fileName,

View file

@ -5,6 +5,7 @@ defineEmits(['edit', 'delete'])
const props = defineProps<{ const props = defineProps<{
nameId: string nameId: string
haveMeta: boolean
}>() }>()
const open = ref(false) const open = ref(false)
@ -39,6 +40,7 @@ const open = ref(false)
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
v-if="!haveMeta"
clickable clickable
@click.stop=" @click.stop="
() => { () => {

View file

@ -226,6 +226,9 @@ onMounted(() => {
> >
<file-item-action <file-item-action
:nameId="value.pathname" :nameId="value.pathname"
:have-meta="
!!(value.metadata && Object.keys(value.metadata).length > 0)
"
@edit=" @edit="
() => () =>
fileFormComponent?.triggerFileEdit( fileFormComponent?.triggerFileEdit(
@ -235,6 +238,7 @@ onMounted(() => {
keyword: value.keyword, keyword: value.keyword,
category: value.category, category: value.category,
author: value.author, author: value.author,
metadata: value.metadata,
}, },
value.pathname, value.pathname,
value.fileName, value.fileName,
@ -353,8 +357,21 @@ onMounted(() => {
dense dense
icon="mdi-trash-can-outline" icon="mdi-trash-can-outline"
@click="() => triggerFileDelete(actionData.row.pathname)" @click="() => triggerFileDelete(actionData.row.pathname)"
v-if="
!(
actionData.row.metadata &&
Object.keys(actionData.row.metadata).length > 0
)
"
id="listViewFileDelete" id="listViewFileDelete"
/> />
<div
style="width: 64px"
v-if="
actionData.row.metadata &&
Object.keys(actionData.row.metadata).length > 0
"
></div>
</div> </div>
</q-td> </q-td>
</template> </template>

View file

@ -379,7 +379,7 @@ const onRowClick = ((_, row) => {
fileFormComponent?.triggerFileEdit( fileFormComponent?.triggerFileEdit(
data.row, data.row,
data.row.pathname, data.row.pathname,
data.row.filename, data.row.fileName,
) )
" "
id="listViewFileEdit" id="listViewFileEdit"
@ -391,8 +391,20 @@ const onRowClick = ((_, row) => {
color="negative" color="negative"
icon="mdi-trash-can-outline" icon="mdi-trash-can-outline"
@click.stop="() => triggerFileDelete(data.row.pathname)" @click.stop="() => triggerFileDelete(data.row.pathname)"
v-if="
!(
data.row.metadata &&
Object.keys(data.row.metadata).length > 0
)
"
data-testid="listViewFileDelete" data-testid="listViewFileDelete"
/> />
<div
style="width: 64px"
v-if="
data.row.metadata && Object.keys(data.row.metadata).length > 0
"
></div>
</div> </div>
</q-td> </q-td>
</template> </template>

View file

@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ defineProps<{
notification: boolean notification: boolean
errorState: 'fileExist' | 'externalFileExist'
}>() }>()
defineEmits(['update:notification', 'confirm', 'cancel']) defineEmits(['update:notification', 'confirm', 'cancel'])
@ -22,11 +23,19 @@ defineEmits(['update:notification', 'confirm', 'cancel'])
</div> </div>
</div> </div>
<div> <div>
<h6 class="q-my-none">นยนการเพมขอม</h6> <h6 class="q-my-none">
{{
errorState === 'externalFileExist'
? 'พบไฟล์ชื่อช้ำในระบบอื่น'
: 'ยืนยันการเพิ่มข้อมูล'
}}
</h6>
<p class="q-my-none"> <p class="q-my-none">
พบไฟลอซำในระบบ หากดำเนนการต {{
ไฟลอยจะถกแทนทวยไฟลใหม errorState === 'externalFileExist'
องการยนยนการอปโหลดไฟลหรอไม ? 'พบไฟล์ชื่อช้ำในระบบอื่น จะไม่สามารถอัปโหลดไฟล์นี้ได้'
: 'ไฟล์ที่มีอยู่จะถูกแทนที่ด้วยไฟล์ใหม่ต้องการยืนยันการอัปโหลดไฟล์นี้หรือไม่'
}}
</p> </p>
</div> </div>
</div> </div>
@ -44,7 +53,12 @@ defineEmits(['update:notification', 'confirm', 'cancel'])
label="ดำเนินการต่อ" label="ดำเนินการต่อ"
v-close-popup v-close-popup
color="warning" color="warning"
@click="() => $emit('confirm')" @click="
() =>
errorState === 'externalFileExist'
? $emit('cancel')
: $emit('confirm')
"
/> />
</q-card-actions> </q-card-actions>
</q-card> </q-card>

View file

@ -132,7 +132,9 @@ async function downloadSubmit(path: string | undefined) {
<span>อเรอง</span> <span>อเรอง</span>
</div> </div>
<div class="col-grow"> <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>
</div> </div>
<q-separator /> <q-separator />
@ -150,7 +152,9 @@ async function downloadSubmit(path: string | undefined) {
<span>เจาของผลงาน</span> <span>เจาของผลงาน</span>
</div> </div>
<div class="col-grow"> <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>
</div> </div>
<q-separator /> <q-separator />

View file

@ -12,6 +12,21 @@ import { useFileInfoStore } from '@/stores/file-info-data'
import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue' import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue'
import useStorage from '@/stores/storage' 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 loaderStore = useLoader()
const storageStore = useStorage() const storageStore = useStorage()
const { currentInfo } = storeToRefs(storageStore) const { currentInfo } = storeToRefs(storageStore)
@ -26,21 +41,12 @@ const {
advSearchDataRow, advSearchDataRow,
} = storeToRefs(useSearchDataStore()) } = storeToRefs(useSearchDataStore())
const { getFoundFile } = useSearchDataStore() const { getFoundFile } = useSearchDataStore()
const submitSearchData = ref<{
AND: { const submitSearchData = ref<Search>({
field: string
value: string
exact?: boolean
}[]
OR: {
field: string
value: string
exact?: boolean
}[]
}>({
AND: [], AND: [],
OR: [], OR: [],
}) })
const props = defineProps<{ const props = defineProps<{
mode: 'admin' | 'user' mode: 'admin' | 'user'
}>() }>()
@ -50,55 +56,50 @@ async function submitSearch() {
if (searchData.value.value.trim() !== '') { if (searchData.value.value.trim() !== '') {
submitSearchData.value = { AND: [], OR: [] } submitSearchData.value = { AND: [], OR: [] }
if (props.mode === 'admin') { 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) => { optionsField.value.forEach((option) => {
submitSearchData.value.OR.push({ submitSearchData.value.OR?.push({
field: option.value, field: option.value,
value: searchData.value.value, value: searchData.value.value,
exact: true,
}) })
}) })
submitSearchData.value.OR.push({ submitSearchData.value.OR?.push({
field: 'fileName', field: 'fileName',
value: searchData.value.value, value: searchData.value.value,
exact: true,
}) })
submitSearchData.value.OR.push({ submitSearchData.value.OR?.push({
field: 'metadata.author', field: 'metadata.author',
value: searchData.value.value, value: searchData.value.value,
exact: true,
}) })
submitSearchData.value.OR.push({ submitSearchData.value.OR?.push({
field: 'metadata.subject', field: 'metadata.subject',
value: searchData.value.value, value: searchData.value.value,
exact: true,
}) })
submitSearchData.value.OR.push({ submitSearchData.value.OR?.push({
field: 'fileType', field: 'fileType',
value: mime.getType(searchData.value.value) || '', value: mime.getType(searchData.value.value) || '',
exact: true,
}) })
if (currentInfo.value.path !== '/') {
submitSearchData.value.AND.push({
field: 'path',
value: currentInfo.value.path,
})
}
} else { } else {
submitSearchData.value.recursive = true
submitSearchData.value.path = []
if (searchData.value.field == 'title') { if (searchData.value.field == 'title') {
submitSearchData.value.OR.push({ submitSearchData.value.OR?.push({
field: 'metadata.subject', field: 'metadata.subject',
value: searchData.value.value, value: searchData.value.value,
exact: isExact.value,
}) })
} }
if (searchData.value.field == 'author') { if (searchData.value.field == 'author') {
submitSearchData.value.OR.push({ submitSearchData.value.OR?.push({
field: 'metadata.author', field: 'metadata.author',
value: searchData.value.value, value: searchData.value.value,
exact: isExact.value,
}) })
} }
submitSearchData.value.OR.push({ submitSearchData.value.OR?.push({
field: searchData.value.field, field: searchData.value.field,
value: searchData.value.value, value: searchData.value.value,
exact: isExact.value, exact: isExact.value,
@ -111,31 +112,56 @@ async function submitSearch() {
(d: { field: string; value: string; op: string; exact: boolean }) => { (d: { field: string; value: string; op: string; exact: boolean }) => {
if (d.field && d.value.trim() !== '') { if (d.field && d.value.trim() !== '') {
const op = d.op === 'AND' ? 'AND' : 'OR' const op = d.op === 'AND' ? 'AND' : 'OR'
if (d.field == 'title') { if (
submitSearchData.value[op].push({ (op === 'AND' && d.field === 'title') ||
field: 'metadata.subject', 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, value: d.value,
exact: d.exact, 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) { if (advField.keyword.length > 0) {
for (let i = 0; i < advField.keyword.length; i++) { for (let i = 0; i < advField.keyword.length; i++) {
submitSearchData.value.AND.push({ submitSearchData.value.AND?.push({
field: 'keyword', field: 'keyword',
value: advField.keyword[i], value: advField.keyword[i],
exact: true, exact: true,
@ -143,7 +169,7 @@ async function submitSearch() {
} }
} }
if (advField.description.trim() !== '') { if (advField.description.trim() !== '') {
submitSearchData.value.AND.push({ submitSearchData.value.AND?.push({
field: 'description', field: 'description',
value: advField.description, value: advField.description,
exact: true, exact: true,

View file

@ -69,7 +69,7 @@ export const useFileInfoStore = defineStore('info', () => {
if (extension) return extension if (extension) return extension
if (fileName && fileName.includes('.')) { if (fileName && fileName.includes('.')) {
return fileName.substring(fileName.lastIndexOf('.')) return fileName.substring(fileName.lastIndexOf('.') + 1)
} }
return 'ไม่ทราบประเภท' return 'ไม่ทราบประเภท'

View file

@ -27,7 +27,7 @@ export interface StorageFile {
title: string title: string
description: string description: string
author: string author: string
metadata: Record<string, unknown> metadata?: Record<string, unknown>
category: string[] category: string[]
keyword: string[] keyword: string[]
updatedAt: string updatedAt: string

View file

@ -1,8 +1,10 @@
import { Body, Controller, Post, Route, Security, SuccessResponse, Tags } from "tsoa"; import { Body, Controller, Post, Route, Security, SuccessResponse, Tags } from "tsoa";
import HttpStatusCode from "../interfaces/http-status"; import HttpStatusCode from "../interfaces/http-status";
import esClient from "../elasticsearch"; import esClient from "../elasticsearch";
import { Search } from "../interfaces/search"; import { Search, SearchInfo, SearchOperator, SearchOptions } from "../interfaces/search";
import { StorageFile } from "../interfaces/storage-fs"; 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; const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX;
@ -14,22 +16,57 @@ export class SearchController extends Controller {
@Tags("Search") @Tags("Search")
@Security("bearerAuth") @Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ") @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 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> }>({ const result = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX, index: DEFAULT_INDEX,
query: { query: {
bool: { bool: {
must: search.AND?.map((v) => ({ must: search.AND?.map(searchMapCallback),
[type[search.exact || v.exact ? 1 : 0]]: { [v.field]: v.value }, should: search.OR?.map(searchMapCallback),
})), must_not: { match: { hidden: true } },
should: search.OR?.map((v) => ({ },
[type[search.exact || v.exact ? 1 : 0]]: { [v.field]: v.value }, },
})), post_filter: {
must_not: { [["match", "prefix"][search.recursive ? 1 : 0]]: {
match: { hidden: true }, path: `${search.path?.join("/")}/`.replace(/^\//, "") || "",
},
}, },
}, },
size: 10000, size: 10000,

View file

@ -1,13 +1,34 @@
export interface Search { import { QueryDslQueryContainer } from "@elastic/elasticsearch/lib/api/types";
AND?: {
field: string; export interface SearchOptions {
value: string; recursive?: boolean;
exact?: boolean; path?: string[];
}[];
OR?: {
field: string;
value: string;
exact?: boolean;
}[];
exact?: boolean; 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)),
},
};
};
}

View file

@ -27,6 +27,8 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."
// for failed queue that will come later // for failed queue that will come later
const cachedBuffer: Record<string, Buffer> = {}; const cachedBuffer: Record<string, Buffer> = {};
const cachedMetadata: Record<string, { size: number; type: string }> = {}; 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> { export async function handler(key: string, event: string): Promise<boolean> {
console.info(`[AMQ] Messages received - key: ${key}, event: ${event}`); 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); 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 console.info(`[AMQ] Key: ${key} - ${JSON.stringify(cachedRecord[key] ?? "Not Found", null, 2)}`);
? await handleFoundRecord(rec, cachedBuffer[key], cachedMetadata[key])
: await handleNotFoundRecord(pathname, cachedBuffer[key], cachedMetadata[key]); const result = cachedRecord[key]
? await handleFoundRecord(cachedRecord[key], cachedBuffer[key], cachedMetadata[key], key)
: await handleNotFoundRecord(pathname, cachedBuffer[key], cachedMetadata[key], key);
if (result) { if (result) {
delete cachedBuffer[key]; delete cachedBuffer[key];
delete cachedMetadata[key]; delete cachedMetadata[key];
delete cachedRecord[key];
errorKey = errorKey.filter((v) => v !== key);
} }
return result; return result;
@ -96,7 +102,7 @@ async function popInfo(pathname: string) {
return result.hits.hits[0]._source; 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; return false;
} }
@ -126,10 +132,11 @@ async function handleNotFoundRecord(
pathname: string, pathname: string,
buffer: Buffer, buffer: Buffer,
stat: { size: number; type: string }, stat: { size: number; type: string },
key: string,
) { ) {
const path = stripLeadingSlash(pathname.split("/").slice(0, -1).join("/") + "/"); const path = stripLeadingSlash(pathname.split("/").slice(0, -1).join("/") + "/");
const filename = pathname.split("/").at(-1); 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 = { const metadata = {
pathname, pathname,
@ -158,7 +165,12 @@ async function handleNotFoundRecord(
document: { data: base64, ...metadata }, document: { data: base64, ...metadata },
refresh: "wait_for", 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; if (!result) return false;
@ -173,6 +185,7 @@ async function handleFoundRecord(
metadata: StorageFile, metadata: StorageFile,
buffer: Buffer, buffer: Buffer,
stat: { size: number; type: string }, stat: { size: number; type: string },
key: string,
) { ) {
metadata.fileSize = stat.size; metadata.fileSize = stat.size;
metadata.fileType = stat.type; metadata.fileType = stat.type;
@ -182,10 +195,19 @@ async function handleFoundRecord(
.index({ .index({
pipeline: "attachment", pipeline: "attachment",
index: DEFAULT_INDEX!, 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", 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; if (!result) return false;

View file

@ -45,11 +45,21 @@ const models: TsoaRoute.Models = {
"additionalProperties": false, "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 // 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", "dataType": "refObject",
"properties": { "properties": {
"AND": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"exact":{"dataType":"boolean"},"value":{"dataType":"string","required":true},"field":{"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}}}}, "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"}, "exact": {"dataType":"boolean"},
}, },
"additionalProperties": false, "additionalProperties": false,
@ -175,7 +185,7 @@ export function RegisterRoutes(app: Router) {
function SearchController_searchFile(request: any, response: any, next: any) { function SearchController_searchFile(request: any, response: any, next: any) {
const args = { 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 // 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 // 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', app.post('/storage/folder/size',
authenticateMiddleware([{"bearerAuth":["management-role","admin"]}]), authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(StorageController)), ...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.folderSize)), ...(fetchMiddlewares<RequestHandler>(StorageController.prototype.folderSize)),

View file

@ -110,47 +110,33 @@
"type": "object", "type": "object",
"additionalProperties": false "additionalProperties": false
}, },
"Search": { "SearchInfo": {
"properties": { "properties": {
"AND": { "field": {
"items": { "type": "string"
"properties": {
"exact": {
"type": "boolean"
},
"value": {
"type": "string"
},
"field": {
"type": "string"
}
},
"required": [
"value",
"field"
],
"type": "object"
},
"type": "array"
}, },
"OR": { "value": {
"type": "string"
},
"exact": {
"type": "boolean"
}
},
"required": [
"field",
"value"
],
"type": "object",
"additionalProperties": false
},
"SearchOptions": {
"properties": {
"recursive": {
"type": "boolean"
},
"path": {
"items": { "items": {
"properties": { "type": "string"
"exact": {
"type": "boolean"
},
"value": {
"type": "string"
},
"field": {
"type": "string"
}
},
"required": [
"value",
"field"
],
"type": "object"
}, },
"type": "array" "type": "array"
}, },
@ -608,7 +594,50 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "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": [ "security": [
{ {
"bearerAuth": [ "bearerAuth": []
"management-role",
"admin"
]
} }
], ],
"parameters": [], "parameters": [],