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

View File

@ -1,38 +1,46 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller";
import EmberObject, { action } from "@ember/object";
import { service } from "@ember/service";
import { extractError } from "discourse/lib/ajax-error";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
export default class AutomationNew extends Controller {
@service router;
@tracked filterText = "";
form = null;
error = null;
init() {
super.init(...arguments);
this._resetForm();
@action
updateFilterText(event) {
this.filterText = event.target.value;
}
@action
saveAutomation() {
this.set("error", null);
resetFilterText() {
this.filterText = "";
}
this.model.automation
.save(this.form.getProperties("name", "script"))
.then(() => {
this._resetForm();
get scriptableContent() {
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
);
})
.catch((e) => {
this.set("error", extractError(e));
});
}
_resetForm() {
this.set("form", EmberObject.create({ name: null, script: null }));
}
}

View File

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

View File

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

View File

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

View File

@ -33,6 +33,7 @@
</td>
{{else}}
<td
class="automations__status"
role="button"
{{on "click" (fn this.editAutomation automation)}}
>{{format-enabled-automation
@ -40,16 +41,23 @@
automation.trigger
}}</td>
<td
class="automations__name"
tabindex="0"
role="button"
{{on "keypress" (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
class="automations__script"
role="button"
{{on "click" (fn this.editAutomation automation)}}
>{{if automation.trigger.id automation.trigger.name "-"}}</td>
<td
class="automations__version"
role="button"
{{on "click" (fn this.editAutomation automation)}}
>{{automation.script.name}} (v{{automation.script.version}})</td>
@ -64,7 +72,7 @@
</td>
{{/if}}
<td>
<td class="automations__delete">
<DButton
@icon="trash-can"
@action={{action "destroyAutomation" automation}}
@ -75,14 +83,4 @@
{{/each}}
</tbody>
</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}}

View File

@ -1,52 +1,33 @@
<section class="discourse-automation-form new">
<form class="form-horizontal">
<FormError @error={{error}} />
<div class="control-group">
<label class="control-label">
{{i18n "discourse_automation.models.automation.name.label"}}
</label>
<div
class="admin-section-landing__header"
{{did-insert this.resetFilterText}}
>
<h2>{{i18n "discourse_automation.select_script"}}</h2>
<div class="controls">
<TextField
@value={{form.name}}
@type="text"
@autofocus="autofocus"
@name="automation-name"
class="input-large"
<input
type="text"
placeholder={{i18n "discourse_automation.filter_placeholder"}}
{{on "input" this.updateFilterText}}
class="admin-section-landing__header-filter"
/>
</div>
{{#unless this.model.scripts.length}}
<div class="alert alert-info">
<p>{{i18n "discourse_automation.no_automation_yet"}}</p>
</div>
{{/unless}}
<div class="control-group">
<label class="control-label">
{{i18n "discourse_automation.models.automation.script.label"}}
</label>
<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 class="admin-section-landing__wrapper">
{{#each this.scriptableContent as |script|}}
<AdminSectionLandingItem
{{on "click" (fn this.selectScriptToEdit script)}}
@titleLabelTranslated={{script.name}}
@descriptionLabelTranslated={{script.description}}
/>
</div>
{{/each}}
</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>

View File

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

View File

@ -30,9 +30,8 @@ module DiscourseAutomation
@running_in_background = true
end
MIN_NAME_LENGTH = 5
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)
if ![Topic, Post, User].any? { |m| target.is_a?(m) }

View File

@ -7,6 +7,46 @@
td[role="button"] {
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 {
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 {
margin: 0;
}

View File

@ -8,7 +8,8 @@ en:
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_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:
trigger_section:
forced: This trigger is forced by script.
@ -384,6 +385,7 @@ en:
fields:
custom_field_name:
label: "User Custom Field name"
unnamed_automation: "Unnamed automation"
models:
script:

View File

@ -99,6 +99,7 @@ en:
doc: Allows to send multiple pms to a user. Each PM accepts a delay.
suspend_user_by_email:
title: Suspend user by email
description: Automatically suspend an account based on email address
user_global_notice:
title: User global notice
description: Allows to display a global notice for a user
@ -135,6 +136,13 @@ en:
button_text: Done
add_user_to_group_through_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:
title: "Group Category Notification Default"
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.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)
expect(automation).to be_valid
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe "DiscourseAutomation | error", type: :system do
describe "DiscourseAutomation | error", type: :system, js: true do
fab!(:admin)
before do
@ -10,14 +10,13 @@ describe "DiscourseAutomation | error", type: :system do
context "when saving the form with an error" 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
fill_in("automation-name", with: "aaaaa")
select_kit = PageObjects::Components::SelectKit.new(".scriptables")
select_kit.expand
select_kit.select_row_by_value("post")
find(".create-automation").click
expect(page).to have_selector("input[name='automation-name']")
find('input[name="automation-name"]').set("aaaaa")
select_kit = PageObjects::Components::SelectKit.new(".triggerables")
select_kit.expand
select_kit.select_row_by_value("recurring")
@ -29,6 +28,8 @@ describe "DiscourseAutomation | error", type: :system do
{ name: "topic", target: "script", target_name: "post" },
),
)
expect(find('input[name="automation-name"]').value).to eq("aaaaa")
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 }
context "when the script is not selected" do
it "shows an error" do
new_automation_page.visit.fill_name("aaaaa").create
context "when a script is clicked" do
it "navigates to automation edit route" do
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

View File

@ -22,13 +22,9 @@ describe "DiscourseAutomation | smoke test", type: :system, js: true do
it "populate correctly" do
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")
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.expand
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
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")
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.expand
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(".update-automation").click
expect(page).to have_field("automation-name", with: "aaaaa")
expect(page).to have_css('[role="button"]', text: "aaaaa")
end
end