diff --git a/README.md b/README.md index 6af8396ac..e462b2780 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,128 @@

- Plane Logo + Plane Logo

+

Plane

+

Open-source, self-hosted project planning tool

+

Discord Discord

-
-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. -

+

+ + Plane Screens + +

+ +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 + +

+ + Plane Issue Details + +

+

+ + Plane Cycles and Modules + +

+

+ + Plane Quick Lists + +

+

+ + Plane Command K + +

+ +## 📚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. diff --git a/apiserver/.env.example b/apiserver/.env.example index 2241e2217..15056f072 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -19,3 +19,6 @@ GITHUB_CLIENT_SECRET="" # Flags DISABLE_COLLECTSTATIC=1 DOCKERIZED=1 +# GPT Envs +OPENAI_API_KEY=0 +GPT_ENGINE=0 \ No newline at end of file diff --git a/apiserver/back_migration.py b/apiserver/back_migration.py index 93f07134f..1ba312934 100644 --- a/apiserver/back_migration.py +++ b/apiserver/back_migration.py @@ -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") diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py index 8e976d318..ea9edd82c 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -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") diff --git a/apiserver/plane/api/views/people.py b/apiserver/plane/api/views/people.py index cafda3efd..78ae5b2fc 100644 --- a/apiserver/plane/api/views/people.py +++ b/apiserver/plane/api/views/people.py @@ -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 ) diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 4b1af4bed..b3c8f669a 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -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, } diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index ad46b6758..9fad9c969 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -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" /> diff --git a/apps/app/components/command-palette/change-issue-assignee.tsx b/apps/app/components/command-palette/change-issue-assignee.tsx index 54e9a4f21..09f597e2e 100644 --- a/apps/app/components/command-palette/change-issue-assignee.tsx +++ b/apps/app/components/command-palette/change-issue-assignee.tsx @@ -98,8 +98,7 @@ export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue } handleIssueAssignees(option.value)} - className="focus:bg-slate-200 focus:outline-none" - tabIndex={0} + className="focus:outline-none" > {option.content} diff --git a/apps/app/components/command-palette/change-issue-priority.tsx b/apps/app/components/command-palette/change-issue-priority.tsx index 4c8661131..b6eca1df8 100644 --- a/apps/app/components/command-palette/change-issue-priority.tsx +++ b/apps/app/components/command-palette/change-issue-priority.tsx @@ -60,8 +60,7 @@ export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue } handleIssueState(priority)} - className="focus:bg-slate-200 focus:outline-none" - tabIndex={0} + className="focus:outline-none" >
{getPriorityIcon(priority)} diff --git a/apps/app/components/command-palette/change-issue-state.tsx b/apps/app/components/command-palette/change-issue-state.tsx index a2d06050d..2eef56193 100644 --- a/apps/app/components/command-palette/change-issue-state.tsx +++ b/apps/app/components/command-palette/change-issue-state.tsx @@ -75,8 +75,7 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue }) = handleIssueState(state.id)} - className="focus:bg-slate-200 focus:outline-none" - tabIndex={0} + className="focus:outline-none" >
{getStateGroupIcon(state.group, "16", "16", state.color)} diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index dd715260c..650ab5a65 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -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(""); const [results, setResults] = useState({ @@ -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} /> + 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" >
@@ -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" >
@@ -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" >
@@ -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" >
@@ -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" >
{issueDetails?.assignees.includes(user.id) ? ( @@ -565,11 +573,7 @@ export const CommandPalette: React.FC = () => {
- +
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" >
@@ -591,11 +594,7 @@ export const CommandPalette: React.FC = () => { )} - +
Create new issue @@ -608,8 +607,7 @@ export const CommandPalette: React.FC = () => {
@@ -625,8 +623,7 @@ export const CommandPalette: React.FC = () => {
@@ -639,8 +636,7 @@ export const CommandPalette: React.FC = () => {
@@ -651,11 +647,7 @@ export const CommandPalette: React.FC = () => { - +
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" >
@@ -685,8 +676,7 @@ export const CommandPalette: React.FC = () => {
@@ -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" >
@@ -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" >
@@ -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" >
@@ -745,8 +732,7 @@ export const CommandPalette: React.FC = () => { "_blank" ); }} - className="focus:bg-gray-200 focus:outline-none" - tabIndex={0} + className="focus:outline-none" >
@@ -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" >
@@ -774,8 +759,7 @@ export const CommandPalette: React.FC = () => { <> goToSettings()} - className="focus:bg-gray-200 focus:outline-none" - tabIndex={0} + className="focus:outline-none" >
@@ -784,8 +768,7 @@ export const CommandPalette: React.FC = () => { goToSettings("members")} - className="focus:bg-gray-200 focus:outline-none" - tabIndex={0} + className="focus:outline-none" >
@@ -794,8 +777,7 @@ export const CommandPalette: React.FC = () => { goToSettings("billing")} - className="focus:bg-gray-200 focus:outline-none" - tabIndex={0} + className="focus:outline-none" >
@@ -804,8 +786,7 @@ export const CommandPalette: React.FC = () => { goToSettings("integrations")} - className="focus:bg-gray-200 focus:outline-none" - tabIndex={0} + className="focus:outline-none" >
@@ -814,8 +795,7 @@ export const CommandPalette: React.FC = () => { goToSettings("import-export")} - className="focus:bg-gray-200 focus:outline-none" - tabIndex={0} + className="focus:outline-none" >
diff --git a/apps/app/components/command-palette/shortcuts-modal.tsx b/apps/app/components/command-palette/shortcuts-modal.tsx index 0cdb051f6..9c5309138 100644 --- a/apps/app/components/command-palette/shortcuts-modal.tsx +++ b/apps/app/components/command-palette/shortcuts-modal.tsx @@ -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" }, { diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index d31481d6f..3b28ef428 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -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 = ({ 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 = ({ 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 = ({ Copy issue link + + + Open issue in new tab + +