Skip to content

Commit 6ba6fc5

Browse files
authored
feat: Preview by publishing to subdomain only aka staging (#4143)
## Description closes #2644 - [x] - Merge this #4156 into this PR - If the user has no custom domains: - [x] Checkbox for Pro users is not shown. - [x] Checkbox for Free users is shown but disabled (Tooltip enabled). - If the user has custom domains: - No Pro: - [x] Checkboxes are shown disabled with Tooltips. - Pro: - Users can select any checkbox (no Tooltips). - Current indexing logic: - If a user has custom domain(s), upon publishing, the `*.wstd.io` domain will receive the meta tag: ```html <meta name="robots" content="noindex, nofollow"> ``` - [x] - Fix example.com in tests (NEXT PR #4156) - [ ] - saas cleanup should be changed (we need to preserve latest build by domain now) (NEXT PR) - [x] - Disable Publish Button if no checkboxes are selected - "With the Pro, you’ll gain the ability to publish to each domain independently!" <img width="477" alt="image" src="https://github.com/user-attachments/assets/105577e5-318f-4146-a3eb-38c2117f6f79"> ## Steps for reproduction ### Non Pro - Login as a new non Pro User `pass:[email protected]` - Create a new Project, Click Publish. Expect Disabled Checkbox with Tolltip is visible. Publish is Enabled. <img width="445" alt="image" src="https://github.com/user-attachments/assets/e48654af-1969-4f96-b048-a3fa72303b8f"> - Publish, check it published. - Add custom domain, Publish, check both are published. ### Pro - Login as a Pro User `pass` - Open new Project. Expect No checkbox. One wstd domain. <img width="351" alt="image" src="https://github.com/user-attachments/assets/c62f9ae5-669a-46ad-a2f2-32ba333068f9"> - Publish. Expect it published. - Add domain. See checkboxes visible after domain activated. - Publish. Expect both published. - Uncheck checkboxes, see publish button disabled. See it has tooltip. ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent 0ff51eb commit 6ba6fc5

File tree

20 files changed

+1109
-672
lines changed

20 files changed

+1109
-672
lines changed

apps/builder/app/builder/features/topbar/add-domain.tsx

Lines changed: 37 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,65 +7,61 @@ import {
77
theme,
88
Text,
99
Grid,
10+
toast,
1011
} from "@webstudio-is/design-system";
1112
import { validateDomain } from "@webstudio-is/domain";
1213
import type { Project } from "@webstudio-is/project";
13-
import { useId, useState } from "react";
14+
import { useId, useOptimistic, useRef, useState } from "react";
1415
import { CustomCodeIcon } from "@webstudio-is/icons";
15-
import { trpcClient } from "~/shared/trpc/trpc-client";
16+
import { nativeClient } from "~/shared/trpc/trpc-client";
1617

1718
type DomainsAddProps = {
1819
projectId: Project["id"];
1920
onCreate: (domain: string) => void;
2021
onExportClick: () => void;
21-
refreshDomainResult: (
22-
input: { projectId: Project["id"] },
23-
onSuccess: () => void
24-
) => void;
25-
domainState: "idle" | "submitting";
26-
isPublishing: boolean;
22+
refresh: () => Promise<void>;
2723
};
2824

2925
export const AddDomain = ({
3026
projectId,
3127
onCreate,
32-
refreshDomainResult,
33-
domainState,
34-
isPublishing,
28+
refresh,
3529
onExportClick,
3630
}: DomainsAddProps) => {
3731
const id = useId();
38-
const {
39-
send: create,
40-
state: сreateState,
41-
error: сreateSystemError,
42-
} = trpcClient.domain.create.useMutation();
4332
const [isOpen, setIsOpen] = useState(false);
44-
const [domain, setDomain] = useState("");
4533
const [error, setError] = useState<string>();
34+
const buttonRef = useRef<HTMLButtonElement>(null);
35+
const [isPending, setIsPendingOptimistic] = useOptimistic(false);
4636

47-
const handleCreate = () => {
48-
setError(undefined);
37+
const handleCreateDomain = async (formData: FormData) => {
38+
// Will be automatically reset on action end
39+
setIsPendingOptimistic(true);
4940

41+
const domain = formData.get("domain")?.toString() ?? "";
5042
const validationResult = validateDomain(domain);
5143

5244
if (validationResult.success === false) {
5345
setError(validationResult.error);
5446
return;
5547
}
5648

57-
create({ domain: validationResult.domain, projectId }, (data) => {
58-
if (data.success === false) {
59-
setError(data.error);
60-
return;
61-
}
62-
63-
refreshDomainResult({ projectId }, () => {
64-
setDomain("");
65-
setIsOpen(false);
66-
onCreate(validationResult.domain);
67-
});
49+
const result = await nativeClient.domain.create.mutate({
50+
domain,
51+
projectId,
6852
});
53+
54+
if (result.success === false) {
55+
toast.error(result.error);
56+
setError(result.error);
57+
return;
58+
}
59+
60+
onCreate(domain);
61+
62+
await refresh();
63+
64+
setIsOpen(false);
6965
};
7066

7167
return (
@@ -81,7 +77,6 @@ export const AddDomain = ({
8177
direction={"column"}
8278
onKeyDown={(event) => {
8379
if (event.key === "Escape") {
84-
setDomain("");
8580
setIsOpen(false);
8681
event.preventDefault();
8782
}
@@ -94,56 +89,43 @@ export const AddDomain = ({
9489
</Label>
9590
<InputField
9691
id={id}
92+
name="domain"
9793
autoFocus
9894
placeholder="your-domain.com"
99-
value={domain}
100-
disabled={
101-
isPublishing || сreateState !== "idle" || domainState !== "idle"
102-
}
95+
disabled={isPending}
10396
onKeyDown={(event) => {
10497
if (event.key === "Enter") {
105-
handleCreate();
98+
buttonRef.current
99+
?.closest("form")
100+
?.requestSubmit(buttonRef.current);
106101
}
107102
if (event.key === "Escape") {
108-
setDomain("");
109103
setIsOpen(false);
110104
event.preventDefault();
111105
}
112106
}}
113-
onChange={(event) => {
114-
setError(undefined);
115-
setDomain(event.target.value);
116-
}}
117107
color={error !== undefined ? "error" : undefined}
118108
/>
119109
{error !== undefined && (
120110
<>
121111
<Text color="destructive">{error}</Text>
122112
</>
123113
)}
124-
{сreateSystemError !== undefined && (
125-
<>
126-
{/* Something happened with network, api etc */}
127-
<Text color="destructive">{сreateSystemError}</Text>
128-
<Text color="subtle">Please try again later</Text>
129-
</>
130-
)}
131114
</>
132115
)}
133116

134117
<Grid gap={2} columns={2}>
135118
<Button
136-
disabled={
137-
isPublishing || сreateState !== "idle" || domainState !== "idle"
138-
}
119+
ref={buttonRef}
120+
formAction={handleCreateDomain}
121+
state={isPending ? "pending" : undefined}
139122
color={isOpen ? "primary" : "neutral"}
140-
onClick={() => {
123+
onClick={(event) => {
141124
if (isOpen === false) {
142125
setIsOpen(true);
126+
event.preventDefault();
143127
return;
144128
}
145-
146-
handleCreate();
147129
}}
148130
>
149131
{isOpen ? "Add domain" : "Add a new domain"}
@@ -152,6 +134,7 @@ export const AddDomain = ({
152134
<Button
153135
color={"dark"}
154136
prefix={<CustomCodeIcon />}
137+
type="button"
155138
onClick={onExportClick}
156139
>
157140
Export

apps/builder/app/builder/features/topbar/collapsible-domain-section.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Label,
55
theme,
66
SectionTitle,
7+
Grid,
78
} from "@webstudio-is/design-system";
89
import { useEffect, useRef, useState, type ReactNode } from "react";
910
import { CollapsibleSectionRoot } from "~/builder/shared/collapsible-section";
@@ -13,9 +14,11 @@ export const CollapsibleDomainSection = ({
1314
children,
1415
title,
1516
suffix,
17+
prefix,
1618
}: {
1719
initiallyOpen?: boolean;
1820
children: ReactNode;
21+
prefix: ReactNode;
1922
suffix: ReactNode;
2023
title: string;
2124
}) => {
@@ -53,7 +56,10 @@ export const CollapsibleDomainSection = ({
5356
</Box>
5457
}
5558
>
56-
<Label truncate>{title}</Label>
59+
<Grid flow={"column"} align="center" justify={"start"}>
60+
{prefix}
61+
<Label truncate>{title}</Label>
62+
</Grid>
5763
</SectionTitle>
5864
}
5965
>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { useStore } from "@nanostores/react";
2+
import {
3+
Flex,
4+
Tooltip,
5+
Text,
6+
theme,
7+
Link,
8+
buttonStyle,
9+
Checkbox,
10+
} from "@webstudio-is/design-system";
11+
import { $userPlanFeatures } from "~/builder/shared/nano-states";
12+
import { $project } from "~/shared/nano-states";
13+
14+
export const domainToPublishName = "domainToPublish[]";
15+
16+
interface DomainCheckboxProps {
17+
defaultChecked?: boolean;
18+
domain: string;
19+
buildId: string | undefined;
20+
disabled?: boolean;
21+
}
22+
23+
const DomainCheckbox = (props: DomainCheckboxProps) => {
24+
const hasProPlan = useStore($userPlanFeatures).hasProPlan;
25+
const project = useStore($project);
26+
27+
if (project === undefined) {
28+
return;
29+
}
30+
31+
const tooltipContentForFreeUsers = hasProPlan ? undefined : (
32+
<Flex direction="column" gap="2" css={{ maxWidth: theme.spacing[28] }}>
33+
<Text variant="titles">Publish to Staging</Text>
34+
<Text>
35+
<Flex direction="column">
36+
Staging allows you to preview a production version of your site
37+
without potentially breaking what production site visitors will see.
38+
<>
39+
<br />
40+
<br />
41+
Upgrade to Pro account to publish to each domain individually.
42+
<br /> <br />
43+
<Link
44+
className={buttonStyle({ color: "gradient" })}
45+
color="contrast"
46+
underline="none"
47+
href="https://webstudio.is/pricing"
48+
target="_blank"
49+
>
50+
Upgrade
51+
</Link>
52+
</>
53+
</Flex>
54+
</Text>
55+
</Flex>
56+
);
57+
58+
const defaultChecked = hasProPlan ? props.defaultChecked : true;
59+
const disabled = hasProPlan ? props.disabled : true;
60+
61+
const hideDomainCheckbox =
62+
project.domainsVirtual.filter(
63+
(domain) => domain.status === "ACTIVE" && domain.verified
64+
).length === 0 && hasProPlan;
65+
66+
return (
67+
<div style={{ display: hideDomainCheckbox ? "none" : "contents" }}>
68+
<Tooltip content={tooltipContentForFreeUsers} variant="wrapped">
69+
<Checkbox
70+
disabled={disabled}
71+
key={props.buildId ?? "-"}
72+
defaultChecked={hideDomainCheckbox || defaultChecked}
73+
css={{ pointerEvents: "all" }}
74+
name={domainToPublishName}
75+
value={props.domain}
76+
/>
77+
</Tooltip>
78+
</div>
79+
);
80+
};
81+
82+
export default DomainCheckbox;

0 commit comments

Comments
 (0)