feat: finish collection migration
This commit is contained in:
@@ -11,7 +11,7 @@ const token = useTokenStore();
|
|||||||
|
|
||||||
const isBurgerActive = ref<boolean>(false);
|
const isBurgerActive = ref<boolean>(false);
|
||||||
|
|
||||||
const messagebox = ref<InstanceType<typeof MessageBox> | null>(null);
|
const messagebox = ref<InstanceType<typeof MessageBox>>();
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
const tokenStore = useTokenStore();
|
const tokenStore = useTokenStore();
|
||||||
@@ -42,7 +42,7 @@ const toggleBurger = () => {
|
|||||||
<nav class="navbar has-shadow is-spaced bd-navbar" role="navigation" aria-label="main navigation">
|
<nav class="navbar has-shadow is-spaced bd-navbar" role="navigation" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<router-link class="navbar-item" to="/">
|
<router-link class="navbar-item" to="/">
|
||||||
<img src="/public/favicon.ico"><b style="margin:0 0 0 14px;">coconut-leaf</b>
|
<img src="/favicon.ico"><b style="margin:0 0 0 14px;">coconut-leaf</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<a role="button" class="navbar-burger burger" :class="{ 'is-active': isBurgerActive }" @click="toggleBurger"
|
<a role="button" class="navbar-burger burger" :class="{ 'is-active': isBurgerActive }" @click="toggleBurger"
|
||||||
|
|||||||
103
frontend/src/components/collection/OwnedItem.vue
Normal file
103
frontend/src/components/collection/OwnedItem.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
uuid: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'delete', uuid: string): void
|
||||||
|
(e: 'share', uuid: string): void
|
||||||
|
(e: 'update', uuid: string, name: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isEditing = ref<boolean>(false);
|
||||||
|
const editingName = ref<string>("");
|
||||||
|
|
||||||
|
const editItem = () => {
|
||||||
|
isEditing.value = true;
|
||||||
|
editingName.value = props.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareItem = () => {
|
||||||
|
emit('share', props.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteItem = () => {
|
||||||
|
emit('delete', props.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateItem = () => {
|
||||||
|
let new_name = editingName.value;
|
||||||
|
editingName.value = "";
|
||||||
|
isEditing.value = false;
|
||||||
|
emit('update', props.uuid, new_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelUpdateItem = () => {
|
||||||
|
editingName.value = "";
|
||||||
|
isEditing.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="item card">
|
||||||
|
<div class="item-words">
|
||||||
|
<p v-show="!isEditing">{{ name }}</p>
|
||||||
|
<div v-show="isEditing" class="control">
|
||||||
|
<input v-model="editingName" class="input" type="text"></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-icon control" v-show="!isEditing" @click="editItem">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon icon="fas fa-pen"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
<div class="item-icon control" v-show="!isEditing" @click="shareItem">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-share"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
<div class="item-icon control" v-show="!isEditing" @click="deleteItem">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-trash"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-icon control" v-show="isEditing" @click="updateItem">
|
||||||
|
<button class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-check"></font-awesome-icon></span></button>
|
||||||
|
</div>
|
||||||
|
<div class="item-icon control" v-show="isEditing" @click="cancelUpdateItem">
|
||||||
|
<button class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-times"></font-awesome-icon></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div.item {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-words {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-icon {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
52
frontend/src/components/collection/SharingItem.vue
Normal file
52
frontend/src/components/collection/SharingItem.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'delete', username: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const deleteItem = () => {
|
||||||
|
emit('delete', props.username);
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="item card">
|
||||||
|
<div class="item-words">
|
||||||
|
<p>{{ username }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-icon control" @click="deleteItem">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon icon="fas fa-trash"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div.item {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-words {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-icon {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,7 +3,7 @@ import { createPinia } from 'pinia'
|
|||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { faUser, faLock } from '@fortawesome/free-solid-svg-icons'
|
import { faUser, faLock, faPen, faShare, faTrash, faCheck, faTimes, faPlus, faSync } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ const app = createApp(App);
|
|||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
library.add(faUser, faLock);
|
library.add(faUser, faLock, faPen, faShare, faTrash, faCheck, faTimes, faPlus, faSync);
|
||||||
app.component('font-awesome-icon', FontAwesomeIcon);
|
app.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const routes = [
|
|||||||
{ path: '/admin', name: "Admin", meta: { requireLoggedInCheck: true }, component: Admin },
|
{ path: '/admin', name: "Admin", meta: { requireLoggedInCheck: true }, component: Admin },
|
||||||
|
|
||||||
{ path: '/calendar/event', name: "CalendarEvent", meta: { requireLoggedInCheck: true }, component: CalendarEvent },
|
{ path: '/calendar/event', name: "CalendarEvent", meta: { requireLoggedInCheck: true }, component: CalendarEvent },
|
||||||
{ path: '/login', name: "Collection", meta: { requireLoggedOutCheck: true }, component: Login },
|
{ path: '/login', name: "Login", meta: { requireLoggedOutCheck: true }, component: Login },
|
||||||
|
|
||||||
{ path: '/404', name: "NotFound", component: NotFound },
|
{ path: '/404', name: "NotFound", component: NotFound },
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,254 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useTokenStore } from '@/stores/token';
|
||||||
|
import MessageBox from '@/components/MessageBox.vue';
|
||||||
|
import {
|
||||||
|
addOwn as apiCollectionAddOwn,
|
||||||
|
getFullOwn as apiCollectionGetFullOwn,
|
||||||
|
getSharing as apiCollectionGetSharing,
|
||||||
|
getDetailOwn as apiCollectionGetDetailOwn,
|
||||||
|
type CollectionRow,
|
||||||
|
deleteOwn as apiCollectionDeleteOwn,
|
||||||
|
updateOwn as apiCollectionUpdateOwn,
|
||||||
|
addSharing as apiCollectionAddSharing,
|
||||||
|
deleteSharing as apiCollectionDeleteSharing
|
||||||
|
} from '@/api/collection';
|
||||||
|
import OwnedItem from '@/components/collection/OwnedItem.vue';
|
||||||
|
import SharingItem from '@/components/collection/SharingItem.vue';
|
||||||
|
|
||||||
|
const ownedItems = ref<Map<string, CollectionRow>>(new Map());
|
||||||
|
const sharingItems = ref<string[]>([]);
|
||||||
|
const sharingItemUuid = ref<string | null>(null);
|
||||||
|
const isSharingItem = computed(() => {
|
||||||
|
return typeof sharingItemUuid.value === 'string';
|
||||||
|
})
|
||||||
|
const pendingOwnedItemName = ref<string>("");
|
||||||
|
const pendingSharingItemUsername = ref<string>("");
|
||||||
|
|
||||||
|
const token = useTokenStore();
|
||||||
|
const messagebox = ref<InstanceType<typeof MessageBox>>();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshOwnedItem();
|
||||||
|
})
|
||||||
|
|
||||||
|
const refreshOwnedItem = async () => {
|
||||||
|
// clean owned items
|
||||||
|
ownedItems.value.clear();
|
||||||
|
|
||||||
|
// Fetch data
|
||||||
|
let rv = await apiCollectionGetFullOwn(token.currentToken);
|
||||||
|
if (typeof rv === 'undefined') {
|
||||||
|
console.error("fail to fetch owned collection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Add into dict for rendering
|
||||||
|
for (let item of rv) {
|
||||||
|
ownedItems.value.set(item[0], item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean sharing status
|
||||||
|
sharingItemUuid.value = null;
|
||||||
|
await refreshSharingItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
const addOwnedItem = async () => {
|
||||||
|
let new_name = pendingOwnedItemName.value;
|
||||||
|
if (new_name === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// first add it
|
||||||
|
let rv = await apiCollectionAddOwn(token.currentToken, new_name);
|
||||||
|
if (typeof rv === 'undefined') {
|
||||||
|
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// second get its detail
|
||||||
|
let rv2 = await apiCollectionGetDetailOwn(token.currentToken, rv);
|
||||||
|
if (typeof rv2 === 'undefined') {
|
||||||
|
messagebox.value?.show("A get operation failed. It may caused by server internal error or your limited permission. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// render to list
|
||||||
|
ownedItems.value.set(rv2[0], rv2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOwnedItem = async (uuid: string) => {
|
||||||
|
let rv = await apiCollectionDeleteOwn(token.currentToken, uuid, ownedItems.value.get(uuid)![2]);
|
||||||
|
if (rv) {
|
||||||
|
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove from rendered list
|
||||||
|
ownedItems.value.delete(uuid);
|
||||||
|
|
||||||
|
// if we are editing sharing target of this item,
|
||||||
|
// we should escape this status
|
||||||
|
if (sharingItemUuid.value === uuid) {
|
||||||
|
sharingItemUuid.value = null;
|
||||||
|
await refreshSharingItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareOwnedItem = async (uuid: string) => {
|
||||||
|
// if there is no editing sharing target, set it.
|
||||||
|
// otherwise override it or toggle it according to its old value.
|
||||||
|
let old_value = sharingItemUuid.value;
|
||||||
|
if (typeof old_value === 'string') {
|
||||||
|
if (old_value === uuid) {
|
||||||
|
sharingItemUuid.value = null;
|
||||||
|
} else {
|
||||||
|
sharingItemUuid.value = uuid;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sharingItemUuid.value = uuid;
|
||||||
|
}
|
||||||
|
// refresh sharing list
|
||||||
|
await refreshSharingItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOwnedItem = async (uuid: string, name: string) => {
|
||||||
|
let lastChange = ownedItems.value.get(uuid)![2];
|
||||||
|
let rv = await apiCollectionUpdateOwn(token.currentToken, uuid, name, lastChange);
|
||||||
|
if (typeof rv === 'undefined') {
|
||||||
|
messagebox.value?.show("An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return value is new lastChange, so we update it.
|
||||||
|
let row = ownedItems.value.get(uuid)!;
|
||||||
|
row[1] = name;
|
||||||
|
row[2] = rv;
|
||||||
|
|
||||||
|
// if current item is sharing, update it at the same time
|
||||||
|
if (sharingItemUuid.value === uuid) {
|
||||||
|
await refreshSharingItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshSharingItem = async () => {
|
||||||
|
// clear sharing item
|
||||||
|
sharingItems.value = [];
|
||||||
|
|
||||||
|
// if we are not in sharing mode, return directly
|
||||||
|
if (!isSharingItem.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch data
|
||||||
|
let uuid = sharingItemUuid.value!;
|
||||||
|
let rv = await apiCollectionGetSharing(token.currentToken, uuid);
|
||||||
|
if (typeof rv === 'undefined') {
|
||||||
|
console.error(`fail to fetch sharing target for collection ${uuid}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// render then
|
||||||
|
sharingItems.value = rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSharingItem = async () => {
|
||||||
|
let uuid = sharingItemUuid.value!;
|
||||||
|
let lastChange = ownedItems.value.get(uuid)![2];
|
||||||
|
let new_username = pendingSharingItemUsername.value;
|
||||||
|
if (new_username === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// order adding
|
||||||
|
let rv = await apiCollectionAddSharing(token.currentToken, uuid, new_username, lastChange);
|
||||||
|
if (typeof rv === 'undefined') {
|
||||||
|
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push new item in list
|
||||||
|
sharingItems.value.push(new_username);
|
||||||
|
// the return value is the new last change
|
||||||
|
ownedItems.value.get(uuid)![2] = rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSharingItem = async (username: string) => {
|
||||||
|
let uuid = sharingItemUuid.value!;
|
||||||
|
let lastChange = ownedItems.value.get(uuid)![2];
|
||||||
|
|
||||||
|
// order deleting
|
||||||
|
let rv = await apiCollectionDeleteSharing(token.currentToken, uuid, username, lastChange);
|
||||||
|
if (typeof rv === 'undefined') {
|
||||||
|
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove this item
|
||||||
|
sharingItems.value.splice(sharingItems.value.indexOf(username), 1);
|
||||||
|
// the return value is the new last change
|
||||||
|
ownedItems.value.get(uuid)![2] = rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Congratulations</h1>
|
<div class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
||||||
<p>This is collection.</p>
|
<h1 class="title">Owned</h1>
|
||||||
|
<div class="control-list">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control">
|
||||||
|
<input v-model="pendingOwnedItemName" class="input" type="text">
|
||||||
|
</div>
|
||||||
|
<div class="control" @click="addOwnedItem">
|
||||||
|
<a class="button is-primary">
|
||||||
|
<span class="icon is-small"><font-awesome-icon icon="fas fa-plus"></font-awesome-icon></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control" @click="refreshOwnedItem">
|
||||||
|
<a class="button is-primary">
|
||||||
|
<span class="icon is-small"><font-awesome-icon icon="fas fa-sync"></font-awesome-icon></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="item in ownedItems.values()" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
||||||
|
<OwnedItem :uuid="item[0]" :name="item[1]" @delete="deleteOwnedItem" @share="shareOwnedItem"
|
||||||
|
@update="updateOwnedItem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="isSharingItem" class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
||||||
|
<h1 class="title">Sharing target</h1>
|
||||||
|
<label class="label">
|
||||||
|
<span>Editing: </span>
|
||||||
|
<span>{{ isSharingItem ? ownedItems.get(sharingItemUuid!)![1] : "" }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="control-list">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control">
|
||||||
|
<input v-model="pendingSharingItemUsername" class="input" type="text">
|
||||||
|
</div>
|
||||||
|
<div class="control" @click="addSharingItem">
|
||||||
|
<a class="button is-primary">
|
||||||
|
<span class="icon is-small"><font-awesome-icon icon="fas fa-plus"></font-awesome-icon></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control" @click="refreshSharingItem">
|
||||||
|
<a class="button is-primary">
|
||||||
|
<span class="icon is-small"><font-awesome-icon icon="fas fa-sync"></font-awesome-icon></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="item in sharingItems" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
||||||
|
<SharingItem :username="item" @delete="deleteSharingItem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MessageBox ref="messagebox" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const isLoggingIn = ref<boolean>(false);
|
|||||||
const username = ref<string>("");
|
const username = ref<string>("");
|
||||||
const password = ref<string>("");
|
const password = ref<string>("");
|
||||||
|
|
||||||
const messagebox = ref<InstanceType<typeof MessageBox> | null>(null);
|
const messagebox = ref<InstanceType<typeof MessageBox>>();
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
// disable UI first
|
// disable UI first
|
||||||
|
|||||||
Reference in New Issue
Block a user