chore: resolved merge conflicts

This commit is contained in:
guru_sainath 2024-05-27 23:04:26 +05:30
commit 638662d33a
38 changed files with 2342 additions and 1581 deletions

View File

@ -1,4 +1,5 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
trailingSlash: true, trailingSlash: true,
reactStrictMode: false, reactStrictMode: false,

View File

@ -128,7 +128,7 @@ services:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable} image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-} platform: ${DOCKER_PLATFORM:-}
pull_policy: ${PULL_POLICY:-always} pull_policy: ${PULL_POLICY:-always}
restart: no restart: "no"
command: ./bin/docker-entrypoint-migrator.sh command: ./bin/docker-entrypoint-migrator.sh
volumes: volumes:
- logs_migrator:/code/plane/logs - logs_migrator:/code/plane/logs

3
space/.gitignore vendored
View File

@ -37,3 +37,6 @@ next-env.d.ts
# env # env
.env .env
# Sentry Config File
.env.sentry-build-plugin

9
space/instrumentation.ts Normal file
View File

@ -0,0 +1,9 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}

View File

@ -28,12 +28,46 @@ const nextConfig = {
}, },
}; };
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
module.exports = withSentryConfig( const sentryConfig = {
nextConfig, // For all available options, see:
{ silent: true, authToken: process.env.SENTRY_AUTH_TOKEN }, // https://github.com/getsentry/sentry-webpack-plugin#options
{ hideSourceMaps: true }
); org: process.env.SENTRY_ORG_ID || "plane-hq",
project: process.env.SENTRY_PROJECT_ID || "plane-space",
authToken: process.env.SENTRY_AUTH_TOKEN,
// Only print logs for uploading source maps in CI
silent: true,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
}
if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) {
module.exports = withSentryConfig(nextConfig, sentryConfig);
} else { } else {
module.exports = nextConfig; module.exports = nextConfig;
} }

View File

@ -23,7 +23,7 @@
"@plane/rich-text-editor": "*", "@plane/rich-text-editor": "*",
"@plane/types": "*", "@plane/types": "*",
"@plane/ui": "*", "@plane/ui": "*",
"@sentry/nextjs": "^7.108.0", "@sentry/nextjs": "^8",
"axios": "^1.3.4", "axios": "^1.3.4",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"dompurify": "^3.0.11", "dompurify": "^3.0.11",

View File

@ -1,18 +0,0 @@
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

View File

@ -0,0 +1,31 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
replaysOnErrorSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
integrations: [
Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
maskAllText: true,
blockAllMedia: true,
}),
],
});

View File

@ -1,18 +0,0 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever middleware or an Edge route handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

View File

@ -0,0 +1,17 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@ -4,15 +4,16 @@
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({ Sentry.init({
dsn: SENTRY_DSN, dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development", environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control // Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0, tracesSampleRate: 1,
// ...
// Note: if you want to override the automatic release value, do not set a // Setting this option to true will print useful information to the console while you're setting up Sentry.
// `release` value here - use the environment variable `SENTRY_RELEASE`, so debug: false,
// that it will also get attached to your source maps
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: process.env.NODE_ENV === 'development',
}); });

View File

