UX: admins embedding page follows admin ux guideline

Conversion of `/admin/customize/embedding` page to follow admin UX guidelines.
This commit is contained in:
Krzysztof Kotlarek 2024-11-26 08:29:05 +11:00
parent b3fa335c7d
commit f1a5e4d968
29 changed files with 742 additions and 429 deletions

View File

@ -0,0 +1,142 @@
import Component from "@glimmer/component";
import { inject as controller } from "@ember/controller";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import { service } from "@ember/service";
import BackButton from "discourse/components/back-button";
import Form from "discourse/components/form";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
import CategoryChooser from "select-kit/components/category-chooser";
import TagChooser from "select-kit/components/tag-chooser";
import UserChooser from "select-kit/components/user-chooser";
export default class AdminEmbeddingHostForm extends Component {
@service router;
@service site;
@service store;
@controller adminEmbedding;
get isUpdate() {
return this.args.host;
}
get header() {
return this.isUpdate
? "admin.embedding.host_form.edit_header"
: "admin.embedding.host_form.add_header";
}
get formData() {
if (this.isUpdate) {
return {
host: this.args.host.host,
allowed_paths: this.args.host.allowed_paths,
category: this.args.host.category_id,
tags: this.args.host.tags,
user: this.args.host.user,
};
} else {
return {};
}
}
@action
async save(data) {
const host = this.args.host || this.store.createRecord("embeddable-host");
try {
await host.save({
...data,
user: data.user?.at(0),
category_id: data.category,
});
if (!this.isUpdate) {
this.adminEmbedding.embedding.embeddable_hosts.push(host);
}
this.router.transitionTo("adminEmbedding");
} catch (error) {
popupAjaxError(error);
}
}
<template>
<BackButton @route="adminEmbedding" @label="admin.embedding.back" />
<div class="admin-config-area">
<div class="admin-config-area__primary-content admin-embedding-host-form">
<AdminConfigAreaCard @heading={{this.header}}>
<:content>
<Form @onSubmit={{this.save}} @data={{this.formData}} as |form|>
<form.Field
@name="host"
@title={{i18n "admin.embedding.host"}}
@validation="required"
@format="large"
as |field|
>
<field.Input placeholder="example.com" />
</form.Field>
<form.Field
@name="allowed_paths"
@title={{i18n "admin.embedding.allowed_paths"}}
@format="large"
as |field|
>
<field.Input placeholder="/blog/.*" />
</form.Field>
<form.Field
@name="category"
@title={{i18n "admin.embedding.category"}}
as |field|
>
<field.Custom>
<CategoryChooser
@value={{field.value}}
@onChange={{field.set}}
class="admin-embedding-host-form__category"
/>
</field.Custom>
</form.Field>
<form.Field
@name="tags"
@title={{i18n "admin.embedding.tags"}}
as |field|
>
<field.Custom>
<TagChooser
@tags={{field.value}}
@everyTag={{true}}
@excludeSynonyms={{true}}
@unlimitedTagCount={{true}}
@onChange={{field.set}}
@options={{hash
filterPlaceholder="category.tags_placeholder"
}}
class="admin-embedding-host-form__tags"
/>
</field.Custom>
</form.Field>
<form.Field
@name="user"
@title={{i18n "admin.embedding.post_author"}}
as |field|
>
<field.Custom>
<UserChooser
@value={{field.value}}
@onChange={{field.set}}
@options={{hash maximum=1 excludeCurrentUser=false}}
class="admin-embedding-host-form__post_author"
/>
</field.Custom>
</form.Field>
<form.Submit @label="admin.embedding.host_form.save" />
</Form>
</:content>
</AdminConfigAreaCard>
</div>
</div>
</template>
}

View File

