feat: select users in campaign access explanations
This commit is contained in:
@@ -669,7 +669,9 @@ manifest = ModuleManifest(
|
|||||||
"and ownership-transfer records describe governance visibility separately from the content grant. Saved imports expose "
|
"and ownership-transfer records describe governance visibility separately from the content grant. Saved imports expose "
|
||||||
"only their stable source identity and revision, while independently user-owned mapping profiles never inherit a Campaign "
|
"only their stable source identity and revision, while independently user-owned mapping profiles never inherit a Campaign "
|
||||||
"share. Persisted validation, build, snapshot, and review artifacts use typed version references. Reusable templates and "
|
"share. Persisted validation, build, snapshot, and review artifacts use typed version references. Reusable templates and "
|
||||||
"durable export packages stay with their optional owning modules and fail closed when Campaign is asked to explain them."
|
"durable export packages stay with their optional owning modules and fail closed when Campaign is asked to explain them. "
|
||||||
|
"The shared explanation dialog evaluates the signed-in user by default. Policy may permit a tenant-bounded selected-user "
|
||||||
|
"administrator diagnostic; Access returns only permitted subject metadata and records every cross-user explanation in audit evidence."
|
||||||
),
|
),
|
||||||
layer="available",
|
layer="available",
|
||||||
documentation_types=("admin",),
|
documentation_types=("admin",),
|
||||||
@@ -693,7 +695,7 @@ manifest = ModuleManifest(
|
|||||||
kind="repository",
|
kind="repository",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
related_modules=("access", "mail", "postbox", "templates"),
|
related_modules=("access", "audit", "mail", "policy", "postbox", "templates"),
|
||||||
translations={
|
translations={
|
||||||
"de": {
|
"de": {
|
||||||
"title": "Zugriff auf untergeordnete Campaign-Nachweise erklaeren",
|
"title": "Zugriff auf untergeordnete Campaign-Nachweise erklaeren",
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import {
|
|||||||
type ReferenceOptionProvider
|
type ReferenceOptionProvider
|
||||||
} from "@govoplan/core-webui";
|
} 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,
|
||||||
|
fetchResourceAccessExplanationSubjects
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
export type {
|
export type {
|
||||||
AccessDecisionProvenanceItem,
|
AccessDecisionProvenanceItem,
|
||||||
ResourceAccessExplanationUser as AccessExplanationUser,
|
ResourceAccessExplanationUser as AccessExplanationUser,
|
||||||
ResourceAccessExplanationResponse
|
ResourceAccessExplanationResponse,
|
||||||
|
ResourceAccessExplanationSubjectsResponse
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
|
|
||||||
export type CampaignListResponse =
|
export type CampaignListResponse =
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
fetchResourceAccessExplanation,
|
fetchResourceAccessExplanation,
|
||||||
|
fetchResourceAccessExplanationSubjects,
|
||||||
getCampaignShares,
|
getCampaignShares,
|
||||||
campaignShareTargetProvider,
|
campaignShareTargetProvider,
|
||||||
revokeCampaignShare,
|
revokeCampaignShare,
|
||||||
upsertCampaignShare,
|
upsertCampaignShare,
|
||||||
type CampaignShare,
|
type CampaignShare,
|
||||||
type ResourceAccessExplanationResponse } from
|
type ResourceAccessExplanationResponse,
|
||||||
|
type ResourceAccessExplanationSubjectsResponse } from
|
||||||
"../../../api/campaigns";
|
"../../../api/campaigns";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -72,6 +74,8 @@ export default function CampaignAccessCard({
|
|||||||
const [sharePermission, setSharePermission] = useState<"read" | "write">("read");
|
const [sharePermission, setSharePermission] = useState<"read" | "write">("read");
|
||||||
const [accessExplanationOpen, setAccessExplanationOpen] = useState(false);
|
const [accessExplanationOpen, setAccessExplanationOpen] = useState(false);
|
||||||
const [accessExplanation, setAccessExplanation] = useState<ResourceAccessExplanationResponse | null>(null);
|
const [accessExplanation, setAccessExplanation] = useState<ResourceAccessExplanationResponse | null>(null);
|
||||||
|
const [accessExplanationSubjects, setAccessExplanationSubjects] = useState<ResourceAccessExplanationSubjectsResponse | null>(null);
|
||||||
|
const [accessExplanationSubjectId, setAccessExplanationSubjectId] = useState("");
|
||||||
const [accessExplanationLoading, setAccessExplanationLoading] = useState(false);
|
const [accessExplanationLoading, setAccessExplanationLoading] = useState(false);
|
||||||
const [ownershipTransfers, setOwnershipTransfers] = useState<OwnershipTransfer[]>([]);
|
const [ownershipTransfers, setOwnershipTransfers] = useState<OwnershipTransfer[]>([]);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -346,13 +350,24 @@ export default function CampaignAccessCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openAccessExplanation() {
|
async function openAccessExplanation() {
|
||||||
if (!auth.user?.id) return;
|
const currentUserId = auth.principal?.membership_id || auth.user?.id;
|
||||||
|
if (!currentUserId) return;
|
||||||
setAccessExplanationOpen(true);
|
setAccessExplanationOpen(true);
|
||||||
setAccessExplanation(null);
|
setAccessExplanation(null);
|
||||||
|
setAccessExplanationSubjects(null);
|
||||||
setAccessExplanationLoading(true);
|
setAccessExplanationLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const subjects = await fetchResourceAccessExplanationSubjects(settings, {
|
||||||
|
tenantId: (auth.active_tenant ?? auth.tenant)?.id
|
||||||
|
});
|
||||||
|
const initialSubjectId = subjects.users.some((user) => user.id === currentUserId)
|
||||||
|
? currentUserId
|
||||||
|
: subjects.users[0]?.id;
|
||||||
|
if (!initialSubjectId) throw new Error("No permitted access-explanation subject is available.");
|
||||||
|
setAccessExplanationSubjects(subjects);
|
||||||
|
setAccessExplanationSubjectId(initialSubjectId);
|
||||||
setAccessExplanation(await fetchResourceAccessExplanation(settings, {
|
setAccessExplanation(await fetchResourceAccessExplanation(settings, {
|
||||||
userId: auth.user.id,
|
userId: initialSubjectId,
|
||||||
resourceType: "campaign",
|
resourceType: "campaign",
|
||||||
resourceId: campaign.id,
|
resourceId: campaign.id,
|
||||||
action: "campaigns:campaign:read",
|
action: "campaigns:campaign:read",
|
||||||
@@ -366,6 +381,26 @@ export default function CampaignAccessCard({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectAccessExplanationSubject(userId: string) {
|
||||||
|
if (userId === accessExplanationSubjectId) return;
|
||||||
|
setAccessExplanationLoading(true);
|
||||||
|
try {
|
||||||
|
const explanation = await fetchResourceAccessExplanation(settings, {
|
||||||
|
userId,
|
||||||
|
resourceType: "campaign",
|
||||||
|
resourceId: campaign.id,
|
||||||
|
action: "campaigns:campaign:read",
|
||||||
|
tenantId: (auth.active_tenant ?? auth.tenant)?.id
|
||||||
|
});
|
||||||
|
setAccessExplanation(explanation);
|
||||||
|
setAccessExplanationSubjectId(userId);
|
||||||
|
} catch (err) {
|
||||||
|
onError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setAccessExplanationLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns = useMemo<DataGridColumn<CampaignShare>[]>(() => [
|
const columns = useMemo<DataGridColumn<CampaignShare>[]>(() => [
|
||||||
{
|
{
|
||||||
id: "target",
|
id: "target",
|
||||||
@@ -517,8 +552,15 @@ export default function CampaignAccessCard({
|
|||||||
<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>
|
||||||
|
|
||||||
<Dialog open={accessExplanationOpen} className="campaign-access-dialog" title="i18n:govoplan-core.access_explanation.75ee7f62" onClose={() => { if (!accessExplanationLoading) { setAccessExplanationOpen(false); setAccessExplanation(null); } }} footer={<Button onClick={() => { setAccessExplanationOpen(false); setAccessExplanation(null); }} disabled={accessExplanationLoading}>i18n:govoplan-campaign.close.bbfa773e</Button>}>
|
<Dialog open={accessExplanationOpen} className="campaign-access-dialog" title="i18n:govoplan-core.access_explanation.75ee7f62" onClose={() => { if (!accessExplanationLoading) { setAccessExplanationOpen(false); setAccessExplanation(null); setAccessExplanationSubjects(null); setAccessExplanationSubjectId(""); } }} footer={<Button onClick={() => { setAccessExplanationOpen(false); setAccessExplanation(null); setAccessExplanationSubjects(null); setAccessExplanationSubjectId(""); }} disabled={accessExplanationLoading}>i18n:govoplan-campaign.close.bbfa773e</Button>}>
|
||||||
<ResourceAccessExplanation loading={accessExplanationLoading} explanation={accessExplanation} fallbackResourceLabel={campaign.name || campaign.external_id || campaign.id} />
|
<ResourceAccessExplanation
|
||||||
|
loading={accessExplanationLoading}
|
||||||
|
explanation={accessExplanation}
|
||||||
|
subjects={accessExplanationSubjects}
|
||||||
|
selectedUserId={accessExplanationSubjectId}
|
||||||
|
onSelectedUserIdChange={(userId) => void selectAccessExplanationSubject(userId)}
|
||||||
|
fallbackResourceLabel={campaign.name || campaign.external_id || campaign.id}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog open={Boolean(revokeTarget)} title="i18n:govoplan-campaign.remove_campaign_share.2c2d42eb" message="i18n:govoplan-campaign.remove_this_user_s_or_group_s_explicit_access_to.5ff07672" confirmLabel="i18n:govoplan-campaign.remove_share.69c932e1" tone="danger" busy={busy} onCancel={() => setRevokeTarget(null)} onConfirm={() => void revoke()} />
|
<ConfirmDialog open={Boolean(revokeTarget)} title="i18n:govoplan-campaign.remove_campaign_share.2c2d42eb" message="i18n:govoplan-campaign.remove_this_user_s_or_group_s_explicit_access_to.5ff07672" confirmLabel="i18n:govoplan-campaign.remove_share.69c932e1" tone="danger" busy={busy} onCancel={() => setRevokeTarget(null)} onConfirm={() => void revoke()} />
|
||||||
|
|||||||
Reference in New Issue
Block a user