@ -8,9 +8,6 @@
"NEXT_PUBLIC_SPACE_BASE_URL", "NEXT_PUBLIC_SPACE_BASE_URL",
"NEXT_PUBLIC_SPACE_BASE_PATH", "NEXT_PUBLIC_SPACE_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL", "NEXT_PUBLIC_WEB_BASE_URL",
"NEXT_PUBLIC_SENTRY_DSN",
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
"NEXT_PUBLIC_ENABLE_SENTRY",
"NEXT_PUBLIC_TRACK_EVENTS", "NEXT_PUBLIC_TRACK_EVENTS",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN", "NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_CRISP_ID", "NEXT_PUBLIC_CRISP_ID",
@ -21,7 +18,12 @@
"NEXT_PUBLIC_POSTHOG_HOST", "NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG", "NEXT_PUBLIC_POSTHOG_DEBUG",
"NEXT_PUBLIC_SUPPORT_EMAIL", "NEXT_PUBLIC_SUPPORT_EMAIL",
"SENTRY_AUTH_TOKEN" "SENTRY_AUTH_TOKEN",
"SENTRY_ORG_ID",
"SENTRY_PROJECT_ID",
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
"NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_MONITORING_ENABLED"
], ],
"pipeline": { "pipeline": {
"build": { "build": {

View File

@ -2,6 +2,8 @@ import React, { FC } from "react";
import Link from "next/link"; import Link from "next/link";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
interface IListItemProps { interface IListItemProps {
title: string; title: string;
@ -12,6 +14,7 @@ interface IListItemProps {
actionableItems?: JSX.Element; actionableItems?: JSX.Element;
isMobile?: boolean; isMobile?: boolean;
parentRef: React.RefObject<HTMLDivElement>; parentRef: React.RefObject<HTMLDivElement>;
className?: string;
} }
export const ListItem: FC<IListItemProps> = (props) => { export const ListItem: FC<IListItemProps> = (props) => {
@ -24,12 +27,18 @@ export const ListItem: FC<IListItemProps> = (props) => {
onItemClick, onItemClick,
isMobile = false, isMobile = false,
parentRef, parentRef,
className = "",
} = props; } = props;
return ( return (
<div ref={parentRef} className="relative"> <div ref={parentRef} className="relative">
<Link href={itemLink} onClick={onItemClick}> <Link href={itemLink} onClick={onItemClick}>
<div className="group h-24 sm:h-[52px] flex w-full flex-col items-center justify-between gap-3 sm:gap-5 px-6 py-4 sm:py-0 text-sm border-b border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90 sm:flex-row"> <div
className={cn(
"group h-24 sm:h-[52px] flex w-full flex-col items-center justify-between gap-3 sm:gap-5 px-6 py-4 sm:py-0 text-sm border-b border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90 sm:flex-row",
className
)}
>
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden"> <div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
<div className="relative flex w-full items-center gap-3 overflow-hidden"> <div className="relative flex w-full items-center gap-3 overflow-hidden">
<div className="flex items-center gap-4 truncate"> <div className="flex items-center gap-4 truncate">

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react";
import Image from "next/image"; import Image from "next/image";
// headless ui // headless ui
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
@ -15,6 +15,7 @@ import {
// hooks // hooks
import { Avatar, StateGroupIcon } from "@plane/ui"; import { Avatar, StateGroupIcon } from "@plane/ui";
import { SingleProgressStats } from "@/components/core"; import { SingleProgressStats } from "@/components/core";
import { useProjectState } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage"; import useLocalStorage from "@/hooks/use-local-storage";
// images // images
import emptyLabel from "public/empty-state/empty_label.svg"; import emptyLabel from "public/empty-state/empty_label.svg";
@ -44,20 +45,23 @@ type Props = {
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void; handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
}; };
export const SidebarProgressStats: React.FC<Props> = ({ export const SidebarProgressStats: React.FC<Props> = observer((props) => {
distribution, const {
groupedIssues, distribution,
totalIssues, groupedIssues,
module, totalIssues,
roundedTab, module,
noBackground, roundedTab,
isPeekView = false, noBackground,
isCompleted = false, isPeekView = false,
filters, isCompleted = false,
handleFiltersUpdate, filters,
}) => { handleFiltersUpdate,
} = props;
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees"); const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
const { groupedProjectStates } = useProjectState();
const currentValue = (tab: string | null) => { const currentValue = (tab: string | null) => {
switch (tab) { switch (tab) {
case "Assignees": case "Assignees":
@ -71,6 +75,12 @@ export const SidebarProgressStats: React.FC<Props> = ({
} }
}; };
const getStateGroupState = (stateGroup: string) => {
const stateGroupStates = groupedProjectStates?.[stateGroup];
const stateGroupStatesId = stateGroupStates?.map((state) => state.id);
return stateGroupStatesId;
};
return ( return (
<Tab.Group <Tab.Group
defaultIndex={currentValue(tab)} defaultIndex={currentValue(tab)}
@ -261,10 +271,14 @@ export const SidebarProgressStats: React.FC<Props> = ({
} }
completed={groupedIssues[group]} completed={groupedIssues[group]}
total={totalIssues} total={totalIssues}
{...(!isPeekView &&
!isCompleted && {
onClick: () => handleFiltersUpdate("state", getStateGroupState(group) ?? []),
})}
/> />
))} ))}
</Tab.Panel> </Tab.Panel>
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
); );
}; });

View File

@ -1,4 +1,5 @@
import { FC } from "react"; import { FC } from "react";
import Link from "next/link";
// types // types
import { ICycle } from "@plane/types"; import { ICycle } from "@plane/types";
// components // components
@ -8,14 +9,19 @@ import { EmptyState } from "@/components/empty-state";
import { EmptyStateType } from "@/constants/empty-state"; import { EmptyStateType } from "@/constants/empty-state";
export type ActiveCycleProductivityProps = { export type ActiveCycleProductivityProps = {
workspaceSlug: string;
projectId: string;
cycle: ICycle; cycle: ICycle;
}; };
export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props) => { export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props) => {
const { cycle } = props; const { workspaceSlug, projectId, cycle } = props;
return ( return (
<div className="flex flex-col justify-center min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg"> <Link
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}
className="flex flex-col justify-center min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg"
>
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3> <h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3>
</div> </div>
@ -53,6 +59,6 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props)
</div> </div>
</> </>
)} )}
</div> </Link>
); );
}; };

View File

@ -1,4 +1,5 @@
import { FC } from "react"; import { FC } from "react";
import Link from "next/link";
// types // types
import { ICycle } from "@plane/types"; import { ICycle } from "@plane/types";
// ui // ui
@ -10,11 +11,13 @@ import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
import { EmptyStateType } from "@/constants/empty-state"; import { EmptyStateType } from "@/constants/empty-state";
export type ActiveCycleProgressProps = { export type ActiveCycleProgressProps = {
workspaceSlug: string;
projectId: string;
cycle: ICycle; cycle: ICycle;
}; };
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => { export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
const { cycle } = props; const { workspaceSlug, projectId, cycle } = props;
const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({ const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
id: index, id: index,
@ -31,7 +34,10 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
}; };
return ( return (
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg"> <Link
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}
className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg"
>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3> <h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>
@ -85,6 +91,6 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE} layout="screen-simple" size="sm" /> <EmptyState type={EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE} layout="screen-simple" size="sm" />
</div> </div>
)} )}
</div> </Link>
); );
}; };

View File

