diff --git a/admin/app/general/form.tsx b/admin/app/general/form.tsx index 380f39357..4422ee91f 100644 --- a/admin/app/general/form.tsx +++ b/admin/app/general/form.tsx @@ -86,6 +86,7 @@ export const GeneralConfigurationForm: FC = observer( value={instanceAdmins[0]?.user_detail?.email ?? ""} placeholder="Admin email" className="w-full cursor-not-allowed !text-custom-text-400" + autoComplete="on" disabled /> diff --git a/admin/core/components/admin-sidebar/help-section.tsx b/admin/core/components/admin-sidebar/help-section.tsx index 4b516dff0..abba68e3e 100644 --- a/admin/core/components/admin-sidebar/help-section.tsx +++ b/admin/core/components/admin-sidebar/help-section.tsx @@ -96,7 +96,7 @@ export const HelpSection: FC = observer(() => { leaveTo="transform opacity-0 scale-95" >
{ placeholder="Wilber" value={formData.first_name} onChange={(e) => handleFormChange("first_name", e.target.value)} + autoComplete="on" autoFocus />
@@ -190,6 +191,7 @@ export const InstanceSetupForm: FC = (props) => { placeholder="Wright" value={formData.last_name} onChange={(e) => handleFormChange("last_name", e.target.value)} + autoComplete="on" /> @@ -208,6 +210,7 @@ export const InstanceSetupForm: FC = (props) => { value={formData.email} onChange={(e) => handleFormChange("email", e.target.value)} hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false} + autoComplete="on" /> {errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (

{errorData.message}

@@ -247,6 +250,7 @@ export const InstanceSetupForm: FC = (props) => { hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false} onFocus={() => setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} + autoComplete="on" /> {showPassword.password ? ( - - } - /> - ); - } - return ( <> } /> @@ -63,6 +28,6 @@ const ProjectSettingLayout: FC = observer((props) => { ); -}); +}; export default ProjectSettingLayout; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx index af1c82e12..28c65f680 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx @@ -2,17 +2,26 @@ import { observer } from "mobx-react"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project"; // hooks -import { useProject } from "@/hooks/store"; +import { useProject, useUser } from "@/hooks/store"; const MembersSettingsPage = observer(() => { // store const { currentProjectDetails } = useProject(); + const { + canPerformProjectViewerActions, + membership: { currentProjectRole }, + } = useUser(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined; + if (currentProjectRole && !canPerformProjectViewerActions) { + return ; + } + return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx index c90f155eb..91630e922 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx @@ -1,6 +1,7 @@ "use client"; import React from "react"; +import { observer } from "mobx-react"; import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; // ui @@ -14,7 +15,7 @@ import { useUser } from "@/hooks/store"; // plane web constants import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project"; -export const ProjectSettingsSidebar = () => { +export const ProjectSettingsSidebar = observer(() => { const { workspaceSlug, projectId } = useParams(); const pathname = usePathname(); // mobx store @@ -62,4 +63,4 @@ export const ProjectSettingsSidebar = () => { ); -}; +}); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx index 4132662bf..b03310a09 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx @@ -3,18 +3,27 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectStateRoot } from "@/components/project-states"; // hook -import { useProject } from "@/hooks/store"; +import { useProject, useUser } from "@/hooks/store"; const StatesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); // store const { currentProjectDetails } = useProject(); + const { + canPerformProjectMemberActions, + membership: { currentProjectRole }, + } = useUser(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; + if (currentProjectRole && !canPerformProjectMemberActions) { + return ; + } + return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx index 26d5fd873..27f84d94e 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx @@ -12,21 +12,17 @@ import { BreadcrumbLink, Logo } from "@/components/common"; import { ViewListHeader } from "@/components/views"; import { ViewAppliedFiltersList } from "@/components/views/applied-filters"; // constants -import { EUserProjectRoles } from "@/constants/project"; import { EViewAccess } from "@/constants/views"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useCommandPalette, useProject, useProjectView, useUser } from "@/hooks/store"; +import { useCommandPalette, useProject, useProjectView } from "@/hooks/store"; export const ProjectViewsHeader = observer(() => { // router const { workspaceSlug } = useParams(); // store hooks const { toggleCreateViewModal } = useCommandPalette(); - const { - membership: { currentProjectRole }, - } = useUser(); const { currentProjectDetails, loader } = useProject(); const { filters, updateFilters, clearAllFilters } = useProjectView(); @@ -49,9 +45,6 @@ export const ProjectViewsHeader = observer(() => { const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0; - const canUserCreateView = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - return ( <>
@@ -83,13 +76,11 @@ export const ProjectViewsHeader = observer(() => {
- {canUserCreateView && ( -
- -
- )} +
+ +
{isFiltersApplied && ( diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx new file mode 100644 index 000000000..a308e1978 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { ReactNode } from "react"; +// components +import { AppHeader, ContentWrapper } from "@/components/core"; +// local components +import { ProjectsListHeader } from "@/plane-web/components/projects/header"; +import { ProjectsListMobileHeader } from "@/plane-web/components/projects/mobile-header"; +export default function ProjectListLayout({ children }: { children: ReactNode }) { + return ( + <> + } mobileHeader={} /> + {children} + + ); +} diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx new file mode 100644 index 000000000..ac6e5c3cd --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx @@ -0,0 +1,4 @@ +import { ProjectPageRoot } from "@/plane-web/components/projects/page"; + +const ProjectsPage = () => ; +export default ProjectsPage; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx index 259c412dc..a308e1978 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx @@ -4,9 +4,8 @@ import { ReactNode } from "react"; // components import { AppHeader, ContentWrapper } from "@/components/core"; // local components -import { ProjectsListHeader } from "./header"; -import { ProjectsListMobileHeader } from "./mobile-header"; - +import { ProjectsListHeader } from "@/plane-web/components/projects/header"; +import { ProjectsListMobileHeader } from "@/plane-web/components/projects/mobile-header"; export default function ProjectListLayout({ children }: { children: ReactNode }) { return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx index 40e7f30a2..ac6e5c3cd 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx @@ -1,84 +1,4 @@ -"use client"; - -import { useCallback } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// types -import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types"; -// components -import { PageHead } from "@/components/core"; -import { ProjectAppliedFiltersList, ProjectCardList } from "@/components/project"; -// helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; -// hooks -import { useProject, useProjectFilter, useWorkspace } from "@/hooks/store"; - -const ProjectsPage = observer(() => { - // store - const { workspaceSlug } = useParams(); - const { currentWorkspace } = useWorkspace(); - const { totalProjectIds, filteredProjectIds } = useProject(); - const { - currentWorkspaceFilters, - currentWorkspaceAppliedDisplayFilters, - clearAllFilters, - clearAllAppliedDisplayFilters, - updateFilters, - updateDisplayFilters, - } = useProjectFilter(); - // derived values - const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined; - - const handleRemoveFilter = useCallback( - (key: keyof TProjectFilters, value: string | null) => { - if (!workspaceSlug) return; - let newValues = currentWorkspaceFilters?.[key] ?? []; - - if (!value) newValues = []; - else newValues = newValues.filter((val) => val !== value); - - updateFilters(workspaceSlug.toString(), { [key]: newValues }); - }, - [currentWorkspaceFilters, updateFilters, workspaceSlug] - ); - - const handleRemoveDisplayFilter = useCallback( - (key: TProjectAppliedDisplayFilterKeys) => { - if (!workspaceSlug) return; - updateDisplayFilters(workspaceSlug.toString(), { [key]: false }); - }, - [updateDisplayFilters, workspaceSlug] - ); - - const handleClearAllFilters = useCallback(() => { - if (!workspaceSlug) return; - clearAllFilters(workspaceSlug.toString()); - clearAllAppliedDisplayFilters(workspaceSlug.toString()); - }, [clearAllFilters, clearAllAppliedDisplayFilters, workspaceSlug]); - - return ( - <> - -
- {(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 || - currentWorkspaceAppliedDisplayFilters?.length !== 0) && ( -
- -
- )} - -
- - ); -}); +import { ProjectPageRoot } from "@/plane-web/components/projects/page"; +const ProjectsPage = () => ; export default ProjectsPage; diff --git a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx index 906fee328..bfc583b78 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx @@ -8,13 +8,13 @@ import useSWR from "swr"; import { Button } from "@plane/ui"; // component import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token"; +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { EmptyState } from "@/components/empty-state"; import { APITokenSettingsLoader } from "@/components/ui"; // constants import { EmptyStateType } from "@/constants/empty-state"; import { API_TOKENS_LIST } from "@/constants/fetch-keys"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // store hooks import { useUser, useWorkspace } from "@/hooks/store"; // services @@ -29,27 +29,22 @@ const ApiTokensPage = observer(() => { const { workspaceSlug } = useParams(); // store hooks const { + canPerformWorkspaceAdminActions, membership: { currentWorkspaceRole }, } = useUser(); const { currentWorkspace } = useWorkspace(); - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; - - const { data: tokens } = useSWR(workspaceSlug && isAdmin ? API_TOKENS_LIST(workspaceSlug.toString()) : null, () => - workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null + const { data: tokens } = useSWR( + workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null, + () => + workspaceSlug && canPerformWorkspaceAdminActions ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null ); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; - if (!isAdmin) - return ( - <> - -
-

You are not authorized to access this page.

-
- - ); + if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { + return ; + } if (!tokens) { return ; @@ -92,4 +87,4 @@ const ApiTokensPage = observer(() => { ); }); -export default ApiTokensPage; \ No newline at end of file +export default ApiTokensPage; diff --git a/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx index 3e3c586c4..0158c3c98 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx @@ -2,9 +2,8 @@ import { observer } from "mobx-react"; // component +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks import { useUser, useWorkspace } from "@/hooks/store"; // plane web components @@ -13,22 +12,16 @@ import { BillingRoot } from "@/plane-web/components/workspace"; const BillingSettingsPage = observer(() => { // store hooks const { + canPerformWorkspaceAdminActions, membership: { currentWorkspaceRole }, } = useUser(); const { currentWorkspace } = useWorkspace(); // derived values - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined; - if (!isAdmin) - return ( - <> - -
-

You are not authorized to access this page.

-
- - ); + if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { + return ; + } return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx index 59fd4d2c7..85b8cd644 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx @@ -2,39 +2,39 @@ import { observer } from "mobx-react"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import ExportGuide from "@/components/exporter/guide"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; +// helpers +import { cn } from "@/helpers/common.helper"; // hooks import { useUser, useWorkspace } from "@/hooks/store"; const ExportsPage = observer(() => { // store hooks const { + canPerformWorkspaceViewerActions, + canPerformWorkspaceMemberActions, membership: { currentWorkspaceRole }, } = useUser(); const { currentWorkspace } = useWorkspace(); // derived values - const hasPageAccess = - currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined; - if (!hasPageAccess) - return ( - <> - -
-

You are not authorized to access this page.

-
- - ); + // if user is not authorized to view this page + if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) { + return ; + } return ( <> -
+

Exports

@@ -44,4 +44,4 @@ const ExportsPage = observer(() => { ); }); -export default ExportsPage; \ No newline at end of file +export default ExportsPage; diff --git a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx index 52b225639..0d48bef73 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx @@ -9,12 +9,13 @@ import { IWorkspaceBulkInviteFormData } from "@plane/types"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "@/components/workspace"; // constants import { MEMBER_INVITED } from "@/constants/event-tracker"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers +import { cn } from "@/helpers/common.helper"; import { getUserRole } from "@/helpers/user.helper"; // hooks import { useEventTracker, useMember, useUser, useWorkspace } from "@/hooks/store"; @@ -28,6 +29,9 @@ const WorkspaceMembersSettingsPage = observer(() => { // store hooks const { captureEvent } = useEventTracker(); const { + canPerformWorkspaceAdminActions, + canPerformWorkspaceViewerActions, + canPerformWorkspaceMemberActions, membership: { currentWorkspaceRole }, } = useUser(); const { @@ -79,9 +83,13 @@ const WorkspaceMembersSettingsPage = observer(() => { }; // derived values - const isAdmin = currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN].includes(currentWorkspaceRole); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined; + // if user is not authorized to view this page + if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) { + return ; + } + return ( <> @@ -90,7 +98,11 @@ const WorkspaceMembersSettingsPage = observer(() => { onClose={() => setInviteModal(false)} onSubmit={handleWorkspaceInvite} /> -
+

Members

@@ -103,13 +115,13 @@ const WorkspaceMembersSettingsPage = observer(() => { onChange={(e) => setSearchQuery(e.target.value)} />
- {isAdmin && ( + {canPerformWorkspaceAdminActions && ( )}
- +
); diff --git a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx index 695f1f16b..a887c4144 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx @@ -7,6 +7,7 @@ import useSWR from "swr"; // ui import { Button } from "@plane/ui"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { EmptyState } from "@/components/empty-state"; import { WebhookSettingsLoader } from "@/components/ui"; @@ -23,16 +24,15 @@ const WebhooksListPage = observer(() => { const { workspaceSlug } = useParams(); // mobx store const { + canPerformWorkspaceAdminActions, membership: { currentWorkspaceRole }, } = useUser(); const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); - const isAdmin = currentWorkspaceRole === 20; - useSWR( - workspaceSlug && isAdmin ? `WEBHOOKS_LIST_${workspaceSlug}` : null, - workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null + workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null, + workspaceSlug && canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug.toString()) : null ); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined; @@ -42,15 +42,9 @@ const WebhooksListPage = observer(() => { if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey(); }, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]); - if (!isAdmin) - return ( - <> - -
-

You are not authorized to access this page.

-
- - ); + if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { + return ; + } if (!webhooks) return ; @@ -95,4 +89,4 @@ const WebhooksListPage = observer(() => { ); }); -export default WebhooksListPage; \ No newline at end of file +export default WebhooksListPage; diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/sidebar.tsx index 074b5655e..338a8488b 100644 --- a/web/app/[workspaceSlug]/(projects)/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/sidebar.tsx @@ -13,7 +13,7 @@ import { import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu"; import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme } from "@/hooks/store"; +import { useAppTheme, useUser } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; // plane web components import useSize from "@/hooks/use-window-size"; @@ -23,6 +23,7 @@ export interface IAppSidebar {} export const AppSidebar: FC = observer(() => { // store hooks + const { canPerformWorkspaceMemberActions } = useUser(); const { toggleSidebar, sidebarCollapsed } = useAppTheme(); const windowSize = useSize(); // refs @@ -54,10 +55,14 @@ export const AppSidebar: FC = observer(() => {
-
+
@@ -69,8 +74,8 @@ export const AppSidebar: FC = observer(() => { })} />
@@ -81,7 +86,7 @@ export const AppSidebar: FC = observer(() => { "opacity-0": !sidebarCollapsed, })} /> - + {canPerformWorkspaceMemberActions && }
diff --git a/web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx b/web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx index 686e4f7b6..82e11b208 100644 --- a/web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx @@ -14,11 +14,10 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/com import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks -import { useLabel, useMember, useUser, useIssues, useGlobalView } from "@/hooks/store"; +import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store"; export const GlobalIssuesHeader = observer(() => { // states @@ -30,9 +29,6 @@ export const GlobalIssuesHeader = observer(() => { issuesFilter: { filters, updateFilters }, } = useIssues(EIssuesStoreType.GLOBAL); const { getViewDetailsById } = useGlobalView(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); const { workspaceLabels } = useLabel(); const { workspace: { workspaceMemberIds }, @@ -97,8 +93,6 @@ export const GlobalIssuesHeader = observer(() => { [workspaceSlug, updateFilters, globalViewId] ); - const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - const isLocked = viewDetails?.is_locked; return ( @@ -142,11 +136,10 @@ export const GlobalIssuesHeader = observer(() => { )} - {isAuthorizedUser && ( - - )} + +
diff --git a/web/app/accounts/forgot-password/page.tsx b/web/app/accounts/forgot-password/page.tsx index 3e86e8eff..91516c5b9 100644 --- a/web/app/accounts/forgot-password/page.tsx +++ b/web/app/accounts/forgot-password/page.tsx @@ -154,6 +154,7 @@ export default function ForgotPasswordPage() { hasError={Boolean(errors.email)} placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" + autoComplete="on" disabled={resendTimerCode > 0} /> )} diff --git a/web/app/accounts/reset-password/page.tsx b/web/app/accounts/reset-password/page.tsx index 4d5303b98..04d6e3115 100644 --- a/web/app/accounts/reset-password/page.tsx +++ b/web/app/accounts/reset-password/page.tsx @@ -153,6 +153,7 @@ export default function ResetPasswordPage() { //hasError={Boolean(errors.email)} placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed" + autoComplete="on" disabled />
@@ -173,6 +174,7 @@ export default function ResetPasswordPage() { minLength={8} onFocus={() => setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} + autoComplete="on" autoFocus /> {showPassword.password ? ( diff --git a/web/app/accounts/set-password/page.tsx b/web/app/accounts/set-password/page.tsx index df1b5d567..f3ac35b76 100644 --- a/web/app/accounts/set-password/page.tsx +++ b/web/app/accounts/set-password/page.tsx @@ -147,6 +147,7 @@ const SetPasswordPage = observer(() => { //hasError={Boolean(errors.email)} placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed" + autoComplete="on" disabled />
@@ -167,6 +168,7 @@ const SetPasswordPage = observer(() => { minLength={8} onFocus={() => setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} + autoComplete="on" autoFocus /> {showPassword.password ? ( diff --git a/web/app/profile/page.tsx b/web/app/profile/page.tsx index 03a738a48..c9929db1e 100644 --- a/web/app/profile/page.tsx +++ b/web/app/profile/page.tsx @@ -245,6 +245,7 @@ const ProfileSettingsPage = observer(() => { placeholder="Enter your first name" className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`} maxLength={24} + autoComplete="on" /> )} /> @@ -269,6 +270,7 @@ const ProfileSettingsPage = observer(() => { placeholder="Enter your last name" className="w-full rounded-md" maxLength={24} + autoComplete="on" /> )} /> @@ -296,6 +298,7 @@ const ProfileSettingsPage = observer(() => { className={`w-full cursor-not-allowed rounded-md !bg-custom-background-80 ${ errors.email ? "border-red-500" : "" }`} + autoComplete="on" disabled /> )} diff --git a/web/ce/components/pages/editor/ai/ask-pi-menu.tsx b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx new file mode 100644 index 000000000..d3440ea47 --- /dev/null +++ b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx @@ -0,0 +1,103 @@ +import { useState } from "react"; +import { CircleArrowUp, CornerDownRight, RefreshCcw, Sparkles } from "lucide-react"; +// ui +import { Tooltip } from "@plane/ui"; +// components +import { RichTextReadOnlyEditor } from "@/components/editor"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type Props = { + handleInsertText: (insertOnNextLine: boolean) => void; + handleRegenerate: () => Promise; + isRegenerating: boolean; + response: string | undefined; +}; + +export const AskPiMenu: React.FC = (props) => { + const { handleInsertText, handleRegenerate, isRegenerating, response } = props; + // states + const [query, setQuery] = useState(""); + + return ( + <> +
+ + + + {response ? ( +
+ +
+ + + + + + + +
+
+ ) : ( +

Pi is answering...

+ )} +
+
+
+ + + + setQuery(e.target.value)} + placeholder="Tell Pi what to do..." + /> + + + +
+
+ + ); +}; diff --git a/web/ce/components/pages/editor/ai/index.ts b/web/ce/components/pages/editor/ai/index.ts new file mode 100644 index 000000000..d21eb63d7 --- /dev/null +++ b/web/ce/components/pages/editor/ai/index.ts @@ -0,0 +1,2 @@ +export * from "./ask-pi-menu"; +export * from "./menu"; diff --git a/web/ce/components/pages/editor/ai/menu.tsx b/web/ce/components/pages/editor/ai/menu.tsx new file mode 100644 index 000000000..7610595f7 --- /dev/null +++ b/web/ce/components/pages/editor/ai/menu.tsx @@ -0,0 +1,290 @@ +"use client"; + +import React, { RefObject, useRef, useState } from "react"; +import { useParams } from "next/navigation"; +import { ChevronRight, CornerDownRight, LucideIcon, RefreshCcw, Sparkles, TriangleAlert } from "lucide-react"; +// plane editor +import { EditorRefApi } from "@plane/editor"; +// plane ui +import { Tooltip } from "@plane/ui"; +// components +import { RichTextReadOnlyEditor } from "@/components/editor"; +// helpers +import { cn } from "@/helpers/common.helper"; +// plane web constants +import { AI_EDITOR_TASKS, LOADING_TEXTS } from "@/plane-web/constants/ai"; +// plane web services +import { AIService, TTaskPayload } from "@/services/ai.service"; +import { AskPiMenu } from "./ask-pi-menu"; +const aiService = new AIService(); + +type Props = { + editorRef: RefObject; + onClose: () => void; +}; + +const MENU_ITEMS: { + icon: LucideIcon; + key: AI_EDITOR_TASKS; + label: string; +}[] = [ + { + key: AI_EDITOR_TASKS.ASK_ANYTHING, + icon: Sparkles, + label: "Ask Pi", + }, +]; + +const TONES_LIST = [ + { + key: "default", + label: "Default", + casual_score: 5, + formal_score: 5, + }, + { + key: "professional", + label: "💼 Professional", + casual_score: 0, + formal_score: 10, + }, + { + key: "casual", + label: "😃 Casual", + casual_score: 10, + formal_score: 0, + }, +]; + +export const EditorAIMenu: React.FC = (props) => { + const { editorRef, onClose } = props; + // states + const [activeTask, setActiveTask] = useState(null); + const [response, setResponse] = useState(undefined); + const [isRegenerating, setIsRegenerating] = useState(false); + // refs + const responseContainerRef = useRef(null); + // params + const { workspaceSlug } = useParams(); + const handleGenerateResponse = async (payload: TTaskPayload) => { + if (!workspaceSlug) return; + await aiService.performEditorTask(workspaceSlug.toString(), payload).then((res) => setResponse(res.response)); + }; + // handle task click + const handleClick = async (key: AI_EDITOR_TASKS) => { + const selection = editorRef.current?.getSelectedText(); + if (!selection || activeTask === key) return; + setActiveTask(key); + if (key === AI_EDITOR_TASKS.ASK_ANYTHING) return; + setResponse(undefined); + setIsRegenerating(false); + await handleGenerateResponse({ + task: key, + text_input: selection, + }); + }; + // handle re-generate response + const handleRegenerate = async () => { + const selection = editorRef.current?.getSelectedText(); + if (!selection || !activeTask) return; + setIsRegenerating(true); + await handleGenerateResponse({ + task: activeTask, + text_input: selection, + }) + .then(() => + responseContainerRef.current?.scrollTo({ + top: 0, + behavior: "smooth", + }) + ) + .finally(() => setIsRegenerating(false)); + }; + // handle re-generate response + const handleToneChange = async (key: string) => { + const selectedTone = TONES_LIST.find((t) => t.key === key); + const selection = editorRef.current?.getSelectedText(); + if (!selectedTone || !selection || !activeTask) return; + setResponse(undefined); + setIsRegenerating(false); + await handleGenerateResponse({ + casual_score: selectedTone.casual_score, + formal_score: selectedTone.formal_score, + task: activeTask, + text_input: selection, + }).then(() => + responseContainerRef.current?.scrollTo({ + top: 0, + behavior: "smooth", + }) + ); + }; + // handle replace selected text with the response + const handleInsertText = (insertOnNextLine: boolean) => { + if (!response) return; + editorRef.current?.insertText(response, insertOnNextLine); + onClose(); + }; + + return ( +
+
+
+ {MENU_ITEMS.map((item) => { + const isActiveTask = activeTask === item.key; + + return ( + + ); + })} +
+
+ {activeTask === AI_EDITOR_TASKS.ASK_ANYTHING ? ( + + ) : ( + <> +
+ + + + {response ? ( +
+ +
+ + + + + + + +
+
+ ) : ( +

+ {activeTask ? LOADING_TEXTS[activeTask] : "Pi is writing"}... +

+ )} +
+
+ {TONES_LIST.map((tone) => ( + + ))} +
+ + )} +
+
+ {activeTask && ( +
+ + + +

+ By using this feature, you consent to sharing the message with a 3rd party service. +

+
+ )} +
+ ); +}; diff --git a/web/ce/components/pages/editor/index.ts b/web/ce/components/pages/editor/index.ts index 12b3c5295..88b26fa27 100644 --- a/web/ce/components/pages/editor/index.ts +++ b/web/ce/components/pages/editor/index.ts @@ -1 +1,2 @@ +export * from "./ai"; export * from "./embed"; diff --git a/web/ce/components/projects/create/attributes.tsx b/web/ce/components/projects/create/attributes.tsx new file mode 100644 index 000000000..ead92208f --- /dev/null +++ b/web/ce/components/projects/create/attributes.tsx @@ -0,0 +1,80 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { IProject } from "@plane/types"; +import { CustomSelect } from "@plane/ui"; +import { MemberDropdown } from "@/components/dropdowns"; +import { NETWORK_CHOICES } from "@/constants/project"; + +const ProjectAttributes = () => { + const { control } = useFormContext(); + return ( +
+ { + const currentNetwork = NETWORK_CHOICES.find((n) => n.key === value); + + return ( +
+ + {currentNetwork ? ( + <> + + {currentNetwork.label} + + ) : ( + Select network + )} +
+ } + placement="bottom-start" + className="h-full" + buttonClassName="h-full" + noChevron + tabIndex={4} + > + {NETWORK_CHOICES.map((network) => ( + +
+ +
+

{network.label}

+

{network.description}

+
+
+
+ ))} + +
+ ); + }} + /> + { + if (value === undefined || value === null || typeof value === "string") + return ( +
+ onChange(lead === value ? null : lead)} + placeholder="Lead" + multiple={false} + buttonVariant="border-with-text" + tabIndex={5} + /> +
+ ); + else return <>; + }} + /> +
+ ); +}; + +export default ProjectAttributes; diff --git a/web/ce/components/projects/create/root.tsx b/web/ce/components/projects/create/root.tsx new file mode 100644 index 000000000..76fea48f7 --- /dev/null +++ b/web/ce/components/projects/create/root.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useState, FC } from "react"; +import { observer } from "mobx-react"; +import { FormProvider, useForm } from "react-hook-form"; +// ui +import { setToast, TOAST_TYPE } from "@plane/ui"; +// constants +import ProjectCommonAttributes from "@/components/project/create/common-attributes"; +import ProjectCreateHeader from "@/components/project/create/header"; +import ProjectCreateButtons from "@/components/project/create/project-create-buttons"; +import { PROJECT_CREATED } from "@/constants/event-tracker"; +import { PROJECT_UNSPLASH_COVERS } from "@/constants/project"; +// helpers +import { getRandomEmoji } from "@/helpers/emoji.helper"; +// hooks +import { useEventTracker, useProject } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +import { TProject } from "@/plane-web/types/projects"; +import ProjectAttributes from "./attributes"; + +type Props = { + setToFavorite?: boolean; + workspaceSlug: string; + onClose: () => void; + handleNextStep: (projectId: string) => void; + data?: Partial; +}; + +const defaultValues: Partial = { + cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], + description: "", + logo_props: { + in_use: "emoji", + emoji: { + value: getRandomEmoji(), + }, + }, + identifier: "", + name: "", + network: 2, + project_lead: null, +}; + +export const CreateProjectForm: FC = observer((props) => { + const { setToFavorite, workspaceSlug, onClose, handleNextStep } = props; + // store + const { captureProjectEvent } = useEventTracker(); + const { addProjectToFavorites, createProject } = useProject(); + // states + const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); + // form info + const methods = useForm({ + defaultValues, + reValidateMode: "onChange", + }); + const { handleSubmit, reset, setValue } = methods; + const { isMobile } = usePlatformOS(); + const handleAddToFavorites = (projectId: string) => { + if (!workspaceSlug) return; + + addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Couldn't remove the project from favorites. Please try again.", + }); + }); + }; + + const onSubmit = async (formData: Partial) => { + // Upper case identifier + formData.identifier = formData.identifier?.toUpperCase(); + + return createProject(workspaceSlug.toString(), formData) + .then((res) => { + const newPayload = { + ...res, + state: "SUCCESS", + }; + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: newPayload, + }); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Project created successfully.", + }); + if (setToFavorite) { + handleAddToFavorites(res.id); + } + handleNextStep(res.id); + }) + .catch((err) => { + Object.keys(err.data).map((key) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err.data[key], + }); + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: { + ...formData, + state: "FAILED", + }, + }); + }); + }); + }; + + const handleClose = () => { + onClose(); + setIsChangeInIdentifierRequired(true); + setTimeout(() => { + reset(); + }, 300); + }; + + return ( + + + +
+
+ + +
+ + +
+ ); +}); diff --git a/web/ce/components/projects/header.tsx b/web/ce/components/projects/header.tsx new file mode 100644 index 000000000..08871ec9b --- /dev/null +++ b/web/ce/components/projects/header.tsx @@ -0,0 +1,5 @@ +"use client"; + +import { ProjectsBaseHeader } from "@/components/project/header"; + +export const ProjectsListHeader = () => ; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/mobile-header.tsx b/web/ce/components/projects/mobile-header.tsx similarity index 99% rename from web/app/[workspaceSlug]/(projects)/projects/(list)/mobile-header.tsx rename to web/ce/components/projects/mobile-header.tsx index cd8eb9dfe..380472160 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(list)/mobile-header.tsx +++ b/web/ce/components/projects/mobile-header.tsx @@ -1,3 +1,4 @@ +"use client"; import { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; @@ -23,7 +24,6 @@ export const ProjectsListMobileHeader = observer(() => { updateFilters, } = useProjectFilter(); - const { workspace: { workspaceMemberIds }, } = useMember(); diff --git a/web/ce/components/projects/page.tsx b/web/ce/components/projects/page.tsx new file mode 100644 index 000000000..a44ab7df4 --- /dev/null +++ b/web/ce/components/projects/page.tsx @@ -0,0 +1,3 @@ +import Root from "@/components/project/root"; + +export const ProjectPageRoot = () => ; diff --git a/web/ce/constants/ai.ts b/web/ce/constants/ai.ts new file mode 100644 index 000000000..c5c1b04fa --- /dev/null +++ b/web/ce/constants/ai.ts @@ -0,0 +1,9 @@ +export enum AI_EDITOR_TASKS { + ASK_ANYTHING = "ASK_ANYTHING", +} + +export const LOADING_TEXTS: { + [key in AI_EDITOR_TASKS]: string; +} = { + [AI_EDITOR_TASKS.ASK_ANYTHING]: "Pi is generating response", +}; diff --git a/web/ce/constants/project/settings/tabs.ts b/web/ce/constants/project/settings/tabs.ts index 6da1430ca..d067fba92 100644 --- a/web/ce/constants/project/settings/tabs.ts +++ b/web/ce/constants/project/settings/tabs.ts @@ -10,7 +10,7 @@ export const PROJECT_SETTINGS = { key: "general", label: "General", href: `/settings`, - access: EUserProjectRoles.MEMBER, + access: EUserProjectRoles.GUEST, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`, Icon: SettingIcon, }, @@ -18,7 +18,7 @@ export const PROJECT_SETTINGS = { key: "members", label: "Members", href: `/settings/members`, - access: EUserProjectRoles.MEMBER, + access: EUserProjectRoles.VIEWER, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, Icon: SettingIcon, }, @@ -72,11 +72,11 @@ export const PROJECT_SETTINGS_LINKS: { highlight: (pathname: string, baseUrl: string) => boolean; Icon: React.FC; }[] = [ - PROJECT_SETTINGS["general"], - PROJECT_SETTINGS["members"], - PROJECT_SETTINGS["features"], - PROJECT_SETTINGS["states"], - PROJECT_SETTINGS["labels"], - PROJECT_SETTINGS["estimates"], - PROJECT_SETTINGS["automations"], - ]; + PROJECT_SETTINGS["general"], + PROJECT_SETTINGS["members"], + PROJECT_SETTINGS["features"], + PROJECT_SETTINGS["states"], + PROJECT_SETTINGS["labels"], + PROJECT_SETTINGS["estimates"], + PROJECT_SETTINGS["automations"], +]; diff --git a/web/ce/constants/workspace.ts b/web/ce/constants/workspace.ts index b89ced416..109a7a4d1 100644 --- a/web/ce/constants/workspace.ts +++ b/web/ce/constants/workspace.ts @@ -17,7 +17,7 @@ export const WORKSPACE_SETTINGS = { key: "members", label: "Members", href: `/settings/members`, - access: EUserWorkspaceRoles.GUEST, + access: EUserWorkspaceRoles.VIEWER, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, Icon: SettingIcon, }, @@ -33,7 +33,7 @@ export const WORKSPACE_SETTINGS = { key: "export", label: "Exports", href: `/settings/exports`, - access: EUserWorkspaceRoles.MEMBER, + access: EUserWorkspaceRoles.VIEWER, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`, Icon: SettingIcon, }, diff --git a/web/ce/hooks/use-editor-flagging.ts b/web/ce/hooks/use-editor-flagging.ts new file mode 100644 index 000000000..9077d216e --- /dev/null +++ b/web/ce/hooks/use-editor-flagging.ts @@ -0,0 +1,13 @@ +// editor +import { TExtensions } from "@plane/editor"; + +/** + * @description extensions disabled in various editors + */ +export const useEditorFlagging = (): { + documentEditor: TExtensions[]; + richTextEditor: TExtensions[]; +} => ({ + documentEditor: ["ai"], + richTextEditor: ["ai"], +}); diff --git a/web/ce/hooks/use-issue-embed.tsx b/web/ce/hooks/use-issue-embed.tsx index 5ca4d4b02..5d02d978f 100644 --- a/web/ce/hooks/use-issue-embed.tsx +++ b/web/ce/hooks/use-issue-embed.tsx @@ -1,5 +1,5 @@ // editor -import { TEmbedConfig, TReadOnlyEmbedConfig } from "@plane/editor"; +import { TEmbedConfig } from "@plane/editor"; // types import { TPageEmbedType } from "@plane/types"; // plane web components @@ -13,12 +13,7 @@ export const useIssueEmbed = (workspaceSlug: string, projectId: string, queryTyp widgetCallback, }; - const issueEmbedReadOnlyProps: TReadOnlyEmbedConfig["issue"] = { - widgetCallback, - }; - return { issueEmbedProps, - issueEmbedReadOnlyProps, }; }; diff --git a/web/ce/types/projects/index.ts b/web/ce/types/projects/index.ts new file mode 100644 index 000000000..244d8c4df --- /dev/null +++ b/web/ce/types/projects/index.ts @@ -0,0 +1 @@ +export * from "./projects"; diff --git a/web/ce/types/projects/projects.ts b/web/ce/types/projects/projects.ts new file mode 100644 index 000000000..567c9488d --- /dev/null +++ b/web/ce/types/projects/projects.ts @@ -0,0 +1,3 @@ +import { IProject } from "@plane/types"; + +export type TProject = IProject; diff --git a/web/core/components/account/auth-forms/email.tsx b/web/core/components/account/auth-forms/email.tsx index 9acfcc5cc..ab328a7e1 100644 --- a/web/core/components/account/auth-forms/email.tsx +++ b/web/core/components/account/auth-forms/email.tsx @@ -63,6 +63,7 @@ export const AuthEmailForm: FC = observer((props) => { className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} + autoComplete="on" autoFocus /> {email.length > 0 && ( diff --git a/web/core/components/account/auth-forms/password.tsx b/web/core/components/account/auth-forms/password.tsx index 618c578b0..088dc3194 100644 --- a/web/core/components/account/auth-forms/password.tsx +++ b/web/core/components/account/auth-forms/password.tsx @@ -208,6 +208,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" onFocus={() => setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} + autoComplete="on" autoFocus /> {showPassword?.password ? ( diff --git a/web/core/components/account/auth-forms/unique-code.tsx b/web/core/components/account/auth-forms/unique-code.tsx index 8c7b8a60f..530874eb9 100644 --- a/web/core/components/account/auth-forms/unique-code.tsx +++ b/web/core/components/account/auth-forms/unique-code.tsx @@ -107,6 +107,7 @@ export const AuthUniqueCodeForm: React.FC = (props) => { onChange={(e) => handleFormChange("email", e.target.value)} placeholder="name@company.com" className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`} + autoComplete="on" disabled /> {uniqueCodeFormData.email.length > 0 && ( diff --git a/web/core/components/auth-screens/not-authorized-view.tsx b/web/core/components/auth-screens/not-authorized-view.tsx index f8f101dd3..fe344f468 100644 --- a/web/core/components/auth-screens/not-authorized-view.tsx +++ b/web/core/components/auth-screens/not-authorized-view.tsx @@ -1,62 +1,33 @@ import React from "react"; import { observer } from "mobx-react"; import Image from "next/image"; -import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -// hooks -import { useUser } from "@/hooks/store"; // layouts import DefaultLayout from "@/layouts/default-layout"; // images import ProjectNotAuthorizedImg from "@/public/auth/project-not-authorized.svg"; +import Unauthorized from "@/public/auth/unauthorized.svg"; import WorkspaceNotAuthorizedImg from "@/public/auth/workspace-not-authorized.svg"; type Props = { actionButton?: React.ReactNode; - type: "project" | "workspace"; + section?: "settings" | "general"; + isProjectView?: boolean; }; export const NotAuthorizedView: React.FC = observer((props) => { - const { actionButton, type } = props; - // router - const searchParams = useSearchParams(); - const next_path = searchParams.get("next_path"); - // hooks - const { data: currentUser } = useUser(); + const { actionButton, section = "general", isProjectView = false } = props; + + // assets + const settingAsset = isProjectView ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg; + const asset = section === "settings" ? settingAsset : Unauthorized; return (
- ProjectSettingImg + ProjectSettingImg

Oops! You are not authorized to view this page

- -
- {currentUser ? ( -

- You have signed in as {currentUser.email}.
- - Sign in - {" "} - with different account that has access to this page. -

- ) : ( -

- You need to{" "} - - Sign in - {" "} - with an account that has access to this page. -

- )} -
- {actionButton}
diff --git a/web/core/components/command-palette/actions/help-actions.tsx b/web/core/components/command-palette/actions/help-actions.tsx index ad54c542b..49a888798 100644 --- a/web/core/components/command-palette/actions/help-actions.tsx +++ b/web/core/components/command-palette/actions/help-actions.tsx @@ -1,19 +1,21 @@ "use client"; import { Command } from "cmdk"; +import { observer } from "mobx-react"; import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react"; // ui import { DiscordIcon } from "@plane/ui"; // hooks -import { useCommandPalette } from "@/hooks/store"; +import { useCommandPalette, useTransient } from "@/hooks/store"; type Props = { closePalette: () => void; }; -export const CommandPaletteHelpActions: React.FC = (props) => { +export const CommandPaletteHelpActions: React.FC = observer((props) => { const { closePalette } = props; // hooks const { toggleShortcutModal } = useCommandPalette(); + const { toggleIntercom } = useTransient(); return ( @@ -68,9 +70,7 @@ export const CommandPaletteHelpActions: React.FC = (props) => { { closePalette(); - if (window) { - window.$crisp.push(["do", "chat:show"]); - } + toggleIntercom(true); }} className="focus:outline-none" > @@ -81,4 +81,4 @@ export const CommandPaletteHelpActions: React.FC = (props) => { ); -}; +}); diff --git a/web/core/components/command-palette/command-palette.tsx b/web/core/components/command-palette/command-palette.tsx index 099d53f90..62f049145 100644 --- a/web/core/components/command-palette/command-palette.tsx +++ b/web/core/components/command-palette/command-palette.tsx @@ -43,8 +43,8 @@ export const CommandPalette: FC = observer(() => { const { platform } = usePlatformOS(); const { data: currentUser, - canPerformProjectCreateActions, - canPerformWorkspaceCreateActions, + canPerformProjectMemberActions, + canPerformWorkspaceMemberActions, canPerformAnyCreateAction, canPerformProjectAdminActions, } = useUser(); @@ -103,15 +103,15 @@ export const CommandPalette: FC = observer(() => { // auth const performProjectCreateActions = useCallback( (showToast: boolean = true) => { - if (!canPerformProjectCreateActions && showToast) + if (!canPerformProjectMemberActions && showToast) setToast({ type: TOAST_TYPE.ERROR, title: "You don't have permission to perform this action.", }); - return canPerformProjectCreateActions; + return canPerformProjectMemberActions; }, - [canPerformProjectCreateActions] + [canPerformProjectMemberActions] ); const performProjectBulkDeleteActions = useCallback( @@ -129,14 +129,14 @@ export const CommandPalette: FC = observer(() => { const performWorkspaceCreateActions = useCallback( (showToast: boolean = true) => { - if (!canPerformWorkspaceCreateActions && showToast) + if (!canPerformWorkspaceMemberActions && showToast) setToast({ type: TOAST_TYPE.ERROR, title: "You don't have permission to perform this action.", }); - return canPerformWorkspaceCreateActions; + return canPerformWorkspaceMemberActions; }, - [canPerformWorkspaceCreateActions] + [canPerformWorkspaceMemberActions] ); const performAnyProjectCreateActions = useCallback( @@ -212,7 +212,6 @@ export const CommandPalette: FC = observer(() => { toggleCreatePageModal, toggleCreateProjectModal, toggleCreateViewModal, - toggleShortcutModal, ] ); @@ -261,15 +260,18 @@ export const CommandPalette: FC = observer(() => { if ( Object.keys(shortcutsList.global).includes(keyPressed) && ((!projectId && performAnyProjectCreateActions()) || performProjectCreateActions()) - ) + ) { shortcutsList.global[keyPressed].action(); + } // workspace authorized actions else if ( Object.keys(shortcutsList.workspace).includes(keyPressed) && workspaceSlug && performWorkspaceCreateActions() - ) + ) { + e.preventDefault(); shortcutsList.workspace[keyPressed].action(); + } // project authorized actions else if ( Object.keys(shortcutsList.project).includes(keyPressed) && @@ -283,16 +285,18 @@ export const CommandPalette: FC = observer(() => { } }, [ - performAnyProjectCreateActions, - performProjectCreateActions, - performProjectBulkDeleteActions, - performWorkspaceCreateActions, copyIssueUrlToClipboard, isAnyModalOpen, + platform, + performAnyProjectCreateActions, + performProjectBulkDeleteActions, + performProjectCreateActions, + performWorkspaceCreateActions, projectId, setTrackElement, shortcutsList, toggleCommandPaletteModal, + toggleShortcutModal, toggleSidebar, workspaceSlug, ] diff --git a/web/core/components/common/latest-feature-block.tsx b/web/core/components/common/latest-feature-block.tsx index 48595241b..a108e05bf 100644 --- a/web/core/components/common/latest-feature-block.tsx +++ b/web/core/components/common/latest-feature-block.tsx @@ -4,7 +4,7 @@ import { useTheme } from "next-themes"; // icons import { Lightbulb } from "lucide-react"; // images -import latestFeatures from "@/public/onboarding/onboarding-pages.svg"; +import latestFeatures from "@/public/onboarding/onboarding-pages.webp"; export const LatestFeatureBlock = () => { const { resolvedTheme } = useTheme(); diff --git a/web/core/components/cycles/delete-modal.tsx b/web/core/components/cycles/delete-modal.tsx index b9b1fdbc5..e31a785b5 100644 --- a/web/core/components/cycles/delete-modal.tsx +++ b/web/core/components/cycles/delete-modal.tsx @@ -42,6 +42,7 @@ export const CycleDeleteModal: React.FC = observer((props) => { try { await deleteCycle(workspaceSlug, projectId, cycle.id) .then(() => { + if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", @@ -68,8 +69,6 @@ export const CycleDeleteModal: React.FC = observer((props) => { }); }) .finally(() => handleClose()); - - if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); } catch (error) { setToast({ type: TOAST_TYPE.ERROR, diff --git a/web/core/components/dashboard/project-empty-state.tsx b/web/core/components/dashboard/project-empty-state.tsx index ed8623735..a42f5a5b0 100644 --- a/web/core/components/dashboard/project-empty-state.tsx +++ b/web/core/components/dashboard/project-empty-state.tsx @@ -9,7 +9,7 @@ import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks import { useCommandPalette, useEventTracker, useUser } from "@/hooks/store"; // assets -import ProjectEmptyStateImage from "@/public/empty-state/dashboard/project.svg"; +import ProjectEmptyStateImage from "@/public/empty-state/onboarding/dashboard-light.webp"; export const DashboardProjectEmptyState = observer(() => { // store hooks diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index 447f4f309..4ded0e84c 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -94,11 +94,7 @@ export const LiteTextEditor = React.forwardRef { - if (isMutableRefObject(ref)) { - rest.onEnterKeyPress?.(ref.current?.getHTML() ?? ""); - } - }} + handleSubmit={(e) => rest.onEnterKeyPress?.(e)} isCommentEmpty={isEmpty} isSubmitting={isSubmitting} showAccessSpecifier={showAccessSpecifier} diff --git a/web/core/components/gantt-chart/blocks/blocks-list.tsx b/web/core/components/gantt-chart/blocks/blocks-list.tsx index 8c94b07d0..c4ffae138 100644 --- a/web/core/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/core/components/gantt-chart/blocks/blocks-list.tsx @@ -14,10 +14,10 @@ export type GanttChartBlocksProps = { getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; blockToRender: (data: any) => React.ReactNode; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - enableBlockLeftResize: boolean; - enableBlockRightResize: boolean; - enableBlockMove: boolean; - enableAddBlock: boolean; + enableBlockLeftResize: boolean | ((blockId: string) => boolean); + enableBlockRightResize: boolean | ((blockId: string) => boolean); + enableBlockMove: boolean | ((blockId: string) => boolean); + enableAddBlock: boolean | ((blockId: string) => boolean); ganttContainerRef: React.RefObject; showAllBlocks: boolean; selectionHelpers: TSelectionHelper; @@ -55,10 +55,14 @@ export const GanttChartBlocksList: FC = (props) => { showAllBlocks={showAllBlocks} blockToRender={blockToRender} blockUpdateHandler={blockUpdateHandler} - enableBlockLeftResize={enableBlockLeftResize} - enableBlockRightResize={enableBlockRightResize} - enableBlockMove={enableBlockMove} - enableAddBlock={enableAddBlock} + enableBlockLeftResize={ + typeof enableBlockLeftResize === "function" ? enableBlockLeftResize(blockId) : enableBlockLeftResize + } + enableBlockRightResize={ + typeof enableBlockRightResize === "function" ? enableBlockRightResize(blockId) : enableBlockRightResize + } + enableBlockMove={typeof enableBlockMove === "function" ? enableBlockMove(blockId) : enableBlockMove} + enableAddBlock={typeof enableAddBlock === "function" ? enableAddBlock(blockId) : enableAddBlock} ganttContainerRef={ganttContainerRef} selectionHelpers={selectionHelpers} /> diff --git a/web/core/components/gantt-chart/chart/header.tsx b/web/core/components/gantt-chart/chart/header.tsx index 8756e200f..4e16436df 100644 --- a/web/core/components/gantt-chart/chart/header.tsx +++ b/web/core/components/gantt-chart/chart/header.tsx @@ -16,10 +16,12 @@ type Props = { handleToday: () => void; loaderTitle: string; toggleFullScreenMode: () => void; + showToday: boolean; }; export const GanttChartHeader: React.FC = observer((props) => { - const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode } = props; + const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode, showToday } = + props; // chart hook const { currentView } = useGanttChart(); @@ -46,9 +48,15 @@ export const GanttChartHeader: React.FC = observer((props) => { ))}
- + {showToday && ( + + )}