Skip to content
This repository was archived by the owner on Dec 14, 2023. It is now read-only.

Commit ca14739

Browse files
authored
Add new page for accept/deny access to Dojo (#251)
* Add new page for accept/deny access to Dojo through requests to join * Bugfix: display of the declined message was wrong due to how ternary actions works * Add error message on conflict * Add info regarding translations and tests in README * Make it less dependable on the params and more from what the API returns (dojoId) so that it's compatible with the old UI flow * Fix integration tests * Update translation version
1 parent 00d2fa2 commit ca14739

File tree

11 files changed

+515
-16
lines changed

11 files changed

+515
-16
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ To run Cypress tests with the Cypress UI (good for debugging issues), you will f
1212
yarn cypress:open
1313
```
1414

15-
This will open a window where you can select what spec to run.
15+
This will open a window where you can select what spec to run. Notice that if you have a new version of the translations which is not published yet, tests on strings containing interpolation will fail as they are not depending on the linked version of your own repo. Running it headless will solve that.
1616

1717
### Headless
1818
You can also run the Cypress tests headless through Docker. You'll first need to install Cypress within the Docker container by running
@@ -28,7 +28,8 @@ docker-compose run --rm cypress
2828
```
2929

3030
## wdio tests
31-
The selenium-based wdio tests will run along with the unit tests when you run
31+
The selenium-based wdio tests are legacy tests, waiting to be migrated to cypress. They are not actively maintained and are there only for reference until migrated.
32+
To run the tests
3233
```
33-
docker-compose run --rm test
34+
docker-compose run --rm test e2e-with-mocks
3435
```
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import page from '../../pages/manage-membership-requests';
2+
3+
describe('Membership requests', () => {
4+
beforeEach(() => {
5+
cy.server();
6+
});
7+
it('should show the accept flow for a mentor', () => {
8+
cy.route('/api/2.0/users/instance', 'fx:parentLoggedIn').as('loggedIn');
9+
cy.route('/api/3.0/dojos/d1/membership-requests/rq1', { id: 'rq1', dojoId: 'd1', userType: 'mentor' }).as('load');
10+
cy.route('PUT', '/api/3.0/dojos/d1/membership-requests/rq1', {}).as('actOnRQ');
11+
cy.visit('/dashboard/dojos/d1/join-requests/rq1/status/accept');
12+
cy.wait('@loggedIn');
13+
cy.wait('@load');
14+
cy.get(page.circle).should('be.visible');
15+
cy.wait('@actOnRQ');
16+
cy.get(page.checkmark).should('be.visible');
17+
cy.get(page.text).invoke('text').should('eq', 'The user is now a mentor for your Dojo. This means they can now book mentor tickets and check in users to your events.');
18+
});
19+
it('should show the accept flow for a champion', () => {
20+
cy.route('/api/2.0/users/instance', 'fx:parentLoggedIn').as('loggedIn');
21+
cy.route('/api/3.0/dojos/d1/membership-requests/rq1', { id: 'rq1', dojoId: 'd1', userType: 'champion' }).as('load');
22+
cy.route('PUT', '/api/3.0/dojos/d1/membership-requests/rq1', {}).as('actOnRQ');
23+
cy.visit('/dashboard/dojos/d1/join-requests/rq1/status/accept');
24+
cy.wait('@loggedIn');
25+
cy.wait('@load');
26+
cy.get(page.circle).should('be.visible');
27+
cy.wait('@actOnRQ');
28+
cy.get(page.checkmark).should('be.visible');
29+
cy.get(page.text).invoke('text').should('eq', 'The user is now a champion for your Dojo. This means they can now create events, modify the Dojo page and award badges.');
30+
});
31+
it('should show the declined flow', () => {
32+
cy.route('/api/2.0/users/instance', 'fx:parentLoggedIn').as('loggedIn');
33+
cy.route('/api/3.0/dojos/d1/membership-requests/rq1', { id: 'rq1', dojoId: 'd1', userType: 'champion' }).as('load');
34+
cy.route('DELETE', '/api/3.0/dojos/d1/membership-requests/rq1', {}).as('actOnRQ');
35+
cy.visit('/dashboard/dojos/d1/join-requests/rq1/status/refuse');
36+
cy.wait('@loggedIn');
37+
cy.wait('@load');
38+
cy.get(page.circle).should('be.visible');
39+
cy.wait('@actOnRQ');
40+
cy.get(page.checkmark).should('be.visible');
41+
cy.get(page.text).invoke('text').should('eq', 'The request to join your Dojo has been refused.');
42+
});
43+
it('should show an error message when the status is not supported', () => {
44+
cy.route('/api/2.0/users/instance', 'fx:parentLoggedIn').as('loggedIn');
45+
cy.route('/api/3.0/dojos/d1/membership-requests/rq1', { id: 'rq1', dojoId: 'd1', userType: 'champion' }).as('load');
46+
cy.visit('/dashboard/dojos/d1/join-requests/rq1/status/banana');
47+
cy.wait('@loggedIn');
48+
cy.wait('@load');
49+
cy.get(page.checkmark).should('not.be.visible');
50+
cy.get(page.circle).should('not.be.visible');
51+
cy.get(page.text).invoke('text').should('eq', 'Invalid action, try again or contact support.');
52+
});
53+
it('should show an error message when the user is not allowed to load the membership request', () => {
54+
cy.route({ method: 'GET', url: '/api/3.0/dojos/d1/membership-requests/rq1', status: 401, response: {} }).as('load');
55+
cy.route('/api/2.0/users/instance', 'fx:parentLoggedIn').as('loggedIn');
56+
cy.visit('/dashboard/dojos/d1/join-requests/rq1/status/accept');
57+
cy.wait('@loggedIn');
58+
cy.wait('@load');
59+
cy.get(page.checkmark).should('not.be.visible');
60+
cy.get(page.circle).should('not.be.visible');
61+
cy.get(page.text).invoke('text').should('eq', 'Invalid action, try again or contact support.');
62+
});
63+
it('should show an error message when the user is already a member of the Dojo', () => {
64+
cy.route('/api/2.0/users/instance', 'fx:parentLoggedIn').as('loggedIn');
65+
cy.route('/api/3.0/dojos/d1/membership-requests/rq1', { id: 'rq1', dojoId: 'd1', userType: 'champion' }).as('load');
66+
cy.route({ method: 'PUT', url: '/api/3.0/dojos/d1/membership-requests/rq1', status: 400, response: {} }).as('actOnRQ');
67+
cy.visit('/dashboard/dojos/d1/join-requests/rq1/status/accept');
68+
cy.wait('@loggedIn');
69+
cy.wait('@load');
70+
cy.wait('@actOnRQ');
71+
cy.get(page.checkmark).should('not.be.visible');
72+
cy.get(page.circle).should('not.be.visible');
73+
cy.get(page.text).invoke('text').should('eq', 'Invalid action, try again or contact support.');
74+
cy.get(page.description).invoke('text').should('eq', 'This user is already part of your Dojo, please go to your Dojo\'s user management page to change the user\'s role.');
75+
});
76+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
// The actual checkmark is within an ::after, so we check if the circle has finished rotating
3+
checkmark: '.cd-manage-request-to-join .circle-loader--done',
4+
circle: '.cd-manage-request-to-join .circle-loader',
5+
text: '.cd-manage-request-to-join h1',
6+
description: '.cd-manage-request-to-join h2',
7+
};

index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ exports.register = function (server, options, next) {
8080
}
8181
});
8282