@ -62,13 +62,18 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
cycleId={currentProjectActiveCycleId} cycleId={currentProjectActiveCycleId}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
className="!border-b-transparent"
/> />
)} )}
<div className="bg-custom-background-90 py-6 px-8"> <div className="bg-custom-background-100 pt-3 pb-6 px-6">
<div className="grid grid-cols-1 bg-custom-background-90 gap-3 lg:grid-cols-2 xl:grid-cols-3"> <div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress cycle={activeCycle} /> <ActiveCycleProgress workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
<ActiveCycleProductivity cycle={activeCycle} /> <ActiveCycleProductivity
<ActiveCycleStats cycle={activeCycle} workspaceSlug={workspaceSlug} projectId={projectId} /> workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
/>
<ActiveCycleStats workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -22,10 +22,11 @@ type TCyclesListItem = {
handleRemoveFromFavorites?: () => void; handleRemoveFromFavorites?: () => void;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
className?: string;
}; };
export const CyclesListItem: FC<TCyclesListItem> = observer((props) => { export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
const { cycleId, workspaceSlug, projectId } = props; const { cycleId, workspaceSlug, projectId, className = "" } = props;
// refs // refs
const parentRef = useRef(null); const parentRef = useRef(null);
// router // router
@ -83,6 +84,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
onItemClick={(e) => { onItemClick={(e) => {
if (cycleDetails.archived_at) openCycleOverview(e); if (cycleDetails.archived_at) openCycleOverview(e);
}} }}
className={className}
prependTitleElement={ prependTitleElement={
<CircularProgressIndicator size={30} percentage={progress} strokeWidth={3}> <CircularProgressIndicator size={30} percentage={progress} strokeWidth={3}>
{isCompleted ? ( {isCompleted ? (

View File

@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
import isEqual from "lodash/isEqual";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@ -199,14 +200,18 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const handleFiltersUpdate = useCallback( const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => { (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? []; let newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) { if (Array.isArray(value)) {
// this validation is majorly for the filter start_date, target_date custom if (key === "state") {
value.forEach((val) => { if (isEqual(newValues, value)) newValues = [];
if (!newValues.includes(val)) newValues.push(val); else newValues = value;
else newValues.splice(newValues.indexOf(val), 1); } else {
}); value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
}
} else { } else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value); else newValues.push(value);

View File

@ -103,7 +103,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
<div className="flex items-center gap-1 bg-green-500/20 text-green-700 rounded px-1.5 py-1"> <div className="flex items-center gap-1 bg-green-500/20 text-green-700 rounded px-1.5 py-1">
<Tooltip tooltipHeading="Title" tooltipContent={parentIssue.name} isMobile={isMobile}> <Tooltip tooltipHeading="Title" tooltipContent={parentIssue.name} isMobile={isMobile}>
<Link <Link
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`} href={`/${workspaceSlug}/projects/${parentIssue.project_id}/issues/${parentIssue?.id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-xs font-medium" className="text-xs font-medium"

View File

@ -56,7 +56,7 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
Sibling issues Sibling issues
</div> </div>
<IssueParentSiblings currentIssue={issue} parentIssue={parentIssue} /> <IssueParentSiblings workspaceSlug={workspaceSlug} currentIssue={issue} parentIssue={parentIssue} />
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null })} onClick={() => issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null })}

View File

@ -1,4 +1,5 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
// ui // ui
import { CustomMenu, LayersIcon } from "@plane/ui"; import { CustomMenu, LayersIcon } from "@plane/ui";
@ -6,15 +7,15 @@ import { CustomMenu, LayersIcon } from "@plane/ui";
import { useIssueDetail, useProject } from "@/hooks/store"; import { useIssueDetail, useProject } from "@/hooks/store";
type TIssueParentSiblingItem = { type TIssueParentSiblingItem = {
workspaceSlug: string;
issueId: string; issueId: string;
}; };
export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = (props) => { export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = observer((props) => {
const { issueId } = props; const { workspaceSlug, issueId } = props;
// hooks // hooks
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const { const {
peekIssue,
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
@ -27,7 +28,7 @@ export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = (props) => {
<> <>
<CustomMenu.MenuItem key={issueDetail.id}> <CustomMenu.MenuItem key={issueDetail.id}>
<Link <Link
href={`/${peekIssue?.workspaceSlug}/projects/${issueDetail?.project_id as string}/issues/${issueDetail.id}`} href={`/${workspaceSlug}/projects/${issueDetail?.project_id as string}/issues/${issueDetail.id}`}
className="flex items-center gap-2 py-2" className="flex items-center gap-2 py-2"
> >
<LayersIcon className="h-4 w-4" /> <LayersIcon className="h-4 w-4" />
@ -36,4 +37,4 @@ export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = (props) => {
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</> </>
); );
}; });

View File

@ -1,4 +1,5 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// components // components
@ -8,25 +9,25 @@ import { useIssueDetail } from "@/hooks/store";
import { IssueParentSiblingItem } from "./sibling-item"; import { IssueParentSiblingItem } from "./sibling-item";
export type TIssueParentSiblings = { export type TIssueParentSiblings = {
workspaceSlug: string;
currentIssue: TIssue; currentIssue: TIssue;
parentIssue: TIssue; parentIssue: TIssue;
}; };
export const IssueParentSiblings: FC<TIssueParentSiblings> = (props) => { export const IssueParentSiblings: FC<TIssueParentSiblings> = observer((props) => {
const { currentIssue, parentIssue } = props; const { workspaceSlug, currentIssue, parentIssue } = props;
// hooks // hooks
const { const {
peekIssue,
fetchSubIssues, fetchSubIssues,
subIssues: { subIssuesByIssueId }, subIssues: { subIssuesByIssueId },
} = useIssueDetail(); } = useIssueDetail();
const { isLoading } = useSWR( const { isLoading } = useSWR(
peekIssue && parentIssue && parentIssue.project_id parentIssue && parentIssue.project_id
? `ISSUE_PARENT_CHILD_ISSUES_${peekIssue?.workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}` ? `ISSUE_PARENT_CHILD_ISSUES_${workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}`
: null, : null,
peekIssue && parentIssue && parentIssue.project_id parentIssue && parentIssue.project_id
? () => fetchSubIssues(peekIssue?.workspaceSlug, parentIssue.project_id, parentIssue.id) ? () => fetchSubIssues(workspaceSlug, parentIssue.project_id, parentIssue.id)
: null : null
); );
@ -40,7 +41,10 @@ export const IssueParentSiblings: FC<TIssueParentSiblings> = (props) => {
</div> </div>
) : subIssueIds && subIssueIds.length > 0 ? ( ) : subIssueIds && subIssueIds.length > 0 ? (
subIssueIds.map( subIssueIds.map(
(issueId) => currentIssue.id != issueId && <IssueParentSiblingItem key={issueId} issueId={issueId} /> (issueId) =>
currentIssue.id != issueId && (
<IssueParentSiblingItem key={issueId} workspaceSlug={workspaceSlug} issueId={issueId} />
)
) )
) : ( ) : (
<div className="flex items-center gap-2 whitespace-nowrap px-1 py-1 text-left text-xs text-custom-text-200"> <div className="flex items-center gap-2 whitespace-nowrap px-1 py-1 text-left text-xs text-custom-text-200">
@ -49,4 +53,4 @@ export const IssueParentSiblings: FC<TIssueParentSiblings> = (props) => {
)} )}
</div> </div>
); );
}; });

View File

@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import isEqual from "lodash/isEqual";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@ -252,14 +253,18 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const handleFiltersUpdate = useCallback( const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => { (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? []; let newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) { if (Array.isArray(value)) {
// this validation is majorly for the filter start_date, target_date custom if (key === "state") {
value.forEach((val) => { if (isEqual(newValues, value)) newValues = [];
if (!newValues.includes(val)) newValues.push(val); else newValues = value;
else newValues.splice(newValues.indexOf(val), 1); } else {
}); value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
}
} else { } else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value); else newValues.push(value);

View File

@ -16,9 +16,11 @@ export const ProjectCardList = observer(() => {
// store hooks // store hooks
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { workspaceProjectIds, filteredProjectIds, getProjectById } = useProject(); const { workspaceProjectIds, filteredProjectIds, getProjectById, loader } = useProject();
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter(); const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();
if (!filteredProjectIds || !workspaceProjectIds || loader) return <ProjectsLoader />;
if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects) if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects)
return ( return (
<EmptyState <EmptyState
@ -30,8 +32,6 @@ export const ProjectCardList = observer(() => {
/> />
); );
if (!filteredProjectIds) return <ProjectsLoader />;
if (filteredProjectIds.length === 0) if (filteredProjectIds.length === 0)
return ( return (
<div className="grid h-full w-full place-items-center"> <div className="grid h-full w-full place-items-center">

View File

@ -1,8 +1,13 @@
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { createRoot } from "react-dom/client";
import { import {
MoreVertical, MoreVertical,
PenSquare, PenSquare,
@ -28,6 +33,7 @@ import {
ContrastIcon, ContrastIcon,
LayersIcon, LayersIcon,
setPromiseToast, setPromiseToast,
DropIndicator,
} from "@plane/ui"; } from "@plane/ui";
import { LeaveProjectModal, ProjectLogo, PublishProjectModal } from "@/components/project"; import { LeaveProjectModal, ProjectLogo, PublishProjectModal } from "@/components/project";
import { EUserProjectRoles } from "@/constants/project"; import { EUserProjectRoles } from "@/constants/project";
@ -36,17 +42,23 @@ import { cn } from "@/helpers/common.helper";
import { useAppTheme, useEventTracker, useProject } from "@/hooks/store"; import { useAppTheme, useEventTracker, useProject } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
import { HIGHLIGHT_CLASS, highlightIssueOnDrop } from "../issues/issue-layouts/utils";
// helpers // helpers
// components // components
type Props = { type Props = {
projectId: string; projectId: string;
provided?: DraggableProvided;
snapshot?: DraggableStateSnapshot;
handleCopyText: () => void; handleCopyText: () => void;
shortContextMenu?: boolean; handleOnProjectDrop?: (
sourceId: string | undefined,
destinationId: string | undefined,
shouldDropAtEnd: boolean
) => void;
projectListType: "JOINED" | "FAVORITES";
disableDrag?: boolean; disableDrag?: boolean;
disableDrop?: boolean;
isLastChild: boolean;
}; };
const navigation = (workspaceSlug: string, projectId: string) => [ const navigation = (workspaceSlug: string, projectId: string) => [
@ -89,7 +101,8 @@ const navigation = (workspaceSlug: string, projectId: string) => [
export const ProjectSidebarListItem: React.FC<Props> = observer((props) => { export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { projectId, provided, snapshot, handleCopyText, shortContextMenu = false, disableDrag } = props; const { projectId, handleCopyText, disableDrag, disableDrop, isLastChild, handleOnProjectDrop, projectListType } =
props;
// store hooks // store hooks
const { sidebarCollapsed: isCollapsed, toggleSidebar } = useAppTheme(); const { sidebarCollapsed: isCollapsed, toggleSidebar } = useAppTheme();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
@ -99,8 +112,12 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
const [publishModalOpen, setPublishModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false);
const [isMenuActive, setIsMenuActive] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined);
// refs // refs
const actionSectionRef = useRef<HTMLDivElement | null>(null); const actionSectionRef = useRef<HTMLDivElement | null>(null);
const projectRef = useRef<HTMLDivElement | null>(null);
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId: URLProjectId } = router.query; const { workspaceSlug, projectId: URLProjectId } = router.query;
@ -160,7 +177,97 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
} }
}; };
useEffect(() => {
const element = projectRef.current;
const dragHandleElement = dragHandleRef.current;
if (!element) return;
return combine(
draggable({
element,
canDrag: () => !disableDrag,
dragHandle: dragHandleElement ?? undefined,
getInitialData: () => ({ id: projectId, dragInstanceId: "PROJECTS" }),
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
},
onGenerateDragPreview: ({ nativeSetDragImage }) => {
// Add a custom drag image
setCustomNativeDragPreview({
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
render: ({ container }) => {
const root = createRoot(container);
root.render(
<div className="rounded flex items-center bg-custom-background-100 text-sm p-1 pr-2">
<div className="flex items-center h-7 w-5 grid place-items-center flex-shrink-0">
{project && <ProjectLogo logo={project?.logo_props} />}
</div>
<p className="truncate text-custom-sidebar-text-200">{project?.name}</p>
</div>
);
return () => root.unmount();
},
nativeSetDragImage,
});
},
}),
dropTargetForElements({
element,
canDrop: ({ source }) =>
!disableDrop && source?.data?.id !== projectId && source?.data?.dragInstanceId === "PROJECTS",
getData: ({ input, element }) => {
const data = { id: projectId };
// attach instruction for last in list
return attachInstruction(data, {
input,
element,
currentLevel: 0,
indentPerLevel: 0,
mode: isLastChild ? "last-in-group" : "standard",
});
},
onDrag: ({ self }) => {
const extractedInstruction = extractInstruction(self?.data)?.type;
// check if the highlight is to be shown above or below
setInstruction(
extractedInstruction
? extractedInstruction === "reorder-below" && isLastChild
? "DRAG_BELOW"
: "DRAG_OVER"
: undefined
);
},
onDragLeave: () => {
setInstruction(undefined);
},
onDrop: ({ self, source }) => {
setInstruction(undefined);
const extractedInstruction = extractInstruction(self?.data)?.type;
const currentInstruction = extractedInstruction
? extractedInstruction === "reorder-below" && isLastChild
? "DRAG_BELOW"
: "DRAG_OVER"
: undefined;
if (!currentInstruction) return;
const sourceId = source?.data?.id as string | undefined;
const destinationId = self?.data?.id as string | undefined;
handleOnProjectDrop && handleOnProjectDrop(sourceId, destinationId, currentInstruction === "DRAG_BELOW");
highlightIssueOnDrop(`sidebar-${sourceId}-${projectListType}`);
},
})
);
}, [projectRef?.current, dragHandleRef?.current, projectId, isLastChild, projectListType, handleOnProjectDrop]);
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
useOutsideClickDetector(projectRef, () => projectRef?.current?.classList?.remove(HIGHLIGHT_CLASS));
if (!project) return null; if (!project) return null;
@ -168,48 +275,47 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
<> <>
<PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} /> <PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} />
<LeaveProjectModal project={project} isOpen={leaveProjectModalOpen} onClose={handleLeaveProjectModalClose} /> <LeaveProjectModal project={project} isOpen={leaveProjectModalOpen} onClose={handleLeaveProjectModalClose} />
<Disclosure key={`${project.id} ${URLProjectId}`} defaultOpen={URLProjectId === project.id}> <Disclosure key={`${project.id}_${URLProjectId}`} ref={projectRef} defaultOpen={URLProjectId === project.id}>
{({ open }) => ( {({ open }) => (
<> <div
id={`sidebar-${projectId}-${projectListType}`}
className={cn("rounded relative", { "bg-custom-sidebar-background-80 opacity-60": isDragging })}
>
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
<div <div
className={cn( className={cn(
"group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80", "group relative flex w-full items-center rounded-md py-1 text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80",
{ {
"opacity-60": snapshot?.isDragging,
"bg-custom-sidebar-background-80": isMenuActive, "bg-custom-sidebar-background-80": isMenuActive,
"pl-8": disableDrag,
} }
)} )}
> >
{provided && !disableDrag && ( {!disableDrag && (
<Tooltip <Tooltip
isMobile={isMobile} isMobile={isMobile}
tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"} tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"}
position="top-right" position="top-right"
disabled={isDragging}
> >
<button <button
type="button" type="button"
className={cn( className={cn(
"absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400", "flex opacity-0 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 ml-2",
{ {
"group-hover:flex": !isCollapsed, "group-hover:opacity-100": !isCollapsed,
"cursor-not-allowed opacity-60": project.sort_order === null, "cursor-not-allowed opacity-60": project.sort_order === null,
flex: isMenuActive, flex: isMenuActive,
} }
)} )}
{...provided?.dragHandleProps} ref={dragHandleRef}
> >
<MoreVertical className="h-3.5" /> <MoreVertical className="-ml-3 h-3.5" />
<MoreVertical className="-ml-5 h-3.5" /> <MoreVertical className="-ml-5 h-3.5" />
</button> </button>
</Tooltip> </Tooltip>
)} )}
<Tooltip <Tooltip tooltipContent={`${project.name}`} position="right" disabled={!isCollapsed} isMobile={isMobile}>
tooltipContent={`${project.name}`}
position="right"
className="ml-2"
disabled={!isCollapsed}
isMobile={isMobile}
>
<Disclosure.Button <Disclosure.Button
as="div" as="div"
className={cn( className={cn(
@ -220,11 +326,11 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
)} )}
> >
<div <div
className={cn("flex w-full flex-grow items-center gap-1 truncate", { className={cn("flex w-full flex-grow items-center gap-1 truncate -ml-1", {
"justify-center": isCollapsed, "justify-center": isCollapsed,
})} })}
> >
<div className="h-7 w-7 grid place-items-center flex-shrink-0"> <div className="h-7 w-5 grid place-items-center flex-shrink-0">
<ProjectLogo logo={project.logo_props} /> <ProjectLogo logo={project.logo_props} />
</div> </div>
{!isCollapsed && <p className="truncate text-custom-sidebar-text-200">{project.name}</p>} {!isCollapsed && <p className="truncate text-custom-sidebar-text-200">{project.name}</p>}
@ -380,7 +486,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
})} })}
</Disclosure.Panel> </Disclosure.Panel>
</Transition> </Transition>
</> {isLastChild && <DropIndicator isVisible={instruction === "DRAG_BELOW"} />}
</div>
)} )}
</Disclosure> </Disclosure>
</> </>

