[WEB-5290] feat: selfhosted check (#8227)

* feat: add in common py

* fix: update marketing consent screen based on is self managed flag

* improvement: enhance ImagePickerPopover with dynamic tab options based on Unsplash configuration

* refactor: product updates modal to include changelog

* [WEB-5290] feat: implement fallback for product updates changelog with loading state and error handling

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
Prateek Shourya 2025-12-03 15:53:42 +05:30 committed by GitHub
parent 5f7ffcb37a
commit 7c74d0a403
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 373 additions and 61 deletions

View file

@ -1,4 +1,4 @@
import React, { useState, useRef, useCallback } from "react";
import React, { useState, useRef, useCallback, useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useDropzone } from "react-dropzone";
@ -16,24 +16,16 @@ import { Input, Loader } from "@plane/ui";
// helpers
import { getFileURL } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
// services
import { FileService } from "@/services/file.service";
const tabOptions = [
{
key: "unsplash",
title: "Unsplash",
},
{
key: "images",
title: "Images",
},
{
key: "upload",
title: "Upload",
},
];
type TTabOption = {
key: string;
title: string;
isEnabled: boolean;
};
type Props = {
label: string | React.ReactNode;
@ -63,6 +55,30 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
const ref = useRef<HTMLDivElement>(null);
// router params
const { workspaceSlug } = useParams();
// store hooks
const { config } = useInstance();
// derived values
const hasUnsplashConfigured = config?.has_unsplash_configured || false;
const tabOptions: TTabOption[] = useMemo(
() => [
{
key: "unsplash",
title: "Unsplash",
isEnabled: hasUnsplashConfigured,
},
{
key: "images",
title: "Images",
isEnabled: true,
},
{
key: "upload",
title: "Upload",
isEnabled: true,
},
],
[hasUnsplashConfigured]
);
const { data: unsplashImages, error: unsplashError } = useSWR(
`UNSPLASH_IMAGES_${searchParams}`,

View file

@ -0,0 +1,32 @@
import { EmptyStateDetailed } from "@plane/propel/empty-state";
type TProductUpdatesFallbackProps = {
description: string;
variant: "cloud" | "self-managed";
};
export function ProductUpdatesFallback(props: TProductUpdatesFallbackProps) {
const { description, variant } = props;
// derived values
const changelogUrl =
variant === "cloud"
? "https://plane.so/changelog?category=cloud"
: "https://plane.so/changelog?category=self-hosted";
return (
<div className="py-8">
<EmptyStateDetailed
assetKey="changelog"
description={description}
align="center"
actions={[
{
label: "Go to changelog",
variant: "primary",
onClick: () => window.open(changelogUrl, "_blank"),
},
]}
/>
</div>
);
}

View file

@ -1,18 +1,15 @@
import type { FC } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { USER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// components
import { ProductUpdatesFooter } from "@/components/global";
// helpers
import { captureView } from "@/helpers/event-tracker.helper";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
// plane web components
import { ProductUpdatesHeader } from "@/plane-web/components/global";
import { ProductUpdatesChangelog } from "@/plane-web/components/global/product-updates/changelog";
import { ProductUpdatesHeader } from "@/plane-web/components/global/product-updates/header";
export type ProductUpdatesModalProps = {
isOpen: boolean;
@ -21,8 +18,6 @@ export type ProductUpdatesModalProps = {
export const ProductUpdatesModal = observer(function ProductUpdatesModal(props: ProductUpdatesModalProps) {
const { isOpen, handleClose } = props;
const { t } = useTranslation();
const { config } = useInstance();
useEffect(() => {
if (isOpen) {
@ -33,27 +28,7 @@ export const ProductUpdatesModal = observer(function ProductUpdatesModal(props:
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXXXL}>
<ProductUpdatesHeader />
<div className="flex flex-col h-[60vh] vertical-scrollbar scrollbar-xs overflow-hidden overflow-y-scroll px-6 mx-0.5">
{config?.instance_changelog_url && config?.instance_changelog_url !== "" ? (
<iframe src={config?.instance_changelog_url} className="w-full h-full" />
) : (
<div className="flex flex-col items-center justify-center w-full h-full mb-8">
<div className="text-lg font-medium">{t("we_are_having_trouble_fetching_the_updates")}</div>
<div className="text-sm text-custom-text-200">
{t("please_visit")}
<a
data-ph-element={USER_TRACKER_ELEMENTS.CHANGELOG_REDIRECTED}
href="https://go.plane.so/p-changelog"
target="_blank"
className="text-sm text-custom-primary-100 font-medium hover:text-custom-primary-200 underline underline-offset-1 outline-none"
>
{t("our_changelogs")}
</a>{" "}
{t("for_the_latest_updates")}.
</div>
</div>
)}
</div>
<ProductUpdatesChangelog />
<ProductUpdatesFooter />
</ModalCore>
);

View file

@ -22,6 +22,7 @@ import { AuthService } from "@/services/auth.service";
import { CommonOnboardingHeader } from "../common";
import { MarketingConsent } from "./consent";
import { SetPasswordRoot } from "./set-password";
import { useInstance } from "@/hooks/store/use-instance";
type Props = {
handleStepChange: (step: EOnboardingSteps, skipInvites?: boolean) => void;
@ -55,6 +56,7 @@ export const ProfileSetupStep = observer(function ProfileSetupStep({ handleStepC
// store hooks
const { data: user, updateCurrentUser } = useUser();
const { updateUserProfile } = useUserProfile();
const { config: instanceConfig } = useInstance();
// form info
const {
getValues,
@ -253,12 +255,14 @@ export const ProfileSetupStep = observer(function ProfileSetupStep({ handleStepC
</Button>
{/* Marketing Consent */}
<MarketingConsent
isChecked={!!watch("has_marketing_email_consent")}
handleChange={(has_marketing_email_consent) =>
setValue("has_marketing_email_consent", has_marketing_email_consent)
}
/>
{!instanceConfig.is_self_managed && (
<MarketingConsent
isChecked={!!watch("has_marketing_email_consent")}
handleChange={(has_marketing_email_consent) =>
setValue("has_marketing_email_consent", has_marketing_email_consent)
}
/>
)}
</form>
);
});