Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c1f8a93
Add `SelectOptionsFormField`
BurntimeX Apr 16, 2025
697a60e
Add additional row on enter
BurntimeX Apr 16, 2025
c06763d
Add drag and drop support
BurntimeX Apr 16, 2025
24140e5
Basic form option types
BurntimeX Apr 22, 2025
9c75f50
Use new form options for the contact form
BurntimeX Apr 22, 2025
0f2c108
Add form option formatters
BurntimeX Apr 22, 2025
621c7b1
Migrate contact form submit to command
BurntimeX Apr 22, 2025
ef7d4b1
Migrate contact form attachments to a pure file processor
BurntimeX Apr 25, 2025
d85d113
Remove obsolete code
BurntimeX Apr 25, 2025
996cdd3
Deprecate `ContactAttachmentObjectType`
BurntimeX Apr 25, 2025
8c531f0
Save the upload time of file uploads
BurntimeX Apr 25, 2025
7eb608c
Add cleanup for old contact form attachments
BurntimeX Apr 25, 2025
027a14e
Re-add spam check
BurntimeX Apr 28, 2025
92af395
Simplify contact form phrases
BurntimeX Apr 28, 2025
75a9df8
Deprecate custom options
BurntimeX Apr 28, 2025
a8017f7
Improve guest version of the contact form
BurntimeX Apr 28, 2025
b3a5f36
Add flood control for the contact form
BurntimeX Apr 28, 2025
0cd905b
Add additional form option types
BurntimeX Apr 28, 2025
2107cfa
Remove duplicate code
BurntimeX Apr 28, 2025
4046975
Add missing language variables
BurntimeX Apr 29, 2025
5789cb4
Add `AbstractFormOptionAddForm`
BurntimeX Apr 30, 2025
b230517
Add methods for list view filtering
BurntimeX Apr 30, 2025
78614b8
Fix sql syntax error
BurntimeX Apr 30, 2025
6e788ba
Fix SQL queries for creating the default options
BurntimeX May 1, 2025
047b331
Fix loading of a simple ckeditor
BurntimeX May 2, 2025
58f9721
Add "wysiwyg" form option type
BurntimeX May 2, 2025
7621048
Add script for the migration of existing contact options
BurntimeX May 2, 2025
3fd3594
Apply suggestions from code review
BurntimeX May 6, 2025
78c8dc1
Apply suggestions from code review
BurntimeX May 6, 2025
407b30e
Rename `configurationData` to `configuration`
BurntimeX May 6, 2025
4ae055d
Merge branch '6.2-form-options' of https://github.com/WoltLab/WCF int…
BurntimeX May 6, 2025
60aa0e1
Make `CurrencyFormatter` final
BurntimeX May 6, 2025
3508aac
Apply suggestions from code review
BurntimeX May 6, 2025
ece77b9
Add abstract implementation for select options
BurntimeX May 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions com.woltlab.wcf/objectType.xml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@
<definitionname>com.woltlab.wcf.message</definitionname>
<disallowedBBCodesPermission>user.signature.disallowedBBCodes</disallowedBBCodesPermission>
</type>
<type>
<name>com.woltlab.wcf.genericFormOption</name>
<definitionname>com.woltlab.wcf.message</definitionname>
</type>
<type>
<name>com.woltlab.wcf.user.signature</name>
<definitionname>com.woltlab.wcf.attachment.objectType</definitionname>
Expand Down Expand Up @@ -223,6 +227,11 @@
<definitionname>com.woltlab.wcf.attachment.objectType</definitionname>
<classname>wcf\system\attachment\ContactAttachmentObjectType</classname>
</type>
<type>
<name>com.woltlab.wcf.contact.form</name>
<definitionname>com.woltlab.wcf.file</definitionname>
<classname>wcf\system\file\processor\ContactFormFileProcessor</classname>
</type>
<!-- page -->
<type>
<name>com.woltlab.wcf.page.recentActivityEvent</name>
Expand Down Expand Up @@ -1722,6 +1731,10 @@
<name>com.woltlab.wcf.lostPasswordForm</name>
<definitionname>com.woltlab.wcf.floodControl</definitionname>
</type>
<type>
<name>com.woltlab.wcf.contactForm</name>
<definitionname>com.woltlab.wcf.floodControl</definitionname>
</type>
<type>
<name>com.woltlab.wcf.search</name>
<definitionname>com.woltlab.wcf.floodControl</definitionname>
Expand Down
7 changes: 7 additions & 0 deletions com.woltlab.wcf/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,11 @@

<instruction type="script">acp/install_com.woltlab.wcf_step2.php</instruction>
</instructions>

<!--
Required order of the following steps for the update to 6.2:
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_62_step1.php</instruction>
<instruction type="script">acp/update_com.woltlab.wcf_6.2_contactOptions.php</instruction>
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_62_step2.php</instruction>
-->
</package>
93 changes: 1 addition & 92 deletions com.woltlab.wcf/templates/contact.tpl
Original file line number Diff line number Diff line change
@@ -1,96 +1,5 @@
{include file='header'}

{include file='shared_formError'}

