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

[WEB-581] fix: issue editing functionality enhancement in Create/Edit modal #3809

Merged
merged 5 commits into from
Feb 27, 2024
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
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
Loading