Merge pull request #297 from nextcloud/design/remove-forms-emptycontent

Further design improvements after review
This commit is contained in:
Jan-Christoph Borchardt 2020-04-28 00:22:13 +02:00 committed by GitHub
commit aa9fa028e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 233 additions and 195 deletions

View file

@ -37,16 +37,21 @@
{{ t('forms', 'Loading forms …') }}
</EmptyContent>
<EmptyContent v-else-if="noForms">
{{ t('forms', 'No forms in here') }}
{{ t('forms', 'No forms created yet') }}
<template #desc>
<button class="primary" @click="onNewForm">
{{ t('forms', 'Create a new one') }}
{{ t('forms', 'Create a form') }}
</button>
</template>
</EmptyContent>
<EmptyContent v-else>
{{ t('forms', 'Please select a form') }}
{{ t('forms', 'Select a form or create a new one') }}
<template #desc>
<button class="primary" @click="onNewForm">
{{ t('forms', 'Create new form') }}
</button>
</template>
</EmptyContent>
</AppContent>

View file

@ -38,9 +38,9 @@
</ActionLink>
<ActionRouter :close-after-click="true"
:exact="true"
icon="icon-forms"
icon="icon-comment"
:to="{ name: 'results', params: { hash: form.hash } }">
{{ t('forms', 'Show results') }}
{{ t('forms', 'Responses') }}
</ActionRouter>
<!-- <ActionRouter :close-after-click="true"
:exact="true"
@ -131,7 +131,7 @@ export default {
? t('forms', 'Form link copied')
: t('forms', 'Cannot copy, please copy the link manually')
}
return t('forms', 'Copy to clipboard')
return t('forms', 'Copy share link')
},
},

View file

