From aac08f9680eecf617ab2f2a91a3141c97ba57003 Mon Sep 17 00:00:00 2001 From: b-saikrishnakanth Date: Fri, 5 Dec 2025 17:09:17 +0530 Subject: [PATCH 1/2] feat: add timezone selection to workspace onboarding, creation and settings --- apps/web/app/(all)/create-workspace/page.tsx | 11 +- .../onboarding/steps/workspace/create.tsx | 109 +++++++++------- .../workspace/create-workspace-form.tsx | 100 +++++++++------ .../workspace/settings/workspace-details.tsx | 117 +++++++++++------- packages/i18n/src/locales/cs/translations.ts | 4 + packages/i18n/src/locales/de/translations.ts | 4 + packages/i18n/src/locales/en/translations.ts | 4 + packages/i18n/src/locales/es/translations.ts | 4 + packages/i18n/src/locales/fr/translations.ts | 4 + packages/i18n/src/locales/id/translations.ts | 4 + packages/i18n/src/locales/it/translations.ts | 4 + packages/i18n/src/locales/ja/translations.ts | 4 + packages/i18n/src/locales/ko/translations.ts | 4 + packages/i18n/src/locales/pl/translations.ts | 4 + .../i18n/src/locales/pt-BR/translations.ts | 4 + packages/i18n/src/locales/ro/translations.ts | 4 + packages/i18n/src/locales/ru/translations.ts | 4 + packages/i18n/src/locales/sk/translations.ts | 4 + .../i18n/src/locales/tr-TR/translations.ts | 4 + packages/i18n/src/locales/ua/translations.ts | 4 + .../i18n/src/locales/vi-VN/translations.ts | 4 + .../i18n/src/locales/zh-CN/translations.ts | 4 + .../i18n/src/locales/zh-TW/translations.ts | 4 + packages/types/src/workspace.ts | 3 +- 24 files changed, 283 insertions(+), 133 deletions(-) diff --git a/apps/web/app/(all)/create-workspace/page.tsx b/apps/web/app/(all)/create-workspace/page.tsx index 7d8cfa82a8f..3df4a1a2be0 100644 --- a/apps/web/app/(all)/create-workspace/page.tsx +++ b/apps/web/app/(all)/create-workspace/page.tsx @@ -18,7 +18,7 @@ import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; // plane web helpers import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper"; -function CreateWorkspacePage() { +const CreateWorkspacePage = observer(function CreateWorkspacePage() { const { t } = useTranslation(); // router const router = useAppRouter(); @@ -26,10 +26,13 @@ function CreateWorkspacePage() { const { data: currentUser } = useUser(); const { updateUserProfile } = useUserProfile(); // states - const [defaultValues, setDefaultValues] = useState>({ + const [defaultValues, setDefaultValues] = useState< + Pick + >({ name: "", slug: "", organization_size: "", + timezone: "UTC", }); // derived values const isWorkspaceCreationDisabled = getIsWorkspaceCreationDisabled(); @@ -104,6 +107,6 @@ function CreateWorkspacePage() { ); -} +}); -export default observer(CreateWorkspacePage); +export default CreateWorkspacePage; diff --git a/apps/web/core/components/onboarding/steps/workspace/create.tsx b/apps/web/core/components/onboarding/steps/workspace/create.tsx index 949144a3759..f492c55499c 100644 --- a/apps/web/core/components/onboarding/steps/workspace/create.tsx +++ b/apps/web/core/components/onboarding/steps/workspace/create.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { CircleCheck } from "lucide-react"; @@ -16,6 +16,7 @@ import type { IUser, IWorkspace } from "@plane/types"; import { Spinner } from "@plane/ui"; import { cn } from "@plane/utils"; // helpers +import { TimezoneSelect } from "@/components/global/timezone-select"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; // hooks import { useWorkspace } from "@/hooks/store/use-workspace"; @@ -64,6 +65,7 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({ name: "", slug: "", organization_size: "", + timezone: "UTC", }, mode: "onChange", }); @@ -71,47 +73,46 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({ const handleCreateWorkspace = async (formData: IWorkspace) => { if (isSubmitting) return; - await workspaceService - .workspaceSlugCheck(formData.slug) - .then(async (res) => { - if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) { - setSlugError(false); - await createWorkspace(formData) - .then(async (workspaceResponse) => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: t("workspace_creation.toast.success.title"), - message: t("workspace_creation.toast.success.message"), - }); - captureSuccess({ - eventName: WORKSPACE_TRACKER_EVENTS.create, - payload: { slug: formData.slug }, - }); - await fetchWorkspaces(); - await completeStep(workspaceResponse.id); - onComplete(formData.organization_size === "Just myself"); - }) - .catch(() => { - captureError({ - eventName: WORKSPACE_TRACKER_EVENTS.create, - payload: { slug: formData.slug }, - error: new Error("Error creating workspace"), - }); - setToast({ - type: TOAST_TYPE.ERROR, - title: t("workspace_creation.toast.error.title"), - message: t("workspace_creation.toast.error.message"), - }); - }); - } else setSlugError(true); - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: t("workspace_creation.toast.error.title"), - message: t("workspace_creation.toast.error.message"), - }) - ); + try { + const res = (await workspaceService.workspaceSlugCheck(formData.slug)) as { status: boolean }; + if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) { + setSlugError(false); + try { + const workspaceResponse = await createWorkspace(formData); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("workspace_creation.toast.success.title"), + message: t("workspace_creation.toast.success.message"), + }); + captureSuccess({ + eventName: WORKSPACE_TRACKER_EVENTS.create, + payload: { slug: formData.slug }, + }); + await fetchWorkspaces(); + await completeStep(workspaceResponse.id); + onComplete(formData.organization_size === "Just myself"); + } catch { + captureError({ + eventName: WORKSPACE_TRACKER_EVENTS.create, + payload: { slug: formData.slug }, + error: new Error("Error creating workspace"), + }); + setToast({ + type: TOAST_TYPE.ERROR, + title: t("workspace_creation.toast.error.title"), + message: t("workspace_creation.toast.error.message"), + }); + } + } else { + setSlugError(true); + } + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("workspace_creation.toast.error.title"), + message: t("workspace_creation.toast.error.message"), + }); + } }; const completeStep = async (workspaceId: string) => { @@ -136,7 +137,12 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({ ); } return ( -
+ { + void handleSubmit(handleCreateWorkspace)(e); + }} + >
@@ -181,6 +187,7 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({ "border-red-500": errors.name, } )} + // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus />
@@ -292,6 +299,22 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({ )}
+
+ +
+ ( + <> + + + )} + /> +
+
{secondaryButton} diff --git a/apps/web/core/components/workspace/settings/workspace-details.tsx b/apps/web/core/components/workspace/settings/workspace-details.tsx index bb535b870c0..7e18d25d18a 100644 --- a/apps/web/core/components/workspace/settings/workspace-details.tsx +++ b/apps/web/core/components/workspace/settings/workspace-details.tsx @@ -19,6 +19,7 @@ import { copyUrlToClipboard, getFileURL } from "@plane/utils"; // components import { WorkspaceImageUploadModal } from "@/components/core/modals/workspace-image-upload-modal"; // helpers +import { TimezoneSelect } from "@/components/global/timezone-select"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { useWorkspace } from "@/hooks/store/use-workspace"; import { useUserPermissions } from "@/hooks/store/user"; @@ -30,6 +31,7 @@ const defaultValues: Partial = { url: "", organization_size: "2-10", logo_url: null, + timezone: "UTC", }; export const WorkspaceDetails = observer(function WorkspaceDetails() { @@ -62,64 +64,69 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() { const payload: Partial = { name: formData.name, organization_size: formData.organization_size, + timezone: formData.timezone, }; - await updateWorkspace(currentWorkspace.slug, payload) - .then(() => { - captureSuccess({ - eventName: WORKSPACE_TRACKER_EVENTS.update, - payload: { slug: currentWorkspace.slug }, - }); - setToast({ - title: "Success!", - type: TOAST_TYPE.SUCCESS, - message: "Workspace updated successfully", - }); - }) - .catch((err) => { - captureError({ - eventName: WORKSPACE_TRACKER_EVENTS.update, - payload: { slug: currentWorkspace.slug }, - error: err, - }); - console.error(err); + try { + await updateWorkspace(currentWorkspace.slug, payload); + captureSuccess({ + eventName: WORKSPACE_TRACKER_EVENTS.update, + payload: { slug: currentWorkspace.slug }, + }); + setToast({ + title: "Success!", + type: TOAST_TYPE.SUCCESS, + message: "Workspace updated successfully", + }); + } catch (err: unknown) { + captureError({ + eventName: WORKSPACE_TRACKER_EVENTS.update, + payload: { slug: currentWorkspace.slug }, + error: err instanceof Error ? err : new Error(String(err)), }); - setTimeout(() => { - setIsLoading(false); - }, 300); + console.error(err); + } finally { + setTimeout(() => { + setIsLoading(false); + }, 300); + } }; const handleRemoveLogo = async () => { if (!currentWorkspace) return; - await updateWorkspace(currentWorkspace.slug, { - logo_url: "", - }) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Workspace picture removed successfully.", - }); - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "There was some error in deleting your profile picture. Please try again.", - }); + try { + await updateWorkspace(currentWorkspace.slug, { + logo_url: "", + }); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Workspace picture removed successfully.", }); + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "There was some error in deleting your profile picture. Please try again.", + }); + } }; const handleCopyUrl = () => { if (!currentWorkspace) return; - copyUrlToClipboard(`${currentWorkspace.slug}`).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Workspace URL copied to the clipboard.", + void copyUrlToClipboard(`${currentWorkspace.slug}`) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Workspace URL copied to the clipboard.", + }); + return undefined; + }) + .catch(() => { + // Silently handle clipboard errors }); - }); }; useEffect(() => { @@ -264,12 +271,30 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() { onChange={onChange} ref={ref} hasError={Boolean(errors.url)} - className="w-full" + className="w-full cursor-not-allowed rounded-md !bg-custom-background-90" disabled /> )} />
+ +
+

{t("workspace_settings.settings.general.workspace_timezone")}

+ ( + <> + + + )} + /> +
{isAdmin && ( @@ -277,7 +302,9 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {