Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: issue links #288

Merged
merged 4 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions apiserver/plane/api/serializers/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@
Cycle,
Module,
ModuleIssue,
IssueLink,
)


class IssueLinkCreateSerializer(serializers.Serializer):
url = serializers.CharField(required=True)
title = serializers.CharField(required=False)


class IssueFlatSerializer(BaseSerializer):
## Contain only flat fields

Expand Down Expand Up @@ -86,6 +92,11 @@ class IssueCreateSerializer(BaseSerializer):
write_only=True,
required=False,
)
links_list = serializers.ListField(
child=IssueLinkCreateSerializer(),
write_only=True,
required=False,
)

class Meta:
model = Issue
Expand All @@ -104,6 +115,7 @@ def create(self, validated_data):
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
links = validated_data.pop("links_list", None)

project = self.context["project"]
issue = Issue.objects.create(**validated_data, project=project)
Expand Down Expand Up @@ -172,13 +184,32 @@ def create(self, validated_data):
batch_size=10,
)

if links is not None:
IssueLink.objects.bulk_create(
[
IssueLink(
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
title=link.get("title", None),
url=link.get("url", None),
)
for link in links
],
batch_size=10,
ignore_conflicts=True,
)

return issue

def update(self, instance, validated_data):
blockers = validated_data.pop("blockers_list", None)
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
links = validated_data.pop("links_list", None)

if blockers is not None:
IssueBlocker.objects.filter(block=instance).delete()
Expand Down Expand Up @@ -248,6 +279,25 @@ def update(self, instance, validated_data):
batch_size=10,
)

if links is not None:
IssueLink.objects.filter(issue=instance).delete()
IssueLink.objects.bulk_create(
[
IssueLink(
issue=instance,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
title=link.get("title", None),
url=link.get("url", None),
)
for link in links
],
batch_size=10,
ignore_conflicts=True,
)

return super().update(instance, validated_data)


Expand Down Expand Up @@ -410,6 +460,12 @@ class Meta:
]


class IssueLinkSerializer(BaseSerializer):
class Meta:
model = IssueLink
fields = "__all__"


class IssueSerializer(BaseSerializer):
project_detail = ProjectSerializer(read_only=True, source="project")
state_detail = StateSerializer(read_only=True, source="state")
Expand All @@ -422,6 +478,7 @@ class IssueSerializer(BaseSerializer):
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
issue_cycle = IssueCycleDetailSerializer(read_only=True)
issue_module = IssueModuleDetailSerializer(read_only=True)
issue_link = IssueLinkSerializer(read_only=True, many=True)
sub_issues_count = serializers.IntegerField(read_only=True)

class Meta:
Expand Down
24 changes: 13 additions & 11 deletions apiserver/plane/api/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
IssueBlocker,
CycleIssue,
ModuleIssue,
IssueLink,
)
from plane.bgtasks.issue_activites_task import issue_activity

Expand Down Expand Up @@ -75,7 +76,6 @@ def perform_update(self, serializer):
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:

issue_activity.delay(
{
"type": "issue.activity",
Expand All @@ -92,7 +92,6 @@ def perform_update(self, serializer):
return super().perform_update(serializer)

def get_queryset(self):

return (
super()
.get_queryset()
Expand Down Expand Up @@ -136,6 +135,12 @@ def get_queryset(self):
).prefetch_related("module__members"),
),
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("issue"),
)
)
)

def grouper(self, issue, group_by):
Expand Down Expand Up @@ -265,6 +270,12 @@ def get(self, request, slug):
queryset=ModuleIssue.objects.select_related("module", "issue"),
),
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("issue"),
)
)
)
serializer = IssueSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
Expand All @@ -277,7 +288,6 @@ def get(self, request, slug):


class WorkSpaceIssuesEndpoint(BaseAPIView):

permission_classes = [
WorkSpaceAdminPermission,
]
Expand All @@ -298,7 +308,6 @@ def get(self, request, slug):


class IssueActivityEndpoint(BaseAPIView):

permission_classes = [
ProjectEntityPermission,
]
Expand Down Expand Up @@ -333,7 +342,6 @@ def get(self, request, slug, project_id, issue_id):


class IssueCommentViewSet(BaseViewSet):

