Skip to content

UN-2649 [FEAT] Worklfow sharing - #1462

Merged
ritwik-g merged 26 commits into
mainfrom
UN-2649-add-sharing-feature-for-workflows
Oct 1, 2025
Merged

UN-2649 [FEAT] Worklfow sharing #1462
ritwik-g merged 26 commits into
mainfrom
UN-2649-add-sharing-feature-for-workflows

Conversation

@johnyrahul

@johnyrahul johnyrahul commented Jul 30, 2025

Copy link
Copy Markdown
Contributor

What

This PR implements workflow sharing functionality that enables users to share workflows with users in the organization. The feature adds:

  • Database models to track workflow sharing (shared_to_org and shared_users fields)
  • API endpoints for managing workflow sharing permissions
  • UI components for sharing workflows in the frontend

Why

Currently, workflows are isolated to individual users, limiting collaboration within organizations. This feature enables team collaboration by allowing workflow creators to share their workflows with:

  • Specific users within the organization

How

Backend Changes:

  • Added shared_to_org (BooleanField) and shared_users (ManyToManyField) to the Workflow model
  • Created migration 0016_workflow_shared_to_org_workflow_shared_users.py for database schema updates
  • Extended workflow serializers to handle sharing fields
  • Added new API endpoints in workflow_v2/urls/workflow.py for sharing operations
  • Updated views to handle sharing permissions and access control
  • Implemented notification system for sending emails when workflows are shared

Frontend Changes:

  • Updated Workflows.jsx component to display sharing UI
  • Extended workflow-service.js with API calls for sharing functionality
  • Added UI controls for managing workflow sharing settings

Can this PR break any existing features. If yes, please list possible items. If no, please explain why.

No, this PR should not break existing features because:

  • The new fields are optional and have default values (shared_to_org defaults to False, shared_users is an empty ManyToMany)
  • Existing workflows will continue to work as private by default
  • The changes are additive and don't modify existing workflow execution logic
  • API endpoints are new and don't change existing endpoint behavior

Database Migrations

  • Migration file: 0016_workflow_shared_to_org_workflow_shared_users.py
  • Adds shared_to_org BooleanField (default=False)
  • Adds shared_users ManyToManyField to User model
  • Migration is backwards compatible

Env Config

No new environment variables required. The notification system uses existing email configuration.

Relevant Docs

  • Workflow sharing feature documentation should be added to the user guide
  • API documentation needs to be updated with new sharing endpoints

Related Issues or PRs

  • Issue: UN-2649 (Add sharing feature for workflows)

Dependencies Versions

No new dependencies added. Uses existing Django and React frameworks.

Notes on Testing

To test this feature:

  1. Create a workflow as a user within an organization
  2. Navigate to the workflow settings/sharing section
  3. Test sharing with organization (toggle shared_to_org)
  4. Test sharing with specific users (add users to shared_users list)
  5. Verify that shared workflows appear for the intended recipients
  6. Confirm email notifications are sent when workflows are shared
  7. Test access control - ensure non-shared users cannot access the workflow

Screenshots

image

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

Implement workflow sharing feature allowing users to share workflows with organization members and specific users.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 30, 2025

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Share workflows with specific users or your entire organization.
    • View who has access to a workflow via a new “Users” listing.
    • Manage sharing from the workflow list with a Share dialog (select users, toggle org-wide access).
    • Receive sharing notifications when supported by installed plugins.
  • Bug Fixes

    • Prevented errors when a workflow has no creator info.
    • Clearer error messages for update and delete failures.
  • Chores

    • Ignored notification plugin directory in version control.
    • Formatting cleanups in backend configuration files.

Walkthrough

Adds workflow-sharing: model fields and migration, manager filtering, serializers, viewset permission and endpoint changes (list shared users), optional notification hook, frontend UI/service for sharing, plus .gitignore and pyproject comment formatting.

Changes

