UX: simplify and shorten new script flow for automations (#29178)

This commit is contained in:
Kris 2024-10-23 14:04:17 -04:00 committed by GitHub
parent b7f76d99e8
commit d471c01ff6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 210 additions and 168 deletions

View File

@ -9,6 +9,7 @@ import I18n from "discourse-i18n";
export default class AutomationEdit extends Controller { export default class AutomationEdit extends Controller {
@service dialog; @service dialog;
@service router;
error = null; error = null;
isUpdatingAutomation = false; isUpdatingAutomation = false;
isTriggeringAutomation = false; isTriggeringAutomation = false;
@ -26,7 +27,7 @@ export default class AutomationEdit extends Controller {
} }
@action @action
saveAutomation() { saveAutomation(routeToIndex = false) {
this.setProperties({ error: null, isUpdatingAutomation: true }); this.setProperties({ error: null, isUpdatingAutomation: true });
return ajax( return ajax(
@ -40,6 +41,9 @@ export default class AutomationEdit extends Controller {
) )
.then(() => { .then(() => {
this.send("refreshRoute"); this.send("refreshRoute");
if (routeToIndex) {
this.router.transitionTo("adminPlugins.discourse-automation.index");
}
}) })
.catch((e) => this._showError(e)) .catch((e) => this._showError(e))
.finally(() => { .finally(() => {

View File

@ -1,38 +1,46 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import EmberObject, { action } from "@ember/object"; import { action } from "@ember/object";
import { service } from "@ember/service"; import { inject as service } from "@ember/service";
import { extractError } from "discourse/lib/ajax-error";
export default class AutomationNew extends Controller { export default class AutomationNew extends Controller {
@service router; @service router;
@tracked filterText = "";
form = null; @action
error = null; updateFilterText(event) {
this.filterText = event.target.value;
init() {
super.init(...arguments);
this._resetForm();
} }
@action @action
saveAutomation() { resetFilterText() {
this.set("error", null); this.filterText = "";
this.model.automation
.save(this.form.getProperties("name", "script"))
.then(() => {
this._resetForm();
this.router.transitionTo(
"adminPlugins.discourse-automation.edit",
this.model.automation.id
);
})
.catch((e) => {
this.set("error", extractError(e));
});
} }
_resetForm() { get scriptableContent() {
this.set("form", EmberObject.create({ name: null, script: null })); let scripts = this.model.scriptables.content;
let filter = this.filterText.toLowerCase();
if (!filter) {
return scripts;
}
return scripts.filter((script) => {
const name = script.name ? script.name.toLowerCase() : "";
const description = script.description
? script.description.toLowerCase()
: "";
return name.includes(filter) || description.includes(filter);
});
}
@action
selectScriptToEdit(newScript) {
this.model.automation.save({ script: newScript.id }).then(() => {
this.router.transitionTo(
"adminPlugins.discourse-automation.edit",
this.model.automation.id
);
});
} }
} }

View File

@ -1,13 +1,22 @@
import { action } from "@ember/object"; import { action } from "@ember/object";
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default class AutomationIndex extends DiscourseRoute { export default class AutomationIndex extends DiscourseRoute {
@service router;
controllerName = "admin-plugins-discourse-automation-index"; controllerName = "admin-plugins-discourse-automation-index";
model() { model() {
return this.store.findAll("discourse-automation-automation"); return this.store.findAll("discourse-automation-automation");
} }
afterModel(model) {
if (!model.length) {
this.router.transitionTo("adminPlugins.discourse-automation.new");
}
}
@action @action
triggerRefresh() { triggerRefresh() {
this.refresh(); this.refresh();

View File

@ -6,6 +6,7 @@ export default class AutomationNew extends DiscourseRoute {
model() { model() {
return hash({ return hash({
scripts: this.store.findAll("discourse-automation-automation"),
scriptables: this.store.findAll("discourse-automation-scriptable"), scriptables: this.store.findAll("discourse-automation-scriptable"),
automation: this.store.createRecord("discourse-automation-automation"), automation: this.store.createRecord("discourse-automation-automation"),
}); });

View File

@ -10,11 +10,12 @@
<div class="controls"> <div class="controls">
<TextField <TextField
@value={{automationForm.name}} @value={{this.automationForm.name}}
@type="text" @type="text"
@autofocus="autofocus" @autofocus={{true}}
@name="automation-name" @name="automation-name"
class="input-large" class="input-large"
@input={{with-event-value (fn (mut this.automationForm.name))}}
/> />
</div> </div>
</div> </div>
@ -26,9 +27,9 @@
<div class="controls"> <div class="controls">
<ComboBox <ComboBox
@value={{automationForm.script}} @value={{this.automationForm.script}}
@content={{model.scriptables}} @content={{this.model.scriptables}}
@onChange={{action "onChangeScript"}} @onChange={{this.onChangeScript}}
@options={{hash filterable=true}} @options={{hash filterable=true}}
class="scriptables" class="scriptables"
/> />
@ -42,7 +43,7 @@
</h2> </h2>
<div class="control-group"> <div class="control-group">
{{#if model.automation.script.forced_triggerable}} {{#if this.model.automation.script.forced_triggerable}}
<div class="alert alert-warning"> <div class="alert alert-warning">
{{i18n {{i18n
"discourse_automation.edit_automation.trigger_section.forced" "discourse_automation.edit_automation.trigger_section.forced"
@ -56,47 +57,50 @@
<div class="controls"> <div class="controls">
<ComboBox <ComboBox
@value={{automationForm.trigger}} @value={{this.automationForm.trigger}}
@content={{model.triggerables}} @content={{this.model.triggerables}}
@onChange={{action "onChangeTrigger"}} @onChange={{this.onChangeTrigger}}
@options={{hash @options={{hash
filterable=true filterable=true
none="discourse_automation.select_trigger" none="discourse_automation.select_trigger"
disabled=model.automation.script.forced_triggerable disabled=this.model.automation.script.forced_triggerable
}} }}
class="triggerables" class="triggerables"
/> />
</div> </div>
</div> </div>
{{#if automationForm.trigger}} {{#if this.automationForm.trigger}}
{{#if model.automation.trigger.doc}} {{#if this.model.automation.trigger.doc}}
<div class="alert alert-info"> <div class="alert alert-info">
<p>{{model.automation.trigger.doc}}</p> <p>{{this.model.automation.trigger.doc}}</p>
</div> </div>
{{/if}} {{/if}}
{{#if {{#if
(and (and
model.automation.enabled this.model.automation.enabled
model.automation.trigger.settings.manual_trigger this.model.automation.trigger.settings.manual_trigger
) )
}} }}
<div class="alert alert-info next-trigger"> <div class="alert alert-info next-trigger">
{{#if nextPendingAutomationAtFormatted}} {{#if this.nextPendingAutomationAtFormatted}}
<p> <p>
{{i18n {{i18n
"discourse_automation.edit_automation.trigger_section.next_pending_automation" "discourse_automation.edit_automation.trigger_section.next_pending_automation"
date=nextPendingAutomationAtFormatted date=this.nextPendingAutomationAtFormatted
}} }}
</p> </p>
{{/if}} {{/if}}
<DButton <DButton
@label="discourse_automation.edit_automation.trigger_section.trigger_now" @label="discourse_automation.edit_automation.trigger_section.trigger_now"
@isLoading={{isTriggeringAutomation}} @isLoading={{this.isTriggeringAutomation}}
@action={{action "onManualAutomationTrigger" model.automation.id}} @action={{fn
this.onManualAutomationTrigger
this.model.automation.id
}}
class="btn-primary trigger-now-btn" class="btn-primary trigger-now-btn"
/> />
</div> </div>
@ -104,50 +108,53 @@
{{#each triggerFields as |field|}} {{#each triggerFields as |field|}}
<AutomationField <AutomationField
@automation={{automation}} @automation={{this.automation}}
@field={{field}} @field={{field}}
@saveAutomation={{action "saveAutomation" automation}} @saveAutomation={{fn this.saveAutomation this.automation}}
/> />
{{/each}} {{/each}}
{{/if}} {{/if}}
</section> </section>
{{#if automationForm.trigger}} {{#if this.automationForm.trigger}}
{{#if scriptFields}} {{#if this.scriptFields}}
<section class="fields-section form-section edit"> <section class="fields-section form-section edit">
<h2 class="title"> <h2 class="title">
{{i18n "discourse_automation.edit_automation.fields_section.title"}} {{i18n "discourse_automation.edit_automation.fields_section.title"}}
</h2> </h2>
{{#if model.automation.script.with_trigger_doc}} {{#if this.model.automation.script.with_trigger_doc}}
<div class="alert alert-info"> <div class="alert alert-info">
<p>{{model.automation.script.with_trigger_doc}}</p> <p>{{this.model.automation.script.with_trigger_doc}}</p>
</div> </div>
{{/if}} {{/if}}
<div class="control-group"> <div class="control-group">
{{#each scriptFields as |field|}} {{#each this.scriptFields as |field|}}
<AutomationField <AutomationField
@automation={{automation}} @automation={{this.automation}}
@field={{field}} @field={{field}}
@saveAutomation={{action "saveAutomation" automation}} @saveAutomation={{fn this.saveAutomation this.automation}}
/> />
{{/each}} {{/each}}
</div> </div>
</section> </section>
{{/if}} {{/if}}
{{#if automationForm.trigger}} {{#if this.automationForm.trigger}}
<div class="control-group automation-enabled alert alert-warning"> <div
class="control-group automation-enabled alert
{{if this.automationForm.enabled 'alert-info' 'alert-warning'}}"
>
<span>{{i18n <span>{{i18n
"discourse_automation.models.automation.enabled.label" "discourse_automation.models.automation.enabled.label"
}}</span> }}</span>
<Input <Input
@type="checkbox" @type="checkbox"
@checked={{automationForm.enabled}} @checked={{this.automationForm.enabled}}
{{on {{on
"click" "click"
(action (mut automationForm.enabled) value="target.checked") (action (mut this.automationForm.enabled) value="target.checked")
}} }}
/> />
</div> </div>
@ -155,10 +162,10 @@
<div class="control-group"> <div class="control-group">
<DButton <DButton
@isLoading={{isUpdatingAutomation}} @isLoading={{this.isUpdatingAutomation}}
@label="discourse_automation.update" @label="discourse_automation.update"
@type="submit" @type="submit"
@action={{action "saveAutomation" automation}} @action={{fn this.saveAutomation this.automation true}}
class="btn-primary update-automation" class="btn-primary update-automation"
/> />
</div> </div>

View File

@ -33,6 +33,7 @@
</td> </td>
{{else}} {{else}}
<td <td
class="automations__status"
role="button" role="button"
{{on "click" (fn this.editAutomation automation)}} {{on "click" (fn this.editAutomation automation)}}
>{{format-enabled-automation >{{format-enabled-automation
@ -40,16 +41,23 @@
automation.trigger automation.trigger
}}</td> }}</td>
<td <td
class="automations__name"
tabindex="0" tabindex="0"
role="button" role="button"
{{on "keypress" (fn this.editAutomation automation)}} {{on "keypress" (fn this.editAutomation automation)}}
{{on "click" (fn this.editAutomation automation)}} {{on "click" (fn this.editAutomation automation)}}
>{{automation.name}}</td> >{{if
automation.name
automation.name
(i18n "discourse_automation.unnamed_automation")
}}</td>
<td <td
class="automations__script"
role="button" role="button"
{{on "click" (fn this.editAutomation automation)}} {{on "click" (fn this.editAutomation automation)}}
>{{if automation.trigger.id automation.trigger.name "-"}}</td> >{{if automation.trigger.id automation.trigger.name "-"}}</td>
<td <td
class="automations__version"
role="button" role="button"
{{on "click" (fn this.editAutomation automation)}} {{on "click" (fn this.editAutomation automation)}}
>{{automation.script.name}} (v{{automation.script.version}})</td> >{{automation.script.name}} (v{{automation.script.version}})</td>
@ -64,7 +72,7 @@
</td> </td>
{{/if}} {{/if}}
<td> <td class="automations__delete">
<DButton <DButton
@icon="trash-can" @icon="trash-can"
@action={{action "destroyAutomation" automation}} @action={{action "destroyAutomation" automation}}
@ -75,14 +83,4 @@
{{/each}} {{/each}}
</tbody> </tbody>
</table> </table>
{{else}}
<div class="alert alert-info">
<p>{{i18n "discourse_automation.no_automation_yet"}}</p>
<DButton
@label="discourse_automation.create"
@icon="plus"
@action={{action "newAutomation"}}
class="btn-primary"
/>
</div>
{{/if}} {{/if}}

View File

@ -1,52 +1,33 @@
<section class="discourse-automation-form new"> <section class="discourse-automation-form new">
<form class="form-horizontal">
<FormError @error={{error}} />
<div class="control-group"> <div
<label class="control-label"> class="admin-section-landing__header"
{{i18n "discourse_automation.models.automation.name.label"}} {{did-insert this.resetFilterText}}
</label> >
<h2>{{i18n "discourse_automation.select_script"}}</h2>
<div class="controls"> <input
<TextField type="text"
@value={{form.name}} placeholder={{i18n "discourse_automation.filter_placeholder"}}
@type="text" {{on "input" this.updateFilterText}}
@autofocus="autofocus" class="admin-section-landing__header-filter"
@name="automation-name" />
class="input-large" </div>
/>
</div> {{#unless this.model.scripts.length}}
<div class="alert alert-info">
<p>{{i18n "discourse_automation.no_automation_yet"}}</p>
</div> </div>
{{/unless}}
<div class="control-group"> <div class="admin-section-landing__wrapper">
<label class="control-label"> {{#each this.scriptableContent as |script|}}
{{i18n "discourse_automation.models.automation.script.label"}} <AdminSectionLandingItem
</label> {{on "click" (fn this.selectScriptToEdit script)}}
@titleLabelTranslated={{script.name}}
@descriptionLabelTranslated={{script.description}}
/>
{{/each}}
</div>
<div class="controls">
<DropdownSelectBox
@value={{form.script}}
@content={{model.scriptables.content}}
@onChange={{fn (mut form.script)}}
@options={{hash
showCaret=true
filterable=true
none="discourse_automation.select_script"
}}
class="scriptables"
/>
</div>
</div>
<div class="control-group">
<div class="controls">
<DButton
@icon="plus"
@label="discourse_automation.create"
@action={{this.saveAutomation}}
class="btn-primary create-automation"
/>
</div>
</div>
</form>
</section> </section>

View File

@ -21,7 +21,7 @@ module DiscourseAutomation
end end
def create def create
automation_params = params.require(:automation).permit(:name, :script, :trigger) automation_params = params.require(:automation).permit(:script, :trigger)
automation = automation =
DiscourseAutomation::Automation.new( DiscourseAutomation::Automation.new(

View File

@ -30,9 +30,8 @@ module DiscourseAutomation
@running_in_background = true @running_in_background = true
end end
MIN_NAME_LENGTH = 5
MAX_NAME_LENGTH = 100 MAX_NAME_LENGTH = 100
validates :name, length: { in: MIN_NAME_LENGTH..MAX_NAME_LENGTH } validates :name, length: { maximum: MAX_NAME_LENGTH }
def add_id_to_custom_field(target, custom_field_key) def add_id_to_custom_field(target, custom_field_key)
if ![Topic, Post, User].any? { |m| target.is_a?(m) } if ![Topic, Post, User].any? { |m| target.is_a?(m) }

View File

@ -7,6 +7,46 @@
td[role="button"] { td[role="button"] {
cursor: pointer; cursor: pointer;
} }
&__name {
word-break: break-word;
}
}
.admin-section-landing__header {
display: flex;
align-items: center;
flex-wrap: wrap;
h2 {
margin: 0 auto 0 0;
}
&-filter {
margin: 0;
flex: 0 1 15em;
}
}
.admin-section-landing__wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
gap: 1em 2em;
margin-top: 1em;
border-top: 3px solid var(--primary-low); // matches tbody border
padding-top: 1em;
}
.admin-section-landing-item {
cursor: pointer;
display: grid;
grid-template-rows: subgrid;
grid-row: span 4;
gap: 0;
&__buttons {
display: none; // empty container
}
&__description {
max-width: 18.75em;
}
} }
} }
@ -42,26 +82,12 @@
} }
} }
.alert-info {
margin-top: 1em;
}
.alert { .alert {
padding: 1em; padding: 1em;
background: var(--primary-very-low);
border-left-style: solid;
border-left-width: 5px;
&.alert-info {
border-left-color: var(--tertiary-low);
}
&.alert-warning {
border-left-color: var(--highlight);
background: var(--highlight-low);
}
&.alert-error {
border-left-color: var(--danger);
background: var(--danger-low);
}
p { p {
margin: 0; margin: 0;
} }

View File

@ -8,7 +8,8 @@ en:
select_trigger: Select a trigger select_trigger: Select a trigger
confirm_automation_reset: This action will reset script and trigger options, new state will be saved, do you want to proceed? confirm_automation_reset: This action will reset script and trigger options, new state will be saved, do you want to proceed?
confirm_automation_trigger: This action will trigger the automation, do you want to proceed? confirm_automation_trigger: This action will trigger the automation, do you want to proceed?
no_automation_yet: You havent created any automation yet. no_automation_yet: You havent created any automations yet. Choose an option below to get started.
filter_placeholder: Filter by name or description...
edit_automation: edit_automation:
trigger_section: trigger_section:
forced: This trigger is forced by script. forced: This trigger is forced by script.
@ -384,6 +385,7 @@ en:
fields: fields:
custom_field_name: custom_field_name:
label: "User Custom Field name" label: "User Custom Field name"
unnamed_automation: "Unnamed automation"
models: models:
script: script:

View File

@ -99,6 +99,7 @@ en:
doc: Allows to send multiple pms to a user. Each PM accepts a delay. doc: Allows to send multiple pms to a user. Each PM accepts a delay.
suspend_user_by_email: suspend_user_by_email:
title: Suspend user by email title: Suspend user by email
description: Automatically suspend an account based on email address
user_global_notice: user_global_notice:
title: User global notice title: User global notice
description: Allows to display a global notice for a user description: Allows to display a global notice for a user
@ -135,6 +136,13 @@ en:
button_text: Done button_text: Done
add_user_to_group_through_custom_field: add_user_to_group_through_custom_field:
title: "Add user to group through User Custom Field" title: "Add user to group through User Custom Field"
description: "Automatically add users to groups when they log in or with a recurring check"
group_category_notification_default: group_category_notification_default:
title: "Group Category Notification Default" title: "Group Category Notification Default"
description: "Set the default notification level of a category for members of a group" description: "Set the default notification level of a category for members of a group"
send_chat_message:
title: "Send Chat Message"
description: "Send a custom chat message to a channel"
random_assign:
title: "Random Assign"
description: "Randomly assign topics to a group"

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class RemoveNameRequirementFromAutomations < ActiveRecord::Migration[7.1]
def change
change_column_null :discourse_automation_automations, :name, true
end
end

View File

@ -187,10 +187,6 @@ describe DiscourseAutomation::Automation do
expect(automation).not_to be_valid expect(automation).not_to be_valid
expect(automation.errors[:name]).to eq(["is too long (maximum is 100 characters)"]) expect(automation.errors[:name]).to eq(["is too long (maximum is 100 characters)"])
automation = Fabricate.build(:automation, name: "b" * 4)
expect(automation).not_to be_valid
expect(automation.errors[:name]).to eq(["is too short (minimum is 5 characters)"])
automation = Fabricate.build(:automation, name: "c" * 50) automation = Fabricate.build(:automation, name: "c" * 50)
expect(automation).to be_valid expect(automation).to be_valid
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
describe "DiscourseAutomation | error", type: :system do describe "DiscourseAutomation | error", type: :system, js: true do
fab!(:admin) fab!(:admin)
before do before do
@ -10,14 +10,13 @@ describe "DiscourseAutomation | error", type: :system do
context "when saving the form with an error" do context "when saving the form with an error" do
it "shows the error correctly" do it "shows the error correctly" do
visit("/admin/plugins/discourse-automation") visit("/admin/plugins/discourse-automation/new")
find(".admin-section-landing__header-filter").set("create a post")
find(".admin-section-landing-item", match: :first).click
find(".new-automation").click expect(page).to have_selector("input[name='automation-name']")
fill_in("automation-name", with: "aaaaa")
select_kit = PageObjects::Components::SelectKit.new(".scriptables") find('input[name="automation-name"]').set("aaaaa")
select_kit.expand
select_kit.select_row_by_value("post")
find(".create-automation").click
select_kit = PageObjects::Components::SelectKit.new(".triggerables") select_kit = PageObjects::Components::SelectKit.new(".triggerables")
select_kit.expand select_kit.expand
select_kit.select_row_by_value("recurring") select_kit.select_row_by_value("recurring")
@ -29,6 +28,8 @@ describe "DiscourseAutomation | error", type: :system do
{ name: "topic", target: "script", target_name: "post" }, { name: "topic", target: "script", target_name: "post" },
), ),
) )
expect(find('input[name="automation-name"]').value).to eq("aaaaa")
end end
end end
end end

View File

@ -10,11 +10,13 @@ describe "DiscourseAutomation | New automation", type: :system, js: true do
let(:new_automation_page) { PageObjects::Pages::NewAutomation.new } let(:new_automation_page) { PageObjects::Pages::NewAutomation.new }
context "when the script is not selected" do context "when a script is clicked" do
it "shows an error" do it "navigates to automation edit route" do
new_automation_page.visit.fill_name("aaaaa").create new_automation_page.visit
expect(new_automation_page).to have_error(I18n.t("errors.messages.blank")) find(".admin-section-landing-item__content", match: :first).click
expect(page).to have_css(".scriptables")
end end
end end
end end

View File

@ -22,13 +22,9 @@ describe "DiscourseAutomation | smoke test", type: :system, js: true do
it "populate correctly" do it "populate correctly" do
visit("/admin/plugins/discourse-automation") visit("/admin/plugins/discourse-automation")
find(".new-automation").click find(".admin-section-landing__header-filter").set("test")
find(".admin-section-landing-item__content", match: :first).click
fill_in("automation-name", with: "aaaaa") fill_in("automation-name", with: "aaaaa")
select_kit = PageObjects::Components::SelectKit.new(".scriptables")
select_kit.expand
select_kit.select_row_by_value("test")
find(".create-automation").click
select_kit = PageObjects::Components::SelectKit.new(".triggerables") select_kit = PageObjects::Components::SelectKit.new(".triggerables")
select_kit.expand select_kit.expand
select_kit.select_row_by_value("post_created_edited") select_kit.select_row_by_value("post_created_edited")
@ -40,12 +36,9 @@ describe "DiscourseAutomation | smoke test", type: :system, js: true do
it "works" do it "works" do
visit("/admin/plugins/discourse-automation") visit("/admin/plugins/discourse-automation")
find(".new-automation").click find(".admin-section-landing__header-filter").set("user group membership through badge")
find(".admin-section-landing-item__content", match: :first).click
fill_in("automation-name", with: "aaaaa") fill_in("automation-name", with: "aaaaa")
select_kit = PageObjects::Components::SelectKit.new(".scriptables")
select_kit.expand
select_kit.select_row_by_value("user_group_membership_through_badge")
find(".create-automation").click
select_kit = PageObjects::Components::SelectKit.new(".triggerables") select_kit = PageObjects::Components::SelectKit.new(".triggerables")
select_kit.expand select_kit.expand
select_kit.select_row_by_value("user_first_logged_in") select_kit.select_row_by_value("user_first_logged_in")
@ -58,6 +51,6 @@ describe "DiscourseAutomation | smoke test", type: :system, js: true do
find(".automation-enabled input").click find(".automation-enabled input").click
find(".update-automation").click find(".update-automation").click
expect(page).to have_field("automation-name", with: "aaaaa") expect(page).to have_css('[role="button"]', text: "aaaaa")
end end
end end