Merge branch 'development'

This commit is contained in:
Methapon2001 2023-11-30 15:42:31 +07:00
commit 31944ab7c8
No known key found for this signature in database
GPG key ID: 849924FEF46BD132
55 changed files with 7423 additions and 1969 deletions

6
Services/.dockerignore Normal file
View file

@ -0,0 +1,6 @@
.DS_Store
node_modules
dist
.env
.env.*
!.env.example

32
Services/Dockerfile Normal file
View file

@ -0,0 +1,32 @@
# docker build -t docker.frappet.com/edm/core .
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
FROM base as server
COPY ./server .
FROM server AS deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM server AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build
FROM base as client
COPY ./client .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build
FROM base as prod
COPY --from=deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
COPY --from=server /app/static /app/static
COPY --from=client /app/dist /app/static
CMD ["node", "./dist/app.js"]

View file

@ -1,12 +1,12 @@
{
"name": "ehr_portfolio",
"version": "0.0.0",
"name": "edm-frontend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ehr_portfolio",
"version": "0.0.0",
"name": "edm-frontend",
"version": "0.1.0",
"dependencies": {
"@fullcalendar/core": "^6.1.8",
"@fullcalendar/daygrid": "^6.1.8",
@ -14,6 +14,8 @@
"@fullcalendar/list": "^6.1.8",
"@fullcalendar/timegrid": "^6.1.8",
"@fullcalendar/vue3": "^6.1.8",
"@johmun/vue-tags-input": "^2.1.0",
"@mayank1513/vue-tag-input": "^1.2.0",
"@quasar/extras": "^1.15.8",
"@vuepic/vue-datepicker": "^5.2.1",
"axios": "^1.6.2",
@ -22,7 +24,8 @@
"quasar": "^2.11.1",
"vite-plugin-pwa": "^0.16.7",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
"vue-router": "^4.1.6",
"vue3-tags-input": "^1.0.12"
},
"devDependencies": {
"@quasar/vite-plugin": "^1.6.0",
@ -31,6 +34,7 @@
"@types/node": "^18.11.12",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@voerro/vue-tagsinput": "^2.7.1",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.6",
@ -44,6 +48,8 @@
"prettier": "^2.7.1",
"sass": "^1.32.12",
"start-server-and-test": "^1.15.2",
"typedoc": "^0.25.3",
"typedoc-plugin-vue": "^1.1.0",
"typescript": "~4.7.4",
"vite": "^4.0.0",
"vitest": "^0.25.6",
@ -2237,6 +2243,36 @@
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true
},
"node_modules/@johmun/vue-tags-input": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@johmun/vue-tags-input/-/vue-tags-input-2.1.0.tgz",
"integrity": "sha512-Fdwfss/TqCqMJbGAkmlzKbcG/ia1MstYjhqPBj+zG7h/166tIcE1TIftUxhT9LZ+RWjRSG0EFA1UyaHQSr3k3Q==",
"dependencies": {
"vue": "^2.6.10"
},
"peerDependencies": {
"vue": "2.x"
}
},
"node_modules/@johmun/vue-tags-input/node_modules/@vue/compiler-sfc": {
"version": "2.7.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.15.tgz",
"integrity": "sha512-FCvIEevPmgCgqFBH7wD+3B97y7u7oj/Wr69zADBf403Tui377bThTjBvekaZvlRr4IwUAu3M6hYZeULZFJbdYg==",
"dependencies": {
"@babel/parser": "^7.18.4",
"postcss": "^8.4.14",
"source-map": "^0.6.1"
}
},
"node_modules/@johmun/vue-tags-input/node_modules/vue": {
"version": "2.7.15",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.15.tgz",
"integrity": "sha512-a29fsXd2G0KMRqIFTpRgpSbWaNBK3lpCTOLuGLEDnlHWdjB8fwl6zyYZ8xCrqkJdatwZb4mGHiEfJjnw0Q6AwQ==",
"dependencies": {
"@vue/compiler-sfc": "2.7.15",
"csstype": "^3.1.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@ -2289,6 +2325,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mayank1513/vue-tag-input": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@mayank1513/vue-tag-input/-/vue-tag-input-1.2.0.tgz",
"integrity": "sha512-COWPFJ4MsPVy84Z+RIC2nzu188AwkJZJQ021Jz04uLfgxbmLrCXhrNfCIrjMD7bb6/Fb2kgybQILIiPqcNJc9w==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/mayank1513"
},
"peerDependencies": {
"vue": "^3"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2826,6 +2874,12 @@
"vue": "^3.0.0"
}
},
"node_modules/@voerro/vue-tagsinput": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@voerro/vue-tagsinput/-/vue-tagsinput-2.7.1.tgz",
"integrity": "sha512-G7ClILHgtc/D1vEw9qp6dlsAstlhwLDkPvEp6w1iY/dmSzu4hhxaMk8jyahjxQRDgiLlrAikESOsfkVhy18YwQ==",
"dev": true
},
"node_modules/@volar/language-core": {
"version": "1.10.10",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.10.10.tgz",
@ -3246,6 +3300,12 @@
"node": ">=8"
}
},
"node_modules/ansi-sequence-parser": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz",
"integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==",
"dev": true
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -3873,6 +3933,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/click-outside-vue3": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/click-outside-vue3/-/click-outside-vue3-4.0.1.tgz",
"integrity": "sha512-sbplNecrup5oGqA3o4bo8XmvHRT6q9fvw21Z67aDbTqB9M6LF7CuYLTlLvNtOgKU6W3zst5H5zJuEh4auqA34g==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -6667,6 +6735,12 @@
"node": ">=6"
}
},
"node_modules/jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
"dev": true
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@ -7039,6 +7113,12 @@
"yallist": "^3.0.2"
}
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"node_modules/magic-string": {
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
@ -7056,6 +7136,18 @@
"integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==",
"dev": true
},
"node_modules/marked": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
"dev": true,
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@ -8337,6 +8429,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/shiki": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz",
"integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==",
"dev": true,
"dependencies": {
"ansi-sequence-parser": "^1.1.0",
"jsonc-parser": "^3.2.0",
"vscode-oniguruma": "^1.7.0",
"vscode-textmate": "^8.0.0"
}
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@ -9096,6 +9200,60 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typedoc": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.4.tgz",
"integrity": "sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA==",
"dev": true,
"dependencies": {
"lunr": "^2.3.9",
"marked": "^4.3.0",
"minimatch": "^9.0.3",
"shiki": "^0.14.1"
},
"bin": {
"typedoc": "bin/typedoc"
},
"engines": {
"node": ">= 16"
},
"peerDependencies": {
"typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x"
}
},
"node_modules/typedoc-plugin-vue": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/typedoc-plugin-vue/-/typedoc-plugin-vue-1.1.0.tgz",
"integrity": "sha512-zbc2jFH3K2e/rRerV8crWY16M2MfZAze5bV6s2Ti0c8De+MjBn7K0EuK3eGoJXkY1n22flYgBz65j0pYu+ffmA==",
"dev": true,
"peerDependencies": {
"typedoc": "0.25.x"
}
},
"node_modules/typedoc/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/typedoc/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
@ -9430,6 +9588,18 @@
}
}
},
"node_modules/vscode-oniguruma": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
"integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
"dev": true
},
"node_modules/vscode-textmate": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
"dev": true
},
"node_modules/vue": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz",
@ -9612,6 +9782,20 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/vue3-tags-input": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/vue3-tags-input/-/vue3-tags-input-1.0.12.tgz",
"integrity": "sha512-s5rG+1W3M8+be0nd9H1nv/8WLjJOO6pShgVz8ALAqOiz3tDH5QhGrDH6fzD14ZjJNRWSa3bRBSXQwHEXffPQ6g==",
"dependencies": {
"click-outside-vue3": "^4.0.1"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"vue": "^3.0.5"
}
},
"node_modules/w3c-xmlserializer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",

View file