View File

@ -1,5 +1,6 @@
import { useState, FC, useRef, useEffect } from "react"; import { useState, FC, useRef, useEffect } from "react";
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { ChevronDown, ChevronRight, Plus } from "lucide-react"; import { ChevronDown, ChevronRight, Plus } from "lucide-react";
@ -54,21 +55,28 @@ export const ProjectSidebarList: FC = observer(() => {
}); });
}; };
const onDragEnd = (result: DropResult) => { const handleOnProjectDrop = (
const { source, destination, draggableId } = result; sourceId: string | undefined,
if (!destination || !workspaceSlug) return; destinationId: string | undefined,
if (source.index === destination.index) return; shouldDropAtEnd: boolean
) => {
if (!sourceId || !destinationId || !workspaceSlug) return;
if (sourceId === destinationId) return;
const joinedProjectsList: IProject[] = []; const joinedProjectsList: IProject[] = [];
joinedProjects.map((projectId) => { joinedProjects.map((projectId) => {
const projectDetails = getProjectById(projectId); const projectDetails = getProjectById(projectId);
if (projectDetails) joinedProjectsList.push(projectDetails); if (projectDetails) joinedProjectsList.push(projectDetails);
}); });
const sourceIndex = joinedProjects.indexOf(sourceId);
const destinationIndex = shouldDropAtEnd ? joinedProjects.length : joinedProjects.indexOf(destinationId);
if (joinedProjectsList.length <= 0) return; if (joinedProjectsList.length <= 0) return;
const updatedSortOrder = orderJoinedProjects(source.index, destination.index, draggableId, joinedProjectsList); const updatedSortOrder = orderJoinedProjects(sourceIndex, destinationIndex, sourceId, joinedProjectsList);
if (updatedSortOrder != undefined) if (updatedSortOrder != undefined)
updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => { updateProjectView(workspaceSlug.toString(), sourceId, { sort_order: updatedSortOrder }).catch(() => {
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
title: "Error!", title: "Error!",
@ -98,7 +106,21 @@ export const ProjectSidebarList: FC = observer(() => {
currentContainerRef.removeEventListener("scroll", handleScroll); currentContainerRef.removeEventListener("scroll", handleScroll);
} }
}; };
}, []); }, [containerRef]);
useEffect(() => {
const element = containerRef.current;
if (!element) return;
return combine(
autoScrollForElements({
element,
canScroll: ({ source }) => source?.data?.dragInstanceId === "PROJECTS",
getAllowedAxis: () => "vertical",
})
);
}, [containerRef]);
return ( return (
<> <>
@ -123,156 +145,117 @@ export const ProjectSidebarList: FC = observer(() => {
} }
)} )}
> >
<DragDropContext onDragEnd={onDragEnd}> <div>
<Droppable droppableId="favorite-projects"> {favoriteProjects && favoriteProjects.length > 0 && (
{(provided) => ( <Disclosure as="div" className="flex flex-col" defaultOpen>
<div ref={provided.innerRef} {...provided.droppableProps}> {({ open }) => (
{favoriteProjects && favoriteProjects.length > 0 && ( <>
<Disclosure as="div" className="flex flex-col" defaultOpen> {!isCollapsed && (
{({ open }) => ( <div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
<> <Disclosure.Button
{!isCollapsed && ( as="button"
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"> type="button"
<Disclosure.Button className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
as="button" >
type="button" Favorites
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80" {open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
> </Disclosure.Button>
Favorites {isAuthorizedUser && (
{open ? ( <button
<ChevronDown className="h-3.5 w-3.5" /> className="opacity-0 group-hover:opacity-100"
) : ( onClick={() => {
<ChevronRight className="h-3.5 w-3.5" /> setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
)} setIsFavoriteProjectCreate(true);
</Disclosure.Button> setIsProjectModalOpen(true);
{isAuthorizedUser && ( }}
<button
className="opacity-0 group-hover:opacity-100"
onClick={() => {
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
setIsFavoriteProjectCreate(true);
setIsProjectModalOpen(true);
}}
>
<Plus className="h-3 w-3" />
</button>
)}
</div>
)}
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
> >
<Disclosure.Panel as="div" className="space-y-2"> <Plus className="h-3 w-3" />
{favoriteProjects.map((projectId, index) => ( </button>
<Draggable )}
key={projectId} </div>
draggableId={projectId} )}
index={index} <Transition
// FIXME refactor the Draggable to a different component enter="transition duration-100 ease-out"
//isDragDisabled={!project.is_member} enterFrom="transform scale-95 opacity-0"
> enterTo="transform scale-100 opacity-100"
{(provided, snapshot) => ( leave="transition duration-75 ease-out"
<div ref={provided.innerRef} {...provided.draggableProps}> leaveFrom="transform scale-100 opacity-100"
<ProjectSidebarListItem leaveTo="transform scale-95 opacity-0"
key={projectId} >
projectId={projectId} <Disclosure.Panel as="div" className="space-y-2">
provided={provided} {favoriteProjects.map((projectId, index) => (
snapshot={snapshot} <ProjectSidebarListItem
handleCopyText={() => handleCopyText(projectId)} key={projectId}
shortContextMenu projectId={projectId}
disableDrag handleCopyText={() => handleCopyText(projectId)}
/> projectListType="FAVORITES"
</div> disableDrag
)} disableDrop
</Draggable> isLastChild={index === favoriteProjects.length - 1}
))} />
</Disclosure.Panel> ))}
</Transition> </Disclosure.Panel>
{provided.placeholder} </Transition>
</> </>
)} )}
</Disclosure> </Disclosure>
)} )}
</div> </div>
)} <div>
</Droppable> {joinedProjects && joinedProjects.length > 0 && (
</DragDropContext> <Disclosure as="div" className="flex flex-col" defaultOpen>
<DragDropContext onDragEnd={onDragEnd}> {({ open }) => (
<Droppable droppableId="joined-projects"> <>
{(provided) => ( {!isCollapsed && (
<div ref={provided.innerRef} {...provided.droppableProps}> <div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
{joinedProjects && joinedProjects.length > 0 && ( <Disclosure.Button
<Disclosure as="div" className="flex flex-col" defaultOpen> as="button"
{({ open }) => ( type="button"
<> className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
{!isCollapsed && ( >
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"> Your projects
<Disclosure.Button {open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
as="button" </Disclosure.Button>
type="button" {isAuthorizedUser && (
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80" <button
> className="opacity-0 group-hover:opacity-100"
Your projects onClick={() => {
{open ? ( setTrackElement("Sidebar");
<ChevronDown className="h-3.5 w-3.5" /> setIsFavoriteProjectCreate(false);
) : ( setIsProjectModalOpen(true);
<ChevronRight className="h-3.5 w-3.5" /> }}
)}
</Disclosure.Button>
{isAuthorizedUser && (
<button
className="opacity-0 group-hover:opacity-100"
onClick={() => {
setTrackElement("Sidebar");
setIsFavoriteProjectCreate(false);
setIsProjectModalOpen(true);
}}
>
<Plus className="h-3 w-3" />
</button>
)}
</div>
)}
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
> >
<Disclosure.Panel as="div" className="space-y-2"> <Plus className="h-3 w-3" />
{joinedProjects.map((projectId, index) => ( </button>
<Draggable key={projectId} draggableId={projectId} index={index}> )}
{(provided, snapshot) => ( </div>
<div ref={provided.innerRef} {...provided.draggableProps}> )}
<ProjectSidebarListItem <Transition
key={projectId} enter="transition duration-100 ease-out"
projectId={projectId} enterFrom="transform scale-95 opacity-0"
provided={provided} enterTo="transform scale-100 opacity-100"
snapshot={snapshot} leave="transition duration-75 ease-out"
handleCopyText={() => handleCopyText(projectId)} leaveFrom="transform scale-100 opacity-100"
/> leaveTo="transform scale-95 opacity-0"
</div> >
)} <Disclosure.Panel as="div">
</Draggable> {joinedProjects.map((projectId, index) => (
))} <ProjectSidebarListItem
</Disclosure.Panel> key={projectId}
</Transition> projectId={projectId}
{provided.placeholder} projectListType="JOINED"
</> handleCopyText={() => handleCopyText(projectId)}
)} isLastChild={index === joinedProjects.length - 1}
</Disclosure> handleOnProjectDrop={handleOnProjectDrop}
)} />
</div> ))}
)} </Disclosure.Panel>
</Droppable> </Transition>
</DragDropContext> </>
)}
</Disclosure>
)}
</div>
{isAuthorizedUser && joinedProjects && joinedProjects.length === 0 && ( {isAuthorizedUser && joinedProjects && joinedProjects.length === 0 && (
<button <button

View File

@ -29,23 +29,16 @@ export const orderJoinedProjects = (
// updating project at the top of the project // updating project at the top of the project
const currentSortOrder = joinedProjects[destinationIndex].sort_order || 0; const currentSortOrder = joinedProjects[destinationIndex].sort_order || 0;
updatedSortOrder = currentSortOrder - sortOrderDefaultValue; updatedSortOrder = currentSortOrder - sortOrderDefaultValue;
} else if (destinationIndex === joinedProjects.length - 1) { } else if (destinationIndex === joinedProjects.length) {
// updating project at the bottom of the project // updating project at the bottom of the project
const currentSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0; const currentSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0;
updatedSortOrder = currentSortOrder + sortOrderDefaultValue; updatedSortOrder = currentSortOrder + sortOrderDefaultValue;
} else { } else {
// updating project in the middle of the project // updating project in the middle of the project
if (sourceIndex > destinationIndex) { const destinationTopProjectSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0;
const destinationTopProjectSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0; const destinationBottomProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0;
const destinationBottomProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0; const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2; updatedSortOrder = updatedValue;
updatedSortOrder = updatedValue;
} else {
const destinationTopProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0;
const destinationBottomProjectSortOrder = joinedProjects[destinationIndex + 1].sort_order || 0;
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
updatedSortOrder = updatedValue;
}
} }
return updatedSortOrder; return updatedSortOrder;

9
web/instrumentation.ts Normal file
View File

@ -0,0 +1,9 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}

View File

@ -82,12 +82,40 @@ const nextConfig = {
}, },
}; };
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) { const sentryConfig = {
module.exports = withSentryConfig( // For all available options, see:
nextConfig, // https://github.com/getsentry/sentry-webpack-plugin#options
{ silent: true, authToken: process.env.SENTRY_AUTH_TOKEN }, org: process.env.SENTRY_ORG_ID || "plane-hq",
{ hideSourceMaps: true } project: process.env.SENTRY_PROJECT_ID || "plane-web",
); authToken: process.env.SENTRY_AUTH_TOKEN,
// Only print logs for uploading source maps in CI
silent: true,
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
}
if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) {
module.exports = withSentryConfig(nextConfig, sentryConfig);
} else { } else {
module.exports = nextConfig; module.exports = nextConfig;
} }

