Clarify activity membership and date input states

This commit is contained in:
SimpleTest 2026-07-22 06:18:12 +03:00
parent fb79adffb2
commit 9acc52ef78
5 changed files with 132 additions and 7 deletions

View File

@ -207,6 +207,7 @@ export const Hooks = {
DateTimePicker: {
mounted() {
this.hiddenInput = this.el.querySelector("[data-datetime-value]")
this.unusedInput = this.el.querySelector("[data-datetime-unused]")
this.dateInput = this.el.querySelector("[data-datetime-date]")
this.timeInput = this.el.querySelector("[data-datetime-time]")
this.timezoneLabel = this.el.querySelector("[data-datetime-timezone]")
@ -264,6 +265,11 @@ export const Hooks = {
this.hiddenInput.dispatchEvent(new Event("input", {bubbles: true}))
}
this.syncUserInput = () => {
if (this.unusedInput) this.unusedInput.disabled = true
this.syncHidden()
}
this.setQuickTime = event => {
const minutes = Number(event.currentTarget.dataset.datetimeOffset)
if (!Number.isFinite(minutes)) return
@ -271,11 +277,11 @@ export const Hooks = {
const date = new Date(Date.now() + minutes * 60_000)
date.setSeconds(0, 0)
this.applyLocalDate(date)
this.syncHidden()
this.syncUserInput()
}
this.dateInput.addEventListener("input", this.syncHidden)
this.timeInput.addEventListener("input", this.syncHidden)
this.dateInput.addEventListener("input", this.syncUserInput)
this.timeInput.addEventListener("input", this.syncUserInput)
this.quickButtons.forEach(button => button.addEventListener("click", this.setQuickTime))
const initial = this.parseUtc(this.hiddenInput.value)
@ -288,8 +294,8 @@ export const Hooks = {
}
},
destroyed() {
this.dateInput?.removeEventListener("input", this.syncHidden)
this.timeInput?.removeEventListener("input", this.syncHidden)
this.dateInput?.removeEventListener("input", this.syncUserInput)
this.timeInput?.removeEventListener("input", this.syncUserInput)
this.quickButtons?.forEach(button => button.removeEventListener("click", this.setQuickTime))
}
},

View File

@ -394,6 +394,7 @@ defmodule WhoNeedHelpWeb.CoreComponents do
assigns
|> assign(:id, field.id)
|> assign(:name, field.name)
|> assign(:unused_name, unused_input_name(field.name))
|> assign(:value, normalized_input_value("datetime-local", safe_form_value(field.value)))
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
@ -418,6 +419,7 @@ defmodule WhoNeedHelpWeb.CoreComponents do
value={@value}
data-datetime-value
/>
<input type="hidden" name={@unused_name} value="" data-datetime-unused />
<div class="grid gap-3 sm:grid-cols-2">
<label>
<span class="label mb-1 text-xs">{gettext("Date")}</span>
@ -464,6 +466,13 @@ defmodule WhoNeedHelpWeb.CoreComponents do
"""
end
defp unused_input_name(name) do
case Regex.run(~r/^(.*)\[([^\[\]]+)\]$/, name, capture: :all_but_first) do
[prefix, field] -> "#{prefix}[_unused_#{field}]"
_other -> "_unused_#{name}"
end
end
# Helper used by inputs to generate form errors
defp error(assigns) do
~H"""

View File

@ -492,6 +492,14 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do
<p class="mt-1 whitespace-pre-wrap text-sm">{message.body}</p>
</article>
</div>
<div
:if={@activity.status != :open}
class="mt-4 rounded-2xl border border-base-300 bg-base-200 p-4 text-sm text-base-content/65"
>
{gettext(
"This activity is closed. The group chat remains available as read-only history."
)}
</div>
<.form
:if={@activity.status == :open}
for={@message_form}
@ -548,7 +556,9 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do
phx-click="join"
class="btn btn-info w-full"
>
{gettext("Request to join")}
{if @viewer_participant,
do: gettext("Request to join again"),
else: gettext("Request to join")}
</button>
<div :if={@viewer_participant && @viewer_participant.status == :requested}>
<span class="badge badge-warning">{gettext("Approval pending")}</span>
@ -556,6 +566,28 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do
{gettext("The organizer has not approved your request yet.")}
</p>
</div>
<div
:if={@viewer_participant && @viewer_participant.status == :left}
class="mt-3 rounded-xl border border-base-300 bg-base-200 p-3"
>
<p class="font-semibold">{gettext("You left this activity")}</p>
<p class="mt-1 text-sm text-base-content/60">
{gettext(
"You are no longer a participant and cannot access the group chat or exact meeting point. You can request to join again while registration is open."
)}
</p>
</div>
<div
:if={@viewer_participant && @viewer_participant.status == :declined}
class="mt-3 rounded-xl border border-base-300 bg-base-200 p-3"
>
<p class="font-semibold">{gettext("Your join request was declined")}</p>
<p class="mt-1 text-sm text-base-content/60">
{gettext(
"You are not a participant. You can send a new request while registration is open."
)}
</p>
</div>
<button
:if={
@activity.status == :open && @viewer_participant &&

View File

@ -368,6 +368,45 @@ defmodule WhoNeedHelp.ActivitiesTest do
assert request.status == :requested
end
test "leaving revokes private activity access and rejoining returns to approval pending",
context do
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
{:ok, request} = Activities.request_to_join(context.participant_scope, activity.id)
{:ok, _approved} = Activities.approve_participant(context.organizer_scope, request.id)
{:ok, approved_activity} = Activities.get_activity(context.participant_scope, activity.id)
assert Activities.coordinates_for(context.participant_scope, approved_activity).exact
assert {:ok, _message} =
Activities.send_message(context.participant_scope, activity.id, %{
"body" => "Visible before leaving"
})
assert {:ok, left} = Activities.leave_activity(context.participant_scope, activity.id)
assert left.status == :left
assert {:ok, left_activity} = Activities.get_activity(context.participant_scope, activity.id)
assert Activities.coordinates_for(context.participant_scope, left_activity).exact == false
assert Activities.paginate_messages(context.participant_scope, left_activity).entries == []
assert {:error, :forbidden} =
Activities.send_message(context.participant_scope, activity.id, %{
"body" => "Must not be visible after leaving"
})
assert {:ok, rejoined} =
Activities.request_to_join(context.participant_scope, activity.id)
assert rejoined.id == request.id
assert rejoined.status == :requested
assert is_nil(rejoined.reviewed_at)
assert is_nil(rejoined.left_at)
assert Enum.map(Activities.list_my_activities(context.participant_scope), & &1.id) == [
activity.id
]
end
test "my activities exclude organizers blocked in either direction", context do
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
{:ok, _request} = Activities.request_to_join(context.participant_scope, activity.id)

View File

@ -276,6 +276,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
)
assert has_element?(view, "#help_request_expires_at[type='hidden']")
assert has_element?(view, "input[name='help_request[_unused_expires_at]']")
assert has_element?(view, "#help_request_expires_at-date[type='date']")
assert has_element?(view, "#help_request_expires_at-time[type='time']")
assert has_element?(view, "#help_request_expires_at-picker[phx-hook='DateTimePicker']")
@ -319,13 +320,22 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
html =
render_change(view, "validate", %{
"activity" => %{"category_id" => hiking.id}
"activity" => %{
"category_id" => hiking.id,
"starts_at" => "",
"_unused_starts_at" => "",
"join_deadline" => "",
"_unused_join_deadline" => ""
}
})
assert html =~ "Route difficulty"
assert html =~ "activity[structured_data][route_plan]"
assert html =~ "activity[structured_data][safety_plan_confirmed]"
assert html =~ "approve participants individually"
refute html =~ "can't be blank"
assert has_element?(view, "input[name='activity[_unused_starts_at]']")
assert has_element?(view, "input[name='activity[_unused_join_deadline]']")
html =
render_change(view, "validate", %{
@ -424,6 +434,33 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert_push_event(participant_view, "reset-message-form", %{id: "activity-message-form"})
assert render(organizer_view) =~ "I will be there."
participant_view
|> element("button[phx-click='leave']")
|> render_click()
participant_html = render(participant_view)
assert participant_html =~ "You left this activity"
assert participant_html =~ "You are no longer a participant"
assert participant_html =~ "Request to join again"
refute participant_html =~ "Approved group chat"
refute participant_html =~ "I will be there."
refute has_element?(participant_view, "#activity-message-form")
participant_view
|> element("button[phx-click='join']")
|> render_click()
assert render(participant_view) =~ "Approval pending"
assert render(organizer_view) =~ "Activity participant"
organizer_view
|> element("button[phx-click='approve']")
|> render_click()
participant_html = render(participant_view)
assert participant_html =~ "Approved group chat"
assert participant_html =~ "I will be there."
participant_view
|> form("#activity-report-form",
report: %{reason: "harassment", details: "Review this activity context."}
@ -466,6 +503,8 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert render(participant_view) =~ "Completed"
refute has_element?(participant_view, "button[phx-click='leave']")
refute has_element?(participant_view, "#activity-message-form")
assert render(participant_view) =~ "read-only history"
stop_live_view(organizer_view)
stop_live_view(participant_view)