forked from github/plane
Merge branch 'develop' of https://github.com/makeplane/plane into stage-release
This commit is contained in:
commit
9f4f1cac42
115
README.md
115
README.md
@ -2,35 +2,128 @@
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<h3 align="center"><b>Plane</b></h3>
|
||||
<p align="center"><b>Open-source, self-hosted project planning tool</b></p>
|
||||
|
||||
<p align="center">
|
||||
<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" />
|
||||
</a>
|
||||
<img alt="Discord" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
|
||||
</p>
|
||||
|
||||
<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.
|
||||
<br /> <br />
|
||||
<p>
|
||||
<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.
|
||||
|
||||
## 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/)
|
||||
|
||||
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
|
||||
- [ ] 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: Production-ready
|
||||
|
||||
## Community
|
||||
## ❤️ Community
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
@ -19,3 +19,6 @@ GITHUB_CLIENT_SECRET=""
|
||||
# Flags
|
||||
DISABLE_COLLECTSTATIC=1
|
||||
DOCKERIZED=1
|
||||
# GPT Envs
|
||||
OPENAI_API_KEY=0
|
||||
GPT_ENGINE=0
|
@ -3,7 +3,8 @@ import uuid
|
||||
import random
|
||||
from django.contrib.auth.hashers import make_password
|
||||
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
|
||||
@ -134,3 +135,42 @@ def update_project_cover_images():
|
||||
except Exception as e:
|
||||
print(e)
|
||||
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",
|
||||
]
|
||||
|
||||
# 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):
|
||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
||||
|
@ -17,6 +17,7 @@ from plane.db.models import (
|
||||
WorkspaceMemberInvite,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
WorkspaceMember,
|
||||
)
|
||||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
@ -72,6 +73,20 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||
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(
|
||||
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
|
@ -21,10 +21,13 @@ ROLE_CHOICES = (
|
||||
|
||||
def get_default_props():
|
||||
return {
|
||||
"filters": {"type": None},
|
||||
"orderBy": "-created_at",
|
||||
"collapsed": True,
|
||||
"issueView": "list",
|
||||
"groupByProperty": None,
|
||||
"orderBy": None,
|
||||
"filterIssue": None,
|
||||
"groupByProperty": True,
|
||||
"showEmptyGroups": True,
|
||||
}
|
||||
|
||||
|
||||
|
@ -121,7 +121,7 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
) || "Email ID is not valid",
|
||||
}}
|
||||
error={errors.email}
|
||||
placeholder="Enter you email Id"
|
||||
placeholder="Enter your Email ID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -98,8 +98,7 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
||||
<Command.Item
|
||||
key={option.value}
|
||||
onSelect={() => handleIssueAssignees(option.value)}
|
||||
className="focus:bg-slate-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
{option.content}
|
||||
</Command.Item>
|
||||
|
@ -60,8 +60,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
||||
<Command.Item
|
||||
key={priority}
|
||||
onSelect={() => handleIssueState(priority)}
|
||||
className="focus:bg-slate-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getPriorityIcon(priority)}
|
||||
|
@ -75,8 +75,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
|
||||
<Command.Item
|
||||
key={state.id}
|
||||
onSelect={() => handleIssueState(state.id)}
|
||||
className="focus:bg-slate-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||
|
@ -51,6 +51,8 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { CreateUpdateModuleModal } from "components/modules";
|
||||
import { CreateProjectModal } from "components/project";
|
||||
import { CreateUpdateViewModal } from "components/views";
|
||||
import { CreateUpdatePageModal } from "components/pages";
|
||||
|
||||
import { Spinner } from "components/ui";
|
||||
// helpers
|
||||
import {
|
||||
@ -76,6 +78,7 @@ export const CommandPalette: React.FC = () => {
|
||||
const [isCreateModuleModalOpen, setIsCreateModuleModalOpen] = useState(false);
|
||||
const [isBulkDeleteIssuesModalOpen, setIsBulkDeleteIssuesModalOpen] = useState(false);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
|
||||
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>("");
|
||||
const [results, setResults] = useState<IWorkspaceSearchResults>({
|
||||
@ -193,6 +196,12 @@ export const CommandPalette: React.FC = () => {
|
||||
} else if (e.key.toLowerCase() === "p") {
|
||||
e.preventDefault();
|
||||
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") {
|
||||
e.preventDefault();
|
||||
toggleCollapsed();
|
||||
@ -323,6 +332,10 @@ export const CommandPalette: React.FC = () => {
|
||||
handleClose={() => setIsCreateViewModalOpen(false)}
|
||||
isOpen={isCreateViewModalOpen}
|
||||
/>
|
||||
<CreateUpdatePageModal
|
||||
isOpen={isCreateUpdatePageModalOpen}
|
||||
handleClose={() => setIsCreateUpdatePageModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{issueId && issueDetails && (
|
||||
@ -479,8 +492,7 @@ export const CommandPalette: React.FC = () => {
|
||||
setIsPaletteOpen(false);
|
||||
}}
|
||||
value={value}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden text-gray-700">
|
||||
<Icon className="h-4 w-4 text-gray-500" color="#6b7280" />
|
||||
@ -506,8 +518,7 @@ export const CommandPalette: React.FC = () => {
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-state"]);
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<Squares2X2Icon className="h-4 w-4 text-gray-500" />
|
||||
@ -520,8 +531,7 @@ export const CommandPalette: React.FC = () => {
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-priority"]);
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<ChartBarIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -534,8 +544,7 @@ export const CommandPalette: React.FC = () => {
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-assignee"]);
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<UsersIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -547,8 +556,7 @@ export const CommandPalette: React.FC = () => {
|
||||
handleIssueAssignees(user.id);
|
||||
setSearchTerm("");
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
{issueDetails?.assignees.includes(user.id) ? (
|
||||
@ -565,11 +573,7 @@ export const CommandPalette: React.FC = () => {
|
||||
</div>
|
||||
</Command.Item>
|
||||
|
||||
<Command.Item
|
||||
onSelect={deleteIssue}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<TrashIcon className="h-4 w-4 text-gray-500" />
|
||||
Delete issue
|
||||
@ -580,8 +584,7 @@ export const CommandPalette: React.FC = () => {
|
||||
setIsPaletteOpen(false);
|
||||
copyIssueUrlToClipboard();
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<LinkIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -591,11 +594,7 @@ export const CommandPalette: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
<Command.Group heading="Issue">
|
||||
<Command.Item
|
||||
onSelect={createNewIssue}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Command.Item onSelect={createNewIssue} className="focus:bg-gray-200">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
|
||||
Create new issue
|
||||
@ -608,8 +607,7 @@ export const CommandPalette: React.FC = () => {
|
||||
<Command.Group heading="Project">
|
||||
<Command.Item
|
||||
onSelect={createNewProject}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
|
||||
@ -625,8 +623,7 @@ export const CommandPalette: React.FC = () => {
|
||||
<Command.Group heading="Cycle">
|
||||
<Command.Item
|
||||
onSelect={createNewCycle}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<ContrastIcon className="h-4 w-4" color="#6b7280" />
|
||||
@ -639,8 +636,7 @@ export const CommandPalette: React.FC = () => {
|
||||
<Command.Group heading="Module">
|
||||
<Command.Item
|
||||
onSelect={createNewModule}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
|
||||
@ -651,11 +647,7 @@ export const CommandPalette: React.FC = () => {
|
||||
</Command.Group>
|
||||
|
||||
<Command.Group heading="View">
|
||||
<Command.Item
|
||||
onSelect={createNewView}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Command.Item onSelect={createNewView} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<ViewListIcon className="h-4 w-4" color="#6b7280" />
|
||||
Create new view
|
||||
@ -673,8 +665,7 @@ export const CommandPalette: React.FC = () => {
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "settings"]);
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4" color="#6b7280" />
|
||||
@ -685,8 +676,7 @@ export const CommandPalette: React.FC = () => {
|
||||
<Command.Group heading="Account">
|
||||
<Command.Item
|
||||
onSelect={createNewWorkspace}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<FolderPlusIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -703,8 +693,7 @@ export const CommandPalette: React.FC = () => {
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<RocketLaunchIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -716,8 +705,7 @@ export const CommandPalette: React.FC = () => {
|
||||
setIsPaletteOpen(false);
|
||||
window.open("https://docs.plane.so/", "_blank");
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<DocumentIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -729,8 +717,7 @@ export const CommandPalette: React.FC = () => {
|
||||
setIsPaletteOpen(false);
|
||||
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<DiscordIcon className="h-4 w-4" color="#6b7280" />
|
||||
@ -745,8 +732,7 @@ export const CommandPalette: React.FC = () => {
|
||||
"_blank"
|
||||
);
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<GithubIcon className="h-4 w-4" color="#6b7280" />
|
||||
@ -758,8 +744,7 @@ export const CommandPalette: React.FC = () => {
|
||||
setIsPaletteOpen(false);
|
||||
(window as any).$crisp.push(["do", "chat:open"]);
|
||||
}}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -774,8 +759,7 @@ export const CommandPalette: React.FC = () => {
|
||||
<>
|
||||
<Command.Item
|
||||
onSelect={() => goToSettings()}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -784,8 +768,7 @@ export const CommandPalette: React.FC = () => {
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => goToSettings("members")}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -794,8 +777,7 @@ export const CommandPalette: React.FC = () => {
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => goToSettings("billing")}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -804,8 +786,7 @@ export const CommandPalette: React.FC = () => {
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => goToSettings("integrations")}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
@ -814,8 +795,7 @@ export const CommandPalette: React.FC = () => {
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => goToSettings("import-export")}
|
||||
className="focus:bg-gray-200 focus:outline-none"
|
||||
tabIndex={0}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
|
@ -33,6 +33,8 @@ const shortcuts = [
|
||||
{ keys: "C", description: "To create issue" },
|
||||
{ keys: "Q", description: "To create cycle" },
|
||||
{ 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: "H", description: "To open shortcuts guide" },
|
||||
{
|
||||
|
@ -33,6 +33,8 @@ import {
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
XMarkIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
@ -110,8 +112,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||
false
|
||||
);
|
||||
|
||||
if (moduleId)
|
||||
else if (moduleId)
|
||||
mutate<
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
@ -123,18 +124,18 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||
false
|
||||
);
|
||||
|
||||
mutate<
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
| IIssue[]
|
||||
>(
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params),
|
||||
(prevData) =>
|
||||
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||
false
|
||||
);
|
||||
else
|
||||
mutate<
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
| IIssue[]
|
||||
>(
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params),
|
||||
(prevData) =>
|
||||
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||
false
|
||||
);
|
||||
|
||||
issuesService
|
||||
.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}>
|
||||
Copy issue link
|
||||
</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>
|
||||
<div
|
||||
className={`mb-3 rounded bg-white shadow ${
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
RectangleGroupIcon,
|
||||
Squares2X2Icon,
|
||||
TrashIcon,
|
||||
UserIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
|
||||
@ -77,6 +78,10 @@ const activityDetails: {
|
||||
message: "set the parent to",
|
||||
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 }) => (
|
||||
|
@ -125,12 +125,12 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{((content && content !== "") || htmlContent) && (
|
||||
<div className="text-sm page-block-section">
|
||||
{((content && content !== "") || htmlContent !== "<p></p>") && (
|
||||
<div className="remirror-section text-sm">
|
||||
Content:
|
||||
<RemirrorRichTextEditor
|
||||
value={htmlContent ?? <p>{content}</p>}
|
||||
customClassName="-mx-3 -my-3"
|
||||
customClassName="-m-3"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
editable={false}
|
||||
|
@ -107,7 +107,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
||||
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
||||
placeholder="Search for images"
|
||||
/>
|
||||
<PrimaryButton className="bg-indigo-600" size="sm">
|
||||
<PrimaryButton type="submit" className="bg-indigo-600" size="sm">
|
||||
Search
|
||||
</PrimaryButton>
|
||||
</form>
|
||||
|
@ -18,7 +18,7 @@ import { AllLists, AllBoards, FilterList } from "components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { CreateUpdateViewModal } from "components/views";
|
||||
import { TransferIssuesModal } from "components/cycles";
|
||||
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||
// ui
|
||||
import { EmptySpace, EmptySpaceItem, PrimaryButton, Spinner } from "components/ui";
|
||||
import { CalendarView } from "./calendar-view";
|
||||
@ -459,23 +459,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
{groupedByIssues ? (
|
||||
isNotEmpty ? (
|
||||
<>
|
||||
{isCompleted && (
|
||||
<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>
|
||||
)}
|
||||
{isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
||||
{issueView === "list" ? (
|
||||
<AllLists
|
||||
type={type}
|
||||
|
@ -27,6 +27,7 @@ import {
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
XMarkIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
@ -178,6 +179,15 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||
Copy issue link
|
||||
</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>
|
||||
<div className="border-b border-gray-300 last:border-b-0">
|
||||
<div
|
||||
|
@ -8,3 +8,4 @@ export * from "./sidebar";
|
||||
export * from "./single-cycle-card";
|
||||
export * from "./empty-cycle";
|
||||
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";
|
||||
// headless ui
|
||||
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||
// react colors
|
||||
import { TwitterPicker } from "react-color";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
// emojis
|
||||
import emojis from "./emojis.json";
|
||||
import icons from "./icons.json";
|
||||
// helpers
|
||||
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
|
||||
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 [isOpen, setIsOpen] = useState(false);
|
||||
const [openColorPicker, setOpenColorPicker] = useState(false);
|
||||
const [activeColor, setActiveColor] = useState<string>("#020617");
|
||||
|
||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||
|
||||
@ -58,20 +69,25 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel className="absolute z-10 mt-2 w-80 rounded-lg bg-white shadow-lg">
|
||||
<div className="h-72 w-80 overflow-auto rounded border bg-white p-2 shadow-2xl">
|
||||
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] bg-white shadow-lg">
|
||||
<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.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) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`-my-1 w-1/2 border-b py-2 text-center text-sm font-medium outline-none transition-colors ${
|
||||
selected ? "border-theme" : "border-transparent"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab.title}
|
||||
<Tab key={tab.key} as={React.Fragment}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setOpenColorPicker(false);
|
||||
}}
|
||||
className={`-my-1 w-1/2 border-b pb-2 text-center text-sm font-medium outline-none transition-colors ${
|
||||
selected ? "border-theme" : "border-transparent"
|
||||
}`}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
@ -79,12 +95,12 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
||||
<Tab.Panel>
|
||||
{recentEmojis.length > 0 && (
|
||||
<div className="py-2">
|
||||
<h3 className="mb-2">Recent Emojis</h3>
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{/* <h3 className="mb-2">Recent Emojis</h3> */}
|
||||
<div className="grid grid-cols-10">
|
||||
{recentEmojis.map((emoji) => (
|
||||
<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}
|
||||
onClick={() => {
|
||||
onChange(emoji);
|
||||
@ -97,13 +113,14 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<hr className="w-full h-[1px] mb-2" />
|
||||
<div>
|
||||
<h3 className="mb-2">All Emojis</h3>
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{/* <h3 className="mb-1">All Emojis</h3> */}
|
||||
<div className="grid grid-cols-10 gap-y-1">
|
||||
{emojis.map((emoji) => (
|
||||
<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}
|
||||
onClick={() => {
|
||||
onChange(emoji);
|
||||
@ -117,9 +134,76 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel className="flex h-full w-full flex-col items-center justify-center">
|
||||
<p>Coming Soon...</p>
|
||||
</Tab.Panel>
|
||||
<div className="py-2">
|
||||
<div className="relative">
|
||||
<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.Group>
|
||||
</div>
|
||||
|
@ -2,4 +2,6 @@ export type Props = {
|
||||
label: string | React.ReactNode;
|
||||
value: any;
|
||||
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_html", htmlValue);
|
||||
}}
|
||||
// placeholder="Enter Your comment..."
|
||||
placeholder="Enter your comment..."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -104,7 +104,7 @@ export const AddComment: React.FC = () => {
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
|
@ -6,6 +6,10 @@ import { useRouter } from "next/router";
|
||||
|
||||
// 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
|
||||
import { GptAssistantModal } from "components/core";
|
||||
import {
|
||||
@ -83,10 +87,13 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||
|
||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
@ -102,6 +109,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const issueName = watch("name");
|
||||
|
||||
const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
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>`);
|
||||
};
|
||||
|
||||
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(() => {
|
||||
setFocus("name");
|
||||
|
||||
@ -245,10 +292,28 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<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
|
||||
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)}
|
||||
>
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
@ -267,7 +332,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
}
|
||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||
placeholder="Description"
|
||||
placeholder="Describe the issue..."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -153,11 +153,20 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
|
||||
await issuesService
|
||||
.createIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, payload)
|
||||
.then((res) => {
|
||||
mutate(ISSUE_DETAILS(issueDetail.id));
|
||||
})
|
||||
.then(() => mutate(ISSUE_DETAILS(issueDetail.id)))
|
||||
.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
|
||||
.createModuleLink(workspaceSlug as string, projectId as string, moduleId as string, payload)
|
||||
.then((res) => {
|
||||
mutate(MODULE_DETAILS(moduleId as string));
|
||||
})
|
||||
.then(() => mutate(MODULE_DETAILS(moduleId as string)))
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't create the link. Please try again.",
|
||||
});
|
||||
if (err.status === 400)
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
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 = {
|
||||
user?: IUser;
|
||||
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 {
|
||||
@ -53,13 +54,15 @@ export const UserDetails: React.FC<Props> = ({ user, setStep }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user)
|
||||
if (user) {
|
||||
reset({
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
role: user.role,
|
||||
});
|
||||
}, [user, reset]);
|
||||
setUserRole(user.role);
|
||||
}
|
||||
}, [user, reset, setUserRole]);
|
||||
|
||||
return (
|
||||
<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 } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={(value: any) => {
|
||||
onChange(value);
|
||||
setUserRole(value ?? null);
|
||||
}}
|
||||
label={value ? value.toString() : "Select your role"}
|
||||
input
|
||||
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 "./create-update-block-inline";
|
||||
export * from "./create-update-page-modal";
|
||||
export * from "./delete-page-modal";
|
||||
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 (
|
||||
<>
|
||||
<CreateUpdatePageModal
|
||||
@ -176,6 +202,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
||||
handleDeletePage={() => handleDeletePage(page)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(page)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
|
||||
partialUpdatePage={partialUpdatePage}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
@ -189,6 +216,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
||||
handleDeletePage={() => handleDeletePage(page)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(page)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
|
||||
partialUpdatePage={partialUpdatePage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -202,6 +230,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
||||
handleDeletePage={() => handleDeletePage(page)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(page)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
|
||||
partialUpdatePage={partialUpdatePage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -8,20 +8,29 @@ import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// react-beautiful-dnd
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// services
|
||||
import pagesService from "services/pages.service";
|
||||
import issuesService from "services/issues.service";
|
||||
import aiService from "services/ai.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CreateUpdateIssueModal } from "components/issues";
|
||||
import { GptAssistantModal } from "components/core";
|
||||
import { CreateUpdateBlockInline } from "components/pages";
|
||||
// ui
|
||||
import { CustomMenu, Input, Loader, TextArea } from "components/ui";
|
||||
import { CustomMenu, Loader } from "components/ui";
|
||||
// icons
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
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
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -32,6 +41,8 @@ import { PAGE_BLOCKS_LIST } from "constants/fetch-keys";
|
||||
type Props = {
|
||||
block: IPageBlock;
|
||||
projectDetails: IProject | undefined;
|
||||
index: number;
|
||||
handleNewBlock: () => void;
|
||||
};
|
||||
|
||||
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 }) => {
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
export const SinglePageBlock: React.FC<Props> = ({
|
||||
block,
|
||||
projectDetails,
|
||||
index,
|
||||
handleNewBlock,
|
||||
}) => {
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [createBlockForm, setCreateBlockForm] = useState(false);
|
||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
|
||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||
|
||||
@ -54,7 +71,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { handleSubmit, watch, reset, setValue, control } = useForm<IPageBlock>({
|
||||
const { handleSubmit, watch, reset, setValue, register } = useForm<IPageBlock>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: {},
|
||||
@ -136,10 +153,6 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const editAndPushBlockIntoIssues = async () => {
|
||||
setCreateUpdateIssueModal(true);
|
||||
};
|
||||
|
||||
const deletePageBlock = async () => {
|
||||
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) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@ -228,110 +279,145 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
||||
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 (
|
||||
<div>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={createUpdateIssueModal}
|
||||
handleClose={() => setCreateUpdateIssueModal(false)}
|
||||
prePopulateData={{
|
||||
name: watch("name"),
|
||||
description: watch("description"),
|
||||
description_html: watch("description_html"),
|
||||
}}
|
||||
/>
|
||||
<div className="-mx-3 mt-4 flex items-center justify-between gap-2">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Block title"
|
||||
value={watch("name")}
|
||||
onBlur={handleSubmit(updatePageBlock)}
|
||||
onChange={(e) => setValue("name", e.target.value)}
|
||||
required={true}
|
||||
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"
|
||||
role="textbox"
|
||||
/>
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
{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"}
|
||||
<Draggable draggableId={block.id} index={index} isDragDisabled={createBlockForm}>
|
||||
{(provided, snapshot) => (
|
||||
<>
|
||||
{createBlockForm ? (
|
||||
<div
|
||||
className="mb-4 pt-4"
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<CreateUpdateBlockInline
|
||||
handleClose={() => setCreateBlockForm(false)}
|
||||
data={block}
|
||||
setIsSyncing={setIsSyncing}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`group relative pl-6 ${
|
||||
snapshot.isDragging ? "border-2 bg-white border-theme shadow-lg rounded-md p-6" : ""
|
||||
}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-4 -left-2 p-0.5 hover:bg-gray-100 rounded hidden group-hover:flex"
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<EllipsisVerticalIcon className="h-[18px]" />
|
||||
<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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
@ -5,9 +5,15 @@ import { useRouter } from "next/router";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// ui
|
||||
import { CustomMenu, Loader } from "components/ui";
|
||||
import { CustomMenu, Loader, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
LockClosedIcon,
|
||||
LockOpenIcon,
|
||||
PencilIcon,
|
||||
StarIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { renderShortTime } from "helpers/date-time.helper";
|
||||
@ -20,6 +26,7 @@ type TSingleStatProps = {
|
||||
handleDeletePage: () => void;
|
||||
handleAddToFavorites: () => void;
|
||||
handleRemoveFromFavorites: () => void;
|
||||
partialUpdatePage: (page: IPage, formData: Partial<IPage>) => void;
|
||||
};
|
||||
|
||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||
@ -37,90 +44,126 @@ export const SinglePageDetailedItem: React.FC<TSingleStatProps> = ({
|
||||
handleDeletePage,
|
||||
handleAddToFavorites,
|
||||
handleRemoveFromFavorites,
|
||||
partialUpdatePage,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
return (
|
||||
<div className="relative rounded border p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}>
|
||||
<a className="after:absolute after:inset-0">
|
||||
<div className="relative first:rounded-t last:rounded-b border">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}>
|
||||
<a className="block p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="mr-2 truncate text-sm font-medium">{truncateText(page.name, 75)}</p>
|
||||
</a>
|
||||
</Link>
|
||||
{page.label_details.length > 0 &&
|
||||
page.label_details.map((label) => (
|
||||
<div
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||
style={{
|
||||
backgroundColor: `${
|
||||
label?.color && label.color !== "" ? label.color : "#000000"
|
||||
}20`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{page.label_details.length > 0 &&
|
||||
page.label_details.map((label) => (
|
||||
<div
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||
style={{
|
||||
backgroundColor: `${
|
||||
label?.color && label.color !== "" ? label.color : "#000000"
|
||||
}20`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor:
|
||||
label?.color && label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-gray-400">{renderShortTime(page.updated_at)}</p>
|
||||
{page.is_favorite ? (
|
||||
<button onClick={handleRemoveFromFavorites} className="z-10 grid place-items-center">
|
||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleAddToFavorites}
|
||||
type="button"
|
||||
className="z-10 grid place-items-center"
|
||||
>
|
||||
<StarIcon className="h-4 w-4 " color="#858E96" />
|
||||
</button>
|
||||
)}
|
||||
<CustomMenu verticalEllipsis>
|
||||
<CustomMenu.MenuItem onClick={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={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-6 space-y-2 text-sm text-gray-600">
|
||||
<div className="page-block-section -m-4 -mt-6">
|
||||
{page.blocks.length > 0 ? (
|
||||
<RemirrorRichTextEditor
|
||||
value={
|
||||
!page.blocks[0].description ||
|
||||
(typeof page.blocks[0].description === "object" &&
|
||||
Object.keys(page.blocks[0].description).length === 0)
|
||||
? page.blocks[0].description_html
|
||||
: page.blocks[0].description
|
||||
}
|
||||
editable={false}
|
||||
customClassName="text-gray-500"
|
||||
noBorder
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-gray-400">{renderShortTime(page.updated_at)}</p>
|
||||
{page.is_favorite ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveFromFavorites();
|
||||
}}
|
||||
className="z-10 grid place-items-center"
|
||||
>
|
||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleAddToFavorites();
|
||||
}}
|
||||
className="z-10 grid place-items-center"
|
||||
>
|
||||
<StarIcon className="h-4 w-4 " color="#858E96" />
|
||||
</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 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>
|
||||
);
|
||||
};
|
||||
|
@ -6,7 +6,14 @@ import { useRouter } from "next/router";
|
||||
// ui
|
||||
import { CustomMenu, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { DocumentTextIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
DocumentTextIcon,
|
||||
LockClosedIcon,
|
||||
LockOpenIcon,
|
||||
PencilIcon,
|
||||
StarIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
|
||||
@ -19,6 +26,7 @@ type TSingleStatProps = {
|
||||
handleDeletePage: () => void;
|
||||
handleAddToFavorites: () => void;
|
||||
handleRemoveFromFavorites: () => void;
|
||||
partialUpdatePage: (page: IPage, formData: Partial<IPage>) => void;
|
||||
};
|
||||
|
||||
export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
||||
@ -27,6 +35,7 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
||||
handleDeletePage,
|
||||
handleAddToFavorites,
|
||||
handleRemoveFromFavorites,
|
||||
partialUpdatePage,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -94,6 +103,29 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
||||
<StarIcon className="h-4 w-4 " color="#858e96" />
|
||||
</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.MenuItem
|
||||
onClick={(e: any) => {
|
||||
|
@ -185,25 +185,23 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="relative">
|
||||
<Remirror
|
||||
manager={manager}
|
||||
initialContent={state}
|
||||
classNames={[
|
||||
`p-4 relative focus:outline-none rounded-md focus:border-gray-200 ${
|
||||
noBorder ? "" : "border"
|
||||
} ${borderOnFocus ? "focus:border" : ""} ${customClassName}`,
|
||||
} ${borderOnFocus ? "focus:border" : "focus:border-0"} ${customClassName}`,
|
||||
]}
|
||||
editable={editable}
|
||||
onBlur={() => {
|
||||
onBlur(jsonValue, htmlValue);
|
||||
}}
|
||||
>
|
||||
{/* {(!value || value === "" || value?.content?.[0]?.content === undefined) && (
|
||||
<p className="pointer-events-none absolute top-[8.8rem] left-12 text-gray-300">
|
||||
{placeholder || "Enter text..."}
|
||||
</p>
|
||||
)} */}
|
||||
{(!value || value === "" || value?.content?.[0]?.content === undefined) && placeholder && (
|
||||
<p className="absolute pointer-events-none top-4 left-4 text-gray-300">{placeholder}</p>
|
||||
)}
|
||||
<EditorComponent />
|
||||
|
||||
{imageLoader && (
|
||||
|
@ -27,6 +27,10 @@ export const EmptyState: React.FC<Props> = ({ type, title, description, imgURL,
|
||||
return "P";
|
||||
case "issue":
|
||||
return "C";
|
||||
case "view":
|
||||
return "V";
|
||||
case "page":
|
||||
return "D"
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -81,8 +81,8 @@ export const SingleViewItem: React.FC<Props> = ({ view, setSelectedView }) => {
|
||||
};
|
||||
|
||||
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 flex-col w-full gap-3">
|
||||
<div className="flex justify-between w-full">
|
||||
@ -137,7 +137,7 @@ export const SingleViewItem: React.FC<Props> = ({ view, setSelectedView }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
@ -27,6 +27,15 @@ type Props = {
|
||||
setDefaultValues: Dispatch<SetStateAction<any>>;
|
||||
};
|
||||
|
||||
const restrictedUrls = [
|
||||
"create-workspace",
|
||||
"error",
|
||||
"invitations",
|
||||
"magic-sign-in",
|
||||
"onboarding",
|
||||
"signin",
|
||||
];
|
||||
|
||||
export const CreateWorkspaceForm: React.FC<Props> = ({
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
@ -49,7 +58,7 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
|
||||
await workspaceService
|
||||
.workspaceSlugCheck(formData.slug)
|
||||
.then(async (res) => {
|
||||
if (res.status === true) {
|
||||
if (res.status === true && !restrictedUrls.includes(formData.slug)) {
|
||||
setSlugError(false);
|
||||
await workspaceService
|
||||
.createWorkspace(formData)
|
||||
|
@ -80,11 +80,8 @@ export const handleIssuesMutation: THandleIssuesMutation = (
|
||||
|
||||
let newGroup: IIssue[] = [];
|
||||
|
||||
if (selectedGroupBy === "priority") {
|
||||
newGroup = prevData[formData.priority ?? ""] ?? [];
|
||||
} else if (selectedGroupBy === "state") {
|
||||
newGroup = prevData[formData.state ?? ""] ?? [];
|
||||
}
|
||||
if (selectedGroupBy === "priority") newGroup = prevData[formData.priority ?? ""] ?? [];
|
||||
else if (selectedGroupBy === "state") newGroup = prevData[formData.state ?? ""] ?? [];
|
||||
|
||||
const updatedIssue = {
|
||||
...oldGroup[issueIndex],
|
||||
|
@ -125,7 +125,15 @@ const useIssuesView = () => {
|
||||
return issuesToGroup ? Object.assign(emptyStatesObject, issuesToGroup) : undefined;
|
||||
|
||||
return issuesToGroup;
|
||||
}, [projectIssues, cycleIssues, moduleIssues, groupByProperty, cycleId, moduleId]);
|
||||
}, [
|
||||
projectIssues,
|
||||
cycleIssues,
|
||||
moduleIssues,
|
||||
groupByProperty,
|
||||
cycleId,
|
||||
moduleId,
|
||||
emptyStatesObject,
|
||||
]);
|
||||
|
||||
const isEmpty =
|
||||
Object.values(groupedByIssues ?? {}).every((group) => group.length === 0) ||
|
||||
|
@ -29,10 +29,10 @@ const SettingsNavbar: React.FC<Props> = ({ profilePage = false }) => {
|
||||
label: "Integrations",
|
||||
href: `/${workspaceSlug}/settings/integrations`,
|
||||
},
|
||||
{
|
||||
label: "Import/Export",
|
||||
href: `/${workspaceSlug}/settings/import-export`,
|
||||
},
|
||||
// {
|
||||
// label: "Import/Export",
|
||||
// href: `/${workspaceSlug}/settings/import-export`,
|
||||
// },
|
||||
];
|
||||
|
||||
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";
|
||||
|
||||
@ -10,6 +10,9 @@ import { useForm } from "react-hook-form";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// react-color
|
||||
import { TwitterPicker } from "react-color";
|
||||
// react-beautiful-dnd
|
||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
// lib
|
||||
import { requiredAdmin, requiredAuth } from "lib/auth";
|
||||
// services
|
||||
@ -21,16 +24,24 @@ import useToast from "hooks/use-toast";
|
||||
// layouts
|
||||
import AppLayout from "layouts/app-layout";
|
||||
// components
|
||||
import { SinglePageBlock } from "components/pages";
|
||||
import { CreateUpdateBlockInline, SinglePageBlock } from "components/pages";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { CustomSearchSelect, Loader, PrimaryButton, TextArea, Tooltip } from "components/ui";
|
||||
// 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";
|
||||
// helpers
|
||||
import { renderShortTime } from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import type { NextPage, GetServerSidePropsContext } from "next";
|
||||
import { IIssueLabels, IPage, IPageBlock, UserAuth } from "types";
|
||||
@ -43,14 +54,16 @@ import {
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
const [isAddingBlock, setIsAddingBlock] = useState(false);
|
||||
const [createBlockForm, setCreateBlockForm] = useState(false);
|
||||
|
||||
const scrollToRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, pageId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { handleSubmit, reset, watch, setValue, control } = useForm<IPage>({
|
||||
const { handleSubmit, reset, watch, setValue } = useForm<IPage>({
|
||||
defaultValues: { name: "" },
|
||||
});
|
||||
|
||||
@ -65,11 +78,11 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
pagesService.getPageDetails(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
pageId as string
|
||||
)
|
||||
pagesService.getPageDetails(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
pageId as string
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
@ -77,11 +90,11 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
workspaceSlug && projectId && pageId ? PAGE_BLOCKS_LIST(pageId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
pagesService.listPageBlocks(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
pageId as string
|
||||
)
|
||||
pagesService.listPageBlocks(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
pageId as string
|
||||
)
|
||||
: 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 = () => {
|
||||
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 originURL =
|
||||
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 =
|
||||
labels?.map((label) => ({
|
||||
value: label.id,
|
||||
@ -272,8 +308,9 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||
style={{
|
||||
backgroundColor: `${label?.color && label.color !== "" ? label.color : "#000000"
|
||||
}20`,
|
||||
backgroundColor: `${
|
||||
label?.color && label.color !== "" ? label.color : "#000000"
|
||||
}20`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
@ -341,8 +378,9 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
<>
|
||||
<Popover.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") !== "" ? (
|
||||
<span
|
||||
@ -352,7 +390,7 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ColorPalletteIcon height={16} width={16} />
|
||||
<ColorPalletteIcon height={16} width={16} color="#000000" />
|
||||
)}
|
||||
</Popover.Button>
|
||||
|
||||
@ -376,6 +414,19 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
)}
|
||||
</Popover>
|
||||
</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 ? (
|
||||
<button onClick={handleRemoveFromFavorites} className="z-10">
|
||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||
@ -403,34 +454,44 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
||||
<div className="px-3">
|
||||
{pageBlocks ? (
|
||||
<>
|
||||
{pageBlocks.length !== 0 && (
|
||||
<div className="space-y-4 divide-y">
|
||||
{pageBlocks.map((block, index) => (
|
||||
<>
|
||||
<SinglePageBlock
|
||||
key={block.id}
|
||||
block={block}
|
||||
projectDetails={projectDetails}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
{pageBlocks.length !== 0 && (
|
||||
<StrictModeDroppable droppableId="blocks-list">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{pageBlocks.map((block, index) => (
|
||||
<SinglePageBlock
|
||||
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>
|
||||
)}
|
||||
<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>
|
||||
|
@ -167,7 +167,10 @@ const ProjectPages: NextPage<UserAuth> = (props) => {
|
||||
right={
|
||||
<PrimaryButton
|
||||
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" />
|
||||
Create Page
|
||||
|
@ -46,7 +46,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const scollToRef = useRef<HTMLDivElement>(null);
|
||||
const scrollToRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
@ -130,7 +130,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
||||
setLabelForm={setLabelForm}
|
||||
isUpdating={isUpdating}
|
||||
labelToUpdate={labelToUpdate}
|
||||
ref={scollToRef}
|
||||
ref={scrollToRef}
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
@ -147,7 +147,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
||||
addLabelToGroup={() => addLabelToGroup(label)}
|
||||
editLabel={(label) => {
|
||||
editLabel(label);
|
||||
scollToRef.current?.scrollIntoView({
|
||||
scrollToRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
@ -163,7 +163,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
||||
addLabelToGroup={addLabelToGroup}
|
||||
editLabel={(label) => {
|
||||
editLabel(label);
|
||||
scollToRef.current?.scrollIntoView({
|
||||
scrollToRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
|
@ -67,7 +67,14 @@ const ProjectViews: NextPage<UserAuth> = (props) => {
|
||||
}
|
||||
right={
|
||||
<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" />
|
||||
Create View
|
||||
</PrimaryButton>
|
||||
@ -100,7 +107,6 @@ const ProjectViews: NextPage<UserAuth> = (props) => {
|
||||
title="Create New View"
|
||||
description="Views aid in saving your issues by applying various filters and grouping options."
|
||||
imgURL={emptyView}
|
||||
action={() => setIsCreateViewModalOpen(true)}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
|
@ -37,15 +37,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// TODO: cache user info
|
||||
|
||||
jitsu
|
||||
.id(
|
||||
{
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
},
|
||||
true
|
||||
)
|
||||
.id({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
})
|
||||
.then(() => {
|
||||
jitsu.track(eventName, {
|
||||
...extra,
|
||||
|
@ -24,6 +24,7 @@ import type { NextPage, GetServerSidePropsContext } from "next";
|
||||
|
||||
const Onboarding: NextPage = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [userRole, setUserRole] = useState<string | null>(null);
|
||||
|
||||
const [workspace, setWorkspace] = useState();
|
||||
|
||||
@ -40,7 +41,7 @@ const Onboarding: NextPage = () => {
|
||||
<Image src={Logo} height="50" alt="Plane Logo" />
|
||||
</div>
|
||||
{step === 1 ? (
|
||||
<UserDetails user={user} setStep={setStep} />
|
||||
<UserDetails user={user} setStep={setStep} setUserRole={setUserRole} />
|
||||
) : step === 2 ? (
|
||||
<Workspace setStep={setStep} setWorkspace={setWorkspace} />
|
||||
) : (
|
||||
@ -69,7 +70,7 @@ const Onboarding: NextPage = () => {
|
||||
onClick={() => {
|
||||
if (step === 8) {
|
||||
userService
|
||||
.updateUserOnBoard()
|
||||
.updateUserOnBoard({ userRole })
|
||||
.then(() => {
|
||||
router.push("/");
|
||||
})
|
||||
|
@ -343,7 +343,7 @@ class ProjectIssuesServices extends APIService {
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -175,7 +175,7 @@ class ProjectIssuesServices extends APIService {
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import type {
|
||||
IPageBlock,
|
||||
IProject,
|
||||
IState,
|
||||
IView,
|
||||
IWorkspace,
|
||||
} from "types";
|
||||
|
||||
@ -37,6 +38,8 @@ type ModuleEventType = "MODULE_CREATE" | "MODULE_UPDATE" | "MODULE_DELETE";
|
||||
|
||||
type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE";
|
||||
|
||||
type ViewEventType = "VIEW_CREATE" | "VIEW_UPDATE" | "VIEW_DELETE";
|
||||
|
||||
type PageBlocksEventType =
|
||||
| "PAGE_BLOCK_CREATE"
|
||||
| "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();
|
||||
|
@ -47,10 +47,16 @@ class UserService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateUserOnBoard(): Promise<any> {
|
||||
return this.patch("/api/users/me/onboard/", { is_onboarded: true })
|
||||
async updateUserOnBoard({ userRole }: any): Promise<any> {
|
||||
return this.patch("/api/users/me/onboard/", {
|
||||
is_onboarded: true,
|
||||
})
|
||||
.then((response) => {
|
||||
if (trackEvent) trackEventServices.trackUserOnboardingCompleteEvent(response.data);
|
||||
if (trackEvent)
|
||||
trackEventServices.trackUserOnboardingCompleteEvent({
|
||||
...response.data,
|
||||
user_role: userRole ?? "None",
|
||||
});
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -1,10 +1,15 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
// types
|
||||
import { IView } from "types/views";
|
||||
|
||||
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 {
|
||||
constructor() {
|
||||
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> {
|
||||
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) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -25,7 +33,10 @@ class ViewServices extends APIService {
|
||||
data: IView
|
||||
): Promise<any> {
|
||||
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) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -41,7 +52,10 @@ class ViewServices extends APIService {
|
||||
`/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) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -49,7 +63,10 @@ class ViewServices extends APIService {
|
||||
|
||||
async deleteView(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||
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) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -111,7 +128,6 @@ class ViewServices extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new ViewServices();
|
||||
|
@ -31,5 +31,9 @@
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
.remirror-section .remirror-editor-wrapper .remirror-editor {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.remirror-editor-wrapper {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
@ -128,4 +128,9 @@
|
||||
.react-datepicker-popper {
|
||||
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 */
|
||||
|
Loading…
Reference in New Issue
Block a user