serializer_class = IssueCommentSerializer
model = IssueComment
permission_classes = [
Expand Down Expand Up @@ -436,7 +444,6 @@ def list(self, request, slug, project_id):

def create(self, request, slug, project_id):
try:

issue_property, created = IssueProperty.objects.get_or_create(
user=request.user,
project_id=project_id,
Expand All @@ -463,7 +470,6 @@ def create(self, request, slug, project_id):


class LabelViewSet(BaseViewSet):

serializer_class = LabelSerializer
model = Label
permission_classes = [
Expand All @@ -490,14 +496,12 @@ def get_queryset(self):


class BulkDeleteIssuesEndpoint(BaseAPIView):

permission_classes = [
ProjectEntityPermission,
]

def delete(self, request, slug, project_id):
try:

issue_ids = request.data.get("issue_ids", [])

if not len(issue_ids):
Expand Down Expand Up @@ -527,14 +531,12 @@ def delete(self, request, slug, project_id):


class SubIssuesEndpoint(BaseAPIView):

permission_classes = [
ProjectEntityPermission,
]

def get(self, request, slug, project_id, issue_id):
try:

sub_issues = (
Issue.objects.filter(
parent_id=issue_id, workspace__slug=slug, project_id=project_id
Expand Down
1 change: 1 addition & 0 deletions apiserver/plane/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
IssueAssignee,
Label,
IssueBlocker,
IssueLink,
)

from .asset import FileAsset
Expand Down
17 changes: 17 additions & 0 deletions apiserver/plane/db/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,23 @@ def __str__(self):
return f"{self.issue.name} {self.assignee.email}"


class IssueLink(ProjectBaseModel):
title = models.CharField(max_length=255, null=True)
url = models.URLField()
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, related_name="issue_link"
)

class Meta:
verbose_name = "Issue Link"
verbose_name_plural = "Issue Links"
db_table = "issue_links"
ordering = ("-created_at",)

def __str__(self):
return f"{self.issue.name} {self.url}"


class IssueActivity(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_activity"
Expand Down
1 change: 1 addition & 0 deletions apps/app/components/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export * from "./existing-issues-list-modal";
export * from "./image-upload-modal";
export * from "./issues-view-filter";
export * from "./issues-view";
export * from "./link-modal";
export * from "./not-authorized-view";
Original file line number Diff line number Diff line change
Expand Up @@ -8,62 +8,36 @@ import { mutate } from "swr";
import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import modulesService from "services/modules.service";
// ui
import { Button, Input } from "components/ui";
// types
import type { IModule, ModuleLink } from "types";
// fetch-keys
import { MODULE_DETAILS } from "constants/fetch-keys";
import type { IIssueLink, ModuleLink } from "types";

type Props = {
isOpen: boolean;
module: IModule | undefined;
handleClose: () => void;
onFormSubmit: (formData: IIssueLink | ModuleLink) => void;
};

const defaultValues: ModuleLink = {
title: "",
url: "",
};

export const ModuleLinkModal: React.FC<Props> = ({ isOpen, module, handleClose }) => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;

export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }) => {
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
setError,
} = useForm<ModuleLink>({
defaultValues,
});

const onSubmit = async (formData: ModuleLink) => {
if (!workspaceSlug || !projectId || !moduleId) return;

const previousLinks = module?.link_module.map((l) => ({ title: l.title, url: l.url }));

const payload: Partial<IModule> = {
links_list: [...(previousLinks ?? []), formData],
};
await onFormSubmit(formData);

await modulesService
.patchModule(workspaceSlug as string, projectId as string, moduleId as string, payload)
.then((res) => {
mutate(MODULE_DETAILS(moduleId as string));
onClose();
})
.catch((err) => {
Object.keys(err).map((key) => {
setError(key as keyof ModuleLink, {
message: err[key].join(", "),
});
});
});
onClose();
};

const onClose = () => {
Expand Down
1 change: 1 addition & 0 deletions apps/app/components/issues/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "./activity";
export * from "./delete-issue-modal";
export * from "./description-form";
export * from "./form";
export * from "./links-list";
export * from "./modal";
export * from "./my-issues-list-item";
export * from "./parent-issues-list-modal";
Expand Down
Loading