Skip to content

Commit 16d1d34

Browse files
Merge pull request #52 from nafundi/forms
Form stories
2 parents e3e3942 + 07cdb72 commit 16d1d34

File tree

9 files changed

+423
-127
lines changed

9 files changed

+423
-127
lines changed

lib/components/form/list.vue

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,33 +34,40 @@ except according to the terms contained in the LICENSE file.
3434
<table v-else id="form-list-table" class="table table-hover">
3535
<thead>
3636
<tr>
37-
<th>Form ID</th>
37+
<th>Name</th>
38+
<th>Created by</th>
3839
<th>Last Modified</th>
3940
<th>Last Submission</th>
4041
</tr>
4142
</thead>
4243
<tbody>
43-
<router-link v-for="form of forms" :key="form.xmlFormId"
44-
:to="`/forms/${form.xmlFormId}`" tag="tr"
45-
:class="highlight(form, 'xmlFormId')">
44+
<tr v-for="form of forms" :key="form.xmlFormId">
4645
<td>
47-
<div>{{ form.xmlFormId }}</div>
48-
<!-- TODO: Not yet implemented. -->
49-
<div>??? submissions</div>
46+
<div>
47+
<router-link :to="`/forms/${form.xmlFormId}/submissions`"
48+
class="form-list-form-name">
49+
{{ form.name || form.xmlFormId }}
50+
</router-link>
51+
</div>
52+
<div v-if="form.name != null" class="form-list-form-id">
53+
{{ form.xmlFormId }}
54+
</div>
55+
<div>{{ submissions(form) }}</div>
56+
</td>
57+
<td>
58+
{{ form.createdBy != null ? form.createdBy.displayName : '' }}
5059
</td>
5160
<td>
5261
{{ updatedAt(form) }}
5362
</td>
54-
<!-- TODO: Not yet implemented. -->
5563
<td>
56-
???
64+
{{ lastSubmission(form) }}
5765
</td>
58-
</router-link>
66+
</tr>
5967
</tbody>
6068
</table>
6169
</template>
62-
<form-new v-bind="newForm" @hide="newForm.state = false"
63-
@create="afterCreate"/>
70+
<form-new v-bind="newForm" @hide="newForm.state = false"/>
6471
</page-body>
6572
</div>
6673
</template>
@@ -70,19 +77,17 @@ import moment from 'moment';
7077

7178
import FormNew from './new.vue';
7279
import alert from '../../mixins/alert';
73-
import highlight from '../../mixins/highlight';
7480
import request from '../../mixins/request';
7581

