Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions lib/textbin/pastes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1098,10 +1098,16 @@ defmodule Textbin.Pastes do
end
end

defp audience_allowed?("workspace", _policy), do: true
defp audience_allowed?("unlisted", policy), do: policy in ["unlisted", "public"]
defp audience_allowed?("public", "public"), do: true
defp audience_allowed?(_audience, _policy), do: false
@doc """
Whether a workspace's external sharing policy permits a paste audience.

Public so the editor can show forbidden audiences as disabled using the same
rule the changeset enforces.
"""
def audience_allowed?("workspace", _policy), do: true
def audience_allowed?("unlisted", policy), do: policy in ["unlisted", "public"]
def audience_allowed?("public", "public"), do: true
def audience_allowed?(_audience, _policy), do: false

defp lock_authorized_workspace(scope, workspace_id) do
lock_authorized_workspace(scope, workspace_id, "FOR UPDATE")
Expand Down
16 changes: 8 additions & 8 deletions lib/textbin_web/live/ui/audit_log_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule TextbinWeb.UI.AuditLogLive do

alias Textbin.Organizations
alias Textbin.Organizations.{AuditEvent, Organization}
alias TextbinWeb.Sharing

@page_size 25

Expand Down Expand Up @@ -206,10 +207,10 @@ defmodule TextbinWeb.UI.AuditLogLive do
defp event_title("workspace.membership.added"), do: "Workspace member added"
defp event_title("workspace.membership.removed"), do: "Workspace member removed"
defp event_title("workspace.membership.role_changed"), do: "Workspace role changed"
defp event_title("workspace.visibility_changed"), do: "Workspace visibility changed"
defp event_title("workspace.visibility_changed"), do: "Who can join changed"

defp event_title("workspace.external_sharing_policy_changed"),
do: "External sharing policy changed"
do: "Sharing outside the team changed"

defp event_title("workspace.recovery_window_changed"), do: "Recovery window changed"
defp event_title("workspace.recovery_access_granted"), do: "Workspace access recovered"
Expand All @@ -225,7 +226,7 @@ defmodule TextbinWeb.UI.AuditLogLive do
action: "workspace.created",
metadata: %{"visibility" => visibility}
}),
do: "Created #{visibility_article(visibility)} #{visibility} workspace."
do: "Created a workspace. Who can join: #{Sharing.join_policy_label(visibility)}."

defp event_description(%AuditEvent{
action: "workspace.deleted",
Expand Down Expand Up @@ -289,13 +290,15 @@ defmodule TextbinWeb.UI.AuditLogLive do
action: "workspace.visibility_changed",
metadata: %{"old" => old, "new" => new}
}),
do: "Changed workspace visibility from #{old} to #{new}."
do:
"Changed who can join from #{Sharing.join_policy_label(old)} to #{Sharing.join_policy_label(new)}."

defp event_description(%AuditEvent{
action: "workspace.external_sharing_policy_changed",
metadata: %{"old" => old, "new" => new}
}),
do: "Changed external sharing from #{old} to #{new}."
do:
"Changed sharing outside the team from #{Sharing.sharing_policy_label(old)} to #{Sharing.sharing_policy_label(new)}."

