Skip to content

Commit 93aad5d

Browse files
committed
feat: add color question type
Signed-off-by: Christian Hartmann <chris-hartmann@gmx.de>
1 parent 9de4ea5 commit 93aad5d

10 files changed

Lines changed: 229 additions & 20 deletions

File tree

lib/Constants.php

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,37 +66,39 @@ class Constants {
6666
*/
6767

6868
// Available AnswerTypes
69-
public const ANSWER_TYPE_MULTIPLE = 'multiple';
70-
public const ANSWER_TYPE_MULTIPLEUNIQUE = 'multiple_unique';
71-
public const ANSWER_TYPE_DROPDOWN = 'dropdown';
72-
public const ANSWER_TYPE_SHORT = 'short';
73-
public const ANSWER_TYPE_LONG = 'long';
69+
public const ANSWER_TYPE_COLOR = 'color';
7470
public const ANSWER_TYPE_DATE = 'date';
7571
public const ANSWER_TYPE_DATETIME = 'datetime';
76-
public const ANSWER_TYPE_TIME = 'time';
72+
public const ANSWER_TYPE_DROPDOWN = 'dropdown';
7773
public const ANSWER_TYPE_FILE = 'file';
7874
public const ANSWER_TYPE_LINEARSCALE = 'linearscale';
75+
public const ANSWER_TYPE_LONG = 'long';
76+
public const ANSWER_TYPE_MULTIPLE = 'multiple';
77+
public const ANSWER_TYPE_MULTIPLEUNIQUE = 'multiple_unique';
78+
public const ANSWER_TYPE_SHORT = 'short';
79+
public const ANSWER_TYPE_TIME = 'time';
7980

8081
// All AnswerTypes
8182
public const ANSWER_TYPES = [
82-
self::ANSWER_TYPE_MULTIPLE,
83-
self::ANSWER_TYPE_MULTIPLEUNIQUE,
84-
self::ANSWER_TYPE_DROPDOWN,
85-
self::ANSWER_TYPE_SHORT,
86-
self::ANSWER_TYPE_LONG,
83+
self::ANSWER_TYPE_COLOR,
8784
self::ANSWER_TYPE_DATE,
8885
self::ANSWER_TYPE_DATETIME,
89-
self::ANSWER_TYPE_TIME,
86+
self::ANSWER_TYPE_DROPDOWN,
9087
self::ANSWER_TYPE_FILE,
9188
self::ANSWER_TYPE_LINEARSCALE,
89+
self::ANSWER_TYPE_LONG,
90+
self::ANSWER_TYPE_MULTIPLE,
91+
self::ANSWER_TYPE_MULTIPLEUNIQUE,
92+
self::ANSWER_TYPE_SHORT,
93+
self::ANSWER_TYPE_TIME,
9294
];
9395

9496
// AnswerTypes, that need/have predefined Options
9597
public const ANSWER_TYPES_PREDEFINED = [
96-
self::ANSWER_TYPE_MULTIPLE,
97-
self::ANSWER_TYPE_MULTIPLEUNIQUE,
9898
self::ANSWER_TYPE_DROPDOWN,
9999
self::ANSWER_TYPE_LINEARSCALE,
100+
self::ANSWER_TYPE_MULTIPLE,
101+
self::ANSWER_TYPE_MULTIPLEUNIQUE,
100102
];
101103

102104
// AnswerTypes for date/time questions
@@ -194,10 +196,10 @@ class Constants {
194196
* !! Keep in sync with src/mixins/ShareTypes.js !!
195197
*/
196198
public const SHARE_TYPES_USED = [
197-
IShare::TYPE_USER,
199+
IShare::TYPE_CIRCLE,
198200
IShare::TYPE_GROUP,
199201
IShare::TYPE_LINK,
200-
IShare::TYPE_CIRCLE
202+
IShare::TYPE_USER,
201203
];
202204

203205
/**
@@ -214,18 +216,18 @@ class Constants {
214216

215217
public const PERMISSION_ALL = [
216218
self::PERMISSION_EDIT,
219+
self::PERMISSION_EMBED,
217220
self::PERMISSION_RESULTS,
218221
self::PERMISSION_RESULTS_DELETE,
219222
self::PERMISSION_SUBMIT,
220-
self::PERMISSION_EMBED,
221223
];
222224

223225
/**
224226
* !! Keep in sync with src/FormsEmptyContent.vue !!
225227
* InitialStates for emptyContent to render as...
226228
*/
227-
public const EMPTY_NOTFOUND = 'notfound';
228229
public const EMPTY_EXPIRED = 'expired';
230+
public const EMPTY_NOTFOUND = 'notfound';
229231

230232
/**
231233
* Constants related to extra settings for questions

lib/Service/SubmissionService.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,11 @@ public function validateSubmission(array $questions, array $answers, string $for
453453
throw new \InvalidArgumentException(sprintf('Invalid input for question "%s".', $question['text']));
454454
}
455455

456+
// Handle color questions
457+
if ($question['type'] === Constants::ANSWER_TYPE_COLOR && !preg_match('/^#[a-f0-9]{6}$/i', $answers[$questionId][0])) {
458+
throw new \InvalidArgumentException(sprintf('Invalid color string for question "%s".', $question['text']));
459+
}
460+
456461
// Handle file questions
457462
if ($question['type'] === Constants::ANSWER_TYPE_FILE) {
458463
$maxAllowedFilesCount = $question['extraSettings']['maxAllowedFilesCount'] ?? 0;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Christian Hartmann <chris-hartmann@gmx.de>
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<span
8+
:aria-hidden="!title"
9+
:aria-label="title"
10+
class="material-design-icon palette-icon"
11+
role="img"
12+
v-bind="$attrs"
13+
@click="$emit('click', $event)">
14+
<svg
15+
:fill="fillColor"
16+
class="material-design-icon__svg"
17+
:height="size"
18+
:width="size"
19+
viewBox="0 -960 960 960">
20+
<path
21+
d="M480-96q-78.72 0-148.8-30.24-70.08-30.24-122.4-82.56-52.32-52.32-82.56-122.4Q96-401.28 96-480q0-80 30.5-149.5t84-122Q264-804 335.5-834t152.75-30q77.39 0 146.07 27Q703-810 754-763t80.5 110Q864-590 864-518q0 96-67.08 163-67.09 67-162.92 67h-67.76q-8.24 0-14.24 5t-6 12.67Q546-255 561-245q15 10 15 53 0 37-27 66.5T480-96ZM264-444q25 0 42.5-17.5T324-504q0-25-17.5-42.5T264-564q-25 0-42.5 17.5T204-504q0 25 17.5 42.5T264-444Zm120-144q25 0 42.5-17.5T444-648q0-25-17.5-42.5T384-708q-25 0-42.5 17.5T324-648q0 25 17.5 42.5T384-588Zm192 0q25 0 42.5-17.5T636-648q0-25-17.5-42.5T576-708q-25 0-42.5 17.5T516-648q0 25 17.5 42.5T576-588Zm120 144q25 0 42.5-17.5T756-504q0-25-17.5-42.5T696-564q-25 0-42.5 17.5T636-504q0 25 17.5 42.5T696-444Z" />
22+
<title v-if="title">{{ title }}</title>
23+
</svg>
24+
</span>
25+
</template>
26+
27+
<script>
28+
export default {
29+
name: 'IconPalette',
30+
props: {
31+
title: {
32+
type: String,
33+
default: '',
34+
},
35+
fillColor: {
36+
type: String,
37+
default: 'currentColor',
38+
},
39+
size: {
40+
type: Number,
41+
default: 20,
42+
},
43+
},
44+
}
45+
</script>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<Question
8+
v-bind="questionProps"
9+
:title-placeholder="answerType.titlePlaceholder"
10+
:warning-invalid="answerType.warningInvalid"
11+
v-on="commonListeners">
12+
<div class="question__content">
13+
<NcColorPicker
14+
:model-value="pickedColor"
15+
advanced-fields
16+
@update:model-value="onUpdatePickedColor">
17+
<NcButton :disabled="!readOnly">{{
18+
colorPickerPlaceholder
19+
}}</NcButton>
20+
</NcColorPicker>
21+
<div :style="{ 'background-color': pickedColor }" class="color__field" />
22+
</div>
23+
</Question>
24+
</template>
25+
26+
<script>
27+
import NcButton from '@nextcloud/vue/components/NcButton'
28+
import NcColorPicker from '@nextcloud/vue/components/NcColorPicker'
29+
30+
import QuestionMixin from '../../mixins/QuestionMixin.js'
31+
32+
export default {
33+
name: 'QuestionColor',
34+
35+
components: {
36+
NcButton,
37+
NcColorPicker,
38+
},
39+
40+
mixins: [QuestionMixin],
41+
42+
data() {
43+
return {
44+
isLoading: false,
45+
}
46+
},
47+
48+
computed: {
49+
colorPickerPlaceholder() {
50+
return this.readOnly
51+
? this.answerType.submitPlaceholder
52+
: this.answerType.createPlaceholder
53+
},
54+
55+
pickedColor() {
56+
return this.values[0]
57+
},
58+
},
59+
60+
methods: {
61+
onUpdatePickedColor(color) {
62+
this.$emit('update:values', [color])
63+
},
64+
},
65+
}
66+
</script>
67+
68+
<style lang="scss" scoped>
69+
.question__content {
70+
display: flex;
71+
gap: var(--clickable-area-small);
72+
}
73+
74+
.color__field {
75+
width: 100px;
76+
height: var(--default-clickable-area);
77+
border-radius: var(--border-radius-element);
78+
}
79+
</style>

src/components/Results/Answer.vue

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
</a>
2323
</p>
2424
</template>
25+
<template v-else-if="questionType === 'color'">
26+
<div class="color__result">
27+
<NcHighlight :text="answerText" :search="highlight" />
28+
<div
29+
:style="{ 'background-color': answerText }"
30+
class="color__field" />
31+
</div>
32+
</template>
2533
<p v-else class="answer__text" dir="auto">
2634
<NcHighlight :text="answerText" :search="highlight" />
2735
</p>
@@ -54,6 +62,10 @@ export default {
5462
type: String,
5563
required: true,
5664
},
65+
questionType: {
66+
type: String,
67+
required: true,
68+
},
5769
highlight: {
5870
type: String,
5971
required: false,
@@ -81,5 +93,17 @@ export default {
8193
top: 4px;
8294
}
8395
}
96+
97+
.color__field {
98+
width: 100px;
99+
height: var(--default-clickable-area);
100+
border-radius: var(--border-radius-element);
101+
}
102+
103+
.color__result {
104+
align-items: center;
105+
display: flex;
106+
gap: var(--clickable-area-small);
107+
}
84108
}
85109
</style>

src/components/Results/ResultsSummary.vue

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@
4848
{{ answer.text }}
4949
</a>
5050
</template>
51+
<template v-else-if="question.type === 'color'">
52+
<div class="color__result">
53+
{{ answer.text }}
54+
<div
55+
v-if="answer.id !== 0"
56+
:style="{ 'background-color': answer.text }"
57+
class="color__field" />
58+
</div>
59+
</template>
5160
<template v-else>
5261
{{ answer.text }}
5362
</template>
@@ -368,5 +377,17 @@ export default {
368377
}
369378
}
370379
}
380+
381+
.color__field {
382+
width: 100px;
383+
height: var(--default-clickable-area);
384+
border-radius: var(--border-radius-element);
385+
}
386+
387+
.color__result {
388+
align-items: center;
389+
display: flex;
390+
gap: var(--clickable-area-small);
391+
}
371392
}
372393
</style>

src/components/Results/Submission.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
:highlight="highlight"
4040
:answer-text="question.squashedAnswers"
4141
:answers="question.answers"
42-
:question-text="question.text" />
42+
:question-text="question.text"
43+
:question-type="question.type" />
4344
</div>
4445
</template>
4546

@@ -120,6 +121,7 @@ export default {
120121
answeredQuestionsArray.push({
121122
id: question.id,
122123
text: question.text,
124+
type: question.type,
123125
answers: answers.map((answer) => {
124126
return {
125127
id: answer.id,
@@ -138,6 +140,7 @@ export default {
138140
answeredQuestionsArray.push({
139141
id: question.id,
140142
text: question.text,
143+
type: question.type,
141144
squashedAnswers,
142145
})
143146
} else {
@@ -148,6 +151,7 @@ export default {
148151
answeredQuestionsArray.push({
149152
id: question.id,
150153
text: question.text,
154+
type: question.type,
151155
squashedAnswers,
152156
})
153157
}

src/models/AnswerTypes.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import QuestionColor from '../components/Questions/QuestionColor.vue'
67
import QuestionDate from '../components/Questions/QuestionDate.vue'
78
import QuestionDropdown from '../components/Questions/QuestionDropdown.vue'
89
import QuestionFile from '../components/Questions/QuestionFile.vue'
@@ -17,6 +18,7 @@ import IconCheckboxOutline from 'vue-material-design-icons/CheckboxOutline.vue'
1718
import IconClockOutline from 'vue-material-design-icons/ClockOutline.vue'
1819
import IconFile from 'vue-material-design-icons/File.vue'
1920
import IconLinearScale from '../components/Icons/IconLinearScale.vue'
21+
import IconPalette from '../components/Icons/IconPalette.vue'
2022
import IconRadioboxMarked from 'vue-material-design-icons/RadioboxMarked.vue'
2123
import IconTextLong from 'vue-material-design-icons/TextLong.vue'
2224
import IconTextShort from 'vue-material-design-icons/TextShort.vue'
@@ -32,6 +34,7 @@ import IconTextShort from 'vue-material-design-icons/TextShort.vue'
3234
* @property {string} datetime Date and Time Answer
3335
* @property {string} time Time Answer
3436
* @property {string} linearscale Linear Scale Answer
37+
* @property {string} color Color Answer
3538
*/
3639
export default {
3740
/**
@@ -198,4 +201,16 @@ export default {
198201
titlePlaceholder: t('forms', 'Linear scale question title'),
199202
warningInvalid: t('forms', 'This question needs a title!'),
200203
},
204+
205+
color: {
206+
component: QuestionColor,
207+
icon: IconPalette,
208+
label: t('forms', 'Color'),
209+
predefined: false,
210+
211+
titlePlaceholder: t('forms', 'Color question title'),
212+
createPlaceholder: t('forms', 'People can pick a color'),
213+
submitPlaceholder: t('forms', 'Pick a color'),
214+
warningInvalid: t('forms', 'This question needs a title!'),
215+
},
201216
}

tests/Integration/Api/RespectAdminSettingsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ private static function sharedTestForms(): array {
128128
'submissionMessage' => '',
129129
'permissions' => [
130130
'edit',
131+
'embed',
131132
'results',
132133
'results_delete',
133134
'submit',
134-
'embed',
135135
],
136136
'canSubmit' => true,
137137
'submissionCount' => 0,

0 commit comments

Comments
 (0)