diff --git a/README.md b/README.md index 6af8396acdd..e462b278057 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). + + +## ⚡️ 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 +``` -Visit https://app.plane.so to get started with Plane. +> If running in a cloud env replace localhost with public facing IP address of the VM -## Documentation + +- 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 2241e2217fe..15056f0722e 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 93f07134f8e..1ba31293490 100644 --- a/apiserver/back_migration.py +++ b/apiserver/back_migration.py @@ -3,7 +3,8 @@ 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 8e976d318ed..ea9edd82c9f 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -139,6 +139,16 @@ class Meta: "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 cafda3efdbb..78ae5b2fcde 100644 --- a/apiserver/plane/api/views/people.py +++ b/apiserver/plane/api/views/people.py @@ -17,6 +17,7 @@ WorkspaceMemberInvite, Issue, IssueActivity, + WorkspaceMember, ) from plane.utils.paginator import BasePaginator @@ -72,6 +73,20 @@ def patch(self, request): 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 4b1af4bed7d..b3c8f669ad8 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -21,10 +21,13 @@ 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 ad46b6758e7..9fad9c9693d 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 54e9a4f21f8..09f597e2e55 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 4c8661131f4..b6eca1df8d0 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 a2d06050d15..2eef5619338 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 dd715260c08..650ab5a6507 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 0cdb051f6c4..9c5309138d0 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 d31481d6ff3..3b28ef428ad 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 + +