Cohort / File(s) Summary
VCS ignore
.gitignore
Added ignore rule for backend/plugins/notification/** with a header comment and a trailing blank line.
Backend config formatting
backend/pyproject.toml
Whitespace/comment formatting adjusted on dependency lines only; no functional or version changes.
Workflow model & migration
backend/workflow_manager/workflow_v2/models/workflow.py, backend/workflow_manager/workflow_v2/migrations/0017_workflow_shared_to_org_workflow_shared_users.py
Added shared_users: ManyToManyField(User, related_name="shared_workflows", blank=True) and shared_to_org: BooleanField(default=False, db_comment="Whether this workflow is shared with the entire organization"); added WorkflowModelManager.for_user(user) to return workflows created by or shared with the user; migration 0017 creates the new fields/relations.
Workflow serializers
backend/workflow_manager/workflow_v2/serializers.py
Added SharedUserListSerializer using SerializerMethodField; made created_by_email safer; added methods to expose shared users and creator info.
Workflow views & URLs
backend/workflow_manager/workflow_v2/views.py, backend/workflow_manager/workflow_v2/urls/workflow.py
Replaced static permission_classes with get_permissions; viewset queryset uses for_user(request.user); added partial_update override to trigger optional sharing notifications when shared_users changes (guarded by plugin availability); added list_of_shared_users action exposed at /<uuid:pk>/users/.
Frontend UI
frontend/src/components/workflows/workflow/Workflows.jsx
Added share UI state and handlers (handleShare, onShare), parallel fetch for all users and shared users, lazy-loaded SharePermission dialog, and improved error messages.
Frontend service
frontend/src/components/workflows/workflow/workflow-service.js
Added API methods: getSharedUsers(id), updateSharing(id, sharedUsers, shareWithEveryone), and getAllUsers() (axiosPrivate requests; PATCH includes CSRF header).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant FE as Frontend (Workflows.jsx)
  participant S as workflowService
  participant API as Backend API
  participant VS as WorkflowViewSet
  participant DB as Database
  participant NP as Notification Plugin

  rect rgb(240,248,255)
  note over User,FE: Open Share dialog
  User->>FE: click "Share"
  FE->>S: getAllUsers() + getSharedUsers(id) (parallel)
  par Fetch all users
    S->>API: GET /users/
    API->>DB: query users
    DB-->>API: users
    API-->>S: users payload
  and Fetch shared users
    S->>API: GET /workflow/{id}/users/
    API->>VS: list_of_shared_users
    VS->>DB: load workflow + shared_users
    DB-->>VS: workflow + shared_users
    VS-->>API: serialized shared info
    API-->>S: shared users payload
  end
  S-->>FE: combined payload
  FE-->>User: render share dialog
  end

  rect rgb(245,255,245)
  note over User,FE: Apply sharing changes
  User->>FE: submit selected users / shareToOrg
  FE->>S: updateSharing(id, users, shareToOrg)
  S->>API: PATCH /workflow/{id}/ {shared_users, shared_to_org}
  API->>VS: partial_update
  VS->>DB: save workflow + M2M updates
  DB-->>VS: saved
  alt Notification plugin available
    VS->>NP: send_sharing_notification(newly_shared)
    NP-->>VS: ack/failure (ignored)
  else Plugin unavailable
    note right of VS: notifications skipped
  end
  VS-->>API: 200 updated
  API-->>S: 200
  S-->>FE: success
  FE-->>User: show success, refresh list
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks (1 passed, 1 warning, 1 inconclusive)

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ❓ Inconclusive The PR description largely follows the repository template and includes What, Why, How, Database Migrations, Env Config, testing steps, screenshots, and a checklist, so it is mostly complete and informative. However there are factual inconsistencies that need clarification: the description references migration "0016_workflow_shared_to_org_workflow_shared_users.py" while the changeset shows a migration named/numbered 0017, and the description states email notifications are implemented while the backend code appears to send notifications conditionally via an optional plugin. Because of these discrepancies I cannot conclusively verify the description's accuracy without the author confirming the correct migration number and the exact notification behavior. Please update the PR description to match the actual migration file included in the branch (confirm whether it is 0016 or 0017 and use the exact filename), and clarify the notification implementation (state whether emails are always sent or only when the notification plugin/service is available). After those corrections the description will be consistent with the code changes and can be re-evaluated.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title "UN-2649 [FEAT] Worklfow sharing" accurately references the primary change (adding workflow sharing) and is concise, but it contains a typo ("Worklfow" → "Workflow") and could be slightly cleaned for consistency with repository title conventions; keeping the ticket ID is helpful but the [FEAT] tag may be redundant depending on your project's style. Overall the title is related to the changeset and communicates the main intent, but it should be corrected for professionalism and clarity before merge.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly identifies the main purpose of the changeset—adding workflow sharing functionality—and includes the relevant ticket identifier and feature flag, making it concise and directly related to the primary change. It effectively communicates the core update without extraneous details. However, there is a minor typo in “Worklfow” that could be corrected for clarity.
Description Check ✅ Passed The pull request description fully adheres to the repository’s template by providing detailed content under each required heading—including What, Why, How, impact on existing features, database migrations, environment configuration, documentation notes, related issues, dependency versions, testing guidance, screenshots, and the checklist—ensuring that all sections are present and appropriately filled out.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch UN-2649-add-sharing-feature-for-workflows

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between ed76bdc and 5a2ba07.

📒 Files selected for processing (1)
  • backend/pyproject.toml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
backend/pyproject.toml (1)

34-37: Comment formatting looks good

The tightened inline comments keep the dependency block tidy without affecting behavior. 👍


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

johnyrahul and others added 8 commits July 30, 2025 15:20
- Add permission check for partial_update (PATCH) in WorkflowViewSet
- Use handleException for better API error messages in workflow delete and sharing operations
- Recreate migration 0016 to combine shared_to_org and shared_users fields

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Install SendGrid Python SDK (v6.12.4) using uv
- Add SendGrid configuration to Django settings
- Create reusable email service with dynamic template support
- Implement sharing notification service for workflows and text extractors
- Add email notification constants and custom exceptions
- Update workflow and text extractor views to send notifications
- Configure environment variables for SendGrid integration
- Generate proper organization-aware URLs for shared resources

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ful fallback

- Move notification files from utils/notification to plugins/notification
- Follow established plugin architecture pattern for consistency
- Add graceful import handling in workflow views for optional notifications
- Update documentation to reflect new plugin structure
- Maintain backward compatibility with direct instantiation pattern
- Clean up old notification files and imports

The notification plugin now degrades gracefully when unavailable, allowing
core workflow sharing functionality to continue without email notifications.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove SendGrid from pyproject.toml dependencies
- Update uv.lock to reflect dependency removal
- SendGrid is now an optional plugin dependency

The notification plugin can optionally install SendGrid when needed,
making it truly optional for the core application.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…m:Zipstack/unstract into UN-2649-add-sharing-feature-for-workflows
@johnyrahul johnyrahul changed the title UN-2649 [FEATURE] Add workflow sharing functionality feat: Add workflow sharing functionality with organization members and specific users Aug 31, 2025
@johnyrahul johnyrahul changed the title feat: Add workflow sharing functionality with organization members and specific users UN-2649 [FEAT] Worklfow sharing Sep 2, 2025
@johnyrahul
johnyrahul marked this pull request as ready for review September 2, 2025 04:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (10)
.gitignore (1)

628-630: Fix typo and align plugin ignore pattern (optional).

  • Typo: "Notfication" → "Notification".
  • Optional: If you want to keep a sample plugin tracked (like auth), mirror the allowlist pattern.

Apply:

-# Notfication plugins
+# Notification plugins
 backend/plugins/notification/**

If you plan to keep a sample folder:

-backend/plugins/notification/**
+backend/plugins/notification/*
+!backend/plugins/notification/notification_sample
backend/workflow_manager/workflow_v2/models/workflow.py (1)

85-93: Add an index for shared_to_org (optional performance).

Filtering by org and shared_to_org will be common; a boolean index (or composite on organization, shared_to_org) can help on large tables.

Model change:

-    shared_to_org = models.BooleanField(
-        default=False,
-        db_comment="Whether this workflow is shared with the entire organization",
-    )
+    shared_to_org = models.BooleanField(
+        default=False,
+        db_index=True,
+        db_comment="Whether this workflow is shared with the entire organization",
+    )

Follow with a migration. If queries commonly use both fields, prefer:

class Meta:
    indexes = [
        models.Index(fields=["organization", "shared_to_org"]),
    ]
backend/workflow_manager/workflow_v2/serializers.py (3)

48-61: Avoid redundant creator fields in the API

You now expose both created_by (full relation in "all") and created_by_email. If both are consumed, fine; otherwise consider deprecating created_by_email in favor of a consistent object shape (id, email) to reduce duplication.


132-150: Reduce DB work and add light typing in method fields

Using SerializerMethodField here is fine, but minor tweaks will trim allocations and clarify intent.

  • Pull only needed columns (id, email) without instantiating User models.
  • Add return type hints for readability.

Apply:

 class SharedUserListSerializer(ModelSerializer):
@@
-    def get_shared_users(self, obj):
-        """Return list of shared users with id and email."""
-        return [{"id": user.id, "email": user.email} for user in obj.shared_users.all()]
+    def get_shared_users(self, obj: Workflow) -> list[dict]:
+        """Return list of shared users with id and email."""
+        return list(obj.shared_users.values("id", "email"))
@@
-    def get_created_by(self, obj):
-        """Return creator details."""
-        if obj.created_by:
-            return {"id": obj.created_by.id, "email": obj.created_by.email}
-        return None
+    def get_created_by(self, obj: Workflow) -> dict | None:
+        """Return creator details."""
+        if obj.created_by_id:
+            return {"id": obj.created_by_id, "email": obj.created_by.email}
+        return None

132-150: Ensure org-scoped sharing and prefetch in the view

Confirm the view that serves/updates sharing:

  • Enforces that all shared_users belong to the request.user’s organization (defense-in-depth; serializer-level validate_shared_users is ideal).
  • Uses select_related("created_by") and prefetch_related("shared_users") to avoid N+1 when listing.

I can help add serializer-side validation if desired.

Example (outside this file, for the ViewSet action):

qs = (
    Workflow.objects
    .select_related("created_by")
    .prefetch_related("shared_users")
    .filter(pk=pk, org=request.user.org)
)

Optional serializer guard (in WorkflowSerializer):

def validate_shared_users(self, users):
    request = self.context.get("request")
    if not request:
        return users
    invalid = [u for u in users if u.org_id != request.user.org_id]
    if invalid:
        raise ValidationError("All shared users must be from your organization.")
    return users
frontend/src/components/workflows/workflow/workflow-service.js (2)

85-98: Normalize payload to user IDs before PATCH

Backend serializers typically expect a list of user IDs for ManyToMany updates. To be robust against callers passing objects, normalize here.

     updateSharing: (id, sharedUsers, shareWithEveryone) => {
-      options = {
+      const normalizedUserIds = Array.isArray(sharedUsers)
+        ? sharedUsers.map((u) => (u && typeof u === "object" ? u.id : u))
+        : [];
+      options = {
         url: `${path}/workflow/${id}/`,
         method: "PATCH",
         headers: {
           "X-CSRFToken": csrfToken,
         },
         data: {
-          shared_users: sharedUsers,
+          shared_users: normalizedUserIds,
           shared_to_org: shareWithEveryone,
         },
       };
       return axiosPrivate(options);
     },

78-84: Avoid mutating a shared options object

Using a file-scoped mutable options can cause accidental leakage between calls. Prefer per-call const.

-      options = {
+      const options = {
         url: `${path}/workflow/${id}/users/`,
         method: "GET",
       };

Repeat similarly for updateSharing and getAllUsers.

Also applies to: 85-98, 99-105

frontend/src/components/workflows/workflow/Workflows.jsx (1)

185-215: Remove redundant selectedWorkflow set and harden users shape

You setSelectedWorkflow(workflow) and overwrite it after fetching. Remove the first to avoid flicker. Also, consider guarding for different response shapes.

-  const handleShare = async (event, workflow, isEdit) => {
+  const handleShare = async (event, workflow, isEdit) => {
     event.stopPropagation();
-    setSelectedWorkflow(workflow);
     setSharePermissionEdit(isEdit);
     setShareLoading(true);
@@
-      const userList =
-        usersResponse?.data?.members?.map((member) => ({
+      const members = usersResponse?.data?.members
+        ?? usersResponse?.data?.results
+        ?? usersResponse?.data
+        ?? [];
+      const userList = members.map((member) => ({
           id: member.id,
           email: member.email,
-        })) || [];
+      })) || [];
backend/workflow_manager/workflow_v2/views.py (2)

16-25: Broaden exception handling to gracefully disable notifications on any init/import failure.

Catching only ImportError misses runtime errors during service instantiation.

-try:
-    from plugins.notification.constants import ResourceType
-    from plugins.notification.sharing_notification import SharingNotificationService
-
-    NOTIFICATION_PLUGIN_AVAILABLE = True
-    sharing_notification_service = SharingNotificationService()
-except ImportError:
-    NOTIFICATION_PLUGIN_AVAILABLE = False
-    sharing_notification_service = None
+try:
+    from plugins.notification.constants import ResourceType
+    from plugins.notification.sharing_notification import SharingNotificationService
+    sharing_notification_service = SharingNotificationService()
+    NOTIFICATION_PLUGIN_AVAILABLE = True
+except Exception:
+    NOTIFICATION_PLUGIN_AVAILABLE = False
+    sharing_notification_service = None

77-82: Avoid potential duplicates and minor simplification.

If for_user() uses OR filters, duplicates can appear; also the if/else can be simplified.

-        queryset = (
-            Workflow.objects.for_user(self.request.user).filter(**filter_args)
-            if filter_args
-            else Workflow.objects.for_user(self.request.user)
-        )
+        qs = Workflow.objects.for_user(self.request.user)
+        if filter_args:
+            qs = qs.filter(**filter_args)
+        queryset = qs.distinct()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between cb88b73 and 38204f5.

📒 Files selected for processing (9)
  • .gitignore (1 hunks)
  • backend/pyproject.toml (2 hunks)
  • backend/workflow_manager/workflow_v2/migrations/0016_workflow_shared_to_org_workflow_shared_users.py (1 hunks)
  • backend/workflow_manager/workflow_v2/models/workflow.py (2 hunks)
  • backend/workflow_manager/workflow_v2/serializers.py (3 hunks)
  • backend/workflow_manager/workflow_v2/urls/workflow.py (2 hunks)
  • backend/workflow_manager/workflow_v2/views.py (7 hunks)
  • frontend/src/components/workflows/workflow/Workflows.jsx (5 hunks)
  • frontend/src/components/workflows/workflow/workflow-service.js (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (11)
backend/workflow_manager/workflow_v2/migrations/0016_workflow_shared_to_org_workflow_shared_users.py (1)

1-30: Migration LGTM.

  • Correct swappable dependency and additive fields.
  • Backward compatible with default=False and empty M2M.
backend/workflow_manager/workflow_v2/urls/workflow.py (1)

26-26: Permissions enforcement correct for list_of_shared_users
get_permissions returns IsOwnerOrSharedUser for this action and the view’s get_object() is scoped via Workflow.objects.for_user(self.request.user), so only owners or users already granted access can call this endpoint; the serializer only returns obj.shared_users, preventing any cross-org email leakage.

backend/workflow_manager/workflow_v2/serializers.py (1)

58-60: Null-safe created_by_email addition — LGTM

Safely handles missing creators and prevents AttributeError. Matches frontend expectations for a flat email field.

frontend/src/components/workflows/workflow/workflow-service.js (2)

78-84: Shared users fetch — LGTM

Endpoint wiring and method shape are consistent with the rest of the service.


99-105: Confirm users endpoint response shape

Code that consumes this expects data.members[]. If the API returns another shape (e.g., results or plain list), this will break. Please confirm and align.

frontend/src/components/workflows/workflow/Workflows.jsx (3)

57-62: State additions for sharing — LGTM

Clear, minimal state to drive the SharePermission UI.


167-171: Better error surfacing on delete — LGTM

Hooking into handleException improves UX consistency.


337-351: Lazy-loaded SharePermission wiring — LGTM

Props cover all needed inputs; nice to keep bundle size lean.

backend/workflow_manager/workflow_v2/views.py (3)

46-46: LGTM: serializer import for the new endpoint is correct.


336-341: LGTM: shared users endpoint.

Serializer usage is correct; permission falls back to IsOwnerOrSharedUser.


6-6: Keep existing import; the suggested path is invalid.
Permission classes live in backend/permissions/permission.py — there is no workflow_manager/workflow_v2/permissions module. The current import (from permissions.permission import …) is correct.

Likely an incorrect or invalid review comment.

Comment thread backend/pyproject.toml
Comment thread backend/workflow_manager/workflow_v2/models/workflow.py
Comment thread backend/workflow_manager/workflow_v2/views.py
Comment thread backend/workflow_manager/workflow_v2/views.py
Comment thread frontend/src/components/workflows/workflow/Workflows.jsx
Comment thread backend/workflow_manager/workflow_v2/serializers.py
Comment thread backend/workflow_manager/workflow_v2/views.py
Comment thread backend/workflow_manager/workflow_v2/views.py Outdated
Comment thread backend/workflow_manager/workflow_v2/views.py Outdated
johnyrahul and others added 3 commits September 3, 2025 12:14
Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com>
Signed-off-by: Rahul Johny <116638720+johnyrahul@users.noreply.github.com>
Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com>
Signed-off-by: Rahul Johny <116638720+johnyrahul@users.noreply.github.com>

@vishnuszipstack vishnuszipstack left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@johnyrahul LGTM. remove the console errors in FE

Comment thread frontend/src/components/workflows/workflow/Workflows.jsx Outdated
Comment thread frontend/src/components/workflows/workflow/Workflows.jsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
frontend/src/components/workflows/workflow/Workflows.jsx (2)

81-83: Replace console.error with surfaced alert

Surface the failure to the user, consistent with other catch paths.

-      .catch(() => {
-        console.error("Unable to get project list");
-      });
+      .catch((err) => {
+        setAlertDetails(handleException(err, "Unable to get project list"));
+      });

216-237: Send only user IDs to backend (prevents 400s) and dedupe

Map selections to IDs and remove duplicates before calling updateSharing.

-  const onShare = async (selectedUsers, workflow, shareWithEveryone) => {
+  const onShare = async (selectedUsers, workflow, shareWithEveryone) => {
     setShareLoading(true);
     try {
-      await projectApiService.updateSharing(
-        workflow.id,
-        selectedUsers,
-        shareWithEveryone
-      );
+      const userIds = Array.isArray(selectedUsers)
+        ? Array.from(
+            new Set(
+              selectedUsers.map((u) => (u && typeof u === "object" ? u.id : u))
+            )
+          )
+        : [];
+      await projectApiService.updateSharing(workflow.id, userIds, shareWithEveryone);
🧹 Nitpick comments (1)
frontend/src/components/workflows/workflow/Workflows.jsx (1)

335-349: Clear adapter on modal close to avoid stale selection

Prevents reopening the modal with previous selection when switching items.

-              setOpen={setShareOpen}
+              setOpen={(open) => {
+                setShareOpen(open);
+                if (!open) setSelectedWorkflow(undefined);
+              }}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 992646b and 807a3dd.

📒 Files selected for processing (1)
  • frontend/src/components/workflows/workflow/Workflows.jsx (5 hunks)
🔇 Additional comments (3)
frontend/src/components/workflows/workflow/Workflows.jsx (3)

57-62: Sharing UI state wiring looks good

State shape and defaults make sense for the SharePermission flow.


313-314: Verify ViewTools passes the event argument to handleShare

handleShare relies on an event (now guarded), but confirm ViewTools’ callback signature to avoid surprises.


167-171: handleException returns an alert object and doesn’t dispatch alerts itself
useExceptionHandler’s handleException implementation always returns an alert-like object and never calls setAlertDetails internally, so wrapping its result in setAlertDetails is correct.

Likely an incorrect or invalid review comment.

Comment thread frontend/src/components/workflows/workflow/Workflows.jsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
backend/workflow_manager/workflow_v2/migrations/0017_workflow_shared_to_org_workflow_shared_users.py (1)

7-29: Silence Ruff RUF012 for migrations instead of editing generated files.

Ruff flags mutable class attrs (dependencies/operations). Prefer per-file ignore for migrations to avoid churn in auto-generated code.

Add to pyproject.toml:

 [tool.ruff]
 target-version = "py311"
 
+[tool.ruff.lint]
+per-file-ignores = { "*/migrations/*.py" = ["RUF012"] }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 363190e and 2406762.

📒 Files selected for processing (1)
  • backend/workflow_manager/workflow_v2/migrations/0017_workflow_shared_to_org_workflow_shared_users.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.12.2)
backend/workflow_manager/workflow_v2/migrations/0017_workflow_shared_to_org_workflow_shared_users.py

8-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


13-29: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
backend/workflow_manager/workflow_v2/migrations/0017_workflow_shared_to_org_workflow_shared_users.py (3)

8-11: Confirm migration numbering & update PR/docs

backend/workflow_manager/workflow_v2/migrations/0017_workflow_shared_to_org_workflow_shared_users.py depends on ('workflow_v2','0016_add_filehistory_deletion_index') (line 10); no migration named 0016_workflow_shared_to_org_workflow_shared_users was found. Update PR text/screenshots to reference 0017 and confirm the migration chain is linear.


14-21: Add a DB index on Workflow.shared_to_org if you query by it

Repo search shows shared_to_org is used in other models but no code explicitly filters the Workflow model by this flag; add db_index=True in the migration if list queries will commonly filter on Workflow.shared_to_org. If True is rare, prefer a partial index (WHERE shared_to_org = TRUE) in a follow-up migration.

Apply:

-            field=models.BooleanField(
+            field=models.BooleanField(
                 db_comment="Whether this workflow is shared with the entire organization",
                 default=False,
+                db_index=True,
             ),

Optional partial-index (follow-up migration):

migrations.AddIndex(
    model_name="workflow",
    index=models.Index(
        name="wf_shared_true_idx",
        fields=[],  # leave empty when using condition-only
        condition=models.Q(shared_to_org=True),
    ),
)

22-28: Enforce org-only sharing in serializer/viewset

DB M2M can’t express workflow.org == user.org. I did not find server-side validation preventing cross-org users from being added to Workflow.shared_users — update the code that mutates sharing to reject users outside the workflow’s organization and add tests. Key places to add the check:

  • backend/workflow_manager/workflow_v2/models/workflow.py (shared_users M2M)
  • backend/workflow_manager/workflow_v2/views.py (partial_update handling / notification flow)
  • backend/workflow_manager/workflow_v2/serializers.py (WorkflowSerializer — validate/update shared_users)

Minimal checks:

  • Validate each user belongs to the workflow’s org before saving.
  • Unit test: attempt to add a user from another org → expect 400/403.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
frontend/src/components/workflows/workflow/Workflows.jsx (3)

189-190: Redundant state update for selectedWorkflow

The workflow is set twice - once at the beginning and again after fetching shared users data. This causes unnecessary re-renders.

   const handleShare = async (event, workflow, isEdit) => {
     event?.stopPropagation?.();
-    setSelectedWorkflow(workflow);
     setSharePermissionEdit(isEdit);
     setShareLoading(true);

     try {
       const [usersResponse, sharedUsersResponse] = await Promise.all([
         projectApiService.getAllUsers(),
         projectApiService.getSharedUsers(workflow.id),
       ]);

       const userList =
         usersResponse?.data?.members?.map((member) => ({
           id: member.id,
           email: member.email,
         })) || [];

       // Pass the complete user list - SharePermission component will handle filtering
       setAllUsers(userList);
-      setSelectedWorkflow(sharedUsersResponse.data);
+      // Merge workflow data with sharing information
+      setSelectedWorkflow({ ...workflow, ...sharedUsersResponse.data });
       setShareOpen(true);

Also applies to: 207-207


187-189: Guard event parameter to avoid potential errors

The event parameter should be guarded as it could be undefined in some cases.

   const handleShare = async (event, workflow, isEdit) => {
-    event.stopPropagation();
+    event?.stopPropagation?.();
     setSelectedWorkflow(workflow);

218-224: Map user objects to IDs before sending to backend

The selectedUsers parameter may contain user objects from the SharePermission component, which could cause 400 errors when sent to the backend API.

   const onShare = async (selectedUsers, workflow, shareWithEveryone) => {
     setShareLoading(true);
     try {
+      const userIds = Array.isArray(selectedUsers)
+        ? selectedUsers.map((u) => (u && typeof u === "object" ? u.id : u))
+        : [];
       await projectApiService.updateSharing(
         workflow.id,
-        selectedUsers,
+        userIds,
         shareWithEveryone
       );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 2406762 and 8744434.

📒 Files selected for processing (1)
  • frontend/src/components/workflows/workflow/Workflows.jsx (6 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
frontend/src/components/workflows/workflow/Workflows.jsx (2)

57-61: Well-structured state management for sharing feature

Good separation of concerns with distinct state variables for the sharing workflow. The implementation properly manages loading states and user data.


337-351: Clean integration of SharePermission component

The lazy loading of the SharePermission component is properly implemented with appropriate conditional rendering and prop passing.

Comment thread frontend/src/components/workflows/workflow/Workflows.jsx
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Rahul Johny <116638720+johnyrahul@users.noreply.github.com>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$

@ritwik-g
ritwik-g merged commit 407d9f5 into main Oct 1, 2025
5 of 6 checks passed
@ritwik-g
ritwik-g deleted the UN-2649-add-sharing-feature-for-workflows branch October 1, 2025 07:15
Deepak-Kesavan pushed a commit that referenced this pull request Oct 7, 2025
* UN-2649 [FEATURE] Add workflow sharing functionality

Implement workflow sharing feature allowing users to share workflows with organization members and specific users.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [FIX] Improve workflow sharing permissions and error handling

- Add permission check for partial_update (PATCH) in WorkflowViewSet
- Use handleException for better API error messages in workflow delete and sharing operations
- Recreate migration 0016 to combine shared_to_org and shared_users fields

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Add email notifications for resource sharing via SendGrid

- Install SendGrid Python SDK (v6.12.4) using uv
- Add SendGrid configuration to Django settings
- Create reusable email service with dynamic template support
- Implement sharing notification service for workflows and text extractors
- Add email notification constants and custom exceptions
- Update workflow and text extractor views to send notifications
- Configure environment variables for SendGrid integration
- Generate proper organization-aware URLs for shared resources

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Move notification system to plugins architecture with graceful fallback

- Move notification files from utils/notification to plugins/notification
- Follow established plugin architecture pattern for consistency
- Add graceful import handling in workflow views for optional notifications
- Update documentation to reflect new plugin structure
- Maintain backward compatibility with direct instantiation pattern
- Clean up old notification files and imports

The notification plugin now degrades gracefully when unavailable, allowing
core workflow sharing functionality to continue without email notifications.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore: Remove SendGrid dependency from core requirements

- Remove SendGrid from pyproject.toml dependencies
- Update uv.lock to reflect dependency removal
- SendGrid is now an optional plugin dependency

The notification plugin can optionally install SendGrid when needed,
making it truly optional for the core application.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update backend/workflow_manager/workflow_v2/views.py

Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com>
Signed-off-by: Rahul Johny <116638720+johnyrahul@users.noreply.github.com>

* Update backend/workflow_manager/workflow_v2/views.py

Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com>
Signed-off-by: Rahul Johny <116638720+johnyrahul@users.noreply.github.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Removed console logs

* Fixed docker build issue

* Updated the migration

* Handled the edit with proper alert

* Update frontend/src/components/workflows/workflow/Workflows.jsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Rahul Johny <116638720+johnyrahul@users.noreply.github.com>

---------

Signed-off-by: Rahul Johny <116638720+johnyrahul@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
chandrasekharan-zipstack added a commit that referenced this pull request Jul 31, 2026
… endpoints (#2208)

* UN-3770 [FIX] Make list pagination consistent across shared resource endpoints

#2187 added opt-in pagination to workflows, prompt studio, adapters and
connectors but only wired the Workflows page. The other three could not
follow: their `for_user()` managers used `DISTINCT ON`, which forces
Postgres to order by the distinct expression, so those viewsets were
pinned to `order_by("id")` and could not order by `modified_at`.

Backend
- Swap `.distinct("id")` / `.distinct("tool_id")` for plain `.distinct()`
  in the adapter, connector and prompt studio managers. Every arm of the
  sharing predicate is a PK subquery, not a join, so no duplicate rows
  exist to collapse and the swap is behaviour-preserving. Workflows has
  shipped this way since #1462.
- Replace the per-view `order_by()` calls with a declarative
  `ordering = ["-modified_at", "pk"]`. `OrderingFilter` is already in
  `DEFAULT_FILTER_BACKENDS`, so this needs no `filter_backends` override
  (which would drop `OrganizationFilterBackend`).
- Drop Workflow's `?order_by=asc|desc`; it has no consumer, and
  `?ordering=` now covers it through the standard filter.

Frontend
- Add `unwrapList` / `fetchAllPages` helpers. Selectors page to
  exhaustion rather than silently showing only the first 50 rows.
- Route all adapter, connector and workflow selectors through them.
- Convert the Prompt Studio, adapters and connectors pages to
  server-side pagination and search via `usePaginatedList`, replacing the
  client-side `useListSearch` filter (now deleted).
- Move `<Pagination>` into `ViewTools` so all four pages share one
  implementation: page size 10, size changer on, `["10","20","50"]`.
  Workflows had the changer disabled; that inconsistency goes away.
- Stop assigning `fetchListRef.current` during render in Workflows.

Endpoints stay opt-in paginated here, so every change is safe against
both response shapes. Flipping them to unconditional `CustomPagination`
is a separate change, after this has been validated on staging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [FIX] Address review: pk tie-breaker on client ordering, shared list hook

Backend:
- DeterministicOrderingFilter appends `pk` to whatever ordering is in
  effect. `?ordering=` replaces the view's `ordering` outright, so the
  tie-breaker has to be applied by the filter rather than declared on the
  view. Swapped into DEFAULT_FILTER_BACKENDS; a no-op for views that
  declare no ordering and receive no `?ordering=`.
- Tests assert the returned sequence rather than set membership, and pin
  the tie-breaker with rows sharing one `modified_at`. Both fail without
  the filter.

Frontend:
- usePaginatedResource owns the request for all four list pages: params,
  unwrapping, the empty-page step back, and the loading flag. Replaces
  four copy-pasted fetch functions (the Sonar duplication) and the
  fetchListRef indirection.
- Only the newest request may write state, so a slow response can no
  longer restore the previous page, search term or adapter type.
- The loading flag is held by the superseding request, so it no longer
  clears while the step-back page is still in flight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [MISC] Scope to backend + selectors; drop listing-page conversions

The Prompt Studio, adapters and connectors listing pages are being replaced
wholesale by the ResourceTable in #2200. Converting them here to
usePaginatedResource only to have that work overwritten created the entire
conflict surface between the two PRs, plus a second pagination hook.

Reverts the four listing-page conversions, the ViewTools pagination move and
the useListSearch deletion, and drops usePaginatedResource. What remains is
the part #2200 depends on and cannot do itself: the DISTINCT ON removal,
declarative ordering with a pk tie-breaker, and the selector page-following
that keeps dropdowns whole once pagination goes unconditional.

usePaginatedResource.test.js goes with the hook; its coverage should be
ported onto the surviving usePaginatedList in #2200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159NvRHFywvkNGji8ECQqeV

* UN-3769 [FEAT] Sortable resource lists with co-owner ownership (#2200)

* UN-3769 [FEAT] Sortable resource list table with server-side sort, search & pagination

Replace the sparse ListView/ViewTools list UI with a shared sortable
ResourceTable (Name / Owned By / Created Date / Actions) across Adapters,
Workflows, Prompt Studio and Connectors. The Owned By column shows the owner
avatar/name/email plus co-owner count and opens the co-owner modal.

Sort (name/owner/created via the header dropdowns), owner-inclusive search and
pagination are server-driven through a new apply_search_and_sort helper, whose
pk__in re-wrap lifts the Postgres DISTINCT ON each for_user() manager carries so
any column is orderable. Prompt Studio re-applies its prompt_count annotation
after the re-wrap. Delete the now-unused ListView and ViewTools.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3769 [FIX] Dedupe list fetch into shared helpers; fix stale-response race

Resolve SonarCloud/Greptile/CodeRabbit review on the resource-list rollout:

- Extract buildPagedParams + applyPagedResponse into usePaginatedList so the
  four list pages stop copy-pasting the params/response blocks (clears the
  SonarCloud new-code duplication gate).
- applyPagedResponse drops stale responses via a per-page sequence token so a
  slow older request can't overwrite a newer query, and returns the empty-page
  stepback refetch so loading isn't cleared before replacement data arrives.
- ResourceTable detects image icons by URL/data scheme instead of length, so
  compound (ZWJ) emoji no longer render as a broken <img>.
- list_query lowercases sort_by before the dict lookup (matches order handling).
- ToolSettings resets loading when a delete request fails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3769 [FIX] Collapse duplicated list-page preamble to clear duplication gate

The first review pass left SonarCloud at 8.4% new-code duplication; the real
duplicated blocks were the per-page preamble and the co-owner modal JSX, not
the fetch body. Fix both:

- usePaginatedList now owns fetchRef (pages assign fetchRef.current) and returns
  handleListRefresh, so pages drop their local fetchListRef + identical
  handleListRefresh useCallback.
- Add CoOwnerModal, a thin wrapper mapping a useCoOwnerManagement() bag +
  resourceType onto the CoOwnerManagement modal; the list pages now consume the
  hook as one object and render <CoOwnerModal .../> instead of repeating the
  11-prop invocation.

Net ~150 fewer lines; duplication drops well under the 3% gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3769 [FIX] Gate list-fetch catch/finally on the request sequence

The seq guard only suppressed stale successful responses; each page's catch and
finally still ran unconditionally, so a superseded request could clear loading
while a newer one was pending, or surface an error for a query the user had
already moved past. Gate both on seq === seqRef.current so only the newest
request owns the loading state and error reporting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3769 [FIX] Show a retryable error on list-fetch failure; use codePointAt

- On fetch failure the list pages set displayList to [], so a failed initial
  load rendered a misleading "No X available" empty state. Track an explicit
  loadError instead and render a retryable error (Retry refetches the current
  page), so a failure is no longer shown as an empty success.
- colorForSeed uses String#codePointAt over charCodeAt (SonarCloud S7758).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3769 [FIX] Track "Me" in the Owned By cell by displayed owner, not membership

is_owner is true for any OWNER membership, so a co-owner viewing a resource they
didn't create saw "Me" over the primary owner's avatar/email. Key the "Me" label
on the displayed owner email instead; the creator viewing their own resource
still reads "Me" via the email match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3769 [FIX] Gate delete-failure loading clear on the request sequence

The adapter delete catch cleared isLoading unconditionally, so a failed delete
could hide the spinner for a newer in-flight fetch (search/sort/paginate/refresh)
and expose obsolete results. Snapshot the request token at delete start and clear
loading only if no newer fetch has taken it over.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3769 [FIX] Keep adapter delete out of the shared list-loading state

ToolSettings was the only list driving the shared isLoading from a row delete,
which produced a string of overlap races (stuck loading, clobbering a newer
fetch, concurrent deletes clearing each other). Drop loading from the delete
entirely, matching the other four lists: success refetches via handleListRefresh
(which owns the spinner), failure just toasts. Removes the race class by
construction rather than adding another guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3769 [FIX] Refresh the current list view, not the params captured earlier

handleListRefresh closed over pagination/search/sort, so a refresh captured in a
pending mutation's .then (e.g. a delete) would refetch the stale page/search/order
and overwrite the view the user had since navigated to. Make it a stable callback
that reads the latest params from a ref, so post-mutation refresh always targets
the current view. Fixes it for every list's create/edit/import/delete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3769 [FIX] Fix list fetch state handling and dead pagination

Pipelines and API deployments still passed the removed `fetchData` option, so
the hook's `fetchRef` stayed null and their pagination and search were silent
no-ops. Both now assign `fetchRef` directly and drop their local `fetchListRef`.

Route every fetch (navigation, last-page stepback, adapter-type reset) through
`requestList`, so the recorded request params always match what lands on screen
and a post-mutation refresh replays the view the user actually asked for.

Realign those params with the displayed view when the newest request fails, so
a failed navigation can't leave a later refresh jumping to a page that never
loaded. Give the workflow edit modal its own loading flag so saving no longer
drives the shared list spinner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3769 [FIX] Address self-review on resource list views

- Owned By names a live owner: owner_email() on HasMembersMixin + 4 list
  serializers, instead of created_by which can be a removed creator.
- Retryable load error is reachable after the first load (gate loadError
  ahead of the length branches on all 4 pages).
- Workflows list-fetch failure surfaces via handleException, not a bare
  console.error.
- Correct usePaginatedList appliedRef comment; requestList returns the
  fetch promise so applyPagedResponse's documented stepback holds.
- Fix stale prompt_count Subquery rationale comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3769 [FIX] Match resource-table owner avatars to Figma pastel palette

Swap the saturated avatar swatches for light pastel fills paired with a
darker same-hue initial, matching the design. Applies to all resource
list views via the shared ResourceTable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3769 [FIX] Lighten resource-table owner-avatar initials per design

Lighten avatar initial color (Ant -7 -> -6) and reduce initial size
(12px -> 11px) per design review feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3769 [FEAT] Resource list: add Modified column + PS prompt count, default modified-desc sort

Add a sortable Modified column and rename Created Date -> Created so both dates are visible. Surface the already-serialized prompt_count as "Prompts: N" on the Prompt Studio list. Default all resource lists to modified-desc so the visible Modified column matches the sort (restores #2187 Workflows ordering). Frontend-only; backend already served both dates and prompt_count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3769 [FEAT] Resource list: relative Modified time, canonical date format, owner search-only

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3769 [FIX] Resource list review fixes: name-only search, clear-sort restores default

Address Chandru's re-review on #2200:
- Owned By is display-only: drop created_by__email from ordering_fields and
  the search Q-filter across the 4 viewsets so search matches the shown owner
  (name-only) instead of the creator, and no dead owner-sort surface remains.
- usePaginatedList: "Clear Sort" restores the default ordering (not an empty
  one) and list mounts request the seeded sort, so the header and rows agree.
- Remove dead code: orphaned useListSearch.js, the dead avatar-initials branch,
  and the unreferenced .listWrapper rule; trim over-narrated comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3769 [FIX] Tests: pin name-only search, dropped owner-ordering fallback, owner_email

Cover the review changes in the shared list-pagination contract test:
- ?search= matches the resource name only, not the owner's email.
- ?ordering=created_by__email is a dropped field -> ignored, list stays
  newest-first (had it survived, same-creator rows would be pk-ordered).
- owner_email() names the earliest live OWNER, skips service accounts, None
  with no owner (shared mixin, pinned once).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3770 [FIX] Address review: deterministic owner, keyboard row actions, payload guard

- owner_email(): break created_at ties by pk so the "Owned By" label is
  stable across requests; cover equal timestamps in the shared test.
- ResourceTable row actions rendered as non-focusable icon spans; wrap in
  real buttons so edit/share/delete are keyboard reachable and Popconfirm
  gets a focusable trigger.
- applyPagedResponse: guard non-array payloads (204 body, stray object)
  before they reach antd Table dataSource.
- Client-ordering test: two modified_at groups so honored ascending order
  is distinguishable from the -modified_at default, not just the pk tie.
- Drop redundant "# Name search." comments on the name-only search blocks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3770 [FIX] Use aria-disabled on row actions so the deprecated tooltip survives

A native disabled button suppresses hover, hiding the "deprecated" tooltip.
aria-disabled keeps the control focusable and hoverable; the onClick guard
already no-ops when deprecated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3770 [FIX] Polish resource list table: sort affordance + layout

- Don't highlight the default sort column on load; the header lights up
  only once the user explicitly picks a sort. usePaginatedList tracks a
  `userSorted` flag threaded to ResourceTable/SortHeader.
- Name column absorbs the slack while Owned By/Created/Modified/Actions
  share one compact fixed width, so they read as an evenly-spaced group
  and Name stays dominant.
- Owner name/email ellipsize within their cell (drop the 190px cap, let
  the Space item shrink), removing the trailing-gap skew.
- Table scrolls inside its own container below its min-width instead of
  crushing columns on narrow screens.
- Created timestamp gets ellipsis+tooltip as a safety for long values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [FEAT] Search resource lists by owner too, not just name

Owner search matches the displayed owner — the OWNER membership that backs
the Owned By column — via a search-time subquery (name OR owner email),
across all four shared list endpoints. Reuses the sharing_helpers
varchar/UUID object_id cast. `created_by` stays audit-only (UN-2202);
service accounts and non-owner (VIEWER) members are excluded.

This intentionally reverses the name-only narrowing from UN-3769: search
now agrees with what the Owned By column shows, which was that change's goal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [FEAT] Update search placeholder to "Search by name or owner"

The four owner-searchable list pages now advertise owner search via the
placeholder; ToolNavBar's other consumers keep the "Search by name" default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [FEAT] List all co-owners in the Owned By tooltip

Adds owner_emails() (all live OWNER emails, earliest-first) on HasMembersMixin,
exposed by the four list serializers. The Owned By cell still shows the primary
owner + `+N` inline, but its tooltip now names every co-owner — so a search that
matched a co-owner hidden behind `+N` is explainable on hover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [REFACTOR] Collapse owner_email into owner_emails

owner_email was owner_emails[0] — one derivable scalar of redundancy. Drop it
from the four list serializers and read owner_emails[0] on the frontend instead
(ResourceTable, and the cloud Projects card in the companion cloud PR). The
model's owner_email() accessor stays for callers/tests that want just the head.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [REVERT] Drop the table column-width/layout tweaks for now

Revert the layout half of the earlier "sort affordance + layout" change:
restore the proportional column widths (34/22/15/15/14%), the 190px owner
name/email cap, and drop the min-width scroll container + Created ellipsis.
The sort-affordance (userSorted) fix stays. Layout to be revisited later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [FEAT] ResourceTable: extraColumns + onRowClick (for lookups) (#2221)

UN-3770 [FEAT] ResourceTable: extraColumns + onRowClick override

Let callers inject resource-specific columns (inserted before Actions) and
override the default relative row-click nav — needed so the Prompt Studio
lookups table can reuse this widget while keeping its Files/Latest Version
columns and its absolute, stateful navigation.


Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: re-trigger pre-commit.ci (transient mergeable-check error)

* UN-3770 [FIX] Address review: owner fallback+mask, scoped tiebreaker, index, prefix search

- ResourceTable: fall back to created_by_email so rows with no live OWNER
  membership render the creator instead of "Unknown".
- Adapter serializer: mask owner_emails to ["Unstract"] for frictionless
  adapters so the Owned By column keeps the org-wide mask.
- Drop redundant .distinct() from adapter/connector/prompt_studio for_user
  (every arm is a PK subquery, no join, nothing to collapse).
- Add (organization, -modified_at) index to the 4 resource models backing the
  default list ordering.
- fetchAllPages: request MAX_PAGE_SIZE up front so the common case is one
  round-trip; the loop stays as the tail guard.
- Pin plain OrderingFilter on the two high-volume execution-log endpoints so
  they don't inherit the pk tiebreaker (unindexed Sort) from the global default.
- Owner search matches the email prefix (local part) so a bare domain fragment
  doesn't return every row in a single-domain org.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [FIX] Address Greptile: stable log pagination + drop stale list responses

Two P1 findings from Greptile:

- Execution-log endpoints (file_execution, execution_log_view) dropped their
  plain-OrderingFilter override and now inherit the global deterministic
  filter, so tied created_at/event_time rows can't repeat or omit across pages.
  Each request is already scoped to a single execution_id, so the pk
  tie-breaker sorts a narrow set, not the whole table.

- Pipelines and ApiDeployment list fetches adopt the monotonic seq guard via
  applyPagedResponse, so a slow superseded response can no longer overwrite a
  newer search/page/type selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

* UN-3770 [FIX] Route remaining paginated viewsets through deterministic ordering

Greptile P1: ExecutionViewSet still bypassed the global DeterministicOrderingFilter
with a plain OrderingFilter, so tied created_at rows could repeat or omit across
pages. Four other pre-existing paginated viewsets (tags, usage_v2, dashboard_metrics,
pipeline_v2) had the same override. Swapped each to DeterministicOrderingFilter,
appending the pk tie-breaker while keeping their existing backends unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Kirtiman Mishra <110175055+kirtimanmishrazipstack@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants