mirror of
https://github.com/discourse/discourse.git
synced 2026-08-08 12:08:12 -05:00
DEV: apply common filter component to AI persona admin (#34300)
This applies the common `AdminFilterControls` component here: <img width="2216" height="144" alt="image" src="https://github.com/user-attachments/assets/3e6ad0e9-9f14-4974-a80c-49e0d1b0b108" /> I also wanted to add the layout control button, so I added the ability to use an actions block in `AdminFilterControls` — this required creating two separate blocks `actions` and `content`, which required updating the other instances of `AdminFilterControls` ... but this should allow more flexibility going forward
This commit is contained in:
+138
-207
@@ -1,6 +1,6 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { concat, fn, hash } from "@ember/helper";
|
||||
import { concat, fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
@@ -8,9 +8,7 @@ import { gt } from "truth-helpers";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DPageSubheader from "discourse/components/d-page-subheader";
|
||||
import DSelect from "discourse/components/d-select";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import FilterInput from "discourse/components/filter-input";
|
||||
import avatar from "discourse/helpers/avatar";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
@@ -18,6 +16,7 @@ import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
|
||||
import AdminFilterControls from "admin/components/admin-filter-controls";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
import AiPersona from "../admin/models/ai-persona";
|
||||
import AiPersonaEditor from "./ai-persona-editor";
|
||||
@@ -41,8 +40,6 @@ export default class AiPersonaListEditor extends Component {
|
||||
@service capabilities;
|
||||
@service dialog;
|
||||
|
||||
@tracked filterValue = "";
|
||||
@tracked featureFilter = "all";
|
||||
@tracked currentLayout = LAYOUT_BUTTONS[0];
|
||||
|
||||
constructor() {
|
||||
@@ -56,44 +53,13 @@ export default class AiPersonaListEditor extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
get filteredPersonas() {
|
||||
let personas = this.args.personas || [];
|
||||
|
||||
// Filter by feature if not "all"
|
||||
if (this.featureFilter !== "all") {
|
||||
personas = personas.filter((persona) =>
|
||||
(persona.features || []).some(
|
||||
(feature) => feature.module_name === this.featureFilter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by search term if present
|
||||
if (this.filterValue) {
|
||||
const term = this.filterValue.toLowerCase();
|
||||
personas = personas.filter((persona) => {
|
||||
const textMatches =
|
||||
persona.name?.toLowerCase().includes(term) ||
|
||||
persona.description?.toLowerCase().includes(term);
|
||||
|
||||
const featureMatches = (persona.features || []).some((feature) =>
|
||||
feature.module_name?.toLowerCase().includes(term)
|
||||
);
|
||||
|
||||
const llmMatches = persona.default_llm?.display_name
|
||||
?.toLowerCase()
|
||||
.includes(term);
|
||||
|
||||
return textMatches || featureMatches || llmMatches;
|
||||
});
|
||||
}
|
||||
|
||||
return personas;
|
||||
get searchableProps() {
|
||||
return ["name", "description"];
|
||||
}
|
||||
|
||||
get featureFilterOptions() {
|
||||
get dropdownOptions() {
|
||||
let features = [];
|
||||
(this.args.personas || []).forEach((persona) => {
|
||||
(this.args.personas?.content || []).forEach((persona) => {
|
||||
(persona.features || []).forEach((feature) => {
|
||||
if (feature?.module_name && !features.includes(feature.module_name)) {
|
||||
features.push(feature.module_name);
|
||||
@@ -105,10 +71,15 @@ export default class AiPersonaListEditor extends Component {
|
||||
{
|
||||
value: "all",
|
||||
label: i18n("discourse_ai.ai_persona.filters.all_features"),
|
||||
filterFn: () => true,
|
||||
},
|
||||
...features.map((name) => ({
|
||||
value: name,
|
||||
label: i18n(`discourse_ai.features.${name}.name`),
|
||||
filterFn: (persona) =>
|
||||
(persona.features || []).some(
|
||||
(feature) => feature.module_name === name
|
||||
),
|
||||
})),
|
||||
];
|
||||
}
|
||||
@@ -127,23 +98,6 @@ export default class AiPersonaListEditor extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onNameFilterChange(event) {
|
||||
this.filterValue = event.target?.value || "";
|
||||
}
|
||||
|
||||
@action
|
||||
onFeatureFilterChange(value) {
|
||||
this.featureFilter = value;
|
||||
}
|
||||
|
||||
@action
|
||||
resetAndFocus() {
|
||||
this.filterValue = "";
|
||||
this.featureFilter = "all";
|
||||
document.querySelector(".admin-filter__input").focus();
|
||||
}
|
||||
|
||||
@action
|
||||
onRegisterApi(api) {
|
||||
this.dMenu = api;
|
||||
@@ -264,56 +218,135 @@ export default class AiPersonaListEditor extends Component {
|
||||
/>
|
||||
</:actions>
|
||||
</DPageSubheader>
|
||||
|
||||
{{#if @personas}}
|
||||
<div class="ai-persona-list-editor__controls">
|
||||
<FilterInput
|
||||
placeholder={{i18n "discourse_ai.ai_persona.filters.text"}}
|
||||
@filterAction={{this.onNameFilterChange}}
|
||||
@value={{this.filterValue}}
|
||||
class="admin-filter__input"
|
||||
@icons={{hash left="magnifying-glass"}}
|
||||
/>
|
||||
<DSelect
|
||||
@value={{this.featureFilter}}
|
||||
@includeNone={{false}}
|
||||
@onChange={{this.onFeatureFilterChange}}
|
||||
as |select|
|
||||
>
|
||||
{{#each this.featureFilterOptions as |option|}}
|
||||
<select.Option @value={{option.value}}>
|
||||
{{option.label}}
|
||||
</select.Option>
|
||||
{{/each}}
|
||||
</DSelect>
|
||||
{{#if this.capabilities.viewport.md}}
|
||||
<DMenu
|
||||
@modalForMobile={{true}}
|
||||
@autofocus={{true}}
|
||||
@identifier="persona-list-layout"
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
@triggerClass="btn-default btn-icon"
|
||||
<AdminFilterControls
|
||||
@array={{@personas.content}}
|
||||
@searchableProps={{this.searchableProps}}
|
||||
@dropdownOptions={{this.dropdownOptions}}
|
||||
@inputPlaceholder={{i18n "discourse_ai.ai_persona.filters.text"}}
|
||||
@noResultsMessage={{i18n
|
||||
"discourse_ai.ai_persona.filters.no_results"
|
||||
}}
|
||||
>
|
||||
<:actions>
|
||||
{{#if this.capabilities.viewport.md}}
|
||||
<DMenu
|
||||
@modalForMobile={{true}}
|
||||
@autofocus={{true}}
|
||||
@identifier="persona-list-layout"
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
@triggerClass="btn-default btn-icon"
|
||||
>
|
||||
<:trigger>
|
||||
{{icon this.currentLayout.icon}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
{{#each LAYOUT_BUTTONS as |button|}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label={{button.label}}
|
||||
@icon={{button.icon}}
|
||||
class="btn-transparent"
|
||||
@action={{fn this.onLayoutSelect button.id}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/each}}
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
{{/if}}
|
||||
</:actions>
|
||||
<:content as |filteredPersonas|>
|
||||
<table
|
||||
class={{concatClass
|
||||
"content-list ai-persona-list-editor d-admin-table"
|
||||
(concat "--layout-" this.currentLayout.id)
|
||||
}}
|
||||
>
|
||||
<:trigger>
|
||||
{{icon this.currentLayout.icon}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
{{#each LAYOUT_BUTTONS as |button|}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label={{button.label}}
|
||||
@icon={{button.icon}}
|
||||
class="btn-transparent"
|
||||
@action={{fn this.onLayoutSelect button.id}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/each}}
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
{{/if}}
|
||||
</div>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "discourse_ai.ai_persona.name"}}</th>
|
||||
<th>{{i18n "discourse_ai.llms.short_title"}}</th>
|
||||
<th>{{i18n "discourse_ai.features.short_title"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each filteredPersonas as |persona|}}
|
||||
<tr
|
||||
data-persona-id={{persona.id}}
|
||||
class={{concatClass
|
||||
"ai-persona-list__row d-admin-row__content"
|
||||
(if persona.priority "--priority")
|
||||
(if persona.enabled "--enabled")
|
||||
}}
|
||||
>
|
||||
<td class="d-admin-row__overview">
|
||||
<div class="ai-persona-list__name-with-description">
|
||||
<h3 class="ai-persona-list__name">
|
||||
{{#if persona.user}}
|
||||
{{avatar persona.user imageSize="tiny"}}
|
||||
{{/if}}
|
||||
{{persona.name}}
|
||||
</h3>
|
||||
<div class="ai-persona-list__description">
|
||||
{{persona.description}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-admin-row__llms">
|
||||
{{#if persona.default_llm}}
|
||||
<span class="--card-label">
|
||||
{{i18n "discourse_ai.ai_persona.llms_list"}}
|
||||
</span>
|
||||
<DButton
|
||||
class="btn-flat btn-small ai-persona-list__row-item-feature"
|
||||
@translatedLabel={{persona.default_llm.display_name}}
|
||||
@route="adminPlugins.show.discourse-ai-llms.edit"
|
||||
@routeModels={{persona.default_llm.id}}
|
||||
/>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__features">
|
||||
{{#if persona.features.length}}
|
||||
<span class="--card-label">
|
||||
{{i18n
|
||||
"discourse_ai.ai_persona.features_list"
|
||||
count=persona.features.length
|
||||
}}
|
||||
</span>
|
||||
{{#each persona.features as |feature index|}}
|
||||
<span class="d-admin-row__row-feature-list">
|
||||
{{#if (gt index 0)}}, {{/if}}
|
||||
<DButton
|
||||
class="btn-flat btn-small ai-persona-list__row-item-feature"
|
||||
@translatedLabel={{i18n
|
||||
(concat
|
||||
"discourse_ai.features."
|
||||
feature.module_name
|
||||
".name"
|
||||
)
|
||||
}}
|
||||
@route="adminPlugins.show.discourse-ai-features.edit"
|
||||
@routeModels={{feature.id}}
|
||||
/>
|
||||
</span>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls">
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-personas.edit"
|
||||
@model={{persona}}
|
||||
class="btn btn-text btn-small"
|
||||
>{{i18n "discourse_ai.ai_persona.edit"}} </LinkTo>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</:content>
|
||||
</AdminFilterControls>
|
||||
{{else}}
|
||||
<AdminConfigAreaEmptyList
|
||||
@ctaLabel="discourse_ai.ai_persona.new"
|
||||
@@ -322,108 +355,6 @@ export default class AiPersonaListEditor extends Component {
|
||||
@emptyLabel="discourse_ai.ai_persona.no_personas"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.filteredPersonas}}
|
||||
<table
|
||||
class={{concatClass
|
||||
"content-list ai-persona-list-editor d-admin-table"
|
||||
(concat "--layout-" this.currentLayout.id)
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "discourse_ai.ai_persona.name"}}</th>
|
||||
<th>{{i18n "discourse_ai.llms.short_title"}}</th>
|
||||
<th>{{i18n "discourse_ai.features.short_title"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.filteredPersonas as |persona|}}
|
||||
<tr
|
||||
data-persona-id={{persona.id}}
|
||||
class={{concatClass
|
||||
"ai-persona-list__row d-admin-row__content"
|
||||
(if persona.priority "--priority")
|
||||
(if persona.enabled "--enabled")
|
||||
}}
|
||||
>
|
||||
<td class="d-admin-row__overview">
|
||||
<div class="ai-persona-list__name-with-description">
|
||||
<h3 class="ai-persona-list__name">
|
||||
{{#if persona.user}}
|
||||
{{avatar persona.user imageSize="tiny"}}
|
||||
{{/if}}
|
||||
{{persona.name}}
|
||||
</h3>
|
||||
<div class="ai-persona-list__description">
|
||||
{{persona.description}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-admin-row__llms">
|
||||
{{#if persona.default_llm}}
|
||||
<span class="--card-label">
|
||||
{{i18n "discourse_ai.ai_persona.llms_list"}}
|
||||
</span>
|
||||
<DButton
|
||||
class="btn-flat btn-small ai-persona-list__row-item-feature"
|
||||
@translatedLabel={{persona.default_llm.display_name}}
|
||||
@route="adminPlugins.show.discourse-ai-llms.edit"
|
||||
@routeModels={{persona.default_llm.id}}
|
||||
/>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__features">
|
||||
{{#if persona.features.length}}
|
||||
<span class="--card-label">
|
||||
{{i18n
|
||||
"discourse_ai.ai_persona.features_list"
|
||||
count=persona.features.length
|
||||
}}
|
||||
</span>
|
||||
{{#each persona.features as |feature index|}}
|
||||
<span class="d-admin-row__row-feature-list">
|
||||
{{#if (gt index 0)}}, {{/if}}
|
||||
<DButton
|
||||
class="btn-flat btn-small ai-persona-list__row-item-feature"
|
||||
@translatedLabel={{i18n
|
||||
(concat
|
||||
"discourse_ai.features."
|
||||
feature.module_name
|
||||
".name"
|
||||
)
|
||||
}}
|
||||
@route="adminPlugins.show.discourse-ai-features.edit"
|
||||
@routeModels={{feature.id}}
|
||||
/>
|
||||
</span>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls">
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-personas.edit"
|
||||
@model={{persona}}
|
||||
class="btn btn-text btn-small"
|
||||
>{{i18n "discourse_ai.ai_persona.edit"}} </LinkTo>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="ai-persona-list-editor__no-results">
|
||||
|
||||
<h3>{{i18n "discourse_ai.ai_persona.filters.no_results"}}</h3>
|
||||
|
||||
<DButton
|
||||
@icon="arrow-rotate-left"
|
||||
@label="discourse_ai.ai_persona.filters.reset"
|
||||
@action={{this.resetAndFocus}}
|
||||
class="btn-default"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user