Clarify activity membership and date input states
This commit is contained in:
parent
fb79adffb2
commit
9acc52ef78
|
|
@ -207,6 +207,7 @@ export const Hooks = {
|
||||||
DateTimePicker: {
|
DateTimePicker: {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.hiddenInput = this.el.querySelector("[data-datetime-value]")
|
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.dateInput = this.el.querySelector("[data-datetime-date]")
|
||||||
this.timeInput = this.el.querySelector("[data-datetime-time]")
|
this.timeInput = this.el.querySelector("[data-datetime-time]")
|
||||||
this.timezoneLabel = this.el.querySelector("[data-datetime-timezone]")
|
this.timezoneLabel = this.el.querySelector("[data-datetime-timezone]")
|
||||||
|
|
@ -264,6 +265,11 @@ export const Hooks = {
|
||||||
this.hiddenInput.dispatchEvent(new Event("input", {bubbles: true}))
|
this.hiddenInput.dispatchEvent(new Event("input", {bubbles: true}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.syncUserInput = () => {
|
||||||
|
if (this.unusedInput) this.unusedInput.disabled = true
|
||||||
|
this.syncHidden()
|
||||||
|
}
|
||||||
|
|
||||||
this.setQuickTime = event => {
|
this.setQuickTime = event => {
|
||||||
const minutes = Number(event.currentTarget.dataset.datetimeOffset)
|
const minutes = Number(event.currentTarget.dataset.datetimeOffset)
|
||||||
if (!Number.isFinite(minutes)) return
|
if (!Number.isFinite(minutes)) return
|
||||||
|
|
@ -271,11 +277,11 @@ export const Hooks = {
|
||||||
const date = new Date(Date.now() + minutes * 60_000)
|
const date = new Date(Date.now() + minutes * 60_000)
|
||||||
date.setSeconds(0, 0)
|
date.setSeconds(0, 0)
|
||||||
this.applyLocalDate(date)
|
this.applyLocalDate(date)
|
||||||
this.syncHidden()
|
this.syncUserInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dateInput.addEventListener("input", this.syncHidden)
|
this.dateInput.addEventListener("input", this.syncUserInput)
|
||||||
this.timeInput.addEventListener("input", this.syncHidden)
|
this.timeInput.addEventListener("input", this.syncUserInput)
|
||||||
this.quickButtons.forEach(button => button.addEventListener("click", this.setQuickTime))
|
this.quickButtons.forEach(button => button.addEventListener("click", this.setQuickTime))
|
||||||
|
|
||||||
const initial = this.parseUtc(this.hiddenInput.value)
|
const initial = this.parseUtc(this.hiddenInput.value)
|
||||||
|
|
@ -288,8 +294,8 @@ export const Hooks = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.dateInput?.removeEventListener("input", this.syncHidden)
|
this.dateInput?.removeEventListener("input", this.syncUserInput)
|
||||||
this.timeInput?.removeEventListener("input", this.syncHidden)
|
this.timeInput?.removeEventListener("input", this.syncUserInput)
|
||||||
this.quickButtons?.forEach(button => button.removeEventListener("click", this.setQuickTime))
|
this.quickButtons?.forEach(button => button.removeEventListener("click", this.setQuickTime))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -394,6 +394,7 @@ defmodule WhoNeedHelpWeb.CoreComponents do
|
||||||
assigns
|
assigns
|
||||||
|> assign(:id, field.id)
|
|> assign(:id, field.id)
|
||||||
|> assign(:name, field.name)
|
|> 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(:value, normalized_input_value("datetime-local", safe_form_value(field.value)))
|
||||||
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
|
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
|
||||||
|
|
||||||
|
|
@ -418,6 +419,7 @@ defmodule WhoNeedHelpWeb.CoreComponents do
|
||||||
value={@value}
|
value={@value}
|
||||||
data-datetime-value
|
data-datetime-value
|
||||||
/>
|
/>
|
||||||
|
<input type="hidden" name={@unused_name} value="" data-datetime-unused />
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
<label>
|
<label>
|
||||||
<span class="label mb-1 text-xs">{gettext("Date")}</span>
|
<span class="label mb-1 text-xs">{gettext("Date")}</span>
|
||||||
|
|
@ -464,6 +466,13 @@ defmodule WhoNeedHelpWeb.CoreComponents do
|
||||||
"""
|
"""
|
||||||
end
|
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
|
# Helper used by inputs to generate form errors
|
||||||
defp error(assigns) do
|
defp error(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
|
||||||
|
|
@ -492,6 +492,14 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do
|
||||||
<p class="mt-1 whitespace-pre-wrap text-sm">{message.body}</p>
|
<p class="mt-1 whitespace-pre-wrap text-sm">{message.body}</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</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
|
<.form
|
||||||
:if={@activity.status == :open}
|
:if={@activity.status == :open}
|
||||||
for={@message_form}
|
for={@message_form}
|
||||||
|
|
@ -548,7 +556,9 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do
|
||||||
phx-click="join"
|
phx-click="join"
|
||||||
class="btn btn-info w-full"
|
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>
|
</button>
|
||||||
<div :if={@viewer_participant && @viewer_participant.status == :requested}>
|
<div :if={@viewer_participant && @viewer_participant.status == :requested}>
|
||||||
<span class="badge badge-warning">{gettext("Approval pending")}</span>
|
<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.")}
|
{gettext("The organizer has not approved your request yet.")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
<button
|
||||||
:if={
|
:if={
|
||||||
@activity.status == :open && @viewer_participant &&
|
@activity.status == :open && @viewer_participant &&
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,45 @@ defmodule WhoNeedHelp.ActivitiesTest do
|
||||||
assert request.status == :requested
|
assert request.status == :requested
|
||||||
end
|
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
|
test "my activities exclude organizers blocked in either direction", context do
|
||||||
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
|
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
|
||||||
{:ok, _request} = Activities.request_to_join(context.participant_scope, activity.id)
|
{:ok, _request} = Activities.request_to_join(context.participant_scope, activity.id)
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert has_element?(view, "#help_request_expires_at[type='hidden']")
|
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-date[type='date']")
|
||||||
assert has_element?(view, "#help_request_expires_at-time[type='time']")
|
assert has_element?(view, "#help_request_expires_at-time[type='time']")
|
||||||
assert has_element?(view, "#help_request_expires_at-picker[phx-hook='DateTimePicker']")
|
assert has_element?(view, "#help_request_expires_at-picker[phx-hook='DateTimePicker']")
|
||||||
|
|
@ -319,13 +320,22 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
|
|
||||||
html =
|
html =
|
||||||
render_change(view, "validate", %{
|
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 =~ "Route difficulty"
|
||||||
assert html =~ "activity[structured_data][route_plan]"
|
assert html =~ "activity[structured_data][route_plan]"
|
||||||
assert html =~ "activity[structured_data][safety_plan_confirmed]"
|
assert html =~ "activity[structured_data][safety_plan_confirmed]"
|
||||||
assert html =~ "approve participants individually"
|
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 =
|
html =
|
||||||
render_change(view, "validate", %{
|
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_push_event(participant_view, "reset-message-form", %{id: "activity-message-form"})
|
||||||
assert render(organizer_view) =~ "I will be there."
|
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
|
participant_view
|
||||||
|> form("#activity-report-form",
|
|> form("#activity-report-form",
|
||||||
report: %{reason: "harassment", details: "Review this activity context."}
|
report: %{reason: "harassment", details: "Review this activity context."}
|
||||||
|
|
@ -466,6 +503,8 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
|
|
||||||
assert render(participant_view) =~ "Completed"
|
assert render(participant_view) =~ "Completed"
|
||||||
refute has_element?(participant_view, "button[phx-click='leave']")
|
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(organizer_view)
|
||||||
stop_live_view(participant_view)
|
stop_live_view(participant_view)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user