feat: add message component #1082

Merged
dpschen merged 10 commits from feature/message into main 2021-11-28 14:18:28 +00:00
23 changed files with 170 additions and 127 deletions

View File

@ -8,7 +8,7 @@ const testAndAssertFailed = fixture => {
cy.wait(5000) // It can take waaaayy too long to log the user in
cy.url().should('include', '/')
cy.get('div.notification.is-danger').contains('Wrong username or password.')
cy.get('div.message.danger').contains('Wrong username or password.')
}
context('Login', () => {

View File

@ -32,7 +32,7 @@ context('Registration', () => {
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
})
it('Should fail', () => {
it.only('Should fail', () => {
const fixture = {
username: 'test',
password: '123456',
@ -45,6 +45,6 @@ context('Registration', () => {
cy.get('#password').type(fixture.password)
cy.get('#passwordValidation').type(fixture.password)
cy.get('#register-submit').click()
cy.get('div.notification.is-danger').contains('A user with this username already exists.')
cy.get('div.message.danger').contains('A user with this username already exists.')
})
})

View File

@ -129,7 +129,10 @@
"ignorePatterns": [
"*.test.*",
"cypress/*"
]
],
"globals": {
"defineProps": "readonly"
}
},
"postcss": {
"plugins": {

View File

@ -30,27 +30,25 @@
<a @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</a>
</div>
<div
class="notification is-success mt-2"
v-if="successMsg !== '' && errorMsg === ''"
>
<message variant="success" v-if="successMsg !== '' && errorMsg === ''" class="mt-2">
{{ successMsg }}
</div>
<div
class="notification is-danger mt-2"
v-if="errorMsg !== '' && successMsg === ''"
>
</message>
<message variant="danger" v-if="errorMsg !== '' && successMsg === ''" class="mt-2">
{{ errorMsg }}
</div>
</message>
</div>
</template>
<script>
import Message from '@/components/misc/message'
import {parseURL} from 'ufo'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
export default {
name: 'apiConfig',
components: {
Message,
},
data() {
return {
configureApi: false,

View File

@ -1,15 +1,18 @@
<template>
<div class="notification is-danger">
<message variant="danger">
<i18n-t keypath="loadingError.failed">
<a @click="reload">{{ $t('loadingError.tryAgain') }}</a>
<a href="https://vikunja.io/contact/" rel="noreferrer noopener nofollow" target="_blank">{{ $t('loadingError.contact') }}</a>
</i18n-t>
</div>
</message>
</template>
<script>
import Message from '@/components/misc/message'
export default {
name: 'error',
components: {Message},
methods: {
reload() {
window.location.reload()

View File

@ -4,13 +4,11 @@
<template v-for="(s, i) in shortcuts" :key="i">
<h3>{{ $t(s.title) }}</h3>
<div class="message is-primary">
<div class="message-body">
{{
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
}}
</div>
</div>
<message>
{{
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
}}
</message>
<dl class="shortcut-list">
<template v-for="(sc, si) in s.shortcuts" :key="si">
@ -30,11 +28,15 @@
<script>
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message'
import {KEYBOARD_SHORTCUTS} from './shortcuts'
export default {
name: 'keyboard-shortcuts',
components: {Shortcut},
components: {
Message,
Shortcut,
},
data() {
return {
shortcuts: KEYBOARD_SHORTCUTS,

View File

@ -0,0 +1,41 @@
<template>
<div class="message" :class="variant">
konrad marked this conversation as resolved Outdated

picky: remove brachets

picky: remove brachets

Done.

Done.
<slot/>
</div>
</template>
<script setup>
defineProps({
variant: {
konrad marked this conversation as resolved Outdated

is as a prop is already used by vues component component. There it indicates the component / tag. Since the meaning of this prop name is already taken I think we should use something different here. E.g. variant which is used in the modal.vue.

`is` as a prop is already used by vues `component` component. There it indicates the component / tag. Since the meaning of this prop name is already taken I think we should use something different here. E.g. `variant` which is used in the modal.vue.

I like variant - changed it.

I like `variant` - changed it.

Disabling might be prevented by removing const props = .

Disabling might be prevented by removing `const props = `.

That seems to work! I thought setting the prop with const props = ... was required for props to work.

That seems to work! I thought setting the prop with `const props = ...` was required for props to work.
The props [are automatically exposed via the context](https://vue-next-template-explorer.netlify.app/#%7B%22src%22%3A%22%3Ctemplate%3E%5Cn%5Ct%3Cdiv%20class%3D%5C%22message%5C%22%20%3Aclass%3D%5C%22variant%5C%22%3E%5Cn%20%20%20%20%7B%7Bbla%7D%7D%5Cn%5Ct%5Ct%3Cslot%2F%3E%5Cn%5Ct%3C%2Fdiv%3E%5Cn%3C%2Ftemplate%3E%22%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22filename%22%3A%22Foo.vue%22%2C%22prefixIdentifiers%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%2C%22inline%22%3Afalse%2C%22ssrCssVars%22%3A%22%7B%20color%20%7D%22%2C%22compatConfig%22%3A%7B%22MODE%22%3A3%7D%2C%22whitespace%22%3A%22condense%22%2C%22bindingMetadata%22%3A%7B%22TestComponent%22%3A%22setup-const%22%2C%22setupRef%22%3A%22setup-ref%22%2C%22setupConst%22%3A%22setup-const%22%2C%22setupLet%22%3A%22setup-let%22%2C%22setupMaybeRef%22%3A%22setup-maybe-ref%22%2C%22setupProp%22%3A%22props%22%2C%22vMySetupDir%22%3A%22setup-const%22%7D%7D%7D)

Neat, I didn't know that.

Neat, I didn't know that.

I think that's a composition api only thing. AFAIK with the option api you get the props in the render function via that $props argument.

I think that's a composition api only thing. AFAIK with the option api you get the props in the render function via that `$props` argument.
type: String,
default: 'info',
},
})
</script>
<style lang="scss" scoped>
.message {
padding: .75rem 1rem;
konrad marked this conversation as resolved
Review

Remove margin (we don't know all contexts this component will be used in).

Remove margin (we don't know all contexts this component will be used in).
Review

Done.

Done.
border-radius: $radius;
}
konrad marked this conversation as resolved Outdated

No need for the first &: the default prop already solves this.

No need for the first `&`: the default prop already solves this.

Done.

Done.
.info {
border: 1px solid var(--primary);
background: hsla(var(--primary-hsl), .05);
}
konrad marked this conversation as resolved Outdated

Don't indent classes: adds more (not needed) specifity.

Don't indent classes: adds more (not needed) specifity.

Done.

Done.
.danger {
border: 1px solid var(--danger);
background: hsla(var(--danger-h), var(--danger-s), var(--danger-l), .05);
}
.warning {
border: 1px solid var(--warning);
background: hsla(var(--warning-h), var(--warning-s), var(--warning-l), .05);
}
.success {
border: 1px solid var(--success);
background: hsla(var(--success-h), var(--success-s), var(--success-l), .05);
}
</style>

View File

@ -2,14 +2,9 @@
<div class="no-auth-wrapper">
<div class="noauth-container">
<Logo class="logo" width="400" height="117" />
<div class="message is-info" v-if="motd !== ''">
<div class="message-header">
<p>{{ $t('misc.info') }}</p>
</div>
<div class="message-body">
{{ motd }}
</div>
</div>
<message v-if="motd !== ''" class="my-2">
{{ motd }}
</message>
<slot/>
</div>
</div>
@ -17,6 +12,7 @@
<script setup>
import Logo from '@/components/home/Logo.vue'
import message from '@/components/misc/message'
import {useStore} from 'vuex'
import {computed} from 'vue'

View File

@ -16,7 +16,7 @@
<p v-if="error === errorNoApiUrl">
{{ $t('ready.noApiUrlConfigured') }}
</p>
<div class="notification is-danger" v-else>
<message variant="danger" v-else>
<p>
{{ $t('ready.errorOccured') }}<br/>
{{ error }}
@ -24,7 +24,7 @@
<p>
{{ $t('ready.checkApiUrl') }}
</p>
</div>
</message>
<api-config :configure-open="true" @found-api="load"/>
</card>
</no-auth-wrapper>
@ -43,6 +43,7 @@
<script>
import Logo from '@/assets/logo.svg?component'
import ApiConfig from '@/components/misc/api-config'
import Message from '@/components/misc/message'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
import {mapState} from 'vuex'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
@ -50,6 +51,7 @@ import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
export default {
name: 'ready',
components: {
Message,
Logo,
NoAuthWrapper,
ApiConfig,

View File

@ -193,10 +193,6 @@ export default {
align-items: center;
}
.message-body {
padding: .5rem .75rem;
}
}
}

View File

@ -23,7 +23,7 @@
@import "bulma-css-variables/sass/elements/content";
@import "bulma-css-variables/sass/elements/icon";
@import "bulma-css-variables/sass/elements/image";
@import "bulma-css-variables/sass/elements/notification";
//@import "bulma-css-variables/sass/elements/notification"; // not used
@import "bulma-css-variables/sass/elements/progress";
@import "bulma-css-variables/sass/elements/table";
@import "bulma-css-variables/sass/elements/tag";
@ -48,7 +48,7 @@
// @import "bulma-css-variables/sass/components/level"; // not used
@import "bulma-css-variables/sass/components/media";
@import "bulma-css-variables/sass/components/menu";
@import "bulma-css-variables/sass/components/message";
//@import "bulma-css-variables/sass/components/message"; // not used
@import "bulma-css-variables/sass/components/modal";
@import "bulma-css-variables/sass/components/navbar";
@import "bulma-css-variables/sass/components/pagination";

View File

@ -7,5 +7,4 @@
@import "form";
@import "link-share";
@import "loading";
@import "notification";
@import "flatpickr";

View File

@ -1,12 +0,0 @@
.notification {
border: $thickness solid $border;
}
.notifications {
left: 0.5rem !important;
bottom: 1rem !important;
}
.message .message-body {
border: $thickness solid;
}

View File

@ -3,7 +3,7 @@
<h2 v-if="userInfo">
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
</h2>
<div class="notification is-danger" v-if="deletionScheduledAt !== null">
<message variant="danger" v-if="deletionScheduledAt !== null">
{{
$t('user.deletion.scheduled', {
date: formatDateShort(deletionScheduledAt),
@ -13,7 +13,7 @@
<router-link :to="{name: 'user.settings', hash: '#deletion'}">
{{ $t('user.deletion.scheduledCancel') }}
</router-link>
</div>
</message>
<add-task
:listId="defaultListId"
@taskAdded="updateTaskList"
@ -57,6 +57,7 @@
<script>
import {mapState} from 'vuex'
import Message from '@/components/misc/message'
import ShowTasks from './tasks/ShowTasks.vue'
import {getHistory} from '../modules/listHistory'
import ListCard from '@/components/list/partials/list-card.vue'
@ -67,6 +68,7 @@ import {parseDateOrNull} from '../helpers/parseDateOrNull'
export default {
name: 'Home',
components: {
Message,
ListCard,
ShowTasks,
AddTask,
@ -147,7 +149,7 @@ export default {
flex-wrap: wrap;
max-height: calc(#{$list-height * 2} + #{$list-spacing * 2} - 4px);
overflow: hidden;
@media screen and (max-width: $mobile) {
max-height: calc(#{$list-height * 4} + #{$list-spacing * 4} - 4px);
}

View File

@ -36,9 +36,9 @@
</div>
</div>
<transition name="fade">
<div class="notification is-warning" v-if="currentList.isArchived">
<message variant="warning" v-if="currentList.isArchived" class="mb-4">
{{ $t('list.archived') }}
</div>
</message>
</transition>
<router-view/>
@ -46,6 +46,7 @@
</template>
<script>
import Message from '@/components/misc/message'
import ListModel from '../../models/list'
import ListService from '../../services/list'
import {CURRENT_LIST} from '../../store/mutation-types'
@ -53,6 +54,7 @@ import {getListView} from '../../helpers/saveListView'
import {saveListToHistory} from '../../modules/listHistory'
export default {
components: {Message},
data() {
return {
listService: new ListService(),

View File

@ -39,7 +39,7 @@
<div class="migration-in-progress">
<img :alt="migrator.name" :src="migrator.icon" class="logo"/>
<div class="progress-dots">
<span v-for="i in progressDotsCount" :key="i" />
<span v-for="i in progressDotsCount" :key="i"/>
</div>
<Logo class="logo"/>
</div>
@ -57,11 +57,9 @@
</div>
</div>
<div v-else>
<div class="message is-primary">
<div class="message-body">
{{ message }}
</div>
</div>
<message class="mb-4">
{{ message }}
</message>
<x-button :to="{name: 'home'}">{{ $t('misc.refresh') }}</x-button>
</div>
</div>
@ -71,6 +69,7 @@
import AbstractMigrationService from '@/services/migrator/abstractMigration'
import AbstractMigrationFileService from '@/services/migrator/abstractMigrationFile'
import Logo from '@/assets/logo.svg?component'
import Message from '@/components/misc/message'
import {MIGRATORS} from './migrators'
@ -79,7 +78,10 @@ const PROGRESS_DOTS_COUNT = 8
export default {
name: 'MigrateService',
components: { Logo },
components: {
Logo,
Message,
},
data() {
return {
@ -101,7 +103,7 @@ export default {
beforeRouteEnter(to) {
if (MIGRATORS[to.params.service] === undefined) {
return { name: 'not-found' }
return {name: 'not-found'}
}
},
@ -122,7 +124,7 @@ export default {
if (this.migrator.isFileMigrator) {
return
}
this.authUrl = await this.migrationService.getAuthUrl().then(({url}) => url)
this.migratorAuthCode = location.hash.startsWith('#token=')
@ -150,7 +152,7 @@ export default {
this.lastMigrationDate = null
this.message = ''
let migrationConfig = { code: this.migratorAuthCode }
let migrationConfig = {code: this.migratorAuthCode}
if (this.migrator.isFileMigrator) {
if (this.$refs.uploadInput.files.length === 0) {
@ -160,7 +162,7 @@ export default {
}
try {
const { message } = await this.migrationService.migrate(migrationConfig)
const {message} = await this.migrationService.migrate(migrationConfig)
this.message = message
return this.$store.dispatch('namespaces/loadNamespaces')
} finally {
@ -173,24 +175,24 @@ export default {
<style lang="scss" scoped>
.migration-in-progress-container {
max-width: 400px;
margin: 4rem auto 0;
text-align: center;
max-width: 400px;
margin: 4rem auto 0;
text-align: center;
}
.migration-in-progress {
text-align: center;
display: flex;
max-width: 400px;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
text-align: center;
display: flex;
max-width: 400px;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.logo {
display: block;
max-height: 100px;
max-width: 100px;
display: block;
max-height: 100px;
max-width: 100px;
}
.progress-dots {

View File

@ -28,19 +28,20 @@
<div class="field">
<label class="label">{{ $t('namespace.attributes.color') }}</label>
<div class="control">
<color-picker v-model="namespace.hexColor" />
<color-picker v-model="namespace.hexColor"/>
</div>
</div>
<div class="notification is-info mt-4">
<message class="mt-4">
<h4 class="title">{{ $t('namespace.create.tooltip') }}</h4>
{{ $t('namespace.create.explanation') }}
</div>
</message>
</create-edit>
</template>
<script>
import Message from '@/components/misc/message'
import NamespaceModel from '../../models/namespace'
import NamespaceService from '../../services/namespace'
import CreateEdit from '@/components/misc/create-edit.vue'
@ -56,6 +57,7 @@ export default {
}
},
components: {
Message,
ColorPicker,
CreateEdit,
},
@ -72,7 +74,7 @@ export default {
const namespace = await this.namespaceService.create(this.namespace)
this.$store.commit('namespaces/addNamespace', namespace)
this.$message.success({message: this.$t('namespace.create.success') })
this.$message.success({message: this.$t('namespace.create.success')})
this.$router.back()
},
},

View File

@ -1,8 +1,8 @@
<template>
<div>
<div class="notification is-info is-light has-text-centered" v-if="loading">
<message v-if="loading">
{{ $t('sharing.authenticating') }}
</div>
</message>
<div v-if="authenticateWithPassword" class="box">
<p class="pb-2">
{{ $t('sharing.passwordRequired') }}
@ -25,18 +25,20 @@
{{ $t('user.auth.login') }}
</x-button>
<div class="notification is-danger mt-4" v-if="errorMessage !== ''">
<message variant="danger" class="mt-4" v-if="errorMessage !== ''">
{{ errorMessage }}
</div>
</message>
</div>
</div>
</template>
<script>
import {mapGetters} from 'vuex'
import Message from '@/components/misc/message'
export default {
name: 'LinkSharingAuth',
components: {Message},
data() {
return {
loading: true,
@ -72,7 +74,7 @@ export default {
password: this.password,
})
this.$router.push({name: 'list.list', params: {listId: r.list_id}})
} catch(e) {
} catch (e) {
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13001) {
this.authenticateWithPassword = true
return

View File

@ -2,9 +2,9 @@
<div>
<h2 class="title has-text-centered">Login</h2>
<div class="box">
<div class="notification is-success has-text-centered" v-if="confirmedEmailSuccess">
<message variant="success" class="has-text-centered" v-if="confirmedEmailSuccess">
{{ $t('user.auth.confirmEmailSuccess') }}
</div>
</message>
<api-config @foundApi="hasApiUrl = true"/>
<form @submit.prevent="submit" id="loginform" v-if="hasApiUrl && localAuthEnabled">
<div class="field">
@ -78,9 +78,9 @@
</router-link>
</div>
</div>
<div class="notification is-danger" v-if="errorMessage">
<message variant="danger" v-if="errorMessage">
{{ errorMessage }}
</div>
</message>
</form>
<div
@ -110,11 +110,13 @@ import {LOADING} from '@/store/mutation-types'
import legal from '../../components/misc/legal'
import ApiConfig from '@/components/misc/api-config.vue'
import {getErrorText} from '@/message'
import Message from '@/components/misc/message'
import {redirectToProvider} from '../../helpers/redirectToProvider'
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
export default {
components: {
Message,
ApiConfig,
legal,
},
@ -149,7 +151,7 @@ export default {
const last = getLastVisited()
if (last !== null) {
this.$router.push({
name: last.name,
name: last.name,
params: last.params,
})
clearLastVisited()
@ -206,7 +208,7 @@ export default {
try {
await this.$store.dispatch('auth/login', credentials)
this.$store.commit('auth/needsTotpPasscode', false)
} catch(e) {
} catch (e) {
if (e.response && e.response.data.code === 1017 && !credentials.totpPasscode) {
return
}

View File

@ -1,11 +1,11 @@
<template>
<div>
<div class="notification is-danger" v-if="errorMessage">
<message variant="danger" v-if="errorMessage">
{{ errorMessage }}
</div>
<div class="notification is-info" v-if="loading">
</message>
<message v-if="loading">
{{ $t('user.auth.authenticating') }}
</div>
</message>
</div>
</template>
@ -14,10 +14,12 @@ import {mapState} from 'vuex'
import {LOADING} from '@/store/mutation-types'
import {getErrorText} from '@/message'
import Message from '@/components/misc/message'
import {clearLastVisited, getLastVisited} from '../../helpers/saveLastVisited'
export default {
name: 'Auth',
components: {Message},
data() {
return {
errorMessage: '',

View File

@ -45,37 +45,37 @@
</x-button>
</div>
</div>
<div class="notification is-info" v-if="this.passwordResetService.loading">
<message v-if="this.passwordResetService.loading">
{{ $t('misc.loading') }}
</div>
<div class="notification is-danger" v-if="errorMsg">
</message>
<message v-if="errorMsg">
{{ errorMsg }}
</div>
</message>
</form>
<div class="has-text-centered" v-if="successMessage">
<div class="notification is-success">
<message variant="success">
{{ successMessage }}
</div>
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
</div>
<Legal />
<Legal/>
</div>
</div>
</template>
<script setup>
import {ref, reactive} from 'vue'
import { useI18n } from 'vue-i18n'
import {useI18n} from 'vue-i18n'
import Legal from '@/components/misc/legal'
import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset'
import { useTitle } from '@/composables/useTitle'
import {useTitle} from '@/composables/useTitle'
import Message from '@/components/misc/message'
const { t } = useI18n()
const {t} = useI18n()
useTitle(() => t('user.auth.resetPassword'))
const credentials = reactive({
@ -97,10 +97,10 @@ async function submit() {
const passwordReset = new PasswordResetModel({newPassword: credentials.password})
try {
const { message } = passwordResetService.resetPassword(passwordReset)
const {message} = passwordResetService.resetPassword(passwordReset)
successMessage.value = message
localStorage.removeItem('passwordResetToken')
} catch(e) {
} catch (e) {
errorMsg.value = e.response.data.message
}
}

View File

@ -83,12 +83,12 @@
</x-button>
</div>
</div>
<div class="notification is-info" v-if="loading">
<message v-if="loading">
{{ $t('misc.loading') }}
</div>
<div class="notification is-danger" v-if="errorMessage !== ''">
</message>
<message variant="danger" v-if="errorMessage !== ''">
{{ errorMessage }}
</div>
</message>
</form>
<legal/>
</div>
@ -97,13 +97,13 @@
<script setup>
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
import { useI18n } from 'vue-i18n'
import {useI18n} from 'vue-i18n'
import router from '@/router'
import { store } from '@/store'
import { useTitle } from '@/composables/useTitle'
import {store} from '@/store'
import {useTitle} from '@/composables/useTitle'
import Legal from '@/components/misc/legal'
import Message from '@/components/misc/message'
// FIXME: use the `beforeEnter` hook of vue-router
// Check if the user is already logged in, if so, redirect them to the homepage
@ -113,7 +113,7 @@ onBeforeMount(() => {
}
})
const { t } = useI18n()
const {t} = useI18n()
useTitle(() => t('user.auth.register'))
const credentials = reactive({
@ -137,7 +137,7 @@ async function submit() {
try {
await store.dispatch('auth/register', toRaw(credentials))
} catch(e) {
} catch (e) {
errorMessage.value = e.message
}
}

View File

@ -31,14 +31,14 @@
</x-button>
</div>
</div>
<div class="notification is-danger" v-if="errorMsg">
<message variant="danger" v-if="errorMsg">
{{ errorMsg }}
</div>
</message>
</form>
<div class="has-text-centered" v-if="isSuccess">
<div class="notification is-success">
<message variant="success">
{{ $t('user.auth.resetPasswordSuccess') }}
</div>
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
@ -57,6 +57,7 @@ import Legal from '@/components/misc/legal'
import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset'
import { useTitle } from '@/composables/useTitle'
import Message from '@/components/misc/message'
const { t } = useI18n()
useTitle(() => t('user.auth.resetPassword'))