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 = "";
+}
+
-
-
+
+
-
+
+
+
An error occurred:
+
+
+
+
+
+
+
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 167df76..0000000
Binary files a/src/assets/img/trash-solid.png and /dev/null differ
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 @@
-
-
-
-
-
An error occurred...
-
-
{{ errorMsg }}
-
-
- Start Over
-
-
-
-
-
-
-
-
-
\ 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 @@
+
+
+
+
+
+ Lists
+
+
+
+
+
+
+
+
+
+
There are no lists, yet.
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ ToDo List
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
-
-
-
-
-
-
- ToDo List
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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 @@