FIX: Form validation bugs, new modal api, glimmer (#182)

This commit is contained in:
Jarek Radosz 2023-12-11 20:00:34 +01:00 committed by GitHub
parent bc81d30dc8
commit 30ac835e1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 583 additions and 692 deletions

View File

@ -5,14 +5,14 @@
@icon="pencil-alt"
@title="chat_integration.edit_channel"
@label="chat_integration.edit_channel"
@action={{action @editChannel @channel}}
@action={{fn @editChannel @channel}}
/>
<DButton
@icon="rocket"
@title="chat_integration.test_channel"
@label="chat_integration.test_channel"
@action={{action @test @channel}}
@action={{fn @test @channel}}
@class="btn-chat-test"
/>
@ -20,7 +20,7 @@
@icon="trash-alt"
@title="chat_integration.delete_channel"
@label="chat_integration.delete_channel"
@action={{action this.deleteChannel @channel}}
@action={{fn this.deleteChannel @channel}}
@class="cancel delete-channel"
/>
</div>
@ -30,7 +30,7 @@
<DButton
@class="delete btn-danger"
@icon="exclamation-triangle"
@action={{action @showError @channel}}
@action={{fn @showError @channel}}
/>
{{/if}}
@ -56,7 +56,7 @@
{{#each @channel.rules as |rule|}}
<RuleRow
@rule={{rule}}
@edit={{action @editRuleWithChannel rule @channel}}
@edit={{fn @editRuleWithChannel rule @channel}}
@refresh={{@refresh}}
/>
{{/each}}
@ -71,7 +71,7 @@
@icon="plus"
@title="chat_integration.create_rule"
@label="chat_integration.create_rule"
@action={{action @createRule @channel}}
@action={{fn @createRule @channel}}
/>
</div>
</div>

View File

@ -4,7 +4,7 @@
{{i18n
(concat
"chat_integration.provider."
@model.channel.provider
@channel.provider
".param."
@param.key
".title"
@ -13,14 +13,14 @@
</label>
</td>
<td>
<Input
name={{concat "param-" @param.key}}
@value={{this.inputValue}}
{{on "change" this.updateValue}}
<input
{{on "input" this.updateValue}}
value={{get @channel.data @param.key}}
type="text"
name="param-{{@param.key}}"
/>
<InputTip @validation={{this.validate}} />
<InputTip @validation={{this.validation}} />
</td>
</tr>
@ -31,7 +31,7 @@
{{i18n
(concat
"chat_integration.provider."
@model.channel.provider
@channel.provider
".param."
@param.key
".help"

View File

@ -1,51 +1,34 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import EmberObject, { action } from "@ember/object";
import { action } from "@ember/object";
import I18n from "I18n";
export default class ChannelParamRow extends Component {
@tracked inputValue = this.args.model.channel.data[this.args.param.key] || "";
get validation() {
const value = this.args.channel.get(`data.${this.args.param.key}`);
get validate() {
const parameter = this.args.param;
const regString = parameter.regex;
const regex = new RegExp(regString);
if (this.inputValue === "") {
// Fail silently if field blank
this.args.isValidParams(false);
return EmberObject.create({
failed: true,
});
} else if (!regString) {
// Pass silently if no regex available for provider
this.args.isValidParams(true);
return EmberObject.create({
ok: true,
});
} else if (regex.test(this.inputValue)) {
// Test against regex
this.args.isValidParams(true);
return EmberObject.create({
if (!value?.trim()) {
return { failed: true };
} else if (!this.args.param.regex) {
return { ok: true };
} else if (new RegExp(this.args.param.regex).test(value)) {
return {
ok: true,
reason: I18n.t(
"chat_integration.edit_channel_modal.channel_validation.ok"
),
});
};
} else {
// Failed regex
this.args.isValidParams(false);
return EmberObject.create({
return {
failed: true,
reason: I18n.t(
"chat_integration.edit_channel_modal.channel_validation.fail"
),
});
};
}
}
@action
updateValue(event) {
this.args.model.channel.data[this.args.param.key] = event.target.value;
this.args.channel.set(`data.${this.args.param.key}`, event.target.value);
}
}

View File

@ -0,0 +1,4 @@
<DModal @closeModal={{@closeModal}} id="chat_integration_error_modal">
<h4>{{i18n @model.error_key}}</h4>
<pre>{{@model.error_info}}</pre>
</DModal>

View File

@ -0,0 +1,54 @@
<DModal
{{on "submit" this.save}}
@title={{i18n "chat_integration.edit_channel_modal.title"}}
@closeModal={{@closeModal}}
@tagName="form"
id="chat-integration-edit-channel-modal"
>
<:body>
<table>
<tbody>
<tr class="input">
<td class="label">
<label for="provider">
{{i18n "chat_integration.edit_channel_modal.provider"}}
</label>
</td>
<td>
{{i18n
(concat
"chat_integration.provider." @model.channel.provider ".title"
)
}}
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td></td>
</tr>
{{#each @model.provider.channel_parameters as |param|}}
<ChannelParamRow @param={{param}} @channel={{@model.channel}} />
{{/each}}
</tbody>
</table>
</:body>
<:footer>
<DButton
@action={{this.save}}
@label="chat_integration.edit_channel_modal.save"
@disabled={{not this.validParams}}
type="submit"
id="save-channel"
class="btn-primary btn-large"
/>
<DButton
@action={{@closeModal}}
@label="chat_integration.edit_channel_modal.cancel"
class="btn-large"
/>
</:footer>
</DModal>

View File

@ -0,0 +1,31 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default class EditChannel extends Component {
get validParams() {
return this.args.model.provider.channel_parameters.every((param) => {
const value = this.args.model.channel.get(`data.${param.key}`);
if (!value?.trim()) {
return false;
}
if (!param.regex) {
return true;
}
return new RegExp(param.regex).test(value);
});
}
@action
async save() {
try {
await this.args.model.channel.save();
this.args.closeModal();
} catch (e) {
popupAjaxError(e);
}
}
}

View File

@ -0,0 +1,201 @@
<DModal
{{on "submit" this.save}}
@title={{i18n "chat_integration.edit_rule_modal.title"}}
@closeModal={{@closeModal}}
@tagName="form"
id="chat-integration-edit-rule_modal"
>
<:body>
<table>
<tbody>
<tr class="input">
<td class="label">
<label for="provider">
{{i18n "chat_integration.edit_rule_modal.provider"}}
</label>
</td>
<td>
{{i18n
(concat
"chat_integration.provider." @model.channel.provider ".title"
)
}}
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td></td>
</tr>
<tr class="input">
<td class="label">
<label for="channel">
{{i18n "chat_integration.edit_rule_modal.channel"}}
</label>
</td>
<td>
<ChannelData
@provider={{@model.provider}}
@channel={{@model.channel}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td></td>
</tr>
<tr class="input">
<td class="label">
<label for="filter">
{{i18n "chat_integration.edit_rule_modal.type"}}
</label>
</td>
<td>
<ComboBox
@name="type"
@content={{@model.rule.available_types}}
@value={{@model.rule.type}}
@onChange={{fn (mut @model.rule.type)}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n "chat_integration.edit_rule_modal.instructions.type"}}
</label>
</td>
</tr>
<tr class="input">
<td class="label">
<label for="filter">
{{i18n "chat_integration.edit_rule_modal.filter"}}
</label>
</td>
<td>
<ComboBox
@name="filter"
@content={{@model.rule.available_filters}}
@value={{@model.rule.filter}}
@onChange={{fn (mut @model.rule.filter)}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n "chat_integration.edit_rule_modal.instructions.filter"}}
</label>
</td>
</tr>
{{#if (eq @model.rule.type "normal")}}
<tr class="input">
<td class="label">
<label for="category">
{{i18n "chat_integration.edit_rule_modal.category"}}
</label>
</td>
<td>
<CategoryChooser
@name="category"
@options={{hash none="chat_integration.all_categories"}}
@value={{@model.rule.category_id}}
@onChange={{fn (mut @model.rule.category_id)}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n
"chat_integration.edit_rule_modal.instructions.category"
}}
</label>
</td>
</tr>
{{else}}
<tr class="input">
<td class="label">
<label for="group">
{{i18n "chat_integration.edit_rule_modal.group"}}
</label>
</td>
<td>
<ComboBox
@content={{@model.groups}}
@valueProperty="id"
@value={{@model.rule.group_id}}
@onChange={{fn (mut @model.rule.group_id)}}
@options={{hash none="chat_integration.choose_group"}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n "chat_integration.edit_rule_modal.instructions.group"}}
</label>
</td>
</tr>
{{/if}}
{{#if this.siteSettings.tagging_enabled}}
<tr class="input">
<td class="label">
<label for="tags">
{{i18n "chat_integration.edit_rule_modal.tags"}}
</label>
</td>
<td>
<TagChooser
@placeholderKey="chat_integration.all_tags"
@name="tags"
@tags={{@model.rule.tags}}
@everyTag="true"
@onChange={{fn (mut @model.rule.tags)}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n "chat_integration.edit_rule_modal.instructions.tags"}}
</label>
</td>
</tr>
{{/if}}
</tbody>
</table>
</:body>
<:footer>
<DButton
@action={{fn this.save @model.rule}}
@label="chat_integration.edit_rule_modal.save"
type="submit"
id="save-rule"
class="btn-primary btn-large"
/>
<DButton
@label="chat_integration.edit_rule_modal.cancel"
@action={{@closeModal}}
class="btn-large"
/>
</:footer>
</DModal>

View File

@ -0,0 +1,18 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default class EditRule extends Component {
@service siteSettings;
@action
async save(rule) {
try {
await rule.save();
this.args.closeModal();
} catch (e) {
popupAjaxError(e);
}
}
}

View File

@ -0,0 +1,44 @@
<DModal
{{on "submit" this.send}}
@title={{i18n "chat_integration.test_modal.title"}}
@closeModal={{@closeModal}}
@flash={{this.flash}}
@flashType="success"
@tagName="form"
id="chat_integration_test_modal"
>
<:body>
<table>
<tbody>
<tr class="input">
<td class="label">
<label for="channel">
{{i18n "chat_integration.test_modal.topic"}}
</label>
</td>
<td>
<ChooseTopic @selectedTopicId={{this.topicId}} />
</td>
</tr>
</tbody>
</table>
</:body>
<:footer>
<ConditionalLoadingSpinner @condition={{this.loading}}>
<DButton
@action={{this.send}}
@label="chat_integration.test_modal.send"
@disabled={{not this.topicId}}
type="submit"
id="send-test"
class="btn-primary btn-large"
/>
<DButton
@action={{@closeModal}}
@label="chat_integration.test_modal.close"
class="btn-large"
/>
</ConditionalLoadingSpinner>
</:footer>
</DModal>

View File

@ -0,0 +1,32 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import I18n from "I18n";
export default class TestIntegration extends Component {
@tracked loading = false;
@tracked flash;
@tracked topicId;
@action
async send() {
this.loading = true;
try {
await ajax("/admin/plugins/chat-integration/test", {
data: {
channel_id: this.args.model.channel.id,
topic_id: this.topicId,
},
type: "POST",
});
this.loading = false;
this.flash = I18n.t("chat_integration.test_modal.success");
} catch (e) {
popupAjaxError(e);
}
}
}

View File

@ -40,7 +40,7 @@
@class="delete"
@icon="far-trash-alt"
@title="chat_integration.rule_table.delete_rule"
@action={{action this.delete}}
@action={{this.delete}}
@actionParam={{@rule}}
/>
</td>

View File

@ -2,6 +2,7 @@ import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default class RuleRow extends Component {
@service siteSettings;

View File

@ -1,23 +1,19 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller";
import { action } from "@ember/object";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import ChannelErrorModal from "../components/modal/channel-error";
import EditChannelModal from "../components/modal/edit-channel";
import EditRuleModal from "../components/modal/edit-rule";
import TestModal from "../components/modal/test-integration";
const MODALS = {
editChannel: "admin-plugins-chat-integration-edit-channel",
testChannel: "admin-plugins-chat-integration-test",
editRule: "admin-plugins-chat-integration-edit-rule",
channelError: "admin-plugins-chat-integration-channel-error",
};
export default class AdminPluginsChatIntegrationEditRule extends Controller {
@tracked modalShowing = false;
export default class AdminPluginsChatIntegrationProvider extends Controller {
@service modal;
@service store;
get anyErrors() {
const channels = this.model.channels;
let anyErrors = false;
channels.forEach((channel) => {
this.model.channels.forEach((channel) => {
if (channel.error_key) {
anyErrors = true;
}
@ -26,77 +22,67 @@ export default class AdminPluginsChatIntegrationEditRule extends Controller {
return anyErrors;
}
triggerModal(model, modal) {
this.modalShowing = true;
showModal(modal, {
model,
admin: true,
async triggerModal(modal, model) {
await this.modal.show(modal, {
model: {
...model,
admin: true,
},
});
this.refresh();
}
@action
createChannel() {
return this.triggerModal(
{
channel: this.store.createRecord("channel", {
provider: this.model.provider.id,
data: {},
}),
provider: this.model.provider,
},
MODALS.editChannel
);
return this.triggerModal(EditChannelModal, {
channel: this.store.createRecord("channel", {
provider: this.model.provider.id,
data: {},
}),
provider: this.model.provider,
});
}
@action
editChannel(channel) {
return this.triggerModal(
{
channel,
provider: this.model.provider,
},
MODALS.editChannel
);
return this.triggerModal(EditChannelModal, {
channel,
provider: this.model.provider,
});
}
@action
testChannel(channel) {
return this.triggerModal({ channel }, MODALS.testChannel);
return this.triggerModal(TestModal, { channel });
}
@action
createRule(channel) {
return this.triggerModal(
{
rule: this.store.createRecord("rule", {
channel_id: channel.id,
channel,
}),
return this.triggerModal(EditRuleModal, {
rule: this.store.createRecord("rule", {
channel_id: channel.id,
channel,
provider: this.model.provider,
groups: this.model.groups,
},
MODALS.editRule
);
}),
channel,
provider: this.model.provider,
groups: this.model.groups,
});
}
@action
editRuleWithChannel(rule, channel) {
return this.triggerModal(
{
rule,
channel,
provider: this.model.provider,
groups: this.model.groups,
},
MODALS.editRule
);
return this.triggerModal(EditRuleModal, {
rule,
channel,
provider: this.model.provider,
groups: this.model.groups,
});
}
@action
showError(channel) {
return this.triggerModal({ channel }, MODALS.channelError);
return this.triggerModal(ChannelErrorModal, { channel });
}
@action

View File

@ -1,42 +0,0 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default class AdminPluginsChatIntegrationEditChannel extends Controller.extend(
ModalFunctionality
) {
@tracked validParams = false;
@action
isValidParams(validity) {
return (this.validParams = validity);
}
@action
handleKeyUp(e) {
if (e.code === "Enter" && this.validParams) {
this.save();
}
}
@action
cancel() {
this.send("closeModal");
}
@action
save() {
if (!this.validParams) {
return;
}
this.model.channel
.save()
.then(() => {
this.send("closeModal");
})
.catch(popupAjaxError);
}
}

View File

@ -1,51 +0,0 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default class AdminPluginsChatIntegrationEditRule extends Controller.extend(
ModalFunctionality
) {
@service siteSettings;
@tracked saveDisabled = false;
get showCategory() {
return this.model.rule.type === "normal";
}
get currentRuleType() {
return this.model.rule.type;
}
@action
save(rule) {
if (this.saveDisabled) {
return;
}
rule
.save()
.then(() => this.send("closeModal"))
.catch(popupAjaxError);
}
@action
handleKeyUp(e) {
if (e.code === "Enter") {
this.save();
}
}
@action
onChangeRuleType(type) {
this.model.rule.type = type;
this.currentRuleType = type;
if (type !== "normal") {
this.showCategory = false;
} else {
this.showCategory = true;
}
}
}

View File

@ -1,43 +0,0 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { not } from "@ember/object/computed";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import I18n from "I18n";
export default class AdminPluginsChatIntegrationTest extends Controller.extend(
ModalFunctionality
) {
@tracked loading = false;
@not("model.topic_id") sendDisabled;
@action
handleKeyUp(e) {
if (e.code === "Enter" && !this.sendDisabled) {
this.send();
}
}
@action
send() {
if (this.sendDisabled) {
return;
}
this.loading = true;
ajax("/admin/plugins/chat-integration/test", {
data: {
channel_id: this.model.channel.id,
topic_id: this.model.topic_id,
},
type: "POST",
})
.then(() => {
this.loading = false;
this.flash(I18n.t("chat_integration.test_modal.success"), "success");
})
.catch(popupAjaxError);
}
}

View File

@ -1,45 +1,38 @@
import { action } from "@ember/object";
import RSVP from "rsvp";
import Group from "discourse/models/group";
import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsChatIntegrationProvider extends DiscourseRoute {
model(params) {
return RSVP.hash({
channels: this.store.findAll("channel", { provider: params.provider }),
provider: this.modelFor("admin-plugins-chat-integration").findBy(
async model(params) {
const [channels, provider, groups] = await Promise.all([
this.store.findAll("channel", { provider: params.provider }),
this.modelFor("admin-plugins-chat-integration").findBy(
"id",
params.provider
),
groups: Group.findAll(),
}).then((value) => {
value.channels.forEach((channel) => {
channel.set(
"rules",
channel.rules.map((rule) => {
rule = this.store.createRecord("rule", rule);
rule.set("channel", channel);
return rule;
})
);
});
Group.findAll(),
]);
return value;
channels.forEach((channel) => {
channel.set(
"rules",
channel.rules.map((rule) => {
rule = this.store.createRecord("rule", rule);
rule.set("channel", channel);
return rule;
})
);
});
return {
channels,
provider,
groups,
};
}
serialize(model) {
return { provider: model["provider"].get("id") };
}
@action
closeModal() {
if (this.controller.modalShowing) {
this.refresh();
this.controller.modalShowing = false;
}
return true; // Continue bubbling up, so the modal actually closes
return { provider: model.provider.id };
}
@action

View File

@ -11,8 +11,6 @@ export default class AdminPluginsChatIntegration extends DiscourseRoute {
@action
showSettings() {
this.router.transitionTo("adminSiteSettingsCategory", "plugins", {
queryParams: { filter: "chat_integration" },
});
this.router.transitionTo("adminSiteSettingsCategory", "chat_integration");
}
}

View File

@ -1,4 +0,0 @@
<DModalBody @id="chat_integration_error_modal">
<h4>{{i18n model.error_key}}</h4>
<pre>{{model.error_info}}</pre>
</DModalBody>

View File

@ -1,60 +0,0 @@
<DModalBody
@title="chat_integration.edit_channel_modal.title"
@id="chat-integration-edit-channel-modal"
{{on "keyup" this.handleKeyUp}}
>
<div>
<form {{action "save" on="submit"}}>
<table>
<tbody>
<tr class="input">
<td class="label">
<label for="provider">
{{i18n "chat_integration.edit_channel_modal.provider"}}
</label>
</td>
<td>
{{i18n
(concat
"chat_integration.provider." model.channel.provider ".title"
)
}}
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td></td>
</tr>
{{#each this.model.provider.channel_parameters as |param|}}
<ChannelParamRow
@param={{param}}
@model={{this.model}}
@validParams={{this.validParams}}
@isValidParams={{this.isValidParams}}
/>
{{/each}}
</tbody>
</table>
</form>
</div>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary btn-large"
@id="save-channel"
@title="chat_integration.edit_channel_modal.save"
@label="chat_integration.edit_channel_modal.save"
@action={{action "save"}}
@disabled={{not this.validParams}}
/>
<DButton
@class="btn-large"
@title="chat_integration.edit_channel_modal.cancel"
@label="chat_integration.edit_channel_modal.cancel"
@action={{action "cancel"}}
/>
</div>

View File

@ -1,206 +0,0 @@
<DModalBody
@title="chat_integration.edit_rule_modal.title"
@id="chat-integration-edit-rule_modal"
{{on "keyup" this.handleKeyUp}}
>
<div>
<form {{action "save" on="submit"}}>
<table>
<tbody>
<tr class="input">
<td class="label">
<label for="provider">
{{i18n "chat_integration.edit_rule_modal.provider"}}
</label>
</td>
<td>
{{i18n
(concat
"chat_integration.provider."
this.model.channel.provider
".title"
)
}}
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td></td>
</tr>
<tr class="input">
<td class="label">
<label for="channel">
{{i18n "chat_integration.edit_rule_modal.channel"}}
</label>
</td>
<td>
<ChannelData
@provider={{this.model.provider}}
@channel={{this.model.channel}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td></td>
</tr>
<tr class="input">
<td class="label">
<label for="filter">
{{i18n "chat_integration.edit_rule_modal.type"}}
</label>
</td>
<td>
<ComboBox
@name="type"
@content={{this.model.rule.available_types}}
@value={{this.model.rule.type}}
@onChange={{action (mut this.model.rule.type)}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n "chat_integration.edit_rule_modal.instructions.type"}}
</label>
</td>
</tr>
<tr class="input">
<td class="label">
<label for="filter">
{{i18n "chat_integration.edit_rule_modal.filter"}}
</label>
</td>
<td>
<ComboBox
@name="filter"
@content={{this.model.rule.available_filters}}
@value={{this.model.rule.filter}}
@onChange={{action (mut this.model.rule.filter)}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n "chat_integration.edit_rule_modal.instructions.filter"}}
</label>
</td>
</tr>
{{#if (eq this.model.rule.type "normal")}}
<tr class="input">
<td class="label">
<label for="category">
{{i18n "chat_integration.edit_rule_modal.category"}}
</label>
</td>
<td>
<CategoryChooser
@name="category"
@options={{hash none="chat_integration.all_categories"}}
@value={{this.model.rule.category_id}}
@onChange={{action (mut this.model.rule.category_id)}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n
"chat_integration.edit_rule_modal.instructions.category"
}}
</label>
</td>
</tr>
{{else}}
<tr class="input">
<td class="label">
<label for="group">
{{i18n "chat_integration.edit_rule_modal.group"}}
</label>
</td>
<td>
<ComboBox
@content={{this.model.groups}}
@valueProperty="id"
@value={{this.model.rule.group_id}}
@onChange={{action (mut this.model.rule.group_id)}}
@options={{hash none="chat_integration.choose_group"}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n "chat_integration.edit_rule_modal.instructions.group"}}
</label>
</td>
</tr>
{{/if}}
{{#if this.siteSettings.tagging_enabled}}
<tr class="input">
<td class="label">
<label for="tags">
{{i18n "chat_integration.edit_rule_modal.tags"}}
</label>
</td>
<td>
<TagChooser
@placeholderKey="chat_integration.all_tags"
@name="tags"
@tags={{this.model.rule.tags}}
@everyTag="true"
@onChange={{action (mut this.model.rule.tags)}}
/>
</td>
</tr>
<tr class="chat-instructions">
<td></td>
<td>
<label>
{{i18n "chat_integration.edit_rule_modal.instructions.tags"}}
</label>
</td>
</tr>
{{/if}}
</tbody>
</table>
</form>
</div>
</DModalBody>
<div class="modal-footer">
<DButton
@id="save-rule"
@class="btn-primary btn-large"
@title="chat_integration.edit_rule_modal.save"
@label="chat_integration.edit_rule_modal.save"
@action={{action "save"}}
@actionParam={{this.model.rule}}
@disabled={{this.saveDisabled}}
/>
<DButton
@class="btn-large"
@title="chat_integration.edit_rule_modal.cancel"
@label="chat_integration.edit_rule_modal.cancel"
@action={{route-action "closeModal"}}
/>
</div>

View File

@ -1,43 +0,0 @@
<DModalBody
@title="chat_integration.test_modal.title"
@id="chat_integration_test_modal"
{{on "keyup" this.handleKeyUp}}
>
<div>
<form {{action "send" on="submit"}}>
<table>
<tbody>
<tr class="input">
<td class="label">
<label for="channel">
{{i18n "chat_integration.test_modal.topic"}}
</label>
</td>
<td>
<ChooseTopic @selectedTopicId={{this.model.topic_id}} />
</td>
</tr>
</tbody>
</table>
</form>
</div>
</DModalBody>
<div class="modal-footer">
<ConditionalLoadingSpinner @condition={{this.loading}}>
<DButton
@class="btn-primary btn-large"
@id="send-test"
@title="chat_integration.test_modal.send"
@action={{action "send"}}
@label="chat_integration.test_modal.send"
@disabled={{this.sendDisabled}}
/>
<DButton
@class="btn-large"
@title="chat_integration.test_modal.close"
@action={{route-action "closeModal"}}
@label="chat_integration.test_modal.close"
/>
</ConditionalLoadingSpinner>
</div>

View File

@ -11,23 +11,22 @@
<ChannelDetails
@channel={{channel}}
@provider={{this.model.provider}}
@refresh={{action "refresh"}}
@editChannel={{action "editChannel"}}
@test={{action "testChannel"}}
@createRule={{action "createRule"}}
@editRuleWithChannel={{action "editRuleWithChannel"}}
@showError={{action "showError"}}
@refresh={{this.refresh}}
@editChannel={{this.editChannel}}
@test={{this.testChannel}}
@createRule={{this.createRule}}
@editRuleWithChannel={{this.editRuleWithChannel}}
@showError={{this.showError}}
/>
{{/each}}
<div class="table-footer">
<div class="pull-right">
<DButton
@id="create-channel"
@icon="plus"
@title="chat_integration.create_channel"
@action={{fn this.createChannel this.model.provider}}
@label="chat_integration.create_channel"
@action={{action "createChannel" this.model.provider}}
@icon="plus"
@id="create-channel"
/>
</div>
</div>

View File

@ -21,6 +21,7 @@
@title="chat_integration.settings"
@label="chat_integration.settings"
@action={{route-action "showSettings"}}
class="chat-integration-settings-button"
/>
</div>

View File

@ -3,7 +3,7 @@ import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import DiscourseRoute from "discourse/routes/discourse";
export default class Trascript extends DiscourseRoute {
export default class Transcript extends DiscourseRoute {
@service currentUser;
@service composer;
@service router;
@ -15,12 +15,10 @@ export default class Trascript extends DiscourseRoute {
return;
}
const secret = params.secret;
await this.router.replaceWith("discovery.latest").followRedirects();
try {
const result = await ajax(`/chat-transcript/${secret}`);
const result = await ajax(`/chat-transcript/${params.secret}`);
this.composer.openNewTopic({
body: result.content,
});

View File

@ -12,7 +12,7 @@
}
div.table-footer {
margin: 10px;
margin-top: 10px;
}
div.error {
@ -26,7 +26,7 @@
}
div.channel-details {
margin: 20px 10px;
margin-top: 20px;
border: 1px solid var(--primary-low);
div.channel-header {
@ -48,11 +48,8 @@
}
}
.admin-controls {
padding: 10px;
.nav-pills {
padding: 0;
}
.chat-integration-settings-button {
margin-right: 10px;
}
}

View File

@ -9,7 +9,7 @@
enabled_site_setting :chat_integration_enabled
register_asset "stylesheets/chat-integration-admin.scss"
register_asset "stylesheets/chat-integration.scss"
register_svg_icon "rocket" if respond_to?(:register_svg_icon)
register_svg_icon "fa-arrow-circle-o-right" if respond_to?(:register_svg_icon)

View File

@ -1,9 +1,8 @@
# frozen_string_literal: true
require "rails_helper"
require_relative "../dummy_provider"
describe "Chat Controller", type: :request do
RSpec.describe "Chat Controller", type: :request do
let(:topic) { Fabricate(:post).topic }
let(:admin) { Fabricate(:admin) }
let(:category) { Fabricate(:category) }

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
require "rails_helper"
describe "Public Controller", type: :request do
RSpec.describe "Public Controller", type: :request do
before { SiteSetting.chat_integration_enabled = true }
describe "loading a transcript" do
@ -16,7 +14,7 @@ describe "Public Controller", type: :request do
expect(response.body).to eq('{"content":"Some content here"}')
end
it "should 404 for non-existant transcript" do
it "should 404 for non-existent transcript" do
key = "abcdefghijk"
get "/chat-transcript/#{key}.json"

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
RSpec.describe "Create channel", type: :system do
fab!(:admin)
before do
SiteSetting.chat_integration_enabled = true
SiteSetting.chat_integration_discord_enabled = true
sign_in(admin)
end
it "creates and displays a new channel" do
visit("/admin/plugins/chat-integration/discord")
expect(page).to have_no_css(".channel-details")
click_button(I18n.t("js.chat_integration.create_channel"))
find("input[name='param-name']").fill_in(with: "bloop")
find("input[name='param-webhook_url']").fill_in(with: "https://discord.com/api/webhooks/bloop")
click_button(I18n.t("js.chat_integration.edit_channel_modal.save"))
expect(page).to have_css(".channel-details")
expect(find(".channel-info")).to have_content("bloop")
end
end

View File

@ -1,11 +1,6 @@
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
import { click, fillIn, triggerEvent, visit } from "@ember/test-helpers";
import { test } from "qunit";
import {
acceptance,
exists,
query,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
const response = (object) => {
return [200, { "Content-Type": "text/html; charset=utf-8" }, object];
@ -91,130 +86,114 @@ acceptance("Chat Integration", function (needs) {
test("Rules load successfully", async function (assert) {
await visit("/admin/plugins/chat-integration");
assert.ok(
exists("#admin-plugin-chat table"),
"it shows the table of rules"
);
assert
.dom("#admin-plugin-chat table")
.exists("it shows the table of rules");
assert.strictEqual(
queryAll("#admin-plugin-chat table tr td")[0].innerText.trim(),
"All posts and replies",
"rule displayed"
);
assert
.dom("#admin-plugin-chat table tr td")
.hasText("All posts and replies", "rule displayed");
});
test("Create channel works", async function (assert) {
await visit("/admin/plugins/chat-integration");
await click("#create-channel");
assert.ok(
exists("#chat-integration-edit-channel-modal"),
"it displays the modal"
);
assert.ok(query("#save-channel").disabled, "it disables the save button");
assert
.dom("#chat-integration-edit-channel-modal")
.exists("it displays the modal");
assert.dom("#save-channel").isDisabled();
await fillIn("#chat-integration-edit-channel-modal input", "#general");
assert.notOk(query("#save-channel").disabled, "it enables the save button");
assert.dom("#save-channel").isEnabled();
await click("#save-channel");
assert.notOk(
exists("#chat-integration-edit-channel-modal"),
"modal closes on save"
);
assert
.dom("#chat-integration-edit-channel-modal")
.doesNotExist("modal closes on save");
});
test("Edit channel works", async function (assert) {
await visit("/admin/plugins/chat-integration");
await click(".channel-header button");
assert.ok(
exists("#chat-integration-edit-channel-modal"),
"it displays the modal"
);
assert.notOk(query("#save-channel").disabled, "save is enabled");
assert
.dom("#chat-integration-edit-channel-modal")
.exists("it displays the modal");
assert.dom("#save-channel").isEnabled();
await fillIn("#chat-integration-edit-channel-modal input", " general");
assert.ok(query("#save-channel").disabled, "it disables the save button");
assert.dom("#save-channel").isDisabled();
await fillIn("#chat-integration-edit-channel-modal input", "#random");
assert.dom("#save-channel").isEnabled();
// Press enter
await triggerKeyEvent(
"#chat-integration-edit-channel-modal",
"keydown",
"Enter"
);
await triggerEvent("#chat-integration-edit-channel-modal", "submit");
assert.notOk(
exists("#chat-integration-edit-channel-modal"),
"modal saves on enter"
);
assert
.dom("#chat-integration-edit-channel-modal")
.doesNotExist("modal saves on enter");
});
test("Create rule works", async function (assert) {
await visit("/admin/plugins/chat-integration");
assert.ok(exists(".channel-footer button"), "create button is displayed");
assert.dom(".channel-footer button").exists("create button is displayed");
await click(".channel-footer button");
assert.ok(
exists("#chat-integration-edit-rule_modal"),
"modal opens on edit"
);
assert.notOk(query("#save-rule").disabled, "save is enabled");
assert
.dom("#chat-integration-edit-rule_modal")
.exists("modal opens on edit");
assert.dom("#save-rule").isEnabled();
await click("#save-rule");
assert.notOk(
exists("#chat-integration-edit-rule_modal"),
"modal closes on save"
);
assert
.dom("#chat-integration-edit-rule_modal")
.doesNotExist("modal closes on save");
});
test("Edit rule works", async function (assert) {
await visit("/admin/plugins/chat-integration");
assert.ok(exists(".edit"), "edit button is displayed");
assert.dom(".edit").exists("edit button is displayed");
await click(".edit");
assert.ok(
exists("#chat-integration-edit-rule_modal"),
"modal opens on edit"
);
assert.notOk(query("#save-rule").disabled, "it enables the save button");
assert
.dom("#chat-integration-edit-rule_modal")
.exists("modal opens on edit");
assert.dom("#save-rule").isEnabled();
await click("#save-rule");
assert.notOk(
exists("#chat-integration-edit-rule_modal"),
"modal closes on save"
);
assert
.dom("#chat-integration-edit-rule_modal")
.doesNotExist("modal closes on save");
});
test("Delete channel works", async function (assert) {
await visit("/admin/plugins/chat-integration");
assert.ok(
exists(".channel-header .delete-channel"),
"delete buttons exists"
);
assert
.dom(".channel-header .delete-channel")
.exists("delete buttons exists");
await click(".channel-header .delete-channel");
assert.ok(exists("div.dialog-content"), "modal is displayed");
assert.dom("div.dialog-content").exists("dialog is displayed");
await click("div.dialog-content .btn-danger");
assert.notOk(exists("div.dialog-content"), "modal has closed");
assert.dom("div.dialog-content").doesNotExist("dialog has closed");
});
test("Delete rule works", async function (assert) {
await visit("/admin/plugins/chat-integration");
assert.ok(exists(".delete"));
assert.dom(".delete").exists();
await click(".delete");
});
@ -223,23 +202,21 @@ acceptance("Chat Integration", function (needs) {
await click(".btn-chat-test");
assert.ok(exists("#chat_integration_test_modal"), "it displays the modal");
assert.ok(query("#send-test").disabled, "it disables the send button");
assert.dom("#chat_integration_test_modal").exists("it displays the modal");
assert.dom("#send-test").isDisabled();
await fillIn("#choose-topic-title", "9318");
await click("#chat_integration_test_modal .radio");
assert.notOk(query("#send-test").disabled, "it enables the send button");
assert.dom("#send-test").isEnabled();
await click("#send-test");
assert.ok(
exists("#chat_integration_test_modal"),
"modal doesn't close on send"
);
assert.ok(
exists("#modal-alert.alert-success"),
"success message displayed"
);
assert
.dom("#chat_integration_test_modal")
.exists("modal doesn't close on send");
assert
.dom("#modal-alert.alert-success")
.exists("success message displayed");
});
});