diff --git a/ui/src/i18n/catalogs.ts b/ui/src/i18n/catalogs.ts index 1a33674e..499475bf 100644 --- a/ui/src/i18n/catalogs.ts +++ b/ui/src/i18n/catalogs.ts @@ -129,6 +129,8 @@ const englishApplyMessages = { "apply.organization.choose": "Choose an organization", "apply.organization.continue": "Continue", "apply.organization.manage": "Manage organizations", + "apply.organization.pickPrompt": + "This organization can't start a community — it may be a personal workspace, already run one, or you aren't its owner. Open your organizations and pick one you own.", "apply.near.description": "Link the NEAR account you're applying with.", "apply.near.link": "Link NEAR account", "apply.kind.label": "Type", @@ -152,7 +154,6 @@ const englishApplyMessages = { "apply.slug.taken": "{hostname} is taken", "apply.slug.organizationTaken": "An organization already uses this address. Choose another one.", "apply.slug.usesOrganization": "Will use your organization @{slug}.", - "apply.slug.useDifferentAddress": "Use a different address", "apply.motivation.label": "Why you want to run it", "apply.motivation.placeholder": "Who is the community, and what will you do for it?", "apply.motivation.description": "Reviewers read this before approving.", @@ -326,6 +327,8 @@ const spanishAppMessages = { "apply.organization.choose": "Elige una organización", "apply.organization.continue": "Continuar", "apply.organization.manage": "Gestionar organizaciones", + "apply.organization.pickPrompt": + "Esta organización no puede crear una comunidad: puede ser un espacio personal, tener ya una, o no eres su dueño. Abre tus organizaciones y elige una que te pertenezca.", "apply.near.description": "Vincula la cuenta de NEAR con la que presentas la solicitud.", "apply.near.link": "Vincular cuenta de NEAR", "apply.kind.label": "Tipo", @@ -349,7 +352,6 @@ const spanishAppMessages = { "apply.slug.taken": "{hostname} está ocupado", "apply.slug.organizationTaken": "Una organización ya usa esta dirección. Elige otra.", "apply.slug.usesOrganization": "Usará tu organización @{slug}.", - "apply.slug.useDifferentAddress": "Usar otra dirección", "apply.motivation.label": "Por qué quieres gestionarla", "apply.motivation.placeholder": "¿A quién sirve la comunidad y qué harás por ella?", "apply.motivation.description": "Los revisores leen esto antes de aprobar.", @@ -506,6 +508,8 @@ const frenchAppMessages = { "apply.organization.choose": "Choisir une organisation", "apply.organization.continue": "Continuer", "apply.organization.manage": "Gérer les organisations", + "apply.organization.pickPrompt": + "Cette organisation ne peut pas créer de communauté : il se peut que ce soit un espace personnel, qu'elle en ait déjà une, ou que vous n'en soyez pas le propriétaire. Ouvrez vos organisations et choisissez-en une qui vous appartient.", "apply.near.description": "Associez le compte NEAR utilisé pour votre demande.", "apply.near.link": "Associer un compte NEAR", "apply.kind.label": "Type", @@ -530,7 +534,6 @@ const frenchAppMessages = { "apply.slug.organizationTaken": "Une organisation utilise déjà cette adresse. Choisissez-en une autre.", "apply.slug.usesOrganization": "Utilisera votre organisation @{slug}.", - "apply.slug.useDifferentAddress": "Utiliser une autre adresse", "apply.motivation.label": "Pourquoi souhaitez-vous la gérer ?", "apply.motivation.placeholder": "À qui s’adresse la communauté et que ferez-vous pour elle ?", "apply.motivation.description": @@ -686,6 +689,8 @@ const chineseAppMessages = { "apply.organization.choose": "选择组织", "apply.organization.continue": "继续", "apply.organization.manage": "管理组织", + "apply.organization.pickPrompt": + "该组织无法创建社区:它可能是个人工作区、已有社区,或者你不是它的所有者。打开你的组织页面,选择一个你拥有的组织。", "apply.near.description": "关联你用于申请的 NEAR 账户。", "apply.near.link": "关联 NEAR 账户", "apply.kind.label": "类型", @@ -709,7 +714,6 @@ const chineseAppMessages = { "apply.slug.taken": "{hostname} 已被占用", "apply.slug.organizationTaken": "已有组织使用此地址。请选择其他地址。", "apply.slug.usesOrganization": "将使用你的组织 @{slug}。", - "apply.slug.useDifferentAddress": "使用其他地址", "apply.motivation.label": "为什么要运营它", "apply.motivation.placeholder": "社区面向哪些人?你会为社区做什么?", "apply.motivation.description": "审核人员会在批准前阅读此内容。", diff --git a/ui/src/routes/_authenticated/_dashboard/-apply-node-fields.tsx b/ui/src/routes/_authenticated/_dashboard/-apply-node-fields.tsx index e7bf867c..32c65ee1 100644 --- a/ui/src/routes/_authenticated/_dashboard/-apply-node-fields.tsx +++ b/ui/src/routes/_authenticated/_dashboard/-apply-node-fields.tsx @@ -1,4 +1,3 @@ -import { Button } from "@/components/ui/button"; import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { @@ -41,7 +40,6 @@ export function ApplyNodeFields({ preflightLoading, organizationSlugAvailable, linkedOrganizationSlug, - onUnlockSlug, rootNodes, rootParentId, setRootParentId, @@ -57,7 +55,6 @@ export function ApplyNodeFields({ preflightLoading: boolean; organizationSlugAvailable?: boolean; linkedOrganizationSlug?: string; - onUnlockSlug?: () => void; rootNodes: NodeOption[]; rootParentId: string; setRootParentId: (value: string) => void; @@ -220,6 +217,24 @@ export function ApplyNodeFields({ const errors = field.state.meta.isTouched ? field.state.meta.errors : []; const available = preflight?.hostname.available; const organizationTaken = organizationSlugAvailable === false; + if (linkedSlug) { + return ( + 0 || undefined}> + {t("apply.slug.label")} +

+ {linkedSlug} + {gatewayId ? `.${gatewayId}` : ""} +

+ + {t("apply.slug.usesOrganization", { slug: linkedSlug })} + + +
+ ); + } return ( { slugManuallyEdited.current = true; @@ -254,32 +268,18 @@ export function ApplyNodeFields({ - {linkedSlug - ? t("apply.slug.usesOrganization", { slug: linkedSlug }) - : !hostname - ? t("apply.slug.rules") - : preflightLoading - ? t("apply.slug.checking") - : organizationTaken - ? t("apply.slug.organizationTaken") - : available === true - ? t("apply.slug.available", { hostname }) - : available === false - ? t("apply.slug.taken", { hostname }) - : hostname} + {!hostname + ? t("apply.slug.rules") + : preflightLoading + ? t("apply.slug.checking") + : organizationTaken + ? t("apply.slug.organizationTaken") + : available === true + ? t("apply.slug.available", { hostname }) + : available === false + ? t("apply.slug.taken", { hostname }) + : hostname} - {linkedSlug && onUnlockSlug && ( - - )} ); diff --git a/ui/src/routes/_authenticated/_dashboard/-apply-ui.test.tsx b/ui/src/routes/_authenticated/_dashboard/-apply-ui.test.tsx index 5a347cb9..84dd7d1d 100644 --- a/ui/src/routes/_authenticated/_dashboard/-apply-ui.test.tsx +++ b/ui/src/routes/_authenticated/_dashboard/-apply-ui.test.tsx @@ -53,6 +53,7 @@ const harness = vi.hoisted(() => ({ apiClient: null as ApplyApiClient | null, authClient: null as ApplyAuthClient | null, activeOrganizationId: "org-1" as string | null, + sessionActiveOrganizationId: "org-1" as string | null, daoConnection: { status: "connected", daoAccountId: "dao.sputnik" } as { status: string; daoAccountId: string | null; @@ -155,10 +156,13 @@ function createApiClient(): ApplyApiClient { function createAuthClient(): ApplyAuthClient { return { - getSession: vi.fn().mockResolvedValue({ - data: { user: { id: "user-1" }, session: { activeOrganizationId: "org-1" } }, + getSession: vi.fn(async () => ({ + data: { + user: { id: "user-1" }, + session: { activeOrganizationId: harness.sessionActiveOrganizationId }, + }, error: null, - }), + })), organization: { list: vi.fn().mockResolvedValue({ data: [{ id: "org-1", name: "CityNode" }], @@ -203,6 +207,10 @@ async function fillValidApplication() { }); } +function freeFormMode() { + harness.sessionActiveOrganizationId = null; +} + function stepStatus(id: string) { return screen.getByTestId(`apply.step-${id}`).getAttribute("data-status"); } @@ -212,6 +220,7 @@ beforeEach(() => { harness.apiClient = createApiClient(); harness.authClient = createAuthClient(); harness.activeOrganizationId = "org-1"; + harness.sessionActiveOrganizationId = "org-1"; harness.daoConnection = { status: "connected", daoAccountId: "dao.sputnik" }; harness.daoAccountIdForVerification = "dao.sputnik"; harness.nearAccount = "applicant.near"; @@ -232,6 +241,7 @@ afterEach(() => { describe("apply route submission", () => { it("submits the validated payload, shows the proposal id, and only invalidates proposal queries", async () => { + freeFormMode(); const queryClient = createQueryClient(); queryClient.setQueryData(proposalReviewQueryKeys.list("pending"), []); queryClient.setQueryData(proposalReviewQueryKeys.pendingCount(), 0); @@ -289,6 +299,7 @@ describe("apply route submission", () => { }); it("starts as a standalone city and requires a country for a state", async () => { + freeFormMode(); await renderApply(); fireEvent.click(screen.getByRole("button", { name: "verify DAO" })); await waitFor(() => expect(stepStatus("details")).toBe("current")); @@ -485,6 +496,7 @@ describe("apply route submission", () => { }); it("does not carry an earlier application's organization into a new application", async () => { + freeFormMode(); harness.apiClient?.proposals.getMyNodeApplications.mockResolvedValue({ data: [ { @@ -560,6 +572,7 @@ describe("apply route submission", () => { }); it("keeps the organization when revising a rejected legacy application", async () => { + freeFormMode(); harness.apiClient?.proposals.getMyNodeApplications.mockResolvedValue({ data: [ { @@ -612,34 +625,11 @@ describe("apply route submission", () => { }); } - it("links the applicant's own organization instead of reporting the address as taken", async () => { + it("reports the address as taken when a free-form application matches another organization", async () => { + freeFormMode(); malaysiaOrganization(); await renderApply(); await fillMalaysiaApplication(); - await waitFor(() => - expect(screen.getByTestId("apply.slug-status").textContent).toContain("@malaysia"), - ); - const submitButton = screen.getByRole("button", { name: "Submit for review" }); - await waitFor(() => expect(submitButton).toHaveProperty("disabled", false)); - fireEvent.click(submitButton); - - await waitFor(() => expect(harness.apiClient?.proposals.propose).toHaveBeenCalledOnce()); - expect(harness.apiClient?.proposals.propose.mock.calls[0]?.[0].payload).toMatchObject({ - slug: "malaysia", - orgId: "org-malaysia", - }); - }); - - it("still reports the address as taken when the matching organization has another owner", async () => { - malaysiaOrganization(); - harness.apiClient?.auth.listMembers.mockResolvedValue({ - members: [ - { userId: "user-9", role: "owner" }, - { userId: "user-1", role: "member" }, - ], - }); - await renderApply(); - await fillMalaysiaApplication(); await waitFor(() => expect(screen.getByTestId("apply.slug-status").textContent).toContain( "An organization already uses this address", @@ -651,6 +641,31 @@ describe("apply route submission", () => { expect(harness.apiClient?.proposals.propose).not.toHaveBeenCalled(); }); + it("prefills and locks the active organization without an ?org= parameter", async () => { + await renderApply(); + fireEvent.click(screen.getByRole("button", { name: "verify DAO" })); + await waitFor(() => + expect((screen.getByTestId("apply.name") as HTMLInputElement).value).toBe("CityNode"), + ); + expect(screen.getByTestId("apply.slug-locked").textContent).toBe("citynode.citynode.app"); + expect(screen.queryByTestId("apply.slug")).toBeNull(); + expect(screen.queryByTestId("apply.slug-unlock")).toBeNull(); + expect(harness.apiClient?.auth.checkSlug).not.toHaveBeenCalled(); + fireEvent.change(screen.getByLabelText("Why you want to run it"), { + target: { value: "Serve the local community." }, + }); + const submitButton = screen.getByRole("button", { name: "Submit for review" }); + await waitFor(() => expect(submitButton).toHaveProperty("disabled", false)); + fireEvent.click(submitButton); + + await waitFor(() => expect(harness.apiClient?.proposals.propose).toHaveBeenCalledOnce()); + expect(harness.apiClient?.proposals.propose.mock.calls[0]?.[0].payload).toMatchObject({ + name: "CityNode", + slug: "citynode", + orgId: "org-1", + }); + }); + it("prefills the organization name and locks the address when arriving with ?org=", async () => { harness.search = { org: "malaysia" }; malaysiaOrganization(); @@ -659,9 +674,10 @@ describe("apply route submission", () => { await waitFor(() => expect((screen.getByTestId("apply.name") as HTMLInputElement).value).toBe("Malaysia"), ); - const slugInput = screen.getByTestId("apply.slug") as HTMLInputElement; - expect(slugInput.value).toBe("malaysia"); - expect(slugInput.readOnly).toBe(true); + expect(screen.getByTestId("apply.slug-locked").textContent).toBe("malaysia.citynode.app"); + expect(screen.queryByTestId("apply.slug")).toBeNull(); + expect(screen.queryByTestId("apply.slug-unlock")).toBeNull(); + expect(harness.apiClient?.auth.checkSlug).not.toHaveBeenCalled(); fireEvent.change(screen.getByLabelText("Why you want to run it"), { target: { value: "Serve the local community." }, }); @@ -671,25 +687,58 @@ describe("apply route submission", () => { await waitFor(() => expect(harness.apiClient?.proposals.propose).toHaveBeenCalledOnce()); expect(harness.apiClient?.proposals.propose.mock.calls[0]?.[0].payload).toMatchObject({ + name: "Malaysia", slug: "malaysia", orgId: "org-malaysia", }); }); - it("releases the locked address when the applicant chooses a different one", async () => { + it("shows no unlock control once the web address is locked to the organization", async () => { harness.search = { org: "malaysia" }; malaysiaOrganization(); await renderApply(); fireEvent.click(screen.getByRole("button", { name: "verify DAO" })); - await waitFor(() => - expect((screen.getByTestId("apply.slug") as HTMLInputElement).readOnly).toBe(true), - ); - fireEvent.click(screen.getByTestId("apply.slug-unlock")); - await waitFor(() => - expect(screen.getByTestId("apply.slug-status").textContent).toContain( - "An organization already uses this address", - ), - ); + await waitFor(() => expect(screen.getByTestId("apply.slug-locked")).toBeTruthy()); + expect(screen.queryByTestId("apply.slug-unlock")).toBeNull(); + expect(harness.apiClient?.auth.checkSlug).not.toHaveBeenCalled(); + }); + + it("prompts for another organization when the active one already runs a community", async () => { + harness.apiClient?.auth.listOrganizations.mockResolvedValue([ + { + id: "org-1", + name: "CityNode", + slug: "citynode", + status: "active", + metadata: { communityProposalId: "proposal-1" }, + }, + ]); + await renderApply(); + fireEvent.click(screen.getByRole("button", { name: "verify DAO" })); + await waitFor(() => expect(screen.getByText("Choose an organization")).toBeTruthy()); + expect(screen.queryByTestId("apply.name")).toBeNull(); + expect(harness.apiClient?.proposals.propose).not.toHaveBeenCalled(); + }); + + it("prompts for another organization when the applicant is not the active organization's owner", async () => { + harness.apiClient?.auth.listMembers.mockResolvedValue({ + members: [ + { userId: "user-9", role: "owner" }, + { userId: "user-1", role: "member" }, + ], + }); + await renderApply(); + fireEvent.click(screen.getByRole("button", { name: "verify DAO" })); + await waitFor(() => expect(screen.getByText("Choose an organization")).toBeTruthy()); + expect(screen.queryByTestId("apply.name")).toBeNull(); + expect(harness.apiClient?.proposals.propose).not.toHaveBeenCalled(); + }); + + it("keeps the plain application form when there is no selected organization", async () => { + freeFormMode(); + await renderApply(); + fireEvent.click(screen.getByRole("button", { name: "verify DAO" })); + expect(await screen.findByTestId("apply.slug")).toBeTruthy(); }); it("starts with the NEAR account when there is no active organization", async () => { diff --git a/ui/src/routes/_authenticated/_dashboard/apply.tsx b/ui/src/routes/_authenticated/_dashboard/apply.tsx index aed8b64b..9ce5447e 100644 --- a/ui/src/routes/_authenticated/_dashboard/apply.tsx +++ b/ui/src/routes/_authenticated/_dashboard/apply.tsx @@ -1,10 +1,11 @@ +import { RocketLaunchIcon } from "@phosphor-icons/react"; import { useSelector } from "@tanstack/react-form"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, Link } from "@tanstack/react-router"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { getGatewayId, sessionQueryOptions, useApiClient, useAuthClient } from "@/app"; -import { PageContainer, PageHeader } from "@/components"; +import { EmptyState, PageContainer, PageHeader } from "@/components"; import { Button } from "@/components/ui/button"; import { FieldGroup } from "@/components/ui/field"; import { Spinner } from "@/components/ui/spinner"; @@ -12,6 +13,7 @@ import { AppActionError, appErrorMessage } from "@/i18n/error-message"; import { resolveAppLocale, translateAppMessage, useAppTranslation } from "@/i18n/runtime"; import { useDaoConnection } from "@/lib/dao-connect"; import { pageTitle } from "@/lib/page-title"; +import { isPersonalOrganization } from "@/lib/personal-organization"; import { childNodesQueryOptions, nodeByIdQueryOptions, @@ -91,14 +93,15 @@ function ApplyPage() { const [revising, setRevising] = useState(false); const [verifiedDaoAccountId, setVerifiedDaoAccountId] = useState(null); const [reopenedStep, setReopenedStep] = useState(null); - const [slugUnlocked, setSlugUnlocked] = useState(false); const handleDaoVerified = useCallback( ({ daoAccountId }: { daoAccountId: string }) => setVerifiedDaoAccountId(daoAccountId), [], ); const myApplications = useQuery(myNodeApplicationsQueryOptions(apiClient, nearAccountId ?? "")); - const { data: session } = useQuery(sessionQueryOptions(authClient)); - const { data: organizations } = useQuery(organizationsQueryOptions(apiClient)); + const { data: session, isPending: sessionPending } = useQuery(sessionQueryOptions(authClient)); + const { data: organizations, isPending: organizationsPending } = useQuery( + organizationsQueryOptions(apiClient), + ); const latestApplication = myApplications.data?.data.reduce< (typeof myApplications.data.data)[number] | undefined >( @@ -161,46 +164,52 @@ function ApplyPage() { const formValues = useSelector(form.store, (state) => state.values); const hostname = formValues.slug && gatewayId ? `${formValues.slug}.${gatewayId}` : ""; const legacyOrgId = retainedOrgId(formValues.slug); - const linkedOrgCandidate = (organizations ?? []).find( - (organization) => - organization.slug === formValues.slug && - organization.status !== "rejected" && - !organization.metadata?.communityProposalId, - ); - const { data: linkedMembers, isFetching: linkedMembersLoading } = useQuery({ - queryKey: ["apply-linked-org-members", linkedOrgCandidate?.id], + const activeOrgId = session?.session?.activeOrganizationId; + const requestedOrg = orgSlugParam + ? (organizations ?? []).find((organization) => organization.slug === orgSlugParam) + : undefined; + const activeOrg = activeOrgId + ? (organizations ?? []).find((organization) => organization.id === activeOrgId) + : undefined; + const candidateOrg = requestedOrg ?? activeOrg; + const candidateOrgEligible = + !!candidateOrg && + candidateOrg.status === "active" && + !candidateOrg.metadata?.communityProposalId && + !isPersonalOrganization(candidateOrg, session?.user?.id); + const { data: orgMembers, isFetching: orgMembersLoading } = useQuery({ + queryKey: ["apply-linked-org-members", candidateOrg?.id], queryFn: async () => { const { members } = await apiClient.auth.listMembers({ - organizationId: linkedOrgCandidate!.id, + organizationId: candidateOrg!.id, }); return members; }, - enabled: !!linkedOrgCandidate, + enabled: candidateOrgEligible, staleTime: 30 * 1000, }); - const linkedOrganization = - linkedOrgCandidate && - !slugUnlocked && - (linkedMembers ?? []).some( - (member) => member.userId === session?.user?.id && member.role === "owner", - ) - ? linkedOrgCandidate - : undefined; - useEffect(() => { - if (formValues.slug) return; - setSlugUnlocked(false); - }, [formValues.slug]); + const linkedOrganization = candidateOrgEligible + ? (orgMembers ?? []).some( + (member) => member.userId === session?.user?.id && member.role === "owner", + ) + ? candidateOrg + : undefined + : undefined; + const organizationEditMode = !!linkedOrganization && !revising; + const organizationPrompt = !!candidateOrg && !organizationEditMode && !revising; useEffect(() => { - if (!orgSlugParam || !organizations) return; - const requested = organizations.find((organization) => organization.slug === orgSlugParam); - if (!requested) return; - if (!form.getFieldValue("slug")) { - form.setFieldValue("slug", requested.slug, { dontUpdateMeta: true }); + if (!organizationEditMode || !linkedOrganization) return; + if (form.getFieldValue("slug") !== linkedOrganization.slug) { + form.setFieldValue("slug", linkedOrganization.slug, { dontUpdateMeta: true }); } if (!form.getFieldValue("name")) { - form.setFieldValue("name", requested.name, { dontUpdateMeta: true }); + form.setFieldValue("name", linkedOrganization.name, { dontUpdateMeta: true }); } - }, [form, orgSlugParam, organizations]); + }, [form, linkedOrganization, organizationEditMode]); + const resolutionPending = + organizationsPending || + sessionPending || + (candidateOrgEligible && orgMembers === undefined && orgMembersLoading); const { data: queriedRootNodes } = useQuery(rootNodesQueryOptions(apiClient)); const rootNodes = (queriedRootNodes ?? initialRootNodes).filter( @@ -220,13 +229,14 @@ function ApplyPage() { ...bindingPreflightQueryOptions(apiClient, hostname), enabled: !!hostname, }); + const freeFormApplication = !candidateOrg || revising; const { data: organizationSlug, isFetching: organizationSlugLoading } = useQuery({ queryKey: ["apply-organization-slug", formValues.slug], queryFn: () => apiClient.auth.checkSlug({ slug: formValues.slug }), - enabled: !!formValues.slug && !legacyOrgId && !linkedOrganization, + enabled: freeFormApplication && !!formValues.slug && !legacyOrgId, }); const organizationSlugAvailable = - !!legacyOrgId || !!linkedOrganization || organizationSlug?.status === true; + !!legacyOrgId || (!!linkedOrganization && !revising) || organizationSlug?.status === true; const canSubmit = canSubmitNodeApplication({ values: formValues, @@ -235,8 +245,7 @@ function ApplyPage() { submitterAccountId: nearAccountId, hostnameAvailable: preflight?.hostname.available === true, organizationSlugAvailable, - preflightLoading: - preflightLoading || organizationSlugLoading || (!!linkedOrgCandidate && linkedMembersLoading), + preflightLoading: preflightLoading || organizationSlugLoading, submitting: submitMutation.isPending, }); @@ -244,6 +253,8 @@ function ApplyPage() { !!daoConnection.daoAccountId && verifiedDaoAccountId === daoConnection.daoAccountId; const steps = resolveApplySteps({ near: !!nearAccountId, dao: daoVerified }, reopenedStep); + const routePending = (!!nearAccountId && myApplications.isPending) || resolutionPending; + if (nearAccountId && myApplications.isError && !submittedProposalId) { return ( @@ -255,7 +266,7 @@ function ApplyPage() { ); } - if (nearAccountId && myApplications.isPending && !submittedProposalId) { + if (routePending && !submittedProposalId) { return (
@@ -353,45 +364,53 @@ function ApplyPage() { status={steps.status("details")} last > -
{ - event.preventDefault(); - void form.handleSubmit(); - }} - > - - }> + {t("apply.organization.manage")} + + } + /> + ) : ( + { + event.preventDefault(); + void form.handleSubmit(); + }} + > + + setSlugUnlocked(true)} - rootNodes={rootNodes} - rootParentId={rootParentId} - setRootParentId={setRootParentId} - slugManuallyEdited={slugManuallyEdited} - stateNodes={stateNodes} - statesLoading={statesLoading} - /> - - - - + } + linkedOrganizationSlug={ + !revising && linkedOrganization ? linkedOrganization.slug : undefined + } + rootNodes={rootNodes} + rootParentId={rootParentId} + setRootParentId={setRootParentId} + slugManuallyEdited={slugManuallyEdited} + stateNodes={stateNodes} + statesLoading={statesLoading} + /> + +
+ + + )}