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
Showing only changes of commit 7e5901a2cf - Show all commits

View File

@ -1,227 +1,215 @@
<template> <template>
<main> <main>
<context-holder /> <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>
</div> </div>
<table v-else-if="!documentStore.loading && documentStore.mainDocument"> <table v-else-if="!documentStore.loading && documentStore.mainDocument">
<thead> <thead>
<tr> <tr>
<th class="selection"><input type="checkbox" v-model="allSelected" :indeterminate="selectionIndeterminate"></th> <th class="selection"><input type="checkbox" v-model="allSelected" :indeterminate="selectionIndeterminate"></th>
<th class="sortcolumn" :class="{sortactive: sort === 'name'}" @click="toggleSort('name')">Name</th> <th class="sortcolumn" :class="{sortactive: sort === 'name'}" @click="toggleSort('name')">Name</th>
<th class="sortcolumn modified right" :class="{sortactive: sort === 'modified'}" @click="toggleSort('modified')">Modified</th> <th class="sortcolumn modified right" :class="{sortactive: sort === 'modified'}" @click="toggleSort('modified')">Modified</th>
<th class="sortcolumn size right" :class="{sortactive: sort === 'size'}" @click="toggleSort('size')">Size</th> <th class="sortcolumn size right" :class="{sortactive: sort === 'size'}" @click="toggleSort('size')">Size</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="doc of sorted(documentStore.mainDocument as FolderDocument[])" :key="doc.key" :class="doc.type === 'folder' ? 'folder' : 'file'"> <tr v-for="doc of sorted(documentStore.mainDocument as FolderDocument[])" :key="doc.key" :class="doc.type === 'folder' ? 'folder' : 'file'">
<td class="selection"><input type="checkbox" v-model="doc.selected"></td> <td class="selection"><input type="checkbox" v-model="doc.selected"></td>
<td class="name"> <td class="name">
<template v-if="editing === doc"><FileRenameInput :doc="doc" :rename="rename" :exit="() => { editing = null}"/></template> <template v-if="editing === doc"><FileRenameInput :doc="doc" :rename="rename" :exit="() => { editing = null}"/></template>
<template v-else> <template v-else>
<a :href="url_for(doc)">{{doc.name}}</a> <a :href="url_for(doc)">{{doc.name}}</a>
<button @click="() => editing = doc">🖊</button> <button @click="() => editing = doc">🖊</button>
</template> </template>
</td> </td>
<td class="right">{{doc.modified}}</td> <td class="right">{{doc.modified}}</td>
<td class="right">{{doc.sizedisp}}</td> <td class="right">{{doc.sizedisp}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</main> </main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, h, computed, reactive, watchEffect } from 'vue' import { ref, computed } from 'vue'
import type { UnwrapRef } from 'vue' import { useDocumentStore } from '@/stores/documents'
import { useDocumentStore } from '@/stores/documents' import Router from '@/router/index';
import Router from '@/router/index'; import type { Document, FolderDocument } from '@/repositories/Document';
import { message } from 'ant-design-vue'; import FileCarousel from './FileCarousel.vue';
import type { Document, FolderDocument } from '@/repositories/Document'; import FileRenameInput from './FileRenameInput.vue'
import FileCarousel from './FileCarousel.vue';
import FileRenameInput from './FileRenameInput.vue'
import createWebSocket from '@/repositories/WS'; import createWebSocket from '@/repositories/WS';
const [messageApi, contextHolder] = message.useMessage(); const documentStore = useDocumentStore()
const linkBasePath = computed(()=>{
type Key = string | number; const path = Router.currentRoute.value.path
const documentStore = useDocumentStore() return path === '/' ? '' : path
const editableData: UnwrapRef<Record<string, Document>> = reactive({}); })
const state = reactive<{ const filesBasePath = computed(() => `/files${linkBasePath.value}`)
selectedRowKeys: Key[]; const url_for = (doc: FolderDocument) => (
}>({ doc.type === "folder" ?
selectedRowKeys: [], `#${linkBasePath.value}/${doc.name}` :
}); `${filesBasePath.value}/${doc.name}`
)
const linkBasePath = computed(()=>{ // File rename
const path = Router.currentRoute.value.path const editing = ref<FolderDocument | null>(null)
return path === '/' ? '' : path const rename = (doc: FolderDocument, newName: string) => {
}) const oldName = doc.name
const filesBasePath = computed(() => `/files${linkBasePath.value}`) const control = createWebSocket("/api/control", (ev: MessageEvent) => {
const url_for = (doc: FolderDocument) => ( const msg = JSON.parse(ev.data)
doc.type === "folder" ? if ("error" in msg) {
`#${linkBasePath.value}/${doc.name}` : console.error("Rename failed", msg.error.message, msg.error)
`${filesBasePath.value}/${doc.name}` doc.name = oldName
) } else {
// File rename console.log("Rename succeeded", msg)
const editing = ref<FolderDocument | null>(null)
const rename = (doc: FolderDocument, newName: string) => {
const oldName = doc.name
const control = createWebSocket("/api/control", (ev: MessageEvent) => {
const msg = JSON.parse(ev.data)
if ("error" in msg) {
console.error("Rename failed", msg.error.message, msg.error)
doc.name = oldName
} else {
console.log("Rename succeeded", msg)
}
})
control.onopen = () => {
control.send(JSON.stringify({
"op": "rename",
"path": `${linkBasePath.value}/${oldName}`,
"to": newName
}))
}
doc.name = newName // We should get an update from watch but this is quicker
}
// Column sort
const toggleSort = (name: string) => { sort.value = sort.value === name ? "" : name }
const sort = ref<string>("")
const sortCompare = {
"name": (a: Document, b: Document) => a.name.localeCompare(b.name),
"modified": (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
"size": (a: FolderDocument, b: FolderDocument) => b.size - a.size
}
const sorted = (documents: FolderDocument[]) => {
const cmp = sortCompare[sort.value as keyof typeof sortCompare]
const sorted = [...documents]
if (cmp) sorted.sort(cmp)
return sorted
}
const selectionIndeterminate = computed({
get: () => {
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.some((doc: Document) => doc.selected) && !allSelected.value
},
set: (value: boolean) => {}
})
const allSelected = computed({
get: () => {
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.every((doc: Document) => doc.selected)
},
set: (value: boolean) => {
if (documentStore.mainDocument) {
documentStore.mainDocument.forEach((doc: Document) => doc.selected = value)
}
} }
}) })
</script> control.onopen = () => {
control.send(JSON.stringify({
"op": "rename",
"path": `${linkBasePath.value}/${oldName}`,
"to": newName
}))
}
doc.name = newName // We should get an update from watch but this is quicker
}
<style> // Column sort
table { const toggleSort = (name: string) => { sort.value = sort.value === name ? "" : name }
width: 100%; const sort = ref<string>("")
table-layout: fixed; const sortCompare = {
"name": (a: Document, b: Document) => a.name.localeCompare(b.name),
"modified": (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
"size": (a: FolderDocument, b: FolderDocument) => b.size - a.size
}
const sorted = (documents: FolderDocument[]) => {
const cmp = sortCompare[sort.value as keyof typeof sortCompare]
const sorted = [...documents]
if (cmp) sorted.sort(cmp)
return sorted
}
const selectionIndeterminate = computed({
get: () => {
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.some((doc: Document) => doc.selected) && !allSelected.value
},
set: (value: boolean) => {}
})
const allSelected = computed({
get: () => {
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.every((doc: Document) => doc.selected)
},
set: (value: boolean) => {
if (documentStore.mainDocument) {
documentStore.mainDocument.forEach((doc: Document) => doc.selected = value)
}
} }
table input[type=checkbox] { })
width: 1em; </script>
height: 1em;
} <style>
table .modified { width: 10em; } table {
table .size { width: 6em; } width: 100%;
table th, table td { table-layout: fixed;
padding: .5em; }
font-weight: normal; table input[type=checkbox] {
text-align: left; width: 1em;
white-space: nowrap; height: 1em;
overflow: hidden; }
text-overflow: ellipsis; table .modified { width: 10em; }
} table .size { width: 6em; }
.name { table th, table td {
white-space: nowrap; padding: .5em;
text-overflow: initial; font-weight: normal;
overflow: initial; text-align: left;
} white-space: nowrap;
.name button { overflow: hidden;
visibility: hidden; text-overflow: ellipsis;
padding-left: 1em; }
} .name {
.name:hover button { white-space: nowrap;
visibility: visible; text-overflow: initial;
} overflow: initial;
.name button { }
cursor: pointer; .name button {
border: 0; visibility: hidden;
background: transparent; padding-left: 1em;
} }
thead tr { .name:hover button {
border: 1px solid #ddd; visibility: visible;
background: #ddd; }
} .name button {
tbody tr { cursor: pointer;
background: #444; border: 0;
color: #ddd; background: transparent;
} }
tbody tr:hover { thead tr {
background: #00f8; border: 1px solid #ddd;
} background: #ddd;
.right { }
text-align: right; tbody tr {
} background: #444;
.selection { color: #ddd;
width: 2em; }
} tbody tr:hover {
.sortcolumn:hover { background: #00f8;
cursor: pointer; }
} .right {
.sortcolumn:hover::after { text-align: right;
color: #f80; }
} .selection {
.sortcolumn { width: 2em;
padding-right: 1.7em; }
} .sortcolumn:hover {
.sortcolumn::after { cursor: pointer;
content: "▸"; }
color: #888; .sortcolumn:hover::after {
margin: 0 1em 0 .5em; color: #f80;
position: absolute; }
transition: transform 0.2s linear; .sortcolumn {
} padding-right: 1.7em;
.sortactive::after { }
transform: rotate(90deg); .sortcolumn::after {
color: #000; content: "▸";
} color: #888;
main { margin: 0 1em 0 .5em;
padding: 5px; position: absolute;
height: 100%; transition: transform 0.2s linear;
} }
.more-action{ .sortactive::after {
display: flex; transform: rotate(90deg);
flex-direction: column; color: #000;
justify-content: start; }
} main {
.action-container{ padding: 5px;
display: flex; height: 100%;
align-items: center; }
} .more-action{
.edit-action{ display: flex;
min-width: 5%; flex-direction: column;
} justify-content: start;
.carousel-container{ }
height: inherit; .action-container{
} display: flex;
.name a { align-items: center;
text-decoration: none; }
} .edit-action{
.file .name::before { min-width: 5%;
content: '📄 '; }
font-size: 1.5em; .carousel-container{
} height: inherit;
.folder .name::before { }
content: '📁 '; .name a {
font-size: 1.5em; text-decoration: none;
} }
</style> .file .name::before {
content: '📄 ';
font-size: 1.5em;
}
.folder .name::before {
content: '📁 ';
font-size: 1.5em;
}
</style>