- 9 minutes to read

Comments and Annotations

This spoke covers the comment data model, @mention functionality, notifications, and thread structure for Mapify's collaborative annotation system. For the full overview, see the Multi-User Collaboration hub.


Why Use Comments?

Comments transform Mapify from a visualization tool into a collaborative workspace:

  • Decisions made in context – Questions answered directly on entities; no meeting scheduling needed
  • @mention notifications – Tag team members to direct their attention to specific entities
  • Approval workflows – Track "Pending Review" → "Approved" → "Closed" lifecycle
  • Permanent documentation – Decision rationale preserved in audit trail
  • Knowledge transfer – New team members read historical discussions to understand "why"
  • Compliance evidence – Demonstrate review and approval for SOX/GDPR audits

Typical use cases:

  • Compliance approval: "Legal has reviewed this GDPR integration – approved ✓"
  • Incident root-cause analysis: "Why did this integration fail last Tuesday?"
  • Architecture questions: "Should we use REST or SOAP for this System?"
  • Change coordination: "Don't modify this until Friday's deployment window"
  • Knowledge capture: "This Service requires special firewall rules – see ticket #1234"

Comment Data Structure

Field Name Type Example Value Purpose
CommentId GUID 7c9e6679-7425-40de-944b-e07fc1f90ae7 Unique identifier
EntityId GUID 3fa85f64-5717-4562-b3fc-2c963f66afa6 Entity this comment is attached to
EntityType String Integration Type (denormalized for reporting)
EntityName String SAP to Salesforce Name (denormalized for email notifications)
UserId GUID 8d2f3a4b-1c5e-6f7d-8a9b-0c1d2e3f4a5b User who created the comment
UserName String Alice Johnson Display name (denormalized)
UserEmail String alice.johnson@contoso.com Email for notifications (denormalized)
CommentText String (max 4,000 chars) @bob.taylor Can you review the error handling? Content (supports Markdown and @mentions)
ParentCommentId GUID (nullable) null (top-level) or GUID (reply) Parent comment for threading; null for top-level
CreatedDate DateTime (UTC) 2026-01-19T14:23:45Z When created
ModifiedDate DateTime (UTC, nullable) 2026-01-19T15:10:00Z When last edited; null if never edited
Status Enum Open, Resolved, Closed Approval workflow state
MentionedUsers JSON Array ["bob.taylor@contoso.com"] Users mentioned via @mention (for notifications)
IsEdited Boolean false Shows "(edited)" label in UI
IsDeleted Boolean false Soft delete flag — hides comment without removing audit record

Database Schema

CREATE TABLE [dbo].[EntityComments]
(
    [CommentId]        UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
    [EntityId]         UNIQUEIDENTIFIER NOT NULL,
    [EntityType]       NVARCHAR(50)     NOT NULL,
    [EntityName]       NVARCHAR(255)    NULL,  -- Denormalized
    [UserId]           UNIQUEIDENTIFIER NOT NULL,
    [UserName]         NVARCHAR(255)    NOT NULL,  -- Denormalized
    [UserEmail]        NVARCHAR(255)    NOT NULL,  -- Denormalized
    [CommentText]      NVARCHAR(4000)   NOT NULL,
    [ParentCommentId]  UNIQUEIDENTIFIER NULL REFERENCES [dbo].[EntityComments]([CommentId]),
    [Status]           NVARCHAR(20)     NOT NULL DEFAULT 'Open',  -- 'Open','Resolved','Closed'
    [MentionedUsers]   NVARCHAR(MAX)    NULL,  -- JSON array of email addresses
    [CreatedDate]      DATETIME2        NOT NULL DEFAULT GETUTCDATE(),
    [ModifiedDate]     DATETIME2        NULL,
    [IsEdited]         BIT              NOT NULL DEFAULT 0,
    [IsDeleted]        BIT              NOT NULL DEFAULT 0,

    INDEX [IX_Comments_EntityId]        ([EntityId], [CreatedDate] DESC),
    INDEX [IX_Comments_ParentCommentId] ([ParentCommentId]),
    INDEX [IX_Comments_UserId]          ([UserId]),
    INDEX [IX_Comments_Status]          ([Status], [EntityId])
);

