diff --git a/apps/builder/app/builder/features/topbar/add-domain.tsx b/apps/builder/app/builder/features/topbar/add-domain.tsx index 8a87a750e243..b8dcc66120b6 100644 --- a/apps/builder/app/builder/features/topbar/add-domain.tsx +++ b/apps/builder/app/builder/features/topbar/add-domain.tsx @@ -7,46 +7,38 @@ import { theme, Text, Grid, + toast, } from "@webstudio-is/design-system"; import { validateDomain } from "@webstudio-is/domain"; import type { Project } from "@webstudio-is/project"; -import { useId, useState } from "react"; +import { useId, useOptimistic, useRef, useState } from "react"; import { CustomCodeIcon } from "@webstudio-is/icons"; -import { trpcClient } from "~/shared/trpc/trpc-client"; +import { nativeClient } from "~/shared/trpc/trpc-client"; type DomainsAddProps = { projectId: Project["id"]; onCreate: (domain: string) => void; onExportClick: () => void; - refreshDomainResult: ( - input: { projectId: Project["id"] }, - onSuccess: () => void - ) => void; - domainState: "idle" | "submitting"; - isPublishing: boolean; + refresh: () => Promise; }; export const AddDomain = ({ projectId, onCreate, - refreshDomainResult, - domainState, - isPublishing, + refresh, onExportClick, }: DomainsAddProps) => { const id = useId(); - const { - send: create, - state: сreateState, - error: сreateSystemError, - } = trpcClient.domain.create.useMutation(); const [isOpen, setIsOpen] = useState(false); - const [domain, setDomain] = useState(""); const [error, setError] = useState(); + const buttonRef = useRef(null); + const [isPending, setIsPendingOptimistic] = useOptimistic(false); - const handleCreate = () => { - setError(undefined); + const handleCreateDomain = async (formData: FormData) => { + // Will be automatically reset on action end + setIsPendingOptimistic(true); + const domain = formData.get("domain")?.toString() ?? ""; const validationResult = validateDomain(domain); if (validationResult.success === false) { @@ -54,18 +46,22 @@ export const AddDomain = ({ return; } - create({ domain: validationResult.domain, projectId }, (data) => { - if (data.success === false) { - setError(data.error); - return; - } - - refreshDomainResult({ projectId }, () => { - setDomain(""); - setIsOpen(false); - onCreate(validationResult.domain); - }); + const result = await nativeClient.domain.create.mutate({ + domain, + projectId, }); + + if (result.success === false) { + toast.error(result.error); + setError(result.error); + return; + } + + onCreate(domain); + + await refresh(); + + setIsOpen(false); }; return ( @@ -81,7 +77,6 @@ export const AddDomain = ({ direction={"column"} onKeyDown={(event) => { if (event.key === "Escape") { - setDomain(""); setIsOpen(false); event.preventDefault(); } @@ -94,26 +89,21 @@ export const AddDomain = ({ { if (event.key === "Enter") { - handleCreate(); + buttonRef.current + ?.closest("form") + ?.requestSubmit(buttonRef.current); } if (event.key === "Escape") { - setDomain(""); setIsOpen(false); event.preventDefault(); } }} - onChange={(event) => { - setError(undefined); - setDomain(event.target.value); - }} color={error !== undefined ? "error" : undefined} /> {error !== undefined && ( @@ -121,29 +111,21 @@ export const AddDomain = ({ {error} )} - {сreateSystemError !== undefined && ( - <> - {/* Something happened with network, api etc */} - {сreateSystemError} - Please try again later - - )} )} @@ -425,40 +392,28 @@ const DomainItem = (props: { {status !== "UNVERIFIED" && ( <> - {updateStatusData?.success === false && ( - {updateStatusData.error} + {updateStatusError && ( + {updateStatusError} )} {updateStatusError !== undefined && ( {updateStatusError} )} )} - {removeData?.success === false && ( - {removeData.error} - )} - - {removeSystemError !== undefined && ( - {removeSystemError} - )} - @@ -467,11 +422,11 @@ const DomainItem = (props: { {status === "UNVERIFIED" && ( <> - {verifyData?.success === false ? ( + {verifyError ? ( Status: Failed to verify
- {verifyData.error} + {verifyError}
) : ( <> @@ -558,16 +513,20 @@ const DomainItem = (props: { { // Sometimes Entri modal dialog hangs even if it's successful, // until they fix that, we'll just refresh the status here on every onClose event if (status === "UNVERIFIED") { - handleVerify(); + startTransition(async () => { + await handleVerify(); + await handleUpdateStatus(); + }); return; } - - handleUpdateStatus(); + startTransition(async () => { + await handleUpdateStatus(); + }); }} isPublishing={props.isPublishing} /> @@ -579,19 +538,14 @@ const DomainItem = (props: { type DomainsProps = { newDomains: Set; domains: Domain[]; - refreshDomainResult: ( - input: { projectId: Project["id"] }, - onSuccess?: () => void - ) => void; - domainState: "idle" | "submitting"; + refresh: () => Promise; isPublishing: boolean; }; export const Domains = ({ newDomains, domains, - refreshDomainResult, - domainState, + refresh, isPublishing, }: DomainsProps) => { return ( @@ -601,8 +555,7 @@ export const Domains = ({ key={projectDomain.domain} projectDomain={projectDomain} initiallyOpen={newDomains.has(projectDomain.domain)} - refreshDomainResult={refreshDomainResult} - domainState={domainState} + refresh={refresh} isPublishing={isPublishing} /> ))} diff --git a/apps/builder/app/builder/features/topbar/publish.tsx b/apps/builder/app/builder/features/topbar/publish.tsx index 2003a16f779f..3190aaf8515c 100644 --- a/apps/builder/app/builder/features/topbar/publish.tsx +++ b/apps/builder/app/builder/features/topbar/publish.tsx @@ -1,5 +1,4 @@ import { - useCallback, useEffect, useMemo, useState, @@ -24,7 +23,6 @@ import { InputField, Separator, ScrollArea, - Box, rawTheme, Select, theme, @@ -62,24 +60,11 @@ import { isFeatureEnabled } from "@webstudio-is/feature-flags"; import type { Templates } from "@webstudio-is/sdk"; import { formatDistance } from "date-fns/formatDistance"; -type ProjectData = - | { - success: true; - project: Project; - } - | { - success: false; - error: string; - }; - type ChangeProjectDomainProps = { project: Project; projectState: "idle" | "submitting"; isPublishing: boolean; - projectLoad: ( - props: { projectId: Project["id"] }, - callback: (projectData: ProjectData) => void - ) => void; + projectLoad: () => void; }; type TimeoutId = undefined | ReturnType; @@ -102,32 +87,6 @@ const ChangeProjectDomain = ({ const [domain, setDomain] = useState(project.domain); const [error, setError] = useState(); - const refreshProject = useCallback( - () => - projectLoad({ projectId: project.id }, (projectData) => { - if (projectData?.success === false) { - setError(projectData.error); - return; - } - - const currenProject = $project.get(); - - if (currenProject?.id === projectData.project.id) { - $project.set({ - ...currenProject, - domain: projectData.project.domain, - }); - } - - setDomain(projectData.project.domain); - }), - [projectLoad, project.id] - ); - - useEffect(() => { - refreshProject(); - }, [refreshProject]); - const handleUpdateProjectDomain = () => { const validationResult = validateProjectDomain(domain); @@ -149,7 +108,7 @@ const ChangeProjectDomain = ({ return; } - refreshProject(); + projectLoad(); }); }; @@ -537,6 +496,7 @@ const PublishStatic = ({ ); }; +/* const ErrorText = ({ children }: { children: string }) => ( ( Please try again later ); +*/ const useCanAddDomain = () => { const { load, data } = trpcClient.domain.countTotalDomains.useQuery(); @@ -569,38 +530,45 @@ const useCanAddDomain = () => { return { canAddDomain, maxDomainsAllowedPerUser }; }; +const refreshProject = async () => { + const result = await nativeClient.domain.project.query({ + projectId: $project.get()!.id, + }); + + if (result.success) { + $project.set(result.project); + return; + } + + toast.error(result.error); +}; + const Content = (props: { projectId: Project["id"]; onExportClick: () => void; }) => { const [newDomains, setNewDomains] = useState(new Set()); - const { - load: projectLoad, - data: projectData, - state: projectState, - error: projectSystemError, - } = trpcClient.domain.project.useQuery(); + const project = useStore($project); - useEffect(() => { - projectLoad({ projectId: props.projectId }); - }, [props.projectId, projectLoad]); + if (project == null) { + throw new Error("Project not found"); + } + const projectState = "idle"; - projectData?.success; + const handleLoadProject = () => { + refreshProject(); + }; const domainsToPublish = useMemo( () => - projectData?.success - ? projectData.project.domainsVirtual.filter( - (projectDomain) => getStatus(projectDomain) === "VERIFIED_ACTIVE" - ) - : [], - [projectData] + project.domainsVirtual.filter( + (projectDomain) => getStatus(projectDomain) === "VERIFIED_ACTIVE" + ), + [project.domainsVirtual] ); - const latestBuildVirtual = projectData?.success - ? (projectData.project.latestBuildVirtual ?? undefined) - : undefined; + const latestBuildVirtual = project.latestBuildVirtual; const hasPendingState = latestBuildVirtual ? getPublishStatusAndTextNew(latestBuildVirtual).status === "PENDING" @@ -615,7 +583,7 @@ const Content = (props: { const { canAddDomain, maxDomainsAllowedPerUser } = useCanAddDomain(); return ( - <> +
{canAddDomain === false && ( @@ -639,59 +607,44 @@ const Content = (props: { )} - {projectSystemError !== undefined && ( - {projectSystemError} - )} - {projectData?.success && ( - - )} + - {projectData?.success && ( - - )} + + { setNewDomains((prev) => { return new Set([...prev, domain]); }); }} - isPublishing={isPublishing} onExportClick={props.onExportClick} /> - {projectData?.success === true && ( - { - projectLoad({ projectId: props.projectId }); - }} - isPublishing={isPublishing} - setIsPublishing={setIsPublishing} - /> - )} - {projectData?.success !== true && ( - - )} - + + + ); }; diff --git a/packages/prisma-client/prisma/migrations/20240920091253_domain-ordering/migration.sql b/packages/prisma-client/prisma/migrations/20240920091253_domain-ordering/migration.sql new file mode 100644 index 000000000000..78ead59d2f29 --- /dev/null +++ b/packages/prisma-client/prisma/migrations/20240920091253_domain-ordering/migration.sql @@ -0,0 +1,34 @@ +-- Create the "domainsVirtual" function to return all domain-related data for a specific project. +CREATE OR REPLACE FUNCTION "domainsVirtual"("Project") +RETURNS SETOF "domainsVirtual" AS $$ + -- This function retrieves all the domain information associated with a specific project by joining the Domain and ProjectDomain tables. + -- It returns a result set conforming to the "domainsVirtual" structure, including fields such as domain status, error, verification status, etc. + SELECT + "Domain".id || '-' || "ProjectDomain"."projectId" as id, + "Domain".id AS "domainId", -- Domain ID from Domain table + "ProjectDomain"."projectId", -- Project ID from ProjectDomain table + "Domain".domain, -- Domain name + "Domain".status, -- Current domain status + "Domain".error, -- Error message, if any + "Domain"."txtRecord" AS "domainTxtRecord", -- Current TXT record from Domain table + "ProjectDomain"."txtRecord" AS "expectedTxtRecord", -- Expected TXT record from ProjectDomain table + "ProjectDomain"."cname" AS "cname", + CASE + WHEN "Domain"."txtRecord" = "ProjectDomain"."txtRecord" THEN true -- If TXT records match, domain is verified + ELSE false + END AS "verified", -- Boolean flag for verification status + "ProjectDomain"."createdAt", -- Creation timestamp from ProjectDomain table + "Domain"."updatedAt" -- Last updated timestamp from Domain table + FROM + "Domain" + JOIN + "ProjectDomain" ON "Domain".id = "ProjectDomain"."domainId" -- Joining Domain and ProjectDomain on domainId + WHERE + "ProjectDomain"."projectId" = $1.id -- Filtering by projectId passed as an argument to the function + ORDER BY "ProjectDomain"."createdAt", "Domain".id; -- Stable sort +$$ +STABLE +LANGUAGE sql; + +-- Add function-specific comments to explain its behavior. +COMMENT ON FUNCTION "domainsVirtual"("Project") IS 'Function that retrieves domain-related data for a given project by joining the Domain and ProjectDomain tables. It returns a result set that conforms to the structure defined in the domainsVirtual virtual table.';