feat: move user settings to multiple components #889

Merged
konrad merged 27 commits from feature/user-settings into main 2021-10-26 20:58:03 +00:00
Owner

This PR Moves all user settings to separate pages to declutter them and make it easier for users to quickly find what they're looking for.

Here's how it looks:

image

This PR Moves all user settings to separate pages to declutter them and make it easier for users to quickly find what they're looking for. Here's how it looks: ![image](/attachments/c3773e36-dab8-444f-8dfe-6955b2fa4b5e)
dpschen approved these changes 2021-10-23 19:25:58 +00:00
dpschen left a comment
Member

👋 I hope the comments don't sound too rude / harsh.
Realized that after finishing everthing and don't want to rewrite again.
See them all together as ideas for improvements / personal views that can happen in future or never :)

👋 I hope the comments don't sound too rude / harsh. Realized that after finishing everthing and don't want to rewrite again. See them all together as ideas for improvements / personal views that can happen in future or never :)
@ -288,2 +2,2 @@
</p>
</card>
<div class="user-settings">
<ul>

Wrap ul in <nav> element

Wrap ul in `<nav>` element
Author
Owner

Done!

Done!
konrad marked this conversation as resolved
@ -415,1 +64,3 @@
}
computed: mapState({
totpEnabled: state => state.config.totpEnabled,
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,

How about making migratorsEnabled a getter in the store?

How about making `migratorsEnabled` a getter in the store?

The rest can be simplified with:

computed: {
	...mapState('config', ['totpEnabled', 'caldavEnabled'],
	[...]
}
The rest can be simplified with: ```js computed: { ...mapState('config', ['totpEnabled', 'caldavEnabled'], [...] } ```
Author
Owner

Done (both of it).

Done (both of it).
konrad marked this conversation as resolved
@ -418,0 +73,4 @@
.user-settings {
display: flex;
max-width: $widescreen;
margin: 0 auto;

If we put the margin in a wrapper component, this component can be used in a different context without change, e.g. a modal.
As a rule of thumb: never add outer margin / positioning to a component wrapper element.

If we put the margin in a wrapper component, this component can be used in a different context without change, e.g. a modal. As a rule of thumb: never add outer margin / positioning to a component wrapper element.
Author
Owner

You mean wrapping the <router-view> this is mounted in with a margin: 0 auto?

You mean wrapping the `<router-view>` this is mounted in with a `margin: 0 auto`?

No. I guess that also doesn't make sense.

My idea was

  • either to create a wrapper component for similar usecases where the view is centered
  • or to create a wrapper div inside this component so that the centered content styling is encapsulated in this component.

Reason is that it make components self enclosed and don't have leaking style that prevents them (generally) from other future forms of usage. In this case it means that this component prevents itself from beeing used in a use case where we don't want the centering margin.

Maybe a simpler and easier example would be:
Never add position: fixed to the outer div of a component but instead always do that from the outside.
Because then you can use that component also in cases where it's not fixed.

Sum up: don't include the outer positioning of a component inside.
All that makes it much easier to maintain a design system.

No. I guess that also doesn't make sense. My idea was - either to create a wrapper component for similar usecases where the view is centered - or to create a wrapper div inside this component so that the centered content styling is encapsulated in this component. Reason is that it make components self enclosed and don't have leaking style that prevents them (generally) from other future forms of usage. In this case it means that this component prevents itself from beeing used in a use case where we don't want the centering margin. Maybe a simpler and easier example would be: Never add `position: fixed` to the outer div of a component but instead always do that from the outside. Because then you can use that component also in cases where it's not fixed. Sum up: don't include the outer positioning of a component inside. All that makes it much easier to maintain a design system.
Author
Owner

I see, thanks for clarifying. Sounds very reasonable.

I've added a wrapper div in the component, hope that clears it up.

I see, thanks for clarifying. Sounds very reasonable. I've added a wrapper div in the component, hope that clears it up.

Sounds good!

I think abstracting this to another component just makes sense when we have a usecase somewhere else =)

Sounds good! I think abstracting this to another component just makes sense when we have a usecase somewhere else =)
konrad marked this conversation as resolved
@ -418,0 +75,4 @@
max-width: $widescreen;
margin: 0 auto;
ul {

ul, and li a don't have to be nested.

Better would be to add classes to the elements because they make maintaining the CSS simpler by:

  • making overwrites simpler. This is because tags have a higher specifity.
  • making it simpler to change the tag of the affected elements later.
`ul`, and `li a` don't have to be nested. Better would be to add classes to the elements because they make maintaining the CSS simpler by: - making overwrites simpler. This is because tags have a higher specifity. - making it simpler to change the tag of the affected elements later.
Author
Owner

Done.

Done.
konrad marked this conversation as resolved
@ -0,0 +66,4 @@
import {Cropper} from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
import AvatarService from '../../../services/avatar'

Use @ alias so that we don't have to rewrite import paths every time we move a component to a new folder

Use `@` alias so that we don't have to rewrite import paths every time we move a component to a new folder
Author
Owner

Yeah that's something I need to teach webstorm so it doesn't mess it up all the time.

Adjusted in all new components.

Yeah that's something I need to teach webstorm so it doesn't mess it up all the time. Adjusted in all new components.
konrad marked this conversation as resolved
@ -0,0 +1,58 @@
<template>
<card v-if="caldavEnabled" :title="$t('user.settings.caldav.title')">

This would show a empty view if caldav gets disabled out of whatever reason.

How about using the beforeRouteEnter hook, redirect to the general settings and throwing an error?

This would show a empty view if caldav gets disabled out of whatever reason. How about using the `beforeRouteEnter` hook, redirect to the general settings and throwing an error?
Author
Owner

How would I access the config in state? this doesn't seem to be available in the before hook.

How would I access the config in state? `this` doesn't seem to [be available](https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards) in the before hook.

You can import the store directly =)

EDIT:
Btw, another advantage of using the store directly in these cases is:
You can make the guards reusable and import them in multiple components. Allthough I guess in this case we don't need that.

You can import the store directly =) **EDIT:** Btw, another advantage of using the store directly in these cases is: You can make the guards reusable and import them in multiple components. Allthough I guess in this case we don't need that.
Author
Owner

But couldn't that lead to issues where the store isn't pouplated already when calling it here? The only way to ever trigger that hook is by manually entering it into the browser's address bar which then boots everything and populates the store (and also does a call to the config api endpoint to figure out if caldav is enabled or not). That means it wouldn't even work in the one case where it was called. In all other cases, there's just no link to that page that could trigger it.

But couldn't that lead to issues where the store isn't pouplated already when calling it here? The only way to ever trigger that hook is by manually entering it into the browser's address bar which then boots everything and populates the store (and also does a call to the config api endpoint to figure out if caldav is enabled or not). That means it wouldn't even work in the one case where it was called. In all other cases, there's just no link to that page that could trigger it.

Solving this with new issue: #906

Solving this with new issue: https://kolaente.dev/vikunja/frontend/issues/906
dpschen marked this conversation as resolved
@ -0,0 +17,4 @@
</div>
</div>
<p>
<a href="https://vikunja.io/docs/caldav/" rel="noreferrer noopener nofollow" target="_blank">

The url should be a const that is defined at the same place as the link behind "powered by Vikunja".
As a minimum maybe inside this document but outside the default export so that it can be combined with the latter in a later step easily.

The url should be a const that is defined at the same place as the link behind "powered by Vikunja". As a minimum maybe inside this document but outside the default export so that it can be combined with the latter in a later step easily.
Author
Owner

Makes sense. Done.

Makes sense. Done.
konrad marked this conversation as resolved
@ -0,0 +35,4 @@
},
computed: {
caldavUrl() {
let apiBase = window.API_URL.replace('/api/v1', '')

This feels like a hack.
The apiBase should be the one that gets defined in the initial api-config.vue

Regardless of the above: this url manipulation is a great case for https://github.com/unjs/ufo/ that we already use in the api-config.vue

How about moving this to the store as a getter. This way this component has nothing to do with the api configuration and the included hack.

This feels like a hack. The apiBase should be the one that gets defined in the initial api-config.vue Regardless of the above: this url manipulation is a great case for https://github.com/unjs/ufo/ that we already use in the `api-config.vue` How about moving this to the store as a getter. This way this component has nothing to do with the api configuration and the included hack.
Author
Owner

That really is a hack from ancient times...

Moved it to a getter.

That really is a hack from ancient times... Moved it to a getter.
konrad marked this conversation as resolved
@ -0,0 +45,4 @@
return `${apiBase}/dav/principals/${this.userInfo.username}/`
},
...mapState({

See comment above regariding usage of mapState

See comment above regariding usage of `mapState`
Author
Owner

Even though the two variables from state I'm using here come from different components?

Even though the two variables from state I'm using here come from different components?

you need to use it then twice of course. I just thought it's less verbose because you just need two lines and don't have all that duplication (caldavEnabled once as prop in the store and once as the local representation — we want it to be the same either way).

you need to use it then twice of course. I just thought it's less verbose because you just need two lines and don't have all that duplication (`caldavEnabled` once as prop in the store and once as the local representation — we want it to be the same either way).
Author
Owner

I've change the config one but kept the other one. I wanted to keep having auth.info as userinfo instead of just info which it would have been if I'd just use mapState('auth', ['info']). Hope that makes sense.

I've change the config one but kept the other one. I wanted to keep having `auth.info` as `userinfo` instead of just `info` which it would have been if I'd just use `mapState('auth', ['info'])`. Hope that makes sense.
konrad marked this conversation as resolved
@ -0,0 +52,4 @@
this.setTitle(`${this.$t('user.settings.updateEmailTitle')} - ${this.$t('user.settings.title')}`)
},
methods: {
async updateEmail() {

I think we should move methods like these in the store as actions.
This makes it possible to do stuff like updating the email from other locations easily.
Make that's not the best example but for other settings this would help us reduce a lot of repetitive code.

I think we should move methods like these in the store as actions. This makes it possible to do stuff like updating the email from other locations easily. Make that's not the best example but for other settings this would help us reduce a lot of repetitive code.
Author
Owner

I'm not sure if you should be able to change the email from somwhere else than this component. What other settings functions do you have in mind?

I'm not sure if you should be able to change the email from somwhere else than this component. What other settings functions do you have in mind?

For example could this be useful to make the changes directly but add a notification with an undo action. The undo action triggers that function in the store with the cached old state.
Another idea: resetting email (not sure if this is really the same logic, didn't check)

It's the idea of keeping bussiness logic separated from the view logic. That makes it easy to access the same logic from different components.

For example could this be useful to make the changes directly but add a notification with an undo action. The undo action triggers that function in the store with the cached old state. Another idea: resetting email (not sure if this is really the same logic, didn't check) It's the idea of keeping bussiness logic separated from the view logic. That makes it easy to access the same logic from different components.
Author
Owner

If it's just the undo action, you could probably get away with just using an arrow function. (that actually works already, see here).

I do get your point though. Shouldn't I then replace this everywhere? How do I decide what goes directly into a component and what doesn't?

If it's just the undo action, you could probably get away with just using an arrow function. (that actually works already, see [here](https://kolaente.dev/vikunja/frontend/src/branch/main/src/views/tasks/TaskDetailView.vue#L644)). I do get your point though. Shouldn't I then replace this everywhere? How do I decide what goes directly into a component and what doesn't?
@ -0,0 +6,4 @@
<input
@keyup.enter="updateSettings"
class="input"
id="newName"

The id should include a random id to prevent future errors of duplicate ids.

The id should include a random id to prevent future errors of duplicate ids.
Author
Owner

Done.

Done.
konrad marked this conversation as resolved
@ -0,0 +44,4 @@
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" v-model="playSoundWhenDone"/>

When this gets changed to true we should play the actual sound.

When this gets changed to `true` we should play the actual sound.
konrad marked this conversation as resolved
@ -0,0 +132,4 @@
availableLanguages() {
return Object.entries(availableLanguages)
.map(l => ({code: l[0], title: l[1]}))
.sort((a, b) => a.title > b.title)

We should use localCompare for sorting by abc

We should use [localCompare](https://stackoverflow.com/a/45544166/15522256) for sorting by abc
konrad marked this conversation as resolved
@ -0,0 +4,4 @@
<div class="field">
<label class="label" for="newPassword">{{ $t('user.settings.newPassword') }}</label>
<div class="control">
<input

We should add the correct autocomplete attributes so that the changed login data is easier available in password managers.

We should add [the correct autocomplete attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values) so that the changed login data is easier available in password managers.
konrad marked this conversation as resolved
@ -0,0 +1,130 @@
<template>
<card :title="$t('user.settings.totp.title')" v-if="totpEnabled">

Same here with the beforeRouteEnter.

Same here with the `beforeRouteEnter`.

See new issue: #906

See new issue: https://kolaente.dev/vikunja/frontend/issues/906
dpschen marked this conversation as resolved
@ -0,0 +19,4 @@
<div class="field">
<label class="label" for="totpConfirmPasscode">{{ $t('user.settings.totp.passcode') }}</label>
<div class="control">
<input

Add autocomplete="one-time-code"

Add `autocomplete="one-time-code"`
konrad marked this conversation as resolved
@ -0,0 +35,4 @@
{{ $t('user.settings.totp.setupSuccess') }}
</p>
<p v-if="!totpDisableForm">
<x-button @click="totpDisableForm = true" class="is-danger">{{ $t('misc.disable') }}</x-button>

This should be a toggle button so that a curious user can go back in the previous ui state.

This should be a toggle button so that a curious user can go back in the previous ui state.
Author
Owner

But the button is not visible when totpDisableForm is true. It requires a different action to disable it.

But the button is not visible when `totpDisableForm` is true. It requires a different action to disable it.

What I meant is: UI should be reversible.
See https://www.interaction-design.org/literature/article/shneiderman-s-eight-golden-rules-will-help-you-design-better-interfaces
under "Permit easy reversal of actions".

If a user clicks that button to enable that form, there should be a way to go back to the old state where the form was collapsed without the need to reload.
My idea was to keep that button and make a toggle button out of it.
As an alternative there could be a close button (?). But since it's some kind of collapsable I thought the toggle button makes sense here.

What I meant is: UI should be reversible. See https://www.interaction-design.org/literature/article/shneiderman-s-eight-golden-rules-will-help-you-design-better-interfaces under "Permit easy reversal of actions". If a user clicks that button to enable that form, there should be a way to go back to the old state where the form was collapsed without the need to reload. My idea was to keep that button and make a toggle button out of it. As an alternative there could be a close button (?). But since it's some kind of collapsable I thought the toggle button makes sense here.
Author
Owner

Good point. I've added a "cancel" button next to the "disable" button which reverses the UI state. I think that should be enough.

Good point. I've added a "cancel" button next to the "disable" button which reverses the UI state. I think that should be enough.
konrad marked this conversation as resolved
@ -0,0 +106,4 @@
},
async totpSetQrCode() {
const qr = await this.totpService.qrcode()
const urlCreator = window.URL || window.webkitURL

Remove window.webkitURL. In other places in the codebase window.URL.createObjectURL is called directly.

Remove `window.webkitURL`. In other places in the codebase `window.URL.createObjectURL` is called directly.
konrad marked this conversation as resolved
konrad force-pushed feature/user-settings from 27d1b2f301 to 3ff2c99a35 2021-10-25 19:18:29 +00:00 Compare
konrad added 1 commit 2021-10-25 19:33:02 +00:00
continuous-integration/drone/pr Build is failing Details
f26feafee5
feat: less nesting for tags
konrad added 1 commit 2021-10-25 19:36:57 +00:00
continuous-integration/drone/pr Build is failing Details
8d1d80dec8
chore: simplify vuex state
konrad added 1 commit 2021-10-25 19:39:53 +00:00
continuous-integration/drone/pr Build is failing Details
7421c78c79
chore: use @ for imports
konrad added 1 commit 2021-10-25 19:49:29 +00:00
continuous-integration/drone/pr Build is failing Details
7d0cab8fc9
chore: move external links to a consts file
dpschen reviewed 2021-10-25 19:50:00 +00:00
@ -36,2 +36,4 @@
},
}),
getters: {
migratorsEnabled: state => state.availableMigrators !== null && state.availableMigrators.length > 0,

use state.availableMigrators?.length > 0

use `state.availableMigrators?.length > 0`
konrad marked this conversation as resolved
konrad added 1 commit 2021-10-25 19:57:53 +00:00
continuous-integration/drone/pr Build was killed Details
3f072663f0
fix: add wrapper div for sizing
konrad added 1 commit 2021-10-25 20:05:44 +00:00
continuous-integration/drone/pr Build is failing Details
f93b84eec7
chore: add random id to id field
konrad added 1 commit 2021-10-25 20:07:58 +00:00
continuous-integration/drone/pr Build is failing Details
0b6ec922a4
feat: play pop when changing the setting
konrad added 1 commit 2021-10-25 20:09:42 +00:00
continuous-integration/drone/pr Build was killed Details
e61c7aeac1
feat: use localeCompare for sorting
konrad added 1 commit 2021-10-25 20:11:44 +00:00
continuous-integration/drone/pr Build was killed Details
3f0611c087
feat: use correct autocomplete attributes
konrad added 1 commit 2021-10-25 20:16:29 +00:00
continuous-integration/drone/pr Build was killed Details
43ef07e100
chore: cleanup
konrad added 1 commit 2021-10-25 20:17:06 +00:00
continuous-integration/drone/pr Build was killed Details
3b64b24da2
chore: cleanup
konrad added 1 commit 2021-10-25 20:19:02 +00:00
continuous-integration/drone/pr Build is failing Details
97e761690d
fix: frontend tests
konrad added 1 commit 2021-10-25 20:29:31 +00:00
continuous-integration/drone/pr Build is failing Details
9a356b2564
feat: move calculating api base url to store getter
konrad added 1 commit 2021-10-25 20:31:44 +00:00
continuous-integration/drone/pr Build is failing Details
a98c7e9377
fix(tests): input selector in frontend test
konrad added 1 commit 2021-10-25 20:34:57 +00:00
continuous-integration/drone/pr Build is passing Details
505e008895
fix(tests): lint
konrad added 1 commit 2021-10-26 19:11:38 +00:00
continuous-integration/drone/pr Build is passing Details
14c5efee92
chore: simplify usage of mapState
konrad added 1 commit 2021-10-26 19:24:51 +00:00
continuous-integration/drone/pr Build is passing Details
53dd4324a1
fix: add cancel button
Author
Owner

Ready to merge?

Ready to merge?
konrad merged commit 5040a76781 into main 2021-10-26 20:58:03 +00:00
konrad deleted branch feature/user-settings 2021-10-26 20:58:03 +00:00
This repo is archived. You cannot comment on pull requests.
No description provided.