forked from github/plane
Merge pull request #703 from makeplane/stage-release
promote: stage-release to production
This commit is contained in:
commit
3d6f2dd3dc
115
README.md
115
README.md
@ -2,35 +2,128 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://plane.so">
|
<a href="https://plane.so">
|
||||||
<img src="https://res.cloudinary.com/dgxawjvpo/image/upload/v1673379660/Plane/plane-logo_0m83xue7R_f0v9r9.png" alt="Plane Logo" width="350">
|
<img src="https://res.cloudinary.com/toolspacedev/image/upload/v1680596414/Plane/Plane_Icon_Blue_on_White_150x150_muysa3.jpg" alt="Plane Logo" width="70">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3 align="center"><b>Plane</b></h3>
|
||||||
|
<p align="center"><b>Open-source, self-hosted project planning tool</b></p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.com/invite/A92xrEGCge">
|
<a href="https://discord.com/invite/A92xrEGCge">
|
||||||
<img alt="Discord" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
|
<img alt="Discord" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
|
||||||
</a>
|
</a>
|
||||||
<img alt="Discord" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
|
<img alt="Discord" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
Plane is an open-source project planning tool that is designed to help individuals and teams streamline their issues, sprints, and product roadmaps. It is easy to use and can be accessed by anyone, making it an ideal choice for a wide range of projects and organizations.
|
<p>
|
||||||
<br /> <br />
|
<a href="https://app.plane.so/" target="_blank">
|
||||||
|
<img
|
||||||
|
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680599798/Plane/plane_1_1_tnb32j.png"
|
||||||
|
alt="Plane Screens"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Meet Plane. An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘♀️.
|
||||||
|
|
||||||
|
|
||||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/29tPNhaV) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/29tPNhaV) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||||
|
|
||||||
## Getting Started
|
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||||
|
|
||||||
Visit https://app.plane.so to get started with Plane.
|
|
||||||
|
|
||||||
## Documentation
|
## ⚡️ Quick start with Docker Compose
|
||||||
|
|
||||||
|
### Docker Compose Setup
|
||||||
|
|
||||||
|
- Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/makeplane/plane
|
||||||
|
```
|
||||||
|
|
||||||
|
- Change Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd plane
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run setup.sh
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup.sh localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
> If running in a cloud env replace localhost with public facing IP address of the VM
|
||||||
|
|
||||||
|
|
||||||
|
- Run Docker compose up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
* **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
|
||||||
|
* **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
|
||||||
|
* **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
|
||||||
|
* **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
|
||||||
|
* **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
|
||||||
|
* **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
|
||||||
|
* **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
|
||||||
|
* **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
|
||||||
|
* **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
|
||||||
|
|
||||||
|
## 📸 Screenshots
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://app.plane.so/" target="_blank">
|
||||||
|
<img
|
||||||
|
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601719/Plane/plane_2_iqao52.png"
|
||||||
|
alt="Plane Issue Details"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://app.plane.so/" target="_blank">
|
||||||
|
<img
|
||||||
|
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680604273/Plane/plane_5_1_nwsl3a.png"
|
||||||
|
alt="Plane Cycles and Modules"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://app.plane.so/" target="_blank">
|
||||||
|
<img
|
||||||
|
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601713/Plane/plane_4_cqm0g8.png"
|
||||||
|
alt="Plane Quick Lists"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://app.plane.so/" target="_blank">
|
||||||
|
<img
|
||||||
|
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601712/Plane/plane_3_1_cu4fsc.png"
|
||||||
|
alt="Plane Command K"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 📚Documentation
|
||||||
|
|
||||||
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
|
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
|
||||||
|
|
||||||
To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md).
|
To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md).
|
||||||
|
|
||||||
## Status
|
## 🔋 Status
|
||||||
|
|
||||||
|
|
||||||
- [x] Early Community Previews: We are open-sourcing and sharing the development version of Plane
|
- [x] Early Community Previews: We are open-sourcing and sharing the development version of Plane
|
||||||
- [ ] Alpha: We are testing Plane with a closed set of customers
|
- [ ] Alpha: We are testing Plane with a closed set of customers
|
||||||
@ -38,7 +131,7 @@ To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/m
|
|||||||
- [ ] Public Beta: Stable enough for most non-enterprise use-cases
|
- [ ] Public Beta: Stable enough for most non-enterprise use-cases
|
||||||
- [ ] Public: Production-ready
|
- [ ] Public: Production-ready
|
||||||
|
|
||||||
## Community
|
## ❤️ Community
|
||||||
|
|
||||||
The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.
|
The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.
|
||||||
|
|
||||||
@ -46,6 +139,6 @@ To chat with other community members you can join the [Plane Discord](https://di
|
|||||||
|
|
||||||
Our [Code of Conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community channels.
|
Our [Code of Conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community channels.
|
||||||
|
|
||||||
## Security
|
## ⛓️ Security
|
||||||
|
|
||||||
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. Email security@plane.so to disclose any security vulnerabilities.
|
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. Email security@plane.so to disclose any security vulnerabilities.
|
||||||
|
@ -19,3 +19,6 @@ GITHUB_CLIENT_SECRET=""
|
|||||||
# Flags
|
# Flags
|
||||||
DISABLE_COLLECTSTATIC=1
|
DISABLE_COLLECTSTATIC=1
|
||||||
DOCKERIZED=1
|
DOCKERIZED=1
|
||||||
|
# GPT Envs
|
||||||
|
OPENAI_API_KEY=0
|
||||||
|
GPT_ENGINE=0
|
@ -3,7 +3,8 @@ import uuid
|
|||||||
import random
|
import random
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from plane.db.models import ProjectIdentifier
|
from plane.db.models import ProjectIdentifier
|
||||||
from plane.db.models import Issue, IssueComment, User, Project
|
from plane.db.models import Issue, IssueComment, User, Project, ProjectMember
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Update description and description html values for old descriptions
|
# Update description and description html values for old descriptions
|
||||||
@ -134,3 +135,42 @@ def update_project_cover_images():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
print("Failed")
|
print("Failed")
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_view_property():
|
||||||
|
try:
|
||||||
|
project_members = ProjectMember.objects.all()
|
||||||
|
updated_project_members = []
|
||||||
|
for project_member in project_members:
|
||||||
|
project_member.default_props = {
|
||||||
|
"filters": {"type": None},
|
||||||
|
"orderBy": "-created_at",
|
||||||
|
"collapsed": True,
|
||||||
|
"issueView": "list",
|
||||||
|
"filterIssue": None,
|
||||||
|
"groupByProperty": True,
|
||||||
|
"showEmptyGroups": True,
|
||||||
|
}
|
||||||
|
updated_project_members.append(project_member)
|
||||||
|
|
||||||
|
ProjectMember.objects.bulk_update(
|
||||||
|
updated_project_members, ["default_props"], batch_size=100
|
||||||
|
)
|
||||||
|
print("Success")
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print("Failed")
|
||||||
|
|
||||||
|
def update_label_color():
|
||||||
|
try:
|
||||||
|
labels = Label.objects.filter(color="")
|
||||||
|
updated_labels = []
|
||||||
|
for label in labels:
|
||||||
|
label.color = "#" + "%06x" % random.randint(0, 0xFFFFFF)
|
||||||
|
updated_labels.append(label)
|
||||||
|
|
||||||
|
Label.objects.bulk_update(updated_labels, ["color"], batch_size=100)
|
||||||
|
print("Success")
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print("Failed")
|
||||||
|
@ -139,6 +139,16 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
"module",
|
"module",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Validation if url already exists
|
||||||
|
def create(self, validated_data):
|
||||||
|
if ModuleLink.objects.filter(
|
||||||
|
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
||||||
|
).exists():
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"error": "URL already exists for this Issue"}
|
||||||
|
)
|
||||||
|
return ModuleLink.objects.create(**validated_data)
|
||||||
|
|
||||||
|
|
||||||
class ModuleSerializer(BaseSerializer):
|
class ModuleSerializer(BaseSerializer):
|
||||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
project_detail = ProjectSerializer(read_only=True, source="project")
|
||||||
|
@ -17,6 +17,7 @@ from plane.db.models import (
|
|||||||
WorkspaceMemberInvite,
|
WorkspaceMemberInvite,
|
||||||
Issue,
|
Issue,
|
||||||
IssueActivity,
|
IssueActivity,
|
||||||
|
WorkspaceMember,
|
||||||
)
|
)
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
|
||||||
@ -72,6 +73,20 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
|||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id)
|
||||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
if user.last_workspace_id is not None:
|
||||||
|
user_role = WorkspaceMember.objects.filter(
|
||||||
|
workspace_id=user.last_workspace_id, member=request.user.id
|
||||||
|
).first()
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": "Updated successfully",
|
||||||
|
"role": user_role.company_role
|
||||||
|
if user_role is not None
|
||||||
|
else None,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||||
)
|
)
|
||||||
|
@ -21,10 +21,13 @@ ROLE_CHOICES = (
|
|||||||
|
|
||||||
def get_default_props():
|
def get_default_props():
|
||||||
return {
|
return {
|
||||||
|
"filters": {"type": None},
|
||||||
|
"orderBy": "-created_at",
|
||||||
|
"collapsed": True,
|
||||||
"issueView": "list",
|
"issueView": "list",
|
||||||
"groupByProperty": None,
|
|
||||||
"orderBy": None,
|
|
||||||
"filterIssue": None,
|
"filterIssue": None,
|
||||||
|
"groupByProperty": True,
|
||||||
|
"showEmptyGroups": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
|||||||
) || "Email ID is not valid",
|
) || "Email ID is not valid",
|
||||||
}}
|
}}
|
||||||
error={errors.email}
|
error={errors.email}
|
||||||
placeholder="Enter you email Id"
|
placeholder="Enter your Email ID"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -98,8 +98,7 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
|||||||
<Command.Item
|
<Command.Item
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onSelect={() => handleIssueAssignees(option.value)}
|
onSelect={() => handleIssueAssignees(option.value)}
|
||||||
className="focus:bg-slate-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
{option.content}
|
{option.content}
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
@ -60,8 +60,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
|||||||
<Command.Item
|
<Command.Item
|
||||||
key={priority}
|
key={priority}
|
||||||
onSelect={() => handleIssueState(priority)}
|
onSelect={() => handleIssueState(priority)}
|
||||||
className="focus:bg-slate-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{getPriorityIcon(priority)}
|
{getPriorityIcon(priority)}
|
||||||
|
@ -75,8 +75,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
|
|||||||
<Command.Item
|
<Command.Item
|
||||||
key={state.id}
|
key={state.id}
|
||||||
onSelect={() => handleIssueState(state.id)}
|
onSelect={() => handleIssueState(state.id)}
|
||||||
className="focus:bg-slate-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||||
|
@ -51,6 +51,8 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
|||||||
import { CreateUpdateModuleModal } from "components/modules";
|
import { CreateUpdateModuleModal } from "components/modules";
|
||||||
import { CreateProjectModal } from "components/project";
|
import { CreateProjectModal } from "components/project";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
|
import { CreateUpdatePageModal } from "components/pages";
|
||||||
|
|
||||||
import { Spinner } from "components/ui";
|
import { Spinner } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import {
|
import {
|
||||||
@ -76,6 +78,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
const [isCreateModuleModalOpen, setIsCreateModuleModalOpen] = useState(false);
|
const [isCreateModuleModalOpen, setIsCreateModuleModalOpen] = useState(false);
|
||||||
const [isBulkDeleteIssuesModalOpen, setIsBulkDeleteIssuesModalOpen] = useState(false);
|
const [isBulkDeleteIssuesModalOpen, setIsBulkDeleteIssuesModalOpen] = useState(false);
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
|
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = React.useState<string>("");
|
const [searchTerm, setSearchTerm] = React.useState<string>("");
|
||||||
const [results, setResults] = useState<IWorkspaceSearchResults>({
|
const [results, setResults] = useState<IWorkspaceSearchResults>({
|
||||||
@ -193,6 +196,12 @@ export const CommandPalette: React.FC = () => {
|
|||||||
} else if (e.key.toLowerCase() === "p") {
|
} else if (e.key.toLowerCase() === "p") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsProjectModalOpen(true);
|
setIsProjectModalOpen(true);
|
||||||
|
} else if (e.key.toLowerCase() === "v") {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsCreateViewModalOpen(true);
|
||||||
|
} else if (e.key.toLowerCase() === "d") {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsCreateUpdatePageModalOpen(true);
|
||||||
} else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleCollapsed();
|
toggleCollapsed();
|
||||||
@ -323,6 +332,10 @@ export const CommandPalette: React.FC = () => {
|
|||||||
handleClose={() => setIsCreateViewModalOpen(false)}
|
handleClose={() => setIsCreateViewModalOpen(false)}
|
||||||
isOpen={isCreateViewModalOpen}
|
isOpen={isCreateViewModalOpen}
|
||||||
/>
|
/>
|
||||||
|
<CreateUpdatePageModal
|
||||||
|
isOpen={isCreateUpdatePageModalOpen}
|
||||||
|
handleClose={() => setIsCreateUpdatePageModalOpen(false)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{issueId && issueDetails && (
|
{issueId && issueDetails && (
|
||||||
@ -479,8 +492,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setIsPaletteOpen(false);
|
setIsPaletteOpen(false);
|
||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 overflow-hidden text-gray-700">
|
<div className="flex items-center gap-2 overflow-hidden text-gray-700">
|
||||||
<Icon className="h-4 w-4 text-gray-500" color="#6b7280" />
|
<Icon className="h-4 w-4 text-gray-500" color="#6b7280" />
|
||||||
@ -506,8 +518,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setPages([...pages, "change-issue-state"]);
|
setPages([...pages, "change-issue-state"]);
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<Squares2X2Icon className="h-4 w-4 text-gray-500" />
|
<Squares2X2Icon className="h-4 w-4 text-gray-500" />
|
||||||
@ -520,8 +531,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setPages([...pages, "change-issue-priority"]);
|
setPages([...pages, "change-issue-priority"]);
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<ChartBarIcon className="h-4 w-4 text-gray-500" />
|
<ChartBarIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -534,8 +544,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setPages([...pages, "change-issue-assignee"]);
|
setPages([...pages, "change-issue-assignee"]);
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<UsersIcon className="h-4 w-4 text-gray-500" />
|
<UsersIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -547,8 +556,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
handleIssueAssignees(user.id);
|
handleIssueAssignees(user.id);
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
{issueDetails?.assignees.includes(user.id) ? (
|
{issueDetails?.assignees.includes(user.id) ? (
|
||||||
@ -565,11 +573,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
|
||||||
<Command.Item
|
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
||||||
onSelect={deleteIssue}
|
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<TrashIcon className="h-4 w-4 text-gray-500" />
|
<TrashIcon className="h-4 w-4 text-gray-500" />
|
||||||
Delete issue
|
Delete issue
|
||||||
@ -580,8 +584,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setIsPaletteOpen(false);
|
setIsPaletteOpen(false);
|
||||||
copyIssueUrlToClipboard();
|
copyIssueUrlToClipboard();
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<LinkIcon className="h-4 w-4 text-gray-500" />
|
<LinkIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -591,11 +594,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Command.Group heading="Issue">
|
<Command.Group heading="Issue">
|
||||||
<Command.Item
|
<Command.Item onSelect={createNewIssue} className="focus:bg-gray-200">
|
||||||
onSelect={createNewIssue}
|
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
|
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new issue
|
Create new issue
|
||||||
@ -608,8 +607,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
<Command.Group heading="Project">
|
<Command.Group heading="Project">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={createNewProject}
|
onSelect={createNewProject}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
|
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
|
||||||
@ -625,8 +623,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
<Command.Group heading="Cycle">
|
<Command.Group heading="Cycle">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={createNewCycle}
|
onSelect={createNewCycle}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<ContrastIcon className="h-4 w-4" color="#6b7280" />
|
<ContrastIcon className="h-4 w-4" color="#6b7280" />
|
||||||
@ -639,8 +636,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
<Command.Group heading="Module">
|
<Command.Group heading="Module">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={createNewModule}
|
onSelect={createNewModule}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
|
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
|
||||||
@ -651,11 +647,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
</Command.Group>
|
</Command.Group>
|
||||||
|
|
||||||
<Command.Group heading="View">
|
<Command.Group heading="View">
|
||||||
<Command.Item
|
<Command.Item onSelect={createNewView} className="focus:outline-none">
|
||||||
onSelect={createNewView}
|
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<ViewListIcon className="h-4 w-4" color="#6b7280" />
|
<ViewListIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new view
|
Create new view
|
||||||
@ -673,8 +665,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setPages([...pages, "settings"]);
|
setPages([...pages, "settings"]);
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<SettingIcon className="h-4 w-4" color="#6b7280" />
|
<SettingIcon className="h-4 w-4" color="#6b7280" />
|
||||||
@ -685,8 +676,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
<Command.Group heading="Account">
|
<Command.Group heading="Account">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={createNewWorkspace}
|
onSelect={createNewWorkspace}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<FolderPlusIcon className="h-4 w-4 text-gray-500" />
|
<FolderPlusIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -703,8 +693,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
});
|
});
|
||||||
document.dispatchEvent(e);
|
document.dispatchEvent(e);
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<RocketLaunchIcon className="h-4 w-4 text-gray-500" />
|
<RocketLaunchIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -716,8 +705,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setIsPaletteOpen(false);
|
setIsPaletteOpen(false);
|
||||||
window.open("https://docs.plane.so/", "_blank");
|
window.open("https://docs.plane.so/", "_blank");
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<DocumentIcon className="h-4 w-4 text-gray-500" />
|
<DocumentIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -729,8 +717,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setIsPaletteOpen(false);
|
setIsPaletteOpen(false);
|
||||||
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
|
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<DiscordIcon className="h-4 w-4" color="#6b7280" />
|
<DiscordIcon className="h-4 w-4" color="#6b7280" />
|
||||||
@ -745,8 +732,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
"_blank"
|
"_blank"
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<GithubIcon className="h-4 w-4" color="#6b7280" />
|
<GithubIcon className="h-4 w-4" color="#6b7280" />
|
||||||
@ -758,8 +744,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setIsPaletteOpen(false);
|
setIsPaletteOpen(false);
|
||||||
(window as any).$crisp.push(["do", "chat:open"]);
|
(window as any).$crisp.push(["do", "chat:open"]);
|
||||||
}}
|
}}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-gray-500" />
|
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -774,8 +759,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => goToSettings()}
|
onSelect={() => goToSettings()}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -784,8 +768,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
</Command.Item>
|
</Command.Item>
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => goToSettings("members")}
|
onSelect={() => goToSettings("members")}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -794,8 +777,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
</Command.Item>
|
</Command.Item>
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => goToSettings("billing")}
|
onSelect={() => goToSettings("billing")}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -804,8 +786,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
</Command.Item>
|
</Command.Item>
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => goToSettings("integrations")}
|
onSelect={() => goToSettings("integrations")}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||||
@ -814,8 +795,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
</Command.Item>
|
</Command.Item>
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => goToSettings("import-export")}
|
onSelect={() => goToSettings("import-export")}
|
||||||
className="focus:bg-gray-200 focus:outline-none"
|
className="focus:outline-none"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-gray-700">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||||
|
@ -33,6 +33,8 @@ const shortcuts = [
|
|||||||
{ keys: "C", description: "To create issue" },
|
{ keys: "C", description: "To create issue" },
|
||||||
{ keys: "Q", description: "To create cycle" },
|
{ keys: "Q", description: "To create cycle" },
|
||||||
{ keys: "M", description: "To create module" },
|
{ keys: "M", description: "To create module" },
|
||||||
|
{ keys: "V", description: "To create view" },
|
||||||
|
{ keys: "D", description: "To create page" },
|
||||||
{ keys: "Delete", description: "To bulk delete issues" },
|
{ keys: "Delete", description: "To bulk delete issues" },
|
||||||
{ keys: "H", description: "To open shortcuts guide" },
|
{ keys: "H", description: "To open shortcuts guide" },
|
||||||
{
|
{
|
||||||
|
@ -33,6 +33,8 @@ import {
|
|||||||
PencilIcon,
|
PencilIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
|
ArrowTopRightOnSquareIcon,
|
||||||
|
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { handleIssuesMutation } from "constants/issue";
|
import { handleIssuesMutation } from "constants/issue";
|
||||||
@ -110,8 +112,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
else if (moduleId)
|
||||||
if (moduleId)
|
|
||||||
mutate<
|
mutate<
|
||||||
| {
|
| {
|
||||||
[key: string]: IIssue[];
|
[key: string]: IIssue[];
|
||||||
@ -123,18 +124,18 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
else
|
||||||
mutate<
|
mutate<
|
||||||
| {
|
| {
|
||||||
[key: string]: IIssue[];
|
[key: string]: IIssue[];
|
||||||
}
|
}
|
||||||
| IIssue[]
|
| IIssue[]
|
||||||
>(
|
>(
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params),
|
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params),
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
||||||
@ -212,6 +213,15 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||||
Copy issue link
|
Copy issue link
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||||
|
Open issue in new tab
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</a>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div
|
<div
|
||||||
className={`mb-3 rounded bg-white shadow ${
|
className={`mb-3 rounded bg-white shadow ${
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
ChatBubbleLeftEllipsisIcon,
|
ChatBubbleLeftEllipsisIcon,
|
||||||
RectangleGroupIcon,
|
RectangleGroupIcon,
|
||||||
Squares2X2Icon,
|
Squares2X2Icon,
|
||||||
|
TrashIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
|
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
|
||||||
@ -77,6 +78,10 @@ const activityDetails: {
|
|||||||
message: "set the parent to",
|
message: "set the parent to",
|
||||||
icon: <UserIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: <UserIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
issue: {
|
||||||
|
message: "deleted the issue.",
|
||||||
|
icon: <TrashIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Feeds: React.FC<any> = ({ activities }) => (
|
export const Feeds: React.FC<any> = ({ activities }) => (
|
||||||
|
@ -125,12 +125,12 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
isOpen ? "block" : "hidden"
|
isOpen ? "block" : "hidden"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{((content && content !== "") || htmlContent) && (
|
{((content && content !== "") || htmlContent !== "<p></p>") && (
|
||||||
<div className="text-sm page-block-section">
|
<div className="remirror-section text-sm">
|
||||||
Content:
|
Content:
|
||||||
<RemirrorRichTextEditor
|
<RemirrorRichTextEditor
|
||||||
value={htmlContent ?? <p>{content}</p>}
|
value={htmlContent ?? <p>{content}</p>}
|
||||||
customClassName="-mx-3 -my-3"
|
customClassName="-m-3"
|
||||||
noBorder
|
noBorder
|
||||||
borderOnFocus={false}
|
borderOnFocus={false}
|
||||||
editable={false}
|
editable={false}
|
||||||
|
@ -107,7 +107,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
|||||||
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
||||||
placeholder="Search for images"
|
placeholder="Search for images"
|
||||||
/>
|
/>
|
||||||
<PrimaryButton className="bg-indigo-600" size="sm">
|
<PrimaryButton type="submit" className="bg-indigo-600" size="sm">
|
||||||
Search
|
Search
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</form>
|
</form>
|
||||||
|
@ -18,7 +18,7 @@ import { AllLists, AllBoards, FilterList } from "components/core";
|
|||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
import { TransferIssuesModal } from "components/cycles";
|
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||||
// ui
|
// ui
|
||||||
import { EmptySpace, EmptySpaceItem, PrimaryButton, Spinner } from "components/ui";
|
import { EmptySpace, EmptySpaceItem, PrimaryButton, Spinner } from "components/ui";
|
||||||
import { CalendarView } from "./calendar-view";
|
import { CalendarView } from "./calendar-view";
|
||||||
@ -459,23 +459,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
{groupedByIssues ? (
|
{groupedByIssues ? (
|
||||||
isNotEmpty ? (
|
isNotEmpty ? (
|
||||||
<>
|
<>
|
||||||
{isCompleted && (
|
{isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
|
||||||
<ExclamationIcon height={14} width={14} />
|
|
||||||
<span>Completed cycles are not editable.</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<PrimaryButton
|
|
||||||
onClick={() => setTransferIssuesModal(true)}
|
|
||||||
className="flex items-center gap-3 rounded-lg"
|
|
||||||
>
|
|
||||||
<TransferIcon className="h-4 w-4" />
|
|
||||||
<span>Transfer Issues</span>
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{issueView === "list" ? (
|
{issueView === "list" ? (
|
||||||
<AllLists
|
<AllLists
|
||||||
type={type}
|
type={type}
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
PencilIcon,
|
PencilIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
|
ArrowTopRightOnSquareIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||||
@ -178,6 +179,15 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||||
Copy issue link
|
Copy issue link
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||||
|
Open issue in new tab
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</a>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div className="border-b border-gray-300 last:border-b-0">
|
<div className="border-b border-gray-300 last:border-b-0">
|
||||||
<div
|
<div
|
||||||
|
@ -8,3 +8,4 @@ export * from "./sidebar";
|
|||||||
export * from "./single-cycle-card";
|
export * from "./single-cycle-card";
|
||||||
export * from "./empty-cycle";
|
export * from "./empty-cycle";
|
||||||
export * from "./transfer-issues-modal";
|
export * from "./transfer-issues-modal";
|
||||||
|
export * from "./transfer-issues";
|
56
apps/app/components/cycles/transfer-issues.tsx
Normal file
56
apps/app/components/cycles/transfer-issues.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// component
|
||||||
|
import { PrimaryButton, Tooltip } from "components/ui";
|
||||||
|
// icon
|
||||||
|
import { ExclamationIcon, TransferIcon } from "components/icons";
|
||||||
|
// services
|
||||||
|
import cycleServices from "services/cycles.service";
|
||||||
|
// fetch-key
|
||||||
|
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
|
|
||||||
|
const { data: cycleDetails } = useSWR(
|
||||||
|
cycleId ? CYCLE_DETAILS(cycleId as string) : null,
|
||||||
|
workspaceSlug && projectId && cycleId
|
||||||
|
? () =>
|
||||||
|
cycleServices.getCycleDetails(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
cycleId as string
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const transferableIssuesCount = cycleDetails
|
||||||
|
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
|
||||||
|
: 0;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between -mt-4 mb-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<ExclamationIcon height={14} width={14} />
|
||||||
|
<span>Completed cycles are not editable.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{transferableIssuesCount > 0 && (
|
||||||
|
<div>
|
||||||
|
<PrimaryButton onClick={handleClick} className="flex items-center gap-3 rounded-lg">
|
||||||
|
<TransferIcon className="h-4 w-4" />
|
||||||
|
<span className="text-white">Transfer Issues</span>
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
607
apps/app/components/emoji-icon-picker/icons.json
Normal file
607
apps/app/components/emoji-icon-picker/icons.json
Normal file
@ -0,0 +1,607 @@
|
|||||||
|
{
|
||||||
|
"material_rounded": [
|
||||||
|
{
|
||||||
|
"name": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "done"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "check_circle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "favorite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "arrow_back"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "star"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "logout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "add_circle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cancel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "arrow_drop_down"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "more_vert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "check"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "check_box"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toggle_on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "open_in_new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "refresh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "login"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "radio_button_unchecked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "more_horiz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "apps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "radio_button_checked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toggle_off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bolt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "arrow_upward"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "filter_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_forever"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "autorenew"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sort"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sync"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "add_box"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "block"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "restart_alt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "menu_open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shopping_cart_checkout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expand_circle_down"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "backspace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "undo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "done_all"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "do_not_disturb_on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "open_in_full"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "double_arrow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sync_alt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zoom_in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "done_outline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "drag_indicator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fullscreen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "star_half"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "settings_accessibility"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reply"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exit_to_app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unfold_more"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "library_add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cached"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "select_check_box"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "terminal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "change_circle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "disabled_by_default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swap_horiz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swap_vert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "app_registration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "download_for_offline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "close_fullscreen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "file_open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minimize"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "open_with"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dataset"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "add_task"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyboard_voice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_new_folder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "forward"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "settings_applications"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "compare_arrows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "redo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zoom_out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "publish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "token"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "switch_access_shortcut"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fullscreen_exit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sort_by_alpha"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_sweep"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "indeterminate_check_box"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "view_timeline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "settings_backup_restore"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "arrow_drop_down_circle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "assistant_navigation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sync_problem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clear_all"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "density_medium"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "heart_plus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "filter_alt_off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expand"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "subdirectory_arrow_right"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "download_done"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "arrow_outward"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "123"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swipe_left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "auto_mode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "saved_search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "place_item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_update_alt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "javascript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search_off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "select_all"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fit_screen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swipe_up"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dynamic_form"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hide_source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swipe_right"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "switch_access_shortcut_add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "browse_gallery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "css"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "density_small"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "assistant_direction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "check_small"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "youtube_searched_for"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "move_up"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swap_horizontal_circle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data_thresholding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_mobile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "move_down"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dataset_linked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyboard_command_key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "view_kanban"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swipe_down"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "key_off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "transcribe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "send_time_extension"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swipe_down_alt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swipe_left_alt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swipe_right_alt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "swipe_up_alt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyboard_option_key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cycle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rebase"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rebase_edit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "empty_dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "magic_exchange"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "acute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "point_scan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "step_into"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cheer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "emoticon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "explosion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "water_bottle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "weather_hail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syringe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pill"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "genetics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "allergy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "medical_mask"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body_fat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "barefoot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "infrared"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wrist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metabolism"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "conditions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "taunt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "altitude"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tibia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "footprint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eyeglasses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "man_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "woman_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rheumatology"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tornado"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "landslide"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "foggy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "severe_cold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tsunami"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vape_free"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sign_language"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "emoji_symbols"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clear_night"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "emoji_food_beverage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "thunderstorm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "communication"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rocket"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quiz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mood"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gavel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eco"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "diamond"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "forest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rainy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skull"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Tab, Transition, Popover } from "@headlessui/react";
|
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||||
|
// react colors
|
||||||
|
import { TwitterPicker } from "react-color";
|
||||||
// types
|
// types
|
||||||
import { Props } from "./types";
|
import { Props } from "./types";
|
||||||
// emojis
|
// emojis
|
||||||
import emojis from "./emojis.json";
|
import emojis from "./emojis.json";
|
||||||
|
import icons from "./icons.json";
|
||||||
// helpers
|
// helpers
|
||||||
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
|
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
|
||||||
import { getRandomEmoji } from "helpers/common.helper";
|
import { getRandomEmoji } from "helpers/common.helper";
|
||||||
@ -22,10 +25,18 @@ const tabOptions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
const EmojiIconPicker: React.FC<Props> = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onIconColorChange,
|
||||||
|
onIconsClick,
|
||||||
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [openColorPicker, setOpenColorPicker] = useState(false);
|
||||||
|
const [activeColor, setActiveColor] = useState<string>("#020617");
|
||||||
|
|
||||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
|
|
||||||
@ -58,20 +69,25 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute z-10 mt-2 w-80 rounded-lg bg-white shadow-lg">
|
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] bg-white shadow-lg">
|
||||||
<div className="h-72 w-80 overflow-auto rounded border bg-white p-2 shadow-2xl">
|
<div className="h-[230px] w-[250px] overflow-auto border rounded-[4px] bg-white p-2 shadow-xl">
|
||||||
<Tab.Group as="div" className="flex h-full w-full flex-col">
|
<Tab.Group as="div" className="flex h-full w-full flex-col">
|
||||||
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 border-b p-1">
|
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
|
||||||
{tabOptions.map((tab) => (
|
{tabOptions.map((tab) => (
|
||||||
<Tab
|
<Tab key={tab.key} as={React.Fragment}>
|
||||||
key={tab.key}
|
{({ selected }) => (
|
||||||
className={({ selected }) =>
|
<button
|
||||||
`-my-1 w-1/2 border-b py-2 text-center text-sm font-medium outline-none transition-colors ${
|
type="button"
|
||||||
selected ? "border-theme" : "border-transparent"
|
onClick={() => {
|
||||||
}`
|
setOpenColorPicker(false);
|
||||||
}
|
}}
|
||||||
>
|
className={`-my-1 w-1/2 border-b pb-2 text-center text-sm font-medium outline-none transition-colors ${
|
||||||
{tab.title}
|
selected ? "border-theme" : "border-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
@ -79,12 +95,12 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
|||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
{recentEmojis.length > 0 && (
|
{recentEmojis.length > 0 && (
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<h3 className="mb-2">Recent Emojis</h3>
|
{/* <h3 className="mb-2">Recent Emojis</h3> */}
|
||||||
<div className="grid grid-cols-9 gap-2">
|
<div className="grid grid-cols-10">
|
||||||
{recentEmojis.map((emoji) => (
|
{recentEmojis.map((emoji) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="select-none text-lg hover:bg-hover-gray"
|
className="h-4 w-4 select-none text-sm hover:bg-hover-gray flex items-center justify-between"
|
||||||
key={emoji}
|
key={emoji}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(emoji);
|
onChange(emoji);
|
||||||
@ -97,13 +113,14 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<hr className="w-full h-[1px] mb-2" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2">All Emojis</h3>
|
{/* <h3 className="mb-1">All Emojis</h3> */}
|
||||||
<div className="grid grid-cols-9 gap-2">
|
<div className="grid grid-cols-10 gap-y-1">
|
||||||
{emojis.map((emoji) => (
|
{emojis.map((emoji) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="select-none text-lg hover:bg-hover-gray"
|
className="h-4 w-4 mb-1 select-none text-sm hover:bg-hover-gray flex items-center"
|
||||||
key={emoji}
|
key={emoji}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(emoji);
|
onChange(emoji);
|
||||||
@ -117,9 +134,76 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel className="flex h-full w-full flex-col items-center justify-center">
|
<div className="py-2">
|
||||||
<p>Coming Soon...</p>
|
<div className="relative">
|
||||||
</Tab.Panel>
|
<div className="pb-2 flex items-center justify-between">
|
||||||
|
{[
|
||||||
|
"#D687FF",
|
||||||
|
"#F7AE59",
|
||||||
|
"#FF6B00",
|
||||||
|
"#8CC1FF",
|
||||||
|
"#FCBE1D",
|
||||||
|
"#18904F",
|
||||||
|
"#ADF672",
|
||||||
|
"#05C3FF",
|
||||||
|
"#000000",
|
||||||
|
].map((curCol) => (
|
||||||
|
<span
|
||||||
|
className="w-4 h-4 rounded-full cursor-pointer"
|
||||||
|
style={{ backgroundColor: curCol }}
|
||||||
|
onClick={() => setActiveColor(curCol)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenColorPicker((prev) => !prev)}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-4 h-4 rounded-full conical-gradient"
|
||||||
|
style={{ backgroundColor: activeColor }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TwitterPicker
|
||||||
|
className={`m-2 !absolute top-4 left-4 z-10 ${
|
||||||
|
openColorPicker ? "block" : "hidden"
|
||||||
|
}`}
|
||||||
|
color={activeColor}
|
||||||
|
onChange={(color) => {
|
||||||
|
setActiveColor(color.hex);
|
||||||
|
if (onIconColorChange) onIconColorChange(color.hex);
|
||||||
|
}}
|
||||||
|
triangle="hide"
|
||||||
|
width="205px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className="w-full h-[1px] mb-1" />
|
||||||
|
<Tab.Panel className="flex h-full w-full flex-col justify-center">
|
||||||
|
<div className="grid grid-cols-10 mt-1 ml-1 gap-1">
|
||||||
|
{icons.material_rounded.map((icon) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-4 w-4 mb-1 select-none text-lg hover:bg-hover-gray flex items-center"
|
||||||
|
key={icon.name}
|
||||||
|
onClick={() => {
|
||||||
|
if (onIconsClick) onIconsClick(icon.name);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{ color: activeColor }}
|
||||||
|
className="material-symbols-rounded text-base"
|
||||||
|
>
|
||||||
|
{icon.name}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Tab.Panel>
|
||||||
|
</div>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,4 +2,6 @@ export type Props = {
|
|||||||
label: string | React.ReactNode;
|
label: string | React.ReactNode;
|
||||||
value: any;
|
value: any;
|
||||||
onChange: (data: any) => void;
|
onChange: (data: any) => void;
|
||||||
|
onIconsClick?: (data: any) => void;
|
||||||
|
onIconColorChange?: (data: any) => void;
|
||||||
};
|
};
|
||||||
|
@ -96,7 +96,7 @@ export const AddComment: React.FC = () => {
|
|||||||
setValue("comment_json", jsonValue);
|
setValue("comment_json", jsonValue);
|
||||||
setValue("comment_html", htmlValue);
|
setValue("comment_html", htmlValue);
|
||||||
}}
|
}}
|
||||||
// placeholder="Enter Your comment..."
|
placeholder="Enter your comment..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -104,7 +104,7 @@ export const AddComment: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="rounded-md bg-gray-300 p-2 px-4 text-sm text-black hover:bg-gray-300"
|
className="rounded-md bg-gray-300 p-2 px-4 text-sm text-black hover:bg-gray-300 mt-4"
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Adding..." : "Comment"}
|
{isSubmitting ? "Adding..." : "Comment"}
|
||||||
</button>
|
</button>
|
||||||
|
@ -6,6 +6,10 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import aiService from "services/ai.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { GptAssistantModal } from "components/core";
|
import { GptAssistantModal } from "components/core";
|
||||||
import {
|
import {
|
||||||
@ -83,10 +87,13 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||||
|
|
||||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||||
|
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
@ -102,6 +109,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const issueName = watch("name");
|
||||||
|
|
||||||
const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
const similarIssue = issues?.find((i: IIssue) => cosineSimilarity(i.name, value) > 0.7);
|
const similarIssue = issues?.find((i: IIssue) => cosineSimilarity(i.name, value) > 0.7);
|
||||||
@ -126,6 +135,44 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
|
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handelAutoGenerateDescription = async () => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
setIAmFeelingLucky(true);
|
||||||
|
|
||||||
|
aiService
|
||||||
|
.createGptTask(workspaceSlug as string, projectId as string, {
|
||||||
|
prompt: issueName,
|
||||||
|
task: "Generate a proper description for this issue in context of a project management software.",
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.response === "")
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message:
|
||||||
|
"Issue title isn't informative enough to generate the description. Please try with a different title.",
|
||||||
|
});
|
||||||
|
else handleAiAssistance(res.response_html);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.status === 429)
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message:
|
||||||
|
"You have reached the maximum number of requests of 50 requests per month per user.",
|
||||||
|
});
|
||||||
|
else
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Some error occurred. Please try again.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => setIAmFeelingLucky(false));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocus("name");
|
setFocus("name");
|
||||||
|
|
||||||
@ -245,10 +292,28 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex justify-end -mb-2 mr-2">
|
<div className="flex justify-end -mb-2">
|
||||||
|
{issueName && issueName !== "" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100 ${
|
||||||
|
iAmFeelingLucky ? "cursor-wait" : ""
|
||||||
|
}`}
|
||||||
|
onClick={handelAutoGenerateDescription}
|
||||||
|
disabled={iAmFeelingLucky}
|
||||||
|
>
|
||||||
|
{iAmFeelingLucky ? (
|
||||||
|
"Generating response..."
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
|
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
|
||||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
||||||
>
|
>
|
||||||
<SparklesIcon className="h-4 w-4" />
|
<SparklesIcon className="h-4 w-4" />
|
||||||
@ -267,7 +332,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
}
|
}
|
||||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||||
placeholder="Description"
|
placeholder="Describe the issue..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -153,11 +153,20 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
|
|
||||||
await issuesService
|
await issuesService
|
||||||
.createIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, payload)
|
.createIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, payload)
|
||||||
.then((res) => {
|
.then(() => mutate(ISSUE_DETAILS(issueDetail.id)))
|
||||||
mutate(ISSUE_DETAILS(issueDetail.id));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
if (err.status === 400)
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "This URL already exists for this issue.",
|
||||||
|
});
|
||||||
|
else
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Something went wrong. Please try again.",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -107,15 +107,20 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
|
|||||||
|
|
||||||
await modulesService
|
await modulesService
|
||||||
.createModuleLink(workspaceSlug as string, projectId as string, moduleId as string, payload)
|
.createModuleLink(workspaceSlug as string, projectId as string, moduleId as string, payload)
|
||||||
.then((res) => {
|
.then(() => mutate(MODULE_DETAILS(moduleId as string)))
|
||||||
mutate(MODULE_DETAILS(moduleId as string));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setToastAlert({
|
if (err.status === 400)
|
||||||
type: "error",
|
setToastAlert({
|
||||||
title: "Error!",
|
type: "error",
|
||||||
message: "Couldn't create the link. Please try again.",
|
title: "Error!",
|
||||||
});
|
message: "This URL already exists for this module.",
|
||||||
|
});
|
||||||
|
else
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Something went wrong. Please try again.",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,9 +22,10 @@ const defaultValues: Partial<IUser> = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
user?: IUser;
|
user?: IUser;
|
||||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
setUserRole: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserDetails: React.FC<Props> = ({ user, setStep }) => {
|
export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) => {
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -53,13 +54,15 @@ export const UserDetails: React.FC<Props> = ({ user, setStep }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user)
|
if (user) {
|
||||||
reset({
|
reset({
|
||||||
first_name: user.first_name,
|
first_name: user.first_name,
|
||||||
last_name: user.last_name,
|
last_name: user.last_name,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
});
|
});
|
||||||
}, [user, reset]);
|
setUserRole(user.role);
|
||||||
|
}
|
||||||
|
}, [user, reset, setUserRole]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="flex w-full items-center justify-center" onSubmit={handleSubmit(onSubmit)}>
|
<form className="flex w-full items-center justify-center" onSubmit={handleSubmit(onSubmit)}>
|
||||||
@ -101,7 +104,10 @@ export const UserDetails: React.FC<Props> = ({ user, setStep }) => {
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={(value: any) => {
|
||||||
|
onChange(value);
|
||||||
|
setUserRole(value ?? null);
|
||||||
|
}}
|
||||||
label={value ? value.toString() : "Select your role"}
|
label={value ? value.toString() : "Select your role"}
|
||||||
input
|
input
|
||||||
width="w-full"
|
width="w-full"
|
||||||
|
210
apps/app/components/pages/create-update-block-inline.tsx
Normal file
210
apps/app/components/pages/create-update-block-inline.tsx
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import pagesService from "services/pages.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// ui
|
||||||
|
import { Input, Loader, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
|
// types
|
||||||
|
import { IPageBlock } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PAGE_BLOCKS_LIST } from "constants/fetch-keys";
|
||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleClose: () => void;
|
||||||
|
data?: IPageBlock;
|
||||||
|
setIsSyncing?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
focus?: keyof IPageBlock;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
name: "",
|
||||||
|
description: "<p></p>",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<Loader className="mx-4 mt-6">
|
||||||
|
<Loader.Item height="100px" width="100%" />
|
||||||
|
</Loader>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||||
|
handleClose,
|
||||||
|
data,
|
||||||
|
setIsSyncing,
|
||||||
|
focus,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, pageId } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
setFocus,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = useForm<IPageBlock>({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
if (data) handleClose();
|
||||||
|
|
||||||
|
reset();
|
||||||
|
}, [handleClose, reset, data]);
|
||||||
|
|
||||||
|
const createPageBlock = async (formData: Partial<IPageBlock>) => {
|
||||||
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
|
|
||||||
|
await pagesService
|
||||||
|
.createPageBlock(workspaceSlug as string, projectId as string, pageId as string, {
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description ?? "",
|
||||||
|
description_html: formData.description_html ?? "<p></p>",
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
mutate<IPageBlock[]>(
|
||||||
|
PAGE_BLOCKS_LIST(pageId as string),
|
||||||
|
(prevData) => [...(prevData as IPageBlock[]), res],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Page could not be created. Please try again.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => onClose());
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePageBlock = async (formData: Partial<IPageBlock>) => {
|
||||||
|
if (!workspaceSlug || !projectId || !pageId || !data) return;
|
||||||
|
|
||||||
|
if (data.issue && data.sync && setIsSyncing) setIsSyncing(true);
|
||||||
|
|
||||||
|
mutate<IPageBlock[]>(
|
||||||
|
PAGE_BLOCKS_LIST(pageId as string),
|
||||||
|
(prevData) =>
|
||||||
|
prevData?.map((p) => {
|
||||||
|
if (p.id === data.id) return { ...p, ...formData };
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await pagesService
|
||||||
|
.patchPageBlock(workspaceSlug as string, projectId as string, pageId as string, data.id, {
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
description_html: formData.description_html,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
mutate(PAGE_BLOCKS_LIST(pageId as string));
|
||||||
|
if (data.issue && data.sync)
|
||||||
|
issuesService
|
||||||
|
.patchIssue(workspaceSlug as string, projectId as string, data.issue, {
|
||||||
|
name: res.name,
|
||||||
|
description: res.description,
|
||||||
|
description_html: res.description_html,
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (setIsSyncing) setIsSyncing(false);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => onClose());
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focus) setFocus(focus);
|
||||||
|
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
description_html: data.description_html,
|
||||||
|
});
|
||||||
|
}, [reset, data, focus, setFocus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") handleClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") handleClose();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [handleClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border rounded-[10px] p-2 ml-6">
|
||||||
|
<form onSubmit={data ? handleSubmit(updatePageBlock) : handleSubmit(createPageBlock)}>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
placeholder="Title"
|
||||||
|
register={register}
|
||||||
|
className="min-h-10 block w-full resize-none overflow-hidden border-none bg-transparent py-1 text-base ring-0 -ml-2 focus:ring-gray-200"
|
||||||
|
role="textbox"
|
||||||
|
autoComplete="off"
|
||||||
|
maxLength={255}
|
||||||
|
/>
|
||||||
|
<div className="page-block-section font relative -mx-3 -mt-3">
|
||||||
|
<Controller
|
||||||
|
name="description"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<RemirrorRichTextEditor
|
||||||
|
value={
|
||||||
|
!value || (typeof value === "object" && Object.keys(value).length === 0)
|
||||||
|
? watch("description_html")
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||||
|
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||||
|
placeholder="Description"
|
||||||
|
customClassName="text-sm"
|
||||||
|
noBorder
|
||||||
|
borderOnFocus={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end items-center gap-2">
|
||||||
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" disabled={watch("name") === ""} loading={isSubmitting}>
|
||||||
|
{data
|
||||||
|
? isSubmitting
|
||||||
|
? "Updating..."
|
||||||
|
: "Update block"
|
||||||
|
: isSubmitting
|
||||||
|
? "Adding..."
|
||||||
|
: "Add block"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./pages-list";
|
export * from "./pages-list";
|
||||||
|
export * from "./create-update-block-inline";
|
||||||
export * from "./create-update-page-modal";
|
export * from "./create-update-page-modal";
|
||||||
export * from "./delete-page-modal";
|
export * from "./delete-page-modal";
|
||||||
export * from "./page-form";
|
export * from "./page-form";
|
||||||
|
@ -152,6 +152,32 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const partialUpdatePage = (page: IPage, formData: Partial<IPage>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
mutate<IPage[]>(
|
||||||
|
ALL_PAGES_LIST(projectId as string),
|
||||||
|
(prevData) => (prevData ?? []).map((p) => ({ ...p, ...formData })),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
mutate<IPage[]>(
|
||||||
|
MY_PAGES_LIST(projectId as string),
|
||||||
|
(prevData) => (prevData ?? []).map((p) => ({ ...p, ...formData })),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
mutate<IPage[]>(
|
||||||
|
FAVORITE_PAGES_LIST(projectId as string),
|
||||||
|
(prevData) => (prevData ?? []).map((p) => ({ ...p, ...formData })),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
pagesService
|
||||||
|
.patchPage(workspaceSlug as string, projectId as string, page.id, formData)
|
||||||
|
.then(() => {
|
||||||
|
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdatePageModal
|
<CreateUpdatePageModal
|
||||||
@ -176,6 +202,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
|||||||
handleDeletePage={() => handleDeletePage(page)}
|
handleDeletePage={() => handleDeletePage(page)}
|
||||||
handleAddToFavorites={() => handleAddToFavorites(page)}
|
handleAddToFavorites={() => handleAddToFavorites(page)}
|
||||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
|
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
|
||||||
|
partialUpdatePage={partialUpdatePage}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -189,6 +216,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
|||||||
handleDeletePage={() => handleDeletePage(page)}
|
handleDeletePage={() => handleDeletePage(page)}
|
||||||
handleAddToFavorites={() => handleAddToFavorites(page)}
|
handleAddToFavorites={() => handleAddToFavorites(page)}
|
||||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
|
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
|
||||||
|
partialUpdatePage={partialUpdatePage}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -202,6 +230,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
|||||||
handleDeletePage={() => handleDeletePage(page)}
|
handleDeletePage={() => handleDeletePage(page)}
|
||||||
handleAddToFavorites={() => handleAddToFavorites(page)}
|
handleAddToFavorites={() => handleAddToFavorites(page)}
|
||||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
|
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
|
||||||
|
partialUpdatePage={partialUpdatePage}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,20 +8,29 @@ import { mutate } from "swr";
|
|||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
// react-beautiful-dnd
|
||||||
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// services
|
// services
|
||||||
import pagesService from "services/pages.service";
|
import pagesService from "services/pages.service";
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
|
import aiService from "services/ai.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateIssueModal } from "components/issues";
|
|
||||||
import { GptAssistantModal } from "components/core";
|
import { GptAssistantModal } from "components/core";
|
||||||
|
import { CreateUpdateBlockInline } from "components/pages";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Input, Loader, TextArea } from "components/ui";
|
import { CustomMenu, Loader } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { LayerDiagonalIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
import { ArrowPathIcon } from "@heroicons/react/20/solid";
|
import { ArrowPathIcon } from "@heroicons/react/20/solid";
|
||||||
import { BoltIcon, CheckIcon, SparklesIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
BoltIcon,
|
||||||
|
CheckIcon,
|
||||||
|
EllipsisVerticalIcon,
|
||||||
|
PencilIcon,
|
||||||
|
SparklesIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -32,6 +41,8 @@ import { PAGE_BLOCKS_LIST } from "constants/fetch-keys";
|
|||||||
type Props = {
|
type Props = {
|
||||||
block: IPageBlock;
|
block: IPageBlock;
|
||||||
projectDetails: IProject | undefined;
|
projectDetails: IProject | undefined;
|
||||||
|
index: number;
|
||||||
|
handleNewBlock: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||||
@ -43,9 +54,15 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
export const SinglePageBlock: React.FC<Props> = ({
|
||||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
block,
|
||||||
|
projectDetails,
|
||||||
|
index,
|
||||||
|
handleNewBlock,
|
||||||
|
}) => {
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
|
const [createBlockForm, setCreateBlockForm] = useState(false);
|
||||||
|
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||||
|
|
||||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||||
|
|
||||||
@ -54,7 +71,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { handleSubmit, watch, reset, setValue, control } = useForm<IPageBlock>({
|
const { handleSubmit, watch, reset, setValue, register } = useForm<IPageBlock>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
description: {},
|
description: {},
|
||||||
@ -136,10 +153,6 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const editAndPushBlockIntoIssues = async () => {
|
|
||||||
setCreateUpdateIssueModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deletePageBlock = async () => {
|
const deletePageBlock = async () => {
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
|
|
||||||
@ -160,6 +173,44 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handelAutoGenerateDescription = async () => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
setIAmFeelingLucky(true);
|
||||||
|
|
||||||
|
aiService
|
||||||
|
.createGptTask(workspaceSlug as string, projectId as string, {
|
||||||
|
prompt: block.name,
|
||||||
|
task: "Generate a proper description for this issue in context of a project management software.",
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.response === "")
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message:
|
||||||
|
"Block title isn't informative enough to generate the description. Please try with a different title.",
|
||||||
|
});
|
||||||
|
else handleAiAssistance(res.response_html);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.status === 429)
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message:
|
||||||
|
"You have reached the maximum number of requests of 50 requests per month per user.",
|
||||||
|
});
|
||||||
|
else
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Some error occurred. Please try again.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => setIAmFeelingLucky(false));
|
||||||
|
};
|
||||||
|
|
||||||
const handleAiAssistance = async (response: string) => {
|
const handleAiAssistance = async (response: string) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
@ -228,110 +279,145 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
reset({ ...block });
|
reset({ ...block });
|
||||||
}, [reset, block]);
|
}, [reset, block]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !createBlockForm) handleNewBlock();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !createBlockForm) handleNewBlock();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [handleNewBlock, createBlockForm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Draggable draggableId={block.id} index={index} isDragDisabled={createBlockForm}>
|
||||||
<CreateUpdateIssueModal
|
{(provided, snapshot) => (
|
||||||
isOpen={createUpdateIssueModal}
|
<>
|
||||||
handleClose={() => setCreateUpdateIssueModal(false)}
|
{createBlockForm ? (
|
||||||
prePopulateData={{
|
<div
|
||||||
name: watch("name"),
|
className="mb-4 pt-4"
|
||||||
description: watch("description"),
|
ref={provided.innerRef}
|
||||||
description_html: watch("description_html"),
|
{...provided.draggableProps}
|
||||||
}}
|
{...provided.dragHandleProps}
|
||||||
/>
|
>
|
||||||
<div className="-mx-3 mt-4 flex items-center justify-between gap-2">
|
<CreateUpdateBlockInline
|
||||||
<Input
|
handleClose={() => setCreateBlockForm(false)}
|
||||||
id="name"
|
data={block}
|
||||||
name="name"
|
setIsSyncing={setIsSyncing}
|
||||||
placeholder="Block title"
|
/>
|
||||||
value={watch("name")}
|
</div>
|
||||||
onBlur={handleSubmit(updatePageBlock)}
|
) : (
|
||||||
onChange={(e) => setValue("name", e.target.value)}
|
<div
|
||||||
required={true}
|
className={`group relative pl-6 ${
|
||||||
className="min-h-10 block w-full resize-none overflow-hidden border-none bg-transparent py-1 text-base font-medium ring-0 focus:ring-1 focus:ring-gray-200"
|
snapshot.isDragging ? "border-2 bg-white border-theme shadow-lg rounded-md p-6" : ""
|
||||||
role="textbox"
|
}`}
|
||||||
/>
|
ref={provided.innerRef}
|
||||||
<div className="flex flex-shrink-0 items-center gap-2">
|
{...provided.draggableProps}
|
||||||
{block.issue && block.sync && (
|
>
|
||||||
<div className="flex flex-shrink-0 cursor-default items-center gap-1 rounded bg-gray-100 py-1 px-1.5 text-xs">
|
<button
|
||||||
{isSyncing ? (
|
type="button"
|
||||||
<ArrowPathIcon className="h-3 w-3 animate-spin" />
|
className="absolute top-4 -left-2 p-0.5 hover:bg-gray-100 rounded hidden group-hover:flex"
|
||||||
) : (
|
{...provided.dragHandleProps}
|
||||||
<CheckIcon className="h-3 w-3" />
|
>
|
||||||
)}
|
<EllipsisVerticalIcon className="h-[18px]" />
|
||||||
{isSyncing ? "Syncing..." : "Synced"}
|
<EllipsisVerticalIcon className="h-[18px] -ml-3" />
|
||||||
|
</button>
|
||||||
|
<div className="absolute top-4 right-0 items-center gap-2 hidden group-hover:flex bg-white pl-4">
|
||||||
|
{block.issue && block.sync && (
|
||||||
|
<div className="flex flex-shrink-0 cursor-default items-center gap-1 rounded bg-gray-100 py-1 px-1.5 text-xs">
|
||||||
|
{isSyncing ? (
|
||||||
|
<ArrowPathIcon className="h-3 w-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<CheckIcon className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
{isSyncing ? "Syncing..." : "Synced"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100 ${
|
||||||
|
iAmFeelingLucky ? "cursor-wait bg-gray-100" : ""
|
||||||
|
}`}
|
||||||
|
onClick={handelAutoGenerateDescription}
|
||||||
|
disabled={iAmFeelingLucky}
|
||||||
|
>
|
||||||
|
{iAmFeelingLucky ? (
|
||||||
|
"Generating response..."
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
|
||||||
|
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
<SparklesIcon className="h-4 w-4" />
|
||||||
|
AI
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
|
||||||
|
onClick={() => setCreateBlockForm(true)}
|
||||||
|
>
|
||||||
|
<PencilIcon className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<CustomMenu label={<BoltIcon className="h-4.5 w-3.5" />} noBorder noChevron>
|
||||||
|
{block.issue ? (
|
||||||
|
<>
|
||||||
|
<CustomMenu.MenuItem onClick={handleBlockSync}>
|
||||||
|
<>Turn sync {block.sync ? "off" : "on"}</>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
|
Copy issue link
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<CustomMenu.MenuItem onClick={pushBlockIntoIssues}>
|
||||||
|
Push into issues
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
<CustomMenu.MenuItem onClick={deletePageBlock}>Delete block</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex items-start gap-2 ${
|
||||||
|
snapshot.isDragging ? "" : "py-4 [&:not(:last-child)]:border-b"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{block.issue && (
|
||||||
|
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${block.issue}`}>
|
||||||
|
<a className="flex flex-shrink-0 items-center gap-1 rounded bg-gray-100 px-1.5 py-1 text-xs">
|
||||||
|
<LayerDiagonalIcon height="16" width="16" color="black" />
|
||||||
|
{projectDetails?.identifier}-{block.issue_detail?.sequence_id}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<h3
|
||||||
|
className="font-medium text-sm break-all"
|
||||||
|
onClick={() => setCreateBlockForm(true)}
|
||||||
|
>
|
||||||
|
{block.name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<GptAssistantModal
|
||||||
|
block={block}
|
||||||
|
isOpen={gptAssistantModal}
|
||||||
|
handleClose={() => setGptAssistantModal(false)}
|
||||||
|
inset="top-8 left-0"
|
||||||
|
content={block.description_stripped}
|
||||||
|
htmlContent={block.description_html}
|
||||||
|
onResponse={handleAiAssistance}
|
||||||
|
projectId={projectId as string}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{block.issue && (
|
</>
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${block.issue}`}>
|
)}
|
||||||
<a className="flex flex-shrink-0 items-center gap-1 rounded bg-gray-100 px-1.5 py-1 text-xs">
|
</Draggable>
|
||||||
<LayerDiagonalIcon height="16" width="16" color="black" />
|
|
||||||
{projectDetails?.identifier}-{block.issue_detail?.sequence_id}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
|
|
||||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
<SparklesIcon className="h-4 w-4" />
|
|
||||||
AI
|
|
||||||
</button>
|
|
||||||
<CustomMenu label={<BoltIcon className="h-4.5 w-3.5" />} noBorder noChevron>
|
|
||||||
{block.issue ? (
|
|
||||||
<>
|
|
||||||
<CustomMenu.MenuItem onClick={handleBlockSync}>
|
|
||||||
<>Turn sync {block.sync ? "off" : "on"}</>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CustomMenu.MenuItem onClick={pushBlockIntoIssues}>
|
|
||||||
Push into issues
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{/* <CustomMenu.MenuItem onClick={editAndPushBlockIntoIssues}>
|
|
||||||
Edit and push into issues
|
|
||||||
</CustomMenu.MenuItem> */}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<CustomMenu.MenuItem onClick={deletePageBlock}>Delete block</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="page-block-section font relative -mx-3 -mt-3">
|
|
||||||
<Controller
|
|
||||||
name="description"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<RemirrorRichTextEditor
|
|
||||||
value={
|
|
||||||
!value || (typeof value === "object" && Object.keys(value).length === 0)
|
|
||||||
? watch("description_html")
|
|
||||||
: value
|
|
||||||
}
|
|
||||||
onBlur={handleSubmit(updatePageBlock)}
|
|
||||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
|
||||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
|
||||||
placeholder="Block description..."
|
|
||||||
customClassName="border border-transparent"
|
|
||||||
noBorder
|
|
||||||
borderOnFocus
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<GptAssistantModal
|
|
||||||
block={block}
|
|
||||||
isOpen={gptAssistantModal}
|
|
||||||
handleClose={() => setGptAssistantModal(false)}
|
|
||||||
inset="top-2 left-0"
|
|
||||||
content={block.description_stripped}
|
|
||||||
htmlContent={block.description_html}
|
|
||||||
onResponse={handleAiAssistance}
|
|
||||||
projectId={projectId as string}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,9 +5,15 @@ import { useRouter } from "next/router";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Loader } from "components/ui";
|
import { CustomMenu, Loader, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
LockClosedIcon,
|
||||||
|
LockOpenIcon,
|
||||||
|
PencilIcon,
|
||||||
|
StarIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
import { renderShortTime } from "helpers/date-time.helper";
|
import { renderShortTime } from "helpers/date-time.helper";
|
||||||
@ -20,6 +26,7 @@ type TSingleStatProps = {
|
|||||||
handleDeletePage: () => void;
|
handleDeletePage: () => void;
|
||||||
handleAddToFavorites: () => void;
|
handleAddToFavorites: () => void;
|
||||||
handleRemoveFromFavorites: () => void;
|
handleRemoveFromFavorites: () => void;
|
||||||
|
partialUpdatePage: (page: IPage, formData: Partial<IPage>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||||
@ -37,90 +44,126 @@ export const SinglePageDetailedItem: React.FC<TSingleStatProps> = ({
|
|||||||
handleDeletePage,
|
handleDeletePage,
|
||||||
handleAddToFavorites,
|
handleAddToFavorites,
|
||||||
handleRemoveFromFavorites,
|
handleRemoveFromFavorites,
|
||||||
|
partialUpdatePage,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative rounded border p-4">
|
<div className="relative first:rounded-t last:rounded-b border">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}>
|
||||||
<div className="flex items-center gap-2">
|
<a className="block p-4">
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}>
|
<div className="flex items-center justify-between gap-2">
|
||||||
<a className="after:absolute after:inset-0">
|
<div className="flex items-center gap-2">
|
||||||
<p className="mr-2 truncate text-sm font-medium">{truncateText(page.name, 75)}</p>
|
<p className="mr-2 truncate text-sm font-medium">{truncateText(page.name, 75)}</p>
|
||||||
</a>
|
{page.label_details.length > 0 &&
|
||||||
</Link>
|
page.label_details.map((label) => (
|
||||||
{page.label_details.length > 0 &&
|
<div
|
||||||
page.label_details.map((label) => (
|
key={label.id}
|
||||||
<div
|
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||||
key={label.id}
|
style={{
|
||||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
backgroundColor: `${
|
||||||
style={{
|
label?.color && label.color !== "" ? label.color : "#000000"
|
||||||
backgroundColor: `${
|
}20`,
|
||||||
label?.color && label.color !== "" ? label.color : "#000000"
|
}}
|
||||||
}20`,
|
>
|
||||||
}}
|
<span
|
||||||
>
|
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||||
<span
|
style={{
|
||||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
backgroundColor:
|
||||||
style={{
|
label?.color && label.color !== "" ? label.color : "#000000",
|
||||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000000",
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
{label.name}
|
||||||
{label.name}
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-sm text-gray-400">{renderShortTime(page.updated_at)}</p>
|
<p className="text-sm text-gray-400">{renderShortTime(page.updated_at)}</p>
|
||||||
{page.is_favorite ? (
|
{page.is_favorite ? (
|
||||||
<button onClick={handleRemoveFromFavorites} className="z-10 grid place-items-center">
|
<button
|
||||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
type="button"
|
||||||
</button>
|
onClick={(e) => {
|
||||||
) : (
|
e.preventDefault();
|
||||||
<button
|
e.stopPropagation();
|
||||||
onClick={handleAddToFavorites}
|
handleRemoveFromFavorites();
|
||||||
type="button"
|
}}
|
||||||
className="z-10 grid place-items-center"
|
className="z-10 grid place-items-center"
|
||||||
>
|
>
|
||||||
<StarIcon className="h-4 w-4 " color="#858E96" />
|
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
) : (
|
||||||
<CustomMenu verticalEllipsis>
|
<button
|
||||||
<CustomMenu.MenuItem onClick={handleEditPage}>
|
type="button"
|
||||||
<span className="flex items-center justify-start gap-2">
|
onClick={(e) => {
|
||||||
<PencilIcon className="h-3.5 w-3.5" />
|
e.preventDefault();
|
||||||
<span>Edit Page</span>
|
e.stopPropagation();
|
||||||
</span>
|
handleAddToFavorites();
|
||||||
</CustomMenu.MenuItem>
|
}}
|
||||||
<CustomMenu.MenuItem onClick={handleDeletePage}>
|
className="z-10 grid place-items-center"
|
||||||
<span className="flex items-center justify-start gap-2">
|
>
|
||||||
<TrashIcon className="h-3.5 w-3.5" />
|
<StarIcon className="h-4 w-4 " color="#858E96" />
|
||||||
<span>Delete Page</span>
|
</button>
|
||||||
</span>
|
)}
|
||||||
</CustomMenu.MenuItem>
|
<Tooltip
|
||||||
</CustomMenu>
|
tooltipContent={`${
|
||||||
</div>
|
page.access
|
||||||
</div>
|
? "This page is only visible to you."
|
||||||
<div className="relative mt-6 space-y-2 text-sm text-gray-600">
|
: "This page can be viewed by anyone in the project."
|
||||||
<div className="page-block-section -m-4 -mt-6">
|
}`}
|
||||||
{page.blocks.length > 0 ? (
|
theme="dark"
|
||||||
<RemirrorRichTextEditor
|
>
|
||||||
value={
|
<button
|
||||||
!page.blocks[0].description ||
|
type="button"
|
||||||
(typeof page.blocks[0].description === "object" &&
|
onClick={(e) => {
|
||||||
Object.keys(page.blocks[0].description).length === 0)
|
e.preventDefault();
|
||||||
? page.blocks[0].description_html
|
e.stopPropagation();
|
||||||
: page.blocks[0].description
|
partialUpdatePage(page, { access: page.access ? 0 : 1 });
|
||||||
}
|
}}
|
||||||
editable={false}
|
>
|
||||||
customClassName="text-gray-500"
|
{page.access ? (
|
||||||
noBorder
|
<LockClosedIcon className="h-4 w-4" color="#858e96" />
|
||||||
/>
|
) : (
|
||||||
) : null}
|
<LockOpenIcon className="h-4 w-4" color="#858e96" />
|
||||||
</div>
|
)}
|
||||||
</div>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<CustomMenu verticalEllipsis>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={(e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEditPage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<PencilIcon className="h-3.5 w-3.5" />
|
||||||
|
<span>Edit Page</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={(e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeletePage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<TrashIcon className="h-3.5 w-3.5" />
|
||||||
|
<span>Delete Page</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative mt-2 space-y-2 text-sm text-gray-600">
|
||||||
|
{page.blocks.length > 0
|
||||||
|
? page.blocks.slice(0, 3).map((block) => <h4>{block.name}</h4>)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,14 @@ import { useRouter } from "next/router";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Tooltip } from "components/ui";
|
import { CustomMenu, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { DocumentTextIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
DocumentTextIcon,
|
||||||
|
LockClosedIcon,
|
||||||
|
LockOpenIcon,
|
||||||
|
PencilIcon,
|
||||||
|
StarIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
|
import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
|
||||||
@ -19,6 +26,7 @@ type TSingleStatProps = {
|
|||||||
handleDeletePage: () => void;
|
handleDeletePage: () => void;
|
||||||
handleAddToFavorites: () => void;
|
handleAddToFavorites: () => void;
|
||||||
handleRemoveFromFavorites: () => void;
|
handleRemoveFromFavorites: () => void;
|
||||||
|
partialUpdatePage: (page: IPage, formData: Partial<IPage>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
||||||
@ -27,6 +35,7 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
|||||||
handleDeletePage,
|
handleDeletePage,
|
||||||
handleAddToFavorites,
|
handleAddToFavorites,
|
||||||
handleRemoveFromFavorites,
|
handleRemoveFromFavorites,
|
||||||
|
partialUpdatePage,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
@ -94,6 +103,29 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
|||||||
<StarIcon className="h-4 w-4 " color="#858e96" />
|
<StarIcon className="h-4 w-4 " color="#858e96" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip
|
||||||
|
tooltipContent={`${
|
||||||
|
page.access
|
||||||
|
? "This page is only visible to you."
|
||||||
|
: "This page can be viewed by anyone in the project."
|
||||||
|
}`}
|
||||||
|
theme="dark"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
partialUpdatePage(page, { access: page.access ? 0 : 1 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{page.access ? (
|
||||||
|
<LockClosedIcon className="h-4 w-4" color="#858e96" />
|
||||||
|
) : (
|
||||||
|
<LockOpenIcon className="h-4 w-4" color="#858e96" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
<CustomMenu width="auto" verticalEllipsis>
|
<CustomMenu width="auto" verticalEllipsis>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e: any) => {
|
onClick={(e: any) => {
|
||||||
|
@ -185,25 +185,23 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 mb-4">
|
<div className="relative">
|
||||||
<Remirror
|
<Remirror
|
||||||
manager={manager}
|
manager={manager}
|
||||||
initialContent={state}
|
initialContent={state}
|
||||||
classNames={[
|
classNames={[
|
||||||
`p-4 relative focus:outline-none rounded-md focus:border-gray-200 ${
|
`p-4 relative focus:outline-none rounded-md focus:border-gray-200 ${
|
||||||
noBorder ? "" : "border"
|
noBorder ? "" : "border"
|
||||||
} ${borderOnFocus ? "focus:border" : ""} ${customClassName}`,
|
} ${borderOnFocus ? "focus:border" : "focus:border-0"} ${customClassName}`,
|
||||||
]}
|
]}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
onBlur(jsonValue, htmlValue);
|
onBlur(jsonValue, htmlValue);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* {(!value || value === "" || value?.content?.[0]?.content === undefined) && (
|
{(!value || value === "" || value?.content?.[0]?.content === undefined) && placeholder && (
|
||||||
<p className="pointer-events-none absolute top-[8.8rem] left-12 text-gray-300">
|
<p className="absolute pointer-events-none top-4 left-4 text-gray-300">{placeholder}</p>
|
||||||
{placeholder || "Enter text..."}
|
)}
|
||||||
</p>
|
|
||||||
)} */}
|
|
||||||
<EditorComponent />
|
<EditorComponent />
|
||||||
|
|
||||||
{imageLoader && (
|
{imageLoader && (
|
||||||
|
@ -27,6 +27,10 @@ export const EmptyState: React.FC<Props> = ({ type, title, description, imgURL,
|
|||||||
return "P";
|
return "P";
|
||||||
case "issue":
|
case "issue":
|
||||||
return "C";
|
return "C";
|
||||||
|
case "view":
|
||||||
|
return "V";
|
||||||
|
case "page":
|
||||||
|
return "D"
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -81,8 +81,8 @@ export const SingleViewItem: React.FC<Props> = ({ view, setSelectedView }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
|
<a>
|
||||||
<div className="flex items-center cursor-pointer justify-between border-b bg-white p-4 first:rounded-t-[10px] last:rounded-b-[10px]">
|
<div className="flex items-center cursor-pointer justify-between border-b bg-white p-4 first:rounded-t-[10px] last:rounded-b-[10px]">
|
||||||
<div className="flex flex-col w-full gap-3">
|
<div className="flex flex-col w-full gap-3">
|
||||||
<div className="flex justify-between w-full">
|
<div className="flex justify-between w-full">
|
||||||
@ -137,7 +137,7 @@ export const SingleViewItem: React.FC<Props> = ({ view, setSelectedView }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</a>
|
||||||
</>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,15 @@ type Props = {
|
|||||||
setDefaultValues: Dispatch<SetStateAction<any>>;
|
setDefaultValues: Dispatch<SetStateAction<any>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restrictedUrls = [
|
||||||
|
"create-workspace",
|
||||||
|
"error",
|
||||||
|
"invitations",
|
||||||
|
"magic-sign-in",
|
||||||
|
"onboarding",
|
||||||
|
"signin",
|
||||||
|
];
|
||||||
|
|
||||||
export const CreateWorkspaceForm: React.FC<Props> = ({
|
export const CreateWorkspaceForm: React.FC<Props> = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@ -49,7 +58,7 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
|
|||||||
await workspaceService
|
await workspaceService
|
||||||
.workspaceSlugCheck(formData.slug)
|
.workspaceSlugCheck(formData.slug)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (res.status === true) {
|
if (res.status === true && !restrictedUrls.includes(formData.slug)) {
|
||||||
setSlugError(false);
|
setSlugError(false);
|
||||||
await workspaceService
|
await workspaceService
|
||||||
.createWorkspace(formData)
|
.createWorkspace(formData)
|
||||||
|
@ -80,11 +80,8 @@ export const handleIssuesMutation: THandleIssuesMutation = (
|
|||||||
|
|
||||||
let newGroup: IIssue[] = [];
|
let newGroup: IIssue[] = [];
|
||||||
|
|
||||||
if (selectedGroupBy === "priority") {
|
if (selectedGroupBy === "priority") newGroup = prevData[formData.priority ?? ""] ?? [];
|
||||||
newGroup = prevData[formData.priority ?? ""] ?? [];
|
else if (selectedGroupBy === "state") newGroup = prevData[formData.state ?? ""] ?? [];
|
||||||
} else if (selectedGroupBy === "state") {
|
|
||||||
newGroup = prevData[formData.state ?? ""] ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedIssue = {
|
const updatedIssue = {
|
||||||
...oldGroup[issueIndex],
|
...oldGroup[issueIndex],
|
||||||
|
@ -125,7 +125,15 @@ const useIssuesView = () => {
|
|||||||
return issuesToGroup ? Object.assign(emptyStatesObject, issuesToGroup) : undefined;
|
return issuesToGroup ? Object.assign(emptyStatesObject, issuesToGroup) : undefined;
|
||||||
|
|
||||||
return issuesToGroup;
|
return issuesToGroup;
|
||||||
}, [projectIssues, cycleIssues, moduleIssues, groupByProperty, cycleId, moduleId]);
|
}, [
|
||||||
|
projectIssues,
|
||||||
|
cycleIssues,
|
||||||
|
moduleIssues,
|
||||||
|
groupByProperty,
|
||||||
|
cycleId,
|
||||||
|
moduleId,
|
||||||
|
emptyStatesObject,
|
||||||
|
]);
|
||||||
|
|
||||||
const isEmpty =
|
const isEmpty =
|
||||||
Object.values(groupedByIssues ?? {}).every((group) => group.length === 0) ||
|
Object.values(groupedByIssues ?? {}).every((group) => group.length === 0) ||
|
||||||
|
@ -29,10 +29,10 @@ const SettingsNavbar: React.FC<Props> = ({ profilePage = false }) => {
|
|||||||
label: "Integrations",
|
label: "Integrations",
|
||||||
href: `/${workspaceSlug}/settings/integrations`,
|
href: `/${workspaceSlug}/settings/integrations`,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
label: "Import/Export",
|
// label: "Import/Export",
|
||||||
href: `/${workspaceSlug}/settings/import-export`,
|
// href: `/${workspaceSlug}/settings/import-export`,
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
const projectLinks: Array<{
|
const projectLinks: Array<{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
@ -10,6 +10,9 @@ import { useForm } from "react-hook-form";
|
|||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// react-color
|
// react-color
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
|
// react-beautiful-dnd
|
||||||
|
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||||
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
// lib
|
// lib
|
||||||
import { requiredAdmin, requiredAuth } from "lib/auth";
|
import { requiredAdmin, requiredAuth } from "lib/auth";
|
||||||
// services
|
// services
|
||||||
@ -21,16 +24,24 @@ import useToast from "hooks/use-toast";
|
|||||||
// layouts
|
// layouts
|
||||||
import AppLayout from "layouts/app-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { SinglePageBlock } from "components/pages";
|
import { CreateUpdateBlockInline, SinglePageBlock } from "components/pages";
|
||||||
// ui
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
import { CustomSearchSelect, Loader, PrimaryButton, TextArea, Tooltip } from "components/ui";
|
import { CustomSearchSelect, Loader, PrimaryButton, TextArea, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowLeftIcon, PlusIcon, ShareIcon, StarIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
LockClosedIcon,
|
||||||
|
LockOpenIcon,
|
||||||
|
PlusIcon,
|
||||||
|
ShareIcon,
|
||||||
|
StarIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
import { ColorPalletteIcon } from "components/icons";
|
import { ColorPalletteIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortTime } from "helpers/date-time.helper";
|
import { renderShortTime } from "helpers/date-time.helper";
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
|
import { orderArrayBy } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import type { NextPage, GetServerSidePropsContext } from "next";
|
import type { NextPage, GetServerSidePropsContext } from "next";
|
||||||
import { IIssueLabels, IPage, IPageBlock, UserAuth } from "types";
|
import { IIssueLabels, IPage, IPageBlock, UserAuth } from "types";
|
||||||
@ -43,14 +54,16 @@ import {
|
|||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
const SinglePage: NextPage<UserAuth> = (props) => {
|
const SinglePage: NextPage<UserAuth> = (props) => {
|
||||||
const [isAddingBlock, setIsAddingBlock] = useState(false);
|
const [createBlockForm, setCreateBlockForm] = useState(false);
|
||||||
|
|
||||||
|
const scrollToRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, pageId } = router.query;
|
const { workspaceSlug, projectId, pageId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { handleSubmit, reset, watch, setValue, control } = useForm<IPage>({
|
const { handleSubmit, reset, watch, setValue } = useForm<IPage>({
|
||||||
defaultValues: { name: "" },
|
defaultValues: { name: "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,11 +78,11 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null,
|
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () =>
|
? () =>
|
||||||
pagesService.getPageDetails(
|
pagesService.getPageDetails(
|
||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
pageId as string
|
pageId as string
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -77,11 +90,11 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
workspaceSlug && projectId && pageId ? PAGE_BLOCKS_LIST(pageId as string) : null,
|
workspaceSlug && projectId && pageId ? PAGE_BLOCKS_LIST(pageId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () =>
|
? () =>
|
||||||
pagesService.listPageBlocks(
|
pagesService.listPageBlocks(
|
||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
pageId as string
|
pageId as string
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -131,34 +144,6 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPageBlock = async () => {
|
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
|
||||||
|
|
||||||
setIsAddingBlock(true);
|
|
||||||
|
|
||||||
await pagesService
|
|
||||||
.createPageBlock(workspaceSlug as string, projectId as string, pageId as string, {
|
|
||||||
name: "New block",
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
mutate<IPageBlock[]>(
|
|
||||||
PAGE_BLOCKS_LIST(pageId as string),
|
|
||||||
(prevData) => [...(prevData as IPageBlock[]), res],
|
|
||||||
false
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Page could not be created. Please try again.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsAddingBlock(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddToFavorites = () => {
|
const handleAddToFavorites = () => {
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
|
|
||||||
@ -195,6 +180,50 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination || !workspaceSlug || !projectId || !pageId || !pageBlocks) return;
|
||||||
|
|
||||||
|
const { source, destination } = result;
|
||||||
|
|
||||||
|
let newSortOrder = pageBlocks.find((p) => p.id === result.draggableId)?.sort_order ?? 65535;
|
||||||
|
|
||||||
|
if (destination.index === 0) newSortOrder = pageBlocks[0].sort_order - 10000;
|
||||||
|
else if (destination.index === pageBlocks.length - 1)
|
||||||
|
newSortOrder = pageBlocks[pageBlocks.length - 1].sort_order + 10000;
|
||||||
|
else {
|
||||||
|
if (destination.index > source.index)
|
||||||
|
newSortOrder =
|
||||||
|
(pageBlocks[destination.index].sort_order +
|
||||||
|
pageBlocks[destination.index + 1].sort_order) /
|
||||||
|
2;
|
||||||
|
else if (destination.index < source.index)
|
||||||
|
newSortOrder =
|
||||||
|
(pageBlocks[destination.index - 1].sort_order +
|
||||||
|
pageBlocks[destination.index].sort_order) /
|
||||||
|
2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBlocksList = pageBlocks.map((p) => ({
|
||||||
|
...p,
|
||||||
|
sort_order: p.id === result.draggableId ? newSortOrder : p.sort_order,
|
||||||
|
}));
|
||||||
|
mutate<IPageBlock[]>(
|
||||||
|
PAGE_BLOCKS_LIST(pageId as string),
|
||||||
|
orderArrayBy(newBlocksList, "sort_order", "ascending"),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
pagesService.patchPageBlock(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
pageId as string,
|
||||||
|
result.draggableId,
|
||||||
|
{
|
||||||
|
sort_order: newSortOrder,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
const originURL =
|
const originURL =
|
||||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
@ -210,6 +239,13 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNewBlock = () => {
|
||||||
|
setCreateBlockForm(true);
|
||||||
|
scrollToRef.current?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const options =
|
const options =
|
||||||
labels?.map((label) => ({
|
labels?.map((label) => ({
|
||||||
value: label.id,
|
value: label.id,
|
||||||
@ -272,8 +308,9 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
key={label.id}
|
key={label.id}
|
||||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${label?.color && label.color !== "" ? label.color : "#000000"
|
backgroundColor: `${
|
||||||
}20`,
|
label?.color && label.color !== "" ? label.color : "#000000"
|
||||||
|
}20`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -341,8 +378,9 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
type="button"
|
type="button"
|
||||||
className={`group inline-flex items-center outline-none ${open ? "text-gray-900" : "text-gray-500"
|
className={`group inline-flex items-center outline-none ${
|
||||||
}`}
|
open ? "text-gray-900" : "text-gray-500"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{watch("color") && watch("color") !== "" ? (
|
{watch("color") && watch("color") !== "" ? (
|
||||||
<span
|
<span
|
||||||
@ -352,7 +390,7 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ColorPalletteIcon height={16} width={16} />
|
<ColorPalletteIcon height={16} width={16} color="#000000" />
|
||||||
)}
|
)}
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
@ -376,6 +414,19 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
{pageDetails.access ? (
|
||||||
|
<button onClick={() => partialUpdatePage({ access: 0 })} className="z-10">
|
||||||
|
<LockClosedIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => partialUpdatePage({ access: 1 })}
|
||||||
|
type="button"
|
||||||
|
className="z-10"
|
||||||
|
>
|
||||||
|
<LockOpenIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{pageDetails.is_favorite ? (
|
{pageDetails.is_favorite ? (
|
||||||
<button onClick={handleRemoveFromFavorites} className="z-10">
|
<button onClick={handleRemoveFromFavorites} className="z-10">
|
||||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||||
@ -403,34 +454,44 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
{pageBlocks ? (
|
{pageBlocks ? (
|
||||||
<>
|
<>
|
||||||
{pageBlocks.length !== 0 && (
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
<div className="space-y-4 divide-y">
|
{pageBlocks.length !== 0 && (
|
||||||
{pageBlocks.map((block, index) => (
|
<StrictModeDroppable droppableId="blocks-list">
|
||||||
<>
|
{(provided) => (
|
||||||
<SinglePageBlock
|
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
key={block.id}
|
{pageBlocks.map((block, index) => (
|
||||||
block={block}
|
<SinglePageBlock
|
||||||
projectDetails={projectDetails}
|
key={block.id}
|
||||||
/>
|
block={block}
|
||||||
</>
|
projectDetails={projectDetails}
|
||||||
))}
|
index={index}
|
||||||
|
handleNewBlock={handleNewBlock}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StrictModeDroppable>
|
||||||
|
)}
|
||||||
|
</DragDropContext>
|
||||||
|
{!createBlockForm && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 rounded bg-gray-100 px-2.5 py-1 ml-6 text-xs hover:bg-gray-200 mt-4"
|
||||||
|
onClick={handleNewBlock}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-3 w-3" />
|
||||||
|
Add new block
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{createBlockForm && (
|
||||||
|
<div className="mt-4" ref={scrollToRef}>
|
||||||
|
<CreateUpdateBlockInline
|
||||||
|
handleClose={() => setCreateBlockForm(false)}
|
||||||
|
focus="name"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-1 rounded bg-gray-100 px-2.5 py-1 text-xs hover:bg-gray-200"
|
|
||||||
onClick={createPageBlock}
|
|
||||||
disabled={isAddingBlock}
|
|
||||||
>
|
|
||||||
{isAddingBlock ? (
|
|
||||||
"Adding block..."
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PlusIcon className="h-3 w-3" />
|
|
||||||
Add new block
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Loader>
|
<Loader>
|
||||||
|
@ -167,7 +167,10 @@ const ProjectPages: NextPage<UserAuth> = (props) => {
|
|||||||
right={
|
right={
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => setCreateUpdatePageModal(true)}
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "d" });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="w-4 h-4" />
|
<PlusIcon className="w-4 h-4" />
|
||||||
Create Page
|
Create Page
|
||||||
|
@ -46,7 +46,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const scollToRef = useRef<HTMLDivElement>(null);
|
const scrollToRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { data: projectDetails } = useSWR(
|
const { data: projectDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
@ -130,7 +130,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
|||||||
setLabelForm={setLabelForm}
|
setLabelForm={setLabelForm}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
labelToUpdate={labelToUpdate}
|
labelToUpdate={labelToUpdate}
|
||||||
ref={scollToRef}
|
ref={scrollToRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<>
|
<>
|
||||||
@ -147,7 +147,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
|||||||
addLabelToGroup={() => addLabelToGroup(label)}
|
addLabelToGroup={() => addLabelToGroup(label)}
|
||||||
editLabel={(label) => {
|
editLabel={(label) => {
|
||||||
editLabel(label);
|
editLabel(label);
|
||||||
scollToRef.current?.scrollIntoView({
|
scrollToRef.current?.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@ -163,7 +163,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
|||||||
addLabelToGroup={addLabelToGroup}
|
addLabelToGroup={addLabelToGroup}
|
||||||
editLabel={(label) => {
|
editLabel={(label) => {
|
||||||
editLabel(label);
|
editLabel(label);
|
||||||
scollToRef.current?.scrollIntoView({
|
scrollToRef.current?.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -67,7 +67,14 @@ const ProjectViews: NextPage<UserAuth> = (props) => {
|
|||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<PrimaryButton type="button" className="flex items-center gap-2" onClick={() => setIsCreateViewModalOpen(true)}>
|
<PrimaryButton
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "v" });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PlusIcon className="w-4 h-4" />
|
<PlusIcon className="w-4 h-4" />
|
||||||
Create View
|
Create View
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
@ -100,7 +107,6 @@ const ProjectViews: NextPage<UserAuth> = (props) => {
|
|||||||
title="Create New View"
|
title="Create New View"
|
||||||
description="Views aid in saving your issues by applying various filters and grouping options."
|
description="Views aid in saving your issues by applying various filters and grouping options."
|
||||||
imgURL={emptyView}
|
imgURL={emptyView}
|
||||||
action={() => setIsCreateViewModalOpen(true)}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
@ -37,15 +37,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
// TODO: cache user info
|
// TODO: cache user info
|
||||||
|
|
||||||
jitsu
|
jitsu
|
||||||
.id(
|
.id({
|
||||||
{
|
id: user.id,
|
||||||
id: user.id,
|
email: user.email,
|
||||||
email: user.email,
|
first_name: user.first_name,
|
||||||
first_name: user.first_name,
|
last_name: user.last_name,
|
||||||
last_name: user.last_name,
|
})
|
||||||
},
|
|
||||||
true
|
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
jitsu.track(eventName, {
|
jitsu.track(eventName, {
|
||||||
...extra,
|
...extra,
|
||||||
|
@ -24,6 +24,7 @@ import type { NextPage, GetServerSidePropsContext } from "next";
|
|||||||
|
|
||||||
const Onboarding: NextPage = () => {
|
const Onboarding: NextPage = () => {
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
|
const [userRole, setUserRole] = useState<string | null>(null);
|
||||||
|
|
||||||
const [workspace, setWorkspace] = useState();
|
const [workspace, setWorkspace] = useState();
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ const Onboarding: NextPage = () => {
|
|||||||
<Image src={Logo} height="50" alt="Plane Logo" />
|
<Image src={Logo} height="50" alt="Plane Logo" />
|
||||||
</div>
|
</div>
|
||||||
{step === 1 ? (
|
{step === 1 ? (
|
||||||
<UserDetails user={user} setStep={setStep} />
|
<UserDetails user={user} setStep={setStep} setUserRole={setUserRole} />
|
||||||
) : step === 2 ? (
|
) : step === 2 ? (
|
||||||
<Workspace setStep={setStep} setWorkspace={setWorkspace} />
|
<Workspace setStep={setStep} setWorkspace={setWorkspace} />
|
||||||
) : (
|
) : (
|
||||||
@ -69,7 +70,7 @@ const Onboarding: NextPage = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (step === 8) {
|
if (step === 8) {
|
||||||
userService
|
userService
|
||||||
.updateUserOnBoard()
|
.updateUserOnBoard({ userRole })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
})
|
})
|
||||||
|
@ -343,7 +343,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
)
|
)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
)
|
)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import type {
|
|||||||
IPageBlock,
|
IPageBlock,
|
||||||
IProject,
|
IProject,
|
||||||
IState,
|
IState,
|
||||||
|
IView,
|
||||||
IWorkspace,
|
IWorkspace,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
@ -37,6 +38,8 @@ type ModuleEventType = "MODULE_CREATE" | "MODULE_UPDATE" | "MODULE_DELETE";
|
|||||||
|
|
||||||
type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE";
|
type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE";
|
||||||
|
|
||||||
|
type ViewEventType = "VIEW_CREATE" | "VIEW_UPDATE" | "VIEW_DELETE";
|
||||||
|
|
||||||
type PageBlocksEventType =
|
type PageBlocksEventType =
|
||||||
| "PAGE_BLOCK_CREATE"
|
| "PAGE_BLOCK_CREATE"
|
||||||
| "PAGE_BLOCK_UPDATE"
|
| "PAGE_BLOCK_UPDATE"
|
||||||
@ -365,6 +368,30 @@ class TrackEventServices extends APIService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async trackViewEvent(data: IView, eventName: ViewEventType): Promise<any> {
|
||||||
|
let payload: any;
|
||||||
|
if (eventName === "VIEW_DELETE") payload = data;
|
||||||
|
else
|
||||||
|
payload = {
|
||||||
|
labels: Boolean(data.query_data.labels),
|
||||||
|
assignees: Boolean(data.query_data.assignees),
|
||||||
|
priority: Boolean(data.query_data.priority),
|
||||||
|
state: Boolean(data.query_data.state),
|
||||||
|
created_by: Boolean(data.query_data.created_by),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.request({
|
||||||
|
url: "/api/track-event",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
eventName,
|
||||||
|
extra: {
|
||||||
|
...payload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackEventServices = new TrackEventServices();
|
const trackEventServices = new TrackEventServices();
|
||||||
|
@ -47,10 +47,16 @@ class UserService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserOnBoard(): Promise<any> {
|
async updateUserOnBoard({ userRole }: any): Promise<any> {
|
||||||
return this.patch("/api/users/me/onboard/", { is_onboarded: true })
|
return this.patch("/api/users/me/onboard/", {
|
||||||
|
is_onboarded: true,
|
||||||
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (trackEvent) trackEventServices.trackUserOnboardingCompleteEvent(response.data);
|
if (trackEvent)
|
||||||
|
trackEventServices.trackUserOnboardingCompleteEvent({
|
||||||
|
...response.data,
|
||||||
|
user_role: userRole ?? "None",
|
||||||
|
});
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import trackEventServices from "services/track-event.service";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import { IView } from "types/views";
|
import { IView } from "types/views";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
|
const trackEvent =
|
||||||
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class ViewServices extends APIService {
|
class ViewServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||||
@ -12,7 +17,10 @@ class ViewServices extends APIService {
|
|||||||
|
|
||||||
async createView(workspaceSlug: string, projectId: string, data: IView): Promise<any> {
|
async createView(workspaceSlug: string, projectId: string, data: IView): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => {
|
||||||
|
if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE");
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
@ -25,7 +33,10 @@ class ViewServices extends APIService {
|
|||||||
data: IView
|
data: IView
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
|
return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => {
|
||||||
|
if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE");
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
@ -41,7 +52,10 @@ class ViewServices extends APIService {
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`,
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => response?.data)
|
.then((response) => {
|
||||||
|
if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE");
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
@ -49,7 +63,10 @@ class ViewServices extends APIService {
|
|||||||
|
|
||||||
async deleteView(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
async deleteView(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => {
|
||||||
|
if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE");
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
@ -111,7 +128,6 @@ class ViewServices extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ViewServices();
|
export default new ViewServices();
|
||||||
|
@ -31,5 +31,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[cmdk-item]:hover {
|
[cmdk-item]:hover {
|
||||||
background-color: rgb(243 244 246);
|
background-color: rgb(229 231 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-item][aria-selected="true"] {
|
||||||
|
background-color: rgb(229 231 235);
|
||||||
}
|
}
|
||||||
|
@ -363,6 +363,10 @@ img.ProseMirror-separator {
|
|||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remirror-section .remirror-editor-wrapper .remirror-editor {
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.remirror-editor-wrapper {
|
.remirror-editor-wrapper {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
@ -128,4 +128,9 @@
|
|||||||
.react-datepicker-popper {
|
.react-datepicker-popper {
|
||||||
z-index: 30 !important;
|
z-index: 30 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conical-gradient{
|
||||||
|
background: conic-gradient(from 180deg at 50% 50%, #FF6B00 0deg, #F7AE59 70.5deg, #3F76FF 151.12deg, #05C3FF 213deg, #18914F 289.87deg, #F6F172 329.25deg, #FF6B00 360deg);
|
||||||
|
}
|
||||||
|
|
||||||
/* end react datepicker styling */
|
/* end react datepicker styling */
|
||||||
|
Loading…
Reference in New Issue
Block a user