326 lines
7.8 KiB
Vue

<!--
Root component for the Todo app that is displayed if the user is logged in.
Part of the basebox sample Todo app.
https://basebox.tech
-->
<template>
<main>
<h1>
ToDo List
</h1>
<div id="filter-form" class="toolbar">
<label class="switch">
<input type="checkbox" v-model="showCompleted">
Show completed
</label>
<button type="button" @click="addTask()" class="ms-auto btn btn-primary btn-large">Add Item</button>
<div class="ms-auto">
<div class="d-flex">
<label class="col-form-label" for="list-selector">Select List: </label>
<div class="ms-2">
<select id="list-selector" class="form-select" v-model="currentList">
<option :value="0" selected>All</option>
<option v-for="item in gStore.lists" :key="item.id" :value="item.id">{{ item.title }}</option>
</select>
</div>
</div>
</div>
</div>
<div id="todo-container">
<transition-group name="list" tag="div">
<div :class="todoItemClass(task)" v-for="(task, i) in filteredItems" :id="'task-' + task.id" :key="task.id">
<div :class="todoCheckedClass(task)" @click="toggleCompleted(task)">
&nbsp;
</div>
<div class="item-v-container">
<input class="item-title" type="text" @blur="saveTask(task)" v-model="task.title">
<input class="todo-description" type="text" @blur="saveTask(task)" v-model="task.description">
</div>
<div class="item-meta">
<div class="dropdown">
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-trash"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="" @click="deleteTask(task)">Delete this item</a></li>
</ul>
</div>
</div>
</div>
<div class="alert alert-info" v-if="filteredItems.length === 0">
<p><i class="bi bi-info-circle"></i>
Sorry, there are no todo items{{ currentList !== 0 ? " in this list" : ""}}.
</p>
<p v-if="currentList !== 0">You can add items or select another list.</p>
<hr>
<button type="button" class="btn btn-primary btn-sm" @click="addTask()">Create Todo Item</button>
</div>
</transition-group>
</div>
</main>
</template>
<script>
import {gqlQuery} from "../util/net";
import {showError, store} from "../store";
import {showModal} from "../util/misc";
const NEW_TASK_ID = "new-task-id";
export default {
name: "Todo",
components: [],
/** Current state of the component */
data() {
return {
/** whether to show completed items */
showCompleted: true,
/* the id of the list currently being shown */
currentList: 0,
}
},
methods: {
/**
* Save a task to the database.
* @param task the task to save; this is an object as received from the broker.
*/
saveTask(task) {
console.info(`Saving task '${task.title}' with id ${task.id}`);
let request = "";
if (task.id !== NEW_TASK_ID) {
/* save changes to existing task */
request = `mutation {
updateTask(
id: "${task.id}",
title: "${task.title}",
description: "${task.description}",
completed: ${task.completed ? "true" : "false"},
list: {
id: "${task.list.id}"
},
) {
id
}
}`;
} else {
/* create new task */
request = `mutation {
createTask(
title: "${task.title}",
description: "${task.description}",
completed: ${task.completed ? "true" : "false"},
list: {
id: "${task.list.id}"
},
user: {
username: "${store.session.username}"
}
) {
id
}
}`;
}
/* send query */
gqlQuery(
request
).then(data => {
/* Save the task's id in case it was just created */
task.id = data.Task.id;
}).catch(e => {
const errMsg = `Failed to save task: ${e}`;
console.error(errMsg);
showError(errMsg);
});
},
/**
* Delete a task.
* @param task - task to delete as an object from the store.
*/
deleteTask(task) {
if (task.id !== NEW_TASK_ID) {
/* Task must be also deleted from the server */
gqlQuery(`mutation {
deleteTask(id: "${task.id}") {
id
}
}`);
}
/* delete task from the store. */
const idx = store.tasks.findIndex(item => item.id === task.id);
if (idx !== -1) {
store.tasks.splice(idx, 1);
}
},
/**
* Add a task to the current list.
*/
addTask() {
if (!this.currentList) {
showModal("No list selected",
'Please select a specific list (not "All") before adding an item.');
return;
}
store.tasks.push({
id: NEW_TASK_ID,
completed: false,
title: "Enter task here",
description: "",
list: {
id: this.currentList,
}
});
/* wait a moment, then scroll list to the bottom */
setTimeout(function() {
document.getElementById("todo-container").scrollTo({
top: 100000,
behavior: "smooth"
});
/* select text in new item's title field */
const titleInput = document.querySelector(`#task-${NEW_TASK_ID} .item-title`);
titleInput.setSelectionRange(0, 1000);
titleInput.focus();
}, 100);
},
/**
* Toggle completed state of a todo item.
* @param task the todo item to toggle.
*/
toggleCompleted(task) {
task.completed = !task.completed;
},
/**
* Return class object for a todo checked symbol
*
* @param task the item
*/
todoCheckedClass(task) {
return {
"todo-checked": true,
"completed": task.completed,
"uncompleted": !task.completed,
}
},
/**
* Return class object for a todo item
*
* @param task the item
*/
todoItemClass(task) {
return {
"list-item": true,
"completed": task.completed,
}
},
},
computed: {
/**
* Add global store to the context, so we can refer to it in the template.
*/
gStore() {
return store;
},
/**
* Return array of tasks to show.
*/
filteredItems() {
return store.tasks.filter((item) => {
/* hide completed items if the option to show them is not set */
if (!this.showCompleted && item.completed) {
return false;
}
/* only return items of the current list */
if (this.currentList && item.list.id !== this.currentList) {
return false;
}
return true;
})
}
},
}
</script>
<style lang="scss" scoped>
#todo-container {
margin-top: 20px;
}
h1 {
margin-bottom: 25px;
}
.btn-icon {
width: 16px;
height: 16px;
cursor: pointer;
}
.todo-checked {
width: 80px;
height: 80px;
cursor: pointer;
margin-right: 20px;
min-height: 80px;
border-radius: 50%;
border: 4px solid var(--bs-border-color);
background-size: 80%;
background-position: center center;
background-repeat: no-repeat;
&.uncompleted {
&:hover {
background-image: url(../assets/img/check-gray.svg);
}
}
&.completed {
border-color: #41ab57;
background-image: url(../assets/img/check-green.svg);
}
}
.header-buttons {
float:right;
font-size: 1rem;
}
.switch {
padding-left: .5em;
input {
transform: scale(150%);
margin-right: 6px;
}
}
</style>