mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: Convert chat plugin UI to new show plugin and admin UI guidelines (#28632)
This commit converts the current chat plugin UI into the new "show plugin" UI already followed by AI and Gamification. In the process, I also: * Made a dedicated /new route to create new webhooks * Converted the webhook form to FormKit * Made some fixes and improvements to the `AdminPluginConfigPage`, `AdminPageHeader`, and `AdminPageSubheader` generic components, so more plugins can adopt the UI guidelines too. This includes adding a header outlet so plugins can add action buttons to the plugin show page header. * Fixes the submit button loading state for FormKit (by Joffrey) --------- Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
parent
56877e9acf
commit
61c1d35f17
@ -127,15 +127,17 @@ export default class AdminFlagItem extends Component {
|
|||||||
}}</p>
|
}}</p>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="admin-flag-item__options">
|
<DToggleSwitch
|
||||||
<DToggleSwitch
|
@state={{this.enabled}}
|
||||||
@state={{this.enabled}}
|
class="admin-flag-item__toggle {{@flag.name_key}}"
|
||||||
class="admin-flag-item__toggle {{@flag.name_key}}"
|
{{on "click" (fn this.toggleFlagEnabled @flag)}}
|
||||||
{{on "click" (fn this.toggleFlagEnabled @flag)}}
|
/>
|
||||||
/>
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="admin-flag-item__options admin-table-row-controls">
|
||||||
|
|
||||||
<DButton
|
<DButton
|
||||||
class="btn btn-secondary admin-flag-item__edit"
|
class="btn-small admin-flag-item__edit"
|
||||||
@action={{this.edit}}
|
@action={{this.edit}}
|
||||||
@label="admin.config_areas.flags.edit"
|
@label="admin.config_areas.flags.edit"
|
||||||
@disabled={{not this.canEdit}}
|
@disabled={{not this.canEdit}}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
import { hash } from "@ember/helper";
|
import { hash } from "@ember/helper";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container";
|
import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container";
|
||||||
@ -10,43 +11,62 @@ import {
|
|||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
} from "admin/components/admin-page-action-button";
|
} from "admin/components/admin-page-action-button";
|
||||||
|
|
||||||
const AdminPageHeader = <template>
|
export default class AdminPageHeader extends Component {
|
||||||
<div class="admin-page-header">
|
get title() {
|
||||||
<div class="admin-page-header__breadcrumbs">
|
if (this.args.titleLabelTranslated) {
|
||||||
<DBreadcrumbsContainer />
|
return this.args.titleLabelTranslated;
|
||||||
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
|
} else if (this.args.titleLabel) {
|
||||||
{{yield to="breadcrumbs"}}
|
return i18n(this.args.titleLabel);
|
||||||
</div>
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<div class="admin-page-header__title-row">
|
get description() {
|
||||||
{{#if @titleLabel}}
|
if (this.args.descriptionLabelTranslated) {
|
||||||
<h1 class="admin-page-header__title">{{i18n @titleLabel}}</h1>
|
return this.args.descriptionLabelTranslated;
|
||||||
{{/if}}
|
} else if (this.args.descriptionLabel) {
|
||||||
<div class="admin-page-header__actions">
|
return i18n(this.args.descriptionLabel);
|
||||||
{{yield
|
}
|
||||||
(hash Primary=PrimaryButton Default=DefaultButton Danger=DangerButton)
|
}
|
||||||
to="actions"
|
|
||||||
}}
|
<template>
|
||||||
|
<div class="admin-page-header">
|
||||||
|
<div class="admin-page-header__breadcrumbs">
|
||||||
|
<DBreadcrumbsContainer />
|
||||||
|
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
|
||||||
|
{{yield to="breadcrumbs"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if @descriptionLabel}}
|
<div class="admin-page-header__title-row">
|
||||||
<p class="admin-page-header__description">
|
{{#if this.title}}
|
||||||
{{i18n @descriptionLabel}}
|
<h1 class="admin-page-header__title">{{this.title}}</h1>
|
||||||
{{#if @learnMoreUrl}}
|
|
||||||
{{htmlSafe (i18n "learn_more_with_link" url=@learnMoreUrl)}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</p>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#unless @hideTabs}}
|
<div class="admin-page-header__actions">
|
||||||
<div class="admin-nav-submenu">
|
{{yield
|
||||||
<HorizontalOverflowNav class="admin-nav-submenu__tabs">
|
(hash
|
||||||
{{yield to="tabs"}}
|
Primary=PrimaryButton Default=DefaultButton Danger=DangerButton
|
||||||
</HorizontalOverflowNav>
|
)
|
||||||
|
to="actions"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/unless}}
|
|
||||||
</div>
|
|
||||||
</template>;
|
|
||||||
|
|
||||||
export default AdminPageHeader;
|
{{#if this.description}}
|
||||||
|
<p class="admin-page-header__description">
|
||||||
|
{{htmlSafe this.description}}
|
||||||
|
{{#if @learnMoreUrl}}
|
||||||
|
{{htmlSafe (i18n "learn_more_with_link" url=@learnMoreUrl)}}
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#unless @hideTabs}}
|
||||||
|
<div class="admin-nav-submenu">
|
||||||
|
<HorizontalOverflowNav class="admin-nav-submenu__tabs">
|
||||||
|
{{yield to="tabs"}}
|
||||||
|
</HorizontalOverflowNav>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
import { hash } from "@ember/helper";
|
import { hash } from "@ember/helper";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import i18n from "discourse-common/helpers/i18n";
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
@ -7,27 +8,45 @@ import {
|
|||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
} from "admin/components/admin-page-action-button";
|
} from "admin/components/admin-page-action-button";
|
||||||
|
|
||||||
const AdminPageSubheader = <template>
|
export default class AdminPageSubheader extends Component {
|
||||||
<div class="admin-page-subheader">
|
get title() {
|
||||||
<div class="admin-page-subheader__title-row">
|
if (this.args.titleLabelTranslated) {
|
||||||
<h3 class="admin-page-subheader__title">{{i18n @titleLabel}}</h3>
|
return this.args.titleLabelTranslated;
|
||||||
<div class="admin-page-subheader__actions">
|
} else if (this.args.titleLabel) {
|
||||||
{{yield
|
return i18n(this.args.titleLabel);
|
||||||
(hash Primary=PrimaryButton Default=DefaultButton Danger=DangerButton)
|
}
|
||||||
to="actions"
|
}
|
||||||
}}
|
|
||||||
|
get description() {
|
||||||
|
if (this.args.descriptionLabelTranslated) {
|
||||||
|
return this.args.descriptionLabelTranslated;
|
||||||
|
} else if (this.args.descriptionLabel) {
|
||||||
|
return i18n(this.args.descriptionLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="admin-page-subheader">
|
||||||
|
<div class="admin-page-subheader__title-row">
|
||||||
|
<h3 class="admin-page-subheader__title">{{this.title}}</h3>
|
||||||
|
<div class="admin-page-subheader__actions">
|
||||||
|
{{yield
|
||||||
|
(hash
|
||||||
|
Primary=PrimaryButton Default=DefaultButton Danger=DangerButton
|
||||||
|
)
|
||||||
|
to="actions"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if @descriptionLabel}}
|
{{#if this.description}}
|
||||||
<p class="admin-page-header__description">
|
<p class="admin-page-subheader__description">
|
||||||
{{i18n @descriptionLabel}}
|
{{htmlSafe this.description}}
|
||||||
{{#if @learnMoreUrl}}
|
{{#if @learnMoreUrl}}
|
||||||
{{htmlSafe (i18n "learn_more_with_link" url=@learnMoreUrl)}}
|
{{htmlSafe (i18n "learn_more_with_link" url=@learnMoreUrl)}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>;
|
}
|
||||||
|
|
||||||
export default AdminPageSubheader;
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import i18n from "discourse-common/helpers/i18n";
|
|
||||||
|
|
||||||
const AdminPluginConfigMetadata = <template>
|
|
||||||
<div class="admin-plugin-config-page__metadata">
|
|
||||||
<div class="admin-plugin-config-area__metadata-title">
|
|
||||||
<h1>
|
|
||||||
{{@plugin.nameTitleized}}
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
{{@plugin.about}}
|
|
||||||
{{#if @plugin.linkUrl}}
|
|
||||||
|
|
|
||||||
<a href={{@plugin.linkUrl}} rel="noopener noreferrer" target="_blank">
|
|
||||||
{{i18n "admin.plugins.learn_more"}}
|
|
||||||
</a>
|
|
||||||
{{/if}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>;
|
|
||||||
|
|
||||||
export default AdminPluginConfigMetadata;
|
|
@ -1,11 +1,12 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
|
import { hash } from "@ember/helper";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container";
|
|
||||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||||
|
import NavItem from "discourse/components/nav-item";
|
||||||
|
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||||
import i18n from "discourse-common/helpers/i18n";
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import AdminPageHeader from "./admin-page-header";
|
||||||
import AdminPluginConfigArea from "./admin-plugin-config-area";
|
import AdminPluginConfigArea from "./admin-plugin-config-area";
|
||||||
import AdminPluginConfigMetadata from "./admin-plugin-config-metadata";
|
|
||||||
import AdminPluginConfigTopNav from "./admin-plugin-config-top-nav";
|
|
||||||
|
|
||||||
export default class AdminPluginConfigPage extends Component {
|
export default class AdminPluginConfigPage extends Component {
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@ -23,25 +24,56 @@ export default class AdminPluginConfigPage extends Component {
|
|||||||
return classes.join(" ");
|
return classes.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
linkText(navLink) {
|
||||||
|
if (navLink.label) {
|
||||||
|
return i18n(navLink.label);
|
||||||
|
} else {
|
||||||
|
return navLink.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="admin-plugin-config-page">
|
<div class="admin-plugin-config-page">
|
||||||
<DBreadcrumbsContainer />
|
<AdminPageHeader
|
||||||
|
@titleLabelTranslated={{@plugin.nameTitleized}}
|
||||||
|
@descriptionLabelTranslated={{@plugin.about}}
|
||||||
|
@learnMoreUrl={{@plugin.linkUrl}}
|
||||||
|
>
|
||||||
|
<:breadcrumbs>
|
||||||
|
|
||||||
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
|
<DBreadcrumbsItem
|
||||||
<DBreadcrumbsItem
|
@path="/admin/plugins"
|
||||||
@path="/admin/plugins"
|
@label={{i18n "admin.plugins.title"}}
|
||||||
@label={{i18n "admin.plugins.title"}}
|
/>
|
||||||
/>
|
<DBreadcrumbsItem
|
||||||
<DBreadcrumbsItem
|
@path="/admin/plugins/{{@plugin.name}}"
|
||||||
@path="/admin/plugins/{{@plugin.name}}"
|
@label={{@plugin.nameTitleized}}
|
||||||
@label={{@plugin.nameTitleized}}
|
/>
|
||||||
/>
|
</:breadcrumbs>
|
||||||
|
<:tabs>
|
||||||
<AdminPluginConfigMetadata @plugin={{@plugin}} />
|
{{#if this.adminPluginNavManager.isTopMode}}
|
||||||
|
{{#each
|
||||||
{{#if this.adminPluginNavManager.isTopMode}}
|
this.adminPluginNavManager.currentConfigNav.links
|
||||||
<AdminPluginConfigTopNav />
|
as |navLink|
|
||||||
{{/if}}
|
}}
|
||||||
|
<NavItem
|
||||||
|
@route={{navLink.route}}
|
||||||
|
@i18nLabel={{this.linkText navLink}}
|
||||||
|
title={{this.linkText navLink}}
|
||||||
|
class="admin-plugin-config-page__top-nav-item"
|
||||||
|
>
|
||||||
|
{{this.linkText navLink}}
|
||||||
|
</NavItem>
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
</:tabs>
|
||||||
|
<:actions as |actions|>
|
||||||
|
<PluginOutlet
|
||||||
|
@name="admin-plugin-config-page-actions"
|
||||||
|
@outletArgs={{hash plugin=@plugin actions=actions}}
|
||||||
|
/>
|
||||||
|
</:actions>
|
||||||
|
</AdminPageHeader>
|
||||||
|
|
||||||
<div class="admin-plugin-config-page__content">
|
<div class="admin-plugin-config-page__content">
|
||||||
<div class={{this.mainAreaClasses}}>
|
<div class={{this.mainAreaClasses}}>
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import HorizontalOverflowNav from "discourse/components/horizontal-overflow-nav";
|
|
||||||
import NavItem from "discourse/components/nav-item";
|
|
||||||
import i18n from "discourse-common/helpers/i18n";
|
|
||||||
|
|
||||||
export default class AdminPluginConfigTopNav extends Component {
|
|
||||||
@service adminPluginNavManager;
|
|
||||||
|
|
||||||
linkText(navLink) {
|
|
||||||
if (navLink.label) {
|
|
||||||
return i18n(navLink.label);
|
|
||||||
} else {
|
|
||||||
return navLink.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="admin-nav-submenu">
|
|
||||||
<HorizontalOverflowNav
|
|
||||||
class="plugin-nav admin-plugin-config-page__top-nav"
|
|
||||||
>
|
|
||||||
{{#each this.adminPluginNavManager.currentConfigNav.links as |navLink|}}
|
|
||||||
<NavItem
|
|
||||||
@route={{navLink.route}}
|
|
||||||
@i18nLabel={{this.linkText navLink}}
|
|
||||||
title={{this.linkText navLink}}
|
|
||||||
class="admin-plugin-config-page__top-nav-item"
|
|
||||||
>
|
|
||||||
{{this.linkText navLink}}
|
|
||||||
</NavItem>
|
|
||||||
{{/each}}
|
|
||||||
</HorizontalOverflowNav>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
}
|
|
@ -1,8 +1,9 @@
|
|||||||
import Route from "@ember/routing/route";
|
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
import SiteSetting from "admin/models/site-setting";
|
import SiteSetting from "admin/models/site-setting";
|
||||||
|
|
||||||
export default class AdminPluginsShowSettingsRoute extends Route {
|
export default class AdminPluginsShowSettingsRoute extends DiscourseRoute {
|
||||||
@service router;
|
@service router;
|
||||||
|
|
||||||
queryParams = {
|
queryParams = {
|
||||||
@ -17,4 +18,8 @@ export default class AdminPluginsShowSettingsRoute extends Route {
|
|||||||
initialFilter: params.filter,
|
initialFilter: params.filter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
titleToken() {
|
||||||
|
return I18n.t("admin.plugins.change_settings_short");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import Route from "@ember/routing/route";
|
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { sanitize } from "discourse/lib/text";
|
import { sanitize } from "discourse/lib/text";
|
||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
import AdminPlugin from "admin/models/admin-plugin";
|
import AdminPlugin from "admin/models/admin-plugin";
|
||||||
|
|
||||||
export default class AdminPluginsShowRoute extends Route {
|
export default class AdminPluginsShowRoute extends DiscourseRoute {
|
||||||
@service router;
|
@service router;
|
||||||
@service adminPluginNavManager;
|
@service adminPluginNavManager;
|
||||||
|
|
||||||
@ -21,4 +21,8 @@ export default class AdminPluginsShowRoute extends Route {
|
|||||||
deactivate() {
|
deactivate() {
|
||||||
this.adminPluginNavManager.currentPlugin = null;
|
this.adminPluginNavManager.currentPlugin = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
titleToken() {
|
||||||
|
return this.adminPluginNavManager.currentPlugin.nameTitleized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { inject as service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ export default class FKSubmit extends Component {
|
|||||||
@forwardEvent="true"
|
@forwardEvent="true"
|
||||||
class="btn-primary form-kit__button"
|
class="btn-primary form-kit__button"
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={{@isSubmitting}}
|
@isLoading={{@isLoading}}
|
||||||
...attributes
|
...attributes
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -38,4 +38,14 @@ module("Integration | Component | FormKit | Layout | Submit", function (hooks) {
|
|||||||
.dom(".form-kit__button")
|
.dom(".form-kit__button")
|
||||||
.hasText(I18n.t("cancel"), "it allows to override the label");
|
.hasText(I18n.t("cancel"), "it allows to override the label");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("@isLoading", async function (assert) {
|
||||||
|
await render(<template>
|
||||||
|
<Form as |form|>
|
||||||
|
<form.Submit @label="cancel" @isLoading={{true}} />
|
||||||
|
</Form>
|
||||||
|
</template>);
|
||||||
|
|
||||||
|
assert.dom(".form-kit__button .d-icon-spinner").exists();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -142,6 +142,18 @@ $mobile-breakpoint: 700px;
|
|||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-table-row-controls {
|
||||||
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5em;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.fk-d-menu__trigger {
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-contents table.grid {
|
.admin-contents table.grid {
|
||||||
|
@ -7,24 +7,12 @@
|
|||||||
&__description {
|
&__description {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
&__options {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.d-toggle-switch__label {
|
.d-toggle-switch__label {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.d-toggle-switch {
|
.d-toggle-switch {
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
}
|
}
|
||||||
.btn-secondary {
|
|
||||||
padding: 0.25em 0.325em;
|
|
||||||
margin-right: 0.75em;
|
|
||||||
}
|
|
||||||
.flag-menu-trigger {
|
|
||||||
padding: 0.25em 0.325em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__delete.btn,
|
&__delete.btn,
|
||||||
&__delete.btn:hover {
|
&__delete.btn:hover {
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
|
||||||
|
|
||||||
|
export default class AdminChatIncomingWebhooksList extends Component {
|
||||||
|
@service dialog;
|
||||||
|
|
||||||
|
@tracked loading = false;
|
||||||
|
|
||||||
|
get sortedWebhooks() {
|
||||||
|
return this.args.webhooks?.sortBy("updated_at").reverse() || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
destroyWebhook(webhook) {
|
||||||
|
this.dialog.deleteConfirm({
|
||||||
|
message: I18n.t("chat.incoming_webhooks.confirm_destroy"),
|
||||||
|
didConfirm: async () => {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ajax(`/admin/plugins/chat/hooks/${webhook.id}`, {
|
||||||
|
type: "DELETE",
|
||||||
|
});
|
||||||
|
this.args.webhooks.removeObject(webhook);
|
||||||
|
} catch (err) {
|
||||||
|
popupAjaxError(err);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>{{i18n "chat.incoming_webhooks.name"}}</th>
|
||||||
|
<th>{{i18n "chat.incoming_webhooks.emoji"}}</th>
|
||||||
|
<th>{{i18n "chat.incoming_webhooks.username"}}</th>
|
||||||
|
<th>{{i18n "chat.incoming_webhooks.description"}}</th>
|
||||||
|
<th>{{i18n "chat.incoming_webhooks.channel"}}</th>
|
||||||
|
<th></th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{{#each this.sortedWebhooks as |webhook|}}
|
||||||
|
<tr class="incoming-chat-webhooks-row" data-webhook-id={{webhook.id}}>
|
||||||
|
<td>{{webhook.name}}</td>
|
||||||
|
<td>{{replaceEmoji webhook.emoji}}</td>
|
||||||
|
<td>{{webhook.username}}</td>
|
||||||
|
<td>{{webhook.description}}</td>
|
||||||
|
<td><ChannelTitle @channel={{webhook.chat_channel}} /></td>
|
||||||
|
<td
|
||||||
|
class="incoming-chat-webhooks-row__controls admin-table-row-controls"
|
||||||
|
>
|
||||||
|
<LinkTo
|
||||||
|
@route="adminPlugins.show.discourse-chat-incoming-webhooks.show"
|
||||||
|
@model={{webhook.id}}
|
||||||
|
class="btn btn-small admin-chat-incoming-webhooks-edit"
|
||||||
|
>{{i18n "chat.incoming_webhooks.edit"}}</LinkTo>
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@icon="trash-alt"
|
||||||
|
@title="chat.incoming_webhooks.delete"
|
||||||
|
@action={{fn this.destroyWebhook webhook}}
|
||||||
|
class="btn-danger btn-small admin-chat-incoming-webhooks-delete"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
}
|
@ -1,14 +1,20 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import DButton from "discourse/components/d-button";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import i18n from "discourse-common/helpers/i18n";
|
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
export default class ChatAdminExportMessages extends Component {
|
export default class ChatAdminPluginActions extends Component {
|
||||||
@service chatAdminApi;
|
|
||||||
@service dialog;
|
@service dialog;
|
||||||
|
@service chatAdminApi;
|
||||||
|
|
||||||
|
@action
|
||||||
|
confirmExportMessages() {
|
||||||
|
return this.dialog.confirm({
|
||||||
|
message: I18n.t("chat.admin.export_messages.confirm_export"),
|
||||||
|
didConfirm: () => this.exportMessages(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async exportMessages() {
|
async exportMessages() {
|
||||||
@ -23,15 +29,11 @@ export default class ChatAdminExportMessages extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="admin-section">
|
<@outletArgs.actions.Primary
|
||||||
<h3>{{i18n "chat.admin.export_messages.title"}}</h3>
|
@label="chat.admin.export_messages.create_export"
|
||||||
<p>{{i18n "chat.admin.export_messages.description"}}</p>
|
@title="chat.admin.export_messages.create_export"
|
||||||
<DButton
|
@action={{this.confirmExportMessages}}
|
||||||
@label="chat.admin.export_messages.create_export"
|
class="admin-chat-export"
|
||||||
@title="chat.admin.export_messages.create_export"
|
/>
|
||||||
@action={{this.exportMessages}}
|
|
||||||
class="btn-primary"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { not } from "truth-helpers";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import EmojiPicker from "discourse/components/emoji-picker";
|
||||||
|
import Form from "discourse/components/form";
|
||||||
|
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
import ChatChannelChooser from "discourse/plugins/chat/discourse/components/chat-channel-chooser";
|
||||||
|
|
||||||
|
export default class ChatIncomingWebhookEditForm extends Component {
|
||||||
|
@service toasts;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
@tracked emojiPickerIsActive = false;
|
||||||
|
|
||||||
|
get formData() {
|
||||||
|
return {
|
||||||
|
name: this.args.webhook?.name,
|
||||||
|
description: this.args.webhook?.description,
|
||||||
|
username: this.args.webhook?.username,
|
||||||
|
chat_channel_id: this.args.webhook?.chat_channel.id,
|
||||||
|
emoji: this.args.webhook?.emoji,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
emojiSelected(setData, emoji) {
|
||||||
|
setData("emoji", `:${emoji}:`);
|
||||||
|
this.emojiPickerIsActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
resetEmoji(setData) {
|
||||||
|
setData("emoji", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async save(data) {
|
||||||
|
try {
|
||||||
|
if (this.args.webhook?.id) {
|
||||||
|
await ajax(`/admin/plugins/chat/hooks/${this.args.webhook.id}`, {
|
||||||
|
data,
|
||||||
|
type: "PUT",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toasts.success({
|
||||||
|
duration: 3000,
|
||||||
|
data: {
|
||||||
|
message: I18n.t("chat.incoming_webhooks.saved"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const webhook = await ajax(`/admin/plugins/chat/hooks`, {
|
||||||
|
data,
|
||||||
|
type: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toasts.success({
|
||||||
|
duration: 3000,
|
||||||
|
data: {
|
||||||
|
message: I18n.t("chat.incoming_webhooks.created"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.router
|
||||||
|
.transitionTo(
|
||||||
|
"adminPlugins.show.discourse-chat-incoming-webhooks.show",
|
||||||
|
webhook
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.router.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
popupAjaxError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form @data={{this.formData}} @onSubmit={{this.save}} as |form|>
|
||||||
|
<form.Field
|
||||||
|
@name="name"
|
||||||
|
@title={{i18n "chat.incoming_webhooks.name"}}
|
||||||
|
@validation="required"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Input />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="description"
|
||||||
|
@title={{i18n "chat.incoming_webhooks.description"}}
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Textarea />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="username"
|
||||||
|
@title={{i18n "chat.incoming_webhooks.username"}}
|
||||||
|
@description={{i18n "chat.incoming_webhooks.username_instructions"}}
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Input />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="chat_channel_id"
|
||||||
|
@title={{i18n "chat.incoming_webhooks.post_to"}}
|
||||||
|
@validation="required"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Custom>
|
||||||
|
<ChatChannelChooser
|
||||||
|
@content={{@chatChannels}}
|
||||||
|
@value={{field.value}}
|
||||||
|
@onChange={{field.set}}
|
||||||
|
/>
|
||||||
|
</field.Custom>
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="emoji"
|
||||||
|
@title={{i18n "chat.incoming_webhooks.emoji"}}
|
||||||
|
@description={{i18n "chat.incoming_webhooks.emoji_instructions"}}
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Custom>
|
||||||
|
{{#if field.value}}
|
||||||
|
{{i18n "chat.incoming_webhooks.current_emoji"}}
|
||||||
|
|
||||||
|
<span class="incoming-chat-webhooks-current-emoji">
|
||||||
|
{{replaceEmoji field.value}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<EmojiPicker
|
||||||
|
@isActive={{this.emojiPickerIsActive}}
|
||||||
|
@isEditorFocused={{true}}
|
||||||
|
@emojiSelected={{fn this.emojiSelected form.set}}
|
||||||
|
@onEmojiPickerClose={{fn (mut this.emojiPickerIsActive) false}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#unless this.emojiPickerIsActive}}
|
||||||
|
<form.Row as |row|>
|
||||||
|
<row.Col @size={{6}}>
|
||||||
|
<DButton
|
||||||
|
@label="chat.incoming_webhooks.select_emoji"
|
||||||
|
@action={{fn (mut this.emojiPickerIsActive) true}}
|
||||||
|
class="btn-primary admin-chat-webhooks-select-emoji"
|
||||||
|
/>
|
||||||
|
</row.Col>
|
||||||
|
<row.Col @size={{6}}>
|
||||||
|
<DButton
|
||||||
|
@label="chat.incoming_webhooks.reset_emoji"
|
||||||
|
@action={{fn this.resetEmoji form.set}}
|
||||||
|
@disabled={{not field.value}}
|
||||||
|
class="admin-chat-webhooks-clear-emoji"
|
||||||
|
/>
|
||||||
|
</row.Col>
|
||||||
|
</form.Row>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
</field.Custom>
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
{{#if @webhook.url}}
|
||||||
|
<form.Container
|
||||||
|
@name="url"
|
||||||
|
@title={{i18n "chat.incoming_webhooks.url"}}
|
||||||
|
@subtitle={{i18n "chat.incoming_webhooks.url_instructions"}}
|
||||||
|
>
|
||||||
|
<code>{{@webhook.url}}</code>
|
||||||
|
</form.Container>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<form.Submit />
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import EmberObject from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||||
|
|
||||||
|
export default class DiscourseChatIncomingWebhooksIndex extends DiscourseRoute {
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
async model() {
|
||||||
|
if (!this.currentUser?.admin) {
|
||||||
|
return { model: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const model = await ajax("/admin/plugins/chat/hooks.json");
|
||||||
|
|
||||||
|
model.chat_channels = model.chat_channels.map((channel) =>
|
||||||
|
ChatChannel.create(channel)
|
||||||
|
);
|
||||||
|
|
||||||
|
model.incoming_chat_webhooks = model.incoming_chat_webhooks.map(
|
||||||
|
(webhook) => {
|
||||||
|
webhook.chat_channel = ChatChannel.create(webhook.chat_channel);
|
||||||
|
return EmberObject.create(webhook);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
} catch (err) {
|
||||||
|
popupAjaxError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
titleToken() {
|
||||||
|
return I18n.t("chat.incoming_webhooks.title");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import EmberObject from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||||
|
|
||||||
|
export default class DiscourseChatIncomingWebhooksNew extends DiscourseRoute {
|
||||||
|
@service adminPluginNavManager;
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
async model() {
|
||||||
|
if (!this.currentUser?.admin) {
|
||||||
|
return { model: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const model = await ajax("/admin/plugins/chat/hooks/new.json");
|
||||||
|
|
||||||
|
model.webhook = EmberObject.create(model.webhook);
|
||||||
|
model.webhook.chat_channel = ChatChannel.create(
|
||||||
|
model.webhook.chat_channel
|
||||||
|
);
|
||||||
|
model.chat_channels = model.chat_channels.map((channel) =>
|
||||||
|
ChatChannel.create(channel)
|
||||||
|
);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
} catch (err) {
|
||||||
|
popupAjaxError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import EmberObject from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||||
|
|
||||||
|
export default class DiscourseChatIncomingWebhooksShow extends DiscourseRoute {
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
async model(params) {
|
||||||
|
if (!this.currentUser?.admin) {
|
||||||
|
return { model: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const model = await ajax(`/admin/plugins/chat/hooks/${params.id}.json`);
|
||||||
|
|
||||||
|
model.webhook = EmberObject.create(model.webhook);
|
||||||
|
model.webhook.chat_channel = ChatChannel.create(
|
||||||
|
model.webhook.chat_channel
|
||||||
|
);
|
||||||
|
model.chat_channels = model.chat_channels.map((channel) =>
|
||||||
|
ChatChannel.create(channel)
|
||||||
|
);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
} catch (err) {
|
||||||
|
popupAjaxError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<DBreadcrumbsItem
|
||||||
|
@path="/admin/plugins/chat/hooks"
|
||||||
|
@label={{i18n "chat.incoming_webhooks.title"}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="discourse-chat-incoming-webhooks admin-detail">
|
||||||
|
<AdminPageSubheader
|
||||||
|
@titleLabel="chat.incoming_webhooks.title"
|
||||||
|
@descriptionLabel="chat.incoming_webhooks.instructions"
|
||||||
|
>
|
||||||
|
<:actions as |actions|>
|
||||||
|
<actions.Primary
|
||||||
|
@label="chat.incoming_webhooks.new"
|
||||||
|
@title="chat.incoming_webhooks.new"
|
||||||
|
@route="adminPlugins.show.discourse-chat-incoming-webhooks.new"
|
||||||
|
@routeModels="chat"
|
||||||
|
class="admin-incoming-webhooks-new"
|
||||||
|
/>
|
||||||
|
</:actions>
|
||||||
|
</AdminPageSubheader>
|
||||||
|
|
||||||
|
<div class="incoming-chat-webhooks">
|
||||||
|
{{#if this.model.incoming_chat_webhooks}}
|
||||||
|
<AdminChatIncomingWebhooksList
|
||||||
|
@webhooks={{this.model.incoming_chat_webhooks}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
{{i18n "chat.incoming_webhooks.none"}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,9 @@
|
|||||||
|
<div class="admin-detail discourse-chat-incoming-webhooks">
|
||||||
|
<BackButton
|
||||||
|
@label="chat.incoming_webhooks.back"
|
||||||
|
@route="adminPlugins.show.discourse-chat-incoming-webhooks.index"
|
||||||
|
class="incoming-chat-webhooks-back"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ChatIncomingWebhookEditForm @chatChannels={{model.chat_channels}} />
|
||||||
|
</div>
|
@ -0,0 +1,14 @@
|
|||||||
|
<div class="admin-detail discourse-chat-incoming-webhooks">
|
||||||
|
<BackButton
|
||||||
|
@label="chat.incoming_webhooks.back"
|
||||||
|
@route="adminPlugins.show.discourse-chat-incoming-webhooks.index"
|
||||||
|
class="incoming-chat-webhooks-back"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConditionalLoadingSpinner @condition={{not model.webhook}}>
|
||||||
|
<ChatIncomingWebhookEditForm
|
||||||
|
@webhook={{model.webhook}}
|
||||||
|
@chatChannels={{model.chat_channels}}
|
||||||
|
/>
|
||||||
|
</ConditionalLoadingSpinner>
|
||||||
|
</div>
|
@ -16,6 +16,25 @@ module Chat
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
webhook =
|
||||||
|
Chat::IncomingWebhook.includes(:chat_channel).find(params[:incoming_chat_webhook_id])
|
||||||
|
render_serialized(
|
||||||
|
{ chat_channels: Chat::Channel.public_channels, webhook: webhook },
|
||||||
|
Chat::AdminChatWebhookShowSerializer,
|
||||||
|
root: false,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
serialized_channels =
|
||||||
|
Chat::Channel.public_channels.map do |channel|
|
||||||
|
Chat::ChannelSerializer.new(channel, scope: Guardian.new(current_user))
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: serialized_channels, root: "chat_channels"
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
params.require(%i[name chat_channel_id])
|
params.require(%i[name chat_channel_id])
|
||||||
|
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chat
|
||||||
|
class AdminChatWebhookShowSerializer < ApplicationSerializer
|
||||||
|
has_many :chat_channels, serializer: Chat::ChannelSerializer, embed: :objects
|
||||||
|
has_one :webhook, serializer: Chat::IncomingWebhookSerializer, embed: :objects
|
||||||
|
|
||||||
|
def chat_channels
|
||||||
|
object[:chat_channels]
|
||||||
|
end
|
||||||
|
|
||||||
|
def webhook
|
||||||
|
object[:webhook]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,7 +1,14 @@
|
|||||||
export default {
|
export default {
|
||||||
resource: "admin.adminPlugins",
|
resource: "admin.adminPlugins.show",
|
||||||
path: "/plugins",
|
path: "/plugins",
|
||||||
map() {
|
map() {
|
||||||
this.route("chat");
|
this.route(
|
||||||
|
"discourse-chat-incoming-webhooks",
|
||||||
|
{ path: "hooks" },
|
||||||
|
function () {
|
||||||
|
this.route("new");
|
||||||
|
this.route("show", { path: "/:id" });
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,139 +0,0 @@
|
|||||||
import Controller from "@ember/controller";
|
|
||||||
import EmberObject, { action, computed } from "@ember/object";
|
|
||||||
import { and } from "@ember/object/computed";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
||||||
import I18n from "discourse-i18n";
|
|
||||||
|
|
||||||
export default class AdminPluginsChatController extends Controller {
|
|
||||||
@service dialog;
|
|
||||||
queryParams = [
|
|
||||||
{
|
|
||||||
selectedWebhookId: "id",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
loading = false;
|
|
||||||
creatingNew = false;
|
|
||||||
newWebhookName = "";
|
|
||||||
newWebhookChannelId = null;
|
|
||||||
emojiPickerIsActive = false;
|
|
||||||
|
|
||||||
@and("newWebhookName", "newWebhookChannelId") nameAndChannelValid;
|
|
||||||
|
|
||||||
@computed("model.incoming_chat_webhooks.@each.updated_at")
|
|
||||||
get sortedWebhooks() {
|
|
||||||
return (
|
|
||||||
this.model.incoming_chat_webhooks?.sortBy("updated_at").reverse() || []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed("selectedWebhookId")
|
|
||||||
get selectedWebhook() {
|
|
||||||
if (!this.selectedWebhookId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = parseInt(this.selectedWebhookId, 10);
|
|
||||||
return this.model.incoming_chat_webhooks.findBy("id", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed("selectedWebhook.name", "selectedWebhook.chat_channel.id")
|
|
||||||
get saveEditDisabled() {
|
|
||||||
return !this.selectedWebhook.name || !this.selectedWebhook.chat_channel.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
createNewWebhook() {
|
|
||||||
if (this.loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set("loading", true);
|
|
||||||
const data = {
|
|
||||||
name: this.newWebhookName,
|
|
||||||
chat_channel_id: this.newWebhookChannelId,
|
|
||||||
};
|
|
||||||
|
|
||||||
return ajax("/admin/plugins/chat/hooks", { data, type: "POST" })
|
|
||||||
.then((webhook) => {
|
|
||||||
const newWebhook = EmberObject.create(webhook);
|
|
||||||
this.set(
|
|
||||||
"model.incoming_chat_webhooks",
|
|
||||||
[newWebhook].concat(this.model.incoming_chat_webhooks)
|
|
||||||
);
|
|
||||||
this.resetNewWebhook();
|
|
||||||
this.setProperties({
|
|
||||||
loading: false,
|
|
||||||
selectedWebhookId: newWebhook.id,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
resetNewWebhook() {
|
|
||||||
this.setProperties({
|
|
||||||
creatingNew: false,
|
|
||||||
newWebhookName: "",
|
|
||||||
newWebhookChannelId: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
destroyWebhook(webhook) {
|
|
||||||
this.dialog.deleteConfirm({
|
|
||||||
message: I18n.t("chat.incoming_webhooks.confirm_destroy"),
|
|
||||||
didConfirm: () => {
|
|
||||||
this.set("loading", true);
|
|
||||||
return ajax(`/admin/plugins/chat/hooks/${webhook.id}`, {
|
|
||||||
type: "DELETE",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.model.incoming_chat_webhooks.removeObject(webhook);
|
|
||||||
this.set("loading", false);
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
emojiSelected(emoji) {
|
|
||||||
this.selectedWebhook.set("emoji", `:${emoji}:`);
|
|
||||||
return this.set("emojiPickerIsActive", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
saveEdit() {
|
|
||||||
this.set("loading", true);
|
|
||||||
const data = {
|
|
||||||
name: this.selectedWebhook.name,
|
|
||||||
chat_channel_id: this.selectedWebhook.chat_channel.id,
|
|
||||||
description: this.selectedWebhook.description,
|
|
||||||
emoji: this.selectedWebhook.emoji,
|
|
||||||
username: this.selectedWebhook.username,
|
|
||||||
};
|
|
||||||
return ajax(`/admin/plugins/chat/hooks/${this.selectedWebhook.id}`, {
|
|
||||||
data,
|
|
||||||
type: "PUT",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.selectedWebhook.set("updated_at", new Date());
|
|
||||||
this.setProperties({
|
|
||||||
loading: false,
|
|
||||||
selectedWebhookId: null,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
changeChatChannel(chatChannelId) {
|
|
||||||
this.selectedWebhook.set(
|
|
||||||
"chat_channel",
|
|
||||||
this.model.chat_channels.findBy("id", chatChannelId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,28 @@
|
|||||||
|
import { PLUGIN_NAV_MODE_TOP } from "discourse/lib/admin-plugin-config-nav";
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
import ChatAdminPluginActions from "discourse/plugins/chat/admin/components/chat-admin-plugin-actions";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "discourse-chat-admin-plugin-configuration-nav",
|
||||||
|
|
||||||
|
initialize(container) {
|
||||||
|
const currentUser = container.lookup("service:current-user");
|
||||||
|
if (!currentUser?.admin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
withPluginApi("1.1.0", (api) => {
|
||||||
|
api.addAdminPluginConfigurationNav("chat", PLUGIN_NAV_MODE_TOP, [
|
||||||
|
{
|
||||||
|
label: "chat.incoming_webhooks.title",
|
||||||
|
route: "adminPlugins.show.discourse-chat-incoming-webhooks",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.renderInOutlet(
|
||||||
|
"admin-plugin-config-page-actions",
|
||||||
|
ChatAdminPluginActions
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
@ -1,24 +0,0 @@
|
|||||||
import EmberObject from "@ember/object";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
|
||||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
|
||||||
|
|
||||||
export default class AdminPluginsChatRoute extends DiscourseRoute {
|
|
||||||
model() {
|
|
||||||
if (!this.currentUser?.admin) {
|
|
||||||
return { model: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
return ajax("/admin/plugins/chat/hooks.json").then((model) => {
|
|
||||||
model.incoming_chat_webhooks = model.incoming_chat_webhooks.map(
|
|
||||||
(webhook) => EmberObject.create(webhook)
|
|
||||||
);
|
|
||||||
|
|
||||||
model.chat_channels = model.chat_channels.map((channel) => {
|
|
||||||
return ChatChannel.create(channel);
|
|
||||||
});
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,196 +0,0 @@
|
|||||||
<Chat::Admin::ExportMessages />
|
|
||||||
|
|
||||||
{{#if this.selectedWebhook}}
|
|
||||||
<DButton
|
|
||||||
@icon="chevron-left"
|
|
||||||
@label="chat.incoming_webhooks.back"
|
|
||||||
@title="chat.incoming_webhooks.back"
|
|
||||||
@action={{fn (mut this.selectedWebhookId) null}}
|
|
||||||
class="incoming-chat-webhooks-back"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form class="form-vertical">
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label">
|
|
||||||
{{i18n "chat.incoming_webhooks.name"}}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
@type="text"
|
|
||||||
@value={{this.selectedWebhook.name}}
|
|
||||||
placeholder={{i18n "chat.incoming_webhooks.name"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label">
|
|
||||||
{{i18n "chat.incoming_webhooks.description"}}
|
|
||||||
</label>
|
|
||||||
<Textarea @value={{this.selectedWebhook.description}} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label">
|
|
||||||
{{i18n "chat.incoming_webhooks.username"}}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
@type="text"
|
|
||||||
@value={{this.selectedWebhook.username}}
|
|
||||||
placeholder={{i18n "chat.incoming_webhooks.system"}}
|
|
||||||
/>
|
|
||||||
<div class="control-instructions">
|
|
||||||
{{i18n "chat.incoming_webhooks.username_instructions"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label">
|
|
||||||
{{i18n "chat.incoming_webhooks.post_to"}}
|
|
||||||
</label>
|
|
||||||
<ChatChannelChooser
|
|
||||||
@content={{this.model.chat_channels}}
|
|
||||||
@value={{this.selectedWebhook.chat_channel.id}}
|
|
||||||
@onChange={{action "changeChatChannel"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label">
|
|
||||||
{{#if this.selectedWebhook.emoji}}
|
|
||||||
{{i18n "chat.incoming_webhooks.current_emoji"}}
|
|
||||||
|
|
||||||
<span class="incoming-chat-webhooks-current-emoji">
|
|
||||||
{{replace-emoji this.selectedWebhook.emoji}}
|
|
||||||
</span>
|
|
||||||
{{else}}
|
|
||||||
{{i18n "chat.incoming_webhooks.no_emoji"}}
|
|
||||||
{{/if}}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<EmojiPicker
|
|
||||||
@isActive={{this.emojiPickerIsActive}}
|
|
||||||
@isEditorFocused={{true}}
|
|
||||||
@emojiSelected={{action "emojiSelected"}}
|
|
||||||
@onEmojiPickerClose={{fn (mut this.emojiPickerIsActive) false}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{#unless this.emojiPickerIsActive}}
|
|
||||||
<DButton
|
|
||||||
@label="chat.incoming_webhooks.select_emoji"
|
|
||||||
@action={{fn (mut this.emojiPickerIsActive) true}}
|
|
||||||
class="btn-primary"
|
|
||||||
/>
|
|
||||||
<DButton
|
|
||||||
@label="chat.incoming_webhooks.reset_emoji"
|
|
||||||
@action={{fn (mut this.selectedWebhook.emoji) null}}
|
|
||||||
@disabled={{not this.selectedWebhook.emoji}}
|
|
||||||
/>
|
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
<div class="control-instructions">
|
|
||||||
{{i18n "chat.incoming_webhooks.emoji_instructions"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label">{{i18n "chat.incoming_webhooks.url"}}</label>
|
|
||||||
<label>{{this.selectedWebhook.url}}</label>
|
|
||||||
<div class="control-instructions">
|
|
||||||
{{i18n "chat.incoming_webhooks.url_instructions"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DButton
|
|
||||||
@label="chat.incoming_webhooks.save"
|
|
||||||
@title="chat.incoming_webhooks.save"
|
|
||||||
@action={{this.saveEdit}}
|
|
||||||
@disabled={{this.saveEditDisabled}}
|
|
||||||
class="btn-primary"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
{{else}}
|
|
||||||
{{! Index view }}
|
|
||||||
<h3>{{i18n "chat.incoming_webhooks.title"}}</h3>
|
|
||||||
|
|
||||||
{{#if this.creatingNew}}
|
|
||||||
<div class="new-incoming-webhook-container">
|
|
||||||
<Input
|
|
||||||
@type="text"
|
|
||||||
@value={{this.newWebhookName}}
|
|
||||||
placeholder={{i18n "chat.incoming_webhooks.name_placeholder"}}
|
|
||||||
/>
|
|
||||||
<ChatChannelChooser
|
|
||||||
@content={{this.model.chat_channels}}
|
|
||||||
@value={{this.newWebhookChannelId}}
|
|
||||||
@onChange={{fn (mut this.newWebhookChannelId)}}
|
|
||||||
/>
|
|
||||||
<DButton
|
|
||||||
@label="chat.create"
|
|
||||||
@title="chat.create"
|
|
||||||
@disabled={{not this.nameAndChannelValid}}
|
|
||||||
@action={{this.createNewWebhook}}
|
|
||||||
class="btn-primary create-new-incoming-webhook-btn"
|
|
||||||
/>
|
|
||||||
<DButton
|
|
||||||
@label="chat.cancel"
|
|
||||||
@title="chat.cancel"
|
|
||||||
@action={{this.resetNewWebhook}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<DButton
|
|
||||||
@label="chat.incoming_webhooks.new"
|
|
||||||
@title="chat.incoming_webhooks.new"
|
|
||||||
@action={{fn (mut this.creatingNew) true}}
|
|
||||||
class="btn-primary"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<p>{{html-safe (i18n "chat.incoming_webhooks.instructions")}}</p>
|
|
||||||
|
|
||||||
<div class="incoming-chat-webhooks">
|
|
||||||
{{#if this.model.incoming_chat_webhooks}}
|
|
||||||
{{#each this.sortedWebhooks as |webhook|}}
|
|
||||||
<div class="incoming-chat-webhooks--row">
|
|
||||||
<div class="incoming-chat-webhooks--row--details">
|
|
||||||
<div class="incoming-chat-webhooks--row--details--name">
|
|
||||||
{{webhook.name}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{{#if webhook.emoji}}
|
|
||||||
{{replace-emoji webhook.emoji}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if webhook.username}}
|
|
||||||
{{webhook.username}}
|
|
||||||
{{else}}
|
|
||||||
{{i18n "chat.incoming_webhooks.system"}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div><ChannelTitle @channel={{webhook.chat_channel}} /></div>
|
|
||||||
<div>{{webhook.description}}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="incoming-chat-webhooks--row--controls">
|
|
||||||
<div>
|
|
||||||
<DButton
|
|
||||||
@icon="pencil-alt"
|
|
||||||
@label="chat.incoming_webhooks.edit"
|
|
||||||
@action={{fn (mut this.selectedWebhookId) webhook.id}}
|
|
||||||
/>
|
|
||||||
<DButton
|
|
||||||
@icon="trash-alt"
|
|
||||||
@title="chat.incoming_webhooks.delete"
|
|
||||||
@action={{fn this.destroyWebhook webhook}}
|
|
||||||
class="btn-danger"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
{{else}}
|
|
||||||
{{i18n "chat.incoming_webhooks.none"}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
@ -506,6 +506,7 @@ en:
|
|||||||
title: "Export chat messages"
|
title: "Export chat messages"
|
||||||
description: "This exports all messages from all channels."
|
description: "This exports all messages from all channels."
|
||||||
create_export: "Create export"
|
create_export: "Create export"
|
||||||
|
confirm_export: "This exports all messages from all channels and sends the result to you in a personal message. Are you sure you want to export?"
|
||||||
export_has_started: "The export has started. You'll receive a PM when it's ready."
|
export_has_started: "The export has started. You'll receive a PM when it's ready."
|
||||||
|
|
||||||
my_threads:
|
my_threads:
|
||||||
@ -528,6 +529,7 @@ en:
|
|||||||
incoming_webhooks:
|
incoming_webhooks:
|
||||||
back: "Back"
|
back: "Back"
|
||||||
channel_placeholder: "Select a channel"
|
channel_placeholder: "Select a channel"
|
||||||
|
channel: "Channel"
|
||||||
confirm_destroy: "Are you sure you want to delete this incoming webhook? This cannot be un-done."
|
confirm_destroy: "Are you sure you want to delete this incoming webhook? This cannot be un-done."
|
||||||
current_emoji: "Current Emoji"
|
current_emoji: "Current Emoji"
|
||||||
description: "Description"
|
description: "Description"
|
||||||
@ -551,6 +553,8 @@ en:
|
|||||||
username: "Username"
|
username: "Username"
|
||||||
username_instructions: "Username of bot that posts to channel. Defaults to 'system' when left blank."
|
username_instructions: "Username of bot that posts to channel. Defaults to 'system' when left blank."
|
||||||
instructions: "Incoming webhooks can be used by external systems to post messages into a designated chat channel as a bot user via the <code>/hooks/:key</code> endpoint. The payload consists of a single <code>text</code> parameter, which is limited to 2000 characters.<br><br>We also support limited Slack-formatted <code>text</code> parameters, extracting links and mentions based on the format at <a href=\"https://api.slack.com/reference/surfaces/formatting\">https://api.slack.com/reference/surfaces/formatting</a>, but the <code>/hooks/:key/slack</code> endpoint must be used for this."
|
instructions: "Incoming webhooks can be used by external systems to post messages into a designated chat channel as a bot user via the <code>/hooks/:key</code> endpoint. The payload consists of a single <code>text</code> parameter, which is limited to 2000 characters.<br><br>We also support limited Slack-formatted <code>text</code> parameters, extracting links and mentions based on the format at <a href=\"https://api.slack.com/reference/surfaces/formatting\">https://api.slack.com/reference/surfaces/formatting</a>, but the <code>/hooks/:key/slack</code> endpoint must be used for this."
|
||||||
|
saved: "Webhook changes saved"
|
||||||
|
created: "Webhook created"
|
||||||
|
|
||||||
selection:
|
selection:
|
||||||
cancel: "Cancel"
|
cancel: "Cancel"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# name: chat
|
# name: chat
|
||||||
# about: Adds chat functionality.
|
# about: Adds chat functionality to your site so it can natively support both long-form and short-form communication needs of your online community.
|
||||||
# meta_topic_id: 230881
|
# meta_topic_id: 230881
|
||||||
# version: 0.4
|
# version: 0.4
|
||||||
# authors: Kane York, Mark VanLandingham, Martin Brennan, Joffrey Jaffeux
|
# authors: Kane York, Mark VanLandingham, Martin Brennan, Joffrey Jaffeux
|
||||||
@ -27,7 +27,7 @@ register_svg_icon "file-image"
|
|||||||
register_svg_icon "stop-circle"
|
register_svg_icon "stop-circle"
|
||||||
|
|
||||||
# route: /admin/plugins/chat
|
# route: /admin/plugins/chat
|
||||||
add_admin_route "chat.admin.title", "chat"
|
add_admin_route "chat.admin.title", "chat", use_new_show_route: true
|
||||||
|
|
||||||
GlobalSetting.add_default(:allow_unsecure_chat_uploads, false)
|
GlobalSetting.add_default(:allow_unsecure_chat_uploads, false)
|
||||||
|
|
||||||
@ -443,6 +443,11 @@ after_initialize do
|
|||||||
put "/admin/plugins/chat/hooks/:incoming_chat_webhook_id" =>
|
put "/admin/plugins/chat/hooks/:incoming_chat_webhook_id" =>
|
||||||
"chat/admin/incoming_webhooks#update",
|
"chat/admin/incoming_webhooks#update",
|
||||||
:constraints => StaffConstraint.new
|
:constraints => StaffConstraint.new
|
||||||
|
get "/admin/plugins/chat/hooks/new" => "chat/admin/incoming_webhooks#new",
|
||||||
|
:constraints => StaffConstraint.new
|
||||||
|
get "/admin/plugins/chat/hooks/:incoming_chat_webhook_id" =>
|
||||||
|
"chat/admin/incoming_webhooks#show",
|
||||||
|
:constraints => StaffConstraint.new
|
||||||
delete "/admin/plugins/chat/hooks/:incoming_chat_webhook_id" =>
|
delete "/admin/plugins/chat/hooks/:incoming_chat_webhook_id" =>
|
||||||
"chat/admin/incoming_webhooks#destroy",
|
"chat/admin/incoming_webhooks#destroy",
|
||||||
:constraints => StaffConstraint.new
|
:constraints => StaffConstraint.new
|
||||||
|
@ -178,8 +178,8 @@ Fabricator(:chat_webhook_event, class_name: "Chat::WebhookEvent") do
|
|||||||
end
|
end
|
||||||
|
|
||||||
Fabricator(:incoming_chat_webhook, class_name: "Chat::IncomingWebhook") do
|
Fabricator(:incoming_chat_webhook, class_name: "Chat::IncomingWebhook") do
|
||||||
name { sequence(:name) { |i| "#{i + 1}" } }
|
name { sequence(:name) { |i| "Test webhook #{i + 1}" } }
|
||||||
key { sequence(:key) { |i| "#{i + 1}" } }
|
emoji { %w[:joy: :rocket: :handshake:].sample }
|
||||||
chat_channel { Fabricate(:chat_channel, chatable: Fabricate(:category)) }
|
chat_channel { Fabricate(:chat_channel, chatable: Fabricate(:category)) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ RSpec.describe ApplicationController do
|
|||||||
"admin_route" => {
|
"admin_route" => {
|
||||||
"label" => "chat.admin.title",
|
"label" => "chat.admin.title",
|
||||||
"location" => "chat",
|
"location" => "chat",
|
||||||
"use_new_show_route" => false,
|
"use_new_show_route" => true,
|
||||||
},
|
},
|
||||||
"enabled" => true,
|
"enabled" => true,
|
||||||
},
|
},
|
||||||
|
@ -1,26 +1,32 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
RSpec.describe "Chat CSV exports", type: :system do
|
RSpec.describe "Admin Chat CSV exports", type: :system do
|
||||||
fab!(:admin)
|
let(:dialog) { PageObjects::Components::Dialog.new }
|
||||||
let(:csv_export_pm_page) { PageObjects::Pages::CSVExportPM.new }
|
let(:csv_export_pm_page) { PageObjects::Pages::CSVExportPM.new }
|
||||||
|
fab!(:current_user) { Fabricate(:admin) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Jobs.run_immediately!
|
Jobs.run_immediately!
|
||||||
sign_in(admin)
|
sign_in(current_user)
|
||||||
chat_system_bootstrap
|
chat_system_bootstrap
|
||||||
end
|
end
|
||||||
|
|
||||||
xit "exports chat messages" do
|
it "exports chat messages" do
|
||||||
|
Jobs.run_immediately!
|
||||||
message_1 = Fabricate(:chat_message, created_at: 12.months.ago)
|
message_1 = Fabricate(:chat_message, created_at: 12.months.ago)
|
||||||
message_2 = Fabricate(:chat_message, created_at: 6.months.ago)
|
message_2 = Fabricate(:chat_message, created_at: 6.months.ago)
|
||||||
message_3 = Fabricate(:chat_message, created_at: 1.months.ago)
|
message_3 = Fabricate(:chat_message, created_at: 1.months.ago)
|
||||||
message_4 = Fabricate(:chat_message, created_at: Time.now)
|
message_4 = Fabricate(:chat_message, created_at: Time.now)
|
||||||
|
|
||||||
visit "/admin/plugins/chat"
|
visit "/admin/plugins/chat"
|
||||||
click_button "Create export"
|
click_button I18n.t("js.chat.admin.export_messages.create_export")
|
||||||
|
dialog.click_yes
|
||||||
|
|
||||||
visit "/u/#{admin.username}/messages"
|
visit "/u/#{current_user.username}/messages"
|
||||||
click_link "[Chat Message] Data export complete"
|
click_link I18n.t(
|
||||||
|
"system_messages.csv_export_succeeded.subject_template",
|
||||||
|
export_title: "Chat Message",
|
||||||
|
)
|
||||||
expect(csv_export_pm_page).to have_download_link
|
expect(csv_export_pm_page).to have_download_link
|
||||||
exported_data = csv_export_pm_page.download_and_extract
|
exported_data = csv_export_pm_page.download_and_extract
|
||||||
|
|
||||||
|
69
plugins/chat/spec/system/admin/incoming_webhooks_spec.rb
Normal file
69
plugins/chat/spec/system/admin/incoming_webhooks_spec.rb
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe "Admin Chat Incoming Webhooks", type: :system do
|
||||||
|
fab!(:current_user) { Fabricate(:admin) }
|
||||||
|
fab!(:chat_channel_1) { Fabricate(:chat_channel) }
|
||||||
|
|
||||||
|
let(:dialog) { PageObjects::Components::Dialog.new }
|
||||||
|
let(:admin_incoming_webhooks_page) { PageObjects::Pages::AdminIncomingWebhooks.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
chat_system_bootstrap(current_user)
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can create incoming webhooks" do
|
||||||
|
admin_incoming_webhooks_page.visit
|
||||||
|
admin_incoming_webhooks_page.click_new
|
||||||
|
admin_incoming_webhooks_page.form.field("name").fill_in("Test webhook")
|
||||||
|
admin_incoming_webhooks_page.form.field("description").fill_in("Some test content")
|
||||||
|
admin_incoming_webhooks_page.form.field("username").fill_in("system")
|
||||||
|
admin_incoming_webhooks_page.channel_chooser.expand
|
||||||
|
admin_incoming_webhooks_page.channel_chooser.select_row_by_value(chat_channel_1.id)
|
||||||
|
admin_incoming_webhooks_page.channel_chooser.collapse
|
||||||
|
# TODO (martin) Add an emoji selection once Joffrey's emoji selector
|
||||||
|
# unification has landed in core.
|
||||||
|
|
||||||
|
admin_incoming_webhooks_page.form.submit
|
||||||
|
|
||||||
|
expect(page).to have_content(I18n.t("js.chat.incoming_webhooks.created"))
|
||||||
|
expect(page).to have_content(Chat::IncomingWebhook.find_by(name: "Test webhook").url)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "existing webhooks" do
|
||||||
|
fab!(:webhook_1) { Fabricate(:incoming_chat_webhook) }
|
||||||
|
fab!(:webhook_2) { Fabricate(:incoming_chat_webhook) }
|
||||||
|
|
||||||
|
it "can list existing incoming webhooks" do
|
||||||
|
admin_incoming_webhooks_page.visit
|
||||||
|
expect(page).to have_content(webhook_1.name)
|
||||||
|
expect(page).to have_content(webhook_1.chat_channel.title)
|
||||||
|
expect(page).to have_content(webhook_2.name)
|
||||||
|
expect(page).to have_content(webhook_2.chat_channel.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can edit an existing incoming webhook" do
|
||||||
|
admin_incoming_webhooks_page.visit
|
||||||
|
admin_incoming_webhooks_page
|
||||||
|
.list_row(webhook_1.id)
|
||||||
|
.find(".admin-chat-incoming-webhooks-edit")
|
||||||
|
.click
|
||||||
|
expect(admin_incoming_webhooks_page.form.field("name").value).to eq(webhook_1.name)
|
||||||
|
admin_incoming_webhooks_page.form.field("name").fill_in("Wow so cool")
|
||||||
|
admin_incoming_webhooks_page.form.submit
|
||||||
|
expect(page).to have_content(I18n.t("js.chat.incoming_webhooks.saved"))
|
||||||
|
admin_incoming_webhooks_page.visit
|
||||||
|
expect(page).to have_content("Wow so cool")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can delete an existing incoming webhook" do
|
||||||
|
admin_incoming_webhooks_page.visit
|
||||||
|
admin_incoming_webhooks_page
|
||||||
|
.list_row(webhook_1.id)
|
||||||
|
.find(".admin-chat-incoming-webhooks-delete")
|
||||||
|
.click
|
||||||
|
dialog.click_danger
|
||||||
|
expect(page).not_to have_content(webhook_1.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module PageObjects
|
||||||
|
module Pages
|
||||||
|
class AdminIncomingWebhooks < PageObjects::Pages::Base
|
||||||
|
def visit
|
||||||
|
page.visit("/admin/plugins/chat/hooks")
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_new
|
||||||
|
find(".admin-incoming-webhooks-new").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def channel_chooser
|
||||||
|
PageObjects::Components::SelectKit.new(".chat-channel-chooser")
|
||||||
|
end
|
||||||
|
|
||||||
|
def form
|
||||||
|
PageObjects::Components::FormKit.new(".discourse-chat-incoming-webhooks .form-kit")
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_row(webhook_id)
|
||||||
|
find(".incoming-chat-webhooks-row[data-webhook-id='#{webhook_id}']")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user