<form method="post" action="{link controller='Contact'}{/link}">
<section class="section">
<h2 class="sectionTitle">{lang}wcf.contact.sender.information{/lang}</h2>

<dl{if $errorField == 'name'} class="formError"{/if}>
<dt><label for="name">{lang}wcf.contact.sender{/lang}</label> <span class="customOptionRequired">*</span></dt>
<dd>
<input type="text" id="name" name="name" value="{$name}" required class="long">
{if $errorField == 'name'}
<small class="innerError">
{if $errorType == 'empty'}
{lang}wcf.global.form.error.empty{/lang}
{else}
{lang}wcf.contact.sender.error.{@$errorType}{/lang}
{/if}
</small>
{/if}
</dd>
</dl>

<dl{if $errorField == 'email'} class="formError"{/if}>
<dt><label for="email">{lang}wcf.user.email{/lang}</label> <span class="customOptionRequired">*</span></dt>
<dd>
<input type="email" id="email" name="email" value="{$email}" required class="medium">
{if $errorField == 'email'}
<small class="innerError">
{if $errorType == 'empty'}
{lang}wcf.global.form.error.empty{/lang}
{else}
{lang}wcf.user.email.error.{@$errorType}{/lang}
{/if}
</small>
{/if}
</dd>
</dl>

{event name='informationFields'}
</section>

<section class="section">
<h2 class="sectionTitle">{lang}wcf.contact.data{/lang}</h2>

{if $recipientList|count > 1}
<dl{if $errorField == 'recipientID'} class="formError"{/if}>
<dt><label for="recipientID">{lang}wcf.contact.recipientID{/lang}</label> <span class="customOptionRequired">*</span></dt>
<dd>
<select name="recipientID" id="recipientID" required>
<option value="">{lang}wcf.global.noSelection{/lang}</option>
{foreach from=$recipientList item=recipient}
<option value="{$recipient->recipientID}"{if $recipient->recipientID == $recipientID} selected{/if}>{$recipient}</option>
{/foreach}
</select>
{if $errorField == 'recipientID'}
<small class="innerError">
{if $errorType == 'empty'}
{lang}wcf.global.form.error.empty{/lang}
{else}
{lang}wcf.contact.recipientID.error.{@$errorType}{/lang}
{/if}
</small>
{/if}
</dd>
</dl>
{/if}

{include file='customOptionFieldList'}

{event name='optionFields'}

{if CONTACT_FORM_ENABLE_ATTACHMENTS && !$attachmentHandler|empty && $attachmentHandler->canUpload()}
<div class="contactFormAttachments">
{include file='shared_messageFormAttachments' wysiwygSelector=''}
</div>
{/if}
</section>

{event name='sections'}

{include file='shared_captcha'}

<div class="formSubmit">
<input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
{csrfToken}
</div>
</form>

<p class="formFieldRequiredNotice">
<span class="formFieldRequired">*</span>
{lang}wcf.global.form.required{/lang}
</p>
{unsafe:$form->getHtml()}

{include file='footer'}
16 changes: 16 additions & 0 deletions com.woltlab.wcf/templates/shared_selectOptionsFormField.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<input type="hidden" {*
*}id="{$field->getPrefixedId()}" {*
*}name="{$field->getPrefixedId()}" {*
*}value="{$field->getValue()}"{*
*}>

<script data-relocate="true">
require(['WoltLabSuite/Core/Form/Builder/Field/SelectOptions'], ({ setup }) => {
{jsphrase name='wcf.form.selectOptions.key'}
{jsphrase name='wcf.form.selectOptions.value'}

const availableLanguages = { {implode from=$availableLanguages key=languageID item=languageName}{$languageID}: '{unsafe:$languageName|encodeJS}'{/implode} };

setup(document.getElementById('{unsafe:$field->getPrefixedId()|encodeJS}'), availableLanguages);
});
</script>
2 changes: 1 addition & 1 deletion ts/WoltLabSuite/Core/Component/Ckeditor/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ class ConfigurationBuilder {

#setupMention(): void {
if (!this.#features.mention) {
this.#removePlugins.push("Mention", "WoltlabMention");
this.#removePlugins.push("WoltlabMention");
}
}

Expand Down
174 changes: 174 additions & 0 deletions ts/WoltLabSuite/Core/Form/Builder/Field/SelectOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { identify } from "WoltLabSuite/Core/Dom/Util";
import { getPhrase } from "WoltLabSuite/Core/Language";
import { getValues, init as initI18n } from "WoltLabSuite/Core/Language/Input";
import Sortable from "sortablejs";

type Data = {
key: string;
value: Record<string, string>;
};

type Languages = Record<string, string>;

let _languages: Languages;

export function setup(formField: HTMLInputElement, languages: Languages): void {
_languages = languages;

const ul = createUi(formField);

formField.form?.addEventListener("submit", () => {
setHiddenValue(formField);
});

new Sortable(ul, {
direction: "vertical",
animation: 150,
fallbackOnBody: true,
draggable: "li",
handle: ".selectOptionsListItem__handle",
});
}

