This commit is contained in:
Simon Prévost 2022-12-16 08:56:46 -05:00
parent 38897cdc53
commit 4b142e7b23
50 changed files with 472 additions and 167 deletions

View File

@ -12,6 +12,23 @@ if config_env() == :dev do
config :accent, Accent.Repo, log: false
end
if config_env() == :test do
events = ~w(sync add_translations create_collaborator create_comment complete_review new_conflicts)
config :accent, Accent.Hook,
outbounds: [{Accent.Hook.Outbounds.Mock, events: events}],
inbounds: [{Accent.Hook.Inbounds.Mock, events: events}]
else
config :accent, Accent.Hook,
outbounds: [
{Accent.Hook.Outbounds.Discord, events: ~w(sync complete_review new_conflicts)},
{Accent.Hook.Outbounds.Email, events: ~w(create_collaborator create_comment)},
{Accent.Hook.Outbounds.Slack, events: ~w(sync complete_review new_conflicts)},
{Accent.Hook.Outbounds.Websocket, events: ~w(sync create_collaborator create_comment complete_review new_conflicts)}
],
inbounds: [{Accent.Hook.Inbounds.GitHub, events: ~w(sync)}]
end
config :accent, Accent.Endpoint,
render_errors: [accepts: ~w(json)],
pubsub_server: Accent.PubSub

View File

@ -26,15 +26,6 @@ config :accent, Accent.Endpoint,
]
]
config :accent, Accent.Hook,
outbounds: [
{Accent.Hook.Outbounds.Discord, events: ~w(sync)},
{Accent.Hook.Outbounds.Email, events: ~w(create_collaborator create_comment)},
{Accent.Hook.Outbounds.Slack, events: ~w(sync)},
{Accent.Hook.Outbounds.Websocket, events: ~w(sync create_collaborator create_comment)}
],
inbounds: [{Accent.Hook.Inbounds.GitHub, events: ~w(sync)}]
config :logger, :console,
format: "$metadata[$level] $message\n",
metadata: ~w(current_user graphql_operation hook_service hook_url)a

View File

@ -4,15 +4,6 @@ config :accent, Accent.Endpoint,
check_origin: false,
server: true
config :accent, Accent.Hook,
outbounds: [
{Accent.Hook.Outbounds.Discord, events: ~w(sync)},
{Accent.Hook.Outbounds.Email, events: ~w(create_collaborator create_comment)},
{Accent.Hook.Outbounds.Slack, events: ~w(sync)},
{Accent.Hook.Outbounds.Websocket, events: ~w(sync create_collaborator create_comment)}
],
inbounds: [{Accent.Hook.Inbounds.GitHub, events: ~w(sync)}]
config :logger, :console,
format: "$time $metadata[$level] $message\n",
level: :info,

View File

