Skip to content
45 changes: 26 additions & 19 deletions apps/api/plane/app/views/intake/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
60 changes: 34 additions & 26 deletions apps/api/plane/app/views/issue/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const workspaceService = new WorkspaceService();
type TFormData = {
id: string;
description_html: string;
isMigrationUpdate: boolean;
};

type Props = {
Expand Down Expand Up @@ -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<void>;
onSubmit: (value: string, isMigrationUpdate?: boolean) => Promise<void>;
/**
* @description Placeholder, if not provided, the placeholder will be the default placeholder
*/
Expand Down Expand Up @@ -108,6 +109,7 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props)
const [localDescription, setLocalDescription] = useState<TFormData>({
id: entityId,
description_html: initialValue?.trim() ?? "",
isMigrationUpdate: false,
});
// ref to track if there are unsaved changes
const hasUnsavedChanges = useRef(false);
Expand All @@ -119,17 +121,18 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props)
// translation
const { t } = useTranslation();
// form info
const { handleSubmit, reset, control } = useForm<TFormData>({
const { handleSubmit, reset, control, setValue } = useForm<TFormData>({
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]
);
Expand All @@ -140,10 +143,12 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props)
reset({
id: entityId,
description_html: initialValue?.trim() === "" ? "<p></p>" : (initialValue ?? "<p></p>"),
isMigrationUpdate: false,
});
setLocalDescription({
id: entityId,
description_html: initialValue?.trim() === "" ? "<p></p>" : (initialValue ?? "<p></p>"),
isMigrationUpdate: false,
});
// Reset unsaved changes flag when form is reset
hasUnsavedChanges.current = false;
Expand Down Expand Up @@ -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();
}}
Expand Down
3 changes: 2 additions & 1 deletion apps/web/core/components/inbox/content/issue-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,11 @@ export const InboxIssueMainContent = observer(function InboxIssueMainContent(pro
entityId={issue.id}
fileAssetType={EFileAssetType.ISSUE_DESCRIPTION}
initialValue={issue.description_html ?? "<p></p>"}
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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" } : {}),
});
Comment on lines 137 to 142
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "no_activity" -S

Repository: makeplane/plane

Length of output: 456


🏁 Script executed:

#!/bin/bash
rg -n "skip_activity" -S

Repository: makeplane/plane

Length of output: 488


🏁 Script executed:

#!/bin/bash
# Search for issue operations/update patterns to understand payload structure
rg -n "issueOperations\.update|description_html" -S -A 3 -B 1

Repository: makeplane/plane

Length of output: 50372


🏁 Script executed:

#!/bin/bash
# Check the exact backend logic for skip_activity in issue updates
sed -n '610,670p' apps/api/plane/app/views/issue/base.py

Repository: makeplane/plane

Length of output: 2688


🏁 Script executed:

#!/bin/bash
# Check intake view as well
sed -n '320,430p' apps/api/plane/app/views/intake/base.py

Repository: makeplane/plane

Length of output: 4883


Field name mismatch blocks activity suppression; rename no_activity to skip_activity

The handler sends no_activity: "true" but the backend's partial_update method (issue/base.py:615, intake/base.py:325) pops and checks for skip_activity. Because the field name does not match, the backend defaults skip_activity to False, causing activity to be logged on every migration update instead of suppressing it.

Additionally, the field is sent as a string "true" rather than a boolean. The backend's logic if not skip_activity or not is_description_update: expects a boolean value.

Rename no_activity to skip_activity and send it as a boolean (true instead of "true"):

...(isMigrationUpdate ? { skip_activity: true } : {}),
🤖 Prompt for AI Agents
In apps/web/core/components/issues/issue-detail/main-content.tsx around lines
137 to 142, the update payload uses the wrong field name and type for
suppressing activity: it sends no_activity: "true" but the backend expects
skip_activity as a boolean. Change the conditional payload to use skip_activity:
true (boolean) instead of no_activity: "true" so the backend's partial_update
logic correctly detects and respects activity suppression.

}}
projectId={issue.project_id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" } : {}),
});
Comment on lines 137 to 142
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for no_activity usage patterns
rg -n "no_activity" -S --type-add 'web:tsx,ts' -t web

Repository: makeplane/plane

Length of output: 207


🏁 Script executed:

# Search for skip_activity or similar alternative patterns
rg -n "skip_activity|skipActivity" -S --type-add 'web:tsx,ts' -t web

Repository: makeplane/plane

Length of output: 207


🏁 Script executed:

# Search for issueOperations.update calls to see how no_activity is used
rg -B 2 -A 2 "issueOperations\.update" --type-add 'web:tsx,ts' -t web

Repository: makeplane/plane

Length of output: 207


🏁 Script executed:

# Check the issueOperations definition to understand the update signature
fd -e ts -e tsx | xargs grep -l "issueOperations\s*=" | head -5

Repository: makeplane/plane

Length of output: 484


🏁 Script executed:

# Search for no_activity without type restrictions
rg "no_activity" -n

