feat(scheduling-webui): manage participant invitations
This commit is contained in:
28
README.md
28
README.md
@@ -209,13 +209,29 @@ Organizers and Scheduling administrators use the participant-specific
|
|||||||
invitation action instead:
|
invitation action instead:
|
||||||
|
|
||||||
- `POST /scheduling/requests/{request_id}/participants/{participant_id}/invitation`
|
- `POST /scheduling/requests/{request_id}/participants/{participant_id}/invitation`
|
||||||
with `{"action":"copy"}` rotates the previous invitation and returns the new
|
with `{"action":"copy","participant_revision":"..."}` rotates the previous
|
||||||
relative action URL once. The response is marked `no-store`.
|
invitation and returns the new relative action URL once. The response is
|
||||||
|
marked `no-store`.
|
||||||
- The same endpoint with `{"action":"send"}` rotates the invitation and passes
|
- The same endpoint with `{"action":"send"}` rotates the invitation and passes
|
||||||
its URL directly to the notification dispatch job. Neither the API response
|
its URL directly to the notification dispatch job; it also requires the
|
||||||
nor Scheduling's durable notification projection contains the token.
|
current `participant_revision`. Neither the API response nor Scheduling's
|
||||||
- `DELETE` on the same resource revokes the active link immediately; exact
|
durable notification projection contains the token.
|
||||||
repeats are idempotent.
|
- `DELETE` on the same resource uses a JSON body containing the current
|
||||||
|
`participant_revision` and revokes the active link immediately. A revoke
|
||||||
|
against the refreshed no-link projection is an idempotent replay.
|
||||||
|
|
||||||
|
The semantic participant revision includes the current invitation identity.
|
||||||
|
It is checked after the participant row is locked, so stale copy, send, and
|
||||||
|
revoke commands return `409` before rotating a newer link or delivering to a
|
||||||
|
changed recipient.
|
||||||
|
|
||||||
|
The participant DataGrid presents copy, send, and revoke as a fixed icon-only
|
||||||
|
action group. Authorized but unavailable actions remain visible and explain
|
||||||
|
why they are disabled: for example, delivery is disabled without a dispatch
|
||||||
|
provider or recipient target, and revoke is disabled when no active link
|
||||||
|
exists. The delivery capability is exposed only in management projections.
|
||||||
|
Copy accepts only the same-origin Scheduling public path and does not persist
|
||||||
|
the bearer URL in component state, logs, or browser storage.
|
||||||
|
|
||||||
Links can be issued in any request state. Collection state and deadline checks
|
Links can be issued in any request state. Collection state and deadline checks
|
||||||
remain independent submission requirements, so a link to a draft, closed, or
|
remain independent submission requirements, so a link to a draft, closed, or
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ assert.match(page, /title=\{I18N\.invitedRequests\}/);
|
|||||||
assert.ok(page.indexOf("title={I18N.myRequests}") < page.indexOf("title={I18N.invitedRequests}"));
|
assert.ok(page.indexOf("title={I18N.myRequests}") < page.indexOf("title={I18N.invitedRequests}"));
|
||||||
assert.match(page, /<SelectionList label=\{title\} className="scheduling-request-list">/);
|
assert.match(page, /<SelectionList label=\{title\} className="scheduling-request-list">/);
|
||||||
assert.match(page, /<SelectionListItem[\s\S]*selected=\{selectedId === request\.id\}[\s\S]*className="scheduling-list-item"/);
|
assert.match(page, /<SelectionListItem[\s\S]*selected=\{selectedId === request\.id\}[\s\S]*className="scheduling-list-item"/);
|
||||||
|
assert.match(page, /className="scheduling-list-item"[\s\S]{0,100}disabled=\{disabled\}/);
|
||||||
|
|
||||||
assert.match(page, /editorMode \? \(/);
|
assert.match(page, /editorMode \? \(/);
|
||||||
assert.match(page, /id="scheduling-editor-form"/);
|
assert.match(page, /id="scheduling-editor-form"/);
|
||||||
@@ -86,6 +87,26 @@ assert.match(page, /showDecisionAction=\{canManageSelected\}/);
|
|||||||
assert.match(page, /<IconButton[\s\S]*label=\{I18N\.refresh\}/);
|
assert.match(page, /<IconButton[\s\S]*label=\{I18N\.refresh\}/);
|
||||||
assert.doesNotMatch(page, /AdminIconButton/);
|
assert.doesNotMatch(page, /AdminIconButton/);
|
||||||
|
|
||||||
|
const participantGridStart = page.indexOf("function ParticipantsGrid(");
|
||||||
|
const participantGridEnd = page.indexOf("function invitationActionDisabledReason", participantGridStart);
|
||||||
|
const participantGrid = page.slice(participantGridStart, participantGridEnd);
|
||||||
|
assert.match(participantGrid, /\.\.\.\(canManage \? \[\{/);
|
||||||
|
assert.match(participantGrid, /minimumSlots=\{3\}/);
|
||||||
|
assert.ok(participantGrid.indexOf('id: "copy-invitation"') < participantGrid.indexOf('id: "send-invitation"'));
|
||||||
|
assert.ok(participantGrid.indexOf('id: "send-invitation"') < participantGrid.indexOf('id: "revoke-invitation"'));
|
||||||
|
assert.match(participantGrid, /schedulingInvitationActionBlocks\(request, participant, now\)/);
|
||||||
|
assert.match(participantGrid, /disabledReason: copyDisabledReason/);
|
||||||
|
assert.match(participantGrid, /disabledReason: deliveryDisabledReason/);
|
||||||
|
assert.match(participantGrid, /disabledReason: revokeDisabledReason/);
|
||||||
|
assert.match(page, /<ConfirmDialog[\s\S]*title=\{I18N\.revokeInvitationLabel\}[\s\S]*tone="danger"/);
|
||||||
|
assert.match(page, /navigator\.clipboard\.writeText\(value\)/);
|
||||||
|
assert.match(page, /navigator\.clipboard\.write\(\[new ClipboardItem/);
|
||||||
|
assert.match(page, /schedulingPublicInvitationUrl\(response\.action_url, window\.location\.origin\)/);
|
||||||
|
assert.match(page, /\["failed", "skipped"\]\.includes\(result\.status\)/);
|
||||||
|
assert.match(page, /isApiError\(err, 409\)/);
|
||||||
|
assert.match(page, /scheduleExpiryRefresh/);
|
||||||
|
assert.doesNotMatch(page, /(?:localStorage|sessionStorage).*action_url|action_url.*(?:localStorage|sessionStorage)/);
|
||||||
|
|
||||||
assert.match(page, /submitSchedulingAvailability\(settings, selected\.id/);
|
assert.match(page, /submitSchedulingAvailability\(settings, selected\.id/);
|
||||||
assert.match(page, /option_revision: slot\.revision/);
|
assert.match(page, /option_revision: slot\.revision/);
|
||||||
assert.match(page, /getSchedulingAvailabilityResponse\(settings, selected\.id\)/);
|
assert.match(page, /getSchedulingAvailabilityResponse\(settings, selected\.id\)/);
|
||||||
@@ -106,7 +127,8 @@ for (const field of [
|
|||||||
"allow_comments",
|
"allow_comments",
|
||||||
"participant_email_required",
|
"participant_email_required",
|
||||||
"anonymous_password_protection_enabled",
|
"anonymous_password_protection_enabled",
|
||||||
"public_participation_policy_enforcement_available"
|
"public_participation_policy_enforcement_available",
|
||||||
|
"participant_invitation_delivery_available"
|
||||||
]) {
|
]) {
|
||||||
assert.match(api, new RegExp(`${field}:`));
|
assert.match(api, new RegExp(`${field}:`));
|
||||||
}
|
}
|
||||||
@@ -117,6 +139,9 @@ assert.match(page, /slots: slots\.map\(\(slot\) => \(\{/);
|
|||||||
assert.match(page, /participants: participants[\s\S]*create_participant_invitations: true/);
|
assert.match(page, /participants: participants[\s\S]*create_participant_invitations: true/);
|
||||||
assert.match(api, /\/api\/v1\/scheduling\/requests\/\$\{requestId\}\/responses/);
|
assert.match(api, /\/api\/v1\/scheduling\/requests\/\$\{requestId\}\/responses/);
|
||||||
assert.match(api, /\/api\/v1\/scheduling\/requests\/\$\{requestId\}\/responses\/me/);
|
assert.match(api, /\/api\/v1\/scheduling\/requests\/\$\{requestId\}\/responses\/me/);
|
||||||
|
assert.match(api, /issueSchedulingParticipantInvitation\([\s\S]*json\(\{ action, participant_revision: participantRevision \}\)/);
|
||||||
|
assert.match(api, /revokeSchedulingParticipantInvitation\([\s\S]*method: "DELETE"[\s\S]*participant_revision: participantRevision/);
|
||||||
|
assert.match(api, /participants\/\$\{encodeURIComponent\(participantId\)\}\/invitation/);
|
||||||
assert.match(api, /\/api\/v1\/scheduling\/public\/\$\{encodeURIComponent\(requestId\)\}\/\$\{encodeURIComponent\(token\)\}/);
|
assert.match(api, /\/api\/v1\/scheduling\/public\/\$\{encodeURIComponent\(requestId\)\}\/\$\{encodeURIComponent\(token\)\}/);
|
||||||
assert.match(page, /useSearchParams\(\)/);
|
assert.match(page, /useSearchParams\(\)/);
|
||||||
assert.match(page, /Promise\.allSettled/);
|
assert.match(page, /Promise\.allSettled/);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
CalendarCheck,
|
CalendarCheck,
|
||||||
Check,
|
Check,
|
||||||
Clock,
|
Clock,
|
||||||
|
Copy,
|
||||||
|
Link2Off,
|
||||||
Pencil,
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -15,6 +17,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
ConfirmDialog,
|
||||||
DataGrid,
|
DataGrid,
|
||||||
DataGridRowActions,
|
DataGridRowActions,
|
||||||
DateTimeField,
|
DateTimeField,
|
||||||
@@ -33,6 +36,7 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
hasScope,
|
hasScope,
|
||||||
i18nMessage,
|
i18nMessage,
|
||||||
|
isApiError,
|
||||||
usePlatformLanguage,
|
usePlatformLanguage,
|
||||||
usePlatformUiCapability,
|
usePlatformUiCapability,
|
||||||
useUnsavedChanges,
|
useUnsavedChanges,
|
||||||
@@ -54,15 +58,18 @@ import {
|
|||||||
decideSchedulingRequest,
|
decideSchedulingRequest,
|
||||||
evaluateSchedulingFreeBusy,
|
evaluateSchedulingFreeBusy,
|
||||||
getSchedulingAvailabilityResponse,
|
getSchedulingAvailabilityResponse,
|
||||||
|
issueSchedulingParticipantInvitation,
|
||||||
listSchedulingNotifications,
|
listSchedulingNotifications,
|
||||||
listSchedulingRequests,
|
listSchedulingRequests,
|
||||||
openSchedulingRequest,
|
openSchedulingRequest,
|
||||||
|
revokeSchedulingParticipantInvitation,
|
||||||
searchSchedulingPeople,
|
searchSchedulingPeople,
|
||||||
schedulingSummary,
|
schedulingSummary,
|
||||||
submitSchedulingAvailability,
|
submitSchedulingAvailability,
|
||||||
updateSchedulingRequest,
|
updateSchedulingRequest,
|
||||||
type SchedulingCandidateSlot,
|
type SchedulingCandidateSlot,
|
||||||
type SchedulingAvailabilityValue,
|
type SchedulingAvailabilityValue,
|
||||||
|
type SchedulingInvitationActionResponse,
|
||||||
type SchedulingNotification,
|
type SchedulingNotification,
|
||||||
type SchedulingParticipant,
|
type SchedulingParticipant,
|
||||||
type SchedulingPollOptionResult,
|
type SchedulingPollOptionResult,
|
||||||
@@ -76,10 +83,13 @@ import {
|
|||||||
participantDraftFromResponse,
|
participantDraftFromResponse,
|
||||||
participantDraftsFromPicker,
|
participantDraftsFromPicker,
|
||||||
participantPayload,
|
participantPayload,
|
||||||
|
schedulingInvitationActionBlocks,
|
||||||
|
schedulingPublicInvitationUrl,
|
||||||
schedulingRelevantTimestamp,
|
schedulingRelevantTimestamp,
|
||||||
schedulingRequestIsOwned,
|
schedulingRequestIsOwned,
|
||||||
schedulingSortPhase,
|
schedulingSortPhase,
|
||||||
type SchedulingActor,
|
type SchedulingActor,
|
||||||
|
type SchedulingInvitationActionBlock,
|
||||||
type SchedulingParticipantDraft,
|
type SchedulingParticipantDraft,
|
||||||
type SchedulingRequestGroups
|
type SchedulingRequestGroups
|
||||||
} from "./schedulingViewModel";
|
} from "./schedulingViewModel";
|
||||||
@@ -97,6 +107,10 @@ type SlotDraft = {
|
|||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
type ParticipantDraft = SchedulingParticipantDraft;
|
type ParticipantDraft = SchedulingParticipantDraft;
|
||||||
|
type InvitationRevokeTarget = {
|
||||||
|
requestId: string;
|
||||||
|
participant: SchedulingParticipant;
|
||||||
|
};
|
||||||
|
|
||||||
const I18N = {
|
const I18N = {
|
||||||
actions: "i18n:govoplan-core.actions.c3cd636a",
|
actions: "i18n:govoplan-core.actions.c3cd636a",
|
||||||
@@ -118,10 +132,13 @@ const I18N = {
|
|||||||
calendarUnavailable: "i18n:govoplan-scheduling.calendar_integration_requires_the_calendar_module_plus_c.f892cb1e",
|
calendarUnavailable: "i18n:govoplan-scheduling.calendar_integration_requires_the_calendar_module_plus_c.f892cb1e",
|
||||||
candidateAvailability: "i18n:govoplan-scheduling.candidate_availability.9541c4b5",
|
candidateAvailability: "i18n:govoplan-scheduling.candidate_availability.9541c4b5",
|
||||||
candidateSlots: "i18n:govoplan-scheduling.candidate_slots.c414946b",
|
candidateSlots: "i18n:govoplan-scheduling.candidate_slots.c414946b",
|
||||||
|
cancellationNoticeExpired: "i18n:govoplan-scheduling.the_cancellation_notice_has_expired_a_new_link_cannot_be_issued.9c6ccc7c",
|
||||||
checkFreeBusy: "i18n:govoplan-scheduling.check_free_busy.e9700e00",
|
checkFreeBusy: "i18n:govoplan-scheduling.check_free_busy.e9700e00",
|
||||||
chooseAvailability: "i18n:govoplan-scheduling.choose_availability.ac95b8f6",
|
chooseAvailability: "i18n:govoplan-scheduling.choose_availability.ac95b8f6",
|
||||||
|
clipboardUnavailable: "i18n:govoplan-scheduling.the_invitation_link_could_not_be_copied_check_browser_clipboard_permissions_and_try_again.a8b17cbc",
|
||||||
closePoll: "i18n:govoplan-scheduling.close_poll.a6a18916",
|
closePoll: "i18n:govoplan-scheduling.close_poll.a6a18916",
|
||||||
closed: "i18n:govoplan-scheduling.closed.88d86b77",
|
closed: "i18n:govoplan-scheduling.closed.88d86b77",
|
||||||
|
copyInvitationLink: "i18n:govoplan-scheduling.copy_a_fresh_invitation_link_for_value0.e3799c79",
|
||||||
description: "i18n:govoplan-scheduling.description.55f8ebc8",
|
description: "i18n:govoplan-scheduling.description.55f8ebc8",
|
||||||
determined: "i18n:govoplan-scheduling.determined.9f23293d",
|
determined: "i18n:govoplan-scheduling.determined.9f23293d",
|
||||||
decideUnavailable: "i18n:govoplan-scheduling.a_slot_can_be_selected_after_the_request_is_closed.f91ec02d",
|
decideUnavailable: "i18n:govoplan-scheduling.a_slot_can_be_selected_after_the_request_is_closed.f91ec02d",
|
||||||
@@ -133,7 +150,13 @@ const I18N = {
|
|||||||
generalSettings: "i18n:govoplan-scheduling.participation_settings.8dc6f62c",
|
generalSettings: "i18n:govoplan-scheduling.participation_settings.8dc6f62c",
|
||||||
free: "i18n:govoplan-scheduling.free.75f52718",
|
free: "i18n:govoplan-scheduling.free.75f52718",
|
||||||
holds: "i18n:govoplan-scheduling.create_tentative_holds.51c4744e",
|
holds: "i18n:govoplan-scheduling.create_tentative_holds.51c4744e",
|
||||||
|
invitationDeliveryUnavailable: "i18n:govoplan-scheduling.automatic_invitation_delivery_is_unavailable_copy_the_link_instead.4e39d0b3",
|
||||||
|
invitationDeliveryFailed: "i18n:govoplan-scheduling.invitation_delivery_failed_the_link_was_created_but_was_not_delivered.8db0c306",
|
||||||
|
invitationDeliveryRequested: "i18n:govoplan-scheduling.invitation_delivery_requested.1aaa78ba",
|
||||||
|
invitationChanged: "i18n:govoplan-scheduling.this_invitation_changed_the_request_was_reloaded_try_again.c7095533",
|
||||||
invitedRequests: "i18n:govoplan-scheduling.scheduling_requests_for_me.1d521aba",
|
invitedRequests: "i18n:govoplan-scheduling.scheduling_requests_for_me.1d521aba",
|
||||||
|
invitationLinkCopied: "i18n:govoplan-scheduling.invitation_link_copied.332973ec",
|
||||||
|
invitationLinkRevoked: "i18n:govoplan-scheduling.invitation_link_revoked.c7dd20d4",
|
||||||
loading: "i18n:govoplan-scheduling.loading_scheduling_requests.f42be95d",
|
loading: "i18n:govoplan-scheduling.loading_scheduling_requests.f42be95d",
|
||||||
location: "i18n:govoplan-scheduling.location.d219c681",
|
location: "i18n:govoplan-scheduling.location.d219c681",
|
||||||
allowComments: "i18n:govoplan-scheduling.allow_comments.d63202a6",
|
allowComments: "i18n:govoplan-scheduling.allow_comments.d63202a6",
|
||||||
@@ -155,6 +178,8 @@ const I18N = {
|
|||||||
myRequests: "i18n:govoplan-scheduling.my_scheduling_requests.d28ef235",
|
myRequests: "i18n:govoplan-scheduling.my_scheduling_requests.d28ef235",
|
||||||
name: "i18n:govoplan-scheduling.name.709a2322",
|
name: "i18n:govoplan-scheduling.name.709a2322",
|
||||||
newRequest: "i18n:govoplan-scheduling.new_scheduling_request.2080f675",
|
newRequest: "i18n:govoplan-scheduling.new_scheduling_request.2080f675",
|
||||||
|
noActiveInvitation: "i18n:govoplan-scheduling.no_active_invitation_link_to_revoke.4ad0f0cc",
|
||||||
|
noDeliveryTarget: "i18n:govoplan-scheduling.this_participant_has_no_deliverable_email_address_or_account.dbe14180",
|
||||||
noNotifications: "i18n:govoplan-scheduling.no_notifications_have_been_created.c8d43ca3",
|
noNotifications: "i18n:govoplan-scheduling.no_notifications_have_been_created.c8d43ca3",
|
||||||
notificationsUnavailable: "i18n:govoplan-scheduling.notifications_could_not_be_loaded.f0e1b2c3",
|
notificationsUnavailable: "i18n:govoplan-scheduling.notifications_could_not_be_loaded.f0e1b2c3",
|
||||||
noParticipants: "i18n:govoplan-scheduling.no_participants.73a89101",
|
noParticipants: "i18n:govoplan-scheduling.no_participants.73a89101",
|
||||||
@@ -165,6 +190,7 @@ const I18N = {
|
|||||||
notifyOnAnswersHelp: "i18n:govoplan-scheduling.create_an_organizer_notification_whenever_a_response_is.253ddca8",
|
notifyOnAnswersHelp: "i18n:govoplan-scheduling.create_an_organizer_notification_whenever_a_response_is.253ddca8",
|
||||||
open: "i18n:govoplan-scheduling.open.cf9b7706",
|
open: "i18n:govoplan-scheduling.open.cf9b7706",
|
||||||
openPoll: "i18n:govoplan-scheduling.open_poll.2beac9a7",
|
openPoll: "i18n:govoplan-scheduling.open_poll.2beac9a7",
|
||||||
|
participant: "i18n:govoplan-scheduling.participant.554f4235",
|
||||||
participantEmail: "i18n:govoplan-scheduling.participant_email.2cadfd9e",
|
participantEmail: "i18n:govoplan-scheduling.participant_email.2cadfd9e",
|
||||||
participantRosterHidden: "i18n:govoplan-scheduling.participant_names_and_statuses_are_hidden_aggregate_counts_r.d811a69a",
|
participantRosterHidden: "i18n:govoplan-scheduling.participant_names_and_statuses_are_hidden_aggregate_counts_r.d811a69a",
|
||||||
participantPrivacy: "i18n:govoplan-scheduling.participant_privacy.108c470f",
|
participantPrivacy: "i18n:govoplan-scheduling.participant_privacy.108c470f",
|
||||||
@@ -181,7 +207,12 @@ const I18N = {
|
|||||||
capacity: "i18n:govoplan-scheduling.capacity.d3c375f8",
|
capacity: "i18n:govoplan-scheduling.capacity.d3c375f8",
|
||||||
past: "i18n:govoplan-scheduling.past.405c12fb",
|
past: "i18n:govoplan-scheduling.past.405c12fb",
|
||||||
refresh: "i18n:govoplan-scheduling.refresh_requests.0a3ed7a1",
|
refresh: "i18n:govoplan-scheduling.refresh_requests.0a3ed7a1",
|
||||||
|
reloadInvitation: "i18n:govoplan-scheduling.reload_the_request_before_changing_this_invitation.9e685df4",
|
||||||
reminder: "i18n:govoplan-scheduling.send_reminder.cf5eb3bf",
|
reminder: "i18n:govoplan-scheduling.send_reminder.cf5eb3bf",
|
||||||
|
revokeInvitation: "i18n:govoplan-scheduling.revoke_the_invitation_link_for_value0.15a9c9fa",
|
||||||
|
revokeInvitationConfirm: "i18n:govoplan-scheduling.revoke_the_current_invitation_link_for_value0_it_will_stop_working_immediately.3cdc5817",
|
||||||
|
revokeInvitationLabel: "i18n:govoplan-scheduling.revoke_invitation_link.87bf89cf",
|
||||||
|
revokeLink: "i18n:govoplan-scheduling.revoke_link.da371ee1",
|
||||||
requestFailed: "i18n:govoplan-scheduling.request_failed.9fcda32c",
|
requestFailed: "i18n:govoplan-scheduling.request_failed.9fcda32c",
|
||||||
publicPolicyUnavailable: "i18n:govoplan-scheduling.guest_links_are_not_issued_while_the_configured_participation.0794ebf0",
|
publicPolicyUnavailable: "i18n:govoplan-scheduling.guest_links_are_not_issued_while_the_configured_participation.0794ebf0",
|
||||||
policyLocked: "i18n:govoplan-scheduling.participation_controls_are_locked_after_invitation_links_are_issued.66f5a740",
|
policyLocked: "i18n:govoplan-scheduling.participation_controls_are_locked_after_invitation_links_are_issued.66f5a740",
|
||||||
@@ -199,6 +230,7 @@ const I18N = {
|
|||||||
unsavedResponse: "i18n:govoplan-scheduling.save_or_discard_your_unsent_availability_changes_before_leaving.97e10df1",
|
unsavedResponse: "i18n:govoplan-scheduling.save_or_discard_your_unsent_availability_changes_before_leaving.97e10df1",
|
||||||
singleChoice: "i18n:govoplan-scheduling.participants_can_choose_only_one_option.4311f51c",
|
singleChoice: "i18n:govoplan-scheduling.participants_can_choose_only_one_option.4311f51c",
|
||||||
singleChoiceHelp: "i18n:govoplan-scheduling.each_participant_can_answer_yes_to_at_most_one_candidate.5313a465",
|
singleChoiceHelp: "i18n:govoplan-scheduling.each_participant_can_answer_yes_to_at_most_one_candidate.5313a465",
|
||||||
|
sendInvitation: "i18n:govoplan-scheduling.send_a_fresh_invitation_to_value0.fd8d9dea",
|
||||||
sendResponse: "i18n:govoplan-scheduling.submit_response.a5f0c053",
|
sendResponse: "i18n:govoplan-scheduling.submit_response.a5f0c053",
|
||||||
start: "i18n:govoplan-scheduling.start.952f3754",
|
start: "i18n:govoplan-scheduling.start.952f3754",
|
||||||
statusLabel: "i18n:govoplan-scheduling.status.bae7d5be",
|
statusLabel: "i18n:govoplan-scheduling.status.bae7d5be",
|
||||||
@@ -255,6 +287,8 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
const [summaryUnavailable, setSummaryUnavailable] = useState(false);
|
const [summaryUnavailable, setSummaryUnavailable] = useState(false);
|
||||||
const [notificationsUnavailable, setNotificationsUnavailable] = useState(false);
|
const [notificationsUnavailable, setNotificationsUnavailable] = useState(false);
|
||||||
const [detailsLoading, setDetailsLoading] = useState(false);
|
const [detailsLoading, setDetailsLoading] = useState(false);
|
||||||
|
const [revokeInvitationTarget, setRevokeInvitationTarget] = useState<InvitationRevokeTarget | null>(null);
|
||||||
|
const [invitationActionClock, setInvitationActionClock] = useState(() => new Date());
|
||||||
const detailLoadSequence = useRef(0);
|
const detailLoadSequence = useRef(0);
|
||||||
|
|
||||||
const canWrite = hasScope(auth, "scheduling:schedule:write");
|
const canWrite = hasScope(auth, "scheduling:schedule:write");
|
||||||
@@ -356,6 +390,29 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
void loadDetails(selected.id);
|
void loadDetails(selected.id);
|
||||||
}, [editorMode, selected?.id]);
|
}, [editorMode, selected?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInvitationActionClock(new Date());
|
||||||
|
if (selected?.status !== "cancelled" || !selected.cancellation_notice_until) return undefined;
|
||||||
|
const expiresAt = Date.parse(selected.cancellation_notice_until);
|
||||||
|
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return undefined;
|
||||||
|
let timer: number | undefined;
|
||||||
|
const scheduleExpiryRefresh = () => {
|
||||||
|
const remaining = expiresAt - Date.now();
|
||||||
|
if (remaining <= 0) {
|
||||||
|
setInvitationActionClock(new Date());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timer = window.setTimeout(
|
||||||
|
scheduleExpiryRefresh,
|
||||||
|
Math.min(remaining + 25, 2_147_483_647)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
scheduleExpiryRefresh();
|
||||||
|
return () => {
|
||||||
|
if (timer !== undefined) window.clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [selected?.cancellation_notice_until, selected?.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selected?.id || !selectedParticipant || !canRespond || editorMode || selected.status !== "collecting") {
|
if (!selected?.id || !selectedParticipant || !canRespond || editorMode || selected.status !== "collecting") {
|
||||||
setAvailabilityLoading(false);
|
setAvailabilityLoading(false);
|
||||||
@@ -645,6 +702,91 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runParticipantInvitationAction(
|
||||||
|
requestId: string,
|
||||||
|
participant: SchedulingParticipant,
|
||||||
|
action: "copy" | "send" | "revoke"
|
||||||
|
): Promise<boolean> {
|
||||||
|
const request = requests.find((item) => item.id === requestId);
|
||||||
|
if (!request || !(canAdminister || (canWrite && schedulingRequestIsOwned(request, actor)))) return false;
|
||||||
|
if (!participant.revision) {
|
||||||
|
setError(I18N.reloadInvitation);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let succeeded = false;
|
||||||
|
let failureMessage = "";
|
||||||
|
let completionMessage = "";
|
||||||
|
setSaving(true);
|
||||||
|
setError("");
|
||||||
|
setSuccess("");
|
||||||
|
try {
|
||||||
|
let copied = true;
|
||||||
|
let result: SchedulingInvitationActionResponse;
|
||||||
|
if (action === "copy") {
|
||||||
|
const copyResult = await copySchedulingInvitationLink(
|
||||||
|
issueSchedulingParticipantInvitation(
|
||||||
|
settings,
|
||||||
|
requestId,
|
||||||
|
participant.id,
|
||||||
|
participant.revision,
|
||||||
|
action
|
||||||
|
)
|
||||||
|
);
|
||||||
|
result = copyResult.response;
|
||||||
|
copied = copyResult.copied;
|
||||||
|
} else if (action === "send") {
|
||||||
|
result = await issueSchedulingParticipantInvitation(
|
||||||
|
settings,
|
||||||
|
requestId,
|
||||||
|
participant.id,
|
||||||
|
participant.revision,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = await revokeSchedulingParticipantInvitation(
|
||||||
|
settings,
|
||||||
|
requestId,
|
||||||
|
participant.id,
|
||||||
|
participant.revision
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (action === "copy") {
|
||||||
|
if (!copied) {
|
||||||
|
throw new Error(translateText(I18N.clipboardUnavailable));
|
||||||
|
}
|
||||||
|
completionMessage = I18N.invitationLinkCopied;
|
||||||
|
} else if (action === "send") {
|
||||||
|
if (["failed", "skipped"].includes(result.status)) {
|
||||||
|
throw new Error(translateText(I18N.invitationDeliveryFailed));
|
||||||
|
}
|
||||||
|
completionMessage = I18N.invitationDeliveryRequested;
|
||||||
|
} else {
|
||||||
|
completionMessage = I18N.invitationLinkRevoked;
|
||||||
|
}
|
||||||
|
succeeded = true;
|
||||||
|
} catch (err) {
|
||||||
|
if (isApiError(err, 409)) {
|
||||||
|
failureMessage = I18N.invitationChanged;
|
||||||
|
} else {
|
||||||
|
failureMessage = errorMessage(err, translateText(I18N.requestFailed));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await loadRequests(requestId);
|
||||||
|
await loadDetails(requestId);
|
||||||
|
setSaving(false);
|
||||||
|
if (failureMessage) setError(failureMessage);
|
||||||
|
if (completionMessage) setSuccess(completionMessage);
|
||||||
|
}
|
||||||
|
return succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRevokeInvitation() {
|
||||||
|
if (!revokeInvitationTarget) return;
|
||||||
|
const target = revokeInvitationTarget;
|
||||||
|
setRevokeInvitationTarget(null);
|
||||||
|
await runParticipantInvitationAction(target.requestId, target.participant, "revoke");
|
||||||
|
}
|
||||||
|
|
||||||
async function sendAvailability(event: FormEvent<HTMLFormElement>) {
|
async function sendAvailability(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await persistAvailability();
|
await persistAvailability();
|
||||||
@@ -706,9 +848,9 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
label={I18N.refresh}
|
label={I18N.refresh}
|
||||||
icon={<RefreshCw aria-hidden="true" size={16} />}
|
icon={<RefreshCw aria-hidden="true" size={16} />}
|
||||||
onClick={() => requestNavigation(() => void loadRequests(selected?.id))}
|
onClick={() => requestNavigation(() => void loadRequests(selected?.id))}
|
||||||
disabled={loading} />
|
disabled={loading || saving} />
|
||||||
{canCreateOrWrite ? (
|
{canCreateOrWrite ? (
|
||||||
<Button type="button" variant="primary" onClick={beginCreate}>
|
<Button type="button" variant="primary" onClick={beginCreate} disabled={saving}>
|
||||||
<Plus aria-hidden="true" size={16} /> {I18N.add}
|
<Plus aria-hidden="true" size={16} /> {I18N.add}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -720,12 +862,14 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
requests={groups.owned}
|
requests={groups.owned}
|
||||||
selectedId={editorMode ? undefined : selected?.id}
|
selectedId={editorMode ? undefined : selected?.id}
|
||||||
actor={actor}
|
actor={actor}
|
||||||
|
disabled={saving}
|
||||||
onSelect={selectRequest} />
|
onSelect={selectRequest} />
|
||||||
<RequestGroup
|
<RequestGroup
|
||||||
title={I18N.invitedRequests}
|
title={I18N.invitedRequests}
|
||||||
requests={groups.invited}
|
requests={groups.invited}
|
||||||
selectedId={editorMode ? undefined : selected?.id}
|
selectedId={editorMode ? undefined : selected?.id}
|
||||||
actor={actor}
|
actor={actor}
|
||||||
|
disabled={saving}
|
||||||
onSelect={selectRequest} />
|
onSelect={selectRequest} />
|
||||||
{canAdminister && groups.other.length ? (
|
{canAdminister && groups.other.length ? (
|
||||||
<RequestGroup
|
<RequestGroup
|
||||||
@@ -733,6 +877,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
requests={groups.other}
|
requests={groups.other}
|
||||||
selectedId={editorMode ? undefined : selected?.id}
|
selectedId={editorMode ? undefined : selected?.id}
|
||||||
actor={actor}
|
actor={actor}
|
||||||
|
disabled={saving}
|
||||||
onSelect={selectRequest} />
|
onSelect={selectRequest} />
|
||||||
) : null}
|
) : null}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -1060,7 +1205,18 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{selected.effective_participant_visibility === "names_and_statuses" ? (
|
{selected.effective_participant_visibility === "names_and_statuses" ? (
|
||||||
<ParticipantsGrid participants={selected.participants} />
|
<ParticipantsGrid
|
||||||
|
request={selected}
|
||||||
|
participants={selected.participants}
|
||||||
|
canManage={canManageSelected}
|
||||||
|
saving={saving}
|
||||||
|
now={invitationActionClock}
|
||||||
|
onCopy={(participant) => void runParticipantInvitationAction(selected.id, participant, "copy")}
|
||||||
|
onSend={(participant) => void runParticipantInvitationAction(selected.id, participant, "send")}
|
||||||
|
onRevoke={(participant) => setRevokeInvitationTarget({
|
||||||
|
requestId: selected.id,
|
||||||
|
participant
|
||||||
|
})} />
|
||||||
) : <p className="scheduling-capability-note">{I18N.participantRosterHidden}</p>}
|
) : <p className="scheduling-capability-note">{I18N.participantRosterHidden}</p>}
|
||||||
</Card>
|
</Card>
|
||||||
<Card title={I18N.notifications}>
|
<Card title={I18N.notifications}>
|
||||||
@@ -1090,6 +1246,17 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={Boolean(revokeInvitationTarget)}
|
||||||
|
title={I18N.revokeInvitationLabel}
|
||||||
|
message={i18nMessage(I18N.revokeInvitationConfirm, {
|
||||||
|
value0: participantDisplayLabel(revokeInvitationTarget?.participant ?? null)
|
||||||
|
})}
|
||||||
|
confirmLabel={I18N.revokeLink}
|
||||||
|
tone="danger"
|
||||||
|
busy={saving}
|
||||||
|
onCancel={() => setRevokeInvitationTarget(null)}
|
||||||
|
onConfirm={() => void confirmRevokeInvitation()} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1177,12 +1344,14 @@ function RequestGroup({
|
|||||||
requests,
|
requests,
|
||||||
selectedId,
|
selectedId,
|
||||||
actor,
|
actor,
|
||||||
|
disabled,
|
||||||
onSelect
|
onSelect
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
requests: SchedulingRequest[];
|
requests: SchedulingRequest[];
|
||||||
selectedId?: string;
|
selectedId?: string;
|
||||||
actor: SchedulingActor;
|
actor: SchedulingActor;
|
||||||
|
disabled: boolean;
|
||||||
onSelect: (requestId: string) => void;
|
onSelect: (requestId: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -1196,6 +1365,7 @@ function RequestGroup({
|
|||||||
key={request.id}
|
key={request.id}
|
||||||
selected={selectedId === request.id}
|
selected={selectedId === request.id}
|
||||||
className="scheduling-list-item"
|
className="scheduling-list-item"
|
||||||
|
disabled={disabled}
|
||||||
onClick={() => onSelect(request.id)}>
|
onClick={() => onSelect(request.id)}>
|
||||||
<span><strong>{request.title}</strong><small>{formatRelevantDate(request)}</small></span>
|
<span><strong>{request.title}</strong><small>{formatRelevantDate(request)}</small></span>
|
||||||
<StatusBadge status={request.status} label={requestStatusLabel(request, actor)} />
|
<StatusBadge status={request.status} label={requestStatusLabel(request, actor)} />
|
||||||
@@ -1636,7 +1806,25 @@ function CandidateSlotsGrid({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ParticipantsGrid({ participants }: { participants: SchedulingParticipant[] }) {
|
function ParticipantsGrid({
|
||||||
|
request,
|
||||||
|
participants,
|
||||||
|
canManage,
|
||||||
|
saving,
|
||||||
|
now,
|
||||||
|
onCopy,
|
||||||
|
onSend,
|
||||||
|
onRevoke
|
||||||
|
}: {
|
||||||
|
request: SchedulingRequest;
|
||||||
|
participants: SchedulingParticipant[];
|
||||||
|
canManage: boolean;
|
||||||
|
saving: boolean;
|
||||||
|
now: Date;
|
||||||
|
onCopy: (participant: SchedulingParticipant) => void;
|
||||||
|
onSend: (participant: SchedulingParticipant) => void;
|
||||||
|
onRevoke: (participant: SchedulingParticipant) => void;
|
||||||
|
}) {
|
||||||
const columns: DataGridColumn<SchedulingParticipant>[] = [
|
const columns: DataGridColumn<SchedulingParticipant>[] = [
|
||||||
{ id: "name", header: I18N.name, minWidth: 180, resizable: true, value: (participant) => participant.display_name ?? "" },
|
{ id: "name", header: I18N.name, minWidth: 180, resizable: true, value: (participant) => participant.display_name ?? "" },
|
||||||
{ id: "email", header: I18N.participantEmail, minWidth: 220, resizable: true, value: (participant) => participant.email ?? "" },
|
{ id: "email", header: I18N.participantEmail, minWidth: 220, resizable: true, value: (participant) => participant.email ?? "" },
|
||||||
@@ -1646,7 +1834,52 @@ function ParticipantsGrid({ participants }: { participants: SchedulingParticipan
|
|||||||
width: 140,
|
width: 140,
|
||||||
value: (participant) => participantStatusLabel(participant.status),
|
value: (participant) => participantStatusLabel(participant.status),
|
||||||
render: (participant) => <StatusBadge status={participant.status} label={participantStatusLabel(participant.status)} />
|
render: (participant) => <StatusBadge status={participant.status} label={participantStatusLabel(participant.status)} />
|
||||||
}
|
},
|
||||||
|
...(canManage ? [{
|
||||||
|
id: "actions",
|
||||||
|
header: I18N.actions,
|
||||||
|
width: 132,
|
||||||
|
sticky: "end",
|
||||||
|
align: "right",
|
||||||
|
render: (participant) => {
|
||||||
|
const label = participantDisplayLabel(participant);
|
||||||
|
const blocks = schedulingInvitationActionBlocks(request, participant, now);
|
||||||
|
const copyDisabledReason = invitationActionDisabledReason(blocks.copy, request);
|
||||||
|
const deliveryDisabledReason = invitationActionDisabledReason(blocks.send, request);
|
||||||
|
const revokeDisabledReason = invitationActionDisabledReason(blocks.revoke, request);
|
||||||
|
return (
|
||||||
|
<TableActionGroup
|
||||||
|
minimumSlots={3}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
id: "copy-invitation",
|
||||||
|
label: i18nMessage(I18N.copyInvitationLink, { value0: label }),
|
||||||
|
icon: <Copy aria-hidden="true" size={16} />,
|
||||||
|
disabled: saving || Boolean(copyDisabledReason),
|
||||||
|
disabledReason: copyDisabledReason,
|
||||||
|
onClick: () => onCopy(participant)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "send-invitation",
|
||||||
|
label: i18nMessage(I18N.sendInvitation, { value0: label }),
|
||||||
|
icon: <Send aria-hidden="true" size={16} />,
|
||||||
|
disabled: saving || Boolean(deliveryDisabledReason),
|
||||||
|
disabledReason: deliveryDisabledReason,
|
||||||
|
onClick: () => onSend(participant)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "revoke-invitation",
|
||||||
|
label: i18nMessage(I18N.revokeInvitation, { value0: label }),
|
||||||
|
icon: <Link2Off aria-hidden="true" size={16} />,
|
||||||
|
variant: "danger",
|
||||||
|
disabled: saving || Boolean(revokeDisabledReason),
|
||||||
|
disabledReason: revokeDisabledReason,
|
||||||
|
onClick: () => onRevoke(participant)
|
||||||
|
}
|
||||||
|
]} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} satisfies DataGridColumn<SchedulingParticipant>] : [])
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1660,6 +1893,68 @@ function ParticipantsGrid({ participants }: { participants: SchedulingParticipan
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invitationActionDisabledReason(
|
||||||
|
block: SchedulingInvitationActionBlock | null,
|
||||||
|
request: SchedulingRequest
|
||||||
|
): string | undefined {
|
||||||
|
if (block === "participation_policy_unavailable") {
|
||||||
|
return request.public_participation_policy_enforcement_reason || I18N.publicPolicyUnavailable;
|
||||||
|
}
|
||||||
|
if (block === "cancellation_notice_expired") return I18N.cancellationNoticeExpired;
|
||||||
|
if (block === "delivery_unavailable") return I18N.invitationDeliveryUnavailable;
|
||||||
|
if (block === "no_delivery_target") return I18N.noDeliveryTarget;
|
||||||
|
if (block === "no_active_invitation") return I18N.noActiveInvitation;
|
||||||
|
if (block === "participant_revision_unavailable") return I18N.reloadInvitation;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function participantDisplayLabel(participant: SchedulingParticipant | null): string {
|
||||||
|
return participant?.display_name?.trim() || participant?.email?.trim() || I18N.participant;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copySchedulingInvitationLink(
|
||||||
|
responsePromise: Promise<SchedulingInvitationActionResponse>
|
||||||
|
): Promise<{ response: SchedulingInvitationActionResponse; copied: boolean }> {
|
||||||
|
if (
|
||||||
|
typeof navigator !== "undefined" &&
|
||||||
|
navigator.clipboard?.write &&
|
||||||
|
typeof ClipboardItem !== "undefined"
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const content = responsePromise.then((response) => {
|
||||||
|
const value = response.action_url
|
||||||
|
? schedulingPublicInvitationUrl(response.action_url, window.location.origin)
|
||||||
|
: null;
|
||||||
|
return new Blob([value ?? ""], { type: "text/plain" });
|
||||||
|
}).catch(() => new Blob([], { type: "text/plain" }));
|
||||||
|
await navigator.clipboard.write([new ClipboardItem({ "text/plain": content })]);
|
||||||
|
const response = await responsePromise;
|
||||||
|
const copied = Boolean(
|
||||||
|
response.action_url &&
|
||||||
|
schedulingPublicInvitationUrl(response.action_url, window.location.origin)
|
||||||
|
);
|
||||||
|
return { response, copied };
|
||||||
|
} catch {
|
||||||
|
// The request result is reused below; no second invitation is issued.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await responsePromise;
|
||||||
|
const value = response.action_url
|
||||||
|
? schedulingPublicInvitationUrl(response.action_url, window.location.origin)
|
||||||
|
: null;
|
||||||
|
if (!value) return { response, copied: false };
|
||||||
|
try {
|
||||||
|
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
return { response, copied: true };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// The caller reports the bounded clipboard failure without exposing the URL.
|
||||||
|
}
|
||||||
|
return { response, copied: false };
|
||||||
|
}
|
||||||
|
|
||||||
function firstRequest(groups: SchedulingRequestGroups, includeManaged: boolean): SchedulingRequest | null {
|
function firstRequest(groups: SchedulingRequestGroups, includeManaged: boolean): SchedulingRequest | null {
|
||||||
return groups.owned[0] ?? groups.invited[0] ?? (includeManaged ? groups.other[0] : null) ?? null;
|
return groups.owned[0] ?? groups.invited[0] ?? (includeManaged ? groups.other[0] : null) ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
export const generatedTranslations = {
|
export const generatedTranslations = {
|
||||||
en: {
|
en: {
|
||||||
"i18n:govoplan-scheduling.access_details.79c06b89": "Access details",
|
"i18n:govoplan-scheduling.access_details.79c06b89": "Access details",
|
||||||
|
"i18n:govoplan-scheduling.automatic_invitation_delivery_is_unavailable_copy_the_link_instead.4e39d0b3": "Automatic invitation delivery is unavailable; copy the link instead.",
|
||||||
"i18n:govoplan-scheduling.cancellation_notice_available_until.f840d1e6": "Cancellation notice available until",
|
"i18n:govoplan-scheduling.cancellation_notice_available_until.f840d1e6": "Cancellation notice available until",
|
||||||
|
"i18n:govoplan-scheduling.copy_a_fresh_invitation_link_for_value0.e3799c79": "Copy a fresh invitation link for {value0}",
|
||||||
"i18n:govoplan-scheduling.enter_the_details_supplied_with_the_invitation_for_privacy.81ba419c": "Enter the details supplied with the invitation. For privacy, invalid and expired links use the same response.",
|
"i18n:govoplan-scheduling.enter_the_details_supplied_with_the_invitation_for_privacy.81ba419c": "Enter the details supplied with the invitation. For privacy, invalid and expired links use the same response.",
|
||||||
"i18n:govoplan-scheduling.loading_scheduling_request.43c39c1b": "Loading scheduling request…",
|
"i18n:govoplan-scheduling.loading_scheduling_request.43c39c1b": "Loading scheduling request…",
|
||||||
|
"i18n:govoplan-scheduling.invitation_delivery_failed_the_link_was_created_but_was_not_delivered.8db0c306": "Invitation delivery failed. The link was created but was not delivered.",
|
||||||
|
"i18n:govoplan-scheduling.invitation_delivery_requested.1aaa78ba": "Invitation delivery requested.",
|
||||||
|
"i18n:govoplan-scheduling.invitation_link_copied.332973ec": "Invitation link copied.",
|
||||||
|
"i18n:govoplan-scheduling.invitation_link_revoked.c7dd20d4": "Invitation link revoked.",
|
||||||
|
"i18n:govoplan-scheduling.no_active_invitation_link_to_revoke.4ad0f0cc": "No active invitation link to revoke.",
|
||||||
"i18n:govoplan-scheduling.open_in_scheduling.48df1541": "Open in Scheduling",
|
"i18n:govoplan-scheduling.open_in_scheduling.48df1541": "Open in Scheduling",
|
||||||
"i18n:govoplan-scheduling.open_scheduling_request.31829cce": "Open scheduling request",
|
"i18n:govoplan-scheduling.open_scheduling_request.31829cce": "Open scheduling request",
|
||||||
"i18n:govoplan-scheduling.response_deadline.7fd9e3aa": "Response deadline",
|
"i18n:govoplan-scheduling.response_deadline.7fd9e3aa": "Response deadline",
|
||||||
|
"i18n:govoplan-scheduling.reload_the_request_before_changing_this_invitation.9e685df4": "Reload the request before changing this invitation.",
|
||||||
|
"i18n:govoplan-scheduling.participant.554f4235": "Participant",
|
||||||
|
"i18n:govoplan-scheduling.revoke_invitation_link.87bf89cf": "Revoke invitation link",
|
||||||
|
"i18n:govoplan-scheduling.revoke_link.da371ee1": "Revoke link",
|
||||||
|
"i18n:govoplan-scheduling.revoke_the_current_invitation_link_for_value0_it_will_stop_working_immediately.3cdc5817": "Revoke the current invitation link for {value0}? It will stop working immediately.",
|
||||||
|
"i18n:govoplan-scheduling.revoke_the_invitation_link_for_value0.15a9c9fa": "Revoke the invitation link for {value0}",
|
||||||
|
"i18n:govoplan-scheduling.send_a_fresh_invitation_to_value0.fd8d9dea": "Send a fresh invitation to {value0}",
|
||||||
|
"i18n:govoplan-scheduling.the_cancellation_notice_has_expired_a_new_link_cannot_be_issued.9c6ccc7c": "The cancellation notice has expired; a new link cannot be issued.",
|
||||||
|
"i18n:govoplan-scheduling.the_invitation_link_could_not_be_copied_check_browser_clipboard_permissions_and_try_again.a8b17cbc": "The invitation link could not be copied. Check browser clipboard permissions and try again.",
|
||||||
|
"i18n:govoplan-scheduling.this_participant_has_no_deliverable_email_address_or_account.dbe14180": "This participant has no deliverable email address or account.",
|
||||||
|
"i18n:govoplan-scheduling.this_invitation_changed_the_request_was_reloaded_try_again.c7095533": "This invitation changed. The request was reloaded; try again.",
|
||||||
"i18n:govoplan-scheduling.this_scheduling_link_is_invalid_expired_or_the_access_details.8e7aa197": "This scheduling link is invalid, expired, or the access details do not match.",
|
"i18n:govoplan-scheduling.this_scheduling_link_is_invalid_expired_or_the_access_details.8e7aa197": "This scheduling link is invalid, expired, or the access details do not match.",
|
||||||
"i18n:govoplan-scheduling.this_scheduling_request_was_cancelled.1af3c85e": "This scheduling request was cancelled.",
|
"i18n:govoplan-scheduling.this_scheduling_request_was_cancelled.1af3c85e": "This scheduling request was cancelled.",
|
||||||
"i18n:govoplan-scheduling.this_scheduling_request_is_no_longer_accepting_responses.c612e78a": "This scheduling request is no longer accepting responses.",
|
"i18n:govoplan-scheduling.this_scheduling_request_is_no_longer_accepting_responses.c612e78a": "This scheduling request is no longer accepting responses.",
|
||||||
@@ -155,12 +173,30 @@ export const generatedTranslations = {
|
|||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
"i18n:govoplan-scheduling.access_details.79c06b89": "Zugangsdaten",
|
"i18n:govoplan-scheduling.access_details.79c06b89": "Zugangsdaten",
|
||||||
|
"i18n:govoplan-scheduling.automatic_invitation_delivery_is_unavailable_copy_the_link_instead.4e39d0b3": "Die automatische Einladungszustellung ist nicht verfügbar; kopieren Sie stattdessen den Link.",
|
||||||
"i18n:govoplan-scheduling.cancellation_notice_available_until.f840d1e6": "Stornierungshinweis verfügbar bis",
|
"i18n:govoplan-scheduling.cancellation_notice_available_until.f840d1e6": "Stornierungshinweis verfügbar bis",
|
||||||
|
"i18n:govoplan-scheduling.copy_a_fresh_invitation_link_for_value0.e3799c79": "Einen neuen Einladungslink für {value0} kopieren",
|
||||||
"i18n:govoplan-scheduling.enter_the_details_supplied_with_the_invitation_for_privacy.81ba419c": "Geben Sie die mit der Einladung übermittelten Daten ein. Aus Datenschutzgründen wird für ungültige und abgelaufene Links dieselbe Meldung angezeigt.",
|
"i18n:govoplan-scheduling.enter_the_details_supplied_with_the_invitation_for_privacy.81ba419c": "Geben Sie die mit der Einladung übermittelten Daten ein. Aus Datenschutzgründen wird für ungültige und abgelaufene Links dieselbe Meldung angezeigt.",
|
||||||
"i18n:govoplan-scheduling.loading_scheduling_request.43c39c1b": "Terminanfrage wird geladen …",
|
"i18n:govoplan-scheduling.loading_scheduling_request.43c39c1b": "Terminanfrage wird geladen …",
|
||||||
|
"i18n:govoplan-scheduling.invitation_delivery_failed_the_link_was_created_but_was_not_delivered.8db0c306": "Die Zustellung der Einladung ist fehlgeschlagen. Der Link wurde erstellt, aber nicht zugestellt.",
|
||||||
|
"i18n:govoplan-scheduling.invitation_delivery_requested.1aaa78ba": "Die Zustellung der Einladung wurde angefordert.",
|
||||||
|
"i18n:govoplan-scheduling.invitation_link_copied.332973ec": "Einladungslink kopiert.",
|
||||||
|
"i18n:govoplan-scheduling.invitation_link_revoked.c7dd20d4": "Einladungslink widerrufen.",
|
||||||
|
"i18n:govoplan-scheduling.no_active_invitation_link_to_revoke.4ad0f0cc": "Kein aktiver Einladungslink zum Widerrufen vorhanden.",
|
||||||
"i18n:govoplan-scheduling.open_in_scheduling.48df1541": "In der Terminplanung öffnen",
|
"i18n:govoplan-scheduling.open_in_scheduling.48df1541": "In der Terminplanung öffnen",
|
||||||
"i18n:govoplan-scheduling.open_scheduling_request.31829cce": "Terminanfrage öffnen",
|
"i18n:govoplan-scheduling.open_scheduling_request.31829cce": "Terminanfrage öffnen",
|
||||||
"i18n:govoplan-scheduling.response_deadline.7fd9e3aa": "Antwortfrist",
|
"i18n:govoplan-scheduling.response_deadline.7fd9e3aa": "Antwortfrist",
|
||||||
|
"i18n:govoplan-scheduling.reload_the_request_before_changing_this_invitation.9e685df4": "Laden Sie die Anfrage neu, bevor Sie diese Einladung ändern.",
|
||||||
|
"i18n:govoplan-scheduling.participant.554f4235": "Teilnehmende Person",
|
||||||
|
"i18n:govoplan-scheduling.revoke_invitation_link.87bf89cf": "Einladungslink widerrufen",
|
||||||
|
"i18n:govoplan-scheduling.revoke_link.da371ee1": "Link widerrufen",
|
||||||
|
"i18n:govoplan-scheduling.revoke_the_current_invitation_link_for_value0_it_will_stop_working_immediately.3cdc5817": "Den aktuellen Einladungslink für {value0} widerrufen? Er funktioniert danach sofort nicht mehr.",
|
||||||
|
"i18n:govoplan-scheduling.revoke_the_invitation_link_for_value0.15a9c9fa": "Den Einladungslink für {value0} widerrufen",
|
||||||
|
"i18n:govoplan-scheduling.send_a_fresh_invitation_to_value0.fd8d9dea": "Eine neue Einladung an {value0} senden",
|
||||||
|
"i18n:govoplan-scheduling.the_cancellation_notice_has_expired_a_new_link_cannot_be_issued.9c6ccc7c": "Der Stornierungshinweis ist abgelaufen; ein neuer Link kann nicht ausgestellt werden.",
|
||||||
|
"i18n:govoplan-scheduling.the_invitation_link_could_not_be_copied_check_browser_clipboard_permissions_and_try_again.a8b17cbc": "Der Einladungslink konnte nicht kopiert werden. Prüfen Sie die Zwischenablageberechtigungen des Browsers und versuchen Sie es erneut.",
|
||||||
|
"i18n:govoplan-scheduling.this_participant_has_no_deliverable_email_address_or_account.dbe14180": "Für diese teilnehmende Person ist weder eine zustellbare E-Mail-Adresse noch ein Konto hinterlegt.",
|
||||||
|
"i18n:govoplan-scheduling.this_invitation_changed_the_request_was_reloaded_try_again.c7095533": "Diese Einladung wurde zwischenzeitlich geändert. Die Anfrage wurde neu geladen; versuchen Sie es erneut.",
|
||||||
"i18n:govoplan-scheduling.this_scheduling_link_is_invalid_expired_or_the_access_details.8e7aa197": "Dieser Terminlink ist ungültig oder abgelaufen, oder die Zugangsdaten stimmen nicht überein.",
|
"i18n:govoplan-scheduling.this_scheduling_link_is_invalid_expired_or_the_access_details.8e7aa197": "Dieser Terminlink ist ungültig oder abgelaufen, oder die Zugangsdaten stimmen nicht überein.",
|
||||||
"i18n:govoplan-scheduling.this_scheduling_request_was_cancelled.1af3c85e": "Diese Terminanfrage wurde storniert.",
|
"i18n:govoplan-scheduling.this_scheduling_request_was_cancelled.1af3c85e": "Diese Terminanfrage wurde storniert.",
|
||||||
"i18n:govoplan-scheduling.this_scheduling_request_is_no_longer_accepting_responses.c612e78a": "Diese Terminanfrage nimmt keine Antworten mehr an.",
|
"i18n:govoplan-scheduling.this_scheduling_request_is_no_longer_accepting_responses.c612e78a": "Diese Terminanfrage nimmt keine Antworten mehr an.",
|
||||||
|
|||||||
Reference in New Issue
Block a user