From 3e2ac648c2329a85b58d9bb88b65348321859dc4 Mon Sep 17 00:00:00 2001 From: Markus Thielen Date: Wed, 15 Mar 2023 17:57:12 +0100 Subject: [PATCH] WIP... --- bbconf/bb_todo_resolvers.toml | 173 +++++++++++------ index.html | 2 +- package-lock.json | 52 +++++ package.json | 3 + src/App.vue | 127 +++++------- src/assets/base.css | 89 --------- src/assets/img/trash-solid.png | Bin 2624 -> 0 bytes src/assets/main.scss | 151 ++++++--------- src/components/FatalError.vue | 57 ------ src/components/Lists.vue | 117 +++++++++++ src/components/Todo.vue | 286 +++++++++++++++++++++++++++ src/components/TodoRoot.vue | 342 --------------------------------- src/main.js | 4 +- src/router/index.js | 19 +- src/store.js | 92 +++++++++ src/util/net.js | 59 ++++-- src/util/oauth.js | 29 +-- src/views/AboutView.vue | 8 +- src/views/HomeView.vue | 15 +- 19 files changed, 828 insertions(+), 797 deletions(-) delete mode 100644 src/assets/base.css delete mode 100644 src/assets/img/trash-solid.png delete mode 100644 src/components/FatalError.vue create mode 100644 src/components/Lists.vue create mode 100644 src/components/Todo.vue delete mode 100644 src/components/TodoRoot.vue diff --git a/bbconf/bb_todo_resolvers.toml b/bbconf/bb_todo_resolvers.toml index 92a1574..6c3201b 100644 --- a/bbconf/bb_todo_resolvers.toml +++ b/bbconf/bb_todo_resolvers.toml @@ -1,77 +1,140 @@ -[resolvers.deleteTask] -operation_name = "deleteTask" +[resolvers.createTask] +operation_name = "createTask" -[resolvers.deleteTask.resolver] -command_type = "SQLDelete" +[resolvers.createTask.resolver] +command_type = "SQLInsert" + +[resolvers.createTask.resolver.command] +table = "Task" columns = [] -tables = [] -where_clauses = [["Task", "id", "= '$id'"]] -join_clauses = [] -modify_table = ["Task", ""] -modify_values = [] -aggregate_final_json_result = true +where_clauses = [] +aggregate_result = true + +[[resolvers.createTask.resolver.command.modify_values]] +column = "title" +value = "'$title'" + +[[resolvers.createTask.resolver.command.modify_values]] +column = "description" +value = "'$description'" + +[[resolvers.createTask.resolver.command.modify_values]] +column = "completed" +value = "$completed" + +[[resolvers.createTask.resolver.command.modify_values]] +column = "list_id" +value = "'$list.$id'" + +[[resolvers.createTask.resolver.command.modify_values]] +column = "user_username" +value = "'$user.$username'" [resolvers.updateTask] operation_name = "updateTask" [resolvers.updateTask.resolver] command_type = "SQLUpdate" + +[resolvers.updateTask.resolver.command] +table = "Task" columns = [] -tables = [] -where_clauses = [["Task", "id", "= '$id'"]] -join_clauses = [] -modify_table = ["Task", ""] -modify_values = [["title", "'$title'"], ["description", "'$description'"], ["completed", "$completed"], ["list_id", "'$list.$id'"]] -aggregate_final_json_result = true +aggregate_result = true -[resolvers.createUser] -operation_name = "createUser" +[[resolvers.updateTask.resolver.command.modify_values]] +column = "title" +value = "'$title'" -[resolvers.createUser.resolver] -command_type = "SQLInsert" -columns = [] -tables = [] -where_clauses = [] -join_clauses = [] -modify_table = ["User", ""] -modify_values = [["username", "'$username'"], ["name", "'$name'"]] -aggregate_final_json_result = true +[[resolvers.updateTask.resolver.command.modify_values]] +column = "description" +value = "'$description'" -[resolvers.createList] -operation_name = "createList" +[[resolvers.updateTask.resolver.command.modify_values]] +column = "completed" +value = "$completed" -[resolvers.createList.resolver] -command_type = "SQLInsert" -columns = [] -tables = [] -where_clauses = [] -join_clauses = [] -modify_table = ["List", ""] -modify_values = [["title", "'$title'"], ["user_username", "'$user.$username'"]] -aggregate_final_json_result = true +[[resolvers.updateTask.resolver.command.modify_values]] +column = "list_id" +value = "'$list.$id'" -[resolvers.createTask] -operation_name = "createTask" - -[resolvers.createTask.resolver] -command_type = "SQLInsert" -columns = [] -tables = [] -where_clauses = [] -join_clauses = [] -modify_table = ["Task", ""] -modify_values = [["title", "'$title'"], ["description", "'$description'"], ["completed", "$completed"], ["list_id", "'$list.$id'"], ["user_username", "'$user.$username'"]] -aggregate_final_json_result = true +[[resolvers.updateTask.resolver.command.where_clauses]] +table = "Task" +column = "id" +condition_str = "= '$id'" +index = "" [resolvers.getUser] operation_name = "getUser" [resolvers.getUser.resolver] command_type = "SQLSelect" + +[resolvers.getUser.resolver.command] +table = "User" columns = [] -tables = [["User", ""]] -where_clauses = [["User", "username", "= '$username'"]] -join_clauses = [] -modify_table = ["", ""] modify_values = [] -aggregate_final_json_result = true +aggregate_result = true + +[[resolvers.getUser.resolver.command.where_clauses]] +table = "User" +column = "username" +condition_str = "= '$username'" +index = "" + +[resolvers.deleteTask] +operation_name = "deleteTask" + +[resolvers.deleteTask.resolver] +command_type = "SQLDelete" + +[resolvers.deleteTask.resolver.command] +table = "Task" +columns = [] +modify_values = [] +aggregate_result = true + +[[resolvers.deleteTask.resolver.command.where_clauses]] +table = "Task" +column = "id" +condition_str = "= '$id'" +index = "" + +[resolvers.createUser] +operation_name = "createUser" + +[resolvers.createUser.resolver] +command_type = "SQLInsert" + +[resolvers.createUser.resolver.command] +table = "User" +columns = [] +where_clauses = [] +aggregate_result = true + +[[resolvers.createUser.resolver.command.modify_values]] +column = "username" +value = "'$username'" + +[[resolvers.createUser.resolver.command.modify_values]] +column = "name" +value = "'$name'" + +[resolvers.createList] +operation_name = "createList" + +[resolvers.createList.resolver] +command_type = "SQLInsert" + +[resolvers.createList.resolver.command] +table = "List" +columns = [] +where_clauses = [] +aggregate_result = true + +[[resolvers.createList.resolver.command.modify_values]] +column = "title" +value = "'$title'" + +[[resolvers.createList.resolver.command.modify_values]] +column = "user_username" +value = "'$user.$username'" diff --git a/index.html b/index.html index bad0bf1..58f192c 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/package-lock.json b/package-lock.json index e637168..5f57371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,8 +5,12 @@ "requires": true, "packages": { "": { + "name": "todo", "version": "0.0.0", "dependencies": { + "@popperjs/core": "^2.11.6", + "bootstrap": "^5.3.0-alpha1", + "bootstrap-icons": "^1.10.3", "vue": "^3.2.47", "vue-router": "^4.1.6" }, @@ -379,6 +383,15 @@ "node": ">=12" } }, + "node_modules/@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz", @@ -521,6 +534,29 @@ "node": ">=8" } }, + "node_modules/bootstrap": { + "version": "5.3.0-alpha1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.0-alpha1.tgz", + "integrity": "sha512-ABZpKK4ObS3kKlIqH+ZVDqoy5t/bhFG0oHTAzByUdon7YIom0lpCeTqRniDzJmbtcWkNe800VVPBiJgxSYTYew==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.6" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz", + "integrity": "sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw==" + }, "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -1141,6 +1177,11 @@ "dev": true, "optional": true }, + "@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + }, "@vitejs/plugin-vue": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz", @@ -1268,6 +1309,17 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bootstrap": { + "version": "5.3.0-alpha1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.0-alpha1.tgz", + "integrity": "sha512-ABZpKK4ObS3kKlIqH+ZVDqoy5t/bhFG0oHTAzByUdon7YIom0lpCeTqRniDzJmbtcWkNe800VVPBiJgxSYTYew==", + "requires": {} + }, + "bootstrap-icons": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz", + "integrity": "sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw==" + }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", diff --git a/package.json b/package.json index 9ed4346..6a77e89 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ "preview": "vite preview" }, "dependencies": { + "@popperjs/core": "^2.11.6", + "bootstrap": "^5.3.0-alpha1", + "bootstrap-icons": "^1.10.3", "vue": "^3.2.47", "vue-router": "^4.1.6" }, diff --git a/src/App.vue b/src/App.vue index cc9f782..4aa2291 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,7 +3,6 @@ import { RouterLink, RouterView } from 'vue-router' import HelloWorld from './components/Welcome.vue' import { store } from './store.js' import {clearSession} from "./store.js"; -import FatalError from "./components/FatalError.vue"; /** * Logout the user. @@ -13,98 +12,64 @@ function logOut() { location.href = "/"; } +/** + * Remove error alert from the screen. + */ +function dismissError() { + store.fatalError = ""; +} + diff --git a/src/assets/base.css b/src/assets/base.css deleted file mode 100644 index cbeb30a..0000000 --- a/src/assets/base.css +++ /dev/null @@ -1,89 +0,0 @@ -/* color palette from */ -:root { - --vt-c-white: #ffffff; - --vt-c-white-soft: #f8f8f8; - --vt-c-white-mute: #f2f2f2; - - --vt-c-black: #181818; - --vt-c-black-soft: #222222; - --vt-c-black-mute: #282828; - - --vt-c-indigo: #2c3e50; - - --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); - --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); - --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); - --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); - - --vt-c-text-light-1: var(--vt-c-indigo); - --vt-c-text-light-2: rgba(60, 60, 60, 0.66); - --vt-c-text-dark-1: var(--vt-c-white); - --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); -} - -/* semantic color variables for this project */ -:root { - --color-background: var(--vt-c-white); - --color-background-soft: var(--vt-c-white-soft); - --color-background-mute: var(--vt-c-white-mute); - - --color-border: var(--vt-c-divider-light-2); - --color-border-hover: var(--vt-c-divider-light-1); - - --color-heading: var(--vt-c-text-light-1); - --color-text: var(--vt-c-text-light-1); - - --section-gap: 160px; - - --color-input-background: #fff; - --color-input: rgba(0,0,0,.85); - --color-input-border-focus: rgba(0,0,0,.2); - --color-error-text: darkred; - --color-item-background: white; - --color-item-text: rgb(33, 53, 71); - --item-box-shadow: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07); -} - -@media (prefers-color-scheme: dark) { - :root { - --color-background: var(--vt-c-black); - --color-background-soft: var(--vt-c-black-soft); - --color-background-mute: var(--vt-c-black-mute); - - --color-border: var(--vt-c-divider-dark-2); - --color-border-hover: var(--vt-c-divider-dark-1); - - --color-heading: var(--vt-c-text-dark-1); - --color-text: var(--vt-c-text-dark-2); - --color-input-background: rgba(255,255,255,.05); - --color-input: rgba(255,255,255,.8); - --color-input-border-focus: rgba(0,0,0,.4); - --color-error-text: red; - --color-item-background: #242424; - --color-item-text: white; - --item-box-shadow: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07); - } -} - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - position: relative; - font-weight: normal; -} - -body { - min-height: 100vh; - color: var(--color-text); - background: var(--color-background); - transition: color 0.5s, background-color 0.5s; - line-height: 1.6; - font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, - Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; - font-size: 15px; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} diff --git a/src/assets/img/trash-solid.png b/src/assets/img/trash-solid.png deleted file mode 100644 index 167df76088c1cf2880c8eb8f5b15d244a1cb913d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2624 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 diff --git a/src/assets/main.scss b/src/assets/main.scss index 47d2a64..6c24e9f 100644 --- a/src/assets/main.scss +++ b/src/assets/main.scss @@ -1,113 +1,74 @@ -@import './base.css'; - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - - font-weight: normal; -} - -ul{ - padding-left: 18px; -} - -a, -.primary { - text-decoration: none; - color: #3AB2FF; - transition: 0.4s; -} - -@media (hover: hover) { - a:hover { - background-color: hsla(160, 100%, 37%, 0.2); - } -} - -@media (min-width: 1024px) { - body { - display: flex; - place-items: center; - } - - #app { - display: grid; - grid-template-columns: 1fr 1fr; - padding: 0 4rem 0 2rem; - } -} - - -.form-group { - margin: 25px 0; +main { + padding-top: 20px; label { - text-transform: uppercase; - font-size: .8rem; - font-weight: 700; - display: block; - margin-bottom: 5px; + white-space: nowrap; + } +} + +.toolbar { + display: flex; + background-color: var(--bs-tertiary-bg); + border: 1px solid var(--bs-border-color); + border-radius: .3em; + padding: 5px; + align-items: center; +} + +.list-item { + display: flex; + background-color: var(--bs-tertiary-bg); + box-shadow: var(--item-box-shadow); + margin-bottom: 10px; + padding: 15px; + border-radius: .3em; + border: 3px solid transparent; + &.completed { + border: 3px solid #41ab57; } input { - border: 1px solid var(--color-border); - padding: 6px 8px; - display: block; - border-radius: 2px; - width: 100%; - background-color: var(--color-input-background); - color: var(--color-input); - appearance: none; + border: none; + border-bottom: 1px solid var(--bs-border-color); + outline: none; + margin-bottom: 15px; + color: var(--color-item-text); + background: transparent; &:focus { - border-color: var(--color-input-border-focus); + outline: none; } } + .item-title { + font-size: 1.5rem; + } } -.form-buttons { - margin-top: 25px; - padding-top: 20px; - border-top: 1px solid var(--color-border); +.item-v-container { display: flex; - gap: 1rem; + flex-direction: column; + flex-grow: 1; + background-color: inherit; } -.btn { - padding: 5px 20px; - border: 1px solid var(--color-border); - border-radius: 2px; - cursor: pointer; - &.btn-primary { - background-color: #1890ff; - color: white; - border-color: #1890ff; +.item-meta { + padding: 5px 5px 5px 25px; + display: flex; + flex-direction: column; + align-items: end; +} + +@keyframes bounce-in { + 0% { + transform: scale(0); } - &.btn-large { - font-size: 1.25rem; - padding: 6px 25px; + 100% { + transform: scale(1); } } -.btn-container { - margin: 30px 0; +.list-enter-active { + animation: bounce-in 0.4s; +} +.list-leave-active { + animation: bounce-in 0.4s reverse; } -form .error { - color: var(--color-error-text); - margin: 25px 0 0 0; -} -select.form-control:not([multiple]) { - cursor: pointer; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAI9JREFUeNpiZACCyMjIBCDVD8SOy5cvv8BAAgDqNQBS+4G4EKh3ASPUsPlQ+Q+kGIpkmABUKJEJ6jIYAEnshyok1TAQ6AcZ6Ah1GdGG4jAM7DtGQgrQvU9ILSOxColVw0is7VA2QV8wkhA+DMQECSMJgc5AyDCcBhIwFG9aZSQxrRFM+IwkJGAGYnIRQIABACQuXCKovu2mAAAAAElFTkSuQmCC); - background-size: 1rem; - background-repeat: no-repeat; - background-position: calc(100% - 1rem) center; - padding: .5em 3em .5em 1em; - background-color: var(--color-background); - color: var(--color-text); - border: 1px solid var(--color-border); - border-radius: .4rem; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} \ No newline at end of file diff --git a/src/components/FatalError.vue b/src/components/FatalError.vue deleted file mode 100644 index eade193..0000000 --- a/src/components/FatalError.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/components/Lists.vue b/src/components/Lists.vue new file mode 100644 index 0000000..395d907 --- /dev/null +++ b/src/components/Lists.vue @@ -0,0 +1,117 @@ + + + + + + diff --git a/src/components/Todo.vue b/src/components/Todo.vue new file mode 100644 index 0000000..cbf4d74 --- /dev/null +++ b/src/components/Todo.vue @@ -0,0 +1,286 @@ + + + + + + \ No newline at end of file diff --git a/src/components/TodoRoot.vue b/src/components/TodoRoot.vue deleted file mode 100644 index 5e8d33e..0000000 --- a/src/components/TodoRoot.vue +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/main.js b/src/main.js index 93b5db6..a093a54 100644 --- a/src/main.js +++ b/src/main.js @@ -7,7 +7,9 @@ import { createApp } from 'vue' import App from './App.vue' import router from './router' - +import "bootstrap/dist/css/bootstrap.min.css" +import "bootstrap" +import 'bootstrap-icons/font/bootstrap-icons.css' import './assets/main.scss' const app = createApp(App) diff --git a/src/router/index.js b/src/router/index.js index 68b9954..b92431f 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -8,9 +8,9 @@ import {showError, store} from "../store"; import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' -import OAUthError from "../components/FatalError.vue"; import { objectToQueryString } from "../util/net"; -import {createUser, oauthCallbackHandler} from "../util/oauth"; +import {oauthCallbackHandler} from "../util/oauth"; +import {storeInit} from "../store"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -28,6 +28,11 @@ const router = createRouter({ // which is lazy-loaded when the route is visited. component: () => import('../views/AboutView.vue') }, + { + path: '/lists', + name: 'lists', + component: () => import('../components/Lists.vue') + }, ] }) @@ -43,7 +48,6 @@ router.beforeEach(async (to, from) => { /* Pass the query string to the handler function */ try { await oauthCallbackHandler(queryString); - /* Success; redirect to home */ } catch (err) { const errorMsg = `Failed to get session info from basebox/finish OpenID Connect login: ${err}`; console.error(errorMsg); @@ -51,15 +55,6 @@ router.beforeEach(async (to, from) => { showError(errorMsg); } - /* We have to create the logged-in user explicitly; see comment for `createUser()` */ - try { - await createUser(); - } catch (err) { - const errorMsg = `Failed to create user: ${err}`; - console.error(errorMsg); - showError(errorMsg); - } - return {name: 'home'}; } diff --git a/src/store.js b/src/store.js index 97d1199..dc7fa57 100644 --- a/src/store.js +++ b/src/store.js @@ -5,6 +5,7 @@ * https://basebox.tech */ import { reactive } from 'vue' +import {gqlQuery} from "./util/net"; export const store = reactive({ @@ -20,6 +21,34 @@ export const store = reactive({ /** Fatal error message, if any */ fatalError: "", + /* array of todo lists currently known */ + 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: "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 } }, + ], + }); /** @@ -49,3 +78,66 @@ export function showError(message) { export function clearError() { store.fatalError = ""; } + +/** + * Initialize data store and database. + * + * Must be called by the OAuth login completion handler as soon as session data is available. + * + * If this is the first run, we need to create the logged on user, since he/she is so far known + * only to the OpenID Connect server (Keycloak). + * + * Additionally, we also create the default list here and save everything in the store. + * + * @param session - the session info as returned by the OpenID Connect login. + */ +export async function storeInit(session) { + + /* save user session in the store */ + store.session = { ...session }; + store.userName = session.first_name ? session.first_name : session.username; + + /* Create user and default list. + * We cannot run these requests in parallel, since the user record must exist in the database + * before we can create the default list. + */ + const tasks = []; + tasks.push( + gqlQuery(`mutation { + createUser( + username: "${session.username}", + name: "${session.first_name} ${session.last_name}" + ) { + username + } + }`)); + tasks.push(gqlQuery(`mutation { + createList( + title: "Default", + user: "${session.username}" + ) { + title + } + }`)); + + const allErrors = []; + + for (const task of tasks) { + try { + await task; + } catch (e) { + const errStr = e.toString(); + if (errStr.indexOf("already exists") === -1) { + /* this is an error */ + const e = `Init task failed: ${errStr}`; + console.error(e); + allErrors.push(e); + } + } + } + + if (allErrors.length) { + showError(allErrors.join("

")); + } + +} \ No newline at end of file diff --git a/src/util/net.js b/src/util/net.js index 3a004f8..af735ef 100644 --- a/src/util/net.js +++ b/src/util/net.js @@ -40,8 +40,26 @@ class GqlError extends Error { } else if (error instanceof String) { errorMessages.push(error); - } else { - /* assume this is a GraphQL server response (JSON) */ + } else if ('errors' in error) { + /** assume this is a GraphQL server response (JSON) of the following form: + * { + * "errors": [ + * { + * "message": "Cannot query field \"__typenam\" on type \"Query\".", + * "locations": [ + * { + * "line": 1, + * "column": 2 + * } + * ], + * "extensions": { + * "code": "GRAPHQL_VALIDATION_FAILED", + * "stacktrace": [] + * } + * } + * ] + * } + */ try { for (const e of error.errors) { let s = e.message; @@ -57,9 +75,12 @@ class GqlError extends Error { errorMessages.push(s); } } catch(e) { - errorMessages.push("Failed to interpret error: " + e.toString()); + errorMessages.push("GraphQL error response of unknown format " + e.toString()); errorMessages.push(error.toString()); } + } else { + /* ubnknown error format */ + errorMessages.push(error.toString()); } /* concat errors into a single message for the parent classes */ @@ -71,20 +92,6 @@ class GqlError extends Error { this.name = "GqlError"; } - /** - * Check if this error is an authentication error (401). - */ - is401() { - /* check if one of the server errors has a 401 extension */ - for (const error of this.errors) { - if (error.extensions && error.extensions.code.toString() === "401") { - return true; - } - } - - return false; - } - } /** @@ -111,7 +118,7 @@ export function gqlQuery(query) fetchOpt.headers["Authorization"] = `Bearer ${store.session.token}`; } - console.info(fetchOpt); + console.info(`Sending request:\n${query}`); return fetch(`${store.baseboxHost}/graphql`, fetchOpt).then( /* fetch success */ @@ -140,9 +147,19 @@ export function gqlQuery(query) return new Promise((resolve, reject) => reject("Unauthorized")); } - return new Promise((resolve, reject) => { - reject(new GqlError(response.statusText)); - }); + /* try to get JSON error object */ + try { + const json = await response.json(); + return new Promise((resolve, reject) => { + reject(new GqlError(json)); + }); + } catch (e) { + /* no JSON in response, fall through */ + return new Promise((resolve, reject) => { + reject(new GqlError(response.statusText)); + }); + } + } }, diff --git a/src/util/oauth.js b/src/util/oauth.js index 2f31e22..d64d555 100644 --- a/src/util/oauth.js +++ b/src/util/oauth.js @@ -6,7 +6,7 @@ */ import {store} from "../store"; -import {gqlQuery} from "./net"; +import {storeInit} from "../store"; /** * Handle OAuth callback and complete login process. @@ -52,30 +52,7 @@ export async function oauthCallbackHandler(queryString) { throw new Error("Failed to get session data: " + response.statusText); } - /* Store session data */ + /* Store session data and initialize data store. */ const rspJson = await response.json(); - store.session = { ...rspJson }; - store.userName = store.session.first_name ? store.session.first_name : store.session.username; + await storeInit(rspJson); } - -/** - * Create the user on the broker/todo database. - * - * Since user management is done by Keycloak, which uses its own user database, we must - * make sure that out user is known in the app database (basebox broker). After successful - * login, this function must be called to do that. - * - * @throws Error if the user creation failed. - */ -export async function createUser() { - - await gqlQuery(`mutation { - createUser( - username: "${store.session.username}", - name: "${store.session.first_name} ${store.session.last_name}" - ) { - username - } - }`); - -} \ No newline at end of file diff --git a/src/views/AboutView.vue b/src/views/AboutView.vue index d2582c9..a168d68 100644 --- a/src/views/AboutView.vue +++ b/src/views/AboutView.vue @@ -10,11 +10,5 @@ import TheAbout from '../components/TheAbout.vue' diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 03e1e67..aa5973c 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -8,8 +8,7 @@