From 7342f3efd84c3ebd4c985bc1316df7a33f86ad20 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 29 Oct 2026 14:59:57 -0500 Subject: [PATCH] fix(apply): start community applications from the selected organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /apply now resolves the applying organization up front — the ?org= parameter first, otherwise the session's active organization — instead of reactively linking whatever the user happens to type into the web address field. An eligible org (active, not personal, no community bound, applicant is owner) prefills the name, locks the submission to its slug, submits the payload with orgId, and renders the resulting web address as static text below the name instead of an input, so the "An organization already uses this address" error can no longer appear for the applicant's own organization. An ineligible selection prompts the applicant to pick another organization rather than falling back to a confusing free-form slug field; applicants with no organization keep the plain form and its availability checks. --- ui/src/i18n/catalogs.ts | 12 +- .../_dashboard/-apply-node-fields.tsx | 60 +++--- .../_dashboard/-apply-ui.test.tsx | 131 ++++++++----- .../_authenticated/_dashboard/apply.tsx | 173 ++++++++++-------- 4 files changed, 224 insertions(+), 152 deletions(-) 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} + /> + +
+ + + )}