Repository: makeplane/plane

Length of output: 456


🏁 Script executed:

# Search for skip_activity or similar patterns
rg "skip_activity|skipActivity" -n

Repository: makeplane/plane

Length of output: 488


🏁 Script executed:

# Search for issueOperations.update calls
rg "issueOperations\.update" -B 2 -A 2 -n

Repository: makeplane/plane

Length of output: 15603


🏁 Script executed:

# Inspect the target file
cat -n apps/web/core/components/issues/peek-overview/issue-detail.tsx | head -150

Repository: makeplane/plane

Length of output: 6562


🏁 Script executed:

# Search for issueOperations.update implementation to see how payload is sent
rg -n "const.*issueOperations.*=" --type-add 'web:tsx,ts,js' -A 5 | head -50

Repository: makeplane/plane

Length of output: 4914


🏁 Script executed:

# Look for the update method definition in issue operations
fd -e ts -e tsx | xargs grep -l "update.*workspaceSlug.*projectId" | grep -i operation

Repository: makeplane/plane

Length of output: 137


🏁 Script executed:

# Check for any API middleware or transformation
rg -n "no_activity|skip_activity" apps/web --type-add 'web:tsx,ts,js' -B 3 -A 3

Repository: makeplane/plane

Length of output: 2378


🏁 Script executed:

# Get the full issueOperations definition from peek-overview/root.tsx
cat -n apps/web/core/components/issues/peek-overview/root.tsx | sed -n '63,150p'

Repository: makeplane/plane

Length of output: 3922


🏁 Script executed:

# Search for the update method definition in issueOperations
rg -n "update.*async.*workspaceSlug" apps/web/core/components/issues/peek-overview/root.tsx -A 10

Repository: makeplane/plane

Length of output: 587


🏁 Script executed:

# Look for API service or client that processes the update payload
rg -n "class.*IssueService|export.*function.*update" --type-add 'web:tsx,ts,js' apps/web/services -A 3

Repository: makeplane/plane

Length of output: 116


🏁 Script executed:

# Find where issues.updateIssue is defined
rg -n "updateIssue.*async" --type-add 'web:tsx,ts,js' -A 5 | head -80

Repository: makeplane/plane

Length of output: 7001


🏁 Script executed:

# Search for issues store/hook definition
rg -n "useIssues|issues.*useMemo" apps/web/core/components/issues/peek-overview/root.tsx -B 5 -A 3

Repository: makeplane/plane

Length of output: 1158


🏁 Script executed:

# Find the actual store implementation
fd -type f -name "*.ts" -o -name "*.tsx" | xargs rg -l "updateIssue" | grep -E "store|hook" | head -5

Repository: makeplane/plane

Length of output: 472


🏁 Script executed:

# Get the full updateIssue implementation from issue.store.ts
cat -n apps/web/core/store/issue/issue-details/issue.store.ts | sed -n '175,220p'

Repository: makeplane/plane

Length of output: 2275


🏁 Script executed:

# Search for projectIssues service to see the actual API call
rg -n "projectIssues\." apps/web/core/store/issue/issue-details/issue.store.ts -B 2 -A 2

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Find where projectIssues is instantiated/imported
rg -n "projectIssues" apps/web/core/store/issue/issue-details/issue.store.ts | head -20

Repository: makeplane/plane

Length of output: 268


🏁 Script executed:

# Find rootIssueStore definition
rg -n "class.*RootIssueStore|projectIssues.*=" apps/web/core/store/issue/issue-details/root.store.ts -A 3 | head -40

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Search for projectIssues store file
fd -name "*project*issues*" -o -name "*root*store*" | grep -E "store|issue" | head -10

Repository: makeplane/plane

Length of output: 289


🏁 Script executed:

# Find the actual API service that sends the payload
rg -n "class.*ProjectIssues|updateIssue.*async" apps/web/core/store -A 5 | grep -A 5 "updateIssue"

Repository: makeplane/plane

Length of output: 2750


🏁 Script executed:

# Find BaseIssuesStore which likely has the actual updateIssue implementation
fd "base*issues*" -type f

Repository: makeplane/plane

Length of output: 229


🏁 Script executed:

# Search for the store file that contains updateIssue with API call
rg "updateIssue.*service\.|issueService\.update" --type-add 'web:tsx,ts,js' -B 3 -A 8

Repository: makeplane/plane

Length of output: 3137


🏁 Script executed:

# Look for where the data payload is actually sent (likely in a service file)
rg "PUT.*issue|issueService" apps/web/core/store -A 5 | head -60

Repository: makeplane/plane

Length of output: 4408


🏁 Script executed:

# Find IssueService definition
fd "issue.*service" -type f | head -5

Repository: makeplane/plane

Length of output: 229


🏁 Script executed:

# Search for IssueService class and updateIssue method
rg "class IssueService|updateIssue.*async" -A 10 | head -100

Repository: makeplane/plane

Length of output: 9263


🏁 Script executed:

# Get IssueService.updateIssue method
cat apps/web/core/services/issue/issue.service.ts | grep -n "updateIssue" -A 15 | head -50

Repository: makeplane/plane

Length of output: 1882


🏁 Script executed:

# Get the full IssueService file to find updateIssue method
wc -l apps/web/core/services/issue/issue.service.ts

Repository: makeplane/plane

Length of output: 108


🏁 Script executed:

# Search more specifically for updateIssue pattern
rg "updateIssue.*\(" apps/web/core/services/issue/issue.service.ts -A 10

Repository: makeplane/plane

Length of output: 1236


🏁 Script executed:

# Look at the full IssueService to see all methods
cat -n apps/web/core/services/issue/issue.service.ts | head -100

Repository: makeplane/plane

Length of output: 3673


🏁 Script executed:

# Search for BaseIssuesStore which extends or contains projectIssues
rg "class.*BaseIssues|updateIssue.*=" apps/web/core/store/issue/helpers/base-issues.store.ts -A 8 | head -100

Repository: makeplane/plane

Length of output: 390


🏁 Script executed:

# Search for updateIssue in BaseIssuesStore
rg "updateIssue.*=" apps/web/core/store/issue/helpers/base-issues.store.ts -A 20 | head -80

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Also check the size and search for issueUpdate which might be the method name
wc -l apps/web/core/store/issue/helpers/base-issues.store.ts

Repository: makeplane/plane

Length of output: 118


🏁 Script executed:

# Search for issueUpdate method
rg "issueUpdate\s*=" apps/web/core/store/issue/helpers/base-issues.store.ts -A 30 | head -100

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Also search for PATCH or patchIssue calls
rg "patchIssue\|issueService\.patch" apps/web/core/store/issue/helpers/base-issues.store.ts -B 3 -A 8 | head -80

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Find ProjectIssues store definition
fd "project.*issues.*store" -type f

Repository: makeplane/plane

Length of output: 229


🏁 Script executed:

# Search for class ProjectIssues
rg "class ProjectIssues" apps/web/core/store -A 30

Repository: makeplane/plane

Length of output: 4816


🏁 Script executed:

# Look at the root store to see how projectIssues is initialized
rg "projectIssues" apps/web/core/store/issue/root-issue-store.ts -B 2 -A 5 | head -50

Repository: makeplane/plane

Length of output: 144


🏁 Script executed:

# Search BaseIssuesStore for all methods containing "update" or "issue"
rg "^\s*(updateIssue|issueUpdate)\s*=" apps/web/core/store/issue/helpers/base-issues.store.ts -A 25

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# More broad search - look for methods that call issueService
rg "this\.issueService\." apps/web/core/store/issue/helpers/base-issues.store.ts | head -20

Repository: makeplane/plane

Length of output: 1054


Field name and type mismatch: frontend sends no_activity: "true" but backend expects skip_activity as boolean

The code sends no_activity: "true" (string) to the backend, but the API expects skip_activity (boolean). Backend code in apps/api/plane/app/views/issue/base.py:615 explicitly pops skip_activity from the request payload:

skip_activity = request.data.pop("skip_activity", False)

The no_activity field will be silently ignored by the backend, and the migration flag won't suppress activity logging. Change the payload to:

...(isMigrationUpdate ? { skip_activity: true } : {})

This affects three files: peek-overview/issue-detail.tsx, issue-detail/main-content.tsx, and inbox/content/issue-root.tsx.

🤖 Prompt for AI Agents
In apps/web/core/components/issues/peek-overview/issue-detail.tsx around lines
137-142 (and likewise update issue-detail/main-content.tsx and
inbox/content/issue-root.tsx), the payload uses no_activity: "true" (a string)
which the backend ignores; change the migration flag to send skip_activity: true
(a boolean) when isMigrationUpdate is true so the backend's
request.data.pop("skip_activity", False) receives the correct boolean field and
suppresses activity logging.

}}
setIsSubmitting={(value) => setIsSubmitting(value)}
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/core/extensions/unique-id/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const createIdsForView = (view: EditorView, options: UniqueIDOptions) =>
});

tr.setMeta("addToHistory", false);
tr.setMeta("uniqueIdOnlyChange", true);

view.dispatch(tr);
};
6 changes: 5 additions & 1 deletion packages/editor/src/core/hooks/use-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/core/types/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid TypeScript syntax for optional parameter. The destructured parameter syntax is incorrect. The correct syntax for an optional third parameter should be:

onChange?: (json: object, html: string, options?: { isMigrationUpdate?: boolean }) => void;

The current syntax { isMigrationUpdate }?: { isMigrationUpdate?: boolean } is not valid TypeScript.

Suggested change
onChange?: (json: object, html: string, { isMigrationUpdate }?: { isMigrationUpdate?: boolean }) => void;
onChange?: (json: object, html: string, options?: { isMigrationUpdate?: boolean }) => void;

Copilot uses AI. Check for mistakes.
onEnterKeyPress?: (e?: any) => void;
onTransaction?: () => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
Expand Down
Loading