FEATURE: Merge discourse-automation (#26432)

Automation (previously known as discourse-automation) is now a core plugin.
This commit is contained in:
Osama Sayegh 2024-04-03 18:20:43 +03:00 committed by GitHub
parent 2190c9b957
commit 3d4faf3272
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
314 changed files with 21182 additions and 10 deletions

2
.gitignore vendored
View File

@ -40,6 +40,8 @@
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
!/plugins/discourse-lazy-videos/
!/plugins/automation/
/plugins/automation/gems
!/plugins/chat/
!/plugins/poll/
!/plugins/styleguide

View File

@ -19,9 +19,11 @@ end
#
# Indexes
#
# index_post_custom_fields_on_name_and_value (name, "left"(value, 200))
# index_post_custom_fields_on_notice (post_id) UNIQUE WHERE ((name)::text = 'notice'::text)
# index_post_custom_fields_on_post_id (post_id) UNIQUE WHERE ((name)::text = 'missing uploads'::text)
# index_post_custom_fields_on_post_id_and_name (post_id,name)
# index_post_id_where_missing_uploads_ignored (post_id) UNIQUE WHERE ((name)::text = 'missing uploads ignored'::text)
# idx_post_custom_fields_discourse_automation_unique_id_partial (post_id,value) UNIQUE WHERE ((name)::text = 'discourse_automation_ids'::text)
# index_post_custom_fields_on_name_and_value (name, "left"(value, 200))
# index_post_custom_fields_on_notice (post_id) UNIQUE WHERE ((name)::text = 'notice'::text)
# index_post_custom_fields_on_post_id (post_id) UNIQUE WHERE ((name)::text = 'missing uploads'::text)
# index_post_custom_fields_on_post_id_and_name (post_id,name)
# index_post_custom_fields_on_stalled_wiki_triggered_at (post_id) UNIQUE WHERE ((name)::text = 'stalled_wiki_triggered_at'::text)
# index_post_id_where_missing_uploads_ignored (post_id) UNIQUE WHERE ((name)::text = 'missing uploads ignored'::text)
#

View File

@ -19,6 +19,8 @@ end
#
# Indexes
#
# index_topic_custom_fields_on_topic_id_and_name (topic_id,name)
# topic_custom_fields_value_key_idx (value,name) WHERE ((value IS NOT NULL) AND (char_length(value) < 400))
# idx_topic_custom_fields_auto_responder_triggered_ids_partial (topic_id,value) UNIQUE WHERE ((name)::text = 'auto_responder_triggered_ids'::text)
# idx_topic_custom_fields_discourse_automation_unique_id_partial (topic_id,value) UNIQUE WHERE ((name)::text = 'discourse_automation_ids'::text)
# index_topic_custom_fields_on_topic_id_and_name (topic_id,name)
# topic_custom_fields_value_key_idx (value,name) WHERE ((value IS NOT NULL) AND (char_length(value) < 400))
#

View File

@ -26,5 +26,6 @@ end
#
# Indexes
#
# index_user_custom_fields_on_user_id_and_name (user_id,name)
# idx_user_custom_fields_discourse_automation_unique_id_partial (user_id,value) UNIQUE WHERE ((name)::text = 'discourse_automation_ids'::text)
# index_user_custom_fields_on_user_id_and_name (user_id,name)
#

View File

@ -16,7 +16,6 @@ class Plugin::Metadata
discourse-apple-auth
discourse-assign
discourse-auto-deactivate
discourse-automation
discourse-bbcode
discourse-bbcode-color
discourse-bcc
@ -91,6 +90,7 @@ class Plugin::Metadata
discourse-yearly-review
discourse-zendesk-plugin
discourse-zoom
automation
docker_manager
chat
poll

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
module DiscourseAutomation
class AdminAutomationsController < ::Admin::AdminController
requires_plugin DiscourseAutomation::PLUGIN_NAME
def index
automations = DiscourseAutomation::Automation.order(:name).all
serializer =
ActiveModel::ArraySerializer.new(
automations,
each_serializer: DiscourseAutomation::AutomationSerializer,
root: "automations",
).as_json
render_json_dump(serializer)
end
def show
automation = DiscourseAutomation::Automation.find(params[:id])
render_serialized_automation(automation)
end
def create
automation_params = params.require(:automation).permit(:name, :script, :trigger)
automation =
DiscourseAutomation::Automation.new(
automation_params.merge(last_updated_by_id: current_user.id),
)
if automation.scriptable.forced_triggerable
automation.trigger = scriptable.forced_triggerable[:triggerable].to_s
end
automation.save!
render_serialized_automation(automation)
end
def update
params.require(:automation)
automation = DiscourseAutomation::Automation.find(params[:id])
if automation.scriptable.forced_triggerable
params[:trigger] = automation.scriptable.forced_triggerable[:triggerable].to_s
end
attributes =
request.parameters[:automation].slice(:name, :id, :script, :trigger, :enabled).merge(
last_updated_by_id: current_user.id,
)
if automation.trigger != params[:automation][:trigger]
params[:automation][:fields] = []
attributes[:enabled] = false
automation.fields.destroy_all
end
if automation.script != params[:automation][:script]
attributes[:trigger] = nil
params[:automation][:fields] = []
attributes[:enabled] = false
automation.fields.destroy_all
automation.tap { |r| r.assign_attributes(attributes) }.save!(validate: false)
else
Array(params[:automation][:fields])
.reject(&:empty?)
.each do |field|
automation.upsert_field!(
field[:name],
field[:component],
field[:metadata],
target: field[:target],
)
end
automation.tap { |r| r.assign_attributes(attributes) }.save!
end
render_serialized_automation(automation)
end
def destroy
automation = DiscourseAutomation::Automation.find(params[:id])
automation.destroy!
render json: success_json
end
private
def render_serialized_automation(automation)
serializer =
DiscourseAutomation::AutomationSerializer.new(automation, root: "automation").as_json
render_json_dump(serializer)
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module DiscourseAutomation
class AdminController < ::Admin::AdminController
requires_plugin DiscourseAutomation::PLUGIN_NAME
def index
end
def new
end
def edit
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module DiscourseAutomation
class AdminScriptablesController < ::Admin::AdminController
requires_plugin DiscourseAutomation::PLUGIN_NAME
def index
scriptables =
DiscourseAutomation::Scriptable.all.map do |s|
id = s.to_s.gsub(/^__scriptable_/, "")
{
id: id,
name: I18n.t("discourse_automation.scriptables.#{id}.title"),
description: I18n.t("discourse_automation.scriptables.#{id}.description", default: ""),
doc: I18n.t("discourse_automation.scriptables.#{id}.doc", default: ""),
}
end
scriptables.sort_by! { |s| s[:name] }
render_json_dump(scriptables: scriptables)
end
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module DiscourseAutomation
class AdminTriggerablesController < ::Admin::AdminController
requires_plugin DiscourseAutomation::PLUGIN_NAME
def index
if params[:automation_id].present?
automation = DiscourseAutomation::Automation.find(params[:automation_id])
scriptable = automation.scriptable
triggerables = scriptable.triggerables
else
triggerables = DiscourseAutomation::Triggerable.all
end
triggerables =
triggerables.map do |s|
id = s.to_s.gsub(/^__triggerable_/, "")
{
id: id,
name: I18n.t("discourse_automation.triggerables.#{id}.title"),
description: I18n.t("discourse_automation.triggerables.#{id}.description", default: ""),
doc: I18n.t("discourse_automation.triggerables.#{id}.doc", default: ""),
}
end
render_json_dump(triggerables: triggerables)
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module DiscourseAutomation
class AppendLastCheckedByController < ApplicationController
requires_plugin DiscourseAutomation::PLUGIN_NAME
requires_login
def post_checked
post = Post.find(params[:post_id])
guardian.ensure_can_edit!(post)
topic = post.topic
raise Discourse::NotFound if topic.blank?
topic.custom_fields[DiscourseAutomation::TOPIC_LAST_CHECKED_BY] = current_user.username
topic.custom_fields[DiscourseAutomation::TOPIC_LAST_CHECKED_AT] = Time.zone.now.to_s
topic.save_custom_fields
post.rebake!
render json: success_json
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module DiscourseAutomation
class AutomationsController < ApplicationController
requires_plugin DiscourseAutomation::PLUGIN_NAME
before_action :ensure_admin
def trigger
automation = DiscourseAutomation::Automation.find(params[:id])
automation.trigger_in_background!(params.merge(kind: DiscourseAutomation::Triggers::API_CALL))
render json: success_json
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module DiscourseAutomation
class UserGlobalNoticesController < ApplicationController
requires_plugin DiscourseAutomation::PLUGIN_NAME
requires_login
def destroy
notice =
DiscourseAutomation::UserGlobalNotice.find_by(user_id: current_user.id, id: params[:id])
raise Discourse::NotFound unless notice
notice.destroy!
head :no_content
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Jobs
class DiscourseAutomationCallZapierWebhook < ::Jobs::Base
def execute(args)
RateLimiter.new(nil, "discourse_automation_call_zapier", 5, 30).performed!
result =
Excon.post(
args["webhook_url"],
body: args["context"].to_json,
headers: {
"Content-Type" => "application/json",
"Accept" => "application/json",
},
)
if result.status != 200
Rails.logger.warn(
"Failed to call Zapier webhook at #{args["webhook_url"]} Status: #{result.status}: #{result.status_line}",
)
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Jobs
class DiscourseAutomationTrigger < ::Jobs::Base
RETRY_TIMES = [5.minute, 15.minute, 120.minute]
sidekiq_options retry: RETRY_TIMES.size
sidekiq_retry_in do |count, exception|
# returning nil/0 will trigger the default sidekiq
# retry formula
#
# See https://github.com/mperham/sidekiq/blob/3330df0ee37cfd3e0cd3ef01e3e66b584b99d488/lib/sidekiq/job_retry.rb#L216-L234
case exception.wrapped
when SocketError
return RETRY_TIMES[count]
end
end
def execute(args)
automation = DiscourseAutomation::Automation.find_by(id: args[:automation_id], enabled: true)
return if !automation
context = DiscourseAutomation::Automation.deserialize_context(args[:context])
automation.running_in_background!
automation.trigger!(context)
end
end
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Jobs
class DiscourseAutomationTracker < ::Jobs::Scheduled
every 1.minute
BATCH_LIMIT ||= 300
def execute(_args = nil)
return unless SiteSetting.discourse_automation_enabled
DiscourseAutomation::PendingAutomation
.includes(:automation)
.limit(BATCH_LIMIT)
.where("execute_at < ?", Time.now)
.find_each { |pending_automation| run_pending_automation(pending_automation) }
DiscourseAutomation::PendingPm
.includes(:automation)
.limit(BATCH_LIMIT)
.where("execute_at < ?", Time.now)
.find_each { |pending_pm| send_pending_pm(pending_pm) }
end
def send_pending_pm(pending_pm)
DiscourseAutomation::Scriptable::Utils.send_pm(
pending_pm.attributes.slice("target_usernames", "title", "raw"),
sender: pending_pm.sender,
prefers_encrypt: pending_pm.prefers_encrypt,
)
pending_pm.destroy!
end
def run_pending_automation(pending_automation)
pending_automation.automation.trigger!(
"kind" => pending_automation.automation.trigger,
"execute_at" => pending_automation.execute_at,
)
pending_automation.destroy!
end
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Jobs
class StalledTopicTracker < ::Jobs::Scheduled
every 1.hour
def execute(_args = nil)
name = DiscourseAutomation::Triggers::STALLED_TOPIC
DiscourseAutomation::Automation
.where(trigger: name, enabled: true)
.find_each do |automation|
fields = automation.serialized_fields
stalled_after = fields.dig("stalled_after", "value")
stalled_duration = ISO8601::Duration.new(stalled_after).to_seconds
stalled_date = stalled_duration.seconds.ago
categories = fields.dig("categories", "value")
tags = fields.dig("tags", "value")
StalledTopicFinder
.call(stalled_date, categories: categories, tags: tags)
.each do |result|
topic = Topic.find_by(id: result.id)
next unless topic
run_trigger(automation, topic)
end
end
end
def run_trigger(automation, topic)
automation.trigger!(
"kind" => DiscourseAutomation::Triggers::STALLED_TOPIC,
"topic" => topic,
"placeholders" => {
"topic_url" => topic.url,
},
)
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module Jobs
class StalledWikiTracker < ::Jobs::Scheduled
every 10.minutes
def execute(_args = nil)
name = DiscourseAutomation::Triggers::STALLED_WIKI
DiscourseAutomation::Automation
.where(trigger: name, enabled: true)
.find_each do |automation|
stalled_after = automation.trigger_field("stalled_after")
stalled_duration = ISO8601::Duration.new(stalled_after["value"]).to_seconds
finder = Post.where("wiki = TRUE AND last_version_at <= ?", stalled_duration.seconds.ago)
restricted_category = automation.trigger_field("restricted_category")
if restricted_category["value"]
finder =
finder.joins(:topic).where("topics.category_id = ?", restricted_category["value"])
end
finder.each do |post|
last_trigger_date = post.custom_fields["stalled_wiki_triggered_at"]
if last_trigger_date
retriggered_after = automation.trigger_field("retriggered_after")
retrigger_duration = ISO8601::Duration.new(retriggered_after["value"]).to_seconds
next if Time.parse(last_trigger_date) + retrigger_duration >= Time.zone.now
end
post.upsert_custom_fields(stalled_wiki_triggered_at: Time.zone.now)
run_trigger(automation, post)
end
end
end
def run_trigger(automation, post)
user_ids =
(
post.post_revisions.order("post_revisions.created_at DESC").limit(5).pluck(:user_id) +
[post.user_id]
).compact.uniq
automation.trigger!(
"kind" => DiscourseAutomation::Triggers::STALLED_WIKI,
"post" => post,
"topic" => post.topic,
"usernames" => User.where(id: user_ids).pluck(:username),
"placeholders" => {
"wiki_url" => Discourse.base_url + post.url,
},
)
end
end
end

View File

@ -0,0 +1,172 @@
# frozen_string_literal: true
module DiscourseAutomation
class Automation < ActiveRecord::Base
self.table_name = "discourse_automation_automations"
has_many :fields,
class_name: "DiscourseAutomation::Field",
dependent: :delete_all,
foreign_key: "automation_id"
has_many :pending_automations,
class_name: "DiscourseAutomation::PendingAutomation",
dependent: :delete_all,
foreign_key: "automation_id"
has_many :pending_pms,
class_name: "DiscourseAutomation::PendingPm",
dependent: :delete_all,
foreign_key: "automation_id"
validates :script, presence: true
validate :validate_trigger_fields
attr_accessor :running_in_background
def running_in_background!
@running_in_background = true
end
MIN_NAME_LENGTH = 5
MAX_NAME_LENGTH = 30
validates :name, length: { in: MIN_NAME_LENGTH..MAX_NAME_LENGTH }
def attach_custom_field(target)
if ![Topic, Post, User].any? { |m| target.is_a?(m) }
raise "Expected an instance of Topic/Post/User."
end
now = Time.now
fk = target.custom_fields_fk
row = {
fk => target.id,
:name => DiscourseAutomation::CUSTOM_FIELD,
:value => id,
:created_at => now,
:updated_at => now,
}
relation = "#{target.class.name}CustomField".constantize
relation.upsert(
row,
unique_by:
"idx_#{target.class.name.downcase}_custom_fields_discourse_automation_unique_id_partial",
)
end
def detach_custom_field(target)
if ![Topic, Post, User].any? { |m| target.is_a?(m) }
raise "Expected an instance of Topic/Post/User."
end
fk = target.custom_fields_fk
relation = "#{target.class.name}CustomField".constantize
relation.where(
fk => target.id,
:name => DiscourseAutomation::CUSTOM_FIELD,
:value => id,
).delete_all
end
def trigger_field(name)
field = fields.find_by(target: "trigger", name: name)
field ? field.metadata : {}
end
def has_trigger_field?(name)
!!fields.find_by(target: "trigger", name: name)
end
def script_field(name)
field = fields.find_by(target: "script", name: name)
field ? field.metadata : {}
end
def upsert_field!(name, component, metadata, target: "script")
field = fields.find_or_initialize_by(name: name, component: component, target: target)
field.update!(metadata: metadata)
end
def self.deserialize_context(context)
new_context = ActiveSupport::HashWithIndifferentAccess.new
context.each do |key, value|
if key.start_with?("_serialized_")
new_key = key[12..-1]
found = nil
if value["class"] == "Symbol"
found = value["value"].to_sym
else
found = value["class"].constantize.find_by(id: value["id"])
end
new_context[new_key] = found
else
new_context[key] = value
end
end
new_context
end
def self.serialize_context(context)
new_context = {}
context.each do |k, v|
if v.is_a?(Symbol)
new_context["_serialized_#{k}"] = { "class" => "Symbol", "value" => v.to_s }
elsif v.is_a?(ActiveRecord::Base)
new_context["_serialized_#{k}"] = { "class" => v.class.name, "id" => v.id }
else
new_context[k] = v
end
end
new_context
end
def trigger_in_background!(context = {})
Jobs.enqueue(
:discourse_automation_trigger,
automation_id: id,
context: self.class.serialize_context(context),
)
end
def trigger!(context = {})
if enabled
if scriptable.background && !running_in_background
trigger_in_background!(context)
else
triggerable&.on_call&.call(self, serialized_fields)
scriptable.script.call(context, serialized_fields, self)
end
end
end
def triggerable
trigger && @triggerable ||= DiscourseAutomation::Triggerable.new(trigger, self)
end
def scriptable
script && @scriptable ||= DiscourseAutomation::Scriptable.new(script, self)
end
def serialized_fields
fields
&.pluck(:name, :metadata)
&.reduce({}) do |acc, hash|
name, field = hash
acc[name] = field
acc
end || {}
end
def reset!
pending_automations.delete_all
pending_pms.delete_all
scriptable&.on_reset&.call(self)
end
private
def validate_trigger_fields
!triggerable || triggerable.valid?(self)
end
end
end

View File

@ -0,0 +1,246 @@
# frozen_string_literal: true
module DiscourseAutomation
class Field < ActiveRecord::Base
self.table_name = "discourse_automation_fields"
belongs_to :automation, class_name: "DiscourseAutomation::Automation"
around_save :on_update_callback
def on_update_callback
previous_fields = automation.serialized_fields
automation.reset!
yield
automation&.triggerable&.on_update&.call(
automation,
automation.serialized_fields,
previous_fields,
)
end
validate :required_field
def required_field
if template && template[:required] && metadata && metadata["value"].blank?
raise_required_field(name, target, targetable)
end
end
validate :validator
def validator
if template && template[:validator]
error = template[:validator].call(metadata["value"])
errors.add(:base, error) if error
end
end
def targetable
target == "trigger" ? automation.triggerable : automation.scriptable
end
def template
targetable&.fields&.find do |tf|
targetable.id == target && tf[:name].to_s == name && tf[:component].to_s == component
end
end
validate :metadata_schema
def metadata_schema
if !(targetable.components.include?(component.to_sym))
errors.add(
:base,
I18n.t(
"discourse_automation.models.fields.invalid_field",
component: component,
target: target,
target_name: targetable.name,
),
)
else
schema = SCHEMAS[component]
if !schema ||
!JSONSchemer.schema({ "type" => "object", "properties" => schema }).valid?(metadata)
errors.add(
:base,
I18n.t(
"discourse_automation.models.fields.invalid_metadata",
component: component,
field: name,
),
)
end
end
end
SCHEMAS = {
"key-value" => {
"type" => "array",
"uniqueItems" => true,
"items" => {
"type" => "object",
"title" => "group",
"properties" => {
"key" => {
"type" => "string",
},
"value" => {
"type" => "string",
},
},
},
},
"choices" => {
"value" => {
"type" => %w[string integer null],
},
},
"tags" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"trust-levels" => {
"value" => {
"type" => "array",
"items" => [{ type: "integer" }],
},
},
"categories" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"category" => {
"value" => {
"type" => %w[string integer null],
},
},
"category_notification_level" => {
"value" => {
"type" => "integer",
},
},
"custom_field" => {
"value" => {
"type" => "integer",
},
},
"custom_fields" => {
"value" => {
"type" => [{ type: "string" }],
},
},
"user" => {
"value" => {
"type" => "string",
},
},
"user_profile" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"users" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"text" => {
"value" => {
"type" => %w[string integer null],
},
},
"post" => {
"value" => {
"type" => %w[string integer null],
},
},
"message" => {
"value" => {
"type" => %w[string integer null],
},
},
"boolean" => {
"value" => {
"type" => ["boolean"],
},
},
"text_list" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"date_time" => {
"value" => {
"type" => "string",
},
},
"group" => {
"value" => {
"type" => "integer",
},
},
"email_group_user" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"pms" => {
type: "array",
items: [
{
type: "object",
properties: {
"raw" => {
"type" => "string",
},
"title" => {
"type" => "string",
},
"delay" => {
"type" => "integer",
},
"prefers_encrypt" => {
"type" => "boolean",
},
},
},
],
},
"period" => {
"type" => "object",
"properties" => {
"interval" => {
"type" => "integer",
},
"frequency" => {
"type" => "string",
},
},
},
}
private
def raise_required_field(name, target, targetable)
errors.add(
:base,
I18n.t(
"discourse_automation.models.fields.required_field",
name: name,
target: target,
target_name: targetable.name,
),
)
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module DiscourseAutomation
class PendingAutomation < ActiveRecord::Base
self.table_name = "discourse_automation_pending_automations"
belongs_to :automation, class_name: "DiscourseAutomation::Automation"
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module DiscourseAutomation
class PendingPm < ActiveRecord::Base
self.table_name = "discourse_automation_pending_pms"
belongs_to :automation, class_name: "DiscourseAutomation::Automation"
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module DiscourseAutomation
class UserGlobalNotice < ActiveRecord::Base
self.table_name = "discourse_automation_user_global_notices"
belongs_to :user
end
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
class StalledTopicFinder
def self.call(stalled_date, tags: nil, categories: nil)
sql = <<~SQL
SELECT t.id
FROM topics t
SQL
sql += <<~SQL if tags
JOIN topic_tags ON topic_tags.topic_id = t.id
JOIN tags
ON tags.name IN (:tags)
AND tags.id = topic_tags.tag_id
SQL
sql += <<~SQL
WHERE t.deleted_at IS NULL
AND t.posts_count > 0
AND t.archetype != 'private_message'
AND NOT t.closed
AND NOT t.archived
AND NOT EXISTS (
SELECT p.id
FROM posts p
WHERE t.id = p.topic_id
AND p.deleted_at IS NULL
AND t.user_id = p.user_id
AND p.created_at > :stalled_date
LIMIT 1
)
SQL
sql += <<~SQL if categories
AND t.category_id IN (:categories)
SQL
sql += <<~SQL
LIMIT 250
SQL
DB.query(sql, categories: categories, tags: tags, stalled_date: stalled_date)
end
end

View File

@ -0,0 +1,120 @@
# frozen_string_literal: true
module DiscourseAutomation
class AutomationSerializer < ApplicationSerializer
attributes :id
attributes :name
attributes :enabled
attributes :script
attributes :trigger
attributes :updated_at
attributes :last_updated_by
attributes :next_pending_automation_at
attributes :placeholders
def last_updated_by
BasicUserSerializer.new(
User.find_by(id: object.last_updated_by_id) || Discourse.system_user,
root: false,
).as_json
end
def include_next_pending_automation_at?
object.pending_automations.exists?
end
def next_pending_automation_at
object&.pending_automations&.first&.execute_at
end
def placeholders
scriptable_placeholders =
DiscourseAutomation
.filter_by_trigger(scriptable&.placeholders || [], object.trigger)
.map { |placeholder| placeholder[:name] }
triggerable_placeholders = triggerable&.placeholders || []
(scriptable_placeholders + triggerable_placeholders).map do |placeholder|
placeholder.to_s.gsub(/\s+/, "_").underscore
end
end
def script
key = "discourse_automation.scriptables"
doc_key = "#{key}.#{object.script}.doc"
script_with_trigger_key = "#{key}.#{object.script}_with_#{object.trigger}.doc"
{
id: object.script,
version: scriptable.version,
name: I18n.t("#{key}.#{object.script}.title"),
description: I18n.t("#{key}.#{object.script}.description"),
doc: I18n.exists?(doc_key, :en) ? I18n.t(doc_key) : nil,
with_trigger_doc:
I18n.exists?(script_with_trigger_key, :en) ? I18n.t(script_with_trigger_key) : nil,
forced_triggerable: scriptable.forced_triggerable,
not_found: scriptable.not_found,
templates:
process_templates(filter_fields_with_priority(scriptable.fields, object.trigger&.to_sym)),
fields: process_fields(object.fields.where(target: "script")),
}
end
def trigger
key = "discourse_automation.triggerables"
doc_key = "#{key}.#{object.trigger}.doc"
{
id: object.trigger,
name: I18n.t("#{key}.#{object.trigger}.title"),
description: I18n.t("#{key}.#{object.trigger}.description"),
doc: I18n.exists?(doc_key, :en) ? I18n.t(doc_key) : nil,
not_found: triggerable&.not_found,
templates: process_templates(triggerable&.fields || []),
fields: process_fields(object.fields.where(target: "trigger")),
settings: triggerable&.settings,
}
end
private
def filter_fields_with_priority(arr, trigger)
unique_with_priority = {}
arr.each do |item|
name = item[:name]
if (item[:triggerable]&.to_sym == trigger&.to_sym || item[:triggerable].nil?) &&
(!unique_with_priority.key?(name) || unique_with_priority[name][:triggerable].nil?)
unique_with_priority[name] = item
end
end
unique_with_priority.values
end
def process_templates(fields)
ActiveModel::ArraySerializer.new(
fields,
each_serializer: DiscourseAutomation::TemplateSerializer,
scope: {
automation: object,
},
).as_json
end
def process_fields(fields)
ActiveModel::ArraySerializer.new(
fields || [],
each_serializer: DiscourseAutomation::FieldSerializer,
).as_json || []
end
def scriptable
object.scriptable
end
def triggerable
object.triggerable
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module DiscourseAutomation
class FieldSerializer < ApplicationSerializer
attributes :id, :component, :name, :metadata, :is_required
def metadata
object.metadata || {}
end
def is_required
object.template&.dig(:required)
end
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module DiscourseAutomation
class TemplateSerializer < ApplicationSerializer
attributes :name,
:component,
:extra,
:accepts_placeholders,
:accepted_contexts,
:read_only,
:default_value,
:is_required
def default_value
scope[:automation].scriptable&.forced_triggerable&.dig(:state, name) || object[:default_value]
end
def read_only
scope[:automation].scriptable&.forced_triggerable&.dig(:state, name).present?
end
def name
object[:name]
end
def component
object[:component]
end
def extra
object[:extra]
end
def accepts_placeholders
object[:accepts_placeholders]
end
def accepted_contexts
object[:accepted_contexts]
end
def is_required
object[:required]
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module DiscourseAutomation
class TriggerSerializer < ApplicationSerializer
attributes :id, :name, :metadata
def metadata
((options[:trigger_metadata] || {}).stringify_keys).merge(object.metadata || {})
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module DiscourseAutomation
class UserGlobalNoticeSerializer < ApplicationSerializer
attributes :id, :notice, :level, :created_at, :updated_at, :identifier
def level
object.level || "info"
end
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module DiscourseAutomation
class UserBadgeGrantedHandler
def self.handle(automation, badge_id, user_id)
tracked_badge_id = automation.trigger_field("badge")["value"]
return if tracked_badge_id != badge_id
badge = Badge.find(badge_id)
only_first_grant = automation.trigger_field("only_first_grant")["value"]
return if only_first_grant && UserBadge.where(user_id: user_id, badge_id: badge_id).count > 1
user = User.find(user_id)
automation.trigger!(
"kind" => DiscourseAutomation::Triggers::USER_BADGE_GRANTED,
"usernames" => [user.username],
"badge" => badge,
"placeholders" => {
"badge_name" => badge.name,
"grant_count" => badge.grant_count,
},
)
end
end
end

View File

@ -0,0 +1,11 @@
import RestAdapter from "discourse/adapters/rest";
export default class Adapter extends RestAdapter {
basePath() {
return "/admin/plugins/discourse-automation/";
}
pathFor() {
return super.pathFor(...arguments).replace("_", "-") + ".json";
}
}

View File

@ -0,0 +1,9 @@
import DiscourseAutomationAdapter from "./discourse-automation-adapter";
export default class AutomationAdapter extends DiscourseAutomationAdapter {
jsonMode = true;
apiNameFor() {
return "automation";
}
}

View File

@ -0,0 +1,9 @@
import DiscourseAutomationAdapter from "./discourse-automation-adapter";
export default class ScriptableAdapter extends DiscourseAutomationAdapter {
jsonMode = true;
apiNameFor() {
return "scriptable";
}
}

View File

@ -0,0 +1,9 @@
import DiscourseAutomationAdapter from "./discourse-automation-adapter";
export default class TriggerableAdapter extends DiscourseAutomationAdapter {
jsonMode = true;
apiNameFor() {
return "triggerable";
}
}

View File

@ -0,0 +1,23 @@
import RestModel from "discourse/models/rest";
const ATTRIBUTES = ["name", "script", "fields", "trigger", "id"];
export default class Automation extends RestModel {
updateProperties() {
return {
id: this.id,
name: this.name,
fields: this.fields,
script: this.script.id,
trigger: {
id: this.trigger.id,
name: this.trigger.name,
metadata: this.trigger.metadata,
},
};
}
createProperties() {
return this.getProperties(ATTRIBUTES);
}
}

View File

@ -0,0 +1,52 @@
import { tracked } from "@glimmer/tracking";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
export default class DiscourseAutomationField {
static create(template, target, json = {}) {
const field = new DiscourseAutomationField();
field.acceptsPlaceholders = template.accepts_placeholders;
field.acceptedContexts = template.accepted_contexts;
field.targetName = target.name;
field.targetType = target.type;
field.name = template.name;
field.component = template.component;
field.isDisabled = template.read_only;
// backwards compatibility with forced scriptable fields
if (field.isDisabled) {
field.metadata.value =
template.default_value || template.value || json?.metadata?.value;
} else {
field.metadata.value =
template.value || json?.metadata?.value || template.default_value;
}
// null is not a valid value for metadata.value
if (field.metadata.value === null) {
field.metadata.value = undefined;
}
field.isRequired = template.is_required;
field.extra = new TrackedObject(template.extra);
return field;
}
@tracked acceptsPlaceholders = false;
@tracked component;
@tracked extra = new TrackedObject();
@tracked isDisabled = false;
@tracked isRequired = false;
@tracked metadata = new TrackedObject();
@tracked name;
@tracked targetType;
@tracked targetName;
toJSON() {
return {
name: this.name,
target: this.targetType,
component: this.component,
metadata: this.metadata,
};
}
}

View File

@ -0,0 +1,3 @@
import RestModel from "discourse/models/rest";
export default class Script extends RestModel {}

View File

@ -0,0 +1,97 @@
import Component from "@glimmer/component";
import I18n from "I18n";
import DaBooleanField from "./fields/da-boolean-field";
import DaCategoriesField from "./fields/da-categories-field";
import DaCategoryField from "./fields/da-category-field";
import DaCategoryNotificationlevelField from "./fields/da-category-notification-level-field";
import DaChoicesField from "./fields/da-choices-field";
import DaCustomField from "./fields/da-custom-field";
import DaCustomFields from "./fields/da-custom-fields";
import DaDateTimeField from "./fields/da-date-time-field";
import DaEmailGroupUserField from "./fields/da-email-group-user-field";
import DaGroupField from "./fields/da-group-field";
import DaKeyValueField from "./fields/da-key-value-field";
import DaMessageField from "./fields/da-message-field";
import DaPeriodField from "./fields/da-period-field";
import DaPmsField from "./fields/da-pms-field";
import DaPostField from "./fields/da-post-field";
import DaTagsField from "./fields/da-tags-field";
import DaTextField from "./fields/da-text-field";
import DaTextListField from "./fields/da-text-list-field";
import DaTrustLevelsField from "./fields/da-trust-levels-field";
import DaUserField from "./fields/da-user-field";
import DaUserProfileField from "./fields/da-user-profile-field";
import DaUsersField from "./fields/da-users-field";
const FIELD_COMPONENTS = {
period: DaPeriodField,
date_time: DaDateTimeField,
text_list: DaTextListField,
pms: DaPmsField,
text: DaTextField,
message: DaMessageField,
categories: DaCategoriesField,
user: DaUserField,
users: DaUsersField,
user_profile: DaUserProfileField,
post: DaPostField,
tags: DaTagsField,
"key-value": DaKeyValueField,
boolean: DaBooleanField,
"trust-levels": DaTrustLevelsField,
category: DaCategoryField,
group: DaGroupField,
choices: DaChoicesField,
category_notification_level: DaCategoryNotificationlevelField,
email_group_user: DaEmailGroupUserField,
custom_field: DaCustomField,
custom_fields: DaCustomFields,
};
export default class AutomationField extends Component {
<template>
{{#if this.displayField}}
<this.component
@field={{@field}}
@placeholders={{@automation.placeholders}}
@label={{this.label}}
@description={{this.description}}
@saveAutomation={{@saveAutomation}}
/>
{{/if}}
</template>
get component() {
return FIELD_COMPONENTS[this.args.field.component];
}
get label() {
return I18n.t(
`discourse_automation${this.target}fields.${this.args.field.name}.label`
);
}
get displayField() {
const triggerId = this.args.automation?.trigger?.id;
const triggerable = this.args.field?.triggerable;
return triggerId && (!triggerable || triggerable === triggerId);
}
get placeholdersString() {
return this.args.field.placeholders.join(", ");
}
get target() {
return this.args.field.targetType === "script"
? `.scriptables.${this.args.automation.script.id.replace(/-/g, "_")}.`
: `.triggerables.${this.args.automation.trigger.id.replace(/-/g, "_")}.`;
}
get translationKey() {
return `discourse_automation${this.target}fields.${this.args.field.name}.description`;
}
get description() {
return I18n.lookup(this.translationKey);
}
}

View File

@ -0,0 +1,15 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
export default class BaseField extends Component {
get displayPlaceholders() {
return (
this.args.placeholders?.length && this.args.field?.acceptsPlaceholders
);
}
@action
mutValue(newValue) {
this.args.field.metadata.value = newValue;
}
}

View File

@ -0,0 +1,32 @@
import { Input } from "@ember/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class BooleanField extends BaseField {
<template>
<section class="field boolean-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<Input
@type="checkbox"
@checked={{@field.metadata.value}}
{{on "input" this.onInput}}
disabled={{@field.isDisabled}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
@action
onInput(event) {
this.mutValue(event.target.checked);
}
}

View File

@ -0,0 +1,38 @@
import { fn, hash } from "@ember/helper";
import { action } from "@ember/object";
import Category from "discourse/models/category";
import CategorySelector from "select-kit/components/category-selector";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class CategoriesField extends BaseField {
<template>
{{! template-lint-disable no-redundant-fn }}
<section class="field categories-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<CategorySelector
@categories={{this.categories}}
@onChange={{fn this.onChangeCategories}}
@options={{hash clearable=true disabled=@field.isDisabled}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
get categories() {
const ids = this.args.field?.metadata?.value || [];
return ids.map((id) => Category.findById(id)).filter(Boolean);
}
@action
onChangeCategories(categories) {
this.mutValue(categories.mapBy("id"));
}
}

View File

@ -0,0 +1,25 @@
import { hash } from "@ember/helper";
import CategoryChooser from "select-kit/components/category-chooser";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class CategoryField extends BaseField {
<template>
<section class="field category-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<CategoryChooser
@value={{@field.metadata.value}}
@onChange={{this.mutValue}}
@options={{hash clearable=true disabled=@field.isDisabled}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
}

View File

@ -0,0 +1,25 @@
import { hash } from "@ember/helper";
import CategoryNotificationsButton from "select-kit/components/category-notifications-button";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class CategoryNotficationLevelField extends BaseField {
<template>
<section class="field category-notification-level-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<CategoryNotificationsButton
@value={{@field.metadata.value}}
@onChange={{this.mutValue}}
@options={{hash showFullTitle=true}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
}

View File

@ -0,0 +1,38 @@
import { hash } from "@ember/helper";
import I18n from "I18n";
import ComboBox from "select-kit/components/combo-box";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class ChoicesField extends BaseField {
<template>
<div class="field control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<ComboBox
@value={{@field.metadata.value}}
@content={{this.replacedContent}}
@onChange={{this.mutValue}}
@options={{hash
allowAny=false
clearable=true
disabled=@field.isDisabled
}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</template>
get replacedContent() {
return (this.args.field.extra.content || []).map((r) => {
return {
id: r.id,
name: r.translated_name || I18n.t(r.name),
};
});
}
}

View File

@ -0,0 +1,37 @@
import { tracked } from "@glimmer/tracking";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service";
import { bind } from "discourse-common/utils/decorators";
import ComboBox from "select-kit/components/combo-box";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class GroupField extends BaseField {
@service store;
@tracked allCustomFields = [];
<template>
<section class="field group-field" {{didInsert this.loadUserFields}}>
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<ComboBox
@content={{this.allCustomFields}}
@value={{@field.metadata.value}}
@onChange={{this.mutValue}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
@bind
loadUserFields() {
this.store.findAll("user-field").then((fields) => {
this.allCustomFields = fields.content;
});
}
}

View File

@ -0,0 +1,41 @@
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service";
import { bind } from "discourse-common/utils/decorators";
import MultiSelect from "select-kit/components/multi-select";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class GroupField extends BaseField {
@service store;
@tracked allCustomFields = [];
<template>
<section class="field group-field" {{didInsert this.loadUserFields}}>
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<MultiSelect
@value={{@field.metadata.value}}
@content={{this.allCustomFields}}
@onChange={{this.mutValue}}
@nameProperty={{null}}
@valueProperty={{null}}
@options={{hash allowAny=false disabled=@field.isDisabled}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
@bind
loadUserFields() {
this.store.findAll("user-field").then((fields) => {
this.allCustomFields = fields.content.map((field) => field.name);
});
}
}

View File

@ -0,0 +1,62 @@
import { Input } from "@ember/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class DateTimeField extends BaseField {
<template>
<section class="field date-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<div class="controls-row">
<Input
@type="datetime-local"
@value={{readonly this.localTime}}
disabled={{@field.isDisabled}}
{{on "input" this.convertToUniversalTime}}
/>
{{#if @field.metadata.value}}
<DButton
@icon="trash-alt"
@action={{this.reset}}
@disabled={{@field.isDisabled}}
/>
{{/if}}
</div>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
@action
convertToUniversalTime(event) {
const date = event.target.value;
if (!date) {
return;
}
this.mutValue(moment(date).utc().format());
}
@action
reset() {
this.mutValue(null);
}
get localTime() {
return (
this.args.field.metadata.value &&
moment(this.args.field.metadata.value)
.local()
.format(moment.HTML5_FMT.DATETIME_LOCAL)
);
}
}

View File

@ -0,0 +1,61 @@
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import EmailGroupUserChooser from "select-kit/components/email-group-user-chooser";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class EmailGroupUserField extends BaseField {
@tracked recipients;
@tracked groups = [];
<template>
<section class="field email-group-user-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<EmailGroupUserChooser
@value={{@field.metadata.value}}
@onChange={{this.mutValue}}
@options={{hash
includeGroups=true
includeMessageableGroups=true
allowEmails=true
autoWrap=true
disabled=@field.isDisabled
}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
@action
updateRecipients(selected, content) {
const newGroups = content.filterBy("isGroup").mapBy("id");
this._updateGroups(selected, newGroups);
this.recipients = selected.join(",");
}
_updateGroups(selected, newGroups) {
const groups = [];
this.groups.forEach((existing) => {
if (selected.includes(existing)) {
groups.addObject(existing);
}
});
newGroups.forEach((newGroup) => {
if (!groups.includes(newGroup)) {
groups.addObject(newGroup);
}
});
this.groups = groups;
}
}

View File

@ -0,0 +1,9 @@
const FieldDescription = <template>
{{#if @description}}
<p class="control-description">
{{@description}}
</p>
{{/if}}
</template>;
export default FieldDescription;

View File

@ -0,0 +1,14 @@
const FieldLabel = <template>
{{#if @label}}
<label class="control-label">
<span>
{{@label}}
{{#if @field.isRequired}}
*
{{/if}}
</span>
</label>
{{/if}}
</template>;
export default FieldLabel;

View File

@ -0,0 +1,51 @@
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import Group from "discourse/models/group";
import GroupChooser from "select-kit/components/group-chooser";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class GroupField extends BaseField {
@tracked allGroups = [];
<template>
<section class="field group-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<GroupChooser
@content={{this.allGroups}}
@value={{@field.metadata.value}}
@labelProperty="name"
@onChange={{this.setGroupField}}
@options={{hash maximum=1 disabled=@field.isDisabled}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
constructor() {
super(...arguments);
Group.findAll({
ignore_automatic: this.args.field.extra.ignore_automatic ?? false,
}).then((groups) => {
if (this.isDestroying || this.isDestroyed) {
return;
}
this.allGroups = groups;
});
}
@action
setGroupField(groupIds) {
this.mutValue(groupIds?.firstObject);
}
}

View File

@ -0,0 +1,104 @@
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import ModalJsonSchemaEditor from "discourse/components/modal/json-schema-editor";
import I18n from "I18n";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class KeyValueField extends BaseField {
@tracked showJsonEditorModal = false;
jsonSchema = {
type: "array",
uniqueItems: true,
items: {
type: "object",
title: "group",
properties: {
key: {
type: "string",
},
value: {
type: "string",
format: "textarea",
},
},
},
};
<template>
<section class="field key-value-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<DButton class="configure-btn" @action={{this.openModal}}>
{{this.showJsonModalLabel}}
</DButton>
{{#if this.showJsonEditorModal}}
<ModalJsonSchemaEditor
@model={{hash
value=this.value
updateValue=this.handleValueChange
settingName=@label
jsonSchema=this.jsonSchema
}}
@closeModal={{this.closeModal}}
/>
{{/if}}
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
get value() {
return (
this.args.field.metadata.value ||
'[{"key":"example","value":"You posted {{key}}"}]'
);
}
get keyCount() {
if (this.args.field.metadata.value) {
return JSON.parse(this.value).length;
}
return 0;
}
get showJsonModalLabel() {
if (this.keyCount === 0) {
return I18n.t(
"discourse_automation.fields.key_value.label_without_count"
);
} else {
return I18n.t("discourse_automation.fields.key_value.label_with_count", {
count: this.keyCount,
});
}
}
@action
handleValueChange(value) {
if (value !== this.args.field.metadata.value) {
this.mutValue(value);
this.args.saveAutomation();
}
}
@action
openModal() {
this.showJsonEditorModal = true;
}
@action
closeModal() {
this.showJsonEditorModal = false;
}
}

View File

@ -0,0 +1,41 @@
import { TextArea } from "@ember/legacy-built-in-components";
import { action } from "@ember/object";
import PlaceholdersList from "../placeholders-list";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class MessageField extends BaseField {
<template>
<section class="field message-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<div class="field-wrapper">
<TextArea
@value={{@field.metadata.value}}
@input={{this.updateValue}}
@disabled={{@field.isDisabled}}
/>
<DAFieldDescription @description={{@description}} />
{{#if this.displayPlaceholders}}
<PlaceholdersList
@currentValue={{@field.metadata.value}}
@placeholders={{@placeholder}}
@onCopy={{this.test}}
/>
{{/if}}
</div>
</div>
</div>
</section>
</template>
@action
updateValue(event) {
this.mutValue(event.target.value);
}
}

View File

@ -0,0 +1,86 @@
import { tracked } from "@glimmer/tracking";
import { Input } from "@ember/component";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import I18n from "I18n";
import ComboBox from "select-kit/components/combo-box";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class PeriodField extends BaseField {
@tracked interval = 1;
@tracked frequency = null;
constructor() {
super(...arguments);
next(() => {
if (!this.args.field.metadata.value) {
this.args.field.metadata.value = new TrackedObject({
interval: 1,
frequency: null,
});
}
this.interval = this.args.field.metadata.value.interval;
this.frequency = this.args.field.metadata.value.frequency;
});
}
get recurringLabel() {
return I18n.t("discourse_automation.triggerables.recurring.every");
}
get replacedContent() {
return (this.args.field?.extra?.content || []).map((r) => {
return {
id: r.id,
name: I18n.t(r.name),
};
});
}
@action
mutInterval(event) {
this.args.field.metadata.value.interval = event.target.value;
}
@action
mutFrequency(value) {
this.args.field.metadata.value.frequency = value;
this.frequency = value;
}
<template>
<div class="field period-field control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
{{this.recurringLabel}}
<Input
@type="number"
defaultValue="1"
@value={{this.interval}}
disabled={{@field.isDisabled}}
required={{@field.isRequired}}
{{on "input" this.mutInterval}}
/>
<ComboBox
@value={{this.frequency}}
@content={{this.replacedContent}}
@onChange={{this.mutFrequency}}
@options={{hash allowAny=false disabled=@field.isDisabled}}
@required={{@field.isRequired}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</template>
}

View File

@ -0,0 +1,199 @@
import { Input } from "@ember/component";
import { concat, fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { TrackedArray, TrackedObject } from "@ember-compat/tracked-built-ins";
import DButton from "discourse/components/d-button";
import DEditor from "discourse/components/d-editor";
import I18n from "I18n";
import PlaceholdersList from "../placeholders-list";
import BaseField from "./da-base-field";
import DAFieldLabel from "./da-field-label";
export default class PmsField extends BaseField {
@service dialog;
noPmCreatedLabel = I18n.t("discourse_automation.fields.pms.no_pm_created");
prefersEncryptLabel = I18n.t(
"discourse_automation.fields.pms.prefers_encrypt.label"
);
delayLabel = I18n.t("discourse_automation.fields.pms.delay.label");
pmTitleLabel = I18n.t("discourse_automation.fields.pms.title.label");
rawLabel = I18n.t("discourse_automation.fields.pms.raw.label");
<template>
<section class="field pms-field">
{{#if @field.metadata.value.length}}
<section class="actions header">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<DButton
@icon="plus"
@action={{this.insertPM}}
class="btn-primary insert-pm"
/>
</section>
{{/if}}
{{#each @field.metadata.value as |pm|}}
<div class="pm-field">
<div class="control-group">
<DAFieldLabel @label={{this.pmTitleLabel}} @field={{@field}} />
<div class="controls">
<div class="field-wrapper">
<Input
id={{concat @field.targetType @field.name "title"}}
@value={{pm.title}}
class="pm-input pm-title"
{{on "input" (fn this.mutPmTitle pm)}}
disabled={{@field.isDisabled}}
name={{@field.name}}
/>
{{#if this.displayPlaceholders}}
<PlaceholdersList
@currentValue={{pm.title}}
@placeholders={{@placeholders}}
@onCopy={{fn this.updatePmTitle pm}}
/>
{{/if}}
</div>
</div>
</div>
<div class="control-group">
<DAFieldLabel @label={{this.rawLabel}} @field={{@field}} />
<div class="controls">
<div class="field-wrapper">
<DEditor @value={{pm.raw}} />
{{#if this.displayPlaceholders}}
<PlaceholdersList
@currentValue={{pm.raw}}
@placeholders={{@placeholders}}
@onCopy={{fn this.updatePmRaw pm}}
/>
{{/if}}
</div>
</div>
</div>
<div class="control-group">
<label class="control-label">
{{this.delayLabel}}
</label>
<div class="controls">
<Input
@value={{pm.delay}}
class="input-large pm-input pm-delay"
{{on "input" (fn this.mutPmDelay pm)}}
disabled={{@field.isDisabled}}
/>
</div>
</div>
<div class="control-group">
<label class="control-label">
{{this.prefersEncryptLabel}}
</label>
<div class="controls">
<Input
@type="checkbox"
class="pm-prefers-encrypt"
@checked={{pm.prefers_encrypt}}
{{on "click" (fn this.prefersEncrypt pm)}}
disabled={{@field.isDisabled}}
/>
</div>
</div>
<section class="actions">
<DButton
@icon="trash-alt"
@action={{fn this.removePM pm}}
class="btn-danger"
@disabled={{@field.isDisabled}}
/>
</section>
</div>
{{else}}
<div class="no-pm">
<p>{{this.noPmCreatedLabel}}</p>
<DButton
@icon="plus"
@label="discourse_automation.fields.pms.add_pm"
@action={{this.insertPM}}
class="btn-primary insert-pm"
@disabled={{@field.isDisabled}}
/>
</div>
{{/each}}
</section>
</template>
constructor() {
super(...arguments);
// a hack to prevent warnings about modifying multiple times in the same runloop
next(() => {
this.args.field.metadata.value = new TrackedArray(
(this.args.field.metadata.value || []).map((pm) => {
return new TrackedObject(pm);
})
);
});
}
@action
removePM(pm) {
this.dialog.yesNoConfirm({
message: I18n.t("discourse_automation.fields.pms.confirm_remove_pm"),
didConfirm: () => {
return this.args.field.metadata.value.removeObject(pm);
},
});
}
@action
insertPM() {
this.args.field.metadata.value.pushObject(
new TrackedObject({
title: "",
raw: "",
delay: 0,
prefers_encrypt: true,
})
);
}
@action
prefersEncrypt(pm, event) {
pm.prefers_encrypt = event.target.checked;
}
@action
mutPmTitle(pm, event) {
pm.title = event.target.value;
}
@action
mutPmDelay(pm, event) {
pm.delay = event.target.value;
}
@action
updatePmRaw(pm, newRaw) {
pm.raw = newRaw;
}
@action
updatePmTitle(pm, newRaw) {
pm.title = newRaw;
}
}

View File

@ -0,0 +1,31 @@
import DEditor from "discourse/components/d-editor";
import PlaceholdersList from "../placeholders-list";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class PostField extends BaseField {
<template>
<section class="field post-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<div class="field-wrapper">
<DEditor @value={{@field.metadata.value}} />
<DAFieldDescription @description={{@description}} />
{{#if this.displayPlaceholders}}
<PlaceholdersList
@currentValue={{@field.metadata.value}}
@placeholders={{@placeholders}}
@onCopy={{this.mutValue}}
/>
{{/if}}
</div>
</div>
</div>
</section>
</template>
}

View File

@ -0,0 +1,24 @@
import { hash } from "@ember/helper";
import TagChooser from "select-kit/components/tag-chooser";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class TagsField extends BaseField {
<template>
<section class="field tags-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<TagChooser
@tags={{@field.metadata.value}}
@options={{hash allowAny=false disabled=@field.isDisabled}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
}

View File

@ -0,0 +1,43 @@
import { Input } from "@ember/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import PlaceholdersList from "../placeholders-list";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class TextField extends BaseField {
<template>
<section class="field text-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<div class="field-wrapper">
<Input
@value={{@field.metadata.value}}
disabled={{@field.isDisabled}}
{{on "input" this.mutText}}
name={{@field.name}}
/>
<DAFieldDescription @description={{@description}} />
{{#if this.displayPlaceholders}}
<PlaceholdersList
@currentValue={{@field.metadata.value}}
@placeholders={{@placeholders}}
@onCopy={{this.mutValue}}
/>
{{/if}}
</div>
</div>
</div>
</section>
</template>
@action
mutText(event) {
this.mutValue(event.target.value);
}
}

View File

@ -0,0 +1,28 @@
import { hash } from "@ember/helper";
import MultiSelect from "select-kit/components/multi-select";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class TextListField extends BaseField {
<template>
<section class="field text-list-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<MultiSelect
@value={{@field.metadata.value}}
@content={{@field.metadata.value}}
@onChange={{this.mutValue}}
@nameProperty={{null}}
@valueProperty={{null}}
@options={{hash allowAny=true disabled=@field.isDisabled}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
}

View File

@ -0,0 +1,27 @@
import { inject as service } from "@ember/service";
import MultiSelect from "select-kit/components/multi-select";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class TrustLevelsField extends BaseField {
@service site;
<template>
<section class="field category-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<MultiSelect
@value={{@field.metadata.value}}
@content={{this.site.trustLevels}}
@onChange={{this.mutValue}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
}

View File

@ -0,0 +1,53 @@
import { fn, hash } from "@ember/helper";
import { action } from "@ember/object";
import I18n from "I18n";
import UserChooser from "select-kit/components/user-chooser";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class UserField extends BaseField {
@action
onChangeUsername(usernames) {
this.mutValue(usernames[0]);
}
@action
modifyContent(field, content) {
content = field.acceptedContexts
.map((context) => {
return {
name: I18n.t(
`discourse_automation.scriptables.${field.targetName}.fields.${field.name}.${context}_context`
),
username: context,
};
})
.concat(content);
return content;
}
<template>
<section class="field user-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<UserChooser
@value={{@field.metadata.value}}
@onChange={{this.onChangeUsername}}
@modifyContent={{fn this.modifyContent @field}}
@options={{hash
maximum=1
excludeCurrentUser=false
disabled=@field.isDisabled
}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
}

View File

@ -0,0 +1,38 @@
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import MultiSelect from "select-kit/components/multi-select";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class UserProfileField extends BaseField {
@tracked allProfileFields = [];
userProfileFields = [
"bio_raw",
"website",
"location",
"date_of_birth",
"timezone",
];
<template>
<section class="field group-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<MultiSelect
@value={{@field.metadata.value}}
@content={{this.userProfileFields}}
@onChange={{this.mutValue}}
@nameProperty={{null}}
@valueProperty={{null}}
@options={{hash allowAny=true disabled=@field.isDisabled}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
}

View File

@ -0,0 +1,33 @@
import { hash } from "@ember/helper";
import UserChooser from "select-kit/components/user-chooser";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class UsersField extends BaseField {
<template>
<section class="field users-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<UserChooser
@value={{@field.metadata.value}}
@onChange={{this.mutValue}}
@options={{hash
excludeCurrentUser=false
disabled=@field.isDisabled
allowEmails=true
}}
/>
{{#if @field.metadata.allowsAutomation}}
<span class="help-inline error">{{@field.metadata.error}}</span>
{{/if}}
<DAFieldDescription @description={{@description}} />
</div>
</div>
</section>
</template>
}

View File

@ -0,0 +1,23 @@
import Component from "@glimmer/component";
import { fn } from "@ember/helper";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
export default class PlaceholdersList extends Component {
<template>
<div class="placeholders-list">
{{#each @placeholders as |placeholder|}}
<DButton
@translatedLabel={{placeholder}}
class="placeholder-item"
@action={{fn this.copyPlaceholder placeholder}}
/>
{{/each}}
</div>
</template>
@action
copyPlaceholder(placeholder) {
this.args.onCopy(`${this.args.currentValue}{{${placeholder}}}`);
}
}

View File

@ -0,0 +1,113 @@
import Controller from "@ember/controller";
import { action, computed, set } from "@ember/object";
import { filterBy, reads } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { extractError } from "discourse/lib/ajax-error";
import I18n from "I18n";
export default class AutomationEdit extends Controller {
@service dialog;
error = null;
isUpdatingAutomation = false;
isTriggeringAutomation = false;
@reads("model.automation") automation;
@filterBy("automationForm.fields", "targetType", "script") scriptFields;
@filterBy("automationForm.fields", "targetType", "trigger") triggerFields;
@computed("model.automation.next_pending_automation_at")
get nextPendingAutomationAtFormatted() {
const date = this.model?.automation?.next_pending_automation_at;
if (date) {
return moment(date).format("LLLL");
}
}
@action
saveAutomation() {
this.setProperties({ error: null, isUpdatingAutomation: true });
return ajax(
`/admin/plugins/discourse-automation/automations/${this.model.automation.id}.json`,
{
type: "PUT",
data: JSON.stringify({ automation: this.automationForm }),
dataType: "json",
contentType: "application/json",
}
)
.then(() => {
this.send("refreshRoute");
})
.catch((e) => this._showError(e))
.finally(() => {
this.set("isUpdatingAutomation", false);
});
}
@action
onChangeTrigger(id) {
if (this.automationForm.trigger && this.automationForm.trigger !== id) {
this._confirmReset(() => {
set(this.automationForm, "trigger", id);
this.saveAutomation();
});
} else if (!this.automationForm.trigger) {
set(this.automationForm, "trigger", id);
this.saveAutomation();
}
}
@action
onManualAutomationTrigger(id) {
this._confirmTrigger(() => {
this.set("isTriggeringAutomation", true);
return ajax(`/automations/${id}/trigger.json`, {
type: "post",
})
.catch((e) => this.set("error", extractError(e)))
.finally(() => {
this.set("isTriggeringAutomation", false);
});
});
}
@action
onChangeScript(id) {
if (this.automationForm.script !== id) {
this._confirmReset(() => {
set(this.automationForm, "script", id);
this.saveAutomation();
});
}
}
_confirmReset(callback) {
this.dialog.yesNoConfirm({
message: I18n.t("discourse_automation.confirm_automation_reset"),
didConfirm: () => {
return callback && callback();
},
});
}
_confirmTrigger(callback) {
this.dialog.yesNoConfirm({
message: I18n.t("discourse_automation.confirm_automation_trigger"),
didConfirm: () => {
return callback && callback();
},
});
}
_showError(error) {
this.set("error", extractError(error));
schedule("afterRender", () => {
window.scrollTo(0, 0);
});
}
}

View File

@ -0,0 +1,39 @@
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 escape from "discourse-common/lib/escape";
import I18n from "I18n";
export default class AutomationIndex extends Controller {
@service dialog;
@service router;
@action
editAutomation(automation) {
this.router.transitionTo(
"adminPlugins.discourse-automation.edit",
automation.id
);
}
@action
newAutomation() {
this.router.transitionTo("adminPlugins.discourse-automation.new");
}
@action
destroyAutomation(automation) {
this.dialog.deleteConfirm({
message: I18n.t("discourse_automation.destroy_automation.confirm", {
name: escape(automation.name),
}),
didConfirm: () => {
return automation
.destroyRecord()
.then(() => this.send("triggerRefresh"))
.catch(popupAjaxError);
},
});
}
}

View File

@ -0,0 +1,38 @@
import Controller from "@ember/controller";
import EmberObject, { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { extractError } from "discourse/lib/ajax-error";
export default class AutomationNew extends Controller {
@service router;
form = null;
error = null;
init() {
super.init(...arguments);
this._resetForm();
}
@action
saveAutomation(automation) {
this.set("error", null);
automation
.save(this.form.getProperties("name", "script"))
.then(() => {
this._resetForm();
this.router.transitionTo(
"adminPlugins.discourse-automation.edit",
automation.id
);
})
.catch((e) => {
this.set("error", extractError(e));
});
}
_resetForm() {
this.set("form", EmberObject.create({ name: null, script: null }));
}
}

View File

@ -0,0 +1,19 @@
import Controller from "@ember/controller";
import { action, computed } from "@ember/object";
import { inject as service } from "@ember/service";
export default class Automation extends Controller {
@service router;
@computed("router.currentRouteName")
get showNewAutomation() {
return (
this.router.currentRouteName === "adminPlugins.discourse-automation.index"
);
}
@action
newAutomation() {
this.router.transitionTo("adminPlugins.discourse-automation.new");
}
}

View File

@ -0,0 +1,16 @@
export default {
resource: "admin.adminPlugins",
path: "/plugins",
map() {
this.route(
"discourse-automation",
function () {
this.route("new");
this.route("edit", { path: "/:id" });
}
);
},
};

View File

@ -0,0 +1,20 @@
import { htmlSafe } from "@ember/template";
import { iconHTML } from "discourse-common/lib/icon-library";
export default function formatEnabledAutomation(enabled, trigger) {
if (enabled && trigger.id) {
return htmlSafe(
iconHTML("check", {
class: "enabled-automation",
title: "discourse_automation.models.automation.enabled.label",
})
);
} else {
return htmlSafe(
iconHTML("times", {
class: "disabled-automation",
title: "discourse_automation.models.automation.disabled.label",
})
);
}
}

View File

@ -0,0 +1,84 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { withPluginApi } from "discourse/lib/plugin-api";
import { makeArray } from "discourse-common/lib/helpers";
let _lastCheckedByHandlers = {};
function _handleLastCheckedByEvent(event) {
ajax(`/append-last-checked-by/${event.currentTarget.postId}`, {
type: "PUT",
}).catch(popupAjaxError);
}
function _cleanUp() {
Object.values(_lastCheckedByHandlers || {}).forEach((handler) => {
handler.removeEventListener("click", _handleLastCheckedByEvent);
});
_lastCheckedByHandlers = {};
}
function _initializeDiscourseAutomation(api) {
_initializeGLobalUserNotices(api);
if (api.getCurrentUser()) {
api.decorateCookedElement(_decorateCheckedButton, {
id: "discourse-automation",
});
api.cleanupStream(_cleanUp);
}
}
function _decorateCheckedButton(element, postDecorator) {
if (!postDecorator) {
return;
}
const elems = element.querySelectorAll(".btn-checked");
const postModel = postDecorator.getModel();
Array.from(elems).forEach((elem) => {
const postId = postModel.id;
elem.postId = postId;
if (_lastCheckedByHandlers[postId]) {
_lastCheckedByHandlers[postId].removeEventListener(
"click",
_handleLastCheckedByEvent,
false
);
delete _lastCheckedByHandlers[postId];
}
_lastCheckedByHandlers[postId] = elem;
elem.addEventListener("click", _handleLastCheckedByEvent, false);
});
}
function _initializeGLobalUserNotices(api) {
const currentUser = api.getCurrentUser();
makeArray(currentUser?.global_notices).forEach((userGlobalNotice) => {
api.addGlobalNotice("", userGlobalNotice.identifier, {
html: userGlobalNotice.notice,
level: userGlobalNotice.level,
dismissable: true,
dismissDuration: moment.duration(1, "week"),
onDismiss() {
ajax(`/user-global-notices/${userGlobalNotice.id}.json`, {
type: "DELETE",
}).catch(popupAjaxError);
},
});
});
}
export default {
name: "discourse-automation",
initialize() {
withPluginApi("0.8.24", _initializeDiscourseAutomation);
},
};

View File

@ -0,0 +1,44 @@
/*
Fabricators are used to create fake data for testing purposes.
The following fabricators are available in lib folder to allow
styleguide to use them, and eventually to generate dummy data
in a placeholder component. It should not be used for any other case.
*/
import Automation from "../admin/models/discourse-automation-automation";
import Field from "../admin/models/discourse-automation-field";
let sequence = 0;
function fieldFabricator(args = {}) {
const template = args.template || {};
template.accepts_placeholders = args.accepts_placeholders ?? true;
template.accepted_contexts = args.accepted_contexts ?? [];
template.name = args.name ?? "name";
template.component = args.component ?? "boolean";
template.value = args.value ?? false;
template.is_required = args.is_required ?? false;
template.extra = args.extra ?? {};
return Field.create(template, {
type: args.target ?? "script",
name: "script_name",
});
}
function automationFabricator(args = {}) {
const automation = new Automation();
automation.id = args.id || sequence++;
automation.trigger = {
id: (sequence++).toString(),
};
automation.script = {
id: (sequence++).toString(),
};
return automation;
}
export default {
field: fieldFabricator,
automation: automationFabricator,
};

View File

@ -0,0 +1,59 @@
import { action } from "@ember/object";
import { hash } from "rsvp";
import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse";
import Field from "../admin/models/discourse-automation-field";
export default class AutomationEdit extends DiscourseRoute {
controllerName = "admin-plugins-discourse-automation-edit";
model(params) {
return hash({
scriptables: this.store
.findAll("discourse-automation-scriptable")
.then((result) => result.content),
triggerables: ajax(
`/admin/plugins/discourse-automation/triggerables.json?automation_id=${params.id}`
).then((result) => (result ? result.triggerables : [])),
automation: this.store.find("discourse-automation-automation", params.id),
});
}
_fieldsForTarget(automation, target) {
return (automation[target].templates || []).map((template) => {
const jsonField = automation[target].fields.find(
(f) => f.name === template.name && f.component === template.component
);
return Field.create(
template,
{
name: automation[target].id,
type: target,
},
jsonField
);
});
}
setupController(controller, model) {
const automation = model.automation;
controller.setProperties({
model,
error: null,
automationForm: {
name: automation.name,
enabled: automation.enabled,
trigger: automation.trigger?.id,
script: automation.script?.id,
fields: this._fieldsForTarget(automation, "script").concat(
this._fieldsForTarget(automation, "trigger")
),
},
});
}
@action
refreshRoute() {
return this.refresh();
}
}

View File

@ -0,0 +1,15 @@
import { action } from "@ember/object";
import DiscourseRoute from "discourse/routes/discourse";
export default class AutomationIndex extends DiscourseRoute {
controllerName = "admin-plugins-discourse-automation-index";
model() {
return this.store.findAll("discourse-automation-automation");
}
@action
triggerRefresh() {
this.refresh();
}
}

View File

@ -0,0 +1,13 @@
import { hash } from "rsvp";
import DiscourseRoute from "discourse/routes/discourse";
export default class AutomationNew extends DiscourseRoute {
controllerName = "admin-plugins-discourse-automation-new";
model() {
return hash({
scriptables: this.store.findAll("discourse-automation-scriptable"),
automation: this.store.createRecord("discourse-automation-automation"),
});
}
}

View File

@ -0,0 +1,5 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class Automation extends DiscourseRoute {
controllerName = "admin-plugins-discourse-automation";
}

View File

@ -0,0 +1,169 @@
<ScrollTracker @name="discourse-automation-edit" />
<section class="discourse-automation-form edit">
<form class="form-horizontal">
<FormError @error={{error}} />
<section class="form-section edit">
<div class="control-group">
<label class="control-label">
{{i18n "discourse_automation.models.automation.name.label"}}
</label>
<div class="controls">
<TextField
@value={{automationForm.name}}
@type="text"
@autofocus="autofocus"
@name="automation-name"
class="input-large"
/>
</div>
</div>
<div class="control-group">
<label class="control-label">
{{i18n "discourse_automation.models.script.name.label"}}
</label>
<div class="controls">
<ComboBox
@value={{automationForm.script}}
@content={{model.scriptables}}
@onChange={{action "onChangeScript"}}
@options={{hash filterable=true}}
class="scriptables"
/>
</div>
</div>
</section>
<section class="trigger-section form-section edit">
<h2 class="title">
{{i18n "discourse_automation.edit_automation.trigger_section.title"}}
</h2>
<div class="control-group">
{{#if model.automation.script.forced_triggerable}}
<div class="alert alert-warning">
{{i18n
"discourse_automation.edit_automation.trigger_section.forced"
}}
</div>
{{/if}}
<label class="control-label">
{{i18n "discourse_automation.models.trigger.name.label"}}
</label>
<div class="controls">
<ComboBox
@value={{automationForm.trigger}}
@content={{model.triggerables}}
@onChange={{action "onChangeTrigger"}}
@options={{hash
filterable=true
none="discourse_automation.select_trigger"
disabled=model.automation.script.forced_triggerable
}}
class="triggerables"
/>
</div>
</div>
{{#if automationForm.trigger}}
{{#if model.automation.trigger.doc}}
<div class="alert alert-info">
<p>{{model.automation.trigger.doc}}</p>
</div>
{{/if}}
{{#if
(and
model.automation.enabled
model.automation.trigger.settings.manual_trigger
)
}}
<div class="alert alert-info next-trigger">
{{#if nextPendingAutomationAtFormatted}}
<p>
{{i18n
"discourse_automation.edit_automation.trigger_section.next_pending_automation"
date=nextPendingAutomationAtFormatted
}}
</p>
{{/if}}
<DButton
@label="discourse_automation.edit_automation.trigger_section.trigger_now"
@isLoading={{isTriggeringAutomation}}
@action={{action "onManualAutomationTrigger" model.automation.id}}
class="btn-primary trigger-now-btn"
/>
</div>
{{/if}}
{{#each triggerFields as |field|}}
<AutomationField
@automation={{automation}}
@field={{field}}
@saveAutomation={{action "saveAutomation" automation}}
/>
{{/each}}
{{/if}}
</section>
{{#if automationForm.trigger}}
{{#if 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}}
<div class="alert alert-info">
<p>{{model.automation.script.with_trigger_doc}}</p>
</div>
{{/if}}
<div class="control-group">
{{#each scriptFields as |field|}}
<AutomationField
@automation={{automation}}
@field={{field}}
@saveAutomation={{action "saveAutomation" automation}}
/>
{{/each}}
</div>
</section>
{{/if}}
{{#if automationForm.trigger}}
<div class="control-group automation-enabled alert alert-warning">
<span>{{i18n
"discourse_automation.models.automation.enabled.label"
}}</span>
<Input
@type="checkbox"
@checked={{automationForm.enabled}}
{{on
"click"
(action (mut automationForm.enabled) value="target.checked")
}}
/>
</div>
{{/if}}
<div class="control-group">
<DButton
@isLoading={{isUpdatingAutomation}}
@label="discourse_automation.update"
@type="submit"
@action={{action "saveAutomation" automation}}
class="btn-primary update-automation"
/>
</div>
{{/if}}
</form>
</section>

View File

@ -0,0 +1,88 @@
{{#if model.length}}
<table class="automations">
<thead>
<tr>
<th></th>
<th>{{i18n "discourse_automation.models.automation.name.label"}}</th>
<th>{{i18n "discourse_automation.models.automation.trigger.label"}}</th>
<th>{{i18n "discourse_automation.models.automation.script.label"}}</th>
<th>{{i18n
"discourse_automation.models.automation.last_updated_by.label"
}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each model as |automation|}}
<tr>
{{#if automation.script.not_found}}
<td colspan="5" class="alert alert-danger">
{{i18n
"discourse_automation.scriptables.not_found"
script=automation.script.id
automation=automation.name
}}
</td>
{{else if automation.trigger.not_found}}
<td colspan="5" class="alert alert-danger">
{{i18n
"discourse_automation.triggerables.not_found"
trigger=automation.trigger.id
automation=automation.name
}}
</td>
{{else}}
<td
role="button"
{{on "click" (fn this.editAutomation automation)}}
>{{format-enabled-automation
automation.enabled
automation.trigger
}}</td>
<td
tabindex="0"
role="button"
{{on "keypress" (fn this.editAutomation automation)}}
{{on "click" (fn this.editAutomation automation)}}
>{{automation.name}}</td>
<td
role="button"
{{on "click" (fn this.editAutomation automation)}}
>{{if automation.trigger.id automation.trigger.name "-"}}</td>
<td
role="button"
{{on "click" (fn this.editAutomation automation)}}
>{{automation.script.name}} (v{{automation.script.version}})</td>
<td>
<a
href={{automation.last_updated_by.userPath}}
data-user-card={{automation.last_updated_by.username}}
>
{{avatar automation.last_updated_by imageSize="small"}}
</a>
{{format-date automation.updated_at leaveAgo="true"}}
</td>
{{/if}}
<td>
<DButton
@icon="trash-alt"
@action={{action "destroyAutomation" automation}}
class="btn-danger"
/>
</td>
</tr>
{{/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

@ -0,0 +1,53 @@
<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="controls">
<TextField
@value={{form.name}}
@type="text"
@autofocus="autofocus"
@name="automation-name"
class="input-large"
/>
</div>
</div>
<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={{action (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={{action "saveAutomation" model.automation}}
class="btn-primary create-automation"
/>
</div>
</div>
</form>
</section>

View File

@ -0,0 +1,53 @@
<LinkTo
@route="adminPlugins.discourse-automation"
class="discourse-automation-title"
>
<svg
width="32"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 41.3 41.3"
><title>Asset 4</title><path
d="M20.42,40.21a2.52,2.52,0,0,1-1.78-.78l-.08-.07-1.94-2a2.48,2.48,0,0,1-.72-1.23l-1.15.64a2.51,2.51,0,0,1-1.25.33,2.59,2.59,0,0,1-1.86-.79l-.73-.76a1.79,1.79,0,0,1-.24-.21l-.84-.88a1.31,1.31,0,0,1-.27-.24l-4.67-4.9A2.5,2.5,0,0,1,4.5,26.3l.92-1.47-.48-.14a2.3,2.3,0,0,1-.9-.47l-.17-.14L1.72,21.93a2.09,2.09,0,0,1-.14-.17A2.55,2.55,0,0,1,1,20.1l.08-3.85a2.57,2.57,0,0,1,2-2.44l2.73-.65L6,12.78,4.59,10.33a2.57,2.57,0,0,1,.47-3.11L7.84,4.56a2.56,2.56,0,0,1,1.77-.72A2.74,2.74,0,0,1,11,4.23l2.46,1.52.3-.11.78-2.77A2.6,2.6,0,0,1,17,1l3.88.08.14,0a1,1,0,0,1,.24,0,2.65,2.65,0,0,1,.51.15h0a1.11,1.11,0,0,1,.31.15l.28.2.08.06a.52.52,0,0,1,.11.1l2.08,2.08a2.57,2.57,0,0,1,.73,1.25l.1.45,1.56-.87a2.5,2.5,0,0,1,1.24-.33,2.62,2.62,0,0,1,1.87.8l.55.57.21.19L32,7a1.43,1.43,0,0,1,.29.24l4.63,4.86a2.56,2.56,0,0,1,.43,3.12l-.84,1.36a2.66,2.66,0,0,1,.86.5,1.27,1.27,0,0,1,.12.1l.09.1,2,2a.64.64,0,0,1,.1.13,2.56,2.56,0,0,1,.69,1.77L40.21,25a2.56,2.56,0,0,1-2,2.45l-2.62.61c-.07.18-.14.35-.22.53l1.3,2.33A2.55,2.55,0,0,1,36.24,34l-2.78,2.67a2.6,2.6,0,0,1-1.78.71h0A2.61,2.61,0,0,1,30.33,37l-2.2-1.36c-.21.08-.42.17-.63.24l-.71,2.52a2.57,2.57,0,0,1-2.47,1.87Z"
></path><path
d="M17,2l3.85.08.13,0h0l.07,0a1.27,1.27,0,0,1,.38.11l.1,0,.08.06.16.12.11.08,2,2h0a1.54,1.54,0,0,1,.52.83l.41,1.74,2.72-1.52a1.61,1.61,0,0,1,.75-.2h.05a1.57,1.57,0,0,1,1.1.48l.65.69h0a.25.25,0,0,1,.1.08l1.14,1.19a.32.32,0,0,1,.17.12l4.64,4.86a1.58,1.58,0,0,1,.3,1.91l-1.53,2.48,1.16.33a1.54,1.54,0,0,1,.64.38h0l0,0h0L37.82,19l0,0,1,1h0a1.55,1.55,0,0,1,.51,1.17L39.21,25A1.55,1.55,0,0,1,38,26.48l-3.13.74a12.5,12.5,0,0,1-.61,1.41l1.56,2.79a1.56,1.56,0,0,1-.28,1.9L32.77,36a1.56,1.56,0,0,1-1.09.44,1.53,1.53,0,0,1-.82-.24L28.2,34.54a13.33,13.33,0,0,1-1.52.6l-.85,3a1.57,1.57,0,0,1-1.51,1.14h0l-3.85-.09a1.57,1.57,0,0,1-1.17-.57h0L17.32,36.7a1.47,1.47,0,0,1-.45-.77l-.3-1.27L14.26,36a1.49,1.49,0,0,1-.76.2,1.61,1.61,0,0,1-1.14-.48l-.82-.86a.49.49,0,0,1-.14-.11l-1-1a.53.53,0,0,1-.17-.12L5.61,28.68a1.49,1.49,0,0,1-.26-1.85l1.58-2.55,0-.07-1.69-.48a1.55,1.55,0,0,1-.63-.37h0l-.1-.1h0L3.31,22.1h0l-.86-.87h0A1.58,1.58,0,0,1,2,20.12l.08-3.85a1.58,1.58,0,0,1,1.21-1.49L6.52,14a13.1,13.1,0,0,1,.58-1.27L5.47,9.84a1.56,1.56,0,0,1,.28-1.9L8.53,5.28a1.55,1.55,0,0,1,1.08-.43h0a1.48,1.48,0,0,1,.79.23l2.9,1.79c.39-.17.79-.32,1.21-.46l.92-3.27A1.6,1.6,0,0,1,17,2m0-2h0a3.62,3.62,0,0,0-3.46,2.6l-.49,1.74-1.56-1a3.47,3.47,0,0,0-1.8-.53H9.61a3.58,3.58,0,0,0-2.46,1L4.37,6.5a3.55,3.55,0,0,0-.65,4.32l.9,1.59-1.79.42a3.57,3.57,0,0,0-2.74,3.4L0,20.08a3.55,3.55,0,0,0,.77,2.27,2.09,2.09,0,0,0,.24.28l.87.87h0L3,24.67h0l.11.11a2.48,2.48,0,0,0,.3.25,4.53,4.53,0,0,0,.46.3l-.27.44a3.47,3.47,0,0,0,.52,4.28L8.83,35a2.67,2.67,0,0,0,.34.29l.78.82a2,2,0,0,0,.3.27l.67.7a3.59,3.59,0,0,0,2.58,1.1,3.67,3.67,0,0,0,1.74-.45l.21-.12a3.64,3.64,0,0,0,.48.56l1.92,1.92.11.11a3.55,3.55,0,0,0,2.43,1l3.85.09h.08a3.58,3.58,0,0,0,3.43-2.6l.51-1.78,1.55,1a3.55,3.55,0,0,0,4.34-.45l2.78-2.67a3.57,3.57,0,0,0,.65-4.32l-.9-1.59,1.78-.42A3.55,3.55,0,0,0,41.21,25l.09-3.84a3.55,3.55,0,0,0-.92-2.45l-.17-.19-1-.95,0,0h0l-1-1.06-.13-.12-.19-.16.3-.48a3.58,3.58,0,0,0-.57-4.35L33,6.56a2.3,2.3,0,0,0-.35-.31l-1-1A2.53,2.53,0,0,0,31.39,5l-.5-.52a3.52,3.52,0,0,0-2.49-1.1h-.1a3.53,3.53,0,0,0-1.73.46l-.49.27a3.87,3.87,0,0,0-.69-.93.46.46,0,0,0-.07-.07l-2-2a1.59,1.59,0,0,0-.24-.2L23,.83,22.75.66l0,0a2,2,0,0,0-.62-.31h0a3.83,3.83,0,0,0-.58-.16A1.59,1.59,0,0,0,21.13.1h-.22L17.06,0Z"
></path><path
d="M8.78,24.14l1.93,2.21a13.21,13.21,0,0,0,.83,2.11L9.85,31.19A1.15,1.15,0,0,0,10,32.6l2.66,2.78a1.15,1.15,0,0,0,1.4.21L17,34a11.35,11.35,0,0,0,1.6.69l.77,3.27a1.15,1.15,0,0,0,1.1.89l3.85.09A1.16,1.16,0,0,0,25.43,38l.91-3.23a12.77,12.77,0,0,0,1.89-.74l2.85,1.76a1.13,1.13,0,0,0,1.4-.15L35.26,33a1.15,1.15,0,0,0,.21-1.4l-1.67-3a12.63,12.63,0,0,0,.77-1.77l3.34-.79A1.15,1.15,0,0,0,38.8,25l.09-3.85A1.16,1.16,0,0,0,38,20l-3.4-1a11.44,11.44,0,0,0-.52-1.35l2-3.2a1.18,1.18,0,0,0-.25-1.41L33.19,10.3a1.18,1.18,0,0,0-.81-.36.85.85,0,0,0-.48.15l-3.17,1.77a14.55,14.55,0,0,0-1.8-.81L24.67,9.76c-.12-.52-.23-1.5-.75-1.52L21.15,6.56A1.16,1.16,0,0,0,20,7.4l-1,3.48a11.87,11.87,0,0,0-1.57.61L14.37,9.58a1.23,1.23,0,0,0-.58-.18,1.2,1.2,0,0,0-.83.33l-2.78,2.66a1.17,1.17,0,0,0-.21,1.4l1.74,3.1A11.92,11.92,0,0,0,11,18.52l-3.45.81a1.16,1.16,0,0,0-.89,1.1l.63,2.19C7.26,23.15,9.4,22.1,8.78,24.14Zm14.1-8.74a7.33,7.33,0,1,1-7.48,7.16A7.32,7.32,0,0,1,22.88,15.4Z"
fill="#00aeef"
></path><path
d="M8.54,23.9l1.2,1.48a12.89,12.89,0,0,0,.84,2.11l-1.7,2.74A1.16,1.16,0,0,0,9,31.63l2.66,2.78a1.14,1.14,0,0,0,1.4.21L16,33a12.46,12.46,0,0,0,1.59.68l.77,3.27a1.14,1.14,0,0,0,1.1.89l3.85.09a1.17,1.17,0,0,0,1.14-.84l.91-3.24a11.3,11.3,0,0,0,1.88-.74l2.85,1.76a1.16,1.16,0,0,0,1.41-.14l2.78-2.67a1.15,1.15,0,0,0,.21-1.39l-1.67-3a12.74,12.74,0,0,0,.76-1.77l3.34-.79A1.14,1.14,0,0,0,37.83,24l.09-3.85A1.16,1.16,0,0,0,37.08,19l-3.41-1a11,11,0,0,0-.51-1.35l1.93-3.15a1.15,1.15,0,0,0-.15-1.41L32.28,9.38A1.17,1.17,0,0,0,31.47,9a1,1,0,0,0-.54.1L27.76,10.9a11.24,11.24,0,0,0-1.8-.81l-2.28.18c-1.46,2.2-.07-2.12-.59-2.13L20.18,5.59a1.16,1.16,0,0,0-1.13.84l-1,3.48a12,12,0,0,0-1.58.61L13.4,8.61a1.15,1.15,0,0,0-1.4.15L9.22,11.42A1.14,1.14,0,0,0,9,12.82l1.74,3.11A11.85,11.85,0,0,0,10,17.55l-3.44.81a1.14,1.14,0,0,0-.89,1.1l1.07,2.71C6.74,22.7,9.89,23.35,8.54,23.9Zm13.38-9.47a7.33,7.33,0,1,1-7.49,7.17A7.33,7.33,0,0,1,21.92,14.43Z"
fill="#00a94f"
></path><path
d="M3.25,21.27l3.31.93a12.89,12.89,0,0,0,.84,2.11L5.7,27.05a1.09,1.09,0,0,0,.21,1.35l2.66,2.78a1.08,1.08,0,0,0,1.34.26l2.93-1.63a12.46,12.46,0,0,0,1.59.68l.77.31a1.16,1.16,0,0,0,1.1.89l3.85,3.05a1.17,1.17,0,0,0,1.14-.84l.91-3.24a11.3,11.3,0,0,0,1.88-.74l2.85,1.77a1.18,1.18,0,0,0,1.41-.15l2.78-2.67a1.15,1.15,0,0,0,.21-1.39l-1.67-3a12.74,12.74,0,0,0,.76-1.77l3.34-.79a1.14,1.14,0,0,0,.89-1.09l-2.88-2.37a1.16,1.16,0,0,0-.84-1.14l-.44-2.44A11,11,0,0,0,30,13.54l2-3.2a1.16,1.16,0,0,0-.15-1.4L29.15,6.15a1.15,1.15,0,0,0-.81-.35,1.12,1.12,0,0,0-.59.14L24.58,7.72a11.24,11.24,0,0,0-1.8-.81L22,3.39a1.14,1.14,0,0,0-1.1-.89L17,2.41a1.16,1.16,0,0,0-1.13.84l-1,3.48a12,12,0,0,0-1.58.61L10.22,5.43a1.15,1.15,0,0,0-1.4.15L6,8.24a1.14,1.14,0,0,0-.21,1.4l1.74,3.11a11.85,11.85,0,0,0-.74,1.62l-3.44.81a1.14,1.14,0,0,0-.89,1.1l-.09,3.85A1.17,1.17,0,0,0,3.25,21.27Zm15.49-10a7.33,7.33,0,1,1-7.49,7.17A7.33,7.33,0,0,1,18.74,11.25Z"
fill="#d0232b"
></path><path
d="M24.39,20.28A4.71,4.71,0,0,1,16.8,24a4.71,4.71,0,1,0,6.61-6.61A4.74,4.74,0,0,1,24.39,20.28Z"
fill="#00a94f"
></path><path
d="M4,22,7.32,23a12.12,12.12,0,0,0,.83,2.12L6.46,27.8a1.16,1.16,0,0,0,.22,1.41L9.34,32a1.05,1.05,0,0,0,1.33.21l2.92-1.64a11.35,11.35,0,0,0,1.6.69L17.44,33a1.16,1.16,0,0,0,1.1.89l2.36,1.56A1.14,1.14,0,0,0,22,34.65L23,31.42a13.35,13.35,0,0,0,1.88-.74l2.85,1.76a1.15,1.15,0,0,0,1.41-.15l2.78-2.66a1.15,1.15,0,0,0,.21-1.4l-1.67-3a12.63,12.63,0,0,0,.77-1.77l3.34-.79a1.15,1.15,0,0,0,.89-1.1L34,19.23a1.15,1.15,0,0,0-.84-1.14l-1.92-2.45a12,12,0,0,0-.52-1.34l2-3.2a1.16,1.16,0,0,0-.14-1.41L29.9,6.91a1.15,1.15,0,0,0-.81-.36,1.26,1.26,0,0,0-.59.15L25.34,8.47a14.55,14.55,0,0,0-1.8-.81l-.83-3.52a1.15,1.15,0,0,0-1.1-.89l-3.85-.08A1.15,1.15,0,0,0,16.62,4l-1,3.48a12.58,12.58,0,0,0-1.58.61L11,6.19A1.13,1.13,0,0,0,10.4,6a1.23,1.23,0,0,0-.83.32L6.79,9a1.15,1.15,0,0,0-.21,1.4l1.74,3.1a11.4,11.4,0,0,0-.73,1.63l-3.45.81A1.15,1.15,0,0,0,3.25,17l-.08,3.85A1.15,1.15,0,0,0,4,22ZM19.49,12A7.33,7.33,0,1,1,12,19.17,7.33,7.33,0,0,1,19.49,12Z"
fill="#f15d22"
></path><circle cx="20.64" cy="20.86" r="8.33" fill="#fff"></circle><path
d="M36,17.92l-3.41-1q-.23-.69-.51-1.35l2-3.2A1.18,1.18,0,0,0,33.88,11L31.22,8.22a1.13,1.13,0,0,0-.81-.35,1.1,1.1,0,0,0-.59.14L26.65,9.79A12.73,12.73,0,0,0,24.85,9L24,5.46a1.14,1.14,0,0,0-1.1-.89l-3.85-.09a1.15,1.15,0,0,0-1.13.84L17,8.8a11.87,11.87,0,0,0-1.57.61L12.29,7.5a1.15,1.15,0,0,0-1.4.15L8.1,10.31a1.17,1.17,0,0,0-.21,1.4l1.74,3.1a14,14,0,0,0-.73,1.63l-3.44.81a1.14,1.14,0,0,0-.89,1.1L4.48,22.2a1.16,1.16,0,0,0,.84,1.13l3.31.94a12.89,12.89,0,0,0,.84,2.11l-1.7,2.73a1.16,1.16,0,0,0,.15,1.41l2.66,2.78a1.15,1.15,0,0,0,1.4.21l2.92-1.64a11.87,11.87,0,0,0,1.6.69l.77,3.27a1.14,1.14,0,0,0,1.1.89l3.85.09A1.16,1.16,0,0,0,23.35,36l.92-3.24A11.3,11.3,0,0,0,26.15,32L29,33.75a1.15,1.15,0,0,0,1.4-.14l2.78-2.67a1.14,1.14,0,0,0,.21-1.4l-1.67-3a12.14,12.14,0,0,0,.77-1.77L35.83,24a1.14,1.14,0,0,0,.89-1.1l.09-3.85A1.16,1.16,0,0,0,36,17.92ZM25.72,23.64l-3.84,1.78-2.38.87-3-1.46-1.35-2.51-.22-3.75,2.65-2.65h3.56l3.2,1.55,2.2,2.63Z"
fill="#fff9ae"
></path><path
d="M20.45,26.15a5.92,5.92,0,1,1,5.93-5.92A5.93,5.93,0,0,1,20.45,26.15Zm0-10.57a4.65,4.65,0,1,0,4.65,4.65A4.65,4.65,0,0,0,20.45,15.58Z"
stroke="#000"
stroke-miterlimit="10"
></path></svg>
<h1 class="title">
{{i18n "discourse_automation.title"}}
</h1>
{{#if showNewAutomation}}
<DButton
@label="discourse_automation.create"
@icon="plus"
@action={{action "newAutomation"}}
class="new-automation"
/>
{{/if}}
</LinkTo>
<hr />
{{outlet}}

View File

@ -0,0 +1,5 @@
{{#if error}}
<div class="alert alert-error form-errors">
{{html-safe error}}
</div>
{{/if}}

View File

@ -0,0 +1,12 @@
<div class="control-group">
<label class="control-label">
{{i18n "discourse_automation.triggerables.topic.topic_id.label"}}
</label>
<div class="controls">
<Input
@value={{metadata.topic_id}}
{{on "input" (action (mut metadata.topic_id) value="target.value")}}
/>
</div>
</div>

View File

@ -0,0 +1,313 @@
.discourse-automation {
.automations {
.relative-date {
font-size: $font-down-1;
}
td[role="button"] {
cursor: pointer;
}
}
}
.discourse-automation-title {
display: flex;
align-items: center;
height: 40px;
.title {
margin: 0 0 0 0.5em;
font-weight: 700;
font-size: $font-up-3;
}
.new-automation {
margin-left: auto;
}
}
.enabled-automation {
color: var(--success);
}
.disabled-automation {
color: var(--danger);
}
.discourse-automation-form {
.scriptables,
.triggerables {
.select-kit-body {
max-height: 250px;
}
}
.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;
}
}
.form-horizontal {
.control-label {
margin: 0.25em 0.25em 0.25em 0;
text-align: left;
width: 180px;
line-height: 27px;
}
.controls {
margin-left: 185px;
}
.boolean-field {
.controls {
display: flex;
}
}
}
.automation-presentation {
border-left: 5px solid var(--primary-very-low);
padding: 0.5em;
.automation-name {
font-size: $font-up-2;
}
.automation-doc {
margin: 0;
}
}
.automation-presentation + .control-group {
margin-top: 2em;
}
.scripts-list {
width: 210px;
.select-kit-header {
background: var(--secondary);
border: 1px solid var(--primary-medium);
border-radius: 0;
&:hover {
color: var(--primary);
}
}
}
.script-doc {
background: var(--primary-very-low);
border-left: 3px solid var(--primary-low);
}
.form-section {
.title {
padding-bottom: 0.5em;
margin-bottom: 1em;
border-bottom: 1px solid var(--primary-low);
}
.input-large,
.select-kit,
.d-date-time-input {
width: 300px;
.select-kit,
input[type="text"] {
width: auto;
}
}
.period-field {
line-height: 34px;
.controls {
display: flex;
}
.select-kit {
width: 150px;
margin-left: 6px;
.select-kit-header {
height: 34px;
}
}
input {
width: 100px;
max-height: 34px;
margin: 0 0 0 6px;
}
}
.d-date-time-input {
border-color: var(--primary-medium);
}
}
.control-group {
.control-description {
color: var(--primary-medium);
width: 100%;
margin: 0;
padding: 0.25em 0;
}
&.automation-enabled {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 1em;
.ember-checkbox {
margin: 0 0 0 1em;
padding: 0;
}
.control-label.disabled {
color: var(--danger);
}
}
}
.controls-row {
display: flex;
input {
margin-bottom: 0;
}
}
.field-wrapper {
display: flex;
flex-direction: column;
}
.text-field {
.ember-text-field {
width: 300px;
}
}
.message-field {
textarea {
width: 300px;
height: 200px;
}
}
.next-trigger {
padding: 1em;
p {
margin: 0;
padding: 0 0 0.5em 0;
}
}
.pms-field {
.actions {
display: flex;
justify-content: flex-end;
align-items: center;
&.header {
justify-content: space-between;
margin: 1em 0;
}
}
.pm-field {
margin-bottom: 1em;
.control-group {
display: flex;
flex-direction: column;
.control-label {
width: 100%;
}
.controls {
margin: 0;
}
}
.d-editor,
.d-editor-container {
display: flex;
flex: 1;
justify-content: space-between;
.d-editor-input {
min-height: 200px;
}
}
}
.d-editor-textarea-wrapper {
box-sizing: border-box;
min-width: 300px;
}
.pm-field:not(:last-child) {
margin-bottom: 1em;
}
.pm-title {
width: 100%;
}
.pm-textarea {
width: 100%;
box-sizing: border-box;
height: 200px;
}
.no-pm {
border: 1px solid var(--tertiary);
display: flex;
align-items: center;
justify-content: center;
padding: 1em;
margin: 1em 0;
flex-direction: column;
}
}
}
.placeholders-list {
display: flex;
align-items: center;
flex-wrap: wrap;
.placeholder-item {
border-radius: 3px;
font-size: $font-down-2;
margin: 0.5em 0.5em 0 0;
}
}
details[open] > summary:first-of-type ~ .btn-checked,
details.open > summary:first-of-type ~ .btn-checked {
display: inline-flex;
}

View File

@ -0,0 +1,368 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
ar:
js:
discourse_automation:
title: الأتمتة
create: إنشاء
update: تحديث
select_script: حدِّد برنامجًا نصيًا
select_trigger: حدِّد مشغلًا
confirm_automation_reset: سيؤدي هذا الإجراء إلى إعادة تعيين البرنامج النصي وتشغيل الخيارات، وسيتم حفظ الحالة الجديدة، هل تريد المتابعة؟
confirm_automation_trigger: سيؤدي هذا الإجراء إلى تشغيل الأتمتة، هل تريد المتابعة؟
no_automation_yet: لم تُنشئ أي أتمتة حتى الآن.
edit_automation:
trigger_section:
forced: يتم فرض هذا المشغِّل بواسطة البرنامج النصي.
next_pending_automation: "سيتم تشغيل الأتمتة التالية في: %{date}"
trigger_now: "التشغيل الآن"
title: متى/ماذا...
fields_section:
title: خيارات البرنامج النصي
destroy_automation:
confirm: "هل تريد بالتأكيد حذف `%{name}`؟"
fields:
key_value:
label:
zero: تعديل التكوين (%{count})
one: تعديل التكوين (%{count})
two: تعديل التكوين (%{count})
few: تعديل التكوين (%{count})
many: تعديل التكوين (%{count})
other: تعديل التكوين (%{count})
user:
label: المستخدم
pm:
title:
label: العنوان
raw:
label: النص
pms:
confirm_remove_pm: "هل تريد بالتأكيد إزالة هذه الرسالة الشخصية؟"
placeholder_title: عنوان الرسالة الشخصية
add_pm: إضافة رسالة شخصية
no_pm_created: لم تُنشئ أي رسالة شخصية بعد. سيتم إرسال الرسائل الشخصية بعد تشغيل الأتمتة.
title:
label: العنوان
raw:
label: النص
delay:
label: التأخير (بالدقائق)
prefers_encrypt:
label: يشفِّر الرسائل الشخصية إذا كانت متاحة
group:
label: المجموعة
text:
label: النص
triggerables:
not_found: تعذَّر العثور على المشغِّل `%{trigger}` للأتمتة `%{automation}`، تأكَّد من تثبيت المكوِّن الإضافي ذي الصلة
user_badge_granted:
fields:
badge:
label: شارة
only_first_grant:
label: على المنحة الأولى فقط
stalled_topic:
durations:
PT1H: "ساعة واحدة"
P1D: "يوم واحد"
P1W: "أسبوع واحد"
P2W: "أسبوعان"
P1M: "شهر واحد"
P3M: "ثلاثة أشهر"
P6M: "ستة أشهر"
P1Y: "عام واحد"
fields:
categories:
label: يقتصر على الفئات
tags:
label: يقتصر على الوسوم
stalled_after:
label: توقفت بعد
recurring:
every: كل
frequencies:
minute: دقيقة
hour: ساعة
day: يوم
weekday: يوم من أيام الأسبوع
week: أسبوع
month: شهر
year: عام
fields:
recurrence:
label: التكرار
start_date:
label: تاريخ البدء
stalled_wiki:
durations:
PT1H: "ساعة واحدة"
P1D: "يوم واحد"
P1W: "أسبوع واحد"
P2W: "أسبوعان"
P1M: "شهر واحد"
P3M: "ثلاثة أشهر"
P6M: "ستة أشهر"
P1Y: "عام واحد"
fields:
restricted_category:
label: مقيَّد إلى الفئة
stalled_after:
label: تأخير المشغِّل
description: يحدِّد التأخير بين تعديل Wiki الأخير ومشغِّل الأتمتة
retriggered_after:
label: التأخير في إعادة التشغيل
description: يحدِّد التأخير بين المشغِّل الأول والمشغِّل التالي، إذا لم يتم تعديل Wiki بعد أول مشغِّل
user_added_to_group:
fields:
joined_group:
label: مجموعة متتبَّعة
user_removed_from_group:
fields:
left_group:
label: مجموعة متتبَّعة
user_promoted:
fields:
restricted_group:
label: التقييد إلى المجموعة
trust_level_transition:
label: نقل مستوى الثقة
trust_levels:
ALL: "كل مستويات الثقة"
TL01: "مستوى الثقة 0 إلى المستوى الثقة 1"
TL12: "مستوى الثقة 1 إلى المستوى الثقة 2"
TL23: "مستوى الثقة 2 إلى المستوى الثقة 3"
TL34: "مستوى الثقة 3 إلى المستوى الثقة 4"
point_in_time:
fields:
execute_at:
label: التنفيذ في
topic:
fields:
restricted_topic:
label: معرِّف الموضوع
post_created_edited:
fields:
action_type:
label: نوع الإجراء
description: "اختياري، تقييد التشغيل إلى الأحداث التي تم إنشاؤها أو تحريرها فقط"
valid_trust_levels:
label: مستويات الثقة الصالحة
description: سيتم التشغيل فقط إذا تم إنشاء المنشور بواسطة مستخدم في مستويات الثقة هذه، ويتم تعيينه افتراضيًا على أي مستوى ثقة
restricted_category:
label: الفئة
description: اختياري، لن يتم تشغيله إلا إذا كان موضوع المشاركة ضمن هذه الفئة
restricted_group:
label: مجموعة
description: اختياري، لن يتم تشغيله إلا إذا كان موضوع المشاركة عبارة عن رسالة خاصة في البريد الوارد لهذه المجموعة
ignore_group_members:
label: تجاهل أعضاء المجموعة
description: تخطي المشغِّل إذا كان المرسل عضوًا في المجموعة المحدَّدة أعلاه
ignore_automated:
label: تجاهل الرسائل الآلية
description: تخطي المشغِّل إذا كان لدى المرسل بريد إلكتروني غير صحيح أو كان من مصدرٍ آلي. لا ينطبق ذلك إلا على المنشورات التي تم إنشاؤها عبر البريد الإلكتروني
first_post_only:
label: المنشور الأول فقط
description: لن يتم تشغيله إلا إذا كان المنشور هو أول منشور أنشأه المستخدم
first_topic_only:
label: الموضوع الأول فقط
description: لن يتم تشغيله إلا إذا كان الموضوع هو أول موضوع أنشأه المستخدم
created: تم إنشاؤها
edited: تم تحريرها
category_created_edited:
fields:
restricted_category:
label: الفئة الرئيسية
description: اختياري، يسمح بتقييد تنفيذ المشغِّل لهذه الفئة
pm_created:
fields:
restricted_user:
label: المستخدمون
description: لن يتم تشغيله إلا للرسائل الخاصة المُرسَلة إلى هذا المستخدم
restricted_group:
label: مجموعة
description: لن يتم تشغيله إلا للرسائل الخاصة المُرسَلة إلى هذه المجموعة
ignore_staff:
label: تجاهل فريق العمل
description: تخطي المشغِّل إذا كان المُرسل مستخدمًا من فريق العمل
ignore_group_members:
label: تجاهل أعضاء المجموعة
description: تخطي المشغِّل إذا كان المرسل عضوًا في المجموعة المحدَّدة أعلاه
ignore_automated:
label: تجاهل الرسائل الآلية
description: تخطي المشغِّل إذا كان لدى المرسل بريد إلكتروني غير صحيح أو كان من مصدرٍ آلي. لا ينطبق ذلك إلا على الرسائل الخاصة التي تم إنشاؤها عبر البريد الإلكتروني
valid_trust_levels:
label: مستويات الثقة الصالحة
description: سيتم التشغيل فقط إذا تم إنشاء المنشور بواسطة مستخدم في مستويات الثقة هذه، ويتم تعيينه افتراضيًا على أي مستوى ثقة
after_post_cook:
fields:
valid_trust_levels:
label: مستويات الثقة الصالحة
description: سيتم التشغيل فقط إذا تم إنشاء المنشور بواسطة مستخدم في مستويات الثقة هذه، ويتم تعيينه افتراضيًا على أي مستوى ثقة
restricted_category:
label: الفئة
description: اختياري، لن يتم تشغيله إلا إذا كان موضوع المشاركة ضمن هذه الفئة
restricted_tags:
label: الوسوم
description: اختياري، لن يتم تشغيله إلا إذا كان المنشور يحتوي على أي هذه الوسوم
scriptables:
not_found: تعذَّر العثور على البرنامج النصي `%{script}` للأتمتة `%{automation}`، تأكَّد من تثبيت المكوِّن الإضافي ذي الصلة
zapier_webhook:
fields:
webhook_url:
label: عنوان URL لخطاف الويب
description: "يتوقع عنوان URL صالحًا لخطاف ويب Zapier؛ على سبيل المثال: https://hooks.zapier.com/hooks/catch/xxx/yyy/"
auto_responder:
fields:
once:
label: مرة واحدة
description: يستجيب مرة واحدة فقط لكل موضوع
word_answer_list:
label: قائمة أزواج الكلمات/الإجابات
answering_user:
label: المستخدم المجيب
description: يتم تعيينه افتراضيًا إلى مستخدم النظام
auto_tag_topic:
fields:
tags:
label: الوسوم
description: قائمة الوسوم لإضافتها إلى الموضوع.
post:
fields:
creator:
label: المُنشئ
topic:
label: معرِّف الموضوع
post:
label: محتوى المنشور
group_category_notification_default:
fields:
group:
label: المجموعة
notification_level:
label: مستوى الإشعارات
update_existing_members:
label: تحديث الأعضاء الحاليين
description: يحدِّث مستوى الإشعارات لأعضاء المجموعة الحاليين
user_global_notice:
fields:
level:
label: المستوى
notice:
label: الإخطار
description: يقبل HTML، لا تملأ هذا بإدخال غير موثوق فيه!
levels:
warning: تحذير
info: المعلومات
success: تم بنجاح
error: خطأ
user_group_membership_through_badge:
fields:
badge_name:
label: اسم الشارة
group:
label: مجموعة
description: المجموعة المستهدفة. ستتم إضافة المستخدمين الذين لديهم الشارة المحدَّدة إلى هذه المجموعة
update_user_title_and_flair:
label: تحديث عنوان المستخدم والطابع
description: اختياري، تحديث عنوان المستخدم والطابع
remove_members_without_badge:
label: إزالة الأعضاء الحاليين دون شارة
description: اختياري، إزالة أعضاء المجموعة الحاليين دون الشارة المحدَّدة
badge:
label: شارة
description: تحديد الشارة
suspend_user_by_email:
fields:
suspend_until:
label: التعليق حتى (الإعداد الافتراضي)
reason:
label: السبب (الإعداد الافتراضي)
actor:
label: المستخدم
description: "المستخدم المسؤول عن التعليق (الإعداد الافتراضي: النظام)"
pin_topic:
fields:
pinnable_topic:
label: معرِّف الموضوع
pinned_globally:
label: مثبَّت بشكلٍ عام
pinned_until:
label: مثبَّت حتى
banner_topic:
fields:
topic_id:
label: معرِّف الموضوع
banner_until:
label: التحويل إلى لافتة حتى
user:
label: المستخدم
description: "المستخدم الذي يُنشئ البانر (الإعداد الافتراضي: النظام)"
flag_post_on_words:
fields:
words:
label: الكلمات المتحقَّق منها
topic_required_words:
fields:
words:
label: قائمة الكلمات المطلوبة
gift_exchange:
fields:
gift_exchangers_group:
label: اسم مجموعة المشاركين
giftee_assignment_messages:
label: الرسائل إلى مُرسل الهدية
send_pms:
add_a_pm_btn:
label: إضافة رسالة شخصية
fields:
receiver:
label: مستقبل الرسالة الشخصية
sendable_pms:
label: الرسائل الشخصية
sender:
label: مُرسل الرسائل الشخصية
close_topic:
fields:
topic:
label: معرِّف الموضوع
message:
label: الرسالة الختامية
description: "رسالة اختيارية لإظهارها في سجل \"تم إغلاق الموضوع\""
user:
label: المستخدم
description: "المستخدم الذي يغلق الموضوع (الافتراضي: النظام)"
add_user_to_group_through_custom_field:
fields:
custom_field_name:
label: "اسم الحقل المخصَّص للمستخدم"
models:
script:
name:
label: البرنامج النصي
trigger:
name:
label: المشغِّل
automation:
name:
label: الاسم
trigger:
label: المشغِّل
script:
label: البرنامج النصي
version:
label: الإصدار
enabled:
label: مفعَّل
disabled:
label: متوقف
placeholders:
label: العناصر النائبة
last_updated_at:
label: آخر تحديث
last_updated_by:
label: تم التحديث بواسطة

View File

@ -0,0 +1,7 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
be:

View File

@ -0,0 +1,7 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
bg:

View File

@ -0,0 +1,7 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
bs_BA:

View File

@ -0,0 +1,7 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
ca:

View File

@ -0,0 +1,7 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
cs:

View File

@ -0,0 +1,257 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
da:
js:
discourse_automation:
title: Automatisering
create: Opret
update: Opdater
select_script: Vælg et script
select_trigger: Vælg en udløser
no_automation_yet: Du har ikke oprettet nogen automatisering endnu.
edit_automation:
trigger_section:
trigger_now: "Udløs nu"
title: Hvornår/hvad...
fields_section:
title: Script indstillinger
destroy_automation:
confirm: "Er du sikker på, at du vil slette `%{name}`?"
fields:
user:
label: Bruger
pm:
title:
label: Titel
pms:
confirm_remove_pm: "Er du sikker på, at du vil fjerne denne PM?"
placeholder_title: PM titel
add_pm: Tilføj PM
no_pm_created: Du har ikke oprettet nogen PM endnu. PM'er vil blive sendt, når din automatisering er udløst.
title:
label: Titel
delay:
label: Forsinkelse (minutter)
prefers_encrypt:
label: Krypterer PM hvis tilgængelig
group:
label: Gruppe
text:
label: Tekst
triggerables:
user_badge_granted:
fields:
badge:
label: Emblem
stalled_topic:
durations:
PT1H: "En time"
P1D: "En dag"
P1W: "En uge"
P2W: "To uger"
P1M: "En måned"
P3M: "Tre måneder"
P6M: "Seks måneder"
P1Y: "Et år"
fields:
categories:
label: Begrænset til kategorier
tags:
label: Begrænset til mærker
stalled_after:
label: Gået i stå efter
recurring:
every: Hver
frequencies:
minute: minut
hour: time
day: dag
weekday: hverdag
week: uge
month: måned
year: år
fields:
start_date:
label: Start dato
stalled_wiki:
durations:
PT1H: "En time"
P1D: "En dag"
P1W: "En uge"
P2W: "To uger"
P1M: "En måned"
P3M: "Tre måneder"
P6M: "Seks måneder"
P1Y: "Et år"
fields:
restricted_category:
label: Begrænset til kategori
stalled_after:
label: Udløserforsinkelse
user_added_to_group:
fields:
joined_group:
label: Sporet gruppe
user_removed_from_group:
fields:
left_group:
label: Sporet gruppe
user_promoted:
fields:
restricted_group:
label: Begræns til gruppe
trust_level_transition:
label: Overgang til tillidsniveau
trust_levels:
ALL: "Alle tillidsniveauer"
TL01: "TL0 til TL1"
TL12: "TL1 til TL2"
TL23: "TL2 til TL3"
TL34: "TL3 til TL4"
topic:
fields:
restricted_topic:
label: Emne ID
post_created_edited:
fields:
action_type:
label: Handlingstype
valid_trust_levels:
label: Gyldige tillidsniveauer
restricted_category:
label: Kategori
restricted_group:
label: Gruppe
created: Skabt
edited: Redigeret
category_created_edited:
fields:
restricted_category:
label: Overordnet Kategori
pm_created:
fields:
restricted_group:
label: Gruppe
valid_trust_levels:
label: Gyldige tillidsniveauer
after_post_cook:
fields:
valid_trust_levels:
label: Gyldige tillidsniveauer
restricted_category:
label: Kategori
restricted_tags:
label: Mærker
scriptables:
zapier_webhook:
fields:
webhook_url:
label: Webhook URL
auto_responder:
fields:
once:
label: En Gang
auto_tag_topic:
fields:
tags:
label: Mærker
post:
fields:
topic:
label: Emne ID
group_category_notification_default:
fields:
group:
label: Gruppe
notification_level:
label: Notifikationsniveau
update_existing_members:
label: Opdater eksisterende medlemmer
description: Opdaterer meddelelsesniveauet for eksisterende gruppemedlemmer
user_global_notice:
fields:
level:
label: Niveau
notice:
label: Bemærk
levels:
warning: Advarsel
info: Info
success: Succes
error: Fejl
user_group_membership_through_badge:
fields:
badge_name:
label: Emblem Navn
group:
label: Gruppe
badge:
label: Emblem
suspend_user_by_email:
fields:
actor:
label: Bruger
pin_topic:
fields:
pinnable_topic:
label: Emne ID
pinned_globally:
label: Fastgjort globalt
pinned_until:
label: Fastgjort indtil
banner_topic:
fields:
topic_id:
label: Emne ID
user:
label: Bruger
description: "Brugeren, der opretter banneret (standard: system)"
topic_required_words:
fields:
words:
label: Liste over påkrævede ord
send_pms:
add_a_pm_btn:
label: Tilføj en PM
fields:
sendable_pms:
label: PM'er
close_topic:
fields:
topic:
label: Emne ID
message:
label: Lukkebesked
user:
label: Bruger
description: "Brugeren, der lukker emnet (standard: system)"
models:
script:
name:
label: Script
trigger:
name:
label: Udløser
automation:
name:
label: Navn
trigger:
label: Aftrækkeren
script:
label: Script
version:
label: Version
enabled:
label: Aktiveret
disabled:
label: Deaktiveret
placeholders:
label: Pladsholdere
last_updated_at:
label: Sidst opdateret
last_updated_by:
label: Opdateret af

View File

@ -0,0 +1,378 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
de:
js:
discourse_automation:
title: Automatisierung
create: Erstellen
update: Aktualisieren
select_script: Skript auswählen
select_trigger: Auslöser auswählen
confirm_automation_reset: Diese Aktion setzt die Skript- und Auslöseroptionen zurück, der neue Zustand wird gespeichert. Willst du fortfahren?
confirm_automation_trigger: Diese Aktion löst die Automatisierung aus. Willst du fortfahren?
no_automation_yet: Du hast noch keine Automatisierung erstellt.
edit_automation:
trigger_section:
forced: Dieser Auslöser wird vom Skript erzwungen.
next_pending_automation: "Die nächste Automatisierung wird ausgelöst am: %{date}"
trigger_now: "Jetzt auslösen"
title: Wann/Was 
fields_section:
title: Skriptoptionen
destroy_automation:
confirm: "Bist du sicher, dass du `%{name}` löschen willst?"
fields:
key_value:
label:
one: Konfiguration bearbeiten (%{count})
other: Konfiguration bearbeiten (%{count})
user:
label: Benutzer
pm:
title:
label: Titel
raw:
label: Body
pms:
confirm_remove_pm: "Bist du sicher, dass du diese PN entfernen möchtest?"
placeholder_title: PN-Titel
add_pm: PN hinzufügen
no_pm_created: Du hast noch keine PN erstellt. PN werden verschickt, sobald deine Automatisierung ausgelöst wird.
title:
label: Titel
raw:
label: Body
delay:
label: Verzögerung (Minuten)
prefers_encrypt:
label: Verschlüsselt PN, falls verfügbar
group:
label: Gruppe
text:
label: Text
triggerables:
not_found: Konnte den Auslöser `%{trigger}` für die Automatisierung `%{automation}` nicht finden. Stelle sicher, dass das zugehörige Plug-in installiert ist
user_badge_granted:
fields:
badge:
label: Abzeichen
only_first_grant:
label: Nur bei erster Vergabe
stalled_topic:
durations:
PT1H: "Eine Stunde"
P1D: "Ein Tag"
P1W: "Eine Woche"
P2W: "Zwei Wochen"
P1M: "Ein Monat"
P3M: "Drei Monate"
P6M: "Sechs Monate"
P1Y: "Ein Jahr"
fields:
categories:
label: Beschränkt auf Kategorien
tags:
label: Beschränkt auf Tags
stalled_after:
label: Festgefahren nach
recurring:
every: Jede(n/s)
frequencies:
minute: Minute
hour: Stunde
day: Tag
weekday: Wochentag
week: Woche
month: Monat
year: Jahr
fields:
recurrence:
label: Wiederholung
start_date:
label: Startdatum
stalled_wiki:
durations:
PT1H: "Eine Stunde"
P1D: "Ein Tag"
P1W: "Eine Woche"
P2W: "Zwei Wochen"
P1M: "Ein Monat"
P3M: "Drei Monate"
P6M: "Sechs Monate"
P1Y: "Ein Jahr"
fields:
restricted_category:
label: Auf Kategorie beschränkt
stalled_after:
label: Auslöseverzögerung
description: Definiert die Verzögerung zwischen der letzten Wiki-Bearbeitung und dem Auslöser der Automatisierung
retriggered_after:
label: Verzögerung der erneuten Auslösung
description: Definiert die Verzögerung zwischen dem ersten Auslöser und dem nächsten Auslöser, falls das Wiki nach dem ersten Auslöser noch nicht bearbeitet wurde
user_added_to_group:
fields:
joined_group:
label: Verfolgte Gruppe
user_removed_from_group:
fields:
left_group:
label: Verfolgte Gruppe
user_promoted:
fields:
restricted_group:
label: Auf Gruppe beschränken
trust_level_transition:
label: Vertrauensstufenübergang
trust_levels:
ALL: "Alle Vertrauensstufen"
TL01: "VS0 auf VS1"
TL12: "VS1 auf VS2"
TL23: "VS2 auf VS3"
TL34: "VS3 auf VS4"
point_in_time:
fields:
execute_at:
label: Ausführen um
topic:
fields:
restricted_topic:
label: Themen-ID
post_created_edited:
fields:
action_type:
label: Aktionstyp
description: "Optional, beschränke die Auslösung auf erstellte oder bearbeitete Ereignisse"
valid_trust_levels:
label: Gültige Vertrauensstufen
description: Wird nur ausgelöst, wenn ein Beitrag von einem Benutzer mit diesen Vertrauensstufen erstellt wird, standardmäßig eine beliebige Vertrauensstufe
restricted_category:
label: Kategorie
description: Optional. Wird nur ausgelöst, wenn das Thema des Beitrags in dieser Kategorie ist
restricted_group:
label: Gruppe
description: Optional. Wird nur ausgelöst, wenn das Thema des Beitrags eine private Nachricht im Posteingang dieser Gruppe ist
ignore_group_members:
label: Gruppenmitglieder ignorieren
description: Überspringe den Auslöser, wenn der Absender ein Mitglied der oben angegebenen Gruppe ist
ignore_automated:
label: Automatisierte Nachrichten ignorieren
description: Überspringe den Auslöser, wenn der Absender eine Noreply-E-Mail-Adresse nutzt oder die Nachricht von einer automatischen Quelle stammt. Gilt nur für Beiträge, die per E-Mail erstellt wurden
first_post_only:
label: Nur erster Beitrag
description: Wird nur ausgelöst, wenn der Beitrag der erste Beitrag ist, den ein Benutzer erstellt hat
first_topic_only:
label: Nur erstes Thema
description: Wird nur ausgelöst, wenn das Thema das erste Thema ist, das ein Benutzer erstellt hat
created: Erstellt
edited: Bearbeitet
user_updated:
fields:
user_profile:
label: Benutzerprofilfelder
description: Wird nur ausgelöst, wenn der Benutzer alle diese Felder ausgefüllt hat
custom_fields:
label: Benutzerdefinierte Felder
description: Wird nur ausgelöst, wenn der Benutzer alle diese Felder ausgefüllt hat
once_per_user:
label: Einmal pro Benutzer
description: Wird nur einmal pro Benutzer ausgelöst
category_created_edited:
fields:
restricted_category:
label: Übergeordnete Kategorie
description: Optional, ermöglicht es, die Ausführung des Auslösers auf diese Kategorie zu beschränken
pm_created:
fields:
restricted_user:
label: Benutzer
description: Wird nur für PN ausgelöst, die an diesen Benutzer gesendet werden
restricted_group:
label: Gruppe
description: Wird nur für PN ausgelöst, die an diese Gruppe gesendet werden
ignore_staff:
label: Teammitglieder ignorieren
description: Den Auslöser überspringen, falls der Absender ein Teammitglied ist
ignore_group_members:
label: Gruppenmitglieder ignorieren
description: Überspringe den Auslöser, wenn der Absender ein Mitglied der oben angegebenen Gruppe ist
ignore_automated:
label: Automatisierte Nachrichten ignorieren
description: Überspringe den Auslöser, wenn der Absender eine Noreply-E-Mail-Adresse nutzt oder die Nachricht von einer automatischen Quelle stammt. Gilt nur für PN, die per E-Mail erstellt wurden
valid_trust_levels:
label: Gültige Vertrauensstufen
description: Wird nur ausgelöst, wenn ein Beitrag von einem Benutzer mit diesen Vertrauensstufen erstellt wird, standardmäßig eine beliebige Vertrauensstufe
after_post_cook:
fields:
valid_trust_levels:
label: Gültige Vertrauensstufen
description: Wird nur ausgelöst, wenn ein Beitrag von einem Benutzer mit diesen Vertrauensstufen erstellt wird, standardmäßig eine beliebige Vertrauensstufe
restricted_category:
label: Kategorie
description: Optional. Wird nur ausgelöst, wenn das Thema des Beitrags in dieser Kategorie ist
restricted_tags:
label: Tags
description: Optional. Wird nur ausgelöst, wenn der Beitrag eines dieser Schlagwörter enthält
scriptables:
not_found: Konnte das Skript `%{script}` für die Automatisierung `%{automation}` nicht finden. Stelle sicher, dass das zugehörige Plug-in installiert ist
zapier_webhook:
fields:
webhook_url:
label: Webhook-URL
description: "Erwartet eine gültige Zapier-Webhook-URL, z. B.: https://hooks.zapier.com/hooks/catch/xxx/yyy/"
auto_responder:
fields:
once:
label: Einmal
description: Antwortet nur einmal pro Thema
word_answer_list:
label: Liste von Wort/Antwort-Paaren
description: "Definiert eine Liste von Schlüssel/Wert-Gruppen, wobei `key` der gesuchte Begriff und `value` der Text der Antwort ist. `key` kann leer gelassen werden, um unabhängig vom Inhalt auf alle Auslöser zu reagieren. Beachte, dass `value` `{{key}}` als Platzhalter akzeptiert, der in der Antwort durch den Wert von `key` ersetzt wird. Beachte weiterhin, dass `key` als Regex ausgewertet wird und Sonderzeichen wie `.` maskiert werden müssen, wenn du tatsächlich einen Punkt meinst, z. B.: `\\.`"
answering_user:
label: Antwortender Benutzer
description: Standardmäßig Systembenutzer
auto_tag_topic:
fields:
tags:
label: Schlagwörter
description: Liste der Schlagwörter, die dem Thema hinzugefügt werden sollen.
post:
fields:
creator:
label: Ersteller
post_creator_context: Der Ersteller des Beitrags
updated_user_context: Der aktualisierte Benutzer
topic:
label: Themen-ID
post:
label: Beitragsinhalt
group_category_notification_default:
fields:
group:
label: Gruppe
notification_level:
label: Benachrichtigungsstufe
update_existing_members:
label: Bestehende Mitglieder aktualisieren
description: Aktualisiert die Benachrichtigungsstufe für bestehende Gruppenmitglieder
user_global_notice:
fields:
level:
label: Level
notice:
label: Hinweis
description: Akzeptiert HTML. Gib nur vertrauenswürdige Inhalte ein!
levels:
warning: Warnung
info: Info
success: Erfolg
error: Fehler
user_group_membership_through_badge:
fields:
badge_name:
label: Abzeichenname
group:
label: Gruppe
description: Zielgruppe. Benutzer mit dem angegebenen Abzeichen werden dieser Gruppe hinzugefügt
update_user_title_and_flair:
label: Benutzertitel und Flair aktualisieren
description: Optional. Benutzertitel und Flair aktualisieren
remove_members_without_badge:
label: Bestehende Mitglieder ohne Abzeichen entfernen
description: Optional. Bestehende Gruppenmitglieder ohne das angegebene Abzeichen entfernen
badge:
label: Abzeichen
description: Abzeichen auswählen
suspend_user_by_email:
fields:
suspend_until:
label: Aussetzen bis (Standard)
reason:
label: Grund (Standard)
actor:
label: Benutzer
description: "Der für die Aussetzung verantwortliche Benutzer (Standard: System)"
pin_topic:
fields:
pinnable_topic:
label: Themen-ID
pinned_globally:
label: Global angeheftet
pinned_until:
label: Angeheftet bis
banner_topic:
fields:
topic_id:
label: Themen-ID
banner_until:
label: Banner machen bis
user:
label: Benutzer
description: "Der Benutzer, der das Banner erstellt (Standard: System)"
flag_post_on_words:
fields:
words:
label: Geprüfte Wörter
topic_required_words:
fields:
words:
label: Liste der erforderlichen Wörter
gift_exchange:
fields:
gift_exchangers_group:
label: Gruppenname der Teilnehmer
giftee_assignment_messages:
label: An den Schenkenden gesendete Nachrichten
send_pms:
add_a_pm_btn:
label: PN hinzufügen
fields:
receiver:
label: PN-Empfänger
sendable_pms:
label: PN
sender:
label: PN-Absender
close_topic:
fields:
topic:
label: Themen-ID
message:
label: Nachricht bzgl. Schließung
description: "Optionale Nachricht, die im Eintrag „Thema geschlossen“ angezeigt wird"
user:
label: Benutzer
description: "Der Benutzer, der das Thema schließt (Standard: System)"
add_user_to_group_through_custom_field:
fields:
custom_field_name:
label: "Name des benutzerdefinierten Benutzerfelds"
models:
script:
name:
label: Skript
trigger:
name:
label: Auslöser
automation:
name:
label: Name
trigger:
label: Auslöser
script:
label: Skript
version:
label: Version
enabled:
label: Aktiviert
disabled:
label: Deaktiviert
placeholders:
label: Platzhalter
last_updated_at:
label: Letzte Aktualisierung
last_updated_by:
label: Aktualisiert von

View File

@ -0,0 +1,7 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
el:

View File

@ -0,0 +1,374 @@
en:
js:
discourse_automation:
title: Automation
create: Create
update: Update
select_script: Select a script
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.
edit_automation:
trigger_section:
forced: This trigger is forced by script.
next_pending_automation: "Next automation will trigger at: %{date}"
trigger_now: "Trigger now"
title: When/What...
fields_section:
title: Script options
destroy_automation:
confirm: "Are you sure you want to delete `%{name}`?"
fields:
key_value:
label_without_count: "Configure"
label_with_count:
one: "Edit Configuration (%{count})"
other: "Edit Configuration (%{count})"
user:
label: User
pm:
title:
label: Title
raw:
label: Body
pms:
confirm_remove_pm: "Are you sure you want to remove this PM?"
placeholder_title: PM title
add_pm: Add PM
no_pm_created: You havent created any PM yet. PMs will be sent once your automation is triggered.
title:
label: Title
raw:
label: Body
delay:
label: Delay (minutes)
prefers_encrypt:
label: Encrypts PM if available
group:
label: Group
text:
label: Text
triggerables:
not_found: Couldnt find trigger `%{trigger}` for automation `%{automation}`, ensure the associated plugin is installed
user_badge_granted:
fields:
badge:
label: Badge
only_first_grant:
label: Only on first grant
stalled_topic:
durations:
PT1H: "One hour"
P1D: "One day"
P1W: "One week"
P2W: "Two weeks"
P1M: "One month"
P3M: "Three months"
P6M: "Six months"
P1Y: "One year"
fields:
categories:
label: Limited to categories
tags:
label: Limited to tags
stalled_after:
label: Stalled after
recurring:
every: Every
frequencies:
minute: minute
hour: hour
day: day
weekday: weekday
week: week
month: month
year: year
fields:
recurrence:
label: Recurrence
start_date:
label: Start date
stalled_wiki:
durations:
PT1H: "One hour"
P1D: "One day"
P1W: "One week"
P2W: "Two weeks"
P1M: "One month"
P3M: "Three months"
P6M: "Six months"
P1Y: "One year"
fields:
restricted_category:
label: Restricted to category
stalled_after:
label: Trigger delay
description: Defines delay between last wiki edit and automations trigger
retriggered_after:
label: Re-trigger delay
description: Defines delay between first trigger and next trigger, if wiki has still not been edited after first trigger
user_added_to_group:
fields:
joined_group:
label: Tracked group
user_removed_from_group:
fields:
left_group:
label: Tracked group
user_promoted:
fields:
restricted_group:
label: Restrict to group
trust_level_transition:
label: Trust level transition
trust_levels:
ALL: "All trust levels"
TL01: "TL0 to TL1"
TL12: "TL1 to TL2"
TL23: "TL2 to TL3"
TL34: "TL3 to TL4"
point_in_time:
fields:
execute_at:
label: Execute at
topic:
fields:
restricted_topic:
label: Topic ID
post_created_edited:
fields:
action_type:
label: Action type
description: "Optional, limit triggering to only created or edited events"
valid_trust_levels:
label: Valid trust levels
description: Will trigger only if post is created by user in these trust levels, defaults to any trust level
restricted_category:
label: Category
description: Optional, will trigger only if the post's topic is in this category
restricted_group:
label: Group
description: Optional, will trigger only if the post's topic is a private message in this group's inbox
ignore_group_members:
label: Ignore group members
description: Skip the trigger if sender is a member of the Group specified above
ignore_automated:
label: Ignore automated
description: Skip the trigger if the sender has a noreply email or is from an automated source. Only applies to posts created via email
first_post_only:
label: First post only
description: Will trigger only if the post is the first post a user created
first_topic_only:
label: First topic only
description: Will trigger only if the topic is the first topic a user created
created: Created
edited: Edited
user_updated:
fields:
user_profile:
label: User profile fields
description: Will trigger only if the user has filled all these fields
custom_fields:
label: User custom fields
description: Will trigger only if the user has filled all these fields
once_per_user:
label: Once per user
description: Will trigger only once per user
category_created_edited:
fields:
restricted_category:
label: Parent Category
description: Optional, allows to limit trigger execution to this category
pm_created:
fields:
restricted_user:
label: Users
description: Will trigger only for PMs sent to this user
restricted_group:
label: Group
description: Will trigger only for PMs sent to this group
ignore_staff:
label: Ignore staff
description: Skip the trigger if sender is a staff user
ignore_group_members:
label: Ignore group members
description: Skip the trigger if sender is a member of the Group specified above
ignore_automated:
label: Ignore automated
description: Skip the trigger if the sender has a noreply email or is from an automated source. Only applies to PMs created via email
valid_trust_levels:
label: Valid trust levels
description: Will trigger only if post is created by user in these trust levels, defaults to any trust level
after_post_cook:
fields:
valid_trust_levels:
label: Valid trust levels
description: Will trigger only if post is created by user in these trust levels, defaults to any trust level
restricted_category:
label: Category
description: Optional, will trigger only if the post's topic is in this category
restricted_tags:
label: Tags
description: Optional, will trigger only if the post has any of these tags
scriptables:
not_found: Couldnt find script `%{script}` for automation `%{automation}`, ensure the associated plugin is installed
zapier_webhook:
fields:
webhook_url:
label: Webhook URL
description: "Expects a valid Zapier webhook URL, eg: https://hooks.zapier.com/hooks/catch/xxx/yyy/"
auto_responder:
fields:
once:
label: Once
description: Only responds once by topic
word_answer_list:
label: List of word/answer pairs
description: "Defines a list of key/value groups, where the `key` is the searched term, and `value` the text of the reply. The `key` can be left blank to respond to all triggers, regardless of content. Note that `value` accepts `{{key}}` as a placeholder to be replaced by the value of `key` in the reply. Note that `key` will be evaluated as a regex, and special chars like `.` should be escaped if you actually mean a dot, eg: `\\.`"
answering_user:
label: Answering user
description: Defaults to System user
auto_tag_topic:
fields:
tags:
label: Tags
description: List of tags to add to the topic.
post:
fields:
creator:
label: Creator
post_creator_context: The creator of the post
updated_user_context: The updated user
topic:
label: Topic ID
post:
label: Post content
group_category_notification_default:
fields:
group:
label: Group
notification_level:
label: Notification level
update_existing_members:
label: Update existing members
description: Updates the notification level for existing group members
user_global_notice:
fields:
level:
label: Level
notice:
label: Notice
description: Accepts HTML, do not fill this with untrusted input!
levels:
warning: Warning
info: Info
success: Success
error: Error
user_group_membership_through_badge:
fields:
badge_name:
label: Badge Name
group:
label: Group
description: Target group. Users with the specified badge will be added to this group
update_user_title_and_flair:
label: Update user title and flair
description: Optional, Update user title and flair
remove_members_without_badge:
label: Remove existing members without badge
description: Optional, Remove existing group members without the specified badge
badge:
label: Badge
description: Select badge
suspend_user_by_email:
fields:
suspend_until:
label: Suspend until (default)
reason:
label: Reason (default)
actor:
label: User
description: "The user responsible for the suspension (default: system)"
pin_topic:
fields:
pinnable_topic:
label: Topic ID
pinned_globally:
label: Pinned globally
pinned_until:
label: Pinned until
banner_topic:
fields:
topic_id:
label: Topic ID
banner_until:
label: Make banner until
user:
label: User
description: "The user creating the banner (default: system)"
flag_post_on_words:
fields:
words:
label: Checked words
topic_required_words:
fields:
words:
label: Required words list
gift_exchange:
fields:
gift_exchangers_group:
label: Group name of participants
giftee_assignment_messages:
label: Messages sent to gifter
send_pms:
add_a_pm_btn:
label: Add a PM
fields:
receiver:
label: PM receiver
sendable_pms:
label: PMs
sender:
label: PMs sender
close_topic:
fields:
topic:
label: Topic ID
message:
label: Closing message
description: "Optional message to show on the Topic Closed record"
user:
label: User
description: "The user closing the topic (default: system)"
add_user_to_group_through_custom_field:
fields:
custom_field_name:
label: "User Custom Field name"
models:
script:
name:
label: Script
trigger:
name:
label: Trigger
automation:
name:
label: Name
trigger:
label: Trigger
script:
label: Script
version:
label: Version
enabled:
label: Enabled
disabled:
label: Disabled
placeholders:
label: Placeholders
last_updated_at:
label: Last update
last_updated_by:
label: Updated by

View File

@ -0,0 +1,7 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
en_GB:

View File

@ -0,0 +1,364 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
es:
js:
discourse_automation:
title: Automatización
create: Crear
update: Actualizar
select_script: Selecciona un script
select_trigger: Selecciona un activador
confirm_automation_reset: Esta acción restablecerá las opciones del script y del activador, y se guardará el nuevo estado. ¿Quieres continuar?
confirm_automation_trigger: Esta acción activará la automatización. ¿Quieres continuar?
no_automation_yet: Todavía no has creado ninguna automatización.
edit_automation:
trigger_section:
forced: Este activador está forzado por script.
next_pending_automation: "La siguiente automatización se activará el: %{date}"
trigger_now: "Activar ahora"
title: Cuándo/qué…
fields_section:
title: Opciones del script
destroy_automation:
confirm: "¿Seguro que quieres eliminar «%{name}»?"
fields:
key_value:
label:
one: Editar configuración (%{count})
other: Editar configuración (%{count})
user:
label: Usuario
pm:
title:
label: Título
raw:
label: Cuerpo
pms:
confirm_remove_pm: "¿Seguro que quieres eliminar este MP?"
placeholder_title: Título del mensaje
add_pm: Añadir mensaje
no_pm_created: Todavía no has creado ningún MP. Los MP se enviarán una vez que se active tu automatización.
title:
label: Título
raw:
label: Cuerpo
delay:
label: Retardo (en minutos)
prefers_encrypt:
label: Cifra el MP cuando sea posible
group:
label: Grupo
text:
label: Texto
triggerables:
not_found: No se ha podido encontrar el activador «%{trigger}» para la automatización «%{automation}», asegúrate de que el plugin asociado está instalado
user_badge_granted:
fields:
badge:
label: Insignia
only_first_grant:
label: Solo en la primera concesión
stalled_topic:
durations:
PT1H: "Una hora"
P1D: "Un día"
P1W: "Una semana"
P2W: "Dos semanas"
P1M: "Un mes"
P3M: "Tres meses"
P6M: "Seis meses"
P1Y: "Un año"
fields:
categories:
label: Limitado a las categorías
tags:
label: Limitado a las etiquetas
stalled_after:
label: Estancado tras
recurring:
every: Cada
frequencies:
minute: minuto
hour: hora
day: día
weekday: día laborable (lunes a viernes)
week: semana
month: mes
year: año
fields:
recurrence:
label: Frecuencia
start_date:
label: Fecha de inicio
stalled_wiki:
durations:
PT1H: "Una hora"
P1D: "Un día"
P1W: "Una semana"
P2W: "Dos semanas"
P1M: "Un mes"
P3M: "Tres meses"
P6M: "Seis meses"
P1Y: "Un año"
fields:
restricted_category:
label: Restringido a la categoría
stalled_after:
label: Retardo de activación
description: Define el retraso entre la última edición del wiki y la activación de la automatización
retriggered_after:
label: Retardo antes de volver a activar
description: Define el retraso entre la primera activación y la siguiente, si la wiki todavía no ha sido editada desde la primera activación
user_added_to_group:
fields:
joined_group:
label: Grupo rastreado
user_removed_from_group:
fields:
left_group:
label: Grupo rastreado
user_promoted:
fields:
restricted_group:
label: Restringir al grupo
trust_level_transition:
label: Transición a nivel de confianza
trust_levels:
ALL: "Todos los niveles de confianza"
TL01: "nivel 0 a nivel 1"
TL12: "nivel 1 a nivel 2"
TL23: "nivel 2 a nivel 3"
TL34: "nivel 3 a nivel 4"
point_in_time:
fields:
execute_at:
label: Ejecutar en
topic:
fields:
restricted_topic:
label: ID del tema
post_created_edited:
fields:
action_type:
label: Tipo de acción
description: "Opcional, limite la activación solo a eventos creados o editados"
valid_trust_levels:
label: Niveles de confianza válidos
description: Se activará solo si la publicación es creada por un usuario en estos niveles de confianza. Por defecto, cualquier nivel de confianza
restricted_category:
label: Categoría
description: Opcional, solo se activará si el tema de la publicación está en esta categoría
restricted_group:
label: Grupo
description: Opcional, solo se activará si el tema de la publicación es un mensaje privado en la bandeja de entrada de este grupo
ignore_group_members:
label: Ignorar a los miembros del grupo
description: Omitir el activador si el remitente es un miembro del Grupo especificado anteriormente
ignore_automated:
label: Ignorar la automatización
description: Omite el activador si el remitente tiene un correo electrónico de no respuesta o procede de una fuente automatizada. Solo se aplica a las publicaciones creadas por correo electrónico
first_post_only:
label: Solo la primera publicación
description: Se activará solo si la publicación es la primera que creó un usuario.
first_topic_only:
label: Solo el primer tema
description: Se activará solo si el tema es el primer tema que creó un usuario
created: Creado
edited: Editado
category_created_edited:
fields:
restricted_category:
label: Categoría principal
description: Opcional, permite limitar la ejecución del activador a esta categoría
pm_created:
fields:
restricted_user:
label: Usuarios
description: Se activará solo para los MP enviados a este usuario
restricted_group:
label: Grupo
description: Se activará solo para los MP enviados a este grupo
ignore_staff:
label: Ignorar personal
description: Omitir la activación si el remitente es un usuario del personal
ignore_group_members:
label: Ignorar a los miembros del grupo
description: Omitir el activador si el remitente es un miembro del Grupo especificado anteriormente
ignore_automated:
label: Ignorar la automatización
description: Omite el activador si el remitente tiene un correo electrónico de no respuesta o procede de una fuente automatizada. Solo se aplica a los MP creados por correo electrónico
valid_trust_levels:
label: Niveles de confianza válidos
description: Se activará solo si la publicación es creada por un usuario en estos niveles de confianza. Por defecto, cualquier nivel de confianza
after_post_cook:
fields:
valid_trust_levels:
label: Niveles de confianza válidos
description: Se activará solo si la publicación es creada por un usuario en estos niveles de confianza. Por defecto, cualquier nivel de confianza
restricted_category:
label: Categoría
description: Opcional, solo se activará si el tema de la publicación está en esta categoría
restricted_tags:
label: Etiquetas
description: Opcional, solo se activará si la publicación tiene alguna de estas etiquetas
scriptables:
not_found: No se ha podido encontrar el script «%{script}» para la automatización «%{automation}», asegúrate de que el plugin asociado está instalado
zapier_webhook:
fields:
webhook_url:
label: URL del webhook
description: "Espera una URL válida del webhook de Zapier, por ejemplo: https://hooks.zapier.com/hooks/catch/xxx/yyy/"
auto_responder:
fields:
once:
label: Una vez
description: Solo responde una vez por tema
word_answer_list:
label: Lista de pares de palabras/respuestas
answering_user:
label: Usuario que responde
description: Por defecto, usuario del sistema
auto_tag_topic:
fields:
tags:
label: Etiquetas
description: Lista de etiquetas para añadir al tema.
post:
fields:
creator:
label: Creador
topic:
label: ID del tema
post:
label: Contenido de la publicación
group_category_notification_default:
fields:
group:
label: Grupo
notification_level:
label: Nivel de notificación
update_existing_members:
label: Actualizar miembros existentes
description: Actualiza el nivel de notificación de los miembros del grupo existentes
user_global_notice:
fields:
level:
label: Nivel
notice:
label: Aviso
description: Acepta HTML, ¡no incluyas contenido inseguro o de terceros!
levels:
warning: Advertencia
info: Información
success: Éxito
error: Error
user_group_membership_through_badge:
fields:
badge_name:
label: Nombre de la insignia
group:
label: Grupo
description: Grupo objetivo. Los usuarios con la insignia especificada se añadirán a este grupo
update_user_title_and_flair:
label: Actualizar el título y el estilo del usuario
description: Opcional, actualizar el título y el estilo del usuario
remove_members_without_badge:
label: Eliminar miembros existentes sin insignia
description: Opcional, eliminar miembros del grupo existentes sin la insignia especificada
badge:
label: Insignia
description: Seleccionar insignia
suspend_user_by_email:
fields:
suspend_until:
label: Suspender hasta (por defecto)
reason:
label: Motivo (por defecto)
actor:
label: Usuario
description: "Usuario responsable de la suspensión (por defecto: system)"
pin_topic:
fields:
pinnable_topic:
label: ID del tema
pinned_globally:
label: Fijado globalmente
pinned_until:
label: Fijado hasta
banner_topic:
fields:
topic_id:
label: ID del tema
banner_until:
label: Hacer banner hasta
user:
label: Usuario
description: "Usuario que crea el banner (por defecto: system)"
flag_post_on_words:
fields:
words:
label: Palabras revisadas
topic_required_words:
fields:
words:
label: Lista de palabras necesarias
gift_exchange:
fields:
gift_exchangers_group:
label: Nombre del grupo de participantes
giftee_assignment_messages:
label: Mensajes enviados a quien regala
send_pms:
add_a_pm_btn:
label: Añadir un MP
fields:
receiver:
label: Destinatario del MP
sendable_pms:
label: MPs
sender:
label: Remitente del MP
close_topic:
fields:
topic:
label: ID del tema
message:
label: Mensaje de cierre
description: "Mensaje opcional para mostrar en el indicador al cerrar el tema"
user:
label: Usuario
description: "Usuario que cierra el tema (por defecto: system)"
add_user_to_group_through_custom_field:
fields:
custom_field_name:
label: "Nombre del campo personalizado del usuario"
models:
script:
name:
label: Script
trigger:
name:
label: Activador
automation:
name:
label: Nombre
trigger:
label: Activador
script:
label: Script
version:
label: Versión
enabled:
label: Activado
disabled:
label: Desactivado
placeholders:
label: Marcadores de posición
last_updated_at:
label: Última actualización
last_updated_by:
label: Actualizado por

View File

@ -0,0 +1,7 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
et:

View File

@ -0,0 +1,187 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
fa_IR:
js:
discourse_automation:
title: اتوماسیون
create: ایجاد
update: به‌روزرسانی
destroy_automation:
confirm: "آیا مطمئنید هستید که می‌خواهید «%{name}» را حذف کنید؟"
fields:
user:
label: کاربر
pm:
title:
label: عنوان
raw:
label: بدنه
pms:
title:
label: عنوان
raw:
label: بدنه
delay:
label: تاخیر (دقیقه)
group:
label: گروه
text:
label: متن
triggerables:
user_badge_granted:
fields:
badge:
label: نشان
stalled_topic:
durations:
PT1H: "یک ساعت"
P1D: "یک روز"
P1W: "یک هفته"
P2W: "دو هفته"
P1M: "یک ماه"
P3M: "سه ماه"
P6M: "شش ماه"
P1Y: "یک سال"
fields:
categories:
label: محدود شده به دسته‌بندی‌ها
tags:
label: محدود شده به برچسب‌ها
recurring:
every: هر
frequencies:
minute: دقیقه
hour: ساعت
day: روز
weekday: روز هفته
week: هفته
month: ماه
year: سال
fields:
start_date:
label: تاریخ شروع
stalled_wiki:
durations:
PT1H: "یک ساعت"
P1D: "یک روز"
P1W: "یک هفته"
P2W: "دو هفته"
P1M: "یک ماه"
P3M: "سه ماه"
P6M: "شش ماه"
P1Y: "یک سال"
fields:
restricted_category:
label: محدود به دسته‌بندی
topic:
fields:
restricted_topic:
label: شناسه موضوع
post_created_edited:
fields:
action_type:
label: نوع عمل
restricted_category:
label: دسته‌بندی
restricted_group:
label: گروه
created: ایجاد شده
edited: ویرایش شده
pm_created:
fields:
restricted_group:
label: گروه
after_post_cook:
fields:
restricted_category:
label: دسته‌بندی
restricted_tags:
label: برچسب‌ها
scriptables:
auto_responder:
fields:
once:
label: یک بار
description: فقط یک بار بر اساس موضوع پاسخ می‌دهد
auto_tag_topic:
fields:
tags:
label: برچسب‌ها
post:
fields:
creator:
label: ایجاد کننده
topic:
label: شناسه موضوع
post:
label: محتوای نوشته
group_category_notification_default:
fields:
group:
label: گروه
user_global_notice:
fields:
level:
label: سطح
levels:
warning: هشدار
info: اطلاعات
success: موفقیت
error: خطا
user_group_membership_through_badge:
fields:
group:
label: گروه
badge:
label: نشان
suspend_user_by_email:
fields:
reason:
label: دلیل (پیش‌فرض)
actor:
label: کاربر
pin_topic:
fields:
pinnable_topic:
label: شناسه موضوع
pinned_until:
label: سنجاق شده تا
banner_topic:
fields:
topic_id:
label: شناسه موضوع
user:
label: کاربر
close_topic:
fields:
topic:
label: شناسه موضوع
user:
label: کاربر
add_user_to_group_through_custom_field:
fields:
custom_field_name:
label: "نام فیلد سفارشی کاربر"
models:
script:
name:
label: اسکریپت
automation:
name:
label: نام
script:
label: اسکریپت
version:
label: نسخه
enabled:
label: فعال شد
disabled:
label: غیرفعال شد
last_updated_at:
label: آخرین به‌روزرسانی
last_updated_by:
label: به‌روز شده توسط

View File

@ -0,0 +1,364 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
fi:
js:
discourse_automation:
title: Automaatio
create: Luo
update: Päivitä
select_script: Valitse skripti
select_trigger: Valitse triggeri
confirm_automation_reset: Tämä toiminto nollaa skripti- ja triggeriasetukset, uusi tila tallennetaan. Haluatko jatkaa?
confirm_automation_trigger: Tämä toiminto käynnistää automaation, haluatko jatkaa?
no_automation_yet: Et ole vielä luonut automaatioita.
edit_automation:
trigger_section:
forced: Tämä triggeri on skriptin pakottama.
next_pending_automation: "Seuraava automaatio käynnistyy: %{date}"
trigger_now: "Käynnistä nyt"
title: Milloin/mitä...
fields_section:
title: Skriptiasetukset
destroy_automation:
confirm: "Haluatko varmasti poistaa kohteen \"%{name}\"?"
fields:
key_value:
label:
one: Muokkaa määritystä (%{count})
other: Muokkaa määritystä (%{count})
user:
label: Käyttäjä
pm:
title:
label: Otsikko
raw:
label: Teksti
pms:
confirm_remove_pm: "Haluatko varmasti poistaa tämän yksityisviestin?"
placeholder_title: Yksityisviestin otsikko
add_pm: Lisää yksityisviesti
no_pm_created: Et ole vielä luonut yksityisviestejä. Yksityisviestit lähetetään, kun automaatio käynnistyy.
title:
label: Otsikko
raw:
label: Teksti
delay:
label: Viive (minuutteina)
prefers_encrypt:
label: Salaa yksityisviestin, jos käytettävissä
group:
label: Ryhmä
text:
label: Teksti
triggerables:
not_found: Triggeriä %{trigger} automaatiolle %{automation} ei löytynyt. Varmista, että siihen liittyvä lisäosa on asennettuna.
user_badge_granted:
fields:
badge:
label: Kunniamerkki
only_first_grant:
label: Vain ensimmäisellä myöntämiskerralla
stalled_topic:
durations:
PT1H: "Yksi tunti"
P1D: "Yksi päivä"
P1W: "Yksi viikko"
P2W: "Kaksi viikkoa"
P1M: "Yksi kuukausi"
P3M: "Kolme kuukautta"
P6M: "Kuusi kuukautta"
P1Y: "Yksi vuosi"
fields:
categories:
label: Rajoitettu alueisiin
tags:
label: Rajoitettu tunnisteisiin
stalled_after:
label: Pysähtyi jälkeen
recurring:
every: Joka
frequencies:
minute: minuutti
hour: tunti
day: päivä
weekday: arkipäivä
week: viikko
month: kuukausi
year: vuosi
fields:
recurrence:
label: Toistuminen
start_date:
label: Alkamispäivä
stalled_wiki:
durations:
PT1H: "Yksi tunti"
P1D: "Yksi päivä"
P1W: "Yksi viikko"
P2W: "Kaksi viikkoa"
P1M: "Yksi kuukausi"
P3M: "Kolme kuukautta"
P6M: "Kuusi kuukautta"
P1Y: "Yksi vuosi"
fields:
restricted_category:
label: Rajoitettu alueeseen
stalled_after:
label: Triggerin viive
description: Määrittää viiveen viimeisimmän wiki-muokkauksen ja automaation triggerin välillä
retriggered_after:
label: Käynnistä viive uudelleen
description: Määrittää viiveen ensimmäisen ja seuraavan triggerin välillä, jos wikiä ei ole vieläkään muokattu ensimmäisen triggerin jälkeen
user_added_to_group:
fields:
joined_group:
label: Seurattu ryhmä
user_removed_from_group:
fields:
left_group:
label: Seurattu ryhmä
user_promoted:
fields:
restricted_group:
label: Rajoita ryhmään
trust_level_transition:
label: Luottamustasosiirtymä
trust_levels:
ALL: "Kaikki luottamustasot"
TL01: "LT0 → LT1"
TL12: "LT1 → LT2"
TL23: "LT2 → LT3"
TL34: "LT3 → LT4"
point_in_time:
fields:
execute_at:
label: Suorita
topic:
fields:
restricted_topic:
label: Ketjun tunnus
post_created_edited:
fields:
action_type:
label: Toiminnon tyyppi
description: "Valinnainen, rajaa käynnistyksen vain luomis- tai muokkaustapahtumiin"
valid_trust_levels:
label: Kelvolliset luottamustasot
description: Käynnistyy vain, jos viestin on luonut käyttäjä, jonka luottamustaso on jokin näistä, oletusarvo on mikä tahansa luottamustaso
restricted_category:
label: Alue
description: Valinnainen, käynnistyy vain, jos viestin ketju on tällä alueella
restricted_group:
label: Ryhmä
description: Valinnainen, käynnistyy vain, jos viestin ketju on yksityisviesti tämän ryhmän postilaatikossa
ignore_group_members:
label: Ohita ryhmän jäsenet
description: Ohita triggeri, jos lähettäjä on edellä määritellyn ryhmän jäsen
ignore_automated:
label: Ohita automaattiset
description: Ohita triggeri, jos lähettäjällä on sähköpostiosoite, joka ei ota vastaan vastauksia, tai se on automatisoidusta lähteestä. Koskee vain sähköpostitse luotuja viestejä.
first_post_only:
label: Vain ensimmäinen viesti
description: Käynnistyy vain, jos viesti on ensimmäinen käyttäjän luoma viesti
first_topic_only:
label: Vain ensimmäinen ketju
description: Käynnistyy vain, jos ketju on ensimmäinen käyttäjän luoma ketju
created: Luotu
edited: Muokattu
category_created_edited:
fields:
restricted_category:
label: Ylätason alue
description: Valinnainen, mahdollistaa triggerin suorittamisen rajoittamisen tähän alueeseen
pm_created:
fields:
restricted_user:
label: Käyttäjät
description: Käynnistyy vain tälle käyttäjälle lähetetyille yksityisviesteille
restricted_group:
label: Ryhmä
description: Käynnistyy vain tälle ryhmälle lähetetyille yksityisviesteille
ignore_staff:
label: Ohita henkilökunta
description: Ohita triggeri, jos lähettäjä on henkilökunnan käyttäjä
ignore_group_members:
label: Ohita ryhmän jäsenet
description: Ohita triggeri, jos lähettäjä on edellä määritellyn ryhmän jäsen
ignore_automated:
label: Ohita automaattiset
description: Ohita triggeri, jos lähettäjällä on sähköpostiosoite, joka ei ota vastaan vastauksia, tai se on automatisoidusta lähteestä. Koskee vain sähköpostitse luotuja yksityisviestejä.
valid_trust_levels:
label: Kelvolliset luottamustasot
description: Käynnistyy vain, jos viestin on luonut käyttäjä, jonka luottamustaso on jokin näistä; oletusarvo on mikä tahansa luottamustaso
after_post_cook:
fields:
valid_trust_levels:
label: Kelvolliset luottamustasot
description: Käynnistyy vain, jos viestin on luonut käyttäjä, jonka luottamustaso on jokin näistä; oletusarvo on mikä tahansa luottamustaso
restricted_category:
label: Alue
description: Valinnainen, käynnistyy vain, jos viestin ketju on tällä alueella
restricted_tags:
label: Tunnisteet
description: Valinnainen, käynnistyy vain, jos viestillä on jokin näistä tunnisteista
scriptables:
not_found: Skriptiä %{script} automaatiolle %{automation} ei löytynyt. Varmista, että siihen liittyvä lisäosa on asennettuna.
zapier_webhook:
fields:
webhook_url:
label: Webhookin URL-osoite
description: "Odottaa kelvollista Zapierin webhook-URL-osoitetta, esim. https://hooks.zapier.com/hooks/catch/xxx/yyy/"
auto_responder:
fields:
once:
label: Kerran
description: Vastaa vain kerran ketjua kohden
word_answer_list:
label: Sana ja vastaus -parien luettelo
answering_user:
label: Vastaava käyttäjä
description: Oletuksena järjestelmäkäyttäjä
auto_tag_topic:
fields:
tags:
label: Tunnisteet
description: Luettelo ketjuun lisättävistä tunnisteista.
post:
fields:
creator:
label: Luoja
topic:
label: Ketjun tunnus
post:
label: Viestin sisältö
group_category_notification_default:
fields:
group:
label: Ryhmä
notification_level:
label: Ilmoitustaso
update_existing_members:
label: Päivitä nykyiset jäsenet
description: Päivittää ryhmän nykyisten jäsenten ilmoitustason
user_global_notice:
fields:
level:
label: Taso
notice:
label: Ilmoitus
description: Hyväksyy HTML:n, älä täytä tätä epäluotettavalla syötteellä!
levels:
warning: Varoitus
info: Info
success: Onnistuminen
error: Virhe
user_group_membership_through_badge:
fields:
badge_name:
label: Kunniamerkin nimi
group:
label: Ryhmä
description: Kohderyhmä. Käyttäjät, joilla on määritetty kunniamerkki, lisätään tähän ryhmään
update_user_title_and_flair:
label: Päivitä käyttäjän nimike ja flair
description: Valinnainen, päivitä käyttäjän nimike ja flair
remove_members_without_badge:
label: Poista olemassa olevat jäsenet ilman kunniamerkkiä
description: Valinnainen, poista olemassa olevat ryhmän jäsenet ilman määritettyä kunniamerkkiä
badge:
label: Kunniamerkki
description: Valitse kunniaerkki
suspend_user_by_email:
fields:
suspend_until:
label: Keskeytä asti (oletus)
reason:
label: Syy (oletus)
actor:
label: Käyttäjä
description: "Keskeytyksestä vastaava käyttäjä (oletus: järjestelmä)"
pin_topic:
fields:
pinnable_topic:
label: Ketjun tunnus
pinned_globally:
label: Kiinnitetty yleisesti
pinned_until:
label: Kiinnitetty asti
banner_topic:
fields:
topic_id:
label: Ketjun tunnus
banner_until:
label: Tee banneriksi asti
user:
label: Käyttäjä
description: "Bannerin luova käyttäjä (oletus: järjestelmä)"
flag_post_on_words:
fields:
words:
label: Tarkistetut sanat
topic_required_words:
fields:
words:
label: Pakollisten sanojen luettelo
gift_exchange:
fields:
gift_exchangers_group:
label: Osallistujien ryhmän nimi
giftee_assignment_messages:
label: Viestit lähetetty lahjanantajalle
send_pms:
add_a_pm_btn:
label: Lisää yksityisviesti
fields:
receiver:
label: Yksityisviestin vastaanottaja
sendable_pms:
label: Yksityisviestit
sender:
label: Yksityisviestien lähettäjä
close_topic:
fields:
topic:
label: Ketjun tunnus
message:
label: Sulkemisviesti
description: "Valinnainen viesti, joka näytetään ketju suljettu -tietueessa"
user:
label: Käyttäjä
description: "Ketjun sulkeva käyttäjä (oletus: järjestelmä)"
add_user_to_group_through_custom_field:
fields:
custom_field_name:
label: "Käyttäjän mukautetun kentän nimi"
models:
script:
name:
label: Skripti
trigger:
name:
label: Triggeri
automation:
name:
label: Nimi
trigger:
label: Triggeri
script:
label: Skripti
version:
label: Versio
enabled:
label: Käytössä
disabled:
label: Ei käytössä
placeholders:
label: Paikkamerkit
last_updated_at:
label: Viimeisin päivitys
last_updated_by:
label: Päivittänyt

View File

@ -0,0 +1,364 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
fr:
js:
discourse_automation:
title: Automatisation
create: Créer
update: Actualiser
select_script: Sélectionner un script
select_trigger: Sélectionner un déclencheur
confirm_automation_reset: Cette action réinitialisera les options de script et de déclencheur, le nouvel état sera enregistré, voulez-vous continuer ?
confirm_automation_trigger: Cette action déclenchera l'automatisation, voulez-vous continuer ?
no_automation_yet: Vous n'avez pas encore créé d'automatisation.
edit_automation:
trigger_section:
forced: Ce déclencheur est forcé par le script.
next_pending_automation: "La prochaine automatisation se déclenchera le : %{date}"
trigger_now: "Déclencher maintenant"
title: Quand/quoi…
fields_section:
title: Options de script
destroy_automation:
confirm: "Voulez-vous vraiment supprimer « %{name} » ?"
fields:
key_value:
label:
one: Modifier la configuration (%{count})
other: Modifier la configuration (%{count})
user:
label: Utilisateur
pm:
title:
label: Titre
raw:
label: Corps
pms:
confirm_remove_pm: "Voulez-vous vraiment supprimer ce MP ?"
placeholder_title: Titre du MP
add_pm: Ajouter un MP
no_pm_created: Vous n'avez pas encore créé de MP. Les MP seront envoyés une fois votre automatisation déclenchée.
title:
label: Titre
raw:
label: Corps
delay:
label: Délai (minutes)
prefers_encrypt:
label: Chiffrer le MP si disponible
group:
label: Groupe
text:
label: Texte
triggerables:
not_found: Impossible de trouver le déclencheur « %{trigger} » pour l'automatisation « %{automation} », assurez-vous que l'extension associée est installée
user_badge_granted:
fields:
badge:
label: Badge
only_first_grant:
label: Uniquement lors de la première autorisation
stalled_topic:
durations:
PT1H: "Une heure"
P1D: "Un jour"
P1W: "Une semaine"
P2W: "Deux semaines"
P1M: "Un mois"
P3M: "Trois mois"
P6M: "Six mois"
P1Y: "Un an"
fields:
categories:
label: Limité aux catégories
tags:
label: Limité aux étiquettes
stalled_after:
label: Bloqué après
recurring:
every: Chaque
frequencies:
minute: minute
hour: heure
day: jour
weekday: jour de la semaine
week: semaine
month: mois
year: année
fields:
recurrence:
label: Récurrence
start_date:
label: Date de début
stalled_wiki:
durations:
PT1H: "Une heure"
P1D: "Un jour"
P1W: "Une semaine"
P2W: "Deux semaines"
P1M: "Un mois"
P3M: "Trois mois"
P6M: "Six mois"
P1Y: "Un an"
fields:
restricted_category:
label: Limité à la catégorie
stalled_after:
label: Délai de déclenchement
description: Définit le délai entre la dernière modification du wiki et le déclenchement de l'automatisation
retriggered_after:
label: Délai de redéclenchement
description: Définit le délai entre le premier déclencheur et le déclencheur suivant, si le wiki n'a toujours pas été modifié après le premier déclencheur
user_added_to_group:
fields:
joined_group:
label: Groupe suivi
user_removed_from_group:
fields:
left_group:
label: Groupe suivi
user_promoted:
fields:
restricted_group:
label: Restreindre au groupe
trust_level_transition:
label: Transition de niveau de confiance
trust_levels:
ALL: "Tous les niveaux de confiance"
TL01: "TL0 à TL1"
TL12: "TL1 à TL2"
TL23: "TL2 à TL3"
TL34: "TL3 à TL4"
point_in_time:
fields:
execute_at:
label: Exécuter à
topic:
fields:
restricted_topic:
label: ID de sujet
post_created_edited:
fields:
action_type:
label: Type d'action
description: "Facultatif, limitez le déclenchement aux seuls événements créés ou modifiés"
valid_trust_levels:
label: Niveaux de confiance valides
description: Se déclenchera uniquement si la publication est créée par l'utilisateur dans ces niveaux de confiance. Cette valeur est fixée par défaut sur tous les niveaux de confiance
restricted_category:
label: Catégorie
description: Facultatif, ne se déclenchera que si le sujet de la publication se trouve dans cette catégorie
restricted_group:
label: Groupe
description: Facultatif, se déclenchera uniquement si le sujet de la publication est un message privé dans la boîte de réception de ce groupe
ignore_group_members:
label: Ignorer les membres du groupe
description: Ignorer le déclencheur si l'expéditeur est membre du groupe spécifié ci-dessus
ignore_automated:
label: Ignorer l'automatisation
description: Ignorer le déclencheur si l'expéditeur a un e-mail de non-réponse ou provient d'une source automatique. Ne s'applique qu'aux messages créés par e-mail
first_post_only:
label: Premier message uniquement
description: Se déclenchera uniquement si la publication est la première publication créée par un utilisateur
first_topic_only:
label: Premier sujet uniquement
description: Se déclenchera uniquement si le sujet est le premier sujet créé par un utilisateur
created: Créé
edited: Édité
category_created_edited:
fields:
restricted_category:
label: Catégorie parente
description: Facultatif, permet de limiter l'exécution du déclencheur à cette catégorie
pm_created:
fields:
restricted_user:
label: Utilisateurs
description: Se déclenchera uniquement pour les MP envoyés à cet utilisateur
restricted_group:
label: Groupe
description: Se déclenchera uniquement pour les MP envoyés à ce groupe
ignore_staff:
label: Ignorer le responsable
description: Ignorer le déclencheur si l'expéditeur est un responsable
ignore_group_members:
label: Ignorer les membres du groupe
description: Ignorer le déclencheur si l'expéditeur est membre du groupe spécifié ci-dessus
ignore_automated:
label: Ignorer l'automatisation
description: Ignorer le déclencheur si l'expéditeur a un e-mail de non-réponse ou provient d'une source automatique. Ne s'applique qu'aux MP créés par e-mail
valid_trust_levels:
label: Niveaux de confiance valides
description: Se déclenchera uniquement si la publication est créée par l'utilisateur dans ces niveaux de confiance. Cette valeur est fixée par défaut sur tous les niveaux de confiance
after_post_cook:
fields:
valid_trust_levels:
label: Niveaux de confiance valides
description: Se déclenchera uniquement si la publication est créée par l'utilisateur dans ces niveaux de confiance. Cette valeur est fixée par défaut sur tous les niveaux de confiance
restricted_category:
label: Catégorie
description: Facultatif, ne se déclenchera que si le sujet de la publication se trouve dans cette catégorie
restricted_tags:
label: Étiquettes
description: Facultatif, ne se déclenchera que si la publication contient l'une de ces étiquettes
scriptables:
not_found: Impossible de trouver le script « %{script} » pour l'automatisation « %{automation} », assurez-vous que l'extension associée est installée
zapier_webhook:
fields:
webhook_url:
label: URL du webhook
description: "Attend une URL de webhook Zapier valide, par exemple : https://hooks.zapier.com/hooks/catch/xxx/yyy/"
auto_responder:
fields:
once:
label: Une fois
description: Ne répond qu'une seule fois par sujet
word_answer_list:
label: Liste des paires mot/réponse
answering_user:
label: Utilisateur qui répond
description: Par défaut, utilisateur système
auto_tag_topic:
fields:
tags:
label: Étiquettes
description: Liste des étiquettes à ajouter au sujet.
post:
fields:
creator:
label: Créateur
topic:
label: ID de sujet
post:
label: Contenu du message
group_category_notification_default:
fields:
group:
label: Groupe
notification_level:
label: Niveau de notification
update_existing_members:
label: Mettre à jour les membres existants
description: Met à jour le niveau de notification pour les membres du groupe existant
user_global_notice:
fields:
level:
label: Niveau
notice:
label: Avis
description: Accepte le HTML, ne le remplissez pas avec une entrée non fiable !
levels:
warning: Avertissement
info: Informations
success: Succès
error: Erreur
user_group_membership_through_badge:
fields:
badge_name:
label: Nom du badge
group:
label: Groupe
description: Groupe ciblé. Les utilisateurs ayant le badge spécifié seront ajoutés à ce groupe
update_user_title_and_flair:
label: Mettre à jour le titre et le style de l'utilisateur
description: Facultatif, mettre à jour le titre et le style de l'utilisateur
remove_members_without_badge:
label: Supprimer les membres existants sans badge
description: Facultatif, supprimer les membres du groupe existants sans le badge spécifié.
badge:
label: Badge
description: Sélectionner un badge
suspend_user_by_email:
fields:
suspend_until:
label: Suspendre jusqu'à (par défaut)
reason:
label: Motif (par défaut)
actor:
label: Utilisateur
description: "L'utilisateur responsable de la suspension (par défaut : système)"
pin_topic:
fields:
pinnable_topic:
label: ID de sujet
pinned_globally:
label: Épinglé globalement
pinned_until:
label: Épinglé jusqu'à
banner_topic:
fields:
topic_id:
label: ID de sujet
banner_until:
label: Faire une bannière jusqu'au
user:
label: Utilisateur
description: "L'utilisateur qui crée la bannière (par défaut : système)"
flag_post_on_words:
fields:
words:
label: Mots vérifiés
topic_required_words:
fields:
words:
label: Liste des mots requis
gift_exchange:
fields:
gift_exchangers_group:
label: Nom du groupe de participants
giftee_assignment_messages:
label: Messages envoyés au donateur
send_pms:
add_a_pm_btn:
label: Ajouter un MP
fields:
receiver:
label: Récepteur du MP
sendable_pms:
label: MP
sender:
label: Expéditeur du MP
close_topic:
fields:
topic:
label: ID de sujet
message:
label: Message de fermeture
description: "Message facultatif à afficher dans l'enregistrement Sujet fermé"
user:
label: Utilisateur
description: "L'utilisateur qui ferme le sujet (par défaut : système)"
add_user_to_group_through_custom_field:
fields:
custom_field_name:
label: "Nom du champ personnalisé de l'utilisateur"
models:
script:
name:
label: Script
trigger:
name:
label: Déclencheur
automation:
name:
label: Nom
trigger:
label: Déclencheur
script:
label: Script
version:
label: Version
enabled:
label: Activé
disabled:
label: Désactivé
placeholders:
label: Espaces réservés
last_updated_at:
label: Dernière mise à jour
last_updated_by:
label: Mis à jour par

View File

@ -0,0 +1,7 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
gl:

View File

@ -0,0 +1,362 @@
# WARNING: Never edit this file.
# It will be overwritten when translations are pulled from Crowdin.
#
# To work with us on translations, join this project:
# https://translate.discourse.org/
he:
js:
discourse_automation:
title: אוטומציה
create: יצירה
update: עדכון
select_script: בחירת סקריפט
select_trigger: בחירת גורם מפעיל
confirm_automation_trigger: פעולה זו תקפיץ את האוטומציה, להמשיך?
no_automation_yet: לא יצרת אף אוטומציה עדיין.
edit_automation:
trigger_section:
forced: הקפצה זו נאכפת על ידי סקריפט.
next_pending_automation: "האוטומציה הבאה תוקפץ ב־: %{date}"
trigger_now: "להקפיץ כעת"
title: מתי/מה…
fields_section:
title: אפשרויות סקריפט
destroy_automation:
confirm: "למחוק את `%{name}`?"
fields:
key_value:
label:
one: עריכת הגדרה (%{count})
two: עריכת הגדרות (%{count})
many: עריכת הגדרות (%{count})
other: עריכת הגדרות (%{count})
user:
label: משתמש
pm:
title:
label: כותרת
raw:
label: גוף
pms:
confirm_remove_pm: "להסיר את ההודעה הפרטית הזאת?"
placeholder_title: כותרת ההודעה הפרטית
add_pm: הוספת הודעה פרטית
title:
label: כותרת
raw:
label: גוף
delay:
label: השהיה (דקות)
prefers_encrypt:
label: מצפין הודעות פרטיות אם זמין
group:
label: קבוצה
text:
label: טקסט
triggerables:
not_found: לא ניתן למצוא את גורם ההקפצה `%{trigger}` לאוטומציה `%{automation}`, יש לוודא שהתוסף המשויך מותקן
user_badge_granted:
fields:
badge:
label: עיטור
only_first_grant:
label: רק עם ההענקה הראשונה
stalled_topic:
durations:
PT1H: "שעה"
P1D: "יום"
P1W: "שבוע"
P2W: "שבועיים"
P1M: "חודש"
P3M: "שלושה חודשים"
P6M: "שישה חודשים"
P1Y: "שנה"
fields:
categories:
label: מוגבל לקטגוריות
tags:
label: מוגבל לתגיות
recurring:
every: כל
frequencies:
minute: דקה
hour: שעה
day: יום
weekday: יום חול
week: שבוע
month: חודש
year: שנה
fields:
recurrence:
label: חזרתיות
start_date:
label: תאריך התחלה
stalled_wiki:
durations:
PT1H: "שעה"
P1D: "יום"
P1W: "שבוע"
P2W: "שבועיים"
P1M: "חודש"
P3M: "שלושה חודשים"
P6M: "שישה חודשים"
P1Y: "שנה"
fields:
restricted_category:
label: מוגבל לקטגוריה
stalled_after:
label: השהיית הזנקה
description: הגדרת השהייה בין עריכת הוויקי האחרונה והזנקת האוטומציה
retriggered_after:
label: השהיית הזנקה חוזרת
user_added_to_group:
fields:
joined_group:
label: קבוצה במעקב
user_removed_from_group:
fields:
left_group:
label: קבוצה במעקב
user_promoted:
fields:
restricted_group:
label: הגבלה לקבוצה
trust_level_transition:
label: מעבר בין דרגות אמון
trust_levels:
ALL: "כל דרגות האמון"
TL01: "דרגת אמון 0 ל־1"
TL12: "דרגת אמון 1 ל־2"
TL23: "דרגת אמון 2 ל־3"
TL34: "דרגת אמון 3 ל־4"
point_in_time:
fields:
execute_at:
label: לבצע ב־
topic:
fields:
restricted_topic:
label: מזהה נושא
post_created_edited:
fields:
action_type:
label: סוג פעולה
description: "כרשות, הגבלת ההקפצה לאירועים שנוצרו או נערכו בלבד"
valid_trust_levels:
label: דרגות אמון תקפות
description: יוקפץ רק אם הפוסט נוצר על ידי משתמש בדרגות האמון האלו, ברירת המחדל היא כל דרגת אמון
restricted_category:
label: קטגוריה
description: הגדרת רשות, להזניק רק אם נושא הפוסט בקטגוריה הזאת
restricted_group:
label: קבוצה
description: הגדרת רשות, להזניק רק אם נושא הפוסט הוא הודעה פרטית בתיבת הדואר הנכנס של הקבוצה הזאת
ignore_group_members:
label: התעלמות מחברי הקבוצה
description: לדלג על ההזנקה אם המוען הוא חבר בקבוצה שצוינה לעיל
ignore_automated:
label: התעלמות מאוטומטי
description: לדלג על ההזנקה אם לשולח יש כתובת דוא״ל noreply (לא להגיב) או שהוא ממקור אוטומטי. חל רק על פוסטים שנוצרו דרך דוא״ל
first_post_only:
label: פוסט ראשון בלבד
description: יוקפץ רק אם הפוסט הוא הפוסט הראשון שיצר המשתמש
first_topic_only:
label: נושא ראשון בלבד
description: יוקפץ רק אם הנושא הוא הנושא הראשון שיצר המשתמש
created: נוצרו
edited: נערכו
user_updated:
fields:
user_profile:
label: שדות פרופיל המשתמש
description: יוזנק רק אם המשתמש מילא את השדות האלו
custom_fields:
label: להשתמש בשדות מותאמים אישית
description: יוזנק רק אם המשתמש מילא את השדות האלו
once_per_user:
label: פעם אחת לכל משתמש
description: יוזנק רק פעם אחת לכל משתמש
category_created_edited:
fields:
restricted_category:
label: קטגוריית הורה
description: רשות, מאפשר להגביל הפעלות בקטגוריה הזאת
pm_created:
fields:
restricted_user:
label: משתמשים
description: יוזנק רק עבור הודעות פרטיות שנשלחות למשתמש הזה
restricted_group:
label: קבוצה
description: יוזנק רק עבור הודעות פרטיות שנשלחות לקבוצה הזאת
ignore_staff:
label: התעלמות מהסגל
description: לדלג על גורם ההפעלה אם זה משתמש מהסגל
ignore_group_members:
label: התעלמות מחברי הקבוצה
description: לדלג על ההזנקה אם המוען הוא חבר קבוצה שצוינה לעיל
ignore_automated:
label: התעלמות מאוטומטי
description: לדלג על ההזנקה אם לשולח יש כתובת דוא״ל noreply (לא להגיב) או שהוא ממקור אוטומטי. חל רק על הודעות פרטיות שנוצרו דרך דוא״ל
valid_trust_levels:
label: דרגות אמון תקפות
description: יוקפץ רק אם הפוסט נוצר על ידי משתמש בדרגות האמון האלו, ברירת המחדל היא כל דרגת אמון
after_post_cook:
fields:
valid_trust_levels:
label: דרגות אמון תקפות
description: יוקפץ רק אם הפוסט נוצר על ידי משתמש בדרגות האמון האלו, ברירת המחדל היא כל דרגת אמון
restricted_category:
label: קטגוריה
description: הגדרת רשות, להזניק רק אם נושא הפוסט בקטגוריה הזאת
restricted_tags:
label: תגיות
description: הגדרת רשות, להזניק רק אם לפוסט יש את אחת התגיות הבאות
scriptables:
not_found: לא ניתן למצוא את הסקריפט `%{script}` לאוטומציה `%{automation}`, יש לוודא שהתוסף המשויך מותקן
zapier_webhook:
fields:
webhook_url:
label: כתובת התליה
description: "אמורה להיות כתובת התליה תקפה של Zapier, למשל: https://hooks.zapier.com/hooks/catch/xxx/yyy/"
auto_responder:
fields:
once:
label: פעם אחת
description: מגיב פעם אחת בלבד בכל נושא
word_answer_list:
label: רשימה של צמדי מילה/תשובה
description: "מגדיר רשימה של קבוצות מפתח/ערך כאשר `מפתח` הוא הערך אחריו מחפשים, ו`ערך` הוא טקסט התגובה. את ה`מפתח` אפשר להשאיר ריק כדי להגיב לכל ההזנקות ללא תלות בתוכן. נא לשים לב ש`ערך` מקבל `{{key}}` בתור ממלא מקום להחלפת הערך של ה`מפתח` בתגובה. נא לשים לב ש`מפתח` יפוענח כביטוי רגולרי, ותווים מיוחדים כגון `.` דורשים החרגה אם הכוונה שלך הייתה נקודה, למשל: `\\.`"
answering_user:
label: משתמש שענה
description: ברירת המחדל היא משתמש המערכת
auto_tag_topic:
fields:
tags:
label: תגיות
description: רשימת תגיות להוספה לנושא.
post:
fields:
creator:
label: יוצר
post_creator_context: יוצר הפוסט
updated_user_context: המשתמש שעודכן
topic:
label: מזהה נושא
post:
label: תוכן הפוסט
group_category_notification_default:
fields:
group:
label: קבוצה
notification_level:
label: רמת התראה
update_existing_members:
label: עדכון חברים קיימים
description: מעדכן את רמת ההתראות עבור חברי קבוצה קיימים
user_global_notice:
fields:
level:
label: דרגה
notice:
label: הודעה
description: מקבל HTML, לא למלא את זה בקלט מפוקפק!
levels:
warning: אזהרה
info: מידע
success: הצלחה
error: שגיאה
user_group_membership_through_badge:
fields:
badge_name:
label: שם העיטור
group:
label: קבוצה
description: קבוצת יעד. משתמשים עם העיטור שצוין יתווספו לקבוצה הזאת.
update_user_title_and_flair:
label: עדכון כותרת וסמלון המשתמש
description: רשות, עדכון כותרת וסמלון המשתמש
remove_members_without_badge:
label: הסרת חברים קיימים ללא עיטור
description: כרשות, להסיר חברי קבוצה קיימים בלי עיטורים מסוימים
badge:
label: עיטור
description: בחירת עיטור
suspend_user_by_email:
fields:
suspend_until:
label: להשעות עד (ברירת מחדל)
reason:
label: סיבה (ברירת מחדל)
actor:
label: משתמש
description: "המשתמש האחראי להשעיה (ברירת מחדל: מערכת)"
pin_topic:
fields:
pinnable_topic:
label: מזהה נושא
pinned_globally:
label: נעוץ גלובלית
pinned_until:
label: נעוץ עד
banner_topic:
fields:
topic_id:
label: מזהה נושא
banner_until:
label: להציב ככרזה עד
user:
label: משתמש
description: "המשתמש יוצר את הכרזה (ברירת מחדל: מערכת)"
flag_post_on_words:
fields:
words:
label: מילים שנבדקות
topic_required_words:
fields:
words:
label: רשימת מילים נדרשות
send_pms:
add_a_pm_btn:
label: הוספת הודעה פרטית
close_topic:
fields:
topic:
label: מזהה נושא
message:
label: הודעת סגירה
description: "הודעת רשות שתופיע ברשומת סגירת הנושא"
user:
label: משתמש
description: "המשתמש שסגר את הנושא (ברירת מחדל: מערכת)"
add_user_to_group_through_custom_field:
fields:
custom_field_name:
label: "שם שדה מותאם אישית של משתמש"
models:
script:
name:
label: סקריפט
trigger:
name:
label: גורם מפעיל
automation:
name:
label: שם
trigger:
label: גורם מפעיל
script:
label: סקריפט
version:
label: גירסה
enabled:
label: מופעל
disabled:
label: מושבת
placeholders:
label: ממלאי מקום
last_updated_at:
label: עדכון אחרון
last_updated_by:
label: עודכן על ידי

Some files were not shown because too many files have changed in this diff Show More