perf(campaign): search share targets server-side
This commit is contained in:
@@ -32,6 +32,7 @@ from govoplan_core.core.postbox import (
|
|||||||
CAPABILITY_POSTBOX_DELIVERY,
|
CAPABILITY_POSTBOX_DELIVERY,
|
||||||
CAPABILITY_POSTBOX_DIRECTORY,
|
CAPABILITY_POSTBOX_DIRECTORY,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.core.references import CAPABILITY_ACCESS_REFERENCE_OPTIONS
|
||||||
from govoplan_campaign.backend.change_tracking import register_campaign_change_tracking
|
from govoplan_campaign.backend.change_tracking import register_campaign_change_tracking
|
||||||
from govoplan_campaign.backend.db import models as campaign_models # noqa: F401 - populate Campaign ORM metadata
|
from govoplan_campaign.backend.db import models as campaign_models # noqa: F401 - populate Campaign ORM metadata
|
||||||
from govoplan_campaign.backend.documentation import CAMPAIGN_USER_DOCUMENTATION, documentation_topics
|
from govoplan_campaign.backend.documentation import CAMPAIGN_USER_DOCUMENTATION, documentation_topics
|
||||||
@@ -200,6 +201,7 @@ manifest = ModuleManifest(
|
|||||||
name="Campaigns",
|
name="Campaigns",
|
||||||
version="0.1.12",
|
version="0.1.12",
|
||||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||||
|
optional_capabilities=(CAPABILITY_ACCESS_REFERENCE_OPTIONS,),
|
||||||
optional_dependencies=(
|
optional_dependencies=(
|
||||||
"files",
|
"files",
|
||||||
"mail",
|
"mail",
|
||||||
@@ -215,6 +217,12 @@ manifest = ModuleManifest(
|
|||||||
ModuleInterfaceProvider(name="campaigns.retention", version="0.1.6"),
|
ModuleInterfaceProvider(name="campaigns.retention", version="0.1.6"),
|
||||||
),
|
),
|
||||||
requires_interfaces=(
|
requires_interfaces=(
|
||||||
|
ModuleInterfaceRequirement(
|
||||||
|
name=CAPABILITY_ACCESS_REFERENCE_OPTIONS,
|
||||||
|
version_min="0.1.0",
|
||||||
|
version_max_exclusive="0.2.0",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
ModuleInterfaceRequirement(
|
ModuleInterfaceRequirement(
|
||||||
name="files.campaign_attachments",
|
name="files.campaign_attachments",
|
||||||
version_min="0.1.0",
|
version_min="0.1.0",
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from govoplan_core.api.v1.schemas import (
|
||||||
|
ReferenceOptionListResponse,
|
||||||
|
ReferenceOptionResponse,
|
||||||
|
)
|
||||||
from govoplan_campaign.backend.schemas import (
|
from govoplan_campaign.backend.schemas import (
|
||||||
CampaignShareItem,
|
CampaignShareItem,
|
||||||
CampaignShareListResponse,
|
CampaignShareListResponse,
|
||||||
@@ -19,6 +25,11 @@ from govoplan_campaign.backend.db.models import (
|
|||||||
CampaignShare,
|
CampaignShare,
|
||||||
)
|
)
|
||||||
from govoplan_core.db.session import get_session
|
from govoplan_core.db.session import get_session
|
||||||
|
from govoplan_core.core.references import (
|
||||||
|
access_scope_reference_page,
|
||||||
|
access_scope_reference_provider_available,
|
||||||
|
)
|
||||||
|
from govoplan_core.core.runtime import get_registry
|
||||||
from govoplan_core.security.time import utc_now
|
from govoplan_core.security.time import utc_now
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +42,51 @@ from govoplan_campaign.backend.route_support import (
|
|||||||
router = APIRouter(prefix="/campaigns", tags=["campaigns"])
|
router = APIRouter(prefix="/campaigns", tags=["campaigns"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{campaign_id}/share-target-options",
|
||||||
|
response_model=ReferenceOptionListResponse,
|
||||||
|
)
|
||||||
|
def search_campaign_share_targets(
|
||||||
|
campaign_id: str,
|
||||||
|
target_type: Literal["user", "group"],
|
||||||
|
q: str = "",
|
||||||
|
selected: list[str] = Query(default=[]),
|
||||||
|
limit: int = Query(default=50, ge=1, le=200),
|
||||||
|
cursor: str | None = None,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:share")),
|
||||||
|
) -> ReferenceOptionListResponse:
|
||||||
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
||||||
|
registry = get_registry()
|
||||||
|
try:
|
||||||
|
page = access_scope_reference_page(
|
||||||
|
registry,
|
||||||
|
principal,
|
||||||
|
scope_type=target_type,
|
||||||
|
reference_kind="membership" if target_type == "user" else "group",
|
||||||
|
query=q,
|
||||||
|
selected_values=selected,
|
||||||
|
limit=limit,
|
||||||
|
cursor=cursor,
|
||||||
|
administrative=True,
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
detail=str(exc),
|
||||||
|
) from exc
|
||||||
|
return ReferenceOptionListResponse(
|
||||||
|
options=[
|
||||||
|
ReferenceOptionResponse(**option.to_dict())
|
||||||
|
for option in page.options
|
||||||
|
],
|
||||||
|
provider_available=access_scope_reference_provider_available(registry),
|
||||||
|
next_cursor=page.next_cursor,
|
||||||
|
has_more=page.has_more,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{campaign_id}/share-targets", response_model=CampaignShareTargetsResponse)
|
@router.get("/{campaign_id}/share-targets", response_model=CampaignShareTargetsResponse)
|
||||||
def list_campaign_share_targets(
|
def list_campaign_share_targets(
|
||||||
campaign_id: str,
|
campaign_id: str,
|
||||||
|
|||||||
94
tests/test_share_target_search.py
Normal file
94
tests/test_share_target_search.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from govoplan_campaign.backend.routes.sharing import (
|
||||||
|
search_campaign_share_targets,
|
||||||
|
)
|
||||||
|
from govoplan_core.core.references import (
|
||||||
|
CAPABILITY_ACCESS_REFERENCE_OPTIONS,
|
||||||
|
ReferenceOption,
|
||||||
|
ReferenceSearchPage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Provider:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.requests = []
|
||||||
|
|
||||||
|
def search_reference_options(self, session, principal, *, request):
|
||||||
|
del session, principal
|
||||||
|
self.requests.append(request)
|
||||||
|
return ReferenceSearchPage(
|
||||||
|
options=(
|
||||||
|
ReferenceOption(
|
||||||
|
value="membership-1",
|
||||||
|
label="Ada",
|
||||||
|
kind="membership",
|
||||||
|
source_module="access",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
next_cursor="offset:1",
|
||||||
|
has_more=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Registry:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.provider = _Provider()
|
||||||
|
|
||||||
|
def has_capability(self, name):
|
||||||
|
return name == CAPABILITY_ACCESS_REFERENCE_OPTIONS
|
||||||
|
|
||||||
|
def capability(self, name):
|
||||||
|
return self.provider if self.has_capability(name) else None
|
||||||
|
|
||||||
|
|
||||||
|
class CampaignShareTargetSearchTests(unittest.TestCase):
|
||||||
|
def test_search_uses_membership_references_and_retains_stale_targets(self) -> None:
|
||||||
|
registry = _Registry()
|
||||||
|
principal = SimpleNamespace(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
account_id="account-1",
|
||||||
|
membership_id="membership-1",
|
||||||
|
group_ids=frozenset(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"govoplan_campaign.backend.routes.sharing._get_campaign_for_principal"
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"govoplan_campaign.backend.routes.sharing.get_registry",
|
||||||
|
return_value=registry,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
response = search_campaign_share_targets(
|
||||||
|
"campaign-1",
|
||||||
|
"user",
|
||||||
|
q="Ada",
|
||||||
|
selected=["removed-membership"],
|
||||||
|
limit=25,
|
||||||
|
cursor="offset:0",
|
||||||
|
session=object(), # type: ignore[arg-type]
|
||||||
|
principal=principal, # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
|
||||||
|
request = registry.provider.requests[0]
|
||||||
|
self.assertEqual("membership", request.kind)
|
||||||
|
self.assertEqual("tenant-1", request.tenant_id)
|
||||||
|
self.assertEqual("ada", request.query)
|
||||||
|
self.assertTrue(request.context["administrative"])
|
||||||
|
self.assertEqual(
|
||||||
|
["membership-1", "removed-membership"],
|
||||||
|
[option.value for option in response.options],
|
||||||
|
)
|
||||||
|
self.assertEqual("unavailable", response.options[1].availability)
|
||||||
|
self.assertTrue(response.has_more)
|
||||||
|
self.assertEqual("offset:1", response.next_cursor)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { ApiSettings, CampaignListItem, DeltaDeletedItem } from "../types";
|
import type { ApiSettings, CampaignListItem, DeltaDeletedItem } from "../types";
|
||||||
import { apiDownload, apiFetch } from "./client";
|
import { apiDownload, apiFetch } from "./client";
|
||||||
|
import {
|
||||||
|
apiReferenceOptionProvider,
|
||||||
|
type ReferenceOptionProvider
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
import { campaignJobsQueryParams, type CampaignJobsQueryParameters } from "../features/campaigns/utils/jobListQuery";
|
import { campaignJobsQueryParams, type CampaignJobsQueryParameters } from "../features/campaigns/utils/jobListQuery";
|
||||||
export { fetchResourceAccessExplanation } from "@govoplan/core-webui";
|
export { fetchResourceAccessExplanation } from "@govoplan/core-webui";
|
||||||
export type {
|
export type {
|
||||||
@@ -1125,6 +1129,18 @@ export async function getCampaignShareTargets(settings: ApiSettings, campaignId:
|
|||||||
return apiFetch<CampaignShareTargets>(settings, `/api/v1/campaigns/${campaignId}/share-targets`);
|
return apiFetch<CampaignShareTargets>(settings, `/api/v1/campaigns/${campaignId}/share-targets`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function campaignShareTargetProvider(
|
||||||
|
settings: ApiSettings,
|
||||||
|
campaignId: string,
|
||||||
|
targetType: "user" | "group"
|
||||||
|
): ReferenceOptionProvider {
|
||||||
|
return apiReferenceOptionProvider(
|
||||||
|
settings,
|
||||||
|
`/api/v1/campaigns/${campaignId}/share-target-options`,
|
||||||
|
{ target_type: targetType }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCampaignShares(settings: ApiSettings, campaignId: string): Promise<CampaignShare[]> {
|
export async function getCampaignShares(settings: ApiSettings, campaignId: string): Promise<CampaignShare[]> {
|
||||||
const pageSize = 500;
|
const pageSize = 500;
|
||||||
const shares: CampaignShare[] = [];
|
const shares: CampaignShare[] = [];
|
||||||
|
|||||||
@@ -4,15 +4,20 @@ import { KeyRound, Trash2 } from "lucide-react";
|
|||||||
import {
|
import {
|
||||||
fetchResourceAccessExplanation,
|
fetchResourceAccessExplanation,
|
||||||
getCampaignShares,
|
getCampaignShares,
|
||||||
getCampaignShareTargets,
|
campaignShareTargetProvider,
|
||||||
revokeCampaignShare,
|
revokeCampaignShare,
|
||||||
updateCampaignOwner,
|
updateCampaignOwner,
|
||||||
upsertCampaignShare,
|
upsertCampaignShare,
|
||||||
type CampaignShare,
|
type CampaignShare,
|
||||||
type CampaignShareTargets,
|
|
||||||
type ResourceAccessExplanationResponse } from
|
type ResourceAccessExplanationResponse } from
|
||||||
"../../../api/campaigns";
|
"../../../api/campaigns";
|
||||||
import { Button, ResourceAccessExplanation } from "@govoplan/core-webui";
|
import {
|
||||||
|
Button,
|
||||||
|
ReferenceSelect,
|
||||||
|
ResourceAccessExplanation,
|
||||||
|
type ReferenceOption,
|
||||||
|
type ReferenceOptionProvider
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
import { Card } from "@govoplan/core-webui";
|
import { Card } from "@govoplan/core-webui";
|
||||||
import { ConfirmDialog } from "@govoplan/core-webui";
|
import { ConfirmDialog } from "@govoplan/core-webui";
|
||||||
import { DataGrid, type DataGridColumn } from "@govoplan/core-webui";
|
import { DataGrid, type DataGridColumn } from "@govoplan/core-webui";
|
||||||
@@ -34,7 +39,10 @@ export default function CampaignAccessCard({
|
|||||||
|
|
||||||
}: {settings: ApiSettings;auth: AuthInfo;campaign: CampaignListItem;onChanged: () => Promise<void>;onError: (message: string) => void;}) {
|
}: {settings: ApiSettings;auth: AuthInfo;campaign: CampaignListItem;onChanged: () => Promise<void>;onError: (message: string) => void;}) {
|
||||||
const [available, setAvailable] = useState<boolean | null>(null);
|
const [available, setAvailable] = useState<boolean | null>(null);
|
||||||
const [targets, setTargets] = useState<CampaignShareTargets>({ users: [], groups: [] });
|
const [targets, setTargets] = useState<{
|
||||||
|
users: readonly ReferenceOption[];
|
||||||
|
groups: readonly ReferenceOption[];
|
||||||
|
}>({ users: [], groups: [] });
|
||||||
const [shares, setShares] = useState<CampaignShare[]>([]);
|
const [shares, setShares] = useState<CampaignShare[]>([]);
|
||||||
const [ownerOpen, setOwnerOpen] = useState(false);
|
const [ownerOpen, setOwnerOpen] = useState(false);
|
||||||
const [shareOpen, setShareOpen] = useState(false);
|
const [shareOpen, setShareOpen] = useState(false);
|
||||||
@@ -57,6 +65,14 @@ export default function CampaignAccessCard({
|
|||||||
hasScope(auth, "admin:roles:read") ||
|
hasScope(auth, "admin:roles:read") ||
|
||||||
hasScope(auth, "access:membership:read") ||
|
hasScope(auth, "access:membership:read") ||
|
||||||
hasScope(auth, "access:role:read");
|
hasScope(auth, "access:role:read");
|
||||||
|
const userTargetProvider = useMemo(
|
||||||
|
() => campaignShareTargetProvider(settings, campaign.id, "user"),
|
||||||
|
[campaign.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]
|
||||||
|
);
|
||||||
|
const groupTargetProvider = useMemo(
|
||||||
|
() => campaignShareTargetProvider(settings, campaign.id, "group"),
|
||||||
|
[campaign.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]
|
||||||
|
);
|
||||||
|
|
||||||
useUnsavedDraftGuard({
|
useUnsavedDraftGuard({
|
||||||
dirty: ownerDirty || shareDirty,
|
dirty: ownerDirty || shareDirty,
|
||||||
@@ -69,12 +85,25 @@ export default function CampaignAccessCard({
|
|||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const [nextTargets, nextShares] = await Promise.all([
|
const nextShares = (await getCampaignShares(settings, campaign.id))
|
||||||
getCampaignShareTargets(settings, campaign.id),
|
.filter((item) => !item.revoked_at);
|
||||||
getCampaignShares(settings, campaign.id)]
|
const userIds = uniqueTargetIds(
|
||||||
|
nextShares,
|
||||||
|
"user",
|
||||||
|
campaign.owner_user_id
|
||||||
);
|
);
|
||||||
setTargets(nextTargets);
|
const groupIds = uniqueTargetIds(
|
||||||
setShares(nextShares.filter((item) => !item.revoked_at));
|
nextShares,
|
||||||
|
"group",
|
||||||
|
campaign.owner_group_id
|
||||||
|
);
|
||||||
|
const controller = new AbortController();
|
||||||
|
const [users, groups] = await Promise.all([
|
||||||
|
resolveReferenceOptions(userTargetProvider, userIds, controller.signal),
|
||||||
|
resolveReferenceOptions(groupTargetProvider, groupIds, controller.signal)
|
||||||
|
]);
|
||||||
|
setTargets({ users, groups });
|
||||||
|
setShares(nextShares);
|
||||||
setAvailable(true);
|
setAvailable(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
@@ -87,18 +116,27 @@ export default function CampaignAccessCard({
|
|||||||
setOwnerType(campaign.owner_group_id ? "group" : "user");
|
setOwnerType(campaign.owner_group_id ? "group" : "user");
|
||||||
setOwnerId(campaign.owner_group_id || campaign.owner_user_id || "");
|
setOwnerId(campaign.owner_group_id || campaign.owner_user_id || "");
|
||||||
void load();
|
void load();
|
||||||
}, [campaign.id, campaign.owner_group_id, campaign.owner_user_id, settings.accessToken, settings.apiBaseUrl]);
|
}, [
|
||||||
|
campaign.id,
|
||||||
|
campaign.owner_group_id,
|
||||||
|
campaign.owner_user_id,
|
||||||
|
groupTargetProvider,
|
||||||
|
settings.accessToken,
|
||||||
|
settings.apiBaseUrl,
|
||||||
|
settings.apiKey,
|
||||||
|
userTargetProvider
|
||||||
|
]);
|
||||||
|
|
||||||
const targetOptions = ownerType === "user" ? targets.users : targets.groups;
|
const ownerTargetProvider = ownerType === "user" ? userTargetProvider : groupTargetProvider;
|
||||||
const shareTargetOptions = shareType === "user" ? targets.users : targets.groups;
|
const shareTargetProvider = shareType === "user" ? userTargetProvider : groupTargetProvider;
|
||||||
const targetMap = useMemo(() => new Map([
|
const targetMap = useMemo(() => new Map([
|
||||||
...targets.users.map((item) => [`user:${item.id}`, item] as const),
|
...targets.users.map((item) => [`user:${item.value}`, item] as const),
|
||||||
...targets.groups.map((item) => [`group:${item.id}`, item] as const)]
|
...targets.groups.map((item) => [`group:${item.value}`, item] as const)]
|
||||||
), [targets]);
|
), [targets]);
|
||||||
|
|
||||||
const ownerLabel = campaign.owner_group_id ?
|
const ownerLabel = campaign.owner_group_id ?
|
||||||
targetMap.get(`group:${campaign.owner_group_id}`)?.name || "i18n:govoplan-campaign.group_owner.55630670" :
|
targetMap.get(`group:${campaign.owner_group_id}`)?.label || "i18n:govoplan-campaign.group_owner.55630670" :
|
||||||
targetMap.get(`user:${campaign.owner_user_id || ""}`)?.name || "i18n:govoplan-campaign.user_owner.86e3faab";
|
targetMap.get(`user:${campaign.owner_user_id || ""}`)?.label || "i18n:govoplan-campaign.user_owner.86e3faab";
|
||||||
|
|
||||||
function closeOwnerDialog() {
|
function closeOwnerDialog() {
|
||||||
setOwnerOpen(false);
|
setOwnerOpen(false);
|
||||||
@@ -189,10 +227,10 @@ export default function CampaignAccessCard({
|
|||||||
sticky: "start",
|
sticky: "start",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
value: (row) => targetMap.get(`${row.target_type}:${row.target_id}`)?.name || row.target_id,
|
value: (row) => targetMap.get(`${row.target_type}:${row.target_id}`)?.label || row.target_id,
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
const target = targetMap.get(`${row.target_type}:${row.target_id}`);
|
const target = targetMap.get(`${row.target_type}:${row.target_id}`);
|
||||||
return <div><strong>{target?.name || row.target_id}</strong><div className="muted small-note">{row.target_type}{target?.secondary ? i18nMessage("i18n:govoplan-campaign.value.48afe802", { value0: target.secondary }) : ""}</div></div>;
|
return <div><strong>{target?.label || row.target_id}</strong><div className="muted small-note">{row.target_type}{target?.description ? i18nMessage("i18n:govoplan-campaign.value.48afe802", { value0: target.description }) : ""}</div></div>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ id: "permission", header: "i18n:govoplan-campaign.access.2f81a22d", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.permission, render: (row) => <StatusBadge status={row.permission === "write" ? "active" : "built"} label={row.permission === "write" ? "i18n:govoplan-campaign.can_edit.cb0ab3da" : "i18n:govoplan-campaign.can_view.1b3e4006"} /> },
|
{ id: "permission", header: "i18n:govoplan-campaign.access.2f81a22d", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.permission, render: (row) => <StatusBadge status={row.permission === "write" ? "active" : "built"} label={row.permission === "write" ? "i18n:govoplan-campaign.can_edit.cb0ab3da" : "i18n:govoplan-campaign.can_view.1b3e4006"} /> },
|
||||||
@@ -210,14 +248,32 @@ export default function CampaignAccessCard({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Dialog open={ownerOpen} className="campaign-access-dialog" title="i18n:govoplan-campaign.change_campaign_owner.63f80aef" onClose={() => !busy && closeOwnerDialog()} footer={<><Button onClick={closeOwnerDialog} disabled={busy}>i18n:govoplan-campaign.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveOwner()} disabled={busy || !ownerId}>i18n:govoplan-campaign.save_owner.b6763847</Button></>}>
|
<Dialog open={ownerOpen} className="campaign-access-dialog" title="i18n:govoplan-campaign.change_campaign_owner.63f80aef" onClose={() => !busy && closeOwnerDialog()} footer={<><Button onClick={closeOwnerDialog} disabled={busy}>i18n:govoplan-campaign.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveOwner()} disabled={busy || !ownerId}>i18n:govoplan-campaign.save_owner.b6763847</Button></>}>
|
||||||
<FormField label="i18n:govoplan-campaign.owner_type.6b86eacc"><select value={ownerType} onChange={(event) => {const next = event.target.value as TargetType;setOwnerType(next);setOwnerId((next === "user" ? targets.users : targets.groups)[0]?.id || "");}}><option value="user">i18n:govoplan-campaign.user.9f8a2389</option><option value="group">i18n:govoplan-campaign.group.171a0606</option></select></FormField>
|
<FormField label="i18n:govoplan-campaign.owner_type.6b86eacc"><select value={ownerType} onChange={(event) => {const next = event.target.value as TargetType;setOwnerType(next);setOwnerId("");}}><option value="user">i18n:govoplan-campaign.user.9f8a2389</option><option value="group">i18n:govoplan-campaign.group.171a0606</option></select></FormField>
|
||||||
<FormField label="i18n:govoplan-campaign.owner.89ff3122"><select value={ownerId} onChange={(event) => setOwnerId(event.target.value)}>{targetOptions.map((item) => <option key={item.id} value={item.id}>{item.name}{item.secondary ? i18nMessage("i18n:govoplan-campaign.value.48afe802", { value0: item.secondary }) : ""}</option>)}</select></FormField>
|
<FormField label="i18n:govoplan-campaign.owner.89ff3122">
|
||||||
|
<ReferenceSelect
|
||||||
|
value={ownerId}
|
||||||
|
onChange={(value) => setOwnerId(value)}
|
||||||
|
provider={ownerTargetProvider}
|
||||||
|
selectedOption={targetMap.get(`${ownerType}:${ownerId}`) ?? null}
|
||||||
|
disabled={busy}
|
||||||
|
placeholder="i18n:govoplan-campaign.select.349ac8fb"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
<p className="muted small-note">Changing the owner clears a selected reusable mail profile from the editable current version and requires profile reselection plus validation before live delivery.</p>
|
<p className="muted small-note">Changing the owner clears a selected reusable mail profile from the editable current version and requires profile reselection plus validation before live delivery.</p>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={shareOpen} className="campaign-access-dialog" title="i18n:govoplan-campaign.share_campaign.b605982b" onClose={() => !busy && closeShareDialog()} footer={<><Button onClick={closeShareDialog} disabled={busy}>i18n:govoplan-campaign.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveShare()} disabled={busy || !shareTargetId}>i18n:govoplan-campaign.save_share.bcf6ed94</Button></>}>
|
<Dialog open={shareOpen} className="campaign-access-dialog" title="i18n:govoplan-campaign.share_campaign.b605982b" onClose={() => !busy && closeShareDialog()} footer={<><Button onClick={closeShareDialog} disabled={busy}>i18n:govoplan-campaign.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveShare()} disabled={busy || !shareTargetId}>i18n:govoplan-campaign.save_share.bcf6ed94</Button></>}>
|
||||||
<FormField label="i18n:govoplan-campaign.target_type.a45f8055"><select value={shareType} onChange={(event) => {const next = event.target.value as TargetType;setShareType(next);setShareTargetId((next === "user" ? targets.users : targets.groups)[0]?.id || "");}}><option value="user">i18n:govoplan-campaign.user.9f8a2389</option><option value="group">i18n:govoplan-campaign.group.171a0606</option></select></FormField>
|
<FormField label="i18n:govoplan-campaign.target_type.a45f8055"><select value={shareType} onChange={(event) => {const next = event.target.value as TargetType;setShareType(next);setShareTargetId("");}}><option value="user">i18n:govoplan-campaign.user.9f8a2389</option><option value="group">i18n:govoplan-campaign.group.171a0606</option></select></FormField>
|
||||||
<FormField label="i18n:govoplan-campaign.user_or_group.53406ef0"><select value={shareTargetId} onChange={(event) => setShareTargetId(event.target.value)}><option value="">i18n:govoplan-campaign.select.349ac8fb</option>{shareTargetOptions.map((item) => <option key={item.id} value={item.id}>{item.name}{item.secondary ? i18nMessage("i18n:govoplan-campaign.value.48afe802", { value0: item.secondary }) : ""}</option>)}</select></FormField>
|
<FormField label="i18n:govoplan-campaign.user_or_group.53406ef0">
|
||||||
|
<ReferenceSelect
|
||||||
|
value={shareTargetId}
|
||||||
|
onChange={(value) => setShareTargetId(value)}
|
||||||
|
provider={shareTargetProvider}
|
||||||
|
selectedOption={targetMap.get(`${shareType}:${shareTargetId}`) ?? null}
|
||||||
|
disabled={busy}
|
||||||
|
placeholder="i18n:govoplan-campaign.select.349ac8fb"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
<FormField label="i18n:govoplan-campaign.access.2f81a22d"><select value={sharePermission} onChange={(event) => setSharePermission(event.target.value as "read" | "write")}><option value="read">i18n:govoplan-campaign.can_view.1b3e4006</option><option value="write">i18n:govoplan-campaign.can_edit_and_operate.77acb15e</option></select></FormField>
|
<FormField label="i18n:govoplan-campaign.access.2f81a22d"><select value={sharePermission} onChange={(event) => setSharePermission(event.target.value as "read" | "write")}><option value="read">i18n:govoplan-campaign.can_view.1b3e4006</option><option value="write">i18n:govoplan-campaign.can_edit_and_operate.77acb15e</option></select></FormField>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
@@ -229,3 +285,36 @@ export default function CampaignAccessCard({
|
|||||||
</>);
|
</>);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uniqueTargetIds(
|
||||||
|
shares: readonly CampaignShare[],
|
||||||
|
targetType: TargetType,
|
||||||
|
ownerId?: string | null
|
||||||
|
): string[] {
|
||||||
|
return [...new Set([
|
||||||
|
...(ownerId ? [ownerId] : []),
|
||||||
|
...shares
|
||||||
|
.filter((share) => share.target_type === targetType)
|
||||||
|
.map((share) => share.target_id)
|
||||||
|
])];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveReferenceOptions(
|
||||||
|
provider: ReferenceOptionProvider,
|
||||||
|
values: readonly string[],
|
||||||
|
signal: AbortSignal
|
||||||
|
): Promise<readonly ReferenceOption[]> {
|
||||||
|
if (!values.length) return [];
|
||||||
|
if (!provider.resolve) {
|
||||||
|
return provider.search("", {
|
||||||
|
selectedValues: values,
|
||||||
|
limit: Math.min(200, Math.max(1, values.length)),
|
||||||
|
signal
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return provider.resolve(values, {
|
||||||
|
selectedValues: values,
|
||||||
|
limit: Math.min(200, Math.max(1, values.length)),
|
||||||
|
signal
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user