FIX: better standalone checkbox support (#31130)

Before this commit it was complicated to render a `Checkbox` outside of
a `CheckboxGroup` as you would get no title, no description, no optional
hint and not tooltip.

This commits makes all of this possible by adding a special case for
checkboxes, and sharing code for tooltips and optional hint.

This commit also uses this opportunity to refactor part of the code to
use curryComponent and reduce code duplication.
This commit is contained in:
Joffrey JAFFEUX
2025-02-04 09:58:00 +01:00
committed by GitHub
parent 41ce3d868e
commit 1a8b5b9d42
15 changed files with 197 additions and 195 deletions

View File

@ -13,7 +13,6 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
import ApiKeyUrlsModal from "admin/components/modal/api-key-urls";
import EmailGroupUserChooser from "select-kit/components/email-group-user-chooser";
import DTooltip from "float-kit/components/d-tooltip";
export default class AdminConfigAreasApiKeysNew extends Component {
@service router;
@ -239,7 +238,6 @@ export default class AdminConfigAreasApiKeysNew extends Component {
<table class="scopes-table grid">
<thead>
<tr>
<td></td>
<td></td>
<td>{{i18n "admin.api.scopes.allowed_urls"}}</td>
<td>{{i18n
@ -265,28 +263,18 @@ export default class AdminConfigAreasApiKeysNew extends Component {
<topicsCollection.Field
@name="enabled"
@title={{collectionData.key}}
@showTitle={{false}}
as |field|
>
<field.Checkbox />
</topicsCollection.Field>
</td>
<td>
<div
class="scope-name"
>{{collectionData.name}}</div>
<DTooltip
@icon="circle-question"
@content={{i18n
@tooltip={{i18n
(concat
"admin.api.scopes.descriptions."
scopeName
"."
collectionData.key
)
class="scope-tooltip"
}}
/>
as |field|
>
<field.Checkbox />
</topicsCollection.Field>
</td>
<td>
<DButton

View File

@ -257,13 +257,11 @@ export default class AdminConfigAreasWebhookForm extends Component {
</field.Custom>
</form.Field>
<span>
<PluginOutlet
@name="web-hook-fields"
@connectorTagName="div"
@outletArgs={{hash model=this.webhook}}
/>
</span>
<PluginOutlet
@name="web-hook-fields"
@connectorTagName="div"
@outletArgs={{hash model=this.webhook}}
/>
<form.Field
@name="verify_certificate"

View File

@ -1,31 +1,28 @@
import Component from "@glimmer/component";
import { concat, fn } from "@ember/helper";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import { eq } from "truth-helpers";
import FKLabel from "discourse/form-kit/components/fk/label";
import FKMeta from "discourse/form-kit/components/fk/meta";
import FKOptional from "discourse/form-kit/components/fk/optional";
import FKText from "discourse/form-kit/components/fk/text";
import FKTooltip from "discourse/form-kit/components/fk/tooltip";
import concatClass from "discourse/helpers/concat-class";
import { i18n } from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
export default class FKControlWrapper extends Component {
get controlType() {
if (this.args.component.controlType === "input") {
return this.args.component.controlType + "-" + (this.args.type || "text");
return this.args.field.type + "-" + (this.args.type || "text");
}
return this.args.component.controlType;
return this.args.field.type;
}
get error() {
return (this.args.errors ?? {})[this.args.field.name];
}
get isComponentTooltip() {
return typeof this.args.field.tooltip === "object";
}
get titleFormat() {
return this.args.field.titleFormat || this.args.field.format;
}
@ -52,42 +49,34 @@ export default class FKControlWrapper extends Component {
data-disabled={{@field.disabled}}
data-name={{@field.name}}
data-control-type={{this.controlType}}
{{didInsert (fn @registerField @field.name @field)}}
{{willDestroy (fn @unregisterField @field.name)}}
>
{{#if @field.showTitle}}
<FKLabel
class={{concatClass
"form-kit__container-title"
(if this.titleFormat (concat "--" this.titleFormat))
}}
@fieldId={{@field.id}}
>
<span>{{@field.title}}</span>
{{#unless (eq @field.type "checkbox")}}
{{#if @field.showTitle}}
<FKLabel
class={{concatClass
"form-kit__container-title"
(if this.titleFormat (concat "--" this.titleFormat))
}}
@fieldId={{@field.id}}
>
<span>{{@field.title}}</span>
{{#unless @field.required}}
<span class="form-kit__container-optional">({{i18n
"form_kit.optional"
}})</span>
{{/unless}}
<FKOptional @field={{@field}} />
<FKTooltip @field={{@field}} />
</FKLabel>
{{/if}}
{{#if @field.tooltip}}
{{#if this.isComponentTooltip}}
<@field.tooltip />
{{else}}
<DTooltip @icon="circle-question" @content={{@field.tooltip}} />
{{/if}}
{{/if}}
</FKLabel>
{{/if}}
{{#if @field.description}}
<FKText
class={{concatClass
"form-kit__container-description"
(if this.descriptionFormat (concat "--" this.descriptionFormat))
}}
>{{@field.description}}</FKText>
{{/if}}
{{#if @field.description}}
<FKText
class={{concatClass
"form-kit__container-description"
(if this.descriptionFormat (concat "--" this.descriptionFormat))
}}
>{{@field.description}}</FKText>
{{/if}}
{{/unless}}
<div
class={{concatClass

View File

@ -3,6 +3,8 @@ import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { eq } from "truth-helpers";
import FKLabel from "discourse/form-kit/components/fk/label";
import FKOptional from "discourse/form-kit/components/fk/optional";
import FKTooltip from "discourse/form-kit/components/fk/tooltip";
export default class FKControlCheckbox extends Component {
static controlType = "checkbox";
@ -24,7 +26,9 @@ export default class FKControlCheckbox extends Component {
/>
<span class="form-kit__control-checkbox-content">
<span class="form-kit__control-checkbox-title">
{{@field.title}}
<span>{{@field.title}}</span>
<FKOptional @field={{@field}} />
<FKTooltip @field={{@field}} />
</span>
{{#if (has-block)}}
<span class="form-kit__control-checkbox-description">{{yield}}</span>

View File

@ -1,4 +1,5 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import ValidationParser from "discourse/form-kit/lib/validation-parser";
import Validator from "discourse/form-kit/lib/validator";
@ -8,6 +9,12 @@ import uniqueId from "discourse/helpers/unique-id";
* Represents a field in a form with validation, registration, and field data management capabilities.
*/
export default class FKFieldData extends Component {
/**
* Type of the field.
* @type {string}
*/
@tracked type;
/**
* Unique identifier for the field.
* @type {string}
@ -20,12 +27,6 @@ export default class FKFieldData extends Component {
*/
errorId = uniqueId();
/**
* Type of the field.
* @type {string}
*/
type;
/**
* Initializes the FKFieldData component.
* Validates the presence of required arguments and registers the field.
@ -37,8 +38,6 @@ export default class FKFieldData extends Component {
if (!this.args.title?.length) {
throw new Error("@title is required on `<form.Field />`.");
}
this.args.registerField(this.name, this);
}
/**

View File

@ -1,5 +1,8 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import { getOwner } from "@ember/owner";
import curryComponent from "ember-curry-component";
import FKControlCheckbox from "discourse/form-kit/components/fk/control/checkbox";
import FKControlCode from "discourse/form-kit/components/fk/control/code";
import FKControlComposer from "discourse/form-kit/components/fk/control/composer";
@ -40,6 +43,30 @@ export default class FKField extends Component {
}
}
@action
componentFor(component, field) {
const instance = this;
const baseArguments = {
get errors() {
return instance.args.errors;
},
unregisterField: instance.args.unregisterField,
registerField: instance.args.registerField,
component,
field,
};
if (!component.controlType) {
throw new Error(
`Static property \`controlType\` is required on component:\n\n ${component}`
);
}
field.type = component.controlType;
return curryComponent(FKControlWrapper, baseArguments, getOwner(this));
}
<template>
<FKFieldData
@name={{@name}}
@ -66,104 +93,20 @@ export default class FKField extends Component {
<this.wrapper @size={{@size}}>
{{yield
(hash
Custom=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlCustom
field=field
)
Code=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlCode
field=field
)
Question=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlQuestion
field=field
)
Textarea=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlTextarea
field=field
)
Checkbox=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlCheckbox
field=field
)
Image=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlImage
field=field
)
Password=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlPassword
field=field
)
Composer=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlComposer
field=field
)
Icon=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlIcon
field=field
)
Toggle=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlToggle
field=field
)
Menu=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlMenu
field=field
)
Select=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlSelect
field=field
)
Input=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlInput
field=field
)
RadioGroup=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlRadioGroup
field=field
)
Custom=(this.componentFor FKControlCustom field)
Code=(this.componentFor FKControlCode field)
Question=(this.componentFor FKControlQuestion field)
Textarea=(this.componentFor FKControlTextarea field)
Checkbox=(this.componentFor FKControlCheckbox field)
Image=(this.componentFor FKControlImage field)
Password=(this.componentFor FKControlPassword field)
Composer=(this.componentFor FKControlComposer field)
Icon=(this.componentFor FKControlIcon field)
Toggle=(this.componentFor FKControlToggle field)
Menu=(this.componentFor FKControlMenu field)
Select=(this.componentFor FKControlSelect field)
Input=(this.componentFor FKControlInput field)
RadioGroup=(this.componentFor FKControlRadioGroup field)
errorId=field.errorId
id=field.id
name=field.name

View File

@ -0,0 +1,11 @@
import { i18n } from "discourse-i18n";
const FKOptional = <template>
{{#unless @field.required}}
<span class="form-kit__container-optional">({{i18n
"form_kit.optional"
}})</span>
{{/unless}}
</template>;
export default FKOptional;

View File

@ -0,0 +1,22 @@
import Component from "@glimmer/component";
import DTooltip from "float-kit/components/d-tooltip";
export default class FKTooltip extends Component {
get isComponentTooltip() {
return typeof this.args.field.tooltip === "object";
}
<template>
{{#if @field.tooltip}}
{{#if this.isComponentTooltip}}
<@field.tooltip />
{{else}}
<DTooltip
class="form-kit__tooltip"
@icon="circle-question"
@content={{@field.tooltip}}
/>
{{/if}}
{{/if}}
</template>
}

View File

@ -44,5 +44,31 @@ module(
assert.dom(".form-kit__control-checkbox").hasAttribute("disabled");
});
test("@tooltip", async function (assert) {
await render(<template>
<Form as |form|>
<form.Field @tooltip="test" @name="foo" @title="Foo" as |field|>
<field.Checkbox />
</form.Field>
</Form>
</template>);
assert
.dom(".form-kit__control-checkbox-content .form-kit__tooltip")
.exists();
});
test("optional", async function (assert) {
await render(<template>
<Form as |form|>
<form.Field @name="foo" @title="Foo" as |field|>
<field.Checkbox />
</form.Field>
</Form>
</template>);
assert.form().field("foo").hasTitle("Foo (optional)");
});
}
);

View File

@ -83,8 +83,8 @@ module("Integration | Component | FormKit | Field", function (hooks) {
await render(<template>
<Form as |form|>
<form.Field @name="foo.bar" @title="Foo" @size={{8}}>
Test
<form.Field @name="foo.bar" @title="Foo" @size={{8}} as |field|>
<field.Input />
</form.Field>
</Form>
</template>);
@ -102,8 +102,8 @@ module("Integration | Component | FormKit | Field", function (hooks) {
await render(<template>
<Form as |form|>
<form.Field @name="foo" @size={{8}}>
Test
<form.Field @name="foo" @size={{8}} as |field|>
<field.Input />
</form.Field>
</Form>
</template>);

View File

@ -22,9 +22,41 @@ module(
</Form>
</template>);
assert.form().field("foo").hasTitle("Foo");
assert.form().field("bar").hasTitle("Bar");
assert.form().field("foo").hasTitle("Foo (optional)");
assert.form().field("bar").hasTitle("Bar (optional)");
assert.form().field("bar").hasDescription("A description");
});
test("@title", async function (assert) {
await render(<template>
<Form as |form|>
<form.CheckboxGroup @title="bar" as |checkboxGroup|>
<checkboxGroup.Field @name="foo" @title="Foo" as |field|>
<field.Checkbox />
</checkboxGroup.Field>
</form.CheckboxGroup>
</Form>
</template>);
assert
.dom(".form-kit__checkbox-group .form-kit__fieldset-title")
.hasText("bar");
});
test("@description", async function (assert) {
await render(<template>
<Form as |form|>
<form.CheckboxGroup @description="bar" as |checkboxGroup|>
<checkboxGroup.Field @name="foo" @title="Foo" as |field|>
<field.Checkbox />
</checkboxGroup.Field>
</form.CheckboxGroup>
</Form>
</template>);
assert
.dom(".form-kit__checkbox-group .form-kit__fieldset-description")
.hasText("bar");
});
}
);

View File

@ -811,14 +811,6 @@ $mobile-breakpoint: 700px;
}
}
.badges,
.web-hook-container {
input[type="text"],
textarea {
min-width: 350px;
}
}
.text-successful {
color: var(--success);
}

View File

@ -24,6 +24,10 @@
// Api keys
.admin-api-keys {
.form-kit__container-optional {
display: none;
}
.api-key-show {
.form-element,
.form-element-desc {
@ -136,16 +140,6 @@
display: none;
}
input {
max-width: calc(100% - 10px);
}
.select-kit,
.select-kit.multi-select {
width: 100%;
max-width: 360px;
}
.event-selector {
display: grid;
grid-template-columns: auto auto;

View File

@ -30,3 +30,4 @@
@import "_row";
@import "_section";
@import "_variables";
@import "_tooltip";

View File

@ -0,0 +1,3 @@
.form-kit__tooltip {
color: var(--primary-medium);
}