@ -155,8 +155,6 @@ export default {
justify-content: stretch;
margin-bottom: 22px;
padding-left: 44px;
// room for the new question menu
padding-right: 44px;
user-select: none;
background-color: var(--color-main-background);
@ -169,7 +167,15 @@ export default {
left: 0;
width: 44px;
height: 100%;
opacity: .5;
&:hover,
&:focus {
opacity: 1;
}
cursor: grab;
&:active {
cursor: grabbing;
}
@ -179,7 +185,7 @@ export default {
&__content {
flex: 1 1 100%;
max-width: 100%;
margin: 20px;
margin-bottom: 20px;
padding: 0;
}
@ -189,8 +195,7 @@ export default {
flex: 1 1 100%;
justify-content: space-between;
width: auto;
margin: 20px;
margin-bottom: 0;
margin-top: 20px;
// Using type to have a higher order than the input styling of server
&-title,
@ -205,6 +210,7 @@ export default {
border-bottom: 1px dotted transparent;
border-radius: 0;
font-size: 16px;
font-weight: bold;
line-height: 22px;
}

View file

@ -51,7 +51,7 @@ $top-bar-height: 60px;
button {
cursor: pointer;
&:not(.primary) {
&:not(:first-child) {
width: 44px;
height: 44px;
border: none;

View file

@ -21,9 +21,7 @@
-->
<template>
<div>
<h2> {{ t('forms', 'Share with') }}</h2>
<div class="sharing">
<Multiselect id="ajax"
v-model="shares"
:options="users"
@ -151,7 +149,11 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.sharing {
margin: 8px 8px 8px 36px;
}
.shared-list {
display: flex;
flex-wrap: wrap;

View file

@ -29,21 +29,21 @@
<template>
<AppContent v-if="isLoadingForm">
<EmptyContent icon="icon-loading">
{{ t('forms', 'Loading form “{title}”', { title: form.title }) }}
{{ t('forms', 'Loading {title} …', { title: form.title }) }}
</EmptyContent>
</AppContent>
<AppContent v-else>
<!-- Show results & sidebar button -->
<TopBar>
<button class="primary" @click="showResults">
<span class="icon-forms-white" role="img" />
{{ t('forms', 'Show results') }}
<button @click="showResults">
<span class="icon-comment" role="img" />
{{ t('forms', 'Responses') }}
</button>
<button v-tooltip="t('forms', 'Toggle settings')"
:aria-label="t('forms', 'Toggle settings')"
@click="toggleSidebar">
<span class="icon-settings" role="img" />
<span class="icon-menu-sidebar" role="img" />
</button>
</TopBar>
@ -91,21 +91,11 @@
</Actions>
</div>
<!-- No questions -->
<EmptyContent v-if="hasQuestions">
{{ t('forms', 'This form does not have any questions') }}
<template #desc>
<button class="empty-content__button primary" @click="openQuestionMenu">
<span class="icon-add-white" />
{{ t('forms', 'Add a new one') }}
</button>
</template>
</EmptyContent>
<!-- Questions list -->
<Draggable v-model="form.questions"
:animation="200"
tag="ul"
handle=".question__drag-handle"
@change="onQuestionOrderChange"
@start="isDragging = true"
@end="isDragging = false">
@ -382,7 +372,7 @@ export default {
header,
section {
width: 100%;
max-width: 900px;
max-width: 750px;
}
// Title & description header
@ -394,19 +384,22 @@ export default {
#form-title,
#form-desc {
width: 100%;
margin: 10px; // aerate the header
padding: 0; // makes alignment and desc height calc easier
margin: 16px 0; // aerate the header
padding: 0 16px;
border: none;
}
#form-title {
font-size: 2em;
font-weight: bold;
padding-left: 14px; // align with description (compensate font size diff)
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#form-desc {
// make sure height calculations are correct
box-sizing: content-box !important;
min-height: 60px;
max-height: 200px;
padding-left: 2px; // align with title (compensate font size diff)
margin-top: 0;
resize: none;
}
}
@ -434,7 +427,7 @@ export default {
top: var(--header-height);
display: flex;
align-items: center;
align-self: flex-end;
align-self: flex-start;
width: 44px;
height: var(--top-bar-height);
// make sure this doesn't take any space and appear floating

View file

@ -23,17 +23,43 @@
-->
<template>
<AppContent>
<div>
<button class="button btn primary" @click="download">
<span>{{ "Export to CSV" }}</span>
<AppContent v-if="loadingResults">
<EmptyContent icon="icon-loading">
{{ t('forms', 'Loading responses …') }}
</EmptyContent>
</AppContent>
<AppContent v-else>
<TopBar>
<button @click="showEdit">
<span class="icon-forms" role="img" />
{{ t('forms', 'Back to form') }}
</button>
</TopBar>
<header v-if="!noSubmissions">
<h2>{{ t('forms', 'Responses for {title}', { title: form.title }) }}</h2>
<div v-for="sum in stats" :key="sum">
{{ sum }}
</div>
</header>
<!-- No submissions -->
<section v-if="noSubmissions">
<EmptyContent icon="icon-comment">
{{ t('forms', 'No responses yet') }}
<template #desc>
{{ t('forms', 'Results of submitted forms will show up here') }}
</template>
<!-- Button to copy Share-Link? -->
</EmptyContent>
</section>
<section v-else>
<button id="exportButton" class="primary" @click="download">
<span class="icon-download-white" role="img" />
{{ t('forms', 'Export to CSV') }}
</button>
</div>
<h1>{{ "Statistics" }}</h1>
<div v-for="sum in stats" :key="sum">
{{ sum }}
</div>
<div id="app-content" :class="{'icon-loading': loading}">
<transition-group
name="list"
tag="div"
@ -41,41 +67,44 @@
<ResultItem
key="0"
:header="true" />
<li
is="resultItem"
v-for="(answer, index) in answers"
<ResultItem
v-for="answer in answers"
:key="answer.id"
:answer="answer"
@viewResults="viewFormResults(index, form.form, 'results')" />
:answer="answer" />
</transition-group>
<modal-dialog />
</div>
</section>
</AppContent>
</template>
<script>
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import axios from '@nextcloud/axios'
import EmptyContent from '../components/EmptyContent'
import TopBar from '../components/TopBar'
import ViewsMixin from '../mixins/ViewsMixin'
import ResultItem from '../components/resultItem'
import json2csvParser from 'json2csv'
import axios from '@nextcloud/axios'
import ViewsMixin from '../mixins/ViewsMixin'
import { generateUrl } from '@nextcloud/router'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
export default {
name: 'Results',
components: {
AppContent,
EmptyContent,
ResultItem,
TopBar,
},
mixins: [ViewsMixin],
data() {
return {
loading: true,
loadingResults: true,
answers: [],
}
},
@ -111,41 +140,46 @@ export default {
return sums.sort()
},
noSubmissions() {
return this.answers && this.answers.length === 0
},
},
created() {
this.indexPage = OC.generateUrl('apps/forms/')
this.loadForms()
beforeMount() {
this.loadFormResults()
},
methods: {
loadForms() {
this.loading = true
axios.get(generateUrl('apps/forms/api/v1/submissions/{hash}', { hash: this.$route.params.hash }))
.then((response) => {
if (response.data == null) {
this.answers = null
OC.Notification.showTemporary('Access Denied')
} else {
this.answers = response.data
}
this.loading = false
}, (error) => {
/* eslint-disable-next-line no-console */
console.log(error.response)
this.loading = false
})
},
viewFormResults(index, form, name) {
showEdit() {
this.$router.push({
name: name,
name: 'edit',
params: {
hash: form.id,
hash: this.form.hash,
},
})
},
download() {
async loadFormResults() {
this.loadingResults = true
console.debug('Loading Results')
try {
const response = await axios.get(generateUrl('/apps/forms/api/v1/submissions/{hash}', {
hash: this.form.hash,
}))
this.answers = response.data
console.debug(this.answers)
} catch (error) {
console.error(error)
showError(t('forms', 'There was an error while loading results'))
} finally {
this.loadingResults = false
}
},
download() {
this.loading = true
axios.get(OC.generateUrl('apps/forms/get/form/' + this.$route.params.hash))
.then((response) => {
@ -178,8 +212,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.table {
width: 100%;
margin-top: 45px;
@ -189,12 +222,7 @@ export default {
flex-wrap: nowrap;
}
#emptycontent {
.icon-forms {
background-color: black;
-webkit-mask: url('./img/app.svg') no-repeat 50% 50%;
mask: url('./img/app.svg') no-repeat 50% 50%;
}
#exportButton {
width: max-content;
}
</style>

View file

@ -22,92 +22,94 @@
<template>
<AppSidebar v-show="opened" :title="form.title" @close="onClose">
<div class="configBox ">
<label class="title icon-settings">
{{ t('forms', 'Form configurations') }}
</label>
<h3>{{ t('forms', 'Settings') }}</h3>
<ul>
<li>
<input id="isAnonymous"
v-model="form.isAnonymous"
<input id="isAnonymous"
v-model="form.isAnonymous"
type="checkbox"
class="checkbox"
@change="onAnonChange">
<label for="isAnonymous">
{{ t('forms', 'Anonymous responses') }}
</label>
</li>
<li>
<input id="submitOnce"
v-model="form.submitOnce"
:disabled="form.access.type === 'public' || form.isAnonymous"
type="checkbox"
class="checkbox"
@change="onSubmOnceChange">
<label for="submitOnce">
{{ t('forms', 'Only allow one response per user') }}
</label>
</li>
<li>
<input id="expires"
v-model="formExpires"
type="checkbox"
class="checkbox">
<label for="expires">
{{ t('forms', 'Set expiration date') }}
</label>
<DatetimePicker v-show="formExpires"
id="expiresDatetimePicker"
v-model="form.expires"
v-bind="expirationDatePicker"
@change="onExpiresChange" />
</li>
</ul>
type="checkbox"
class="checkbox"
@change="onAnonChange">
<label for="isAnonymous" class="title">
{{ t('forms', 'Anonymous form') }}
</label>
<input id="submitOnce"
v-model="form.submitOnce"
:disabled="form.access.type === 'public' || form.isAnonymous"
type="checkbox"
class="checkbox"
@change="onSubmOnceChange">
<label for="submitOnce" class="title">
<span>{{ t('forms', 'Only allow one submission per user') }}</span>
</label>
<input id="expires"
v-model="formExpires"
type="checkbox"
class="checkbox">
<label class="title" for="expires">
{{ t('forms', 'Expires') }}
</label>
<DatetimePicker v-show="formExpires"
id="expiresDatetimePicker"
v-model="form.expires"
v-bind="expirationDatePicker"
@change="onExpiresChange" />
</div>
<div class="configBox">
<label class="title icon-user">
{{ t('forms', 'Access') }}
</label>
<input id="registered"
v-model="form.access.type"
type="radio"
value="registered"
class="radio"
@change="onAccessChange">
<label for="registered" class="title">
<div class="title icon-group" />
<span>{{ t('forms', 'Registered users only') }}</span>
</label>
<input id="public"
v-model="form.access.type"
type="radio"
value="public"
class="radio"
@change="onAccessChange">
<label for="public" class="title">
<div class="title icon-link" />
<span>{{ t('forms', 'Public access') }}</span>
</label>
<input id="selected"
v-model="form.access.type"
type="radio"
value="selected"
class="radio"
@change="onAccessChange">
<label for="selected" class="title">
<div class="title icon-shared" />
<span>{{ t('forms', 'Only shared') }}</span>
</label>
</div>
<ShareDiv v-show="form.access.type === 'selected'"
:active-shares="form.shares"
:placeholder="t('forms', 'Name of user or group')"
:hide-names="true"
@update-shares="updateShares"
@remove-share="removeShare" />
<h3>{{ t('forms', 'Sharing') }}</h3>
<ul>
<li>
<input id="registered"
v-model="form.access.type"
type="radio"
value="registered"
class="radio"
@change="onAccessChange">
<label for="registered">
<span class="icon-group">
{{ t('forms', 'Show to all users of this instance') }}
</span>
</label>
</li>
<li>
<input id="public"
v-model="form.access.type"
type="radio"
value="public"
class="radio"
@change="onAccessChange">
<label for="public">
<span class="icon-link">
{{ t('forms', 'Share link') }}
</span>
</label>
</li>
<li>
<input id="selected"
v-model="form.access.type"
type="radio"
value="selected"
class="radio"
@change="onAccessChange">
<label for="selected">
<span class="icon-shared">
{{ t('forms', 'Choose users to share with') }}
</span>
</label>
<ShareDiv v-show="form.access.type === 'selected'"
:active-shares="form.shares"
:placeholder="t('forms', 'Name of user or group')"
:hide-names="true"
@update-shares="updateShares"
@remove-share="removeShare" />
</li>
</ul>
</AppSidebar>
</template>
@ -132,7 +134,7 @@ export default {
data() {
return {
opened: true,
opened: false,
lang: '',
locale: '',
longDateFormat: '',
@ -247,21 +249,22 @@ export default {
</script>
<style lang="scss" scoped>
h3 {
margin-left: 8px;
margin-bottom: 8px;
}
.configBox {
display: flex;
flex-direction: column;
padding: 8px;
& > * {
padding-left: 21px;
}
& > .title {
display: flex;
background-position: 0 2px;
padding-left: 24px;
margin-bottom: 4px;
& > span {
padding-left: 4px;
ul {
margin-bottom: 24px;
label {
padding: 8px;
display: block;
span[class^="icon-"],
span[class*=" icon-"] {
background-position: 4px;
padding-left: 24px;
}
}
}
@ -275,6 +278,7 @@ textarea {
}
#expiresDatetimePicker {
width: 170px;
left: 36px;
width: calc(100% - 44px);
}
</style>