@ -1,87 +1,45 @@
{{#if this.editing}}
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.host"}}</div>
<Input
@value={{this.buffered.host}}
placeholder="example.com"
@enter={{this.save}}
class="host-name"
autofocus={{true}}
/>
</td>
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.allowed_paths"}}</div>
<Input
@value={{this.buffered.allowed_paths}}
placeholder="/blog/.*"
@enter={{this.save}}
class="path-allowlist"
/>
</td>
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.category"}}</div>
<CategoryChooser
@value={{this.category.id}}
@onChangeCategory={{fn (mut this.category)}}
class="small"
/>
</td>
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.tags"}}</div>
<TagChooser
@tags={{this.tags}}
@everyTag={{true}}
@excludeSynonyms={{true}}
@unlimitedTagCount={{true}}
@onChange={{fn (mut this.tags)}}
@options={{hash filterPlaceholder="category.tags_placeholder"}}
/>
</td>
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.user"}}</div>
<UserChooser
@value={{this.user}}
@onChange={{action "onUserChange"}}
@options={{hash maximum=1 excludeCurrentUser=false}}
/>
</td>
<td class="editing-controls">
<td class="d-admin-row__detail">
{{this.host.host}}
</td>
<td class="d-admin-row__detail">
{{this.host.allowed_paths}}
</td>
<td class="d-admin-row__detail">
{{category-badge this.category allowUncategorized=true}}
</td>
<td class="d-admin-row__detail">
{{this.tags}}
</td>
<td class="d-admin-row__detail">
{{this.user}}
</td>
<td class="d-admin-row__controls">
<div class="d-admin-row__controls-options">
<DButton
@icon="check"
@action={{this.save}}
@disabled={{this.cantSave}}
class="btn-primary"
class="btn-small admin-embeddable-host-item__edit"
@route="adminEmbedding.edit"
@routeModels={{this.host}}
@label="admin.embedding.edit"
/>
<DButton
@icon="xmark"
@action={{this.cancel}}
@disabled={{this.host.isSaving}}
class="btn-danger"
/>
</td>
{{else}}
<td>
<div class="label">{{i18n "admin.embedding.host"}}</div>
{{this.host.host}}
</td>
<td>
<div class="label">
{{i18n "admin.embedding.allowed_paths"}}
</div>
{{this.host.allowed_paths}}
</td>
<td>
<div class="label">{{i18n "admin.embedding.category"}}</div>
{{category-badge this.category allowUncategorized=true}}
</td>
<td>
{{this.tags}}
</td>
<td>
{{this.user}}
</td>
<td class="controls">
<DButton @icon="pencil" @action={{this.edit}} />
<DButton @icon="trash-can" @action={{this.delete}} class="btn-danger" />
</td>
{{/if}}
<DMenu
@identifier="embedding-host-menu"
@title={{i18n "admin.embedding.more_options"}}
@icon="ellipsis-vertical"
>
<:content>
<DropdownMenu as |dropdown|>
<dropdown.item>
<DButton
@action={{this.delete}}
@icon="trash-can"
class="btn-transparent admin-embeddable-host-item__delete"
@label="admin.embedding.delete"
/>
</dropdown.item>
</DropdownMenu>
</:content>
</DMenu>
</div>
</td>

View File

@ -1,28 +1,18 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { or } from "@ember/object/computed";
import { service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import { tagName } from "@ember-decorators/component";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { classNames, tagName } from "@ember-decorators/component";
import Category from "discourse/models/category";
import discourseComputed from "discourse-common/utils/decorators";
import { i18n } from "discourse-i18n";
@tagName("tr")
export default class EmbeddableHost extends Component.extend(
bufferedProperty("host")
) {
@classNames("d-admin-row__content")
export default class EmbeddableHost extends Component {
@service dialog;
editToggled = false;
categoryId = null;
category = null;
tags = null;
user = null;
@or("host.isNew", "editToggled") editing;
init() {
super.init(...arguments);
@ -31,51 +21,10 @@ export default class EmbeddableHost extends Component.extend(
const category = Category.findById(categoryId);
this.set("category", category);
this.set("tags", host.tags || []);
this.set("tags", (host.tags || []).join(", "));
this.set("user", host.user);
}
@discourseComputed("buffered.host", "host.isSaving")
cantSave(host, isSaving) {
return isSaving || isEmpty(host);
}
@action
edit() {
this.set("editToggled", true);
}
@action
onUserChange(user) {
this.set("user", user);
}
@action
save() {
if (this.cantSave) {
return;
}
const props = this.buffered.getProperties(
"host",
"allowed_paths",
"class_name"
);
props.category_id = this.category.id;
props.tags = this.tags;
props.user =
Array.isArray(this.user) && this.user.length > 0 ? this.user[0] : null;
const host = this.host;
host
.save(props)
.then(() => {
this.set("editToggled", false);
})
.catch(popupAjaxError);
}
@action
delete() {
return this.dialog.confirm({
@ -87,15 +36,4 @@ export default class EmbeddableHost extends Component.extend(
},
});
}
@action
cancel() {
const host = this.host;
if (host.get("isNew")) {
this.deleteHost(host);
} else {
this.rollbackBuffer();
this.set("editToggled", false);
}
}
}

View File

@ -1,15 +0,0 @@
{{#if this.isCheckbox}}
<label for={{this.inputId}}>
<Input @checked={{this.checked}} id={{this.inputId}} @type="checkbox" />
{{i18n this.translationKey}}
</label>
{{else}}
<label for={{this.inputId}}>{{i18n this.translationKey}}</label>
<Input
@value={{this.value}}
id={{this.inputId}}
placeholder={{this.placeholder}}
/>
{{/if}}
<div class="clearfix"></div>

View File

@ -1,32 +0,0 @@
import Component from "@ember/component";
import { computed } from "@ember/object";
import { dasherize } from "@ember/string";
import { classNames } from "@ember-decorators/component";
import discourseComputed from "discourse-common/utils/decorators";
@classNames("embed-setting")
export default class EmbeddingSetting extends Component {
@discourseComputed("field")
inputId(field) {
return dasherize(field);
}
@discourseComputed("field")
translationKey(field) {
return `admin.embedding.${field}`;
}
@discourseComputed("type")
isCheckbox(type) {
return type === "checkbox";
}
@computed("value")
get checked() {
return !!this.value;
}
set checked(value) {
this.set("value", value);
}
}

View File

@ -0,0 +1,35 @@
import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
export default class AdminEmbeddingCrawlerSettingsController extends Controller {
@service toasts;
@controller adminEmbedding;
get formData() {
const embedding = this.adminEmbedding.embedding;
return {
allowed_embed_selectors: embedding.allowed_embed_selectors,
blocked_embed_selectors: embedding.blocked_embed_selectors,
allowed_embed_classnames: embedding.allowed_embed_classnames,
};
}
@action
async save(data) {
const embedding = this.adminEmbedding.embedding;
try {
await embedding.update(data);
this.toasts.success({
duration: 1500,
data: { message: i18n("admin.embedding.crawler_settings_saved") },
});
} catch (error) {
popupAjaxError(error);
}
}
}

View File

@ -0,0 +1,44 @@
import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { alias } from "@ember/object/computed";
import { service } from "@ember/service";
import discourseComputed from "discourse-common/utils/decorators";
export default class AdminEmbeddingIndexController extends Controller {
@service router;
@service site;
@controller adminEmbedding;
@alias("adminEmbedding.embedding") embedding;
get showEmbeddingCode() {
const hosts = this.get("embedding.embeddable_hosts");
return hosts.length > 0 && !this.site.isMobileDevice;
}
@discourseComputed("embedding.base_url")
embeddingCode(baseUrl) {
const html = `<div id='discourse-comments'></div>
<meta name='discourse-username' content='DISCOURSE_USERNAME'>
<script type="text/javascript">
DiscourseEmbed = {
discourseUrl: '${baseUrl}/',
discourseEmbedUrl: 'EMBED_URL',
// className: 'CLASS_NAME',
};
(function() {
var d = document.createElement('script'); d.type = 'text/javascript'; d.async = true;
d.src = DiscourseEmbed.discourseUrl + 'javascripts/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(d);
})();
</script>`;
return html;
}
@action
deleteHost(host) {
this.get("embedding.embeddable_hosts").removeObject(host);
}
}

View File

@ -0,0 +1,42 @@
import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
export default class AdminEmbeddingSettingsController extends Controller {
@service toasts;
@controller adminEmbedding;
get formData() {
const embedding = this.adminEmbedding.embedding;
return {
embed_by_username: isEmpty(embedding.embed_by_username)
? null
: embedding.embed_by_username,
embed_post_limit: embedding.embed_post_limit,
embed_title_scrubber: embedding.embed_title_scrubber,
embed_truncate: embedding.embed_truncate,
embed_unlisted: embedding.embed_unlisted,
};
}
@action
async save(data) {
const embedding = this.adminEmbedding.embedding;
try {
await embedding.update({
...data,
embed_by_username: data.embed_by_username[0],
});
this.toasts.success({
duration: 1500,
data: { message: i18n("admin.embedding.embedding_settings_saved") },
});
} catch (error) {
popupAjaxError(error);
}
}
}

View File

@ -1,61 +1,13 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed from "discourse-common/utils/decorators";
import { service } from "@ember/service";
export default class AdminEmbeddingController extends Controller {
saved = false;
embedding = null;
// show settings if we have at least one created host
@discourseComputed("embedding.embeddable_hosts.@each.isCreated")
showSecondary() {
const hosts = this.get("embedding.embeddable_hosts");
return hosts.length && hosts.findBy("isCreated");
}
@discourseComputed("embedding.base_url")
embeddingCode(baseUrl) {
const html = `<div id='discourse-comments'></div>
<meta name='discourse-username' content='DISCOURSE_USERNAME'>
<script type="text/javascript">
DiscourseEmbed = {
discourseUrl: '${baseUrl}/',
discourseEmbedUrl: 'EMBED_URL',
// className: 'CLASS_NAME',
};
(function() {
var d = document.createElement('script'); d.type = 'text/javascript'; d.async = true;
d.src = DiscourseEmbed.discourseUrl + 'javascripts/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(d);
})();
</script>`;
return html;
}
@action
saveChanges() {
const embedding = this.embedding;
const updates = embedding.getProperties(embedding.get("fields"));
this.set("saved", false);
this.embedding
.update(updates)
.then(() => this.set("saved", true))
.catch(popupAjaxError);
}
@action
addHost() {
const host = this.store.createRecord("embeddable-host");
this.get("embedding.embeddable_hosts").pushObject(host);
}
@action
deleteHost(host) {
this.get("embedding.embeddable_hosts").removeObject(host);
@service router;
get showHeader() {
return [
"adminEmbedding.index",
"adminEmbedding.settings",
"adminEmbedding.crawler_settings",
].includes(this.router.currentRouteName);
}
}

View File

@ -0,0 +1,13 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
export default class AdminEmbeddingEditRoute extends DiscourseRoute {
async model(params) {
const embedding = await this.store.find("embedding");
return embedding.embeddable_hosts.findBy("id", parseInt(params.id, 10));
}
titleToken() {
return i18n("admin.embedding.host_form.edit_header");
}
}

View File

@ -0,0 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
export default class AdminEmbeddingNewRoute extends DiscourseRoute {
titleToken() {
return i18n("admin.embedding.host_form.add_header");
}
}

View File

@ -96,10 +96,20 @@ export default function () {
this.route("edit", { path: "/:permalink_id" });
}
);
this.route("adminEmbedding", {
path: "/embedding",
resetNamespace: true,
});
this.route(
"adminEmbedding",
{
path: "/embedding",
resetNamespace: true,
},
function () {
this.route("index", { path: "/" });
this.route("settings");
this.route("crawler_settings");
this.route("new");
this.route("edit", { path: "/:id" });
}
);
this.route(
"adminCustomizeEmailTemplates",
{ path: "/email_templates", resetNamespace: true },

View File

@ -0,0 +1,32 @@
<AdminPageSubheader
@titleLabelTranslated={{i18n "admin.embedding.crawling_settings"}}
@descriptionLabelTranslated={{i18n "admin.embedding.crawling_description"}}
/>
<Form @onSubmit={{this.save}} @data={{this.formData}} as |form|>
<form.Field
@name="allowed_embed_selectors"
@title={{i18n "admin.embedding.allowed_embed_selectors"}}
@format="large"
as |field|
>
<field.Input placeholder="article, #story, .post" />
</form.Field>
<form.Field
@name="blocked_embed_selectors"
@title={{i18n "admin.embedding.blocked_embed_selectors"}}
@format="large"
as |field|
>
<field.Input placeholder=".ad-unit, header" />
</form.Field>
<form.Field
@name="allowed_embed_classnames"
@title={{i18n "admin.embedding.allowed_embed_classnames"}}
@format="large"
as |field|
>
<field.Input placeholder="emoji, classname" />
</form.Field>
<form.Submit @label="admin.embedding.save" />
</Form>

View File

@ -0,0 +1 @@
<AdminEmbeddingHostForm @host={{this.model}} />

View File

@ -0,0 +1,42 @@
{{#if this.embedding.embeddable_hosts}}
<table class="d-admin-table">
<thead>
<th>{{i18n "admin.embedding.host"}}</th>
<th>{{i18n "admin.embedding.allowed_paths"}}</th>
<th>{{i18n "admin.embedding.category"}}</th>
<th>{{i18n "admin.embedding.tags"}}</th>
{{#if this.embedding.embed_by_username}}
<th>{{i18n
"admin.embedding.post_author"
author=this.embedding.embed_by_username
}}</th>
{{else}}
<th>{{i18n "admin.embedding.post_author"}}</th>
{{/if}}
</thead>
<tbody>
{{#each this.embedding.embeddable_hosts as |host|}}
<EmbeddableHost @host={{host}} @deleteHost={{action "deleteHost"}} />
{{/each}}
</tbody>
</table>
{{else}}
<AdminConfigAreaEmptyList
@ctaLabel="admin.embedding.add_host"
@ctaRoute="adminEmbedding.new"
@ctaClass="admin-embedding__add-host"
@emptyLabel="admin.embedding.get_started"
/>
{{/if}}
<PluginOutlet
@name="after-embeddable-hosts-table"
@outletArgs={{hash embedding=this.embedding}}
/>
{{#if this.showEmbeddingCode}}
<div class="admin-embedding-index__code">
{{html-safe (i18n "admin.embedding.sample")}}
<HighlightedCode @code={{this.embeddingCode}} @lang="html" />
</div>
{{/if}}

View File

@ -0,0 +1 @@
<AdminEmbeddingHostForm />

View File

@ -0,0 +1,53 @@
<AdminPageSubheader @titleLabelTranslated={{i18n "admin.embedding.settings"}} />
<Form @onSubmit={{this.save}} @data={{this.formData}} as |form|>
<form.Field
@name="embed_by_username"
@title={{i18n "admin.embedding.embed_by_username"}}
@validation="required"
as |field|
>
<field.Custom>
<UserChooser
@value={{field.value}}
@onChange={{field.set}}
@options={{hash maximum=1 excludeCurrentUser=false}}
class="admin-embedding-settings-form__embed_by_username"
/>
</field.Custom>
</form.Field>
<form.Field
@name="embed_post_limit"
@title={{i18n "admin.embedding.embed_post_limit"}}
@format="large"
as |field|
>
<field.Input />
</form.Field>
<form.Field
@name="embed_title_scrubber"
@title={{i18n "admin.embedding.embed_title_scrubber"}}
@format="large"
as |field|
>
<field.Input placeholder="- site.com$" />
</form.Field>
<form.CheckboxGroup as |checkboxGroup|>
<checkboxGroup.Field
@name="embed_truncate"
@title={{i18n "admin.embedding.embed_truncate"}}
as |field|
>
<field.Checkbox />
</checkboxGroup.Field>
<checkboxGroup.Field
@name="embed_unlisted"
@title={{i18n "admin.embedding.embed_unlisted"}}
as |field|
>
<field.Checkbox />
</checkboxGroup.Field>
</form.CheckboxGroup>
<form.Submit @label="admin.embedding.save" />
</Form>

View File

@ -1,111 +1,45 @@
<div class="embeddable-hosts">
{{#if this.embedding.embeddable_hosts}}
<table class="embedding grid">
<thead>
<th style="width: 18%">{{i18n "admin.embedding.host"}}</th>
<th style="width: 18%">{{i18n "admin.embedding.allowed_paths"}}</th>
<th style="width: 18%">{{i18n "admin.embedding.category"}}</th>
<th style="width: 18%">{{i18n "admin.embedding.tags"}}</th>
{{#if this.embedding.embed_by_username}}
<th style="width: 18%">{{i18n
"admin.embedding.post_author"
author=this.embedding.embed_by_username
}}</th>
{{else}}
<th style="width: 18%">{{i18n "admin.embedding.post_author"}}</th>
{{/if}}
<th style="width: 10%">&nbsp;</th>
</thead>
<tbody>
{{#each this.embedding.embeddable_hosts as |host|}}
<EmbeddableHost @host={{host}} @deleteHost={{action "deleteHost"}} />
{{/each}}
</tbody>
</table>
{{else}}
<p>{{i18n "admin.embedding.get_started"}}</p>
<div class="admin-embedding admin-config-page">
{{#if this.showHeader}}
<AdminPageHeader
@titleLabel="admin.embedding.title"
@descriptionLabel="admin.embedding.description"
@learnMoreUrl="https://meta.discourse.org/t/embed-discourse-comments-on-another-website-via-javascript/31963"
>
<:breadcrumbs>
<DBreadcrumbsItem
@path="/admin/customize/embedding"
@label={{i18n "admin.embedding.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>
<actions.Primary
@route="adminEmbedding.new"
@title="admin.embedding.add_host"
@label="admin.embedding.add_host"
class="admin-embedding__header-add-host"
/>
</:actions>
<:tabs>
<NavItem
@route="adminEmbedding.index"
@label="admin.embedding.nav.hosts"
class="admin-embedding-tabs__hosts"
/>
<NavItem
@route="adminEmbedding.settings"
@label="admin.embedding.nav.embedding_settings"
class="admin-embedding-tabs__embedding-settings"
/>
<NavItem
@route="adminEmbedding.crawler_settings"
@label="admin.embedding.nav.crawler_settings"
class="admin-embedding-tabs__crawler-settings"
/>
</:tabs>
</AdminPageHeader>
{{/if}}
<DButton
@label="admin.embedding.add_host"
@action={{this.addHost}}
@icon="plus"
class="btn-primary add-host"
/>
<PluginOutlet
@name="after-embeddable-hosts-table"
@outletArgs={{hash embedding=this.embedding}}
/>
</div>
{{#if this.showSecondary}}
<div class="embedding-secondary">
{{html-safe (i18n "admin.embedding.sample")}}
<HighlightedCode @code={{this.embeddingCode}} @lang="html" />
<div class="admin-container admin-config-page__main-area">
{{outlet}}
</div>
<hr />
<div class="embedding-secondary">
<h3>{{i18n "admin.embedding.settings"}}</h3>
<EmbeddingSetting
@field="embed_by_username"
@value={{this.embedding.embed_by_username}}
/>
<EmbeddingSetting
@field="embed_post_limit"
@value={{this.embedding.embed_post_limit}}
/>
<EmbeddingSetting
@field="embed_title_scrubber"
@value={{this.embedding.embed_title_scrubber}}
@placeholder="- site.com$"
/>
<EmbeddingSetting
@field="embed_truncate"
@value={{this.embedding.embed_truncate}}
@type="checkbox"
/>
<EmbeddingSetting
@field="embed_unlisted"
@value={{this.embedding.embed_unlisted}}
@type="checkbox"
/>
</div>
<div class="embedding-secondary">
<h3>{{i18n "admin.embedding.crawling_settings"}}</h3>
<p class="description">{{i18n "admin.embedding.crawling_description"}}</p>
<EmbeddingSetting
@field="allowed_embed_selectors"
@value={{this.embedding.allowed_embed_selectors}}
@placeholder="article, #story, .post"
/>
<EmbeddingSetting
@field="blocked_embed_selectors"
@value={{this.embedding.blocked_embed_selectors}}
@placeholder=".ad-unit, header"
/>
<EmbeddingSetting
@field="allowed_embed_classnames"
@value={{this.embedding.allowed_embed_classnames}}
@placeholder="emoji, classname"
/>
</div>
<div class="embedding-secondary">
<DButton
@label="admin.embedding.save"
@action={{this.saveChanges}}
@disabled={{this.embedding.isSaving}}
class="btn-primary embed-save"
/>
{{#if this.saved}}{{i18n "saved"}}{{/if}}
</div>
{{/if}}
</div>

View File

@ -68,7 +68,7 @@
class="btn-small admin-permalink-item__edit"
@route="adminPermalinks.edit"
@routeModels={{pl}}
@label="admin.config_areas.flags.edit"
@label="admin.config_areas.permalinks.edit"
/>
<DMenu
@ -84,7 +84,7 @@
@action={{fn this.destroyRecord pl}}
@icon="trash-can"
class="btn-transparent admin-permalink-item__delete"
@label="admin.config_areas.flags.delete"
@label="admin.config_areas.permalinks.delete"
/>
</dropdown.item>
</DropdownMenu>

View File

@ -16,7 +16,6 @@
@route="adminPermalinks.new"
@title="admin.permalink.add"
@label="admin.permalink.add"
@icon="plus"
class="admin-permalinks__header-add-permalink"
/>
</:actions>

View File

@ -897,26 +897,11 @@
}
}
.embedding-secondary {
h3 {
margin: 1em 0;
.admin-embeddable-host-item__delete {
color: var(--danger);
svg.d-icon {
color: var(--danger);
}
margin-bottom: 2em;
.embed-setting {
input[type="text"] {
width: 50%;
}
margin: 0.75em 0;
}
p.description {
color: var(--primary-medium);
margin-bottom: 1em;
max-width: 700px;
}
}
.embedding td input {
margin-bottom: 0;
}
.user-fields {

View File

@ -26,7 +26,7 @@ class Admin::EmbeddableHostsController < Admin::AdminController
host.host = params[:embeddable_host][:host]
host.allowed_paths = params[:embeddable_host][:allowed_paths]
host.category_id = params[:embeddable_host][:category_id]
host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank?
host.category_id = SiteSetting.uncategorized_category_id if host.category.blank?
username = params[:embeddable_host][:user]

View File

@ -8,10 +8,6 @@ class Admin::EmbeddingController < Admin::AdminController
end
def update
if params[:embedding][:embed_by_username].blank?
return render_json_error(I18n.t("site_settings.embed_username_required"))
end
Embedding.settings.each { |s| @embedding.public_send("#{s}=", params[:embedding][s]) }
if @embedding.save
@ -22,6 +18,18 @@ class Admin::EmbeddingController < Admin::AdminController
end
end
def new
end
def edit
end
def settings
end
def crawler_settings
end
protected
def fetch_embedding

View File

@ -5736,6 +5736,12 @@ en:
title: "More options"
move_up: "Move up"
move_down: "Move down"
permalinks:
edit: "Edit"
delete: "Delete"
embeddable_host:
edit: "Edit"
delete: "Delete"
look_and_feel:
title: "Look and feel"
description: "Customize and brand your Discourse site, giving it a distinctive style."
@ -7296,6 +7302,7 @@ en:
embedding:
get_started: "If you'd like to embed Discourse on another website, begin by adding its host."
delete: "Delete"
confirm_delete: "Are you sure you want to delete that host?"
sample: |
<p>Paste the following HTML code into your site to create and embed Discourse topics. Replace <b>EMBED_URL</b> with the canonical URL of the page you are embedding it on.</p>
@ -7304,6 +7311,7 @@ en:
<p>Replace <b>DISCOURSE_USERNAME</b> with the Discourse username of the author that should create the topic. Discourse will automatically lookup the user by the <code>content</code> attribute of the <code>&lt;meta&gt;</code> tags with <code>name</code> attribute set to <code>discourse-username</code> or <code>author</code>. The <code>discourseUserName</code> parameter has been deprecated and will be removed in Discourse 3.2.</p>
title: "Embedding"
description: "Discourse has the ability to embed the comments from a topic in a remote site using a Javascript API that creates an IFRAME"
host: "Allowed Hosts"
allowed_paths: "Path Allowlist"
edit: "edit"
@ -7324,6 +7332,18 @@ en:
blocked_embed_selectors: "CSS selector for elements that are removed from embeds"
allowed_embed_classnames: "Allowed CSS class names"
save: "Save Embedding Settings"
embedding_settings_saved: "Embedding settings saved."
crawler_settings_saved: "Crawler settings saved."
back: "Back to Embedding"
more_options: "More options"
host_form:
add_header: "Add host"
edit_header: "Edit host"
save: "Save"
nav:
hosts: "Hosts"
embedding_settings: "Embedding Settings"
crawler_settings: "Crawler Settings"
permalink:
title: "Permalinks"

View File

@ -223,6 +223,11 @@ Discourse::Application.routes.draw do
get "customize/permalinks" => "permalinks#index", :constraints => AdminConstraint.new
get "customize/embedding" => "embedding#show", :constraints => AdminConstraint.new
put "customize/embedding" => "embedding#update", :constraints => AdminConstraint.new
get "customize/embedding/settings" => "embedding#settings",
:constraints => AdminConstraint.new
get "customize/embedding/crawler_settings" => "embedding#crawler_settings",
:constraints => AdminConstraint.new
get "customize/embedding/:id" => "embedding#edit", :constraints => AdminConstraint.new
resources :themes,
only: %i[index create show update destroy],

View File

@ -3,59 +3,74 @@
RSpec.describe "Admin EmbeddableHost Management", type: :system do
fab!(:admin)
fab!(:author) { Fabricate(:admin) }
fab!(:author_2) { Fabricate(:admin) }
fab!(:category)
fab!(:category2) { Fabricate(:category) }
fab!(:category_2) { Fabricate(:category) }
fab!(:tag)
fab!(:tag2) { Fabricate(:tag) }
fab!(:tag_2) { Fabricate(:tag) }
before { sign_in(admin) }
it "allows admin to add and edit embeddable hosts" do
visit "/admin/customize/embedding"
let(:admin_embedding_page) { PageObjects::Pages::AdminEmbedding.new }
let(:admin_embedding_host_form_page) { PageObjects::Pages::AdminEmbeddingHostForm.new }
let(:admin_embedding_settings_page) { PageObjects::Pages::AdminEmbeddingSettings.new }
find("button.btn-icon-text", text: "Add Host").click
within find("tr.ember-view") do
find('input[placeholder="example.com"]').set("awesome-discourse-site.local")
find('input[placeholder="/blog/.*"]').set("/blog/.*")
it "allows admin to add, edit and delete embeddable hosts" do
admin_embedding_page.visit
category_chooser = PageObjects::Components::SelectKit.new(".category-chooser")
category_chooser.expand
category_chooser.select_row_by_name(category.name)
expect(page).not_to have_css(".admin-embedding-index__code")
tag_chooser = PageObjects::Components::SelectKit.new(".tag-chooser")
tag_chooser.expand
tag_chooser.select_row_by_name(tag.name)
admin_embedding_page.click_add_host
admin_embedding_host_form_page.fill_in_allowed_hosts("awesome-discourse-site.local")
admin_embedding_host_form_page.fill_in_path_allow_list("/blog/.*")
admin_embedding_host_form_page.fill_in_category(category)
admin_embedding_host_form_page.fill_in_tags(tag)
admin_embedding_host_form_page.fill_in_post_author(author)
admin_embedding_host_form_page.click_save
find(".user-chooser").click
find(".select-kit-body .select-kit-filter input").fill_in with: author.username
find(".select-kit-body", text: author.username).click
end
find("td.editing-controls .btn.btn-primary").click
expect(page).to have_content("awesome-discourse-site.local")
expect(page).to have_content("/blog/.*")
expect(page).not_to have_content("#{tag.name},#{tag2.name}")
expect(page).to have_content("#{tag.name}")
expect(page).to have_content("#{category.name}")
expect(page).to have_content("#{author.username}")
# Editing
expect(page).to have_css(".admin-embedding-index__code")
find(".embeddable-hosts tr:first-child .controls svg.d-icon-pencil").find(:xpath, "..").click
admin_embedding_page.click_edit_host
within find(".embeddable-hosts tr:first-child.ember-view") do
find('input[placeholder="example.com"]').set("updated-example.com")
find('input[placeholder="/blog/.*"]').set("/updated-blog/.*")
admin_embedding_host_form_page.fill_in_allowed_hosts("updated-example.com")
admin_embedding_host_form_page.fill_in_path_allow_list("/updated-blog/.*")
admin_embedding_host_form_page.fill_in_category(category_2)
admin_embedding_host_form_page.fill_in_tags(tag_2)
admin_embedding_host_form_page.fill_in_post_author(author_2)
admin_embedding_host_form_page.click_save
category_chooser = PageObjects::Components::SelectKit.new(".category-chooser")
category_chooser.expand
category_chooser.select_row_by_name(category2.name)
tag_chooser = PageObjects::Components::SelectKit.new(".tag-chooser")
tag_chooser.expand
tag_chooser.select_row_by_name(tag2.name)
end
find("td.editing-controls .btn.btn-primary").click
expect(page).to have_content("updated-example.com")
expect(page).to have_content("/updated-blog/.*")
expect(page).to have_content("#{tag.name},#{tag2.name}")
expect(page).to have_content("#{tag.name}, #{tag_2.name}")
expect(page).to have_content("#{category_2.name}")
expect(page).to have_content("#{author_2.username}")
admin_embedding_page.open_embedding_host_menu
admin_embedding_page.click_delete
admin_embedding_page.confirm_delete
expect(page).not_to have_css(".admin-embedding-index__code")
end
it "allows admin to save embedding settings" do
Fabricate(:embeddable_host)
admin_embedding_page.visit
expect(page).not_to have_content("#{author.username}")
admin_embedding_page.click_embedding_settings_tab
admin_embedding_settings_page.fill_in_embed_by_username(author)
admin_embedding_settings_page.click_save
admin_embedding_page.click_embedding_hosts_tab
expect(page).to have_content("#{author.username}")
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module PageObjects
module Pages
class AdminEmbedding < PageObjects::Pages::Base
def visit
page.visit("/admin/customize/embedding")
self
end
def click_embedding_settings_tab
find(".admin-embedding-tabs__embedding-settings").click
end
def click_embedding_hosts_tab
find(".admin-embedding-tabs__hosts").click
end
def click_add_host
find(".admin-embedding__header-add-host").click
self
end
def click_edit_host
find(".admin-embeddable-host-item__edit").click
self
end
def open_embedding_host_menu
find(".embedding-host-menu-trigger").click
self
end
def click_delete
find(".admin-embeddable-host-item__delete").click
self
end
def confirm_delete
find(".dialog-footer .btn-primary").click
expect(page).to have_no_css(".dialog-body", wait: Capybara.default_max_wait_time * 3)
self
end
end
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
module PageObjects
module Pages
class AdminEmbeddingHostForm < PageObjects::Pages::Base
def fill_in_allowed_hosts(url)
form.field("host").fill_in(url)
self
end
def fill_in_path_allow_list(path)
form.field("allowed_paths").fill_in(path)
self
end
def fill_in_category(category)
dropdown = PageObjects::Components::SelectKit.new(".admin-embedding-host-form__category")
dropdown.expand
dropdown.search(category.name)
dropdown.select_row_by_value(category.id)
dropdown.collapse
self
end
def fill_in_tags(tag)
dropdown = PageObjects::Components::SelectKit.new(".admin-embedding-host-form__tags")
dropdown.expand
dropdown.search(tag.name)
dropdown.select_row_by_value(tag.name)
dropdown.collapse
self
end
def fill_in_post_author(author)
dropdown = PageObjects::Components::SelectKit.new(".admin-embedding-host-form__post_author")
dropdown.expand
dropdown.search(author.username)
dropdown.select_row_by_value(author.username)
dropdown.collapse
self
end
def click_save
form.submit
expect(page).to have_css(".d-admin-table")
end
def form
@form ||= PageObjects::Components::FormKit.new(".admin-embedding-host-form .form-kit")
end
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module PageObjects
module Pages
class AdminEmbeddingSettings < PageObjects::Pages::Base
def fill_in_embed_by_username(author)
dropdown =
PageObjects::Components::SelectKit.new(
".admin-embedding-settings-form__embed_by_username",
)
dropdown.expand
dropdown.search(author.username)
dropdown.select_row_by_value(author.username)
dropdown.collapse
self
end
def click_save
form = PageObjects::Components::FormKit.new(".admin-embedding .form-kit")
form.submit
end
end
end
end