Skip to content

Commit

Permalink
[WEB-581] fix: issue editing functionality enhancement in Create/Edit…
Browse files Browse the repository at this point in the history
… modal (#3809)

* chore: draft issue update request

* chore: changed the serializer

* chore: handled issue description in issue modal, inbox issues mutation and draft issue mutaion and changed the endpoints

* chore: handled draft toggle in make a issue payload in issues

* chore: handled issue labels in the inbox issues

---------

Co-authored-by: NarayanBavisetti <[email protected]>
  • Loading branch information
gurusainath and NarayanBavisetti authored Feb 27, 2024
1 parent c858b76 commit 34d6b13
Show file tree
Hide file tree
Showing 16 changed files with 250 additions and 156 deletions.
11 changes: 2 additions & 9 deletions apiserver/plane/app/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -2310,17 +2310,10 @@ def partial_update(self, request, slug, project_id, pk):
status=status.HTTP_404_NOT_FOUND,
)

serializer = IssueSerializer(issue, data=request.data, partial=True)
serializer = IssueCreateSerializer(issue, data=request.data, partial=True)

if serializer.is_valid():
if request.data.get(
"is_draft"
) is not None and not request.data.get("is_draft"):
serializer.save(
created_at=timezone.now(), updated_at=timezone.now()
)
else:
serializer.save()
serializer.save()
issue_activity.delay(
type="issue_draft.activity.updated",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
Expand Down
15 changes: 10 additions & 5 deletions web/components/inbox/inbox-issue-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
id: inboxIssueId,
state: "SUCCESS",
element: "Inbox page",
}
},
});
router.push({
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
Expand Down Expand Up @@ -269,12 +269,17 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
<DayPicker
selected={date ? new Date(date) : undefined}
defaultMonth={date ? new Date(date) : undefined}
onSelect={(date) => { if (!date) return; setDate(date) }}
onSelect={(date) => {
if (!date) return;
setDate(date);
}}
mode="single"
className="border border-custom-border-200 rounded-md p-3"
disabled={[{
before: tomorrow,
}]}
disabled={[
{
before: tomorrow,
},
]}
/>
<Button
variant="primary"
Expand Down
4 changes: 2 additions & 2 deletions web/components/issues/issue-detail/inbox/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
showToast: boolean = true
) => {
try {
const response = await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
if (showToast) {
setToastAlert({
title: "Issue updated successfully",
Expand All @@ -64,7 +64,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
}
captureIssueEvent({
eventName: "Inbox issue updated",
payload: { ...response, state: "SUCCESS", element: "Inbox" },
payload: { ...data, state: "SUCCESS", element: "Inbox" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
Expand Down
4 changes: 4 additions & 0 deletions web/components/issues/issue-detail/inbox/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId}
issueId={issueId}
disabled={!is_editable}
isInboxIssue
onLabelUpdate={(val: string[]) =>
issueOperations.update(workspaceSlug, projectId, issueId, { label_ids: val })
}
/>
</div>
</div>
Expand Down
31 changes: 18 additions & 13 deletions web/components/issues/issue-detail/label/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type TIssueLabel = {
projectId: string;
issueId: string;
disabled: boolean;
isInboxIssue?: boolean;
onLabelUpdate?: (labelIds: string[]) => void;
};

export type TLabelOperations = {
Expand All @@ -21,7 +23,7 @@ export type TLabelOperations = {
};

export const IssueLabel: FC<TIssueLabel> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props;
const { workspaceSlug, projectId, issueId, disabled = false, isInboxIssue = false, onLabelUpdate } = props;
// hooks
const { updateIssue } = useIssueDetail();
const { createLabel } = useLabel();
Expand All @@ -31,12 +33,14 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
() => ({
updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
try {
await updateIssue(workspaceSlug, projectId, issueId, data);
setToastAlert({
title: "Issue updated successfully",
type: "success",
message: "Issue updated successfully",
});
if (onLabelUpdate) onLabelUpdate(data.label_ids || []);
else await updateIssue(workspaceSlug, projectId, issueId, data);
if (!isInboxIssue)
setToastAlert({
title: "Issue updated successfully",
type: "success",
message: "Issue updated successfully",
});
} catch (error) {
setToastAlert({
title: "Issue update failed",
Expand All @@ -48,11 +52,12 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
try {
const labelResponse = await createLabel(workspaceSlug, projectId, data);
setToastAlert({
title: "Label created successfully",
type: "success",
message: "Label created successfully",
});
if (!isInboxIssue)
setToastAlert({
title: "Label created successfully",
type: "success",
message: "Label created successfully",
});
return labelResponse;
} catch (error) {
setToastAlert({
Expand All @@ -64,7 +69,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
}
},
}),
[updateIssue, createLabel, setToastAlert]
[updateIssue, createLabel, setToastAlert, onLabelUpdate]
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,17 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
);
};

const isDraftIssue = router?.asPath?.includes("draft-issues") || false;

const duplicateIssuePayload = omit(
{
...issue,
name: `${issue.name} (copy)`,
is_draft: isDraftIssue ? false : issue.is_draft,
},
["id"]
);

const isDraftIssue = router?.asPath?.includes("draft-issues") || false;

return (
<>
<DeleteIssueModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
<DraftKanBanLayout />
) : null}
{/* issue peek overview */}
<IssuePeekOverview />
<IssuePeekOverview is_draft />
</div>
)}
</div>
Expand Down
169 changes: 99 additions & 70 deletions web/components/issues/issue-modal/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
StateDropdown,
} from "components/dropdowns";
// ui
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
Expand Down Expand Up @@ -162,6 +162,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);

useEffect(() => {
if (data?.description_html) setValue("description_html", data?.description_html);
}, [data?.description_html]);

const issueName = watch("name");

const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
Expand Down Expand Up @@ -365,80 +369,105 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
)}
/>
<div className="relative">
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
<button
type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs bg-custom-background-80 ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
tabIndex={getTabIndex("feeling_lucky")}
>
{iAmFeelingLucky ? (
"Generating response"
) : (
<>
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
</>
)}
</button>
)}
{envConfig?.has_openai_configured && (
<GptAssistantPopover
isOpen={gptAssistantModal}
projectId={projectId}
handleClose={() => {
setGptAssistantModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
{data?.description_html === undefined ? (
<Loader className="min-h-[7rem] space-y-2 py-2 border border-custom-border-200 rounded-md p-2 overflow-hidden">
<Loader.Item width="100%" height="26px" />
<div className="flex items-center gap-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="400px" height="26px" />
</div>
<div className="flex items-center gap-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="400px" height="26px" />
</div>
<Loader.Item width="80%" height="26px" />
<div className="flex items-center gap-2">
<Loader.Item width="50%" height="26px" />
</div>
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
<Loader.Item width="100px" height="26px" />
<Loader.Item width="50px" height="26px" />
</div>
</Loader>
) : (
<Fragment>
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
tabIndex={getTabIndex("ai_assistant")}
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs bg-custom-background-80 ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
tabIndex={getTabIndex("feeling_lucky")}
>
<Sparkle className="h-4 w-4" />
AI
{iAmFeelingLucky ? (
"Generating response"
) : (
<>
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
</>
)}
</button>
}
/>
)}
</div>
<Controller
name="description_html"
control={control}
render={({ field: { value, onChange } }) => (
<RichTextEditorWithRef
cancelUploadImage={fileService.cancelUpload}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
customClassName="min-h-[7rem] border-custom-border-100"
onChange={(description: Object, description_html: string) => {
onChange(description_html);
handleFormChange();
}}
mentionHighlights={mentionHighlights}
mentionSuggestions={mentionSuggestions}
// tabIndex={2}
)}
{envConfig?.has_openai_configured && (
<GptAssistantPopover
isOpen={gptAssistantModal}
projectId={projectId}
handleClose={() => {
setGptAssistantModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
tabIndex={getTabIndex("ai_assistant")}
>
<Sparkle className="h-4 w-4" />
AI
</button>
}
/>
)}
</div>
<Controller
name="description_html"
control={control}
render={({ field: { value, onChange } }) => (
<RichTextEditorWithRef
cancelUploadImage={fileService.cancelUpload}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
initialValue={data?.description_html}
customClassName="min-h-[7rem] border-custom-border-100"
onChange={(description: Object, description_html: string) => {
onChange(description_html);
handleFormChange();
}}
mentionHighlights={mentionHighlights}
mentionSuggestions={mentionSuggestions}
// tabIndex={2}
/>
)}
/>
)}
/>
</Fragment>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<Controller
Expand Down
Loading

0 comments on commit 34d6b13

Please sign in to comment.