Skip to content

Commit edfeced

Browse files
abelsiqueirac-martinez
authored andcommitted
Limit movement based on validation
1 parent 76ac031 commit edfeced

File tree

5 files changed

+205
-69
lines changed

5 files changed

+205
-69
lines changed

cypress/e2e/navigation.cy.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,97 @@ describe('App navigation', () => {
1818
.click()
1919
cy.url().then((e) => expect(e.endsWith('#/')).to.be.true)
2020
})
21+
22+
it('should not allow navigation until errors are fixed', () => {
23+
cy.visit('/start')
24+
cy.dataCy('btn-next')
25+
.should('be.disabled')
26+
cy.dataCy('btn-finish')
27+
.should('be.disabled')
28+
cy.dataCy('input-title')
29+
.type('My Title')
30+
cy.dataCy('btn-finish')
31+
.should('be.disabled')
32+
cy.dataCy('btn-next')
33+
.should('not.be.disabled')
34+
.click()
35+
cy.url()
36+
.should('include', '/authors')
37+
cy.dataCy('btn-next')
38+
.should('be.disabled')
39+
cy.dataCy('btn-finish')
40+
.should('be.disabled')
41+
cy.dataCy('step-identifiers')
42+
.click()
43+
cy.url()
44+
.should('include', '/authors')
45+
cy.dataCy('step-start')
46+
.click()
47+
cy.url()
48+
.should('include', '/start')
49+
cy.dataCy('input-title')
50+
.clear()
51+
cy.dataCy('btn-next')
52+
.should('be.disabled')
53+
cy.dataCy('step-authors')
54+
.click()
55+
cy.url()
56+
.should('include', '/start')
57+
cy.dataCy('input-title')
58+
.type('My Title')
59+
cy.dataCy('step-authors')
60+
.click()
61+
cy.url()
62+
.should('include', '/authors')
63+
cy.dataCy('btn-add-author')
64+
.click()
65+
cy.dataCy('btn-next')
66+
.should('not.be.disabled')
67+
cy.dataCy('step-identifiers')
68+
.click()
69+
cy.url()
70+
.should('include', '/authors')
71+
cy.dataCy('btn-finish')
72+
.should('not.be.disabled')
73+
})
74+
75+
it.only('should not allow download if there are errors', () => {
76+
cy.visit('/start')
77+
cy.dataCy('btn-download')
78+
.should('not.be.enabled')
79+
cy.dataCy('input-title')
80+
.type('A')
81+
cy.visit('/authors')
82+
cy.dataCy('btn-add-author')
83+
.click()
84+
cy.dataCy('btn-download')
85+
.should('not.be.disabled')
86+
})
87+
88+
it('should only allow clicking on advanced screens on stepper if no intermediary screen has errors', () => {
89+
cy.visit('/start')
90+
cy.dataCy('input-title')
91+
.type('A')
92+
cy.visit('/authors')
93+
cy.dataCy('btn-add-author')
94+
.click()
95+
cy.dataCy('btn-next')
96+
.click()
97+
.click()
98+
.click()
99+
cy.dataCy('btn-previous')
100+
.click()
101+
cy.dataCy('input-repository')
102+
.type('bad')
103+
cy.dataCy('btn-previous')
104+
.click()
105+
cy.dataCy('step-abstract')
106+
.click()
107+
cy.url().should('include', '/identifiers')
108+
cy.get('.q-notification__message')
109+
.should('contain.text', 'Fix error in "Related Resources"')
110+
})
111+
21112
describe('basic checks', () => {
22113
beforeEach(() => {
23114
cy.visit('/start')

src/components/DownloadButton.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
color="primary"
66
data-cy="btn-download"
77
download="CITATION.cff"
8+
v-bind:disable="errors.length > 0"
89
icon="download"
910
label="Download"
1011
size="xl"
@@ -17,6 +18,7 @@
1718
<script lang="ts">
1819
import { computed, defineComponent } from 'vue'
1920
import { useCffstr } from 'src/store/cffstr'
21+
import { useValidation } from 'src/store/validation'
2022
2123
const toDownloadUrl = (body: string) => {
2224
return `data:text/vnd.yaml,${encodeURIComponent(body)}`
@@ -26,8 +28,10 @@ export default defineComponent({
2628
name: 'DownloadButton',
2729
setup () {
2830
const { cffstr } = useCffstr()
31+
const { errors } = useValidation()
2932
return {
30-
downloadUrl: computed(() => toDownloadUrl(cffstr.value))
33+
downloadUrl: computed(() => toDownloadUrl(cffstr.value)),
34+
errors
3135
}
3236
}
3337
})

src/components/Stepper.vue

Lines changed: 42 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@
1414
>
1515
<q-step
1616
v-for="(step, stepIndex) in stepNames"
17-
v-bind:active-icon="activeIcon(currentStepIndex > stepIndex && errorPerStep[step].value, step)"
17+
v-bind:active-icon="step === 'finish' ? 'done' : 'edit'"
1818
v-bind:aria-label="toLabel(step)"
1919
v-bind:caption="stepIndex < 2 ? 'required' : (step !== 'finish' ? 'optional' : '')"
2020
v-bind:data-cy="`step-${step}`"
2121
v-bind:done="screenVisited(step) && !errorPerStep[step].value"
2222
v-bind:error="currentStepIndex != stepIndex && screenVisited(step) && errorPerStep[step].value"
23+
v-bind:header-nav="stepIndex !== currentStepIndex && screenVisited(step) && !anyErrorBetween('start', step)"
2324
v-bind:key="step"
2425
v-bind:name="step"
2526
v-bind:order="stepIndex"
2627
v-bind:title="toLabel(step)"
27-
v-on:click="setStepName(step)"
28+
v-on:click="onStepClick(step)"
2829
v-on:keyup.enter="setStepName(step)"
2930
/>
3031
</q-stepper>
@@ -33,82 +34,58 @@
3334

3435
<script lang="ts">
3536
36-
import { ComputedRef, computed } from 'vue'
3737
import { StepNameType, useApp } from 'src/store/app'
38-
import {
39-
byError,
40-
instancePathStartsWithMatcher,
41-
screenAuthorQueries,
42-
screenIdentifiersQueries,
43-
screenKeywordsQueries,
44-
screenRelatedResourcesQueries,
45-
screenStartQueries,
46-
screenVersionSpecificQueries
47-
} from 'src/error-filtering'
48-
import { useValidation } from 'src/store/validation'
38+
import { errorPerStep } from 'src/store/stepper-errors'
39+
import { useQuasar } from 'quasar'
4940
5041
export default {
5142
setup () {
5243
const { currentStepIndex, screenVisited, stepName, setStepName, stepNames } = useApp()
53-
const { errors } = useValidation()
44+
const $q = useQuasar()
45+
const anyErrorBetween = (stepA: StepNameType, stepB: StepNameType) => {
46+
const stepIndexA = stepNames.indexOf(stepA)
47+
const stepIndexB = stepNames.indexOf(stepB)
48+
return stepNames.slice(stepIndexA, stepIndexB)
49+
.map((step) => errorPerStep[step].value)
50+
.reduce((result, errorOnStep) => result || errorOnStep, false)
51+
}
5452
const toLabel = (name: string) => {
5553
if (name === 'start') { // Exception
56-
return 'Basic information'
54+
return 'Basic Information'
5755
}
5856
return name.split('-').map((s) => s.slice(0, 1).toUpperCase() + s.slice(1)).join(' ')
5957
}
60-
const errorStateScreenAuthors = computed(() => {
61-
return screenAuthorQueries
62-
.filter(byError(errors.value, instancePathStartsWithMatcher))
63-
.length > 0
64-
})
65-
const errorStateScreenIdentifiers = computed(() => {
66-
return screenIdentifiersQueries
67-
.filter(byError(errors.value, instancePathStartsWithMatcher))
68-
.length > 0
69-
})
70-
const errorStateScreenKeywords = computed(() => {
71-
return screenKeywordsQueries
72-
.filter(byError(errors.value, instancePathStartsWithMatcher))
73-
.length > 0
74-
})
75-
const errorStateScreenRelatedResources = computed(() => {
76-
return screenRelatedResourcesQueries
77-
.filter(byError(errors.value, instancePathStartsWithMatcher))
78-
.length > 0
79-
})
80-
const errorStateScreenStart = computed(() => {
81-
return screenStartQueries
82-
.filter(byError(errors.value)) // One of the possible errors is instancePath == '', so we use a traditional approach here
83-
.length > 0
84-
})
85-
const errorStateScreenVersionSpecific = computed(() => {
86-
return screenVersionSpecificQueries
87-
.filter(byError(errors.value, instancePathStartsWithMatcher))
88-
.length > 0
89-
})
90-
const errorPerStep: Record<StepNameType, ComputedRef<boolean>> = {
91-
start: errorStateScreenStart,
92-
authors: errorStateScreenAuthors,
93-
identifiers: errorStateScreenIdentifiers,
94-
'related-resources': errorStateScreenRelatedResources,
95-
abstract: computed(() => false),
96-
keywords: errorStateScreenKeywords,
97-
license: computed(() => false),
98-
'version-specific': errorStateScreenVersionSpecific,
99-
finish: computed(() => false)
100-
}
10158
return {
102-
activeIcon: (hasError: boolean, step: StepNameType) => {
103-
if (hasError) {
104-
return 'warning'
105-
} else if (step === 'finish' && errors.value.length === 0) {
106-
return 'done'
107-
} else {
108-
return 'edit'
59+
anyErrorBetween,
60+
currentStepIndex,
61+
onStepClick: (step: StepNameType) => {
62+
const curIndex = currentStepIndex.value
63+
const clickIndex = stepNames.indexOf(step)
64+
if ( // Only allow clicking on stepper if
65+
step !== stepName.value && // it is not the current step
66+
screenVisited(step) && // it is not a new screen
67+
( // don't skip errors when moving forward
68+
curIndex > clickIndex || // it is going back (up); OR
69+
(
70+
!errorPerStep[stepName.value].value && // it is not leaving an erroring screen
71+
!anyErrorBetween(stepName.value, step) // not skipping intermediary erroring screens if going forward
72+
)
73+
)
74+
) {
75+
void setStepName(step)
76+
} else if ( // Can't skip erroring screens
77+
curIndex < clickIndex &&
78+
anyErrorBetween(stepName.value, step)
79+
) {
80+
const firstStepWithError = stepNames.filter((step) => errorPerStep[step].value)[0]
81+
$q.notify({
82+
message: `Fix error in "${toLabel(firstStepWithError)}" before proceeding`,
83+
color: 'negative',
84+
progress: true,
85+
timeout: 1200
86+
})
10987
}
11088
},
111-
currentStepIndex,
11289
errorPerStep,
11390
screenVisited,
11491
setStepName,

src/components/StepperActions.vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
flat
2121
label="Finish"
2222
no-caps
23-
to="/finish"
2423
v-bind:class="cannotGoForward ? 'hidden' : ''"
24+
v-bind:disabled="errors.length > 0"
2525
v-on:click="setStepName('finish')"
2626
/>
2727
<q-btn
@@ -33,24 +33,30 @@
3333
no-caps
3434
unelevated
3535
v-bind:class="cannotGoForward ? 'hidden' : ''"
36+
v-bind:disabled="hasScreenError"
3637
v-on:click="navigateNext"
3738
/>
3839
</q-toolbar>
3940
</template>
4041

4142
<script lang="ts">
42-
import { defineComponent } from 'vue'
43+
import { computed, defineComponent } from 'vue'
44+
import { errorPerStep } from 'src/store/stepper-errors'
4345
import { useApp } from 'src/store/app'
46+
import { useValidation } from 'src/store/validation'
4447
4548
export default defineComponent({
4649
name: 'StepperActions',
4750
4851
setup () {
49-
const { cannotGoBack, cannotGoForward, navigateNext, navigatePrevious, setStepName } = useApp()
52+
const { cannotGoBack, cannotGoForward, navigateNext, navigatePrevious, setStepName, stepName } = useApp()
53+
const { errors } = useValidation()
5054
5155
return {
5256
cannotGoBack,
5357
cannotGoForward,
58+
errors,
59+
hasScreenError: computed(() => { return errorPerStep[stepName.value].value }),
5460
navigateNext,
5561
navigatePrevious,
5662
setStepName

src/store/stepper-errors.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
byError,
3+
instancePathStartsWithMatcher,
4+
screenAuthorQueries,
5+
screenIdentifiersQueries,
6+
screenKeywordsQueries,
7+
screenRelatedResourcesQueries,
8+
screenStartQueries,
9+
screenVersionSpecificQueries
10+
} from 'src/error-filtering'
11+
import { computed } from 'vue'
12+
// import { StepNameType } from 'src/store/app'
13+
import { useValidation } from 'src/store/validation'
14+
15+
const { errors } = useValidation()
16+
17+
const errorStateScreenAuthors = computed(() => {
18+
return screenAuthorQueries
19+
.filter(byError(errors.value, instancePathStartsWithMatcher))
20+
.length > 0
21+
})
22+
const errorStateScreenIdentifiers = computed(() => {
23+
return screenIdentifiersQueries
24+
.filter(byError(errors.value, instancePathStartsWithMatcher))
25+
.length > 0
26+
})
27+
const errorStateScreenKeywords = computed(() => {
28+
return screenKeywordsQueries
29+
.filter(byError(errors.value, instancePathStartsWithMatcher))
30+
.length > 0
31+
})
32+
const errorStateScreenRelatedResources = computed(() => {
33+
return screenRelatedResourcesQueries
34+
.filter(byError(errors.value, instancePathStartsWithMatcher))
35+
.length > 0
36+
})
37+
const errorStateScreenStart = computed(() => {
38+
return screenStartQueries
39+
.filter(byError(errors.value)) // One of the possible errors is instancePath == '', so we use a traditional approach here
40+
.length > 0
41+
})
42+
const errorStateScreenVersionSpecific = computed(() => {
43+
return screenVersionSpecificQueries
44+
.filter(byError(errors.value, instancePathStartsWithMatcher))
45+
.length > 0
46+
})
47+
48+
export const errorPerStep = {
49+
start: errorStateScreenStart,
50+
authors: errorStateScreenAuthors,
51+
identifiers: errorStateScreenIdentifiers,
52+
'related-resources': errorStateScreenRelatedResources,
53+
abstract: computed(() => false),
54+
keywords: errorStateScreenKeywords,
55+
license: computed(() => false),
56+
'version-specific': errorStateScreenVersionSpecific,
57+
finish: computed(() => false)
58+
}

0 commit comments

Comments
 (0)