Frontend created and rewritten a few times, with some backend fixes #1

Merged
leo merged 110 commits from plaintable into main 2023-11-08 20:38:40 +00:00
16 changed files with 303 additions and 178 deletions
Showing only changes of commit b3fd9637eb - Show all commits

View File

@ -16,6 +16,7 @@ declare module 'vue' {
AImage: typeof import('ant-design-vue/es')['Image'] AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch'] AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
AModal: typeof import('ant-design-vue/es')['Modal']
APageHeader: typeof import('ant-design-vue/es')['PageHeader'] APageHeader: typeof import('ant-design-vue/es')['PageHeader']
APopover: typeof import('ant-design-vue/es')['Popover'] APopover: typeof import('ant-design-vue/es')['Popover']
AppNavigation: typeof import('./src/components/AppNavigation.vue')['default'] AppNavigation: typeof import('./src/components/AppNavigation.vue')['default']
@ -27,6 +28,7 @@ declare module 'vue' {
FileExplorer: typeof import('./src/components/FileExplorer.vue')['default'] FileExplorer: typeof import('./src/components/FileExplorer.vue')['default']
FileViewer: typeof import('./src/components/FileViewer.vue')['default'] FileViewer: typeof import('./src/components/FileViewer.vue')['default']
HeaderMain: typeof import('./src/components/HeaderMain.vue')['default'] HeaderMain: typeof import('./src/components/HeaderMain.vue')['default']
LoginModal: typeof import('./src/components/LoginModal.vue')['default']
NotificationLoading: typeof import('./src/components/NotificationLoading.vue')['default'] NotificationLoading: typeof import('./src/components/NotificationLoading.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

View File

@ -5,7 +5,8 @@
--table-background: #535353; --table-background: #535353;
--primary-color: #ffffff; --primary-color: #ffffff;
--secondary-color: #ccc; --secondary-color: #ccc;
--blue-color: #66ffeb --blue-color: #66ffeb;
--red-color: #ff4d4f;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
@ -15,7 +16,8 @@
--table-background: #535353; --table-background: #535353;
--primary-color: #ffffff; --primary-color: #ffffff;
--secondary-color: #ccc; --secondary-color: #ccc;
--blue-color: #66ffeb --blue-color: #66ffeb;
--red-color: #ff4d4f;
} }
} }
body { body {
@ -93,5 +95,11 @@ body {
.ant-empty-description{ .ant-empty-description{
color: var(--primary-color); color: var(--primary-color);
} }
.ant-modal .ant-modal-content{
background-color: var(--secondary-background);
}
.ant-modal .ant-modal-close-x{
color: var(--font-color);
}
} }

View File

@ -1,5 +1,6 @@
<template> <template>
<main> <main>
<context-holder />
<!-- <h2 v-if="!documentStore.loading && documentStore.error"> {{ documentStore.error }} </h2> --> <!-- <h2 v-if="!documentStore.loading && documentStore.error"> {{ documentStore.error }} </h2> -->
<div class="carousel-container" v-if="!documentStore.loading && documentStore.mainDocument[0] && documentStore.mainDocument[0].type === 'file'"> <div class="carousel-container" v-if="!documentStore.loading && documentStore.mainDocument[0] && documentStore.mainDocument[0].type === 'file'">
<FileCarousel></FileCarousel> <FileCarousel></FileCarousel>
@ -32,13 +33,27 @@
<template #content> <template #content>
<div class="more-action"> <div class="more-action">
<div class="action-container"> <div class="action-container">
<a-button type="text" class="action-button" :icon="h(ImportOutlined)"/>Open <a :href="`${record.type === 'folder'? linkBasePath+'#' : filesBasePath}/${record.name}`">
<a-button type="text" class="action-button" :icon="h(ImportOutlined)"/>
</a>
Open
</div>
<div v-if="record.type === 'folder-file'" class="action-container">
<a :href="`${filesBasePath}/${record.name}`" download>
<a-button type="text" class="action-button" :icon="h(DownloadOutlined)"/>
</a>
Download
</div> </div>
<div class="action-container"> <div class="action-container">
<a-button type="text" class="action-button" :icon="h(EditOutlined)"/> Rename <a-button type="text" class="action-button" @click="edit(record.key)" :icon="h(EditOutlined)"/> Rename
</div> </div>
<div class="action-container"> <div class="action-container">
<a-button type="text" class="action-button" :icon="h(LinkOutlined)"/> Share <a-button
type="text"
class="action-button"
@click="share(`${record.type === 'folder'? linkBasePath+'/#' : filesBasePath}/${record.name}`)"
:icon="h(LinkOutlined)"
/> Share
</div> </div>
<div class="action-container"> <div class="action-container">
<a-button type="text" class="action-button" :icon="h(CopyOutlined)"/> Copy <a-button type="text" class="action-button" :icon="h(CopyOutlined)"/> Copy
@ -69,14 +84,14 @@
import { EditOutlined, ImportOutlined, CheckOutlined,CopyOutlined, ScissorOutlined, LinkOutlined, DownloadOutlined, DeleteOutlined, EllipsisOutlined } from '@ant-design/icons-vue' import { EditOutlined, ImportOutlined, CheckOutlined,CopyOutlined, ScissorOutlined, LinkOutlined, DownloadOutlined, DeleteOutlined, EllipsisOutlined } from '@ant-design/icons-vue'
import type { TableColumnsType } from 'ant-design-vue'; import type { TableColumnsType } from 'ant-design-vue';
import Router from '@/router/index'; import Router from '@/router/index';
import { message } from 'ant-design-vue';
import type { Document, FolderDocument } from '@/repositories/Document'; import type { Document, FolderDocument } from '@/repositories/Document';
import FileCarousel from './FileCarousel.vue'; import FileCarousel from './FileCarousel.vue';
const [messageApi, contextHolder] = message.useMessage();
type Key = string | number; type Key = string | number;
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
watchEffect(()=>{
console.log(documentStore.mainDocument)
})
const editableData: UnwrapRef<Record<string, Document>> = reactive({}); const editableData: UnwrapRef<Record<string, Document>> = reactive({});
const state = reactive<{ const state = reactive<{
selectedRowKeys: Key[]; selectedRowKeys: Key[];
@ -143,7 +158,10 @@
Object.assign(documentStore.mainDocument.filter(item => key === item.key)[0], editableData[key]); Object.assign(documentStore.mainDocument.filter(item => key === item.key)[0], editableData[key]);
delete editableData[key]; delete editableData[key];
}; };
const share = async (url : string) => {
await navigator.clipboard.writeText(location.origin + url)
messageApi.success("Link successfully copied to the clipboard");
}
</script> </script>
<style> <style>

View File

@ -30,11 +30,6 @@ import { url_document_get } from '@/repositories/Document';
const dataURL = ref('') const dataURL = ref('')
watchEffect(()=>{ watchEffect(()=>{
console.log('😎😎😎😎')
console.log(url_document_get)
console.log(Router.currentRoute)
console.log(Router.currentRoute.value.path)
console.log('----------')
dataURL.value = new URL( dataURL.value = new URL(
url_document_get + Router.currentRoute.value.path, url_document_get + Router.currentRoute.value.path,
location.origin location.origin

View File

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
import LoginModal from '@/components/LoginModal.vue'
import UploadButton from '@/components/UploadButton.vue' import UploadButton from '@/components/UploadButton.vue'
import { InfoCircleOutlined, SettingOutlined, PlusSquareOutlined, SearchOutlined, DeleteOutlined, DownloadOutlined, FileAddFilled, LinkOutlined, FolderAddFilled, FileFilled, FolderFilled } from '@ant-design/icons-vue' import { InfoCircleOutlined, SettingOutlined, PlusSquareOutlined, SearchOutlined, DeleteOutlined, DownloadOutlined, FileAddFilled, LinkOutlined, FolderAddFilled, FileFilled, FolderFilled } from '@ant-design/icons-vue'
import { h, ref, defineProps } from 'vue'; import { h, ref } from 'vue';
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
const searchQuery = ref<string>('') const searchQuery = ref<string>('')
@ -94,8 +95,8 @@ function download(){
</a-tooltip> </a-tooltip>
</template> </template>
</div> </div>
<div class="actions-list"> <div class="actions-list">
<LoginModal></LoginModal>
<template v-if="showSearchInput"> <template v-if="showSearchInput">
<a-input-search <a-input-search
v-model="searchQuery" v-model="searchQuery"

View File

@ -0,0 +1,90 @@
<template>
<a-tooltip title="Login">
<template v-if="DocumentStore.isUserLogged">
<a-button @click="logout" type="text" class="action-button" :icon="h(UserDeleteOutlined)" />
</template>
<template v-else>
<a-button @click="showModal" type="text" class="action-button" :icon="h(UserOutlined)" />
</template>
</a-tooltip>
<a-modal v-model:open="DocumentStore.user.isOpenLoginModal" :confirm-loading="confirmLoading" okText="Login" @ok="login">
<div class="login-container">
<a-form :model="loginForm">
<a-form-item label="Username" prop="username" :rules="[{ required: true, message: 'Please input your username!' }]">
<a-input v-model:value="loginForm.username" />
</a-form-item>
<a-form-item label="Password" prop="password" :rules="[{ required: true, message: 'Please input your password!' }]">
<a-input type="password" v-model:value="loginForm.password" />
</a-form-item>
<h3 v-if="loginForm.error.length > 0" class="error-text">{{loginForm.error}}</h3>
</a-form>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, h } from 'vue';
import { UserOutlined, UserDeleteOutlined } from '@ant-design/icons-vue';
import { useDocumentStore } from '@/stores/documents';
import { loginUser, logoutUser } from '@/repositories/User';
import type { ISimpleError } from '@/repositories/Client';
const DocumentStore = useDocumentStore();
const confirmLoading = ref<boolean>(false);
const showModal = () => {
DocumentStore.user.isOpenLoginModal = true;
};
const logout = async () => {
try {
await logoutUser();
} catch (error) {} finally {
location.reload();
}
}
const loginForm = ref({
username: '',
password: '',
error: '',
});
const login = async () => {
try {
loginForm.value.error = '';
confirmLoading.value = true;
const user = await loginUser(loginForm.value.username, loginForm.value.password);
if(user){
location.reload();
}
} catch (error) {
const httpError = error as ISimpleError
if(httpError.name){
loginForm.value.error = httpError.message
}
}finally{
confirmLoading.value = false;
}
};
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 30vh;
}
.button-login {
background-color: var(--secondary-color);
color: var(--secondary-background);
}
.ant-btn-primary:not(:disabled):hover{
background-color: var(--blue-color);
}
.error-text {
color :var(--red-color)
}
</style>

View File

@ -1,4 +1,4 @@
import axios from 'axios' import axios, {AxiosError} from 'axios'
/* Base domain for all request */ /* Base domain for all request */
export const baseURL = import.meta.env.VITE_URL_DOCUMENT export const baseURL = import.meta.env.VITE_URL_DOCUMENT
@ -17,9 +17,10 @@ Client.interceptors.response.use(
// Do something with response data // Do something with response data
return response return response
}, },
(error) => { (error: AxiosError<any>) => {
const msg = error.response?.data.message ?? 'Unexpected error' const msg = error.response && error.response.data && error.response.data.error ?
const code = error.code ? Number(error.code) : 500 error.response.data.error.message : 'Unexpected error'
const code = error.code ? Number(error.response?.status) : 500
const standardizedError = new SimpleError(code, msg) const standardizedError = new SimpleError(code, msg)
return Promise.reject(standardizedError) return Promise.reject(standardizedError)

View File

@ -23,6 +23,14 @@ export type FileDocument = BaseDocument & {
data: string; data: string;
}; };
export type errorEvent = {
error: {
code : number;
message: string;
redirect: string;
}
};
export type Document = FolderDocument | FileDocument; export type Document = FolderDocument | FileDocument;
@ -44,19 +52,36 @@ export class DocumentHandler {
case !!msg.update: case !!msg.update:
this.handleUpdateMessage(msg); this.handleUpdateMessage(msg);
break; break;
case !!msg.error:
this.handleError(msg);
break;
default: default:
} }
} }
private handleRootMessage({ root }: { root: FileStructure }) { private handleRootMessage({ root }: { root: FileStructure }) {
if (this.store && this.store.root) this.store.root = root; if (this.store && this.store.root) {
this.store.user.isLoggedIn = true;
this.store.root = root;
}
} }
private handleUpdateMessage(updateData: { update: FileStructure[] }) { private handleUpdateMessage(updateData: { update: FileStructure[] }) {
const root = updateData.update[0] const root = updateData.update[0]
if(root) this.store.root = root if(root) {
this.store.user.isLoggedIn = true;
this.store.root = root
}
}
private handleError(msg: errorEvent){
if(msg.error.code === 401){
this.store.user.isOpenLoginModal = true;
this.store.user.isLoggedIn = false;
return
}
} }
} }
export class DocumentUploadHandler { export class DocumentUploadHandler {
constructor( private store: DocumentStore = useDocumentStore() ) { constructor( private store: DocumentStore = useDocumentStore() ) {
this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this); this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this);

View File

@ -0,0 +1,23 @@
import Client from '@/repositories/Client'
export const url_login = '/login'
export const url_logout = '/logout '
export async function loginUser(username : string, password: string){
try {
const user = await Client.post(url_login, {
username, password
})
return user;
} catch (error) {
throw error
}
}
export async function logoutUser(){
try {
const data = await Client.post(url_logout)
return data;
} catch (error) {
throw error
}
}

View File

@ -1,6 +1,5 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import ExplorerView from '../views/ExplorerView.vue' import ExplorerView from '../views/ExplorerView.vue'
import LoginView from '../views/LoginView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),

View File

@ -10,6 +10,10 @@ type DirectoryData = {
[filename: string]: FileData; [filename: string]: FileData;
}; };
export type FileStructure = {id: string, mtime: number, size: number, dir: DirectoryData}; export type FileStructure = {id: string, mtime: number, size: number, dir: DirectoryData};
type User = {
isOpenLoginModal: boolean,
isLoggedIn : boolean,
}
export type DocumentStore = { export type DocumentStore = {
root: FileStructure, root: FileStructure,
@ -20,6 +24,7 @@ export type DocumentStore = {
wsWatch: WebSocket | undefined, wsWatch: WebSocket | undefined,
wsUpload: WebSocket | undefined, wsUpload: WebSocket | undefined,
selectedDocuments: Document[], selectedDocuments: Document[],
user: User,
error: string, error: string,
} }
@ -35,6 +40,7 @@ export const useDocumentStore = defineStore({
wsUpload: undefined, wsUpload: undefined,
selectedDocuments: [] as Document[], selectedDocuments: [] as Document[],
error: '' as string, error: '' as string,
user: { isLoggedIn: false, isOpenLoginModal: false } as User
}), }),
actions: { actions: {
@ -140,7 +146,7 @@ export const useDocumentStore = defineStore({
for (const d of this.document) { for (const d of this.document) {
if ("mtime" in d) d.modified = formatUnixDate(d.mtime) if ("mtime" in d) d.modified = formatUnixDate(d.mtime)
} }
} },
}, },
getters: { getters: {
mainDocument(): Document[] { mainDocument(): Document[] {
@ -151,6 +157,9 @@ export const useDocumentStore = defineStore({
}, },
rootMain(): DirectoryData | undefined { rootMain(): DirectoryData | undefined {
if(this.root) return this.root.dir if(this.root) return this.root.dir
},
isUserLogged(): boolean{
return this.user.isLoggedIn
} }
}, },
}); });

View File

@ -1,46 +0,0 @@
<template>
<div class="login-container">
<a-form :model="loginForm">
<a-form-item label="Username" prop="username" :rules="[{ required: true, message: 'Please input your username!' }]">
<a-input v-model:value="loginForm.username" />
</a-form-item>
<a-form-item label="Password" prop="password" :rules="[{ required: true, message: 'Please input your password!' }]">
<a-input type="password" v-model:value="loginForm.password" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="login" class="button-login">Login</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const loginForm = ref({
username: '',
password: '',
});
const login = () => {
};
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.button-login {
background-color: var(--secondary-color);
color: var(--secondary-background);
}
.ant-btn-primary:not(:disabled):hover{
background-color: var(--blue-color);
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Vite Vasanko</title> <title>Vite Vasanko</title>
<script type="module" crossorigin src="/assets/index-10851222.js"></script> <script type="module" crossorigin src="/assets/index-25722397.js"></script>
<link rel="stylesheet" href="/assets/index-ee545ab1.css"> <link rel="stylesheet" href="/assets/index-213aec14.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>