mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: resolved merge conflicts
This commit is contained in:
commit
638662d33a
@ -1,4 +1,5 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
trailingSlash: true,
|
trailingSlash: true,
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
|
@ -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
3
space/.gitignore
vendored
@ -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
9
space/instrumentation.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
|
||||||
});
|
|
31
space/sentry.client.config.ts
Normal file
31
space/sentry.client.config.ts
Normal 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,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
@ -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
space/sentry.edge.config.ts
Normal file
17
space/sentry.edge.config.ts
Normal 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,
|
||||||
|
});
|
@ -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',
|
||||||
});
|
});
|
10
turbo.json
10
turbo.json
@ -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": {
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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);
|
||||||
|
@ -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"
|
||||||
|
@ -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 })}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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
|
||||||
|
@ -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
9
web/instrumentation.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
|
||||||
});
|
|
31
web/sentry.client.config.ts
Normal file
31
web/sentry.client.config.ts
Normal 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,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
@ -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
17
web/sentry.edge.config.ts
Normal 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,
|
||||||
|
});
|
@ -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',
|
||||||
});
|
});
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user