This commit is contained in:
Markus Thielen 2023-03-17 10:51:05 +01:00
parent c7dd690ced
commit 44b8dc0c2f
Signed by: markus
GPG Key ID: 3D4980D3EC9C8E26
4 changed files with 226 additions and 144 deletions

View File

@ -13,55 +13,58 @@ import CommunityIcon from './icons/IconCommunity.vue'
</script> </script>
<template> <template>
<AboutItem> <div class="row">
<template #icon> <div class="col-12 col-md-8 offset-md-2 col-lg-6 offset-lg-3">
<DocumentationIcon /> <AboutItem>
</template> <template #icon>
<template #heading>About</template> <DocumentationIcon />
</template>
<template #heading>About</template>
<p>This is a very simple demo app for <a href="https://basebox.tech">basebox</a>, <p>This is a very simple demo app for <a href="https://basebox.tech">basebox</a>,
the medical grade backend that helps you get your project done with ease.</p> the medical grade backend that helps you get your project done with ease.</p>
</AboutItem> </AboutItem>
<AboutItem> <AboutItem>
<template #icon> <template #icon>
<ToolingIcon /> <ToolingIcon />
</template> </template>
<template #heading>Tools and Languages</template> <template #heading>Tools and Languages</template>
<p>We built basebox in 100% <a href="https://rust-lang.org">Rust</a>, but you can use about <p>We built basebox in 100% <a href="https://rust-lang.org">Rust</a>, but you can use about
any tool, language or framework to use basebox as your backend. As long as it can send any tool, language or framework to use basebox as your backend. As long as it can send
<a href="https://graphql.org">GraphQL</a> requests and understands JSON, it can build on basebox!</p> <a href="https://graphql.org">GraphQL</a> requests and understands JSON, it can build on basebox!</p>
</AboutItem> </AboutItem>
<AboutItem> <AboutItem>
<template #icon> <template #icon>
<CommunityIcon /> <CommunityIcon />
</template> </template>
<template #heading>Feedback</template> <template #heading>Feedback</template>
<p><b>We are so eager to hear from you!</b></p> <p><b>We are so eager to hear from you!</b></p>
<ul> <ul>
<li>Are you missing something?</li> <li>Are you missing something?</li>
<li>Any suggestion?</li> <li>Any suggestion?</li>
<li>Any question?</li> <li>Any question?</li>
</ul> </ul>
<p>We'd be REALLY happy to <a href="https://basebox.tech/open-a-aticket">hear from you</a>!</p> <p>We'd be REALLY happy to <a href="https://basebox.tech/open-a-aticket">hear from you</a>!</p>
</AboutItem> </AboutItem>
<AboutItem> <AboutItem>
<template #icon> <template #icon>
<QuestionIcon /> <QuestionIcon />
</template> </template>
<template #heading>Support</template> <template #heading>Support</template>
If you need help, check out our <a href="https://basebox.tech">website</a>.<br> If you need help, check out our <a href="https://basebox.tech">website</a>.<br>
Some support options: Some support options:
<ul> <ul>
<li>See our <a href="https://basebox.tech/faq">FAQs</a></li> <li>See our <a href="https://basebox.tech/faq">FAQs</a></li>
<li>Read the <a href="https://docs.basebox.tech">documentation</a></li> <li>Read the <a href="https://docs.basebox.tech">documentation</a></li>
<li>Open a <a href="https://basebox.tech/open-a-ticket">ticket</a></li> <li>Open a <a href="https://basebox.tech/open-a-ticket">ticket</a></li>
</ul> </ul>
</AboutItem>
</AboutItem>
</div>
</div>
</template> </template>

View File