7682
export default {
7783
name: 'FormList',
7884
components: { FormNew },
79-
mixins: [alert(), request(), highlight()],
85+
mixins: [alert(), request()],
8086
data() {
8187
return {
8288
alert: alert.blank(),
8389
requestId: null,
8490
forms: null,
85-
highlighted: null,
8691
newForm: {
8792
state: false
8893
}
@@ -94,42 +99,44 @@ export default {
9499
methods: {
95100
fetchData() {
96101
this.forms = null;
102+
const headers = { 'X-Extended-Metadata': 'true' };
97103
this
98-
.get('/forms')
104+
.get('/forms', { headers })
99105
.then(forms => {
100106
this.forms = forms;
101107
})
102108
.catch(() => {});
103109
},
110+
submissions(form) {
111+
const count = form.submissions.toLocaleString();
112+
const s = form.submissions !== 1 ? 's' : '';
113+
return `${count} submission${s}`;
114+
},
104115
updatedAt(form) {
105116
const updatedAt = form.updatedAt != null ? form.updatedAt : form.createdAt;
106-
return moment.utc(updatedAt).fromNow();
117+
return moment(updatedAt).fromNow();
107118
},
108-
afterCreate(form) {
109-
this.fetchData();
110-
this.alert = alert.success(`Form ${form.xmlFormId} was created successfully.`);
111-
this.highlighted = form.xmlFormId;
119+
lastSubmission(form) {
120+
const { lastSubmission } = form;
121+
return lastSubmission != null ? moment(lastSubmission).fromNow() : '';
112122
}
113123
}
114124
};
115125
</script>
116126

117127
<style lang="sass">
118128
#form-list-table {
119-
& > thead > tr > th:nth-child(n+2),
120-
& > tbody > tr > td:nth-child(n+2) {
121-
width: 200px;
122-
}
123-
124-
& > tbody {
125-
cursor: pointer;
129+
tbody td {
130+
vertical-align: middle;
126131

127-
& > tr > td {
128-
vertical-align: middle;
132+
.form-list-form-name {
133+
color: unset;
134+
font-size: 30px;
135+
text-decoration: unset;
136+
}
129137

130-
&:first-child > div:first-child {
131-
font-size: 30px;
132-
}
138+
.form-list-form-id {
139+
font-size: 18px;
133140
}
134141
}
135142
}

lib/components/form/new.vue

Lines changed: 145 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,40 @@ including this file, may be copied, modified, propagated, or distributed
1010
except according to the terms contained in the LICENSE file.
1111
-->
1212
<template>
13-
<modal :state="state" @hide="$emit('hide')" backdrop>
13+
<modal ref="modal" :state="state" @hide="hide" backdrop>
1414
<template slot="title">Create Form</template>
1515
<template slot="body">
1616
<alert v-bind="alert" @close="alert.state = false"/>
17-
<form @submit.prevent="submit">
18-
<div class="form-group">
19-
<label for="form-new-xml">Form XML *</label>
20-
<textarea v-model="xml" id="form-new-xml" class="form-control"
21-
required :disabled="awaitingResponse" rows="10">
22-
</textarea>
17+
<div class="modal-introduction">
18+
<p>To create a form, upload an XForms XML file.</p>
19+
<p>
20+
If you don’t already have one, there are
21+
<doc-link>tools available</doc-link> to help you design your form.
22+
</p>
23+
</div>
24+
<div id="drop-zone" :class="dropZoneClass" @dragover.prevent="dragover"
25+
@dragleave="dragleave" @drop.prevent="drop">
26+
<div :style="pointerEvents">
27+
Drop a file here, or
28+
<input type="file" ref="input" class="hidden"
29+
@change="readFile($event.target.files)">
30+
<button type="button" class="btn btn-primary" :disabled="!droppable"
31+
@click="clickFileButton">
32+
choose one
33+
</button>
34+
to upload.
2335
</div>
24-
<button type="submit" class="btn btn-primary" :disabled="awaitingResponse">
36+
<div id="form-new-filename" :style="pointerEvents">{{ filename }}</div>
37+
</div>
38+
<div class="modal-actions">
39+
<button type="button" class="btn btn-primary"
40+
:disabled="awaitingResponse" @click="create">
2541
Create <spinner :state="awaitingResponse"/>
2642
</button>
27-
<button type="button" class="btn btn-default" @click="$emit('hide')">
43+
<button type="button" class="btn btn-link" @click="hide">
2844
Close
2945
</button>
30-
</form>
46+
</div>
3147
</template>
3248
</modal>
3349
</template>
@@ -36,6 +52,8 @@ except according to the terms contained in the LICENSE file.
3652
import alert from '../../mixins/alert';
3753
import request from '../../mixins/request';
3854

55+
const NO_FILE_MESSAGE = 'Please choose a file.';
56+
3957
export default {
4058
name: 'FormNew',
4159
mixins: [alert(), request()],
@@ -49,22 +67,134 @@ export default {
4967
return {
5068
alert: alert.blank(),
5169
requestId: null,
52-
xml: ''
70+
// true if a dragged file is over the drop zone and false if not.
71+
isOverDropZone: false,
72+
reading: false,
73+
filename: '',
74+
xml: null
5375
};
5476
},
77+
computed: {
78+
droppable() {
79+
return !(this.reading || this.awaitingResponse);
80+
},
81+
dropZoneClass() {
82+
return {
83+
'text-center': true,
84+
droppable: this.droppable,
85+
dragover: this.isOverDropZone
86+
};
87+
},
88+
// Used to prevent child elements of #drop-zone from triggering dragleave
89+
// events upon hover. Does not work for IE9 or 10.
90+
pointerEvents() {
91+
return this.isOverDropZone ? { 'pointer-events': 'none' } : {};
92+
}
93+
},
5594
methods: {
56-
submit() {
95+
hide() {
96+
this.$emit('hide');
97+
if (this.alert.type === 'info' && this.alert.message === NO_FILE_MESSAGE)
98+
this.alert = alert.blank();
99+
},
100+
readFile(files) {
101+
if (files.length === 0) return;
102+
const file = files[0];
103+
const reader = new FileReader();
104+
reader.onloadstart = () => {
105+
this.reading = true;
106+
};
107+
reader.onload = (event) => {
108+
this.alert = alert.blank();
109+
this.filename = file.name;
110+
this.xml = event.target.result;
111+
};
112+
reader.onerror = () => {
113+
this.alert = alert.danger('Something went wrong while reading the file.');
114+
this.filename = '';
115+
this.xml = null;
116+
};
117+
reader.onabort = request.onerror;
118+
reader.onloadend = () => {
119+
this.$refs.input.value = '';
120+
this.reading = false;
121+
};
122+
reader.readAsText(file);
123+
},
124+
clickFileButton() {
125+
this.$refs.input.click();
126+
// Focus the modal rather than blurring the button so that the escape key
127+
// still hides the modal.
128+
this.$refs.modal.$el.focus();
129+
},
130+
dragover(event) {
131+
// Putting this line in a dragenter listener did not work. Is it possible
132+
// that a child element of #drop-zone could trigger a dragleave event
133+
// before the dragenter listener is called?
134+
this.isOverDropZone = true;
135+
// eslint-disable-next-line no-param-reassign
136+
if (this.droppable) event.dataTransfer.dropEffect = 'copy';
137+
},
138+
dragleave() {
139+
this.isOverDropZone = false;
140+
},
141+
drop(event) {
142+
if (this.droppable) this.readFile(event.dataTransfer.files);
143+
this.isOverDropZone = false;
144+
},
145+
checkStateBeforeRequest() {
146+
if (this.xml == null) {
147+
this.alert = alert.info(NO_FILE_MESSAGE);
148+
return false;
149+
} else if (this.reading) {
150+
this.alert = alert.info('The file is still being processed.');
151+
return false;
152+
}
153+
return true;
154+
},
155+
create() {
156+
if (!this.checkStateBeforeRequest()) return;
57157
const headers = { 'Content-Type': 'application/xml' };
58158
this
59159
.post('/forms', this.xml, { headers })
60160
.then(form => {
61161
this.$emit('hide');
62-
this.alert = alert.blank();
63-
this.xml = '';
64-
this.$emit('create', form);
162+
// Wait for the modal to hide.
163+
this.$nextTick(() => {
164+
const name = form.name || form.xmlFormId;
165+
this.$alert = alert.success(`The form “${name}” was created successfully.`);
166+
this.$router.push(`/forms/${form.xmlFormId}/submissions`);
167+
});
65168
})
66169
.catch(() => {});
67170
}
68171
}
69172
};
70173
</script>
174+
175+
<style lang="sass">
176+
@import '../../../assets/scss/variables';
177+
178+
#drop-zone {
179+
background-color: $color-input-background;
180+
border: 1px dashed $color-subpanel-border;
181+
182+
&.dragover {
183+
opacity: 0.65;
184+
}
185+
186+
&:not(.droppable) {
187+
cursor: not-allowed;
188+
opacity: 0.65;
189+
}
190+
191+
div {
192+
margin-bottom: 5px;
193+
margin-top: 5px;
194+
}
195+
196+
#form-new-filename {
197+
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
198+
}
199+
}
200+
</style>

lib/components/form/overview.vue

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!--
2+
Copyright 2017 Super Adventure Developers
3+
See the NOTICE file at the top-level directory of this distribution and at
4+
https://github.com/nafundi/super-adventure/blob/master/NOTICE.
5+
6+
This file is part of Super Adventure. It is subject to the license terms in
7+
the LICENSE file found in the top-level directory of this distribution and at
8+
https://www.apache.org/licenses/LICENSE-2.0. No part of Super Adventure,
9+
including this file, may be copied, modified, propagated, or distributed
10+
except according to the terms contained in the LICENSE file.
11+
-->
12+
<template>
13+
<p>Not yet implemented.</p>
14+
</template>
15+
16+
<script>
17+
export default {
18+
name: 'FormOverview',
19+
props: {
20+
form: {
21+
type: Object,
22+
required: true
23+
}
24+
}
25+
};
26+
</script>

lib/components/form/settings.vue

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!--
2+
Copyright 2017 Super Adventure Developers
3+
See the NOTICE file at the top-level directory of this distribution and at
4+
https://github.com/nafundi/super-adventure/blob/master/NOTICE.
5+
6+
This file is part of Super Adventure. It is subject to the license terms in
7+
the LICENSE file found in the top-level directory of this distribution and at
8+
https://www.apache.org/licenses/LICENSE-2.0. No part of Super Adventure,
9+
including this file, may be copied, modified, propagated, or distributed
10+
except according to the terms contained in the LICENSE file.
11+
-->
12+
<template>
13+
<p>Not yet implemented.</p>
14+
</template>
15+
16+
<script>
17+
export default {
18+
name: 'FormSettings',
19+
props: {
20+
form: {
21+
type: Object,
22+
required: true
23+
}
24+
}
25+
};
26+
</script>

0 commit comments

Comments
 (0)