This repository has been archived by the owner on May 20, 2024. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Don't error if missing place type ... might just not have loaded yet * Agreements UI * Refine agreement display * Put agreement form in a card * Small fixes * Support en-GB locale * Loading states for agreements and ui tweaks * Implement cancel button * Implement DateInput and rename to activeTo * Make user list items clickable ... I know, random change in this branch, but it was bugging me! * Add fancy diff view for agreement changes * History detail tweaks * Add websocket updates for agreements * Add create agreement app test * Please Our Holy Linter * i18n additions * Remove duplicate text, more agreement text padding * No wrap for agreement actions
- Loading branch information
1 parent
fbd76f0
commit ecfc7f0
Showing
36 changed files
with
1,421 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# AppTests | ||
|
||
This are for doing tests of the WHOLE application (at least from `App` component), including the routing, layout, etc... | ||
|
||
A few points: | ||
- you can only have ONE test per file | ||
- in theory there could be more, but it's *really* hard to reset everything between them, I failed with wierd vue errors, maybe one day it JustWorks™ | ||
- we only mock the backend for these, see `test/mockBackend` for more | ||
- they are slower, but very comprehensive tests | ||
|
||
## Tips | ||
|
||
If the output diff is not showing you enough you can use: | ||
|
||
```shell | ||
DEBUG_PRINT_LIMIT=100000 yarn test src/AppTests/yourAppTest.spec.js | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import '@testing-library/jest-dom' | ||
import { faker } from '@faker-js/faker' | ||
import userEvent from '@testing-library/user-event' | ||
import { render, configure } from '@testing-library/vue' | ||
|
||
import App from '@/App' | ||
import router from '@/router' | ||
|
||
import { withDefaults } from '>/helpers' | ||
import { useMockBackend, createUser, createGroup, loginAs, db } from '>/mockBackend' | ||
import { addUserToGroup } from '>/mockBackend/groups' | ||
|
||
useMockBackend() | ||
jest.setTimeout(60 * 1000) // we do a lot of stuff here, give it some time! | ||
|
||
configure({ | ||
asyncUtilTimeout: 2000, | ||
}) | ||
|
||
test('create an agreement', async () => { | ||
const { type, click } = userEvent.setup() | ||
|
||
const user = createUser() | ||
const group = createGroup({ features: ['agreements'] }) | ||
addUserToGroup(user, group) | ||
|
||
const otherUser = createUser() | ||
addUserToGroup(otherUser, group) | ||
|
||
user.currentGroup = group.id | ||
loginAs(user) | ||
|
||
const { | ||
getByText, | ||
findByText, | ||
findAllByText, | ||
findByRole, | ||
findByTitle, | ||
} = render(App, withDefaults({ | ||
global: { plugins: [router], stubs: { RouterLink: false } }, | ||
})) | ||
|
||
// go to agreements page and click to make a new one | ||
await click(await findByRole('link', { name: 'Agreements' })) | ||
await click(await findByTitle('Create')) | ||
|
||
const title = faker.lorem.words(5) | ||
const summary = faker.lorem.paragraph() | ||
const content = faker.lorem.paragraphs(10) | ||
|
||
// fill in the form | ||
await type(await findByRole('textbox', { name: 'Agreement Title' }), title) | ||
await type(await findByRole('textbox', { name: 'Summary (optional)' }), summary) | ||
await type(await findByRole('textbox', { name: 'Agreement Text' }), content) | ||
|
||
await click(await findByRole('button', { name: 'Create' })) | ||
|
||
// see it on the page! | ||
// (using the All variant as it appears twice... (breadcrumbs, and body) | ||
await findAllByText(title) | ||
await findByText(summary) | ||
// this gets turned into markdown, so swap newlines for .* regexp... | ||
const re = new RegExp(content.split('\n').join('.*')) | ||
await findByText(re) | ||
|
||
expect(router.currentRoute.value.name).toEqual('agreement') | ||
|
||
// make sure the data ended up in the db | ||
const agreement = db.agreements[0] | ||
expect(agreement.title).toEqual(title) | ||
expect(agreement.summary).toEqual(summary) | ||
expect(agreement.content).toEqual(content) | ||
|
||
// shows it's active | ||
expect(getByText('Active')).toBeInTheDocument() | ||
|
||
// has history view | ||
expect(getByText('agreement created')).toBeInTheDocument() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import axios, { parseCursor } from '@/base/api/axios' | ||
import { underscorizeKeys } from '@/utils/utils' | ||
|
||
export default { | ||
|
||
async create (agreement) { | ||
return convert((await axios.post('/api/agreements/', agreement)).data) | ||
}, | ||
|
||
async get (agreementId) { | ||
return convert((await axios.get(`/api/agreements/${agreementId}/`)).data) | ||
}, | ||
|
||
async list (params = {}) { | ||
const response = (await axios.get('/api/agreements/', { params: underscorizeKeys(params) })).data | ||
return { | ||
...response, | ||
next: parseCursor(response.next), | ||
results: convert(response.results), | ||
} | ||
}, | ||
|
||
async save (agreement) { | ||
return convert((await axios.patch(`/api/agreements/${agreement.id}/`, agreement)).data) | ||
}, | ||
} | ||
|
||
export function convert (val) { | ||
if (Array.isArray(val)) { | ||
return val.map(convert) | ||
} | ||
else { | ||
const result = { ...val } | ||
|
||
const dateFields = [ | ||
'activeFrom', | ||
'activeTo', | ||
'reviewAt', | ||
] | ||
|
||
for (const field of dateFields) { | ||
if (val[field]) { | ||
result[field] = new Date(val[field]) | ||
} | ||
} | ||
|
||
return result | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
<template> | ||
<QCard | ||
class="edit-box" | ||
:class="{ changed: hasChanged }" | ||
> | ||
<form | ||
class="q-gutter-y-lg" | ||
style="max-width: 700px" | ||
@submit.prevent="maybeSave" | ||
> | ||
<h3> | ||
<QIcon | ||
name="fas fa-handshake" | ||
class="q-pr-md" | ||
color="grey" | ||
/> | ||
<template v-if="isNew"> | ||
{{ $t('AGREEMENT.NEW') }} | ||
</template> | ||
<template v-else> | ||
{{ $t('AGREEMENT.EDIT') }} | ||
</template> | ||
</h3> | ||
|
||
<QInput | ||
v-model="edit.title" | ||
:autofocus="!$q.platform.has.touch" | ||
v-bind="titleError" | ||
:label="$t('AGREEMENT.TITLE')" | ||
:hint="$t('AGREEMENT.TITLE_HELPER')" | ||
outlined | ||
class="q-mb-lg" | ||
@blur="v$.edit.title.$touch" | ||
/> | ||
|
||
<div class="row"> | ||
<DateInput | ||
v-model="edit.activeFrom" | ||
:error="hasError('activeFrom')" | ||
:label="$t('AGREEMENT.ACTIVE_FROM')" | ||
/> | ||
<DateInput | ||
v-model="edit.activeTo" | ||
:error="hasError('activeTo')" | ||
:label="$t('AGREEMENT.ACTIVE_TO')" | ||
clearable | ||
/> | ||
</div> | ||
|
||
<DateInput | ||
v-model="edit.reviewAt" | ||
:error="hasError('reviewAt')" | ||
:label="`${$t('AGREEMENT.REVIEW_AT')} (${$t('VALIDATION.OPTIONAL')})`" | ||
clearable | ||
/> | ||
|
||
<MarkdownInput | ||
v-model="edit.summary" | ||
:error="hasError('summary')" | ||
:error-message="firstError('summary')" | ||
:label="`${$t('AGREEMENT.SUMMARY')} (${$t('VALIDATION.OPTIONAL')})`" | ||
outlined | ||
class="q-mb-lg" | ||
@keyup.ctrl.enter="maybeSave" | ||
/> | ||
|
||
<MarkdownInput | ||
v-model="edit.content" | ||
:error="hasError('content')" | ||
:error-message="firstError('content')" | ||
:label="$t('AGREEMENT.CONTENT')" | ||
outlined | ||
class="q-mb-lg" | ||
@keyup.ctrl.enter="maybeSave" | ||
/> | ||
|
||
<div class="row justify-end q-gutter-sm q-mt-sm"> | ||
<QBtn | ||
type="button" | ||
@click="$emit('cancel')" | ||
> | ||
{{ $t('BUTTON.CANCEL') }} | ||
</QBtn> | ||
|
||
<QBtn | ||
type="submit" | ||
color="primary" | ||
:disable="!canSave" | ||
:loading="isPending" | ||
> | ||
{{ $t(isNew ? 'BUTTON.CREATE' : 'BUTTON.SAVE_CHANGES') }} | ||
</QBtn> | ||
</div> | ||
</form> | ||
</QCard> | ||
</template> | ||
|
||
<script> | ||
import useVuelidate from '@vuelidate/core' | ||
import { maxLength, required } from '@vuelidate/validators' | ||
import { | ||
QCard, | ||
QIcon, | ||
QInput, | ||
QBtn, | ||
date, | ||
} from 'quasar' | ||
import editMixin from '@/utils/mixins/editMixin' | ||
import statusMixin, { mapErrors } from '@/utils/mixins/statusMixin' | ||
import DateInput from '@/utils/components/DateInput' | ||
import MarkdownInput from '@/utils/components/MarkdownInput' | ||
export default { | ||
components: { | ||
DateInput, | ||
QCard, | ||
QIcon, | ||
QInput, | ||
QBtn, | ||
MarkdownInput, | ||
}, | ||
mixins: [editMixin, statusMixin], | ||
props: { | ||
value: { | ||
type: Object, | ||
default: () => ({ | ||
title: undefined, | ||
summary: undefined, | ||
content: undefined, | ||
activeFrom: new Date(), | ||
reviewAt: undefined, | ||
}), | ||
}, | ||
}, | ||
emits: [ | ||
'save', | ||
'cancel', | ||
], | ||
setup () { | ||
return { | ||
v$: useVuelidate(), | ||
} | ||
}, | ||
computed: { | ||
...mapErrors({ | ||
title: [ | ||
['required', 'VALIDATION.REQUIRED'], | ||
['maxLength', 'VALIDATION.MAXLENGTH', { max: 241 }], | ||
], | ||
summary: [], | ||
content: [ | ||
['required', 'VALIDATION.REQUIRED'], | ||
], | ||
}), | ||
canSave () { | ||
if (!this.isNew && !this.hasChanged) { | ||
return false | ||
} | ||
return true | ||
}, | ||
smallScreen () { | ||
return this.$q.screen.width < 450 || this.$q.screen.height < 450 | ||
}, | ||
activeFrom: { | ||
get () { | ||
return date.formatDate(this.edit.activeFrom, 'YYYY-MM-DD') | ||
}, | ||
set (val) { | ||
if (val) { | ||
val = date.extractDate(val, 'YYYY-MM-DD') | ||
val = date.adjustDate(this.edit.activeFrom, { year: val.getFullYear(), month: val.getMonth() + 1, date: val.getDate() }) | ||
} | ||
this.edit.activeFrom = val | ||
}, | ||
}, | ||
reviewAt: { | ||
get () { | ||
return date.formatDate(this.edit.reviewAt, 'YYYY-MM-DD') | ||
}, | ||
set (val) { | ||
if (val) { | ||
val = date.extractDate(val, 'YYYY-MM-DD') | ||
val = date.adjustDate(this.edit.reviewAt, { year: val.getFullYear(), month: val.getMonth() + 1, date: val.getDate() }) | ||
} | ||
this.edit.reviewAt = val || null | ||
}, | ||
}, | ||
}, | ||
methods: { | ||
maybeSave () { | ||
if (!this.canSave) return | ||
this.save() | ||
}, | ||
focusReviewAt (evt) { | ||
// If it's a button, it's the "clear" button | ||
// so we don't want to pop open the calendar | ||
if (evt.target.nodeName !== 'BUTTON') { | ||
this.$refs.qReviewAtProxy.show() | ||
} | ||
}, | ||
}, | ||
validations: { | ||
edit: { | ||
title: { | ||
required, | ||
maxLength: maxLength(240), | ||
}, | ||
content: { | ||
required, | ||
}, | ||
}, | ||
}, | ||
} | ||
</script> | ||
|
||
<style scoped lang="sass"> | ||
@import '~editbox' | ||
</style> |
Oops, something went wrong.