diff --git a/lib/plausible/teams/invitations.ex b/lib/plausible/teams/invitations.ex
index 041f47812c2c..caa5780ad6d9 100644
--- a/lib/plausible/teams/invitations.ex
+++ b/lib/plausible/teams/invitations.ex
@@ -35,7 +35,17 @@ defmodule Plausible.Teams.Invitations do
end
end
- def find_team_invitations(user) do
+ def find_team_invitations(%Teams.Team{} = team) do
+ Repo.all(
+ from ti in Teams.Invitation,
+ inner_join: inviter in assoc(ti, :inviter),
+ where: ti.team_id == ^team.id,
+ where: ti.role != :guest,
+ preload: [inviter: inviter]
+ )
+ end
+
+ def find_team_invitations(%Plausible.Auth.User{} = user) do
Repo.all(
from ti in Teams.Invitation,
inner_join: inviter in assoc(ti, :inviter),
diff --git a/lib/plausible/teams/memberships.ex b/lib/plausible/teams/memberships.ex
index 41d4c5fee670..e09497e7ec37 100644
--- a/lib/plausible/teams/memberships.ex
+++ b/lib/plausible/teams/memberships.ex
@@ -7,6 +7,18 @@ defmodule Plausible.Teams.Memberships do
alias Plausible.Repo
alias Plausible.Teams
+ def all_members(team) do
+ query =
+ from tm in Teams.Membership,
+ inner_join: u in assoc(tm, :user),
+ where: tm.team_id == ^team.id,
+ where: tm.role != :guest,
+ order_by: [asc: u.id],
+ preload: [user: u]
+
+ Repo.all(query)
+ end
+
def all_pending_site_transfers(email) do
email
|> pending_site_transfers_query()
diff --git a/lib/plausible/teams/memberships/remove.ex b/lib/plausible/teams/memberships/remove.ex
index 9aa1562e2c43..7ba440884d8e 100644
--- a/lib/plausible/teams/memberships/remove.ex
+++ b/lib/plausible/teams/memberships/remove.ex
@@ -8,14 +8,17 @@ defmodule Plausible.Teams.Memberships.Remove do
def remove(nil, _, _), do: {:error, :permission_denied}
- def remove(team, user_id, current_user) do
+ def remove(team, user_id, current_user, opts \\ []) do
with {:ok, team_membership} <- Memberships.get_team_membership(team, user_id),
{:ok, current_user_role} <- Memberships.team_role(team, current_user),
:ok <- check_can_remove_membership(current_user_role, team_membership.role),
:ok <- check_owner_can_get_removed(team, team_membership.role) do
team_membership = Repo.preload(team_membership, [:team, :user])
Repo.delete!(team_membership)
- send_team_member_removed_email(team_membership)
+
+ if Keyword.get(opts, :send_email?, true) do
+ send_team_member_removed_email(team_membership)
+ end
{:ok, team_membership}
end
@@ -35,7 +38,7 @@ defmodule Plausible.Teams.Memberships.Remove do
defp check_owner_can_get_removed(_team, _role), do: :ok
- defp send_team_member_removed_email(team_membership) do
+ def send_team_member_removed_email(team_membership) do
team_membership
|> PlausibleWeb.Email.team_member_removed()
|> Plausible.Mailer.send()
diff --git a/lib/plausible_web/components/generic.ex b/lib/plausible_web/components/generic.ex
index 587d86c8b50b..73f093e7f082 100644
--- a/lib/plausible_web/components/generic.ex
+++ b/lib/plausible_web/components/generic.ex
@@ -33,7 +33,7 @@ defmodule PlausibleWeb.Components.Generic do
"border border-gray-300 dark:border-gray-500 text-red-700 bg-white dark:bg-gray-900 hover:text-red-500 dark:hover:text-red-400 focus:border-blue-300 dark:text-red-500 active:text-red-800"
}
- @button_base_class "whitespace-nowrap truncate inline-flex items-center justify-center gap-x-2 font-medium rounded-md px-3.5 py-2.5 text-sm shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:bg-gray-400 dark:disabled:text-white dark:disabled:text-gray-400 dark:disabled:bg-gray-700"
+ @button_base_class "whitespace-nowrap truncate inline-flex items-center justify-center gap-x-2 font-medium rounded-md px-3 py-2 text-sm shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:bg-gray-400 dark:disabled:text-white dark:disabled:text-gray-400 dark:disabled:bg-gray-700"
attr(:type, :string, default: "button")
attr(:theme, :string, default: "primary")
diff --git a/lib/plausible_web/controllers/settings_controller.ex b/lib/plausible_web/controllers/settings_controller.ex
index b47c91231906..116b971181a7 100644
--- a/lib/plausible_web/controllers/settings_controller.ex
+++ b/lib/plausible_web/controllers/settings_controller.ex
@@ -40,7 +40,8 @@ defmodule PlausibleWeb.SettingsController do
render(conn, :team_general,
team_name_changeset: name_changeset,
- layout: {PlausibleWeb.LayoutView, :settings}
+ layout: {PlausibleWeb.LayoutView, :settings},
+ connect_live_socket: true
)
end
diff --git a/lib/plausible_web/live/components/team.ex b/lib/plausible_web/live/components/team.ex
new file mode 100644
index 000000000000..2c313736bcf8
--- /dev/null
+++ b/lib/plausible_web/live/components/team.ex
@@ -0,0 +1,122 @@
+defmodule PlausibleWeb.Live.Components.Team do
+ use Phoenix.Component
+ import PlausibleWeb.Components.Generic
+ alias Plausible.Auth.User
+
+ attr :user, User, required: true
+ attr :label, :string, default: nil
+ attr :role, :atom, default: nil
+ attr :my_role, :atom, required: true
+ attr :disabled, :boolean, default: false
+ attr :remove_disabled, :boolean, default: false
+
+ def member(assigns) do
+ ~H"""
+
+
+
+
+ {@user.name}
+
+ {@label}
+
+
+
{@user.email}
+
+
+ <.dropdown class="relative">
+ <:button class="role bg-transparent text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 focus-visible:outline-gray-100 whitespace-nowrap truncate inline-flex items-center gap-x-2 font-medium rounded-md px-3.5 py-2.5 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:bg-gray-400 dark:disabled:text-white dark:disabled:text-gray-400 dark:disabled:bg-gray-700">
+ {@role |> to_string() |> String.capitalize()}
+
+
+ <:menu class="dropdown-items max-w-60">
+ <.role_item
+ phx-value-email={@user.email}
+ phx-value-name={@user.name}
+ role={:owner}
+ disabled={@disabled or @role == :owner}
+ phx-click="update-role"
+ >
+ Manage the team without restrictions
+
+ <.role_item
+ phx-value-email={@user.email}
+ phx-value-name={@user.name}
+ role={:admin}
+ disabled={@disabled or @role == :admin}
+ phx-click="update-role"
+ >
+ Manage all team settings
+
+ <.role_item
+ phx-value-email={@user.email}
+ phx-value-name={@user.name}
+ role={:editor}
+ disabled={@disabled or @role == :editor}
+ phx-click="update-role"
+ >
+ Create and view new sites
+
+ <.role_item
+ phx-value-email={@user.email}
+ phx-value-name={@user.name}
+ role={:billing}
+ disabled={@disabled or @role == :billing}
+ phx-click="update-role"
+ >
+ Manage subscription
+
+ <.role_item
+ phx-value-email={@user.email}
+ phx-value-name={@user.name}
+ role={:viewer}
+ disabled={@disabled or @role == :viewer}
+ phx-click="update-role"
+ >
+ View all sites under your team
+
+ <.dropdown_divider />
+ <.dropdown_item
+ href="#"
+ disabled={@disabled or @remove_disabled}
+ phx-click="remove-member"
+ phx-value-email={@user.email}
+ phx-value-name={@user.name}
+ >
+
+ Remove member
+
+
+ Remove member from your team
+
+
+
+
+
+
+
+ """
+ end
+
+ attr :role, :atom, required: true
+ attr :disabled, :boolean, default: false
+ slot :inner_block, required: true
+ attr :rest, :global
+
+ def role_item(assigns) do
+ ~H"""
+ <.dropdown_item href="#" phx-value-role={@role} disabled={@disabled} {@rest}>
+ {@role |> Atom.to_string() |> String.capitalize()}
+
+ {render_slot(@inner_block)}
+
+
+ """
+ end
+end
diff --git a/lib/plausible_web/live/team_management.ex b/lib/plausible_web/live/team_management.ex
new file mode 100644
index 000000000000..1d53388d564d
--- /dev/null
+++ b/lib/plausible_web/live/team_management.ex
@@ -0,0 +1,288 @@
+defmodule PlausibleWeb.Live.TeamManagement do
+ use PlausibleWeb, :live_view
+
+ alias Plausible.Repo
+ alias Plausible.Teams
+ alias PlausibleWeb.Live.Components.ComboBox
+ alias Plausible.Teams.Invitations.Candidates
+ alias Plausible.Auth.User
+ import PlausibleWeb.Live.Components.Team
+
+ alias PlausibleWeb.Router.Helpers, as: Routes
+ alias PlausibleWeb.Live.TeamManagement.Layout
+
+ def mount(_params, _session, socket) do
+ {:ok, reset(socket)}
+ end
+
+ defp reset(%{assigns: %{current_user: current_user, my_team: my_team}} = socket) do
+ {:ok, my_role} = Teams.Memberships.team_role(my_team, current_user)
+ # XXX handle redirect here
+ true = my_role in [:owner, :admin]
+
+ invitations_sent = Plausible.Teams.Invitations.find_team_invitations(my_team)
+ all_members = Teams.Memberships.all_members(my_team)
+
+ layout = Layout.build_by_email(current_user, invitations_sent ++ all_members)
+
+ assign(socket,
+ layout: layout,
+ my_role: my_role,
+ all_members: all_members,
+ invitations_pending: [],
+ invitations_sent: invitations_sent,
+ team_layout_changed?: false,
+ input_role: :viewer,
+ input_email: ""
+ )
+ end
+
+ def render(assigns) do
+ ~H"""
+ <.flash_messages flash={@flash} />
+
+ <.form for={} phx-submit="input-invitation" phx-change="form-changed">
+
+
+ <.input
+ name="input-email"
+ type="email"
+ value={@input_email}
+ placeholder="Enter e-mail to send invitation to"
+ phx-debounce={200}
+ mt?={false}
+ />
+
+
+ <.dropdown class="relative">
+ <:button class="role border rounded border-indigo-700 bg-transparent text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 focus-visible:outline-gray-100 whitespace-nowrap truncate inline-flex items-center gap-x-2 font-medium rounded-md px-3 py-2 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:bg-gray-400 dark:disabled:text-white dark:disabled:text-gray-400 dark:disabled:bg-gray-700">
+ {@input_role |> Atom.to_string() |> String.capitalize()}
+
+
+ <:menu class="dropdown-items max-w-60">
+ <.role_item role={:owner} disabled={@my_role != :owner} phx-click="switch-role">
+ Manage the team without restrictions
+
+ <.role_item
+ role={:admin}
+ disabled={@my_role not in [:owner, :admin]}
+ phx-click="switch-role"
+ >
+ Manage all team settings
+
+ <.role_item
+ role={:editor}
+ disabled={@my_role not in [:owner, :admin]}
+ phx-click="switch-role"
+ >
+ Create and view new sites
+
+ <.role_item
+ role={:billing}
+ disabled={@my_role not in [:owner, :admin]}
+ phx-click="switch-role"
+ >
+ Manage subscription
+
+ <.role_item
+ role={:viewer}
+ disabled={@my_role not in [:owner, :admin]}
+ phx-click="switch-role"
+ >
+ View all sites under your team
+
+
+
+
+ <.button type="submit" mt?={false}>
+ Invite
+
+
+
+
+ <.member
+ :for={{email, entry} <- Layout.sorted_for_display(@layout)}
+ :if={entry.queued_op != :delete}
+ user={%User{email: entry.email, name: entry.name}}
+ role={entry.role}
+ label={entry.label}
+ my_role={@my_role}
+ remove_disabled={not Layout.removable?(@layout, email)}
+ />
+
+ <.button type="submit" phx-click="save-team-layout" disabled={not @team_layout_changed?}>
+ Save changes
+
+
+ """
+ end
+
+ @roles Plausible.Teams.Membership.roles() -- [:guest]
+ @roles_cast_map Enum.into(@roles, %{}, fn role -> {to_string(role), role} end)
+
+ def handle_event("form-changed", params, socket) do
+ {:noreply, assign(socket, input_email: params["input-email"])}
+ end
+
+ def handle_event("switch-role", %{"role" => role}, socket) do
+ socket = assign(socket, input_role: Map.fetch!(@roles_cast_map, role))
+ {:noreply, socket}
+ end
+
+ def handle_event(
+ "input-invitation",
+ %{"input-email" => email},
+ %{assigns: %{layout: layout, input_role: role}} = socket
+ ) do
+ email = String.trim(email)
+
+ existing_entry =
+ Enum.find_value(layout, fn {key_email, entry} ->
+ if key_email == email, do: entry
+ end)
+
+ socket =
+ cond do
+ existing_entry && existing_entry.queued_op == :delete ->
+ # bring back previously deleted entry (either invitation or membership), and only update role
+ socket
+ |> update_layout(email, nil, queued_op: :update, role: role)
+ |> assign(input_email: "")
+
+ existing_entry ->
+ # trying to add e-mail that's already in the layout
+ socket
+ |> assign(input_email: email)
+ |> put_live_flash(
+ :error,
+ "Make sure the e-mail is valid and is not taken already in your team layout"
+ )
+
+ valid_email?(email) ->
+ invitation = %Teams.Invitation{email: email, role: role}
+
+ socket
+ |> update_layout(email, invitation, queued_op: :send)
+ |> assign(input_email: "")
+ |> put_live_flash(
+ :success,
+ "Invitation pending. Will be sent once you save changes"
+ )
+
+ true ->
+ socket
+ |> assign(input_email: email)
+ |> put_live_flash(
+ :error,
+ "Make sure the e-mail is valid and is not taken already in your team layout"
+ )
+ end
+
+ {:noreply, socket}
+ end
+
+ def handle_event("save-team-layout", _params, %{assigns: assigns} = socket) do
+ result =
+ Repo.transaction(fn ->
+ assigns.layout
+ |> Layout.sorted_for_persistence()
+ |> Enum.reduce([], fn {_, entry}, acc ->
+ case Layout.Entry.persist(entry, assigns) do
+ {:ok, :ignore} -> acc
+ {:ok, persist_result} -> [persist_result | acc]
+ {:error, error} -> Repo.rollback(error)
+ end
+ end)
+ end)
+
+ socket =
+ case result do
+ {:ok, _} ->
+ socket
+ |> reset()
+ |> put_live_flash(:success, "Team layout updated successfully")
+
+ {:error, error} ->
+ socket
+ |> put_live_flash(:error, inspect(error))
+ end
+
+ # socket =
+ # case result do
+ # {:ok, invitations} ->
+ # Enum.each(invitations, fn invitation ->
+ # invitee = Plausible.Auth.find_user_by(email: invitation.email)
+ # Teams.Invitations.InviteToTeam.send_invitation_email(invitation, invitee)
+ # end)
+ #
+ # {:error, :already_a_member} ->
+ # socket
+ # |> put_live_flash(
+ # :error,
+ # "Can't invite e-mails that belong to team members already"
+ # )
+ #
+ # {:error, {:over_limit, limit}} = _ ->
+ # socket
+ # |> put_live_flash(
+ # :error,
+ # "Your account is limited to #{limit} team members. You can upgrade your plan to increase this limit"
+ # )
+ # end
+ #
+ {:noreply, socket}
+ end
+
+ def handle_event("remove-member", %{"email" => email}, %{assigns: %{layout: layout}} = socket) do
+ socket =
+ with :ok <- Layout.verify_removable(layout, email) do
+ socket
+ |> update_layout(email, nil, queued_op: :delete)
+ |> put_live_flash(
+ :success,
+ "Team layout change will be effective once you save your changes"
+ )
+ else
+ {:error, message} ->
+ socket
+ |> put_live_flash(
+ :error,
+ message
+ )
+ end
+
+ {:noreply, socket}
+ end
+
+ def handle_event("update-role", %{"email" => email, "role" => role}, socket) do
+ socket =
+ update_layout(socket, email, nil,
+ role: Map.fetch!(@roles_cast_map, role),
+ queued_op: :update
+ )
+
+ {:noreply, socket}
+ end
+
+ defp valid_email?(email) do
+ String.contains?(email, "@") and String.contains?(email, ".")
+ end
+
+ defp update_layout(%{assigns: %{layout: layout}} = socket, email, object, attrs) do
+ case object do
+ nil ->
+ entry = Map.fetch!(layout, email)
+
+ assign(socket,
+ layout: Layout.put(layout, email, Layout.Entry.patch(entry, attrs)),
+ team_layout_changed?: true
+ )
+
+ _ ->
+ assign(socket,
+ layout: Layout.put(layout, email, Layout.Entry.new(object, attrs)),
+ team_layout_changed?: true
+ )
+ end
+ end
+end
diff --git a/lib/plausible_web/live/team_management/layout.ex b/lib/plausible_web/live/team_management/layout.ex
new file mode 100644
index 000000000000..f498758138ec
--- /dev/null
+++ b/lib/plausible_web/live/team_management/layout.ex
@@ -0,0 +1,74 @@
+defmodule PlausibleWeb.Live.TeamManagement.Layout do
+ alias Plausible.Teams
+
+ alias PlausibleWeb.Live.TeamManagement.Layout.Entry
+
+ def build_by_email(current_user, entities) do
+ Enum.reduce(entities, %{}, fn
+ %Teams.Invitation{id: existing} = invitation, acc when is_integer(existing) ->
+ Map.put(acc, invitation.email, Entry.new(invitation))
+
+ %Teams.Invitation{id: nil} = pending, acc ->
+ Map.put(acc, pending.email, Entry.new(pending))
+
+ %Teams.Membership{} = membership, acc ->
+ Map.put(
+ acc,
+ membership.user.email,
+ Entry.new(membership,
+ label: if(current_user.id == membership.user.id, do: "You", else: "Team Member")
+ )
+ )
+ end)
+ end
+
+ def put(layout, email, entry) do
+ Map.put(layout, email, entry)
+ end
+
+ def verify_removable(layout, email) do
+ with :ok <- ensure_at_least_one_owner(layout, email) do
+ :ok
+ end
+ end
+
+ def removable?(layout, email) do
+ verify_removable(layout, email) == :ok
+ end
+
+ def sorted_for_display(layout) do
+ Enum.sort_by(layout, fn {email, entry} ->
+ primary_criterion =
+ case entry.type do
+ :invitation_pending -> 0
+ :invitation_sent -> 1
+ :membership -> 2
+ end
+
+ secondary_criterion = String.first(entry.name)
+ tertiary_criterion = String.first(email)
+ {primary_criterion, secondary_criterion, tertiary_criterion}
+ end)
+ end
+
+ def sorted_for_persistence(layout) do
+ # sort by deletions first, so team member limits are triggered accurately
+ Enum.sort_by(layout, fn {_email, entry} ->
+ case entry.queued_op do
+ :delete -> 0
+ _ -> 1
+ end
+ end)
+ end
+
+ defp ensure_at_least_one_owner(layout, email) do
+ if Enum.find(layout, fn {_email, entry} ->
+ entry.email != email and
+ entry.role == :owner and
+ entry.type == :membership and
+ entry.queued_op != :delete
+ end),
+ do: :ok,
+ else: {:error, "The team has to have at least one owner"}
+ end
+end
diff --git a/lib/plausible_web/live/team_management/layout/entry.ex b/lib/plausible_web/live/team_management/layout/entry.ex
new file mode 100644
index 000000000000..64a48b4bb8b1
--- /dev/null
+++ b/lib/plausible_web/live/team_management/layout/entry.ex
@@ -0,0 +1,111 @@
+defmodule PlausibleWeb.Live.TeamManagement.Layout.Entry do
+ alias Plausible.Teams
+
+ defstruct [:email, :name, :role, :type, :label, :meta, :queued_op]
+
+ def new(object, attrs \\ [])
+
+ def new(
+ %Teams.Invitation{id: existing} = invitation,
+ attrs
+ )
+ when is_integer(existing) do
+ %__MODULE__{
+ name: "Invited User",
+ email: invitation.email,
+ role: invitation.role,
+ type: :invitation_sent,
+ label: "Invitation Sent",
+ meta: invitation
+ }
+ |> Map.merge(Enum.into(attrs, %{}))
+ end
+
+ def new(%Teams.Invitation{id: nil} = pending, attrs) do
+ %__MODULE__{
+ name: "Invited User",
+ email: pending.email,
+ role: pending.role,
+ type: :invitation_pending,
+ label: "Invitation Pending",
+ meta: pending
+ }
+ |> Map.merge(Enum.into(attrs, %{}))
+ end
+
+ def new(%Teams.Membership{} = membership, attrs) do
+ %__MODULE__{
+ name: membership.user.name,
+ role: membership.role,
+ email: membership.user.email,
+ type: :membership,
+ meta: membership
+ }
+ |> Map.merge(Enum.into(attrs, %{}))
+ end
+
+ def patch(%__MODULE__{} = entry, attrs) do
+ struct!(entry, attrs)
+ end
+
+ def persist(%__MODULE__{queued_op: nil}, _context) do
+ {:ok, :ignore}
+ end
+
+ def persist(%__MODULE__{type: :invitation_pending, queued_op: :delete}, _context) do
+ {:ok, :ignore}
+ end
+
+ def persist(
+ %__MODULE__{email: email, role: role, type: :invitation_pending, queued_op: :send},
+ context
+ ) do
+ Teams.Invitations.InviteToTeam.invite(context.my_team, context.current_user, email, role,
+ send_email?: false
+ )
+ end
+
+ def persist(
+ %__MODULE__{type: :invitation_sent, email: email, role: role, queued_op: :update},
+ context
+ ) do
+ Teams.Invitations.InviteToTeam.invite(context.my_team, context.current_user, email, role,
+ send_email?: false
+ )
+ end
+
+ def persist(
+ %__MODULE__{type: :invitation_sent, queued_op: :delete, meta: meta},
+ context
+ ) do
+ Plausible.Teams.Invitations.Remove.remove(
+ context.my_team,
+ meta.invitation_id,
+ context.current_user
+ )
+ end
+
+ def persist(
+ %__MODULE__{type: :membership, queued_op: :delete, meta: meta},
+ context
+ ) do
+ Plausible.Teams.Memberships.Remove.remove(
+ context.my_team,
+ meta.user.id,
+ context.current_user,
+ send_email?: false
+ )
+ end
+
+ def persist(
+ %__MODULE__{type: :membership, queued_op: :update, role: role, meta: meta},
+ context
+ ) do
+ Plausible.Teams.Memberships.UpdateRole.update(
+ context.my_team,
+ meta.user.id,
+ "#{role}",
+ context.current_user
+ )
+ end
+end
diff --git a/lib/plausible_web/live/team_setup.ex b/lib/plausible_web/live/team_setup.ex
index a33a43ea3c0d..f4fde23a1cad 100644
--- a/lib/plausible_web/live/team_setup.ex
+++ b/lib/plausible_web/live/team_setup.ex
@@ -13,10 +13,16 @@ defmodule PlausibleWeb.Live.TeamSetup do
alias PlausibleWeb.Router.Helpers, as: Routes
+ import PlausibleWeb.Live.Components.Team
+
def mount(params, _session, socket) do
my_team = socket.assigns.my_team
enabled? = Teams.enabled?(my_team)
+ {:ok, my_role} = Teams.Memberships.team_role(my_team, socket.assigns.current_user)
+ true = my_role in [:owner, :admin]
+ socket = assign(socket, my_role: my_role)
+
# XXX: remove dev param, once manual testing is considered done
socket =
case {enabled?, my_team, params["dev"]} do
@@ -51,10 +57,6 @@ defmodule PlausibleWeb.Live.TeamSetup do
{:ok, socket}
end
- def handle_params(_params, _uri, socket) do
- {:noreply, socket}
- end
-
def render(assigns) do
~H"""
<.flash_messages flash={@flash} />
@@ -101,10 +103,10 @@ defmodule PlausibleWeb.Live.TeamSetup do
- <.member user={@current_user} role={:owner} you?={true} />
+ <.member user={@current_user} role={@my_role} disabled={true} label="You" my_role={@my_role} />
<%= for {{email, name}, role} <- @candidates_selected do %>
- <.member user={%User{email: email, name: name}} role={role} />
+ <.member user={%User{email: email, name: name}} role={role} my_role={@my_role} />
<% end %>
<:footer>
@@ -116,98 +118,6 @@ defmodule PlausibleWeb.Live.TeamSetup do
"""
end
- attr :user, User, required: true
- attr :you?, :boolean, default: false
- attr :role, :atom, default: nil
-
- def member(assigns) do
- ~H"""
-
-
-
-
- {@user.name}
-
- You
-
-
-
{@user.email}
-
-
- <.dropdown class="relative">
- <:button class="role bg-transparent text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 focus-visible:outline-gray-100 whitespace-nowrap truncate inline-flex items-center gap-x-2 font-medium rounded-md px-3.5 py-2.5 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:bg-gray-400 dark:disabled:text-white dark:disabled:text-gray-400 dark:disabled:bg-gray-700">
- {@role |> to_string() |> String.capitalize()}
-
-
- <:menu class="dropdown-items max-w-60">
- <.dropdown_item
- href="#"
- disabled={@you? or @role == :admin}
- phx-click="update-role"
- phx-value-role={:admin}
- phx-value-email={@user.email}
- phx-value-name={@user.name}
- >
-
Admin
-
- Manage all team settings
-
-
-
- <.dropdown_item
- href="#"
- disabled={@you? or @role == :editor}
- phx-click="update-role"
- phx-value-role={:editor}
- phx-value-email={@user.email}
- phx-value-name={@user.name}
- >
-
Editor
-
- Create and view new sites
-
-
-
- <.dropdown_item
- href="#"
- disabled={@you? or @role == :viewer}
- phx-click="update-role"
- phx-value-role={:viewer}
- phx-value-email={@user.email}
- phx-value-name={@user.name}
- >
-
Viewer
-
- Can only view all sites under your team
-
-
-
- <.dropdown_divider />
- <.dropdown_item
- href="#"
- disabled={@you?}
- phx-click="remove-member"
- phx-value-email={@user.email}
- phx-value-name={@user.name}
- >
-
- Remove member
-
-
- Remove member from your team
-
-
-
-
-
-
-
- """
- end
-
def handle_info(
{:candidate_selected, %{email: email, role: role}},
%{assigns: %{my_team: team, candidates_selected: candidates, current_user: current_user}} =
diff --git a/lib/plausible_web/templates/settings/team_general.html.heex b/lib/plausible_web/templates/settings/team_general.html.heex
index bbd038d66b1b..2deb037a39ee 100644
--- a/lib/plausible_web/templates/settings/team_general.html.heex
+++ b/lib/plausible_web/templates/settings/team_general.html.heex
@@ -19,4 +19,16 @@
+ <.tile>
+ <:title>
+ Team Members
+
+ <:subtitle>
+ Add, remove or change your team memberships. Take your time, changes apply on save.
+
+ {live_render(@conn, PlausibleWeb.Live.TeamManagement,
+ id: "team-setup",
+ session: %{}
+ )}
+