feat(scheduling): use central people picker
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
|
||||
import {
|
||||
apiFetch,
|
||||
type ApiSettings,
|
||||
type PeoplePickerSearchGroup
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
export type SchedulingStatus = "draft" | "collecting" | "closed" | "decided" | "handed_off" | "cancelled" | "archived";
|
||||
export type SchedulingParticipantVisibility = "aggregates_only" | "names_and_statuses";
|
||||
@@ -299,31 +303,25 @@ export type SchedulingSummaryResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type SchedulingAddressLookupCandidate = {
|
||||
contact_id: string;
|
||||
address_book_id: string;
|
||||
display_name: string;
|
||||
email?: string | null;
|
||||
email_label?: string | null;
|
||||
organization?: string | null;
|
||||
role_title?: string | null;
|
||||
tags: string[];
|
||||
source_kind: string;
|
||||
source_ref?: string | null;
|
||||
source_revision?: string | null;
|
||||
provenance: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SchedulingAddressLookupResponse = {
|
||||
available: boolean;
|
||||
candidates: SchedulingAddressLookupCandidate[];
|
||||
export type SchedulingPeopleSearchResponse = {
|
||||
groups: PeoplePickerSearchGroup[];
|
||||
};
|
||||
|
||||
const json = (payload: unknown) => ({ method: "POST", body: JSON.stringify(payload ?? {}) });
|
||||
|
||||
export function lookupSchedulingAddresses(settings: ApiSettings, query: string, limit = 25): Promise<SchedulingAddressLookupResponse> {
|
||||
export async function searchSchedulingPeople(
|
||||
settings: ApiSettings,
|
||||
query: string,
|
||||
limit = 25,
|
||||
signal?: AbortSignal
|
||||
): Promise<PeoplePickerSearchGroup[]> {
|
||||
const params = new URLSearchParams({ query, limit: String(limit) });
|
||||
return apiFetch<SchedulingAddressLookupResponse>(settings, `/api/v1/scheduling/address-lookup?${params.toString()}`);
|
||||
const response = await apiFetch<SchedulingPeopleSearchResponse>(
|
||||
settings,
|
||||
`/api/v1/scheduling/people?${params.toString()}`,
|
||||
{ signal }
|
||||
);
|
||||
return response.groups;
|
||||
}
|
||||
|
||||
export function listSchedulingRequests(settings: ApiSettings, status?: string): Promise<SchedulingRequestListResponse> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState, type FormEvent } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
Bell,
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
DataGrid,
|
||||
DataGridEmptyAction,
|
||||
DataGridRowActions,
|
||||
DateTimeField,
|
||||
DismissibleAlert,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
IconButton,
|
||||
PageTitle,
|
||||
PasswordField,
|
||||
PeoplePicker,
|
||||
formatDateTime,
|
||||
SelectionList,
|
||||
SelectionListItem,
|
||||
@@ -41,7 +41,9 @@ import {
|
||||
type AuthInfo,
|
||||
type CalendarPickerUiCapability,
|
||||
type DataGridColumn,
|
||||
type FormatDateTimeOptions
|
||||
type FormatDateTimeOptions,
|
||||
type PeoplePickerItem,
|
||||
type PeoplePickerSearch
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
closeSchedulingRequest,
|
||||
@@ -55,6 +57,7 @@ import {
|
||||
listSchedulingNotifications,
|
||||
listSchedulingRequests,
|
||||
openSchedulingRequest,
|
||||
searchSchedulingPeople,
|
||||
schedulingSummary,
|
||||
submitSchedulingAvailability,
|
||||
updateSchedulingRequest,
|
||||
@@ -70,10 +73,14 @@ import {
|
||||
applySchedulingAvailabilityChoice,
|
||||
groupSchedulingRequests,
|
||||
schedulingParticipantForActor,
|
||||
participantDraftFromResponse,
|
||||
participantDraftsFromPicker,
|
||||
participantPayload,
|
||||
schedulingRelevantTimestamp,
|
||||
schedulingRequestIsOwned,
|
||||
schedulingSortPhase,
|
||||
type SchedulingActor,
|
||||
type SchedulingParticipantDraft,
|
||||
type SchedulingRequestGroups
|
||||
} from "./schedulingViewModel";
|
||||
|
||||
@@ -89,17 +96,7 @@ type SlotDraft = {
|
||||
timezone: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
type ParticipantDraft = {
|
||||
draftId: string;
|
||||
sourceId?: string;
|
||||
respondent_id?: string | null;
|
||||
display_name: string;
|
||||
email: string;
|
||||
participant_type: "internal" | "external" | "resource";
|
||||
required: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
identityLocked?: boolean;
|
||||
};
|
||||
type ParticipantDraft = SchedulingParticipantDraft;
|
||||
|
||||
const I18N = {
|
||||
actions: "i18n:govoplan-core.actions.c3cd636a",
|
||||
@@ -174,6 +171,9 @@ const I18N = {
|
||||
participantRosterVisibility: "i18n:govoplan-scheduling.share_participant_names_and_response_statuses.df0bf9e0",
|
||||
participantRosterVisibilityHelp: "i18n:govoplan-scheduling.when_enabled_participants_can_see_other_participants_names.3ac78361",
|
||||
participants: "i18n:govoplan-scheduling.participants.cd56e083",
|
||||
participantPickerHelp: "i18n:govoplan-scheduling.search_visible_accounts_and_contacts_or_add_an_external_perso.877f6b44",
|
||||
allowExternalParticipants: "i18n:govoplan-scheduling.allow_external_participants.a9efcb52",
|
||||
allowExternalParticipantsHelp: "i18n:govoplan-scheduling.when_enabled_people_outside_the_configured_accounts_and.78829735",
|
||||
participation: "i18n:govoplan-scheduling.participation.9ad70cc4",
|
||||
participationRate: "i18n:govoplan-scheduling.participation_rate.46bc5504",
|
||||
responses: "i18n:govoplan-scheduling.responses.3427e3ab",
|
||||
@@ -235,7 +235,8 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
const [calendarId, setCalendarId] = useState("");
|
||||
const [calendarEnabled, setCalendarEnabled] = useState(false);
|
||||
const [slots, setSlots] = useState<SlotDraft[]>(() => initialSlots(translateText));
|
||||
const [participants, setParticipants] = useState<ParticipantDraft[]>(() => [emptyParticipant()]);
|
||||
const [participants, setParticipants] = useState<ParticipantDraft[]>([]);
|
||||
const [allowExternalParticipants, setAllowExternalParticipants] = useState(true);
|
||||
const [participantRosterVisible, setParticipantRosterVisible] = useState(false);
|
||||
const [notifyOnAnswers, setNotifyOnAnswers] = useState(true);
|
||||
const [singleChoice, setSingleChoice] = useState(false);
|
||||
@@ -323,6 +324,10 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
const participationPolicyLocked = Boolean(
|
||||
editorOriginal?.participants.some((participant) => participant.poll_invitation_id)
|
||||
);
|
||||
const participantSearch = useCallback<PeoplePickerSearch>(
|
||||
(query, options) => searchSchedulingPeople(settings, query, options.limit, options.signal),
|
||||
[settings]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void loadRequests();
|
||||
@@ -482,17 +487,10 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
timezone: slot.timezone,
|
||||
metadata: slot.metadata
|
||||
})));
|
||||
setParticipants(request.participants.map((participant) => ({
|
||||
draftId: nextDraftId("participant"),
|
||||
sourceId: participant.id,
|
||||
respondent_id: participant.respondent_id,
|
||||
display_name: participant.display_name ?? "",
|
||||
email: participant.email ?? "",
|
||||
participant_type: normalizeParticipantType(participant.participant_type),
|
||||
required: participant.required ?? true,
|
||||
metadata: participant.metadata,
|
||||
identityLocked: Boolean(participant.poll_invitation_id)
|
||||
})));
|
||||
setParticipants(request.participants.map((participant) => (
|
||||
participantDraftFromResponse(participant, nextDraftId("participant"))
|
||||
)));
|
||||
setAllowExternalParticipants(request.allow_external_participants);
|
||||
setParticipantRosterVisible(request.participant_visibility === "names_and_statuses");
|
||||
setNotifyOnAnswers(request.notify_on_answers);
|
||||
setSingleChoice(request.single_choice);
|
||||
@@ -555,6 +553,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
title: title.trim(),
|
||||
description: description.trim() || null,
|
||||
location: location.trim() || null,
|
||||
allow_external_participants: allowExternalParticipants,
|
||||
participant_visibility: participantRosterVisible ? "names_and_statuses" : "aggregates_only",
|
||||
notify_on_answers: notifyOnAnswers,
|
||||
single_choice: singleChoice,
|
||||
@@ -587,16 +586,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
location: location.trim() || null,
|
||||
metadata: slot.metadata ?? {}
|
||||
})),
|
||||
participants: participants
|
||||
.filter((participant) => participant.email.trim() || participant.display_name.trim())
|
||||
.map((participant) => ({
|
||||
respondent_id: participant.respondent_id ?? null,
|
||||
display_name: participant.display_name.trim() || null,
|
||||
email: participant.email.trim() || null,
|
||||
required: participant.required,
|
||||
participant_type: participant.participant_type,
|
||||
metadata: participant.metadata ?? {}
|
||||
})),
|
||||
participants: participants.map(participantPayload),
|
||||
create_participant_invitations: true
|
||||
});
|
||||
setRequests((items) => [request, ...items]);
|
||||
@@ -617,17 +607,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
location: location.trim() || null,
|
||||
metadata: slot.metadata ?? {}
|
||||
})),
|
||||
participants: participants
|
||||
.filter((participant) => participant.sourceId || participant.email.trim() || participant.display_name.trim())
|
||||
.map((participant) => ({
|
||||
...(participant.sourceId ? { id: participant.sourceId } : {}),
|
||||
respondent_id: participant.respondent_id ?? null,
|
||||
display_name: participant.display_name.trim() || null,
|
||||
email: participant.email.trim() || null,
|
||||
participant_type: participant.participant_type,
|
||||
required: participant.required,
|
||||
metadata: participant.metadata ?? {}
|
||||
})),
|
||||
participants: participants.map(participantPayload),
|
||||
create_participant_invitations: true
|
||||
});
|
||||
replaceRequest(request);
|
||||
@@ -851,16 +831,18 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
<EditableParticipants
|
||||
participants={participants}
|
||||
disabled={!canCreateOrWrite}
|
||||
onAddBelow={(index) => {
|
||||
setParticipants((items) => insertAfter(items, index, emptyParticipant()));
|
||||
allowExternalParticipants={allowExternalParticipants}
|
||||
search={participantSearch}
|
||||
onAllowExternalParticipantsChange={(value) => {
|
||||
setAllowExternalParticipants(value);
|
||||
setDraftDirty(true);
|
||||
}}
|
||||
onChange={(index, patch) => {
|
||||
setParticipants((items) => items.map((item, itemIndex) => itemIndex === index ? { ...item, ...patch } : item));
|
||||
setDraftDirty(true);
|
||||
}}
|
||||
onRemove={(index) => {
|
||||
setParticipants((items) => items.filter((_item, itemIndex) => itemIndex !== index));
|
||||
onChange={(value) => {
|
||||
setParticipants((current) => participantDraftsFromPicker(
|
||||
value,
|
||||
current,
|
||||
() => nextDraftId("participant")
|
||||
));
|
||||
setDraftDirty(true);
|
||||
}} />
|
||||
|
||||
@@ -1174,7 +1156,8 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
setCalendarId("");
|
||||
setCalendarEnabled(false);
|
||||
setSlots(initialSlots(translateText));
|
||||
setParticipants([emptyParticipant()]);
|
||||
setParticipants([]);
|
||||
setAllowExternalParticipants(true);
|
||||
setParticipantRosterVisible(false);
|
||||
setNotifyOnAnswers(true);
|
||||
setSingleChoice(false);
|
||||
@@ -1518,82 +1501,40 @@ function EditableSlots({
|
||||
function EditableParticipants({
|
||||
participants,
|
||||
disabled,
|
||||
onAddBelow,
|
||||
onChange,
|
||||
onRemove
|
||||
allowExternalParticipants,
|
||||
search,
|
||||
onAllowExternalParticipantsChange,
|
||||
onChange
|
||||
}: {
|
||||
participants: ParticipantDraft[];
|
||||
disabled: boolean;
|
||||
onAddBelow: (index: number) => void;
|
||||
onChange: (index: number, patch: Partial<ParticipantDraft>) => void;
|
||||
onRemove: (index: number) => void;
|
||||
allowExternalParticipants: boolean;
|
||||
search: PeoplePickerSearch;
|
||||
onAllowExternalParticipantsChange: (value: boolean) => void;
|
||||
onChange: (participants: PeoplePickerItem[]) => void;
|
||||
}) {
|
||||
const lockedIdentityHelpId = "scheduling-invited-participant-identity-help";
|
||||
const columns: DataGridColumn<ParticipantDraft>[] = [
|
||||
{
|
||||
id: "name",
|
||||
header: I18N.name,
|
||||
minWidth: 180,
|
||||
resizable: true,
|
||||
value: (participant) => participant.display_name,
|
||||
render: (participant, index) => (
|
||||
<input
|
||||
disabled={disabled || participant.identityLocked}
|
||||
maxLength={500}
|
||||
aria-describedby={participant.identityLocked ? lockedIdentityHelpId : undefined}
|
||||
aria-label={I18N.name}
|
||||
value={participant.display_name}
|
||||
onChange={(event) => onChange(index, { display_name: event.target.value })} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
header: I18N.participantEmail,
|
||||
minWidth: 260,
|
||||
resizable: true,
|
||||
value: (participant) => participant.email,
|
||||
render: (participant, index) => (
|
||||
<input
|
||||
disabled={disabled || participant.identityLocked}
|
||||
type="email"
|
||||
maxLength={320}
|
||||
aria-describedby={participant.identityLocked ? lockedIdentityHelpId : undefined}
|
||||
aria-label={I18N.participantEmail}
|
||||
value={participant.email}
|
||||
onChange={(event) => onChange(index, { email: event.target.value })} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: I18N.actions,
|
||||
width: 112,
|
||||
sticky: "end",
|
||||
render: (_participant, index) => (
|
||||
<DataGridRowActions
|
||||
disabled={disabled}
|
||||
reorderable={false}
|
||||
onAddBelow={() => onAddBelow(index)}
|
||||
onRemove={() => onRemove(index)}
|
||||
addLabel={I18N.addParticipant}
|
||||
removeLabel={i18nMessage("i18n:govoplan-scheduling.remove_participant_value.e55f2b70", { value0: index + 1 })} />
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Card title={I18N.participants}>
|
||||
{participants.some((participant) => participant.identityLocked) ? (
|
||||
<p id={lockedIdentityHelpId} className="scheduling-capability-note">{I18N.invitedIdentityLocked}</p>
|
||||
<p className="scheduling-capability-note">{I18N.invitedIdentityLocked}</p>
|
||||
) : null}
|
||||
<DataGrid
|
||||
id="scheduling-create-participants-grid"
|
||||
rows={participants}
|
||||
columns={columns}
|
||||
getRowKey={(participant) => participant.draftId}
|
||||
initialFit="container"
|
||||
emptyText={I18N.noParticipants}
|
||||
emptyActionColumnId="actions"
|
||||
emptyAction={<DataGridEmptyAction disabled={disabled} reorderable={false} onAdd={() => onAddBelow(-1)} label={I18N.addParticipant} />} />
|
||||
<ToggleSwitch
|
||||
label={I18N.allowExternalParticipants}
|
||||
help={I18N.allowExternalParticipantsHelp}
|
||||
checked={allowExternalParticipants}
|
||||
disabled={disabled}
|
||||
onChange={onAllowExternalParticipantsChange} />
|
||||
<PeoplePicker
|
||||
id="scheduling-participant-picker"
|
||||
label={I18N.addParticipant}
|
||||
help={I18N.participantPickerHelp}
|
||||
selectedLabel={I18N.participants}
|
||||
value={participants}
|
||||
search={search}
|
||||
disabled={disabled}
|
||||
allowManualExternal={allowExternalParticipants}
|
||||
manualEmailRequired
|
||||
onChange={onChange} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1792,21 +1733,6 @@ function newSlot(position: number, translateText: (value: string) => string): Sl
|
||||
};
|
||||
}
|
||||
|
||||
function emptyParticipant(): ParticipantDraft {
|
||||
return {
|
||||
draftId: nextDraftId("participant"),
|
||||
display_name: "",
|
||||
email: "",
|
||||
participant_type: "external",
|
||||
required: true,
|
||||
metadata: {}
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeParticipantType(value: string | null): ParticipantDraft["participant_type"] {
|
||||
return value === "internal" || value === "resource" ? value : "external";
|
||||
}
|
||||
|
||||
let draftIdSequence = 0;
|
||||
|
||||
function nextDraftId(prefix: string): string {
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import type {
|
||||
SchedulingAvailabilityValue,
|
||||
SchedulingParticipant,
|
||||
SchedulingParticipantPayload,
|
||||
SchedulingRequest
|
||||
} from "../../api/scheduling";
|
||||
import type { PeoplePickerItem } from "@govoplan/core-webui";
|
||||
|
||||
const DIRECTORY_SELECTION_METADATA_KEY = "directory_selection";
|
||||
|
||||
export type SchedulingParticipantDraft = PeoplePickerItem & {
|
||||
draftId: string;
|
||||
sourceId?: string;
|
||||
respondent_id?: string | null;
|
||||
display_name: string;
|
||||
email: string;
|
||||
participant_type: "internal" | "external" | "resource";
|
||||
required: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
identityLocked?: boolean;
|
||||
};
|
||||
|
||||
export type SchedulingActor = {
|
||||
accountId?: string | null;
|
||||
@@ -25,6 +41,106 @@ export type SchedulingSortPhase =
|
||||
| "determined"
|
||||
| "past";
|
||||
|
||||
type DirectorySelection = {
|
||||
selection_key?: string;
|
||||
kind?: PeoplePickerItem["kind"];
|
||||
reference_id?: string | null;
|
||||
source_module?: string | null;
|
||||
source_label?: string | null;
|
||||
source_revision?: string | null;
|
||||
};
|
||||
|
||||
function directorySelection(metadata?: Record<string, unknown>): DirectorySelection | null {
|
||||
const value = metadata?.[DIRECTORY_SELECTION_METADATA_KEY];
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
return value as DirectorySelection;
|
||||
}
|
||||
|
||||
function normalizedParticipantType(value: string | null): SchedulingParticipantDraft["participant_type"] {
|
||||
return value === "internal" || value === "resource" ? value : "external";
|
||||
}
|
||||
|
||||
function selectionMetadata(item: PeoplePickerItem): Record<string, unknown> {
|
||||
if (item.kind === "external") return {};
|
||||
return {
|
||||
[DIRECTORY_SELECTION_METADATA_KEY]: {
|
||||
selection_key: item.selection_key,
|
||||
kind: item.kind,
|
||||
reference_id: item.reference_id ?? null,
|
||||
source_module: item.source_module ?? null,
|
||||
source_label: item.source_label ?? null,
|
||||
source_revision: item.source_revision ?? null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function participantDraftFromResponse(
|
||||
participant: SchedulingParticipant,
|
||||
draftId: string
|
||||
): SchedulingParticipantDraft {
|
||||
const selection = directorySelection(participant.metadata);
|
||||
const kind = selection?.kind ?? (participant.respondent_id ? "account" : "external");
|
||||
const referenceId = selection?.reference_id
|
||||
?? (kind === "account" ? participant.respondent_id : null);
|
||||
const email = participant.email?.trim().toLowerCase() || null;
|
||||
return {
|
||||
selection_key: selection?.selection_key
|
||||
?? (email ? `${kind}:${email}` : `${kind}:participant:${participant.id}`),
|
||||
kind,
|
||||
reference_id: referenceId,
|
||||
display_name: participant.display_name?.trim() || email || "—",
|
||||
email: email ?? "",
|
||||
source_module: selection?.source_module ?? null,
|
||||
source_label: selection?.source_label ?? null,
|
||||
source_revision: selection?.source_revision ?? null,
|
||||
draftId,
|
||||
sourceId: participant.id,
|
||||
respondent_id: participant.respondent_id,
|
||||
participant_type: normalizedParticipantType(participant.participant_type),
|
||||
required: participant.required ?? true,
|
||||
metadata: participant.metadata ?? {},
|
||||
identityLocked: Boolean(participant.poll_invitation_id)
|
||||
};
|
||||
}
|
||||
|
||||
export function participantDraftsFromPicker(
|
||||
selected: PeoplePickerItem[],
|
||||
current: SchedulingParticipantDraft[],
|
||||
nextDraftId: () => string
|
||||
): SchedulingParticipantDraft[] {
|
||||
const currentByKey = new Map(current.map((participant) => [participant.selection_key, participant]));
|
||||
return selected.map((item) => {
|
||||
const existing = currentByKey.get(item.selection_key);
|
||||
if (existing) return existing;
|
||||
const kind = item.kind === "account" ? "account" : item.kind === "contact" ? "contact" : "external";
|
||||
return {
|
||||
...item,
|
||||
kind,
|
||||
email: item.email?.trim().toLowerCase() || "",
|
||||
draftId: nextDraftId(),
|
||||
respondent_id: kind === "account" ? item.reference_id ?? null : null,
|
||||
participant_type: kind === "account" ? "internal" : "external",
|
||||
required: true,
|
||||
metadata: selectionMetadata(item),
|
||||
identityLocked: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function participantPayload(
|
||||
participant: SchedulingParticipantDraft
|
||||
): SchedulingParticipantPayload & { id?: string } {
|
||||
return {
|
||||
...(participant.sourceId ? { id: participant.sourceId } : {}),
|
||||
respondent_id: participant.respondent_id ?? null,
|
||||
display_name: participant.display_name.trim() || null,
|
||||
email: participant.email.trim() || null,
|
||||
participant_type: participant.participant_type,
|
||||
required: participant.required,
|
||||
metadata: participant.metadata ?? {}
|
||||
};
|
||||
}
|
||||
|
||||
export function schedulingActorIds(actor: SchedulingActor): string[] {
|
||||
return Array.from(new Set([
|
||||
actor.accountId,
|
||||
|
||||
@@ -13,6 +13,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.a_slot_can_be_selected_after_the_request_is_closed.f91ec02d": "A slot can be selected after the request is closed.",
|
||||
"i18n:govoplan-scheduling.add_maybe_between_yes_and_no_for_each_candidate_slot.74dc9db6": "Add Maybe between Available and Unavailable for each candidate slot.",
|
||||
"i18n:govoplan-scheduling.allow_comments.d63202a6": "Allow comments",
|
||||
"i18n:govoplan-scheduling.allow_external_participants.a9efcb52": "Allow external participants",
|
||||
"i18n:govoplan-scheduling.awaiting.42aa82e0": "Awaiting",
|
||||
"i18n:govoplan-scheduling.capacity.d3c375f8": "Capacity",
|
||||
"i18n:govoplan-scheduling.comment.d03495b1": "Comment",
|
||||
@@ -35,6 +36,8 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.password_protect_guest_access.13d7f08b": "Password-protect guest access",
|
||||
"i18n:govoplan-scheduling.people_responding_without_an_account_must_provide_an_email.19fd3dc8": "People responding without an account must provide an email address.",
|
||||
"i18n:govoplan-scheduling.people_who_are_not_signed_in_must_enter_this_password.82bcc4ce": "People who are not signed in must enter this password before viewing the request.",
|
||||
"i18n:govoplan-scheduling.search_visible_accounts_and_contacts_or_add_an_external_perso.877f6b44": "Search accounts and contacts you are allowed to discover. If external participants are enabled, you can also add a name and email address manually.",
|
||||
"i18n:govoplan-scheduling.when_enabled_people_outside_the_configured_accounts_and.78829735": "When enabled, people outside the configured accounts and contacts can be added manually.",
|
||||
"i18n:govoplan-scheduling.provide_a_maybe_option.e39da57a": "Provide a Maybe option",
|
||||
"i18n:govoplan-scheduling.require_an_email_address_from_guests.c2289a58": "Require an email address from guests",
|
||||
"i18n:govoplan-scheduling.responses.3427e3ab": "Responses",
|
||||
@@ -162,6 +165,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.a_slot_can_be_selected_after_the_request_is_closed.f91ec02d": "Ein Terminvorschlag kann ausgewählt werden, nachdem die Anfrage geschlossen wurde.",
|
||||
"i18n:govoplan-scheduling.add_maybe_between_yes_and_no_for_each_candidate_slot.74dc9db6": "Für jeden Terminvorschlag Vielleicht zwischen Verfügbar und Nicht verfügbar anbieten.",
|
||||
"i18n:govoplan-scheduling.allow_comments.d63202a6": "Kommentare erlauben",
|
||||
"i18n:govoplan-scheduling.allow_external_participants.a9efcb52": "Externe Teilnehmende erlauben",
|
||||
"i18n:govoplan-scheduling.awaiting.42aa82e0": "Ausstehend",
|
||||
"i18n:govoplan-scheduling.capacity.d3c375f8": "Kapazität",
|
||||
"i18n:govoplan-scheduling.comment.d03495b1": "Kommentar",
|
||||
@@ -184,6 +188,8 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.password_protect_guest_access.13d7f08b": "Gastzugang mit Passwort schützen",
|
||||
"i18n:govoplan-scheduling.people_responding_without_an_account_must_provide_an_email.19fd3dc8": "Personen ohne Konto müssen für ihre Antwort eine E-Mail-Adresse angeben.",
|
||||
"i18n:govoplan-scheduling.people_who_are_not_signed_in_must_enter_this_password.82bcc4ce": "Nicht angemeldete Personen müssen dieses Passwort eingeben, bevor sie die Anfrage sehen können.",
|
||||
"i18n:govoplan-scheduling.search_visible_accounts_and_contacts_or_add_an_external_perso.877f6b44": "Suchen Sie nach Konten und Kontakten, die Sie sehen dürfen. Wenn externe Teilnehmende erlaubt sind, können Sie Name und E-Mail-Adresse auch manuell hinzufügen.",
|
||||
"i18n:govoplan-scheduling.when_enabled_people_outside_the_configured_accounts_and.78829735": "Wenn diese Option aktiviert ist, können Personen außerhalb der eingerichteten Konten und Kontakte manuell hinzugefügt werden.",
|
||||
"i18n:govoplan-scheduling.provide_a_maybe_option.e39da57a": "Antwort Vielleicht anbieten",
|
||||
"i18n:govoplan-scheduling.require_an_email_address_from_guests.c2289a58": "E-Mail-Adresse von Gästen verlangen",
|
||||
"i18n:govoplan-scheduling.responses.3427e3ab": "Antworten",
|
||||
|
||||
@@ -13,7 +13,7 @@ export const schedulingModule: PlatformWebModule = {
|
||||
label: "Scheduling",
|
||||
version: "0.1.10",
|
||||
dependencies: ["poll"],
|
||||
optionalDependencies: ["calendar", "mail", "notifications", "workflow", "appointments", "addresses"],
|
||||
optionalDependencies: ["access", "calendar", "mail", "notifications", "workflow", "appointments", "addresses"],
|
||||
translations: generatedTranslations,
|
||||
navItems: [{ to: "/scheduling", label: "Scheduling", iconName: "calendar-clock", anyOf: scheduleRead, order: 56 }],
|
||||
routes: [
|
||||
|
||||
Reference in New Issue
Block a user