зеркало из https://github.com/nextcloud/forms.git
feat: Add collapsible list view of submissions
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
This commit is contained in:
Родитель
87a5e47ca8
Коммит
e865815d21
|
@ -59,6 +59,7 @@ export default {
|
|||
|
||||
&__text {
|
||||
white-space: pre-line;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2023 Ferdinand Thiessen <rpm@fthiessen.de>
|
||||
-
|
||||
- @author Ferdinand Thiessen <rpm@fthiessen.de>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<NcListItem :title="submissionDateTime"
|
||||
:bold="false"
|
||||
:details="submissionAge"
|
||||
:link-aria-label="t('forms', 'Click to expand submission')"
|
||||
@click="onExpand">
|
||||
<template #icon>
|
||||
<NcAvatar v-if="!submission.userId.startsWith('anon-user-')"
|
||||
:size="44"
|
||||
:user="submission.userId"
|
||||
:display-name="submission.userDisplayName" />
|
||||
<IconAccountOff v-else :size="44" />
|
||||
</template>
|
||||
<template #subtitle>
|
||||
{{ submission.userDisplayName }}
|
||||
</template>
|
||||
<template #extra>
|
||||
<div v-if="expanded" class="submission">
|
||||
<Answer v-for="question in answeredQuestions"
|
||||
:key="question.id"
|
||||
:answer-text="question.squashedAnswers"
|
||||
:question-text="question.text" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!viewed" #indicator>
|
||||
<IconCheckboxBlankCircle :size="14" fill-color="var(--color-primary)" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<NcActionButton v-if="canDeleteSubmission" @click="onDelete">
|
||||
<template #icon>
|
||||
<IconDelete :size="20" />
|
||||
</template>
|
||||
{{ t('forms', 'Delete this response') }}
|
||||
</NcActionButton>
|
||||
</template>
|
||||
</NcListItem>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Answer from './Answer.vue'
|
||||
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||
import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'
|
||||
import moment from '@nextcloud/moment'
|
||||
import IconAccountOff from 'vue-material-design-icons/AccountOff.vue'
|
||||
import IconCheckboxBlankCircle from 'vue-material-design-icons/CheckboxBlankCircle.vue'
|
||||
import IconDelete from 'vue-material-design-icons/Delete.vue'
|
||||
|
||||
export default {
|
||||
name: 'SubmissionItem',
|
||||
|
||||
components: {
|
||||
Answer,
|
||||
IconAccountOff,
|
||||
IconCheckboxBlankCircle,
|
||||
IconDelete,
|
||||
NcActionButton,
|
||||
NcAvatar,
|
||||
NcListItem,
|
||||
},
|
||||
|
||||
props: {
|
||||
submission: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
questions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
canDeleteSubmission: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
viewed: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Format submission-timestamp to DateTime
|
||||
submissionDateTime() {
|
||||
return moment(this.submission.timestamp, 'X').format('LLLL')
|
||||
},
|
||||
|
||||
/**
|
||||
* Age of the submission, e.g. '11 hours' or '1 year'
|
||||
*/
|
||||
submissionAge() {
|
||||
return moment(this.submission.timestamp, 'X').fromNow(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* Join answered Questions with corresponding answers.
|
||||
* Multiple answers to a question are squashed into one string.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
answeredQuestions() {
|
||||
const answeredQuestionsArray = []
|
||||
|
||||
this.questions.forEach(question => {
|
||||
const answers = this.submission.answers.filter(answer => answer.questionId === question.id)
|
||||
if (!answers.length) {
|
||||
return // no answers, go to next question
|
||||
}
|
||||
const squashedAnswers = answers.map(answer => answer.text).join('; ')
|
||||
|
||||
answeredQuestionsArray.push({
|
||||
id: question.id,
|
||||
text: question.text,
|
||||
squashedAnswers,
|
||||
})
|
||||
})
|
||||
return answeredQuestionsArray
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onDelete() {
|
||||
this.$emit('delete')
|
||||
},
|
||||
|
||||
onExpand() {
|
||||
this.expanded = !this.expanded
|
||||
this.viewed = true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.session {
|
||||
padding-left: 1em;
|
||||
}
|
||||
</style>
|
|
@ -44,23 +44,33 @@
|
|||
<div class="response-actions">
|
||||
<div class="response-actions__radio">
|
||||
<input id="show-summary--true"
|
||||
v-model="showSummary"
|
||||
v-model="viewMode"
|
||||
value="summary"
|
||||
type="radio"
|
||||
:value="true"
|
||||
class="hidden">
|
||||
<label for="show-summary--true"
|
||||
class="response-actions__radio__item"
|
||||
:class="{ 'response-actions__radio__item--active': showSummary }">
|
||||
:class="{ 'response-actions__radio__item--active': viewMode === 'summary' }">
|
||||
{{ t('forms', 'Summary') }}
|
||||
</label>
|
||||
<input id="show-summary--false"
|
||||
v-model="showSummary"
|
||||
<input id="show-list"
|
||||
v-model="viewMode"
|
||||
value="list"
|
||||
type="radio"
|
||||
class="hidden">
|
||||
<label for="show-list"
|
||||
class="response-actions__radio__item"
|
||||
:class="{ 'response-actions__radio__item--active': viewMode === 'list' }">
|
||||
{{ t('forms', 'List') }}
|
||||
</label>
|
||||
<input id="show-summary--false"
|
||||
v-model="viewMode"
|
||||
value="items"
|
||||
type="radio"
|
||||
:value="false"
|
||||
class="hidden">
|
||||
<label for="show-summary--false"
|
||||
class="response-actions__radio__item"
|
||||
:class="{ 'response-actions__radio__item--active': !showSummary }">
|
||||
:class="{ 'response-actions__radio__item--active': viewMode === 'items' }">
|
||||
{{ t('forms', 'Responses') }}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -110,15 +120,27 @@
|
|||
</section>
|
||||
|
||||
<!-- Summary view for visualization -->
|
||||
<section v-if="!noSubmissions && showSummary">
|
||||
<section v-else-if="viewMode === 'summary'">
|
||||
<ResultsSummary v-for="question in form.questions"
|
||||
:key="question.id"
|
||||
:question="question"
|
||||
:submissions="form.submissions" />
|
||||
</section>
|
||||
|
||||
<!-- Responses view for individual responses using list style -->
|
||||
<section v-else-if="viewMode === 'list'">
|
||||
<ul>
|
||||
<SubmissionItem v-for="submission in form.submissions"
|
||||
:key="submission.id"
|
||||
:submission="submission"
|
||||
:questions="form.questions"
|
||||
:can-delete-submission="canDeleteSubmissions"
|
||||
@delete="deleteSubmission(submission.id)" />
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Responses view for individual responses -->
|
||||
<section v-if="!noSubmissions && !showSummary">
|
||||
<section v-else>
|
||||
<Submission v-for="submission in form.submissions"
|
||||
:key="submission.id"
|
||||
:submission="submission"
|
||||
|
@ -151,6 +173,7 @@ import IconShareVariant from 'vue-material-design-icons/ShareVariant.vue'
|
|||
|
||||
import ResultsSummary from '../components/Results/ResultsSummary.vue'
|
||||
import Submission from '../components/Results/Submission.vue'
|
||||
import SubmissionItem from '../components/Results/SubmissionItem.vue'
|
||||
import TopBar from '../components/TopBar.vue'
|
||||
import ViewsMixin from '../mixins/ViewsMixin.js'
|
||||
import answerTypes from '../models/AnswerTypes.js'
|
||||
|
@ -183,6 +206,7 @@ export default {
|
|||
NcLoadingIcon,
|
||||
ResultsSummary,
|
||||
Submission,
|
||||
SubmissionItem,
|
||||
TopBar,
|
||||
},
|
||||
|
||||
|
@ -191,7 +215,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
loadingResults: true,
|
||||
showSummary: true,
|
||||
viewMode: 'summary',
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -368,20 +392,17 @@ export default {
|
|||
margin-right: 8px;
|
||||
|
||||
&__item {
|
||||
border-radius: var(--border-radius-pill);
|
||||
padding: 8px 16px;
|
||||
font-weight: bold;
|
||||
background-color: var(--color-background-dark);
|
||||
|
||||
&:first-of-type {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-radius: var(--border-radius-pill) 0 0 var(--border-radius-pill);
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-radius: 0 var(--border-radius-pill) var(--border-radius-pill) 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче