Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/builder/app/builder/features/pages/page-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,8 @@ const PathField = ({
<br />
<Text>
To make the path dynamic and use it with CMS, you can use
parameters and other features. CMS features are part of the
Pro plan.
parameters and other features. You can publish to staging
for free; upgrade to Pro to publish to custom domains.
</Text>
<Link
className={buttonStyle({ color: "gradient" })}
Expand Down Expand Up @@ -855,8 +855,9 @@ const FormFields = ({
{allowDynamicData === false && (
<PanelBanner>
<Text>
Dynamic routing and redirect are a part of the CMS
functionality.
Dynamic routing and redirect are part of the CMS functionality.
You can publish to staging for free; upgrade to Pro to publish
to custom domains.
</Text>
<Flex align="center" gap={1}>
<UploadIcon />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,9 @@ export const UpgradeBanners = () => (
<li>Redirect</li>
<li>Custom contact email</li>
</Text>
<Text>You can delete these features or upgrade.</Text>
<Text>
You can delete these features or upgrade to publish to custom domains.
</Text>
<Flex align="center" gap={1}>
<UpgradeIcon />
<Link color="inherit" href="#">
Expand Down
55 changes: 43 additions & 12 deletions apps/builder/app/builder/features/publish/publish.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,19 +401,28 @@ const Publish = ({
timesLeft,
disabled,
refresh,
restrictedFeatures,
}: {
project: Project;
timesLeft: number;
disabled: boolean;
refresh: () => Promise<void>;
restrictedFeatures: Map<
string,
| undefined
| { awareness?: Awareness; view?: "pageSettings"; info?: ReactNode }
>;
}) => {
const { maxDailyPublishesPerUser } = useStore($permissions);
const { userPublishCount } = useUserPublishCount();
const [publishError, setPublishError] = useState<
undefined | JSX.Element | string
>();
const [isPublishing, setIsPublishing] = useOptimistic(false);
const buttonRef = useRef<HTMLButtonElement>(null);
const [hasSelectedDomains, setHasSelectedDomains] = useState(false);
const [hasCustomDomainsSelected, setHasCustomDomainsSelected] =
useState(false);
const countdown = usePublishCountdown(isPublishing);

useEffect(() => {
Expand All @@ -425,8 +434,18 @@ const Publish = ({

const handleFormInput = () => {
const formData = new FormData(form);
const domainsSelected = formData.getAll(domainToPublishName).length;
setHasSelectedDomains(domainsSelected > 0);
const domainsSelected = formData
.getAll(domainToPublishName)
.map((domain) => domain.toString());

setHasSelectedDomains(domainsSelected.length > 0);

// Check if any custom domains are selected
// Custom domains are those that are NOT the staging domain (project.domain)
const hasCustom = domainsSelected.some(
(domain) => domain !== project.domain
);
setHasCustomDomainsSelected(hasCustom);
};

const observer = new MutationObserver(() => {
Expand All @@ -445,7 +464,7 @@ const Publish = ({
return () => {
observer.disconnect();
};
}, []);
}, [project.domain]);

const handlePublish = async (formData: FormData) => {
setPublishError(undefined);
Expand Down Expand Up @@ -582,7 +601,12 @@ const Publish = ({
formAction={handlePublish}
color="positive"
state={showPendingState ? "pending" : undefined}
disabled={hasSelectedDomains === false || disabled}
disabled={
hasSelectedDomains === false ||
disabled ||
(restrictedFeatures.size > 0 && hasCustomDomainsSelected) ||
userPublishCount >= maxDailyPublishesPerUser
}
>
{countdown !== undefined && countdown > 0
? `Publishing (${countdown}s)`
Expand Down Expand Up @@ -789,7 +813,7 @@ const buttonLinkClass = css({
...textVariants.link,
}).toString();

const UpgradeBanner = () => {
const UpgradeBanner = ({ hasCustomDomains }: { hasCustomDomains: boolean }) => {
const restrictedFeatures = useStore($restrictedFeatures);
const { canAddDomain } = useCanAddDomain();
const { userPublishCount, maxDailyPublishesPerUser } = useUserPublishCount();
Expand All @@ -812,7 +836,9 @@ const UpgradeBanner = () => {
);
}

if (restrictedFeatures.size > 0) {
// Only show Pro feature upgrade banner if custom domains are available
// Free tier users can still publish to staging domain with Pro features
if (restrictedFeatures.size > 0 && hasCustomDomains) {
return (
<PanelBanner>
<img
Expand Down Expand Up @@ -854,7 +880,9 @@ const UpgradeBanner = () => {
)
)}
</Text>
<Text>You can delete these features or upgrade.</Text>
<Text>
You can delete these features or upgrade to publish to custom domains.
</Text>
<Flex align="center" gap={1}>
<UpgradeIcon />
<Link
Expand Down Expand Up @@ -915,6 +943,11 @@ const Content = (props: {
domain.latestBuildVirtual == null
);

// Check if any custom domains exist (active and verified)
const hasCustomDomains = project.domainsVirtual.some(
(domain) => domain.status === "ACTIVE" && domain.verified
);

return (
<form>
<ScrollArea>
Expand Down Expand Up @@ -947,7 +980,7 @@ const Content = (props: {
}}
onExportClick={props.onExportClick}
/>
<UpgradeBanner />
<UpgradeBanner hasCustomDomains={hasCustomDomains} />
{hasUnpublishedDomains && (
<PanelBanner>
<Flex align="center" gap="1">
Expand All @@ -964,10 +997,8 @@ const Content = (props: {
project={project}
refresh={refreshProject}
timesLeft={maxDailyPublishesPerUser - userPublishCount}
disabled={
restrictedFeatures.size > 0 ||
userPublishCount >= maxDailyPublishesPerUser
}
disabled={false}
restrictedFeatures={restrictedFeatures}
/>
</Flex>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ export const SettingsPanel = ({
width={rawTheme.spacing[28]}
style={{ aspectRatio: "4.1" }}
/>
<Text variant="regularBold">Upgrade for CMS</Text>
<Text variant="regularBold">Upgrade for CMS on custom domains</Text>
<Text>
Integrate content from other tools to create blogs, directories, and
any other structured content.
any other structured content. You can preview CMS on staging without
upgrading.
</Text>
<Flex align="center" gap={1}>
<UpgradeIcon />
Expand Down
32 changes: 9 additions & 23 deletions apps/builder/app/dashboard/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,24 @@ const { getView } = __testing__;

describe("getView", () => {
test("returns 'search' for search path", () => {
expect(getView("/dashboard/search", true, true)).toBe("search");
expect(getView("/dashboard/search", false, true)).toBe("search");
expect(getView("/dashboard/search", true, false)).toBe("search");
expect(getView("/dashboard/search", false, false)).toBe("search");
});

test("returns 'welcome' when no projects on default workspace", () => {
expect(getView("/dashboard", false, true)).toBe("welcome");
expect(getView("/dashboard/templates", false, true)).toBe("welcome");
test("returns 'welcome' when no projects and workspace not suspended", () => {
expect(getView("/dashboard", false, false)).toBe("welcome");
});

test("returns 'projects' when no projects on non-default workspace", () => {
// Non-default workspaces should never show the welcome onboarding page
expect(getView("/dashboard", false, false)).toBe("projects");
test("returns 'projects' when workspace is suspended with no projects", () => {
expect(getView("/dashboard", false, true)).toBe("projects");
});

test("returns 'templates' on non-default workspace with no projects", () => {
expect(getView("/dashboard/templates", false, false)).toBe("templates");
});

test("returns 'templates' for templates path with projects", () => {
expect(getView("/dashboard/templates", true, true)).toBe("templates");
});

test("returns 'projects' for dashboard root with projects", () => {
test("returns 'projects' when there are projects", () => {
expect(getView("/dashboard", true, false)).toBe("projects");
expect(getView("/dashboard", true, true)).toBe("projects");
});

test("returns 'projects' for unknown paths with projects", () => {
expect(getView("/dashboard/unknown", true, true)).toBe("projects");
});

test("search takes priority over welcome", () => {
// Even with no projects, search view should show
expect(getView("/dashboard/search", false, true)).toBe("search");
expect(getView("/dashboard/unknown", true, false)).toBe("projects");
});
});
85 changes: 37 additions & 48 deletions apps/builder/app/dashboard/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
Grid,
IconButton,
} from "@webstudio-is/design-system";
import { BodyIcon, ExtensionIcon } from "@webstudio-is/icons";
import { BodyIcon } from "@webstudio-is/icons";
import {
NavLink,
useLocation,
Expand All @@ -33,10 +33,9 @@ import { dashboardPath } from "~/shared/router-utils";
import { CollapsibleSection } from "~/builder/shared/collapsible-section";
import { ProfileMenu } from "./profile-menu";
import { Projects } from "./projects/projects";
import { Templates } from "./templates/templates";
import { Welcome } from "./welcome/welcome";
import { Header } from "./shared/layout";
import { help } from "~/shared/help";
import { help, socialLinks } from "~/shared/help";
import { SearchResults } from "./search/search-results";
import type { DashboardData } from "./shared/types";
import { Search } from "./search/search-field";
Expand Down Expand Up @@ -194,22 +193,16 @@ export const DashboardSetup = ({ data }: { data: DashboardData }) => {
const getView = (
pathname: string,
hasProjects: boolean,
isDefaultWorkspace: boolean
isWorkspaceSuspended: boolean
) => {
if (pathname === dashboardPath("search")) {
return "search";
}

// Only show the onboarding welcome page on the default workspace
// when the user has no projects yet. Non-default workspaces that are
// empty should show the normal (empty) projects view.
if (hasProjects === false && isDefaultWorkspace) {
if (hasProjects === false && isWorkspaceSuspended === false) {
return "welcome";
}

if (pathname === dashboardPath("templates")) {
return "templates";
}
return "projects";
};

Expand All @@ -229,7 +222,6 @@ export const Dashboard = () => {
publisherHost,
projectToClone,
projects,
templates,
workspaces,
currentWorkspaceId,
} = data;
Expand All @@ -243,38 +235,21 @@ export const Dashboard = () => {
}

const isWorkspaceSuspended = isDowngradedForMember(currentWorkspace);
const hasProjects = projects.length > 0 || isWorkspaceSuspended;
const isDefaultWorkspace =
currentWorkspaceId === undefined ||
workspaces?.find((w) => w.id === currentWorkspaceId)?.isDefault === true;
const view = getView(location.pathname, hasProjects, isDefaultWorkspace);
const hasProjects = projects.length > 0;
const view = getView(location.pathname, hasProjects, isWorkspaceSuspended);

const showWorkspaceSelector =
workspaces !== undefined &&
workspaces.length > 0 &&
currentWorkspaceId !== undefined;

const navItems =
view === "welcome"
? [
{
to: dashboardPath(),
prefix: <ExtensionIcon />,
children: "Welcome",
},
]
: [
{
to: dashboardPath("projects"),
prefix: <BodyIcon />,
children: "Projects",
},
{
to: dashboardPath("templates"),
prefix: <ExtensionIcon />,
children: "Starter templates",
},
];
const navItems = [
{
to: dashboardPath("projects"),
prefix: <BodyIcon />,
children: "Projects",
},
];

return (
<TooltipProvider>
Expand Down Expand Up @@ -360,6 +335,29 @@ export const Dashboard = () => {
children: item.label,
}))}
/>
<Flex
align="center"
gap="2"
css={{
paddingInline: theme.panel.paddingInline,
paddingBlock: theme.spacing[5],
}}
>
<Text variant="labels" color="subtle">
Follow us:
</Text>
{socialLinks.map(({ label, url, icon }) => (
<Link
key={url}
href={url}
target="_blank"
color="subtle"
aria-label={label}
>
{icon}
</Link>
))}
</Flex>
</CollapsibleSection>
</Grid>
{view === "projects" && (
Expand All @@ -372,17 +370,8 @@ export const Dashboard = () => {
isWorkspaceSuspended={isWorkspaceSuspended}
/>
)}
{view === "templates" && (
<Templates
projects={templates}
currentWorkspaceId={currentWorkspaceId}
/>
)}
{view === "welcome" && (
<Welcome
projects={templates}
currentWorkspaceId={currentWorkspaceId}
/>
<Welcome currentWorkspaceId={currentWorkspaceId} />
)}
{view === "search" && <SearchResults {...data} />}
</Flex>
Expand Down
Loading
Loading