function createUi(formField: HTMLInputElement): HTMLUListElement {
const ul = document.createElement("ul");
ul.classList.add("selectOptionsList");
formField.parentElement?.append(ul);

if (formField.value) {
const data = JSON.parse(formField.value) as Data[];
data.forEach((option) => {
createRow(ul, option);
});
} else {
createRow(ul);
}

return ul;
}

function createRow(ul: HTMLUListElement, option?: Data, autoFocus: boolean = false): void {
const li = document.createElement("li");
li.classList.add("selectOptionsListItem");
ul.append(li);

const addButton = getAddButton();
addButton.addEventListener("click", () => {
createRow(ul, undefined, true);
});

const deleteButton = getDeleteButton();
deleteButton.addEventListener("click", () => {
li.remove();

if (!ul.childElementCount) {
createRow(ul);
}
});

const keyInput = getKeyInput();
keyInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
createRow(ul, undefined, true);
}
});
keyInput.value = option ? option.key : "";

const equalsIcon = document.createElement("fa-icon");
equalsIcon.setIcon("equals");

const valueInput = getValueInput();
valueInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
createRow(ul, undefined, true);
}
});

li.append(getSortableHandle(), addButton, deleteButton, keyInput, equalsIcon, valueInput);

const hasI18nValues = option && !Object.hasOwn(option.value, 0);

initI18n(identify(valueInput), hasI18nValues ? option.value : {}, _languages, false);

if (!hasI18nValues) {
valueInput.value = option?.value[0] ?? "";
}

if (autoFocus) {
keyInput.focus();
}
}

function getAddButton(): HTMLButtonElement {
const addIcon = document.createElement("fa-icon");
addIcon.setIcon("plus");

const addButton = document.createElement("button");
addButton.type = "button";
addButton.append(addIcon);
addButton.classList.add("jsTooltip");
addButton.title = getPhrase("wcf.global.button.add");

return addButton;
}

function getDeleteButton(): HTMLButtonElement {
const deleteIcon = document.createElement("fa-icon");
deleteIcon.setIcon("xmark");

const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.append(deleteIcon);
deleteButton.classList.add("jsTooltip");
deleteButton.title = getPhrase("wcf.global.button.delete");

return deleteButton;
}

function getKeyInput(): HTMLInputElement {
const keyInput = document.createElement("input");
keyInput.classList.add("selectOptionsListItem__key");
keyInput.placeholder = getPhrase("wcf.form.selectOptions.key");
keyInput.type = "text";
keyInput.required = true;

return keyInput;
}

function getValueInput(): HTMLInputElement {
const valueInput = document.createElement("input");
valueInput.classList.add("selectOptionsListItem__value");
valueInput.placeholder = getPhrase("wcf.form.selectOptions.value");
valueInput.type = "text";
valueInput.required = true;

return valueInput;
}

function getSortableHandle(): HTMLElement {
const icon = document.createElement("fa-icon");
icon.setIcon("up-down");
const handle = document.createElement("span");
handle.append(icon);
handle.classList.add("selectOptionsListItem__handle");

return handle;
}

function setHiddenValue(formField: HTMLInputElement): void {
const data: Data[] = [];

formField.parentElement?.querySelectorAll(".selectOptionsListItem").forEach((li) => {
const key = li.querySelector<HTMLInputElement>(".selectOptionsListItem__key")!.value;
const valueInput = li.querySelector<HTMLInputElement>(".selectOptionsListItem__value")!;

data.push({
key,
value: Object.fromEntries(getValues(valueInput.id)),
});
});

formField.value = JSON.stringify(data);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
*/

use wcf\system\database\table\column\IntDatabaseTableColumn;
use wcf\system\database\table\column\MediumtextDatabaseTableColumn;
use wcf\system\database\table\column\TextDatabaseTableColumn;
use wcf\system\database\table\column\TinyintDatabaseTableColumn;
use wcf\system\database\table\index\DatabaseTableForeignKey;
use wcf\system\database\table\PartialDatabaseTable;

Expand Down Expand Up @@ -46,5 +49,13 @@
->referencedTable('wcf1_file')
->referencedColumns(['fileID'])
->onDelete('SET NULL'),
])
]),
PartialDatabaseTable::create('wcf1_contact_option')
->columns([
MediumtextDatabaseTableColumn::create('configuration'),
]),
PartialDatabaseTable::create('wcf1_file')
->columns([
IntDatabaseTableColumn::create('uploadTime'),
]),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/**
* Updates the database layout during the update from 6.1 to 6.2.
*
* @author Olaf Braun
* @copyright 2001-2024 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
*/

use wcf\system\database\table\column\MediumtextDatabaseTableColumn;
use wcf\system\database\table\column\TextDatabaseTableColumn;
use wcf\system\database\table\column\TinyintDatabaseTableColumn;
use wcf\system\database\table\PartialDatabaseTable;

return [
PartialDatabaseTable::create('wcf1_contact_option')
->columns([
MediumtextDatabaseTableColumn::create('defaultValue')
->drop(),
TextDatabaseTableColumn::create('validationPattern')
->drop(),
MediumtextDatabaseTableColumn::create('selectOptions')
->drop(),
TinyintDatabaseTableColumn::create('required')
->drop(),
]),
];
Loading