From fdefd110243b33525465155397edc06eb5016919 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 Dec 2025 20:43:53 +0530 Subject: [PATCH 1/6] feat: add no_activity flag to control issue activity tracking during partial updates --- apps/api/plane/app/views/issue/base.py | 58 ++++++++++++++------------ 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index c24db616980..c7941ef5485 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -611,6 +611,9 @@ 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) + + no_activity = request.data.pop("no_activity", "false") == "true" + issue = ( queryset.annotate( label_ids=Coalesce( @@ -659,32 +662,35 @@ 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, - ) + + # Track the issue activity + if not no_activity: + 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) From 8521e4b957e4c4ca411eb5008e2c037f8bc4428c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 Dec 2025 21:05:22 +0530 Subject: [PATCH 2/6] refactor: rename no_activity flag to skip_activity for clarity in issue activity tracking --- apps/api/plane/app/views/issue/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index c7941ef5485..eb02d24b659 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -612,7 +612,8 @@ def partial_update(self, request, slug, project_id, pk=None): queryset = self.get_queryset() queryset = self.apply_annotations(queryset) - no_activity = request.data.pop("no_activity", "false") == "true" + skip_activity = request.data.pop("skip_activity", False) + is_description_update = request.data.get("description_html") is not None issue = ( queryset.annotate( @@ -663,8 +664,7 @@ def partial_update(self, request, slug, project_id, pk=None): if serializer.is_valid(): serializer.save() - # Track the issue activity - if not no_activity: + if not skip_activity or not is_description_update: issue_activity.delay( type="issue.activity.updated", requested_data=requested_data, From 7e2a7e8bc4d39c50a96ca4c46765136c90f0d3a1 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Mon, 8 Dec 2025 21:26:54 +0530 Subject: [PATCH 3/6] enhance description input handling with migration update support --- .../editor/rich-text/description-input/root.tsx | 14 ++++++++++---- .../core/components/inbox/content/issue-root.tsx | 3 ++- .../issues/issue-detail/main-content.tsx | 3 ++- .../issues/peek-overview/issue-detail.tsx | 3 ++- .../editor/src/core/extensions/unique-id/utils.ts | 1 + packages/editor/src/core/hooks/use-editor.ts | 6 +++++- packages/editor/src/core/types/editor.ts | 2 +- 7 files changed, 23 insertions(+), 9 deletions(-) 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..871f0a47197 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 ? { no_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..d6182abd264 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 ? { no_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..887bf6bb6e7 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 ? { no_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); From 29775536680310c849fce4473e6628d1b4aa47c9 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 Dec 2025 21:27:21 +0530 Subject: [PATCH 4/6] feat: implement skip_activity flag to conditionally log issue updates during partial updates --- apps/api/plane/app/views/intake/base.py | 42 ++++++++++++++----------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/apps/api/plane/app/views/intake/base.py b/apps/api/plane/app/views/intake/base.py index ffd72503788..c16ad84942d 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, @@ -419,25 +422,26 @@ def partial_update(self, request, slug, project_id, pk): if issue_serializer: issue_serializer.save() # 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 skip_activity or not is_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() From 968ce2b1e1e99b430adf62b61afc33606befc08e Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Mon, 8 Dec 2025 21:57:54 +0530 Subject: [PATCH 5/6] refactor: skip-activity --- apps/web/core/components/inbox/content/issue-root.tsx | 2 +- apps/web/core/components/issues/issue-detail/main-content.tsx | 2 +- apps/web/core/components/issues/peek-overview/issue-detail.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/core/components/inbox/content/issue-root.tsx b/apps/web/core/components/inbox/content/issue-root.tsx index 871f0a47197..295f3d2dff9 100644 --- a/apps/web/core/components/inbox/content/issue-root.tsx +++ b/apps/web/core/components/inbox/content/issue-root.tsx @@ -205,7 +205,7 @@ export const InboxIssueMainContent = observer(function InboxIssueMainContent(pro if (!issue.id || !issue.project_id) return; await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { description_html: value, - ...(isMigrationUpdate ? { no_activity: "true" } : {}), + ...(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 d6182abd264..f0094bd7c9e 100644 --- a/apps/web/core/components/issues/issue-detail/main-content.tsx +++ b/apps/web/core/components/issues/issue-detail/main-content.tsx @@ -138,7 +138,7 @@ export const IssueMainContent = observer(function IssueMainContent(props: Props) if (!issue.id || !issue.project_id) return; await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { description_html: value, - ...(isMigrationUpdate ? { no_activity: "true" } : {}), + ...(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 887bf6bb6e7..3efdbf6418a 100644 --- a/apps/web/core/components/issues/peek-overview/issue-detail.tsx +++ b/apps/web/core/components/issues/peek-overview/issue-detail.tsx @@ -138,7 +138,7 @@ export const PeekOverviewIssueDetails = observer(function PeekOverviewIssueDetai if (!issue.id || !issue.project_id) return; await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { description_html: value, - ...(isMigrationUpdate ? { no_activity: "true" } : {}), + ...(isMigrationUpdate ? { skip_activity: "true" } : {}), }); }} setIsSubmitting={(value) => setIsSubmitting(value)} From 32597f68f2e71cc230399fd5ef722bddbbdb2640 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 Dec 2025 22:06:45 +0530 Subject: [PATCH 6/6] feat: add migration description update check to conditionally log issue updates --- apps/api/plane/app/views/intake/base.py | 5 ++++- apps/api/plane/app/views/issue/base.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/api/plane/app/views/intake/base.py b/apps/api/plane/app/views/intake/base.py index c16ad84942d..7dd7828cbdf 100644 --- a/apps/api/plane/app/views/intake/base.py +++ b/apps/api/plane/app/views/intake/base.py @@ -421,8 +421,11 @@ 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 not skip_activity or not is_description_update: + if not is_migration_description_update: if issue is not None: issue_activity.delay( type="issue.activity.updated", diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index eb02d24b659..7a5e7dddf62 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -663,8 +663,10 @@ 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() - - if not skip_activity or not is_description_update: + # 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,