83+
server.route({
84+
method: 'GET',
85+
path: '/dashboard/dojos/{dojoId}/join-requests/{id}/status/{status}',
86+
handler :{
87+
file: {
88+
path: 'index.html'
89+
}
90+
}
91+
});
92+
8393
server.route({
8494
method: 'GET',
8595
path: '/login',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"dependencies": {
2323
"@coderdojo/cd-common": "1.1.11",
2424
"bootstrap": "^3.4.1",
25-
"cp-translations": "1.0.127",
25+
"cp-translations": "1.0.128",
2626
"font-awesome": "^4.7.0",
2727
"handlebars": "^4.1.0",
2828
"js-cookie": "^2.1.4",

src/dojos/manage-request-to-join.vue

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<template>
2+
<div class="cd-manage-request-to-join">
3+
<div class="circle-loader" :class="{ 'circle-loader--done': ready }" v-if="loaderIsVisible">
4+
<div class="checkmark draw" :class="{ 'checkmark--visible': ready }"></div>
5+
</div>
6+
<h1>{{ $t(text) }}</h1>
7+
<h2 v-if="errorText" v-html="$t(errorText, { openLink: `<a href='/dashboard/my-dojos/${membershipRequest.dojoId}/users'>`, closeLink: '</a>'})"></h2>
8+
</div>
9+
</template>
10+
<script>
11+
import DojosService from '@/dojos/service';
12+
13+
export default {
14+
name: 'manage-request-to-join',
15+
props: [],
16+
data() {
17+
return {
18+
status: null,
19+
dojoId: null,
20+
requestId: null,
21+
loaderIsVisible: true,
22+
errorText: '',
23+
membershipRequest: null,
24+
ready: false,
25+
};
26+
},
27+
computed: {
28+
userTypeString() {
29+
return this.membershipRequest.userType === 'mentor' ? 'The user is now a mentor for your Dojo. This means they can now book mentor tickets and check in users to your events.' : 'The user is now a champion for your Dojo. This means they can now create events, modify the Dojo page and award badges.';
30+
},
31+
text() {
32+
/* eslint-disable no-nested-ternary */
33+
return this.status === 'accept' && this.loaderIsVisible ?
34+
(this.ready ? this.userTypeString : 'Accepting the user...') :
35+
(this.status === 'refuse' && this.loaderIsVisible ?
36+
(this.ready ? 'The request to join your Dojo has been refused.' : 'Refusing this user from joining your Dojo...') :
37+
'Invalid action, try again or contact support.');
38+
/* eslint-enable no-nested-ternary */
39+
},
40+
},
41+
methods: {
42+
onError() {
43+
this.loaderIsVisible = false;
44+
},
45+
async loadMembershipRequest() {
46+
try {
47+
this.membershipRequest = (await DojosService.membership.loadPending(
48+
this.requestId,
49+
this.dojoId,
50+
)).body;
51+
} catch (e) {
52+
this.onError();
53+
}
54+
},
55+
async actOnMembershipRequest() {
56+
try {
57+
if (this.status === 'accept') {
58+
await DojosService.membership.accept(this.requestId, this.membershipRequest.dojoId);
59+
this.ready = true;
60+
} else if (this.status === 'refuse') {
61+
await DojosService.membership.refuse(this.requestId, this.membershipRequest.dojoId);
62+
this.ready = true;
63+
} else {
64+
// Unexpected scenario
65+
this.onError();
66+
}
67+
} catch (e) {
68+
// Conflict on the creation of the user: we recommend using another path
69+
if (e.status === 400) {
70+
this.errorText = 'This user is already part of your Dojo, please go to your {openLink}Dojo\'s user management page{closeLink} to change the user\'s role.';
71+
}
72+
this.onError();
73+
}
74+
},
75+
},
76+
async created() {
77+
Object.assign(this, this.$route.params);
78+
await this.loadMembershipRequest();
79+
if (this.membershipRequest) {
80+
await this.actOnMembershipRequest();
81+
} else {
82+
// membership not found
83+
this.onError();
84+
}
85+
},
86+
};
87+
</script>
88+
<style lang="less" scoped>
89+
.cd-manage-request-to-join {
90+
text-align: center;
91+
padding: 16px;
92+
}
93+
/* Credit to https://codepen.io/scottloway/pen/zqoLyQ */
94+
@brand-success: #5cb85c;
95+
@loader-size: 7em;
96+
@check-thickness: 0.5em;
97+
@loader-thickness: @check-thickness/2;
98+
@check-height: @loader-size/2;
99+
@check-width: @check-height/2;
100+
@check-left: (@loader-size/6 + @loader-size/12 - @check-thickness/2);
101+
@check-color: @brand-success;
102+
103+
.circle-loader {
104+
margin-bottom: @loader-size/2;
105+
border: 1px solid rgba(0, 0, 0, 0.2);
106+
border-left-color: @check-color;
107+
animation: loader-spin 1.2s infinite linear;
108+
position: relative;
109+
display: inline-block;
110+
vertical-align: top;
111+
border-radius: 50%;
112+
border-width: @loader-thickness;
113+
width: @loader-size;
114+
height: @loader-size;
115+
&--done {
116+
-webkit-animation: none;
117+
animation: none;
118+
border-color: @check-color;
119+
transition: border 500ms ease-out;
120+
}
121+
}
122+
123+
124+
.checkmark {
125+
display: none;
126+
&--visible {
127+
display: block;
128+
}
129+
&.draw:after {
130+
animation-duration: 800ms;
131+
animation-timing-function: ease;
132+
animation-name: checkmark;
133+
transform: scaleX(-1) rotate(135deg);
134+
}
135+
136+
&:after {
137+
opacity: 1;
138+
height: @check-height;
139+
width: @check-width;
140+
transform-origin: left top;
141+
border-right: @check-thickness solid @check-color;
142+
border-top: @check-thickness solid @check-color;
143+
content: '';
144+
left: @check-left;
145+
top: @check-height;
146+
position: absolute;
147+
}
148+
}
149+
150+
@keyframes loader-spin {
151+
0% {
152+
transform: rotate(0deg);
153+
}
154+
100% {
155+
transform: rotate(360deg);
156+
}
157+
}
158+
159+
@keyframes checkmark {
160+
0% {
161+
height: 0;
162+
width: 0;
163+
opacity: 1;
164+
}
165+
20% {
166+
height: 0;
167+
width: @check-width;
168+
opacity: 1;
169+
}
170+
40% {
171+
height: @check-height;
172+
width: @check-width;
173+
opacity: 1;
174+
}
175+
100% {
176+
height: @check-height;
177+
width: @check-width;
178+
opacity: 1;
179+
}
180+
}
181+
</style>

src/dojos/service.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,20 @@ const DojosService = {
7171
});
7272
},
7373
membership: {
74+
loadPending(requestId, dojoId) {
75+
return Vue.http.get(`${Vue.config.apiServer}/api/3.0/dojos/${dojoId}/membership-requests/${requestId}`);
76+
},
7477
request(dojoId, userType) {
75-
return Vue.http.post(`${Vue.config.apiServer}/api/3.0/dojos/${dojoId}/membership-request`, {
78+
return Vue.http.post(`${Vue.config.apiServer}/api/3.0/dojos/${dojoId}/membership-requests`, {
7679
userType,
7780
});
7881
},
82+
accept(requestId, dojoId) {
83+
return Vue.http.put(`${Vue.config.apiServer}/api/3.0/dojos/${dojoId}/membership-requests/${requestId}`);
84+
},
85+
refuse(requestId, dojoId) {
86+
return Vue.http.delete(`${Vue.config.apiServer}/api/3.0/dojos/${dojoId}/membership-requests/${requestId}`);
87+
},
7988
},
8089
lead: {
8190
list(userId) {

src/router/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import UserService from '@/users/service';
1414
import store from '@/store';
1515
import Home from '@/dashboard/cd-dashboard';
1616
import CDFManageUsers from '@/users/cdf-manage';
17+
import ManageRequestToJoin from '@/dojos/manage-request-to-join';
1718
import loggedInNavGuard from './loggedInNavGuard';
1819
import loggedInCDFNavGuard from './loggedInCDFNavGuard';
1920
import orderExistsNavGuard from './orderExistsNavGuard';
@@ -83,6 +84,12 @@ const router = new Router({
8384
component: UserTickets,
8485
beforeEnter: loggedInNavGuard,
8586
},
87+
{
88+
path: '/dashboard/dojos/:dojoId/join-requests/:requestId/status/:status',
89+
name: 'ManageRequestToJoin',
90+
component: ManageRequestToJoin,
91+
beforeEnter: loggedInNavGuard,
92+
},
8693
{
8794
path: '/login',
8895
name: 'Login',

0 commit comments

Comments
 (0)