[WEB-2500] feat: Product updates modal (What's new in Plane) (#5690)

* [WEB-2500] feat: Product updates modal (What's new in Plane)

* fix: build errors.

* fix: lint errors resolved.

* chore: minor improvements.

* chore: minor fixes
This commit is contained in:
Prateek Shourya
2024-10-29 19:26:00 +05:30
committed by GitHub
parent c423d7d9df
commit 4bc751b7ab
19 changed files with 250 additions and 151 deletions

View File

@@ -4,6 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
WORKDIR /code

View File

@@ -4,6 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
RUN apk --no-cache add \
"bash~=5.2" \

View File

@@ -18,3 +18,5 @@ from .admin import (
InstanceAdminSignOutEndpoint,
InstanceAdminUserSessionEndpoint,
)
from .changelog import ChangeLogEndpoint

View File

@@ -0,0 +1,35 @@
# Python imports
import requests
# Django imports
from django.conf import settings
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny
# plane imports
from .base import BaseAPIView
class ChangeLogEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def fetch_change_logs(self):
response = requests.get(settings.INSTANCE_CHANGELOG_URL)
response.raise_for_status()
return response.json()
def get(self, request):
# Fetch the changelog
if settings.INSTANCE_CHANGELOG_URL:
data = self.fetch_change_logs()
return Response(data, status=status.HTTP_200_OK)
else:
return Response(
{"error": "could not fetch changelog please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@@ -177,6 +177,8 @@ class InstanceEndpoint(BaseAPIView):
data["space_base_url"] = settings.SPACE_BASE_URL
data["app_base_url"] = settings.APP_BASE_URL
data["instance_changelog_url"] = settings.INSTANCE_CHANGELOG_URL
instance_data = serializer.data
instance_data["workspaces_exist"] = Workspace.objects.count() >= 1

View File

@@ -11,6 +11,7 @@ from plane.license.api.views import (
InstanceAdminUserMeEndpoint,
InstanceAdminSignOutEndpoint,
InstanceAdminUserSessionEndpoint,
ChangeLogEndpoint
)
urlpatterns = [
@@ -19,6 +20,11 @@ urlpatterns = [
InstanceEndpoint.as_view(),
name="instance",
),
path(
"changelog/",
ChangeLogEndpoint.as_view(),
name="instance-changelog",
),
path(
"admins/",
InstanceAdminEndpoint.as_view(),

View File

@@ -384,6 +384,9 @@ APP_BASE_URL = os.environ.get("APP_BASE_URL")
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
# Instance Changelog URL
INSTANCE_CHANGELOG_URL = os.environ.get("INSTANCE_CHANGELOG_URL", "")
ATTACHMENT_MIME_TYPES = [
# Images
"image/jpeg",

View File

@@ -1,3 +1,2 @@
export * from "./version-number";
export * from "./product-updates";
export * from "./product-updates-modal";
export * from "./product-updates-header";

View File

@@ -0,0 +1,26 @@
import { observer } from "mobx-react";
import Image from "next/image";
// helpers
import { cn } from "@/helpers/common.helper";
// assets
import PlaneLogo from "@/public/plane-logos/blue-without-text.png";
// package.json
import packageJson from "package.json";
export const ProductUpdatesHeader = observer(() => (
<div className="flex gap-2 mx-6 my-4 items-center justify-between flex-shrink-0">
<div className="flex w-full items-center">
<div className="flex gap-2 text-xl font-medium">What&apos;s new</div>
<div
className={cn(
"px-2 mx-2 py-0.5 text-center text-xs font-medium rounded-full bg-custom-primary-100/20 text-custom-primary-100"
)}
>
Version: v{packageJson.version}
</div>
</div>
<div className="flex flex-shrink-0 items-center gap-8">
<Image src={PlaneLogo} alt="Plane" width={24} height={24} />
</div>
</div>
));

View File

@@ -1,9 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
export type ProductUpdatesModalProps = {
isOpen: boolean;
handleClose: () => void;
};
export const ProductUpdatesModal: FC<ProductUpdatesModalProps> = observer(() => <></>);

View File

@@ -1,21 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// ui
import { CustomMenu } from "@plane/ui";
export type ProductUpdatesProps = {
setIsChangeLogOpen: (isOpen: boolean) => void;
};
export const ProductUpdates: FC<ProductUpdatesProps> = observer(() => (
<CustomMenu.MenuItem>
<Link
href="https://go.plane.so/p-changelog"
target="_blank"
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">What&apos;s new</span>
</Link>
</CustomMenu.MenuItem>
));

View File

@@ -1,4 +1,3 @@
export * from "./product-updates-modal";
export * from "./empty-state";
export * from "./latest-feature-block";
export * from "./breadcrumb-link";

View File

@@ -1,111 +0,0 @@
"use client";
import React from "react";
import useSWR from "swr";
// headless ui
import { X } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// services
// components
import { Loader } from "@plane/ui";
import { MarkdownRenderer } from "@/components/ui";
// icons
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { WorkspaceService } from "@/plane-web/services";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
};
// services
const workspaceService = new WorkspaceService();
export const ProductUpdatesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
const { data: updates } = useSWR("PRODUCT_UPDATES", () => workspaceService.getProductUpdates());
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={setIsOpen}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 h-full w-full">
<div className="grid h-full w-full place-items-center p-4">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative min-w-[100%] overflow-hidden rounded-lg bg-custom-background-100 shadow-custom-shadow-md sm:min-w-[50%] sm:max-w-[50%]">
<div className="flex max-h-[90vh] w-full flex-col p-4">
<Dialog.Title as="h3" className="flex items-center justify-between text-lg font-semibold">
<span>Product Updates</span>
<span>
<button type="button" onClick={() => setIsOpen(false)}>
<X className="h-6 w-6 text-custom-text-200 hover:text-custom-text-100" aria-hidden="true" />
</button>
</span>
</Dialog.Title>
{updates && updates.length > 0 ? (
<div className="mt-4 h-full space-y-4 overflow-y-auto">
{updates.map((item, index) => (
<React.Fragment key={item.id}>
<div className="flex items-center gap-3 text-xs text-custom-text-200">
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-background-90 px-3 py-1.5 text-xs">
{item.tag_name}
</span>
<span>{renderFormattedDate(item.published_at)}</span>
{index === 0 && (
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-primary px-3 py-1.5 text-xs text-white">
New
</span>
)}
</div>
<MarkdownRenderer markdown={item.body} />
</React.Fragment>
))}
</div>
) : (
<div className="mt-4 grid w-full place-items-center">
<Loader className="w-full space-y-6">
<div className="space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="20px" width="80%" />
<Loader.Item height="20px" width="80%" />
</div>
<div className="space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="20px" width="80%" />
<Loader.Item height="20px" width="80%" />
</div>
<div className="space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="20px" width="80%" />
<Loader.Item height="20px" width="80%" />
</div>
</Loader>
</div>
)}
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@@ -0,0 +1 @@
export * from "./product-updates";

View File

@@ -0,0 +1,62 @@
import Image from "next/image";
// ui
import { getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// assets
import PlaneLogo from "@/public/plane-logos/blue-without-text.png";
export const ProductUpdatesFooter = () => (
<div className="flex items-center justify-between flex-shrink-0 gap-4 m-6 mb-4">
<div className="flex items-center gap-2">
<a
href="https://go.plane.so/p-docs"
target="_blank"
className="text-sm text-custom-text-200 hover:text-custom-text-100 hover:underline underline-offset-1 outline-none"
>
Docs
</a>
<svg viewBox="0 0 2 2" className="h-0.5 w-0.5 fill-current">
<circle cx={1} cy={1} r={1} />
</svg>
<a
href="https://go.plane.so/p-changelog"
target="_blank"
className="text-sm text-custom-text-200 hover:text-custom-text-100 hover:underline underline-offset-1 outline-none"
>
Full changelog
</a>
<svg viewBox="0 0 2 2" className="h-0.5 w-0.5 fill-current">
<circle cx={1} cy={1} r={1} />
</svg>
<a
href="mailto:support@plane.so"
target="_blank"
className="text-sm text-custom-text-200 hover:text-custom-text-100 hover:underline underline-offset-1 outline-none"
>
Support
</a>
<svg viewBox="0 0 2 2" className="h-0.5 w-0.5 fill-current">
<circle cx={1} cy={1} r={1} />
</svg>
<a
href="https://go.plane.so/p-discord"
target="_blank"
className="text-sm text-custom-text-200 hover:text-custom-text-100 hover:underline underline-offset-1 outline-none"
>
Discord
</a>
</div>
<a
href="https://plane.so/pages"
target="_blank"
className={cn(
getButtonStyling("accent-primary", "sm"),
"flex gap-1.5 items-center text-center font-medium hover:underline underline-offset-2 outline-none"
)}
>
<Image src={PlaneLogo} alt="Plane" width={12} height={12} />
Powered by Plane Pages
</a>
</div>
);

View File

@@ -0,0 +1,2 @@
export * from "./modal";
export * from "./footer";

View File

@@ -0,0 +1,84 @@
import { FC, useRef } from "react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// editor
import { DocumentReadOnlyEditorWithRef, EditorRefApi } from "@plane/editor";
// ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// helpers
import { LogoSpinner } from "@/components/common";
import { ProductUpdatesFooter } from "@/components/global";
// plane web components
import { ProductUpdatesHeader } from "@/plane-web/components/global";
// services
import { InstanceService } from "@/services/instance.service";
const instanceService = new InstanceService();
export type ProductUpdatesModalProps = {
isOpen: boolean;
handleClose: () => void;
};
export const ProductUpdatesModal: FC<ProductUpdatesModalProps> = observer((props) => {
const { isOpen, handleClose } = props;
// refs
const editorRef = useRef<EditorRefApi>(null);
// swr
const { data, isLoading, error } = useSWR(`INSTANCE_CHANGELOG`, () => instanceService.getInstanceChangeLog(), {
shouldRetryOnError: false,
revalidateIfStale: false,
revalidateOnFocus: false,
});
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<ProductUpdatesHeader />
<div className="flex flex-col h-[60vh] vertical-scrollbar scrollbar-xs overflow-hidden overflow-y-scroll px-6 mx-0.5">
{!isLoading && !!error ? (
<div className="flex flex-col items-center justify-center w-full h-full mb-8">
<div className="text-lg font-medium">We are having trouble fetching the updates.</div>
<div className="text-sm text-custom-text-200">
Please visit{" "}
<a
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"
>
our changelogs
</a>{" "}
for the latest updates.
</div>
</div>
) : isLoading ? (
<div className="flex items-center justify-center w-full h-full">
<LogoSpinner />
</div>
) : (
<div className="ml-5">
{data?.id && (
<DocumentReadOnlyEditorWithRef
ref={editorRef}
id={data.id}
initialValue={data.description_html ?? "<p></p>"}
containerClassName="p-0 border-none"
mentionHandler={{
highlights: () => Promise.resolve([]),
}}
embedHandler={{
issue: {
widgetCallback: () => <></>,
},
}}
fileHandler={{
getAssetSrc: () => "",
}}
/>
)}
</div>
)}
</div>
<ProductUpdatesFooter />
</ModalCore>
);
});

View File

@@ -5,14 +5,16 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { FileText, HelpCircle, MessagesSquare, MoveLeft, User } from "lucide-react";
// ui
import { CustomMenu, ToggleSwitch, Tooltip } from "@plane/ui";
import { CustomMenu, Tooltip, ToggleSwitch } from "@plane/ui";
// components
import { ProductUpdatesModal } from "@/components/global";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useCommandPalette, useInstance, useTransient, useUserSettings } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { PlaneVersionNumber, ProductUpdates, ProductUpdatesModal } from "@/plane-web/components/global";
import { PlaneVersionNumber } from "@/plane-web/components/global";
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace";
import { ENABLE_LOCAL_DB_CACHE } from "@/plane-web/constants/issues";
@@ -135,7 +137,15 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
<span className="text-xs">Keyboard shortcuts</span>
</button>
</CustomMenu.MenuItem>
<ProductUpdates setIsChangeLogOpen={setIsChangeLogOpen} />
<CustomMenu.MenuItem>
<button
type="button"
onClick={() => setIsChangeLogOpen(true)}
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">What&apos;s new</span>
</button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<a
href="https://go.plane.so/p-discord"
@@ -163,9 +173,8 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
<Tooltip tooltipContent={`${isCollapsed ? "Expand" : "Hide"}`} isMobile={isMobile}>
<button
type="button"
className={`grid place-items-center rounded-md p-1 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isCollapsed ? "w-full" : ""
}`}
className={`grid place-items-center rounded-md p-1 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${isCollapsed ? "w-full" : ""
}`}
onClick={() => toggleSidebar()}
>
<MoveLeft className={`h-4 w-4 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />

View File

@@ -1,5 +1,5 @@
// types
import type { IInstanceInfo } from "@plane/types";
import type { IInstanceInfo, TPage } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// services
@@ -25,4 +25,12 @@ export class InstanceService extends APIService {
throw error;
});
}
async getInstanceChangeLog(): Promise<TPage> {
return this.get("/api/instances/changelog/")
.then((response) => response.data)
.catch((error) => {
throw error;
});
}
}