@ -10,12 +10,6 @@ config :ueberauth, Ueberauth, providers: [{:dummy, {Accent.Auth.Ueberauth.DummyS
config :accent, Oban, crontab: false, queues: false, plugins: false
events = ~w(sync merge create_collaborator create_comment)
config :accent, Accent.Hook,
outbounds: [{Accent.Hook.Outbounds.Mock, events: events}],
inbounds: [{Accent.Hook.Inbounds.Mock, events: events}]
config :goth, disabled: true
config :tesla, logger_enabled: false, adapter: Tesla.Mock

View File

@ -106,7 +106,11 @@ defmodule Accent.TelemetryUI do
defp absinthe_metrics do
absinthe_tag_values = fn metadata ->
operation_name = Enum.map_join(metadata.blueprint.operations, ",", & &1.name)
operation_name =
metadata.blueprint.operations
|> Enum.map(& &1.name)
|> Enum.uniq()
|> Enum.join(",")
%{operation_name: operation_name}
end
@ -147,8 +151,14 @@ defmodule Accent.TelemetryUI do
graphql_tag_values = fn metadata ->
operation_name =
case metadata.conn.params do
%{"_json" => json} -> Enum.map_join(json, ",", & &1["operationName"])
_ -> nil
%{"_json" => json} ->
json
|> Enum.map(& &1["operationName"])
|> Enum.uniq()
|> Enum.join(",")
_ ->
nil
end
%{operation_name: operation_name}

View File

@ -22,7 +22,7 @@ defmodule Accent.GraphQL.Resolvers.Collaborator do
case CollaboratorCreator.create(params) do
{:ok, collaborator} ->
Accent.Hook.outbound(%Hook.Context{
Hook.outbound(%Hook.Context{
event: "create_collaborator",
project_id: project.id,
user_id: info.context[:conn].assigns[:current_user].id,

View File

@ -9,7 +9,10 @@ defmodule Accent.GraphQL.Types.Integration do
enum :project_integration_event do
value(:sync, as: "sync")
value(:merge, as: "merge")
value(:new_conflicts, as: "new_conflicts")
value(:complete_review, as: "complete_review")
value(:create_collaborator, as: "create_collaborator")
value(:create_comment, as: "create_comment")
end
interface :project_integration do

View File

@ -1,20 +0,0 @@
defmodule Accent.Hook.Outbounds.Discord do
use Oban.Worker, queue: :hook
require EEx
EEx.function_from_file(:def, :sync, "lib/hook/outbounds/discord/templates/sync.eex", [:assigns], trim: true)
alias Accent.Hook.Outbounds.PostURL
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
context = Accent.Hook.Context.from_worker(args)
PostURL.perform("discord", &build_body/1).(context)
end
defp build_body(%{event: "sync", user: user, payload: %{"document_path" => document_path, "batch_operation_stats" => stats}}) do
%{
text: sync(user: user, document_path: document_path, stats: stats)
}
end
end

View File

@ -0,0 +1,9 @@
defmodule Accent.Hook.Outbounds.Discord do
use Oban.Worker, queue: :hook
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
context = Accent.Hook.Context.from_worker(args)
Accent.Hook.Outbounds.PostURL.perform("discord", context, Hook.Outbounds.Discord.Templates)
end
end

View File

@ -0,0 +1,22 @@
defmodule Hook.Outbounds.Discord.Templates do
require EEx
@sync_template """
**<%= @user %>** just synced a file: *<%= @document_path %>*
**Stats:**<%= for %{"action" => action, "count" => count} <- @stats do %>
<%= Phoenix.Naming.humanize(action) %>: *<%= count %>*<% end %>
"""
@new_conflicts_template """
**<%= @user %>** just added *<%= @new_conflicts_count %> strings* to review.
The project is currently **<%= @reviewed_count / @translations_count %>** reviewed (<%= @reviewed_count %>/<%= @translations_count %>)
"""
@complete_review_template """
**<%= @user %>** just finished reviewing all strings!
The project currently has <%= @translations_count %> reviewed translations.
"""
EEx.function_from_string(:def, :sync, @sync_template, [:assigns], trim: true)
EEx.function_from_string(:def, :new_conflicts, @new_conflicts_template, [:assigns], trim: true)
EEx.function_from_string(:def, :complete_review, @complete_review_template, [:assigns], trim: true)
end

View File

@ -1,4 +0,0 @@
**<%= @user.fullname %>** just synced a file: *<%= @document_path %>*
**Stats:**<%= for %{"action" => action, "count" => count} <- @stats do %>
<%= action %>: *<%= count %>*<% end %>

View File

@ -22,8 +22,8 @@ defmodule Accent.Hook.Outbounds.Email do
ProjectInviteEmail.create(emails, user, project)
end
defp build_email(emails, %{event: "create_comment", project: project, payload: payload}) do
CreateCommentEmail.create(emails, project, payload)
defp build_email(emails, %{event: "create_comment", project: project, user: user, payload: payload}) do
CreateCommentEmail.create(emails, user, project, payload)
end
defp fetch_emails(%{event: "create_collaborator", payload: payload}) do

View File

@ -4,23 +4,22 @@ defmodule Accent.Hook.Outbounds.PostURL do
import Ecto.Query, only: [where: 2]
alias Accent.Repo
alias Accent.User
def perform(service, build_body) do
fn context = %{event: event, project: project} ->
body = build_body.(context)
integrations = filter_service_integration_events(project, event, service)
urls = Enum.map(integrations, & &1.data.url)
def perform(service, context, templates) do
urls = fetch_service_integration_urls(context.project, context.event, service)
body = build_body(templates, context)
post_urls(urls, body, service)
end
post_urls(urls, body, service)
end
defp filter_service_integration_events(project, event, service) do
defp fetch_service_integration_urls(project, event, service) do
project
|> Ecto.assoc(:integrations)
|> where(service: ^service)
|> Repo.all()
|> Enum.filter(&(event in &1.events))
|> Enum.map(& &1.data.url)
end
defp post_urls(urls, body, service) do
@ -49,4 +48,34 @@ defmodule Accent.Hook.Outbounds.PostURL do
defp formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"]
defp formatted_diff(diff), do: [Integer.to_string(diff), "µs"]
defp build_body(templates, %{event: "sync", user: user, payload: %{"document_path" => document_path, "batch_operation_stats" => stats}}) do
assigns = %{
user: User.name_with_fallback(user),
document_path: document_path,
stats: stats
}
%{text: templates.sync(assigns)}
end
defp build_body(templates, %{event: "new_conflicts", user: user, payload: payload}) do
assigns = %{
user: User.name_with_fallback(user),
reviewed_count: payload["reviewed_count"],
new_conflicts_count: payload["new_conflicts_count"],
translations_count: payload["translations_count"]
}
%{text: templates.new_conflicts(assigns)}
end
defp build_body(templates, %{event: "complete_review", user: user, payload: payload}) do
assigns = %{
user: User.name_with_fallback(user),
translations_count: payload["translations_count"]
}
%{text: templates.complete_review(assigns)}
end
end

View File

@ -1,20 +0,0 @@
defmodule Accent.Hook.Outbounds.Slack do
use Oban.Worker, queue: :hook
require EEx
EEx.function_from_file(:def, :sync, "lib/hook/outbounds/slack/templates/sync.eex", [:assigns], trim: true)
alias Accent.Hook.Outbounds.PostURL
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
context = Accent.Hook.Context.from_worker(args)
PostURL.perform("slack", &build_body/1).(context)
end
defp build_body(%{event: "sync", user: user, payload: %{"document_path" => document_path, "batch_operation_stats" => stats}}) do
%{
text: sync(user: user, document_path: document_path, stats: stats)
}
end
end

View File

@ -0,0 +1,9 @@
defmodule Accent.Hook.Outbounds.Slack do
use Oban.Worker, queue: :hook
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
context = Accent.Hook.Context.from_worker(args)
Accent.Hook.Outbounds.PostURL.perform("slack", context, Hook.Outbounds.Slack.Templates)
end
end

View File

@ -0,0 +1,22 @@
defmodule Hook.Outbounds.Slack.Templates do
require EEx
@sync_template """
*<%= @user %>* just synced a file: _<%= @document_path %>_
*Stats:*<%= for %{"action" => action, "count" => count} <- @stats do %>
<%= Phoenix.Naming.humanize(action) %>: _<%= count %>_<% end %>
"""
@new_conflicts_template """
*<%= @user %>* just added _<%= @new_conflicts_count %> strings_ to review.
The project is currently *<%= @reviewed_count / @translations_count %>* reviewed (<%= @reviewed_count %>/<%= @translations_count %>)
"""
@complete_review_template """
*<%= @user %>* just finished reviewing all strings!
The project currently has <%= @translations_count %> reviewed translations.
"""
EEx.function_from_string(:def, :sync, @sync_template, [:assigns], trim: true)
EEx.function_from_string(:def, :new_conflicts, @new_conflicts_template, [:assigns], trim: true)
EEx.function_from_string(:def, :complete_review, @complete_review_template, [:assigns], trim: true)
end

View File

@ -1,4 +0,0 @@
*<%= @user.fullname %>* just synced a file: _<%= @document_path %>_
*Stats:*<%= for %{"action" => action, "count" => count} <- @stats do %>
<%= action %>: _<%= count %>_<% end %>

View File

@ -16,6 +16,7 @@ defmodule Langue.Utils.LineByLineHelper.Parser do
acc
|> Map.put(:line, line)
|> Map.put(:captures, Regex.named_captures(prop_line_regex, line))
|> IO.inspect
|> build_entry()
|> add_entries()
end

View File

@ -3,6 +3,7 @@ defmodule Movement.Persisters.Base do
alias Accent.{Operation, Repo}
alias Movement.Mappers.OperationsStats, as: StatMapper
alias Movement.Persisters.ProjectStateChangeWorker
alias Movement.Migrator
# Inserts operations by batch of 500 to prevent parameters
@ -37,9 +38,21 @@ defmodule Movement.Persisters.Base do
end
def execute(context) do
project_state_change_context = %{
project_id: context.assigns[:project] && context.assigns.project.id,
document_id: context.assigns[:document] && context.assigns.document.id,
master_revision_id: context.assigns[:master_revision] && context.assigns.master_revision.id,
revision_id: context.assigns[:revision] && context.assigns.revision.id,
version_id: context.assigns[:version] && context.assigns.version.id,
batch_operation_id: context.assigns[:batch_operation] && context.assigns.batch_operation.id,
user_id: context.assigns[:user_id],
previous_project_state: ProjectStateChangeWorker.get_project_state(context.assigns[:project])
}
context
|> persist_operations()
|> migrate_up_operations()
|> tap(fn _ -> Oban.insert(ProjectStateChangeWorker.new(project_state_change_context)) end)
end
@spec rollback(Movement.Context.t()) :: {Movement.Context.t(), [Operation.t()]}

View File

@ -0,0 +1,88 @@
defmodule Movement.Persisters.ProjectStateChangeWorker do
use Oban.Worker, queue: :hook
import Ecto.Query
alias Accent.Hook
alias Accent.Repo
alias Accent.Scopes.Project, as: ProjectScope
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
args = cast_args(args)
project_state = get_project_state(args.project)
if new_conflicts_to_review?(args.previous_project_state, project_state) do
Hook.outbound(%Hook.Context{
event: "new_conflicts",
project_id: args.project.id,
user_id: args.user.id,
payload: %{
reviewed_count: project_state.project.reviewed_count,
translations_count: project_state.project.translations_count,
new_conflicts_count: project_state.project.translations_count - args.previous_project_state.project.translations_count
}
})
end
if all_reviewed?(args.previous_project_state, project_state) do
Hook.outbound(%Hook.Context{
event: "complete_review",
project_id: args.project.id,
user_id: args.user.id,
payload: %{
translations_count: project_state.project.translations_count
}
})
end
:ok
end
defp new_conflicts_to_review?(previous_state, current_state) do
previous_state.project.conflicts_count < current_state.project.conflicts_count
end
defp all_reviewed?(previous_state, current_state) do
previous_state.project.reviewed_count !== previous_state.project.translations_count and
current_state.project.reviewed_count === current_state.project.translations_count
end
def get_project_state(nil), do: nil
def get_project_state(project) do
project =
Accent.Project
|> from(where: [id: ^project.id])
|> ProjectScope.with_stats()
|> Repo.one()
%{project: Map.take(project, ~w(translations_count reviewed_count conflicts_count)a)}
end
defp cast_project_state(args) do
%{
project: %{
translations_count: args["project"]["translations_count"],
reviewed_count: args["project"]["reviewed_count"],
conflicts_count: args["project"]["conflicts_count"]
}
}
end
defp cast_args(args) do
%{
previous_project_state: cast_project_state(args["previous_project_state"]),
project: get_record(Accent.Project, args["project_id"]),
document: get_record(Accent.Document, args["document_id"]),
master_revision: get_record(Accent.Revision, args["master_revision_id"]),
revision: get_record(Accent.Revision, args["revision_id"]),
version: get_record(Accent.Version, args["version_id"]),
batch_operation: get_record(Accent.Operation, args["batch_operation_id"]),
user: get_record(Accent.User, args["user_id"])
}
end
defp get_record(_schema, nil), do: nil
defp get_record(schema, id), do: Repo.get(schema, id)
end

View File

@ -57,7 +57,7 @@ defmodule Accent.MergeController do
{:ok, _} ->
Accent.Hook.outbound(%HookContext{
event: "merge",
event: "add_translations",
project_id: conn.assigns[:project].id,
user_id: conn.assigns[:current_user].id,
payload: %{

View File

@ -3,12 +3,12 @@ defmodule Accent.CreateCommentEmail do
import Accent.EmailViewConfigHelper, only: [mailer_from: 0, x_smtpapi_header: 0]
@spec create(list(String.t()), Accent.Project.t(), map()) :: Bamboo.Email.t()
def create(emails, project, comment) do
@spec create(list(String.t()), Accent.User.t(), Accent.Project.t(), map()) :: Bamboo.Email.t()
def create(emails, user, project, comment) do
base_email()
|> to(emails)
|> mailer_subject(project)
|> assign(:email, comment["user"]["email"])
|> assign(:user, user)
|> assign(:key, comment["translation"]["key"])
|> assign(:text, comment["text"])
|> assign(:translation_path, translation_path(project, comment["translation"]["id"]))

View File

@ -1,4 +1,4 @@
<p style="<%= paragraph_style("font-size": "14px", color: "#444") %>"><%= @email %> has commented on <code><%= @key %><code>:</p>
<p style="<%= paragraph_style("font-size": "14px", color: "#444") %>"><%= user_display_name(@user) %> has commented on <code><%= @key %><code>:</p>
<p style="<%= paragraph_style("font-size": "18px", color: "#222", "border-left": "3px solid #ddd", "padding-left": "10px") %>"><%= @text %></p>
<p style="<%= paragraph_style("font-size": "15px") %>">You can reply <a style="<%= link_style() %>" href="<%= webapp_url() %><%= @translation_path %>">here</a></p>

View File

@ -1,4 +1,4 @@
<%= @email %> has commented on <%= @key %>:
<%= user_display_name(@user) %> has commented on <%= @key %>:
---
<%= @text %>

View File

@ -1,4 +1,4 @@
<p style="<%= paragraph_style("font-size": "17px", color: "#444") %>"><%= @user.email %> has invited you to collaborate on the project "<%= @project.name %>"</p>
<p style="<%= paragraph_style("font-size": "17px", color: "#444") %>"><%= user_display_name(@user) %> has invited you to collaborate on the project "<%= @project.name %>"</p>
<p style="<%= paragraph_style("font-size": "15px") %>">If you already have an account (<%= @email %>), you can just <a style="<%= link_style() %>" href="<%= webapp_url() %>">login</a> and youll see the project.</p>

View File

@ -1,4 +1,4 @@
<%= @user.email %> has invited you to collaborate on the project "<%= @project.name %>".
<%= user_display_name(@user) %> has invited you to collaborate on the project "<%= @project.name %>".
If you already have an account (<%= @email %>), you can just login (<%= webapp_url() %>) and youll see the project.

View File

@ -3,4 +3,8 @@ defmodule Accent.EmailView do
import Accent.EmailViewStyleHelper
import Accent.EmailViewConfigHelper
def user_display_name(user) do
Accent.User.name_with_fallback(user)
end
end

View File

@ -11,11 +11,10 @@ defmodule AccentTest.CreateCommentEmail do
payload = %{
"text" => comment.text,
"user" => %{"email" => user.email},
"translation" => %{"id" => translation.id, "key" => translation.key}
}
email = Accent.CreateCommentEmail.create(emails, project, payload)
email = Accent.CreateCommentEmail.create(emails, user, project, payload)
assert email.to == emails
assert email.from == {"Accent", "accent-test@example.com"}

View File

@ -40,8 +40,8 @@ defmodule AccentTest.Hook.Outbounds.Discord do
**Test** just synced a file: *foo.json*
**Stats:**
new: *4*
conflict_on_proposed: *10*
New: *4*
Conflict on proposed: *10*
"""
|> String.trim_trailing()
})

View File

@ -34,7 +34,6 @@ defmodule AccentTest.Hook.Outbounds.Email do
payload = %{
"text" => comment.text,
"user" => %{"email" => user.email},
"translation" => %{"id" => translation.id, "key" => translation.key}
}
@ -42,7 +41,7 @@ defmodule AccentTest.Hook.Outbounds.Email do
_ = Email.perform(%Oban.Job{args: context})
refute_delivered_email(CreateCommentEmail.create(["comment@test.com"], project, payload))
refute_delivered_email(CreateCommentEmail.create(["comment@test.com"], user, project, payload))
end
test "comment", %{project: project, translation: translation, user: user} do
@ -52,7 +51,6 @@ defmodule AccentTest.Hook.Outbounds.Email do
payload = %{
"text" => comment.text,
"user" => %{"email" => commenter.email},
"translation" => %{"id" => translation.id, "key" => translation.key}
}
@ -60,7 +58,7 @@ defmodule AccentTest.Hook.Outbounds.Email do
_ = Email.perform(%Oban.Job{args: context})
assert_delivered_email(CreateCommentEmail.create(["foo@test.com"], project, payload))
assert_delivered_email(CreateCommentEmail.create(["foo@test.com"], commenter, project, payload))
end
test "collaborator", %{project: project, user: user} do

View File

@ -46,8 +46,8 @@ defmodule AccentTest.Hook.Outbounds.Slack do
*Test* just synced a file: _foo.json_
*Stats:*
new: _4_
conflict_on_proposed: _10_
New: _4_
Conflict on proposed: _10_
"""
|> String.trim_trailing()
})

View File

@ -0,0 +1,120 @@
defmodule Movement.Persisters.ProjectStateChangeWorkerTest do
use Accent.RepoCase
use Oban.Testing, repo: Accent.Repo
alias Movement.Persisters.ProjectStateChangeWorker, as: Worker
alias Accent.{
Document,
Language,
ProjectCreator,
Repo,
Translation,
User
}
setup do
user = Repo.insert!(%User{email: "test@test.com"})
language = Repo.insert!(%Language{name: "English", slug: Ecto.UUID.generate()})
{:ok, project} = ProjectCreator.create(params: %{main_color: "#f00", name: "My project", language_id: language.id}, user: user)
revision = project |> Repo.preload(:revisions) |> Map.get(:revisions) |> hd()
document = Repo.insert!(%Document{project_id: project.id, path: "test", format: "json"})
{:ok, [revision: revision, document: document, project: project, user: user]}
end
test "noop", %{project: project} do
args = %{
"project_id" => project.id,
"previous_project_state" => %{
"project" => %{
"translations_count" => 0,
"reviewed_count" => 0,
"conflicts_count" => 0
}
}
}
Worker.perform(%Oban.Job{args: args})
refute_enqueued(worker: Accent.Hook.Outbounds.Mock)
end
test "new_conflicts", %{user: user, project: project, revision: revision, document: document} do
%Translation{
key: "a",
proposed_text: "A",
conflicted: true,
corrected_text: "Test",
revision_id: revision.id,
document_id: document.id
}
|> Repo.insert!()
args = %{
"project_id" => project.id,
"user_id" => user.id,
"previous_project_state" => %{
"project" => %{
"translations_count" => 0,
"reviewed_count" => 0,
"conflicts_count" => 0
}
}
}
Worker.perform(%Oban.Job{args: args})
assert_enqueued(
worker: Accent.Hook.Outbounds.Mock,
args: %{
"event" => "new_conflicts",
"payload" => %{
"reviewed_count" => 0,
"translations_count" => 1,
"new_conflicts_count" => 1
},
"project_id" => project.id,
"user_id" => user.id
}
)
end
test "complete_review", %{user: user, project: project, revision: revision, document: document} do
%Translation{
key: "a",
proposed_text: "A",
conflicted: false,
corrected_text: "Test",
revision_id: revision.id,
document_id: document.id
}
|> Repo.insert!()
args = %{
"project_id" => project.id,
"user_id" => user.id,
"previous_project_state" => %{
"project" => %{
"translations_count" => 1,
"reviewed_count" => 0,
"conflicts_count" => 1
}
}
}
Worker.perform(%Oban.Job{args: args})
assert_enqueued(
worker: Accent.Hook.Outbounds.Mock,
args: %{
"event" => "complete_review",
"payload" => %{
"translations_count" => 1
},
"project_id" => project.id,
"user_id" => user.id
}
)
end
end

View File

@ -51,7 +51,7 @@ defmodule AccentTest.MergeController do
assert_enqueued(
worker: Accent.Hook.Outbounds.Mock,
args: %{
"event" => "merge",
"event" => "add_translations",
"payload" => %{"language_name" => "french", "merge_type" => nil},
"project_id" => project.id,
"user_id" => user.id
@ -102,7 +102,7 @@ defmodule AccentTest.MergeController do
assert_enqueued(
worker: Accent.Hook.Outbounds.Mock,
args: %{
"event" => "merge",
"event" => "add_translations",
"payload" => %{"language_name" => "french", "merge_type" => nil},
"project_id" => project.id,
"user_id" => user.id
@ -134,7 +134,7 @@ defmodule AccentTest.MergeController do
assert_enqueued(
worker: Accent.Hook.Outbounds.Mock,
args: %{
"event" => "merge",
"event" => "add_translations",
"payload" => %{"language_name" => "french", "merge_type" => nil},
"project_id" => project.id,
"user_id" => user.id

View File

@ -0,0 +1,7 @@
import {helper} from '@ember/component/helper';
const arrayIncludes = ([container, item]: [any[], any]) => {
return container.includes(item);
};
export default helper(arrayIncludes);

View File

@ -455,7 +455,9 @@
"events": {
"title": "Which events would you like to trigger this webhook?",
"options": {
"sync": "Sync"
"sync": "Sync with any changes",
"new_conflicts": "New strings to review",
"complete_review": "Project is 100% reviewed"
}
}
}

View File

@ -39,8 +39,7 @@
left: 1px;
transition: 0.2s ease-in-out;
transition-property: color, opacity, background;
padding: 4px 12px 3px;
margin-bottom: 6px;
padding: 7px 12px 6px;
text-decoration: none;
font-weight: 600;
font-size: 13px;
@ -56,6 +55,7 @@
.list-item-link-icon {
transform: scale(1);
opacity: 1;
}
.list-item-link-text {
@ -70,6 +70,7 @@
.list-item-link-icon {
transform: scale(1);
opacity: 1;
}
.list-item-link-text {
@ -89,10 +90,11 @@
.list-item-link-icon {
transition: 0.2s ease-in-out;
transition-property: fill;
transition-property: fill, opacity;
display: inline-block;
width: 13px;
height: 13px;
opacity: 0.6;
}
@media (max-width: 800px) {

View File

@ -164,18 +164,18 @@
</LinkTo>
</li>
{{/if}}
</div>
<li local-class="list-item">
<LinkTo
@route="logged-in.project.edit"
@model={{@project.id}}
local-class="list-item-link"
>
{{inline-svg "/assets/gear.svg" local-class="list-item-link-icon"}}
<span local-class="list-item-link-text">
{{t "components.project_navigation.settings_link_title"}}
</span>
</LinkTo>
</li>
<li local-class="list-item">
<LinkTo
@route="logged-in.project.edit"
@model={{@project.id}}
local-class="list-item-link"
>
{{inline-svg "/assets/gear.svg" local-class="list-item-link-icon"}}
<span local-class="list-item-link-text">
{{t "components.project_navigation.settings_link_title"}}
</span>
</LinkTo>
</li>
</div>
</ul>

View File

@ -54,7 +54,7 @@ export default class IntegrationsForm extends Component<Args> {
url: string;
@tracked
syncChecked: boolean;
events: string[];
@tracked
repository: string;
@ -93,10 +93,6 @@ export default class IntegrationsForm extends Component<Args> {
return `project-settings/integrations/form/${this.service.toLowerCase()}`;
}
get events() {
return this.syncChecked ? ['SYNC'] : [];
}
@action
didUpdateIntegration() {
if (this.args.integration) {
@ -117,8 +113,7 @@ export default class IntegrationsForm extends Component<Args> {
this.service = this.integration.service || this.services[0];
this.url = this.integration.data.url;
this.syncChecked =
this.integration.events && this.integration.events.includes('SYNC');
this.events = this.integration.events;
this.repository = this.integration.data.repository;
this.token = this.integration.data.token;
this.defaultRef = this.integration.data.defaultRef;
@ -140,8 +135,8 @@ export default class IntegrationsForm extends Component<Args> {
}
@action
setSyncChecked(syncChecked: boolean) {
this.syncChecked = syncChecked;
setEventsChecked(events: string[]) {
this.events = events;
}
@action

View File

@ -1,18 +1,36 @@
import {inject as service} from '@ember/service';
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {tracked} from '@glimmer/tracking';
import IntlService from 'ember-intl/services/intl';
interface Args {
title: string;
syncChecked: boolean;
syncCheckLabel: string;
onChangeSyncChecked: (checked: boolean) => void;
events: string[];
onChangeEventsChecked: (events: string[]) => void;
}
export default class DataControlCheckboxes extends Component<Args> {
@action
changeSyncChecked(event: Event) {
const target = event.target as HTMLInputElement;
@service('intl')
intl: IntlService;
this.args.onChangeSyncChecked(target.checked);
allEvents = [
{value: 'SYNC', label: 'components.project_settings.integrations.events.options.sync'},
{value: 'NEW_CONFLICTS', label: 'components.project_settings.integrations.events.options.new_conflicts'},
{value: 'COMPLETE_REVIEW', label: 'components.project_settings.integrations.events.options.complete_review'},
];
@tracked
selectedEvents: Set<string> = new Set(this.args.events);
@action
changeEventChecked(event: string) {
if (this.selectedEvents.has(event)) {
this.selectedEvents.delete(event);
} else {
this.selectedEvents.add(event);
}
this.args.onChangeEventsChecked(Array.from(this.selectedEvents));
}
}

View File

@ -6,12 +6,21 @@
align-items: center;
margin-right: 10px;
border: 1px solid var(--background-light-highlight);
padding: 3px 6px;
padding: 4px 6px;
border-radius: 3px;
background: var(--background-light);
background: var(--input-background);
cursor: pointer;
transition: 0.2s ease-in-out;
transition-property: background;
&:hover,
&:focus {
background: var(--background-light);
}
input {
margin-right: 5px;
cursor: pointer;
}
}
}

View File

@ -3,12 +3,14 @@
{{@title}}
</h3>
<label local-class="checkbox">
<Input
@type="checkbox"
@checked={{@syncChecked}}
{{on "change" this.changeSyncChecked}}
/>
{{@syncCheckLabel}}
</label>
{{#each this.allEvents as |event|}}
<label local-class="checkbox">
<Input
@type="checkbox"
@checked={{if (array-includes @events event.value) 'checked'}}
{{on "change" (fn this.changeEventChecked event.value)}}
/>
{{t event.label}}
</label>
{{/each}}
</div>

View File

@ -8,9 +8,9 @@ interface Args {
errors: any;
url: any;
project: any;
syncChecked: any;
events: any;
onChangeUrl: (url: string) => void;
onChangeSyncChecked: (syncChecked: boolean) => void;
onChangeEventsChecked: (events: string[]) => void;
onChangeRepository: (repository: string) => void;
onChangeToken: (token: string) => void;
onChangeDefaultRef: (defaultRef: string) => void;

View File

@ -10,7 +10,6 @@
<ProjectSettings::Integrations::Form::DataControlCheckboxes
@title={{t "components.project_settings.integrations.events.title"}}
@syncCheckLabel={{t "components.project_settings.integrations.events.options.sync"}}
@syncChecked={{@syncChecked}}
@onChangeSyncChecked={{@onChangeSyncChecked}}
@events={{@events}}
@onChangeEventsChecked={{@onChangeEventsChecked}}
/>

View File

@ -10,9 +10,7 @@ interface Args {
errors: any;
url: any;
project: any;
syncChecked: any;
onChangeUrl: (url: string) => void;
onChangeSyncChecked: (syncChecked: boolean) => void;
onChangeRepository: (repository: string) => void;
onChangeToken: (token: string) => void;
onChangeDefaultRef: (defaultRef: string) => void;

View File

@ -8,9 +8,9 @@ interface Args {
errors: any;
url: any;
project: any;
syncChecked: any;
events: any;
onChangeUrl: (url: string) => void;
onChangeSyncChecked: (syncChecked: boolean) => void;
onChangeEventsChecked: (events: string[]) => void;
onChangeRepository: (repository: string) => void;
onChangeToken: (token: string) => void;
onChangeDefaultRef: (defaultRef: string) => void;

View File

@ -10,7 +10,6 @@
<ProjectSettings::Integrations::Form::DataControlCheckboxes
@title={{t "components.project_settings.integrations.events.title"}}
@syncCheckLabel={{t "components.project_settings.integrations.events.options.sync"}}
@syncChecked={{@syncChecked}}
@onChangeSyncChecked={{@onChangeSyncChecked}}
@events={{@events}}
@onChangeEventsChecked={{@onChangeEventsChecked}}
/>

View File

@ -1,6 +1,7 @@
.project-settings-integrations-form {
padding: 10px 15px;
border: 1px solid var(--background-light-highlight);
background: var(--background-light);
border-radius: 3px;
}

View File

@ -33,9 +33,9 @@
errors=this.errors
url=this.url
project=@project
syncChecked=this.syncChecked
events=this.events
onChangeUrl=(fn this.setUrl)
onChangeSyncChecked=(fn this.setSyncChecked)
onChangeEventsChecked=(fn this.setEventsChecked)
onChangeRepository=(fn this.setRepository)
onChangeToken=(fn this.setToken)
onChangeDefaultRef=(fn this.setDefaultRef)

View File

@ -167,7 +167,7 @@ html {
'Segoe UI Symbol';
--font-monospace: 'Fira Code', 'Monaco', Courrier, monospace;
--screen-lg: 1300px;
--screen-lg: 1400px;
--screen-md: 640px;
--screen-sm: 440px;
@ -307,6 +307,7 @@ input[type='radio'] {
width: 1rem;
height: 1rem;
background: var(--body-background);
accent-color: var(--body-background);
border: 2px solid var(--color-grey);
&:checked {

View File

@ -5,6 +5,6 @@
>
<path
d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403z"
fill="#161614"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB