fix: activity tracking description (#8268)

* feat: add no_activity flag to control issue activity tracking during partial updates

* refactor: rename no_activity flag to skip_activity for clarity in issue activity tracking

* enhance description input handling with migration update support

* feat: implement skip_activity flag to conditionally log issue updates during partial updates

* refactor: skip-activity

* feat: add migration description update check to conditionally log issue updates

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
Vipin Chaudhary 2025-12-08 22:18:14 +05:30 committed by GitHub
parent f0bc2bd3bd
commit a9e9cb2983
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 83 additions and 54 deletions

View file

@ -322,6 +322,9 @@ class IntakeIssueViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue) @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
def partial_update(self, request, slug, project_id, pk): 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_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
intake_issue = IntakeIssue.objects.get( intake_issue = IntakeIssue.objects.get(
issue_id=pk, issue_id=pk,
@ -418,7 +421,11 @@ class IntakeIssueViewSet(BaseViewSet):
# Both serializers are valid, now save them # Both serializers are valid, now save them
if issue_serializer: if issue_serializer:
issue_serializer.save() 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 # Log all the updates
if not is_migration_description_update:
if issue is not None: if issue is not None:
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",

View file

@ -611,6 +611,10 @@ class IssueViewSet(BaseViewSet):
def partial_update(self, request, slug, project_id, pk=None): def partial_update(self, request, slug, project_id, pk=None):
queryset = self.get_queryset() queryset = self.get_queryset()
queryset = self.apply_annotations(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 = ( issue = (
queryset.annotate( queryset.annotate(
label_ids=Coalesce( label_ids=Coalesce(
@ -659,6 +663,10 @@ class IssueViewSet(BaseViewSet):
serializer = IssueCreateSerializer(issue, data=request.data, partial=True, context={"project_id": project_id}) serializer = IssueCreateSerializer(issue, data=request.data, partial=True, context={"project_id": project_id})
if serializer.is_valid(): if serializer.is_valid():
serializer.save() 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 is_migration_description_update:
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
requested_data=requested_data, requested_data=requested_data,

View file

@ -22,6 +22,7 @@ const workspaceService = new WorkspaceService();
type TFormData = { type TFormData = {
id: string; id: string;
description_html: string; description_html: string;
isMigrationUpdate: boolean;
}; };
type Props = { type Props = {
@ -56,7 +57,7 @@ type Props = {
/** /**
* @description Submit handler, the actual function which will be called when the form is submitted * @description Submit handler, the actual function which will be called when the form is submitted
*/ */
onSubmit: (value: string) => Promise<void>; onSubmit: (value: string, isMigrationUpdate?: boolean) => Promise<void>;
/** /**
* @description Placeholder, if not provided, the placeholder will be the default placeholder * @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<TFormData>({ const [localDescription, setLocalDescription] = useState<TFormData>({
id: entityId, id: entityId,
description_html: initialValue?.trim() ?? "", description_html: initialValue?.trim() ?? "",
isMigrationUpdate: false,
}); });
// ref to track if there are unsaved changes // ref to track if there are unsaved changes
const hasUnsavedChanges = useRef(false); const hasUnsavedChanges = useRef(false);
@ -119,17 +121,18 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props)
// translation // translation
const { t } = useTranslation(); const { t } = useTranslation();
// form info // form info
const { handleSubmit, reset, control } = useForm<TFormData>({ const { handleSubmit, reset, control, setValue } = useForm<TFormData>({
defaultValues: { defaultValues: {
id: entityId, id: entityId,
description_html: initialValue?.trim() ?? "", description_html: initialValue?.trim() ?? "",
isMigrationUpdate: false,
}, },
}); });
// submit handler // submit handler
const handleDescriptionFormSubmit = useCallback( const handleDescriptionFormSubmit = useCallback(
async (formData: TFormData) => { async (formData: TFormData) => {
await onSubmit(formData.description_html); await onSubmit(formData.description_html, formData.isMigrationUpdate);
}, },
[onSubmit] [onSubmit]
); );
@ -140,10 +143,12 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props)
reset({ reset({
id: entityId, id: entityId,
description_html: initialValue?.trim() === "" ? "<p></p>" : (initialValue ?? "<p></p>"), description_html: initialValue?.trim() === "" ? "<p></p>" : (initialValue ?? "<p></p>"),
isMigrationUpdate: false,
}); });
setLocalDescription({ setLocalDescription({
id: entityId, id: entityId,
description_html: initialValue?.trim() === "" ? "<p></p>" : (initialValue ?? "<p></p>"), description_html: initialValue?.trim() === "" ? "<p></p>" : (initialValue ?? "<p></p>"),
isMigrationUpdate: false,
}); });
// Reset unsaved changes flag when form is reset // Reset unsaved changes flag when form is reset
hasUnsavedChanges.current = false; hasUnsavedChanges.current = false;
@ -206,9 +211,10 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props)
workspaceId={workspaceDetails.id} workspaceId={workspaceDetails.id}
projectId={projectId} projectId={projectId}
dragDropEnabled dragDropEnabled
onChange={(_description, description_html) => { onChange={(_description, description_html, options) => {
setIsSubmitting("submitting"); setIsSubmitting("submitting");
onChange(description_html); onChange(description_html);
setValue("isMigrationUpdate", options?.isMigrationUpdate ?? false);
hasUnsavedChanges.current = true; hasUnsavedChanges.current = true;
debouncedFormSave(); debouncedFormSave();
}} }}

View file

@ -201,10 +201,11 @@ export const InboxIssueMainContent = observer(function InboxIssueMainContent(pro
entityId={issue.id} entityId={issue.id}
fileAssetType={EFileAssetType.ISSUE_DESCRIPTION} fileAssetType={EFileAssetType.ISSUE_DESCRIPTION}
initialValue={issue.description_html ?? "<p></p>"} initialValue={issue.description_html ?? "<p></p>"}
onSubmit={async (value) => { onSubmit={async (value, isMigrationUpdate) => {
if (!issue.id || !issue.project_id) return; if (!issue.id || !issue.project_id) return;
await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { await issueOperations.update(workspaceSlug, issue.project_id, issue.id, {
description_html: value, description_html: value,
...(isMigrationUpdate ? { skip_activity: "true" } : {}),
}); });
}} }}
projectId={issue.project_id} projectId={issue.project_id}

View file

@ -134,10 +134,11 @@ export const IssueMainContent = observer(function IssueMainContent(props: Props)
entityId={issue.id} entityId={issue.id}
fileAssetType={EFileAssetType.ISSUE_DESCRIPTION} fileAssetType={EFileAssetType.ISSUE_DESCRIPTION}
initialValue={issue.description_html} initialValue={issue.description_html}
onSubmit={async (value) => { onSubmit={async (value, isMigrationUpdate) => {
if (!issue.id || !issue.project_id) return; if (!issue.id || !issue.project_id) return;
await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { await issueOperations.update(workspaceSlug, issue.project_id, issue.id, {
description_html: value, description_html: value,
...(isMigrationUpdate ? { skip_activity: "true" } : {}),
}); });
}} }}
projectId={issue.project_id} projectId={issue.project_id}

View file

@ -134,10 +134,11 @@ export const PeekOverviewIssueDetails = observer(function PeekOverviewIssueDetai
entityId={issue.id} entityId={issue.id}
fileAssetType={EFileAssetType.ISSUE_DESCRIPTION} fileAssetType={EFileAssetType.ISSUE_DESCRIPTION}
initialValue={issueDescription} initialValue={issueDescription}
onSubmit={async (value) => { onSubmit={async (value, isMigrationUpdate) => {
if (!issue.id || !issue.project_id) return; if (!issue.id || !issue.project_id) return;
await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { await issueOperations.update(workspaceSlug, issue.project_id, issue.id, {
description_html: value, description_html: value,
...(isMigrationUpdate ? { skip_activity: "true" } : {}),
}); });
}} }}
setIsSubmitting={(value) => setIsSubmitting(value)} setIsSubmitting={(value) => setIsSubmitting(value)}

View file

@ -30,6 +30,7 @@ export const createIdsForView = (view: EditorView, options: UniqueIDOptions) =>
}); });
tr.setMeta("addToHistory", false); tr.setMeta("addToHistory", false);
tr.setMeta("uniqueIdOnlyChange", true);
view.dispatch(tr); view.dispatch(tr);
}; };

View file

@ -80,7 +80,11 @@ export const useEditor = (props: TEditorHookProps) => {
onTransaction: () => { onTransaction: () => {
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), onDestroy: () => handleEditorReady?.(false),
onFocus: onEditorFocus, onFocus: onEditorFocus,
}, },

View file

@ -160,7 +160,7 @@ export type IEditorProps = {
mentionHandler: TMentionHandler; mentionHandler: TMentionHandler;
onAssetChange?: (assets: TEditorAsset[]) => void; onAssetChange?: (assets: TEditorAsset[]) => void;
onEditorFocus?: () => void; onEditorFocus?: () => void;
onChange?: (json: object, html: string) => void; onChange?: (json: object, html: string, { isMigrationUpdate }?: { isMigrationUpdate?: boolean }) => void;
onEnterKeyPress?: (e?: any) => void; onEnterKeyPress?: (e?: any) => void;
onTransaction?: () => void; onTransaction?: () => void;
placeholder?: string | ((isFocused: boolean, value: string) => string); placeholder?: string | ((isFocused: boolean, value: string) => string);