defp event_description(%AuditEvent{
action: "workspace.recovery_window_changed",
Expand Down Expand Up @@ -337,9 +340,6 @@ defmodule TextbinWeb.UI.AuditLogLive do
defp short_id(id) when is_binary(id), do: String.slice(id, 0, 8)
defp short_id(_id), do: "unknown"

defp visibility_article("open"), do: "an"
defp visibility_article(_visibility), do: "a"

defp workspace_description(%{"workspace_name" => name}), do: " in “#{name}”"
defp workspace_description(_metadata), do: ""

Expand Down
10 changes: 3 additions & 7 deletions lib/textbin_web/live/ui/organization_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule TextbinWeb.UI.OrganizationLive do
alias Textbin.Accounts
alias Textbin.Organizations
alias Textbin.Organizations.{Organization, OrganizationMembership, Policy}
alias TextbinWeb.Sharing

@impl true
def mount(_params, _session, socket), do: {:ok, socket}
Expand Down Expand Up @@ -428,15 +429,10 @@ defmodule TextbinWeb.UI.OrganizationLive do
</div>
<div class="mt-5 flex items-center gap-2 text-xs text-base-content/60">
<.icon
name={
if(workspace.visibility == "private",
do: "hero-lock-closed",
else: "hero-globe-alt"
)
}
name={Sharing.join_policy_icon(workspace.visibility)}
class="size-4"
/>
<span>{String.capitalize(workspace.visibility)}</span>
<span>{Sharing.join_policy_label(workspace.visibility)}</span>
</div>
</.link>
</div>
Expand Down
53 changes: 14 additions & 39 deletions lib/textbin_web/live/ui/paste_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule TextbinWeb.UI.PasteLive do
alias Textbin.Pastes
alias Textbin.Pastes.ContentType
alias Textbin.Pastes.Paste
alias TextbinWeb.Sharing
alias TextbinWeb.UI.Removal

embed_templates "paste_live/*"
Expand Down Expand Up @@ -347,47 +348,21 @@ defmodule TextbinWeb.UI.PasteLive do
defp audience_options(scope) do
policy = scope.workspace.external_sharing_policy

[
audience_option("workspace", true),
audience_option("unlisted", policy in ["unlisted", "public"]),
audience_option("public", policy == "public")
]
for audience <- Sharing.audiences() do
%{
value: audience,
label: Sharing.audience_label(audience),
description: Sharing.audience_description(audience),
allowed?: Pastes.audience_allowed?(audience, policy)
}
end
end

defp audience_option("workspace", allowed?),
do: %{
value: "workspace",
label: "Workspace",
description: "Everyone in the workspace",
allowed?: allowed?
}

defp audience_option("unlisted", allowed?),
do: %{
value: "unlisted",
label: "Anyone with the link",
description: "Not listed anywhere; the link is the key",
allowed?: allowed?
}

defp audience_option("public", allowed?),
do: %{
value: "public",
label: "Public",
description: "Open to everyone",
allowed?: allowed?
}

defp audience_sentence(%Paste{audience: "workspace"}, %{workspace: %{name: name}}),
do: "Everyone in #{name} can see this paste."

defp audience_sentence(%Paste{audience: "workspace"}, _scope),
do: "Everyone in the workspace can see this paste."

defp audience_sentence(%Paste{audience: "unlisted"}, _scope),
do: "Anyone with the link can see this paste."

defp audience_sentence(%Paste{audience: "public"}, _scope), do: "This paste is public."
defp audience_sentence(%Paste{audience: audience}, scope),
do: Sharing.audience_sentence(audience, scope_workspace_name(scope))

defp scope_workspace_name(%{workspace: %{name: name}}), do: name
defp scope_workspace_name(_scope), do: nil

defp expiry_sentence(nil), do: "It never expires."

Expand Down
10 changes: 2 additions & 8 deletions lib/textbin_web/live/ui/paste_live/detail.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,15 @@
class="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-lg border border-base-300 bg-base-100 px-4 py-3 text-sm text-base-content/80"
>
<.icon
name={
case @paste.audience do
"public" -> "hero-globe-alt"
"unlisted" -> "hero-link"
_audience -> "hero-user-group"
end
}
name={Sharing.audience_icon(@paste.audience)}
class="size-4 shrink-0 text-base-content/50"
/>
<span id="paste-audience-sentence">
{audience_sentence(@paste, @current_scope)} {expiry_sentence(@paste.expires_at)}
</span>
<span class="ml-auto flex flex-wrap items-center gap-2">
<span id="paste-audience" class="badge badge-sm badge-outline">
{String.capitalize(@paste.audience)}
{Sharing.audience_label(@paste.audience)}
</span>
<%= if @paste.expires_at do %>
<time
Expand Down
2 changes: 1 addition & 1 deletion lib/textbin_web/live/ui/paste_live/index.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
<div class="flex flex-wrap items-center gap-3">
<span class="badge badge-sm badge-outline">{paste.syntax_highlight}</span>
<span id={"paste-audience-#{paste.id}"} class="badge badge-sm badge-outline">
{String.capitalize(paste.audience)}
{Sharing.audience_label(paste.audience)}
</span>
<time
class="text-xs text-base-content/50"
Expand Down
2 changes: 1 addition & 1 deletion lib/textbin_web/live/ui/paste_live/new.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
:if={!option.allowed?}
class="mt-1 block text-xs font-medium text-warning"
>
Not allowed by the workspace sharing policy
Off in this workspace's “Sharing outside the team” setting
</span>
</span>
</label>
Expand Down
39 changes: 25 additions & 14 deletions lib/textbin_web/live/ui/workspace_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule TextbinWeb.UI.WorkspaceLive do
alias Textbin.Accounts
alias Textbin.Organizations
alias Textbin.Organizations.{Policy, Workspace, WorkspaceMembership}
alias TextbinWeb.Sharing

@impl true
def mount(_params, _session, socket), do: {:ok, socket}
Expand Down Expand Up @@ -112,6 +113,12 @@ defmodule TextbinWeb.UI.WorkspaceLive do
end
end

# Re-render on change so the sentence under each setting describes the
# option being considered, before anything is saved.
def handle_event("change_settings", %{"workspace" => workspace_params}, socket) do
{:noreply, assign(socket, :settings_form, to_form(workspace_params, as: :workspace))}
end

def handle_event("update_settings", %{"workspace" => workspace_params}, socket) do
case Organizations.change_workspace_settings(
socket.assigns.current_scope,
Expand Down Expand Up @@ -187,25 +194,29 @@ defmodule TextbinWeb.UI.WorkspaceLive do
:if={@workspace_owner?}
for={@settings_form}
id="workspace-settings-form"
phx-change="change_settings"
phx-submit="update_settings"
>
<.input
:if={!@current_scope.workspace.is_default}
field={@settings_form[:visibility]}
type="select"
label="Visibility"
options={[{"Open", "open"}, {"Private", "private"}]}
/>
<div :if={!@current_scope.workspace.is_default}>
<.input
field={@settings_form[:visibility]}
type="select"
label="Who can join"
options={Sharing.join_policy_options()}
/>
<p id="workspace-join-policy-sentence" class="-mt-1 mb-3 text-xs text-base-content/60">
{Sharing.join_policy_sentence(@settings_form[:visibility].value)}
</p>
</div>
<.input
field={@settings_form[:external_sharing_policy]}
type="select"
label="External sharing"
options={[
{"Disabled", "disabled"},
{"Unlisted links", "unlisted"},
{"Public", "public"}
]}
label="Sharing outside the team"
options={Sharing.sharing_policy_options()}
/>
<p id="workspace-sharing-policy-sentence" class="-mt-1 mb-3 text-xs text-base-content/60">
{Sharing.sharing_policy_sentence(@settings_form[:external_sharing_policy].value)}
</p>
<.input
field={@settings_form[:recovery_window_days]}
type="select"
Expand All @@ -220,7 +231,7 @@ defmodule TextbinWeb.UI.WorkspaceLive do
id="workspace-settings-readonly"
class="text-sm text-base-content/60"
>
Visibility: {String.capitalize(@current_scope.workspace.visibility)} · External sharing: {String.capitalize(
Who can join: {Sharing.join_policy_label(@current_scope.workspace.visibility)} · Sharing outside the team: {Sharing.sharing_policy_label(
@current_scope.workspace.external_sharing_policy
)} · Removed pastes kept for {@current_scope.workspace.recovery_window_days} days
</p>
Expand Down
30 changes: 13 additions & 17 deletions lib/textbin_web/live/ui/workspace_management_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule TextbinWeb.UI.WorkspaceManagementLive do

alias Textbin.Organizations
alias Textbin.Organizations.{Organization, Policy, Workspace}
alias TextbinWeb.Sharing

@impl true
def mount(_params, _session, socket), do: {:ok, socket}
Expand Down Expand Up @@ -144,8 +145,8 @@ defmodule TextbinWeb.UI.WorkspaceManagementLive do
Workspaces
</h1>
<p class="mt-3 max-w-2xl text-sm leading-6 text-base-content/60">
Open workspaces are available to everyone in the organization. Private workspaces
appear only after an owner adds you.
Some workspaces are open to anyone in the organization. Invite-only workspaces
appear here once an owner adds you.
</p>
</div>
<.link
Expand Down Expand Up @@ -183,12 +184,7 @@ defmodule TextbinWeb.UI.WorkspaceManagementLive do
<div class="flex min-w-0 items-start gap-4">
<div class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-base-200 text-base-content/55">
<.icon
name={
if(row.workspace.visibility == "private",
do: "hero-lock-closed",
else: "hero-globe-alt"
)
}
name={Sharing.join_policy_icon(row.workspace.visibility)}
class="size-5"
/>
</div>
Expand All @@ -203,8 +199,8 @@ defmodule TextbinWeb.UI.WorkspaceManagementLive do
<p class="mt-1 truncate font-mono text-xs text-base-content/45">
/{row.workspace.slug}
</p>
<p class="mt-2 text-xs capitalize text-base-content/55">
{row.workspace.visibility} workspace
<p class="mt-2 text-xs text-base-content/55">
Who can join: {Sharing.join_policy_label(row.workspace.visibility)}
</p>
</div>
</div>
Expand Down Expand Up @@ -311,14 +307,14 @@ defmodule TextbinWeb.UI.WorkspaceManagementLive do
<.input
field={@workspace_form[:visibility]}
type="select"
label="Who can find and join this workspace?"
options={[
{"Everyone in the organization", "open"},
{"Invited members only", "private"}
]}
label="Who can join"
options={Sharing.join_policy_options()}
/>
<p class="mt-1.5 text-xs leading-5 text-base-content/50">
Private workspaces stay hidden from organization members who have not been invited.
<p
id="workspace-join-policy-sentence"
class="mt-1.5 text-xs leading-5 text-base-content/50"
>
{Sharing.join_policy_sentence(@workspace_form[:visibility].value)}
</p>
</div>

Expand Down
Loading
Loading