@ -2,6 +2,7 @@
"name": "edm-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
@ -17,47 +18,42 @@
"docs:typedoc": "typedoc && scp -r fe-typedoc projects-doc:~/projects/project-docs/edm/ && rm -r fe-typedoc"
},
"dependencies": {
"@fullcalendar/core": "^6.1.8",
"@fullcalendar/daygrid": "^6.1.8",
"@fullcalendar/interaction": "^6.1.8",
"@fullcalendar/list": "^6.1.8",
"@fullcalendar/timegrid": "^6.1.8",
"@fullcalendar/vue3": "^6.1.8",
"@quasar/extras": "^1.15.8",
"@vuepic/vue-datepicker": "^5.2.1",
"@quasar/extras": "^1.16.8",
"@tsconfig/node18": "^18.2.2",
"axios": "^1.6.2",
"keycloak-js": "^22.0.5",
"pinia": "^2.1.4",
"quasar": "^2.11.1",
"vite-plugin-pwa": "^0.16.7",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
"keycloak-js": "^23.0.0",
"pinia": "^2.1.7",
"quasar": "^2.14.0",
"vite-plugin-pwa": "^0.17.2",
"vue": "^3.3.9",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@mayank1513/vue-tag-input": "^1.2.0",
"@quasar/vite-plugin": "^1.6.0",
"@rushstack/eslint-patch": "^1.1.4",
"@types/jsdom": "^20.0.1",
"@types/node": "^18.11.12",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.6",
"@vue/tsconfig": "^0.1.3",
"cypress": "^12.0.2",
"eslint": "^8.22.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-vue": "^9.3.0",
"jsdom": "^20.0.3",
"@rushstack/eslint-patch": "^1.6.0",
"@types/jsdom": "^21.1.6",
"@types/node": "^20.10.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/test-utils": "^2.4.2",
"@vue/tsconfig": "^0.4.0",
"cypress": "^13.6.0",
"eslint": "^8.54.0",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-vue": "^9.18.1",
"jsdom": "^23.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"sass": "^1.32.12",
"start-server-and-test": "^1.15.2",
"typedoc": "^0.25.3",
"prettier": "^3.1.0",
"sass": "^1.69.5",
"start-server-and-test": "^2.0.3",
"typedoc": "^0.25.4",
"typedoc-plugin-vue": "^1.1.0",
"typescript": "~4.7.4",
"vite": "^4.0.0",
"vitest": "^0.25.6",
"vue-tsc": "^1.0.12"
"typescript": "~5.3.2",
"vite": "^5.0.2",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.22"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
{
"realm": "vue",
"realm": "EDM",
"auth-server-url": "https://edm-id.frappet.synology.me/",
"ssl-required": "external",
"resource": "vuejs",
"resource": "EDM-V1",
"public-client": true,
"confidential-port": 0
}

View file

@ -1,7 +1,5 @@
<script setup lang="ts"></script>
<template>
<div id="azay-admin-app">
<div>
<router-view v-slot="{ Component }">
<transition>
<component :is="Component" />
@ -9,5 +7,3 @@
</router-view>
</div>
</template>
<style scoped></style>

View file

@ -0,0 +1,52 @@
<script setup lang="ts">
defineEmits(['confirm', 'cancel', 'update:open'])
const props = withDefaults(
defineProps<{
open: boolean
}>(),
{
open: false,
},
)
</script>
<template>
<q-dialog
persistent
transition-show="scale"
transition-hide="scale"
:model-value="props.open"
@update:model-value="(v) => $emit('update:open', v)"
>
<q-card style="width: 400px">
<q-card-section>
<span class="text-h6">
<q-icon name="error" color="negative" size="2.5rem" />แจงเตอนการลบ
</span>
</q-card-section>
<q-card-section class="q-pt-none">
าดำเนนการตอจะทำการลบ
</q-card-section>
<q-card-actions align="right" class="bg-white text-primary">
<q-space />
<q-btn
label="ยกเลิก"
flat
v-close-popup
@click="() => ($emit('update:open', !open))"
/>
<q-btn
flat
v-close-popup
label="ลบ"
class="text-red"
@click="() => $emit('confirm')"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>

View file

@ -0,0 +1,181 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
const props = withDefaults(
defineProps<{
open: boolean
error: { fileExist?: boolean }
mode: 'create' | 'edit'
title?: string
description?: string
keyword?: string
category?: string
}>(),
{
open: false,
},
)
const emit = defineEmits([
'update:open',
'update:title',
'update:description',
'update:keyword',
'update:category',
'filechange',
'submit',
])
function keydown(e: KeyboardEvent) {
if (e.key === 'Escape' && props.open === true) {
emit('update:open', false)
reset()
}
}
function reset() {
file.value = undefined
emit('update:title', '')
emit('update:description', '')
emit('update:keyword', '')
emit('update:category', '')
}
function submit() {
emit('submit', {
mode: props.mode,
file: file.value,
title: props.title ?? '',
description: props.description ?? '',
keyword: props.keyword ?? '',
category: props.category ?? '',
})
reset()
}
onMounted(() => window.addEventListener('keydown', keydown))
onUnmounted(() => window.addEventListener('keydown', keydown))
const file = ref<File | undefined>()
</script>
<template>
<q-drawer
overlay
bordered
class="q-pa-md"
side="right"
tabindex="0"
:width="300"
:breakpoint="500"
:model-value="open"
@update:model-value="(v) => $emit('update:open', v)"
>
<q-form @submit.prevent="submit">
<q-toolbar class="q-mb-md q-pa-none">
<q-toolbar-title>
<span class="text-weight-bold" v-if="mode === 'create'">
สรางเอกสาร
</span>
<span class="text-weight-bold" v-if="mode === 'edit'">
แกไขเอกสาร
</span>
</q-toolbar-title>
<q-btn
v-close-popup
flat
round
dense
icon="close"
color="red"
@click="() => ($emit('update:open', !open), reset())"
/>
</q-toolbar>
<section class="q-mb-md">
<span class="text-weight-bold q-mb-sm block">พโหลดไฟล</span>
<q-file
dense
outlined
v-model="file"
@update:model-value="(v) => $emit('filechange', v.name)"
:label="file?.name ? undefined : 'เลือกไฟล์'"
:error="!!error.fileExist"
:error-message="
error.fileExist ? 'พบไฟล์ในระบบ ข้อมูลในระบบจะถูกเขียนทับ' : ''
"
>
<template v-slot:prepend>
<q-icon name="attach_file" />
</template>
</q-file>
</section>
<section class="q-mb-md">
<span class="text-weight-bold">อเรอง</span>
<q-input
outlined
dense
class="q-my-sm"
placeholder="กรอกชื่อเรื่อง"
:model-value="title"
@update:model-value="(v) => $emit('update:title', v)"
/>
</section>
<section class="q-mb-md">
<span class="text-weight-bold">รายละเอยดของเอกสาร</span>
<q-input
outlined
dense
class="q-mt-sm no-resize"
type="textarea"
placeholder="กรอกรายละเอียด"
:model-value="description"
@update:model-value="(v) => $emit('update:description', v)"
/>
</section>
<section class="q-mb-md">
<span class="text-weight-bold">กล/หมวดหม</span>
<q-input
outlined
dense
class="q-mt-sm"
placeholder="เลือกกลุ่ม/หมวดหมู่"
:model-value="category"
@update:model-value="(v) => $emit('update:category', v)"
/>
</section>
<section class="q-mb-md">
<span class="text-weight-bold">คำสำค</span>
<q-input
outlined
dense
class="q-mt-sm"
placeholder="คำสำคัญ"
:model-value="keyword"
@update:model-value="(v) => $emit('update:keyword', v)"
/>
</section>
<section :style="{ display: 'flex', gap: '.5rem' }">
<q-btn label="บันทึก" type="submit" color="primary" />
<q-btn
label="ยกเลิก"
type="reset"
color="primary"
flat
@click="() => ($emit('update:open', false), reset())"
/>
</section>
</q-form>
</q-drawer>
</template>
<style scoped>
.no-resize :deep(textarea) {
resize: none;
}
</style>

View file

@ -3,33 +3,34 @@ import { useFileInfoStore } from '@/stores/file-info-data'
const { mimeFileMapping } = useFileInfoStore()
defineProps<{ fileMimeType: any; size: string }>()
defineProps<{ fileMimeType: string | undefined; size: string }>()
function getIcon(mimeType: string) {
if (mimeFileMapping.hasOwnProperty(mimeType)) {
return mimeFileMapping[mimeType].icon
} else {
return 'mdi-file-question-outline'
}
return mimeType && mimeFileMapping.hasOwnProperty(mimeType)
? mimeFileMapping[mimeType].icon
: 'mdi-file-question-outline'
}
function getColor(mimeType: string) {
if (mimeFileMapping.hasOwnProperty(mimeType)) {
return mimeFileMapping[mimeType].color
} else {
return 'blue-11'
}
return mimeType && mimeFileMapping.hasOwnProperty(mimeType)
? mimeFileMapping[mimeType].color
: 'blue-11'
}
function getSize(s: string) {
if (s === 'preview') {
return '6em'
function getIconSize(s: string) {
type SizeMapping = {
[key: string]: string
}
const sizeMapping: SizeMapping = {
preview: '6em',
list: '2em',
}
return sizeMapping[s]
}
</script>
<template>
<q-icon
:name="getIcon(fileMimeType)"
:color="getColor(fileMimeType)"
:size="getSize(size)"
:name="fileMimeType && getIcon(fileMimeType)"
:color="fileMimeType && getColor(fileMimeType)"
:size="getIconSize(size)"
/>
</template>

View file

@ -2,333 +2,404 @@
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import FileIcon from '@/components/FileIcon.vue'
import FileItemAction from '@/components/FileItemAction.vue'
import DialogDelete from '@/components/DialogDelete.vue'
import FileForm from './FileForm.vue'
import FolderForm from './FolderForm.vue'
import UploadExistDialog from './UploadExistDialog.vue'
import { useTreeDataStore } from '@/stores/tree-data'
import { useFileInfoStore } from '@/stores/file-info-data'
const { isPreview } = storeToRefs(useFileInfoStore())
const { getFileInfo } = useFileInfoStore()
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย']
const { currentFolder, currentFile, currentDept } = storeToRefs(
useTreeDataStore()
)
const { getFolder, createFolder, editFolder } = useTreeDataStore()
const drawer = ref<boolean>(false)
const drawerFile = ref<boolean>(false)
const drawerStatus = ref<'edit' | 'create'>('create')
const input = ref<string>('')
const inputFile = ref<File>()
const fileTitle = ref<string>('')
const fileDesc = ref<string>('')
const fileCategory = ref<string>('')
const optionsCategory = [
{ label: 'ศิลปะ', value: 'art' },
{ label: 'ภาพวาด', value: 'drawing' },
{ label: 'ภาษาไทย', value: 'thai' },
]
const fileKeyword = ref<string>('')
const editPathname = ref<string>('')
const currentIcon = computed(() =>
currentDept.value === 0
? 'mdi-file-cabinet'
: currentDept.value === 1
? 'inbox'
: 'o_folder_open'
)
const props = withDefaults(
defineProps<{ action: boolean; viewMode: 'view_list' | 'view_module' }>(),
{
action: false,
}
},
)
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const
const { getFileInfo, getFileNameFormat } = useFileInfoStore()
const { currentFolder, currentFile, currentDept, currentPath } =
storeToRefs(useTreeDataStore())
const {
createFolder,
editFolder,
getFolder,
deleteFolder,
uploadFile,
updateFile,
deleteFile,
checkFile,
} = useTreeDataStore()
const currentIcon = computed(() =>
currentDept.value === 0
? 'mdi-file-cabinet'
: currentDept.value === 1
? 'inbox'
: 'o_folder_open',
)
const dialogDeleteState = ref<boolean>(false)
const deleteFormPath = ref<string>('')
const deleteFormType = ref<'deleteFolder' | 'deleteFile'>()
const folderFormState = ref<boolean>(false)
const folderFormPath = ref<string>('')
const folderFormData = ref<{
name?: string
}>({})
const folderFormType = ref<'edit' | 'create'>('create')
const fileFormState = ref<boolean>(false)
const fileFormPath = ref<string>('')
const fileFormData = ref<{
file?: File
title?: string
description?: string
keyword?: string
category?: string
}>({})
const fileFormType = ref<'edit' | 'create'>('create')
const fileFormError = ref<{ fileExist?: boolean }>({})
const fileExistNotification = ref<boolean>(false)
function triggerFolderDelete(pathname: string) {
deleteFormType.value = 'deleteFolder'
deleteFormPath.value = pathname
dialogDeleteState.value = !dialogDeleteState.value
}
function triggerFileDelete(pathname: string) {
deleteFormType.value = 'deleteFile'
deleteFormPath.value = pathname
dialogDeleteState.value = !dialogDeleteState.value
}
function triggerFolderCreate() {
folderFormType.value = 'create'
folderFormData.value = {}
folderFormState.value = !folderFormState.value
}
function triggerFolderEdit(name: string, pathname: string) {
folderFormType.value = 'edit'
folderFormPath.value = pathname
folderFormData.value.name = name
folderFormState.value = true
}
async function submitFolderForm(value: {
mode: 'create' | 'edit'
name: string
}) {
if (value.mode === 'create') {
await createFolder(value.name)
} else {
await editFolder(value.name, folderFormPath.value)
}
}
function triggerFileCreate() {
fileFormType.value = 'create'
fileFormData.value = {}
fileFormState.value = !fileFormState.value
}
function triggerFileEdit(
value: {
title: string
description: string
keyword: string
category: string
},
pathname: string,
) {
fileFormState.value = true
fileFormType.value = 'edit'
fileFormPath.value = pathname
fileFormData.value = {
title: value.title,
description: value.description,
keyword: value.keyword,
category: value.keyword,
}
}
const currentParam = ref<Parameters<typeof submitFileForm>[0]>()
async function submitFileForm(
value: {
mode: 'create' | 'edit'
file?: File
title: string
description: string
keyword: string
category: string
},
force = false,
) {
currentParam.value = value
if (value.file && checkFile(value.file.name) && !force) {
fileExistNotification.value = true
return
}
if (value.mode === 'create' && value.file) {
await uploadFile(currentPath.value, value.file, {
title: value.title,
description: value.description,
keyword: value.keyword,
category: value.category,
})
} else {
await updateFile(
fileFormPath.value,
{
title: value.title,
description: value.description,
keyword: value.keyword,
category: value.category,
},
value.file,
)
}
fileFormData.value = {}
fileFormState.value = false
currentParam.value = undefined
}
</script>
<template>
<div class="q-mt-md">
<div class="q-gutter-md">
<div class="text-h6 q-mt-md" v-if="currentDept > 2">
{{ DEPT_NAME[currentDept] }}
</div>
<div class="grid q-mt-md">
<div v-for="value in currentFolder">
<div
:key="value.name"
v-for="value in currentFolder"
class="inline-block"
:style="{
position: 'relative',
display: 'flex',
gap: '0.5rem',
flexDirection: currentDept > 2 ? 'row' : 'column',
alignItems: 'center',
padding: currentDept > 2 ? '.5rem 0' : '.5rem',
}"
class="box"
@click="() => getFolder(value.pathname)"
>
<div class="box border-radius-inherit">
<q-card flat @click="() => getFolder(value.pathname)">
<q-card-section class="column justify-center relative q-px-xl">
<q-icon :name="currentIcon" size="6em" color="primary" />
<div
class="absolute"
style="top: 0.5rem; right: 0.5rem"
v-if="props.action"
>
<file-item-action
:editname="value.name"
:pathname="value.pathname"
@editname="
() => {
drawer = !drawer
drawerStatus = 'edit'
editPathname = value.pathname
}
"
/>
</div>
<span class="text-center q-pt-md">{{ value.name }}</span>
</q-card-section>
</q-card>
<div class="q-px-md flex items-center justify-center">
<q-icon
:name="currentIcon"
:size="currentDept > 2 ? '3em' : '6em'"
color="primary"
class="col"
/>
</div>
<div
class="absolute flex items-center justify-center"
style="top: 0.5rem; right: 0.5rem"
:style="{ bottom: currentDept > 2 ? '0.5rem' : 'unset' }"
v-if="props.action"
>
<file-item-action
@delete="() => triggerFolderDelete(value.pathname)"
@edit="() => triggerFolderEdit(value.name, value.pathname)"
/>
</div>
<div
class="text-overflow-handle block text-center"
:class="{
'q-px-md': currentDept < 3,
'q-pr-xl': currentDept > 2,
}"
style="max-width: 100%"
>
{{ value.name }}
</div>
</div>
</div>
<div v-if="props.action && currentDept < 4">
<div
class="inline-block"
v-if="props.action && currentDept < 4"
tabindex="0"
@keydown.esc="() => (drawer = false)"
class="dashed"
:style="{
position: 'relative',
display: 'flex',
gap: '0.5rem',
flexDirection: currentDept > 2 ? 'row' : 'column',
alignItems: 'center',
padding: currentDept > 2 ? '.5rem 0' : '.5rem',
}"
@click="() => triggerFolderCreate()"
>
<div class="dashed border-radius-inherit">
<q-card
flat
@click="
() => {
drawer = !drawer
drawerStatus = 'create'
drawerFile = false
}
"
<div
class="q-px-md flex items-center justify-center"
style="position: relative"
>
<q-icon
:name="currentIcon"
:size="currentDept > 2 ? '3em' : '6em'"
color="primary"
class="col"
/>
<q-btn
round
:dense="currentDept > 2"
color="white"
size="10px"
style="position: absolute; bottom: 0"
:style="{ right: currentDept > 2 ? '.5rem' : '1.75rem' }"
>
<q-card-section class="column justify-center relative q-px-xl">
<q-icon
:name="currentIcon"
class="add-icon"
size="6em"
color="primary"
/>
<q-btn round class="add-button" color="white" size="10px">
<q-icon name="add" color="primary" size="1.5rem"></q-icon>
</q-btn>
<span class="text-center q-pt-md"
>สราง{{ DEPT_NAME[currentDept] }}ใหม</span
>
</q-card-section>
</q-card>
<q-icon name="add" color="primary" size="1.5rem" />
</q-btn>
</div>
<div
class="text-overflow-handle block text-center"
:class="{
'q-px-md': currentDept < 3,
'q-pr-xl': currentDept > 2,
}"
style="max-width: 100%"
>
สราง{{ DEPT_NAME[currentDept] }}ใหม
</div>
</div>
</div>
</div>
<div class="q-mt-md">
<div class="q-gutter-md">
<div
style="grid-column: 1 / span 5"
class="text-h6 q-mt-md"
v-if="currentDept > 2"
>
เอกสาร
</div>
<div class="grid q-mt-md">
<div v-for="value in currentFile">
<div
v-for="(value, index) in currentFile"
:key="value.title"
class="inline-block"
:style="{
position: 'relative',
display: 'flex',
gap: '0.5rem',
flexDirection: 'column',
alignItems: 'center',
padding: '1rem',
maxWidth: '100%',
}"
class="box"
@click="() => getFileInfo(value)"
>
<div class="box border-radius-inherit">
<q-card
flat
@click="
() => {
getFileInfo(currentFile[index])
isPreview = true
}
<div class="q-px-md flex items-center justify-center">
<file-icon
size="preview"
:fileMimeType="value.fileType ? value.fileType : 'unknow'"
/>
</div>
<div
class="absolute"
style="top: 0.5rem; right: 0.5rem"
v-if="props.action"
>
<file-item-action
@edit="
() =>
triggerFileEdit(
{
title: value.title,
description: value.description,
keyword: value.keyword.join(','),
category: value.category.join(','),
},
value.pathname,
)
"
>
<q-card-section class="column justify-center relative q-px-xl">
<q-icon name="description" size="6em" color="primary" />
<div
class="absolute"
style="top: 0.5rem; right: 0.5rem"
v-if="props.action"
>
<!-- TODO: Edit file data -->
</div>
<span class="text-center q-pt-md">{{ value.title }}</span>
</q-card-section>
</q-card>
@delete="() => triggerFileDelete(value.pathname)"
/>
</div>
<div
class="text-overflow-handle block q-px-md text-center"
style="max-width: 100%"
>
{{ getFileNameFormat(value.fileName) }}
</div>
</div>
<div class="inline-block" v-if="props.action && currentDept > 2">
<div class="dashed border-radius-inherit">
<q-card
flat
@click="
() => {
drawerFile = !drawerFile
drawer = false
}
"
</div>
<div v-if="props.action && currentDept > 2">
<div
:style="{
display: 'flex',
gap: '0.5rem',
flexDirection: 'column',
alignItems: 'center',
padding: '1rem',
maxWidth: '100%',
}"
class="dashed"
@click="() => triggerFileCreate()"
>
<div
class="q-px-md flex items-center justify-center"
style="position: relative"
>
<q-icon name="mdi-file" class="add-icon" size="6em" color="primary" />
<q-btn
round
color="white"
size="10px"
style="position: absolute; right: 1.75rem; bottom: 0"
>
<q-card-section class="column justify-center relative q-px-xl">
<q-icon
name="description"
class="add-icon"
size="6em"
color="primary"
/>
<q-btn round class="add-button" color="white" size="10px">
<q-icon name="add" color="primary" size="1.5rem"></q-icon>
</q-btn>
<span class="text-center q-pt-md">สรางไฟลใหม</span>
</q-card-section>
</q-card>
<q-icon name="add" color="primary" size="1.5rem" />
</q-btn>
</div>
<div
class="text-overflow-handle block q-px-md text-center"
style="max-width: 100%"
>
สรางไฟลใหม
</div>
</div>
</div>
</div>
<q-drawer
class="q-pa-md"
side="right"
v-model="drawer"
bordered
:width="300"
:breakpoint="500"
overlay
behavior="mobile"
tabindex="0"
@keydown.esc="
() => {
drawer = false
input = ''
}
<file-form
:mode="fileFormType"
:error="fileFormError"
v-model:open="fileFormState"
v-model:title="fileFormData.title"
v-model:description="fileFormData.description"
v-model:keyword="fileFormData.keyword"
v-model:category="fileFormData.category"
@filechange="(name: string) => (fileFormError.fileExist = checkFile(name))"
@submit="submitFileForm"
/>
<folder-form
:mode="folderFormType"
:tree="DEPT_NAME[currentDept]"
v-if="currentDept < 4"
v-model:open="folderFormState"
v-model:name="folderFormData.name"
@submit="submitFolderForm"
/>
<upload-exist-dialog
v-model:notification="fileExistNotification"
@confirm="() => currentParam && submitFileForm(currentParam, true)"
@cancel="() => (currentParam = undefined)"
/>
<dialog-delete
v-model:open="dialogDeleteState"
@confirm="
() =>
deleteFormType &&
{ deleteFolder, deleteFile }[deleteFormType](deleteFormPath)
"
@keyup.enter="
() => {
drawer = false
drawerStatus === 'create'
? createFolder(input)
: editFolder(input, editPathname)
input = ''
}
"
>
<q-toolbar class="q-mb-md q-pa-none" @keydown.esc="() => (drawer = false)">
<q-toolbar-title>
<span class="text-weight-bold" v-if="drawerStatus == 'edit'"
>แกไขช{{ DEPT_NAME[currentDept] }}</span
>
<span class="text-weight-bold" v-if="drawerStatus == 'create'"
>สราง{{ DEPT_NAME[currentDept] }}</span
>
</q-toolbar-title>
<q-btn
flat
round
dense
icon="close"
v-close-popup
color="red"
@click="() => (drawer = !drawer)"
/>
</q-toolbar>
<span class="text-weight-bold">{{ DEPT_NAME[currentDept] }}</span>
<q-input
class="q-my-md"
outlined
v-model="input"
:placeholder="`กรอกชื่อ${DEPT_NAME[currentDept]}`"
dense
/>
<q-btn
class="q-px-md"
label="บันทึก"
type="submit"
color="primary"
dense
@click="
() => {
drawer = !drawer
drawerStatus === 'create'
? createFolder(input)
: editFolder(input, editPathname)
input = ''
}
"
/>
</q-drawer>
<q-drawer
class="q-pa-md"
side="right"
v-model="drawerFile"
bordered
:width="300"
:breakpoint="500"
overlay
>
<q-toolbar class="q-mb-md q-pa-none">
<q-toolbar-title>
<span class="text-weight-bold">สรางเอกสาร</span>
</q-toolbar-title>
<q-btn
flat
round
dense
icon="close"
v-close-popup
color="red"
@click="() => (drawerFile = !drawerFile)"
/>
</q-toolbar>
<span class="text-weight-bold">พโหลดไฟล</span>
<q-input
model-value="inputfile"
class="q-my-md"
outlined
type="file"
:placeholder="`กรอกชื่อ${DEPT_NAME[currentDept]}`"
dense
/>
<span class="text-weight-bold">อเรอง</span>
<q-input
class="q-my-md"
outlined
v-model="fileTitle"
placeholder="กรอกชื่อเรื่อง"
dense
/>
<span class="text-weight-bold">รายละเอยดของเอกสาร</span>
<q-input
class="q-my-md"
outlined
type="textarea"
v-model="fileDesc"
placeholder="กรอกรายละเอียด"
dense
/>
<span class="text-weight-bold">กล/หมวดหม</span>
<q-select
class="q-my-md"
outlined
:options="optionsCategory"
v-model="fileCategory"
placeholder="เลือกกลุ่ม/หมวดหมู่"
dense
/>
<span class="text-weight-bold">คำสำค</span>
<q-input
class="q-my-md"
outlined
v-model="fileKeyword"
placeholder="กรอกคำสำคัญ"
dense
/>
<q-btn
class="q-px-md"
label="บันทึก"
type="submit"
color="primary"
dense
@click="
() => {
drawerFile = !drawerFile
}
"
/>
</q-drawer>
/>
</template>
<style lang="scss" scoped>
.box {
display: inline-block;
border: 2px solid #f1f2f4;
border: 2px solid $separator-color;
border-radius: 8px;
cursor: pointer;
}
@ -336,7 +407,7 @@ const props = withDefaults(
.dashed {
opacity: 0.4;
display: inline-block;
border: 2px solid #babdc3;
border: 2px solid #6985c2;
border-radius: 8px;
cursor: pointer;
border-style: dashed;
@ -348,8 +419,38 @@ const props = withDefaults(
.add-button {
position: absolute;
top: 75px;
right: 45px;
top: 50%;
right: 40%;
background-color: white;
}
.add-button-folder-level {
position: absolute;
left: 30px;
top: 22px;
background-color: white;
}
.text-overflow-handle {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.grid {
display: grid;
width: 100%;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
@media (min-width: $breakpoint-md-min) {
.grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.grid .box {
position: relative;
}
</style>

View file

@ -1,21 +1,5 @@
<script lang="ts" setup>
import { useTreeDataStore } from '@/stores/tree-data'
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
const { currentDept } = storeToRefs(useTreeDataStore())
const { deleteFolder, editFolder } = useTreeDataStore()
defineProps<{
pathname: string
editname: string
}>()
defineEmits([
'editname',
'deletename',
])
const editdrawer = ref(false)
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย']
const editInput = ref<string>()
defineEmits(['edit', 'delete'])
</script>
<template>
@ -23,9 +7,7 @@ const editInput = ref<string>()
<q-menu auto-close>
<q-list dense>
<q-item clickable>
<q-item-section @click="() => (
$emit('editname')
)">
<q-item-section @click.prevent.stop="() => $emit('edit')">
<div class="row items-center">
<q-icon name="edit" color="positive" />
<span class="q-ml-sm">แกไข</span>
@ -33,7 +15,7 @@ const editInput = ref<string>()
</q-item-section>
</q-item>
<q-item clickable>
<q-item-section @click="() => deleteFolder(pathname)">
<q-item-section @click.prevent.stop="() => $emit('delete')">
<div class="row items-center">
<q-icon name="delete" color="negative" />
<span class="q-ml-sm">ลบ</span>
@ -43,50 +25,4 @@ const editInput = ref<string>()
</q-list>
</q-menu>
</q-btn>
<!-- <q-drawer
class="q-pa-md"
side="left"
v-model="editdrawer"
bordered
:width="300"
:breakpoint="500"
overlay
>
<q-toolbar class="q-mb-md q-pa-none">
<q-toolbar-title>
<span class="text-weight-bold">แกไขช{{ DEPT_NAME[currentDept] }}</span>
</q-toolbar-title>
<q-btn
flat
round
dense
icon="close"
v-close-popup
color="red"
@click="editdrawer = !editdrawer"
/>
</q-toolbar>
<span class="text-weight-bold">{{ DEPT_NAME[currentDept] }}</span>
<q-input
class="q-my-md"
outlined
v-model="editInput"
:placeholder="`กรอกชื่อ${DEPT_NAME[currentDept]}`"
dense
/>
<q-btn
class="q-px-md"
label="บันทึก"
type="submit"
color="primary"
dense
@click="
() => {
editdrawer = !editdrawer
editFolder(editname,editInput)
editInput = ''
}
"
/>
</q-drawer> -->
</template>

View file

@ -2,9 +2,9 @@
import { storeToRefs } from 'pinia'
import { useSearchDataStore } from '@/stores/searched-data'
import { useFileInfoStore } from '@/stores/file-info-data'
import FileIcon from '@/components/FileIcon.vue'
const { foundFile } = storeToRefs(useSearchDataStore())
const { isPreview } = storeToRefs(useFileInfoStore())
const { getFileInfo } = useFileInfoStore()
</script>
@ -22,12 +22,15 @@ const { getFileInfo } = useFileInfoStore()
@click="
() => {
getFileInfo(foundFile[index])
isPreview = true
}
"
>
<q-card-section class="column justify-center relative q-px-xl">
<q-icon name="description" size="6em" color="primary" />
<file-icon
size="preview"
:fileMimeType="value.fileType"
ref="fileIconComp"
/>
<span class="text-center q-pt-md">{{ value.title }}</span>
</q-card-section>
</q-card>

View file

@ -0,0 +1,103 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
const props = withDefaults(
defineProps<{
open: boolean
mode: 'create' | 'edit'
tree: 'ตู้เอกสาร' | 'ลิ้นชัก' | 'แฟ้ม' | 'แฟ้มย่อย'
name?: string
}>(),
{
open: false,
},
)
const emit = defineEmits(['update:open', 'update:name', 'submit'])
function reset() {
emit('update:name', undefined)
}
function keydown(e: KeyboardEvent) {
if (e.key === 'Escape' && props.open === true) {
emit('update:open', false)
reset()
}
}
function submit() {
emit('submit', {
mode: props.mode,
name: props.name,
})
reset()
}
onMounted(() => window.addEventListener('keydown', keydown))
onUnmounted(() => window.addEventListener('keydown', keydown))
</script>
<template>
<q-drawer
overlay
bordered
class="q-pa-md"
side="right"
tabindex="0"
:width="300"
:breakpoint="500"
:model-value="open"
>
<q-form @submit.prevent="submit">
<q-toolbar class="q-mb-md q-pa-none">
<q-toolbar-title>
<span class="text-weight-bold" v-if="mode === 'create'">
สราง{{ tree }}
</span>
<span class="text-weight-bold" v-if="mode === 'edit'">
แกไข{{ tree }}
</span>
</q-toolbar-title>
<q-btn
v-close-popup
flat
round
dense
icon="close"
color="red"
@click="() => $emit('update:open', !open)"
/>
</q-toolbar>
<section class="q-mb-md">
<span class="text-weight-bold">{{ tree }}</span>
<q-input
outlined
dense
class="q-my-sm"
placeholder="กรอกชื่อ"
:model-value="name"
@update:model-value="(v) => $emit('update:name', v)"
/>
</section>
<section :style="{ display: 'flex', gap: '.5rem' }">
<q-btn label="บันทึก" type="submit" color="primary" />
<q-btn
label="ยกเลิก"
type="reset"
color="primary"
flat
@click="() => ($emit('update:open', false), reset())"
/>
</section>
</q-form>
</q-drawer>
</template>
<style scoped>
.no-resize :deep(textarea) {
resize: none;
}
</style>

View file

@ -0,0 +1,39 @@
<script setup lang="ts">
import { watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useErrorStore } from '@/stores/error'
import { useLoader } from '@/stores/loader'
const { visible, title, msg } = storeToRefs(useErrorStore())
const loader = useLoader()
watch(visible, () => {
loader.hide()
})
</script>
<template>
<q-dialog transition-show="scale" transition-hide="scale" v-model="visible">
<q-card style="width: 400px">
<q-card-section>
<span class="text-h6">
<q-icon name="error" color="negative" size="2.5rem" />{{
title
}}</span
>
</q-card-section>
<q-card-section class="q-pt-none">{{ msg }}</q-card-section>
<q-card-actions align="right" class="bg-white text-primary">
<q-space />
<q-btn
flat
v-close-popup
label="ปิด"
@click="() => (visible = !visible)"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>

View file

@ -0,0 +1,466 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import type { QTableProps } from 'quasar'
import { useTreeDataStore, type TreeDataFolder } from '@/stores/tree-data'
import { useFileInfoStore } from '@/stores/file-info-data'
import FileIcon from '@/components/FileIcon.vue'
import DialogDelete from '@/components/DialogDelete.vue'
import FileForm from './FileForm.vue'
import FolderForm from './FolderForm.vue'
const { getFormatDate, getSize, getType, getFileInfo } = useFileInfoStore()
const { listDataFile, listDataFolder, currentDept, currentPath } = storeToRefs(
useTreeDataStore()
)
const {
createFolder,
editFolder,
getFolder,
deleteFolder,
uploadFile,
updateFile,
deleteFile,
checkFile,
} = useTreeDataStore()
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const
const props = defineProps<{
mode: 'admin' | 'user'
}>()
const currentLevel = computed(() =>
currentDept.value === 0
? 'ตู้จัดเก็บเอกสาร'
: currentDept.value === 1
? 'ลิ้นชัก'
: currentDept.value === 2
? 'แฟ้ม'
: 'แฟ้มย่อย'
)
const currentIcon = computed(() =>
currentDept.value === 0
? 'mdi-file-cabinet'
: currentDept.value === 1
? 'inbox'
: 'o_folder_open'
)
const folderFormState = ref<boolean>(false)
const folderFormPath = ref<string>('')
const folderFormData = ref<{
name?: string
}>({})
const folderFormType = ref<'edit' | 'create'>('create')
const fileFormError = ref<{ fileExist?: boolean }>({})
const fileFormState = ref<boolean>(false)
const fileFormPath = ref<string>('')
const fileFormData = ref<{
file?: File
title?: string
description?: string
keyword?: string
category?: string
}>({})
const fileFormType = ref<'edit' | 'create'>('create')
const dialogDeleteState = ref<boolean>(false)
const deleteFormPath = ref<string>('')
const deleteFormType = ref<'deleteFolder' | 'deleteFile'>()
function triggerFolderDelete(pathname: string) {
deleteFormType.value = 'deleteFolder'
deleteFormPath.value = pathname
dialogDeleteState.value = !dialogDeleteState.value
}
function triggerFileDelete(pathname: string) {
deleteFormType.value = 'deleteFile'
deleteFormPath.value = pathname
dialogDeleteState.value = !dialogDeleteState.value
}
function triggerFolderCreate() {
folderFormType.value = 'create'
folderFormData.value = {}
folderFormState.value = !folderFormState.value
}
function triggerFolderEdit(name: string, pathname: string) {
folderFormType.value = 'edit'
folderFormPath.value = pathname
folderFormData.value.name = name
folderFormState.value = true
}
async function submitFolderForm(value: {
mode: 'create' | 'edit'
name: string
}) {
if (value.mode === 'create') {
await createFolder(value.name)
} else {
await editFolder(value.name, folderFormPath.value)
}
}
function triggerFileCreate() {
fileFormType.value = 'create'
fileFormData.value = {}
fileFormState.value = !fileFormState.value
}
function triggerFileEdit(
value: {
title: string
description: string
keyword: string
category: string
},
pathname: string
) {
fileFormState.value = true
fileFormType.value = 'edit'
fileFormPath.value = pathname
fileFormData.value = {
title: value.title,
description: value.description,
keyword: value.keyword,
category: value.keyword,
}
}
async function submitFileForm(value: {
mode: 'create' | 'edit'
file: File
title: string
description: string
keyword: string
category: string
}) {
if (value.mode === 'create') {
await uploadFile(currentPath.value, value.file, {
title: value.title,
description: value.description,
keyword: value.keyword,
category: value.category,
})
} else {
await updateFile(
fileFormPath.value,
{
title: value.title,
description: value.description,
keyword: value.keyword,
category: value.category,
},
value.file
)
}
fileFormData.value = {}
fileFormState.value = false
}
const columnsFolder: QTableProps['columns'] = [
{
name: 'name',
required: true,
label: 'ชื่อ',
align: 'left',
field: (row) => row.name,
format: (val) => `${val}`,
sortable: true,
style: 'width: 1000px',
},
{
name: 'createdBy',
align: 'center',
label: 'สร้างโดย',
field: 'createdBy',
style: 'width: 20px',
sortable: true,
},
{
name: 'createdAt',
align: 'center',
label: 'วันที่สร้าง',
field: 'createdAt',
sortable: true,
style: 'width: 20px',
},
{
name: 'actions',
align: 'center',
label: '',
field: '',
style: 'width: 5px',
},
]
const columnsFile: QTableProps['columns'] = [
{
name: 'name',
required: true,
label: 'ชื่อไฟล์',
align: 'left',
field: (row) => row.fileName,
format: (val) => `${val}`,
sortable: true,
style: 'width: 200px',
},
{
name: 'title',
align: 'center',
label: 'ชื่อเรื่อง',
field: 'title',
style: 'width: 200px',
sortable: true,
},
{
name: 'fileType',
align: 'center',
label: 'ประเภทของไฟล์',
field: 'fileType',
sortable: true,
style: 'width: 200px',
},
{
name: 'actions',
align: 'center',
label: '',
field: '',
style: 'width: 20px',
},
]
const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
getFolder(row.pathname)
}
</script>
<template>
<div class="q-pa-md">
<div class="q-gutter-sm">
<div
class="flex flex-break d justify-between space-between"
v-if="currentDept >= 1"
>
<div>
<span class="text-h6">{{ currentLevel }}</span>
</div>
<div>
<q-btn
v-if="props.mode == 'admin' && currentDept != 4"
outline
push
class="q-px-md q-ml-md q-py-sm"
:label="'สร้าง' + currentLevel"
type="submit"
color="primary"
dense
icon="add"
@click="() => triggerFolderCreate()"
/>
</div>
</div>
<q-table
flat
bordered
:rows="listDataFolder"
:columns="columnsFolder"
row-key="name"
hide-bottom
:rows-per-page-options="[0]"
@row-click="onRowClick"
class="cursor"
>
<template v-slot:body-cell-name="nameRow">
<q-td style="width: 50%">
<q-icon :name="currentIcon" size="2em" color="primary" />
{{ nameRow.row.name }}
</q-td>
</template>
<template v-slot:body-cell-createdAt="createdAtRow">
<q-td>
<div class="justify-center">
{{ getFormatDate(createdAtRow.row.createdAt) }}
</div>
</q-td>
</template>
<template v-slot:body-cell-actions="actionsRow">
<q-td class="justify-center">
<div>
<q-icon class="q-ma-sm" name="info" size="2em" color="primary" />
<q-tooltip
anchor="center left"
self="center right"
:offset="[5, 1]"
>
{{ actionsRow.row.name }}
</q-tooltip>
</div>
<div v-if="props.mode === 'admin'">
<q-btn
flat
color="positive"
dense
icon="edit"
@click.stop="
triggerFolderEdit(
actionsRow.row.name,
actionsRow.row.pathname
)
"
/>
<q-btn
flat
color="negative"
dense
icon="delete"
@click.stop="triggerFolderDelete(actionsRow.row.pathname)"
/>
</div>
</q-td>
</template>
</q-table>
</div>
</div>
<div class="q-pa-md" v-if="currentDept >= 3">
<div class="q-gutter-sm">
<div class="flex flex-break d justify-between space-between">
<div>
<span class="text-h6">เอกสาร</span>
</div>
<div>
<q-btn
v-if="props.mode == 'admin'"
outline
push
class="q-px-md q-ml-md q-py-sm"
label="สร้างเอกสาร"
type="submit"
color="primary"
dense
icon="add"
@click="() => triggerFileCreate()"
/>
</div>
</div>
<q-table
flat
bordered
:rows="listDataFile"
:columns="columnsFile"
row-key="name"
hide-bottom
:rows-per-page-options="[0]"
class="cursor"
>
<template v-slot:body-cell-name="nameRow">
<q-td
style="width: 50%"
@click="
() => {
currentDept >= 3
? getFileInfo(nameRow.row)
: getFolder(nameRow.row.pathname)
}
"
>
<file-icon size="list" :fileMimeType="nameRow.row.fileType" />
{{ nameRow.row.fileName }}
</q-td>
</template>
<template v-slot:body-cell-fileType="fileTypeRow">
<q-td>
<div class="justify-center">
{{ getType(fileTypeRow.row.fileType) }}
</div>
</q-td>
</template>
<template v-slot:body-cell-actions="actionsRow">
<q-td class="justify-center">
<div>
<q-icon class="q-ma-sm" name="info" size="2em" color="primary" />
<q-tooltip
anchor="center left"
self="center right"
:offset="[5, 1]"
>
{{ getSize(actionsRow.row.fileSize) }}
</q-tooltip>
</div>
<div v-if="props.mode === 'admin'">
<q-btn
flat
color="positive"
dense
icon="edit"
@click="
() => triggerFileEdit(actionsRow.row, actionsRow.row.pathname)
"
/>
<q-btn
flat
color="negative"
dense
icon="delete"
@click="() => triggerFileDelete(actionsRow.row.pathname)"
/>
</div>
</q-td>
</template>
</q-table>
</div>
</div>
<file-form
:mode="fileFormType"
:error="fileFormError"
v-model:open="fileFormState"
v-model:title="fileFormData.title"
v-model:description="fileFormData.description"
v-model:keyword="fileFormData.keyword"
v-model:category="fileFormData.category"
@filechange="(name: string) => (fileFormError.fileExist = checkFile(name))"
@submit="submitFileForm"
/>
<folder-form
:mode="folderFormType"
:tree="DEPT_NAME[currentDept]"
v-if="currentDept < 4"
v-model:open="folderFormState"
v-model:name="folderFormData.name"
@submit="submitFolderForm"
/>
<dialog-delete
v-model:open="dialogDeleteState"
@confirm="
() =>
deleteFormType &&
(deleteFolder(deleteFormPath), deleteFile(deleteFormPath))
"
/>
</template>
<style lang="scss" scoped>
.justify-center {
display: flex;
justify-content: center;
align-items: center;
}
.cursor {
cursor: pointer;
}
</style>

View file

@ -4,24 +4,51 @@ import { storeToRefs } from 'pinia'
import { useTreeDataStore } from '@/stores/tree-data'
import { useSearchDataStore } from '@/stores/searched-data'
import { useFileInfoStore } from '@/stores/file-info-data'
import FileItem from '@/components/FileItem.vue'
import TreeExplorer from '@/components/TreeExplorer.vue'
import FileItem from './FileItem.vue'
import TreeExplorer from './TreeExplorer.vue'
import FileSearched from '.FileSearched.vue'
import ListView from './ListView.vue'
import FolderForm from './FolderForm.vue'
import GlobalErrorDialog from './GlobalErrorDialog.vue'
import SearchBar from '@/modules/01_user/components/SearchBar.vue'
import FileSearched from '@/components/FileSearched.vue'
import FileDownload from '@/modules/01_user/components/FileDownload.vue'
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย', 'ไฟล์']
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const
const { isPreview } = storeToRefs(useFileInfoStore())
const { isFilePreview } = storeToRefs(useFileInfoStore())
const { isSearch } = storeToRefs(useSearchDataStore())
const { data, currentDept } = storeToRefs(useTreeDataStore())
const { getCabinet, gotoParent } = useTreeDataStore()
const { data, currentDept, currentPath } = storeToRefs(useTreeDataStore())
const { createFolder, getCabinet, gotoParent, getFolder } = useTreeDataStore()
const viewMode = ref<'view_list' | 'view_module'>('view_list')
const inputSearch = ref<string>()
const props = defineProps<{
mode: 'admin' | 'user'
}>()
const folderFormState = ref<boolean>(false)
const folderFormType = ref<'edit' | 'create'>('create')
const folderFormData = ref<{
name?: string
}>({})
function triggerFolderCreate() {
folderFormType.value = 'create'
folderFormData.value = {}
folderFormState.value = !folderFormState.value
}
async function submitFolderForm(value: {
mode: 'create' | 'edit'
name: string
}) {
if (value.mode === 'create') {
await createFolder(value.name)
}
}
onMounted(getCabinet)
</script>
<template>
@ -65,58 +92,100 @@ onMounted(getCabinet)
</div>
</div>
<div class="col">
<file-download v-if="isPreview === true"/>
<div class="bg-white rounded-borders shadow-5 relative" v-if="isPreview === false">
<file-download v-if="isFilePreview === true" />
<div
class="bg-white rounded-borders shadow-5 relative"
v-if="isFilePreview === false"
>
<search-bar v-if="mode === 'user'" />
<div class="bg-white q-pa-md">
<div class="row items-center justify-between">
<span class="text-h6">
<q-btn
flat
dense
class="q-mr-sm q-px-sm"
v-if="currentDept > 0 && isSearch === false"
@click="() => gotoParent()"
>
<q-icon name="arrow_back" size="1rem" color="primary"
/></q-btn>
<span v-if="isSearch === false">{{ DEPT_NAME[currentDept] }}</span>
<span class="text-body1">
<div class="row items-center">
<q-btn
flat
dense
class="q-mr-sm q-px-sm"
v-if="currentDept > 0 && isSearch === false"
@click="() => gotoParent()"
>
<q-icon name="arrow_back" size="1rem" color="primary" />
</q-btn>
<q-breadcrumbs v-if="isSearch === false" active-color="primary">
<q-breadcrumbs-el
v-if="currentPath === '/' || !currentPath"
label="ตู้เอกสารทั้งหมด"
/>
<q-breadcrumbs-el
class="text-black"
v-for="fragments in currentPath.split('/').filter(Boolean)"
:label="fragments"
/>
</q-breadcrumbs>
</div>
<span v-if="isSearch === true">ผลการค้นหา</span>
<q-btn
v-if="mode === 'admin' && viewMode === 'view_module'"
class="q-px-md q-ml-md"
v-if="
mode === 'admin' &&
viewMode === 'view_module' &&
currentDept === 0
"
class="q-px-md q-ml-md al"
label="สร้างตู้เก็บเอกสาร"
type="submit"
color="primary"
dense
icon="add"
@click=""
@click="() => triggerFolderCreate()"
/>
</span>
<q-btn
flat
color="blue-grey-2"
:icon="viewMode"
@click="
() => {
viewMode =
viewMode === 'view_list' ? 'view_module' : 'view_list'
}
"
/>
<div>
<q-btn
flat
dense
color="blue-grey-2"
icon="refresh"
class="q-mr-sm"
@click="() => getFolder(currentPath)"
/>
<q-btn
flat
dense
color="blue-grey-2"
:icon="viewMode"
@click="
() => {
viewMode =
viewMode === 'view_list' ? 'view_module' : 'view_list'
}
"
/>
</div>
</div>
<div>
<file-searched v-if="isSearch === true" />
<file-item
:viewMode="viewMode"
:action="props.mode === 'admin'"
v-if="isSearch === false"
v-if="isSearch === false && viewMode === 'view_list'"
/>
<list-view v-if="viewMode === 'view_module'" :mode="mode" />
</div>
</div>
</div>
</div>
</section>
<folder-form
:mode="folderFormType"
:tree="DEPT_NAME[currentDept]"
v-if="currentDept < 4"
v-model:open="folderFormState"
v-model:name="folderFormData.name"
@submit="submitFolderForm"
/>
<global-error-dialog />
</template>
<style lang="scss" scoped>

View file

@ -1,10 +1,8 @@
<script setup lang="ts">
import { getUsername, logout } from '@/services/KeyCloakService'
import { ref } from 'vue'
import KeyCloakService from '@/services/KeyCloakService'
const dropdownOpen = ref<boolean>(false)
const accountName = ref<string>()
accountName.value = KeyCloakService.GetUserName()
const accountName = ref<string>(getUsername())
</script>
<template>
@ -32,16 +30,7 @@ accountName.value = KeyCloakService.GetUserName()
</div>
<q-btn-dropdown stretch flat v-model="dropdownOpen">
<q-list>
<q-item
clickable
v-close-popup
tabindex="0"
@click="
() => {
KeyCloakService.CallLogOut()
}
"
>
<q-item clickable v-close-popup tabindex="0" @click="() => logout()">
<q-item-section avatar>
<q-avatar icon="logout" color="primary" text-color="white" caption>
</q-avatar>
@ -50,7 +39,6 @@ accountName.value = KeyCloakService.GetUserName()
<q-item-label>Logout</q-item-label>
</q-item-section>
</q-item>
<q-separator inset spaced></q-separator>
</q-list>
</q-btn-dropdown>
</template>

View file

@ -0,0 +1,40 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import TagInput from "@mayank1513/vue-tag-input";
import "@mayank1513/vue-tag-input/style.css";
const autocompleteItems = [
"No dependencies",
"Autocompletion",
"Keep Focused",
"Fast Settup",
"Mini Sized",
"Customizable",
"Backspace/Delete to remove tag",
"Turns red when backspace/delete is pressed",
"Examples",
"Docs",
"Copy/Paste",
]
const tags = ref<string[]>([]);
</script>
<template>
<q-btn @click="()=> {console.log(tags);
}">test</q-btn>
<tag-input tagBgColor="rgb(189, 184, 179)" :autocomplete-items="autocompleteItems" v-model="tags" />
<!-- <q-btn
class="q-px-md"
label="บันทึก"
type="submit"
color="primary"
dense
@click="
() => {
$emit('update:drawerFile')
}
"
/> -->
</template>

View file

@ -1,5 +1,11 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useTreeDataStore, type TreeDataFolder } from '@/stores/tree-data'
import { useSearchDataStore } from '@/stores/searched-data'
import { useFileInfoStore } from '@/stores/file-info-data'
const { isSearch } = storeToRefs(useSearchDataStore())
const { isFilePreview } = storeToRefs(useFileInfoStore())
const { getFolder } = useTreeDataStore()
const props = withDefaults(
@ -9,7 +15,7 @@ const props = withDefaults(
}>(),
{
level: 0,
}
},
)
</script>
@ -17,7 +23,13 @@ const props = withDefaults(
<div>
<q-list v-for="folder in data" class="rounded-borders">
<q-expansion-item
@click="() => getFolder(folder.pathname, false)"
@click="
() => {
getFolder(folder.pathname, false)
isSearch = false
isFilePreview = false
}
"
:header-inset-level="level * 0.25"
:group="level.toString()"
:hide-expand-icon="level === 4"
@ -25,10 +37,11 @@ const props = withDefaults(
level === 1
? 'mdi-file-cabinet'
: level === 2
? 'inbox'
: 'o_folder_open'
? 'inbox'
: 'o_folder_open'
"
:label="folder.name"
class="text-overflow-handle"
v-model="folder.status"
>
<tree-explorer
@ -41,12 +54,15 @@ const props = withDefaults(
</div>
</template>
<style lang="scss">
.q-item[aria-expanded='true'] {
<style lang="scss" scoped>
:deep(.q-item[aria-expanded='true']) {
color: $primary;
}
.q-item[aria-expanded='true'] .q-icon {
color: $primary;
.text-overflow-handle,
:deep(.q-item__label) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View file

@ -0,0 +1,49 @@
<script lang="ts" setup>
defineProps<{
notification: boolean
}>()
defineEmits(['update:notification', 'confirm', 'cancel'])
</script>
<template>
<q-dialog
:model-value="notification"
@update:model-value="(v) => $emit('update:notification', v)"
transition-show="scale"
transition-hide="scale"
>
<q-card style="width: 400px">
<q-card-section>
<div class="text-h6">
<q-icon
name="error"
color="warning"
size="2.5rem"
/>
</div>
</q-card-section>
<q-card-section class="q-pt-none">
หากดำเนนการตอขอมลจะถกเขยนท
</q-card-section>
<q-card-actions align="right" class="bg-white text-primary">
<q-space />
<q-btn
flat
label="ยกเลิก"
v-close-popup
@click="() => $emit('cancel')"
/>
<q-btn
flat
label="ดำเนินการต่อ"
v-close-popup
class="text-red"
@click="() => $emit('confirm')"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>

View file

@ -5,40 +5,30 @@ import th from 'quasar/lang/th'
import App from './App.vue'
import HttpService from '@/services/HttpService'
import quasarUserOptions from './quasar-user-options'
import router from './router'
import 'quasar/src/css/index.sass'
import '@vuepic/vue-datepicker/dist/main.css'
import { login } from './services/KeyCloakService'
const app = createApp(App)
const pinia = createPinia()
login().then(async () => {
const app = createApp(App)
const pinia = createPinia()
app.use(router)
app.use(pinia)
app.use((await import('./router')).default)
app.use(pinia)
app.use(Quasar, {
...quasarUserOptions,
plugins: {
Dialog,
Loading,
},
lang: th,
})
app.use(Quasar, {
...quasarUserOptions,
plugins: {
Dialog,
Loading,
},
lang: th,
app.component(
'full-loader',
defineAsyncComponent(() => import('@/components/FullLoader.vue')),
)
app.mount('#app')
})
app.component(
'full-loader',
defineAsyncComponent(() => import('@/components/FullLoader.vue'))
)
app.component(
'datepicker',
defineAsyncComponent(() => import('@vuepic/vue-datepicker'))
)
app.mount('#app')
HttpService.configureAxiosKeycloak()
console.log(import.meta.env)

View file

@ -1,7 +1,11 @@
divdivdivdivdivdiv
<script setup lang="ts">
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
const isAdvncSearchClick = ref<boolean>(false)
import { useSearchDataStore } from '@/stores/searched-data'
const { isAdvSearchCall } = storeToRefs(useSearchDataStore())
const optionsField = [
{ label: 'ชื่อเรื่อง (title)', value: 'title' },
{ label: 'คำสำคัญ (keyword)', value: 'keyword' },
@ -10,23 +14,32 @@ const optionsOp = [
{ label: 'และ', value: 'AND' },
{ label: 'หรือ', value: 'OR' },
]
const advSearchData = ref<
const advSearchDataRow = ref<
{
op: 'AND' | 'OR'
field: 'title' | 'keyword'
value: string
}[]
>([])
>([
{
op: 'AND',
field: 'title',
value: '',
},
])
const advSearchDataField = ref<{
keyword: string
description: string
}>({
keyword: '',
description: '',
})
const props = defineProps<{
field: string
value: string
keyword?: string
description?: string
searchSubmit: Function
submitSearchData: {
AND: {
field?: string
value?: string
field: string
value: string
}[]
OR: {
field: string
@ -36,185 +49,164 @@ const props = defineProps<{
}>()
defineExpose({
clearAdvData,
advSearchData,
advSearchDataRow,
advSearchDataField,
})
defineEmits([
'update:field',
'update:value',
'update:keyword',
'update:description',
])
function addSearchData() {
advSearchData.value.push({
function addAdvSearchData() {
advSearchDataRow.value.push({
op: 'AND',
field: 'title',
value: '',
})
}
function clearAdvData() {
advSearchData.value = []
function delAdvSearchData(index: number) {
advSearchDataRow.value.length > 1 && advSearchDataRow.value.splice(index, 1)
}
function clearAdvSearchData() {
isAdvSearchCall.value = false
advSearchDataRow.value = [
{
op: 'AND',
field: 'title',
value: '',
},
]
advSearchDataField.value = {
keyword: '',
description: '',
}
}
</script>
<template>
<div>
<q-btn
outline
flat
color="primary"
icon="mdi-tools"
label="ค้นหาขั้นสูง"
@click="() => (isAdvncSearchClick = true)"
class="q-mt-sm"
style="width: 148px"
v-if="isAdvSearchCall === false"
@click="() => (isAdvSearchCall = true)"
/>
</div>
<q-dialog v-model="isAdvncSearchClick">
<q-card style="width: 1000px; max-width: 80vw">
<q-toolbar class="bg-grey-1 q-py-sm q-px-lg">
<q-toolbar-title>
<span class="text-weight-bold">นหาขนส</span>
</q-toolbar-title>
<q-btn flat round dense icon="close" v-close-popup color="red" />
</q-toolbar>
<q-separator />
<div v-if="isAdvSearchCall === true">
<div class="column bg-white q-pa-sm">
<div class="row items-center justify-between q-pb-md">
<span class="text-primary text-weight-medium q-pl-md q-pt-xs"
><q-icon name="mdi-tools" class="q-pr-sm" size="sm" />
นหาขนส</span
>
<q-btn
flat
dense
color="red"
icon="close"
@click="clearAdvSearchData"
/>
</div>
<div class="col">
<q-card-section class="row q-mt-sm">
<div class="col">
<div class="row q-col-gutter-md q-mb-md items-center">
<div class="col-12 col-md-3">
<q-select
dense
outlined
emit-value
map-options
:options="optionsField"
:model-value="props.field"
@update:model-value="(value) => $emit('update:field', value)"
/>
</div>
<div class="col-12 col-md-8">
<q-input
dense
outlined
placeholder="เอกสาร"
:model-value="props.value"
@update:model-value="(value) => $emit('update:value', value)"
/>
</div>
<div class="col-1">
<q-btn
dense
color="teal"
icon="mdi-plus"
v-if="advSearchData.length === 0"
@click="addSearchData"
/>
</div>
</div>
<div
class="row q-col-gutter-md q-mb-md items-center"
v-for="(_, index) in advSearchData"
>
<div class="col-4 col-md-2">
<q-select
dense
outlined
emit-value
map-options
v-model="advSearchData[index].op"
:options="optionsOp"
/>
</div>
<div class="col-8 col-md-3">
<q-select
dense
outlined
emit-value
map-options
v-model="advSearchData[index].field"
:options="optionsField"
/>
</div>
<div class="col-12 col-md-6">
<q-input
dense
outlined
v-model="advSearchData[index].value"
placeholder="เอกสาร"
/>
</div>
<div class="col-1">
<q-btn
dense
color="teal"
icon="mdi-plus"
v-if="index === advSearchData.length - 1"
@click="addSearchData"
/>
<q-btn
dense
flat
icon="mdi-minus"
color="red"
v-if="index === advSearchData.length - 1"
@click="() => advSearchData.pop()"
/>
</div>
</div>
</div>
</q-card-section>
<q-card-section class="row q-mb-md">
<div class="col">
<span> </span>
<q-separator class="q-mb-lg" />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3 q-gutter-y-sm">
<span class="text-weight-bold">คำสำค</span>
<q-input
dense
outlined
placeholder="กรอกคำสำคัญ"
:model-value="props.keyword"
@update:model-value="
(value) => $emit('update:keyword', value)
"
/>
</div>
<div class="col-12 col-md-4 q-gutter-y-sm">
<span class="text-weight-bold">รายละเอยดของเอกสาร</span>
<q-input
dense
outlined
placeholder="กรอกรายละเอียด"
:model-value="props.description"
@update:model-value="
(value) => $emit('update:description', value)
"
/>
</div>
</div>
</div>
</q-card-section>
<q-separator />
<q-toolbar class="row justify-end">
<div class="q-py-md">
<div class="column q-px-lg">
<div
class="row items-center q-pb-xs q-col-gutter-md"
v-for="(item, index) in advSearchDataRow"
:key="index"
>
<div class="row items-center" style="width: 45px; height: 45px">
<q-btn
color="primary"
label="ค้นหา"
icon="mdi-magnify"
v-close-popup
@click="() => props.searchSubmit()"
dense
color="teal"
icon="mdi-plus"
v-if="index === advSearchDataRow.length - 1"
@click="addAdvSearchData"
/>
</div>
</q-toolbar>
<div class="col-4 col-md-2">
<q-select
dense
outlined
emit-value
map-options
v-model="item.op"
:options="optionsOp"
/>
</div>
<div class="col-grow col-md-3">
<q-select
dense
outlined
emit-value
map-options
v-model="item.field"
:options="optionsField"
/>
</div>
<div class="col-grow">
<q-input dense outlined v-model="item.value" placeholder="เอกสาร"
><template v-slot:append>
<q-icon
name="close"
@click="() => (item.value = '')"
class="cursor-pointer"
/>
</template>
</q-input>
</div>
<div>
<q-btn
dense
flat
icon="mdi-trash-can-outline"
color="red"
@click="() => delAdvSearchData(index)"
>
<q-tooltip
class="bg-red"
anchor="top middle"
self="bottom middle"
:offset="[10, 10]"
v-if="advSearchDataRow.length === 1"
>
<strong>ไมสามารถลบได</strong>
</q-tooltip>
</q-btn>
</div>
</div>
<q-separator class="q-mb-md q-mt-sm" />
<div class="row q-col-gutter-md q-pb-md">
<div class="col-12 col-md-5">
<q-input
dense
outlined
placeholder="คำสำคัญ:"
v-model="advSearchDataField.keyword"
/>
</div>
<div class="col-12 col-md-grow">
<q-input
dense
outlined
placeholder="รายละเอียด:"
v-model="advSearchDataField.description"
/>
</div>
</div>
</div>
</q-card>
</q-dialog>
</div>
<div class="row justify-end">
<q-btn
class="q-mt-md"
style="width: 150px"
color="primary"
label="ค้นหา"
icon="mdi-magnify"
@click="() => props.searchSubmit()"
/>
</div>
</div>
</template>

View file

@ -1,25 +1,51 @@
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { storeToRefs } from 'pinia'
import axiosClient from '@/services/HttpService'
import type { EhrFile } from '@/stores/tree-data'
import { useFileInfoStore } from '@/stores/file-info-data'
import FileIcon from '@/components/FileIcon.vue'
const { isPreview, fileInfo } = storeToRefs(useFileInfoStore())
const { getType } = useFileInfoStore()
const fileIconComp = ref<InstanceType<typeof FileIcon>>()
const { isFilePreview, fileInfo } = storeToRefs(useFileInfoStore())
const { getType, getFormatDate, getSize, getFileNameFormat } =
useFileInfoStore()
async function downloadSubmit(path: any) {
const [cabinet, drawer, folder, file] = path.split('/')
const formatPath = `/cabinet/${cabinet}/drawer/${drawer}/folder/${folder}/file/${file}`
const res = await axiosClient.get<EhrFile & { download: string }>(
`${import.meta.env.VITE_API_ENDPOINT}${formatPath}`,
)
await axios
.get(res.data.download, {
method: 'GET',
responseType: 'blob',
headers: {
'Content-Type': 'application/json',
Accept: res.data.fileType,
},
})
.then((r) => {
const a = document.createElement('a')
a.href = window.URL.createObjectURL(r.data)
a.download = res.data.fileName
a.click()
})
}
</script>
<template>
<div class="bg-white rounded-borders shadow-5 relative">
<div class="bg-white rounded-borders shadow-5 relative q-pb-md">
<div class="bg-white q-pa-md">
<div class="row items-center justify-between">
<div class="row items-center">
<span class="text-h6">
<q-btn
flat
dense
class="q-mr-sm q-px-sm"
@click="() => (isPreview = false)"
@click="() => (isFilePreview = false)"
>
<q-icon
class="pointer"
@ -28,43 +54,55 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
color="primary"
/>
</q-btn>
{{ fileInfo?.title }}</span
{{ getFileNameFormat(fileInfo?.fileName) }}</span
>
</div>
<div class="row">
<div class="q-pa-sm">
<div class="col">
<div class="box border-radius-inherit">
<q-card flat>
<q-card-section class="column justify-center relative q-px-xl">
<file-icon
size="preview"
:fileMimeType="fileInfo?.fileType"
ref="fileIconComp"
/>
<div class="absolute" style="top: 0.5rem; right: 0.5rem">
<file-item-action :edit="() => {}" :delete="() => {}" />
</div>
<span class="text-center q-pt-md text-h6">{{
fileInfo?.title
}}</span>
</q-card-section>
</q-card>
<div class="row q-mt-lg justify-center">
<div class="column q-pa-sm">
<div class="grid">
<div
:style="{
position: 'relative',
display: 'flex',
gap: '0.5rem',
flexDirection: 'column',
alignItems: 'center',
padding: '1rem',
maxWidth: '100%',
}"
class="box"
>
<div class="q-px-md flex items-center justify-center">
<file-icon
size="preview"
:fileMimeType="
fileInfo?.fileType ? fileInfo?.fileType : 'unknow'
"
/>
</div>
<div
class="text-overflow-handle block q-px-md text-center"
style="max-width: 100%"
>
{{ getFileNameFormat(fileInfo?.fileName) }}
</div>
</div>
<div class="column q-py-sm">
<q-btn
color="primary"
label="ดาวน์โหลด"
icon="mdi-download"
class="q-py-sm"
@click="() => downloadSubmit(fileInfo?.pathname)"
/>
</div>
</div>
<div class="column q-py-md">
<q-btn
color="primary"
label="ดาวน์โหลด"
icon="mdi-download"
class="q-py-sm"
/>
</div>
</div>
<div class="col q-px-lg q-gutter-md">
<div class="col-grow q-px-lg q-gutter-md q-pt-md">
<div class="row">
<div class="col-2">
<div class="col-12 col-md-2">
<span>อไฟล</span>
</div>
<div class="col-grow">
@ -73,7 +111,7 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
</div>
<q-separator />
<div class="row">
<div class="col-2">
<div class="col-12 col-md-2">
<span>อเรอง</span>
</div>
<div class="col-grow">
@ -82,7 +120,7 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
</div>
<q-separator />
<div class="row">
<div class="col-2">
<div class="col-12 col-md-2">
<span>รายละเอยด</span>
</div>
<div class="col-grow">
@ -91,34 +129,44 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
</div>
<q-separator />
<div class="row">
<div class="col-2">
<div class="col-12 col-md-2">
<span>กล/หมวดหม</span>
</div>
<div class="col-grow">
<span class="text-grey">{{ fileInfo?.category }}</span>
<span
class="text-grey"
v-for="category in fileInfo?.category"
:key="category"
>{{ category }}</span
>
</div>
</div>
<q-separator />
<div class="row">
<div class="col-2">
<div class="col-12 col-md-2">
<span>คำสำค</span>
</div>
<div class="col-grow">
<span class="text-grey">{{ fileInfo?.keyword }}</span>
<span
class="text-grey"
v-for="keyword in fileInfo?.keyword"
:key="keyword"
>{{ keyword }}</span
>
</div>
</div>
<q-separator />
<div class="row">
<div class="col-2">
<div class="col-12 col-md-2">
<span>ขนาดไฟล</span>
</div>
<div class="col-grow">
<span class="text-grey">{{ fileInfo?.fileSize }}</span>
<span class="text-grey">{{ getSize(fileInfo?.fileSize) }}</span>
</div>
</div>
<q-separator />
<div class="row">
<div class="col-2">
<div class="col-12 col-md-2">
<span>ประเภทไฟล</span>
</div>
<div class="col-grow">
@ -127,11 +175,13 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
</div>
<q-separator />
<div class="row">
<div class="col-2">
<div class="col-12 col-md-2">
<span>นทปโหลด</span>
</div>
<div class="col-grow">
<span class="text-grey">{{ fileInfo?.createdAt }}</span>
<span class="text-grey">{{
getFormatDate(fileInfo?.createdAt)
}}</span>
</div>
</div>
</div>
@ -142,7 +192,24 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
<style lang="scss" scoped>
.box {
border: 2px solid #f1f2f4;
border: 2px solid $separator-color;
border-radius: 8px;
}
.text-overflow-handle {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.grid {
display: grid;
width: 250px;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 1rem;
}
.grid .box {
position: relative;
}
</style>

View file

@ -1,19 +1,18 @@
<script setup lang="ts">
import axios from 'axios'
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import axiosClient from '@/services/HttpService'
import type { EhrFile } from '@/stores/tree-data'
import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue'
import { useSearchDataStore } from '@/stores/searched-data'
import { useLoader } from '@/stores/loader'
import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue'
const loaderStore = useLoader()
const { isSearch } = storeToRefs(useSearchDataStore())
const { isSearch, isAdvSearchCall } = storeToRefs(useSearchDataStore())
const { getFoundFile } = useSearchDataStore()
const advSearchComp = ref<InstanceType<typeof AdvancedSearch>>()
const isAdvSearchCall = ref<boolean>(false)
const optionsField = [
{ label: 'ชื่อเรื่อง (title)', value: 'title' },
{ label: 'คำสำคัญ (keyword)', value: 'keyword' },
@ -21,106 +20,71 @@ const optionsField = [
const searchData = ref<{
field: string
value: string
keyword?: string
description?: string
}>({
field: 'title',
value: '',
keyword: '',
description: '',
})
const submitSearchData = ref<{
AND: {
field?: string
value?: string
}[]
AND: { field: string; value: string }[]
OR: { field: string; value: string }[]
}>({
AND: [],
OR: [],
})
function clearSearchData() {
searchData.value = {
field: 'title',
value: '',
keyword: '',
description: '',
}
submitSearchData.value = {
AND: [],
OR: [],
}
advSearchComp.value?.clearAdvData()
isAdvSearchCall.value = false
isSearch.value = false
}
async function searchSubmit() {
submitSearchData.value = {
AND: [],
OR: [],
}
if (searchData.value.field && searchData.value) {
submitSearchData.value?.AND.push({
if (searchData.value.value.trim() !== '') {
submitSearchData.value = { AND: [], OR: [] }
submitSearchData.value.AND.push({
field: searchData.value.field,
value: searchData.value.value,
})
if (searchData.value.keyword) {
submitSearchData.value?.AND.push({
field: 'keyword',
value: searchData.value.keyword,
})
}
if (searchData.value.description) {
submitSearchData.value?.AND.push({
field: 'description',
value: searchData.value.description,
})
}
}
if (isAdvSearchCall.value) {
if (
advSearchComp.value?.advSearchData &&
advSearchComp.value.advSearchData.length > 0
) {
advSearchComp.value?.advSearchData.forEach(
(d: { field: string; value: string; op: string }) => {
if (d.field && d.value) {
if (d.op === 'AND') {
submitSearchData.value.AND.push({
field: d.field,
value: d.value,
})
} else if (d.op === 'OR') {
submitSearchData.value.OR.push({ field: d.field, value: d.value })
}
}
if (isAdvSearchCall.value && advSearchComp.value) {
const advField = advSearchComp.value.advSearchDataField
const advRow = advSearchComp.value.advSearchDataRow
advRow.forEach((d: { field: string; value: string; op: string }) => {
if (d.field && d.value.trim() !== '') {
const op = d.op === 'AND' ? 'AND' : 'OR'
submitSearchData.value[op].push({ field: d.field, value: d.value })
}
)
})
if (advField.keyword.trim() !== '') {
submitSearchData.value.AND.push({
field: 'keyword',
value: advField.keyword,
})
}
if (advField.description.trim() !== '') {
submitSearchData.value.AND.push({
field: 'description',
value: advField.description,
})
}
}
try {
loaderStore.show()
const res = await axiosClient.post<EhrFile[]>(
`${import.meta.env.VITE_API_ENDPOINT}/search`,
submitSearchData.value
)
getFoundFile(res.data)
isSearch.value = true
} catch (error) {
console.error('Error during the request', error)
} finally {
loaderStore.hide()
}
}
try {
loaderStore.show()
isAdvSearchCall.value = true
const res = await axios.post<EhrFile[]>(
`${import.meta.env.VITE_API_ENDPOINT}/search`,
submitSearchData.value
)
getFoundFile(res.data)
isSearch.value = true
} catch (error) {
console.error('Error during the request', error)
} finally {
loaderStore.hide()
}
}
</script>
<template>
<div class="q-py-lg q-px-md bg-grey-1">
<div class="row items-center q-gutter-md">
<div class="q-pa-md bg-grey-1">
<div class="row items-center q-col-gutter-md">
<div class="col-12 col-md-3">
<q-select
dense
@ -140,47 +104,36 @@ async function searchSubmit() {
v-model="searchData.value"
placeholder="เอกสาร"
@keydown.enter.prevent="searchSubmit"
/>
</div>
<div v-if="isAdvSearchCall === false">
<q-btn
class="col-12 col-md-2"
color="primary"
label="ค้นหา"
icon="mdi-magnify"
@click="searchSubmit"
/>
>
<template v-slot:append>
<q-icon
name="close"
@click="() => (searchData.value = '')"
class="cursor-pointer"
/>
</template>
</q-input>
</div>
</div>
<div
v-if="isAdvSearchCall"
class="row items-center justify-between q-gutter-y-md q-mt-xs"
>
<div>
<advanced-search
ref="advSearchComp"
:searchSubmit="searchSubmit"
:submit-search-data="submitSearchData"
v-model:field="searchData.field"
v-model:value="searchData.value"
v-model:keyword="searchData.keyword"
v-model:description="searchData.description"
/>
</div>
<div class="q-gutter-x-md">
<q-btn
color="primary"
label="ค้นหา"
icon="mdi-magnify"
@click="searchSubmit"
/>
<q-btn
color="orange-12"
label="ล้างข้อมูล"
icon="close"
@click="clearSearchData"
/>
<div class="column">
<div class="row items-center justify-between q-gutter-y-md q-pt-sm">
<div class="column col-grow">
<advanced-search
ref="advSearchComp"
:searchSubmit="searchSubmit"
:submit-search-data="submitSearchData"
/>
</div>
<div v-if="isAdvSearchCall === false">
<q-btn
style="width: 150px"
color="primary"
label="ค้นหา"
icon="mdi-magnify"
@click="searchSubmit"
/>
</div>
</div>
</div>
</div>

View file

@ -1,49 +1,60 @@
import { createRouter, createWebHistory } from 'vue-router'
import UserModule from '@/modules/01_user/router'
import AdminModule from '@/modules/02_admin/router'
import KeyCloakService from '@/services/KeyCloakService'
import { getRole, getToken, login } from '@/services/KeyCloakService'
const history = createWebHistory(import.meta.env.BASE_URL)
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
history,
routes: [
{
path: '/',
name: 'UserModule',
component: () => import('@/views/MainLayout.vue'),
children: [...UserModule],
meta: {
statusAccount: false,
beforeEnter: async (_to, _from, next) => {
const token = await getToken()
if (token) return next()
await login(async () => {
if (await getToken()) return next()
return next('/')
})
},
children: [...UserModule],
meta: { statusAccount: false },
},
{
path: '/admin',
name: 'AdminModule',
component: () => import('@/views/MainLayout.vue'),
beforeEnter: (_to, _from, next) => {
const token = KeyCloakService.GetAccesToken()
beforeEnter: async (_to, _from, next) => {
const token = await getToken()
if (token) {
next()
} else {
KeyCloakService.CallLogin(() => {
const tokenAfterLogin = KeyCloakService.GetAccesToken()
if (tokenAfterLogin) {
next()
} else {
console.error('ไม่สามารถดึง Token หลังจากล็อกอินได้')
next('/')
}
})
const roles = getRole()
if (token && roles.includes('admin')) {
return next()
}
return next('/')
}
await login(async () => {
const token = await getToken()
const roles = getRole()
if (token && roles.includes('admin')) {
return next()
}
return next('/')
})
},
meta: {
statusAccount: true,
},
meta: { statusAccount: true },
children: [...AdminModule],
},
/**
* 404 Not Found
* ref: https://router.vuejs.org/guide/essentials/dynamic-matching.html#catch-all-404-not-found-route
*/
{
path: '/:pathMatch(.*)*',
name: 'NotFound',

View file

@ -1,37 +1,32 @@
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import axios from 'axios'
import KeyCloakService from '@/services/KeyCloakService'
import { getToken } from './KeyCloakService'
import { useErrorStore } from '@/stores/error'
const HttpMethods = {
GET: 'GET',
POST: 'POST',
DELETE: 'DELETE',
}
const error = useErrorStore()
const _axios = axios.create()
const cb = (config: InternalAxiosRequestConfig) => {
config.headers.Authorization = `Bearer ${KeyCloakService.GetAccesToken()}`
console.log(config.headers)
const instance = axios.create()
instance.interceptors.request.use(async (config) => {
config.headers.Authorization = `Bearer ${await getToken()}`
return config
}
})
const configureAxiosKeycloak = (): void => {
_axios.interceptors.request.use(
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
if (KeyCloakService.IsLoggedIn()) {
KeyCloakService.UpdateToken(cb(config))
}
return config
instance.interceptors.response.use(
(res) => res,
(err) => {
const status = err.response.status
const data = err.response.data
error.title = 'เกิดข้อผิดพลาด'
if (status === 500) {
error.msg = 'เกิดข้อผิดพลาด ไม่สามารถดำเนินการต่อได้ กรุณาลองใหม่อีกครั้ง'
} else {
error.msg = data.message
}
)
}
const getAxiosClient = (): AxiosInstance => _axios
error.show()
},
)
const HttpService = {
HttpMethods,
configureAxiosKeycloak,
getAxiosClient,
}
export default HttpService
export default instance

View file

@ -1,71 +1,42 @@
import Keycloak from "keycloak-js";
import Keycloak from 'keycloak-js'
const keycloakInstance = new Keycloak();
const keycloak = new Keycloak()
interface CallbackOneParam<T1 = void, T2 = void> {
(param1: T1): T2;
}
/**
* Initializes Keycloak instance and calls the provided callback function if successfully authenticated.
*
* @param onAuthenticatedCallback
*/
const Login = (onAuthenticatedCallback: CallbackOneParam): void => {
keycloakInstance
.init({ onLoad: "login-required" })
.then(function (authenticated) {
authenticated ? onAuthenticatedCallback() : alert("non authenticated");
export async function login(cb?: (...args: any[]) => void) {
const auth = await keycloak
.init({
onLoad: 'login-required',
responseMode: 'query',
checkLoginIframe: false,
})
.catch((e) => {
console.dir(e);
console.log(`keycloak init exception: ${e}`);
});
};
.catch((e) => console.dir(e))
const UserName = (): string | undefined =>
keycloakInstance?.tokenParsed?.preferred_username;
const Token = (): string | undefined => keycloakInstance?.token;
const IdToken = (): string | undefined => keycloakInstance?.idToken;
const LogOut = () => keycloakInstance.logout();
/*
const UserRoles = (): string[] | undefined => {
if (keycloakInstance.resourceAccess === undefined) return undefined;
if (keycloakInstance.resourceAccess["express-client"] === undefined) return undefined;
return keycloakInstance.resourceAccess["express-client"].roles;
};
*/
const UserRoles = ():string[] =>{
return DecodeToken()?.role
if (auth && cb) cb()
}
export async function logout() {
await keycloak.logout()
}
const updateToken = (successCallback: any) =>
keycloakInstance.updateToken(5).then(successCallback).catch(doLogin);
export async function getToken() {
await keycloak.updateToken(60).catch(() => login())
return keycloak.token
}
const doLogin = keycloakInstance.login;
export function getUsername(): string {
return keycloak.tokenParsed?.preferred_username
}
const isLoggedIn = () => !!keycloakInstance.token;
export function getRole(): string[] {
const decoded = keycloak.tokenParsed
const DecodeToken = ()=>{return keycloakInstance.tokenParsed}
const DecodeIdToken = ()=>{return keycloakInstance.idTokenParsed}
const KeycloakService = {
CallLogin: Login,
GetUserName: UserName,
GetAccesToken: Token,
GetIdToken: IdToken,
CallLogOut: LogOut,
GetUserRoles: UserRoles,
UpdateToken: updateToken,
IsLoggedIn: isLoggedIn,
GetDecodeToken:DecodeToken,
GetDecodeIdToken:DecodeIdToken
};
export default KeycloakService;
if (decoded && decoded.resource_access && decoded.azp) {
return decoded.resource_access[decoded.azp].roles
}
return []
}
export function isLoggedIn() {
return !!keycloak.token
}

View file

@ -0,0 +1,28 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useErrorStore = defineStore('error-data', () => {
const visible = ref<boolean>(false)
const title = ref<string>('')
const msg = ref<string>('')
function set(obj: { title: string; msg: string }, timeout?: number) {
title.value = obj.title
msg.value = obj.msg
show(timeout)
}
function show(timeout: number = -1) {
visible.value = true
if (timeout > 0) {
setTimeout(() => (visible.value = false), timeout)
}
}
function hide() {
visible.value = false
}
return { visible, title, msg, show, hide, set }
})

View file

@ -6,156 +6,182 @@ import type { EhrFile } from '@/stores/tree-data'
export interface MimeMap {
[key: string]: { icon: string; color: string; type: string }
}
export interface TypeSetting {
[key: string]: { icon: string; color: string }
}
export const useFileInfoStore = defineStore('info', () => {
const fileInfo = ref<EhrFile>()
const isPreview = ref<Boolean>(false)
const isFilePreview = ref<Boolean>(false)
const file: 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' },
txt: { icon: 'mdi-file-document-outline', color: 'blue-11' },
image: { icon: 'mdi-file-image-outline', color: 'blue-11' },
}
const mimeFileMapping: MimeMap = {
'application/msword': {
icon: 'mdi-file-word-outline',
color: 'blue-11',
...file.word,
type: '.doc',
},
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': {
icon: 'mdi-file-word-outline',
color: 'blue-11',
...file.word,
type: '.docx',
},
'application/vnd.openxmlformats-officedocument.wordprocessingml.template': {
icon: 'mdi-file-word-outline',
color: 'blue-11',
...file.word,
type: '.dotx',
},
'application/vnd.ms-word.document.macroEnabled.12': {
icon: 'mdi-file-word-outline',
color: 'blue-11',
...file.word,
type: '.docm',
},
'application/vnd.ms-word.template.macroEnabled.12': {
icon: 'mdi-file-word-outline',
color: 'blue-11',
...file.word,
type: '.dotm',
},
'application/vnd.ms-excel': {
icon: 'mdi-file-excel-outline',
color: 'green-12',
...file.excel,
type: '.xls',
},
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
icon: 'mdi-file-excel-outline',
color: 'green-12',
...file.excel,
type: '.xlsx',
},
'application/vnd.openxmlformats-officedocument.spreadsheetml.template': {
icon: 'mdi-file-excel-outline',
color: 'green-12',
...file.excel,
type: '.xltx',
},
'application/vnd.ms-excel.sheet.macroEnabled.12': {
icon: 'mdi-file-excel-outline',
color: 'green-12',
...file.excel,
type: '.xlsm',
},
'application/vnd.ms-excel.template.macroEnabled.12': {
icon: 'mdi-file-excel-outline',
color: 'green-12',
...file.excel,
type: '.xltm',
},
'application/vnd.ms-excel.addin.macroEnabled.12': {
icon: 'mdi-file-excel-outline',
color: 'green-12',
...file.excel,
type: '.xlam',
},
'application/vnd.ms-excel.sheet.binary.macroEnabled.12': {
icon: 'mdi-file-excel-outline',
color: 'green-12',
...file.excel,
type: '.xlsb',
},
'application/vnd.ms-powerpoint': {
icon: 'mdi-file-powerpoint-outline',
color: 'orange-12',
...file.powerpoint,
type: '.ppt',
},
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
{
icon: 'mdi-file-powerpoint-outline',
color: 'orange-12',
...file.powerpoint,
type: '.pptx',
},
'application/vnd.openxmlformats-officedocument.presentationml.template': {
icon: 'mdi-file-powerpoint-outline',
color: 'orange-12',
...file.powerpoint,
type: '.potx',
},
'application/vnd.openxmlformats-officedocument.presentationml.slideshow': {
icon: 'mdi-file-powerpoint-outline',
color: 'orange-12',
...file.powerpoint,
type: '.ppsx',
},
'application/vnd.ms-powerpoint.addin.macroEnabled.12': {
icon: 'mdi-file-powerpoint-outline',
color: 'orange-12',
...file.powerpoint,
type: '.ppam',
},
'application/vnd.ms-powerpoint.presentation.macroEnabled.12': {
icon: 'mdi-file-powerpoint-outline',
color: 'orange-12',
...file.powerpoint,
type: '.pptm',
},
'application/vnd.ms-powerpoint.template.macroEnabled.12': {
icon: 'mdi-file-powerpoint-outline',
color: 'orange-12',
...file.powerpoint,
type: '.potm',
},
'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': {
icon: 'mdi-file-powerpoint-outline',
color: 'orange-12',
...file.powerpoint,
type: '.ppsm',
},
'application/pdf': {
icon: 'mdi-file-document-outline',
color: 'red-12',
...file.pdf,
type: '.pdf',
},
'text/plain': {
icon: 'mdi-file-document-outline',
color: 'blue-11',
...file.txt,
type: '.txt',
},
'image/x-png': {
icon: 'mdi-file-image-outline',
color: 'blue-11',
...file.image,
type: '.png',
},
'image/x-citrix-jpeg': {
icon: 'mdi-file-image-outline',
color: 'blue-11',
...file.image,
type: '.jpg, jpeg',
},
}
function getType(mimeType: any) {
if (mimeFileMapping.hasOwnProperty(mimeType)) {
function getType(mimeType: string | undefined): string {
if (!!mimeType && mimeFileMapping.hasOwnProperty(mimeType)) {
return mimeFileMapping[mimeType].type
} else {
return 'unknow type'
}
return 'unknown type'
}
function getFormatDate(dateTime: string | undefined): string {
if (dateTime === undefined) {
return 'unknown date'
}
const date = new Date(dateTime)
return date.toLocaleDateString('th-TH', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
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'
}
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let i = 0
let sizeNumber = parseFloat(size)
while (sizeNumber >= 1024 && i < units.length - 1) {
sizeNumber /= 1024
i++
}
return sizeNumber.toFixed(2) + ' ' + units[i]
}
async function getFileInfo(data: EhrFile) {
isFilePreview.value = true
fileInfo.value = data
}
return {
mimeFileMapping,
isPreview,
isFilePreview,
fileInfo,
getFileInfo,
getType,
getFormatDate,
getFileNameFormat,
getSize,
getFileInfo,
}
})

View file

@ -4,6 +4,7 @@ import type { EhrFile } from '@/stores/tree-data'
export const useSearchDataStore = defineStore('searched', () => {
const foundFile = ref<EhrFile[]>([])
const isAdvSearchCall = ref<boolean>(false)
const isSearch = ref<Boolean>(false)
async function getFoundFile(data: EhrFile[]) {
@ -11,8 +12,9 @@ export const useSearchDataStore = defineStore('searched', () => {
}
return {
isSearch,
foundFile,
isSearch,
isAdvSearchCall,
getFoundFile,
}
})

View file

@ -1,9 +1,8 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { useLoader } from '@/stores/loader'
import HttpService from '@/services/HttpService'
import axiosClient from '@/services/HttpService'
const axiosClient = HttpService.getAxiosClient()
const apiEndpoint: string = import.meta.env.VITE_API_ENDPOINT
export interface EhrFolder {
@ -41,13 +40,16 @@ export interface TreeDataFolder {
export const useTreeDataStore = defineStore('changeCabinet', () => {
const loader = useLoader()
const indexToRemove = ref<number>()
const fileNameRemove = ref<string>('')
const data = ref<TreeDataFolder[]>([])
const currentFolder = ref<TreeDataFolder[]>([])
const currentFile = ref<EhrFile[]>([])
const currentPath = ref<string>('')
const currentDept = ref<number>(0)
const listDataFolder = ref<TreeDataFolder[]>()
const listDataFile = ref<EhrFile[]>()
async function getCabinet() {
const res = await axiosClient.get<EhrFolder[]>(`${apiEndpoint}cabinet`)
@ -56,6 +58,8 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
status: false,
folder: [],
}))
listDataFolder.value = data.value
}
async function getFolder(pathname: string, updateStatus = true) {
@ -129,6 +133,8 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
await getFile(pathname)
listDataFolder.value = currentFolder.value
return loader.hide()
}
@ -151,6 +157,8 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
currentFile.value = res.data
listDataFile.value = currentFile.value
return loader.hide()
}
@ -232,16 +240,145 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
await getFolder(pathname.join('/') + '/')
}
async function uploadFile(
pathname: string,
file: File,
metadata: {
title: string
description: string
keyword: string
category: string
}
) {
loader.show()
const pathArray: string[] = pathname.split('/').filter(Boolean)
if (pathArray.length <= 2) return
let requestPath = 'cabinet'
if (pathArray.length >= 1) requestPath += `/${pathArray[0]}`
if (pathArray.length >= 2) requestPath += `/drawer/${pathArray[1]}`
if (pathArray.length >= 3) requestPath += `/folder/${pathArray[2]}`
if (pathArray.length >= 4) requestPath += `/subfolder/${pathArray[3]}`
requestPath += '/file'
const res = await axiosClient.post<EhrFile & { upload: string }>(
`${apiEndpoint}${requestPath}`,
{
file: file.name,
...metadata,
}
)
if (res && res.data.upload) {
await fetch(res.data.upload, {
method: 'PUT',
body: file,
})
loader.hide()
}
if (currentDept.value === 0) {
await getCabinet()
} else await getFolder(currentPath.value)
await getFile(currentPath.value)
return loader.hide()
}
async function updateFile(
pathname: string,
metadata: {
title: string
description: string
keyword: string
category: string
},
file?: File
) {
loader.show()
const pathArray: string[] = pathname.split('/').filter(Boolean)
if (pathArray.length < 4) return loader.hide()
let requestPath = `cabinet/${pathArray[0]}/drawer/${pathArray[1]}`
if (pathArray.length >= 4) requestPath += `/folder/${pathArray[2]}`
if (pathArray.length >= 5) requestPath += `/subfolder/${pathArray[3]}`
requestPath += `/file/${pathArray.at(-1)}`
const res = await axiosClient.patch<{ upload: string }>(
`${apiEndpoint}${requestPath}`,
{ file: file?.name, ...metadata }
)
if (res && res.data.upload) {
await fetch(res.data.upload, {
method: 'PUT',
body: file,
})
loader.hide()
}
if (currentDept.value === 0) {
await getCabinet()
} else await getFolder(currentPath.value)
await getFile(currentPath.value)
return loader.hide()
}
async function deleteFile(pathname: string) {
loader.show()
const pathArray: string[] = pathname.split('/').filter(Boolean)
if (pathArray.length < 4) return loader.hide()
let requestPath = `cabinet/${pathArray[0]}/drawer/${pathArray[1]}`
if (pathArray.length >= 4) requestPath += `/folder/${pathArray[2]}`
if (pathArray.length >= 5) requestPath += `/subfolder/${pathArray[3]}`
requestPath += `/file/${pathArray.at(-1)}`
await axiosClient.delete(`${apiEndpoint}${requestPath}`)
if (currentDept.value === 0) {
await getCabinet()
} else await getFolder(currentPath.value)
await getFile(currentPath.value)
currentFile.value = currentFile.value.filter((v) => v.pathname !== pathname)
return loader.hide()
}
function checkFile(fileName: string) {
return currentFile.value.some((element) => element.fileName === fileName)
}
return {
data,
currentFolder,
currentFile,
currentDept,
currentPath,
listDataFile,
listDataFolder,
getCabinet,
getFolder,
uploadFile,
updateFile,
deleteFile,
gotoParent,
createFolder,
deleteFolder,
editFolder,
checkFile,
}
})

View file

@ -18,7 +18,6 @@ body
-webkit-font-smoothing: antialiased
-moz-osx-font-smoothing: grayscale
$separator-color: #EDEDED !default
$shadow-color: #c1c1c7 !default

View file

@ -24,14 +24,14 @@ const { loader } = storeToRefs(loaderStore)
</div>
<q-space></q-space>
<profile v-if="$route.meta.statusAccount" />
<profile />
</q-toolbar>
</q-header>
<q-page-container>
<QPage>
<q-page>
<router-view :key="$route.fullPath" />
</QPage>
</q-page>
</q-page-container>
<full-loader :visibility="loader" />
</q-layout>

View file

@ -1,5 +1,5 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {

View file

@ -1,5 +1,5 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"extends": ["@tsconfig/node18/tsconfig.json", "@vue/tsconfig/tsconfig.json"],
"include": [
"vite.config.*",
"vitest.config.*",

View file

@ -1,18 +1,13 @@
PUBLIC_KEY=
PORT=
MINIO_HOST=localhost
MINIO_PORT=443
MINIO_SSL=true
MINIO_PORT=9000
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_BUCKET=
ELASTICSEARCH_PROTOCOL=http
ELASTICSEARCH_HOST=localhost
ELASTICSEARCH_PORT=9200
ELASTICSEARCH_INDEX=
AMQ_URL=amqp://admin:1234@localhost:9999
AMQ_QUEUE=queue

View file

@ -0,0 +1,66 @@
version: "3.8"
name: "ehr"
services:
elasticsearch:
build:
context: .
restart: unless-stopped
ports:
- 9200:9200
volumes:
- elasticsearch-data:/usr/share/elasticsearch/data
environment:
- xpack.security.enabled=false
- discovery.type=single-node
kibana:
image: kibana:8.10.2
restart: unless-stopped
depends_on:
- elasticsearch
ports:
- 5601:5601
volumes:
- kibana-data:/usr/share/kibana/data
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
minio:
image: minio/minio:latest
restart: unless-stopped
depends_on:
- elasticsearch
command: server --console-address ":9001" /data
ports:
- 9000:9000
- 9001:9001
volumes:
- minio-data:/data
environment:
- MINIO_ROOT_USER=ehr
- MINIO_ROOT_PASSWORD=P@ssw0rd
keycloak:
image: quay.io/keycloak/keycloak:22.0.3
restart: unless-stopped
command:
- start-dev
ports:
- 8080:8080
volumes:
- keycloak-data:/opt/keycloak/data
environment:
- KEYCLOAK_ADMIN=ehr
- KEYCLOAK_ADMIN_PASSWORD=P@ssw0rd
volumes:
elasticsearch-data:
driver: local
kibana-data:
driver: local
minio-data:
driver: local
keycloak-data:
driver: local

3392
Services/server/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -19,9 +19,6 @@
"dependencies": {
"@elastic/elasticsearch": "^8.10.0",
"@tsoa/runtime": "^5.0.0",
"@types/cors": "^2.8.16",
"@types/jsonwebtoken": "^9.0.5",
"@types/multer": "^1.4.10",
"amqplib": "^0.10.3",
"concurrently": "^8.2.2",
"cors": "^2.8.5",
@ -29,14 +26,16 @@
"express": "^4.18.2",
"fast-jwt": "^3.3.1",
"minio": "^7.1.3",
"multer": "1.4.5-lts.1",
"prettier": "^3.1.0",
"promise.any": "^2.0.6",
"swagger-ui-express": "^5.0.0",
"tsoa": "^5.1.1"
},
"devDependencies": {
"@types/amqplib": "^0.10.4",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.9.0",
"@types/swagger-ui-express": "^4.1.6",
"nodemon": "^3.0.1",

View file

@ -11,15 +11,6 @@ dependencies:
'@tsoa/runtime':
specifier: ^5.0.0
version: 5.0.0
'@types/cors':
specifier: ^2.8.16
version: 2.8.16
'@types/jsonwebtoken':
specifier: ^9.0.5
version: 9.0.5
'@types/multer':
specifier: ^1.4.10
version: 1.4.10
amqplib:
specifier: ^0.10.3
version: 0.10.3
@ -41,12 +32,12 @@ dependencies:
minio:
specifier: ^7.1.3
version: 7.1.3
multer:
specifier: 1.4.5-lts.1
version: 1.4.5-lts.1
prettier:
specifier: ^3.1.0
version: 3.1.0
promise.any:
specifier: ^2.0.6
version: 2.0.6
swagger-ui-express:
specifier: ^5.0.0
version: 5.0.0(express@4.18.2)
@ -58,9 +49,15 @@ devDependencies:
'@types/amqplib':
specifier: ^0.10.4
version: 0.10.4
'@types/cors':
specifier: ^2.8.17
version: 2.8.17
'@types/express':
specifier: ^4.17.21
version: 4.17.21
'@types/jsonwebtoken':
specifier: ^9.0.5
version: 9.0.5
'@types/node':
specifier: ^20.9.0
version: 20.9.0
@ -218,11 +215,11 @@ packages:
dependencies:
'@types/node': 20.9.0
/@types/cors@2.8.16:
resolution: {integrity: sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==}
/@types/cors@2.8.17:
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
dependencies:
'@types/node': 20.9.0
dev: false
dev: true
/@types/express-serve-static-core@4.17.41:
resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==}
@ -247,7 +244,7 @@ packages:
resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==}
dependencies:
'@types/node': 20.9.0
dev: false
dev: true
/@types/mime@1.3.5:
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
@ -357,10 +354,6 @@ packages:
picomatch: 2.3.1
dev: true
/append-field@1.0.0:
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
dev: false
/arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
dev: true
@ -488,21 +481,10 @@ packages:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
dev: false
/buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: false
/buffer-more-ints@1.0.0:
resolution: {integrity: sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==}
dev: false
/busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
dependencies:
streamsearch: 1.1.0
dev: false
/bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@ -562,16 +544,6 @@ packages:
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
/concat-stream@1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 2.3.8
typedarray: 0.0.6
dev: false
/concurrently@8.2.2:
resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==}
engines: {node: ^14.13.0 || >=16.0.0}
@ -1306,10 +1278,6 @@ packages:
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
dev: false
/isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: false
/isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
dev: false
@ -1449,13 +1417,6 @@ packages:
xml2js: 0.5.0
dev: false
/mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
dependencies:
minimist: 1.2.8
dev: false
/mnemonist@0.39.5:
resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==}
dependencies:
@ -1473,19 +1434,6 @@ packages:
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
/multer@1.4.5-lts.1:
resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==}
engines: {node: '>= 6.0.0'}
dependencies:
append-field: 1.0.0
busboy: 1.6.0
concat-stream: 1.6.2
mkdirp: 0.5.6
object-assign: 4.1.1
type-is: 1.6.18
xtend: 4.0.2
dev: false
/negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
@ -1590,10 +1538,6 @@ packages:
hasBin: true
dev: false
/process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: false
/promise.any@2.0.6:
resolution: {integrity: sha512-Ew/MrPtTjiHnnki0AA2hS2o65JaZ5n+5pp08JSyWWUdeOGF4F41P+Dn+rdqnaOV/FTxhR6eBDX412luwn3th9g==}
engines: {node: '>= 0.4'}
@ -1664,18 +1608,6 @@ packages:
string_decoder: 0.10.31
dev: false
/readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
dev: false
/readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@ -1884,11 +1816,6 @@ packages:
internal-slot: 1.0.6
dev: false
/streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
dev: false
/strict-uri-encode@2.0.0:
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
engines: {node: '>=4'}
@ -1932,12 +1859,6 @@ packages:
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
dev: false
/string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
safe-buffer: 5.1.2
dev: false
/string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies:
@ -2110,10 +2031,6 @@ packages:
is-typed-array: 1.1.12
dev: false
/typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
dev: false
/typedoc@0.25.3(typescript@5.2.2):
resolution: {integrity: sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==}
engines: {node: '>= 16'}
@ -2292,11 +2209,6 @@ packages:
engines: {node: '>=4.0'}
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
dev: false
/y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}

View file

@ -19,7 +19,6 @@ if (process.env.NODE_ENV !== "production") app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static("static"));
app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { explorer: false }));
RegisterRoutes(router);
@ -27,10 +26,9 @@ RegisterRoutes(router);
app.use(swaggerSpecs.basePath, router);
app.use(errorHandler);
app.use(express.static('static'))
app.get('*',(req,res)=>{
res.sendFile(`${process.cwd()}/static/index.html`)
})
app.use((_req, res, _next) => {
res.sendFile(`${process.cwd()}/static/index.html`);
});
app.listen(PORT, "0.0.0.0", () =>
console.log(`[APP] Application is running on http://localhost:${PORT}`),

View file

@ -114,7 +114,8 @@ export class CabinetController extends Controller {
list.map(async (current) => {
if (!current.name) return;
const destination = `${replaceIllegalChars(body.name)}/${current.name.slice(path.length)}`;
const base = `${replaceIllegalChars(body.name)}/`;
const destination = `${base}${current.name.slice(path.length)}`;
const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient
@ -138,7 +139,11 @@ export class CabinetController extends Controller {
await esClient.update({
index: DEFAULT_INDEX!,
id: data._id,
doc: { pathname: destination },
doc: {
pathname: destination,
path: destination.split("/").slice(0, -1).join("/") + "/",
},
refresh: "wait_for",
});
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);

View file

@ -131,9 +131,8 @@ export class DrawerController extends Controller {
list.map(async (current) => {
if (!current.name) return;
const destination = `${cabinetName}/${replaceIllegalChars(body.name)}/${current.name.slice(
path.length,
)}`;
const base = `${cabinetName}/${replaceIllegalChars(body.name)}/`;
const destination = `${base}${current.name.slice(path.length)}`;
const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient
@ -157,7 +156,11 @@ export class DrawerController extends Controller {
await esClient.update({
index: DEFAULT_INDEX!,
id: data._id,
doc: { pathname: destination },
doc: {
pathname: destination,
path: destination.split("/").slice(0, -1).join("/") + "/",
},
refresh: "wait_for",
});
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);

View file

@ -43,8 +43,8 @@ export class FileController extends Controller {
const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX!,
query: {
prefix: {
pathname: `${cabinetName}/${drawerName}/${folderName}/`,
match: {
path: `${cabinetName}/${drawerName}/${folderName}/`,
},
},
size: 10000,
@ -84,6 +84,10 @@ export class FileController extends Controller {
@Path() drawerName: string,
@Path() folderName: string,
) {
if (body.file.length > 85) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเิกนกำหนด");
}
const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
const pathname = `${basePath}${body.file}`;
@ -111,10 +115,11 @@ export class FileController extends Controller {
.catch((e) => console.error(e));
}
const rec = result ? result.hits.hits[0]._source : false;
const rec = result && result.hits.hits.length > 0 ? result.hits.hits[0]._source : false;
const metadata: Partial<StorageFile> = {
pathname,
path: basePath,
fileName: body.file,
fileSize: 0,
fileType: "",
@ -132,6 +137,7 @@ export class FileController extends Controller {
await esClient.index({
index: DEFAULT_INDEX!,
document: metadata,
refresh: "wait_for",
});
return {
@ -165,6 +171,10 @@ export class FileController extends Controller {
keyword?: string;
},
): Promise<void | { upload: string }> {
if (body.file && body.file.length > 85) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเิกนกำหนด");
}
const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
const pathname = `${basePath}${fileName}`;
@ -201,9 +211,12 @@ export class FileController extends Controller {
id,
doc: {
pathname: destination,
path: basePath,
fileName: body.file,
updatedAt: new Date().toISOString(),
updatedBy: request.user.preferred_username ?? "n/a",
},
refresh: "wait_for",
})
.then(() => minioClient.removeObject(DEFAULT_BUCKET!, pathname));
} else {
@ -230,6 +243,7 @@ export class FileController extends Controller {
updatedAt: new Date().toISOString(),
updatedBy: request.user.preferred_username ?? "n/a",
},
refresh: "wait_for",
});
}
}

View file

@ -131,16 +131,15 @@ export class FolderController extends Controller {
@Path() drawerName: string,
@Path() folderName: string,
) {
const path = `${cabinetName}/${drawerName}/${folderName}`;
const path = `${cabinetName}/${drawerName}/${folderName}/`;
const list = await listItem(DEFAULT_BUCKET!, path, true);
await Promise.all(
list.map(async (current) => {
if (!current.name) return;
const destination = `${cabinetName}/${drawerName}/${replaceIllegalChars(
body.name,
)}/${current.name.slice(path.length)}`;
const base = `${cabinetName}/${drawerName}/${replaceIllegalChars(body.name)}/`;
const destination = `${base}${current.name.slice(path.length)}`;
const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient
@ -164,7 +163,11 @@ export class FolderController extends Controller {
await esClient.update({
index: DEFAULT_INDEX!,
id: data._id,
doc: { pathname: destination },
doc: {
pathname: destination,
path: destination.split("/").slice(0, -1).join("/") + "/",
},
refresh: "wait_for",
});
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);

View file

@ -139,9 +139,10 @@ export class SubFolderController extends Controller {
list.map(async (current) => {
if (!current.name) return;
const destination = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(
const base = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(
body.name,
)}/${current.name.slice(path.length)}`;
)}/`;
const destination = `${base}${current.name.slice(path.length)}`;
const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient
@ -165,7 +166,11 @@ export class SubFolderController extends Controller {
await esClient.update({
index: DEFAULT_INDEX!,
id: data._id,
doc: { pathname: destination },
doc: {
pathname: destination,
path: destination.split("/").slice(0, -1).join("/") + "/",
},
refresh: "wait_for",
});
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);

View file

@ -46,8 +46,8 @@ export class SubFolderFileController extends Controller {
const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX!,
query: {
prefix: {
pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`,
match: {
path: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`,
},
},
size: 10000,
@ -88,6 +88,10 @@ export class SubFolderFileController extends Controller {
@Path() folderName: string,
@Path() subFolderName: string,
) {
if (body.file && body.file.length > 85) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเิกนกำหนด");
}
const basePath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`;
const pathname = `${basePath}${body.file}`;
@ -115,10 +119,11 @@ export class SubFolderFileController extends Controller {
.catch((e) => console.error(e));
}
const rec = result ? result.hits.hits[0]._source : false;
const rec = result && result.hits.hits.length > 0 ? result.hits.hits[0]._source : false;
const metadata: Partial<StorageFile> = {
pathname,
path: basePath,
fileName: body.file,
fileSize: 0,
fileType: "",
@ -136,6 +141,7 @@ export class SubFolderFileController extends Controller {
await esClient.index({
index: DEFAULT_INDEX!,
document: metadata,
refresh: "wait_for",
});
return {
@ -169,6 +175,10 @@ export class SubFolderFileController extends Controller {
keyword?: string;
},
) {
if (body.file && body.file.length > 85) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเิกนกำหนด");
}
const basePath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`;
const pathname = `${basePath}${fileName}`;
@ -205,9 +215,11 @@ export class SubFolderFileController extends Controller {
id,
doc: {
pathname: destination,
fileName: body.file,
updatedAt: new Date().toISOString(),
updatedBy: request.user.preferred_username ?? "n/a",
},
refresh: "wait_for",
})
.then(() => minioClient.removeObject(DEFAULT_BUCKET!, pathname));
} else {
@ -234,6 +246,7 @@ export class SubFolderFileController extends Controller {
updatedAt: new Date().toISOString(),
updatedBy: request.user.preferred_username ?? "n/a",
},
refresh: "wait_for",
});
}
}
@ -261,7 +274,7 @@ export class SubFolderFileController extends Controller {
) {
await minioClient.removeObject(
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}/${fileName}/${subFolderName}/`,
`${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`,
);
return this.setStatus(HttpStatusCode.NO_CONTENT);
}

View file

@ -27,9 +27,7 @@ export interface StorageFile {
category: string[];
keyword: string[];
/**
* @private For internal use only.
*/
path: string;
upload: boolean;
updatedAt: string | Date;

View file

@ -94,11 +94,13 @@ async function handleNotFoundRecord(
buffer: Buffer,
stat: { size: number; type: string },
) {
const path = pathname.split("/").slice(0, -1).join("/") + "/";
const filename = pathname.split("/").at(-1);
const base64 = Buffer.from(buffer).toString("base64");
const metadata = {
pathname,
path,
fileName: filename ?? "n/a", // should not possible to fallback, but just in case.
fileSize: stat.size,
fileType: stat.type,

View file

@ -48,6 +48,7 @@ const models: TsoaRoute.Models = {
"description": {"dataType":"string","required":true},
"category": {"dataType":"array","array":{"dataType":"string"},"required":true},
"keyword": {"dataType":"array","array":{"dataType":"string"},"required":true},
"path": {"dataType":"string","required":true},
"upload": {"dataType":"boolean","required":true},
"updatedAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true},
"updatedBy": {"dataType":"string","required":true},
@ -576,6 +577,83 @@ 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('/storage/d',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.getFolder)),
function StorageController_getFolder(request: any, response: any, next: any) {
const args = {
path: {"in":"query","name":"path","required":true,"dataType":"string"},
bucket: {"in":"query","name":"bucket","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 StorageController();
const promise = controller.getFolder.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('/storage/d',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.createFolder)),
function StorageController_createFolder(request: any, response: any, next: any) {
const args = {
path: {"in":"query","name":"path","required":true,"dataType":"string"},
bucket: {"in":"query","name":"bucket","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 StorageController();
const promise = controller.createFolder.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 201, 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.put('/storage/d',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.updateFolder)),
function StorageController_updateFolder(request: any, response: any, next: any) {
const args = {
body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"to":{"dataType":"nestedObjectLiteral","nestedProperties":{"path":{"dataType":"string","required":true},"bucket":{"dataType":"string","required":true}},"required":true},"from":{"dataType":"nestedObjectLiteral","nestedProperties":{"path":{"dataType":"string","required":true},"bucket":{"dataType":"string","required":true}},"required":true}}},
};
// 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 StorageController();
const promise = controller.updateFolder.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 204, 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',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderController)),

View file

@ -79,6 +79,9 @@
},
"type": "array"
},
"path": {
"type": "string"
},
"upload": {
"type": "boolean"
},
@ -120,6 +123,7 @@
"description",
"category",
"keyword",
"path",
"upload",
"updatedAt",
"updatedBy",
@ -974,6 +978,9 @@
"upload": {
"type": "boolean"
},
"path": {
"type": "string"
},
"keyword": {
"items": {
"type": "string"
@ -1015,6 +1022,7 @@
"updatedBy",
"updatedAt",
"upload",
"path",
"keyword",
"category",
"description",
@ -1364,6 +1372,140 @@
}
}
},
"/storage/d": {
"get": {
"operationId": "GetFolder",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"items": {
"properties": {
"name": {
"type": "string"
},
"pathname": {
"type": "string"
}
},
"required": [
"name",
"pathname"
],
"type": "object"
},
"type": "array"
}
}
}
}
},
"security": [],
"parameters": [
{
"in": "query",
"name": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "bucket",
"required": false,
"schema": {
"type": "string"
}
}
]
},
"post": {
"operationId": "CreateFolder",
"responses": {
"201": {
"description": "Success"
}
},
"security": [],
"parameters": [
{
"in": "query",
"name": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "bucket",
"required": false,
"schema": {
"type": "string"
}
}
]
},
"put": {
"operationId": "UpdateFolder",
"responses": {
"204": {
"description": "Success"
}
},
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"properties": {
"to": {
"properties": {
"path": {
"type": "string"
},
"bucket": {
"type": "string"
}
},
"required": [
"path",
"bucket"
],
"type": "object"
},
"from": {
"properties": {
"path": {
"type": "string"
},
"bucket": {
"type": "string"
}
},
"required": [
"path",
"bucket"
],
"type": "object"
}
},
"required": [
"to",
"from"
],
"type": "object"
}
}
}
}
}
},
"/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder": {
"get": {
"operationId": "ListFolder",
@ -2070,6 +2212,9 @@
"upload": {
"type": "boolean"
},
"path": {
"type": "string"
},
"keyword": {
"items": {
"type": "string"
@ -2111,6 +2256,7 @@
"updatedBy",
"updatedAt",
"upload",
"path",
"keyword",
"category",
"description",

View file

@ -35,7 +35,11 @@ export async function expressAuthentication(
throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.");
}
if (scopes && !scopes.some((v) => payload.resource_access[payload.azp].roles.includes(v))) {
if (
scopes &&
scopes.length > 0 &&
scopes.some((v) => !payload.resource_access[payload.azp].roles.includes(v))
) {
throw new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action.");
}

View file

@ -34,11 +34,14 @@ esClient.indices.putMapping({
pathname: {
type: "keyword",
},
path: {
type: "keyword",
},
},
},
});
minioClient.makeBucket(DEFAULT_BUCKET!, (e) => {
if (!e) console.log("Configuration needed for Bucket Notification to AMQP");
if (!e) console.log("Success. Configuration is needed for Bucket Notification to AMQP.");
console.error(e);
});

View file

@ -12,5 +12,6 @@
"experimentalDecorators": true,
"skipLibCheck": true
}
},
"exclude": ["./tools/**/*"]
}