API Response (JSON with Threading)

{
    "commentId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "entityType": "Integration",
    "entityName": "SAP to Salesforce",
    "userName": "Alice Johnson",
    "userEmail": "alice.johnson@contoso.com",
    "commentText": "@bob.taylor Can you review the error handling logic before we deploy?",
    "parentCommentId": null,
    "status": "Open",
    "mentionedUsers": ["bob.taylor@contoso.com"],
    "createdDate": "2026-01-19T14:23:45Z",
    "isEdited": false,
    "replies": [
        {
            "commentId": "9d4e7891-...",
            "userName": "Bob Taylor",
            "commentText": "Looks good! Suggest adding logging for the fallback path.",
            "status": "Open",
            "createdDate": "2026-01-19T15:10:00Z",
            "replies": []
        }
    ]
}

@Mention Functionality

Mention Syntax

  • By username: @alice.johnson
  • By email: @alice.johnson@contoso.com
  • By display name: @Alice Johnson (fuzzy matched if unique)
  • Multiple: @bob.taylor @charlie.davis (space-separated)

Autocomplete While Typing

When users type @, an autocomplete dropdown appears:

<div class="mention-autocomplete" role="listbox" aria-label="Mention user suggestions">
    <div class="mention-option" role="option" aria-selected="false" tabindex="0">
        <img src="avatar-alice.png" alt="" class="mention-avatar" aria-hidden="true">
        <div class="mention-details">
            <strong>Alice Johnson</strong>
            <small>alice.johnson@contoso.com</small>
        </div>
    </div>
    <div class="mention-option" role="option" aria-selected="false" tabindex="0">
        <img src="avatar-bob.png" alt="" class="mention-avatar" aria-hidden="true">
        <div class="mention-details">
            <strong>Bob Taylor</strong>
            <small>bob.taylor@contoso.com</small>
        </div>
    </div>
</div>

Autocomplete behavior:

  • Triggers immediately when @ is typed
  • Filters as user continues typing: @ali → "Alice Johnson"
  • Keyboard: Arrow keys to select, Enter to insert, Escape to cancel
  • Fuzzy matching: @alice → "Alice Johnson" and "alice.johnson@contoso.com"
  • Max 10 suggestions (sorted by recent collaboration frequency)

Notification Mechanism

When a comment with @mentions is saved:

  1. Parse mentions from CommentText using regex: /@(\S+@\S+\.\S+|[\w.]+)/g
  2. Resolve user emails (match username to user account)
  3. Store in MentionedUsers JSON array field
  4. Send email notifications to each mentioned user
  5. Create in-app notifications (bell icon in Mapify header)

Email Notification Template

Subject: Alice Johnson mentioned you in a comment on "SAP to Salesforce"

Hi Bob,

Alice Johnson mentioned you in a comment on the Integration "SAP to Salesforce":

    @bob.taylor Can you review the error handling logic before we deploy?
    Also cc @charlie.davis for security review.

Click here to view and reply:
https://mapify.contoso.com/integration/3fa85f64-5717-4562-b3fc-2c963f66afa6#comment-7c9e6679

---
This is an automated notification from Nodinite Mapify.
Update your notification preferences: https://mapify.contoso.com/settings/notifications

In-App Notification

<div class="notification-item unread" role="alert">
    <img src="avatar-alice.png" alt="Alice Johnson avatar" class="notification-avatar">
    <div class="notification-content">
        <strong>Alice Johnson</strong> mentioned you in a comment on
        <a href="/integration/3fa85f64#comment-7c9e6679">SAP to Salesforce</a>
        <small class="notification-time">2 minutes ago</small>
    </div>
    <button class="btn-mark-read" aria-label="Mark notification as read">
        <i class="fas fa-check" aria-hidden="true"></i>
    </button>
</div>

User Notification Preferences

Setting Options Default
Email notifications On / Off / Digest (daily summary) On
In-app notifications On / Off On
Mobile push notifications On / Off Off
Quiet hours Disable outside business hours by timezone Off

