This commit introduces the `behaviorTransformer` API to safely override behaviors defined in Discourse.
Two new plugin APIs are introduced:
- `addBehaviorTransformerName` which allows plugins and theme-components to add a new valid transformer name if they want to provide overridable behaviors;
- `registerBehaviorTransformer` to register a transformer to override behaviors.
It also introduces the function `applyBehaviorTransformer` which can be imported from `discourse/lib/transformer`. This is used to mark a callback containing the desired behavior as overridable and applies the transformer logic.
How does it work?
## Marking a behavior as overridable:
To mark a behavior as overridable, in Discourse core, first the transformer name must be added to `app/assets/javascripts/discourse/app/lib/transformer/registry.js`. For plugins and theme-components, use the plugin API `addBehaviorTransformerName` instead.
Then, in your component or class, use the function `applyBehaviorTransformer` to mark the Behavior as overridable and handle the logic:
- example:
```js
...
@action
loadMore() {
applyBehaviorTransformer(
"discovery-topic-list-load-more",
() => {
this.documentTitle.updateContextCount(0);
return this.model
.loadMore()
.then(({ moreTopicsUrl, newTopics } = {}) => {
if (
newTopics &&
newTopics.length &&
this.bulkSelectHelper?.bulkSelectEnabled
) {
this.bulkSelectHelper.addTopics(newTopics);
}
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
this.send("loadMore");
}
});
},
{ model: this.model }
);
},
...
```
## Overriding a behavior in plugins or themes
To override a behavior in plugins, themes, or TCs use the plugin API `registerBehaviorTransformer`:
- Example:
```js
withPluginApi("1.35.0", (api) => {
api.registerBehaviorTransformer("example-transformer", ({ context, next }) => {
console.log('we can introduce new behavior here instead', context);
next(); // call next to execute the expected behavior
});
});
```
- Remove image
- Move “if you need to get back here” blurb below main checklist
- Note that each checklist item is a section in the doc
- Add nudge to create a topic in the “Discuss ideas” section
- Move “please join meta” blurb into email configuration section
- Fix a typo and remove some trailing whitespace
This commit introduces the `valueTransformer`API to safely override values defined in Discourse.
Two new plugin APIs are introduced:
- `addValueTransformerName` which allows plugins and theme-components to add a new valid transformer name if they want to provide overridable values;
- `registerValueTransformer` to register a transformer to override values.
It also introduces the function `applyValueTransformer` which can be imported from `discourse/lib/transformer`. This function marks the desired value as overridable and applies the transformer logic.
How does it work?
## Marking a value as overridable:
To mark a value as overridable, in Discourse core, first the transformer name must be added to `app/assets/javascripts/discourse/app/lib/transformer/registry.js`. For plugins and theme-components, use the plugin API `addValueTransformerName` instead.
Then, in your component or class, use the function `applyValueTransformer` to mark the value as overridable and handle the logic:
- example:
```js
export default class HomeLogo extends Component {
@service session;
@service site;
...
get href() {
return applyValueTransformer("home-logo-href", getURL("/"));
}
```
## Overriding a value in plugins or themes
To override a value in plugins, themes, or TCs use the plugin API `registerValueTransformer`:
- Example:
```js
withPluginApi("1.34.0", (api) => {
api.registerValueTransformer("example-transformer", ({ value }) => {
return "new-value";
});
});
```
# Description
Add `addCustomUserFieldValidationCallback` to the user fields validation mixin. This allows you to add a custom validation when checking the validity of custom user field values in the signup form on submit.
```js
addCustomUserFieldValidationCallback((userField) => {
if (userField.field.name === "my custom user field" && userField.value === "foo") {
return EmberObject.create({
failed: true,
reason: I18n.t("value_can_not_be_foo"),
element: userField.field.element,
});
}
});
```
In the case your custom validation deems an input value `failed`, you return an EmberObject with the fields `failed: true`, `reason`, and `element`.
```js
return EmberObject.create({
failed: true,
reason: I18n.t("value_can_not_be_foo"),
element: userField.field.element,
});
```
which will then display your custom `reason` to the user attached to the given user custom field input and will not submit the signup form.
<img width="288" alt="Screenshot 2024-06-06 at 11 08 40 AM" src="https://github.com/discourse/discourse/assets/50783505/11168fb8-8806-43f0-9417-73991bbd1178">
# Other
- Add `addCustomUserFieldValidationCallback` to the plugin api
- Bump plugin api version
- Update plugin api changelog
- Add tests
- Use a single checklist at the top
- Link to appropriate settings where appropriate
- Encourage people to invite others earlier
- Encourage iteration prior to launch
- Move most external links to bottom "Learn More" section
I fixed some links in the admin getting started guide, and changed the way internal links to categories are handled so they will work in other language locales besides US english.
This commit changes the API for registering the plugin config
page nav configuration from a server-side to a JS one;
there is no need for it to be server-side.
It also makes some changes to allow for 2 different ways of displaying
navigation for plugin pages, depending on complexity:
* TOP - This is the best mode for simple plugins without a lot of different
custom configuration pages, and it reuses the grey horizontal nav bar
already used for admins.
* SIDEBAR - This is better for more complex plugins; likely this won't
be used in the near future, but it's readily available if needed
There is a new AdminPluginConfigNavManager service too to manage which
plugin the admin is actively viewing, otherwise we would have trouble
hiding the main plugin nav for admins when viewing a single plugin.
Recently, Discourse introduced the concept of wrapper plugin outlets, which enables plugins and theme-components lo replace the wrapped content:
```
<PluginOutlet @name="wrapper-outlet-example" @outletArgs={{hash model=@model}}>
<div>Overridable content</div>
</PluginOutlet>
```
This commit adds automatic outlets that are placed `before` and `after wrapper plugin outlets. Connectors them can leverage these new automatic outlets to mount content at these positions, which greatly enhances the use case of the wrapper outlets.
These new auto outlets can be used in two ways:
- Using the standard folder base structure: the folder name that identifies the outlet in which the connector must be mounted must add the suffixes `__before`or `__after` to the outlet name. For the outlet in the example above, the connector should be placed into the `.../connectors/wrapper-outlet-example__before`or `.../connectors/wrapper-outlet-example__after`folders.
- Using API calls: this commit also introduces two new plugin APIs, `api.renderBeforeWrapperOutlet` and `renderAfterWrapperOutlet`. These new APIs can be used in the same way as `api.renderInOutlet`but will only work for wrapper outlets.
For the outlet above when using these new APIs alongside the gjs file format, one could define a component to be placed before the content of the outlet like:
```
api.renderBeforeWrapperOutlet('wrapper-outlet-example', <template>Hello from before the content</template>);
```
or after:
```
api.renderAfterWrapperOutlet('wrapper-outlet-example', <template>Hello from after the content</template>);
```
This commit adds some more links to the admin sidebar and
removes some to give it more parity with the old nav structure.
This also adds the `addAdminSidebarSectionLink` plugin API to
replace the admin-menu plugin outlet, which is used by plugins
like docker-manager to add links to the old admin nav.
- Add plugin outlet to `AdminUserFieldItem`
- Add ability to include custom fields when saving `AdminUserFieldItem`
- Update plugin API with `includeUserFieldPropertiesOnSave` per ☝️
- Add `DiscoursePluginRegistry` to `UserFieldsController` to add custom columns
This commit adds a new admin UI under the route `/admin-revamp`, which is
only accessible if the user is in a group defined by the new `enable_experimental_admin_ui_groups` site setting. It
also adds a special `admin` sidebar panel that is shown instead of the `main`
forum one when the admin is in this area.
![image](https://github.com/discourse/discourse/assets/920448/fa0f25e1-e178-4d94-aa5f-472fd3efd787)
We also add an "Admin Revamp" sidebar link to the community section, which
will only appear if the user is in the setting group:
![image](https://github.com/discourse/discourse/assets/920448/ec05ca8b-5a54-442b-ba89-6af35695c104)
Within this there are subroutes defined like `/admin-revamp/config/:area`,
these areas could contain any UI imaginable, this is just laying down an
initial idea of the structure and how the sidebar will work. Sidebar links are
currently hardcoded.
Some other changes:
* Changed the `main` and `chat` panels sidebar panel keys to use exported const values for reuse
* Allowed custom sidebar sections to hide their headers with the `hideSectionHeader` option
* Add a `groupSettingArray` setting on `this.siteSettings` in JS, which accepts a group site setting name
and splits it by `|` then converts the items in the array to integers, similar to the `_map` magic for ruby
group site settings
* Adds a `hidden` option for sidebar panels which prevents them from showing in separated mode and prevents
the switch button from being shown
---------
Co-authored-by: Krzysztof Kotlarek <kotlarek.krzysztof@gmail.com>
Why this change?
Previously just using the `addToolbarPopupMenuOptionsCallback` plugin
API itself was insufficient because it required the return object to
include an `action` key which only accepted a name of the action
function as a string. This was highly problematic because the action
function had to be defined on the `composer` service which means using
the `modifyClass` API to add the action function. This made the API
awkward to use leading to poor developer experiencec.
What does this change do?
This commit introduces a couple of improvemnts to the API.
1. First the API has been renamed to `addComposerToolbarPopupMenuOption` because
the API no longer accepts a callback function which was quite
redundant. Instead, it now accepts an Object. The
`addToolbarPopupMenuOptionsCallback` API function is deprecated and
will be dropped in Discourse 3.3. Note that passing the API a
function is still supported but will be dropped when the `addToolbarPopupMenuOptionsCallback`
is removed.
2. The `action` key in the Object passed to the function can now be a
function and is passed the `toolbarEvent` object when called.
3. The `condition` on key in the Object passed to the function can now be a
function and is passed the `composer` service when called.
Until now, plugins/themes had to follow very specific directory structures to set up plugin outlet connectors. This commit introduces a new `api.renderInOutlet` API which makes things much more flexible. Any Ember component definition can be passed to this API, and will then be rendered into the named outlet.
For example:
```javascript
import MyComponent from "discourse/plugins/my-plugin/components/my-component";
api.renderInOutlet('user-profile-primary', MyComponent);
```
When using this API alongside the gjs file format, components can be defined inline like
```javascript
api.renderInOutlet('user-profile-primary', <template>Hello world</template>);
```
This PR introduces three new concepts to Discourse codebase through an addon called "FloatKit":
- menu
- tooltip
- toast
## Tooltips
### Component
Simple cases can be express with an API similar to DButton:
```hbs
<DTooltip
@Label={{i18n "foo.bar"}}
@ICON="check"
@content="Something"
/>
```
More complex cases can use blocks:
```hbs
<DTooltip>
<:trigger>
{{d-icon "check"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
Something
</:content>
</DTooltip>
```
### Service
You can manually show a tooltip using the `tooltip` service:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
options
)
// and later manual close or destroy it
tooltipInstance.close();
tooltipInstance.destroy();
// you can also just close any open tooltip through the service
this.tooltip.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const tooltipInstance = this.tooltip.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
tooltipInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
## Menus
Menus are very similar to tooltips and provide the same kind of APIs:
### Component
```hbs
<DMenu @ICON="plus" @Label={{i18n "foo.bar"}}>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</DMenu>
```
They also support blocks:
```hbs
<DMenu>
<:trigger>
{{d-icon "plus"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</:content>
</DMenu>
```
### Service
You can manually show a menu using the `menu` service:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
options
)
// and later manual close or destroy it
menuInstance.close();
menuInstance.destroy();
// you can also just close any open tooltip through the service
this.menu.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const menuInstance = this.menu.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
menuInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
## Toasts
Interacting with toasts is made only through the `toasts` service.
A default component is provided (DDefaultToast) and can be used through dedicated service methods:
- this.toasts.success({ ... });
- this.toasts.warning({ ... });
- this.toasts.info({ ... });
- this.toasts.error({ ... });
- this.toasts.default({ ... });
```javascript
this.toasts.success({
data: {
title: "Foo",
message: "Bar",
actions: [
{
label: "Ok",
class: "btn-primary",
action: (componentArgs) => {
// eslint-disable-next-line no-alert
alert("Closing toast:" + componentArgs.data.title);
componentArgs.close();
},
}
]
},
});
```
You can also provide your own component:
```javascript
this.toasts.show(MyComponent, {
autoClose: false,
class: "foo",
data: { baz: 1 },
})
```
Co-authored-by: Martin Brennan <mjrbrennan@gmail.com>
Co-authored-by: Isaac Janzen <50783505+janzenisaac@users.noreply.github.com>
Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: Jarek Radosz <jradosz@gmail.com>
Second iteration of https://github.com/discourse/discourse/pull/23312 with a fix for embroider not resolving an export file using .gjs extension.
---
This PR introduces three new concepts to Discourse codebase through an addon called "FloatKit":
- menu
- tooltip
- toast
## Tooltips
### Component
Simple cases can be express with an API similar to DButton:
```hbs
<DTooltip
@label={{i18n "foo.bar"}}
@icon="check"
@content="Something"
/>
```
More complex cases can use blocks:
```hbs
<DTooltip>
<:trigger>
{{d-icon "check"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
Something
</:content>
</DTooltip>
```
### Service
You can manually show a tooltip using the `tooltip` service:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
options
)
// and later manual close or destroy it
tooltipInstance.close();
tooltipInstance.destroy();
// you can also just close any open tooltip through the service
this.tooltip.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const tooltipInstance = this.tooltip.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
tooltipInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
## Menus
Menus are very similar to tooltips and provide the same kind of APIs:
### Component
```hbs
<DMenu @icon="plus" @label={{i18n "foo.bar"}}>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</DMenu>
```
They also support blocks:
```hbs
<DMenu>
<:trigger>
{{d-icon "plus"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</:content>
</DMenu>
```
### Service
You can manually show a menu using the `menu` service:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
options
)
// and later manual close or destroy it
menuInstance.close();
menuInstance.destroy();
// you can also just close any open tooltip through the service
this.menu.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const menuInstance = this.menu.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
menuInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
## Toasts
Interacting with toasts is made only through the `toasts` service.
A default component is provided (DDefaultToast) and can be used through dedicated service methods:
- this.toasts.success({ ... });
- this.toasts.warning({ ... });
- this.toasts.info({ ... });
- this.toasts.error({ ... });
- this.toasts.default({ ... });
```javascript
this.toasts.success({
data: {
title: "Foo",
message: "Bar",
actions: [
{
label: "Ok",
class: "btn-primary",
action: (componentArgs) => {
// eslint-disable-next-line no-alert
alert("Closing toast:" + componentArgs.data.title);
componentArgs.close();
},
}
]
},
});
```
You can also provide your own component:
```javascript
this.toasts.show(MyComponent, {
autoClose: false,
class: "foo",
data: { baz: 1 },
})
```
Co-authored-by: Martin Brennan <mjrbrennan@gmail.com>
Co-authored-by: Isaac Janzen <50783505+janzenisaac@users.noreply.github.com>
Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: Jarek Radosz <jradosz@gmail.com>
This PR introduces three new UI elements to Discourse codebase through an addon called "FloatKit":
- menu
- tooltip
- toast
Simple cases can be express with an API similar to DButton:
```hbs
<DTooltip
@label={{i18n "foo.bar"}}
@icon="check"
@content="Something"
/>
```
More complex cases can use blocks:
```hbs
<DTooltip>
<:trigger>
{{d-icon "check"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
Something
</:content>
</DTooltip>
```
You can manually show a tooltip using the `tooltip` service:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
options
)
// and later manually close or destroy it
tooltipInstance.close();
tooltipInstance.destroy();
// you can also just close any open tooltip through the service
this.tooltip.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const tooltipInstance = this.tooltip.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
tooltipInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
Menus are very similar to tooltips and provide the same kind of APIs:
```hbs
<DMenu @icon="plus" @label={{i18n "foo.bar"}}>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</DMenu>
```
They also support blocks:
```hbs
<DMenu>
<:trigger>
{{d-icon "plus"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</:content>
</DMenu>
```
You can manually show a menu using the `menu` service:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
options
)
// and later manually close or destroy it
menuInstance.close();
menuInstance.destroy();
// you can also just close any open tooltip through the service
this.menu.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const menuInstance = this.menu.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
menuInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
Interacting with toasts is made only through the `toasts` service.
A default component is provided (DDefaultToast) and can be used through dedicated service methods:
- this.toasts.success({ ... });
- this.toasts.warning({ ... });
- this.toasts.info({ ... });
- this.toasts.error({ ... });
- this.toasts.default({ ... });
```javascript
this.toasts.success({
data: {
title: "Foo",
message: "Bar",
actions: [
{
label: "Ok",
class: "btn-primary",
action: (componentArgs) => {
// eslint-disable-next-line no-alert
alert("Closing toast:" + componentArgs.data.title);
componentArgs.close();
},
}
]
},
});
```
You can also provide your own component:
```javascript
this.toasts.show(MyComponent, {
autoClose: false,
class: "foo",
data: { baz: 1 },
})
```
Co-authored-by: Martin Brennan <mjrbrennan@gmail.com>
Co-authored-by: Isaac Janzen <50783505+janzenisaac@users.noreply.github.com>
Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: Jarek Radosz <jradosz@gmail.com>
There is a case when developer would like to go to separated mode but not show switch panel buttons. We need additional functions to show/add buttons to support this case.
This is a fairly extensive update to add checklists and to reorganize the presented information so it is more accessible. Some info is relegated to links to docs on meta. For convenience and regular reference, this topic is now linked to from a GETTING STARTED button next to the site title as long as the site is in bootstrap mode.
What does this change do?
This commit removes the experimental label for a bunch of APIs that have
been used in production for quite some time at Discourse so that the
APIs can be released as part of Discourse 3.1