Compare commits

...

17 Commits

Author SHA1 Message Date
Satish Gandham 733b725b5e Extract utility functions out of Storage class 2024-12-24 16:17:00 +05:30
Satish Gandham 3bee051ee9 Fix typos and add comments 2024-12-24 14:32:53 +05:30
Satish Gandham 6b902d458e Add comments to stroage class 2024-12-18 16:00:17 +05:30
sriram veeraghanta 51ca353577 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-12-18 14:58:13 +05:30
sriram veeraghanta 881c744eb9 fix: build errors 2024-12-18 14:57:59 +05:30
Bavisetti Narayan ec41ae61b4 chore: removed the deleted votes and reaction (#6218) 2024-12-18 14:54:03 +05:30
Aaryan Khandelwal 5773c2bde3 chore: gif support for editor (#6219) 2024-12-18 13:17:05 +05:30
M. Palanikannan e33bae2125 [PE-92] fix: removing readonly collaborative document editor (#6209)
* fix: removing readonly editor

* fix: sync state

* fix: indexeddb sync loader added

* fix: remove node error fixed

* style: page title and checkbox

* chore: removing the syncing logic

* revert: is editable check removed in display message

* fix: editable field optional

* fix: editable removed as optional prop

* fix: extra options import fix

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2024-12-18 12:58:18 +05:30
Aaryan Khandelwal 580c4b1930 refactor: remove cn helper function from the editor package (#6217) 2024-12-18 12:22:14 +05:30
Vamsi Krishna ddd4b51b4e fix: labels empty state for drop down (#6216) 2024-12-17 19:14:10 +05:30
Satish Gandham ede4aad55b - Do not clear temp files that are locked. (#6214)
- Handle edge cases in sync workspace
2024-12-17 17:46:24 +05:30
Akshita Goyal 1a715c98b2 chore: added common component for project activity (#6212)
* chore: added common component for project activity

* fix: added enum

* fix: added enum for initiatives
2024-12-17 17:02:59 +05:30
Vamsi Krishna 8e6d885731 [WEB-2678]feat: added functionality to add labels directly from dropdown (#6211)
* enhancement:added functionality to add features directly from dropdown

* fix: fixed import order

* fix: fixed lint errors
2024-12-17 14:29:56 +05:30
Prateek Shourya 4507802aba refactor: enhance workspace and project wrapper modularity (#6207) 2024-12-16 19:01:37 +05:30
Anmol Singh Bhatia 438cc33046 code refactor and improvement (#6203)
* chore: package code refactoring

* chore: component restructuring and refactor

* chore: comment create improvement
2024-12-16 17:24:50 +05:30
Vamsi Krishna 442b0fd7e5 fix: added project sync after transfer issues (#6200) 2024-12-16 15:15:48 +05:30
Dancia 1119b9dc36 Updated README.md (#6182)
* Updated README.md

* minor fixes

* minor fixes
2024-12-16 14:33:08 +05:30
248 changed files with 2681 additions and 1361 deletions
+73 -50
View File
@@ -5,9 +5,7 @@
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_logo_.webp" alt="Plane Logo" width="70">
</a>
</p>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Open-source project management that unlocks customer value</b></p>
<h1 align="center"><b>Plane</b></h1>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
@@ -44,79 +42,85 @@ Meet [Plane](https://dub.sh/plane-website-readme), an open-source project manage
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
## Installation
## 🚀 Installation
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account.
Getting started with Plane is simple. Choose the setup that works best for you:
If you would like to self-host Plane, please see our [deployment guide](https://docs.plane.so/docker-compose).
- **Plane Cloud**
Sign up for a free account on [Plane Cloud](https://app.plane.so)—it's the fastest way to get up and running without worrying about infrastructure.
- **Self-host Plane**
Prefer full control over your data and infrastructure? Install and run Plane on your own servers. Follow our detailed [deployment guides](https://developers.plane.so/self-hosting/overview) to get started.
| Installation methods | Docs link |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://developers.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://developers.plane.so/self-hosting/methods/kubernetes) |
`Instance admins` can configure instance settings with [God-mode](https://docs.plane.so/instance-admin).
`Instance admins` can manage and customize settings using [God mode](https://developers.plane.so/self-hosting/govern/instance-admin).
## 🚀 Features
## 🌟 Features
- **Issues**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking.
- **Issues**
Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues.
- **Cycles**:
Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features.
- **Cycles**
Maintain your teams momentum with Cycles. Track progress effortlessly using burn-down charts and other insightful tools.
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily.
- **Modules**
Simplify complex projects by dividing them into smaller, manageable modules.
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
- **Views**
Customize your workflow by creating filters to display only the most relevant issues. Save and share these views with ease.
- **Pages**: Plane pages, equipped with AI and a rich text editor, let you jot down your thoughts on the fly. Format your text, upload images, hyperlink, or sync your existing ideas into an actionable item or issue.
- **Pages**
Capture and organize ideas using Plane Pages, complete with AI capabilities and a rich text editor. Format text, insert images, add hyperlinks, or convert your notes into actionable items.
- **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work.
- **Analytics**
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
## 🛠️ Quick start for contributors
> Development system must have docker engine installed and running.
## 🛠️ Local development
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute -
### Pre-requisites
- Ensure Docker Engine is installed and running.
1. Clone the code locally using:
### Development setup
Setting up your local environment is simple and straightforward. Follow these steps to get started:
1. Clone the repository:
```
git clone https://github.com/makeplane/plane.git
```
2. Switch to the code folder:
2. Navigate to the project folder:
```
cd plane
```
3. Create your feature or fix branch you plan to work on using:
3. Create a new branch for your feature or fix:
```
git checkout -b <feature-branch-name>
```
4. Open terminal and run:
4. Run the setup script in the terminal:
```
./setup.sh
```
5. Open the code on VSCode or similar equivalent IDE.
6. Review the `.env` files available in various folders.
Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system.
7. Run the docker command to initiate services:
5. Open the project in an IDE such as VS Code.
6. Review the `.env` files in the relevant folders. Refer to [Environment Setup](./ENV_SETUP.md) for details on the environment variables used.
7. Start the services using Docker:
```
docker compose -f docker-compose-local.yml up -d
```
You are ready to make changes to the code. Do not forget to refresh the browser (in case it does not auto-reload).
Thats it! Youre all set to begin coding. Remember to refresh your browser if changes dont auto-reload. Happy contributing! 🎉
Thats it!
## ❤️ Community
The Plane community can be found on [GitHub Discussions](https://github.com/orgs/makeplane/discussions), and our [Discord server](https://discord.com/invite/A92xrEGCge). Our [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community chanels.
Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects.
### Repo Activity
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
## Built with
[![Next JS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white)](https://nextjs.org/)<br/>
[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=green)](https://www.djangoproject.com/)<br/>
[![Node JS](https://img.shields.io/badge/node.js-339933?style=for-the-badge&logo=Node.js&logoColor=white)](https://nodejs.org/en)
## 📸 Screenshots
@@ -165,7 +169,7 @@ Ask questions, report bugs, join discussions, voice ideas, make feature requests
</a>
</p>
</p>
<p>
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/w2okwbtu2/Drive_LlfeY4xn3.png?updatedAt=1709298837917"
@@ -176,23 +180,42 @@ Ask questions, report bugs, join discussions, voice ideas, make feature requests
</p>
</p>
## ⛓️ Security
## 📝 Documentation
Explore Plane's [product documentation](https://docs.plane.so/) and [developer documentation](https://developers.plane.so/) to learn about features, setup, and usage.
If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports.
## ❤️ Community
Email squawk@plane.so to disclose any security vulnerabilities.
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Discord server](https://discord.com/invite/A92xrEGCge). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
## ❤️ Contribute
Feel free to ask questions, report bugs, participate in discussions, share ideas, request features, or showcase your projects. Wed love to hear from you!
There are many ways to contribute to Plane, including:
## 🛡️ Security
- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components.
- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features.
- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)!
- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support.
If you discover a security vulnerability in Plane, please report it responsibly instead of opening a public issue. We take all legitimate reports seriously and will investigate them promptly. See [Security policy](https://github.com/makeplane/plane/blob/master/SECURITY.md) for more info.
To disclose any security issues, please email us at security@plane.so.
## 🤝 Contributing
There are many ways you can contribute to Plane:
- Report [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) or submit [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+).
- Review the [documentation](https://docs.plane.so/) and submit [pull requests](https://github.com/makeplane/docs) to improve it—whether it's fixing typos or adding new content.
- Talk or write about Plane or any other ecosystem integration and [let us know](https://discord.com/invite/A92xrEGCge)!
- Show your support by upvoting [popular feature requests](https://github.com/makeplane/plane/issues).
Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
### Repo activity
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
### We couldn't have done this without you.
<a href="https://github.com/makeplane/plane/graphs/contributors">
<img src="https://contrib.rocks/image?repo=makeplane/plane" />
</a>
## License
This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/makeplane/plane/blob/master/LICENSE.txt).
+7 -1
View File
@@ -126,7 +126,13 @@ class UserAssetsV2Endpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
if type not in allowed_types:
return Response(
{
+4 -2
View File
@@ -91,6 +91,7 @@ def issue_on_results(issues, group_by, sub_group_by):
Case(
When(
votes__isnull=False,
votes__deleted_at__isnull=True,
then=JSONObject(
vote=F("votes__vote"),
actor_details=JSONObject(
@@ -117,13 +118,14 @@ def issue_on_results(issues, group_by, sub_group_by):
default=None,
output_field=JSONField(),
),
filter=Q(votes__isnull=False),
filter=Q(votes__isnull=False,votes__deleted_at__isnull=True),
distinct=True,
),
reaction_items=ArrayAgg(
Case(
When(
issue_reactions__isnull=False,
issue_reactions__deleted_at__isnull=True,
then=JSONObject(
reaction=F("issue_reactions__reaction"),
actor_details=JSONObject(
@@ -150,7 +152,7 @@ def issue_on_results(issues, group_by, sub_group_by):
default=None,
output_field=JSONField(),
),
filter=Q(issue_reactions__isnull=False),
filter=Q(issue_reactions__isnull=False, issue_reactions__deleted_at__isnull=True),
distinct=True,
),
).values(*required_fields, "vote_items", "reaction_items")
+7 -1
View File
@@ -86,7 +86,13 @@ class EntityAssetEndpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = ["image/jpeg", "image/png", "image/webp"]
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
if type not in allowed_types:
return Response(
{
+12 -2
View File
@@ -701,6 +701,7 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
Case(
When(
votes__isnull=False,
votes__deleted_at__isnull=True,
then=JSONObject(
vote=F("votes__vote"),
actor_details=JSONObject(
@@ -732,7 +733,11 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
output_field=JSONField(),
),
filter=Case(
When(votes__isnull=False, then=True),
When(
votes__isnull=False,
votes__deleted_at__isnull=True,
then=True,
),
default=False,
output_field=JSONField(),
),
@@ -742,6 +747,7 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
Case(
When(
issue_reactions__isnull=False,
issue_reactions__deleted_at__isnull=True,
then=JSONObject(
reaction=F("issue_reactions__reaction"),
actor_details=JSONObject(
@@ -775,7 +781,11 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
output_field=JSONField(),
),
filter=Case(
When(issue_reactions__isnull=False, then=True),
When(
issue_reactions__isnull=False,
issue_reactions__deleted_at__isnull=True,
then=True,
),
default=False,
output_field=JSONField(),
),
+5
View File
@@ -39,3 +39,8 @@ export enum EServerGroupByToFilterOptions {
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
-2
View File
@@ -57,7 +57,6 @@
"@tiptap/starter-kit": "^2.1.13",
"@tiptap/suggestion": "^2.0.13",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"highlight.js": "^11.8.0",
"jsx-dom-cjs": "^8.0.3",
"linkifyjs": "^4.1.3",
@@ -66,7 +65,6 @@
"prosemirror-codemark": "^0.4.2",
"prosemirror-utils": "^1.2.2",
"react-moveable": "^0.54.2",
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.9",
"uuid": "^10.0.0",
@@ -19,6 +19,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editable,
editorClassName = "",
embedHandler,
fileHandler,
@@ -44,8 +45,8 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
// use document editor
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
onTransaction,
disabledExtensions,
editable,
editorClassName,
embedHandler,
extensions,
@@ -54,6 +55,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
handleEditorReady,
id,
mentionHandler,
onTransaction,
placeholder,
realtimeConfig,
serverHandler,
@@ -1,81 +0,0 @@
import { forwardRef, MutableRefObject } from "react";
// components
import { DocumentContentLoader, PageRenderer } from "@/components/editors";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// extensions
import { IssueWidget } from "@/extensions";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useReadOnlyCollaborativeEditor } from "@/hooks/use-read-only-collaborative-editor";
// types
import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/types";
const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOnlyEditor) => {
const {
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
embedHandler,
fileHandler,
forwardedRef,
handleEditorReady,
id,
mentionHandler,
realtimeConfig,
serverHandler,
user,
} = props;
const extensions = [];
if (embedHandler?.issue) {
extensions.push(
IssueWidget({
widgetCallback: embedHandler.issue.widgetCallback,
})
);
}
const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({
disabledExtensions,
editorClassName,
extensions,
fileHandler,
forwardedRef,
handleEditorReady,
id,
mentionHandler,
realtimeConfig,
serverHandler,
user,
});
const editorContainerClassName = getEditorClassNames({
containerClassName,
});
if (!editor) return null;
if (!hasServerSynced && !hasServerConnectionFailed) return <DocumentContentLoader />;
return (
<PageRenderer
displayConfig={displayConfig}
id={id}
editor={editor}
editorContainerClassName={editorContainerClassName}
/>
);
};
const CollaborativeDocumentReadOnlyEditorWithRef = forwardRef<
EditorReadOnlyRefApi,
ICollaborativeDocumentReadOnlyEditor
>((props, ref) => (
<CollaborativeDocumentReadOnlyEditor {...props} forwardedRef={ref as MutableRefObject<EditorReadOnlyRefApi | null>} />
));
CollaborativeDocumentReadOnlyEditorWithRef.displayName = "CollaborativeDocumentReadOnlyEditorWithRef";
export { CollaborativeDocumentReadOnlyEditorWithRef };
@@ -1,5 +1,4 @@
export * from "./collaborative-editor";
export * from "./collaborative-read-only-editor";
export * from "./loader";
export * from "./page-renderer";
export * from "./read-only-editor";
@@ -140,10 +140,10 @@ export const PageRenderer = (props: IPageRenderer) => {
>
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
{editor.isEditable && (
<>
<div>
<BlockMenu editor={editor} />
<AIFeaturesMenu menu={aiHandler?.menu} />
</>
</div>
)}
</EditorContainer>
</div>
@@ -1,9 +1,9 @@
import { FC, ReactNode } from "react";
import { Editor } from "@tiptap/react";
// plane utils
import { cn } from "@plane/utils";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// helpers
import { cn } from "@/helpers/common";
// types
import { TDisplayConfig } from "@/types";
@@ -38,6 +38,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
} = props;
const editor = useEditor({
editable: true,
disabledExtensions,
editorClassName,
enableHistory: true,
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import tippy, { Instance } from "tippy.js";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
// types
import { TAIHandler } from "@/types";
@@ -1,9 +1,9 @@
import { Editor } from "@tiptap/core";
import { AlignCenter, AlignLeft, AlignRight, LucideIcon } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// components
import { TextAlignItem } from "@/components/menus";
// helpers
import { cn } from "@/helpers/common";
// types
import { TEditorCommands } from "@/types";
@@ -1,10 +1,11 @@
import { Dispatch, FC, SetStateAction } from "react";
import { Editor } from "@tiptap/react";
import { ALargeSmall, Ban } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// constants
import { COLORS_LIST } from "@/constants/common";
// helpers
import { cn } from "@/helpers/common";
import { BackgroundColorItem, TextColorItem } from "../menu-items";
type Props = {
@@ -1,8 +1,10 @@
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
import { Editor } from "@tiptap/core";
import { Check, Link, Trash } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn, isValidHttpUrl } from "@/helpers/common";
import { isValidHttpUrl } from "@/helpers/common";
import { setLinkEditor, unsetLinkEditor } from "@/helpers/editor-commands";
type Props = {
@@ -1,6 +1,8 @@
import { Dispatch, FC, SetStateAction } from "react";
import { Editor } from "@tiptap/react";
import { Check, ChevronDown } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// components
import {
BulletListItem,
@@ -17,8 +19,6 @@ import {
HeadingSixItem,
EditorMenuItem,
} from "@/components/menus";
// helpers
import { cn } from "@/helpers/common";
// types
import { TEditorCommands } from "@/types";
@@ -1,5 +1,7 @@
import { FC, useEffect, useState } from "react";
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react";
// plane utils
import { cn } from "@plane/utils";
// components
import {
BoldItem,
@@ -13,8 +15,6 @@ import {
} from "@/components/menus";
// extensions
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
// helpers
import { cn } from "@/helpers/common";
// local components
import { TextAlignmentSelector } from "./alignment-selector";
@@ -5,3 +5,6 @@ export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = {
fontSize: "large-font",
fontStyle: "sans-serif",
};
export const ACCEPTED_FILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"];
export const ACCEPTED_FILE_EXTENSIONS = ACCEPTED_FILE_MIME_TYPES.map((type) => `.${type.split("/")[1]}`);
@@ -1,8 +1,8 @@
import { Ban, ChevronDown } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// constants
import { COLORS_LIST } from "@/constants/common";
// helpers
import { cn } from "@/helpers/common";
type Props = {
disabled: boolean;
@@ -2,8 +2,8 @@
import { convertHexEmojiToDecimal } from "@plane/utils";
// plane ui
import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
// types
import { TCalloutBlockAttributes } from "./types";
// utils
@@ -8,8 +8,8 @@ import { common, createLowlight } from "lowlight";
import { CopyIcon, CheckIcon } from "lucide-react";
// ui
import { Tooltip } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
// we just have ts support for now
const lowlight = createLowlight(common);
@@ -1,9 +1,9 @@
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react";
import { NodeSelection } from "@tiptap/pm/state";
// plane utils
import { cn } from "@plane/utils";
// extensions
import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
// helpers
import { cn } from "@/helpers/common";
const MIN_SIZE = 100;
@@ -1,7 +1,9 @@
import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
import { ImageIcon } from "lucide-react";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
// constants
import { ACCEPTED_FILE_EXTENSIONS } from "@/constants/config";
// hooks
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
// extensions
@@ -127,7 +129,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
return "Uploading...";
}
if (draggedInside) {
if (draggedInside && editor.isEditable) {
return "Drop image here";
}
@@ -137,14 +139,16 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
return (
<div
className={cn(
"image-upload-component flex items-center justify-start gap-2 py-3 px-2 rounded-lg text-custom-text-300 hover:text-custom-text-200 bg-custom-background-90 hover:bg-custom-background-80 border border-dashed border-custom-border-300 transition-all duration-200 ease-in-out cursor-default",
"image-upload-component flex items-center justify-start gap-2 py-3 px-2 rounded-lg text-custom-text-300 bg-custom-background-90 border border-dashed border-custom-border-300 transition-all duration-200 ease-in-out cursor-default",
{
"hover:text-custom-text-200 cursor-pointer": editor.isEditable,
"bg-custom-background-80 text-custom-text-200": draggedInside,
"text-custom-primary-200 bg-custom-primary-100/10 hover:bg-custom-primary-100/10 hover:text-custom-primary-200 border-custom-primary-200/10":
selected,
"text-red-500 cursor-default hover:text-red-500": failedToLoadImage,
"bg-red-500/10 hover:bg-red-500/10": failedToLoadImage && selected,
"hover:text-custom-text-200 hover:bg-custom-background-80 cursor-pointer": editor.isEditable,
"bg-custom-background-80 text-custom-text-200": draggedInside && editor.isEditable,
"text-custom-primary-200 bg-custom-primary-100/10 border-custom-primary-200/10 hover:bg-custom-primary-100/10 hover:text-custom-primary-200":
selected && editor.isEditable,
"text-red-500 cursor-default": failedToLoadImage,
"hover:text-red-500": failedToLoadImage && editor.isEditable,
"bg-red-500/10": failedToLoadImage && selected,
"hover:bg-red-500/10": failedToLoadImage && selected && editor.isEditable,
}
)}
onDrop={onDrop}
@@ -164,7 +168,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
ref={fileInputRef}
hidden
type="file"
accept=".jpg,.jpeg,.png,.webp"
accept={ACCEPTED_FILE_EXTENSIONS.join(",")}
onChange={onFileChange}
multiple
/>
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ExternalLink, Maximize, Minus, Plus, X } from "lucide-react";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
type Props = {
image: {
@@ -1,6 +1,6 @@
import { useState } from "react";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
// components
import { ImageFullScreenAction } from "./full-screen";
+54 -45
View File
@@ -2,58 +2,67 @@ import { Extension, Editor } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
export const DropHandlerExtension = () =>
Extension.create({
name: "dropHandler",
priority: 1000,
export const DropHandlerExtension = Extension.create({
name: "dropHandler",
priority: 1000,
addProseMirrorPlugins() {
const editor = this.editor;
return [
new Plugin({
key: new PluginKey("drop-handler-plugin"),
props: {
handlePaste: (view: EditorView, event: ClipboardEvent) => {
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files.length > 0) {
event.preventDefault();
const files = Array.from(event.clipboardData.files);
const imageFiles = files.filter((file) => file.type.startsWith("image"));
addProseMirrorPlugins() {
const editor = this.editor;
return [
new Plugin({
key: new PluginKey("drop-handler-plugin"),
props: {
handlePaste: (view: EditorView, event: ClipboardEvent) => {
if (
editor.isEditable &&
event.clipboardData &&
event.clipboardData.files &&
event.clipboardData.files.length > 0
) {
event.preventDefault();
const files = Array.from(event.clipboardData.files);
const imageFiles = files.filter((file) => file.type.startsWith("image"));
if (imageFiles.length > 0) {
const pos = view.state.selection.from;
if (imageFiles.length > 0) {
const pos = view.state.selection.from;
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
}
return true;
}
return false;
},
handleDrop: (view: EditorView, event: DragEvent, _slice: any, moved: boolean) => {
if (
editor.isEditable &&
!moved &&
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files.length > 0
) {
event.preventDefault();
const files = Array.from(event.dataTransfer.files);
const imageFiles = files.filter((file) => file.type.startsWith("image"));
if (imageFiles.length > 0) {
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (coordinates) {
const pos = coordinates.pos;
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
}
return true;
}
return false;
},
handleDrop: (view: EditorView, event: DragEvent, _slice: any, moved: boolean) => {
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
event.preventDefault();
const files = Array.from(event.dataTransfer.files);
const imageFiles = files.filter((file) => file.type.startsWith("image"));
if (imageFiles.length > 0) {
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (coordinates) {
const pos = coordinates.pos;
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
}
return true;
}
}
return false;
},
}
return false;
},
}),
];
},
});
},
}),
];
},
});
export const insertImagesSafely = async ({
editor,
files,
@@ -47,10 +47,11 @@ type TArguments = {
};
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
editable: boolean;
};
export const CoreEditorExtensions = (args: TArguments): Extensions => {
const { disabledExtensions, enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
const { disabledExtensions, enableHistory, fileHandler, mentionConfig, placeholder, tabIndex, editable } = args;
return [
StarterKit.configure({
@@ -89,7 +90,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
...(enableHistory ? {} : { history: false }),
}),
CustomQuoteExtension,
DropHandlerExtension(),
DropHandlerExtension,
CustomHorizontalRule.configure({
HTMLAttributes: {
class: "py-4 border-custom-border-400",
@@ -137,6 +138,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
CustomCodeInlineExtension,
Markdown.configure({
html: true,
transformCopiedText: true,
transformPastedText: true,
breaks: true,
}),
@@ -145,12 +147,14 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
TableCell,
TableRow,
CustomMention({
mentionSuggestions: mentionConfig.mentionSuggestions,
mentionSuggestions: editable ? mentionConfig.mentionSuggestions : undefined,
mentionHighlights: mentionConfig.mentionHighlights,
readonly: false,
readonly: !editable,
}),
Placeholder.configure({
placeholder: ({ editor, node }) => {
if (!editor.isEditable) return;
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
if (editor.storage.imageComponent.uploadInProgress) return "";
@@ -5,8 +5,8 @@
// @ts-nocheck
import { useEffect, useState } from "react";
import { NodeViewWrapper } from "@tiptap/react";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
// types
import { IMentionHighlight } from "@/types";
@@ -5,8 +5,8 @@ import { Editor } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid";
// ui
import { Avatar } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
// types
import { IMentionSuggestion } from "@/types";
@@ -20,7 +20,6 @@ import {
TableRow,
Table,
CustomMention,
HeadingListExtension,
CustomReadOnlyImageExtension,
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension,
@@ -139,7 +138,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
}),
CharacterCount,
CustomColorExtension,
HeadingListExtension,
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension,
...CoreReadOnlyEditorAdditionalExtensions({
@@ -1,5 +1,5 @@
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
// types
import { ISlashCommandItem } from "@/types";
+2 -6
View File
@@ -1,6 +1,6 @@
import { EditorState, Selection } from "@tiptap/pm/state";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
// plane utils
import { cn } from "@plane/utils";
interface EditorClassNames {
noBorder?: boolean;
@@ -18,10 +18,6 @@ export const getEditorClassNames = ({ noBorder, borderOnFocus, containerClassNam
containerClassName
);
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Helper function to find the parent node of a specific type
export function findParentNodeOfType(selection: Selection, typeName: string) {
let depth = selection.$anchor.depth;
@@ -15,6 +15,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
const {
onTransaction,
disabledExtensions,
editable,
editorClassName,
editorProps = {},
embedHandler,
@@ -75,7 +76,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
const editor = useEditor({
disabledExtensions,
id,
onTransaction,
editable,
editorProps,
editorClassName,
enableHistory: false,
@@ -97,9 +98,10 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
}),
],
fileHandler,
handleEditorReady,
forwardedRef,
handleEditorReady,
mentionHandler,
onTransaction,
placeholder,
provider,
tabIndex,
+37 -31
View File
@@ -27,6 +27,7 @@ import type {
} from "@/types";
export interface CustomEditorProps {
editable: boolean;
editorClassName: string;
editorProps?: EditorProps;
enableHistory: boolean;
@@ -55,6 +56,7 @@ export interface CustomEditorProps {
export const useEditor = (props: CustomEditorProps) => {
const {
disabledExtensions,
editable = true,
editorClassName,
editorProps = {},
enableHistory,
@@ -74,42 +76,46 @@ export const useEditor = (props: CustomEditorProps) => {
autofocus = false,
} = props;
// states
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
// refs
const editorRef: MutableRefObject<Editor | null> = useRef(null);
const savedSelectionRef = useRef(savedSelection);
const editor = useTiptapEditor({
autofocus,
editorProps: {
...CoreEditorProps({
editorClassName,
}),
...editorProps,
const editor = useTiptapEditor(
{
editable,
autofocus,
editorProps: {
...CoreEditorProps({
editorClassName,
}),
...editorProps,
},
extensions: [
...CoreEditorExtensions({
editable,
disabledExtensions,
enableHistory,
fileHandler,
mentionConfig: {
mentionSuggestions: mentionHandler.suggestions ?? (() => Promise.resolve<IMentionSuggestion[]>([])),
mentionHighlights: mentionHandler.highlights,
},
placeholder,
tabIndex,
}),
...extensions,
],
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
onCreate: () => handleEditorReady?.(true),
onTransaction: ({ editor }) => {
setSavedSelection(editor.state.selection);
onTransaction?.();
},
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
onDestroy: () => handleEditorReady?.(false),
},
extensions: [
...CoreEditorExtensions({
disabledExtensions,
enableHistory,
fileHandler,
mentionConfig: {
mentionSuggestions: mentionHandler.suggestions ?? (() => Promise.resolve<IMentionSuggestion[]>([])),
mentionHighlights: mentionHandler.highlights,
},
placeholder,
tabIndex,
}),
...extensions,
],
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
onCreate: () => handleEditorReady?.(true),
onTransaction: ({ editor }) => {
setSavedSelection(editor.state.selection);
onTransaction?.();
},
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
onDestroy: () => handleEditorReady?.(false),
});
[editable]
);
// Update the ref whenever savedSelection changes
useEffect(() => {
@@ -105,7 +105,7 @@ export const useDropZone = (args: TDropzoneArgs) => {
async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setDraggedInside(false);
if (e.dataTransfer.files.length === 0) {
if (e.dataTransfer.files.length === 0 || !editor.isEditable) {
return;
}
const filesList = e.dataTransfer.files;
@@ -1,92 +0,0 @@
import { useEffect, useMemo, useState } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import Collaboration from "@tiptap/extension-collaboration";
import { IndexeddbPersistence } from "y-indexeddb";
// extensions
import { HeadingListExtension } from "@/extensions";
// hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
import { TReadOnlyCollaborativeEditorProps } from "@/types";
export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEditorProps) => {
const {
disabledExtensions,
editorClassName,
editorProps = {},
extensions,
fileHandler,
forwardedRef,
handleEditorReady,
id,
mentionHandler,
realtimeConfig,
serverHandler,
user,
} = props;
// states
const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false);
const [hasServerSynced, setHasServerSynced] = useState(false);
// initialize Hocuspocus provider
const provider = useMemo(
() =>
new HocuspocusProvider({
name: id,
url: realtimeConfig.url,
token: JSON.stringify(user),
parameters: realtimeConfig.queryParams,
onAuthenticationFailed: () => {
serverHandler?.onServerError?.();
setHasServerConnectionFailed(true);
},
onConnect: () => serverHandler?.onConnect?.(),
onClose: (data) => {
if (data.event.code === 1006) {
serverHandler?.onServerError?.();
setHasServerConnectionFailed(true);
}
},
onSynced: () => setHasServerSynced(true),
}),
[id, realtimeConfig, serverHandler, user]
);
// indexed db integration for offline support
const localProvider = useMemo(
() => (id ? new IndexeddbPersistence(id, provider.document) : undefined),
[id, provider]
);
// destroy and disconnect connection on unmount
useEffect(
() => () => {
provider.destroy();
localProvider?.destroy();
},
[provider, localProvider]
);
const editor = useReadOnlyEditor({
disabledExtensions,
editorProps,
editorClassName,
extensions: [
...(extensions ?? []),
HeadingListExtension,
Collaboration.configure({
document: provider.document,
}),
],
fileHandler,
forwardedRef,
handleEditorReady,
mentionHandler,
provider,
});
return {
editor,
hasServerConnectionFailed,
hasServerSynced,
};
};
@@ -1,3 +1,6 @@
// constants
import { ACCEPTED_FILE_MIME_TYPES } from "@/constants/config";
type TArgs = {
file: File;
maxFileSize: number;
@@ -11,9 +14,8 @@ export const isFileValid = (args: TArgs): boolean => {
return false;
}
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp"];
if (!allowedTypes.includes(file.type)) {
alert("Invalid file type. Please select a JPEG, JPG, PNG, or WEBP image file.");
if (!ACCEPTED_FILE_MIME_TYPES.includes(file.type)) {
alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP or GIF file.");
return false;
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { EditorProps } from "@tiptap/pm/view";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
export type TCoreEditorProps = {
editorClassName: string;
+2 -2
View File
@@ -1,6 +1,6 @@
import { EditorProps } from "@tiptap/pm/view";
// helpers
import { cn } from "@/helpers/common";
// plane utils
import { cn } from "@plane/utils";
// props
import { TCoreEditorProps } from "@/props";
@@ -21,6 +21,7 @@ export type TServerHandler = {
type TCollaborativeEditorHookProps = {
disabledExtensions: TExtensions[];
editable?: boolean;
editorClassName: string;
editorProps?: EditorProps;
extensions?: Extensions;
+1
View File
@@ -138,6 +138,7 @@ export interface IRichTextEditor extends IEditorProps {
export interface ICollaborativeDocumentEditor
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
editable: boolean;
aiHandler?: TAIHandler;
embedHandler: TEmbedConfig;
handleEditorReady?: (value: boolean) => void;
-1
View File
@@ -9,7 +9,6 @@ import "./styles/drag-drop.css";
// editors
export {
CollaborativeDocumentEditorWithRef,
CollaborativeDocumentReadOnlyEditorWithRef,
DocumentReadOnlyEditorWithRef,
LiteTextEditorWithRef,
LiteTextReadOnlyEditorWithRef,
+6 -6
View File
@@ -111,8 +111,12 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
transform: scale(1.05);
}
ul[data-type="taskList"] li > label input[type="checkbox"]:hover {
background-color: rgba(var(--color-background-80)) !important;
.ProseMirror[contenteditable="true"] input[type="checkbox"]:hover {
background-color: rgba(var(--color-background-80));
}
.ProseMirror[contenteditable="false"] input[type="checkbox"] {
pointer-events: none;
}
ul[data-type="taskList"] li > label input[type="checkbox"][checked] {
@@ -151,10 +155,6 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
margin-right: 0.2rem;
margin-top: 0.15rem;
&:hover {
background-color: rgb(var(--color-background-80));
}
&:active {
background-color: rgb(var(--color-background-90));
}
+2
View File
@@ -60,4 +60,6 @@ export enum EFileAssetType {
USER_COVER = "USER_COVER",
WORKSPACE_LOGO = "WORKSPACE_LOGO",
TEAM_SPACE_DESCRIPTION = "TEAM_SPACE_DESCRIPTION",
INITIATIVE_DESCRIPTION = "INITIATIVE_DESCRIPTION",
PROJECT_DESCRIPTION = "PROJECT_DESCRIPTION",
}
+6
View File
@@ -1,3 +1,4 @@
import { EIssueServiceType } from "@plane/constants";
import { TIssuePriorities } from "../issues";
import { TIssueAttachment } from "./issue_attachment";
import { TIssueLink } from "./issue_link";
@@ -39,6 +40,7 @@ export type TBaseIssue = {
updated_by: string;
is_draft: boolean;
is_epic?: boolean;
};
export type IssueRelation = {
@@ -121,3 +123,7 @@ export type TIssueDetailWidget =
| "relations"
| "links"
| "attachments";
export type TIssueServiceType =
| EIssueServiceType.ISSUES
| EIssueServiceType.EPICS;
+1
View File
@@ -136,6 +136,7 @@ export type TProjectIssuesSearchParams = {
issue_id?: string;
workspace_search: boolean;
target_date?: string;
epic?: boolean;
};
export interface ISearchIssueResponse {
+1
View File
@@ -13,4 +13,5 @@ export enum EModalWidth {
XXXXL = "sm:max-w-4xl",
VXL = "sm:max-w-5xl",
VIXL = "sm:max-w-6xl",
VIIXL = "sm:max-w-7xl",
}
+3 -1
View File
@@ -15,8 +15,10 @@
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"clsx": "^2.1.1",
"isomorphic-dompurify": "^2.16.0",
"react": "^18.3.1"
"react": "^18.3.1",
"tailwind-merge": "^2.5.5"
},
"devDependencies": {
"@plane/eslint-config": "*",
+4
View File
@@ -0,0 +1,4 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
+1
View File
@@ -1,3 +1,4 @@
export * from "./color";
export * from "./common";
export * from "./emoji";
export * from "./string";
@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
//plane
import { cn } from "@plane/editor";
// plane utils
import { cn } from "@plane/utils";
// components
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
// hooks
@@ -4,10 +4,12 @@ import { MutableRefObject } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, useSearchParams } from "next/navigation";
// plane
import { cn } from "@plane/editor";
// plane types
import { IIssueDisplayProperties } from "@plane/types";
// plane ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// components
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC";
// helpers
@@ -4,10 +4,12 @@ import { useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, useSearchParams } from "next/navigation";
// types
import { cn } from "@plane/editor";
// plane types
import { IIssueDisplayProperties } from "@plane/types";
// plane ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
@@ -2,9 +2,10 @@
import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react";
import { observer } from "mobx-react";
import { cn } from "@plane/editor";
// plane
// plane types
import { IGroupByColumn, TIssueGroupByOptions, IIssueDisplayProperties, TPaginationData, TLoader } from "@plane/types";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
//
@@ -2,11 +2,12 @@
import { observer } from "mobx-react";
import { Layers, Link, Paperclip } from "lucide-react";
// types
import { cn } from "@plane/editor";
// plane types
import { IIssueDisplayProperties } from "@plane/types";
// plane ui
import { Tooltip } from "@plane/ui";
// ui
// plane utils
import { cn } from "@plane/utils";
// components
import {
IssueBlockDate,
@@ -1,9 +1,10 @@
"use client";
import { observer } from "mobx-react";
// ui
import { cn } from "@plane/editor";
// plane ui
import { ContrastIcon, Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
//hooks
import { useCycle } from "@/hooks/store/use-cycle";
@@ -3,9 +3,10 @@
import { observer } from "mobx-react";
// icons
import { LucideIcon, Users } from "lucide-react";
// ui
import { cn } from "@plane/editor";
// plane ui
import { Avatar, AvatarGroup } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
//
@@ -1,9 +1,10 @@
"use client";
import { observer } from "mobx-react";
// planes
import { cn } from "@plane/editor";
// plane ui
import { DiceIcon, Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useModule } from "@/hooks/store/use-module";
@@ -1,9 +1,10 @@
"use client";
import { observer } from "mobx-react";
// ui
import { cn } from "@plane/editor";
// plane ui
import { StateGroupIcon, Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
//hooks
import { useStates } from "@/hooks/store";
@@ -1,8 +1,9 @@
"use client";
import { CommandPalette } from "@/components/command-palette";
import { WorkspaceAuthWrapper } from "@/layouts/auth-layout";
import { AuthenticationWrapper } from "@/lib/wrappers";
// plane web components
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
import { AppSidebar } from "./sidebar";
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
@@ -1,8 +1,8 @@
"use client";
import { ReactNode } from "react";
// layouts
import { ProjectAuthWrapper } from "@/layouts/auth-layout";
// plane web layouts
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
const ProjectDetailLayout = ({ children }: { children: ReactNode }) => (
<ProjectAuthWrapper>{children}</ProjectAuthWrapper>
@@ -0,0 +1 @@
export * from "./modal";
@@ -0,0 +1,19 @@
"use client";
import React, { FC } from "react";
import { TIssue } from "@plane/types";
export interface EpicModalProps {
data?: Partial<TIssue>;
isOpen: boolean;
onClose: () => void;
beforeFormSubmit?: () => Promise<void>;
onSubmit?: (res: TIssue) => Promise<void>;
fetchIssueDetails?: boolean;
primaryButtonText?: {
default: string;
loading: string;
};
isProjectSelectionDisabled?: boolean;
}
export const CreateUpdateEpicModal: FC<EpicModalProps> = (props) => <></>;
+1
View File
@@ -0,0 +1 @@
export * from "./epic-modal";
@@ -1 +1,9 @@
export const TimelineDependencyPaths = () => <></>;
import { FC } from "react";
type Props = {
isEpic?: boolean;
};
export const TimelineDependencyPaths: FC<Props> = (props) => {
const { isEpic = false } = props;
return <></>;
};
@@ -1,9 +1,12 @@
import { TIssueServiceType } from "@plane/types";
export type TIssueAdditionalPropertyValuesUpdateProps = {
issueId: string;
issueTypeId: string;
projectId: string;
workspaceSlug: string;
isDisabled: boolean;
issueServiceType?: TIssueServiceType;
};
export const IssueAdditionalPropertyValuesUpdate: React.FC<TIssueAdditionalPropertyValuesUpdateProps> = () => <></>;
@@ -1,6 +1,6 @@
import { Plus } from "lucide-react";
// Plane
import { cn } from "@plane/editor";
// plane utils
import { cn } from "@plane/utils";
type Props = {
workspaceSlug: string;
@@ -1,6 +1,7 @@
import { TDeDupeIssue } from "@plane/types";
export const useDebouncedDuplicateIssues = (
workspaceSlug: string | undefined,
workspaceId: string | undefined,
projectId: string | undefined,
formData: { name: string | undefined; description_html?: string | undefined; issueId?: string | undefined }
+15
View File
@@ -0,0 +1,15 @@
import { FC } from "react";
import { observer } from "mobx-react";
// layouts
import { ProjectAuthWrapper as CoreProjectAuthWrapper } from "@/layouts/auth-layout";
export type IProjectAuthWrapper = {
children: React.ReactNode;
};
export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
// props
const { children } = props;
return <CoreProjectAuthWrapper>{children}</CoreProjectAuthWrapper>;
});
+15
View File
@@ -0,0 +1,15 @@
import { FC } from "react";
import { observer } from "mobx-react";
// layouts
import { WorkspaceAuthWrapper as CoreWorkspaceAuthWrapper } from "@/layouts/auth-layout";
export type IWorkspaceAuthWrapper = {
children: React.ReactNode;
};
export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props) => {
// props
const { children } = props;
return <CoreWorkspaceAuthWrapper>{children}</CoreWorkspaceAuthWrapper>;
});
+15
View File
@@ -0,0 +1,15 @@
import { IProjectIssuesFilter, ProjectIssuesFilter } from "@/store/issue/project";
import { IIssueRootStore } from "@/store/issue/root.store";
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export type IProjectEpicsFilter = IProjectIssuesFilter;
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export class ProjectEpicsFilter extends ProjectIssuesFilter implements IProjectEpicsFilter {
constructor(_rootStore: IIssueRootStore) {
super(_rootStore);
// root store
this.rootIssueStore = _rootStore;
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./filter.store";
export * from "./issue.store";
+14
View File
@@ -0,0 +1,14 @@
import { IProjectIssues, ProjectIssues } from "@/store/issue/project";
import { IIssueRootStore } from "@/store/issue/root.store";
import { IProjectEpicsFilter } from "./filter.store";
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export type IProjectEpics = IProjectIssues;
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export class ProjectEpics extends ProjectIssues implements IProjectEpics {
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProjectEpicsFilter) {
super(_rootStore, issueFilterStore);
}
}
@@ -7,7 +7,14 @@ import uniq from "lodash/uniq";
import update from "lodash/update";
import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types";
import { EIssueServiceType } from "@plane/constants";
import {
TIssueActivityComment,
TIssueActivity,
TIssueActivityMap,
TIssueActivityIdMap,
TIssueServiceType,
} from "@plane/types";
// plane web constants
import { EActivityFilterType } from "@/plane-web/constants/issues";
// services
@@ -29,7 +36,7 @@ export interface IIssueActivityStoreActions {
export interface IIssueActivityStore extends IIssueActivityStoreActions {
// observables
sortOrder: 'asc' | 'desc'
sortOrder: "asc" | "desc";
loader: TActivityLoader;
activities: TIssueActivityIdMap;
activityMap: TIssueActivityMap;
@@ -37,20 +44,24 @@ export interface IIssueActivityStore extends IIssueActivityStoreActions {
getActivitiesByIssueId: (issueId: string) => string[] | undefined;
getActivityById: (activityId: string) => TIssueActivity | undefined;
getActivityCommentByIssueId: (issueId: string) => TIssueActivityComment[] | undefined;
toggleSortOrder: ()=>void;
toggleSortOrder: () => void;
}
export class IssueActivityStore implements IIssueActivityStore {
// observables
sortOrder: "asc" | "desc" = 'asc';
sortOrder: "asc" | "desc" = "asc";
loader: TActivityLoader = "fetch";
activities: TIssueActivityIdMap = {};
activityMap: TIssueActivityMap = {};
// services
serviceType;
issueActivityService;
constructor(protected store: CoreRootStore) {
constructor(
protected store: CoreRootStore,
serviceType: TIssueServiceType = EIssueServiceType.ISSUES
) {
makeObservable(this, {
// observables
sortOrder: observable.ref,
@@ -59,10 +70,11 @@ export class IssueActivityStore implements IIssueActivityStore {
activityMap: observable,
// actions
fetchActivities: action,
toggleSortOrder: action
toggleSortOrder: action,
});
this.serviceType = serviceType;
// services
this.issueActivityService = new IssueActivityService();
this.issueActivityService = new IssueActivityService(this.serviceType);
}
// helper methods
@@ -81,8 +93,10 @@ export class IssueActivityStore implements IIssueActivityStore {
let activityComments: TIssueActivityComment[] = [];
const currentStore = this.serviceType === EIssueServiceType.EPICS ? this.store.epic : this.store.issue;
const activities = this.getActivitiesByIssueId(issueId) || [];
const comments = this.store.issue.issueDetail.comment.getCommentsByIssueId(issueId) || [];
const comments = currentStore.issueDetail.comment.getCommentsByIssueId(issueId) || [];
activities.forEach((activityId) => {
const activity = this.getActivityById(activityId);
@@ -95,7 +109,7 @@ export class IssueActivityStore implements IIssueActivityStore {
});
comments.forEach((commentId) => {
const comment = this.store.issue.issueDetail.comment.getCommentById(commentId);
const comment = currentStore.issueDetail.comment.getCommentById(commentId);
if (!comment) return;
activityComments.push({
id: comment.id,
@@ -104,14 +118,14 @@ export class IssueActivityStore implements IIssueActivityStore {
});
});
activityComments = orderBy(activityComments, (e)=>new Date(e.created_at || 0), this.sortOrder);
activityComments = orderBy(activityComments, (e) => new Date(e.created_at || 0), this.sortOrder);
return activityComments;
});
toggleSortOrder = ()=>{
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
}
toggleSortOrder = () => {
this.sortOrder = this.sortOrder === "asc" ? "desc" : "asc";
};
// actions
public async fetchActivities(
+1
View File
@@ -1 +1,2 @@
export * from "./projects";
export * from "./project-activity";
+25
View File
@@ -0,0 +1,25 @@
export interface TProjectActivity {
id: string;
content: string;
createdAt: string;
updatedAt: string;
userId: string;
projectId: string;
created_at: string;
field: string;
verb: string;
actor_detail: {
display_name: string;
id: string;
};
workspace_detail: {
slug: string;
};
project_detail: {
name: string;
};
new_value: string;
old_value: string;
project: string;
new_identifier?: string;
}
+3 -1
View File
@@ -1,6 +1,8 @@
import { LucideIcon } from "lucide-react";
import { cn } from "@plane/editor";
// plane ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
type Props = {
onChange: (value: number) => void;
@@ -0,0 +1,51 @@
"use client";
import { FC, ReactNode } from "react";
import { Network } from "lucide-react";
// hooks
import { Tooltip } from "@plane/ui";
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { TProjectActivity } from "@/plane-web/types";
import { User } from "./user";
type TActivityBlockComponent = {
icon?: ReactNode;
activity: TProjectActivity;
ends: "top" | "bottom" | undefined;
children: ReactNode;
customUserName?: string;
};
export const ActivityBlockComponent: FC<TActivityBlockComponent> = (props) => {
const { icon, activity, ends, children, customUserName } = props;
// hooks
const { isMobile } = usePlatformOS();
if (!activity) return <></>;
return (
<div
className={`relative flex items-center gap-3 text-xs ${
ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2`
}`}
>
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden />
<div className="flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-center z-[4] bg-custom-background-80 text-custom-text-200">
{icon ? icon : <Network className="w-3.5 h-3.5" />}
</div>
<div className="w-full truncate text-custom-text-200">
<User activity={activity} customUserName={customUserName} /> {children}
<div className="mt-1">
<Tooltip
isMobile={isMobile}
tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`}
>
<span className="whitespace-nowrap text-custom-text-350 font-medium">
{calculateTimeAgo(activity.created_at)}
</span>
</Tooltip>
</div>
</div>
</div>
);
};
@@ -0,0 +1,30 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { TProjectActivity } from "@/plane-web/types";
import { ActivityBlockComponent } from "./activity-block";
import { iconsMap, messages } from "./helper";
type TActivityItem = {
activity: TProjectActivity;
showProject?: boolean;
ends?: "top" | "bottom" | undefined;
};
export const ActivityItem: FC<TActivityItem> = observer((props) => {
const { activity, showProject = true, ends } = props;
if (!activity) return null;
const activityType = activity.field;
const { message, customUserName } = messages(activity);
const icon = iconsMap[activityType] || iconsMap.default;
return (
<ActivityBlockComponent icon={icon} activity={activity} ends={ends} customUserName={customUserName}>
<>{message}</>
</ActivityBlockComponent>
);
});
@@ -0,0 +1,279 @@
import { ReactNode } from "react";
import {
Signal,
RotateCcw,
Network,
Link as LinkIcon,
Calendar,
Tag,
Inbox,
AlignLeft,
Users,
Paperclip,
Type,
Triangle,
FileText,
Globe,
Hash,
Clock,
Bell,
LayoutGrid,
GitBranch,
Timer,
ListTodo,
Layers,
} from "lucide-react";
// components
import { ArchiveIcon, DoubleCircleIcon, ContrastIcon, DiceIcon, Intake } from "@plane/ui";
import { TProjectActivity } from "@/plane-web/types";
type ActivityIconMap = {
[key: string]: ReactNode;
};
export const iconsMap: ActivityIconMap = {
priority: <Signal size={14} className="text-custom-text-200" />,
archived_at: <ArchiveIcon className="h-3.5 w-3.5 text-custom-text-200" />,
restored: <RotateCcw className="h-3.5 w-3.5 text-custom-text-200" />,
link: <LinkIcon className="h-3.5 w-3.5 text-custom-text-200" />,
start_date: <Calendar className="h-3.5 w-3.5 text-custom-text-200" />,
target_date: <Calendar className="h-3.5 w-3.5 text-custom-text-200" />,
label: <Tag className="h-3.5 w-3.5 text-custom-text-200" />,
inbox: <Inbox className="h-3.5 w-3.5 text-custom-text-200" />,
description: <AlignLeft className="h-3.5 w-3.5 text-custom-text-200" />,
assignee: <Users className="h-3.5 w-3.5 text-custom-text-200" />,
attachment: <Paperclip className="h-3.5 w-3.5 text-custom-text-200" />,
name: <Type className="h-3.5 w-3.5 text-custom-text-200" />,
state: <DoubleCircleIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />,
estimate: <Triangle size={14} className="text-custom-text-200" />,
cycle: <ContrastIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />,
module: <DiceIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />,
page: <FileText className="h-3.5 w-3.5 text-custom-text-200" />,
network: <Globe className="h-3.5 w-3.5 text-custom-text-200" />,
identifier: <Hash className="h-3.5 w-3.5 text-custom-text-200" />,
timezone: <Clock className="h-3.5 w-3.5 text-custom-text-200" />,
is_project_updates_enabled: <Bell className="h-3.5 w-3.5 text-custom-text-200" />,
is_epic_enabled: <LayoutGrid className="h-3.5 w-3.5 text-custom-text-200" />,
is_workflow_enabled: <GitBranch className="h-3.5 w-3.5 text-custom-text-200" />,
is_time_tracking_enabled: <Timer className="h-3.5 w-3.5 text-custom-text-200" />,
is_issue_type_enabled: <ListTodo className="h-3.5 w-3.5 text-custom-text-200" />,
default: <Network className="h-3.5 w-3.5 text-custom-text-200" />,
module_view: <DiceIcon className="h-3.5 w-3.5 text-custom-text-200" />,
cycle_view: <ContrastIcon className="h-3.5 w-3.5 text-custom-text-200" />,
issue_views_view: <Layers className="h-3.5 w-3.5 text-custom-text-200" />,
page_view: <FileText className="h-3.5 w-3.5 text-custom-text-200" />,
intake_view: <Intake className="h-3.5 w-3.5 text-custom-text-200" />,
};
export const messages = (activity: TProjectActivity): { message: string | ReactNode; customUserName?: string } => {
const activityType = activity.field;
const newValue = activity.new_value;
const oldValue = activity.old_value;
const verb = activity.verb;
const getBooleanActionText = (value: string) => {
if (value === "true") return "enabled";
if (value === "false") return "disabled";
return verb;
};
switch (activityType) {
case "priority":
return {
message: (
<>
set the priority to <span className="font-medium text-custom-text-100">{newValue || "none"}</span>
</>
),
};
case "archived_at":
return {
message: newValue === "restore" ? "restored the project" : "archived the project",
customUserName: newValue === "archive" ? "Plane" : undefined,
};
case "name":
return {
message: (
<>
renamed the project to <span className="font-medium text-custom-text-100">{newValue}</span>
</>
),
};
case "description":
return {
message: newValue ? "updated the project description" : "removed the project description",
};
case "start_date":
return {
message: (
<>
{newValue ? (
<>
set the start date to <span className="font-medium text-custom-text-100">{newValue}</span>
</>
) : (
"removed the start date"
)}
</>
),
};
case "target_date":
return {
message: (
<>
{newValue ? (
<>
set the target date to <span className="font-medium text-custom-text-100">{newValue}</span>
</>
) : (
"removed the target date"
)}
</>
),
};
case "state":
return {
message: (
<>
set the state to <span className="font-medium text-custom-text-100">{newValue || "none"}</span>
</>
),
};
case "estimate":
return {
message: (
<>
{newValue ? (
<>
set the estimate point to <span className="font-medium text-custom-text-100">{newValue}</span>
</>
) : (
<>
removed the estimate point
{oldValue && (
<>
{" "}
<span className="font-medium text-custom-text-100">{oldValue}</span>
</>
)}
</>
)}
</>
),
};
case "cycles":
return {
message: (
<>
<span>
{verb} this project {verb === "removed" ? "from" : "to"} the cycle{" "}
</span>
{verb !== "removed" ? (
<a
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex font-medium text-custom-text-100"
>
{activity.new_value}
</a>
) : (
<span className="font-medium text-custom-text-100">{activity.old_value || "Unknown cycle"}</span>
)}
</>
),
};
case "modules":
return {
message: (
<>
<span>
{verb} this project {verb === "removed" ? "from" : "to"} the module{" "}
</span>
<span className="font-medium text-custom-text-100">
{verb === "removed" ? oldValue : newValue || "Unknown module"}
</span>
</>
),
};
case "labels":
return {
message: (
<>
{verb} the label{" "}
<span className="font-medium text-custom-text-100">{newValue || oldValue || "Untitled label"}</span>
</>
),
};
case "inbox":
return {
message: <>{newValue ? "enabled" : "disabled"} inbox</>,
};
case "page":
return {
message: (
<>
{newValue ? "created" : "removed"} the project page{" "}
<span className="font-medium text-custom-text-100">{newValue || oldValue || "Untitled page"}</span>
</>
),
};
case "network":
return {
message: <>{newValue ? "enabled" : "disabled"} network access</>,
};
case "identifier":
return {
message: (
<>
updated project identifier to <span className="font-medium text-custom-text-100">{newValue || "none"}</span>
</>
),
};
case "timezone":
return {
message: (
<>
changed project timezone to{" "}
<span className="font-medium text-custom-text-100">{newValue || "default"}</span>
</>
),
};
case "module_view":
case "cycle_view":
case "issue_views_view":
case "page_view":
case "intake_view":
return {
message: (
<>
{getBooleanActionText(newValue)} {activityType.replace(/_view$/, "").replace(/_/g, " ")} view
</>
),
};
case "is_project_updates_enabled":
return {
message: <>{getBooleanActionText(newValue)} project updates</>,
};
case "is_epic_enabled":
return {
message: <>{getBooleanActionText(newValue)} epics</>,
};
case "is_workflow_enabled":
return {
message: <>{getBooleanActionText(newValue)} custom workflow</>,
};
case "is_time_tracking_enabled":
return {
message: <>{getBooleanActionText(newValue)} time tracking</>,
};
case "is_issue_type_enabled":
return {
message: <>{getBooleanActionText(newValue)} issue types</>,
};
default:
return {
message: `${verb} ${activityType.replace(/_/g, " ")} `,
};
}
};
@@ -0,0 +1 @@
export * from "./activity-item";
@@ -0,0 +1,27 @@
import { FC } from "react";
import Link from "next/link";
import { TProjectActivity } from "@/plane-web/types";
type TUser = {
activity: TProjectActivity;
customUserName?: string;
};
export const User: FC<TUser> = (props) => {
const { activity, customUserName } = props;
return (
<>
{customUserName || activity.actor_detail?.display_name.includes("-intake") ? (
<span className="text-custom-text-100 font-medium">{customUserName || "Plane"}</span>
) : (
<Link
href={`/${activity?.workspace_detail?.slug}/profile/${activity?.actor_detail?.id}`}
className="hover:underline text-custom-text-100 font-medium"
>
{activity.actor_detail?.display_name}
</Link>
)}
</>
);
};
+1
View File
@@ -5,3 +5,4 @@ export * from "./logo-spinner";
export * from "./logo";
export * from "./pro-icon";
export * from "./count-chip";
export * from "./activity";
+3 -2
View File
@@ -1,9 +1,10 @@
import { useCallback, useMemo } from "react";
import { observer } from "mobx-react";
import { Check } from "lucide-react";
// plane packages
import { cn } from "@plane/editor";
// plane ui
import { Dropdown } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// constants
import { EIssueLayoutTypes, ISSUE_LAYOUT_MAP } from "@/constants/issue";
@@ -3,8 +3,9 @@
import { observer } from "mobx-react";
import { LucideIcon, Users } from "lucide-react";
// plane ui
import { cn } from "@plane/editor";
import { Avatar, AvatarGroup } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { getFileURL } from "@/helpers/file.helper";
// hooks
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
// editor
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
// types
@@ -27,6 +27,7 @@ interface LiteTextEditorWrapperProps
showAccessSpecifier?: boolean;
showSubmitButton?: boolean;
isSubmitting?: boolean;
showToolbarInitially?: boolean;
uploadFile: (file: File) => Promise<string>;
}
@@ -41,10 +42,13 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
showAccessSpecifier = false,
showSubmitButton = true,
isSubmitting = false,
showToolbarInitially = true,
placeholder = "Add comment...",
uploadFile,
...rest
} = props;
// states
const [isFocused, setIsFocused] = useState(showToolbarInitially);
// store hooks
const { data: currentUser } = useUser();
const {
@@ -73,7 +77,11 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
const editorRef = isMutableRefObject<EditorRefApi>(ref) ? ref.current : null;
return (
<div className="border border-custom-border-200 rounded p-3 space-y-3">
<div
className={cn("relative border border-custom-border-200 rounded p-3")}
onFocus={() => !showToolbarInitially && setIsFocused(true)}
onBlur={() => !showToolbarInitially && setIsFocused(false)}
>
<LiteTextEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions}
@@ -92,24 +100,31 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
containerClassName={cn(containerClassName, "relative")}
{...rest}
/>
<IssueCommentToolbar
accessSpecifier={accessSpecifier}
executeCommand={(item) => {
// TODO: update this while toolbar homogenization
// @ts-expect-error type mismatch here
editorRef?.executeMenuItemCommand({
itemKey: item.itemKey,
...item.extraProps,
});
}}
handleAccessChange={handleAccessChange}
handleSubmit={(e) => rest.onEnterKeyPress?.(e)}
isCommentEmpty={isEmpty}
isSubmitting={isSubmitting}
showAccessSpecifier={showAccessSpecifier}
editorRef={editorRef}
showSubmitButton={showSubmitButton}
/>
<div
className={cn(
"transition-all duration-300 ease-out origin-top overflow-hidden",
isFocused ? "max-h-[200px] opacity-100 scale-y-100 mt-3" : "max-h-0 opacity-0 scale-y-0 invisible"
)}
>
<IssueCommentToolbar
accessSpecifier={accessSpecifier}
executeCommand={(item) => {
// TODO: update this while toolbar homogenization
// @ts-expect-error type mismatch here
editorRef?.executeMenuItemCommand({
itemKey: item.itemKey,
...item.extraProps,
});
}}
handleAccessChange={handleAccessChange}
handleSubmit={(e) => rest.onEnterKeyPress?.(e)}
isCommentEmpty={isEmpty}
isSubmitting={isSubmitting}
showAccessSpecifier={showAccessSpecifier}
editorRef={editorRef}
showSubmitButton={showSubmitButton}
/>
</div>
</div>
);
});
@@ -56,6 +56,7 @@ type Props = {
targetDate?: Date
) => ChartDataType | undefined;
quickAdd?: React.JSX.Element | undefined;
isEpic?: boolean;
};
export const GanttChartMainContent: React.FC<Props> = observer((props) => {
@@ -79,6 +80,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
updateCurrentViewRenderPayload,
quickAdd,
updateBlockDates,
isEpic = false,
} = props;
// refs
const ganttContainerRef = useRef<HTMLDivElement>(null);
@@ -159,7 +161,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
entities={{
[GANTT_SELECT_GROUP]: blockIds ?? [],
}}
disabled={!isBulkOperationsEnabled}
disabled={!isBulkOperationsEnabled || isEpic}
>
{(helpers) => (
<>
@@ -187,6 +189,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
title={title}
quickAdd={quickAdd}
selectionHelpers={helpers}
isEpic={isEpic}
/>
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
<ActiveChartView />
@@ -208,7 +211,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
selectionHelpers={helpers}
ganttContainerRef={ganttContainerRef}
/>
<TimelineDependencyPaths />
<TimelineDependencyPaths isEpic={isEpic} />
<TimelineDraggablePath />
<GanttChartBlocksList
blockIds={blockIds}
@@ -41,6 +41,7 @@ type ChartViewRootProps = {
canLoadMoreBlocks?: boolean;
quickAdd?: React.JSX.Element | undefined;
showToday: boolean;
isEpic?: boolean;
};
const timelineViewHelpers = {
@@ -71,6 +72,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
quickAdd,
showToday,
updateBlockDates,
isEpic = false,
} = props;
// states
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
@@ -204,6 +206,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
quickAdd={quickAdd}
updateBlockDates={updateBlockDates}
isEpic={isEpic}
/>
</div>
);
@@ -1,7 +1,7 @@
import { FC } from "react";
import { observer } from "mobx-react";
// Plane
import { cn } from "@plane/editor";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
//
@@ -1,7 +1,7 @@
import { FC } from "react";
import { observer } from "mobx-react";
// Plane
import { cn } from "@plane/editor";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
//
@@ -1,7 +1,7 @@
import { useState } from "react";
import { observer } from "mobx-react";
// Plane
import { cn } from "@plane/editor";
// plane utils
import { cn } from "@plane/utils";
//helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
//hooks
@@ -1,7 +1,7 @@
import { useState } from "react";
import { observer } from "mobx-react";
// Plane
import { cn } from "@plane/editor";
// plane utils
import { cn } from "@plane/utils";
//helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
//hooks
+3
View File
@@ -26,6 +26,7 @@ type GanttChartRootProps = {
bottomSpacing?: boolean;
showAllBlocks?: boolean;
showToday?: boolean;
isEpic?: boolean;
};
export const GanttChartRoot: FC<GanttChartRootProps> = observer((props) => {
@@ -50,6 +51,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = observer((props) => {
showToday = true,
quickAdd,
updateBlockDates,
isEpic = false,
} = props;
const { setBlockIds } = useTimeLineChartStore();
@@ -81,6 +83,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = observer((props) => {
quickAdd={quickAdd}
showToday={showToday}
updateBlockDates={updateBlockDates}
isEpic={isEpic}
/>
);
});
@@ -19,10 +19,11 @@ type Props = {
enableSelection: boolean;
isDragging: boolean;
selectionHelpers?: TSelectionHelper;
isEpic?: boolean;
};
export const IssuesSidebarBlock = observer((props: Props) => {
const { block, enableSelection, isDragging, selectionHelpers } = props;
const { block, enableSelection, isDragging, selectionHelpers, isEpic = false } = props;
// store hooks
const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
const { getIsIssuePeeked } = useIssueDetail();
@@ -73,7 +74,7 @@ export const IssuesSidebarBlock = observer((props: Props) => {
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} />
<IssueGanttSidebarBlock issueId={block.data.id} isEpic={isEpic} />
</div>
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
@@ -29,6 +29,7 @@ type Props = {
enableSelection: boolean;
showAllBlocks?: boolean;
selectionHelpers?: TSelectionHelper;
isEpic?: boolean;
};
export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
@@ -42,6 +43,7 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
ganttContainerRef,
showAllBlocks = false,
selectionHelpers,
isEpic = false,
} = props;
const { getBlockById } = useTimeLineChart(ETimeLineTypeType.ISSUE);
@@ -101,6 +103,7 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
enableSelection={enableSelection}
isDragging={isDragging}
selectionHelpers={selectionHelpers}
isEpic={isEpic}
/>
)}
</GanttDnDHOC>
@@ -23,6 +23,7 @@ type Props = {
title: string;
quickAdd?: React.JSX.Element | undefined;
selectionHelpers: TSelectionHelper;
isEpic?: boolean;
};
export const GanttChartSidebar: React.FC<Props> = observer((props) => {
@@ -38,6 +39,7 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
title,
quickAdd,
selectionHelpers,
isEpic = false,
} = props;
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(GANTT_SELECT_GROUP) === "empty";
@@ -90,6 +92,7 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
ganttContainerRef,
loadMoreBlocks,
selectionHelpers,
isEpic,
})}
</Row>
{quickAdd ? quickAdd : null}
@@ -65,11 +65,16 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
// debounced duplicate issues swr
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectId, {
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
});
const { duplicateIssues } = useDebouncedDuplicateIssues(
workspaceSlug,
projectDetails?.workspace.toString(),
projectId,
{
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
}
);
if (!issue) return <></>;
@@ -1,8 +1,10 @@
import { FC } from "react";
import { ChevronDown, ListFilter } from "lucide-react";
// components
import { cn } from "@plane/editor";
// plane ui
import { getButtonStyling } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// components
import { InboxIssueFilterSelection, InboxIssueOrderByDropdown } from "@/components/inbox/inbox-filter";
import { FiltersDropdown } from "@/components/issues";
import useSize from "@/hooks/use-window-size";

Some files were not shown because too many files have changed in this diff Show More