diff --git a/Document/DFD.drawio b/Document/DFD.drawio new file mode 100644 index 0000000..36b6e2a --- /dev/null +++ b/Document/DFD.drawio @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Services/client/package.json b/Services/client/package.json index 1b2b1ab..86cf8ca 100644 --- a/Services/client/package.json +++ b/Services/client/package.json @@ -20,9 +20,11 @@ "@tsconfig/node18": "^18.2.2", "axios": "^1.6.2", "keycloak-js": "^23.0.0", + "mime": "^4.0.0", "nanoid": "^5.0.4", "pinia": "^2.1.7", "quasar": "^2.14.0", + "socket.io-client": "^4.7.2", "vite-plugin-pwa": "^0.17.2", "vue": "^3.3.9", "vue-router": "^4.2.5" @@ -32,6 +34,7 @@ "@quasar/vite-plugin": "^1.6.0", "@rushstack/eslint-patch": "^1.6.0", "@types/jsdom": "^21.1.6", + "@types/mime-types": "^2.1.4", "@types/node": "^20.10.0", "@vitejs/plugin-vue": "^4.5.0", "@vitejs/plugin-vue-jsx": "^3.1.0", diff --git a/Services/client/pnpm-lock.yaml b/Services/client/pnpm-lock.yaml index 9a7f93b..6ee3805 100644 --- a/Services/client/pnpm-lock.yaml +++ b/Services/client/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: keycloak-js: specifier: ^23.0.0 version: 23.0.0 + mime: + specifier: ^4.0.0 + version: 4.0.0 nanoid: specifier: ^5.0.4 version: 5.0.4 @@ -26,6 +29,9 @@ dependencies: quasar: specifier: ^2.14.0 version: 2.14.0 + socket.io-client: + specifier: ^4.7.2 + version: 4.7.2 vite-plugin-pwa: specifier: ^0.17.2 version: 0.17.2(vite@5.0.2)(workbox-build@7.0.0)(workbox-window@7.0.0) @@ -49,6 +55,9 @@ devDependencies: '@types/jsdom': specifier: ^21.1.6 version: 21.1.6 + '@types/mime-types': + specifier: ^2.1.4 + version: 2.1.4 '@types/node': specifier: ^20.10.0 version: 20.10.0 @@ -1801,6 +1810,10 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: false + /@surma/rollup-plugin-off-main-thread@2.2.3: resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} dependencies: @@ -1840,6 +1853,10 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/mime-types@2.1.4: + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + dev: true + /@types/node@20.10.0: resolution: {integrity: sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==} dependencies: @@ -2823,6 +2840,25 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true + /engine.io-client@6.5.3: + resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-parser: 5.2.1 + ws: 8.11.0 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser@5.2.1: + resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} + engines: {node: '>=10.0.0'} + dev: false + /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -3967,6 +4003,12 @@ packages: dependencies: mime-db: 1.52.0 + /mime@4.0.0: + resolution: {integrity: sha512-pzhgdeqU5pJ9t5WK9m4RT4GgGWqYJylxUf62Yb9datXRwdcw5MjiD1BYI5evF8AgTXN9gtKX3CFLvCUL5fAhEA==} + engines: {node: '>=16'} + hasBin: true + dev: false + /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -4709,6 +4751,30 @@ packages: engines: {node: '>=8'} dev: true + /socket.io-client@4.7.2: + resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-client: 6.5.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -5676,6 +5742,19 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws@8.14.2: resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} engines: {node: '>=10.0.0'} @@ -5703,6 +5782,11 @@ packages: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: true + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: false + /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} diff --git a/Services/client/src/assets/logo-edm.png b/Services/client/src/assets/logo-edm.png new file mode 100644 index 0000000..9f0aa04 Binary files /dev/null and b/Services/client/src/assets/logo-edm.png differ diff --git a/Services/client/src/components/DialogDelete.vue b/Services/client/src/components/DialogDelete.vue index c6bf7e5..9779b9c 100644 --- a/Services/client/src/components/DialogDelete.vue +++ b/Services/client/src/components/DialogDelete.vue @@ -21,15 +21,22 @@ const props = withDefaults( > - - แจ้งเตือนการลบ - + + + + + + + + ยืนยันการลบข้อมูล + ต้องการยืนยันการลบข้อมูลนี้หรือไม่ + + - - - ถ้าดำเนินการต่อจะทำการลบ - - $emit('confirm')" id="dialogDeleteConfirm" /> diff --git a/Services/client/src/components/FileForm.vue b/Services/client/src/components/FileForm.vue index 8fa6a98..5289238 100644 --- a/Services/client/src/components/FileForm.vue +++ b/Services/client/src/components/FileForm.vue @@ -34,9 +34,14 @@ const emit = defineEmits([ 'update:keyword', 'update:category', 'filechange', + 'reset', 'submit', ]) +defineExpose({ + reset, +}) + function keydown(e: KeyboardEvent) { if (e.key === 'Escape' && props.open === true) { emit('update:open', false) @@ -50,6 +55,7 @@ function reset() { emit('update:description', '') emit('update:keyword', '') emit('update:category', '') + emit('reset') } function submit() { @@ -61,7 +67,6 @@ function submit() { keyword: props.keyword, category: props.category, }) - emit('update:open', !open), reset() } const createKeyword = ((val, done) => { @@ -106,12 +111,13 @@ const file = ref() class="q-pa-md" side="right" tabindex="0" + v-click-outside="() => $emit('update:open', false)" :width="300" :breakpoint="500" :model-value="open" @update:model-value="(v) => $emit('update:open', v)" > - + diff --git a/Services/client/src/components/FileIcon.vue b/Services/client/src/components/FileIcon.vue index 0de7cd0..c8c6957 100644 --- a/Services/client/src/components/FileIcon.vue +++ b/Services/client/src/components/FileIcon.vue @@ -1,20 +1,45 @@ - {{ DEPT_NAME[currentDept] }} + + {{ DEPT_NAME[currentDept] }} + triggerFolderCreate()" + id="listViewFolderCreate" + /> + - + triggerFolderCreate()" + @click.stop="() => triggerFolderCreate()" id="triggerFolderCreateFileItem" > @@ -278,17 +293,33 @@ async function submitFileForm( }" style="max-width: 100%" > - สร้าง{{ DEPT_NAME[currentDept] }}ใหม่ + สร้าง{{ DEPT_NAME[currentDept] }} - - เอกสาร + + + + เอกสาร + จำนวน {{ currentFile.length }} รายการ + + triggerFileCreate()" + id="listViewFileCreate" + /> + @@ -309,7 +340,8 @@ async function submitFileForm( triggerFileCreate()" + @click.stop="() => triggerFileCreate()" id="triggerFileCreateFileItem" > @@ -374,13 +406,14 @@ async function submitFileForm( class="text-overflow-handle block q-px-md text-center" style="max-width: 100%" > - สร้างไฟล์ใหม่ + สร้างเอกสาร (fileFormError.fileExist = checkFile(name))" + @reset="() => (fileFormError = {})" + @filechange=" + (name: string) => ( + (fileFormError.fileExist = checkFile(name)), + (fileFormError.fileName2Long = checkFileName(name)) + ) + " @submit="submitFileForm" /> diff --git a/Services/client/src/components/FileItemAction.vue b/Services/client/src/components/FileItemAction.vue index 93c80ca..603ae6c 100644 --- a/Services/client/src/components/FileItemAction.vue +++ b/Services/client/src/components/FileItemAction.vue @@ -1,35 +1,56 @@ - - + + - $emit('edit')" id="FileltemActionEdit"> + { + open = false + $emit('edit') + } + " + > - + แก้ไข $emit('delete')" + @click.stop=" + () => { + open = false + $emit('delete') + } + " id="FileltemActiondelete" > - + ลบ diff --git a/Services/client/src/components/FileSearched.vue b/Services/client/src/components/FileSearched.vue index 4fb2121..6416862 100644 --- a/Services/client/src/components/FileSearched.vue +++ b/Services/client/src/components/FileSearched.vue @@ -156,7 +156,7 @@ function confirmDelete() { setTimeout(() => { isActFoundFile.value = true - }, 300) + }, 1000) } } @@ -252,7 +252,8 @@ onMounted(() => { { :rows="filterFoundFile" :columns="columns" row-key="name" - hide-bottom - :rows-per-page-options="[0]" + :pagination="{ + rowsPerPage: 0, + }" class="cursor" > getFileInfo(nameData.row)"> - + {{ nameData.row.fileName }} @@ -312,7 +318,7 @@ onMounted(() => { - {{ getType(typeData.row.fileType) }} + {{ getType(typeData.row.fileType, typeData.row.fileName) }} @@ -320,7 +326,7 @@ onMounted(() => { - + { flat color="positive" dense - icon="edit" + icon="o_edit" @click=" () => triggerFileEdit(actionData.row, actionData.row.pathname) " @@ -344,7 +350,7 @@ onMounted(() => { flat color="negative" dense - icon="delete" + icon="mdi-trash-can-outline" @click="() => triggerFileDelete(actionData.row.pathname)" id="listViewFileDelete" /> diff --git a/Services/client/src/components/FolderForm.vue b/Services/client/src/components/FolderForm.vue index a41a60f..1383dd2 100644 --- a/Services/client/src/components/FolderForm.vue +++ b/Services/client/src/components/FolderForm.vue @@ -14,13 +14,16 @@ const props = withDefaults( ) const emit = defineEmits(['update:open', 'update:name', 'submit']) -const offensiveWord = ref(false) +const offensiveWord = ref() +const errorMessage = ref('') +const inputNull = ref(false) function checkOffensiveWord(input: string) { return /[\\?%:|"<>#]/.test(input) } function reset() { + offensiveWord.value = undefined emit('update:name', undefined) } @@ -34,8 +37,9 @@ function keydown(e: KeyboardEvent) { function submit() { emit('submit', { mode: props.mode, - name: props.name, + name: props.name?.trim(), }) + emit('update:open', !open), reset() } @@ -50,18 +54,19 @@ onUnmounted(() => window.addEventListener('keydown', keydown)) class="q-pa-md" side="right" tabindex="0" + v-click-outside="() => $emit('update:open', false)" :width="300" :breakpoint="500" :model-value="open" @update:model-value="(v) => $emit('update:open', v)" > - + - + สร้าง{{ tree }} - + แก้ไข{{ tree }} @@ -74,7 +79,6 @@ onUnmounted(() => window.addEventListener('keydown', keydown)) color="red" @click=" () => { - offensiveWord = false $emit('update:open', !open) } " @@ -90,9 +94,14 @@ onUnmounted(() => window.addEventListener('keydown', keydown)) dense class="q-my-sm" placeholder="กรอกชื่อ" - :model-value="name" - error-message="คำต้องห้านจะเปลี่ยนเป็น - เมื่อกดสร้าง" + :model-value="props.name" + :error-message=" + !!props.name + ? 'คำต้องห้านจะเปลี่ยนเป็น - เมื่อกดสร้าง' + : 'โปรดกรอกข้อมูล' + " :error="offensiveWord" + :rules="[(v) => !!props.name]" @update:model-value=" (v) => { $emit('update:name', v) @@ -112,7 +121,6 @@ onUnmounted(() => window.addEventListener('keydown', keydown)) flat @click=" () => { - offensiveWord = false $emit('update:open', false), reset() } " diff --git a/Services/client/src/components/GlobalErrorDialog.vue b/Services/client/src/components/GlobalErrorDialog.vue index 4197bdc..d2d682a 100644 --- a/Services/client/src/components/GlobalErrorDialog.vue +++ b/Services/client/src/components/GlobalErrorDialog.vue @@ -16,15 +16,19 @@ watch(visible, () => { - - {{ - title - }} + + + + + + + + {{ title }} + {{ msg }} + + - {{ msg }} - ({}) const fileFormType = ref<'edit' | 'create'>('create') +const fileFormComponent = ref>() const dialogDeleteState = ref(false) const deleteFormPath = ref('') @@ -176,6 +177,7 @@ async function submitFileForm( fileFormData.value = {} fileFormState.value = false currentParam.value = undefined + fileFormComponent.value?.reset() } const columnsFolder: QTableProps['columns'] = [ @@ -195,7 +197,6 @@ const columnsFolder: QTableProps['columns'] = [ label: 'สร้างโดย', field: 'createdBy', style: 'width: 20px', - sortable: true, }, { @@ -271,13 +272,13 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { triggerFolderCreate()" + @click.stop="() => triggerFolderCreate()" id="listViewFolderCreate" /> @@ -288,10 +289,11 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { bordered :rows="listDataFolder" :columns="columnsFolder" - row-key="name" - hide-bottom - :rows-per-page-options="[0]" + :pagination="{ + rowsPerPage: 20, + }" @row-click="onRowClick" + row-key="name" class="cursor" v-if="currentDept != 4" > @@ -301,6 +303,13 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { {{ nameRow.row.name }} + + + + {{ createdByRow.row.createdBy }} + + + @@ -311,7 +320,13 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { - + { flat color="positive" dense - icon="edit" + icon="o_edit" @click.stop=" triggerFolderEdit( actionsRow.row.name, @@ -334,11 +349,13 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { " id="listViewFolderEdit" /> + @@ -360,13 +377,13 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { v-if="props.mode == 'admin'" outline push - class="q-px-md q-ml-md q-py-sm" + class="q-px-md q-ml-md" label="สร้างเอกสาร" type="submit" color="primary" dense icon="add" - @click="() => triggerFileCreate()" + @click.stop="() => triggerFileCreate()" id="listViewFileCreate" /> @@ -377,8 +394,9 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { :rows="listDataFile" :columns="columnsFile" row-key="name" - hide-bottom - :rows-per-page-options="[0]" + :pagination="{ + rowsPerPage: 20, + }" class="cursor" > @@ -393,7 +411,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { " id="listViewGetFileInfo" > - + {{ nameRow.row.fileName }} @@ -401,7 +419,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { - {{ getType(fileTypeRow.row.fileType) }} + {{ getType(fileTypeRow.row.fileType, fileTypeRow.row.fileName) }} @@ -409,7 +427,12 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { - + { flat color="positive" dense - icon="edit" - @click=" + icon="o_edit" + @click.stop=" () => triggerFileEdit(actionsRow.row, actionsRow.row.pathname) " id="listViewFileEdit" @@ -433,8 +456,8 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { flat color="negative" dense - icon="delete" - @click="() => triggerFileDelete(actionsRow.row.pathname)" + icon="mdi-trash-can-outline" + @click.stop="() => triggerFileDelete(actionsRow.row.pathname)" id="listViewFileDelete" /> @@ -445,6 +468,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { { v-model:description="fileFormData.description" v-model:keyword="fileFormData.keyword" v-model:category="fileFormData.category" + @reset="() => (fileFormError = {})" @filechange="(name: string) => (fileFormError.fileExist = checkFile(name))" @submit="submitFileForm" /> @@ -488,6 +513,10 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => { align-items: center; } +.center-content { + margin-right: 18px; +} + .cursor { cursor: pointer; } diff --git a/Services/client/src/components/PageLayout.vue b/Services/client/src/components/PageLayout.vue index ed84c84..788a52d 100644 --- a/Services/client/src/components/PageLayout.vue +++ b/Services/client/src/components/PageLayout.vue @@ -48,14 +48,20 @@ async function submitFolderForm(value: { } } -onMounted(getCabinet) +onMounted(async () => { + await getCabinet() + + const sessionCurrentPath = sessionStorage.getItem('currentPath') + + if (sessionCurrentPath) await getFolder(sessionCurrentPath) +}) จัดเก็บเอกสาร - สืบค้นเอกสาร + สืบค้นผลงาน @@ -68,16 +74,27 @@ onMounted(getCabinet) { - currentPath = '' - getFolder(currentPath) - } - " > - ตู้จัดเก็บเอกสาร + { + currentPath = '' + getFolder(currentPath) + } + " + >ตู้จัดเก็บเอกสาร + triggerFolderCreate()" + id="createFolder" + /> @@ -117,6 +134,21 @@ onMounted(getCabinet) v-if="currentPath === '/' || !currentPath" label="ตู้เอกสารทั้งหมด" /> + triggerFolderCreate()" + id="createFolder" + /> triggerFolderCreate()" - id="createFolder" - /> (getUsername()) (dropdownOpen = !dropdownOpen)" - class="row q-px-md cursor" + class="row q-px-xs cursor" id="app-toolbar-title" > @@ -17,14 +17,14 @@ const accountName = ref(getUsername()) - + - + {{ accountName }} - เจ้าหน้าที่ + เจ้าหน้าที่ diff --git a/Services/client/src/components/UploadExistDialog.vue b/Services/client/src/components/UploadExistDialog.vue index a34619f..3fdfd81 100644 --- a/Services/client/src/components/UploadExistDialog.vue +++ b/Services/client/src/components/UploadExistDialog.vue @@ -15,19 +15,23 @@ defineEmits(['update:notification', 'confirm', 'cancel']) > - - เตือนพบไฟล์ชื่อซ้ำในระบบ + + + + + + + + ยืนยันการเพิ่มข้อมูล + + พบข้อมูลในระบบ หากดำเนินการต่อ + ข้อมูลที่มีอยู่จะถูกแทนที่ด้วยข้อมูลใหม่ + ต้องการยืนยันการเพิ่มข้อมูลนี้หรือไม่ + + - - หากดำเนินการต่อข้อมูลจะถูกเขียนทับ - - $emit('cancel')" /> $emit('confirm')" /> diff --git a/Services/client/src/lib/directives.ts b/Services/client/src/lib/directives.ts new file mode 100644 index 0000000..b0f9483 --- /dev/null +++ b/Services/client/src/lib/directives.ts @@ -0,0 +1,15 @@ +import type { ObjectDirective } from 'vue' + +export const clickOutside = { + beforeMount(element, binding) { + element.clickOutsideEvent = (e: MouseEvent) => { + if (!(element === e.target || element.contains(e.target))) { + binding.value(e) + } + } + document.addEventListener('click', element.clickOutsideEvent) + }, + unmounted(element) { + document.removeEventListener('click', element.clickOutsideEvent) + }, +} satisfies ObjectDirective diff --git a/Services/client/src/main.ts b/Services/client/src/main.ts index a7cb552..6ab4a8d 100644 --- a/Services/client/src/main.ts +++ b/Services/client/src/main.ts @@ -14,6 +14,11 @@ login().then(async () => { const app = createApp(App) const pinia = createPinia() + app.directive( + 'click-outside', + (await import('@/lib/directives')).clickOutside, + ) + app.use((await import('./router')).default) app.use(pinia) app.use(Quasar, { diff --git a/Services/client/src/modules/01_user/components/AdvancedSearch.vue b/Services/client/src/modules/01_user/components/AdvancedSearch.vue index abf184c..1f5c1b3 100644 --- a/Services/client/src/modules/01_user/components/AdvancedSearch.vue +++ b/Services/client/src/modules/01_user/components/AdvancedSearch.vue @@ -93,10 +93,10 @@ function clearAdvSearchData() { v-for="(item, index) in advSearchDataRow" :key="index" > - + (item.value = '')" class="cursor-pointer" @@ -142,25 +143,16 @@ function clearAdvSearchData() { - + delAdvSearchData(index)" id="delAdvSearchData" - > - - ไม่สามารถลบได้ - - + /> @@ -172,24 +164,34 @@ function clearAdvSearchData() { outlined dense v-model="advSearchDataField.keyword" - placeholder="คำสำคัญ" use-input use-chips multiple hide-dropdown-icon input-debounce="0" - new-value-mode="add" - /> + new-value-mode="add-unique" + >คำสำคัญ: + >รายละเอียด: + (advSearchDataField.description = '')" + class="cursor-pointer" + /> diff --git a/Services/client/src/modules/01_user/components/FileDownload.vue b/Services/client/src/modules/01_user/components/FileDownload.vue index 1ad279f..7d1e19e 100644 --- a/Services/client/src/modules/01_user/components/FileDownload.vue +++ b/Services/client/src/modules/01_user/components/FileDownload.vue @@ -10,6 +10,8 @@ import FileIcon from '@/components/FileIcon.vue' const { isFilePreview, fileInfo } = storeToRefs(useFileInfoStore()) const { getType, getFormatDate, getSize } = useFileInfoStore() +const filePath = + (fileInfo.value?.pathname || '').split('/').slice(0, -1).join(' / ') + ' / ' async function downloadSubmit(path: string | undefined) { if (path) { @@ -66,6 +68,9 @@ async function downloadSubmit(path: string | undefined) { {{ fileInfo?.title }} + + {{ filePath }} + @@ -86,8 +91,9 @@ async function downloadSubmit(path: string | undefined) { @@ -113,7 +119,7 @@ async function downloadSubmit(path: string | undefined) { - + ชื่อไฟล์ @@ -122,7 +128,7 @@ async function downloadSubmit(path: string | undefined) { - + ชื่อเรื่อง @@ -131,7 +137,7 @@ async function downloadSubmit(path: string | undefined) { - + รายละเอียด @@ -140,7 +146,7 @@ async function downloadSubmit(path: string | undefined) { - + กลุ่ม/หมวดหมู่ @@ -149,7 +155,7 @@ async function downloadSubmit(path: string | undefined) { - + คำสำคัญ @@ -158,7 +164,7 @@ async function downloadSubmit(path: string | undefined) { - + ขนาดไฟล์ @@ -167,16 +173,18 @@ async function downloadSubmit(path: string | undefined) { - + ประเภทไฟล์ - {{ getType(fileInfo?.fileType) }} + {{ + getType(fileInfo?.fileType, fileInfo?.fileName) + }} - + วันที่อัปโหลด diff --git a/Services/client/src/modules/01_user/components/SearchBar.vue b/Services/client/src/modules/01_user/components/SearchBar.vue index 824bfde..ccfaea8 100644 --- a/Services/client/src/modules/01_user/components/SearchBar.vue +++ b/Services/client/src/modules/01_user/components/SearchBar.vue @@ -2,14 +2,17 @@ import { ref, watch } from 'vue' import { storeToRefs } from 'pinia' import axiosClient from '@/services/HttpService' +import mime from 'mime' import type { EhrFile } from '@/stores/tree-data' import { useSearchDataStore } from '@/stores/searched-data' import { useLoader } from '@/stores/loader' import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue' +import { useFileInfoStore } from '@/stores/file-info-data' const loaderStore = useLoader() +const { isFilePreview } = storeToRefs(useFileInfoStore()) const { isSearch, isAdvSearchCall, @@ -37,6 +40,7 @@ const props = defineProps<{ }>() async function searchSubmit() { + isFilePreview.value = false if (searchData.value.value.trim() !== '') { submitSearchData.value = { AND: [], OR: [] } if (props.mode === 'admin') { @@ -46,12 +50,19 @@ async function searchSubmit() { value: searchData.value.value, }) }) + submitSearchData.value.OR.push({ + field: 'fileName', + value: searchData.value.value, + }) + submitSearchData.value.OR.push({ + field: 'fileType', + value: mime.getType(searchData.value.value) || '', + }) } else { submitSearchData.value.OR.push({ field: searchData.value.field, value: searchData.value.value, }) - if (isAdvSearchCall.value) { let advField = advSearchDataField.value let advRow = advSearchDataRow.value @@ -106,6 +117,15 @@ watch( } }, ) + +watch( + () => searchData.value.value, + (search) => { + if (search.length === 0) { + isSearch.value = false + } + }, +) @@ -115,11 +135,14 @@ watch( outlined dense label="ค้นหา" + debounce="500" bg-color="white" v-model="searchData.value" id="inputSearch" + @update:model-value="searchSubmit" @keydown.enter.prevent="searchSubmit" > + @@ -153,6 +176,7 @@ watch( > ((searchData.value = ''), (isSearch = false))" class="cursor-pointer" diff --git a/Services/client/src/services/KeyCloakService.ts b/Services/client/src/services/KeyCloakService.ts index e16335e..8c7ce11 100644 --- a/Services/client/src/services/KeyCloakService.ts +++ b/Services/client/src/services/KeyCloakService.ts @@ -1,6 +1,6 @@ import Keycloak from 'keycloak-js' -const keycloak = new Keycloak() +const keycloak = new Keycloak('/keycloak.json') export async function login(cb?: (...args: any[]) => void) { const auth = await keycloak diff --git a/Services/client/src/stores/file-info-data.ts b/Services/client/src/stores/file-info-data.ts index 1b7ec41..f030f9f 100644 --- a/Services/client/src/stores/file-info-data.ts +++ b/Services/client/src/stores/file-info-data.ts @@ -1,10 +1,11 @@ import { ref } from 'vue' import { defineStore } from 'pinia' +import mime from 'mime' import type { EhrFile } from '@/stores/tree-data' export interface MimeMap { - [key: string]: { icon: string; color: string; type: string } + [key: string]: { icon: string; color: string } } export interface TypeSetting { [key: string]: { icon: string; color: string } @@ -13,129 +14,118 @@ export interface TypeSetting { export const useFileInfoStore = defineStore('info', () => { const fileInfo = ref() const isFilePreview = ref(false) - const file: TypeSetting = { + const fileIcon: TypeSetting = { word: { icon: 'mdi-file-word-outline', color: 'blue-11' }, excel: { icon: 'mdi-file-excel-outline', color: 'green-4' }, powerpoint: { icon: 'mdi-file-powerpoint-outline', color: 'orange-4' }, - pdf: { icon: 'mdi-file-document-outline', color: 'red-11' }, + pdf: { icon: 'mdi-file-pdf-outline', color: 'red-11' }, txt: { icon: 'mdi-file-document-outline', color: 'blue-11' }, image: { icon: 'mdi-file-image-outline', color: 'blue-11' }, } const mimeFileMapping: MimeMap = { 'application/msword': { - ...file.word, - type: '.doc', + ...fileIcon.word, }, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': { - ...file.word, - type: '.docx', + ...fileIcon.word, }, 'application/vnd.openxmlformats-officedocument.wordprocessingml.template': { - ...file.word, - type: '.dotx', + ...fileIcon.word, }, 'application/vnd.ms-word.document.macroEnabled.12': { - ...file.word, - type: '.docm', + ...fileIcon.word, }, 'application/vnd.ms-word.template.macroEnabled.12': { - ...file.word, - type: '.dotm', + ...fileIcon.word, }, 'application/vnd.ms-excel': { - ...file.excel, - type: '.xls', + ...fileIcon.excel, }, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { - ...file.excel, - type: '.xlsx', + ...fileIcon.excel, }, 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': { - ...file.excel, - type: '.xltx', + ...fileIcon.excel, }, 'application/vnd.ms-excel.sheet.macroEnabled.12': { - ...file.excel, - type: '.xlsm', + ...fileIcon.excel, }, 'application/vnd.ms-excel.template.macroEnabled.12': { - ...file.excel, - type: '.xltm', + ...fileIcon.excel, }, 'application/vnd.ms-excel.addin.macroEnabled.12': { - ...file.excel, - type: '.xlam', + ...fileIcon.excel, }, 'application/vnd.ms-excel.sheet.binary.macroEnabled.12': { - ...file.excel, - type: '.xlsb', + ...fileIcon.excel, }, 'application/vnd.ms-powerpoint': { - ...file.powerpoint, - type: '.ppt', + ...fileIcon.powerpoint, }, 'application/vnd.openxmlformats-officedocument.presentationml.presentation': { - ...file.powerpoint, - type: '.pptx', + ...fileIcon.powerpoint, }, 'application/vnd.openxmlformats-officedocument.presentationml.template': { - ...file.powerpoint, - type: '.potx', + ...fileIcon.powerpoint, }, 'application/vnd.openxmlformats-officedocument.presentationml.slideshow': { - ...file.powerpoint, - type: '.ppsx', + ...fileIcon.powerpoint, }, 'application/vnd.ms-powerpoint.addin.macroEnabled.12': { - ...file.powerpoint, - type: '.ppam', + ...fileIcon.powerpoint, }, 'application/vnd.ms-powerpoint.presentation.macroEnabled.12': { - ...file.powerpoint, - type: '.pptm', + ...fileIcon.powerpoint, }, 'application/vnd.ms-powerpoint.template.macroEnabled.12': { - ...file.powerpoint, - type: '.potm', + ...fileIcon.powerpoint, }, 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': { - ...file.powerpoint, - type: '.ppsm', + ...fileIcon.powerpoint, }, 'application/pdf': { - ...file.pdf, - type: '.pdf', + ...fileIcon.pdf, }, 'text/plain': { - ...file.txt, - type: '.txt', + ...fileIcon.txt, }, - 'image/x-png': { - ...file.image, - type: '.png', + 'image/png': { + ...fileIcon.image, }, - 'image/x-citrix-jpeg': { - ...file.image, - type: '.jpg, jpeg', + 'image/jpeg': { + ...fileIcon.image, }, } - function getType(mimeType: string | undefined): string { - if (!!mimeType && mimeFileMapping.hasOwnProperty(mimeType)) { - return mimeFileMapping[mimeType].type + function getType( + mimeType: string | undefined, + fileName: string | undefined, + ): string { + if (mimeType === undefined) { + return 'ไม่ทราบประเภท' } - return 'unknown type' + + const extFomMime = mime.getExtension(mimeType) + if (extFomMime) { + return '.' + extFomMime + } + if (fileName && fileName.includes('.')) { + const dotIndex = fileName.lastIndexOf('.') + const extension = fileName.substring(dotIndex) + return extension + } + return 'ไม่ทราบประเภท' } function getFormatDate(dateTime: string | undefined): string { if (dateTime === undefined) { - return 'unknown date' + return 'ไม่ทราบวันที่' } const date = new Date(dateTime) return date.toLocaleDateString('th-TH', { @@ -145,19 +135,9 @@ export const useFileInfoStore = defineStore('info', () => { }) } - function getFileNameFormat(fileName: string | undefined): string { - if (fileName === undefined) { - return 'unknow name' - } - const dotIndex = fileName.lastIndexOf('.') - const fileNameOnly = fileName.substring(0, dotIndex) - - return fileNameOnly - } - function getSize(size: string | undefined): string { if (size === undefined) { - return 'unknow size' + return 'ไม่ทราบขนาด' } const units = ['B', 'KB', 'MB', 'GB', 'TB'] let i = 0 @@ -169,7 +149,7 @@ export const useFileInfoStore = defineStore('info', () => { return sizeNumber.toFixed(2) + ' ' + units[i] } - async function getFileInfo(data: EhrFile) { + function getFileInfo(data: EhrFile) { isFilePreview.value = true fileInfo.value = data } @@ -180,7 +160,6 @@ export const useFileInfoStore = defineStore('info', () => { fileInfo, getType, getFormatDate, - getFileNameFormat, getSize, getFileInfo, } diff --git a/Services/client/src/stores/tree-data.ts b/Services/client/src/stores/tree-data.ts index e0f2a49..a55f60c 100644 --- a/Services/client/src/stores/tree-data.ts +++ b/Services/client/src/stores/tree-data.ts @@ -62,6 +62,8 @@ export const useTreeDataStore = defineStore('changeCabinet', () => { async function getFolder(pathname: string, updateStatus = true) { loader.show() + sessionStorage.setItem('currentPath', pathname) + const pathArray: string[] = pathname.split('/').filter(Boolean) currentDept.value = pathArray.length diff --git a/Services/client/src/views/ErrorNotFoundPage.vue b/Services/client/src/views/ErrorNotFoundPage.vue index 4768349..4fc018b 100644 --- a/Services/client/src/views/ErrorNotFoundPage.vue +++ b/Services/client/src/views/ErrorNotFoundPage.vue @@ -7,15 +7,13 @@ export default defineComponent({ - + - ไม่พบหน้าที่ต้องการ - (404 Page Not Found) + 404 + ไม่พบหน้าที่ต้องการ - + { currentPath = '' diff --git a/Services/client/tests-examples/demo-todo-app.spec.ts b/Services/client/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 2fd6016..0000000 --- a/Services/client/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -]; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} diff --git a/Services/client/tests/README.md b/Services/client/tests/README.md new file mode 100644 index 0000000..be97ba8 --- /dev/null +++ b/Services/client/tests/README.md @@ -0,0 +1,35 @@ +# Test by playwrigth + +## คำสั่งต่างๆ + +คำสั่งที่ใช้ในการ รัน เช่น + + +``` +npx playwright test tests/folder-CRUD.spec.ts +``` +playwrigth ui +``` +npx playwright test --ui +``` + +tests/folder-CRUD.spec.ts คือตำเเหน่งของไฟล์ + +## ไฟล์ test +ไฟล์งานอยู่ใน Directory tests +- delete-file-grid-view.spec.ts `ทดสอบการสร้างไฟล์ใน folder และลบ` +``` +npx playwright test tests/delete-file-grid-view.spec.ts +``` +- folder-CRUD.spec.ts `ทดสอบการสร้างตู้ ลิ้นชัก folder subfolder แล้วเข้าไปในสุดจากนั้นออกมกเเก้ไขเเล้วลบที่ละรำดับ` + +``` +npx playwright test tests/folder-CRUD.spec.ts +``` + + + + + + + diff --git a/Services/client/tests/delete-file-grid-view.spec.ts b/Services/client/tests/delete-file-grid-view.spec.ts new file mode 100644 index 0000000..7aee17d --- /dev/null +++ b/Services/client/tests/delete-file-grid-view.spec.ts @@ -0,0 +1,101 @@ +import { test, expect, Page } from '@playwright/test' + +test.describe.configure({ mode: 'serial' }) + +let page: Page + +test.beforeAll(async ({ browser }) => { + page = await browser.newPage() +}) + +test.afterAll(async () => { + await page.reload() + await page.click("//button[@data-testid='actiontest-delete-file/']") + await page.click("(//div[@id='FileltemActiondelete']//div)[2]") + await page.click("(//button[@id='dialogDeleteConfirm']//span)[2]") + await page.close() +}) + +test('Login', async ({}) => { + await page.goto('http://localhost:3010/admin') + + await expect(page).toHaveTitle('Sign in to EDM') + await page.fill("input[name='username']", 'admin') + await page.fill("input[name='password']", 'P@ssw0rd') + await page.click("input[name='login']") +}) + +test('Create Cabinet', async () => { + await page.click("//div[@id='triggerFolderCreateFileItem']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", 'test-delete-file') + await page.click("//button[@type='submit']") + await expect(page.locator("(//div[@class='col'])[3]")).toContainText( + /test-delete-file/, + ) +}) + +test('Go into Cabinet', async () => { + await page.click("//div[@data-pathname='test-delete-file/']") + await expect( + page.locator("//div[contains(@class,'flex items-center')]//div[1]"), + ).toContainText(/test-delete-file/) +}) + +test('Create Drawer', async () => { + await page.click("//div[@id='triggerFolderCreateFileItem']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", 'test-delete-file') + await page.click("//button[@type='submit']") + await expect(page.locator("(//div[@class='col'])[3]")).toContainText( + /test-delete-file/, + ) +}) + +test('Go into Drawer', async () => { + await page.click("//div[@data-pathname='test-delete-file/test-delete-file/']") + await expect( + page.locator("//div[contains(@class,'flex items-center')]//div[3]"), + ).toContainText(/test-delete-file/) +}) + +test('Create Folder', async () => { + await page.click("//div[@id='triggerFolderCreateFileItem']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", 'test-delete-file') + await page.click("//button[@type='submit']") + await expect(page.locator("(//div[@class='col'])[3]")).toContainText( + /test-delete-file/, + ) +}) + +test('Go into Folder', async () => { + await page.click( + "//div[@data-pathname='test-delete-file/test-delete-file/test-delete-file/']", + ) + await expect( + page.locator("//div[contains(@class,'flex items-center')]//div[5]"), + ).toContainText(/test-delete-file/) +}) + +test('Upload File', async () => { + await page.click("//div[@id='triggerFileCreateFileItem']") + await page.setInputFiles("//input[@type='file']", 'tests/file.txt') + await page.fill("//input[@placeholder='กรอกชื่อเรื่อง']", 'test-delete-file') + await page.click("(//form[@class='q-form']//button)[2]") + await page.waitForTimeout(3000) + await expect( + page.locator( + "//div[@data-pathname='test-delete-file/test-delete-file/test-delete-file/file.txt']", + ), + ).toContainText(/test-delete-file/) +}) + +test('Delete File', async () => { + await page.click( + "//button[@data-testid='actiontest-delete-file/test-delete-file/test-delete-file/file.txt']", + ) + await page.click("//div[@id='FileltemActiondelete']") + await page.click("//button[@id='dialogDeleteConfirm']") + await page.waitForTimeout(3000) + await expect( + page.locator("(//div[@class='grid q-mt-md'])[2]"), + ).not.toContainText(/test-delete-file/) +}) diff --git a/Services/client/tests/example.spec.ts b/Services/client/tests/example.spec.ts deleted file mode 100644 index 54a906a..0000000 --- a/Services/client/tests/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/Services/client/tests/file.txt b/Services/client/tests/file.txt new file mode 100644 index 0000000..0d09132 --- /dev/null +++ b/Services/client/tests/file.txt @@ -0,0 +1 @@ +Test by Playwright! diff --git a/Services/client/tests/folder-CRUD.spec.ts b/Services/client/tests/folder-CRUD.spec.ts new file mode 100644 index 0000000..a6d9bc2 --- /dev/null +++ b/Services/client/tests/folder-CRUD.spec.ts @@ -0,0 +1,277 @@ +import { test, expect, Page } from '@playwright/test' +import { nanoid } from 'nanoid' + +test.describe.configure({ mode: 'serial' }) +const cabinet = nanoid() +const drawer = nanoid() +const folder = nanoid() +const subfolder = nanoid() +const newName = nanoid() + +let page: Page + +test.beforeAll(async ({ browser }) => { + page = await browser.newPage() +}) + +test.afterAll(async () => { + await page.close() +}) + +test('Login', async () => { + await page.goto('http://localhost:3010/admin') + await expect(page).toHaveTitle('Sign in to EDM') + await page.fill("input[name='username']", 'oom') + await page.fill("input[name='password']", 'oom') + await page.click("input[name='login']") + + await page.waitForTimeout(2000) +}) + +test('Create Cabinet', async () => { + await page.waitForTimeout(2000) + await page.click("//div[@id='triggerFolderCreateFileItem']") + + await page.fill( + "//input[@placeholder='กรอกชื่อ']", + cabinet, + ) + await page.click("//button[@type='submit']") + + await page.waitForTimeout(300) + + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(cabinet) + + await page.waitForTimeout(300) +}) + +test('Go to Cabinet', async () => { + await page.click(`(//div[text()='${cabinet}'])[2]`) +}) + +test('Create drawer', async () => { + await page.click("//div[@id='triggerFolderCreateFileItem']") + await page.fill( + "//input[@placeholder='กรอกชื่อ']", + drawer, + ) + await page.click("//button[@type='submit']") + + await page.waitForTimeout(300) + + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(drawer) + + await page.waitForTimeout(300) +}) + +test('Go into drawer', async () => { + await page.click(`(//div[text()='${drawer}'])[2]`) + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).not.toHaveText(drawer) +}) + +test('Create Folder', async () => { + await page.click("//div[@id='triggerFolderCreateFileItem']") + await page.fill( + "//input[@placeholder='กรอกชื่อ']", + folder, + ) + await page.click("//button[@type='submit']") + await page.waitForTimeout(300) + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(folder) +}) + +test('Go into Folder', async () => { + await page.click(`(//div[text()='${folder}'])[2]`) + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).not.toHaveText(folder) +}) + +test('Create Subfolder', async () => { + await page.click("//div[@id='triggerFolderCreateFileItem']") + await page.fill( + "//input[@placeholder='กรอกชื่อ']", + subfolder, + ) + await page.click("(//button[@type='submit'])[3]") + await page.waitForTimeout(300) + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(subfolder) +}) + +test('Go into Subfolder', async () => { + + await page.click(`//div[text()='${subfolder}']`) + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).not.toHaveText(folder) +}) + +test('Back to Folder', async () => { + await page.click("//i[text()='arrow_back']") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(subfolder) +}) + +test('Edit Subfolder', async () => { + await page.click( + `//button[@data-testid='action${cabinet}/${drawer}/${folder}/${subfolder}/']`, + ) + await page.click("(//div[@role='listitem'])[1]") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", newName) + await page.click("(//button[@id='FoldeSubmit'])[1]") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(newName) +}) + +test('Delete Subfolder Cancel', async () => { + await page.click( + `//button[@data-testid='action${cabinet}/${drawer}/${folder}/${newName}/']`, + ) + await page.click("(//div[@role='listitem'])[2]") + await page.click("(//div[@class='q-space']/following-sibling::button)[2]") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(newName) +}) + +test('Delete Subfolder Confirm', async () => { + await page.click( + `//button[@data-testid='action${cabinet}/${drawer}/${folder}/${newName}/']`, + ) + await page.click("(//div[@role='listitem'])[2]") + await page.click( + "(//div[contains(@class,'q-card__actions justify-end')]//button)[2]", + ) + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).not.toHaveText(newName) +}) + +test('Back to drawer', async () => { + await page.click("//i[text()='arrow_back']") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(folder) +}) + +test('Edit Folder', async () => { + await page.click( + `//button[@data-testid='action${cabinet}/${drawer}/${folder}/']`, + ) + await page.click("(//div[@role='listitem'])[1]") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", newName) + await page.click("(//button[@id='FoldeSubmit'])[1]") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(newName) +}) + +test('Delete Folder Cancel', async () => { + await page.click( + `//button[@data-testid='action${cabinet}/${drawer}/${newName}/']`, + ) + await page.click("(//div[@role='listitem'])[2]") + await page.click("(//div[@class='q-space']/following-sibling::button)[2]") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(newName) +}) + +test('Delete Folder Confirm', async () => { + await page.click( + `//button[@data-testid='action${cabinet}/${drawer}/${newName}/']`, + ) + await page.click("(//div[@role='listitem'])[2]") + await page.click( + "(//div[contains(@class,'q-card__actions justify-end')]//button)[2]", + ) + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).not.toHaveText(newName) +}) + +test('Back to Cabinet', async () => { + await page.click("//i[text()='arrow_back']") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(drawer) +}) + +test('Edit drawer', async () => { + await page.click(`//button[@data-testid='action${cabinet}/${drawer}/']`) + await page.click("(//div[@role='listitem'])[1]") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", newName) + await page.click("(//button[@id='FoldeSubmit'])[1]") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(newName) +}) + +test('Delete Drawer Cancel', async () => { + await page.click(`//button[@data-testid='action${cabinet}/${newName}/']`) + await page.click("(//div[@role='listitem'])[2]") + await page.click("(//div[@class='q-space']/following-sibling::button)[2]") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(newName) +}) + +test('Delete Drawer Confirm', async () => { + await page.click(`//button[@data-testid='action${cabinet}/${newName}/']`) + await page.click("(//div[@role='listitem'])[2]") + await page.click( + "(//div[contains(@class,'q-card__actions justify-end')]//button)[2]", + ) + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).not.toHaveText(newName) +}) + +test('Back to Home', async () => { + await page.click("//i[text()='arrow_back']") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(cabinet) +}) + +test('Edit Cabinet', async () => { + await page.click(`//button[@data-testid='action${cabinet}/']`) + await page.click("(//div[@role='listitem'])[1]") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", newName) + await page.click("(//button[@id='FoldeSubmit'])[1]") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(newName) +}) + +test('Delete Cabinet Cancel', async () => { + await page.click(`//button[@data-testid='action${newName}/']`) + await page.click("(//div[@role='listitem'])[2]") + await page.click("(//div[@class='q-space']/following-sibling::button)[2]") + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).toContainText(newName) +}) + +test('Delete Cabinet Confirm', async () => { + await page.click(`//button[@data-testid='action${newName}/']`) + await page.click("(//div[@role='listitem'])[2]") + await page.click( + "(//div[contains(@class,'q-card__actions justify-end')]//button)[2]", + ) + await expect( + page.locator("(//div[contains(@class,'bg-white rounded-borders')])[2]"), + ).not.toHaveText(newName) +}) diff --git a/Services/client/tests/listview.spec.ts b/Services/client/tests/listview.spec.ts new file mode 100644 index 0000000..0e99731 --- /dev/null +++ b/Services/client/tests/listview.spec.ts @@ -0,0 +1,96 @@ +import { test, expect, Page } from '@playwright/test' + +test.describe.configure({ mode: 'serial' }) + +let page: Page + +test.beforeAll(async ({ browser }) => { + page = await browser.newPage() +}) + +test.afterAll(async () => { + await page.close() +}) + +test('Login', async ({}) => { + await page.goto('http://localhost:3010/admin') + await expect(page).toHaveTitle('Sign in to EDM') + await page.fill("input[name='username']", 'admin') + await page.fill("input[name='password']", 'P@ssw0rd') + await page.click("input[name='login']") +}) + +test('listViewMode', async () => { + await page.click("(//i[@role='img'])[2]") +}) + +test('Create Cabinet', async () => { + await page.click("//span[text()='สร้างตู้เก็บเอกสาร']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[2]", 'oat-test') + await page.click("(//span[text()='บันทึก'])[3]") +}) + +test('Go into Cabinet', async () => { + await page.click('//td[contains(text(),"oat-test")]') +}) + +test('Create Drawer', async () => { + await page.click("//span[text()='สร้างลิ้นชัก']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", 'oat-drawer2') + await page.click("(//button[@type='submit'])[3]") +}) + +test('Go into Drawer', async () => { + await page.click("//table[@class='q-table']/tbody[1]/tr[1]/td[1]") +}) + +test('Create Folder', async () => { + await page.click("//span[text()='สร้างแฟ้ม']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", 'oat-folder2') + await page.click("(//span[text()='บันทึก'])[2]") +}) + +test('Go into Folder', async () => { + await page.click("//table[@class='q-table']") +}) + +test('Create subFolder', async () => { + await page.click("//span[text()='สร้างแฟ้มย่อย']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]","oat-subfolder2") + await page.click("(//span[text()='บันทึก'])[2]") +}) + +test('Go into SubFolder', async () => { + await page.click("//table[@class='q-table']") +}) + +test('Upload File', async () => { + await page.click("//span[text()='สร้างเอกสาร']") + await page.locator("//input[@type='file']").setInputFiles('tests/test.docx') + await page.fill("//input[@placeholder='กรอกชื่อเรื่อง']",'oattest2') + await page.click("(//button[@type='submit'])[2]") + await page.waitForTimeout(3000) + await page.click("//i[text()='refresh']") + await page.waitForTimeout(1000) +}) + +test('Go into File', async () => { + await page.click("//table[@class='q-table']/tbody[1]/tr[1]/td[1]") +}) + +test('Download File', async () => { + await page.click("//span[text()='ดาวน์โหลด']") + await page.waitForTimeout(2000) +}) + +test('Go Back to MainLayout', async () => { + await page.click("//i[text()='arrow_back']") + await page.click("//span[text()='ตู้จัดเก็บเอกสาร']") +}) + +test('Delete Cabinet', async () => { + await page.click("//button[@data-testid='oat-test']") + await page.click("//span[text()='ลบ']") + await page.waitForTimeout(2000) +}) + diff --git a/Services/client/tests/test.docx b/Services/client/tests/test.docx new file mode 100644 index 0000000..041bbcb Binary files /dev/null and b/Services/client/tests/test.docx differ diff --git a/Services/client/tests/upload-file-grid-view.spec.ts b/Services/client/tests/upload-file-grid-view.spec.ts new file mode 100644 index 0000000..1ce4571 --- /dev/null +++ b/Services/client/tests/upload-file-grid-view.spec.ts @@ -0,0 +1,89 @@ +import { test, expect, Page } from '@playwright/test' + +test.describe.configure({ mode: 'serial' }) + +let page: Page + +test.beforeAll(async ({ browser }) => { + page = await browser.newPage() +}) + +test.afterAll(async () => { + await page.reload() + await page.click("//button[@data-testid='actiontest-upload-file/']") + await page.click("(//div[@id='FileltemActiondelete']//div)[2]") + await page.click("(//button[@id='dialogDeleteConfirm']//span)[2]") + await page.close() +}) + +test('Login', async ({}) => { + await page.goto('http://localhost:3010/admin') + + await expect(page).toHaveTitle('Sign in to EDM') + await page.fill("input[name='username']", 'admin') + await page.fill("input[name='password']", 'P@ssw0rd') + await page.click("input[name='login']") +}) + +test('Create Cabinet', async () => { + await page.click("//div[@id='triggerFolderCreateFileItem']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", 'test-upload-file') + await page.click("//button[@type='submit']") + await expect(page.locator("(//div[@class='col'])[3]")).toContainText( + /test-upload-file/, + ) +}) + +test('Go into Cabinet', async () => { + await page.click("//div[@data-pathname='test-upload-file/']") + await expect( + page.locator("//div[contains(@class,'flex items-center')]//div[1]"), + ).toContainText(/test-upload-file/) +}) + +test('Create Drawer', async () => { + await page.click("//div[@id='triggerFolderCreateFileItem']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", 'test-upload-file') + await page.click("//button[@type='submit']") + await expect(page.locator("(//div[@class='col'])[3]")).toContainText( + /test-upload-file/, + ) +}) + +test('Go into Drawer', async () => { + await page.click("//div[@data-pathname='test-upload-file/test-upload-file/']") + await expect( + page.locator("//div[contains(@class,'flex items-center')]//div[3]"), + ).toContainText(/test-upload-file/) +}) + +test('Create Folder', async () => { + await page.click("//div[@id='triggerFolderCreateFileItem']") + await page.fill("(//input[@placeholder='กรอกชื่อ'])[1]", 'test-upload-file') + await page.click("//button[@type='submit']") + await expect(page.locator("(//div[@class='col'])[3]")).toContainText( + /test-upload-file/, + ) +}) + +test('Go into Folder', async () => { + await page.click( + "//div[@data-pathname='test-upload-file/test-upload-file/test-upload-file/']", + ) + await expect( + page.locator("//div[contains(@class,'flex items-center')]//div[5]"), + ).toContainText(/test-upload-file/) +}) + +test('Upload File', async () => { + await page.click("//div[@id='triggerFileCreateFileItem']") + await page.setInputFiles("//input[@type='file']", 'tests/file.txt') + await page.fill("//input[@placeholder='กรอกชื่อเรื่อง']", 'test-upload-file') + await page.click("(//form[@class='q-form']//button)[2]") + await page.waitForTimeout(3000) + await expect( + page.locator( + "//div[@data-pathname='test-upload-file/test-upload-file/test-upload-file/file.txt']", + ), + ).toContainText(/test-upload-file/) +}) diff --git a/Services/server/package.json b/Services/server/package.json index 669e1c2..6598d58 100644 --- a/Services/server/package.json +++ b/Services/server/package.json @@ -28,6 +28,7 @@ "minio": "^7.1.3", "prettier": "^3.1.0", "promise.any": "^2.0.6", + "socket.io": "^4.7.2", "swagger-ui-express": "^5.0.0", "tsoa": "^5.1.1" }, diff --git a/Services/server/pnpm-lock.yaml b/Services/server/pnpm-lock.yaml index 40bc60c..25109c4 100644 --- a/Services/server/pnpm-lock.yaml +++ b/Services/server/pnpm-lock.yaml @@ -38,6 +38,9 @@ dependencies: promise.any: specifier: ^2.0.6 version: 2.0.6 + socket.io: + specifier: ^4.7.2 + version: 4.7.2 swagger-ui-express: specifier: ^5.0.0 version: 5.0.0(express@4.18.2) @@ -154,6 +157,10 @@ packages: engines: {node: '>=8'} dev: false + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: false + /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -215,11 +222,14 @@ packages: dependencies: '@types/node': 20.9.0 + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + dev: false + /@types/cors@2.8.17: resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} dependencies: '@types/node': 20.9.0 - dev: true /@types/express-serve-static-core@4.17.41: resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} @@ -420,6 +430,11 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -576,6 +591,11 @@ packages: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: false + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -706,6 +726,31 @@ packages: engines: {node: '>= 0.8'} dev: false + /engine.io-parser@5.2.1: + resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} + engines: {node: '>=10.0.0'} + dev: false + + /engine.io@6.5.4: + resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==} + engines: {node: '>=10.2.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.9.0 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.2.1 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /es-abstract@1.22.3: resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} engines: {node: '>= 0.4'} @@ -1786,6 +1831,42 @@ packages: semver: 7.5.4 dev: true + /socket.io-adapter@2.5.2: + resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} + dependencies: + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /socket.io@4.7.2: + resolution: {integrity: sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==} + engines: {node: '>=10.2.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4 + engine.io: 6.5.4 + socket.io-adapter: 2.5.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -2192,6 +2273,19 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: false + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} diff --git a/Services/server/src/app.ts b/Services/server/src/app.ts index d0d7c7b..fdff76d 100644 --- a/Services/server/src/app.ts +++ b/Services/server/src/app.ts @@ -2,6 +2,8 @@ import "dotenv/config"; import express from "express"; import swaggerUi from "swagger-ui-express"; import cors from "cors"; +import { createServer } from "http"; +import { Server } from "socket.io"; import { RegisterRoutes } from "./routes"; import errorHandler from "./middlewares/exception"; @@ -9,6 +11,7 @@ import rabbitmq from "./rabbitmq"; import swaggerSpecs from "./swagger.json"; import { handler as amqHandler } from "./rabbitmq/handler"; +import { setInstance } from "./lib/websocket"; const PORT = +(process.env.PORT || 80); @@ -31,7 +34,24 @@ app.use((_req, res, _next) => { res.sendFile(`${process.cwd()}/static/index.html`); }); -app.listen(PORT, "0.0.0.0", () => +const server = createServer(app); +const io = new Server(server, { + cors: { + origin: "*", + }, +}); + +setInstance(io); + +io.on("connection", (socket) => { + console.log("User Connected"); + + socket.on("disconnected", () => { + console.log("User Disconnected"); + }); +}); + +server.listen(PORT, "0.0.0.0", () => console.log(`[APP] Application is running on http://localhost:${PORT}`), ); diff --git a/Services/server/src/controllers/cabinetController.ts b/Services/server/src/controllers/cabinetController.ts index 463cb43..8d20f3f 100644 --- a/Services/server/src/controllers/cabinetController.ts +++ b/Services/server/src/controllers/cabinetController.ts @@ -22,6 +22,7 @@ import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/mi import HttpStatusCode from "../interfaces/http-status"; import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; +import { getInstance } from "../lib/websocket"; const DEFAULT_BUCKET = process.env.MINIO_BUCKET; const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; @@ -86,6 +87,9 @@ export class CabinetController extends Controller { if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + const io = getInstance(); + io?.emit("CreateFolder", { pathname: `${replaceIllegalChars(body.name)}/` }); + return this.setStatus(HttpStatusCode.CREATED); } @@ -155,6 +159,9 @@ export class CabinetController extends Controller { }), ); + const io = getInstance(); + io?.emit("EditFolder", { from: `${cabinetName}/`, to: `${replaceIllegalChars(body.name)}/` }); + return this.setStatus(HttpStatusCode.NO_CONTENT); } @@ -180,6 +187,26 @@ export class CabinetController extends Controller { stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); + const io = getInstance(); + io?.emit("DeleteFolder", { pathname: `${cabinetName}/` }); + return this.setStatus(HttpStatusCode.NO_CONTENT); } + + /** + * @example cabinetName "ตู้เอกสาร 1" + */ + @Get("/{cabinetName}/size") + @Tags("ตู้เอกสาร") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + public async calc(@Path() cabinetName: string) { + const list = await listItem(DEFAULT_BUCKET!, `${cabinetName}/`, true).catch((e) => + console.error(`Error List Folder: ${e}`), + ); + + if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"); + + return { size: list.reduce((a, c) => a + c.size, 0) }; + } } diff --git a/Services/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts index 9d3a505..1311697 100644 --- a/Services/server/src/controllers/drawerController.ts +++ b/Services/server/src/controllers/drawerController.ts @@ -23,6 +23,7 @@ import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from " import HttpStatusCode from "../interfaces/http-status"; import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; import HttpError from "../interfaces/http-error"; +import { getInstance } from "../lib/websocket"; const DEFAULT_BUCKET = process.env.MINIO_BUCKET; const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; @@ -101,6 +102,9 @@ export class DrawerController extends Controller { if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + const io = getInstance(); + io?.emit("CreateFolder", { pathname: `${basePath}${replaceIllegalChars(body.name)}/` }); + return this.setStatus(HttpStatusCode.CREATED); } @@ -172,6 +176,12 @@ export class DrawerController extends Controller { }), ); + const io = getInstance(); + io?.emit("EditFolder", { + from: `${cabinetName}/${drawerName}/`, + to: `${cabinetName}/${replaceIllegalChars(body.name)}/`, + }); + return this.setStatus(HttpStatusCode.NO_CONTENT); } @@ -201,6 +211,29 @@ export class DrawerController extends Controller { stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); + const io = getInstance(); + io?.emit("DeleteFolder", { + pathname: `${cabinetName}/${drawerName}/`, + }); + return this.setStatus(HttpStatusCode.NO_CONTENT); } + + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + */ + @Get("/{drawerName}/size") + @Tags("ลิ้นชัก") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + public async calc(@Path() cabinetName: string, @Path() drawerName: string) { + const list = await listItem(DEFAULT_BUCKET!, `${cabinetName}/${drawerName}/`, true).catch((e) => + console.error(`Error List Folder: ${e}`), + ); + + if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"); + + return { size: list.reduce((a, c) => a + c.size, 0) }; + } } diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index 9a2e663..06bd264 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -23,6 +23,7 @@ import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from " import HttpStatusCode from "../interfaces/http-status"; import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; import HttpError from "../interfaces/http-error"; +import { getInstance } from "../lib/websocket"; const DEFAULT_BUCKET = process.env.MINIO_BUCKET; const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; @@ -106,6 +107,9 @@ export class FolderController extends Controller { if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + const io = getInstance(); + io?.emit("CreateFolder", { pathname: `${basePath}${replaceIllegalChars(body.name)}/` }); + return this.setStatus(HttpStatusCode.CREATED); } @@ -179,6 +183,12 @@ export class FolderController extends Controller { }), ); + const io = getInstance(); + io?.emit("EditFolder", { + from: `${cabinetName}/${drawerName}/${folderName}`, + to: `${cabinetName}/${drawerName}/${replaceIllegalChars(body.name)}/`, + }); + return this.setStatus(HttpStatusCode.NO_CONTENT); } @@ -213,6 +223,34 @@ export class FolderController extends Controller { stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); + const io = getInstance(); + io?.emit("DeleteFolder", { pathname: `${cabinetName}/${drawerName}/${folderName}/` }); + return this.setStatus(HttpStatusCode.NO_CONTENT); } + + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + * @example folderName "แฟ้ม 1" + */ + @Get("/{folderName}/size") + @Tags("แฟ้ม") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + public async calc( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + ) { + const list = await listItem( + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}/`, + true, + ).catch((e) => console.error(`Error List Folder: ${e}`)); + + if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"); + + return { size: list.reduce((a, c) => a + c.size, 0) }; + } } diff --git a/Services/server/src/controllers/subFolderController.ts b/Services/server/src/controllers/subFolderController.ts index 9199a30..1454aa6 100644 --- a/Services/server/src/controllers/subFolderController.ts +++ b/Services/server/src/controllers/subFolderController.ts @@ -23,6 +23,7 @@ import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from " import HttpStatusCode from "../interfaces/http-status"; import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; import HttpError from "../interfaces/http-error"; +import { getInstance } from "../lib/websocket"; const DEFAULT_BUCKET = process.env.MINIO_BUCKET; const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; @@ -105,6 +106,9 @@ export class SubFolderController extends Controller { if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + const io = getInstance(); + io?.emit("CreateFolder", { pathname: `${basePath}${replaceIllegalChars(body.name)}/` }); + return this.setStatus(HttpStatusCode.CREATED); } @@ -182,6 +186,12 @@ export class SubFolderController extends Controller { }), ); + const io = getInstance(); + io?.emit("EditFolder", { + from: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`, + to: `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(body.name)}/`, + }); + return this.setStatus(HttpStatusCode.NO_CONTENT); } @@ -218,6 +228,38 @@ export class SubFolderController extends Controller { stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); + const io = getInstance(); + io?.emit("DeleteFolder", { + pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, + }); + return this.setStatus(HttpStatusCode.NO_CONTENT); } + + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + * @example folderName "แฟ้ม 1" + * @example subFolderName "แฟ้มย่อย 1" + */ + @Get("/{subFolderName}/size") + @Tags("แฟ้มย่อย") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + public async calc( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() subFolderName: string, + ) { + const list = await listItem( + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, + true, + ).catch((e) => console.error(`Error List Folder: ${e}`)); + + if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"); + + return { size: list.reduce((a, c) => a + c.size, 0) }; + } } diff --git a/Services/server/src/lib/websocket.ts b/Services/server/src/lib/websocket.ts new file mode 100644 index 0000000..6783db2 --- /dev/null +++ b/Services/server/src/lib/websocket.ts @@ -0,0 +1,11 @@ +import { Server } from "socket.io"; + +let io: Server | null = null; + +export function setInstance(server: Server) { + io = server; +} + +export function getInstance() { + return io; +} diff --git a/Services/server/src/rabbitmq/handler.ts b/Services/server/src/rabbitmq/handler.ts index f00a990..0beefe0 100644 --- a/Services/server/src/rabbitmq/handler.ts +++ b/Services/server/src/rabbitmq/handler.ts @@ -1,6 +1,7 @@ import { StorageFile } from "../interfaces/storage-fs"; import esClient from "../elasticsearch"; import minioClient from "../minio"; +import { getInstance } from "../lib/websocket"; const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; @@ -27,7 +28,7 @@ export async function handler(key: string, event: string): Promise { cachedBuffer[key] = buffer; } catch (e: any) { if (e.code === "NoSuchKey") { - console.info(`[AMQ] Key: ${key} received but cannot be found.`) + console.info(`[AMQ] Key: ${key} received but cannot be found.`); delete cachedBuffer[key]; delete cachedMetadata[key]; await ensureDelete(pathname); @@ -43,7 +44,7 @@ export async function handler(key: string, event: string): Promise { const rec = await popInfo(pathname); - console.info(`[AMQ] Key: ${key} - ${rec ?? 'Not Found.'}`) + console.info(`[AMQ] Key: ${key} - ${rec ?? "Not Found."}`); const result = rec ? await handleFoundRecord(rec, cachedBuffer[key], cachedMetadata[key]) @@ -94,6 +95,10 @@ async function ensureDelete(pathname: string) { conflicts: "proceed", }) .catch((e) => console.error(e)); + + const io = getInstance(); + io?.send("FileDelete", { pathname }); + return true; } @@ -132,12 +137,16 @@ async function handleNotFoundRecord( pipeline: "attachment", index: DEFAULT_INDEX!, document: { data: base64, ...metadata }, + refresh: "wait_for", }) .catch((e) => console.error(e)); - if (result) return true; + if (!result) return false; - return false; + const io = getInstance(); + io?.send("FileUpdate", metadata); + + return true; } async function handleFoundRecord( @@ -154,10 +163,14 @@ async function handleFoundRecord( pipeline: "attachment", index: DEFAULT_INDEX!, document: { data: Buffer.from(buffer).toString("base64"), ...metadata }, + refresh: "wait_for", }) .catch((e) => console.error(e)); - if (result) return true; + if (!result) return false; - return false; + const io = getInstance(); + io?.send("FileUpdate", metadata); + + return true; } diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index a056828..7b3897c 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -180,6 +180,32 @@ 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.get('/cabinet/:cabinetName/size', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(CabinetController)), + ...(fetchMiddlewares(CabinetController.prototype.calc)), + + function CabinetController_calc(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + }; + + // 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 + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new CabinetController(); + + + const promise = controller.calc.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } catch (err) { + return next(err); + } + }); + // 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.get('/cabinet/:cabinetName/drawer', authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(DrawerController)), @@ -289,6 +315,33 @@ 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.get('/cabinet/:cabinetName/drawer/:drawerName/size', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(DrawerController)), + ...(fetchMiddlewares(DrawerController.prototype.calc)), + + function DrawerController_calc(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + }; + + // 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 + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new DrawerController(); + + + const promise = controller.calc.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } catch (err) { + return next(err); + } + }); + // 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.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file', authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FileController)), @@ -549,6 +602,34 @@ 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.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/size', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(FolderController)), + ...(fetchMiddlewares(FolderController.prototype.calc)), + + function FolderController_calc(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + }; + + // 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 + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new FolderController(); + + + const promise = controller.calc.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } catch (err) { + return next(err); + } + }); + // 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('/search', authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SearchController)), @@ -692,6 +773,35 @@ 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.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/size', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(SubFolderController)), + ...(fetchMiddlewares(SubFolderController.prototype.calc)), + + function SubFolderController_calc(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"}, + }; + + // 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 + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new SubFolderController(); + + + const promise = controller.calc.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } catch (err) { + return next(err); + } + }); + // 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.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file', authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderFileController)), diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index 37ab26f..7dd5811 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -375,6 +375,51 @@ ] } }, + "/cabinet/{cabinetName}/size": { + "get": { + "operationId": "Calc", + "responses": { + "200": { + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "properties": { + "size": { + "type": "number", + "format": "double" + } + }, + "required": [ + "size" + ], + "type": "object" + } + } + } + } + }, + "tags": [ + "ตู้เอกสาร" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + }, + "example": "ตู้เอกสาร 1" + } + ] + } + }, "/cabinet/{cabinetName}/drawer": { "get": { "operationId": "ListDrawer", @@ -592,6 +637,60 @@ ] } }, + "/cabinet/{cabinetName}/drawer/{drawerName}/size": { + "get": { + "operationId": "Calc", + "responses": { + "200": { + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "properties": { + "size": { + "type": "number", + "format": "double" + } + }, + "required": [ + "size" + ], + "type": "object" + } + } + } + } + }, + "tags": [ + "ลิ้นชัก" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + }, + "example": "ตู้เอกสาร 1" + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + }, + "example": "ลิ้นชัก 1" + } + ] + } + }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file": { "get": { "operationId": "GetFile", @@ -1461,6 +1560,69 @@ ] } }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/size": { + "get": { + "operationId": "Calc", + "responses": { + "200": { + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "properties": { + "size": { + "type": "number", + "format": "double" + } + }, + "required": [ + "size" + ], + "type": "object" + } + } + } + } + }, + "tags": [ + "แฟ้ม" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + }, + "example": "ตู้เอกสาร 1" + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + }, + "example": "ลิ้นชัก 1" + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + }, + "example": "แฟ้ม 1" + } + ] + } + }, "/search": { "post": { "operationId": "SearchFile", @@ -1788,6 +1950,78 @@ ] } }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}/size": { + "get": { + "operationId": "Calc", + "responses": { + "200": { + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "properties": { + "size": { + "type": "number", + "format": "double" + } + }, + "required": [ + "size" + ], + "type": "object" + } + } + } + } + }, + "tags": [ + "แฟ้มย่อย" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + }, + "example": "ตู้เอกสาร 1" + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + }, + "example": "ลิ้นชัก 1" + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + }, + "example": "แฟ้ม 1" + }, + { + "in": "path", + "name": "subFolderName", + "required": true, + "schema": { + "type": "string" + }, + "example": "แฟ้มย่อย 1" + } + ] + } + }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}/file": { "get": { "operationId": "GetFile", diff --git a/Services/server/src/utils/minio.ts b/Services/server/src/utils/minio.ts index 4dc5b18..6c35fc5 100644 --- a/Services/server/src/utils/minio.ts +++ b/Services/server/src/utils/minio.ts @@ -9,7 +9,7 @@ import minioClient from "../minio"; * @returns illegal character replaced path */ export function replaceIllegalChars(path: string, replace = "-") { - return path.replace(/[/\\?%*:|"<>#]/g, replace); + return path.replace(/[/\\?%*:|"<>#]/g, replace).trim(); } /**
ต้องการยืนยันการลบข้อมูลนี้หรือไม่
{{ msg }}
+ พบข้อมูลในระบบ หากดำเนินการต่อ + ข้อมูลที่มีอยู่จะถูกแทนที่ด้วยข้อมูลใหม่ + ต้องการยืนยันการเพิ่มข้อมูลนี้หรือไม่ +