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 …') }} {{ t('forms', 'Loading forms …') }}
</EmptyContent> </EmptyContent>
<EmptyContent v-else-if="noForms"> <EmptyContent v-else-if="noForms">
{{ t('forms', 'No forms in here') }} {{ t('forms', 'No forms created yet') }}
<template #desc> <template #desc>
<button class="primary" @click="onNewForm"> <button class="primary" @click="onNewForm">
{{ t('forms', 'Create a new one') }} {{ t('forms', 'Create a form') }}
</button> </button>
</template> </template>
</EmptyContent> </EmptyContent>
<EmptyContent v-else> <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> </EmptyContent>
</AppContent> </AppContent>

View file

@ -38,9 +38,9 @@
</ActionLink> </ActionLink>
<ActionRouter :close-after-click="true" <ActionRouter :close-after-click="true"
:exact="true" :exact="true"
icon="icon-forms" icon="icon-comment"
:to="{ name: 'results', params: { hash: form.hash } }"> :to="{ name: 'results', params: { hash: form.hash } }">
{{ t('forms', 'Show results') }} {{ t('forms', 'Responses') }}
</ActionRouter> </ActionRouter>
<!-- <ActionRouter :close-after-click="true" <!-- <ActionRouter :close-after-click="true"
:exact="true" :exact="true"
@ -131,7 +131,7 @@ export default {
? t('forms', 'Form link copied') ? t('forms', 'Form link copied')
: t('forms', 'Cannot copy, please copy the link manually') : 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; justify-content: stretch;
margin-bottom: 22px; margin-bottom: 22px;
padding-left: 44px; padding-left: 44px;
// room for the new question menu
padding-right: 44px;
user-select: none; user-select: none;
background-color: var(--color-main-background); background-color: var(--color-main-background);
@ -169,7 +167,15 @@ export default {
left: 0; left: 0;
width: 44px; width: 44px;
height: 100%; height: 100%;
opacity: .5;
&:hover,
&:focus {
opacity: 1;
}
cursor: grab; cursor: grab;
&:active { &:active {
cursor: grabbing; cursor: grabbing;
} }
@ -179,7 +185,7 @@ export default {
&__content { &__content {
flex: 1 1 100%; flex: 1 1 100%;
max-width: 100%; max-width: 100%;
margin: 20px; margin-bottom: 20px;
padding: 0; padding: 0;
} }
@ -189,8 +195,7 @@ export default {
flex: 1 1 100%; flex: 1 1 100%;
justify-content: space-between; justify-content: space-between;
width: auto; width: auto;
margin: 20px; margin-top: 20px;
margin-bottom: 0;
// Using type to have a higher order than the input styling of server // Using type to have a higher order than the input styling of server
&-title, &-title,
@ -205,6 +210,7 @@ export default {
border-bottom: 1px dotted transparent; border-bottom: 1px dotted transparent;
border-radius: 0; border-radius: 0;
font-size: 16px; font-size: 16px;
font-weight: bold;
line-height: 22px; line-height: 22px;
} }

View file

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

View file

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

View file

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

View file

@ -23,17 +23,43 @@
--> -->
<template> <template>
<AppContent> <AppContent v-if="loadingResults">
<div> <EmptyContent icon="icon-loading">
<button class="button btn primary" @click="download"> {{ t('forms', 'Loading responses …') }}
<span>{{ "Export to CSV" }}</span> </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> </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 <transition-group
name="list" name="list"
tag="div" tag="div"
@ -41,41 +67,44 @@
<ResultItem <ResultItem
key="0" key="0"
:header="true" /> :header="true" />
<li <ResultItem
is="resultItem" v-for="answer in answers"
v-for="(answer, index) in answers"
:key="answer.id" :key="answer.id"
:answer="answer" :answer="answer" />
@viewResults="viewFormResults(index, form.form, 'results')" />
</transition-group> </transition-group>
<modal-dialog /> </section>
</div>
</AppContent> </AppContent>
</template> </template>
<script> <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 ResultItem from '../components/resultItem'
import json2csvParser from 'json2csv' 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 { export default {
name: 'Results', name: 'Results',
components: { components: {
AppContent, AppContent,
EmptyContent,
ResultItem, ResultItem,
TopBar,
}, },
mixins: [ViewsMixin], mixins: [ViewsMixin],
data() { data() {
return { return {
loading: true, loadingResults: true,
answers: [], answers: [],
} }
}, },
@ -111,41 +140,46 @@ export default {
return sums.sort() return sums.sort()
}, },
noSubmissions() {
return this.answers && this.answers.length === 0
},
}, },
created() { beforeMount() {
this.indexPage = OC.generateUrl('apps/forms/') this.loadFormResults()
this.loadForms()
}, },
methods: { methods: {
loadForms() {
this.loading = true showEdit() {
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) {
this.$router.push({ this.$router.push({
name: name, name: 'edit',
params: { 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 this.loading = true
axios.get(OC.generateUrl('apps/forms/get/form/' + this.$route.params.hash)) axios.get(OC.generateUrl('apps/forms/get/form/' + this.$route.params.hash))
.then((response) => { .then((response) => {
@ -178,8 +212,7 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.table { .table {
width: 100%; width: 100%;
margin-top: 45px; margin-top: 45px;
@ -189,12 +222,7 @@ export default {
flex-wrap: nowrap; flex-wrap: nowrap;
} }
#emptycontent { #exportButton {
.icon-forms { width: max-content;
background-color: black;
-webkit-mask: url('./img/app.svg') no-repeat 50% 50%;
mask: url('./img/app.svg') no-repeat 50% 50%;
}
} }
</style> </style>

View file

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