diff --git a/apps/api/plane/app/views/intake/base.py b/apps/api/plane/app/views/intake/base.py index ffd72503788..7dd7828cbdf 100644 --- a/apps/api/plane/app/views/intake/base.py +++ b/apps/api/plane/app/views/intake/base.py @@ -322,6 +322,9 @@ def create(self, request, slug, project_id): @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue) def partial_update(self, request, slug, project_id, pk): + skip_activity = request.data.pop("skip_activity", False) + is_description_update = request.data.get("description_html") is not None + intake_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first() intake_issue = IntakeIssue.objects.get( issue_id=pk, @@ -418,26 +421,30 @@ def partial_update(self, request, slug, project_id, pk): # Both serializers are valid, now save them if issue_serializer: issue_serializer.save() + + # Check if the update is a migration description update + is_migration_description_update = skip_activity and is_description_update # Log all the updates - if issue is not None: - issue_activity.delay( - type="issue.activity.updated", - requested_data=issue_requested_data, - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=issue_current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=base_host(request=request, is_app=True), - intake=str(intake_issue.id), - ) - # updated issue description version - issue_description_version_task.delay( - updated_issue=issue_current_instance, - issue_id=str(pk), - user_id=request.user.id, - ) + if not is_migration_description_update: + if issue is not None: + issue_activity.delay( + type="issue.activity.updated", + requested_data=issue_requested_data, + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=issue_current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=base_host(request=request, is_app=True), + intake=str(intake_issue.id), + ) + # updated issue description version + issue_description_version_task.delay( + updated_issue=issue_current_instance, + issue_id=str(pk), + user_id=request.user.id, + ) if intake_serializer: intake_serializer.save() diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index c24db616980..7a5e7dddf62 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -611,6 +611,10 @@ def retrieve(self, request, slug, project_id, pk=None): def partial_update(self, request, slug, project_id, pk=None): queryset = self.get_queryset() queryset = self.apply_annotations(queryset) + + skip_activity = request.data.pop("skip_activity", False) + is_description_update = request.data.get("description_html") is not None + issue = ( queryset.annotate( label_ids=Coalesce( @@ -659,32 +663,36 @@ def partial_update(self, request, slug, project_id, pk=None): serializer = IssueCreateSerializer(issue, data=request.data, partial=True, context={"project_id": project_id}) if serializer.is_valid(): serializer.save() - issue_activity.delay( - type="issue.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=base_host(request=request, is_app=True), - ) - model_activity.delay( - model_name="issue", - model_id=str(serializer.data.get("id", None)), - requested_data=request.data, - current_instance=current_instance, - actor_id=request.user.id, - slug=slug, - origin=base_host(request=request, is_app=True), - ) - # updated issue description version - issue_description_version_task.delay( - updated_issue=current_instance, - issue_id=str(serializer.data.get("id", None)), - user_id=request.user.id, - ) + # Check if the update is a migration description update + is_migration_description_update = skip_activity and is_description_update + # Log all the updates + if not is_migration_description_update: + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=base_host(request=request, is_app=True), + ) + model_activity.delay( + model_name="issue", + model_id=str(serializer.data.get("id", None)), + requested_data=request.data, + current_instance=current_instance, + actor_id=request.user.id, + slug=slug, + origin=base_host(request=request, is_app=True), + ) + # updated issue description version + issue_description_version_task.delay( + updated_issue=current_instance, + issue_id=str(serializer.data.get("id", None)), + user_id=request.user.id, + ) return Response(status=status.HTTP_204_NO_CONTENT) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apps/web/core/components/editor/rich-text/description-input/root.tsx b/apps/web/core/components/editor/rich-text/description-input/root.tsx index 57d167fe878..7c95e1ace5a 100644 --- a/apps/web/core/components/editor/rich-text/description-input/root.tsx +++ b/apps/web/core/components/editor/rich-text/description-input/root.tsx @@ -22,6 +22,7 @@ const workspaceService = new WorkspaceService(); type TFormData = { id: string; description_html: string; + isMigrationUpdate: boolean; }; type Props = { @@ -56,7 +57,7 @@ type Props = { /** * @description Submit handler, the actual function which will be called when the form is submitted */ - onSubmit: (value: string) => Promise; + onSubmit: (value: string, isMigrationUpdate?: boolean) => Promise; /** * @description Placeholder, if not provided, the placeholder will be the default placeholder */ @@ -108,6 +109,7 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props) const [localDescription, setLocalDescription] = useState({ id: entityId, description_html: initialValue?.trim() ?? "", + isMigrationUpdate: false, }); // ref to track if there are unsaved changes const hasUnsavedChanges = useRef(false); @@ -119,17 +121,18 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props) // translation const { t } = useTranslation(); // form info - const { handleSubmit, reset, control } = useForm({ + const { handleSubmit, reset, control, setValue } = useForm({ defaultValues: { id: entityId, description_html: initialValue?.trim() ?? "", + isMigrationUpdate: false, }, }); // submit handler const handleDescriptionFormSubmit = useCallback( async (formData: TFormData) => { - await onSubmit(formData.description_html); + await onSubmit(formData.description_html, formData.isMigrationUpdate); }, [onSubmit] ); @@ -140,10 +143,12 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props) reset({ id: entityId, description_html: initialValue?.trim() === "" ? "

" : (initialValue ?? "

"), + isMigrationUpdate: false, }); setLocalDescription({ id: entityId, description_html: initialValue?.trim() === "" ? "

" : (initialValue ?? "

"), + isMigrationUpdate: false, }); // Reset unsaved changes flag when form is reset hasUnsavedChanges.current = false; @@ -206,9 +211,10 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props) workspaceId={workspaceDetails.id} projectId={projectId} dragDropEnabled - onChange={(_description, description_html) => { + onChange={(_description, description_html, options) => { setIsSubmitting("submitting"); onChange(description_html); + setValue("isMigrationUpdate", options?.isMigrationUpdate ?? false); hasUnsavedChanges.current = true; debouncedFormSave(); }} diff --git a/apps/web/core/components/inbox/content/issue-root.tsx b/apps/web/core/components/inbox/content/issue-root.tsx index c7d2441cbbe..295f3d2dff9 100644 --- a/apps/web/core/components/inbox/content/issue-root.tsx +++ b/apps/web/core/components/inbox/content/issue-root.tsx @@ -201,10 +201,11 @@ export const InboxIssueMainContent = observer(function InboxIssueMainContent(pro entityId={issue.id} fileAssetType={EFileAssetType.ISSUE_DESCRIPTION} initialValue={issue.description_html ?? "

"} - onSubmit={async (value) => { + onSubmit={async (value, isMigrationUpdate) => { if (!issue.id || !issue.project_id) return; await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { description_html: value, + ...(isMigrationUpdate ? { skip_activity: "true" } : {}), }); }} projectId={issue.project_id} diff --git a/apps/web/core/components/issues/issue-detail/main-content.tsx b/apps/web/core/components/issues/issue-detail/main-content.tsx index 20d49547a60..f0094bd7c9e 100644 --- a/apps/web/core/components/issues/issue-detail/main-content.tsx +++ b/apps/web/core/components/issues/issue-detail/main-content.tsx @@ -134,10 +134,11 @@ export const IssueMainContent = observer(function IssueMainContent(props: Props) entityId={issue.id} fileAssetType={EFileAssetType.ISSUE_DESCRIPTION} initialValue={issue.description_html} - onSubmit={async (value) => { + onSubmit={async (value, isMigrationUpdate) => { if (!issue.id || !issue.project_id) return; await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { description_html: value, + ...(isMigrationUpdate ? { skip_activity: "true" } : {}), }); }} projectId={issue.project_id} diff --git a/apps/web/core/components/issues/peek-overview/issue-detail.tsx b/apps/web/core/components/issues/peek-overview/issue-detail.tsx index e696e878f89..3efdbf6418a 100644 --- a/apps/web/core/components/issues/peek-overview/issue-detail.tsx +++ b/apps/web/core/components/issues/peek-overview/issue-detail.tsx @@ -134,10 +134,11 @@ export const PeekOverviewIssueDetails = observer(function PeekOverviewIssueDetai entityId={issue.id} fileAssetType={EFileAssetType.ISSUE_DESCRIPTION} initialValue={issueDescription} - onSubmit={async (value) => { + onSubmit={async (value, isMigrationUpdate) => { if (!issue.id || !issue.project_id) return; await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { description_html: value, + ...(isMigrationUpdate ? { skip_activity: "true" } : {}), }); }} setIsSubmitting={(value) => setIsSubmitting(value)} diff --git a/packages/editor/src/core/extensions/unique-id/utils.ts b/packages/editor/src/core/extensions/unique-id/utils.ts index 617ff5dc621..22606c9c6ec 100644 --- a/packages/editor/src/core/extensions/unique-id/utils.ts +++ b/packages/editor/src/core/extensions/unique-id/utils.ts @@ -30,6 +30,7 @@ export const createIdsForView = (view: EditorView, options: UniqueIDOptions) => }); tr.setMeta("addToHistory", false); + tr.setMeta("uniqueIdOnlyChange", true); view.dispatch(tr); }; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index cc3343441fc..5328a3ea5aa 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -80,7 +80,11 @@ export const useEditor = (props: TEditorHookProps) => { onTransaction: () => { onTransaction?.(); }, - onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()), + onUpdate: ({ editor, transaction }) => { + // Check if this update is only due to migration update + const isMigrationUpdate = transaction?.getMeta("uniqueIdOnlyChange") === true; + onChange?.(editor.getJSON(), editor.getHTML(), { isMigrationUpdate }); + }, onDestroy: () => handleEditorReady?.(false), onFocus: onEditorFocus, }, diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index be7cc6afe5d..3701536b51e 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -160,7 +160,7 @@ export type IEditorProps = { mentionHandler: TMentionHandler; onAssetChange?: (assets: TEditorAsset[]) => void; onEditorFocus?: () => void; - onChange?: (json: object, html: string) => void; + onChange?: (json: object, html: string, { isMigrationUpdate }?: { isMigrationUpdate?: boolean }) => void; onEnterKeyPress?: (e?: any) => void; onTransaction?: () => void; placeholder?: string | ((isFocused: boolean, value: string) => string);