Merge pull request #1659 from nextcloud/feat/custom-submission-message-385

feat: Implement custom submission message
This commit is contained in:
Chartman123 2023-10-16 22:21:58 +02:00 коммит произвёл GitHub
Родитель 3f639a6ea2 fed3d4c11e
Коммит ef0bdcd9f6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 223 добавлений и 21 удалений

Просмотреть файл

@ -120,6 +120,7 @@ Returns the full-depth object of the requested form (without submissions).
"title": "Form 1",
"description": "Description Text",
"ownerId": "jonas",
"submissionMessage": "Thank **you** for submitting the form."
"created": 1611240961,
"access": {
"permitAllUsers": false,

Просмотреть файл

@ -1,24 +1,25 @@
# Forms Data Structure
**State: Forms v3.0.0 - 23.03.2022**
**State: Forms v3.3.1 - 08.10.2023**
This document describes the Object-Structure, that is used within the Forms App and on Forms API v2. It does partially **not** equal the actual database structure behind.
## Data Structures
### Form
| Property | Type | Restrictions | Description |
|-------------|-----------------|--------------|-------------|
| id | Integer | unique | An instance-wide unique id of the form |
| hash | 16-char String | unique | An instance-wide unique hash |
| title | String | max. 256 ch. | The form title |
| Property | Type | Restrictions | Description |
|-------------|-----------------|---------------|-------------|
| id | Integer | unique | An instance-wide unique id of the form |
| hash | 16-char String | unique | An instance-wide unique hash |
| title | String | max. 256 ch. | The form title |
| description | String | max. 8192 ch. | The Form description |
| ownerId | String | | The nextcloud userId of the form owner |
| created | unix timestamp | | When the form has been created |
| ownerId | String | | The nextcloud userId of the form owner |
| submissionMessage | String | max. 2048 ch. | Optional custom message, with Markdown support, to be shown to users when the form is submitted (default is used if set to null) |
| created | unix timestamp | | When the form has been created |
| access | [Access-Object](#access-object) | | Describing access-settings of the form |
| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ |
| isAnonymous | Boolean | | If Answers will be stored anonymously |
| submitMultiple | Boolean | | If users are allowed to submit multiple times to the form |
| showExpiration | Boolean | | If the expiration date will be shown on the form |
| canSubmit | Boolean | | If the user can Submit to the form, i.e. calculated information out of `submitMultiple` and existing submissions. |
| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ |
| isAnonymous | Boolean | | If Answers will be stored anonymously |
| submitMultiple | Boolean | | If users are allowed to submit multiple times to the form |
| showExpiration | Boolean | | If the expiration date will be shown on the form |
| canSubmit | Boolean | | If the user can Submit to the form, i.e. calculated information out of `submitMultiple` and existing submissions. |
| permissions | Array of [Permissions](#permissions) | Array of permissions regarding the form |
| questions | Array of [Questions](#question) | | Array of questions belonging to the form |
| shares | Array of [Shares](#share) | | Array of shares of the form |

Просмотреть файл

@ -46,6 +46,7 @@ class Constants {
public const MAX_STRING_LENGTHS = [
'formTitle' => 256,
'formDescription' => 8192,
'submissionMessage' => 2048,
'questionText' => 2048,
'questionDescription' => 4096,
'optionText' => 1024,

Просмотреть файл

@ -339,8 +339,10 @@ class ApiController extends OCSController {
}
// Create FormEntity with given Params & Id.
$form = Form::fromParams($keyValuePairs);
$form->setId($id);
foreach ($keyValuePairs as $key => $value) {
$method = 'set' . ucfirst($key);
$form->$method($value);
}
// Update changed Columns in Db.
$this->formMapper->update($form);

Просмотреть файл

@ -52,6 +52,8 @@ use OCP\AppFramework\Db\Entity;
* @method void setShowExpiration(bool $value)
* @method integer getLastUpdated()
* @method void setLastUpdated(integer $value)
* @method ?string getSubmissionMessage()
* @method void setSubmissionMessage(?string $value)
*/
class Form extends Entity {
protected $hash;
@ -64,6 +66,7 @@ class Form extends Entity {
protected $isAnonymous;
protected $submitMultiple;
protected $showExpiration;
protected $submissionMessage;
protected $lastUpdated;
/**
@ -102,7 +105,8 @@ class Form extends Entity {
'isAnonymous' => (bool)$this->getIsAnonymous(),
'submitMultiple' => (bool)$this->getSubmitMultiple(),
'showExpiration' => (bool)$this->getShowExpiration(),
'lastUpdated' => (int)$this->getLastUpdated()
'lastUpdated' => (int)$this->getLastUpdated(),
'submissionMessage' => $this->getSubmissionMessage(),
];
}
}

Просмотреть файл

@ -0,0 +1,61 @@
<?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 Version030400Date20230628011500 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_forms');
if (!$table->hasColumn('submission_message')) {
$table->addColumn('submission_message', Types::STRING, [
'notnull' => false,
'default' => null,
'length' => 2048,
'comment' => 'custom thank you message',
]);
return $schema;
}
return null;
}
}

Просмотреть файл

@ -59,6 +59,35 @@
{{ t('forms', 'Show expiration date on form') }}
</NcCheckboxRadioSwitch>
</div>
<NcCheckboxRadioSwitch :checked="hasCustomSubmissionMessage"
type="switch"
@update:checked="onUpdateHasCustomSubmissionMessage">
{{ t('forms', 'Custom submission message') }}
</NcCheckboxRadioSwitch>
<div v-show="hasCustomSubmissionMessage"
class="settings-div--indent submission-message"
:tabindex="editMessage ? undefined : '0'"
@focus="editMessage = true">
<textarea v-if="editMessage || !form.submissionMessage"
v-click-outside="() => { editMessage = false }"
aria-describedby="forms-submission-message-description"
:aria-label="t('forms', 'Custom submission message')"
:value="form.submissionMessage"
:maxlength="maxStringLengths.submissionMessage"
:placeholder="t('forms', 'Message to show after a user submitted the form (formatting using Markdown is supported)')"
class="submission-message__input"
@blur="editMessage = false"
@change="onSubmissionMessageChange" />
<!-- eslint-disable vue/no-v-html -->
<div v-else
:aria-label="t('forms', 'Custom submission message')"
class="submission-message__output"
v-html="submissionMessageHTML" />
<!-- eslint-enable vue/no-v-html -->
<div id="forms-submission-message-description" class="submission-message__description">
{{ t('forms', 'Message to show after a user submitted the form. Please note that the message will not be translated!') }}
</div>
</div>
</div>
</template>
@ -68,14 +97,23 @@ import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadi
import NcDatetimePicker from '@nextcloud/vue/dist/Components/NcDatetimePicker.js'
import ShareTypes from '../../mixins/ShareTypes.js'
import { directive as ClickOutside } from 'v-click-outside'
import { loadState } from '@nextcloud/initial-state'
export default {
components: {
NcCheckboxRadioSwitch,
NcDatetimePicker,
},
directives: {
ClickOutside,
},
mixins: [ShareTypes],
inject: ['$markdownit'],
props: {
form: {
type: Object,
@ -89,10 +127,20 @@ export default {
stringify: this.stringifyDate,
parse: this.parseTimestampToDate,
},
maxStringLengths: loadState('forms', 'maxStringLengths'),
/** If custom submission message is shown as input or rendered markdown */
editMessage: false,
}
},
computed: {
/**
* If the form has a custom submission message or the user wants to add one (settings switch)
*/
hasCustomSubmissionMessage() {
return this.form?.submissionMessage !== undefined && this.form?.submissionMessage !== null
},
/**
* Submit Multiple is disabled, if it cannot be controlled.
*/
@ -125,6 +173,12 @@ export default {
expirationDate() {
return moment(this.form.expires, 'X').toDate()
},
/**
* The submission message rendered as HTML
*/
submissionMessageHTML() {
return this.$markdownit.render(this.form.submissionMessage || '')
},
},
methods: {
@ -159,6 +213,22 @@ export default {
this.$emit('update:formProp', 'expires', parseInt(moment(datetime).format('X')))
},
onSubmissionMessageChange({ target }) {
this.$emit('update:formProp', 'submissionMessage', target.value)
},
/**
* Enable or disable the whole custom submission message
* Disabled means the value is set to null.
*/
onUpdateHasCustomSubmissionMessage() {
if (this.hasCustomSubmissionMessage) {
this.$emit('update:formProp', 'submissionMessage', null)
} else {
this.$emit('update:formProp', 'submissionMessage', '')
}
},
/**
* Datepicker timestamp to string
*
@ -215,4 +285,32 @@ export default {
.settings-div--indent {
margin-inline-start: 40px;
}
.submission-message {
&__description {
color: var(--color-text-maxcontrast);
font-size: 13px;
}
&__input, &__output {
width: 100%;
min-height: 100px;
line-height: 24px;
}
&__output {
@import '../../scssmixins/markdownOutput';
padding: 12px;
margin-block: 3px;
border: 2px solid var(--color-border-maxcontrast);
border-radius: var(--border-radius-large);
&:hover {
border-color: var(--color-primary-element);
}
@include markdown-output;
}
}
</style>

Просмотреть файл

@ -24,7 +24,7 @@
@mixin markdown-output {
overflow-wrap: break-word;
::v-deep {
:deep() {
>:not(:first-child) {
margin-block-start: 1.5em;
}
@ -37,6 +37,11 @@
color: var(--color-main-text);
}
a {
color: var(--color-primary-element);
text-decoration: underline;
}
blockquote {
padding-inline-start: 1em;
border-inline-start: 4px solid var(--color-primary-element);

Просмотреть файл

@ -64,10 +64,15 @@
</template>
</NcEmptyContent>
<NcEmptyContent v-else-if="success || !form.canSubmit"
:title="t('forms', 'Thank you for completing the form!')">
:title="t('forms', 'Thank you for completing the form!')"
:description="form.submissionMessage">
<template #icon>
<IconCheck :size="64" />
</template>
<template v-if="submissionMessageHTML" #description>
<!-- eslint-disable-next-line vue/no-v-html -->
<p class="submission-message" v-html="submissionMessageHTML" />
</template>
</NcEmptyContent>
<NcEmptyContent v-else-if="isExpired"
:title="t('forms', 'Form expired')"
@ -237,6 +242,16 @@ export default {
return message
},
/**
* Rendered HTML of the custom submission message
*/
submissionMessageHTML() {
if (this.form.submissionMessage && (this.success || !this.form.canSubmit)) {
return this.markdownit.render(this.form.submissionMessage)
}
return ''
},
expirationMessage() {
const relativeDate = moment(this.form.expires, 'X').fromNow()
if (this.isExpired) {
@ -424,6 +439,11 @@ export default {
}
}
.submission-message {
@include markdown-output;
text-align: center;
}
form {
.question {
// Less padding needed as submit view does not have drag handles

Просмотреть файл

@ -64,6 +64,7 @@ class ApiV2Test extends TestCase {
'submit_multiple' => false,
'show_expiration' => false,
'last_updated' => 123456789,
'submission_message' => 'Back to website',
'questions' => [
[
'type' => 'short',
@ -166,6 +167,7 @@ class ApiV2Test extends TestCase {
'submit_multiple' => false,
'show_expiration' => false,
'last_updated' => 123456789,
'submission_message' => '',
'questions' => [
[
'type' => 'short',
@ -214,6 +216,7 @@ class ApiV2Test extends TestCase {
'submit_multiple' => $qb->createNamedParameter($form['submit_multiple'], IQueryBuilder::PARAM_BOOL),
'show_expiration' => $qb->createNamedParameter($form['show_expiration'], IQueryBuilder::PARAM_BOOL),
'last_updated' => $qb->createNamedParameter($form['last_updated'], IQueryBuilder::PARAM_INT),
'submission_message' => $qb->createNamedParameter($form['submission_message'], IQueryBuilder::PARAM_STR),
]);
$qb->executeStatement();
$formId = $qb->getLastInsertId();
@ -471,6 +474,7 @@ class ApiV2Test extends TestCase {
'questions' => [],
'shares' => [],
'submissionCount' => 0,
'submissionMessage' => ''
]
]
];
@ -523,6 +527,7 @@ class ApiV2Test extends TestCase {
'lastUpdated' => 123456789,
'canSubmit' => true,
'permissions' => Constants::PERMISSION_ALL,
'submissionMessage' => 'Back to website',
'questions' => [
[
'type' => 'short',

Просмотреть файл

@ -354,7 +354,8 @@ class ApiControllerTest extends TestCase {
'isAnonymous' => false,
'submitMultiple' => false,
'showExpiration' => false,
'lastUpdated' => 123456789
'lastUpdated' => 123456789,
'submissionMessage' => '',
]]
];
}

Просмотреть файл

@ -114,7 +114,7 @@ class FormsMigratorTest extends TestCase {
public function dataExport() {
return [
'exactlyOneOfEach' => [
'expectedJson' => '[{"title":"Link","description":"","created":1646251830,"access":{"permitAllUsers":false,"showToAllUsers":false},"expires":0,"isAnonymous":false,"submitMultiple":false,"showExpiration":false,"lastUpdated":123456789,"questions":[{"id":14,"order":2,"type":"multiple","isRequired":false,"text":"checkbox","description":"huhu","extraSettings":{},"options":[{"text":"ans1"}]}],"submissions":[{"userId":"anyUser@localhost","timestamp":1651354059,"answers":[{"questionId":14,"text":"ans1"}]}]}]'
'expectedJson' => '[{"title":"Link","description":"","created":1646251830,"access":{"permitAllUsers":false,"showToAllUsers":false},"expires":0,"isAnonymous":false,"submitMultiple":false,"showExpiration":false,"lastUpdated":123456789,"submissionMessage":"Back to website","questions":[{"id":14,"order":2,"type":"multiple","isRequired":false,"text":"checkbox","description":"huhu","extraSettings":{},"options":[{"text":"ans1"}]}],"submissions":[{"userId":"anyUser@localhost","timestamp":1651354059,"answers":[{"questionId":14,"text":"ans1"}]}]}]'
]
];
}
@ -151,6 +151,7 @@ class FormsMigratorTest extends TestCase {
$form->setSubmitMultiple(false);
$form->setShowExpiration(false);
$form->setLastUpdated(123456789);
$form->setSubmissionMessage('Back to website');
$this->formsService->expects($this->once())
->method('getQuestions')

Просмотреть файл

@ -212,6 +212,7 @@ class FormsServiceTest extends TestCase {
'displayName' => 'Some User'
]
],
'submissionMessage' => '',
'permissions' => Constants::PERMISSION_ALL
]]
];
@ -417,7 +418,8 @@ class FormsServiceTest extends TestCase {
'questions' => [],
'permissions' => [
'submit'
]
],
'submissionMessage' => '',
]]
];
}