FIX: Make cancel and reset buttons work for `file_size_restriction` settings (#28347)

This commit fixes a number of bugs in `file_size_restriction` settings and does a little of refactoring to reduce duplicated code in site setting types (the refactoring is necessary to fix one of the bugs).

The bugs in `file_size_restriction` settings that are fixed in this commit:

1. Save/cancel buttons next to a `file_size_restriction` setting are shown upon navigating to the settings page without changes being made to the setting
2. Cancel button that discards changes made to the setting doesn't work
3. Reset button that resets the setting to its default doesn't work
4. Validation error message isn't cleared when resetting/cancelling changes

To repro those bugs, navigate to `/admin/site_settings/category/files` and observe the top 2 settings in the page (`max image size kb` and `max attachment size kb`).

Internal topic: t/134726.
This commit is contained in:
Osama Sayegh 2024-08-15 19:38:47 +03:00 committed by GitHub
parent b545576b3c
commit a92cf019db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 400 additions and 216 deletions

View File

@ -56,6 +56,4 @@
@isEditorFocused={{this.isEditorFocused}} @isEditorFocused={{this.isEditorFocused}}
@emojiSelected={{this.emojiSelected}} @emojiSelected={{this.emojiSelected}}
@onEmojiPickerClose={{this.closeEmojiPicker}} @onEmojiPickerClose={{this.closeEmojiPicker}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />

View File

@ -10,7 +10,6 @@ import I18n from "discourse-i18n";
@classNameBindings(":value-list", ":emoji-list") @classNameBindings(":value-list", ":emoji-list")
export default class EmojiValueList extends Component { export default class EmojiValueList extends Component {
values = null; values = null;
validationMessage = null;
emojiPickerIsActive = false; emojiPickerIsActive = false;
isEditorFocused = false; isEditorFocused = false;
@ -137,16 +136,14 @@ export default class EmojiValueList extends Component {
} }
_validateInput(input) { _validateInput(input) {
this.set("validationMessage", null);
if (!emojiUrlFor(input)) { if (!emojiUrlFor(input)) {
this.set( this.setValidationMessage(
"validationMessage",
I18n.t("admin.site_settings.emoji_list.invalid_input") I18n.t("admin.site_settings.emoji_list.invalid_input")
); );
return false; return false;
} }
this.setValidationMessage(null);
return true; return true;
} }

View File

@ -2,9 +2,6 @@ import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier"; import { on } from "@ember/modifier";
import { action } from "@ember/object"; import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import TextField from "discourse/components/text-field";
import { allowOnlyNumericInput } from "discourse/lib/utilities"; import { allowOnlyNumericInput } from "discourse/lib/utilities";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box"; import ComboBox from "select-kit/components/combo-box";
@ -14,30 +11,34 @@ const UNIT_MB = "mb";
const UNIT_GB = "gb"; const UNIT_GB = "gb";
export default class FileSizeInput extends Component { export default class FileSizeInput extends Component {
@tracked fileSizeUnit; @tracked unit;
@tracked sizeValue;
@tracked pendingSizeValue;
@tracked pendingFileSizeUnit;
constructor(owner, args) { constructor() {
super(owner, args); super(...arguments);
this.originalSizeKB = this.args.sizeValueKB;
this.sizeValue = this.args.sizeValueKB;
this._defaultUnit(); const sizeInKB = this.args.sizeValueKB;
if (sizeInKB >= 1024 * 1024) {
this.unit = UNIT_GB;
} else if (sizeInKB >= 1024) {
this.unit = UNIT_MB;
} else {
this.unit = UNIT_KB;
}
} }
_defaultUnit() { get number() {
this.fileSizeUnit = UNIT_KB; const sizeInKB = this.args.sizeValueKB;
if (this.originalSizeKB <= 1024) { if (!sizeInKB) {
this.onFileSizeUnitChange(UNIT_KB); return;
} else if ( }
this.originalSizeKB > 1024 && if (this.unit === UNIT_KB) {
this.originalSizeKB <= 1024 * 1024 return sizeInKB;
) { }
this.onFileSizeUnitChange(UNIT_MB); if (this.unit === UNIT_MB) {
} else if (this.originalSizeKB > 1024 * 1024) { return sizeInKB / 1024;
this.onFileSizeUnitChange(UNIT_GB); }
if (this.unit === UNIT_GB) {
return sizeInKB / 1024 / 1024;
} }
} }
@ -55,95 +56,69 @@ export default class FileSizeInput extends Component {
} }
@action @action
handleFileSizeChange(value) { handleFileSizeChange(event) {
if (value !== "") { const value = parseFloat(event.target.value);
this.pendingSizeValue = value;
this._onFileSizeChange(value);
}
}
_onFileSizeChange(newSize) { if (isNaN(value)) {
let fileSizeKB; this.args.onChangeSize();
switch (this.fileSizeUnit) { return;
}
let sizeInKB;
switch (this.unit) {
case "kb": case "kb":
fileSizeKB = newSize; sizeInKB = value;
break; break;
case "mb": case "mb":
fileSizeKB = newSize * 1024; sizeInKB = value * 1024;
break; break;
case "gb": case "gb":
fileSizeKB = newSize * 1024 * 1024; sizeInKB = value * 1024 * 1024;
break; break;
} }
if (fileSizeKB > this.args.max) {
this.args.updateValidationMessage( this.args.onChangeSize(sizeInKB);
if (sizeInKB > this.args.max) {
this.args.setValidationMessage(
I18n.t("file_size_input.error.size_too_large", { I18n.t("file_size_input.error.size_too_large", {
provided_file_size: I18n.toHumanSize(fileSizeKB * 1024), provided_file_size: I18n.toHumanSize(sizeInKB * 1024),
max_file_size: I18n.toHumanSize(this.args.max * 1024), max_file_size: I18n.toHumanSize(this.args.max * 1024),
}) })
); );
// Removes the green save checkmark button } else if (sizeInKB < this.args.min) {
this.args.onChangeSize(this.originalSizeKB); this.args.setValidationMessage(
I18n.t("file_size_input.error.size_too_small", {
provided_file_size: I18n.toHumanSize(sizeInKB * 1024),
min_file_size: I18n.toHumanSize(this.args.min * 1024),
})
);
} else { } else {
this.args.onChangeSize(fileSizeKB); this.args.setValidationMessage(null);
this.args.updateValidationMessage(null);
} }
} }
@action @action
onFileSizeUnitChange(newUnit) { onFileSizeUnitChange(newUnit) {
if (this.fileSizeUnit === "kb" && newUnit === "kb") { this.unit = newUnit;
this.pendingSizeValue = this.sizeValue;
}
if (this.fileSizeUnit === "kb" && newUnit === "mb") {
this.pendingSizeValue = this.sizeValue / 1024;
}
if (this.fileSizeUnit === "kb" && newUnit === "gb") {
this.pendingSizeValue = this.sizeValue / 1024 / 1024;
}
if (this.fileSizeUnit === "mb" && newUnit === "kb") {
this.pendingSizeValue = this.sizeValue * 1024;
}
if (this.fileSizeUnit === "mb" && newUnit === "gb") {
this.pendingSizeValue = this.sizeValue / 1024;
}
if (this.fileSizeUnit === "gb" && newUnit === "mb") {
this.pendingSizeValue = this.sizeValue * 1024;
}
if (this.fileSizeUnit === "gb" && newUnit === "kb") {
this.pendingSizeValue = this.sizeValue * 1024 * 1024;
}
this.pendingFileSizeUnit = newUnit;
}
@action
applySizeValueChanges() {
this.sizeValue = this.pendingSizeValue;
}
@action
applyUnitChanges() {
this.fileSizeUnit = this.pendingFileSizeUnit;
} }
<template> <template>
<div class="file-size-picker"> <div class="file-size-picker">
<TextField <input
class="file-size-input" class="file-size-input"
@value={{this.sizeValue}} value={{this.number}}
@onChange={{this.handleFileSizeChange}} type="number"
step="any"
{{on "input" this.handleFileSizeChange}}
{{on "keydown" this.keyDown}} {{on "keydown" this.keyDown}}
{{didInsert this.applySizeValueChanges}}
{{didUpdate this.applySizeValueChanges this.pendingSizeValue}}
/> />
<ComboBox <ComboBox
class="file-size-unit-selector" class="file-size-unit-selector"
@valueProperty="value" @valueProperty="value"
@content={{this.dropdownOptions}} @content={{this.dropdownOptions}}
@value={{this.fileSizeUnit}} @value={{this.unit}}
@onChange={{this.onFileSizeUnitChange}} @onChange={{this.onFileSizeUnitChange}}
{{didInsert this.applyUnitChanges}}
{{didUpdate this.applyUnitChanges this.pendingFileSizeUnit}}
/> />
</div> </div>
</template> </template>

View File

@ -40,6 +40,4 @@
@icon="plus" @icon="plus"
class="add-value-btn btn-small" class="add-value-btn btn-small"
/> />
</div> </div>
<SettingValidationMessage @message={{this.validationMessage}} />

View File

@ -9,7 +9,6 @@ export default class SecretValueList extends Component {
inputDelimiter = null; inputDelimiter = null;
collection = null; collection = null;
values = null; values = null;
validationMessage = null;
didReceiveAttrs() { didReceiveAttrs() {
super.didReceiveAttrs(...arguments); super.didReceiveAttrs(...arguments);
@ -57,16 +56,15 @@ export default class SecretValueList extends Component {
} }
_checkInvalidInput(inputs) { _checkInvalidInput(inputs) {
this.set("validationMessage", null);
for (let input of inputs) { for (let input of inputs) {
if (isEmpty(input) || input.includes("|")) { if (isEmpty(input) || input.includes("|")) {
this.set( this.setValidationMessage(
"validationMessage",
I18n.t("admin.site_settings.secret_list.invalid_input") I18n.t("admin.site_settings.secret_list.invalid_input")
); );
return true; return true;
} }
} }
this.setValidationMessage(null);
} }
_addValue(value, secret) { _addValue(value, secret) {

View File

@ -39,12 +39,16 @@
this.componentName this.componentName
setting=this.setting setting=this.setting
value=this.buffered.value value=this.buffered.value
validationMessage=this.validationMessage
preview=this.preview preview=this.preview
isSecret=this.isSecret isSecret=this.isSecret
allowAny=this.allowAny allowAny=this.allowAny
changeValueCallback=this.changeValueCallback changeValueCallback=this.changeValueCallback
setValidationMessage=this.setValidationMessage
}} }}
<SettingValidationMessage @message={{this.validationMessage}} />
{{#if this.displayDescription}}
<SiteSettings::Description @description={{this.setting.description}} />
{{/if}}
{{/if}} {{/if}}
</div> </div>
@ -53,6 +57,7 @@
<DButton <DButton
@action={{this.update}} @action={{this.update}}
@icon="check" @icon="check"
@disabled={{this.disableSaveButton}}
class="ok setting-controls__ok" class="ok setting-controls__ok"
/> />
<DButton <DButton
@ -71,7 +76,7 @@
{{/if}} {{/if}}
<DButton <DButton
class="btn-default undo" class="btn-default undo setting-controls__undo"
@action={{this.resetDefault}} @action={{this.resetDefault}}
@icon="undo" @icon="undo"
@label="admin.settings.reset" @label="admin.settings.reset"

View File

@ -1,6 +1,4 @@
<label class="checkbox-label"> <label class="checkbox-label">
<Input @type="checkbox" @checked={{this.enabled}} /> <Input @type="checkbox" @checked={{this.enabled}} />
<span>{{html-safe this.setting.description}}</span> <span>{{html-safe this.setting.description}}</span>
</label> </label>
<SettingValidationMessage @message={{this.validationMessage}} />

View File

@ -3,8 +3,6 @@ import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object"; import { action } from "@ember/object";
import didUpdate from "@ember/render-modifiers/modifiers/did-update"; import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import Category from "discourse/models/category"; import Category from "discourse/models/category";
import SettingValidationMessage from "admin/components/setting-validation-message";
import SiteSettingsDescription from "admin/components/site-settings/description";
import CategorySelector from "select-kit/components/category-selector"; import CategorySelector from "select-kit/components/category-selector";
export default class CategoryList extends Component { export default class CategoryList extends Component {
@ -50,9 +48,6 @@ export default class CategoryList extends Component {
@categories={{this.selectedCategories}} @categories={{this.selectedCategories}}
@onChange={{this.onChangeSelectedCategories}} @onChange={{this.onChangeSelectedCategories}}
/> />
<SiteSettingsDescription @description={{@setting.description}} />
<SettingValidationMessage @message={{@validationMessage}} />
</div> </div>
</template> </template>
} }

View File

@ -2,6 +2,4 @@
@value={{this.value}} @value={{this.value}}
@onChange={{fn (mut this.value)}} @onChange={{fn (mut this.value)}}
@options={{hash allowUncategorized=true none=(eq this.setting.default "")}} @options={{hash allowUncategorized=true none=(eq this.setting.default "")}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -4,6 +4,4 @@
@onlyHex={{false}} @onlyHex={{false}}
@styleSelection={{false}} @styleSelection={{false}}
@onChangeColor={{this.onChangeColor}} @onChangeColor={{this.onChangeColor}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -5,7 +5,4 @@
@onChange={{this.onChangeListSetting}} @onChange={{this.onChangeListSetting}}
@onChangeChoices={{this.onChangeChoices}} @onChangeChoices={{this.onChangeChoices}}
@options={{hash allowAny=this.allowAny}} @options={{hash allowAny=this.allowAny}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -1,3 +1,5 @@
<EmojiValueList @setting={{this.setting}} @values={{this.value}} /> <EmojiValueList
<SiteSettings::Description @description={{this.setting.description}} /> @setting={{this.setting}}
<SettingValidationMessage @message={{this.validationMessage}} /> @values={{this.value}}
@setValidationMessage={{this.setValidationMessage}}
/>

View File

@ -7,7 +7,4 @@
@options={{hash castInteger=true allowAny=this.setting.allowsNone}} @options={{hash castInteger=true allowAny=this.setting.allowsNone}}
/> />
{{this.preview}} {{this.preview}}
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -1,34 +1,23 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { fn } from "@ember/helper";
import { action } from "@ember/object"; import { action } from "@ember/object";
import FileSizeInput from "admin/components/file-size-input"; import FileSizeInput from "admin/components/file-size-input";
import SettingValidationMessage from "admin/components/setting-validation-message";
import SiteSettingsDescription from "admin/components/site-settings/description";
export default class FileSizeRestriction extends Component { export default class FileSizeRestriction extends Component {
@tracked _validationMessage = this.args.validationMessage;
@action @action
updateValidationMessage(message) { changeSize(newValue) {
this._validationMessage = message; // Settings are stored as strings, this way the main site setting component
} // doesn't get confused and think the value has changed from default if the
// admin sets it to the same number as the default.
get validationMessage() { this.args.changeValueCallback(newValue?.toString() ?? "");
return this._validationMessage ?? this.args.validationMessage;
} }
<template> <template>
<FileSizeInput <FileSizeInput
@sizeValueKB={{@value}} @sizeValueKB={{@value}}
@onChangeSize={{fn (mut @value)}} @onChangeSize={{this.changeSize}}
@updateValidationMessage={{this.updateValidationMessage}} @max={{@setting.max}}
@min={{if @setting.min @setting.min null}} @min={{@setting.min}}
@max={{if @setting.max @setting.max null}} @setValidationMessage={{@setValidationMessage}}
@message={{this.validationMessage}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettingsDescription @description={{@setting.description}} />
</template> </template>
} }

View File

@ -8,8 +8,6 @@ import DButton from "discourse/components/d-button";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import { makeArray } from "discourse-common/lib/helpers"; import { makeArray } from "discourse-common/lib/helpers";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import SettingValidationMessage from "admin/components/setting-validation-message";
import SiteSettingsDescription from "admin/components/site-settings/description";
import ListSetting from "select-kit/components/list-setting"; import ListSetting from "select-kit/components/list-setting";
const IMAGE_TYPES = [ const IMAGE_TYPES = [
@ -145,8 +143,5 @@ export default class FileTypesList extends Component {
}} }}
class="btn file-types-list__button document" class="btn file-types-list__button document"
/> />
<SettingValidationMessage @message={{@validationMessage}} />
<SiteSettingsDescription @description={{@setting.description}} />
</template> </template>
} }

View File

@ -6,6 +6,4 @@
@nameProperty={{this.nameProperty}} @nameProperty={{this.nameProperty}}
@valueProperty={{this.valueProperty}} @valueProperty={{this.valueProperty}}
@onChange={{this.onChangeGroupListSetting}} @onChange={{this.onChangeGroupListSetting}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -4,7 +4,4 @@
@choices={{this.settingValue}} @choices={{this.settingValue}}
@onChange={{this.onChange}} @onChange={{this.onChange}}
@options={{hash allowAny=this.allowAny}} @options={{hash allowAny=this.allowAny}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -1,8 +1,6 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { on } from "@ember/modifier"; import { on } from "@ember/modifier";
import { action } from "@ember/object"; import { action } from "@ember/object";
import SettingValidationMessage from "admin/components/setting-validation-message";
import SiteSettingDescription from "admin/components/site-settings/description";
export default class SiteSettingsInteger extends Component { export default class SiteSettingsInteger extends Component {
@action @action
@ -37,8 +35,5 @@ export default class SiteSettingsInteger extends Component {
class="input-setting-integer" class="input-setting-integer"
step="1" step="1"
/> />
<SettingValidationMessage @message={{@validationMessage}} />
<SiteSettingDescription @description={{@setting.description}} />
</template> </template>
} }

View File

@ -2,6 +2,4 @@
@values={{this.value}} @values={{this.value}}
@inputDelimiter="|" @inputDelimiter="|"
@choices={{this.setting.choices}} @choices={{this.setting.choices}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -6,7 +6,4 @@
@valueProperty="value" @valueProperty="value"
@onChange={{this.onChangeListSetting}} @onChange={{this.onChangeListSetting}}
@options={{hash allowAny=this.allowAny}} @options={{hash allowAny=this.allowAny}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -2,6 +2,5 @@
@setting={{this.setting}} @setting={{this.setting}}
@values={{this.value}} @values={{this.value}}
@isSecret={{this.isSecret}} @isSecret={{this.isSecret}}
/> @setValidationMessage={{this.setValidationMessage}}
<SettingValidationMessage @message={{this.validationMessage}} /> />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -4,6 +4,4 @@
@onChange={{this.onChange}} @onChange={{this.onChange}}
@choices={{this.setting.choices}} @choices={{this.setting.choices}}
@allowAny={{this.setting.allow_any}} @allowAny={{this.setting.allow_any}}
/> />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -9,7 +9,4 @@
/> />
{{else}} {{else}}
<TextField @value={{this.value}} @classNames="input-setting-string" /> <TextField @value={{this.value}} @classNames="input-setting-string" />
{{/if}} {{/if}}
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -2,6 +2,4 @@
@tagGroups={{this.selectedTagGroups}} @tagGroups={{this.selectedTagGroups}}
@onChange={{this.onTagGroupChange}} @onChange={{this.onTagGroupChange}}
@options={{hash filterPlaceholder="category.required_tag_group.placeholder"}} @options={{hash filterPlaceholder="category.required_tag_group.placeholder"}}
/> />
<SiteSettings::Description @description={{this.setting.description}} />
<SettingValidationMessage @message={{this.validationMessage}} />

View File

@ -4,6 +4,4 @@
@everyTag={{true}} @everyTag={{true}}
@unlimitedTagCount={{true}} @unlimitedTagCount={{true}}
@options={{hash allowAny=false}} @options={{hash allowAny=false}}
/> />
<SiteSettings::Description @description={{this.setting.description}} />
<SettingValidationMessage @message={{this.validationMessage}} />

View File

@ -4,6 +4,4 @@
@additionalParams={{hash for_site_setting=true}} @additionalParams={{hash for_site_setting=true}}
@type="site_setting" @type="site_setting"
@id={{concat "site-setting-image-uploader-" this.setting.setting}} @id={{concat "site-setting-image-uploader-" this.setting.setting}}
/> />
<SiteSettings::Description @description={{this.setting.description}} />
<SettingValidationMessage @message={{this.validationMessage}} />

View File

@ -4,6 +4,4 @@
this.showUploadModal this.showUploadModal
(hash value=this.value setting=this.setting) (hash value=this.value setting=this.setting)
}} }}
/> />
<SiteSettings::Description @description={{this.setting.description}} />
<SettingValidationMessage @message={{this.validationMessage}} />

View File

@ -1,3 +1 @@
<ValueList @values={{this.value}} @addKey="admin.site_settings.add_url" /> <ValueList @values={{this.value}} @addKey="admin.site_settings.add_url" />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -1,3 +1 @@
<ValueList @values={{this.value}} /> <ValueList @values={{this.value}} />
<SettingValidationMessage @message={{this.validationMessage}} />
<SiteSettings::Description @description={{this.setting.description}} />

View File

@ -104,6 +104,10 @@ export default Mixin.create({
this.element.removeEventListener("keydown", this._handleKeydown); this.element.removeEventListener("keydown", this._handleKeydown);
}, },
displayDescription: computed("componentType", function () {
return this.componentType !== "bool";
}),
dirty: computed("buffered.value", "setting.value", function () { dirty: computed("buffered.value", "setting.value", function () {
let bufferVal = this.get("buffered.value"); let bufferVal = this.get("buffered.value");
let settingVal = this.setting?.value; let settingVal = this.setting?.value;
@ -211,6 +215,10 @@ export default Mixin.create({
} }
}), }),
disableSaveButton: computed("validationMessage", function () {
return !!this.validationMessage;
}),
confirmChanges(settingKey) { confirmChanges(settingKey) {
return new Promise((resolve) => { return new Promise((resolve) => {
// Fallback is needed in case the setting does not have a custom confirmation // Fallback is needed in case the setting does not have a custom confirmation
@ -324,12 +332,18 @@ export default Mixin.create({
this.set("buffered.value", value); this.set("buffered.value", value);
}), }),
setValidationMessage: action(function (message) {
this.set("validationMessage", message);
}),
cancel: action(function () { cancel: action(function () {
this.rollbackBuffer(); this.rollbackBuffer();
this.set("validationMessage", null);
}), }),
resetDefault: action(function () { resetDefault: action(function () {
this.set("buffered.value", this.setting.default); this.set("buffered.value", this.setting.default);
this.set("validationMessage", null);
}), }),
toggleSecret: action(function () { toggleSecret: action(function () {
@ -341,6 +355,7 @@ export default Mixin.create({
"buffered.value", "buffered.value",
this.bufferedValues.concat(this.defaultValues).uniq().join("|") this.bufferedValues.concat(this.defaultValues).uniq().join("|")
); );
this.set("validationMessage", null);
return false; return false;
}), }),

View File

@ -8,39 +8,88 @@ module("Integration | Component | file-size-input", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("file size unit selector kb", async function (assert) { test("file size unit selector kb", async function (assert) {
this.set("value", 1024); this.set("value", 1023);
this.set("max", 4096); this.set("max", 4096);
this.set("onChangeSize", () => {}); this.set("onChangeSize", () => {});
this.set("updateValidationMessage", () => {}); this.set("setValidationMessage", () => {});
await render(hbs` await render(hbs`
<FileSizeInput <FileSizeInput
@sizeValueKB={{readonly this.value}} @sizeValueKB={{readonly this.value}}
class="file-size-input-test" class="file-size-input-test"
@onChangeSize={{this.onChangeSize}} @onChangeSize={{this.onChangeSize}}
@updateValidationMessage={{this.updateValidationMessage}} @setValidationMessage={{this.setValidationMessage}}
@max=4096 @max=4096
@message=""
/> />
`); `);
assert.dom(".file-size-input").hasValue("1024", "value is present"); assert.dom(".file-size-input").hasValue("1023", "value is present");
assert.strictEqual(
selectKit(".file-size-unit-selector").header().value(),
"kb",
"the default unit is kb"
);
});
test("file size unit is mb when the starting value is 1mb or more", async function (assert) {
this.set("value", 1024);
this.set("onChangeSize", () => {});
this.set("setValidationMessage", () => {});
await render(hbs`
<FileSizeInput
@sizeValueKB={{readonly this.value}}
class="file-size-input-test"
@onChangeSize={{this.onChangeSize}}
@setValidationMessage={{this.setValidationMessage}}
@max=4096
/>
`);
assert.dom(".file-size-input").hasValue("1", "value is present");
assert.strictEqual(
selectKit(".file-size-unit-selector").header().value(),
"mb",
"the default unit is mb"
);
});
test("file size unit is gb when the starting value is 1gb or more", async function (assert) {
this.set("value", 1024 * 1024);
this.set("onChangeSize", () => {});
this.set("setValidationMessage", () => {});
await render(hbs`
<FileSizeInput
@sizeValueKB={{readonly this.value}}
class="file-size-input-test"
@onChangeSize={{this.onChangeSize}}
@setValidationMessage={{this.setValidationMessage}}
@max=4096
/>
`);
assert.dom(".file-size-input").hasValue("1", "value is present");
assert.strictEqual(
selectKit(".file-size-unit-selector").header().value(),
"gb",
"the default unit is gb"
);
}); });
test("file size unit selector", async function (assert) { test("file size unit selector", async function (assert) {
this.set("value", 4096); this.set("value", 4096);
this.set("max", 8192); this.set("max", 8192);
this.set("onChangeSize", () => {}); this.set("onChangeSize", () => {});
this.set("updateValidationMessage", () => {}); this.set("setValidationMessage", () => {});
await render(hbs` await render(hbs`
<FileSizeInput <FileSizeInput
@sizeValueKB={{readonly this.value}} @sizeValueKB={{readonly this.value}}
class="file-size-input-test" class="file-size-input-test"
@onChangeSize={{this.onChangeSize}} @onChangeSize={{this.onChangeSize}}
@updateValidationMessage={{this.updateValidationMessage}} @setValidationMessage={{this.setValidationMessage}}
@max=4096 @max=4096
@message=""
/> />
`); `);
@ -91,21 +140,22 @@ module("Integration | Component | file-size-input", function (hooks) {
test("file size input error message", async function (assert) { test("file size input error message", async function (assert) {
this.set("value", 4096); this.set("value", 4096);
this.set("max", 8192); this.set("max", 8192);
this.set("min", 2048);
this.set("onChangeSize", () => {}); this.set("onChangeSize", () => {});
let updateValidationMessage = (message) => { let setValidationMessage = (message) => {
this.set("message", message); this.set("message", message);
}; };
this.set("updateValidationMessage", updateValidationMessage); this.set("setValidationMessage", setValidationMessage);
await render(hbs` await render(hbs`
<FileSizeInput <FileSizeInput
@sizeValueKB={{readonly this.value}} @sizeValueKB={{readonly this.value}}
class="file-size-input-test" class="file-size-input-test"
@onChangeSize={{this.onChangeSize}} @onChangeSize={{this.onChangeSize}}
@updateValidationMessage={{this.updateValidationMessage}} @setValidationMessage={{this.setValidationMessage}}
@max={{this.max}} @max={{this.max}}
@message={{this.message}} @min={{this.min}}
/> />
`); `);
@ -124,5 +174,13 @@ module("Integration | Component | file-size-input", function (hooks) {
null, null,
"The message is cleared when the input is less than the max" "The message is cleared when the input is less than the max"
); );
await fillIn(".file-size-input", 1);
assert.strictEqual(
this.message,
"1 MB is smaller than the min allowed 2 MB",
"A message is showed when the input is smaller than the min"
);
}); });
}); });

View File

@ -9,7 +9,14 @@ module("Integration | Component | secret-value-list", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("adding a value", async function (assert) { test("adding a value", async function (assert) {
await render(hbs`<SecretValueList @values={{this.values}} />`); this.set("setValidationMessage", () => {});
await render(hbs`
<SecretValueList
@values={{this.values}}
@setValidationMessage={{this.setValidationMessage}}
/>
`);
this.set("values", "firstKey|FirstValue\nsecondKey|secondValue"); this.set("values", "firstKey|FirstValue\nsecondKey|secondValue");
@ -50,7 +57,17 @@ module("Integration | Component | secret-value-list", function (hooks) {
}); });
test("adding an invalid value", async function (assert) { test("adding an invalid value", async function (assert) {
await render(hbs`<SecretValueList @values={{this.values}} />`); let setValidationMessage = (message) => {
this.set("message", message);
};
this.set("setValidationMessage", setValidationMessage);
await render(hbs`
<SecretValueList
@values={{this.values}}
@setValidationMessage={{this.setValidationMessage}}
/>
`);
await fillIn(".new-value-input.key", "someString"); await fillIn(".new-value-input.key", "someString");
await fillIn(".new-value-input.secret", "keyWithAPipe|Hidden"); await fillIn(".new-value-input.secret", "keyWithAPipe|Hidden");
@ -67,16 +84,22 @@ module("Integration | Component | secret-value-list", function (hooks) {
"it doesn't add the value to the list of values" "it doesn't add the value to the list of values"
); );
assert.ok( assert.strictEqual(
query(".validation-error").innerText.includes( this.message,
I18n.t("admin.site_settings.secret_list.invalid_input") I18n.t("admin.site_settings.secret_list.invalid_input"),
),
"it shows validation error" "it shows validation error"
); );
}); });
test("changing a value", async function (assert) { test("changing a value", async function (assert) {
await render(hbs`<SecretValueList @values={{this.values}} />`); this.set("setValidationMessage", () => {});
await render(hbs`
<SecretValueList
@values={{this.values}}
@setValidationMessage={{this.setValidationMessage}}
/>
`);
this.set("values", "firstKey|FirstValue\nsecondKey|secondValue"); this.set("values", "firstKey|FirstValue\nsecondKey|secondValue");
@ -109,7 +132,14 @@ module("Integration | Component | secret-value-list", function (hooks) {
}); });
test("removing a value", async function (assert) { test("removing a value", async function (assert) {
await render(hbs`<SecretValueList @values={{this.values}} />`); this.set("setValidationMessage", () => {});
await render(hbs`
<SecretValueList
@values={{this.values}}
@setValidationMessage={{this.setValidationMessage}}
/>
`);
this.set("values", "firstKey|FirstValue\nsecondKey|secondValue"); this.set("values", "firstKey|FirstValue\nsecondKey|secondValue");

View File

@ -4,6 +4,7 @@ import { module, skip, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import pretender, { response } from "discourse/tests/helpers/create-pretender"; import pretender, { response } from "discourse/tests/helpers/create-pretender";
import { query } from "discourse/tests/helpers/qunit-helpers"; import { query } from "discourse/tests/helpers/qunit-helpers";
import I18n from "discourse-i18n";
module("Integration | Component | site-setting", function (hooks) { module("Integration | Component | site-setting", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@ -118,3 +119,174 @@ module("Integration | Component | site-setting", function (hooks) {
.hasNoClass("overridden"); .hasNoClass("overridden");
}); });
}); });
module(
"Integration | Component | site-setting | file_size_restriction type",
function (hooks) {
setupRenderingTest(hooks);
test("shows the reset button when the value has been changed from the default", async function (assert) {
this.set("setting", {
setting: "max_image_size_kb",
value: "2048",
default: "1024",
min: 512,
max: 4096,
type: "file_size_restriction",
});
await render(hbs`<SiteSetting @setting={{this.setting}} />`);
assert.dom(".setting-controls__undo").exists("reset button is shown");
});
test("doesn't show the reset button when the value is the same as the default", async function (assert) {
this.set("setting", {
setting: "max_image_size_kb",
value: "1024",
default: "1024",
min: 512,
max: 4096,
type: "file_size_restriction",
});
await render(hbs`<SiteSetting @setting={{this.setting}} />`);
assert
.dom(".setting-controls__undo")
.doesNotExist("reset button is not shown");
});
test("shows validation error when the value exceeds the max limit", async function (assert) {
this.set("setting", {
setting: "max_image_size_kb",
value: "1024",
default: "1024",
min: 512,
max: 4096,
type: "file_size_restriction",
});
await render(hbs`<SiteSetting @setting={{this.setting}} />`);
await fillIn(".file-size-input", "5000");
assert.dom(".validation-error").hasText(
I18n.t("file_size_input.error.size_too_large", {
provided_file_size: "4.9 GB",
max_file_size: "4 MB",
}),
"validation error message is shown"
);
assert.dom(".setting-controls__ok").hasAttribute("disabled");
assert.dom(".setting-controls__cancel").doesNotHaveAttribute("disabled");
});
test("shows validation error when the value is below the min limit", async function (assert) {
this.set("setting", {
setting: "max_image_size_kb",
value: "1000",
default: "1024",
min: 512,
max: 4096,
type: "file_size_restriction",
});
await render(hbs`<SiteSetting @setting={{this.setting}} />`);
await fillIn(".file-size-input", "100");
assert.dom(".validation-error").hasText(
I18n.t("file_size_input.error.size_too_small", {
provided_file_size: "100 KB",
min_file_size: "512 KB",
}),
"validation error message is shown"
);
assert.dom(".setting-controls__ok").hasAttribute("disabled");
assert.dom(".setting-controls__cancel").doesNotHaveAttribute("disabled");
});
test("cancelling pending changes resets the value and removes validation error", async function (assert) {
this.set("setting", {
setting: "max_image_size_kb",
value: "1000",
default: "1024",
min: 512,
max: 4096,
type: "file_size_restriction",
});
await render(hbs`<SiteSetting @setting={{this.setting}} />`);
await fillIn(".file-size-input", "100");
assert.dom(".validation-error").hasNoClass("hidden");
await click(".setting-controls__cancel");
assert
.dom(".file-size-input")
.hasValue("1000", "the value resets to the saved value");
assert.dom(".validation-error").hasClass("hidden");
});
test("resetting to the default value changes the content of input field", async function (assert) {
this.set("setting", {
setting: "max_image_size_kb",
value: "1000",
default: "1024",
min: 512,
max: 4096,
type: "file_size_restriction",
});
await render(hbs`<SiteSetting @setting={{this.setting}} />`);
assert
.dom(".file-size-input")
.hasValue("1000", "the input field contains the custom value");
await click(".setting-controls__undo");
assert
.dom(".file-size-input")
.hasValue("1024", "the input field now contains the default value");
assert
.dom(".setting-controls__undo")
.doesNotExist("the reset button is not shown");
assert.dom(".setting-controls__ok").exists("the save button is shown");
assert
.dom(".setting-controls__cancel")
.exists("the cancel button is shown");
});
test("clearing the input field keeps the cancel button and the validation error shown", async function (assert) {
this.set("setting", {
setting: "max_image_size_kb",
value: "1000",
default: "1024",
min: 512,
max: 4096,
type: "file_size_restriction",
});
await render(hbs`<SiteSetting @setting={{this.setting}} />`);
await fillIn(".file-size-input", "100");
assert.dom(".validation-error").hasNoClass("hidden");
await fillIn(".file-size-input", "");
assert.dom(".validation-error").hasNoClass("hidden");
assert.dom(".setting-controls__ok").exists("the save button is shown");
assert.dom(".setting-controls__ok").hasAttribute("disabled");
assert
.dom(".setting-controls__cancel")
.exists("the cancel button is shown");
assert.dom(".setting-controls__cancel").doesNotHaveAttribute("disabled");
await click(".setting-controls__cancel");
assert.dom(".file-size-input").hasValue("1000");
assert.dom(".validation-error").hasClass("hidden");
assert
.dom(".setting-controls__ok")
.doesNotExist("the save button is not shown");
assert
.dom(".setting-controls__cancel")
.doesNotExist("the cancel button is shown");
});
}
);

View File

@ -3,6 +3,7 @@
.file-size-input { .file-size-input {
flex: 1; flex: 1;
margin-bottom: 0;
} }
.file-size-unit-selector { .file-size-unit-selector {

View File

@ -2525,6 +2525,7 @@ en:
file_size_input: file_size_input:
error: error:
size_too_large: "%{provided_file_size} is greater than the max allowed %{max_file_size}" size_too_large: "%{provided_file_size} is greater than the max allowed %{max_file_size}"
size_too_small: "%{provided_file_size} is smaller than the min allowed %{min_file_size}"
emoji_picker: emoji_picker:
filter_placeholder: Search for emoji filter_placeholder: Search for emoji