View File

@ -17,7 +17,6 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blueprintjs/popover2": "^1.13.3", "@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^2.0.3", "@headlessui/react": "^2.0.3",
"@hello-pangea/dnd": "^16.3.0",
"@nivo/bar": "0.80.0", "@nivo/bar": "0.80.0",
"@nivo/calendar": "0.80.0", "@nivo/calendar": "0.80.0",
"@nivo/core": "0.80.0", "@nivo/core": "0.80.0",
@ -32,7 +31,7 @@
"@plane/types": "*", "@plane/types": "*",
"@plane/ui": "*", "@plane/ui": "*",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@sentry/nextjs": "^7.108.0", "@sentry/nextjs": "^8",
"axios": "^1.1.3", "axios": "^1.1.3",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",

View File

@ -1,18 +0,0 @@
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

View File

@ -0,0 +1,31 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
replaysOnErrorSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
integrations: [
Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
maskAllText: true,
blockAllMedia: true,
}),
],
});

View File

@ -1,18 +0,0 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever middleware or an Edge route handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

17
web/sentry.edge.config.ts Normal file
View File

@ -0,0 +1,17 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@ -4,15 +4,16 @@
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({ Sentry.init({
dsn: SENTRY_DSN, dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development", environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control // Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0, tracesSampleRate: 1,
// ...
// Note: if you want to override the automatic release value, do not set a // Setting this option to true will print useful information to the console while you're setting up Sentry.
// `release` value here - use the environment variable `SENTRY_RELEASE`, so debug: false,
// that it will also get attached to your source maps
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: process.env.NODE_ENV === 'development',
}); });