Comment Threading (Nested Replies)

Threading Structure

  • Top-level comments: ParentCommentId = NULL
  • Replies: ParentCommentId = GUID of parent comment
  • Max nesting depth: 3 levels (top → reply → reply-to-reply)
  • Indentation: 20px per nesting level
Comment #1 (Alice, depth 0)
├── Reply #2 (Bob, depth 1)
│   └── Reply #3 (Alice, depth 2)
│       └── Reply #4 (Charlie, depth 3)  ← Max depth
└── Reply #5 (Charlie, depth 1)

Thread HTML/CSS

<div class="comment-thread">
    <!-- Top-level comment (depth 0) -->
    <div class="comment" data-comment-id="comment-1" data-depth="0">
        <img src="avatar-alice.png" alt="Alice Johnson avatar" class="comment-avatar">
        <div class="comment-content">
            <div class="comment-header">
                <strong>Alice Johnson</strong>
                <time class="comment-time" datetime="2026-01-19T14:23:45Z">2 hours ago</time>
                <span class="comment-status status-open">
                    <i class="fas fa-circle-dot" aria-hidden="true"></i> Open
                </span>
            </div>
            <div class="comment-body">
                @bob.taylor Can you review the error handling logic before we deploy?
            </div>
            <div class="comment-actions">
                <button class="btn-reply"
                        aria-label="Reply to Alice Johnson's comment">
                    <i class="fas fa-reply" aria-hidden="true"></i> Reply
                </button>
                <button class="btn-resolve"
                        aria-label="Mark comment as resolved">
                    <i class="fas fa-check" aria-hidden="true"></i> Resolve
                </button>
            </div>
        </div>
    </div>

    <!-- Nested reply (depth 1) -->
    <div class="comment reply" data-comment-id="comment-2" data-depth="1"
         style="margin-left: 20px;">
        <img src="avatar-bob.png" alt="Bob Taylor avatar" class="comment-avatar">
        <div class="comment-content">
            <div class="comment-header">
                <strong>Bob Taylor</strong>
                <time class="comment-time" datetime="2026-01-19T15:10:00Z">1 hour ago</time>
            </div>
            <div class="comment-body">
                Looks good! One suggestion: add logging for the fallback path.
            </div>
            <div class="comment-actions">
                <button class="btn-reply"
                        aria-label="Reply to Bob Taylor's comment">
                    <i class="fas fa-reply" aria-hidden="true"></i> Reply
                </button>
            </div>
        </div>
    </div>
</div>
.comment-thread { position: relative; }

.comment {
    display: flex;
    gap: 12px;
    margin-bottom: 16px;
}

.comment.reply {
    border-left: 2px solid #e0e0e0;
    padding-left: 12px;
}

.comment-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    border: 2px solid #fff;
    box-shadow: 0 2px 4px rgba(0,0,0,.2);
}

.comment-content {
    flex: 1;
    background: #f8f9fa;
    border-radius: 8px;
    padding: 12px;
}

.comment-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
    font-size: 13px;
    color: #6c757d;
}

.comment-header strong { color: #212529; }

.comment-body { font-size: 14px; line-height: 1.5; color: #212529; }

.comment-actions {
    display: flex;
    gap: 8px;
    margin-top: 8px;
}

.btn-reply, .btn-resolve {
    background: none;
    border: 1px solid #dee2e6;
    border-radius: 4px;
    padding: 4px 10px;
    font-size: 13px;
    cursor: pointer;
    color: #495057;
}

.btn-reply:hover, .btn-resolve:hover { background-color: #f0f0f0; }
.btn-resolve:hover { background-color: #e8f5e9; border-color: #28a745; color: #155724; }

.comment-status {
    padding: 2px 8px;
    border-radius: 100px;
    font-size: 12px;
    font-weight: 600;
}

.status-open     { background: #e3f2fd; color: #0056b3; }
.status-resolved { background: #e8f5e9; color: #155724; }
.status-closed   { background: #f8f9fa; color: #6c757d; }

@media (prefers-reduced-motion: reduce) {
    .comment, .mention-autocomplete { transition: none; animation: none; }
}