Skip to content

Commit

Permalink
Merge pull request #1553 from nextcloud/feat/technical-names
Browse files Browse the repository at this point in the history
Add technical identifiers for questions
  • Loading branch information
Chartman123 committed Jun 27, 2023
2 parents 8e53ceb + 46dfde2 commit 1be02c1
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 14 deletions.
3 changes: 3 additions & 0 deletions docs/API.md
Expand Up @@ -140,6 +140,7 @@ Returns the full-depth object of the requested form (without submissions).
"type": "dropdown",
"isRequired": false,
"text": "Question 1",
"name": "something",
"options": [
{
"id": 1,
Expand All @@ -160,6 +161,7 @@ Returns the full-depth object of the requested form (without submissions).
"type": "short",
"isRequired": true,
"text": "Question 2",
"name": "something_other",
"options": []
}
],
Expand Down Expand Up @@ -242,6 +244,7 @@ Contains only manipulative question-endpoints. To retrieve questions, request th
"order": 3,
"type": "short",
"isRequired": false,
"name": "",
"text": "",
"options": []
}
Expand Down
3 changes: 2 additions & 1 deletion docs/DataStructure.md
Expand Up @@ -58,16 +58,17 @@ This document describes the Object-Structure, that is used within the Forms App
| type | [Question-Type](#question-types) | | Type of the question |
| isRequired | Boolean | | If the question is required to fill the form |
| text | String | max. 2048 ch. | The question-text |
| name | String | | Technical identifier of the question, e.g. used as HTML name attribute |
| options | Array of [Options](#option) | | Array of options belonging to the question. Only relevant for question-type with predefined options. |
```
{
"id": 1,
"formId": 3,
"order": 1,
"type": "dropdown",
"mandatory": false, // deprecated, will be removed in API v2
"isRequired": false,
"text": "Question 1",
"name": "firstname",
"options": []
}
```
Expand Down
5 changes: 5 additions & 0 deletions lib/Db/Question.php
Expand Up @@ -46,6 +46,8 @@
* @method void setText(string $value)
* @method string getDescription()
* @method void setDescription(string $value)
* @method string getName()
* @method void setName(string $value)
* @method object getExtraSettings()
* @method void setExtraSettings(object $value)
*/
Expand All @@ -55,6 +57,7 @@ class Question extends Entity {
protected $type;
protected $isRequired;
protected $text;
protected $name;
protected $description;
protected $extraSettingsJson;

Expand All @@ -65,6 +68,7 @@ public function __construct() {
$this->addType('isRequired', 'bool');
$this->addType('text', 'string');
$this->addType('description', 'string');
$this->addType('name', 'string');
}

public function getExtraSettings(): object {
Expand All @@ -87,6 +91,7 @@ public function read(): array {
'type' => $this->getType(),
'isRequired' => (bool)$this->getIsRequired(),
'text' => (string)$this->getText(),
'name' => (string)$this->getName(),
'description' => (string)$this->getDescription(),
'extraSettings' => $this->getExtraSettings(),
];
Expand Down
60 changes: 60 additions & 0 deletions lib/Migration/Version030200Date20230307141800.php
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <rpm@fthiessen.de>
*
* @author Ferdinand Thiessen <rpm@fthiessen.de>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/

namespace OCA\Forms\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version030200Date20230307141800 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('forms_v2_questions');

if (!$table->hasColumn('name')) {
$table->addColumn('name', Types::STRING, [
'notnull' => false,
'default' => '',
'comment' => 'technical-identifier',
]);

return $schema;
}

return null;
}
}
27 changes: 26 additions & 1 deletion src/components/Questions/Question.vue
Expand Up @@ -48,7 +48,10 @@
:maxlength="maxStringLengths.questionText"
required
@input="onTitleChange">
<h3 v-else class="question__header__title__text" v-text="computedText" />
<h3 v-else
:id="titleId"
class="question__header__title__text"
v-text="computedText" />
<div v-if="!edit && !questionValid"
v-tooltip.auto="warningInvalid"
class="question__header__title__warning"
Expand All @@ -67,6 +70,12 @@
{{ t('forms', 'Required') }}
</NcActionCheckbox>
<slot name="actions" />
<NcActionInput :value="name" :label="t('forms', 'Technical name of the question')" @input="onNameChange">
<template #icon>
<IconIdentifier :size="20" />
</template>
{{ t('forms', 'Technical name') }}
</NcActionInput>
<NcActionButton @click="onDelete">
<template #icon>
<IconDelete :size="20" />
Expand Down Expand Up @@ -99,10 +108,12 @@ import { directive as ClickOutside } from 'v-click-outside'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js'
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
import IconDelete from 'vue-material-design-icons/Delete.vue'
import IconDragHorizontalVariant from 'vue-material-design-icons/DragHorizontalVariant.vue'
import IconIdentifier from 'vue-material-design-icons/Identifier.vue'
export default {
name: 'Question',
Expand All @@ -115,9 +126,11 @@ export default {
IconAlertCircleOutline,
IconDelete,
IconDragHorizontalVariant,
IconIdentifier,
NcActions,
NcActionButton,
NcActionCheckbox,
NcActionInput,
},
inject: ['$markdownit'],
Expand Down Expand Up @@ -159,6 +172,10 @@ export default {
type: Object,
required: true,
},
name: {
type: String,
default: '',
},
contentValid: {
type: Boolean,
default: true,
Expand Down Expand Up @@ -199,6 +216,10 @@ export default {
return 'q' + this.index + '_actions'
},
titleId() {
return 'q' + this.index + '_title'
},
hasDescription() {
return this.description !== ''
},
Expand All @@ -224,6 +245,10 @@ export default {
this.$emit('update:description', target.value)
},
onNameChange({ target }) {
this.$emit('update:name', target.value)
},
onRequiredChange(isRequired) {
this.$emit('update:isRequired', isRequired)
},
Expand Down
3 changes: 3 additions & 0 deletions src/components/Questions/QuestionDate.vue
Expand Up @@ -23,6 +23,7 @@
<template>
<Question v-bind.sync="$attrs"
:text="text"
:name="name"
:description="description"
:is-required="isRequired"
:edit.sync="edit"
Expand All @@ -33,6 +34,7 @@
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
<div class="question__content">
<NcDatetimePicker v-model="time"
Expand Down Expand Up @@ -88,6 +90,7 @@ export default {
inputAttr() {
return {
required: this.isRequired,
name: this.name || undefined,
}
},
},
Expand Down
4 changes: 3 additions & 1 deletion src/components/Questions/QuestionDropdown.vue
Expand Up @@ -23,6 +23,7 @@
<template>
<Question v-bind.sync="$attrs"
:text="text"
:name="name"
:description="description"
:is-required="isRequired"
:edit.sync="edit"
Expand All @@ -35,6 +36,7 @@
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
<template #actions>
<NcActionCheckbox :checked="extraSettings?.shuffleOptions"
Expand All @@ -44,7 +46,7 @@
</template>
<NcSelect v-if="!edit"
v-model="selectedOption"
:name="text"
:name="name || undefined"
:placeholder="selectOptionPlaceholder"
:multiple="isMultiple"
:required="isRequired"
Expand Down
3 changes: 3 additions & 0 deletions src/components/Questions/QuestionLong.vue
Expand Up @@ -23,6 +23,7 @@
<template>
<Question v-bind.sync="$attrs"
:text="text"
:name="name"
:description="description"
:is-required="isRequired"
:edit.sync="edit"
Expand All @@ -33,6 +34,7 @@
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
<div class="question__content">
<textarea ref="textarea"
Expand All @@ -44,6 +46,7 @@
class="question__text"
:maxlength="maxStringLengths.answerText"
minlength="1"
:name="name || undefined"
@input="onInput"
@keypress="autoSizeText"
@keydown.ctrl.enter="onKeydownCtrlEnter" />
Expand Down
31 changes: 20 additions & 11 deletions src/components/Questions/QuestionMultiple.vue
Expand Up @@ -23,6 +23,7 @@
<template>
<Question v-bind.sync="$attrs"
:text="text"
:name="name"
:description="description"
:is-required="isRequired"
:edit.sync="edit"
Expand All @@ -35,6 +36,7 @@
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
<template #actions>
<NcActionCheckbox :checked="extraSettings?.shuffleOptions"
Expand All @@ -43,17 +45,19 @@
</NcActionCheckbox>
</template>
<template v-if="!edit">
<NcCheckboxRadioSwitch v-for="(answer) in sortedOptions"
:key="answer.id"
:checked.sync="questionValues"
:value="answer.id.toString()"
:name="`${id}-answer`"
:type="isUnique ? 'radio' : 'checkbox'"
:required="checkRequired(answer.id)"
@update:checked="onChange"
@keydown.enter.exact.prevent="onKeydownEnter">
{{ answer.text }}
</NcCheckboxRadioSwitch>
<fieldset :name="name || undefined" :aria-labelledby="titleId">
<NcCheckboxRadioSwitch v-for="(answer) in sortedOptions"
:key="answer.id"
:checked.sync="questionValues"
:value="answer.id.toString()"
:name="`${id}-answer`"
:type="isUnique ? 'radio' : 'checkbox'"
:required="checkRequired(answer.id)"
@update:checked="onChange"
@keydown.enter.exact.prevent="onKeydownEnter">
{{ answer.text }}
</NcCheckboxRadioSwitch>
</fieldset>
</template>
<template v-else>
Expand Down Expand Up @@ -148,9 +152,14 @@ export default {
shiftDragHandle() {
return this.edit && this.options.length !== 0 && !this.isLastEmpty
},
pseudoIcon() {
return this.isUnique ? IconRadioboxBlank : IconCheckboxBlankOutline
},
titleId() {
return `q${this.$attrs.index}_title`
},
},
watch: {
Expand Down
3 changes: 3 additions & 0 deletions src/components/Questions/QuestionShort.vue
Expand Up @@ -23,6 +23,7 @@
<template>
<Question v-bind.sync="$attrs"
:text="text"
:name="name"
:description="description"
:is-required="isRequired"
:edit.sync="edit"
Expand All @@ -33,6 +34,7 @@
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
<div class="question__content">
<input ref="input"
Expand All @@ -44,6 +46,7 @@
class="question__input"
:maxlength="maxStringLengths.answerText"
minlength="1"
:name="name || undefined"
type="text"
@input="onInput"
@keydown.enter.exact.prevent="onKeydownEnter">
Expand Down

0 comments on commit 1be02c1

Please sign in to comment.