feat(web): keep viewer comments read-only
Hide add-comment affordances and reject submit for viewer; keep author/reviewer/approver comment roles unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+8
-2
@@ -50,6 +50,7 @@ import {
|
||||
} from './lib/projectMemberAdapter';
|
||||
import { mergeWorkflowDocumentsFromServer } from './lib/deliverableSync';
|
||||
import { resolveCurrentDocRole } from './lib/currentDocRole';
|
||||
import { canAddBlockComment } from './lib/commentPolicy';
|
||||
import {
|
||||
pollUntilPostTaskServerDoc,
|
||||
shouldSwitchTemplateSlotToServer,
|
||||
@@ -1233,7 +1234,12 @@ export default function App() {
|
||||
reviewer: docAST.reviewer,
|
||||
approver: docAST.approver,
|
||||
projectPermission: currentUser.projectPermissions?.[activeDocId],
|
||||
isLegacyLocalDoc: !docAST.authorId && !docAST.reviewerId && !docAST.approverId,
|
||||
dutiesAuthoritative: docAST.dutiesAuthoritative,
|
||||
governanceMode: docAST.governanceMode,
|
||||
isLegacyLocalDoc:
|
||||
docAST.dutiesAuthoritative !== true &&
|
||||
docAST.governanceMode !== 'controlled' &&
|
||||
/^doc-p\d+-/.test(docAST.docId),
|
||||
});
|
||||
|
||||
const setDocAST = (newAst: DocumentAST) => {
|
||||
@@ -3416,7 +3422,7 @@ export default function App() {
|
||||
|
||||
// 批注操作
|
||||
const handleAddComment = (comment: Omit<BlockComment, 'id' | 'createdAt'>) => {
|
||||
if (!commentingBlock) return;
|
||||
if (!commentingBlock || !canAddBlockComment(currentDocRole)) return;
|
||||
const newCmt: BlockComment = {
|
||||
...comment,
|
||||
id: 'cmt_' + Math.random().toString(36).substring(2, 8),
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MermaidBlockView } from './blocks/MermaidBlockView';
|
||||
import { MathBlockView } from './blocks/MathBlockView';
|
||||
import { ListBlockView } from './blocks/ListBlockView';
|
||||
import { BlockContextMenu } from './BlockContextMenu';
|
||||
import { canAddBlockComment } from '../../lib/commentPolicy';
|
||||
import { Plus, ArrowUp, ArrowDown, Trash2, MessageSquare } from 'lucide-react';
|
||||
|
||||
export interface BlockRendererProps {
|
||||
@@ -61,7 +62,7 @@ export const BlockRenderer: React.FC<BlockRendererProps> = ({
|
||||
!!ast.editLease?.holderType &&
|
||||
(!ast.editLease.expiresAt || new Date(ast.editLease.expiresAt).getTime() > Date.now());
|
||||
const isEditable = currentRole === 'author' && ast.lifecycleState === 'drafting' && !leaseActive;
|
||||
const canAnnotate = !leaseActive;
|
||||
const canAnnotate = !leaseActive && canAddBlockComment(currentRole);
|
||||
const children = ast.rootBlock.children || [];
|
||||
|
||||
const renderBlockContent = (block: DocumentBlock) => {
|
||||
@@ -267,7 +268,7 @@ export const BlockRenderer: React.FC<BlockRendererProps> = ({
|
||||
)}
|
||||
|
||||
{/* 批注按钮:编辑租约占用期间禁用 */}
|
||||
{canAnnotate && (
|
||||
{(canAnnotate || commentCount > 0) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -283,7 +284,7 @@ export const BlockRenderer: React.FC<BlockRendererProps> = ({
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
title="查看或添加审核批注"
|
||||
title={canAnnotate ? '查看或添加审核批注' : '查看审核批注'}
|
||||
>
|
||||
<MessageSquare size={12} />
|
||||
{commentCount > 0 && <span>{commentCount}</span>}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DocumentBlock, BlockComment } from '../../types/block';
|
||||
import { UserRole } from '../../types/docgov';
|
||||
import { canAddBlockComment, resolveCommentAuthorRole } from '../../lib/commentPolicy';
|
||||
import { X, Send, CheckCircle2, MessageSquare } from 'lucide-react';
|
||||
|
||||
interface CommentSidePanelProps {
|
||||
@@ -22,13 +23,16 @@ export const CommentSidePanel: React.FC<CommentSidePanelProps> = ({
|
||||
const [type, setType] = useState<'suggestion' | 'issue' | 'approval'>('suggestion');
|
||||
|
||||
const comments = block.comments || [];
|
||||
const canCompose = canAddBlockComment(currentRole);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!content.trim()) return;
|
||||
if (!canCompose || !content.trim()) return;
|
||||
const authorRole = resolveCommentAuthorRole(currentRole);
|
||||
if (!authorRole) return;
|
||||
|
||||
onAddComment({
|
||||
authorName: currentUserName,
|
||||
authorRole: currentRole === 'author' ? 'reviewer' : (currentRole as 'reviewer' | 'approver'),
|
||||
authorRole,
|
||||
type,
|
||||
content: content.trim(),
|
||||
resolved: false,
|
||||
@@ -163,7 +167,8 @@ export const CommentSidePanel: React.FC<CommentSidePanelProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部新增批注输入 */}
|
||||
{/* 底部新增批注输入:viewer 只读,不提供提交入口 */}
|
||||
{canCompose ? (
|
||||
<div style={{ padding: '12px 14px', borderTop: '1px solid var(--border)', background: 'var(--bg-subtle)', display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
{(['suggestion', 'issue', 'approval'] as const).map((t) => (
|
||||
@@ -221,6 +226,19 @@ export const CommentSidePanel: React.FC<CommentSidePanelProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-subtle)',
|
||||
fontSize: '12px',
|
||||
color: 'var(--text-muted)',
|
||||
}}
|
||||
>
|
||||
当前为查阅者,仅可阅读已有批注
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
canAddBlockComment,
|
||||
resolveCommentAuthorRole,
|
||||
} from './commentPolicy';
|
||||
|
||||
describe('commentPolicy', () => {
|
||||
it('lets author/reviewer/approver annotate and keeps their comment role', () => {
|
||||
assert.equal(canAddBlockComment('author'), true);
|
||||
assert.equal(canAddBlockComment('reviewer'), true);
|
||||
assert.equal(canAddBlockComment('approver'), true);
|
||||
assert.equal(resolveCommentAuthorRole('author'), 'reviewer');
|
||||
assert.equal(resolveCommentAuthorRole('reviewer'), 'reviewer');
|
||||
assert.equal(resolveCommentAuthorRole('approver'), 'approver');
|
||||
});
|
||||
|
||||
it('keeps viewer read-only: no add entry and no reviewer/approver cast', () => {
|
||||
assert.equal(canAddBlockComment('viewer'), false);
|
||||
assert.equal(resolveCommentAuthorRole('viewer'), null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { UserRole } from '../types/docgov';
|
||||
|
||||
export type CommentAuthorRole = 'reviewer' | 'approver';
|
||||
|
||||
/** viewer 只能读批注,不能打开新增入口或提交。 */
|
||||
export function canAddBlockComment(role: UserRole | string): boolean {
|
||||
return role === 'author' || role === 'reviewer' || role === 'approver';
|
||||
}
|
||||
|
||||
/** author 的批注按既有语义记为 reviewer;viewer 不得被 cast。 */
|
||||
export function resolveCommentAuthorRole(role: UserRole | string): CommentAuthorRole | null {
|
||||
if (role === 'approver') return 'approver';
|
||||
if (role === 'reviewer' || role === 'author') return 'reviewer';
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user