From 4e9bbb4202b3b4a48fbfcff24a12e4d8e9dfc926 Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Thu, 5 Mar 2026 09:20:43 -0500 Subject: [PATCH 01/11] steward not required for process builder --- .../stepContent/general/OverviewSectionForm.tsx | 8 +++----- .../ProcessBuilder/validation/processBuilderValidation.ts | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx index 32c4c0a9a..33d474df6 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx @@ -20,9 +20,7 @@ const AUTOSAVE_DEBOUNCE_MS = 1000; const createOverviewValidator = (t: TranslateFn) => z.object({ - stewardProfileId: z - .string({ message: t('Select a steward for this process') }) - .min(1, { message: t('Select a steward for this process') }), + stewardProfileId: z.string(), name: z .string({ message: t('Enter a process name') }) .min(1, { message: t('Enter a process name') }), @@ -164,7 +162,8 @@ export function OverviewSectionForm({ const form = useAppForm({ defaultValues: { - stewardProfileId: initialStewardProfileId, + stewardProfileId: + instanceData?.stewardProfileId || instance.steward?.id || '', name: initialName, description: initialDescription, organizeByCategories: instanceData?.config?.organizeByCategories ?? true, @@ -219,7 +218,6 @@ export function OverviewSectionForm({ children={(field) => ( field.handleChange(key as string)} diff --git a/apps/app/src/components/decisions/ProcessBuilder/validation/processBuilderValidation.ts b/apps/app/src/components/decisions/ProcessBuilder/validation/processBuilderValidation.ts index 65660b59f..286532679 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/validation/processBuilderValidation.ts +++ b/apps/app/src/components/decisions/ProcessBuilder/validation/processBuilderValidation.ts @@ -22,7 +22,6 @@ const nonEmptyString = z.string().trim().min(1); const overviewSchema = z.object({ name: nonEmptyString, - stewardProfileId: nonEmptyString, description: nonEmptyString, }); From 2b17980015f12d9dcd74915be136dfc920fcba4f Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Thu, 5 Mar 2026 09:20:53 -0500 Subject: [PATCH 02/11] properly set and load steward on backend --- .../common/src/services/decision/createInstanceFromTemplate.ts | 3 +++ packages/common/src/services/decision/getDecisionBySlug.ts | 1 + packages/common/src/services/decision/getInstance.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/packages/common/src/services/decision/createInstanceFromTemplate.ts b/packages/common/src/services/decision/createInstanceFromTemplate.ts index 09e727926..db8e98ef9 100644 --- a/packages/common/src/services/decision/createInstanceFromTemplate.ts +++ b/packages/common/src/services/decision/createInstanceFromTemplate.ts @@ -27,6 +27,8 @@ export type CreateInstanceFromTemplateCoreOptions = { description?: string; phases?: PhaseOverride[]; ownerProfileId: string; + /** Defaults to ownerProfileId when not provided */ + stewardProfileId?: string; creatorAuthUserId: string; creatorEmail: string; stewardProfileId?: string; @@ -45,6 +47,7 @@ export const createInstanceFromTemplateCore = async ({ description, phases, ownerProfileId, + stewardProfileId = ownerProfileId, creatorAuthUserId, creatorEmail, stewardProfileId, diff --git a/packages/common/src/services/decision/getDecisionBySlug.ts b/packages/common/src/services/decision/getDecisionBySlug.ts index bc5b4d0d2..7d3fb23c2 100644 --- a/packages/common/src/services/decision/getDecisionBySlug.ts +++ b/packages/common/src/services/decision/getDecisionBySlug.ts @@ -23,6 +23,7 @@ const decisionProfileQueryConfig = { organization: true, }, }, + steward: true, }, }, }, diff --git a/packages/common/src/services/decision/getInstance.ts b/packages/common/src/services/decision/getInstance.ts index 9967c9737..41ecbcedf 100644 --- a/packages/common/src/services/decision/getInstance.ts +++ b/packages/common/src/services/decision/getInstance.ts @@ -20,6 +20,7 @@ export const getInstance = async ({ instanceId, user }: GetInstanceInput) => { with: { process: true, owner: true, + steward: true, proposals: { columns: { id: true, From d782cfd1a871efaf55c52579fd7aebf7a9bc7d07 Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Thu, 5 Mar 2026 09:32:48 -0500 Subject: [PATCH 03/11] Remove duplicate stewardProfileId --- .../ProcessBuilder/stepContent/general/OverviewSectionForm.tsx | 3 +-- .../common/src/services/decision/createInstanceFromTemplate.ts | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx index 33d474df6..570c57281 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx @@ -162,8 +162,7 @@ export function OverviewSectionForm({ const form = useAppForm({ defaultValues: { - stewardProfileId: - instanceData?.stewardProfileId || instance.steward?.id || '', + stewardProfileId: initialStewardProfileId, name: initialName, description: initialDescription, organizeByCategories: instanceData?.config?.organizeByCategories ?? true, diff --git a/packages/common/src/services/decision/createInstanceFromTemplate.ts b/packages/common/src/services/decision/createInstanceFromTemplate.ts index db8e98ef9..dee4cf017 100644 --- a/packages/common/src/services/decision/createInstanceFromTemplate.ts +++ b/packages/common/src/services/decision/createInstanceFromTemplate.ts @@ -31,7 +31,6 @@ export type CreateInstanceFromTemplateCoreOptions = { stewardProfileId?: string; creatorAuthUserId: string; creatorEmail: string; - stewardProfileId?: string; /** Defaults to DRAFT */ status?: ProcessStatus; }; @@ -50,7 +49,6 @@ export const createInstanceFromTemplateCore = async ({ stewardProfileId = ownerProfileId, creatorAuthUserId, creatorEmail, - stewardProfileId, status = ProcessStatus.DRAFT, }: CreateInstanceFromTemplateCoreOptions) => { const template = await getTemplate(templateId); From c3b0896e534d58ba60c0442f735e0e39b3bc59d4 Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Thu, 5 Mar 2026 11:27:19 -0500 Subject: [PATCH 04/11] Disable process steward selection if not process owner --- .../stepContent/general/OverviewSectionForm.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx index 570c57281..cb6aeac7b 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx @@ -117,6 +117,22 @@ export function OverviewSectionForm({ name: p.name, })); + // Ensure the current steward appears in the dropdown so the Select can + // render its name — even when the viewer isn't the owner and their own + // profile list doesn't include the steward. + if ( + instance.steward && + !profileItems.some((p) => p.id === instance.steward?.id) + ) { + profileItems.push({ + id: instance.steward.id, + name: instance.steward.name ?? '', + }); + } + + // Only the process owner can change the steward + const isProcessOwner = userProfiles?.some((p) => p.id === instance.owner?.id); + // Debounced save: draft persists to API; non-draft only buffers locally. const debouncedSave = useDebouncedCallback((values: OverviewFormData) => { setSaveStatus(decisionProfileId, 'saving'); @@ -218,6 +234,7 @@ export function OverviewSectionForm({ field.handleChange(key as string)} onBlur={field.handleBlur} From 0e6f5a4f32def8c6f1dbb259083b9d715ae56fef Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Thu, 5 Mar 2026 11:31:44 -0500 Subject: [PATCH 05/11] Update description when changing steward is disabled --- .../stepContent/general/OverviewSectionForm.tsx | 12 +++++++++--- apps/app/src/lib/i18n/dictionaries/bn.json | 1 + apps/app/src/lib/i18n/dictionaries/en.json | 1 + apps/app/src/lib/i18n/dictionaries/es.json | 1 + apps/app/src/lib/i18n/dictionaries/fr.json | 1 + apps/app/src/lib/i18n/dictionaries/pt.json | 1 + 6 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx index cb6aeac7b..12b016d77 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx @@ -238,9 +238,15 @@ export function OverviewSectionForm({ selectedKey={field.state.value || null} onSelectionChange={(key) => field.handleChange(key as string)} onBlur={field.handleBlur} - description={t( - 'The organization, coalition, committee or individual responsible for running this process.', - )} + description={ + t( + 'The organization, coalition, committee or individual responsible for running this process.', + ) + + (isProcessOwner + ? '' + : ' ' + + t('Only the process owner can change the steward.')) + } errorMessage={getFieldErrorMessage(field)} > {profileItems.map((item) => ( diff --git a/apps/app/src/lib/i18n/dictionaries/bn.json b/apps/app/src/lib/i18n/dictionaries/bn.json index a2e9e19fc..760797a81 100644 --- a/apps/app/src/lib/i18n/dictionaries/bn.json +++ b/apps/app/src/lib/i18n/dictionaries/bn.json @@ -563,6 +563,7 @@ "Keep this process private": "এই প্রক্রিয়াটি ব্যক্তিগত রাখুন", "Only invited members can view and participate in this process": "শুধুমাত্র আমন্ত্রিত সদস্যরা এই প্রক্রিয়া দেখতে এবং অংশগ্রহণ করতে পারেন", "Process Overview": "প্রক্রিয়া সংক্ষেপ", + "Only the process owner can change the steward.": "শুধুমাত্র প্রক্রিয়ার মালিক তত্ত্বাবধায়ক পরিবর্তন করতে পারেন।", "Organize proposals into categories": "প্রস্তাবগুলি বিভাগে সংগঠিত করুন", "Group proposals into categories for better organization and evaluation": "ভালো সংগঠন এবং মূল্যায়নের জন্য প্রস্তাবগুলি বিভাগে গোষ্ঠীবদ্ধ করুন", "Require collaborative proposals": "সহযোগী প্রস্তাব প্রয়োজন", diff --git a/apps/app/src/lib/i18n/dictionaries/en.json b/apps/app/src/lib/i18n/dictionaries/en.json index 888431b00..fd8a51fd7 100644 --- a/apps/app/src/lib/i18n/dictionaries/en.json +++ b/apps/app/src/lib/i18n/dictionaries/en.json @@ -556,6 +556,7 @@ "Keep this process private": "Keep this process private", "Only invited members can view and participate in this process": "Only invited members can view and participate in this process", "Process Overview": "Process Overview", + "Only the process owner can change the steward.": "Only the process owner can change the steward.", "Organize proposals into categories": "Organize proposals into categories", "Group proposals into categories for better organization and evaluation": "Group proposals into categories for better organization and evaluation", "Require collaborative proposals": "Require collaborative proposals", diff --git a/apps/app/src/lib/i18n/dictionaries/es.json b/apps/app/src/lib/i18n/dictionaries/es.json index b40d0ebd7..263e2af7a 100644 --- a/apps/app/src/lib/i18n/dictionaries/es.json +++ b/apps/app/src/lib/i18n/dictionaries/es.json @@ -555,6 +555,7 @@ "Keep this process private": "Mantener este proceso privado", "Only invited members can view and participate in this process": "Solo los miembros invitados pueden ver y participar en este proceso", "Process Overview": "Resumen del proceso", + "Only the process owner can change the steward.": "Solo el propietario del proceso puede cambiar el administrador.", "Organize proposals into categories": "Organizar propuestas en categorías", "Group proposals into categories for better organization and evaluation": "Agrupar propuestas en categorías para una mejor organización y evaluación", "Require collaborative proposals": "Requerir propuestas colaborativas", diff --git a/apps/app/src/lib/i18n/dictionaries/fr.json b/apps/app/src/lib/i18n/dictionaries/fr.json index a3d5f3b7f..84cfd0ade 100644 --- a/apps/app/src/lib/i18n/dictionaries/fr.json +++ b/apps/app/src/lib/i18n/dictionaries/fr.json @@ -556,6 +556,7 @@ "Keep this process private": "Garder ce processus privé", "Only invited members can view and participate in this process": "Seuls les membres invités peuvent voir et participer à ce processus", "Process Overview": "Aperçu du processus", + "Only the process owner can change the steward.": "Seul le propriétaire du processus peut modifier le responsable.", "Organize proposals into categories": "Organiser les propositions en catégories", "Group proposals into categories for better organization and evaluation": "Regrouper les propositions en catégories pour une meilleure organisation et évaluation", "Require collaborative proposals": "Exiger des propositions collaboratives", diff --git a/apps/app/src/lib/i18n/dictionaries/pt.json b/apps/app/src/lib/i18n/dictionaries/pt.json index 04059e535..0f9db5137 100644 --- a/apps/app/src/lib/i18n/dictionaries/pt.json +++ b/apps/app/src/lib/i18n/dictionaries/pt.json @@ -556,6 +556,7 @@ "Keep this process private": "Manter este processo privado", "Only invited members can view and participate in this process": "Apenas membros convidados podem ver e participar deste processo", "Process Overview": "Visão geral do processo", + "Only the process owner can change the steward.": "Apenas o proprietário do processo pode alterar o administrador.", "Organize proposals into categories": "Organizar propostas em categorias", "Group proposals into categories for better organization and evaluation": "Agrupar propostas em categorias para melhor organização e avaliação", "Require collaborative proposals": "Exigir propostas colaborativas", From 981e95aa3e94b6012e630eb1e140b638c67dbe5e Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Fri, 6 Mar 2026 08:53:57 -0500 Subject: [PATCH 06/11] string template --- .../stepContent/general/OverviewSectionForm.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx index 12b016d77..ab8016e40 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx @@ -238,15 +238,14 @@ export function OverviewSectionForm({ selectedKey={field.state.value || null} onSelectionChange={(key) => field.handleChange(key as string)} onBlur={field.handleBlur} - description={ - t( + description={` + ${t( 'The organization, coalition, committee or individual responsible for running this process.', - ) + - (isProcessOwner - ? '' - : ' ' + - t('Only the process owner can change the steward.')) - } + )} + ${ + isProcessOwner && + t('Only the process owner can change the steward.') + }`} errorMessage={getFieldErrorMessage(field)} > {profileItems.map((item) => ( From be0eb670462ead0cc029405971fada9552004251 Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Fri, 6 Mar 2026 08:54:19 -0500 Subject: [PATCH 07/11] throw if steward sent by non-owner --- .../decision/updateDecisionInstance.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/common/src/services/decision/updateDecisionInstance.ts b/packages/common/src/services/decision/updateDecisionInstance.ts index e0aba7f10..5d9a916ea 100644 --- a/packages/common/src/services/decision/updateDecisionInstance.ts +++ b/packages/common/src/services/decision/updateDecisionInstance.ts @@ -8,7 +8,7 @@ import { import type { User } from '@op/supabase/lib'; import { assertAccess, permission } from 'access-zones'; -import { CommonError, NotFoundError } from '../../utils'; +import { CommonError, NotFoundError, UnauthorizedError } from '../../utils'; import { getProfileAccessUser } from '../access'; import { schemaValidator } from './schemaValidator'; import type { @@ -97,7 +97,23 @@ export const updateDecisionInstance = async ({ updateData.status = status; } - if (stewardProfileId !== undefined) { + if ( + stewardProfileId !== undefined && + stewardProfileId !== existingInstance.stewardProfileId + ) { + const ownerProfileUser = existingInstance.ownerProfileId + ? await getProfileAccessUser({ + user, + profileId: existingInstance.ownerProfileId, + }) + : undefined; + + if (!ownerProfileUser) { + throw new UnauthorizedError( + 'Only the process owner can change the steward', + ); + } + updateData.stewardProfileId = stewardProfileId; } From 1e254a68887565b4216337f81b8f11fb8798c640 Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Fri, 6 Mar 2026 08:54:39 -0500 Subject: [PATCH 08/11] Don't sent steward value if not process owner --- .../decisions/ProcessBuilder/ProcessBuilderHeader.tsx | 9 ++++++++- .../stepContent/general/OverviewSectionForm.tsx | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx index 223213e9b..1ebcafd1d 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx @@ -105,6 +105,11 @@ const ProcessBuilderHeaderContent = ({ const decisionProfileId = decisionProfile?.id; const validation = useProcessBuilderValidation(decisionProfileId); + const { data: userProfiles } = trpc.account.getUserProfiles.useQuery(); + const isProcessOwner = userProfiles?.some( + (p) => p.id === processInstance?.owner?.id, + ); + const storeData = useProcessBuilderStore((s) => decisionProfileId ? s.instances[decisionProfileId] : undefined, ); @@ -150,7 +155,9 @@ const ProcessBuilderHeaderContent = ({ instanceId, name: storeData?.name || undefined, description: storeData?.description || undefined, - stewardProfileId: storeData?.stewardProfileId || undefined, + stewardProfileId: isProcessOwner + ? (storeData?.stewardProfileId || undefined) + : undefined, phases: storeData?.phases, proposalTemplate: storeData?.proposalTemplate, config: storeData?.config, diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx index ab8016e40..86d473b4d 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx @@ -155,7 +155,9 @@ export function OverviewSectionForm({ instanceId, name: values.name, description: values.description, - stewardProfileId: values.stewardProfileId || undefined, + stewardProfileId: isProcessOwner + ? (values.stewardProfileId || undefined) + : undefined, config: { organizeByCategories: values.organizeByCategories, requireCollaborativeProposals: values.requireCollaborativeProposals, From a3f7e72ead75a25c66fc9901046612903010d2ba Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Fri, 6 Mar 2026 09:13:26 -0500 Subject: [PATCH 09/11] format --- .../decisions/ProcessBuilder/ProcessBuilderHeader.tsx | 2 +- .../ProcessBuilder/stepContent/general/OverviewSectionForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx index 1ebcafd1d..b827739e9 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/ProcessBuilderHeader.tsx @@ -156,7 +156,7 @@ const ProcessBuilderHeaderContent = ({ name: storeData?.name || undefined, description: storeData?.description || undefined, stewardProfileId: isProcessOwner - ? (storeData?.stewardProfileId || undefined) + ? storeData?.stewardProfileId || undefined : undefined, phases: storeData?.phases, proposalTemplate: storeData?.proposalTemplate, diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx index 86d473b4d..38e899747 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx @@ -156,7 +156,7 @@ export function OverviewSectionForm({ name: values.name, description: values.description, stewardProfileId: isProcessOwner - ? (values.stewardProfileId || undefined) + ? values.stewardProfileId || undefined : undefined, config: { organizeByCategories: values.organizeByCategories, From c2b45b709af9b93f1cfe80ddb7dbe5176ffa4e0c Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Fri, 6 Mar 2026 13:03:18 -0500 Subject: [PATCH 10/11] Simplify steward description by removing logic --- .../stepContent/general/OverviewSectionForm.tsx | 11 +++-------- apps/app/src/lib/i18n/dictionaries/bn.json | 3 +-- apps/app/src/lib/i18n/dictionaries/en.json | 3 +-- apps/app/src/lib/i18n/dictionaries/es.json | 3 +-- apps/app/src/lib/i18n/dictionaries/fr.json | 3 +-- apps/app/src/lib/i18n/dictionaries/pt.json | 3 +-- 6 files changed, 8 insertions(+), 18 deletions(-) diff --git a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx index 38e899747..f963e8827 100644 --- a/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx +++ b/apps/app/src/components/decisions/ProcessBuilder/stepContent/general/OverviewSectionForm.tsx @@ -240,14 +240,9 @@ export function OverviewSectionForm({ selectedKey={field.state.value || null} onSelectionChange={(key) => field.handleChange(key as string)} onBlur={field.handleBlur} - description={` - ${t( - 'The organization, coalition, committee or individual responsible for running this process.', - )} - ${ - isProcessOwner && - t('Only the process owner can change the steward.') - }`} + description={t( + 'The organization, coalition, committee or individual responsible for running this process. Only the process owner can change the steward.', + )} errorMessage={getFieldErrorMessage(field)} > {profileItems.map((item) => ( diff --git a/apps/app/src/lib/i18n/dictionaries/bn.json b/apps/app/src/lib/i18n/dictionaries/bn.json index 760797a81..e0642d257 100644 --- a/apps/app/src/lib/i18n/dictionaries/bn.json +++ b/apps/app/src/lib/i18n/dictionaries/bn.json @@ -538,7 +538,7 @@ "Please try again in a moment": "অনুগ্রহ করে কিছুক্ষণ পরে আবার চেষ্টা করুন", "Process Stewardship": "প্রক্রিয়া পরিচালনা", "Who is stewarding this process?": "এই প্রক্রিয়াটি কে পরিচালনা করছে?", - "The organization, coalition, committee or individual responsible for running this process.": "এই প্রক্রিয়া পরিচালনার জন্য দায়ী সংস্থা, জোট, কমিটি বা ব্যক্তি।", + "The organization, coalition, committee or individual responsible for running this process. Only the process owner can change the steward.": "এই প্রক্রিয়া পরিচালনার জন্য দায়ী সংস্থা, জোট, কমিটি বা ব্যক্তি। শুধুমাত্র প্রক্রিয়ার মালিক তত্ত্বাবধায়ক পরিবর্তন করতে পারেন।", "Primary focus areas": "প্রাথমিক ফোকাস ক্ষেত্র", "Search focus areas": "ফোকাস ক্ষেত্র খুঁজুন", "Select the strategic areas this process advances": "এই প্রক্রিয়া যে কৌশলগত ক্ষেত্রগুলি অগ্রসর করে তা নির্বাচন করুন", @@ -563,7 +563,6 @@ "Keep this process private": "এই প্রক্রিয়াটি ব্যক্তিগত রাখুন", "Only invited members can view and participate in this process": "শুধুমাত্র আমন্ত্রিত সদস্যরা এই প্রক্রিয়া দেখতে এবং অংশগ্রহণ করতে পারেন", "Process Overview": "প্রক্রিয়া সংক্ষেপ", - "Only the process owner can change the steward.": "শুধুমাত্র প্রক্রিয়ার মালিক তত্ত্বাবধায়ক পরিবর্তন করতে পারেন।", "Organize proposals into categories": "প্রস্তাবগুলি বিভাগে সংগঠিত করুন", "Group proposals into categories for better organization and evaluation": "ভালো সংগঠন এবং মূল্যায়নের জন্য প্রস্তাবগুলি বিভাগে গোষ্ঠীবদ্ধ করুন", "Require collaborative proposals": "সহযোগী প্রস্তাব প্রয়োজন", diff --git a/apps/app/src/lib/i18n/dictionaries/en.json b/apps/app/src/lib/i18n/dictionaries/en.json index fd8a51fd7..5e80b1352 100644 --- a/apps/app/src/lib/i18n/dictionaries/en.json +++ b/apps/app/src/lib/i18n/dictionaries/en.json @@ -531,7 +531,7 @@ "Please try again in a moment": "Please try again in a moment", "Process Stewardship": "Process Stewardship", "Who is stewarding this process?": "Who is stewarding this process?", - "The organization, coalition, committee or individual responsible for running this process.": "The organization, coalition, committee or individual responsible for running this process.", + "The organization, coalition, committee or individual responsible for running this process. Only the process owner can change the steward.": "The organization, coalition, committee or individual responsible for running this process. Only the process owner can change the steward.", "Primary focus areas": "Primary focus areas", "Search focus areas": "Search focus areas", "Select the strategic areas this process advances": "Select the strategic areas this process advances", @@ -556,7 +556,6 @@ "Keep this process private": "Keep this process private", "Only invited members can view and participate in this process": "Only invited members can view and participate in this process", "Process Overview": "Process Overview", - "Only the process owner can change the steward.": "Only the process owner can change the steward.", "Organize proposals into categories": "Organize proposals into categories", "Group proposals into categories for better organization and evaluation": "Group proposals into categories for better organization and evaluation", "Require collaborative proposals": "Require collaborative proposals", diff --git a/apps/app/src/lib/i18n/dictionaries/es.json b/apps/app/src/lib/i18n/dictionaries/es.json index 263e2af7a..43aaf7c96 100644 --- a/apps/app/src/lib/i18n/dictionaries/es.json +++ b/apps/app/src/lib/i18n/dictionaries/es.json @@ -530,7 +530,7 @@ "Please try again in a moment": "Por favor, intenta de nuevo en un momento", "Process Stewardship": "Administración del proceso", "Who is stewarding this process?": "¿Quién está administrando este proceso?", - "The organization, coalition, committee or individual responsible for running this process.": "La organización, coalición, comité o individuo responsable de ejecutar este proceso.", + "The organization, coalition, committee or individual responsible for running this process. Only the process owner can change the steward.": "La organización, coalición, comité o individuo responsable de ejecutar este proceso. Solo el propietario del proceso puede cambiar el administrador.", "Primary focus areas": "Áreas de enfoque principales", "Search focus areas": "Buscar áreas de enfoque", "Select the strategic areas this process advances": "Seleccione las áreas estratégicas que este proceso avanza", @@ -555,7 +555,6 @@ "Keep this process private": "Mantener este proceso privado", "Only invited members can view and participate in this process": "Solo los miembros invitados pueden ver y participar en este proceso", "Process Overview": "Resumen del proceso", - "Only the process owner can change the steward.": "Solo el propietario del proceso puede cambiar el administrador.", "Organize proposals into categories": "Organizar propuestas en categorías", "Group proposals into categories for better organization and evaluation": "Agrupar propuestas en categorías para una mejor organización y evaluación", "Require collaborative proposals": "Requerir propuestas colaborativas", diff --git a/apps/app/src/lib/i18n/dictionaries/fr.json b/apps/app/src/lib/i18n/dictionaries/fr.json index 84cfd0ade..f0d896785 100644 --- a/apps/app/src/lib/i18n/dictionaries/fr.json +++ b/apps/app/src/lib/i18n/dictionaries/fr.json @@ -531,7 +531,7 @@ "Please try again in a moment": "Veuillez réessayer dans un instant", "Process Stewardship": "Gestion du processus", "Who is stewarding this process?": "Qui gère ce processus ?", - "The organization, coalition, committee or individual responsible for running this process.": "L'organisation, la coalition, le comité ou l'individu responsable de l'exécution de ce processus.", + "The organization, coalition, committee or individual responsible for running this process. Only the process owner can change the steward.": "L'organisation, la coalition, le comité ou l'individu responsable de l'exécution de ce processus. Seul le propriétaire du processus peut modifier le responsable.", "Primary focus areas": "Domaines d'intervention principaux", "Search focus areas": "Rechercher des domaines d'intervention", "Select the strategic areas this process advances": "Sélectionnez les domaines stratégiques que ce processus fait progresser", @@ -556,7 +556,6 @@ "Keep this process private": "Garder ce processus privé", "Only invited members can view and participate in this process": "Seuls les membres invités peuvent voir et participer à ce processus", "Process Overview": "Aperçu du processus", - "Only the process owner can change the steward.": "Seul le propriétaire du processus peut modifier le responsable.", "Organize proposals into categories": "Organiser les propositions en catégories", "Group proposals into categories for better organization and evaluation": "Regrouper les propositions en catégories pour une meilleure organisation et évaluation", "Require collaborative proposals": "Exiger des propositions collaboratives", diff --git a/apps/app/src/lib/i18n/dictionaries/pt.json b/apps/app/src/lib/i18n/dictionaries/pt.json index 0f9db5137..367093a5f 100644 --- a/apps/app/src/lib/i18n/dictionaries/pt.json +++ b/apps/app/src/lib/i18n/dictionaries/pt.json @@ -531,7 +531,7 @@ "Please try again in a moment": "Por favor, tente novamente em instantes", "Process Stewardship": "Administração do processo", "Who is stewarding this process?": "Quem está administrando este processo?", - "The organization, coalition, committee or individual responsible for running this process.": "A organização, coalizão, comitê ou indivíduo responsável por executar este processo.", + "The organization, coalition, committee or individual responsible for running this process. Only the process owner can change the steward.": "A organização, coalizão, comitê ou indivíduo responsável por executar este processo. Apenas o proprietário do processo pode alterar o administrador.", "Primary focus areas": "Áreas de foco principais", "Search focus areas": "Pesquisar áreas de foco", "Select the strategic areas this process advances": "Selecione as áreas estratégicas que este processo avança", @@ -556,7 +556,6 @@ "Keep this process private": "Manter este processo privado", "Only invited members can view and participate in this process": "Apenas membros convidados podem ver e participar deste processo", "Process Overview": "Visão geral do processo", - "Only the process owner can change the steward.": "Apenas o proprietário do processo pode alterar o administrador.", "Organize proposals into categories": "Organizar propostas em categorias", "Group proposals into categories for better organization and evaluation": "Agrupar propostas em categorias para melhor organização e avaliação", "Require collaborative proposals": "Exigir propostas colaborativas", From 311c9bf74232eb9a9da9ce5e60387bfdaa89b36d Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Fri, 6 Mar 2026 15:16:59 -0500 Subject: [PATCH 11/11] Add tests --- .../instances/updateDecisionInstance.test.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/services/api/src/routers/decision/instances/updateDecisionInstance.test.ts b/services/api/src/routers/decision/instances/updateDecisionInstance.test.ts index ca3646196..2274227c7 100644 --- a/services/api/src/routers/decision/instances/updateDecisionInstance.test.ts +++ b/services/api/src/routers/decision/instances/updateDecisionInstance.test.ts @@ -673,4 +673,121 @@ describe.concurrent('updateDecisionInstance', () => { expect(phase.name).toBe(`Updated ${phase.phaseId}`); } }); + + it('should allow process owner to update steward', async ({ + task, + onTestFinished, + }) => { + const testData = new TestDecisionsDataManager(task.id, onTestFinished); + + const setup = await testData.createDecisionSetup({ + instanceCount: 1, + grantAccess: true, + }); + + const instance = setup.instances[0]; + if (!instance) { + throw new Error('No instance created'); + } + + const caller = await createAuthenticatedCaller(setup.userEmail); + + // Use the org profile as the new steward (it's a valid profile the owner controls) + const newStewardId = setup.organization.profileId; + + const result = await caller.decision.updateDecisionInstance({ + instanceId: instance.instance.id, + stewardProfileId: newStewardId, + }); + + expect(result.processInstance.id).toBe(instance.instance.id); + + // Verify the steward was updated in the database + const dbInstance = await db._query.processInstances.findFirst({ + where: eq(processInstances.id, instance.instance.id), + }); + + expect(dbInstance!.stewardProfileId).toBe(newStewardId); + }); + + it('should not allow non-owner admin to change steward', async ({ + task, + onTestFinished, + }) => { + const testData = new TestDecisionsDataManager(task.id, onTestFinished); + + const setup = await testData.createDecisionSetup({ + instanceCount: 1, + grantAccess: true, + }); + + const instance = setup.instances[0]; + if (!instance) { + throw new Error('No instance created'); + } + + // Create a member user and grant them admin access on the decision profile + // (skip instanceProfileIds to avoid duplicate profileUsers rows) + const memberUser = await testData.createMemberUser({ + organization: setup.organization, + }); + + // Grant admin access on the decision profile so they pass the general + // admin check but are still not the process owner + await testData.grantProfileAccess( + instance.profileId, + memberUser.authUserId, + memberUser.email, + ); + + const memberCaller = await createAuthenticatedCaller(memberUser.email); + + // Non-owner admin should NOT be able to change the steward + await expect( + memberCaller.decision.updateDecisionInstance({ + instanceId: instance.instance.id, + stewardProfileId: memberUser.profileId, + }), + ).rejects.toThrow(); + }); + + it('should allow non-owner admin to update other fields without changing steward', async ({ + task, + onTestFinished, + }) => { + const testData = new TestDecisionsDataManager(task.id, onTestFinished); + + const setup = await testData.createDecisionSetup({ + instanceCount: 1, + grantAccess: true, + }); + + const instance = setup.instances[0]; + if (!instance) { + throw new Error('No instance created'); + } + + // Create a member user and grant them admin access on the decision profile + // (skip instanceProfileIds to avoid duplicate profileUsers rows) + const memberUser = await testData.createMemberUser({ + organization: setup.organization, + }); + + await testData.grantProfileAccess( + instance.profileId, + memberUser.authUserId, + memberUser.email, + ); + + const memberCaller = await createAuthenticatedCaller(memberUser.email); + + // Non-owner admin should still be able to update other fields + const newName = `Updated by member ${task.id}`; + const result = await memberCaller.decision.updateDecisionInstance({ + instanceId: instance.instance.id, + name: newName, + }); + + expect(result.processInstance.name).toBe(newName); + }); });