Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- Fix: Work package creation with a non-existent project [#923](https://github.com/nextcloud/integration_openproject/pull/923)
- Fix: Project Value in the field could not be removed [#923](https://github.com/nextcloud/integration_openproject/pull/923)
- Fix: Misleading mesesage in select field during wp creation [#924] (https://github.com/nextcloud/integration_openproject/pull/924)

### Removed

Expand Down
5 changes: 5 additions & 0 deletions src/constants/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export const messages = {
externalOIDCProvider: t(APP_ID, 'External Provider'),
tokenExchangeHintText: t(APP_ID, 'When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process.'),
opRequiredVersionAndPlanHint: t(APP_ID, 'Requires OpenProject version {version} (or higher) and an active Corporate plan.', { version: OPENPROJECT_VERSION }),
pleaseSelectProject: t(APP_ID, 'Please select a project'),
noMachingWorkProjectsFound: t(APP_ID, 'No matching work projects found'),
noMachingStausFound: t(APP_ID, 'No matching status found'),
noMachingTypeFound: t(APP_ID, 'No matching type found'),
noMachingAssigneeFound: t(APP_ID, 'No matching assignee found'),
}

export const messagesFmt = {
Expand Down
36 changes: 30 additions & 6 deletions src/views/CreateWorkPackageModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<span>{{ label }}</span>
</template>
<template #no-options>
{{ getNoOptionText }}
{{ getNoOptionTextForProject }}
</template>
</NcSelect>
<p v-if="error.error && error.attribute === 'project'" class="validation-error">
Expand Down Expand Up @@ -85,7 +85,7 @@
{{ option.label }}
</template>
<template #no-options>
{{ t('integration_openproject', 'Please select a project') }}
{{ getNoOptionTextForType }}
</template>
</NcSelect>
<p v-if="customTypeError" class="validation-error type-error" v-html="sanitizedRequiredCustomTypeValidationErrorMessage" /> <!-- eslint-disable-line vue/no-v-html -->
Expand Down Expand Up @@ -116,7 +116,7 @@
{{ option.label }}
</template>
<template #no-options>
{{ t('integration_openproject', 'Please select a project') }}
{{ getNoOptionTextForStatus }}
</template>
</NcSelect>
<p v-if="error.error && error.attribute === 'status'" class="validation-error">
Expand All @@ -143,7 +143,7 @@
{{ option.label }}
</template>
<template #no-options>
{{ t('integration_openproject', 'Please select a project') }}
{{ getNoOptionTextForAssignee }}
</template>
</NcSelect>
<div class="create-workpackage-form--label">
Expand Down Expand Up @@ -171,6 +171,7 @@ import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import { STATE } from '../utils.js'
import debounce from 'lodash/debounce.js'
import { messages } from '../constants/messages.js'

const SEARCH_CHAR_LIMIT = 1
const DEBOUNCE_THRESHOLD = 500
Expand Down Expand Up @@ -301,13 +302,23 @@ export default {
mappedNodes() {
return this.mappedProjects()
},
getNoOptionText() {
getNoOptionTextForProject() {
if (this.availableProjects.length === 0) {
return t('integration_openproject', 'No matching work projects found!')
return messages.noMachingWorkProjectsFound
}
// while projects are being searched we make the no text option empty
return ''
},
getNoOptionTextForStatus() {
return this.getNoOptionText('status')
},
getNoOptionTextForType() {
return this.getNoOptionText('type')
},

getNoOptionTextForAssignee() {
return this.getNoOptionText('assignee')
},
sanitizedRequiredCustomTypeValidationErrorMessage() {
// get the last number from the href i.e `/api/v3/types/1`, which is the type id
const typeID = parseInt(this.type.self.href.match(/\d+$/)[0], 10)
Expand Down Expand Up @@ -606,6 +617,19 @@ export default {
}
return allowedValues
},
getNoOptionText(fieldName) {
if (this.project.label === null) {
return messages.pleaseSelectProject
}
if (fieldName === 'assignee') {
return messages.noMachingAssigneeFound
} else if (fieldName === 'type') {
return messages.noMachingTypeFound
} else if (fieldName === 'status') {
return messages.noMachingStausFound
}
return ''
},
async setAvailableAssigneesForProject(projectId) {
this.availableAssignees = []
let response
Expand Down
85 changes: 77 additions & 8 deletions tests/jest/views/CreateWorkpackageModal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import availableProjectAssignees from '../fixtures/availableProjectAssigneesResp
import workpackageCreatedResponse from '../fixtures/workPackageSuccessfulCreationResponse.json'
import requiredTypeResponse from '../fixtures/formValidationResponseRequiredType.json'
import CreateWorkPackageModal from '../../../src/views/CreateWorkPackageModal.vue'
import { messages } from '../../../src/constants/messages.js'

const localVue = createLocalVue()
jest.mock('@nextcloud/dialogs', () => ({
Expand Down Expand Up @@ -49,7 +50,10 @@ const createWorkPackageUrl = generateOcsUrl('/apps/integration_openproject/api/v
describe('CreateWorkPackageModal.vue', () => {
const createWorkPackageSelector = '.create-workpackage-modal'
const projectSelectSelector = '[data-test-id="available-projects"]'
const firstProjectSelectorSelector = '[data-test-id="available-projects"] [role="listbox"] > li'
const firstProjectSelector = '[data-test-id="available-projects"] [role="listbox"] > li'
const firstTypeSelector = '[data-test-id="available-types"] [role="listbox"] > li'
const firstStatusSelector = '[data-test-id="available-statuses"] [role="listbox"] > li'
const firstAssigneeSelector = '[data-test-id="available-assignees"] [role="listbox"] > li'
const statusSelectSelector = '[data-test-id="available-statuses"]'
const typeSelectSelector = '[data-test-id="available-types"]'
const assigneesSelectSelector = '[data-test-id="available-assignees"]'
Expand Down Expand Up @@ -114,7 +118,7 @@ describe('CreateWorkPackageModal.vue', () => {
)
})

it('should show "No matching work projects found!" when the searched project is not found', async () => {
it('should show "No matching work projects found" when the searched project is not found', async () => {
const axiosSpyWithSearchQuery = jest.spyOn(axios, 'get')
.mockImplementationOnce(() => sendOCSResponse({}))
await inputField.setValue('Scw')
Expand All @@ -128,11 +132,35 @@ describe('CreateWorkPackageModal.vue', () => {
},
},
)
const searchResult = wrapper.find(firstProjectSelectorSelector)
expect(searchResult.text()).toBe('No matching work projects found!')
const searchResult = wrapper.find(firstProjectSelector)
expect(searchResult.text()).toBe(messages.noMachingWorkProjectsFound)
})

it('should auto clear project if there is "No matching work projects found!"', async () => {
it.each([
{
fieldName: 'type',
inputSelector: typeInputFieldSelector,
resultSelector: firstTypeSelector,
},
{
fieldName: 'status',
inputSelector: statusInputFieldSelector,
resultSelector: firstStatusSelector,
},
{
fieldName: 'assignee',
inputSelector: assigneeInputFieldSelector,
resultSelector: firstAssigneeSelector,
},
])('should show "Please select a project" on initial state when the $fieldName is not found', async ({ inputSelector, resultSelector }) => {
const inputField = wrapper.find(inputSelector)
await inputField.setValue(' ')
await inputField.trigger('focus')
const searchResult = wrapper.find(resultSelector)
expect(searchResult.text()).toBe(messages.pleaseSelectProject)
})

it('should auto clear project if there is "No matching work projects found"', async () => {
const axiosSpyWithSearchQuery = jest.spyOn(axios, 'get')
.mockImplementationOnce(() => sendOCSResponse({}))
await inputField.setValue('Scw')
Expand All @@ -147,8 +175,8 @@ describe('CreateWorkPackageModal.vue', () => {
},
},
)
const searchResult = wrapper.find(firstProjectSelectorSelector)
expect(searchResult.text()).toBe('No matching work projects found!')
const searchResult = wrapper.find(firstProjectSelector)
expect(searchResult.text()).toBe(messages.noMachingWorkProjectsFound)
expect(inputField.element.value).toBe('Scw')

// Trigger blur event (user moves to another field)
Expand All @@ -171,7 +199,7 @@ describe('CreateWorkPackageModal.vue', () => {
},
},
)
const searchResult = wrapper.find(firstProjectSelectorSelector)
const searchResult = wrapper.find(firstProjectSelector)
expect(searchResult.text()).toBe('searchedProject')
})

Expand Down Expand Up @@ -674,6 +702,46 @@ describe('CreateWorkPackageModal.vue', () => {
await wrapper.vm.$nextTick()
expect(wrapper.vm.status.label).toBe('')
})

it.each([
{
fieldName: 'type',
inputSelector: typeInputFieldSelector,
resultSelector: firstTypeSelector,
expectedMessage: messages.noMachingTypeFound,
},
{
fieldName: 'status',
inputSelector: statusInputFieldSelector,
resultSelector: firstStatusSelector,
expectedMessage: messages.noMachingStausFound,
},
{
fieldName: 'assignee',
inputSelector: assigneeInputFieldSelector,
resultSelector: firstAssigneeSelector,
expectedMessage: messages.noMachingAssigneeFound,
},
])('should show $expectedMessage when project is set and there is no $fieldName found in search query', async ({ inputSelector, resultSelector, expectedMessage }) => {

wrapper = mountWrapper(true, {
project: {
self: {
href: '/api/v3/projects/4',
title: 'Scrum project',
},
label: 'Scrum project',
children: [],
},
})

const input = wrapper.find(inputSelector)
await input.setValue('non-existent-search')
await input.trigger('focus')

const searchResult = wrapper.find(resultSelector)
expect(searchResult.text()).toBe(expectedMessage)
})
})

it('should emit an event if work package creation is successful', async () => {
Expand Down Expand Up @@ -782,6 +850,7 @@ describe('CreateWorkPackageModal.vue', () => {
},
]
jest.spyOn(axios, 'get')

.mockImplementationOnce(() => sendOCSResponse(availableProjectsResponse))
const axiosSpy = jest.spyOn(axios, 'post')
.mockImplementationOnce(() => sendOCSResponse(requiredTypeResponse))
Expand Down