@ -12,6 +12,24 @@
ToDo List ToDo List
</h1> </h1>
<!-- message modal -->
<div class="modal" tabindex="-1" id="msgbox">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="filter-form" class="toolbar"> <div id="filter-form" class="toolbar">
<label class="switch"> <label class="switch">
@ -26,7 +44,7 @@
<label class="col-form-label" for="list-selector">Select List: </label> <label class="col-form-label" for="list-selector">Select List: </label>
<div class="ms-2"> <div class="ms-2">
<select id="list-selector" class="form-select" v-model="currentList"> <select id="list-selector" class="form-select" v-model="currentList">
<option :value="0">All</option> <option :value="0" selected>All</option>
<option v-for="item in gStore.lists" :key="item.id" :value="item.id">{{ item.title }}</option> <option v-for="item in gStore.lists" :key="item.id" :value="item.id">{{ item.title }}</option>
</select> </select>
</div> </div>
@ -58,7 +76,12 @@
</div> </div>
<div class="alert alert-info" v-if="filteredItems.length === 0"> <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="addItem()">Create Todo Item</button>
</div> </div>
</transition-group> </transition-group>
@ -69,7 +92,10 @@
<script> <script>
import {gqlQuery} from "../util/net"; import {gqlQuery} from "../util/net";
import {store} from "../store"; import {showError, store} from "../store";
import { Modal } from 'bootstrap';
const NEW_TASK_ID = "new-task-id";
export default { export default {
name: "Todo", name: "Todo",
@ -81,7 +107,7 @@ export default {
/** whether to show completed items */ /** whether to show completed items */
showCompleted: true, showCompleted: true,
/* the id of the list currently being shown */ /* the id of the list currently being shown */
currentList: 1, currentList: 0,
} }
}, },
@ -92,23 +118,77 @@ export default {
* @param task the task to save; this is an object as received from the broker. * @param task the task to save; this is an object as received from the broker.
*/ */
saveTask(task) { 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 => {
task.id = data.id;
}).catch(e => {
const errMsg = `Failed to save task: ${e}`;
console.error(errMsg);
showError(errMsg);
});
},
/**
* Show a message nox (Bootstrap modal)
*/
showModal(title, text) {
const el = document.getElementById('msgbox');
el.querySelector(".modal-title").innerHTML = title;
el.querySelector(".modal-body").innerHTML = text;
const modal = new Modal(el);
modal.show();
}, },
/** /**
* Add an item to the current list. * Add an item to the current list.
*/ */
addItem() { addItem() {
if (this.currentList === 0) { if (!this.currentList) {
window.alert("Please select a list at the top right before adding an item."); this.showModal("No list selected",
'Please select a specific list (not "All") before adding an item.');
return; return;
} }
/* fake ID of new item */
const newItemId = store.tasks.length + 1;
store.tasks.push({ store.tasks.push({
id: newItemId, id: NEW_TASK_ID,
completed: false, completed: false,
title: "Enter task here", title: "Enter task here",
description: "", description: "",
@ -194,41 +274,6 @@ export default {
}, },
/**
* mounted lifecycle hook.
*/
mounted() {
if (!store.session) {
console.error("TodoRoot component must not be loaded if user is not logged in.");
return;
}
/* Load user info and his/her lists and todos */
gqlQuery(`query {
getUser(username: "${store.session.userName}") {
name
lists {
id
title
}
tasks {
id
title
description
completed
list {
id
}
}
}
}`).then(rspJson => {
}).catch(err => {
});
},
} }
</script> </script>

View File

@ -5,7 +5,7 @@
* https://basebox.tech * https://basebox.tech
*/ */
import {showError, store} from "../store"; import {loggedIn, showError, store} from "../store";
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import { objectToQueryString } from "../util/net"; import { objectToQueryString } from "../util/net";
@ -41,7 +41,7 @@ router.beforeEach(async (to, from) => {
// Handle OAuth callback request. // Handle OAuth callback request.
// This request is coming in after the user entered her/his credentials at the IdP // This request is coming in after the user entered her/his credentials at the IdP
// login form; the IdP (e.g. Keycloak) redirects the browser to the URL of this route. // login form; the IdP (e.g. Keycloak) redirects the browser to the URL of this route.
if (to.path === "/oauth-callback") { if (to.path.startsWith("/oauth-callback")) {
let queryString = objectToQueryString(to.query); let queryString = objectToQueryString(to.query);
console.info(`Got /oauth-callback with query string '${queryString}`); console.info(`Got /oauth-callback with query string '${queryString}`);
@ -58,6 +58,11 @@ router.beforeEach(async (to, from) => {
return {name: 'home'}; return {name: 'home'};
} }
/* if no user is logged in, we redirect to the home page */
if (!loggedIn() && to.path.length > 1 && to.path !== "/about") {
return {name: 'home'};
}
}); });
export default router export default router

View File

@ -22,32 +22,9 @@ export const store = reactive({
fatalError: "", fatalError: "",
/* array of todo lists currently known */ /* array of todo lists currently known */
lists: [ lists: [],
{ id: 1, title: "Default"},
{ id: 2, title: "House" },
{ id: 3, title: "Car" },
{ id: 4, title: "Work" },
],
/* known tasks */ /* known tasks */
tasks: [ tasks: [],
{ id: 1, completed: false, title: "Go to dentist", description: "Last time is way too long ago...", list: { id: 1 } },
{ id: 2, completed: true, title: "Change engine oil", description: "Use the good one", list: { id: 3 } },
{ id: 3, completed: false, title: "Clean windows", description: "Regualar stuff", list: { id: 1 } },
{ id: 4, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 5, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 6, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 7, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 8, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 9, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 10, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 11, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 12, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 13, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 14, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 15, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 16, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 17, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
],
}); });
@ -101,43 +78,95 @@ export async function storeInit(session) {
* We cannot run these requests in parallel, since the user record must exist in the database * We cannot run these requests in parallel, since the user record must exist in the database
* before we can create the default list. * before we can create the default list.
*/ */
const tasks = []; try {
tasks.push( console.info("NOTE: If next request fails, it is probably because the user already exists. In this case, the error is ignored.");
gqlQuery(`mutation { await gqlQuery(`mutation {
createUser( createUser(
username: "${session.username}", username: "${session.username}",
name: "${session.first_name} ${session.last_name}" name: "${session.first_name} ${session.last_name}"
) { ) {
username username
} }
}`)); }`);
tasks.push(gqlQuery(`mutation { } catch(e) {
createList( const errStr = e.toString();
title: "Default", if (errStr.indexOf("already exists") === -1) {
user: { username: "${session.username}" } /* this is an error */
) { const e = `Failed to create user: ${errStr}`;
title console.error(e);
showError(e);
return;
} }
}`)); }
const allErrors = []; /* Load user info, lists and todos. */
gqlQuery(`query {
for (const task of tasks) { getUser(username: "${store.session.username}") {
try { name
await task; lists {
} catch (e) { id
const errStr = e.toString(); title
if (errStr.indexOf("already exists") === -1) { }
/* this is an error */ tasks {
const e = `Init task failed: ${errStr}`; id
console.error(e); title
allErrors.push(e); description
completed
list {
id
}
} }
} }
} }`).then(data => {
store.lists = data.User.lists ? data.User.lists : [];
store.tasks = data.User.tasks ? data.User.tasks : [];
/* create default list if necessary */
if (store.lists.length === 0) {
gqlQuery(`mutation {
createList(
title: "Default",
user: { username: "${store.session.username}" }
) {
title
}
}`);
}
if (allErrors.length) { /* fix for basebox bug: task.list is returned as an array of List objects, where we expect just a single object */
showError(allErrors.join("<br><br>")); for (const task of store.tasks) {
} if (Array.isArray(task.list)) {
task.list = task.list[0];
}
}
} }).catch(err => {
const e = `Failed to load user/data: ${err.toString()}`;
console.error(e);
showError(e);
});
/* Check if we already have a list named "Default". */
}
const testData = {
lists: [
{ id: 1, title: "Default"},
{ id: 2, title: "House" },
{ id: 3, title: "Car" },
{ id: 4, title: "Work" },
],
/* known tasks */
tasks: [
{ id: 1, completed: false, title: "Go to dentist", description: "Last time is way too long ago...", list: { id: 1 } },
{ id: 2, completed: true, title: "Change engine oil", description: "Use the good one", list: { id: 3 } },
{ id: 3, completed: false, title: "Clean windows", description: "Regualar stuff", list: { id: 1 } },
{ id: 4, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 5, completed: false, title: "Do more benchmarking", description: "Find out how fast this thing really is", list: { id: 4 } },
],
};