View File

@ -14,6 +14,7 @@ import { RootStore } from "../root.store";
export interface IProjectStore { export interface IProjectStore {
// observables // observables
loader: boolean;
projectMap: { projectMap: {
[projectId: string]: IProject; // projectId: project Info [projectId: string]: IProject; // projectId: project Info
}; };
@ -47,6 +48,7 @@ export interface IProjectStore {
export class ProjectStore implements IProjectStore { export class ProjectStore implements IProjectStore {
// observables // observables
loader: boolean = false;
projectMap: { projectMap: {
[projectId: string]: IProject; // projectId: project Info [projectId: string]: IProject; // projectId: project Info
} = {}; } = {};
@ -62,6 +64,7 @@ export class ProjectStore implements IProjectStore {
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
// observables // observables
loader: observable.ref,
projectMap: observable, projectMap: observable,
// computed // computed
filteredProjectIds: computed, filteredProjectIds: computed,
@ -208,15 +211,18 @@ export class ProjectStore implements IProjectStore {
*/ */
fetchProjects = async (workspaceSlug: string) => { fetchProjects = async (workspaceSlug: string) => {
try { try {
this.loader = true;
const projectsResponse = await this.projectService.getProjects(workspaceSlug); const projectsResponse = await this.projectService.getProjects(workspaceSlug);
runInAction(() => { runInAction(() => {
projectsResponse.forEach((project) => { projectsResponse.forEach((project) => {
set(this.projectMap, [project.id], project); set(this.projectMap, [project.id], project);
}); });
this.loader = false;
}); });
return projectsResponse; return projectsResponse;
} catch (error) { } catch (error) {
console.log("Failed to fetch project from workspace store"); console.log("Failed to fetch project from workspace store");
this.loader = false;
throw error; throw error;
} }
}; };

2948
yarn.lock

File diff suppressed because it is too large Load Diff