mirror of
https://github.com/discourse/discourse.git
synced 2026-08-04 10:23:17 -05:00
FEATURE: Introduce new components listing page (#32164)
Follow-up to https://github.com/discourse/discourse/pull/31887 This commit introduces a new design for the components listing page, which is not linked from anywhere in the UI at the moment, but it can be accessed by heading to the `/admin/config/customize/components` path directly. We'll make this new design available from the sidebar and remove the old page once we've tested and validated the new design internally. Internal topic: t/146007. --------- Co-authored-by: Ella <ella.estigoy@gmail.com>
This commit is contained in:
@@ -1,17 +1,66 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { array, concat, hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DPageSubheader from "discourse/components/d-page-subheader";
|
||||
import DSelect from "discourse/components/d-select";
|
||||
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import FilterInput from "discourse/components/filter-input";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { extractErrorInfo } from "discourse/lib/ajax-error";
|
||||
import discourseDebounce from "discourse/lib/debounce";
|
||||
import { INPUT_DELAY } from "discourse/lib/environment";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import InstallThemeCard from "admin/components/admin-config-area-cards/install-theme-card";
|
||||
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
|
||||
import InstallComponentModal from "admin/components/modal/install-theme";
|
||||
import ThemesGrid from "admin/components/themes-grid";
|
||||
import { COMPONENTS } from "admin/models/theme";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
|
||||
const STATUS_FILTER_OPTIONS = [
|
||||
{
|
||||
value: "all",
|
||||
label: "admin.config_areas.themes_and_components.components.filter_by_all",
|
||||
},
|
||||
{
|
||||
value: "active",
|
||||
label:
|
||||
"admin.config_areas.themes_and_components.components.filter_by_active",
|
||||
},
|
||||
{
|
||||
value: "inactive",
|
||||
label:
|
||||
"admin.config_areas.themes_and_components.components.filter_by_inactive",
|
||||
},
|
||||
{
|
||||
value: "updates_available",
|
||||
label:
|
||||
"admin.config_areas.themes_and_components.components.filter_by_updates_available",
|
||||
},
|
||||
];
|
||||
|
||||
export default class AdminConfigAreasComponents extends Component {
|
||||
@service modal;
|
||||
@service router;
|
||||
@service toasts;
|
||||
|
||||
@tracked loading = true;
|
||||
@tracked components = [];
|
||||
@tracked nameFilter;
|
||||
@tracked statusFilter;
|
||||
@tracked hasComponents = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.load();
|
||||
}
|
||||
|
||||
@action
|
||||
installModal() {
|
||||
this.modal.show(InstallComponentModal, {
|
||||
@@ -28,7 +77,7 @@ export default class AdminConfigAreasComponents extends Component {
|
||||
selectedType: COMPONENTS,
|
||||
userId: null,
|
||||
content: [],
|
||||
installedThemes: this.args.components,
|
||||
installedThemes: this.components,
|
||||
addTheme: this.addComponent,
|
||||
updateSelectedType: () => {},
|
||||
showComponentsOnly: true,
|
||||
@@ -45,19 +94,444 @@ export default class AdminConfigAreasComponents extends Component {
|
||||
},
|
||||
duration: 2000,
|
||||
});
|
||||
this.router.refresh();
|
||||
this.load();
|
||||
}
|
||||
|
||||
@action
|
||||
onNameFilterChange(event) {
|
||||
this.loading = true;
|
||||
this.nameFilter = event.target.value;
|
||||
discourseDebounce(this, this.load, INPUT_DELAY);
|
||||
}
|
||||
|
||||
@action
|
||||
onStatusFilterChange(value) {
|
||||
this.statusFilter = value;
|
||||
this.load();
|
||||
}
|
||||
|
||||
@action
|
||||
async load() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const data = await ajax("/admin/config/customize/components", {
|
||||
data: { name: this.nameFilter, status: this.statusFilter },
|
||||
});
|
||||
|
||||
this.components = data.components;
|
||||
|
||||
if (!this.hasComponents && !this.nameFilter && !this.statusFilter) {
|
||||
this.hasComponents = !!data.components.length;
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="admin-detail">
|
||||
<ThemesGrid @themes={{@components}}>
|
||||
<:specialCard>
|
||||
<InstallThemeCard
|
||||
@component={{true}}
|
||||
@openModal={{this.installModal}}
|
||||
/>
|
||||
</:specialCard>
|
||||
</ThemesGrid>
|
||||
<DPageSubheader
|
||||
@titleLabel={{i18n
|
||||
"admin.config_areas.themes_and_components.components.title"
|
||||
}}
|
||||
@descriptionLabel={{i18n
|
||||
"admin.config_areas.themes_and_components.components.description"
|
||||
}}
|
||||
>
|
||||
<:actions as |actions|>
|
||||
<actions.Primary
|
||||
disabled={{this.loading}}
|
||||
@label="admin.config_areas.themes_and_components.components.install"
|
||||
@action={{this.installModal}}
|
||||
/>
|
||||
</:actions>
|
||||
</DPageSubheader>
|
||||
<div class="container">
|
||||
{{#if this.hasComponents}}
|
||||
<div class="d-admin-filter">
|
||||
<div
|
||||
class="admin-filter__input-container admin-config-components__name-filter"
|
||||
>
|
||||
<FilterInput
|
||||
placeholder={{i18n
|
||||
"admin.config_areas.themes_and_components.components.search_components"
|
||||
}}
|
||||
@filterAction={{this.onNameFilterChange}}
|
||||
class="admin-filter__input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="admin-config-components__status-filter">
|
||||
{{i18n
|
||||
"admin.config_areas.themes_and_components.components.filter_by"
|
||||
}}
|
||||
<DSelect
|
||||
@value="all"
|
||||
@includeNone={{false}}
|
||||
@onChange={{this.onStatusFilterChange}}
|
||||
as |select|
|
||||
>
|
||||
{{#each STATUS_FILTER_OPTIONS as |option|}}
|
||||
<select.Option @value={{option.value}}>
|
||||
{{i18n option.label}}
|
||||
</select.Option>
|
||||
{{/each}}
|
||||
</DSelect>
|
||||
</label>
|
||||
</div>
|
||||
{{/if}}
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
||||
{{#if this.components.length}}
|
||||
<table class="d-admin-table component-list">
|
||||
<thead>
|
||||
<th>{{i18n
|
||||
"admin.config_areas.themes_and_components.components.name"
|
||||
}}</th>
|
||||
<th>{{i18n
|
||||
"admin.config_areas.themes_and_components.components.used_on"
|
||||
}}</th>
|
||||
<th>{{i18n
|
||||
"admin.config_areas.themes_and_components.components.enabled"
|
||||
}}</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.components as |comp|}}
|
||||
<ComponentRow @component={{comp}} @refresh={{this.load}} />
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
{{#if this.hasComponents}}
|
||||
{{i18n
|
||||
"admin.config_areas.themes_and_components.components.no_components_found"
|
||||
}}
|
||||
{{else}}
|
||||
<AdminConfigAreaEmptyList
|
||||
@emptyLabel="admin.config_areas.themes_and_components.components.no_components"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ConditionalLoadingSpinner>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
||||
class ComponentRow extends Component {
|
||||
@service toasts;
|
||||
@service dialog;
|
||||
|
||||
@tracked enabled = this.args.component.enabled;
|
||||
@tracked hasUpdates = this.args.component.remote_theme?.commits_behind > 0;
|
||||
@tracked disableToggle = false;
|
||||
@tracked checkingForUpdates = false;
|
||||
@tracked updating = false;
|
||||
|
||||
get parentThemesCell() {
|
||||
const names = this.args.component.parent_themes.map((theme) => theme.name);
|
||||
names.sort();
|
||||
|
||||
if (!names.length) {
|
||||
return;
|
||||
} else if (names.length === 1) {
|
||||
return names[0];
|
||||
} else if (names.length === 2) {
|
||||
return i18n(
|
||||
"admin.config_areas.themes_and_components.components.parent_themes_two",
|
||||
{
|
||||
name1: names[0],
|
||||
name2: names[1],
|
||||
}
|
||||
);
|
||||
} else if (names.length === 3) {
|
||||
return i18n(
|
||||
"admin.config_areas.themes_and_components.components.parent_themes_three",
|
||||
{
|
||||
name1: names[0],
|
||||
name2: names[1],
|
||||
name3: names[2],
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return i18n(
|
||||
"admin.config_areas.themes_and_components.components.parent_themes_more_than_three",
|
||||
{
|
||||
name1: names[0],
|
||||
name2: names[1],
|
||||
name3: names[2],
|
||||
count: names.length - 3,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async toggleEnabled() {
|
||||
this.disableToggle = true;
|
||||
try {
|
||||
const data = await this.save({ enabled: !this.enabled });
|
||||
this.enabled = data.theme.enabled;
|
||||
} finally {
|
||||
this.disableToggle = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async checkForUpdates() {
|
||||
this.checkingForUpdates = true;
|
||||
|
||||
try {
|
||||
const data = await this.save({ remote_check: true });
|
||||
if (data.theme.remote_theme.commits_behind > 0) {
|
||||
this.hasUpdates = true;
|
||||
this.toasts.default({
|
||||
duration: 5000,
|
||||
data: {
|
||||
message: i18n(
|
||||
"admin.config_areas.themes_and_components.components.new_update_for_component",
|
||||
{ name: this.args.component.name }
|
||||
),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.hasUpdates = false;
|
||||
this.toasts.default({
|
||||
duration: 5000,
|
||||
data: {
|
||||
message: i18n(
|
||||
"admin.config_areas.themes_and_components.components.component_up_to_date",
|
||||
{ name: this.args.component.name }
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
this.checkingForUpdates = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async updateToLatest() {
|
||||
this.updating = true;
|
||||
|
||||
try {
|
||||
await this.save({ remote_update: true });
|
||||
this.hasUpdates = false;
|
||||
this.toasts.success({
|
||||
duration: 5000,
|
||||
data: {
|
||||
message: i18n(
|
||||
"admin.config_areas.themes_and_components.components.updated_successfully",
|
||||
{ name: this.args.component.name }
|
||||
),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
this.updating = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
delete() {
|
||||
return this.dialog.yesNoConfirm({
|
||||
message: i18n(
|
||||
"admin.config_areas.themes_and_components.components.delete_confirm",
|
||||
{ name: this.args.component.name }
|
||||
),
|
||||
didConfirm: async () => {
|
||||
try {
|
||||
await ajax(`/admin/themes/${this.args.component.id}`, {
|
||||
type: "DELETE",
|
||||
});
|
||||
this.toasts.success({
|
||||
duration: 5000,
|
||||
data: {
|
||||
message: i18n(
|
||||
"admin.config_areas.themes_and_components.components.deleted_successfully",
|
||||
{ name: this.args.component.name }
|
||||
),
|
||||
},
|
||||
});
|
||||
this.args.refresh();
|
||||
} catch (error) {
|
||||
this.toasts.error({
|
||||
duration: 5000,
|
||||
data: {
|
||||
message: extractErrorInfo(error),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async save(attrs) {
|
||||
try {
|
||||
return await ajax(`/admin/themes/${this.args.component.id}.json`, {
|
||||
type: "PUT",
|
||||
data: {
|
||||
theme: attrs,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.toasts.error({
|
||||
duration: 5000,
|
||||
data: {
|
||||
message: extractErrorInfo(error),
|
||||
},
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<tr
|
||||
data-component-id={{@component.id}}
|
||||
class="d-admin-row__content admin-config-components__component-row
|
||||
{{if this.hasUpdates 'has-update'}}"
|
||||
>
|
||||
<td class="d-admin-row__overview">
|
||||
<div class="d-admin-row__overview-name">
|
||||
{{@component.name}}
|
||||
</div>
|
||||
{{#if @component.remote_theme.authors}}
|
||||
<div
|
||||
class="d-admin-row__overview-author admin-config-components__author-name"
|
||||
>{{i18n
|
||||
"admin.config_areas.themes_and_components.components.by_author"
|
||||
(hash name=@component.remote_theme.authors)
|
||||
}}</div>
|
||||
{{/if}}
|
||||
{{#if @component.description}}
|
||||
<div
|
||||
class="d-admin-row__overview-about admin-config-components__description"
|
||||
>
|
||||
{{@component.description}}
|
||||
{{#if @component.remote_theme.about_url}}
|
||||
<a href={{@component.remote_theme.about_url}}>{{i18n
|
||||
"admin.config_areas.themes_and_components.components.learn_more"
|
||||
}}
|
||||
{{icon "up-right-from-square"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.hasUpdates}}
|
||||
<div
|
||||
class="d-admin-row__overview-about admin-config-components__update-available"
|
||||
>
|
||||
{{i18n
|
||||
"admin.config_areas.themes_and_components.components.update_available"
|
||||
}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail admin-config-components__parent-themes">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "admin.config_areas.themes_and_components.components.used_on"}}
|
||||
</div>
|
||||
<div class="admin-config-components__parent-themes-list">
|
||||
{{#if @component.parent_themes.length}}
|
||||
{{this.parentThemesCell}}
|
||||
{{else}}
|
||||
<div class="status-label --inactive">
|
||||
<div class="status-label-indicator"></div>
|
||||
<div class="status-label-text">
|
||||
{{i18n
|
||||
"admin.config_areas.themes_and_components.components.badge_inactive"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-admin-row__detail">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "admin.config_areas.themes_and_components.components.enabled"}}
|
||||
</div>
|
||||
<DToggleSwitch
|
||||
@state={{this.enabled}}
|
||||
class="admin-config-components__toggle"
|
||||
disabled={{this.disableToggle}}
|
||||
{{on "click" this.toggleEnabled}}
|
||||
/>
|
||||
</td>
|
||||
<td class="d-admin-row__controls">
|
||||
<div class="d-admin-row__controls-options">
|
||||
<DButton
|
||||
class="admin-config-components__edit"
|
||||
@label="admin.config_areas.themes_and_components.components.edit"
|
||||
@route="adminCustomizeThemes.show"
|
||||
@routeModels={{array "themes" @component.id}}
|
||||
/>
|
||||
<DMenu
|
||||
@identifier="component-menu"
|
||||
@title={{i18n "admin.config_areas.flags.more_options.title"}}
|
||||
@icon="ellipsis"
|
||||
@class="btn-default admin-config-components__more-actions"
|
||||
>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
class="btn-transparent admin-config-components__preview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@label="admin.config_areas.themes_and_components.components.preview"
|
||||
@icon="desktop"
|
||||
@href={{getURL
|
||||
(concat "/admin/themes/" @component.id "/preview")
|
||||
}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{#if @component.remote_theme.is_git}}
|
||||
<dropdown.item>
|
||||
{{#if this.hasUpdates}}
|
||||
<DButton
|
||||
class="btn-transparent admin-config-components__update"
|
||||
@label="admin.config_areas.themes_and_components.components.update"
|
||||
@icon="cloud-arrow-down"
|
||||
@action={{this.updateToLatest}}
|
||||
@isLoading={{this.updating}}
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
class="btn-transparent admin-config-components__check-updates"
|
||||
@label="admin.config_areas.themes_and_components.components.check_update"
|
||||
@icon="arrows-rotate"
|
||||
@action={{this.checkForUpdates}}
|
||||
@isLoading={{this.checkingForUpdates}}
|
||||
/>
|
||||
{{/if}}
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
class="btn-transparent admin-config-components__export"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@label="admin.config_areas.themes_and_components.components.export"
|
||||
@icon="download"
|
||||
@href={{getURL
|
||||
(concat
|
||||
"/admin/customize/themes/" @component.id "/export"
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
class="btn-danger admin-config-components__delete"
|
||||
@label="admin.config_areas.themes_and_components.components.delete"
|
||||
@icon="trash-can"
|
||||
@action={{this.delete}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -2,11 +2,6 @@ import DiscourseRoute from "discourse/routes/discourse";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class AdminConfigThemesAndComponentsComponentsRoute extends DiscourseRoute {
|
||||
async model() {
|
||||
const components = await this.store.findAll("theme");
|
||||
return components.reject((t) => !t.component);
|
||||
}
|
||||
|
||||
titleToken() {
|
||||
return i18n("admin.config_areas.themes_and_components.components.title");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ export default RouteTemplate(
|
||||
}}
|
||||
/>
|
||||
|
||||
<Components @components={{@controller.model}} />
|
||||
<Components />
|
||||
</template>
|
||||
);
|
||||
|
||||
@@ -1278,3 +1278,4 @@ a.inline-editable-field {
|
||||
@import "admin/admin_bulk_users_delete_modal";
|
||||
@import "admin/color-palette-editor";
|
||||
@import "admin/admin_config_color_palettes";
|
||||
@import "admin/admin_config_components";
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
.admin-config.components {
|
||||
.admin-config-components {
|
||||
&__filters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__status-filter {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
&__parent-themes-list {
|
||||
@include breakpoint("tablet") {
|
||||
text-align: right;
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
&__update-available {
|
||||
font-size: var(--font-down-1);
|
||||
font-weight: bold;
|
||||
margin: var(--space-1) 0 var(--space-1) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-admin-table.component-list {
|
||||
.has-update {
|
||||
background-color: var(--tertiary-very-low);
|
||||
border-left: solid 3px var(--tertiary);
|
||||
}
|
||||
|
||||
.d-admin-row__overview-about .d-icon {
|
||||
font-size: var(--font-down-3);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
background-color: var(--primary-very-low);
|
||||
padding: var(--space-2);
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
|
||||
@include breakpoint("tablet") {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-filter__input-container {
|
||||
|
||||
@@ -5,5 +5,27 @@ class Admin::Config::CustomizeController < Admin::AdminController
|
||||
end
|
||||
|
||||
def components
|
||||
components = Theme.include_basic_relations.where(component: true).order(:name)
|
||||
|
||||
name_search_term = params[:name].presence&.strip
|
||||
if name_search_term
|
||||
components = components.where("themes.name ILIKE ?", "%#{name_search_term}%")
|
||||
end
|
||||
|
||||
status_filter = params[:status].presence
|
||||
if status_filter
|
||||
case status_filter
|
||||
when "active"
|
||||
components = components.joins(:parent_themes).distinct
|
||||
when "inactive"
|
||||
components = components.left_joins(:parent_themes).where(parent_themes: { id: nil })
|
||||
when "updates_available"
|
||||
components = components.joins(:remote_theme).where(remote_theme: { commits_behind: 1.. })
|
||||
else
|
||||
raise Discourse::InvalidParameters if status_filter != "all"
|
||||
end
|
||||
end
|
||||
|
||||
render json: { components: serialize_data(components, ComponentIndexSerializer) }
|
||||
end
|
||||
end
|
||||
|
||||
+3
-4
@@ -83,20 +83,19 @@ class Theme < ActiveRecord::Base
|
||||
|
||||
scope :include_relations,
|
||||
-> do
|
||||
includes(
|
||||
include_basic_relations.includes(
|
||||
:child_themes,
|
||||
:parent_themes,
|
||||
:remote_theme,
|
||||
:theme_settings,
|
||||
:settings_field,
|
||||
:locale_fields,
|
||||
:user,
|
||||
:color_scheme,
|
||||
:theme_translation_overrides,
|
||||
theme_fields: %i[upload theme_settings_migration],
|
||||
)
|
||||
end
|
||||
|
||||
scope :include_basic_relations, -> { includes(:parent_themes, :remote_theme, :user) }
|
||||
|
||||
delegate :remote_url, to: :remote_theme, private: true, allow_nil: true
|
||||
|
||||
def notify_color_change(color, scheme: nil)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ComponentIndexSerializer < BasicThemeSerializer
|
||||
attributes :remote_theme_id, :supported?, :enabled?, :disabled_at
|
||||
|
||||
has_one :user, serializer: UserNameSerializer, embed: :object
|
||||
has_one :disabled_by, serializer: UserNameSerializer, embed: :object
|
||||
|
||||
has_many :parent_themes, serializer: BasicThemeSerializer, embed: :objects
|
||||
has_one :remote_theme, serializer: RemoteThemeSerializer, embed: :objects
|
||||
|
||||
def parent_themes
|
||||
object.parent_themes
|
||||
end
|
||||
|
||||
def include_disabled_at?
|
||||
object.component? && !object.enabled?
|
||||
end
|
||||
|
||||
def include_disabled_by?
|
||||
include_disabled_at?
|
||||
end
|
||||
end
|
||||
@@ -6201,9 +6201,44 @@ en:
|
||||
back: "Back to themes"
|
||||
components:
|
||||
title: "Components"
|
||||
description: "Customizations that change surface elements of your forum design, or add extra front-end features"
|
||||
components_intro: "Install a new component to get started, or create your own from scratch using these resources."
|
||||
new_component: "New component"
|
||||
back: "Back to components"
|
||||
install: "Install"
|
||||
name: "Name"
|
||||
used_on: "Used on"
|
||||
enabled: "Enabled?"
|
||||
by_author: "By %{name}"
|
||||
learn_more: "Learn more"
|
||||
edit: "Edit"
|
||||
parent_themes_two: "%{name1} and %{name2}"
|
||||
parent_themes_three: "%{name1}, %{name2} and %{name3}"
|
||||
parent_themes_more_than_three:
|
||||
one: "%{name1}, %{name2}, %{name3} and %{count} more"
|
||||
other: "%{name1}, %{name2}, %{name3} and %{count} more"
|
||||
add_to_theme: "Add to theme"
|
||||
preview: "Preview"
|
||||
update: "Update to latest"
|
||||
check_update: "Check for updates"
|
||||
update_available: "Update available!"
|
||||
export: "Export"
|
||||
convert: "Convert"
|
||||
delete: "Delete"
|
||||
filter_by: "Filter by"
|
||||
filter_by_all: "All"
|
||||
filter_by_active: "Active"
|
||||
filter_by_inactive: "Inactive"
|
||||
filter_by_updates_available: "Updates available"
|
||||
search_components: "Type a component name"
|
||||
new_update_for_component: "New update available for %{name}!"
|
||||
component_up_to_date: '"%{name}" is up to date.'
|
||||
updated_successfully: '"%{name}" has been updated successfully.'
|
||||
delete_confirm: 'Are you sure you want to delete "%{name}"?'
|
||||
deleted_successfully: '"%{name}" has been deleted successfully.'
|
||||
no_components: "No components installed."
|
||||
no_components_found: "No components match your filters."
|
||||
badge_inactive: "Unused"
|
||||
user_fields:
|
||||
field: "Field"
|
||||
type: "Type"
|
||||
|
||||
@@ -69,6 +69,7 @@ module SvgSprite
|
||||
circle-xmark
|
||||
clock
|
||||
clock-rotate-left
|
||||
cloud-arrow-down
|
||||
cloud-arrow-up
|
||||
code
|
||||
comment
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Admin::Config::CustomizeController do
|
||||
fab!(:admin)
|
||||
fab!(:parent_theme_1) { Fabricate(:theme) }
|
||||
fab!(:parent_theme_2) { Fabricate(:theme) }
|
||||
|
||||
fab!(:active_component) do
|
||||
Fabricate(
|
||||
:theme,
|
||||
name: "AweSome comp",
|
||||
component: true,
|
||||
parent_themes: [parent_theme_1, parent_theme_2],
|
||||
)
|
||||
end
|
||||
fab!(:inactive_component) { Fabricate(:theme, name: "some comp", component: true) }
|
||||
fab!(:remote_component) do
|
||||
Fabricate(
|
||||
:theme,
|
||||
component: true,
|
||||
remote_theme: RemoteTheme.create!(remote_url: "https://github.com/discourse/discourse-tc"),
|
||||
)
|
||||
end
|
||||
fab!(:remote_component_with_update) do
|
||||
Fabricate(
|
||||
:theme,
|
||||
component: true,
|
||||
remote_theme:
|
||||
RemoteTheme.create!(
|
||||
remote_url: "https://github.com/discourse/discourse",
|
||||
commits_behind: 1,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
before { sign_in(admin) }
|
||||
|
||||
describe "#components" do
|
||||
context "when filtering by `active`" do
|
||||
it "returns components that have a parent theme" do
|
||||
get "/admin/config/customize/components.json", params: { status: "active" }
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["components"].map { |c| c["id"] }).to contain_exactly(
|
||||
active_component.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when filtering by `inactive`" do
|
||||
it "returns components that have no parent theme" do
|
||||
get "/admin/config/customize/components.json", params: { status: "inactive" }
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["components"].map { |c| c["id"] }).to contain_exactly(
|
||||
inactive_component.id,
|
||||
remote_component.id,
|
||||
remote_component_with_update.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when filtering by `updates_available`" do
|
||||
it "returns components that are behind their remote" do
|
||||
get "/admin/config/customize/components.json", params: { status: "updates_available" }
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["components"].map { |c| c["id"] }).to contain_exactly(
|
||||
remote_component_with_update.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when filtering by `all`" do
|
||||
it "returns all components" do
|
||||
get "/admin/config/customize/components.json", params: { status: "all" }
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["components"].map { |c| c["id"] }).to contain_exactly(
|
||||
active_component.id,
|
||||
inactive_component.id,
|
||||
remote_component.id,
|
||||
remote_component_with_update.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when there's no filter param" do
|
||||
it "is equivalent to filtering by `all`" do
|
||||
get "/admin/config/customize/components.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["components"].map { |c| c["id"] }).to contain_exactly(
|
||||
active_component.id,
|
||||
inactive_component.id,
|
||||
remote_component.id,
|
||||
remote_component_with_update.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "can filter components by a search term" do
|
||||
get "/admin/config/customize/components.json", params: { name: "SomE" }
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["components"].map { |c| c["id"] }).to contain_exactly(
|
||||
active_component.id,
|
||||
inactive_component.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe ComponentIndexSerializer do
|
||||
fab!(:theme_1) { Fabricate(:theme) }
|
||||
fab!(:theme_2) { Fabricate(:theme) }
|
||||
|
||||
fab!(:component) do
|
||||
Fabricate(
|
||||
:theme,
|
||||
component: true,
|
||||
parent_themes: [theme_1, theme_2],
|
||||
remote_theme:
|
||||
RemoteTheme.create!(
|
||||
remote_url: "https://github.com/discourse/discourse.git",
|
||||
commits_behind: 3,
|
||||
authors: "CDCK Inc.",
|
||||
),
|
||||
theme_fields: [
|
||||
ThemeField.new(
|
||||
name: "en",
|
||||
type_id: ThemeField.types[:yaml],
|
||||
target_id: Theme.targets[:translations],
|
||||
value: <<~YAML,
|
||||
en:
|
||||
theme_metadata:
|
||||
description: "Description of my component"
|
||||
YAML
|
||||
),
|
||||
],
|
||||
)
|
||||
end
|
||||
|
||||
let(:json) { described_class.new(component, root: false).as_json }
|
||||
|
||||
it "includes remote_theme object" do
|
||||
expect(json[:remote_theme][:id]).to eq(component.remote_theme.id)
|
||||
expect(json[:remote_theme][:commits_behind]).to eq(3)
|
||||
expect(json[:remote_theme][:authors]).to eq("CDCK Inc.")
|
||||
end
|
||||
|
||||
it "includes parent themes objects" do
|
||||
expect(json[:parent_themes].map { |o| o[:name] }).to contain_exactly(theme_1.name, theme_2.name)
|
||||
end
|
||||
|
||||
it "includes the component name" do
|
||||
expect(json[:name]).to eq(component.name)
|
||||
end
|
||||
|
||||
it "includes the component id" do
|
||||
expect(json[:id]).to eq(component.id)
|
||||
end
|
||||
|
||||
it "includes the component description" do
|
||||
expect(json[:description]).to eq("Description of my component")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,338 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Admin Customize Themes Config Area Page", type: :system do
|
||||
fab!(:admin)
|
||||
fab!(:parent_theme) { Fabricate(:theme, name: "A theme") }
|
||||
fab!(:parent_theme_2) { Fabricate(:theme, name: "B theme") }
|
||||
fab!(:parent_theme_3) { Fabricate(:theme, name: "C theme") }
|
||||
fab!(:parent_theme_4) { Fabricate(:theme, name: "D theme") }
|
||||
|
||||
let(:config_area) { PageObjects::Pages::AdminCustomizeComponentsConfigArea.new }
|
||||
let(:toasts) { PageObjects::Components::Toasts.new }
|
||||
let(:dialog) { PageObjects::Components::Dialog.new }
|
||||
|
||||
before { sign_in(admin) }
|
||||
|
||||
context "when there are components installed" do
|
||||
fab!(:enabled_component) do
|
||||
Fabricate(
|
||||
:theme,
|
||||
name: "Glorious component",
|
||||
component: true,
|
||||
enabled: true,
|
||||
parent_themes: [parent_theme, parent_theme_2, parent_theme_3, parent_theme_4],
|
||||
)
|
||||
end
|
||||
fab!(:disabled_component) do
|
||||
Fabricate(:theme, name: "Glossy component", component: true, enabled: false)
|
||||
end
|
||||
fab!(:remote_component) do
|
||||
Fabricate(
|
||||
:theme,
|
||||
component: true,
|
||||
enabled: false,
|
||||
parent_themes: [parent_theme_3],
|
||||
remote_theme:
|
||||
RemoteTheme.create!(
|
||||
remote_url: "https://github.com/discourse/tc-1.git",
|
||||
authors: "CDCK Inc.",
|
||||
),
|
||||
theme_fields: [
|
||||
ThemeField.new(
|
||||
name: "en",
|
||||
type_id: ThemeField.types[:yaml],
|
||||
target_id: Theme.targets[:translations],
|
||||
value: <<~YAML,
|
||||
en:
|
||||
theme_metadata:
|
||||
description: "Description of my remote component"
|
||||
YAML
|
||||
),
|
||||
],
|
||||
)
|
||||
end
|
||||
fab!(:remote_component_with_update) do
|
||||
Fabricate(
|
||||
:theme,
|
||||
component: true,
|
||||
enabled: false,
|
||||
remote_theme:
|
||||
RemoteTheme.create!(remote_url: "https://github.com/discourse/tc-2", commits_behind: 4),
|
||||
)
|
||||
end
|
||||
|
||||
it "can enable/disable components" do
|
||||
config_area.visit
|
||||
|
||||
expect(config_area.component(enabled_component.id).enabled_toggle).to be_checked
|
||||
expect(config_area.component(disabled_component.id).enabled_toggle).to be_unchecked
|
||||
|
||||
config_area.component(enabled_component.id).enabled_toggle.toggle
|
||||
config_area.component(disabled_component.id).enabled_toggle.toggle
|
||||
|
||||
expect(config_area.component(enabled_component.id).enabled_toggle).to be_unchecked
|
||||
expect(config_area.component(disabled_component.id).enabled_toggle).to be_checked
|
||||
|
||||
expect(enabled_component.reload.enabled).to eq(false)
|
||||
expect(disabled_component.reload.enabled).to eq(true)
|
||||
end
|
||||
|
||||
it "can filter components by status" do
|
||||
config_area.visit
|
||||
|
||||
config_area.status_selector.select("active")
|
||||
expect(config_area).to be_loading
|
||||
expect(config_area.components_shown).to contain_exactly(
|
||||
enabled_component.id,
|
||||
remote_component.id,
|
||||
)
|
||||
|
||||
config_area.status_selector.select("inactive")
|
||||
expect(config_area).to be_loading
|
||||
expect(config_area.components_shown).to contain_exactly(
|
||||
disabled_component.id,
|
||||
remote_component_with_update.id,
|
||||
)
|
||||
|
||||
config_area.status_selector.select("updates_available")
|
||||
expect(config_area).to be_loading
|
||||
expect(config_area.components_shown).to contain_exactly(remote_component_with_update.id)
|
||||
end
|
||||
|
||||
it "can filter components by name" do
|
||||
config_area.visit
|
||||
|
||||
config_area.name_filter_input.fill_in(with: "glo")
|
||||
|
||||
expect(config_area).to be_loading
|
||||
expect(config_area.components_shown).to contain_exactly(
|
||||
enabled_component.id,
|
||||
disabled_component.id,
|
||||
)
|
||||
end
|
||||
|
||||
it "keeps the filters shown when there are no components matching the filters" do
|
||||
config_area.visit
|
||||
|
||||
config_area.name_filter_input.fill_in(with: "stringthatshouldnotmatchanything")
|
||||
expect(config_area).to have_no_components_found_text
|
||||
expect(config_area).to have_no_components
|
||||
expect(config_area).to have_name_filter_input
|
||||
expect(config_area).to have_status_selector
|
||||
end
|
||||
|
||||
it "navigates to the component page when clicking the Edit button" do
|
||||
config_area.visit
|
||||
|
||||
config_area.component(enabled_component.id).edit_button.click
|
||||
expect(page).to have_current_path("/admin/customize/themes/#{enabled_component.id}")
|
||||
end
|
||||
|
||||
it "displays various metadata for components" do
|
||||
disabled_component.update!(parent_themes: [parent_theme])
|
||||
remote_component.update!(parent_themes: [parent_theme, parent_theme_2])
|
||||
remote_component_with_update.update!(
|
||||
parent_themes: [parent_theme, parent_theme_2, parent_theme_3],
|
||||
)
|
||||
|
||||
config_area.visit
|
||||
|
||||
expect(config_area.component(remote_component.id)).to have_author("CDCK Inc.")
|
||||
expect(config_area.component(remote_component.id)).to have_description(
|
||||
"Description of my remote component",
|
||||
)
|
||||
expect(config_area.component(remote_component.id)).to be_not_pending_update
|
||||
|
||||
expect(config_area.component(remote_component_with_update.id)).to be_pending_update
|
||||
|
||||
expect(config_area.component(disabled_component.id)).to have_one_parent_theme("A theme")
|
||||
expect(config_area.component(remote_component.id)).to have_two_parent_themes(
|
||||
"A theme",
|
||||
"B theme",
|
||||
)
|
||||
expect(config_area.component(remote_component_with_update.id)).to have_three_parent_themes(
|
||||
"A theme",
|
||||
"B theme",
|
||||
"C theme",
|
||||
)
|
||||
expect(config_area.component(enabled_component.id)).to have_three_and_more_parent_themes(
|
||||
"A theme",
|
||||
"B theme",
|
||||
"C theme",
|
||||
1,
|
||||
)
|
||||
end
|
||||
|
||||
it "shows actions that make sense for each component" do
|
||||
config_area.visit
|
||||
|
||||
config_area.component(enabled_component.id).more_actions_menu.expand
|
||||
expect(config_area.component(enabled_component.id)).to have_no_check_for_updates_button
|
||||
expect(config_area.component(enabled_component.id)).to have_no_update_button
|
||||
expect(config_area.component(enabled_component.id).preview_button["href"]).to end_with(
|
||||
"/admin/themes/#{enabled_component.id}/preview",
|
||||
)
|
||||
expect(config_area.component(enabled_component.id).export_button["href"]).to end_with(
|
||||
"/admin/customize/themes/#{enabled_component.id}/export",
|
||||
)
|
||||
config_area.component(enabled_component.id).more_actions_menu.collapse
|
||||
|
||||
config_area.component(disabled_component.id).more_actions_menu.expand
|
||||
expect(config_area.component(disabled_component.id)).to have_no_check_for_updates_button
|
||||
expect(config_area.component(disabled_component.id)).to have_no_update_button
|
||||
expect(config_area.component(disabled_component.id).preview_button["href"]).to end_with(
|
||||
"/admin/themes/#{disabled_component.id}/preview",
|
||||
)
|
||||
expect(config_area.component(disabled_component.id).export_button["href"]).to end_with(
|
||||
"/admin/customize/themes/#{disabled_component.id}/export",
|
||||
)
|
||||
config_area.component(disabled_component.id).more_actions_menu.collapse
|
||||
|
||||
config_area.component(remote_component.id).more_actions_menu.expand
|
||||
expect(config_area.component(remote_component.id)).to have_check_for_updates_button
|
||||
expect(config_area.component(remote_component.id)).to have_no_update_button
|
||||
expect(config_area.component(remote_component.id).preview_button["href"]).to end_with(
|
||||
"/admin/themes/#{remote_component.id}/preview",
|
||||
)
|
||||
expect(config_area.component(remote_component.id).export_button["href"]).to end_with(
|
||||
"/admin/customize/themes/#{remote_component.id}/export",
|
||||
)
|
||||
config_area.component(remote_component.id).more_actions_menu.collapse
|
||||
|
||||
config_area.component(remote_component_with_update.id).more_actions_menu.expand
|
||||
expect(
|
||||
config_area.component(remote_component_with_update.id),
|
||||
).to have_no_check_for_updates_button
|
||||
expect(config_area.component(remote_component_with_update.id)).to have_update_button
|
||||
expect(
|
||||
config_area.component(remote_component_with_update.id).preview_button["href"],
|
||||
).to end_with("/admin/themes/#{remote_component_with_update.id}/preview")
|
||||
expect(
|
||||
config_area.component(remote_component_with_update.id).export_button["href"],
|
||||
).to end_with("/admin/customize/themes/#{remote_component_with_update.id}/export")
|
||||
config_area.component(remote_component_with_update.id).more_actions_menu.collapse
|
||||
end
|
||||
|
||||
it "can delete a component" do
|
||||
config_area.visit
|
||||
|
||||
config_area.component(disabled_component.id).more_actions_menu.expand
|
||||
config_area.component(disabled_component.id).delete_button.click
|
||||
|
||||
dialog.click_yes
|
||||
|
||||
expect(toasts).to have_success(
|
||||
I18n.t(
|
||||
"admin_js.admin.config_areas.themes_and_components.components.deleted_successfully",
|
||||
name: disabled_component.name,
|
||||
),
|
||||
)
|
||||
expect(Theme.find_by(id: disabled_component.id)).to eq(nil)
|
||||
expect(config_area).to have_no_component(disabled_component.id)
|
||||
end
|
||||
|
||||
describe "checking for updates" do
|
||||
let(:repo) do
|
||||
setup_git_repo("about.json" => { name: "discourse-component-tt1", component: true }.to_json)
|
||||
end
|
||||
|
||||
let(:url) do
|
||||
MockGitImporter.register("https://example.com/discourse-component-tt1.git", repo)
|
||||
end
|
||||
|
||||
before do
|
||||
remote_component.remote_theme.update!(remote_url: url)
|
||||
remote_component.remote_theme.update_from_remote
|
||||
end
|
||||
|
||||
after { `rm -fr #{repo}` }
|
||||
|
||||
around(:each) { |group| MockGitImporter.with_mock { group.run } }
|
||||
|
||||
it 'shows an "Update to latest" button if there is a new update' do
|
||||
config_area.visit
|
||||
config_area.component(remote_component.id).more_actions_menu.expand
|
||||
|
||||
add_to_git_repo(repo, "about.json" => { name: "updated-name", component: true }.to_json)
|
||||
|
||||
config_area.component(remote_component.id).check_for_updates_button.click
|
||||
|
||||
expect(toasts).to have_default(
|
||||
I18n.t(
|
||||
"admin_js.admin.config_areas.themes_and_components.components.new_update_for_component",
|
||||
name: remote_component.name,
|
||||
),
|
||||
)
|
||||
expect(config_area.component(remote_component.id)).to have_update_button
|
||||
expect(config_area.component(remote_component.id)).to be_pending_update
|
||||
end
|
||||
|
||||
it 'keeps the "Check for updates" button if there is no new update' do
|
||||
config_area.visit
|
||||
config_area.component(remote_component.id).more_actions_menu.expand
|
||||
|
||||
config_area.component(remote_component.id).check_for_updates_button.click
|
||||
|
||||
expect(toasts).to have_default(
|
||||
I18n.t(
|
||||
"admin_js.admin.config_areas.themes_and_components.components.component_up_to_date",
|
||||
name: remote_component.name,
|
||||
),
|
||||
)
|
||||
expect(config_area.component(remote_component.id)).to have_check_for_updates_button
|
||||
expect(config_area.component(remote_component.id)).to be_not_pending_update
|
||||
end
|
||||
end
|
||||
|
||||
describe "performing an update" do
|
||||
let(:repo) do
|
||||
setup_git_repo("about.json" => { name: "discourse-component-tt2", component: true }.to_json)
|
||||
end
|
||||
|
||||
let(:url) do
|
||||
MockGitImporter.register("https://example.com/discourse-component-tt2.git", repo)
|
||||
end
|
||||
|
||||
before do
|
||||
remote_component_with_update.remote_theme.update!(remote_url: url)
|
||||
remote_component_with_update.remote_theme.update_from_remote
|
||||
|
||||
add_to_git_repo(repo, "about.json" => { name: "updated-name-tt2", component: true }.to_json)
|
||||
remote_component_with_update.remote_theme.update_remote_version
|
||||
end
|
||||
|
||||
after { `rm -fr #{repo}` }
|
||||
|
||||
around(:each) { |group| MockGitImporter.with_mock { group.run } }
|
||||
|
||||
it 'shows the "Check for updates" button after updating' do
|
||||
config_area.visit
|
||||
|
||||
config_area.component(remote_component_with_update.id).more_actions_menu.expand
|
||||
config_area.component(remote_component_with_update.id).update_button.click
|
||||
|
||||
expect(toasts).to have_success(
|
||||
I18n.t(
|
||||
"admin_js.admin.config_areas.themes_and_components.components.updated_successfully",
|
||||
name: remote_component_with_update.name,
|
||||
),
|
||||
)
|
||||
expect(
|
||||
config_area.component(remote_component_with_update.id),
|
||||
).to have_check_for_updates_button
|
||||
expect(config_area.component(remote_component_with_update.id)).to be_not_pending_update
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are no components installed" do
|
||||
it "doesn't display filters when there are no components installed" do
|
||||
config_area.visit
|
||||
|
||||
expect(config_area).to have_no_components_installed_text
|
||||
expect(config_area).to have_no_components
|
||||
expect(config_area).to have_no_status_selector
|
||||
expect(config_area).to have_no_name_filter_input
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Admin Customize Config Area Page", type: :system do
|
||||
fab!(:admin)
|
||||
|
||||
let(:config_area) { PageObjects::Pages::AdminCustomizeConfigArea.new }
|
||||
let(:install_modal) { PageObjects::Modals::InstallTheme.new }
|
||||
|
||||
before { sign_in(admin) }
|
||||
|
||||
context "when in the themes tab" do
|
||||
it "has a special card for installing new themes" do
|
||||
config_area.visit
|
||||
|
||||
expect(config_area.install_card).to have_text(
|
||||
I18n.t("admin_js.admin.config_areas.themes_and_components.themes.new_theme"),
|
||||
)
|
||||
|
||||
config_area.install_card.find(".btn-primary").click
|
||||
expect(install_modal).to be_open
|
||||
expect(install_modal.popular_options.first).to have_text("Air")
|
||||
end
|
||||
end
|
||||
|
||||
context "when in the components tab" do
|
||||
it "has a special card for installing new components" do
|
||||
config_area.visit_components
|
||||
|
||||
expect(config_area.install_card).to have_text(
|
||||
I18n.t("admin_js.admin.config_areas.themes_and_components.components.new_component"),
|
||||
)
|
||||
|
||||
config_area.install_card.find(".btn-primary").click
|
||||
expect(install_modal).to be_open
|
||||
expect(install_modal.popular_options.first).to have_text("Brand Header")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Admin Customize Themes Config Area Page", type: :system do
|
||||
fab!(:admin)
|
||||
|
||||
let(:config_area) { PageObjects::Pages::AdminCustomizeThemesConfigArea.new }
|
||||
let(:install_modal) { PageObjects::Modals::InstallTheme.new }
|
||||
|
||||
before { sign_in(admin) }
|
||||
|
||||
it "has a special card for installing new themes" do
|
||||
config_area.visit
|
||||
|
||||
expect(config_area.install_card).to have_text(
|
||||
I18n.t("admin_js.admin.config_areas.themes_and_components.themes.new_theme"),
|
||||
)
|
||||
|
||||
config_area.install_card.find(".btn-primary").click
|
||||
expect(install_modal).to be_open
|
||||
expect(install_modal.popular_options.first).to have_text("Air")
|
||||
end
|
||||
end
|
||||
@@ -34,6 +34,14 @@ module PageObjects
|
||||
def option(selector)
|
||||
within("#d-menu-portals") { find(selector) }
|
||||
end
|
||||
|
||||
def has_option?(selector)
|
||||
within("#d-menu-portals") { has_css?(selector) }
|
||||
end
|
||||
|
||||
def has_no_option?(selector)
|
||||
within("#d-menu-portals") { has_no_css?(selector) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Components
|
||||
class DSelect < PageObjects::Components::Base
|
||||
attr_reader :select_element
|
||||
|
||||
def initialize(input)
|
||||
if input.is_a?(Capybara::Node::Element)
|
||||
@select_element = input
|
||||
else
|
||||
@select_element = find(input)
|
||||
end
|
||||
end
|
||||
|
||||
def value
|
||||
@select_element.value
|
||||
end
|
||||
|
||||
def select(value)
|
||||
@select_element.find("option[value='#{value}']").select_option
|
||||
@select_element.execute_script(<<~JS, @select_element)
|
||||
var selector = arguments[0];
|
||||
selector.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||
JS
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -41,7 +41,7 @@ module PageObjects
|
||||
when "menu"
|
||||
component.find(".fk-d-menu__trigger")["data-value"]
|
||||
when "select"
|
||||
component.find("select").value
|
||||
PageObjects::Components::DSelect.new(component.find("select")).value
|
||||
when "composer"
|
||||
component.find("textarea").value
|
||||
when "image"
|
||||
@@ -122,12 +122,9 @@ module PageObjects
|
||||
picker.search(value)
|
||||
picker.select_row_by_value(value)
|
||||
when "select"
|
||||
selector = component.find(".form-kit__control-select")
|
||||
selector.find(".form-kit__control-option[value='#{value}']").select_option
|
||||
selector.execute_script(<<~JS, selector)
|
||||
var selector = arguments[0];
|
||||
selector.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||
JS
|
||||
PageObjects::Components::DSelect.new(component.find(".form-kit__control-select")).select(
|
||||
value,
|
||||
)
|
||||
when "menu"
|
||||
trigger = component.find(".fk-d-menu__trigger.form-kit__control-menu-trigger")
|
||||
trigger.click
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class AdminCustomizeComponentsConfigArea < PageObjects::Pages::Base
|
||||
class ComponentRow < PageObjects::Components::Base
|
||||
def initialize(selector)
|
||||
@selector = selector
|
||||
@element = find(selector)
|
||||
end
|
||||
|
||||
def enabled_toggle
|
||||
PageObjects::Components::DToggleSwitch.new(
|
||||
"#{@selector} .admin-config-components__toggle",
|
||||
)
|
||||
end
|
||||
|
||||
def edit_button
|
||||
@element.find(".admin-config-components__edit")
|
||||
end
|
||||
|
||||
def has_author?(name)
|
||||
@element.find(".admin-config-components__author-name").has_text?(
|
||||
I18n.t(
|
||||
"admin_js.admin.config_areas.themes_and_components.components.by_author",
|
||||
name: name,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
def has_description?(description)
|
||||
@element.find(".admin-config-components__description").has_text?(description)
|
||||
end
|
||||
|
||||
def has_one_parent_theme?(name)
|
||||
@element.find(".admin-config-components__parent-themes").text == name
|
||||
end
|
||||
|
||||
def has_two_parent_themes?(name1, name2)
|
||||
@element.find(".admin-config-components__parent-themes").text ==
|
||||
I18n.t(
|
||||
"admin_js.admin.config_areas.themes_and_components.components.parent_themes_two",
|
||||
name1:,
|
||||
name2:,
|
||||
)
|
||||
end
|
||||
|
||||
def has_three_parent_themes?(name1, name2, name3)
|
||||
@element.find(".admin-config-components__parent-themes").text ==
|
||||
I18n.t(
|
||||
"admin_js.admin.config_areas.themes_and_components.components.parent_themes_three",
|
||||
name1:,
|
||||
name2:,
|
||||
name3:,
|
||||
)
|
||||
end
|
||||
|
||||
def has_three_and_more_parent_themes?(name1, name2, name3, count)
|
||||
@element.find(".admin-config-components__parent-themes").text ==
|
||||
I18n.t(
|
||||
"admin_js.admin.config_areas.themes_and_components.components.parent_themes_more_than_three",
|
||||
name1:,
|
||||
name2:,
|
||||
name3:,
|
||||
count:,
|
||||
)
|
||||
end
|
||||
|
||||
def pending_update?
|
||||
@element.has_css?(".admin-config-components__update-available")
|
||||
end
|
||||
|
||||
def not_pending_update?
|
||||
@element.has_no_css?(".admin-config-components__update-available")
|
||||
end
|
||||
|
||||
def more_actions_menu
|
||||
PageObjects::Components::DMenu.new(@element.find(".component-menu-trigger"))
|
||||
end
|
||||
|
||||
def preview_button
|
||||
more_actions_menu.option(".admin-config-components__preview")
|
||||
end
|
||||
|
||||
def has_check_for_updates_button?
|
||||
more_actions_menu.has_option?(".admin-config-components__check-updates")
|
||||
end
|
||||
|
||||
def has_no_check_for_updates_button?
|
||||
more_actions_menu.has_no_option?(".admin-config-components__check-updates")
|
||||
end
|
||||
|
||||
def check_for_updates_button
|
||||
more_actions_menu.option(".admin-config-components__check-updates")
|
||||
end
|
||||
|
||||
def has_update_button?
|
||||
more_actions_menu.has_option?(".admin-config-components__update")
|
||||
end
|
||||
|
||||
def has_no_update_button?
|
||||
more_actions_menu.has_no_option?(".admin-config-components__update")
|
||||
end
|
||||
|
||||
def update_button
|
||||
more_actions_menu.option(".admin-config-components__update")
|
||||
end
|
||||
|
||||
def export_button
|
||||
more_actions_menu.option(".admin-config-components__export")
|
||||
end
|
||||
|
||||
def delete_button
|
||||
more_actions_menu.option(".admin-config-components__delete")
|
||||
end
|
||||
end
|
||||
|
||||
def visit
|
||||
page.visit("/admin/config/customize/components")
|
||||
end
|
||||
|
||||
def loading?
|
||||
has_css?(".loading-container.visible")
|
||||
end
|
||||
|
||||
def component(id)
|
||||
ComponentRow.new(".admin-config-components__component-row[data-component-id=\"#{id}\"]")
|
||||
end
|
||||
|
||||
def has_no_component?(id)
|
||||
has_no_css?(".admin-config-components__component-row[data-component-id=\"#{id}\"]")
|
||||
end
|
||||
|
||||
def status_selector
|
||||
PageObjects::Components::DSelect.new(find(".admin-config-components__status-filter select"))
|
||||
end
|
||||
|
||||
def name_filter_input
|
||||
find(".admin-config-components__name-filter input")
|
||||
end
|
||||
|
||||
def has_no_components?
|
||||
has_no_css?(".admin-config-components__component-row")
|
||||
end
|
||||
|
||||
def components_shown
|
||||
all(".admin-config-components__component-row").map { |node| node["data-component-id"].to_i }
|
||||
end
|
||||
|
||||
def has_name_filter_input?
|
||||
has_css?(".admin-config-components__name-filter")
|
||||
end
|
||||
|
||||
def has_status_selector?
|
||||
has_css?(".admin-config-components__status-filter")
|
||||
end
|
||||
|
||||
def has_no_name_filter_input?
|
||||
has_no_css?(".admin-config-components__name-filter")
|
||||
end
|
||||
|
||||
def has_no_status_selector?
|
||||
has_no_css?(".admin-config-components__status-filter")
|
||||
end
|
||||
|
||||
def has_no_components_installed_text?
|
||||
page.has_text?(
|
||||
I18n.t("admin_js.admin.config_areas.themes_and_components.components.no_components"),
|
||||
)
|
||||
end
|
||||
|
||||
def has_no_components_found_text?
|
||||
page.has_text?(
|
||||
I18n.t(
|
||||
"admin_js.admin.config_areas.themes_and_components.components.no_components_found",
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+1
-5
@@ -2,15 +2,11 @@
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class AdminCustomizeConfigArea < PageObjects::Pages::Base
|
||||
class AdminCustomizeThemesConfigArea < PageObjects::Pages::Base
|
||||
def visit
|
||||
page.visit("/admin/config/customize")
|
||||
end
|
||||
|
||||
def visit_components
|
||||
page.visit("/admin/config/customize/components")
|
||||
end
|
||||
|
||||
def install_card
|
||||
find(".theme-install-card")
|
||||
end
|
||||
Reference in New Issue
Block a user