From 9f7321ca1ab384a13616712f506758a4b38755f1 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Fri, 22 Aug 2025 17:33:47 +0800 Subject: [PATCH] add create reply --- .../console/app/workflow_comment.py | 18 +++- api/fields/workflow_comment_fields.py | 6 +- ...08_22_0001-add_workflow_comments_tables.py | 78 ---------------- ...27822d22895_add_workflow_comments_table.py | 89 +++++++++++++++++++ api/models/comment.py | 5 ++ api/services/workflow_comment_service.py | 83 ++++++++++++++--- 6 files changed, 180 insertions(+), 99 deletions(-) delete mode 100644 api/migrations/versions/2025_08_22_0001-add_workflow_comments_tables.py create mode 100644 api/migrations/versions/2025_08_22_1726-227822d22895_add_workflow_comments_table.py diff --git a/api/controllers/console/app/workflow_comment.py b/api/controllers/console/app/workflow_comment.py index ea2c7699dc..e9004effb7 100644 --- a/api/controllers/console/app/workflow_comment.py +++ b/api/controllers/console/app/workflow_comment.py @@ -170,13 +170,17 @@ class WorkflowCommentReplyApi(Resource): parser = reqparse.RequestParser() parser.add_argument("content", type=str, required=True, location="json") + parser.add_argument("mentioned_user_ids", type=list, location="json", default=[]) args = parser.parse_args() - reply = WorkflowCommentService.create_reply( - comment_id=comment_id, content=args.content, created_by=current_user.id + result = WorkflowCommentService.create_reply( + comment_id=comment_id, + content=args.content, + created_by=current_user.id, + mentioned_user_ids=args.mentioned_user_ids ) - return reply, 201 + return result, 201 class WorkflowCommentReplyDetailApi(Resource): @@ -196,9 +200,15 @@ class WorkflowCommentReplyDetailApi(Resource): parser = reqparse.RequestParser() parser.add_argument("content", type=str, required=True, location="json") + parser.add_argument("mentioned_user_ids", type=list, location="json", default=[]) args = parser.parse_args() - reply = WorkflowCommentService.update_reply(reply_id=reply_id, user_id=current_user.id, content=args.content) + reply = WorkflowCommentService.update_reply( + reply_id=reply_id, + user_id=current_user.id, + content=args.content, + mentioned_user_ids=args.mentioned_user_ids + ) return reply diff --git a/api/fields/workflow_comment_fields.py b/api/fields/workflow_comment_fields.py index 7b1f0a08bc..33b1fc8067 100644 --- a/api/fields/workflow_comment_fields.py +++ b/api/fields/workflow_comment_fields.py @@ -9,6 +9,7 @@ comment_account_fields = {"id": fields.String, "name": fields.String, "email": f workflow_comment_mention_fields = { "mentioned_user_id": fields.String, "mentioned_user_account": fields.Nested(comment_account_fields, allow_null=True), + "reply_id": fields.String } # Comment reply fields @@ -97,12 +98,9 @@ workflow_comment_pagination_fields = { "limit": fields.Integer, } -# Reply creation response fields +# Reply creation response fields (simplified) workflow_comment_reply_create_fields = { "id": fields.String, - "content": fields.String, - "created_by": fields.String, - "created_by_account": fields.Nested(comment_account_fields, allow_null=True), "created_at": TimestampField, } diff --git a/api/migrations/versions/2025_08_22_0001-add_workflow_comments_tables.py b/api/migrations/versions/2025_08_22_0001-add_workflow_comments_tables.py deleted file mode 100644 index c3f4b56e75..0000000000 --- a/api/migrations/versions/2025_08_22_0001-add_workflow_comments_tables.py +++ /dev/null @@ -1,78 +0,0 @@ -"""add workflow comments tables - -Revision ID: add_workflow_comments_tables -Revises: 1c9ba48be8e4 -Create Date: 2025-08-22 00:00:00.000000 - -""" - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = 'add_workflow_comments_tables' -down_revision = '1c9ba48be8e4' -branch_labels: None = None -depends_on: None = None - - -def upgrade(): - # Create workflow_comments table - op.create_table( - 'workflow_comments', - sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('tenant_id', postgresql.UUID(), nullable=False), - sa.Column('app_id', postgresql.UUID(), nullable=False), - sa.Column('position_x', sa.Float(), nullable=False), - sa.Column('position_y', sa.Float(), nullable=False), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('created_by', postgresql.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('resolved', sa.Boolean(), server_default=sa.text('false'), nullable=False), - sa.Column('resolved_at', sa.DateTime(), nullable=True), - sa.Column('resolved_by', postgresql.UUID(), nullable=True), - sa.PrimaryKeyConstraint('id', name='workflow_comments_pkey') - ) - - # Create indexes for workflow_comments - op.create_index('workflow_comments_app_idx', 'workflow_comments', ['tenant_id', 'app_id']) - op.create_index('workflow_comments_created_at_idx', 'workflow_comments', ['created_at']) - - # Create workflow_comment_replies table - op.create_table( - 'workflow_comment_replies', - sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('comment_id', postgresql.UUID(), nullable=False), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('created_by', postgresql.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.ForeignKeyConstraint(['comment_id'], ['workflow_comments.id'], name='workflow_comment_replies_comment_id_fkey', ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name='workflow_comment_replies_pkey') - ) - - # Create indexes for workflow_comment_replies - op.create_index('comment_replies_comment_idx', 'workflow_comment_replies', ['comment_id']) - op.create_index('comment_replies_created_at_idx', 'workflow_comment_replies', ['created_at']) - - # Create workflow_comment_mentions table - op.create_table( - 'workflow_comment_mentions', - sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('comment_id', postgresql.UUID(), nullable=False), - sa.Column('mentioned_user_id', postgresql.UUID(), nullable=False), - sa.ForeignKeyConstraint(['comment_id'], ['workflow_comments.id'], name='workflow_comment_mentions_comment_id_fkey', ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name='workflow_comment_mentions_pkey') - ) - - # Create indexes for workflow_comment_mentions - op.create_index('comment_mentions_comment_idx', 'workflow_comment_mentions', ['comment_id']) - op.create_index('comment_mentions_user_idx', 'workflow_comment_mentions', ['mentioned_user_id']) - - -def downgrade(): - # Drop tables in reverse order due to foreign key constraints - op.drop_table('workflow_comment_mentions') - op.drop_table('workflow_comment_replies') - op.drop_table('workflow_comments') \ No newline at end of file diff --git a/api/migrations/versions/2025_08_22_1726-227822d22895_add_workflow_comments_table.py b/api/migrations/versions/2025_08_22_1726-227822d22895_add_workflow_comments_table.py new file mode 100644 index 0000000000..9a00b2a89a --- /dev/null +++ b/api/migrations/versions/2025_08_22_1726-227822d22895_add_workflow_comments_table.py @@ -0,0 +1,89 @@ +"""Add workflow comments table + +Revision ID: 227822d22895 +Revises: 1c9ba48be8e4 +Create Date: 2025-08-22 17:26:15.255980 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '227822d22895' +down_revision = '1c9ba48be8e4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('workflow_comments', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('position_x', sa.Float(), nullable=False), + sa.Column('position_y', sa.Float(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('created_by', models.types.StringUUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('resolved', sa.Boolean(), server_default=sa.text('false'), nullable=False), + sa.Column('resolved_at', sa.DateTime(), nullable=True), + sa.Column('resolved_by', models.types.StringUUID(), nullable=True), + sa.PrimaryKeyConstraint('id', name='workflow_comments_pkey') + ) + with op.batch_alter_table('workflow_comments', schema=None) as batch_op: + batch_op.create_index('workflow_comments_app_idx', ['tenant_id', 'app_id'], unique=False) + batch_op.create_index('workflow_comments_created_at_idx', ['created_at'], unique=False) + + op.create_table('workflow_comment_replies', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('comment_id', models.types.StringUUID(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('created_by', models.types.StringUUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.ForeignKeyConstraint(['comment_id'], ['workflow_comments.id'], name=op.f('workflow_comment_replies_comment_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='workflow_comment_replies_pkey') + ) + with op.batch_alter_table('workflow_comment_replies', schema=None) as batch_op: + batch_op.create_index('comment_replies_comment_idx', ['comment_id'], unique=False) + batch_op.create_index('comment_replies_created_at_idx', ['created_at'], unique=False) + + op.create_table('workflow_comment_mentions', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('comment_id', models.types.StringUUID(), nullable=False), + sa.Column('reply_id', models.types.StringUUID(), nullable=True), + sa.Column('mentioned_user_id', models.types.StringUUID(), nullable=False), + sa.ForeignKeyConstraint(['comment_id'], ['workflow_comments.id'], name=op.f('workflow_comment_mentions_comment_id_fkey'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['reply_id'], ['workflow_comment_replies.id'], name=op.f('workflow_comment_mentions_reply_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='workflow_comment_mentions_pkey') + ) + with op.batch_alter_table('workflow_comment_mentions', schema=None) as batch_op: + batch_op.create_index('comment_mentions_comment_idx', ['comment_id'], unique=False) + batch_op.create_index('comment_mentions_reply_idx', ['reply_id'], unique=False) + batch_op.create_index('comment_mentions_user_idx', ['mentioned_user_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('workflow_comment_mentions', schema=None) as batch_op: + batch_op.drop_index('comment_mentions_user_idx') + batch_op.drop_index('comment_mentions_reply_idx') + batch_op.drop_index('comment_mentions_comment_idx') + + op.drop_table('workflow_comment_mentions') + with op.batch_alter_table('workflow_comment_replies', schema=None) as batch_op: + batch_op.drop_index('comment_replies_created_at_idx') + batch_op.drop_index('comment_replies_comment_idx') + + op.drop_table('workflow_comment_replies') + with op.batch_alter_table('workflow_comments', schema=None) as batch_op: + batch_op.drop_index('workflow_comments_created_at_idx') + batch_op.drop_index('workflow_comments_app_idx') + + op.drop_table('workflow_comments') + # ### end Alembic commands ### diff --git a/api/models/comment.py b/api/models/comment.py index a06563e22f..e88705b6d6 100644 --- a/api/models/comment.py +++ b/api/models/comment.py @@ -166,6 +166,7 @@ class WorkflowCommentMention(Base): __table_args__ = ( db.PrimaryKeyConstraint("id", name="workflow_comment_mentions_pkey"), Index("comment_mentions_comment_idx", "comment_id"), + Index("comment_mentions_reply_idx", "reply_id"), Index("comment_mentions_user_idx", "mentioned_user_id"), ) @@ -173,10 +174,14 @@ class WorkflowCommentMention(Base): comment_id: Mapped[str] = mapped_column( StringUUID, db.ForeignKey("workflow_comments.id", ondelete="CASCADE"), nullable=False ) + reply_id: Mapped[Optional[str]] = mapped_column( + StringUUID, db.ForeignKey("workflow_comment_replies.id", ondelete="CASCADE"), nullable=True + ) mentioned_user_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # Relationships comment: Mapped["WorkflowComment"] = relationship("WorkflowComment", back_populates="mentions") + reply: Mapped[Optional["WorkflowCommentReply"]] = relationship("WorkflowCommentReply") @property def mentioned_user_account(self): diff --git a/api/services/workflow_comment_service.py b/api/services/workflow_comment_service.py index edb3850a88..cb6ee09d4b 100644 --- a/api/services/workflow_comment_service.py +++ b/api/services/workflow_comment_service.py @@ -83,7 +83,11 @@ class WorkflowCommentService: mentioned_user_ids = mentioned_user_ids or [] for user_id in mentioned_user_ids: if isinstance(user_id, str) and uuid_value(user_id): - mention = WorkflowCommentMention(comment_id=comment.id, mentioned_user_id=user_id) + mention = WorkflowCommentMention( + comment_id=comment.id, + reply_id=None, # This is a comment mention, not reply mention + mentioned_user_id=user_id + ) session.add(mention) session.commit() @@ -127,7 +131,11 @@ class WorkflowCommentService: mentioned_user_ids = mentioned_user_ids or [] for user_id_str in mentioned_user_ids: if isinstance(user_id_str, str) and uuid_value(user_id_str): - mention = WorkflowCommentMention(comment_id=comment.id, mentioned_user_id=user_id_str) + mention = WorkflowCommentMention( + comment_id=comment.id, + reply_id=None, # This is a comment mention + mentioned_user_id=user_id_str + ) db.session.add(mention) db.session.commit() @@ -176,27 +184,57 @@ class WorkflowCommentService: return comment @staticmethod - def create_reply(comment_id: str, content: str, created_by: str) -> WorkflowCommentReply: + def create_reply( + comment_id: str, + content: str, + created_by: str, + mentioned_user_ids: Optional[list[str]] = None + ) -> dict: """Add a reply to a workflow comment.""" - # Check if comment exists - comment = db.session.get(WorkflowComment, comment_id) - if not comment: - raise NotFound("Comment not found") - if len(content.strip()) == 0: raise ValueError("Reply content cannot be empty") if len(content) > 1000: raise ValueError("Reply content cannot exceed 1000 characters") - reply = WorkflowCommentReply(comment_id=comment_id, content=content, created_by=created_by) + with Session(db.engine) as session: + # Check if comment exists + comment = session.get(WorkflowComment, comment_id) + if not comment: + raise NotFound("Comment not found") - db.session.add(reply) - db.session.commit() - return reply + reply = WorkflowCommentReply(comment_id=comment_id, content=content, created_by=created_by) + + session.add(reply) + session.flush() # Get the reply ID for mentions + + # Create mentions if specified + mentioned_user_ids = mentioned_user_ids or [] + for user_id in mentioned_user_ids: + if isinstance(user_id, str) and uuid_value(user_id): + # Create mention linking to specific reply + mention = WorkflowCommentMention( + comment_id=comment_id, + reply_id=reply.id, # This is a reply mention + mentioned_user_id=user_id + ) + session.add(mention) + + session.commit() + + # Return only what we need - id and created_at + return { + "id": reply.id, + "created_at": reply.created_at + } @staticmethod - def update_reply(reply_id: str, user_id: str, content: str) -> WorkflowCommentReply: + def update_reply( + reply_id: str, + user_id: str, + content: str, + mentioned_user_ids: Optional[list[str]] = None + ) -> WorkflowCommentReply: """Update a comment reply.""" reply = db.session.get(WorkflowCommentReply, reply_id) if not reply: @@ -213,6 +251,25 @@ class WorkflowCommentService: raise ValueError("Reply content cannot exceed 1000 characters") reply.content = content + + # Handle mentions for reply updates - add new mentions to parent comment + mentioned_user_ids = mentioned_user_ids or [] + for user_id_str in mentioned_user_ids: + if isinstance(user_id_str, str) and uuid_value(user_id_str): + # Check if mention already exists to avoid duplicates + existing_mention = db.session.query(WorkflowCommentMention).filter( + WorkflowCommentMention.comment_id == reply.comment_id, + WorkflowCommentMention.mentioned_user_id == user_id_str + ).first() + + if not existing_mention: + mention = WorkflowCommentMention( + comment_id=reply.comment_id, + reply_id=reply.id, # This is a reply mention + mentioned_user_id=user_id_str + ) + db.session.add(mention) + db.session.commit() return reply