intermittent commit
This commit is contained in:
@@ -161,8 +161,33 @@ 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[];
|
||||
};
|
||||
|
||||
const json = (payload: unknown) => ({ method: "POST", body: JSON.stringify(payload ?? {}) });
|
||||
|
||||
export function lookupSchedulingAddresses(settings: ApiSettings, query: string, limit = 25): Promise<SchedulingAddressLookupResponse> {
|
||||
const params = new URLSearchParams({ query, limit: String(limit) });
|
||||
return apiFetch<SchedulingAddressLookupResponse>(settings, `/api/v1/scheduling/address-lookup?${params.toString()}`);
|
||||
}
|
||||
|
||||
export function listSchedulingRequests(settings: ApiSettings, status?: string): Promise<SchedulingRequestListResponse> {
|
||||
const query = status ? `?status=${encodeURIComponent(status)}` : "";
|
||||
return apiFetch<SchedulingRequestListResponse>(settings, `/api/v1/scheduling/requests${query}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState, type FormEvent } from "react";
|
||||
import { Bell, CalendarCheck, Check, Clock, Plus, RefreshCw, Send, XCircle } from "lucide-react";
|
||||
import { Button, hasScope, type ApiSettings, type AuthInfo } from "@govoplan/core-webui";
|
||||
import { Button, EmailAddressInput, hasScope, type ApiSettings, type AuthInfo, type MailboxAddress } from "@govoplan/core-webui";
|
||||
import {
|
||||
closeSchedulingRequest,
|
||||
createSchedulingCalendarEvent,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
evaluateSchedulingFreeBusy,
|
||||
listSchedulingNotifications,
|
||||
listSchedulingRequests,
|
||||
lookupSchedulingAddresses,
|
||||
openSchedulingRequest,
|
||||
schedulingSummary,
|
||||
type SchedulingNotification,
|
||||
@@ -51,6 +52,8 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
{ label: "Option 1", start_at: localValue(defaultStart), end_at: localValue(defaultEnd) }
|
||||
]);
|
||||
const [participants, setParticipants] = useState<ParticipantDraft[]>([{ display_name: "", email: "", required: true }]);
|
||||
const [addressLookupQuery, setAddressLookupQuery] = useState("");
|
||||
const [addressSuggestions, setAddressSuggestions] = useState<MailboxAddress[]>([]);
|
||||
|
||||
const canWrite = hasScope(auth, "scheduling:schedule:write");
|
||||
const selected = useMemo(() => requests.find((item) => item.id === selectedId) || requests[0] || null, [requests, selectedId]);
|
||||
@@ -73,6 +76,37 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
void loadDetails(selected.id);
|
||||
}, [selected?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const query = addressLookupQuery.trim();
|
||||
if (!canWrite || query.length < 2) {
|
||||
setAddressSuggestions([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const timer = window.setTimeout(() => {
|
||||
void lookupSchedulingAddresses(settings, query).
|
||||
then((response) => {
|
||||
if (cancelled) return;
|
||||
if (!response.available) {
|
||||
setAddressSuggestions([]);
|
||||
return;
|
||||
}
|
||||
setAddressSuggestions(
|
||||
response.candidates
|
||||
.map((candidate) => candidate.email ? { name: candidate.display_name || undefined, email: candidate.email } : null)
|
||||
.filter((address): address is MailboxAddress => Boolean(address?.email))
|
||||
);
|
||||
}).
|
||||
catch(() => {
|
||||
if (!cancelled) setAddressSuggestions([]);
|
||||
});
|
||||
}, 200);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [addressLookupQuery, canWrite, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
async function loadRequests() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
@@ -258,7 +292,22 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
{participants.map((participant, index) => (
|
||||
<div className="scheduling-participant-draft" key={index}>
|
||||
<input placeholder="Name" value={participant.display_name} onChange={(event) => updateParticipant(index, "display_name", event.target.value)} />
|
||||
<input placeholder="Email" value={participant.email} onChange={(event) => updateParticipant(index, "email", event.target.value)} />
|
||||
<EmailAddressInput
|
||||
value={participant.email ? [{ name: participant.display_name || undefined, email: participant.email }] : []}
|
||||
suggestions={addressSuggestions}
|
||||
onSuggestionQueryChange={setAddressLookupQuery}
|
||||
allowMultiple={false}
|
||||
disabled={!canWrite}
|
||||
compact
|
||||
addLabel="Add participant"
|
||||
emptyText="Participant email"
|
||||
onChange={(addresses) => {
|
||||
const address = addresses[0];
|
||||
updateParticipantDraft(index, {
|
||||
display_name: address?.name ?? participant.display_name,
|
||||
email: address?.email ?? ""
|
||||
});
|
||||
}} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -333,6 +382,10 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
function updateParticipant(index: number, field: keyof ParticipantDraft, value: string) {
|
||||
setParticipants((items) => items.map((item, itemIndex) => (itemIndex === index ? { ...item, [field]: value } : item)));
|
||||
}
|
||||
|
||||
function updateParticipantDraft(index: number, patch: Partial<ParticipantDraft>) {
|
||||
setParticipants((items) => items.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)));
|
||||
}
|
||||
}
|
||||
|
||||
function isRequestEnvelope(value: unknown): value is { request: SchedulingRequest } {
|
||||
|
||||
@@ -12,9 +12,9 @@ export const schedulingModule: PlatformWebModule = {
|
||||
label: "Scheduling",
|
||||
version: "1.0.0",
|
||||
dependencies: ["poll"],
|
||||
optionalDependencies: ["calendar", "mail", "notifications", "workflow", "appointments"],
|
||||
optionalDependencies: ["calendar", "mail", "notifications", "workflow", "appointments", "addresses"],
|
||||
translations: generatedTranslations,
|
||||
navItems: [{ to: "/scheduling", label: "Scheduling", iconName: "calendar", anyOf: scheduleRead, order: 56 }],
|
||||
navItems: [{ to: "/scheduling", label: "Scheduling", iconName: "calendar-clock", anyOf: scheduleRead, order: 56 }],
|
||||
routes: [
|
||||
{ path: "/scheduling", anyOf: scheduleRead, order: 56, render: ({ settings, auth }) => createElement(SchedulingPage, { settings, auth }) }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user