Skip to content

Commit

Permalink
Merge pull request #3172 from carlobeltrame/picker-fixes
Browse files Browse the repository at this point in the history
Usability improvements around the picker components
  • Loading branch information
carlobeltrame authored Dec 19, 2022
2 parents 2bf1819 + 4eb65ff commit a15f47c
Show file tree
Hide file tree
Showing 35 changed files with 2,683 additions and 1,857 deletions.
2 changes: 2 additions & 0 deletions common/helpers/dayjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import utc from 'dayjs/plugin/utc'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import isBetween from 'dayjs/plugin/isBetween'
import duration from 'dayjs/plugin/duration'

dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(localizedFormat)
dayjs.extend(isBetween)
dayjs.extend(duration)

export default dayjs
14 changes: 14 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"devDependencies": {
"@babel/eslint-parser": "7.19.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/user-event": "14.4.3",
"@testing-library/vue": "5.8.3",
"@vitejs/plugin-vue2": "2.2.0",
"@vue/cli-plugin-babel": "5.0.8",
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/buttons/ButtonBack.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<template>
<v-btn aria-label="Zurück" icon v-bind="$attrs" @click="$router.go(-1)">
<v-btn
icon
:aria-label="$tc('global.button.back')"
v-bind="$attrs"
@click="$router.go(-1)"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
</template>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/buttons/ButtonDelete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<span v-if="!iconOnly" class="d-none d-sm-block">
<slot>{{ $tc('global.button.delete') }}</slot>
</span>
<span class="d-sr-only" :class="{ 'd-sm-none': !iconOnly }">
<slot>{{ $tc('global.button.delete') }}</slot>
</span>
</v-btn>
</template>

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/buttons/ButtonEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<span class="d-none d-sm-block">
<slot>{{ $tc('global.button.edit') }}</slot>
</span>
<span class="d-sr-only d-sm-none">
<slot>{{ $tc('global.button.edit') }}</slot>
</span>
</v-btn>
</template>

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/campAdmin/CreateCampPeriods.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
:name="$tc('entity.period.fields.start')"
vee-id="start"
vee-rules="required"
:max="period.end"
:my="2"
:filled="false"
required
Expand All @@ -59,6 +60,7 @@
input-class="ml-2"
:name="$tc('entity.period.fields.end')"
vee-rules="required|greaterThanOrEqual_date:@start"
:min="period.start"
:my="2"
:filled="false"
required
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/campAdmin/DialogPeriodForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
:name="$tc('entity.period.fields.start')"
vee-id="start"
vee-rules="required"
:max="localPeriod.end"
/>

<e-date-picker
v-model="localPeriod.end"
:name="$tc('entity.period.fields.end')"
vee-rules="required|greaterThanOrEqual_date:@start"
:min="localPeriod.start"
/>
</div>
</template>
Expand Down
24 changes: 5 additions & 19 deletions frontend/src/components/form/api/ApiWrapperAppend.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,6 @@
<v-icon color="green" class="checkIcon" :class="checkIconAddon">
mdi-content-save
</v-icon>
<!--
<v-btn
fab
dark
depressed
x-small
color="success"
class="checkIcon"
:class="checkIconAddon">
<v-icon>mdi-content-save</v-icon>
</v-btn>
-->
</div>

<!-- Retry/Cancel button if saving failed -->
Expand All @@ -31,6 +19,7 @@
color="error"
type="submit"
class="mr-1"
:aria-label="$tc('global.button.tryagain')"
v-on="on"
@click="wrapper.on.save"
>
Expand All @@ -47,6 +36,7 @@
depressed
x-small
color="grey"
:aria-label="$tc('global.button.cancel')"
v-on="on"
@click="wrapper.on.reset"
>
Expand All @@ -68,8 +58,8 @@
color="success"
type="submit"
class="mr-1"
:aria-label="$tc('global.button.save')"
v-on="on"
@click="wrapper.on.save"
>
<v-icon>mdi-check</v-icon>
</v-btn>
Expand All @@ -84,6 +74,7 @@
depressed
x-small
color="grey"
:aria-label="$tc('global.button.cancel')"
v-on="on"
@click="wrapper.on.reset"
>
Expand All @@ -95,12 +86,7 @@
</template>

<!-- Retry button if loading failed -->
<button-retry
v-if="wrapper.hasLoadingError"
text
type="submit"
@click="wrapper.on.reload"
/>
<button-retry v-if="wrapper.hasLoadingError" text @click="wrapper.on.reload" />
</div>
</template>

Expand Down
125 changes: 57 additions & 68 deletions frontend/src/components/form/api/__tests__/ApiColorPicker.spec.js
Original file line number Diff line number Diff line change
@@ -1,101 +1,90 @@
import ApiColorPicker from '../ApiColorPicker'
import ApiWrapper from '@/components/form/api/ApiWrapper'
import Vue from 'vue'
import Vuetify from 'vuetify'
import flushPromises from 'flush-promises'
import formBaseComponents from '@/plugins/formBaseComponents'
import merge from 'lodash/merge'
import { screen, waitFor } from '@testing-library/vue'
import { render } from '@/test/renderWithVuetify.js'
import user from '@testing-library/user-event'
import { ApiMock } from '@/components/form/api/__tests__/ApiMock'
import { extend } from 'vee-validate'
import { i18n } from '@/plugins'
import { mount as mountComponent } from '@vue/test-utils'
import { regex } from 'vee-validate/dist/rules'
import { waitForDebounce } from '@/test/util'

Vue.use(Vuetify)
Vue.use(formBaseComponents)

extend('regex', regex)

describe('An ApiColorPicker', () => {
let vuetify
let wrapper
let apiMock

const fieldName = 'test-field/123'
const COLOR_1 = '#ff0000'
const COLOR_2 = '#ff00ff'
const FIELD_NAME = 'test-field/123'
const FIELD_LABEL = 'Test field'
const COLOR_1 = '#FF0000'
const COLOR_2 = '#FAFFAF'
const PICKER_BUTTON_LABEL_TEXT = 'Dialog öffnen um eine Farbe für Test field zu wählen'

beforeEach(() => {
vuetify = new Vuetify()
apiMock = ApiMock.create()
})

afterEach(() => {
jest.restoreAllMocks()
wrapper.destroy()
})

const mount = (options) => {
const app = Vue.component('App', {
components: { ApiColorPicker },
test('triggers api.patch and status update if input changes', async () => {
// given
apiMock.get().thenReturn(ApiMock.success(COLOR_1).forFieldName(FIELD_NAME))
apiMock.patch().thenReturn(ApiMock.success(COLOR_2))
const { container } = render(ApiColorPicker, {
props: {
fieldName: { type: String, default: fieldName },
autoSave: false,
fieldname: FIELD_NAME,
uri: 'test-field/123',
label: FIELD_LABEL,
required: true,
},
template: `
<div data-app>
<api-color-picker
:auto-save="false"
:fieldname="fieldName"
uri="test-field/123"
label="Test field"
required="true"
/>
</div>
`,
})
apiMock.get().thenReturn(ApiMock.success(COLOR_1).forFieldName(fieldName))
const defaultOptions = {
mocks: {
$tc: () => {},
api: apiMock.getMocks(),
},
}
return mountComponent(app, {
vuetify,
i18n,
attachTo: document.body,
...merge(defaultOptions, options),
})
}

test('triggers api.patch and status update if input changes', async () => {
apiMock.patch().thenReturn(ApiMock.success(COLOR_2))
wrapper = mount()

await flushPromises()

const input = wrapper.find('input')
await input.setValue(COLOR_2)
await input.trigger('submit')

await waitForDebounce()
await flushPromises()

expect(apiMock.getMocks().patch).toBeCalledTimes(1)
expect(wrapper.findComponent(ApiWrapper).vm.localValue).toBe(COLOR_2)
// when
// click the button to open the picker
await user.click(screen.getByLabelText(PICKER_BUTTON_LABEL_TEXT))
// click inside the color picker canvas to select a different color
const canvas = container.querySelector('canvas')
await user.click(canvas, { clientX: 10, clientY: 10 })
// click the save button
await user.click(screen.getByLabelText('Speichern'))

// then
await waitFor(async () => {
const inputField = await screen.findByLabelText(FIELD_LABEL)
expect(inputField.value).toBe(COLOR_2)
expect(apiMock.getMocks().patch).toBeCalledTimes(1)
})
})

test('updates state if value in store is refreshed and has new value', async () => {
wrapper = mount()
apiMock.get().thenReturn(ApiMock.success(COLOR_2).forFieldName(fieldName))

wrapper.findComponent(ApiWrapper).vm.reload()
// given
apiMock.get().thenReturn(ApiMock.networkError().forFieldName(FIELD_NAME))
render(ApiColorPicker, {
props: {
autoSave: false,
fieldname: FIELD_NAME,
uri: 'test-field/123',
label: FIELD_LABEL,
required: true,
},
mocks: {
api: apiMock.getMocks(),
},
})
await screen.findByText('A network error occurred.')
expect((await screen.findByLabelText(FIELD_LABEL)).value).not.toBe(COLOR_1)
const retryButton = await screen.findByText('Erneut versuchen')
apiMock.get().thenReturn(ApiMock.success(COLOR_1).forFieldName(FIELD_NAME))

await waitForDebounce()
await flushPromises()
// when
await user.click(retryButton)

expect(wrapper.findComponent(ApiWrapper).vm.localValue).toBe(COLOR_2)
expect(wrapper.find('input[type=text]').element.value).toBe(COLOR_2)
// then
await waitFor(async () => {
expect((await screen.findByLabelText(FIELD_LABEL)).value).toBe(COLOR_1)
})
})
})
Loading

0 comments on commit a15f47c

Please sign in to comment.