diff --git a/CHANGELOG.md b/CHANGELOG.md index e0a62b9..01705fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,62 @@ # Changelog -## [0.20.0] - 2026-06-11 +## [1.0.0] - 2026-06-11 + +The SDK is now **Markdown-only** for rich content. All TipTap/JSON document APIs and the document structure DSL have been removed. Markdown is converted to native editor blocks on the server and can be read back as Markdown. ### Added -- **📝 Markdown Document API**: New recommended methods for working with document content +- **📝 Markdown Document API**: The only way to work with document content - `replace_markdown_document(document_id, markdown)` - Replace document content with Markdown - `append_markdown_document(document_id, markdown)` - Append Markdown to existing content - `get_markdown_document(document_id)` - Read document content back as a Markdown string - Markdown is converted to native rich editor blocks on the server (headings, lists, tables, code blocks, checklists, links, etc.) - -### Deprecated - -- JSON document methods (`replace_json_document`, `append_json_document`, `get_json_document`) and the JSON document DSL remain supported for backward compatibility, but Markdown methods are now the recommended way to write document content +- **🏷️ Markdown Mentions**: New `@[label](kind:id)` syntax for mentions in documents and comments + - Supported kinds: `user`, `task`, `document`, `milestone`, `project`, `board` + - Example: `@[John](user:6a29be79cb3b2bee09db40bd)` becomes a live mention chip and triggers notifications + - Mentions survive markdown round-trips: `get_markdown_document()` exports them in the same syntax +- **💬 Markdown Comments**: New `markdown` parameter in `post_comment()` and `edit_comment()` + - Markdown is converted to rich comment content on the server and stored with `content_version = 2` + - `markdown` and `content` are mutually exclusive — provide exactly one (otherwise `ValueError` is raised) + - New `Comment.content_version` field (`2` = rich/markdown-based, `None` = legacy HTML) + - `get_comments()` now returns comment content as Markdown (full markdown round-trip, mentions included); legacy comments (`content_version` other than `2`) are returned as raw HTML +- New `Tree` icon in the `Icon` enum + +### Removed (Breaking) + +- **TipTap/JSON document methods**: `get_json_document()`, `replace_json_document()`, `append_json_document()`, `replace_document()`, `append_document()` and their request/response models +- **Document structure DSL** (`vaiz.helpers.document_structure`): all node builders (`paragraph()`, `heading()`, `bullet_list()`, `table()`, `image_block()`, `embed_block()`, `mention_*()`, etc.), all node types, and the `EmbedType` enum + +### Changed (Breaking) + +- `Task.get_task_description(client)` now returns the description as a Markdown string (previously a parsed JSON dict) +- `Task.update_task_description(client, markdown)` now accepts Markdown and uses `replace_markdown_document()` under the hood +- `post_comment()` and `edit_comment()`: the `content` parameter is now optional; provide exactly one of `content` (legacy HTML) or `markdown` (recommended) +- `get_space_members()` now excludes bot members (AI, automation, and integration bots) from the result +- `toggle_milestone()` no longer sets the task's main `milestone` field — only the `milestones` list is updated + +### Migration Guide + +| Removed (TipTap / JSON DSL) | Replacement (Markdown) | +| --- | --- | +| `client.replace_json_document(doc_id, [heading(1, "Title"), paragraph("text")])` | `client.replace_markdown_document(doc_id, "# Title\n\ntext")` | +| `client.append_json_document(doc_id, [paragraph("more")])` | `client.append_markdown_document(doc_id, "more")` | +| `client.replace_document(doc_id, "plain text")` | `client.replace_markdown_document(doc_id, "plain text")` | +| `client.append_document(doc_id, "plain text")` | `client.append_markdown_document(doc_id, "plain text")` | +| `client.get_json_document(doc_id)` | `client.get_markdown_document(doc_id)` | +| `paragraph("Hello ", text("World", bold=True))` | `"Hello **World**"` | +| `heading(2, "Section")` | `"## Section"` | +| `bullet_list("a", "b")` / `ordered_list("a", "b")` | `"- a\n- b"` / `"1. a\n2. b"` | +| `task_list(task_item("Do it", checked=True))` | `"- [x] Do it"` | +| `table(table_row(table_header("H")), table_row("v"))` | `"\| H \|\n\| --- \|\n\| v \|"` | +| `code_block("print(1)", language="python")` | "```python\nprint(1)\n```" | +| `link_text("Vaiz", "https://vaiz.app")` | `"[Vaiz](https://vaiz.app)"` | +| `mention_user("id")` / `mention_task("id")` / ... | `"@[label](user:id)"` / `"@[label](task:id)"` / ... | +| `embed_block(...)`, `image_block(...)`, `toc_block()`, etc. | No direct replacement; managed by the editor UI | +| `client.post_comment(doc_id, content="

Hi there

")` | `client.post_comment(doc_id, markdown="Hi **there**")` | +| Rich `create_task(description=...)` | `description` is plain text only; for rich content: `task = client.create_task(...).task` then `client.replace_markdown_document(task.document, markdown)` | + +Old SDK versions (≤ 0.19.x) keep working: the server-side JSON document endpoints are not removed, only the SDK surface. ## [0.19.0] - 2026-02-17 diff --git a/docs-site/docs/api-reference/comments.md b/docs-site/docs/api-reference/comments.md index d7b0aa1..ae110c8 100644 --- a/docs-site/docs/api-reference/comments.md +++ b/docs-site/docs/api-reference/comments.md @@ -16,19 +16,21 @@ Complete reference for comment-related methods and models. ```python post_comment( document_id: str, - content: str, + content: Optional[str] = None, file_ids: List[str] = None, - reply_to: str = None + reply_to: str = None, + markdown: Optional[str] = None ) -> PostCommentResponse ``` -Post a comment to a document. +Post a comment to a document. Provide exactly one of `markdown` (recommended) or `content` (legacy HTML); passing both or neither raises `ValueError`. **Parameters:** - `document_id` - Document ID to comment on -- `content` - Comment content (HTML supported) +- `content` - Comment content as HTML (legacy format) - `file_ids` - Optional list of file IDs to attach - `reply_to` - Optional parent comment ID for replies +- `markdown` - Comment content as Markdown (recommended). Converted to rich comment content on the server; the stored comment has `content_version = 2` **Returns:** `PostCommentResponse` with created comment @@ -40,7 +42,7 @@ Post a comment to a document. get_comments(document_id: str) -> GetCommentsResponse ``` -Get all comments for a document. +Get all comments for a document. Comment content is returned as Markdown (mentions as `@[label](kind:id)`). Legacy comments (`content_version` other than `2`) are returned as raw HTML. **Parameters:** - `document_id` - Document ID @@ -54,21 +56,23 @@ Get all comments for a document. ```python edit_comment( comment_id: str, - content: str, + content: Optional[str] = None, add_file_ids: List[str] = None, order_file_ids: List[str] = None, - remove_file_ids: List[str] = None + remove_file_ids: List[str] = None, + markdown: Optional[str] = None ) -> EditCommentResponse ``` -Edit comment content and manage files. +Edit comment content and manage files. Provide exactly one of `markdown` (recommended) or `content` (legacy HTML); passing both or neither raises `ValueError`. **Parameters:** - `comment_id` - Comment ID to edit -- `content` - New content +- `content` - New content as HTML (legacy format) - `add_file_ids` - Files to add - `order_file_ids` - New file order - `remove_file_ids` - Files to remove +- `markdown` - New content as Markdown (recommended). Editing with markdown upgrades the comment to `content_version = 2` **Returns:** `EditCommentResponse` with updated comment @@ -146,7 +150,8 @@ Main comment model representing a comment in the system. ```python class Comment: id: str # Comment ID - content: str # HTML content + content: str # Rendered HTML content + content_version: Optional[int] # Content format version (2 = rich/markdown-based, None = legacy HTML) author_id: str # Author user ID document_id: str # Document ID created_at: datetime # Creation timestamp @@ -180,7 +185,8 @@ class CommentReaction: ```python class PostCommentRequest: document_id: str # Required - Document ID - content: str # Required - Comment content (HTML) + content: Optional[str] # Comment content (HTML, legacy) + markdown: Optional[str] # Comment content (Markdown, recommended) file_ids: List[str] # File IDs to attach reply_to: Optional[str] # Parent comment ID for replies ``` @@ -201,7 +207,8 @@ class GetCommentsRequest: ```python class EditCommentRequest: comment_id: str # Required - Comment ID - content: str # Required - New comment content (HTML) + content: Optional[str] # New comment content (HTML, legacy) + markdown: Optional[str] # New comment content (Markdown, recommended) add_file_ids: List[str] # File IDs to add order_file_ids: List[str] # Order of file IDs remove_file_ids: List[str] # File IDs to remove diff --git a/docs-site/docs/api-reference/document-structure.md b/docs-site/docs/api-reference/document-structure.md deleted file mode 100644 index 8fe1397..0000000 --- a/docs-site/docs/api-reference/document-structure.md +++ /dev/null @@ -1,1583 +0,0 @@ ---- -sidebar_position: 3 -sidebar_label: Document Structure -title: Document Structure — JSON Format & Block Types | Vaiz Python SDK -description: Technical reference for the Vaiz document structure JSON format. Learn about blocks, marks, paragraphs, headings, lists, tables, and rich-text formatting. ---- - -# Document Structure - -Technical reference for the document structure JSON format. - -## Content Methods - -These methods work with document content for both task descriptions and standalone documents. - -### `get_json_document` - -```python -get_json_document(document_id: str) -> Dict[str, Any] -``` - -Get the JSON content of a specific document or task description. - -**Parameters:** -- `document_id` - Document ID (from task or standalone document) - -**Returns:** `Dict[str, Any]` - Parsed JSON structure - -**Example:** -```python -# Get standalone document content -content = client.get_json_document("document_id") -print(content) - -# Get task description -task_response = client.get_task("PRJ-123") -description = client.get_json_document(task_response.task.document) -``` - ---- - -### `replace_document` - -```python -replace_document( - document_id: str, - description: str -) -> ReplaceDocumentResponse -``` - -Replace document content with plain text. - -**Parameters:** -- `document_id` - Document ID -- `description` - New content (plain text or markdown-style) - -**Returns:** `ReplaceDocumentResponse` - -**Example:** -```python -# Replace with plain text -client.replace_document( - document_id="doc_id", - description=""" -# Updated Content - -New document content here. -""" -) -``` - ---- - -### `replace_json_document` - -```python -replace_json_document( - document_id: str, - content: List[Dict[str, Any]] -) -> ReplaceJSONDocumentResponse -``` - -Replace document content with structured JSON content. - -**Works for:** -- Task descriptions -- Standalone documents (Project, Space, Member) -- Any document type - -**Parameters:** -- `document_id` - Document ID -- `content` - JSONContent array (see format below) - -**Returns:** `ReplaceJSONDocumentResponse` - -**Example with raw JSON:** -```python -json_content = [ - { - "type": "heading", - "attrs": {"level": 1}, - "content": [{"type": "text", "text": "Title"}] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "This is "}, - {"type": "text", "marks": [{"type": "bold"}], "text": "bold"} - ] - } -] - -client.replace_json_document("doc_id", json_content) -``` - -**Example with helpers (recommended):** -```python -from vaiz import heading, paragraph, text - -content = [ - heading(1, "Title"), - paragraph("This is ", text("bold", bold=True)) -] - -client.replace_json_document("doc_id", content) -``` - -See [Document Structure Helpers Guide](../guides/document-structure-helpers) for all helper functions. - ---- - -### `append_document` - -```python -append_document( - document_id: str, - description: str = None, - files: List[Any] = None -) -> AppendDocumentResponse -``` - -Append plain text content to an existing document without removing existing content. - -**Parameters:** -- `document_id` - Document ID -- `description` - Plain text to append (optional) -- `files` - Files to attach (optional) - -**Returns:** `AppendDocumentResponse` - -**Example:** -```python -# Append text to existing document -client.append_document( - document_id="doc_id", - description="\n\nUpdate: Task completed successfully" -) -``` - ---- - -### `append_json_document` - -```python -append_json_document( - document_id: str, - content: List[Dict[str, Any]] -) -> AppendJSONDocumentResponse -``` - -Append structured JSON content to an existing document without removing existing content. - -**Parameters:** -- `document_id` - Document ID -- `content` - JSONContent array (see format below) - -**Returns:** `AppendJSONDocumentResponse` - -**Example with raw JSON:** -```python -new_section = [ - { - "type": "heading", - "attrs": {"level": 2}, - "content": [{"type": "text", "text": "Update"}] - }, - { - "type": "paragraph", - "content": [{"type": "text", "text": "Additional notes"}] - } -] - -client.append_json_document("doc_id", new_section) -``` - -**Example with helpers:** -```python -from vaiz import heading, paragraph, text - -updates = [ - heading(2, "Update"), - paragraph("Additional ", text("notes", bold=True)) -] - -client.append_json_document("doc_id", updates) -``` - ---- - -## Method Comparison - -| Method | Clears Existing? | Format | Use Case | -|--------|------------------|--------|----------| -| `replace_document` | ✅ Yes | Plain text | Complete replacement with plain text | -| `replace_json_document` | ✅ Yes | JSON structure | Complete replacement with rich content | -| `append_document` | ❌ No | Plain text | Add to existing plain text | -| `append_json_document` | ❌ No | JSON structure | Add to existing rich content | - ---- - -## Format Overview - -Documents consist of an array of **nodes**. Each node has a `type` and optional `content` or `attrs`: - -```json -[ - { - "type": "heading", - "attrs": {"level": 1}, - "content": [...] - }, - { - "type": "paragraph", - "content": [...] - } -] -``` - -## Node Types - -### Text Node - -The most basic node type containing text content. - -```json -{ - "type": "text", - "text": "Hello World" -} -``` - -**With formatting marks:** - -```json -{ - "type": "text", - "text": "Bold text", - "marks": [{"type": "bold"}] -} -``` - -### Paragraph Node - -Block-level node for paragraphs. - -```json -{ - "type": "paragraph", - "content": [ - {"type": "text", "text": "Regular text and "}, - {"type": "text", "text": "bold text", "marks": [{"type": "bold"}]} - ] -} -``` - -### Heading Node - -Heading with level 1-6. - -```json -{ - "type": "heading", - "attrs": {"level": 1}, - "content": [ - {"type": "text", "text": "Main Title"} - ] -} -``` - -**Levels:** -- `level: 1` - H1 (largest) -- `level: 2` - H2 -- `level: 3` - H3 -- `level: 4` - H4 -- `level: 5` - H5 -- `level: 6` - H6 (smallest) - -### Bullet List Node - -Unordered list with bullet points. - -```json -{ - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "First item"}] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Second item"}] - } - ] - } - ] -} -``` - -### Ordered List Node - -Numbered list. - -```json -{ - "type": "orderedList", - "attrs": {"start": 1}, - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "First step"}] - } - ] - } - ] -} -``` - -**Attributes:** -- `start` - Starting number (optional, default: 1) - -### Task List Node (Checklist) - -Interactive checklist with checkable items. - -```json -{ - "type": "taskList", - "attrs": { - "uid": "uniqueId123" - }, - "content": [ - { - "type": "taskItem", - "attrs": { - "checked": true - }, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Completed task"}] - } - ] - }, - { - "type": "taskItem", - "attrs": { - "checked": false - }, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Todo task"}] - } - ] - } - ] -} -``` - -**Attributes:** -- `uid` - Unique identifier (optional, auto-generated) - -**Features:** -- Interactive checkboxes -- Can be nested for multi-level checklists -- Supports checked/unchecked states -- Can contain paragraphs and nested task lists - -### Task Item Node - -Individual item in a task list (checklist). - -```json -{ - "type": "taskItem", - "attrs": { - "checked": false - }, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Task description"}] - }, - { - "type": "taskList", - "content": [...] // Nested checklist - } - ] -} -``` - -**Attributes:** -- `checked` - Whether the task is completed (required, boolean) - -**Content:** -- Can contain paragraphs -- Can contain nested `taskList` nodes for sub-tasks -- Supports multi-level nesting - -### List Item Node - -Individual item in a list. Can contain paragraphs and nested lists. - -```json -{ - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Item content"}] - }, - { - "type": "bulletList", - "content": [...] // Nested list - } - ] -} -``` - -### Blockquote Node - -Blockquote for quoted text or callouts. - -```json -{ - "type": "blockquote", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "This is a quote"}] - } - ] -} -``` - -## Text Formatting Marks - -Marks are applied to text nodes to add formatting: - -### Bold - -```json -{ - "type": "text", - "text": "Bold text", - "marks": [{"type": "bold"}] -} -``` - -### Italic - -```json -{ - "type": "text", - "text": "Italic text", - "marks": [{"type": "italic"}] -} -``` - -### Code - -Inline code formatting. - -```json -{ - "type": "text", - "text": "client.get_task()", - "marks": [{"type": "code"}] -} -``` - -### Link - -Hyperlink with URL. - -```json -{ - "type": "text", - "text": "Click here", - "marks": [ - { - "type": "link", - "attrs": { - "href": "https://vaiz.app", - "target": "_blank" - } - } - ] -} -``` - -**Attributes:** -- `href` - URL (required) -- `target` - Link target (optional, e.g., `"_blank"`) - -### Combined Marks - -Multiple marks can be combined: - -```json -{ - "type": "text", - "text": "Bold italic link", - "marks": [ - {"type": "bold"}, - {"type": "italic"}, - { - "type": "link", - "attrs": {"href": "https://example.com"} - } - ] -} -``` - -## Tables - -Tables use the `extension-table` type and consist of rows and cells. - -### Extension Table - -Table with rows and cells. - -```json -{ - "type": "extension-table", - "attrs": { - "uid": "uniqueId123", - "showRowNumbers": false - }, - "content": [ - { - "type": "tableRow", - "attrs": {"showRowNumbers": false}, - "content": [ - { - "type": "tableCell", - "attrs": {"colspan": 1, "rowspan": 1}, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Name"}] - } - ] - }, - { - "type": "tableCell", - "attrs": {"colspan": 1, "rowspan": 1}, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Status"}] - } - ] - } - ] - }, - { - "type": "tableRow", - "attrs": {"showRowNumbers": false}, - "content": [ - { - "type": "tableCell", - "attrs": {"colspan": 1, "rowspan": 1}, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Task 1"}] - } - ] - }, - { - "type": "tableCell", - "attrs": {"colspan": 1, "rowspan": 1}, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Done"}] - } - ] - } - ] - } - ] -} -``` - -### Table Row - -Row in a table containing cells. - -```json -{ - "type": "tableRow", - "attrs": {"showRowNumbers": false}, - "content": [ - {"type": "tableCell", "attrs": {"colspan": 1, "rowspan": 1}, "content": [...]}, - {"type": "tableCell", "attrs": {"colspan": 1, "rowspan": 1}, "content": [...]} - ] -} -``` - -**Attributes:** -- `showRowNumbers` - Display row numbers (optional, default: false) - -### Table Cell - -Table cell containing content (data cell). - -```json -{ - "type": "tableCell", - "attrs": { - "colspan": 1, - "rowspan": 1 - }, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Cell content"}] - } - ] -} -``` - -**Attributes:** -- `colspan` - Number of columns to span (default: 1) -- `rowspan` - Number of rows to span (default: 1) - -### Table Header - -Table header cell (`` in HTML). Use for semantic table headers. - -```json -{ - "type": "tableHeader", - "attrs": { - "colspan": 1, - "rowspan": 1 - }, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Header Name"}] - } - ] -} -``` - -**Attributes:** -- `colspan` - Number of columns to span (default: 1) -- `rowspan` - Number of rows to span (default: 1) - -**Benefits:** -- Semantic HTML structure -- Better accessibility for screen readers -- Consistent styling -- Supports colspan/rowspan like table cells - -## Nested Structures - -### Nested Lists - -Lists can be nested within list items: - -```json -{ - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Parent item"}] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Nested item"}] - } - ] - } - ] - } - ] - } - ] -} -``` - -### Nested Checklists - -Task lists support multi-level nesting: - -```json -{ - "type": "taskList", - "attrs": {"uid": "mainList"}, - "content": [ - { - "type": "taskItem", - "attrs": {"checked": false}, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Main task"}] - }, - { - "type": "taskList", - "attrs": {"uid": "nestedList"}, - "content": [ - { - "type": "taskItem", - "attrs": {"checked": true}, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Subtask 1"}] - } - ] - }, - { - "type": "taskItem", - "attrs": {"checked": false}, - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Subtask 2"}] - } - ] - } - ] - } - ] - } - ] -} -``` - -**Using helpers for nested checklists:** - -```python -from vaiz import task_list, task_item, paragraph - -content = [ - task_list( - task_item( - paragraph("Phase 1: Planning"), - task_list( - task_item("Define requirements", checked=True), - task_item("Create wireframes", checked=True), - task_item("Review with stakeholders", checked=False) - ), - checked=True - ), - task_item( - paragraph("Phase 2: Development"), - task_list( - task_item("Setup project", checked=False), - task_item("Implement features", checked=False) - ), - checked=False - ) - ) -] -``` - -## Complete Example - -```json -[ - { - "type": "heading", - "attrs": {"level": 1}, - "content": [ - {"type": "text", "text": "Project Documentation"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "This is "}, - {"type": "text", "text": "bold", "marks": [{"type": "bold"}]}, - {"type": "text", "text": " and "}, - {"type": "text", "text": "italic", "marks": [{"type": "italic"}]}, - {"type": "text", "text": " text."} - ] - }, - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "Features"} - ] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Easy to use"}] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Type-safe"}] - } - ] - } - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Learn more at "}, - { - "type": "text", - "text": "docs.vaiz.app", - "marks": [ - { - "type": "link", - "attrs": { - "href": "https://docs.vaiz.app", - "target": "_blank" - } - } - ] - } - ] - } -] -``` - -## Helper Functions (Recommended) - -Instead of manually constructing JSON, use the built-in helper functions: - -```python -from vaiz import heading, paragraph, text, bullet_list, task_list, task_item, blockquote, link_text, table, table_row, table_header - -content = [ - heading(1, "Project Documentation"), - paragraph( - "This is ", - text("bold", bold=True), - " and ", - text("italic", italic=True), - " text." - ), - heading(2, "Features"), - bullet_list( - "Easy to use", - "Type-safe" - ), - heading(2, "Todo"), - task_list( - task_item("Review code", checked=True), - task_item("Deploy to production", checked=False) - ), - blockquote( - paragraph( - text("Important: ", bold=True), - "Always use type-safe helpers for better code quality." - ) - ), - table( - table_row( - table_header("Feature"), - table_header("Status") - ), - table_row("Document helpers", "✅"), - table_row("Table headers", "✅") - ), - paragraph( - "Learn more at ", - link_text("docs.vaiz.app", "https://docs.vaiz.app") - ) -] -``` - -See [Document Structure Helpers Guide](../guides/document-structure-helpers) for complete documentation. - -## Mention Blocks - -Mention blocks create interactive references to users, documents, tasks, and milestones. - -### Structure - -```json -{ - "type": "custom-mention", - "attrs": { - "uid": "unique_id", - "custom": 1, - "inline": true, - "data": { - "item": { - "id": "entity_id", - "kind": "User" // User | Document | Task | Milestone - } - } - }, - "content": [ - {"type": "text", "text": " "} - ] -} -``` - -### Using Helpers (Recommended) - -```python -from vaiz import mention_user, mention_document, mention_task, mention_milestone - -# Mention a user -mention_user("68fa7d4462f676bcd1c054b0") - -# Mention a document -mention_document("68fa7d5762f676bcd1c055da") - -# Mention a task -mention_task("68f2081feda35a3b34ac0318") - -# Mention a milestone -mention_milestone("68fa739962f676bcd1beda2d") -``` - -### In Paragraphs - -```python -from vaiz import paragraph, text, mention_user, mention_task - -paragraph( - text("Assigned to "), - mention_user("member_id"), - text(" - task "), - mention_task("task_id") -) -``` - -### In Tables - -```python -from vaiz import table, table_row, table_header, table_cell, paragraph -from vaiz import mention_user, mention_task - -table( - table_row( - table_header("Assignee"), - table_header("Task") - ), - table_row( - table_cell(paragraph(mention_user("member_id"))), - table_cell(paragraph(mention_task("task_id"))) - ) -) -``` - -### Supported Mention Types - -| Kind | Helper Function | Description | -|------|----------------|-------------| -| `User` | `mention_user(member_id)` | Mention a team member | -| `Document` | `mention_document(id)` | Reference a document | -| `Task` | `mention_task(id)` | Reference a task | -| `Milestone` | `mention_milestone(id)` | Reference a milestone | - -### Getting Entity IDs - -```python -# Get member ID -profile = client.get_profile() -member_id = profile.profile.member_id - -# Or get from space members -members = client.get_space_members(space_id) -member_id = members.members[0].id - -# Get document ID -from vaiz import GetDocumentsRequest, Kind -docs = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=space_id) -) -doc_id = docs.payload.documents[0].id - -# Get task ID -from vaiz import GetTasksRequest -tasks = client.get_tasks(GetTasksRequest()) -task_id = tasks.payload.tasks[0].id - -# Get milestone ID -milestones = client.get_milestones() -milestone_id = milestones.milestones[0].id -``` - -## Navigation Blocks - -### TOC Block - -Table of Contents block that automatically generates an interactive document outline. - -**Structure:** -```json -{ - "type": "doc-siblings", - "attrs": { - "uid": "uniqueId123", - "custom": 1, - "contenteditable": "false" - }, - "content": [ - { - "type": "text", - "text": "{\"type\":\"toc\"}" - } - ] -} -``` - -**Using Helper:** -```python -from vaiz import toc_block, heading, paragraph - -content = [ - toc_block(), - - heading(1, "Introduction"), - paragraph("Content here..."), - - heading(2, "Getting Started"), - paragraph("More content...") -] - -client.replace_json_document(document_id, content) -``` - -**Features:** -- Automatically indexes all headings (h1-h6) -- Creates clickable navigation links -- Shows hierarchical structure -- Updates automatically on changes - -**Note:** Headings must have `uid` attribute for TOC navigation to work (automatically added by `heading()` helper). - ---- - -### Anchors Block - -Displays related documents and backlinks. - -**Structure:** -```json -{ - "type": "doc-siblings", - "attrs": { - "uid": "uniqueId456", - "custom": 1, - "contenteditable": "false" - }, - "content": [ - { - "type": "text", - "text": "{\"type\":\"anchors\"}" - } - ] -} -``` - -**Using Helper:** -```python -from vaiz import anchors_block, heading, paragraph - -content = [ - anchors_block(), - - heading(1, "Documentation"), - paragraph("Related documents shown above...") -] - -client.replace_json_document(document_id, content) -``` - -**Shows:** -- Documents that this document links to -- Documents that link to this one (backlinks) -- Related documents from the space -- Knowledge graph connections - ---- - -### Siblings Block - -Previous/Next navigation between documents in a sequence. - -**Structure:** -```json -{ - "type": "doc-siblings", - "attrs": { - "uid": "uniqueId789", - "custom": 1, - "contenteditable": "false" - }, - "content": [ - { - "type": "text", - "text": "{\"type\":\"siblings\"}" - } - ] -} -``` - -**Using Helper:** -```python -from vaiz import siblings_block, heading, paragraph, horizontal_rule - -content = [ - heading(1, "Tutorial Part 2: Advanced Features"), - paragraph("Main content here..."), - - horizontal_rule(), - - # Navigation at the bottom - siblings_block() # Shows "← Part 1" and "Part 3 →" -] - -client.replace_json_document(document_id, content) -``` - -**Features:** -- Shows Previous and Next documents in sequence -- Creates navigation buttons (Back/Forward) -- Typically placed at page bottom -- Maintains document order in branch - -**Best for:** -- Tutorial series (Part 1 → Part 2 → Part 3) -- Multi-chapter guides -- Sequential documentation -- Step-by-step workflows - ---- - -### Code Block - -Code block with syntax highlighting. - -**Structure:** -```json -{ - "type": "codeBlock", - "attrs": { - "uid": "uniqueIdABC", - "language": "python" - }, - "content": [ - { - "type": "text", - "text": "def hello():\n print(\"Hello, World!\")" - } - ] -} -``` - -**Using Helper:** -```python -from vaiz import code_block, heading, paragraph - -python_code = '''def fibonacci(n): - if n <= 1: - return n - return fibonacci(n-1) + fibonacci(n-2)''' - -content = [ - heading(1, "Code Example"), - paragraph("Here's a Fibonacci function:"), - - code_block( - code=python_code, - language="python" - ) -] - -client.replace_json_document(document_id, content) -``` - -**Supported Languages:** -- Python, JavaScript, TypeScript, Java, C++, Go, Rust -- SQL, JSON, YAML, XML, HTML, CSS, SCSS -- Bash, Shell, PowerShell -- Markdown, LaTeX -- And 50+ more languages - -**Features:** -- Syntax highlighting based on language -- Multiline code support -- Optional language parameter -- Empty blocks supported - ---- - -## Embed Block - -Embed external content from various platforms (YouTube, Figma, Vimeo, CodeSandbox, GitHub Gist, Miro, Iframe). - -**Structure:** -```json -{ - "type": "embed", - "attrs": { - "uid": "uniqueIdXYZ", - "custom": 1, - "contenteditable": "false", - "size": "medium", - "isContentHidden": false - }, - "content": [ - { - "type": "text", - "text": "{\"type\":\"YouTube\",\"url\":\"https://www.youtube.com/watch?v=dQw4w9WgXcQ\",\"extractedUrl\":\"https://www.youtube.com/watch?v=dQw4w9WgXcQ\"}" - } - ] -} -``` - -**Embed Data Fields:** -- `type` - Embed type: `"YouTube"`, `"Figma"`, `"Vimeo"`, `"CodeSandbox"`, `"GitHub Gist"`, `"Miro"`, `"Iframe"` -- `url` - URL of content to embed -- `extractedUrl` - Extracted/processed URL (usually same as url) -- `isContentHidden` - Optional, for Figma/Miro to hide content by default - -**Attributes:** -- `uid` - Unique block identifier -- `custom` - Always 1 for custom blocks -- `contenteditable` - Should be "false" -- `size` - Display size: `"small"`, `"medium"`, or `"large"` -- `isContentHidden` - Whether content is hidden by default - -**Using Helper (Recommended):** -```python -from vaiz import embed_block, heading, paragraph, horizontal_rule - -content = [ - heading(1, "Project Resources"), - - # YouTube video - heading(2, "Demo Video"), - embed_block( - url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", - size="large" - ), - - horizontal_rule(), - - # Figma design - heading(2, "Design Files"), - embed_block( - url="https://www.figma.com/file/example", - size="large", - is_content_hidden=True - ), - - horizontal_rule(), - - # Live code sandbox - heading(2, "Code Example"), - embed_block(url="https://codesandbox.io/s/example") -] - -client.replace_json_document(document_id, content) -``` - -See [EmbedType enum](./enums#embedtype) for type-safe values. - -**Parameters:** -- `url` - URL of content to embed -- `embed_type` - Type of embed (optional, see [EmbedType](./enums#embedtype)) -- `size` - Display size: `"small"`, `"medium"`, `"large"` (default: `"medium"`) -- `is_content_hidden` - Hide content by default (default: `False`) - ---- - -## Image Block - -Embedded images in documents and task descriptions. - -**Structure:** -```json -{ - "type": "image-block", - "attrs": { - "uid": "uniqueIdXYZ", - "custom": 1, - "contenteditable": "false", - "widthPercent": 100 - }, - "content": [ - { - "type": "text", - "text": "{\"id\":\"img123\",\"src\":\"https://...\",\"fileName\":\"screenshot.png\",\"fileType\":\"image/png\",\"extension\":\"png\",\"title\":\"screenshot.png\",\"fileSize\":123456,\"fileId\":\"file_id\",\"dimensions\":[800,600],\"aspectRatio\":1.33,\"caption\":\"Screenshot\"}" - } - ] -} -``` - -**Image Data Fields:** -- `id` - Unique image ID (generated) -- `src` - Image URL (from upload_file) -- `fileName` - File name -- `fileType` - MIME type (e.g., "image/png", "image/jpeg") -- `extension` - File extension (e.g., "png", "jpg") -- `title` - Image title (usually same as fileName) -- `fileSize` - File size in bytes -- `fileId` - File ID from upload_file response -- `dimensions` - Optional [width, height] array -- `aspectRatio` - Optional aspect ratio (width/height) -- `caption` - Optional caption text -- `dominantColor` - Optional color object - -**Attributes:** -- `uid` - Unique block identifier -- `custom` - Always 1 for custom blocks -- `contenteditable` - Should be "false" -- `widthPercent` - Display width percentage (1-100) - -**Using Helper (Recommended):** -```python -from vaiz import image_block, heading, paragraph - -# Upload image first -uploaded = client.upload_file("photo.jpg") - -# Create image block - simple! Just pass the file -image = image_block( - file=uploaded.file, - caption="Product photo", - width_percent=75 -) - -content = [ - heading(1, "Gallery"), - paragraph("Check out our latest product:"), - image -] - -client.replace_json_document(document_id, content) -``` - -**Common Use Cases:** -- Screenshots in bug reports -- Product images in documentation -- Diagrams and flowcharts -- Photo galleries -- Visual examples in guides - ---- - -## Files Block - -File attachments (PDFs, documents, archives, etc.) in documents. - -**Structure:** -```json -{ - "type": "files", - "attrs": { - "uid": "uniqueIdDEF", - "custom": 1, - "contenteditable": "false" - }, - "content": [ - { - "type": "text", - "text": "{\"files\":[{\"id\":\"fileItem1\",\"fileId\":\"uploaded_file_id\",\"createAt\":1234567890000,\"url\":\"https://...\",\"extension\":\"pdf\",\"name\":\"Report.pdf\",\"size\":245678,\"type\":\"Pdf\"}]}" - } - ] -} -``` - -**Files Data Structure:** -- `files` - Array of file items - -**File Item Fields:** -- `id` - Unique file item ID (generated) -- `fileId` - File ID from upload_file response -- `createAt` - Timestamp in milliseconds -- `url` - File download URL -- `extension` - File extension -- `name` - Display name -- `size` - File size in bytes -- `type` - File type category (see below) -- `dominantColor` - Optional color object - -**File Type Categories:** -- `Pdf` - PDF documents -- `Image` - Images (prefer image_block instead) -- `Video` - Video files -- `Excel` - Spreadsheets (.xlsx, .xls, .csv) -- `PowerPoint` - Presentations (.pptx, .ppt) -- `Word` - Word documents (.docx, .doc) -- `Archive` - Compressed files (.zip, .rar, .tar.gz) -- `Text` - Text files (.txt, .md) -- `Code` - Source code files -- `Other` - Generic files - -**Using Helper (Recommended):** -```python -from vaiz import files_block, heading, paragraph - -# Upload files -pdf = client.upload_file("report.pdf") -excel = client.upload_file("data.xlsx") - -# Create file items -files = [ - { - "fileId": pdf.file.id, - "url": pdf.file.url, - "name": "Q4 Report.pdf", - "size": pdf.file.size, - "extension": "pdf", - "type": "Pdf" - }, - { - "fileId": excel.file.id, - "url": excel.file.url, - "name": "Sales Data.xlsx", - "size": excel.file.size, - "extension": "xlsx", - "type": "Excel" - } -] - -content = [ - heading(1, "Quarterly Report"), - paragraph("Attached documents:"), - files_block(*files) -] - -client.replace_json_document(document_id, content) -``` - -**Common Use Cases:** -- Report attachments -- Source code archives -- Documentation PDFs -- Data exports (CSV, Excel) -- Log files -- Configuration files - -**Combining Images and Files:** -```python -from vaiz import heading, paragraph, image_block, files_block, horizontal_rule - -# Upload files -screenshot = client.upload_file("app.png") -manual = client.upload_file("manual.pdf") -source = client.upload_file("source.zip") - -content = [ - heading(1, "Release v2.0"), - - # Image - paragraph("New interface:"), - image_block( - file=screenshot.file, - caption="Main screen" - ), - - horizontal_rule(), - - # Files - paragraph("Download resources:"), - files_block( - { - "fileId": manual.file.id, - "url": manual.file.url, - "name": "User Manual.pdf", - "size": manual.file.size, - "extension": "pdf", - "type": "Pdf" - }, - { - "fileId": source.file.id, - "url": source.file.url, - "name": "Source Code.zip", - "size": source.file.size, - "extension": "zip", - "type": "Archive" - } - ) -] - -client.replace_json_document(document_id, content) -``` - ---- - -## Supported Elements - -### Blocks - -| Node Type | Description | Example | -|-----------|-------------|---------| -| `text` | Text content with optional marks | `{"type": "text", "text": "..."}` | -| `paragraph` | Paragraph block | `{"type": "paragraph", "content": [...]}` | -| `heading` | Heading (H1-H6) with UID | `{"type": "heading", "attrs": {"level": 1, "uid": "..."}}` | -| `bulletList` | Unordered list | `{"type": "bulletList", "content": [...]}` | -| `orderedList` | Numbered list | `{"type": "orderedList", "content": [...]}` | -| `listItem` | List item | `{"type": "listItem", "content": [...]}` | -| `taskList` | Checklist with checkable items | `{"type": "taskList", "attrs": {"uid": "..."}, "content": [...]}` | -| `taskItem` | Checklist item with checkbox | `{"type": "taskItem", "attrs": {"checked": false}, "content": [...]}` | -| `blockquote` | Blockquote for quotes/callouts | `{"type": "blockquote", "content": [...]}` | -| `details` | Collapsible section | `{"type": "details", "content": [...]}` | -| `extension-table` | Table with rows | `{"type": "extension-table", "attrs": {"uid": "..."}, "content": [...]}` | -| `tableRow` | Table row with cells/headers | `{"type": "tableRow", "attrs": {"showRowNumbers": false}, "content": [...]}` | -| `tableCell` | Table data cell | `{"type": "tableCell", "attrs": {"colspan": 1, "rowspan": 1}, "content": [...]}` | -| `tableHeader` | Table header cell (th) | `{"type": "tableHeader", "attrs": {"colspan": 1, "rowspan": 1}, "content": [...]}` | -| `horizontalRule` | Horizontal divider line | `{"type": "horizontalRule"}` | -| `custom-mention` | Mention user/doc/task/milestone | `{"type": "custom-mention", "attrs": {"uid": "...", "custom": 1, "inline": true, "data": {"item": {"id": "...", "kind": "User"}}}}` | -| `image-block` | Embedded image | `{"type": "image-block", "attrs": {"uid": "...", "custom": 1}, "content": [...]}` | -| `files` | File attachments | `{"type": "files", "attrs": {"uid": "...", "custom": 1}, "content": [...]}` | -| `doc-siblings` | Navigation blocks (TOC/Anchors/Siblings) | `{"type": "doc-siblings", "attrs": {"uid": "...", "custom": 1}, "content": [...]}` | -| `codeBlock` | Code with syntax highlighting | `{"type": "codeBlock", "attrs": {"uid": "...", "language": "python"}, "content": [...]}` | - -### Marks - -| Mark Type | Description | -|-----------|-------------| -| `bold` | Bold text | -| `italic` | Italic text | -| `code` | Inline code | -| `link` | Hyperlink with href | - -## See Also - -- [Documents Guide](../guides/documents) - Document management -- [Document Structure Helpers](../guides/document-structure-helpers) - Helper functions -- [Tasks API](./tasks) - Task descriptions use the same format - diff --git a/docs-site/docs/api-reference/documents.md b/docs-site/docs/api-reference/documents.md index a2bded5..0dd0e2d 100644 --- a/docs-site/docs/api-reference/documents.md +++ b/docs-site/docs/api-reference/documents.md @@ -100,6 +100,75 @@ docs = client.get_documents( --- +### `replace_markdown_document` + +```python +replace_markdown_document(document_id: str, markdown: str) -> ReplaceMarkdownDocumentResponse +``` + +Replace document content with Markdown. Markdown is converted to native rich editor blocks on the server (headings, lists, tables, code blocks, checklists, links, etc.). + +**Parameters:** +- `document_id` - Document ID to replace content for +- `markdown` - New content as a Markdown string + +**Returns:** `ReplaceMarkdownDocumentResponse` (empty object on success) + +**Example:** +```python +client.replace_markdown_document( + document_id="document_id", + markdown="# Title\n\nSome **bold** text\n\n- item 1\n- item 2" +) +``` + +--- + +### `append_markdown_document` + +```python +append_markdown_document(document_id: str, markdown: str) -> AppendMarkdownDocumentResponse +``` + +Append Markdown content to the end of an existing document without removing existing content. + +**Parameters:** +- `document_id` - Document ID to append content to +- `markdown` - Content to append as a Markdown string + +**Returns:** `AppendMarkdownDocumentResponse` (empty object on success) + +**Example:** +```python +client.append_markdown_document( + document_id="document_id", + markdown="## Update\n\nAdditional notes" +) +``` + +--- + +### `get_markdown_document` + +```python +get_markdown_document(document_id: str) -> str +``` + +Fetch document content rendered as Markdown. Returns an empty string for documents without rich content. + +**Parameters:** +- `document_id` - Document ID to fetch + +**Returns:** `str` - The document content as a Markdown string + +**Example:** +```python +markdown = client.get_markdown_document("document_id") +print(markdown) +``` + +--- + ## Models ### Document @@ -246,51 +315,65 @@ class GetDocumentsPayload: --- -### GetDocumentRequest +### ReplaceMarkdownDocumentRequest ```python -class GetDocumentRequest: +class ReplaceMarkdownDocumentRequest: document_id: str # Required - Document ID + markdown: str # Required - New content as Markdown ``` --- -### ReplaceDocumentRequest +### ReplaceMarkdownDocumentResponse ```python -class ReplaceDocumentRequest: +class ReplaceMarkdownDocumentResponse: + # Empty response on success + pass +``` + +--- + +### AppendMarkdownDocumentRequest + +```python +class AppendMarkdownDocumentRequest: document_id: str # Required - Document ID - description: str # Required - New document content + markdown: str # Required - Content to append as Markdown ``` --- -### ReplaceDocumentResponse +### AppendMarkdownDocumentResponse ```python -class ReplaceDocumentResponse: +class AppendMarkdownDocumentResponse: # Empty response on success pass ``` --- -### ReplaceJSONDocumentRequest +### GetMarkdownDocumentRequest ```python -class ReplaceJSONDocumentRequest: +class GetMarkdownDocumentRequest: document_id: str # Required - Document ID - content: List[Dict[str, Any]] # Required - JSONContent array in document structure format ``` --- -### ReplaceJSONDocumentResponse +### GetMarkdownDocumentResponse ```python -class ReplaceJSONDocumentResponse: - # Empty response on success - pass +class GetMarkdownDocumentResponse: + type: str # Response type ("GetMarkdownDocument") + payload: GetMarkdownDocumentPayload # Payload with markdown string + + @property + def markdown(self) -> str: # Convenience property + ... ``` --- diff --git a/docs-site/docs/api-reference/enums.md b/docs-site/docs/api-reference/enums.md index 97f3142..8404af9 100644 --- a/docs-site/docs/api-reference/enums.md +++ b/docs-site/docs/api-reference/enums.md @@ -82,46 +82,6 @@ CommentReactionType.PARTY # 🎉 --- -## EmbedType - -Embed content types for document embed blocks: - -```python -from vaiz import EmbedType - -EmbedType.YOUTUBE # YouTube videos -EmbedType.FIGMA # Figma design files -EmbedType.VIMEO # Vimeo videos -EmbedType.CODESANDBOX # CodeSandbox code sandboxes -EmbedType.GITHUB_GIST # GitHub Gists -EmbedType.MIRO # Miro whiteboards -EmbedType.IFRAME # Generic iframe (default) -``` - -**Usage:** - -```python -from vaiz import embed_block, EmbedType - -# YouTube video -embed_block( - url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", - embed_type=EmbedType.YOUTUBE -) - -# Figma design -embed_block( - url="https://www.figma.com/file/example", - embed_type=EmbedType.FIGMA, - is_content_hidden=True -) - -# Default to Iframe when embed_type not specified -embed_block(url="https://example.com/embed") -``` - ---- - ## Kind Entity types for history and documents: diff --git a/docs-site/docs/api-reference/members.md b/docs-site/docs/api-reference/members.md index 463489b..f1aed20 100644 --- a/docs-site/docs/api-reference/members.md +++ b/docs-site/docs/api-reference/members.md @@ -17,7 +17,7 @@ Complete reference for member-related methods. get_space_members() -> GetSpaceMembersResponse ``` -Get all members in the current space. +Get all members in the current space. Bot members (AI, automation, and integration bots) are excluded from the result. **Returns:** `GetSpaceMembersResponse` with list of members diff --git a/docs-site/docs/api-reference/overview.md b/docs-site/docs/api-reference/overview.md index 9714c6b..6275bfd 100644 --- a/docs-site/docs/api-reference/overview.md +++ b/docs-site/docs/api-reference/overview.md @@ -22,7 +22,6 @@ Complete reference documentation for all Vaiz SDK methods, models, and enums. ### Content Management - [Documents](./documents) - Document management methods -- [Document Structure](./document-structure) - Rich content format for documents and task descriptions - [Comments](./comments) - Comment and reaction methods - [Files](./files) - File upload methods diff --git a/docs-site/docs/api-reference/tasks.md b/docs-site/docs/api-reference/tasks.md index 874d569..0c32a77 100644 --- a/docs-site/docs/api-reference/tasks.md +++ b/docs-site/docs/api-reference/tasks.md @@ -25,12 +25,12 @@ Create a new task with optional description and file upload. **Parameters:** - `task` - Task configuration (name, board required; group, project optional) -- `description` - Optional task description (plain text). For rich content, use `replace_json_document` with [document structure format](./document-structure) +- `description` - Optional task description (plain text). For rich content, use `replace_markdown_document` after creating the task - `file` - Optional file to upload and attach **Returns:** `TaskResponse` with created task -**Note:** To add rich formatted content to task descriptions, create the task first, then use `replace_json_document(task.document, content)`. See [Document Structure](./document-structure) for details. +**Note:** To add rich formatted content to task descriptions, create the task first, then use `replace_markdown_document(task.document, markdown)`. See [Tasks Guide](../guides/tasks#task-descriptions) for details. --- @@ -343,6 +343,5 @@ class MoveTasksPayload: ## See Also - [Tasks Guide](../guides/tasks) - Usage examples and patterns -- [Document Structure](./document-structure) - Format for task descriptions with rich content - [Enums](./enums) - TaskPriority and other enums diff --git a/docs-site/docs/guides/comments.md b/docs-site/docs/guides/comments.md index 266801a..748d088 100644 --- a/docs-site/docs/guides/comments.md +++ b/docs-site/docs/guides/comments.md @@ -2,41 +2,68 @@ sidebar_position: 3 sidebar_label: Comments title: Working with Comments — Post, Reply & React | Vaiz Python SDK -description: Learn how to create comments, replies, and manage reactions in your Vaiz projects using the Python SDK. Includes HTML formatting and file attachments. +description: Learn how to create comments, replies, and manage reactions in your Vaiz projects using the Python SDK. Includes Markdown formatting and file attachments. --- # Comments Working with comments, replies, and reactions. +Markdown is the recommended way to write comment content — it is converted to rich comment blocks on the server. HTML `content` remains supported as the legacy format. `markdown` and `content` are mutually exclusive: provide exactly one of them. + ## Creating Comments -### Simple Comment +### Markdown Comment (Recommended) ```python response = client.post_comment( document_id="document_id", - content="

My comment

" + markdown="My comment with **bold** text" ) comment = response.comment print(f"Created: {comment.id}") ``` -### Comment with HTML +Markdown supports rich formatting: ```python -html = """ -

Comment with bold and italic

- +markdown = """ +Comment with **bold** and *italic* + +- Item 1 +- Item 2 + +`inline code` and [links](https://vaiz.app) """ response = client.post_comment( document_id="document_id", - content=html + markdown=markdown +) +``` + +### Comment with Mentions + +Mentions use the `@[label](kind:id)` syntax and trigger notifications for mentioned members: + +```python +member_id = client.get_profile().profile.member_id + +response = client.post_comment( + document_id="document_id", + markdown=f"ping @[Reviewer](user:{member_id}), please take a look" +) +``` + +Supported kinds: `user`, `task`, `document`, `milestone`, `project`, `board`. The label is cosmetic — only the kind and ID are stored. + +### HTML Comment (Legacy) + +```python +response = client.post_comment( + document_id="document_id", + content="

Comment with bold

" ) ``` @@ -52,7 +79,7 @@ doc = client.upload_file("report.pdf", UploadFileType.Pdf) # Post comment response = client.post_comment( document_id="document_id", - content="

Files attached

", + markdown="Files attached", file_ids=[img.file.id, doc.file.id] ) ``` @@ -63,13 +90,13 @@ response = client.post_comment( # Original comment original = client.post_comment( document_id="document_id", - content="

Original

" + markdown="Original" ) # Reply reply = client.post_comment( document_id="document_id", - content="

Reply

", + markdown="Reply", reply_to=original.comment.id ) ``` @@ -96,24 +123,24 @@ See [`CommentReactionType`](../api-reference/enums#commentreactiontype) for all ```python response = client.edit_comment( comment_id="comment_id", - content="

Updated content

" + markdown="Updated content with a [link](https://vaiz.app)" ) ``` ### Managing Files ```python -# Add files (must include content) +# Add files (must include markdown or content) response = client.edit_comment( comment_id="comment_id", - content="

Updated content

", + markdown="Updated content", add_file_ids=["new_file_id"] ) -# Remove files (must include content) +# Remove files (must include markdown or content) response = client.edit_comment( comment_id="comment_id", - content="

Updated content

", + markdown="Updated content", remove_file_ids=["file_id"] ) ``` @@ -127,16 +154,22 @@ print(f"Deleted: {response.comment.deleted_at}") ## Getting Comments +Comment content is returned as Markdown — the same format `post_comment` accepts. Mentions come back as `@[label](kind:id)`, so content can be edited and posted back without losing anything. + ```python response = client.get_comments(document_id="document_id") for comment in response.comments: print(f"Author: {comment.author_id}") - print(f"Content: {comment.content}") + print(f"Content: {comment.content}") # Markdown if comment.reply_to: print(f" Reply to: {comment.reply_to}") ``` +:::note Legacy comments +Comments created before the rich editor migration (`content_version` other than `2`) are returned as raw HTML. Check `comment.content_version` to tell the formats apart. +::: + ## Getting document_id ```python diff --git a/docs-site/docs/guides/document-structure-helpers.md b/docs-site/docs/guides/document-structure-helpers.md deleted file mode 100644 index 05b5649..0000000 --- a/docs-site/docs/guides/document-structure-helpers.md +++ /dev/null @@ -1,1303 +0,0 @@ ---- -sidebar_position: 16 -sidebar_label: Document Structure Helpers -title: Document Structure Helpers — Type-Safe Content Building | Vaiz Python SDK -description: Learn how to use type-safe helper functions to build valid document content with the Vaiz Python SDK. Includes paragraphs, headings, lists, tables, and more. ---- - -# Document Structure Helpers - -Type-safe helper functions for building valid document content. - -## Overview - -The document structure format is used for creating and editing rich content in Vaiz. This includes: - -- **Task descriptions** - Add rich formatted content to task descriptions -- **Standalone documents** - Project, Space, and Member documents -- **Any document content** - Universal format for all document types - -When working with `replace_json_document`, you can use document structure helper functions to build content in a type-safe, readable way instead of manually constructing JSON structures. - -### Why Use Helpers? - -Instead of manually writing JSON structures, helpers provide: -- **Type safety** - Correct structure guaranteed -- **Readability** - Clean, Pythonic syntax -- **Validation** - Only allows supported elements -- **Productivity** - Write less, accomplish more - -## Basic Helpers - -### Text - -Create text nodes with optional formatting: - -```python -from vaiz import text - -# Plain text -text("Hello World") - -# Bold text -text("Important", bold=True) - -# Italic text -text("Emphasis", italic=True) - -# Inline code -text("client.get_task()", code=True) - -# Combined formatting -text("Bold and Italic", bold=True, italic=True) - -# Link -text("Click here", link="https://vaiz.app") -``` - -### Paragraph - -Create paragraph nodes: - -```python -from vaiz import paragraph, text - -# Simple paragraph -paragraph("Hello World") - -# Paragraph with mixed formatting -paragraph( - "This is ", - text("bold text", bold=True), - " and this is ", - text("italic", italic=True), - "." -) -``` - -### Heading - -Create heading nodes (levels 1-6): - -```python -from vaiz import heading, text - -# H1 -heading(1, "Main Title") - -# H2 -heading(2, "Section Title") - -# With formatting -heading(1, text("Important Title", bold=True)) -``` - -### Links - -Create hyperlink text: - -```python -from vaiz import link_text, paragraph - -# Basic link -link_text("Visit Vaiz", "https://vaiz.app") - -# Bold link -link_text("Documentation", "https://docs.vaiz.app", bold=True) - -# In a paragraph -paragraph( - "Check out our ", - link_text("documentation", "https://docs.vaiz.app"), - " for more info." -) -``` - -## Lists - -### Bullet Lists - -Create unordered lists: - -```python -from vaiz import bullet_list, list_item, paragraph, text - -# Simple list from strings -bullet_list( - "First item", - "Second item", - "Third item" -) - -# Complex list items -bullet_list( - list_item(paragraph(text("Bold item", bold=True))), - list_item(paragraph("Regular item")), - "Simple string item" -) -``` - -### Ordered Lists - -Create numbered lists: - -```python -from vaiz import ordered_list, list_item, paragraph - -# Simple numbered list -ordered_list( - "First step", - "Second step", - "Third step" -) - -# Start from specific number -ordered_list( - "Item 5", - "Item 6", - "Item 7", - start=5 -) - -# Complex items -ordered_list( - list_item(paragraph("Step 1: ", text("Install SDK", bold=True))), - list_item(paragraph("Step 2: ", text("Configure", bold=True))) -) -``` - -### Nested Lists - -Create multi-level list structures: - -```python -from vaiz import bullet_list, list_item, paragraph - -bullet_list( - "Top level item", - list_item( - paragraph("Parent item"), - bullet_list( - "Nested item 1", - "Nested item 2", - list_item( - paragraph("Sub-nested item"), - bullet_list("Level 3 item") - ) - ) - ), - "Another top item" -) -``` - -## Tables - -### Simple Table - -Create tables with headers and data rows: - -```python -from vaiz import table, table_row - -# Simple status table -# Note: First row is typically displayed as headers in UI -status_table = table( - table_row("Task", "Status", "Priority"), # Header row - table_row("Design mockups", "Done", "High"), - table_row("API development", "In Progress", "High"), - table_row("Documentation", "Todo", "Medium") -) -``` - -### Table with Header Cells - -Use `table_header()` to create proper header cells (`` in HTML): - -```python -from vaiz import table, table_row, table_header - -# Table with semantic header cells -status_table = table( - table_row( - table_header("Task"), - table_header("Status"), - table_header("Priority") - ), - table_row("Design mockups", "Done", "High"), - table_row("API development", "In Progress", "High"), - table_row("Documentation", "Todo", "Medium") -) -``` - -**Benefits of `table_header()`:** -- Semantic HTML structure (`` vs ``) -- Better accessibility for screen readers -- Consistent header styling -- Support for colspan/rowspan on headers - -### Table with Formatting - -Add formatting to table content: - -```python -from vaiz import table, table_row, table_cell, table_header, text, paragraph - -formatted_table = table( - table_row( - table_header(paragraph(text("Name", bold=True))), - table_header(paragraph(text("Status", bold=True))) - ), - table_row( - table_cell(paragraph(text("Task 1", bold=True))), - table_cell("✅ Done") - ), - table_row( - "Task 2", - table_cell(paragraph(text("In Progress", italic=True))) - ) -) -``` - -### Complex Table Example - -```python -from vaiz import heading, paragraph, table, table_row, table_header, text - -content = [ - heading(1, "📊 Project Metrics"), - - paragraph("Current sprint status:"), - - table( - table_row( - table_header("Metric"), - table_header("Count"), - table_header("Percentage") - ), - table_row("Completed", "28", "60%"), - table_row("In Progress", "12", "26%"), - table_row("Todo", "7", "14%") - ), - - paragraph(text("Total: 47 tasks", bold=True)) -] -``` - -### Table with Merged Headers (colspan/rowspan) - -```python -from vaiz import table, table_row, table_header, table_cell, text, paragraph - -# Quarterly report table with merged header -quarterly_table = table( - # Main header spanning all columns - table_row( - table_header(paragraph(text("Q1-Q4 Performance", bold=True)), colspan=5) - ), - # Subheaders - table_row( - table_header("Metric"), - table_header("Q1"), - table_header("Q2"), - table_header("Q3"), - table_header("Q4") - ), - # Data rows - table_row("Revenue", "$100K", "$120K", "$150K", "$180K"), - table_row("Users", "1,000", "1,500", "2,200", "3,000") -) -``` - -## Visual Elements - -### Horizontal Rule - -Create a horizontal divider line: - -```python -from vaiz import horizontal_rule - -# Horizontal rule (native HTML
style) -horizontal_rule() -``` - -### Blockquote - -Create blockquotes for quoted text or callouts: - -```python -from vaiz import blockquote, paragraph, text - -# Blockquote with formatting -blockquote( - paragraph( - text("Important: ", bold=True), - "This is a critical note that stands out." - ) -) -``` - -**Use cases for blockquotes:** -- Quotations from external sources -- Important callouts or warnings -- Key insights or takeaways -- API documentation notes -- User testimonials or feedback - -## Mention Blocks - -Create interactive mentions to reference users, documents, tasks, and milestones. - -### Overview - -Mention blocks create clickable references to entities in your workspace. When mentioned, users receive notifications and can click to navigate to the referenced item. - -### User Mentions - -Reference team members: - -```python -from vaiz import mention_user, paragraph, text - -# Get member ID from profile -profile = client.get_profile() -member_id = profile.profile.member_id - -# Mention a user -paragraph( - text("Assigned to "), - mention_user(member_id) -) -``` - -### Document Mentions - -Link to other documents: - -```python -from vaiz import mention_document, paragraph, text - -# Get document ID -from vaiz import GetDocumentsRequest, Kind - -docs = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=space_id) -) -doc_id = docs.payload.documents[0].id - -# Mention a document -paragraph( - text("See "), - mention_document(doc_id), - text(" for details") -) -``` - -### Task Mentions - -Reference tasks: - -```python -from vaiz import mention_task, paragraph, text, GetTasksRequest - -# Get task ID -tasks = client.get_tasks(GetTasksRequest()) -task_id = tasks.payload.tasks[0].id - -# Mention a task -paragraph( - text("Related to "), - mention_task(task_id) -) -``` - -### Milestone Mentions - -Link to milestones: - -```python -from vaiz import mention_milestone, paragraph, text - -# Get milestone ID -milestones = client.get_milestones() -milestone_id = milestones.milestones[0].id - -# Mention a milestone -paragraph( - text("Part of "), - mention_milestone(milestone_id) -) -``` - -### Multiple Mentions - -Combine multiple mentions in one paragraph: - -```python -from vaiz import paragraph, text, mention_user, mention_task, mention_milestone - -paragraph( - text("User "), - mention_user(member_id), - text(" is assigned to "), - mention_task(task_id), - text(" in milestone "), - mention_milestone(milestone_id) -) -``` - -### Mentions in Lists - -Use mentions in bullet or ordered lists: - -```python -from vaiz import bullet_list, list_item, paragraph, text, mention_user, mention_task - -bullet_list( - list_item(paragraph( - text("Assignee: "), - mention_user(member_id) - )), - list_item(paragraph( - text("Related task: "), - mention_task(task_id) - )) -) -``` - -### Mentions in Tables - -Create tables with mention references: - -```python -from vaiz import table, table_row, table_header, table_cell, paragraph -from vaiz import mention_user, mention_task - -table( - table_row( - table_header("Assignee"), - table_header("Task"), - table_header("Status") - ), - table_row( - table_cell(paragraph(mention_user(member_id))), - table_cell(paragraph(mention_task(task_id))), - table_cell("In Progress") - ) -) -``` - -### Generic Mention Function - -Use the generic `mention()` function for any entity type: - -```python -from vaiz import mention - -# Explicitly specify the kind -user_mention = mention(member_id, "User") -doc_mention = mention(doc_id, "Document") -task_mention = mention(task_id, "Task") -milestone_mention = mention(milestone_id, "Milestone") -``` - -### Supported Mention Types - -| Function | Entity Type | Description | -|----------|-------------|-------------| -| `mention_user(member_id)` | User | Mention a team member | -| `mention_document(id)` | Document | Reference a document | -| `mention_task(id)` | Task | Reference a task | -| `mention_milestone(id)` | Milestone | Reference a milestone | -| `mention(id, kind)` | Any | Generic mention with explicit kind | - -### Complete Example with Mentions - -```python -from vaiz import VaizClient, GetDocumentsRequest, GetTasksRequest, Kind -from vaiz import ( - heading, paragraph, text, bullet_list, list_item, - table, table_row, table_header, table_cell, - mention_user, mention_task, mention_document -) - -client = VaizClient(api_key="...", space_id="...") - -# Get entity IDs -profile = client.get_profile() -member_id = profile.profile.member_id - -tasks = client.get_tasks(GetTasksRequest()) -task_id = tasks.payload.tasks[0].id - -docs = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=client.space_id) -) -doc_id = docs.payload.documents[0].id - -# Create document with mentions -content = [ - heading(1, "Meeting Notes"), - - paragraph( - text("Attendees: "), - mention_user(member_id) - ), - - heading(2, "Action Items"), - - bullet_list( - list_item(paragraph( - mention_user(member_id), - text(" will complete "), - mention_task(task_id) - )), - list_item(paragraph( - text("Update "), - mention_document(doc_id) - )) - ), - - heading(2, "Task Assignments"), - - table( - table_row( - table_header("Assignee"), - table_header("Task") - ), - table_row( - table_cell(paragraph(mention_user(member_id))), - table_cell(paragraph(mention_task(task_id))) - ) - ) -] - -# Update document -client.replace_json_document(document_id, content) -``` - -## Complete Example - -Build a full document with helpers: - -```python -from vaiz import ( - VaizClient, text, paragraph, heading, - bullet_list, ordered_list, list_item, - link_text, horizontal_rule, blockquote -) - -client = VaizClient(token="your_token") - -# Build content using helpers -content = [ - # Title - heading(1, "📚 Project Documentation"), - - # Subtitle - paragraph( - text("Status: ", italic=True), - text("Active", bold=True) - ), - - horizontal_rule(), - - # Overview - heading(2, "Overview"), - paragraph( - "This project uses ", - text("Vaiz SDK", bold=True), - " for task management. Visit ", - link_text("docs", "https://docs.vaiz.app"), - " to learn more." - ), - - # Important note - blockquote( - paragraph( - text("Note: ", bold=True), - "This SDK provides type-safe helpers for building document content." - ) - ), - - # Features - heading(2, "✨ Features"), - bullet_list( - "Real-time collaboration", - "Rich text editing", - list_item( - paragraph(text("API Integration", bold=True)), - bullet_list( - "Python SDK", - "REST API", - "WebSocket support" - ) - ) - ), - - # Quick Start - heading(2, "Quick Start"), - ordered_list( - list_item(paragraph("Install: ", text("pip install vaiz-sdk", code=True))), - list_item(paragraph("Import: ", text("from vaiz import VaizClient", code=True))), - list_item(paragraph("Use: ", text("client = VaizClient(token='...')", code=True))) - ), - - # Footer - horizontal_rule(), - paragraph( - text("Built with ", italic=True), - text("document structure helpers", code=True) - ) -] - -# Replace document -client.replace_json_document("document_id", content) -``` - -## Files and Images - -### Image Block - -Embed images in documents and task descriptions: - -```python -from vaiz import image_block, heading, paragraph - -# First, upload the image -uploaded = client.upload_file("screenshots/dashboard.png") - -# Create image block - simple! Just pass the file -image = image_block( - file=uploaded.file, - caption="Main dashboard view" -) - -content = [ - heading(1, "App Screenshots"), - paragraph("Here's our main dashboard:"), - image -] - -client.replace_json_document(document_id, content) -``` - -### Image with Dimensions - -Specify image dimensions and aspect ratio: - -```python -from vaiz import image_block, paragraph -from PIL import Image - -# Get image dimensions -img = Image.open("photo.jpg") -width, height = img.size - -# Upload image -uploaded = client.upload_file("photo.jpg") - -# Create image block with custom width -image = image_block( - file=uploaded.file, - caption="Product photo", - width_percent=75 # Display at 75% width -) - -content = [ - paragraph("Product image:"), - image -] -``` - -### Multiple Images - -Add multiple images to a document: - -```python -from vaiz import image_block, heading, paragraph - -# Upload images -screenshot1 = client.upload_file("screen1.png") -screenshot2 = client.upload_file("screen2.png") -screenshot3 = client.upload_file("screen3.png") - -content = [ - heading(1, "Feature Gallery"), - - paragraph("Login screen:"), - image_block( - file=screenshot1.file, - caption="User authentication" - ), - - paragraph("Dashboard:"), - image_block( - file=screenshot2.file, - caption="Main dashboard view" - ), - - paragraph("Settings:"), - image_block( - file=screenshot3.file, - caption="Configuration panel" - ) -] - -client.replace_json_document(document_id, content) -``` - -### Files Block - -Attach files (PDFs, documents, etc.) to documents: - -```python -from vaiz import files_block, heading, paragraph - -# Upload files -pdf = client.upload_file("report.pdf") -excel = client.upload_file("data.xlsx") - -# Create file items -file1 = { - "fileId": pdf.file.id, - "url": pdf.file.url, - "name": "Q4 Report.pdf", - "size": pdf.file.size, - "extension": "pdf", - "type": "Pdf" -} - -file2 = { - "fileId": excel.file.id, - "url": excel.file.url, - "name": "Sales Data.xlsx", - "size": excel.file.size, - "extension": "xlsx", - "type": "Excel" -} - -content = [ - heading(1, "Quarterly Report"), - paragraph("Attached documents:"), - files_block(file1, file2) -] - -client.replace_json_document(document_id, content) -``` - -### Single File Attachment - -Attach a single file: - -```python -from vaiz import files_block, paragraph - -# Upload document -uploaded = client.upload_file("presentation.pptx") - -# Create file item -file_item = { - "fileId": uploaded.file.id, - "url": uploaded.file.url, - "name": "Product Presentation.pptx", - "size": uploaded.file.size, - "extension": "pptx", - "type": "PowerPoint" -} - -content = [ - paragraph("Download the presentation:"), - files_block(file_item) -] -``` - -### Mixed File Types - -Attach various file types together: - -```python -from vaiz import files_block, heading, paragraph - -# Upload different file types -pdf = client.upload_file("manual.pdf") -video = client.upload_file("demo.mp4") -zip_file = client.upload_file("source.zip") - -files = [ - { - "fileId": pdf.file.id, - "url": pdf.file.url, - "name": "User Manual.pdf", - "size": pdf.file.size, - "extension": "pdf", - "type": "Pdf" - }, - { - "fileId": video.file.id, - "url": video.file.url, - "name": "Product Demo.mp4", - "size": video.file.size, - "extension": "mp4", - "type": "Video" - }, - { - "fileId": zip_file.file.id, - "url": zip_file.file.url, - "name": "Source Code.zip", - "size": zip_file.file.size, - "extension": "zip", - "type": "Archive" - } -] - -content = [ - heading(1, "Release Package"), - paragraph("Download all release materials:"), - files_block(*files) # Unpack list -] - -client.replace_json_document(document_id, content) -``` - -### Supported File Types - -The `type` field in file items can be: - -| Type | Extension Examples | Description | -|------|-------------------|-------------| -| `Pdf` | `.pdf` | PDF documents | -| `Image` | `.png`, `.jpg`, `.gif`, `.webp` | Images (use `image_block` instead) | -| `Video` | `.mp4`, `.mov`, `.avi` | Video files | -| `Excel` | `.xlsx`, `.xls`, `.csv` | Spreadsheets | -| `PowerPoint` | `.pptx`, `.ppt` | Presentations | -| `Word` | `.docx`, `.doc` | Word documents | -| `Archive` | `.zip`, `.rar`, `.tar.gz` | Compressed archives | -| `Text` | `.txt`, `.md` | Text files | -| `Code` | `.py`, `.js`, `.java`, etc. | Source code files | -| `Other` | Any other extension | Generic files | - -### Complete Files and Images Example - -```python -from vaiz import ( - VaizClient, heading, paragraph, text, - image_block, files_block, horizontal_rule -) - -client = VaizClient(api_key="...", space_id="...") - -# Upload all files -logo = client.upload_file("company_logo.png") -screenshot = client.upload_file("app_screen.png") -report_pdf = client.upload_file("annual_report.pdf") -data_csv = client.upload_file("data_export.csv") - -# Build document with mixed content -content = [ - heading(1, "📊 Annual Report 2024"), - - # Logo image - image_block( - file=logo.file, - width_percent=50, # Display at 50% width - caption="Company Logo" - ), - - horizontal_rule(), - - # Report content - heading(2, "Executive Summary"), - paragraph("Our annual performance and achievements..."), - - # Screenshot - heading(2, "New Features"), - paragraph("We launched a redesigned application:"), - image_block( - file=screenshot.file, - caption="New dashboard interface" - ), - - horizontal_rule(), - - # Downloadable files - heading(2, "📎 Attachments"), - paragraph(text("Download the full report and data:", bold=True)), - files_block( - { - "fileId": report_pdf.file.id, - "url": report_pdf.file.url, - "name": "Annual Report 2024.pdf", - "size": report_pdf.file.size, - "extension": "pdf", - "type": "Pdf" - }, - { - "fileId": data_csv.file.id, - "url": data_csv.file.url, - "name": "Financial Data.csv", - "size": data_csv.file.size, - "extension": "csv", - "type": "Excel" - } - ) -] - -# Update document -client.replace_json_document(document_id, content) -``` - -### Using with Task Descriptions - -Images and files work great in task descriptions: - -```python -from vaiz import CreateTaskRequest, image_block, files_block, heading, paragraph - -# Upload files -screenshot = client.upload_file("bug_screenshot.png") -log_file = client.upload_file("error_log.txt") - -# Build description with image and file -description_content = [ - heading(2, "Bug Report"), - paragraph("Application crashes when clicking submit button."), - - heading(3, "Screenshot"), - image_block( - file=screenshot.file, - caption="Error state" - ), - - heading(3, "Logs"), - files_block({ - "fileId": log_file.file.id, - "url": log_file.file.url, - "name": "error.log", - "size": log_file.file.size, - "extension": "txt", - "type": "Text" - }) -] - -# Create task with rich description -task = CreateTaskRequest( - name="Fix submit button crash", - description_json=description_content -) - -client.create_task(task) -``` - -## Navigation Blocks - -### TOC Block - Table of Contents - -Automatically generate a table of contents from all headings in the document: - -```python -from vaiz import toc_block, heading, paragraph - -content = [ - # Add TOC at the beginning - toc_block(), - - heading(1, "Introduction"), - paragraph("Welcome to our documentation"), - - heading(2, "Getting Started"), - paragraph("First steps..."), - - heading(2, "Advanced Topics"), - paragraph("Deep dive into features...") -] - -client.replace_json_document(document_id, content) -``` - -**Features:** -- Automatically indexes all headings (h1-h6) -- Creates clickable navigation links -- Shows hierarchical document structure -- Updates automatically when content changes - -**Note:** Headings created with `heading()` automatically get unique IDs required for TOC navigation. - -### Anchors Block - Related Documents - -Display related documents and backlinks: - -```python -from vaiz import anchors_block, heading, paragraph - -content = [ - anchors_block(), # Shows related documents - - heading(1, "Main Content"), - paragraph("Content here...") -] - -client.replace_json_document(document_id, content) -``` - -**Shows:** -- Documents that this document links to -- Documents that link to this one (backlinks) -- Related documents from the space -- Knowledge graph connections - -### Siblings Block - Previous/Next Navigation - -Display Previous/Next navigation between documents in a sequence: - -```python -from vaiz import siblings_block, heading, paragraph, horizontal_rule - -content = [ - heading(1, "Tutorial Part 2: Advanced Features"), - paragraph("Main tutorial content here..."), - - horizontal_rule(), - - # Navigation at the bottom - siblings_block() # Shows "← Previous" and "Next →" buttons -] - -client.replace_json_document(document_id, content) -``` - -**Features:** -- Shows Previous and Next documents in sequence -- Creates navigation buttons (Back/Forward) -- Typically placed at page bottom -- Perfect for linear document flows - -**Useful for:** -- Tutorial series (Part 1 → Part 2 → Part 3) -- Multi-chapter guides -- Documentation sequences -- Step-by-step workflows - -### Code Block - Syntax Highlighting - -Display code with syntax highlighting: - -```python -from vaiz import code_block, heading, paragraph - -python_code = '''def hello(): - print("Hello, World!") - return True''' - -content = [ - heading(1, "Code Example"), - paragraph("Here's a Python function:"), - - code_block( - code=python_code, - language="python" - ), - - paragraph("This code demonstrates a simple function.") -] - -client.replace_json_document(document_id, content) -``` - -**Supported Languages:** -- Python, JavaScript, TypeScript, Java, C++, Go, Rust -- SQL, JSON, YAML, XML, HTML, CSS -- Bash, Shell, PowerShell -- And 50+ more languages - -**Features:** -- Syntax highlighting -- Multiline code support -- Optional language specification -- Automatic formatting - -### Embed Block - External Content - -Embed external content from various platforms: - -```python -from vaiz import embed_block, heading, paragraph, horizontal_rule - -content = [ - heading(1, "Project Resources"), - - # YouTube video - heading(2, "Demo Video"), - paragraph("Watch our product demo:"), - embed_block( - url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", - size="large" - ), - - horizontal_rule(), - - # Figma design - heading(2, "Design Mockups"), - paragraph("View the design files:"), - embed_block( - url="https://www.figma.com/file/example", - size="large", - is_content_hidden=True - ), - - horizontal_rule(), - - # CodeSandbox - heading(2, "Live Code Example"), - paragraph("Try the code live:"), - embed_block(url="https://codesandbox.io/s/example"), - - horizontal_rule(), - - # Generic iframe (default) - heading(2, "External Tool"), - embed_block(url="https://example.com/embed") -] - -client.replace_json_document(document_id, content) -``` - -**Supported Embed Types:** -- YouTube - YouTube videos -- Figma - Figma design files -- Vimeo - Vimeo videos -- CodeSandbox - Interactive code sandboxes -- GitHub Gist - GitHub code snippets -- Miro - Miro whiteboards -- Iframe - Generic iframe embeds (default) - -See [EmbedType enum](../api-reference/enums#embedtype) for type-safe values. - -**Parameters:** -- `url` - URL of content to embed -- `embed_type` - Type of embed (optional, see [EmbedType](../api-reference/enums#embedtype)) -- `size` - Display size: `"small"`, `"medium"`, `"large"` (default: `"medium"`) -- `is_content_hidden` - Hide content by default for Figma/Miro (default: `False`) - -**Use Cases:** -- Video tutorials and demos -- Design system documentation -- Live code examples -- Interactive prototypes -- External tool integrations - -### Complete Navigation Example - -Combine all navigation blocks for optimal user experience: - -```python -from vaiz import ( - toc_block, anchors_block, siblings_block, - heading, paragraph, code_block, bullet_list, horizontal_rule -) - -content = [ - # Top navigation: TOC and related documents - toc_block(), - anchors_block(), - - horizontal_rule(), - - # Main content - heading(1, "API Documentation - Part 2"), - - paragraph("This guide explains how to use our API."), - - heading(2, "Authentication"), - bullet_list( - "Get API key from settings", - "Set environment variable", - "Initialize client" - ), - - heading(2, "Example"), - code_block( - code='client = VaizClient(api_key="...")', - language="python" - ), - - horizontal_rule(), - - # Bottom navigation: Previous/Next pages - siblings_block() # Shows "← Part 1" and "Part 3 →" -] - -client.replace_json_document(document_id, content) -``` - -**Navigation Layout:** -- **Top**: TOC (internal navigation) + Anchors (related docs) -- **Content**: Your document content -- **Bottom**: Siblings (Previous/Next in sequence) - -## Supported Elements - -All helper functions create nodes compatible with the document editor: - -### Nodes -- ✅ `text()` - Text with formatting marks -- ✅ `paragraph()` - Paragraph blocks -- ✅ `heading(level)` - Headings (H1-H6) with automatic UID -- ✅ `bullet_list()` - Unordered lists -- ✅ `ordered_list()` - Numbered lists -- ✅ `list_item()` - List items with content -- ✅ `blockquote()` - Blockquotes for quotes and callouts -- ✅ `details()` - Collapsible sections -- ✅ `table()` - Tables with rows and cells -- ✅ `table_row()` - Table row -- ✅ `table_cell()` - Table cell (use for both data and headers) -- ✅ `table_header()` - Table header cell (semantic th) -- ✅ `mention_user()` - Mention a user -- ✅ `mention_document()` - Reference a document -- ✅ `mention_task()` - Reference a task -- ✅ `mention_milestone()` - Reference a milestone -- ✅ `mention(id, kind)` - Generic mention -- ✅ `image_block()` - Embed images -- ✅ `files_block()` - Attach files -- ✅ `code_block()` - Code with syntax highlighting -- ✅ `embed_block()` - Embed external content (YouTube, Figma, etc.) - -### Navigation Blocks -- ✅ `toc_block()` - Automatic table of contents -- ✅ `anchors_block()` - Related documents and backlinks -- ✅ `siblings_block()` - Same-level documents navigation - -### Marks (Formatting) -- ✅ `bold=True` - Bold text -- ✅ `italic=True` - Italic text -- ✅ `code=True` - Inline code -- ✅ `link="url"` - Hyperlinks - -### Utilities -- ✅ `horizontal_rule()` - Horizontal divider line -- ✅ `link_text()` - Formatted hyperlinks - -## Benefits - -**Type Safety:** -- Type hints for all parameters -- IDE autocomplete support -- Catch errors before runtime - -**Readability:** -- Clean, Pythonic syntax -- Self-documenting code -- Easy to understand structure - -**Validation:** -- Only allows supported document elements -- Prevents invalid structures -- Ensures API compatibility - -## See Also - -- [Documents Guide](./documents) - Document management -- [API Reference: Document Structure](../api-reference/document-structure) - Content methods and format reference -- [API Reference: Documents](../api-reference/documents) - Document management API -- [Ready-to-Run Examples](../patterns/ready-to-run) - Complete examples - diff --git a/docs-site/docs/guides/documents.md b/docs-site/docs/guides/documents.md index a753ae5..662b2e1 100644 --- a/docs-site/docs/guides/documents.md +++ b/docs-site/docs/guides/documents.md @@ -147,7 +147,7 @@ content = """ [Meeting content here] """ -client.replace_document(doc_id, content) +client.replace_markdown_document(doc_id, content) print(f"✅ Document created and populated: {doc_id}") ``` @@ -323,9 +323,9 @@ if updated_doc: In addition to listing and creating documents, you can work with their content using document API methods. -### Markdown Methods (Recommended) +### Markdown Methods -The simplest and recommended way to read and write document content is Markdown. It is converted to native rich editor blocks on the server (headings, lists, tables, code blocks, checklists, links, etc.): +Document content is read and written as Markdown. It is converted to native rich editor blocks on the server (headings, lists, tables, code blocks, checklists, links, etc.): ```python # Replace document content with Markdown @@ -345,151 +345,38 @@ markdown = client.get_markdown_document("document_id") print(markdown) ``` -The JSON-based methods below remain supported for backward compatibility. - -### Get Document Content - -Retrieve the JSON content of any document: - -```python -# Get document content by ID -content = client.get_json_document("document_id") -print(content) # Returns parsed JSON structure -``` - -This method is universal and works for: +These methods are universal and work for: - Task descriptions - Standalone documents - Any document by its ID -### Replace Document Content (Plain Text) - -Replace the entire content of a document with plain text: - -```python -# Replace document content with plain text -client.replace_document( - document_id="document_id", - description="New content here" -) -``` - -**Use cases:** -- Updating task descriptions programmatically -- Bulk content updates -- Template-based content generation - -### Replace Document Content (Rich JSON) +**Supported Markdown features:** +- **Rich text formatting**: Bold, italic, strikethrough, inline code +- **Headings**: Multiple levels (h1-h6) +- **Lists**: Bullet lists, numbered lists, task lists (`- [ ]` / `- [x]`) +- **Links**: Standard Markdown links +- **Code blocks**: Fenced code blocks with syntax highlighting +- **Tables**: Markdown tables +- **Mentions**: `@[label](kind:id)` syntax (see below) +- **And more**: Blockquotes, horizontal rules, etc. -Replace document content with structured rich content. +### Mentions -**💡 Tip:** Use [Document Structure helper functions](./document-structure-helpers) for type-safe, readable content creation! +Mentions are written with a dedicated Markdown syntax and become live mention chips in the editor: ```python -# Replace with rich formatted content -json_content = [ - { - "type": "heading", - "attrs": {"level": 1}, - "content": [ - {"type": "text", "text": "Project Overview"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "This is "}, - { - "type": "text", - "marks": [{"type": "bold"}], - "text": "bold text" - }, - {"type": "text", "text": " and this is "}, - { - "type": "text", - "marks": [{"type": "italic"}], - "text": "italic text" - } - ] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "First item"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Second item"} - ] - } - ] - } - ] - } -] - -client.replace_json_document( - document_id="document_id", - content=json_content +member_id = client.get_profile().profile.member_id + +client.replace_markdown_document( + document_id, + f"Please review, @[Reviewer](user:{member_id})\n\n" + f"Related: @[Spec](document:{document_id})" ) ``` -**Document Structure Format features:** -- **Rich text formatting**: Bold, italic, underline, strikethrough, code -- **Headings**: Multiple levels (h1-h6) -- **Lists**: Bullet lists, numbered lists, task lists -- **Links**: Hyperlinks with custom attributes -- **Code blocks**: Syntax highlighted code -- **Tables**: Structured tabular data -- **And more**: Blockquotes, horizontal rules, mentions, etc. - -**Use cases:** -- Creating structured documentation programmatically -- Importing content from other systems with formatting -- Generating reports with rich formatting -- Building document templates with complex layouts - -**With helper functions (recommended):** -```python -from vaiz import heading, paragraph, text, bullet_list, link_text, horizontal_rule - -content = [ - heading(1, "📚 Documentation"), - paragraph( - "Welcome to our ", - text("project docs", bold=True), - "!" - ), - horizontal_rule(), - heading(2, "Features"), - bullet_list( - "Easy to use", - "Type-safe", - "Well documented" - ), - paragraph( - "Learn more at ", - link_text("our docs", "https://docs.vaiz.app") - ) -] - -client.replace_json_document(document_id, content) -``` +Supported kinds: `user`, `task`, `document`, `milestone`, `project`, `board`. -See [Document Structure Helpers Guide](./document-structure-helpers) for complete documentation. +The label inside `[...]` is cosmetic — only the kind and ID are stored, and the editor renders the live entity name. When reading content back with `get_markdown_document()`, mentions are exported in the same syntax (with a generic label), so they survive read-edit-write round-trips. ### Update Project Document @@ -512,7 +399,7 @@ target_doc = next( if target_doc: # 3. Get current content - current_content = client.get_json_document(target_doc.id) + current_markdown = client.get_markdown_document(target_doc.id) print(f"Current size: {target_doc.size} bytes") # 4. Update content @@ -528,20 +415,16 @@ if target_doc: - Update documentation """ - client.replace_document(target_doc.id, new_content) + client.replace_markdown_document(target_doc.id, new_content) print(f"✅ Updated: {target_doc.title}") ``` ### Document Content Format -Documents are stored as structured JSON. When you use `replace_document`, the content is converted to the appropriate format: +Documents are stored in the Lexical editor format on the server. Markdown is the recommended way to write content — it is converted to native rich blocks (headings, lists, tables, bold/italic text, etc.): ```python -# Plain text -client.replace_document(doc_id, "Simple text") - -# Markdown-style formatting (as plain text) -client.replace_document( +client.replace_markdown_document( doc_id, """ # Header @@ -553,50 +436,6 @@ client.replace_document( **Bold** and *italic* text """ ) - -# Rich JSON format (with actual formatting) -json_content = [ - { - "type": "heading", - "attrs": {"level": 1}, - "content": [{"type": "text", "text": "Header"}] - }, - { - "type": "heading", - "attrs": {"level": 2}, - "content": [{"type": "text", "text": "Subheader"}] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [{ - "type": "paragraph", - "content": [{"type": "text", "text": "List item 1"}] - }] - }, - { - "type": "listItem", - "content": [{ - "type": "paragraph", - "content": [{"type": "text", "text": "List item 2"}] - }] - } - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Bold"}, - {"type": "text", "text": " and "}, - {"type": "text", "marks": [{"type": "italic"}], "text": "italic"}, - {"type": "text", "text": " text"} - ] - } -] - -client.replace_json_document(doc_id, json_content) ``` ## See Also diff --git a/docs-site/docs/guides/members.md b/docs-site/docs/guides/members.md index 81f1224..1a71e89 100644 --- a/docs-site/docs/guides/members.md +++ b/docs-site/docs/guides/members.md @@ -9,6 +9,10 @@ description: Learn how to retrieve space members, user information, and team det Get information about members in your space. +:::note +Bot members (AI, automation, and integration bots) are excluded from `get_space_members()` results. +::: + ## Get Space Members ```python diff --git a/docs-site/docs/guides/tasks.md b/docs-site/docs/guides/tasks.md index 57656e1..bbb0ef1 100644 --- a/docs-site/docs/guides/tasks.md +++ b/docs-site/docs/guides/tasks.md @@ -222,9 +222,7 @@ You can move tasks to different groups in a single request. Each move specifies ## Task Descriptions -Tasks store descriptions as structured JSON documents. The SDK provides convenient methods to work with descriptions directly from Task objects. - -**Important:** Task descriptions support rich content using the same [document structure format](../api-reference/document-structure) as standalone documents. You can use `replace_json_document` to create formatted task descriptions with headings, lists, links, and more. +Every task stores its description as a document (the `task.document` field). The recommended way to read and write descriptions is Markdown — it is converted to native rich blocks on the server. ### Get Task Description @@ -234,9 +232,9 @@ task_response = client.get_task("PRJ-123") task = task_response.task document_id = task.document -# Get document content -description = client.get_json_document(document_id) -print(description) # JSON structure +# Get description as Markdown +description = client.get_markdown_document(document_id) +print(description) ``` ### Update Task Description @@ -248,111 +246,44 @@ Replace task description completely: task_response = client.get_task("PRJ-123") document_id = task_response.task.document -# Update description +# Update description with Markdown new_content = """ # Updated Description This completely replaces the previous content. ## Features -- Complete replacement -- Plain text support -- Direct API access +- **Rich formatting** with Markdown +- Lists, tables, code blocks +- Links and checklists """ -client.replace_document( - document_id=document_id, - description=new_content -) +client.replace_markdown_document(document_id, new_content) ``` -### Rich Formatted Descriptions +### Appending Content -For rich content with formatting, lists, and links, use `replace_json_document`: +Add content to the end of the description without removing existing content: ```python -from vaiz import heading, paragraph, text, bullet_list - -# Get task -task_response = client.get_task("PRJ-123") -document_id = task_response.task.document - -# Create rich content -content = [ - heading(1, "Task Overview"), - paragraph( - "This task requires ", - text("immediate attention", bold=True), - "." - ), - heading(2, "Requirements"), - bullet_list( - "Review design mockups", - "Update documentation", - "Test functionality" - ), - paragraph( - "See ", - text("project docs", link="https://docs.example.com"), - " for details." - ) -] - -# Replace with rich content -client.replace_json_document(document_id, content) -``` - -See [Document Structure](../api-reference/document-structure) for complete format reference and [Document Structure Helpers](./document-structure-helpers) for helper functions. - -### Using Task Helper Methods - -The Task model provides convenient helper methods: - -```python -# Get task -task_response = client.create_task(task) -task_obj = task_response.task - -# Get description using helper -description = task_obj.get_task_description(client) -print(description) - -# Update description using helper -task_obj.update_task_description( - client, - "New task description content" +client.append_markdown_document( + document_id, + "## Update\n\nAdditional notes" ) ``` -### Full Workflow Example - -```python -from vaiz.models import CreateTaskRequest +### Convenience Methods on Task -# 1. Create task with initial description -task = CreateTaskRequest( - name="Documentation Task", - board="board_id", - group="group_id" -) +The `Task` model provides shortcuts for working with descriptions: -response = client.create_task( - task, - description="Initial task description" -) - -# 2. Get task object -task_obj = response.task +```python +task = client.get_task("PRJ-123").task -# 3. Update description later -task_obj.update_task_description( - client, - "Updated task description with more details" -) +# Read description as Markdown +markdown = task.get_task_description(client) -# 4. Read current content -content = task_obj.get_task_description(client) -print(content) +# Replace description with Markdown +task.update_task_description(client, "# New Description\n\nWith **markdown**.") ``` ### Programmatic Description Updates @@ -367,19 +298,12 @@ def add_status_update(task_id: str, status: str): task_response = client.get_task(task_id) doc_id = task_response.task.document - # Get current content - current = client.get_json_document(doc_id) - - # Add status update + # Append status update timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") - new_content = f""" -{current} - ---- -**Status Update ({timestamp})**: {status} -""" - - client.replace_document(doc_id, new_content) + client.append_markdown_document( + doc_id, + f"---\n**Status Update ({timestamp})**: {status}" + ) # Usage add_status_update("PRJ-123", "Design phase completed") diff --git a/docs-site/docs/patterns/documents.md b/docs-site/docs/patterns/documents.md index 24d91ab..fd8858e 100644 --- a/docs-site/docs/patterns/documents.md +++ b/docs-site/docs/patterns/documents.md @@ -119,7 +119,7 @@ def create_project_wiki(client, project_id: str): # Add initial content content = f"# {page_name}\n\n[Add content here]" - client.replace_document(page.id, content) + client.replace_markdown_document(page.id, content) return wiki diff --git a/docs-site/docs/patterns/integrations.md b/docs-site/docs/patterns/integrations.md index 067a44a..50394cc 100644 --- a/docs-site/docs/patterns/integrations.md +++ b/docs-site/docs/patterns/integrations.md @@ -153,14 +153,14 @@ def sync_with_jira(jira_url: str, jira_token: str): jira_issue = jira.issue(jira_id) jira_issue.update( summary=task.name, - description=get_task_description(task.document) + description=client.get_markdown_document(task.document) ) else: # Create new Jira issue new_issue = jira.create_issue( project='PROJECT', summary=task.name, - description=get_task_description(task.document), + description=client.get_markdown_document(task.document), issuetype={'name': 'Task'} ) diff --git a/docs-site/docs/patterns/ready-to-run.md b/docs-site/docs/patterns/ready-to-run.md index a6eea3d..3b58290 100644 --- a/docs-site/docs/patterns/ready-to-run.md +++ b/docs-site/docs/patterns/ready-to-run.md @@ -35,23 +35,11 @@ The SDK includes a collection of ready-to-run examples in the [`/examples`](http - **`get_documents.py`** - List documents by scope (Space/Member/Project) - **`create_document.py`** - Create new documents - **`edit_document.py`** - Edit document metadata (e.g., title) -- **`get_document.py`** - Get single document -- **`replace_document.py`** - Replace document content with plain text -- **`replace_json_document.py`** - Replace document content with rich JSON (document structure format) -- **`replace_json_document_complex.py`** - Complex document with nested lists, inline code, links, and more -- **`replace_json_document_with_helpers.py`** - Type-safe content creation using document structure helper functions -- **`replace_json_document_with_table.py`** - Creating documents with tables for status reports and metrics -- **`append_json_document.py`** - Appending content to existing documents (incremental updates) +- **`get_document.py`** - Get document content as Markdown +- **`markdown_documents.py`** - Replace, append, and read document content with Markdown (tables, checklists, code blocks) ### Advanced Workflows - **`document_hierarchy.py`** - Build nested document structures -- **`document_content_management.py`** - Work with document content -- **`advanced_document_workflows.py`** - Complex document scenarios -- **`mention_blocks.py`** - Create documents with user, task, document, and milestone mentions -- **`advanced_mention_usage.py`** - Advanced usage of mentions in tables, lists, and complex documents -- **`document_navigation_blocks.py`** - TOC, Anchors, and Siblings navigation blocks -- **`document_with_code_blocks.py`** - Code blocks with syntax highlighting -- **`embed_blocks_example.py`** - Embed external content (YouTube, Figma, CodeSandbox, etc.) ## Custom Fields diff --git a/docs-site/docusaurus.config.ts b/docs-site/docusaurus.config.ts index b071d4a..659b198 100644 --- a/docs-site/docusaurus.config.ts +++ b/docs-site/docusaurus.config.ts @@ -212,8 +212,8 @@ const config: Config = { }, items: [ { - label: "v0.19.0", - href: "https://pypi.org/project/vaiz-sdk/0.19.0/", + label: "v1.0.0", + href: "https://pypi.org/project/vaiz-sdk/1.0.0/", position: "left", }, { diff --git a/docs-site/sidebars.ts b/docs-site/sidebars.ts index 47207da..6814170 100644 --- a/docs-site/sidebars.ts +++ b/docs-site/sidebars.ts @@ -15,7 +15,6 @@ const sidebars: SidebarsConfig = { 'guides/documents', 'guides/files', 'guides/helpers', - 'guides/document-structure-helpers', 'guides/history', 'guides/members', 'guides/milestones', @@ -35,7 +34,6 @@ const sidebars: SidebarsConfig = { 'api-reference/custom-fields', 'api-reference/comments', 'api-reference/documents', - 'api-reference/document-structure', 'api-reference/enums', 'api-reference/files', 'api-reference/history', diff --git a/examples/advanced_document_workflows.py b/examples/advanced_document_workflows.py deleted file mode 100644 index 862778e..0000000 --- a/examples/advanced_document_workflows.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/env python3 -""" -Example: Advanced Document Workflows in Vaiz - -This example demonstrates advanced scenarios: -1. Working with specific documents by ID -2. Batch operations across multiple scopes -3. Document content migration and updates -""" - -import os -from datetime import datetime -from vaiz import VaizClient -from vaiz.models import GetDocumentsRequest, Kind - - -def main(): - # Initialize client - api_key = os.getenv("VAIZ_API_KEY") - space_id = os.getenv("VAIZ_SPACE_ID") - - if not api_key or not space_id: - print("Error: Set VAIZ_API_KEY and VAIZ_SPACE_ID environment variables") - return - - client = VaizClient(api_key=api_key, space_id=space_id) - - print("=== Advanced Document Workflows ===\n") - - # Example 1: Update specific document by ID - print("1. Update Specific Document by ID:") - print(" Note: When you know the document ID, work with it directly.") - print(" Avoid loading all documents to search by title - this doesn't scale.") - try: - # Get a few documents to demonstrate - docs = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=space_id) - ) - - if docs.payload.documents: - # Work with first document as example - target_doc = docs.payload.documents[0] - print(f" Working with: {target_doc.title}") - print(f" ID: {target_doc.id}") - - # Update content with timestamp - new_content = f""" -# Document Content - Updated - -Last updated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - -## Latest Updates -- Content updated via SDK -- Timestamp added automatically -- Document ID: {target_doc.id} -""" - - client.replace_document(target_doc.id, new_content) - print(" ✅ Document updated") - else: - print(" No documents found") - except Exception as e: - print(f" Error: {e}") - - print() - - # Example 2: Document content audit across scopes - print("2. Document Content Audit:") - try: - profile = client.get_profile() - member_id = profile.profile.member_id # Use memberId for Member documents - - scopes = [ - (Kind.Space, space_id, "Space"), - (Kind.Member, member_id, "Member"), - ] - - total_docs = 0 - total_size = 0 - - for kind, kind_id, scope_name in scopes: - docs = client.get_documents( - GetDocumentsRequest(kind=kind, kind_id=kind_id) - ) - - scope_size = sum(doc.size for doc in docs.payload.documents) - total_docs += len(docs.payload.documents) - total_size += scope_size - - print(f" {scope_name}:") - print(f" - Documents: {len(docs.payload.documents)}") - print(f" - Total size: {scope_size:,} bytes") - - print("\n Total across scopes:") - print(f" - Documents: {total_docs}") - print(f" - Total size: {total_size:,} bytes") - except Exception as e: - print(f" Error: {e}") - - print() - - # Example 3: Batch content standardization - print("3. Batch Content Standardization:") - try: - # Get project documents - projects = client.get_projects() - - if projects.projects: - project_id = projects.projects[0].id - docs = client.get_documents( - GetDocumentsRequest(kind=Kind.Project, kind_id=project_id) - ) - - standard_header = f""" -# Project Documentation - -**Project**: {projects.projects[0].name} -**Last Updated**: {datetime.now().strftime("%Y-%m-%d")} - ---- - -""" - - updated = 0 - for doc in docs.payload.documents[:5]: # Limit to 5 docs - try: - # Get current content - content = client.get_json_document(doc.id) - - # Add standard header - standardized = standard_header + f"Document: {doc.title}\n\n[Content]" - - client.replace_document(doc.id, standardized) - updated += 1 - print(f" ✅ Standardized: {doc.title}") - except Exception as e: - print(f" ❌ Failed: {doc.title} - {e}") - - print(f"\n Total standardized: {updated} documents") - else: - print(" No projects available") - except Exception as e: - print(f" Error: {e}") - - print() - - # Example 4: Document backup - print("4. Document Content Backup:") - try: - backup_data = [] - - docs = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=space_id) - ) - - for doc in docs.payload.documents[:3]: # Backup first 3 - content = client.get_json_document(doc.id) - - backup_data.append({ - 'id': doc.id, - 'title': doc.title, - 'size': doc.size, - 'created_at': doc.created_at.isoformat(), - 'content': content, - 'backup_timestamp': datetime.now().isoformat() - }) - - print(f" ✅ Backed up: {doc.title}") - - print(f"\n Total backed up: {len(backup_data)} documents") - print(" Backup can be saved to file or database") - except Exception as e: - print(f" Error: {e}") - - print() - - # Example 5: Content migration between documents - print("5. Content Migration Example:") - try: - source_docs = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=space_id) - ) - - if len(source_docs.payload.documents) >= 2: - source_doc = source_docs.payload.documents[0] - target_doc = source_docs.payload.documents[1] - - print(f" Source: {source_doc.title}") - print(f" Target: {target_doc.title}") - - # Create migration note (in real scenario, you'd use source content) - migration_content = f""" -# Migrated Content - -Original document: {source_doc.title} -Migration date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - ---- - -[Migrated content would go here] - ---- - -Source document ID: {source_doc.id} -""" - - # Update target with migrated content - client.replace_document(target_doc.id, migration_content) - print(" ✅ Content migrated successfully") - else: - print(" Not enough documents for migration example") - except Exception as e: - print(f" Error: {e}") - - print() - - # Example 6: Smart content updates - print("6. Smart Content Updates (Conditional):") - try: - docs = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=space_id) - ) - - # Only update documents that haven't been updated recently - from datetime import timedelta - cutoff_date = datetime.now() - timedelta(days=7) - - updated = 0 - skipped = 0 - - for doc in docs.payload.documents[:5]: - if doc.updated_at < cutoff_date: - new_content = f""" -Document: {doc.title} - -This document was automatically updated because it was last modified on {doc.updated_at.strftime("%Y-%m-%d")}. - -Updated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} -""" - - client.replace_document(doc.id, new_content) - updated += 1 - print(f" ✅ Updated (old): {doc.title}") - else: - skipped += 1 - print(f" ⏭️ Skipped (recent): {doc.title}") - - print(f"\n Updated: {updated}, Skipped: {skipped}") - except Exception as e: - print(f" Error: {e}") - - print("\n=== Advanced Workflows Complete ===") - - -if __name__ == "__main__": - main() - diff --git a/examples/advanced_mention_usage.py b/examples/advanced_mention_usage.py deleted file mode 100644 index b2d20e9..0000000 --- a/examples/advanced_mention_usage.py +++ /dev/null @@ -1,314 +0,0 @@ -""" -Example: Advanced Mention Usage in Task Descriptions - -This example demonstrates how to create task descriptions with mentions, -combining them with other document elements for rich content. -""" - -from examples.config import get_client, PROJECT_ID, BOARD_ID -from vaiz import ( - CreateTaskRequest, - TaskPriority, - # Document structure builders - heading, - paragraph, - text, - bullet_list, - list_item, - horizontal_rule, - table, - table_row, - table_header, - table_cell, - # Mention helpers - mention_user, - mention_task, - mention_milestone, - mention_document, -) - - -def main(): - client = get_client() - client.verbose = True - - print("=== Advanced Mention Usage Example ===\n") - - # Get real IDs from API - print("Getting real IDs from API...") - - # Get user profile for current member ID - profile = client.get_profile() - current_member_id = profile.profile.member_id - print(f"✓ Member ID: {current_member_id}") - - # Get real IDs - from vaiz.models import GetDocumentsRequest, Kind - from examples.config import SPACE_ID - - # Get document ID - docs_response = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=SPACE_ID) - ) - reference_doc_id = None - if docs_response.payload.documents: - reference_doc_id = docs_response.payload.documents[0].id - print(f"✓ Reference Document ID: {reference_doc_id}") - - # Get task ID - from vaiz.models import GetTasksRequest - tasks_response = client.get_tasks(GetTasksRequest()) - task_id = None - if tasks_response.payload.tasks: - task_id = tasks_response.payload.tasks[0].id - print(f"✓ Task ID: {task_id}") - - # Get milestone ID - milestones_response = client.get_milestones() - milestone_id = None - if milestones_response.milestones: - milestone_id = milestones_response.milestones[0].id - print(f"✓ Milestone ID: {milestone_id}") - - print() - - # Example 1: Create task with mentions in description - print("1. Creating task with rich description including mentions...") - - # Build rich task description with mentions - task_description = [ - heading(1, "📋 Project Status Update"), - - paragraph( - text("This task is assigned to "), - mention_user(current_member_id), - text(" and requires coordination with the team.") - ), - - horizontal_rule(), - - heading(2, "🎯 Objectives"), - bullet_list( - "Review current progress", - "Update documentation", - list_item( - paragraph( - text("Coordinate with "), - mention_user(current_member_id) - ) - ) - ), - - heading(2, "📎 Related Items"), - ] - - # Add related items if available - related_items = [text("References: ")] - if milestone_id: - related_items.extend([ - text("Milestone "), - mention_milestone(milestone_id), - ]) - if reference_doc_id: - if milestone_id: - related_items.append(text(" | ")) - related_items.extend([ - text("Document "), - mention_document(reference_doc_id), - ]) - - task_description.append(paragraph(*related_items)) - - task_description.extend([ - - horizontal_rule(), - - heading(2, "👥 Team Members"), - table( - table_row( - table_header("Role"), - table_header("Member") - ), - table_row( - table_cell("Lead"), - table_cell( - paragraph(mention_user(current_member_id)) - ) - ), - table_row( - table_cell("Reviewer"), - table_cell( - paragraph(mention_user(current_member_id)) - ) - ) - ), - ]) - - try: - # Note: You need to provide valid PROJECT_ID and BOARD_ID in .env - if not PROJECT_ID or not BOARD_ID: - print("⚠️ Please set VAIZ_PROJECT_ID and VAIZ_BOARD_ID in your .env file") - print("Skipping task creation example...") - else: - task_request = CreateTaskRequest( - name="Project Status Update with Mentions", - group=PROJECT_ID, - board=BOARD_ID, - priority=TaskPriority.General, - ) - - task_response = client.create_task(task_request) - task_id = task_response.task.id - document_id = task_response.task.document - - print(f"✅ Task created: {task_response.task.hrid}") - print(f"Task ID: {task_id}") - print(f"Document ID: {document_id}") - - # Update task description with rich content - client.replace_json_document(document_id, task_description) - print("✅ Task description updated with mentions!") - - except Exception as e: - print(f"❌ Error creating task: {e}") - - print("\n" + "="*50) - - # Example 2: Create document with complex mention structure - print("\n2. Creating document with complex mention structure...") - - # Create new document for testing - from vaiz.models import CreateDocumentRequest - create_request = CreateDocumentRequest( - kind=Kind.Space, - kind_id=SPACE_ID, - title="Advanced Example: Meeting Notes with Mentions", - index=0 - ) - doc_response = client.create_document(create_request) - document_id = doc_response.payload.document.id - print(f"✓ Created document: {document_id}") - - update_content = [ - heading(1, "📝 Meeting Notes"), - - paragraph( - text("Date: 2025-10-23", bold=True) - ), - - heading(2, "Attendees"), - bullet_list( - list_item(paragraph(mention_user(current_member_id))), - list_item(paragraph(mention_user(current_member_id))), - ), - - horizontal_rule(), - - heading(2, "Action Items"), - ] - - # Add action items table if task available - if task_id: - update_content.append(table( - table_row( - table_header("Task"), - table_header("Assignee"), - table_header("Status") - ), - table_row( - table_cell( - paragraph(mention_task(task_id)) - ), - table_cell( - paragraph(mention_user(current_member_id)) - ), - table_cell("In Progress") - ), - table_row( - table_cell("Update documentation"), - table_cell( - paragraph(mention_user(current_member_id)) - ), - table_cell("Pending") - ) - )) - - update_content.append(horizontal_rule()) - - # Add references section - update_content.append(heading(2, "References")) - - references = [] - if milestone_id: - references.append(list_item( - paragraph( - text("Milestone: "), - mention_milestone(milestone_id) - ) - )) - if reference_doc_id: - references.append(list_item( - paragraph( - text("Related doc: "), - mention_document(reference_doc_id) - ) - )) - - if references: - update_content.append(bullet_list(*references)) - - update_content.extend([ - - paragraph( - text("Last updated by "), - mention_user(current_member_id), - text(" on 2025-10-23", italic=True) - ) - ]) - - try: - client.replace_json_document(document_id, update_content) - print("✅ Document updated successfully!") - print(f"\n🔗 View at: https://vaiz.app/document/{document_id}") - - # Verify mentions were created - print("\nVerifying mentions...") - doc_content = client.get_json_document(document_id) - - mention_count = 0 - for node in doc_content.get("default", {}).get("content", []): - if node.get("type") == "paragraph" and "content" in node: - for child in node["content"]: - if child.get("type") == "custom-mention": - mention_count += 1 - elif node.get("type") == "extension-table" and "content" in node: - # Check mentions in tables - for row in node["content"]: - if "content" in row: - for cell in row["content"]: - if "content" in cell: - for cell_node in cell["content"]: - if cell_node.get("type") == "paragraph" and "content" in cell_node: - for child in cell_node["content"]: - if child.get("type") == "custom-mention": - mention_count += 1 - elif node.get("type") == "bulletList" and "content" in node: - # Check mentions in lists - for item in node["content"]: - if "content" in item: - for item_node in item["content"]: - if item_node.get("type") == "paragraph" and "content" in item_node: - for child in item_node["content"]: - if child.get("type") == "custom-mention": - mention_count += 1 - - print(f"✓ Found {mention_count} mention block(s) in document") - - except Exception as e: - print(f"❌ Error updating document: {e}") - - print("\n=== Example completed ===") - - -if __name__ == "__main__": - main() diff --git a/examples/append_json_document.py b/examples/append_json_document.py deleted file mode 100644 index 46b19ec..0000000 --- a/examples/append_json_document.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Example: Appending content to existing documents using appendJSONDocument API. - -This example demonstrates how to add new content to documents without -removing existing content - useful for incremental updates, logs, and notes. -""" - -from examples.config import get_client -from vaiz.models import CreateTaskRequest, TaskPriority -from vaiz import heading, paragraph, text, bullet_list, horizontal_rule, blockquote - - -def main(): - client = get_client() - client.verbose = True - - # Create a task with initial content - task = CreateTaskRequest( - name="Incremental Document Updates Demo", - group=client.space_id, - board="your_board_id", - priority=TaskPriority.General, - description="Initial task description - this will remain" - ) - - try: - task_response = client.create_task(task) - document_id = task_response.task.document - print(f"Created task: {task_response.task.hrid}\n") - - # Get initial content - initial_content = client.get_json_document(document_id) - print(f"Initial content present: {len(initial_content.get('default', {}).get('content', []))} blocks") - - # Append first update - print("\n=== Appending Update #1 ===") - update1 = [ - horizontal_rule(), - heading(2, "Update #1 - Design Phase"), - paragraph( - text("Status: ", bold=True), - "Design mockups completed" - ), - bullet_list( - "Homepage design approved", - "Component library created", - "Style guide finalized" - ) - ] - - client.append_json_document(document_id, update1) - print("✅ Update #1 appended") - - # Append second update - print("\n=== Appending Update #2 ===") - update2 = [ - horizontal_rule(), - heading(2, "Update #2 - Development Progress"), - paragraph( - text("Status: ", bold=True), - "API implementation in progress" - ), - bullet_list( - "User authentication - Done", - "Task management endpoints - In Progress", - "Document API - Planned" - ) - ] - - client.append_json_document(document_id, update2) - print("✅ Update #2 appended") - - # Append third update - print("\n=== Appending Update #3 ===") - update3 = [ - horizontal_rule(), - heading(2, "Update #3 - Final Notes"), - paragraph( - text("Conclusion: ", bold=True), - "All updates have been ", - text("successfully appended", italic=True), - " to the original content." - ), - blockquote( - paragraph( - text("Key Insight: ", bold=True), - "Incremental updates preserve document history while keeping content organized." - ) - ) - ] - - client.append_json_document(document_id, update3) - print("✅ Update #3 appended") - - # Verify all content exists - final_content = client.get_json_document(document_id) - final_blocks = final_content.get('default', {}).get('content', []) - - print(f"\n📊 Final Document Summary:") - print(f" Total blocks: {len(final_blocks)}") - print(f" Original content: ✅ Preserved") - print(f" Update #1: ✅ Added") - print(f" Update #2: ✅ Added") - print(f" Update #3: ✅ Added") - - print(f"\n🎉 Check task {task_response.task.hrid} to see incremental updates!") - print(f" The document now contains the original description plus 3 appended updates") - - except Exception as e: - print(f"❌ Error: {e}") - - -if __name__ == "__main__": - main() - diff --git a/examples/comprehensive_blocks_example.py b/examples/comprehensive_blocks_example.py deleted file mode 100644 index f16dff2..0000000 --- a/examples/comprehensive_blocks_example.py +++ /dev/null @@ -1,266 +0,0 @@ -""" -Example: Comprehensive Document with All Block Types - -This example demonstrates combining mentions, files, images, tables, -and other document elements in one rich document. -""" - -from examples.config import get_client, SPACE_ID -from vaiz import ( - heading, - paragraph, - text, - bullet_list, - list_item, - table, - table_row, - table_header, - table_cell, - horizontal_rule, - blockquote, - mention_user, - mention_task, - mention_document, - image_block, - files_block, -) -from vaiz.models import CreateDocumentRequest, Kind, GetDocumentsRequest, GetTasksRequest -from vaiz.models.enums import UploadFileType -import os - - -def main(): - client = get_client() - client.verbose = True - - print("=== Creating Comprehensive Document with All Block Types ===\n") - - # Step 1: Get entity IDs for mentions - print("Step 1: Getting entity IDs...") - - profile = client.get_profile() - member_id = profile.profile.member_id - print(f"✓ Member ID: {member_id}") - - docs_response = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=SPACE_ID) - ) - ref_doc_id = docs_response.payload.documents[0].id if docs_response.payload.documents else None - if ref_doc_id: - print(f"✓ Reference Document ID: {ref_doc_id}") - - tasks_response = client.get_tasks(GetTasksRequest()) - task_id = tasks_response.payload.tasks[0].id if tasks_response.payload.tasks else None - if task_id: - print(f"✓ Task ID: {task_id}") - - # Step 2: Upload files - print("\nStep 2: Uploading files...") - - image_path = "assets/example.png" - pdf_path = "assets/example.pdf" - - image_uploaded = None - if os.path.exists(image_path): - image_uploaded = client.upload_file(image_path, file_type=UploadFileType.Image) - print(f"✓ Uploaded image: {image_uploaded.file.name}") - - pdf_uploaded = None - if os.path.exists(pdf_path): - pdf_uploaded = client.upload_file(pdf_path, file_type=UploadFileType.Pdf) - print(f"✓ Uploaded PDF: {pdf_uploaded.file.name}") - - # Step 3: Create document - print("\nStep 3: Creating document...") - - create_request = CreateDocumentRequest( - kind=Kind.Space, - kind_id=SPACE_ID, - title="Comprehensive Example: All Block Types", - index=0 - ) - doc_response = client.create_document(create_request) - document_id = doc_response.payload.document.id - print(f"✓ Created document: {document_id}") - - # Step 4: Build rich content with all block types - print("\nStep 4: Building comprehensive content...") - - content = [ - heading(1, "📋 Project Status Report"), - - paragraph( - text("Report Date: 2025-10-23", bold=True), - text(" | Prepared by: "), - mention_user(member_id) if member_id else text("Team") - ), - - horizontal_rule(), - - # Section with task assignment - heading(2, "👥 Team Assignments"), - - paragraph( - text("Current assignee: "), - mention_user(member_id) if member_id else text("TBD") - ), - ] - - # Add table with mentions if task available - if task_id and member_id: - content.extend([ - paragraph(text("Task assignments table:")), - - table( - table_row( - table_header("Assignee"), - table_header("Task"), - table_header("Status") - ), - table_row( - table_cell(paragraph(mention_user(member_id))), - table_cell(paragraph(mention_task(task_id))), - table_cell("In Progress") - ) - ), - ]) - - content.append(horizontal_rule()) - - # Section with image - if image_uploaded: - content.extend([ - heading(2, "📸 Visual Assets"), - - paragraph(text("Project screenshot:")), - - # New simple API - just pass the file! - image_block( - file=image_uploaded.file, - caption="Project Screenshot", - width_percent=80 - ), - - paragraph(text("Image caption: Project Screenshot", italic=True)), - - horizontal_rule(), - ]) - - # Section with files - files_to_attach = [] - - if pdf_uploaded: - files_to_attach.append({ - "fileId": pdf_uploaded.file.id, - "url": pdf_uploaded.file.url, - "name": pdf_uploaded.file.name, - "size": pdf_uploaded.file.size, - "extension": pdf_uploaded.file.ext, - "type": "Pdf" - }) - - if files_to_attach: - content.extend([ - heading(2, "📎 Attached Documents"), - - paragraph(text("Reference materials:")), - - files_block(*files_to_attach), - - paragraph( - text(f"{len(files_to_attach)} document(s) attached", italic=True) - ), - - horizontal_rule(), - ]) - - # Section with references - if ref_doc_id: - content.extend([ - heading(2, "🔗 Related Resources"), - - bullet_list( - list_item(paragraph( - text("Reference document: "), - mention_document(ref_doc_id) - )), - list_item(paragraph( - text("Task link: "), - mention_task(task_id) if task_id else text("N/A") - )) - ), - - horizontal_rule(), - ]) - - # Summary section - content.extend([ - heading(2, "📊 Summary"), - - blockquote( - paragraph( - text("This document demonstrates all supported block types:", bold=True) - ), - paragraph("• Headings and formatted text"), - paragraph("• User mentions and task references"), - paragraph("• Image blocks with embedded images"), - paragraph("• Files blocks with attachments"), - paragraph("• Tables with structured data"), - paragraph("• Lists and blockquotes") - ), - - paragraph( - text("Last updated by "), - mention_user(member_id) if member_id else text("System"), - text(" on 2025-10-23", italic=True) - ) - ]) - - # Step 5: Update document - try: - client.replace_json_document(document_id, content) - print("\n✅ Comprehensive document created successfully!") - print(f"\n🔗 View document: https://vaiz.app/document/{document_id}") - - # Verify all blocks - print("\nVerifying blocks...") - doc_content = client.get_json_document(document_id) - - mention_count = 0 - image_block_count = 0 - files_block_count = 0 - table_count = 0 - - for node in doc_content.get("default", {}).get("content", []): - node_type = node.get("type") - - if node_type == "paragraph" and "content" in node: - for child in node["content"]: - if child.get("type") == "custom-mention": - mention_count += 1 - elif node_type == "image-block": - image_block_count += 1 - elif node_type == "files": - files_block_count += 1 - elif node_type == "extension-table": - table_count += 1 - - print(f"✓ Found {mention_count} mention(s)") - print(f"✓ Found {image_block_count} image block(s)") - print(f"✓ Found {files_block_count} files block(s)") - print(f"✓ Found {table_count} table(s)") - - total_blocks = mention_count + image_block_count + files_block_count + table_count - print(f"\n📦 Total interactive blocks: {total_blocks}") - - except Exception as e: - print(f"❌ Error: {e}") - import traceback - traceback.print_exc() - - print("\n=== Example completed ===") - - -if __name__ == "__main__": - main() - diff --git a/examples/create_document.py b/examples/create_document.py index 3c2666c..b00d977 100644 --- a/examples/create_document.py +++ b/examples/create_document.py @@ -154,12 +154,12 @@ def main(): This document was created and populated automatically via SDK. """ - client.replace_document(doc.id, content) + client.replace_markdown_document(doc.id, content) print("✅ Document populated with content") # Verify - retrieved = client.get_json_document(doc.id) - print(f"✅ Content verified (keys: {list(retrieved.keys())})") + retrieved = client.get_markdown_document(doc.id) + print(f"✅ Content verified ({len(retrieved)} characters)") else: print("No projects available") except Exception as e: diff --git a/examples/document_content_management.py b/examples/document_content_management.py deleted file mode 100644 index 87ebc33..0000000 --- a/examples/document_content_management.py +++ /dev/null @@ -1,260 +0,0 @@ -#!/usr/bin/env python3 -""" -Example: Working with Document Content in Vaiz - -This example demonstrates how to: -1. Get a list of documents (Space/Member/Project) -2. Read document content using get_json_document() -3. Update document content using replace_document() -""" - -import os -from datetime import datetime -from vaiz import VaizClient -from vaiz.models import GetDocumentsRequest, Kind - - -def main(): - # Initialize client - api_key = os.getenv("VAIZ_API_KEY") - space_id = os.getenv("VAIZ_SPACE_ID") - - if not api_key or not space_id: - print("Error: Set VAIZ_API_KEY and VAIZ_SPACE_ID environment variables") - return - - client = VaizClient(api_key=api_key, space_id=space_id, verbose=True) - - print("=== Document Content Management Examples ===\n") - - # Example 1: Working with Space documents - print("1. Working with Space Documents:") - try: - # Get list of Space documents - space_docs = client.get_documents( - GetDocumentsRequest( - kind=Kind.Space, - kind_id=space_id # Use space_id for Space documents - ) - ) - - if space_docs.payload.documents: - doc = space_docs.payload.documents[0] - print(f" Found document: {doc.title}") - print(f" Document ID: {doc.id}") - - # Get current content - content = client.get_json_document(doc.id) - print(f" Current content keys: {list(content.keys())}") - - # Update content - new_content = f""" -# Space Document Update - -Updated at: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - -This is a shared document accessible to all space members. - -## Recent Changes -- Content updated via SDK -- Automatic timestamp added -""" - - client.replace_document(doc.id, new_content) - print(" ✅ Document content updated") - else: - print(" No Space documents found") - except Exception as e: - print(f" Error: {e}") - - print() - - # Example 2: Working with Member documents - print("2. Working with Member (Personal) Documents:") - try: - # Get profile to get member ID (use memberId, not _id) - profile = client.get_profile() - member_id = profile.profile.member_id - - # Get list of Member documents - member_docs = client.get_documents( - GetDocumentsRequest( - kind=Kind.Member, - kind_id=member_id - ) - ) - - if member_docs.payload.documents: - doc = member_docs.payload.documents[0] - print(f" Found personal document: {doc.title}") - print(f" Document ID: {doc.id}") - - # Get current content - content = client.get_json_document(doc.id) - print(f" Current size: {doc.size} bytes") - - # Update content with personal notes - new_content = f""" -# Personal Notes - -Last updated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - -## My Tasks -- Review project documentation -- Update SDK examples -- Test new features - -## Notes -This is a private document visible only to me. -""" - - client.replace_document(doc.id, new_content) - print(" ✅ Personal document updated") - else: - print(" No Member documents found") - except Exception as e: - print(f" Error: {e}") - - print() - - # Example 3: Working with Project documents - print("3. Working with Project Documents:") - try: - # Get list of projects to find a project ID - projects = client.get_projects() - - if projects.projects: - project_id = projects.projects[0].id - print(f" Using project: {projects.projects[0].name}") - - # Get project documents - project_docs = client.get_documents( - GetDocumentsRequest( - kind=Kind.Project, - kind_id=project_id - ) - ) - - if project_docs.payload.documents: - doc = project_docs.payload.documents[0] - print(f" Found document: {doc.title}") - print(f" Document ID: {doc.id}") - print(f" Contributors: {len(doc.contributor_ids)}") - - # Get current content - content = client.get_json_document(doc.id) - print(" Retrieved content") - - # Update with project status - new_content = f""" -# Project Document - -Last updated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - -## Project Status -- Document accessible to project members -- Updated via SDK automation - -## Team Information -- Contributors: {len(doc.contributor_ids)} -- Created: {doc.created_at.strftime("%Y-%m-%d")} -""" - - client.replace_document(doc.id, new_content) - print(" ✅ Project document updated") - else: - print(" No Project documents found") - else: - print(" No projects available") - except Exception as e: - print(f" Error: {e}") - - print() - - # Example 4: Bulk document update - print("4. Bulk Document Update Example:") - try: - project_docs = client.get_documents( - GetDocumentsRequest( - kind=Kind.Project, - kind_id=project_id if 'project_id' in locals() else space_id - ) - ) - - updated_count = 0 - for doc in project_docs.payload.documents[:3]: # Update first 3 documents - try: - # Add timestamp footer to each document - content = client.get_json_document(doc.id) - - new_content = f""" -Document: {doc.title} - ---- -Last batch update: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} -Document ID: {doc.id} -""" - - client.replace_document(doc.id, new_content) - updated_count += 1 - print(f" ✅ Updated: {doc.title}") - except Exception as e: - print(f" ❌ Failed to update {doc.title}: {e}") - - print(f" Total updated: {updated_count} documents") - except Exception as e: - print(f" Error: {e}") - - print() - - # Example 5: Template-based document creation - print("5. Template-Based Content Update:") - try: - template = """ -# Meeting Notes Template - -**Date**: {date} -**Time**: {time} - -## Attendees -- [Add attendees here] - -## Agenda -1. Item 1 -2. Item 2 -3. Item 3 - -## Discussion -[Add discussion notes] - -## Action Items -- [ ] Action item 1 -- [ ] Action item 2 - -## Next Meeting -[Schedule next meeting] -""" - - # Apply template to a document - if 'project_docs' in locals() and project_docs.payload.documents: - doc = project_docs.payload.documents[0] - - formatted_content = template.format( - date=datetime.now().strftime("%Y-%m-%d"), - time=datetime.now().strftime("%H:%M") - ) - - client.replace_document(doc.id, formatted_content) - print(f" ✅ Applied template to: {doc.title}") - print(f" Document ID: {doc.id}") - else: - print(" No documents available for template") - except Exception as e: - print(f" Error: {e}") - - print("\n=== Examples Complete ===") - - -if __name__ == "__main__": - main() - diff --git a/examples/document_hierarchy.py b/examples/document_hierarchy.py index 6bfcb07..74f542b 100644 --- a/examples/document_hierarchy.py +++ b/examples/document_hierarchy.py @@ -218,7 +218,7 @@ def main(): Kind: {doc.kind} """ - client.replace_document(doc.id, content) + client.replace_markdown_document(doc.id, content) print(f" ✅ Updated: {doc.title}") print(f"\n✅ Populated {min(3, len(docs.payload.documents))} documents with content") diff --git a/examples/document_navigation_blocks.py b/examples/document_navigation_blocks.py deleted file mode 100644 index 6603295..0000000 --- a/examples/document_navigation_blocks.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Example demonstrating all document navigation blocks: TOC, Anchors, and Siblings. - -This example shows how to create a document with automatic navigation features: -- Table of Contents (TOC) for internal document navigation (top) -- Anchors for displaying related documents and backlinks (top) -- Siblings for Previous/Next navigation between documents (bottom) -""" - -from config import get_client -from vaiz import ( - toc_block, - anchors_block, - siblings_block, - heading, - paragraph, - bullet_list, - horizontal_rule, - text, - link_text, -) - -# Initialize client -client = get_client() - -# Document ID (replace with your actual document ID) -DOCUMENT_ID = "68fb2452322665c43876937d" - -print("Creating document with navigation blocks...") -print("=" * 80) - -# Create document content with all navigation blocks -content = [ - # Top navigation: TOC and related documents - toc_block(), - anchors_block(), - - horizontal_rule(), - - # Main content - heading(1, "Document Navigation Features"), - - paragraph( - "This document demonstrates the three types of ", - text("navigation blocks", bold=True), - " available in Vaiz: TOC, Anchors, and Siblings." - ), - - horizontal_rule(), - - heading(2, "TOC Block - Table of Contents"), - - paragraph( - "The ", - text("toc_block()", code=True), - " automatically generates an interactive table of contents:" - ), - - bullet_list( - "Shows hierarchical document structure (h1-h6)", - "Creates clickable links for quick navigation", - "Updates automatically when document changes" - ), - - heading(2, "Anchors Block - Related Documents"), - - paragraph( - "The ", - text("anchors_block()", code=True), - " displays document relationships:" - ), - - bullet_list( - "Documents that this document links to", - "Documents that link to this one (backlinks)", - "Related documents from the same space" - ), - - heading(2, "Siblings Block - Previous/Next Navigation"), - - paragraph( - "The ", - text("siblings_block()", code=True), - " provides Previous/Next navigation between documents in a sequence:" - ), - - bullet_list( - "Shows Previous and Next documents in the sequence", - "Creates navigation buttons (Back/Forward)", - "Typically placed at the bottom of the page", - "Perfect for tutorial series and sequential guides" - ), - - horizontal_rule(), - - heading(1, "Usage Examples"), - - heading(2, "Basic TOC Usage"), - - paragraph(text( - '''from vaiz import toc_block, heading, paragraph - -content = [ - toc_block(), - heading(1, "Introduction"), - paragraph("Welcome to our document"), - heading(2, "Getting Started"), - paragraph("First steps...") -] - -client.replace_json_document(document_id, content)''', - code=True - )), - - heading(2, "All Navigation Blocks Together"), - - paragraph(text( - '''from vaiz import toc_block, anchors_block, siblings_block, horizontal_rule - -content = [ - # Top navigation - toc_block(), # Table of contents - anchors_block(), # Related documents - - horizontal_rule(), - - # Your content - heading(1, "Tutorial Part 2"), - paragraph("Main content here..."), - - horizontal_rule(), - - # Bottom navigation - siblings_block() # Previous/Next buttons -] - -client.replace_json_document(document_id, content)''', - code=True - )), - - horizontal_rule(), - - heading(1, "Best Practices"), - - bullet_list( - "Place TOC and Anchors blocks at the document start for better accessibility", - "Place Siblings block at the document end for Previous/Next navigation", - "Use Anchors block for documentation with cross-references", - "Use Siblings block for tutorial series and sequential guides", - "For short documents (< 3 sections), TOC may be redundant" - ), - - paragraph( - text("Tip: ", bold=True), - "Combine these blocks with ", - text("mention_document()", code=True), - " to create links between documents and populate the Anchors block!" - ), - - horizontal_rule(), - - # Bottom navigation - demonstrate siblings placement - siblings_block(), -] - -try: - response = client.replace_json_document(DOCUMENT_ID, content) - print("✅ Document created successfully!") - print() - print("What was added:") - print(" • TOC block (top) - automatic table of contents") - print(" • Anchors block (top) - related documents and backlinks") - print(" • Siblings block (bottom) - Previous/Next navigation") - print(" • Headings with UIDs for TOC navigation") - print() - print("Navigation layout:") - print(" ┌─────────────────────────┐") - print(" │ TOC Block │ ← Internal navigation") - print(" │ Anchors Block │ ← Related docs") - print(" ├─────────────────────────┤") - print(" │ Content... │") - print(" ├─────────────────────────┤") - print(" │ Siblings Block │ ← Previous/Next") - print(" └─────────────────────────┘") - print() - print(f"Open document in Vaiz: {DOCUMENT_ID}") - print() -except Exception as e: - print(f"❌ Error: {e}") - diff --git a/examples/document_with_code_blocks.py b/examples/document_with_code_blocks.py deleted file mode 100644 index f64e079..0000000 --- a/examples/document_with_code_blocks.py +++ /dev/null @@ -1,224 +0,0 @@ -""" -Example of creating documents with code blocks and syntax highlighting. - -Demonstrates using code_block() to display code snippets in various programming languages. -""" - -from config import get_client -from vaiz import ( - heading, - paragraph, - code_block, - bullet_list, - horizontal_rule, - text, - toc_block, -) - -# Initialize client -client = get_client() - -# Document ID (replace with your actual document ID) -DOCUMENT_ID = "68fb2452322665c43876937d" - -print("Creating document with code blocks...") -print("=" * 80) - -# Code examples in different languages -python_example = """def fibonacci(n): - \"\"\"Calculate the nth Fibonacci number.\"\"\" - if n <= 1: - return n - return fibonacci(n-1) + fibonacci(n-2) - -# Usage -result = fibonacci(10) -print(f"Fibonacci(10) = {result}")""" - -javascript_example = """// Async function to fetch data -async function fetchUserData(userId) { - try { - const response = await fetch(`/api/users/${userId}`); - const data = await response.json(); - return data; - } catch (error) { - console.error('Error fetching user:', error); - throw error; - } -}""" - -typescript_example = """interface User { - id: string; - name: string; - email: string; - roles: string[]; -} - -function createUser(data: Partial): User { - return { - id: crypto.randomUUID(), - name: data.name || 'Anonymous', - email: data.email || '', - roles: data.roles || ['user'] - }; -}""" - -bash_example = """#!/bin/bash - -# Install dependencies and start application -echo "Installing dependencies..." -npm install - -echo "Running tests..." -npm test - -echo "Starting application..." -npm start""" - -# Create document with code examples -content = [ - toc_block(), - - heading(1, "Code Examples Documentation"), - - paragraph( - "This document demonstrates using ", - text("code_block()", code=True), - " to display code with syntax highlighting in various programming languages." - ), - - horizontal_rule(), - - heading(2, "Python"), - - paragraph("Example of Fibonacci function implementation:"), - - code_block(code=python_example, language="python"), - - paragraph( - text("Features:", bold=True), - " Recursive implementation with docstring documentation." - ), - - horizontal_rule(), - - heading(2, "JavaScript"), - - paragraph("Async function for API interactions:"), - - code_block(code=javascript_example, language="javascript"), - - paragraph(text("Key points:", bold=True)), - bullet_list( - "Using async/await for asynchronous operations", - "Error handling with try/catch", - "Template literals for URL formatting" - ), - - horizontal_rule(), - - heading(2, "TypeScript"), - - paragraph("Typed user creation function:"), - - code_block(code=typescript_example, language="typescript"), - - paragraph(text("TypeScript benefits:", bold=True)), - bullet_list( - "Strong typing with interfaces", - "Using Partial for optional fields", - "IDE autocomplete and type checking" - ), - - horizontal_rule(), - - heading(2, "Bash"), - - paragraph("Deployment script:"), - - code_block(code=bash_example, language="bash"), - - paragraph("Script installs dependencies, runs tests, and starts the application."), - - horizontal_rule(), - - heading(1, "Usage in Code"), - - heading(2, "Basic Example"), - - paragraph("Creating a simple code block:"), - - code_block( - code='''from vaiz import code_block - -# Create block with Python code -code = code_block( - code='print("Hello, World!")', - language="python" -)''', - language="python" - ), - - heading(2, "Block Without Language"), - - paragraph("You can create a code block without specifying language:"), - - code_block( - code='''code = code_block(code="some code")''', - language="python" - ), - - heading(2, "Empty Block"), - - paragraph("Empty code block for later editing:"), - - code_block( - code='''empty = code_block()''', - language="python" - ), - - horizontal_rule(), - - heading(1, "Supported Languages"), - - paragraph("Code blocks support syntax highlighting for many languages:"), - - bullet_list( - "Python, JavaScript, TypeScript", - "Java, C, C++, C#", - "Go, Rust, Swift, Kotlin", - "Ruby, PHP, Perl", - "HTML, CSS, SCSS", - "SQL, JSON, YAML, XML", - "Bash, Shell, PowerShell", - "Markdown, LaTeX", - "And many more..." - ), - - paragraph( - text("Tip: ", bold=True), - "Always specify the programming language using the ", - text("language", code=True), - " parameter for proper syntax highlighting!" - ), -] - -try: - response = client.replace_json_document(DOCUMENT_ID, content) - print("✅ Document created successfully!") - print() - print("What was added:") - print(" • TOC block for navigation") - print(" • Code examples in Python, JavaScript, TypeScript, Bash") - print(" • Code blocks with syntax highlighting") - print(" • Usage examples and descriptions") - print() - print("Code block features:") - print(" ✓ Syntax highlighting for many languages") - print(" ✓ Multiline code support") - print(" ✓ Can be empty or without language specified") - print(" ✓ Automatic formatting and indentation") - print() -except Exception as e: - print(f"❌ Error: {e}") - diff --git a/examples/embed_blocks_example.py b/examples/embed_blocks_example.py deleted file mode 100644 index d359d0d..0000000 --- a/examples/embed_blocks_example.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -Example: Create a document with various embed blocks (YouTube, Figma, CodeSandbox, etc.). - -This example demonstrates how to use the embed_block() helper to create documents -with embedded content from various platforms. -""" - -from examples.config import get_client -from vaiz import ( - heading, - paragraph, - text, - embed_block, - EmbedType, - horizontal_rule, - bullet_list -) - - -def main(): - client = get_client() - client.verbose = True - - # Replace with your actual document ID - document_id = "YOUR_DOCUMENT_ID" - - # Build document with various embed blocks - content = [ - heading(1, "🎬 Embed Blocks Examples"), - - paragraph( - "This document demonstrates different types of embed blocks supported by Vaiz." - ), - - horizontal_rule(), - - # YouTube embed - heading(2, "YouTube Video"), - paragraph("Embed a YouTube video:"), - embed_block( - url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", - embed_type=EmbedType.YOUTUBE, - size="large" - ), - - horizontal_rule(), - - # Figma embed - heading(2, "Figma Design"), - paragraph("Embed a Figma design file:"), - embed_block( - url="https://www.figma.com/file/example", - embed_type=EmbedType.FIGMA, - size="large", - is_content_hidden=True - ), - - horizontal_rule(), - - # Vimeo embed - heading(2, "Vimeo Video"), - paragraph("Embed a Vimeo video:"), - embed_block( - url="https://vimeo.com/123456789", - embed_type=EmbedType.VIMEO - ), - - horizontal_rule(), - - # CodeSandbox embed - heading(2, "CodeSandbox"), - paragraph("Embed a live code sandbox:"), - embed_block( - url="https://codesandbox.io/s/example", - embed_type=EmbedType.CODESANDBOX, - size="large" - ), - - horizontal_rule(), - - # GitHub Gist embed - heading(2, "GitHub Gist"), - paragraph("Embed a GitHub Gist:"), - embed_block( - url="https://gist.github.com/username/1234567890abcdef", - embed_type=EmbedType.GITHUB_GIST - ), - - horizontal_rule(), - - # Miro board embed - heading(2, "Miro Board"), - paragraph("Embed a Miro whiteboard:"), - embed_block( - url="https://miro.com/app/board/example", - embed_type=EmbedType.MIRO, - size="large", - is_content_hidden=True - ), - - horizontal_rule(), - - # Generic Iframe embed (default, no embed_type needed) - heading(2, "Generic Iframe"), - paragraph("Embed any webpage using iframe:"), - embed_block( - url="https://example.com/embed", - size="medium" - ), - - horizontal_rule(), - - # Summary - heading(2, "📝 Summary"), - paragraph(text("Supported embed types:", bold=True)), - bullet_list( - "YouTube - Video hosting", - "Figma - Design files", - "Vimeo - Video hosting", - "CodeSandbox - Live code editor", - "GitHub Gist - Code snippets", - "Miro - Collaborative whiteboard", - "Iframe - Generic web content" - ), - - paragraph( - text("Size options:", bold=True), - text(" small, medium, large") - ), - - paragraph( - text("Note:", bold=True), - text(" For Figma and Miro embeds, you can use "), - text("is_content_hidden=True", code=True), - text(" to hide the content by default.") - ) - ] - - # Replace document content - print("\n=== Creating Document with Embed Blocks ===") - response = client.replace_json_document(document_id, content) - print(f"✅ Document updated successfully!") - print(f"Document ID: {document_id}") - print("=" * 50) - - -if __name__ == "__main__": - main() - diff --git a/examples/file_and_image_blocks.py b/examples/file_and_image_blocks.py deleted file mode 100644 index c352fe6..0000000 --- a/examples/file_and_image_blocks.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -Example: Using File and Image Blocks in Documents - -This example demonstrates how to: -1. Upload files to Vaiz -2. Create image blocks for images -3. Create files blocks for documents/PDFs -4. Combine them in rich document content -""" - -from examples.config import get_client, SPACE_ID -from vaiz import ( - heading, - paragraph, - text, - image_block, - files_block, - horizontal_rule, -) -from vaiz.models import CreateDocumentRequest, Kind -from vaiz.models.enums import UploadFileType -import os - - -def main(): - client = get_client() - client.verbose = True - - print("=== Creating Document with File and Image Blocks ===\n") - - # Step 1: Upload files - print("Step 1: Uploading files...") - - # Upload an image - image_path = "assets/example.png" - if not os.path.exists(image_path): - print(f"⚠️ Image file not found: {image_path}") - print("Skipping image upload...") - image_uploaded = None - else: - image_uploaded = client.upload_file(image_path, file_type=UploadFileType.Image) - print(f"✓ Uploaded image: {image_uploaded.file.name}") - print(f" File ID: {image_uploaded.file.id}") - print(f" URL: {image_uploaded.file.url}") - print(f" Size: {image_uploaded.file.size} bytes") - - # Upload a PDF - pdf_path = "assets/example.pdf" - if not os.path.exists(pdf_path): - print(f"⚠️ PDF file not found: {pdf_path}") - print("Skipping PDF upload...") - pdf_uploaded = None - else: - pdf_uploaded = client.upload_file(pdf_path, file_type=UploadFileType.Pdf) - print(f"✓ Uploaded PDF: {pdf_uploaded.file.name}") - print(f" File ID: {pdf_uploaded.file.id}") - print(f" URL: {pdf_uploaded.file.url}") - print(f" Size: {pdf_uploaded.file.size} bytes") - - # Step 2: Create document - print("\nStep 2: Creating document...") - - create_request = CreateDocumentRequest( - kind=Kind.Space, - kind_id=SPACE_ID, - title="Example: File and Image Blocks", - index=0 - ) - doc_response = client.create_document(create_request) - document_id = doc_response.payload.document.id - print(f"✓ Created document: {document_id}") - - # Step 3: Build content with file and image blocks - print("\nStep 3: Adding file and image blocks...") - - content = [ - heading(1, "Document with Files and Images"), - - paragraph( - text("This document demonstrates file and image blocks.") - ), - - horizontal_rule(), - ] - - # Add image block if image was uploaded - if image_uploaded: - content.extend([ - heading(2, "Image Block Example"), - - paragraph(text("Here's an embedded image:")), - - # New simple API - just pass the file! - image_block(file=image_uploaded.file), - - paragraph(text("Image displayed above", italic=True)), - - horizontal_rule(), - ]) - - # Add files block if files were uploaded - files_to_attach = [] - - if pdf_uploaded: - files_to_attach.append({ - "fileId": pdf_uploaded.file.id, - "url": pdf_uploaded.file.url, - "name": pdf_uploaded.file.name, - "size": pdf_uploaded.file.size, - "extension": pdf_uploaded.file.ext, - "type": "Pdf" # Pdf, Image, Video, etc. - }) - - # You can add more files here - # if another_file_uploaded: - # files_to_attach.append({...}) - - if files_to_attach: - content.extend([ - heading(2, "Files Block Example"), - - paragraph(text("Attached files:")), - - files_block(*files_to_attach), - - paragraph( - text(f"{len(files_to_attach)} file(s) attached above", italic=True) - ), - - horizontal_rule(), - ]) - - # Add summary - content.extend([ - heading(2, "Summary"), - - paragraph( - text("This document contains:", bold=True) - ), - - paragraph( - text(f"• Image blocks: {1 if image_uploaded else 0}") - ), - paragraph( - text(f"• Files blocks: {1 if files_to_attach else 0}") - ), - paragraph( - text(f"• Total files: {len(files_to_attach) + (1 if image_uploaded else 0)}") - ), - ]) - - # Step 4: Update document - try: - client.replace_json_document(document_id, content) - print("\n✅ Document updated successfully with file and image blocks!") - print(f"\n🔗 View document: https://vaiz.app/document/{document_id}") - - # Verify blocks were created - print("\nVerifying blocks...") - doc_content = client.get_json_document(document_id) - - image_block_count = 0 - files_block_count = 0 - - for node in doc_content.get("default", {}).get("content", []): - if node.get("type") == "image-block": - image_block_count += 1 - elif node.get("type") == "files": - files_block_count += 1 - - print(f"✓ Found {image_block_count} image block(s)") - print(f"✓ Found {files_block_count} files block(s)") - - except Exception as e: - print(f"❌ Error: {e}") - import traceback - traceback.print_exc() - - print("\n=== Example completed ===") - - -if __name__ == "__main__": - main() - diff --git a/examples/get_document.py b/examples/get_document.py index fbb741d..bc42d2f 100644 --- a/examples/get_document.py +++ b/examples/get_document.py @@ -1,5 +1,5 @@ """ -Example: Fetch JSON document content by document ID. +Example: Fetch document content as Markdown by document ID. """ from examples.config import get_client @@ -12,17 +12,11 @@ def main(): # Example document ID document_id = "6878ff0ad2c2d60e246402c2" - doc = client.get_json_document(document_id) - print("\n=== Document JSON ===") - # Print only top-level keys to avoid huge output - if isinstance(doc, dict): - print(f"keys: {list(doc.keys())}") - else: - print(doc) - print("====================\n") + markdown = client.get_markdown_document(document_id) + print("\n=== Document Markdown ===") + print(markdown) + print("=========================\n") if __name__ == "__main__": main() - - diff --git a/examples/markdown_documents.py b/examples/markdown_documents.py new file mode 100644 index 0000000..5e3fdd1 --- /dev/null +++ b/examples/markdown_documents.py @@ -0,0 +1,103 @@ +""" +Example: Working with document content using Markdown. + +Markdown is the only supported way to read and write rich document content +in the SDK. Markdown is converted to native editor blocks on the server +(headings, lists, tables, code blocks, checklists, links, etc.) and can be +read back as Markdown. +""" + +from examples.config import get_client +from examples.test_helpers import get_or_create_document_id + + +def replace_markdown(): + """Replace document content with Markdown.""" + client = get_client() + document_id = get_or_create_document_id() + + markdown = """# Project Overview + +Some **bold** and *italic* text with `inline code` and a [link](https://vaiz.app). + +## Checklist + +- [x] Design approved +- [ ] Implementation +- [ ] Code review + +## Team + +| Name | Role | +| --- | --- | +| Alice | Engineer | +| Bob | Designer | + +```python +print("Code blocks work too") +``` +""" + + client.replace_markdown_document(document_id, markdown) + print(f"✅ Replaced content of document {document_id}") + return document_id + + +def append_markdown(document_id: str): + """Append Markdown content to an existing document.""" + client = get_client() + + client.append_markdown_document( + document_id, + "\n## Update\n\nAppended without removing existing content." + ) + print("✅ Appended markdown content") + + +def read_markdown(document_id: str): + """Read document content back as Markdown.""" + client = get_client() + + markdown = client.get_markdown_document(document_id) + print("=== Document Markdown ===") + print(markdown) + + +def task_description_workflow(): + """Create a task and set a rich Markdown description.""" + from vaiz.models import CreateTaskRequest, TaskPriority + from examples.config import BOARD_ID, GROUP_ID + + client = get_client() + + # 1. Create the task (plain-text description only) + task_response = client.create_task( + CreateTaskRequest( + name="Markdown Description Example", + board=BOARD_ID, + group=GROUP_ID, + priority=TaskPriority.General, + ) + ) + task = task_response.task + + # 2. Set a rich Markdown description via the task's document + client.replace_markdown_document( + task.document, + "# Goal\n\nShip the feature.\n\n- [ ] Implement\n- [ ] Test\n- [ ] Release" + ) + print(f"✅ Task {task.hrid} created with markdown description") + + # 3. Read it back + print(task.get_task_description(client)) + + +def main(): + document_id = replace_markdown() + append_markdown(document_id) + read_markdown(document_id) + task_description_workflow() + + +if __name__ == "__main__": + main() diff --git a/examples/mention_blocks.py b/examples/mention_blocks.py deleted file mode 100644 index db49a98..0000000 --- a/examples/mention_blocks.py +++ /dev/null @@ -1,163 +0,0 @@ -""" -Example: Using Mention Blocks in Documents - -This example demonstrates how to create documents with mention blocks -that reference users, documents, tasks, and milestones. -""" - -from examples.config import get_client -from vaiz.helpers import ( - paragraph, - text, - heading, - mention_user, - mention_document, - mention_task, - mention_milestone, -) - - -def main(): - client = get_client() - client.verbose = True - - print("=== Creating Document with Mention Blocks ===\n") - - # Get real IDs from API - print("Getting real IDs from API...") - - # Get current member ID (for user mentions) - profile = client.get_profile() - member_id = profile.profile.member_id - print(f"✓ Member ID: {member_id}") - - # Get a document ID - from vaiz.models import GetDocumentsRequest, Kind, CreateDocumentRequest - from examples.config import SPACE_ID - - docs_response = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=SPACE_ID) - ) - reference_doc_id = None - if docs_response.payload.documents: - reference_doc_id = docs_response.payload.documents[0].id - print(f"✓ Reference Document ID: {reference_doc_id}") - - # Get a task ID - from vaiz.models import GetTasksRequest - tasks_response = client.get_tasks(GetTasksRequest()) - task_id = None - if tasks_response.payload.tasks: - task_id = tasks_response.payload.tasks[0].id - print(f"✓ Task ID: {task_id}") - - # Get a milestone ID - milestones_response = client.get_milestones() - milestone_id = None - if milestones_response.milestones: - milestone_id = milestones_response.milestones[0].id - print(f"✓ Milestone ID: {milestone_id}") - - print("\nCreating new document with mentions...") - - # Create a new document in Space - create_request = CreateDocumentRequest( - kind=Kind.Space, - kind_id=SPACE_ID, - title="Example: Mention Blocks", - index=0 - ) - doc_response = client.create_document(create_request) - document_id = doc_response.payload.document.id - - print(f"✓ Created document: {document_id}") - - # Build content with real mentions - content = [ - heading(1, "Document with Mentions"), - - paragraph( - text("This document contains various mention types:") - ), - - # Mention a user - paragraph( - text("User mention: "), - mention_user(member_id), - text(" - this mentions a user") - ), - ] - - # Add document mention if available - if reference_doc_id: - content.append(paragraph( - text("Document mention: "), - mention_document(reference_doc_id), - text(" - this mentions another document") - )) - - # Add task mention if available - if task_id: - content.append(paragraph( - text("Task mention: "), - mention_task(task_id), - text(" - this mentions a task") - )) - - # Add milestone mention if available - if milestone_id: - content.append(paragraph( - text("Milestone mention: "), - mention_milestone(milestone_id), - text(" - this mentions a milestone") - )) - - # Add section with multiple mentions - content.append(heading(2, "Multiple Mentions in One Paragraph")) - - multiple_mentions_para = [ - text("You can mention multiple items: "), - mention_user(member_id), - ] - - if task_id: - multiple_mentions_para.extend([ - text(" assigned to task "), - mention_task(task_id), - ]) - - if milestone_id: - multiple_mentions_para.extend([ - text(" in milestone "), - mention_milestone(milestone_id) - ]) - - content.append(paragraph(*multiple_mentions_para)) - - try: - client.replace_json_document(document_id, content) - print("\n✅ Document updated successfully with mention blocks!") - print(f"\n🔗 View document: https://vaiz.app/document/{document_id}") - - # Verify mentions were created - print("\nVerifying mentions...") - doc_content = client.get_json_document(document_id) - - mention_count = 0 - for node in doc_content.get("default", {}).get("content", []): - if node.get("type") == "paragraph" and "content" in node: - for child in node["content"]: - if child.get("type") == "custom-mention": - mention_count += 1 - - print(f"✓ Found {mention_count} mention block(s) in document") - - except Exception as e: - print(f"❌ Error: {e}") - - print("\n=== Example completed ===") - - -if __name__ == "__main__": - main() - diff --git a/examples/post_comment.py b/examples/post_comment.py index 7a09b47..7101147 100644 --- a/examples/post_comment.py +++ b/examples/post_comment.py @@ -74,6 +74,43 @@ def post_comment_with_html(): print("======================\n") +def post_comment_with_markdown(): + """Post a comment with Markdown content (recommended format).""" + client = get_client() + + document_id = get_example_document_id() + + try: + markdown = ( + "Comment with **bold** and *italic*\n\n" + "- Item 1\n" + "- Item 2\n\n" + "`inline code` and a [link](https://vaiz.app)" + ) + + response = client.post_comment( + document_id=document_id, + markdown=markdown + ) + + print("Markdown comment posted successfully!") + print(f"Comment ID: {response.comment.id}") + print(f"Content version: {response.comment.content_version}") # 2 = rich content + print(f"Rendered content: {response.comment.content}") + + # Edit the comment with new markdown + edit_response = client.edit_comment( + comment_id=response.comment.id, + markdown="**Updated** markdown comment" + ) + print(f"Edited at: {edit_response.comment.edited_at}") + + return response.comment.id + + except Exception as e: + print(f"Error posting markdown comment: {e}") + + def post_comment_with_files(): """Post a comment with file attachments to a document.""" client = get_client() @@ -548,6 +585,10 @@ def main(): print("-" * 40) post_comment_with_html() + print("\n1b. Posting Markdown comment (recommended)...") + print("-" * 40) + post_comment_with_markdown() + print("\n2. Posting comment with files...") print("-" * 40) post_comment_with_files() diff --git a/examples/replace_document.py b/examples/replace_document.py deleted file mode 100644 index cf6a49c..0000000 --- a/examples/replace_document.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Example: Replace document content using the replaceDocument API. -""" - -import json -from examples.config import get_client -from vaiz.models import CreateTaskRequest, TaskPriority - - -def main(): - client = get_client() - client.verbose = True - - # First, create a task with initial description to get a document ID - task = CreateTaskRequest( - name="Task for Document Replacement Demo", - group=client.space_id, # Using space_id as fallback for demo - board="your_board_id", - priority=TaskPriority.General, - description="Initial description that will be replaced" - ) - - try: - task_response = client.create_task(task) - document_id = task_response.task.document - print(f"Created task with document ID: {document_id}") - - # Get initial document content - initial_content = client.get_json_document(document_id) - print(f"Initial content: {json.dumps(initial_content, indent=2)}") - - # Create new content as PLAIN TEXT (current API supports plain text only) - new_description_text = ( - "🚀 Document Content Replaced!\n\n" - "This is the new content that completely replaced the original task description.\n\n" - "Features Demonstrated:\n" - "- ✅ Complete content replacement via API\n" - "- 📝 Plain text formatting\n" - "- 🎯 Direct document manipulation\n" - "- 🔄 Real-time content updates\n\n" - "Next Steps:\n" - "1. Verify content changed\n" - "2. Test with different content types\n" - "3. Explore file attachments\n\n" - "Note: Content was replaced using the replace_document API method from the Vaiz Python SDK." - ) - - # Replace document content - print("\n=== Replacing Document Content ===") - replace_response = client.replace_document( - document_id=document_id, - description=new_description_text - ) - print("✅ Document content replaced successfully!") - - # Verify the change - updated_content = client.get_json_document(document_id) - print(f"\nUpdated content: {json.dumps(updated_content, indent=2)}") - - print(f"\n🎉 Document {document_id} content successfully replaced!") - - except Exception as e: - print(f"❌ Error: {e}") - - -if __name__ == "__main__": - main() diff --git a/examples/replace_json_document.py b/examples/replace_json_document.py deleted file mode 100644 index f316d39..0000000 --- a/examples/replace_json_document.py +++ /dev/null @@ -1,171 +0,0 @@ -""" -Example: Replace document content with rich JSON content using the replaceJSONDocument API. - -This example demonstrates how to replace document content with structured JSON content -in the document editor format, allowing for rich formatting, links, and other features. -""" - -import json -from examples.config import get_client -from vaiz.models import CreateTaskRequest, TaskPriority - - -def main(): - client = get_client() - client.verbose = True - - # First, create a task with initial description to get a document ID - task = CreateTaskRequest( - name="Task for JSON Document Replacement Demo", - group=client.space_id, # Using space_id as fallback for demo - board="your_board_id", - priority=TaskPriority.General, - description="Initial description that will be replaced with rich content" - ) - - try: - task_response = client.create_task(task) - document_id = task_response.task.document - print(f"Created task with document ID: {document_id}") - - # Get initial document content - initial_content = client.get_json_document(document_id) - print(f"Initial content: {json.dumps(initial_content, indent=2)}") - - # Create rich JSON content in document structure format - json_content = [ - { - "type": "heading", - "attrs": {"level": 1}, - "content": [ - {"type": "text", "text": "🚀 Document with Rich Content"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "This document was created using "}, - { - "type": "text", - "marks": [{"type": "bold"}], - "text": "structured JSON content" - }, - {"type": "text", "text": " via the replaceJSONDocument API."} - ] - }, - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "✨ Features"} - ] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [{"type": "bold"}], - "text": "Rich text formatting" - }, - {"type": "text", "text": " with bold, italic, and more"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Structured content with "}, - { - "type": "text", - "marks": [{"type": "italic"}], - "text": "headings" - }, - {"type": "text", "text": " and lists"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Direct control over document structure"} - ] - } - ] - } - ] - }, - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "🔗 Links"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "You can also add links: "}, - { - "type": "text", - "marks": [ - { - "type": "link", - "attrs": { - "href": "https://vaiz.app", - "target": "_blank" - } - } - ], - "text": "Visit Vaiz" - } - ] - }, - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [{"type": "code"}], - "text": "code snippets" - }, - {"type": "text", "text": " and more!"} - ] - } - ] - - # Replace document content with JSON - print("\n=== Replacing Document with JSON Content ===") - replace_response = client.replace_json_document( - document_id=document_id, - content=json_content - ) - print("✅ Document content replaced with rich JSON content successfully!") - - # Verify the change - updated_content = client.get_json_document(document_id) - print(f"\nUpdated content: {json.dumps(updated_content, indent=2)}") - - print(f"\n🎉 Document {document_id} now contains rich formatted content!") - - except Exception as e: - print(f"❌ Error: {e}") - - -if __name__ == "__main__": - main() - diff --git a/examples/replace_json_document_complex.py b/examples/replace_json_document_complex.py deleted file mode 100644 index a3debf3..0000000 --- a/examples/replace_json_document_complex.py +++ /dev/null @@ -1,453 +0,0 @@ -""" -Example: Replace document with complex structure using replaceJSONDocument API. - -This example demonstrates creating a comprehensive document with various document structure elements: -- Multiple heading levels -- Rich text formatting (bold, italic, strikethrough, code) -- Bullet lists with nesting -- Ordered lists -- Code blocks with syntax highlighting -- Links with attributes -- Blockquotes -- Horizontal rules -""" - -import json -from examples.config import get_client -from vaiz.models import CreateTaskRequest, TaskPriority - - -def main(): - client = get_client() - client.verbose = True - - # Create a task to get a document ID - task = CreateTaskRequest( - name="Complex Document Structure Demo", - group=client.space_id, - board="your_board_id", - priority=TaskPriority.General, - description="Initial description" - ) - - try: - task_response = client.create_task(task) - document_id = task_response.task.document - print(f"Created task with document ID: {document_id}\n") - - # Create comprehensive complex structure - complex_content = [ - # Main title - { - "type": "heading", - "attrs": {"level": 1}, - "content": [ - {"type": "text", "text": "📚 Comprehensive Project Documentation"} - ] - }, - - # Subtitle with formatting - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "italic"}], "text": "Last updated: 2025-10-22 | "}, - {"type": "text", "marks": [{"type": "bold"}], "text": "Status: Active"} - ] - }, - - # Horizontal separator - { - "type": "horizontalRule" - }, - - # Overview section - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "📋 Project Overview"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "This document provides a "}, - {"type": "text", "marks": [{"type": "bold"}], "text": "comprehensive overview"}, - {"type": "text", "text": " of the project architecture, implementation details, and "}, - { - "type": "text", - "marks": [ - {"type": "link", "attrs": {"href": "https://vaiz.app/roadmap", "target": "_blank"}} - ], - "text": "roadmap" - }, - {"type": "text", "text": "."} - ] - }, - - # Key Features with nested lists - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "✨ Key Features"} - ] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Real-time collaboration: "}, - {"type": "text", "text": "Multiple users can work simultaneously"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Rich text editing: "}, - {"type": "text", "text": "Support for "}, - {"type": "text", "marks": [{"type": "italic"}], "text": "formatting"}, - {"type": "text", "text": ", "}, - {"type": "text", "marks": [{"type": "code"}], "text": "code"}, - {"type": "text", "text": ", and more"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "API Integration: "}, - {"type": "text", "text": "Comprehensive REST API with Python SDK"} - ] - }, - # Nested list - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Task management"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Document collaboration"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Custom field management"} - ] - } - ] - } - ] - } - ] - } - ] - }, - - # Technical Stack - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "🛠 Technical Stack"} - ] - }, - { - "type": "orderedList", - "attrs": {"start": 1}, - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Backend: "}, - {"type": "text", "text": "Node.js, MongoDB, Redis"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Frontend: "}, - {"type": "text", "text": "React, TypeScript, Rich Editor"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "SDK: "}, - {"type": "text", "text": "Python 3.8+"} - ] - } - ] - } - ] - }, - - # Code examples - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "💻 Quick Start"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Install the SDK:"} - ] - }, - { - "type": "codeBlock", - "attrs": {"language": "bash"}, - "content": [ - {"type": "text", "text": "pip install vaiz-sdk"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Initialize the client:"} - ] - }, - { - "type": "codeBlock", - "attrs": {"language": "python"}, - "content": [ - {"type": "text", "text": "from vaiz import VaizClient\n\nclient = VaizClient(token=\"your_token\")\ntask = client.get_task(\"PRJ-123\")"} - ] - }, - - # Important notice - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "💡 Important Notes"} - ] - }, - { - "type": "blockquote", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Security Notice: "}, - {"type": "text", "text": "Always store your API token securely. Never commit tokens to version control. Use environment variables or secure vault solutions."} - ] - } - ] - }, - - # Timeline - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "📅 Implementation Timeline"} - ] - }, - { - "type": "orderedList", - "attrs": {"start": 1}, - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Phase 1: "}, - {"type": "text", "marks": [{"type": "strikethrough"}], "text": "Core API development"}, - {"type": "text", "text": " "}, - {"type": "text", "marks": [{"type": "code"}], "text": "✓ Completed"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Phase 2: "}, - {"type": "text", "text": "Python SDK implementation "}, - {"type": "text", "marks": [{"type": "code"}], "text": "⏳ In Progress"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Phase 3: "}, - {"type": "text", "text": "Advanced features (webhooks, real-time) "}, - {"type": "text", "marks": [{"type": "code"}], "text": "📋 Planned"} - ] - } - ] - } - ] - }, - - # Resources - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "🔗 Resources"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Official links:"} - ] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [{"type": "link", "attrs": {"href": "https://docs.vaiz.app", "target": "_blank"}}], - "text": "Documentation" - } - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [{"type": "link", "attrs": {"href": "https://github.com/vaizcom/vaiz-python-sdk", "target": "_blank"}}], - "text": "GitHub Repository" - } - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [{"type": "link", "attrs": {"href": "https://vaiz.app/community", "target": "_blank"}}], - "text": "Community Forum" - } - ] - } - ] - } - ] - }, - - # Footer - { - "type": "horizontalRule" - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "italic"}], "text": "This document was generated programmatically using the "}, - {"type": "text", "marks": [{"type": "code"}], "text": "replace_json_document"}, - {"type": "text", "marks": [{"type": "italic"}], "text": " API method."} - ] - } - ] - - # Replace document with complex structure - print("=== Replacing Document with Complex Structure ===") - replace_response = client.replace_json_document( - document_id=document_id, - content=complex_content - ) - print("✅ Document content replaced with complex structure!\n") - - # Print summary - print("📊 Document Structure Summary:") - print(" ✓ Multiple heading levels (H1, H2)") - print(" ✓ Rich text formatting (bold, italic, strikethrough, code)") - print(" ✓ Bullet lists with nested sublists") - print(" ✓ Ordered/numbered lists") - print(" ✓ Code blocks with syntax highlighting (bash, python)") - print(" ✓ Hyperlinks with custom attributes") - print(" ✓ Blockquotes for important notices") - print(" ✓ Horizontal rules for visual separation") - print(" ✓ Emoji icons for visual appeal\n") - - # Verify the change - updated_content = client.get_json_document(document_id) - print(f"🎉 Document {document_id} now contains comprehensive structured content!") - print(f"📏 Total content blocks: {len(complex_content)}") - - except Exception as e: - print(f"❌ Error: {e}") - - -if __name__ == "__main__": - main() - diff --git a/examples/replace_json_document_with_helpers.py b/examples/replace_json_document_with_helpers.py deleted file mode 100644 index 2163ab3..0000000 --- a/examples/replace_json_document_with_helpers.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Example: Using Document Structure helper functions for type-safe document content creation. - -This example demonstrates how to use the built-in document structure helper functions -to create rich document content in a type-safe, readable way. -""" - -from examples.config import get_client -from vaiz.models import CreateTaskRequest, TaskPriority - -# Import document structure builder functions -from vaiz import ( - text, paragraph, heading, bullet_list, ordered_list, - list_item, link_text, horizontal_rule, blockquote, details -) - - -def main(): - client = get_client() - client.verbose = True - - # Create a task - task = CreateTaskRequest( - name="Type-Safe Document with Helpers Demo", - group=client.space_id, - board="your_board_id", - priority=TaskPriority.General, - description="Will be replaced" - ) - - try: - task_response = client.create_task(task) - document_id = task_response.task.document - print(f"Created task: {task_response.task.hrid}\n") - - # Build content using helper functions - type-safe and readable! - content = [ - # Title - heading(1, "🚀 Project Documentation"), - - # Subtitle with formatting - paragraph( - text("Last updated: 2025-10-22 | ", italic=True), - text("Status: Active", bold=True) - ), - - # Separator - horizontal_rule(), - - # Overview section - heading(2, "📋 Overview"), - paragraph( - "This project uses ", - text("structured document format", bold=True), - " for rich text editing. Learn more at ", - link_text("Vaiz Docs", "https://docs.vaiz.app"), - "." - ), - - # Features with bullet list - heading(2, "✨ Key Features"), - bullet_list( - # Simple string items - "Real-time collaboration", - "Rich text editing", - # Complex item with nested content - list_item( - paragraph( - text("API Integration", bold=True), - " - Full Python SDK support" - ), - # Nested list! - bullet_list( - "Task management", - "Document operations", - "Custom fields" - ) - ) - ), - - # Tech stack with ordered list - heading(2, "🛠 Tech Stack"), - ordered_list( - list_item(paragraph(text("Backend: ", bold=True), "Node.js, MongoDB")), - list_item(paragraph(text("Frontend: ", bold=True), "React, TypeScript")), - list_item(paragraph(text("SDK: ", bold=True), "Python 3.8+")) - ), - - # Code example - heading(2, "💻 Quick Start"), - paragraph("Install the SDK: ", text("pip install vaiz-sdk", code=True)), - paragraph("Basic usage:"), - paragraph(text("from vaiz import VaizClient", code=True)), - paragraph(text("client = VaizClient(token='...')", code=True)), - paragraph(text("task = client.get_task('PRJ-123')", code=True)), - - # Important note - heading(2, "💡 Important"), - paragraph( - text("Security Notice: ", bold=True), - "Always store your API token securely using environment variables." - ), - - # Quote section - heading(2, "📝 Philosophy"), - blockquote( - paragraph( - text("Good software is like a good joke: ", italic=True), - text("it needs no explanation.", bold=True, italic=True) - ), - paragraph( - "— The Pragmatic Programmer" - ) - ), - - # Collapsible details section - heading(2, "🔍 Advanced Usage"), - details( - "Click to expand: API Configuration", - paragraph( - text("Endpoint: ", bold=True), - text("https://api.vaiz.app/v1", code=True) - ), - paragraph( - text("Authentication: ", bold=True), - "Bearer token required" - ), - paragraph( - text("Rate Limit: ", bold=True), - "1000 requests per hour" - ) - ), - - # Links section - heading(2, "🔗 Resources"), - bullet_list( - list_item(paragraph(link_text("Documentation", "https://docs.vaiz.app"))), - list_item(paragraph(link_text("GitHub", "https://github.com/vaizcom/vaiz-python-sdk"))), - list_item(paragraph(link_text("Community", "https://vaiz.app/community"))) - ), - - # Footer - horizontal_rule(), - paragraph( - text("Generated with ", italic=True), - text("replace_json_document", code=True), - text(" helper functions", italic=True) - ) - ] - - # Replace document content - print("=== Replacing document with type-safe content ===") - client.replace_json_document( - document_id=document_id, - content=content - ) - print("✅ Document replaced successfully!\n") - - print("📊 Content Statistics:") - print(f" Total blocks: {len(content)}") - print(f" Headings: {sum(1 for c in content if c.get('type') == 'heading')}") - print(f" Paragraphs: {sum(1 for c in content if c.get('type') == 'paragraph')}") - print(f" Lists: {sum(1 for c in content if c.get('type') in ['bulletList', 'orderedList'])}") - print(f"\n💡 Content built using document structure helpers!") - - print(f"\n🎉 Check task {task_response.task.hrid} in Vaiz!") - - except Exception as e: - print(f"❌ Error: {e}") - - -if __name__ == "__main__": - main() - diff --git a/examples/replace_json_document_with_table.py b/examples/replace_json_document_with_table.py deleted file mode 100644 index 67f9049..0000000 --- a/examples/replace_json_document_with_table.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Example: Creating documents with tables using Document Structure helpers. - -This example demonstrates how to create structured tables in documents -using the table helper functions. -""" - -from examples.config import get_client -from vaiz.models import CreateTaskRequest, TaskPriority -from vaiz import ( - heading, paragraph, text, table, table_row, - table_cell, horizontal_rule -) - - -def main(): - client = get_client() - client.verbose = True - - # Create a task - task = CreateTaskRequest( - name="Project Status Report with Table", - group=client.space_id, - board="your_board_id", - priority=TaskPriority.General, - description="Initial description" - ) - - try: - task_response = client.create_task(task) - document_id = task_response.task.document - print(f"Created task: {task_response.task.hrid}\n") - - # Build content with table - content = [ - heading(1, "📊 Project Status Report"), - - paragraph( - text("Report Date: ", bold=True), - "2025-10-22" - ), - - horizontal_rule(), - - heading(2, "Task Progress"), - - # Simple table (first row is treated as header in UI) - table( - table_row("Task", "Assignee", "Status", "Priority"), # Header row - table_row("Design UI mockups", "John Doe", "✅ Done", "High"), - table_row("Implement API", "Jane Smith", "⏳ In Progress", "High"), - table_row("Write documentation", "Mike Johnson", "📋 Todo", "Medium"), - table_row("Testing", "Sarah Wilson", "📋 Todo", "Low") - ), - - heading(2, "Milestones"), - - # Table with formatting - table( - table_row( - table_cell(paragraph(text("Milestone", bold=True))), - table_cell(paragraph(text("Due Date", bold=True))), - table_cell(paragraph(text("Completion", bold=True))) - ), - table_row( - table_cell(paragraph(text("Alpha Release", bold=True))), - "2025-11-01", - table_cell(paragraph(text("85%", italic=True))) - ), - table_row( - table_cell(paragraph(text("Beta Release", bold=True))), - "2025-11-15", - table_cell(paragraph(text("40%", italic=True))) - ), - table_row( - table_cell(paragraph(text("Production", bold=True))), - "2025-12-01", - table_cell(paragraph(text("10%", italic=True))) - ) - ), - - heading(2, "Team Metrics"), - - table( - table_row("Metric", "Value"), # Header row - table_row("Total Tasks", "47"), - table_row("Completed", "28"), - table_row("In Progress", "12"), - table_row("Blocked", "3"), - table_row("Overdue", "4") - ), - - horizontal_rule(), - - paragraph( - text("Generated with ", italic=True), - text("table()", code=True), - text(" helper function", italic=True) - ) - ] - - # Replace document - print("=== Creating document with tables ===") - client.replace_json_document( - document_id=document_id, - content=content - ) - print("✅ Document with tables created successfully!\n") - - print("📊 Content Summary:") - print(f" Total blocks: {len(content)}") - print(f" Tables: {sum(1 for c in content if c.get('type') == 'table')}") - print(f" Headings: {sum(1 for c in content if c.get('type') == 'heading')}") - - print(f"\n🎉 Check task {task_response.task.hrid} in Vaiz to see the tables!") - - except Exception as e: - print(f"❌ Error: {e}") - import traceback - traceback.print_exc() - - -if __name__ == "__main__": - main() - diff --git a/examples/table_with_headers.py b/examples/table_with_headers.py deleted file mode 100644 index 1ce8b2e..0000000 --- a/examples/table_with_headers.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env python3 -""" -Example: Using table_header() for semantic table headers - -This example demonstrates the new table_header() function that creates -proper HTML elements instead of for better accessibility -and semantic structure. -""" - -import sys -import os -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from config import get_client -from vaiz import ( - heading, paragraph, text, table, table_row, - table_cell, table_header, horizontal_rule -) -from vaiz.models import CreateDocumentRequest, Kind - - -def main(): - print("=== Table Header Example ===\n") - - client = get_client() - - # Get current user's member ID for Personal document - profile = client.get_profile() - member_id = profile.profile.member_id - - # Create Personal document - doc_response = client.create_document( - CreateDocumentRequest( - kind=Kind.Member, - kind_id=member_id, - title="Table Headers Demo", - index=0 - ) - ) - document_id = doc_response.payload.document.id - print(f"✅ Document created: {document_id}\n") - - # Build document with various table examples - content = [ - heading(1, "📊 Table Headers Demo"), - - paragraph( - "This document demonstrates the ", - text("table_header()", code=True), - " function for creating semantic table headers." - ), - - horizontal_rule(), - - heading(2, "Example 1: Simple Table with Headers"), - - paragraph("Using table_header() for proper semantic HTML structure:"), - - table( - table_row( - table_header("Task"), - table_header("Assignee"), - table_header("Status"), - table_header("Priority") - ), - table_row("Implement API", "John", "✅ Done", "High"), - table_row("Write tests", "Jane", "⏳ In Progress", "High"), - table_row("Documentation", "Mike", "📋 Todo", "Medium") - ), - - horizontal_rule(), - - heading(2, "Example 2: Table with Merged Header"), - - paragraph("Headers support colspan and rowspan just like cells:"), - - table( - # Main header spanning all columns - table_row( - table_header(paragraph(text("Q1-Q4 2025 Performance", bold=True)), colspan=5) - ), - # Subheaders - table_row( - table_header("Metric"), - table_header("Q1"), - table_header("Q2"), - table_header("Q3"), - table_header("Q4") - ), - # Data rows - table_row("Revenue", "$100K", "$120K", "$150K", "$180K"), - table_row("Users", "1,000", "1,500", "2,200", "3,000"), - table_row("Growth", "+20%", "+25%", "+30%", "+35%"), - # Summary row - table_row( - table_cell(paragraph(text("Total Revenue", bold=True))), - table_cell(paragraph(text("$550K", bold=True)), colspan=4) - ) - ), - - horizontal_rule(), - - heading(2, "Example 3: Complex Headers"), - - paragraph("Multiple header rows with different spans:"), - - table( - # Top header - table_row( - table_header(""), - table_header(paragraph(text("Frontend", bold=True)), colspan=2), - table_header(paragraph(text("Backend", bold=True)), colspan=2) - ), - # Sub-headers - table_row( - table_header("Team"), - table_header("React"), - table_header("Vue"), - table_header("Node.js"), - table_header("Python") - ), - # Data rows - table_row("Team A", "3", "2", "2", "1"), - table_row("Team B", "2", "1", "3", "2"), - table_row( - table_cell(paragraph(text("Total", bold=True))), - table_cell("5"), - table_cell("3"), - table_cell("5"), - table_cell("3") - ) - ), - - horizontal_rule(), - - heading(2, "Benefits"), - - paragraph(text("Why use table_header()?", bold=True)), - - paragraph( - "• ", text("Semantic HTML", bold=True), " - Creates proper ", - text("", code=True), " elements instead of ", text("", code=True) - ), - paragraph( - "• ", text("Accessibility", bold=True), " - Screen readers can identify headers" - ), - paragraph( - "• ", text("Consistent styling", bold=True), " - Headers are styled appropriately" - ), - paragraph( - "• ", text("Full feature support", bold=True), " - Works with colspan, rowspan, and formatting" - ), - - horizontal_rule(), - - paragraph( - text("Created with Vaiz Python SDK ", italic=True), - text("v0.9+", code=True) - ) - ] - - # Replace document content - response = client.replace_json_document(document_id, content) - print("✅ Document populated with table header examples") - - # Verify - saved = client.get_json_document(document_id) - saved_blocks = saved.get("default", {}).get("content", []) - tables = [b for b in saved_blocks if b.get("type") == "extension-table"] - - print(f"✅ Created {len(tables)} tables with proper headers") - print(f"\nDocument ID: {document_id}") - print("View this document in Vaiz to see the table headers!") - - -if __name__ == "__main__": - main() - diff --git a/examples/task_list_checklist.py b/examples/task_list_checklist.py deleted file mode 100644 index 9ff7b0c..0000000 --- a/examples/task_list_checklist.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env python3 -""" -Example: Creating and using task lists (checklists) in documents - -Task lists allow you to create interactive checklists with: -- Checkbox items that can be checked/unchecked -- Nested task lists (multi-level checklists) -- Mixed content (paragraphs + task lists) -""" - -import os -from vaiz import VaizClient, heading, paragraph, task_list, task_item - - -def main(): - # Initialize client - api_key = os.getenv("VAIZ_API_KEY") - space_id = os.getenv("VAIZ_SPACE_ID") - - if not api_key or not space_id: - print("Error: Set VAIZ_API_KEY and VAIZ_SPACE_ID environment variables") - return - - client = VaizClient(api_key=api_key, space_id=space_id) - - print("=== Task List (Checklist) Examples ===\n") - - # Example 1: Simple checklist - print("1. Creating simple checklist") - simple_checklist = [ - heading(1, "Daily Tasks"), - paragraph("Here's what needs to be done today:"), - - task_list( - task_item("Review pull requests", checked=True), - task_item("Update documentation", checked=False), - task_item("Deploy to production", checked=False), - ) - ] - - # Example 2: Nested checklist (multi-level) - print("2. Creating nested checklist") - nested_checklist = [ - heading(1, "Project Milestones"), - - task_list( - # Main task with subtasks - task_item( - paragraph("Phase 1: Planning"), - task_list( - task_item("Define requirements", checked=True), - task_item("Create wireframes", checked=True), - task_item("Review with stakeholders", checked=False) - ), - checked=True - ), - - # Another main task with subtasks - task_item( - paragraph("Phase 2: Development"), - task_list( - task_item("Setup project structure", checked=True), - task_item("Implement features", checked=False), - task_item( - paragraph("Testing"), - task_list( - task_item("Unit tests", checked=False), - task_item("Integration tests", checked=False), - task_item("E2E tests", checked=False) - ), - checked=False - ) - ), - checked=False - ), - - # Third main task - task_item( - paragraph("Phase 3: Deployment"), - task_list( - task_item("Prepare production environment", checked=False), - task_item("Deploy application", checked=False), - task_item("Monitor metrics", checked=False) - ), - checked=False - ) - ) - ] - - # Example 3: Shopping list (simple strings) - print("3. Creating shopping list") - shopping_list = [ - heading(1, "Shopping List"), - paragraph("Items to buy this week:"), - - task_list( - "Milk", - "Bread", - "Eggs", - "Coffee", - "Vegetables" - ) - ] - - # Example 4: Mixed content with checklists - print("4. Creating document with mixed content") - mixed_content = [ - heading(1, "Sprint Planning"), - - paragraph("This sprint we will focus on user authentication."), - - heading(2, "Backend Tasks"), - task_list( - task_item("Design database schema", checked=True), - task_item("Implement authentication API", checked=False), - task_item("Add JWT tokens", checked=False), - task_item("Write tests", checked=False) - ), - - heading(2, "Frontend Tasks"), - task_list( - task_item("Create login page", checked=True), - task_item("Create registration page", checked=False), - task_item("Add form validation", checked=False), - task_item("Connect to API", checked=False) - ), - - paragraph("Review meeting scheduled for Friday."), - ] - - # Example 5: Complex nested structure - print("5. Creating complex nested structure") - complex_checklist = [ - heading(1, "Product Launch Checklist"), - - task_list( - task_item( - paragraph("Pre-Launch Preparation"), - task_list( - task_item( - paragraph("Technical Setup"), - task_list( - task_item("Configure servers", checked=True), - task_item("Setup monitoring", checked=True), - task_item("Configure CDN", checked=False) - ), - checked=True - ), - task_item( - paragraph("Content Preparation"), - task_list( - task_item("Write blog post", checked=True), - task_item("Create video demo", checked=False), - task_item("Update website", checked=False) - ), - checked=False - ) - ), - checked=False - ), - - task_item( - paragraph("Launch Day"), - task_list( - task_item("Deploy to production", checked=False), - task_item("Send email announcement", checked=False), - task_item("Post on social media", checked=False), - task_item("Monitor analytics", checked=False) - ), - checked=False - ), - - task_item( - paragraph("Post-Launch"), - task_list( - task_item("Collect user feedback", checked=False), - task_item("Fix critical bugs", checked=False), - task_item("Write retrospective", checked=False) - ), - checked=False - ) - ) - ] - - # You can now use any of these examples to replace document content: - # - # # For task descriptions: - # task_response = client.get_task("PRJ-123") - # client.replace_json_document(task_response.task.document, simple_checklist) - # - # # For standalone documents: - # from vaiz import GetDocumentsRequest, Kind - # docs = client.get_documents(GetDocumentsRequest(kind=Kind.Space, kind_id=space_id)) - # if docs.payload.documents: - # doc_id = docs.payload.documents[0].id - # client.replace_json_document(doc_id, nested_checklist) - - print("\n✅ All checklist examples created!") - print("\nTo use these checklists:") - print("1. Get a document ID (from task or standalone document)") - print("2. Call client.replace_json_document(document_id, content)") - print("\nExample:") - print(" task = client.get_task('PRJ-123')") - print(" client.replace_json_document(task.task.document, simple_checklist)") - - -if __name__ == "__main__": - main() - diff --git a/pyproject.toml b/pyproject.toml index 8a9ff92..57adb66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "vaiz-sdk" -version = "0.20.0" +version = "1.0.0" description = "Official SDK for interacting with the Vaiz API" authors = [{ name = "Vaiz", email = "mail@vaiz.com" }] license = "MIT" @@ -26,7 +26,7 @@ dependencies = ["requests>=2.31.0", "pydantic>=2.0", "python-dotenv>=0.9.0"] dev = ["pytest>=8.0.0", "pytest-mock>=3.12.0"] [project.urls] -Homepage = "https://github.com/vaizcom/vaiz-python-sdk" +Homepage = "https://docs-python-sdk.vaiz.com" Repository = "https://github.com/vaizcom/vaiz-python-sdk" Issues = "https://github.com/vaizcom/vaiz-python-sdk/issues" diff --git a/tests/test_append_document.py b/tests/test_append_document.py deleted file mode 100644 index 83e6f35..0000000 --- a/tests/test_append_document.py +++ /dev/null @@ -1,425 +0,0 @@ -""" -Tests for appendDocument and appendJSONDocument API endpoints. -""" - -import pytest -from tests.test_config import get_test_client -from vaiz.models import CreateTaskRequest, TaskPriority -from vaiz import heading, paragraph, text, bullet_list - - -def test_append_document(): - """Test appending plain text to existing document.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task with initial content - task_request = CreateTaskRequest( - name="Test Append Plain Text", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Initial content" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Verify initial content - initial = client.get_json_document(document_id) - initial_text = str(initial) - assert "Initial content" in initial_text - - # Append additional content - response = client.append_document( - document_id=document_id, - description="\n\nAppended content - this should be added to existing content" - ) - - assert response is not None - - # Verify both old and new content exist - updated = client.get_json_document(document_id) - updated_text = str(updated) - - assert "Initial content" in updated_text, "Original content should still exist" - assert "Appended content" in updated_text, "New content should be added" - - print(f"✅ Successfully appended plain text to document") - - -def test_append_json_document(): - """Test appending JSON content to existing document.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task with initial content - task_request = CreateTaskRequest( - name="Test Append JSON", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Initial paragraph" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Verify initial content - initial = client.get_json_document(document_id) - initial_text = str(initial) - assert "Initial paragraph" in initial_text - - # Append JSON content - appended_content = [ - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "Appended Section"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "This content was "}, - {"type": "text", "marks": [{"type": "bold"}], "text": "appended"}, - {"type": "text", "text": " to the document."} - ] - } - ] - - response = client.append_json_document( - document_id=document_id, - content=appended_content - ) - - assert response is not None - - # Verify both old and new content exist - updated = client.get_json_document(document_id) - updated_text = str(updated) - - assert "Initial paragraph" in updated_text, "Original content should still exist" - assert "Appended Section" in updated_text, "New heading should be added" - assert "appended" in updated_text, "New content should be added" - - print(f"✅ Successfully appended JSON content to document") - - -def test_append_json_document_with_helpers(): - """Test appending JSON content using helper functions.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task with initial content - task_request = CreateTaskRequest( - name="Test Append with Helpers", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Original task description" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Append using helpers - appended_content = [ - heading(2, "Updates"), - paragraph( - text("Update: ", bold=True), - "Added new requirements" - ), - bullet_list( - "Requirement 1", - "Requirement 2", - "Requirement 3" - ) - ] - - response = client.append_json_document( - document_id=document_id, - content=appended_content - ) - - assert response is not None - - # Verify both contents - updated = client.get_json_document(document_id) - updated_text = str(updated) - - assert "Original task description" in updated_text - assert "Updates" in updated_text - assert "Requirement" in updated_text - - print(f"✅ Successfully appended content using helpers") - - -def test_append_multiple_times(): - """Test appending content multiple times to same document.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task - task_request = CreateTaskRequest( - name="Test Multiple Appends", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Base content" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Append first time - client.append_json_document( - document_id, - [paragraph("First append")] - ) - - # Append second time - client.append_json_document( - document_id, - [paragraph("Second append")] - ) - - # Append third time - client.append_json_document( - document_id, - [paragraph("Third append")] - ) - - # Verify all appends - final = client.get_json_document(document_id) - final_text = str(final) - - assert "Base content" in final_text - assert "First append" in final_text - assert "Second append" in final_text - assert "Third append" in final_text - - print(f"✅ Successfully appended content 3 times") - - -def test_append_document_race_condition(): - """Test race condition handling for plain text append - verify order is preserved.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task with marked initial content - task_request = CreateTaskRequest( - name="Test Plain Text Append Order", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="PLAIN_INITIAL_MARKER - Base content" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Verify initial content - initial = client.get_json_document(document_id) - initial_text = str(initial) - assert "PLAIN_INITIAL_MARKER" in initial_text - - # Perform 3 rapid sequential plain text appends - client.append_document(document_id, "\n\nPLAIN_APPEND_1_MARKER - First plain append") - client.append_document(document_id, "\n\nPLAIN_APPEND_2_MARKER - Second plain append") - client.append_document(document_id, "\n\nPLAIN_APPEND_3_MARKER - Third plain append") - - # Retrieve and verify - final = client.get_json_document(document_id) - final_text = str(final) - - # CRITICAL: All markers must be present - assert "PLAIN_INITIAL_MARKER" in final_text, "Initial content missing!" - assert "PLAIN_APPEND_1_MARKER" in final_text, "Plain append 1 missing!" - assert "PLAIN_APPEND_2_MARKER" in final_text, "Plain append 2 missing!" - assert "PLAIN_APPEND_3_MARKER" in final_text, "Plain append 3 missing!" - - # CRITICAL: Verify ORDER - init_pos = final_text.find("PLAIN_INITIAL_MARKER") - app1_pos = final_text.find("PLAIN_APPEND_1_MARKER") - app2_pos = final_text.find("PLAIN_APPEND_2_MARKER") - app3_pos = final_text.find("PLAIN_APPEND_3_MARKER") - - assert init_pos != -1 and app1_pos != -1 and app2_pos != -1 and app3_pos != -1 - - # STRICT ORDER CHECK - assert init_pos < app1_pos, f"INITIAL ({init_pos}) should come before APPEND_1 ({app1_pos})" - assert app1_pos < app2_pos, f"APPEND_1 ({app1_pos}) should come before APPEND_2 ({app2_pos})" - assert app2_pos < app3_pos, f"APPEND_2 ({app2_pos}) should come before APPEND_3 ({app3_pos})" - - print(f"✅ PLAIN TEXT RACE CONDITION TEST PASSED: All appends in correct order") - print(f" ✓ Correct order: INITIAL → APPEND_1 → APPEND_2 → APPEND_3") - - -def test_append_json_document_race_condition(): - """Test race condition handling - verify all 3 appends are saved correctly.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task with marked initial content - task_request = CreateTaskRequest( - name="Test Append Race Condition", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="INITIAL_MARKER - Base content" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Verify initial content - initial = client.get_json_document(document_id) - initial_text = str(initial) - assert "INITIAL_MARKER" in initial_text - - # Perform 3 rapid sequential appends with unique markers - # Each append has a unique marker to verify it was saved - - # Append 1 - append1 = [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "APPEND_1_MARKER - First append content"} - ] - } - ] - client.append_json_document(document_id, append1) - - # Append 2 - append2 = [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "APPEND_2_MARKER - Second append content"} - ] - } - ] - client.append_json_document(document_id, append2) - - # Append 3 - append3 = [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "APPEND_3_MARKER - Third append content"} - ] - } - ] - client.append_json_document(document_id, append3) - - # Retrieve final content and STRICTLY verify all markers present - final = client.get_json_document(document_id) - final_text = str(final) - - # CRITICAL: All 4 markers must be present (initial + 3 appends) - assert "INITIAL_MARKER" in final_text, "Initial content missing - was overwritten!" - assert "APPEND_1_MARKER" in final_text, "Append 1 missing - race condition or lost update!" - assert "APPEND_2_MARKER" in final_text, "Append 2 missing - race condition or lost update!" - assert "APPEND_3_MARKER" in final_text, "Append 3 missing - race condition or lost update!" - - # Count actual markers in content blocks to ensure they're in document structure - final_blocks = final.get("default", {}).get("content", []) - - # Verify we have at least 4 blocks (could be more with formatting) - # Initial paragraph + 3 appended paragraphs - assert len(final_blocks) >= 4, f"Expected at least 4 blocks, got {len(final_blocks)}" - - # Detailed verification - markers_found = { - "INITIAL_MARKER": False, - "APPEND_1_MARKER": False, - "APPEND_2_MARKER": False, - "APPEND_3_MARKER": False - } - - for block in final_blocks: - block_text = str(block) - for marker in markers_found: - if marker in block_text: - markers_found[marker] = True - - # Assert all markers were found in actual blocks - for marker, found in markers_found.items(): - assert found, f"{marker} not found in document blocks!" - - # CRITICAL: Verify ORDER of markers - they must appear in correct sequence - # Find positions of each marker in the full text - initial_pos = final_text.find("INITIAL_MARKER") - append1_pos = final_text.find("APPEND_1_MARKER") - append2_pos = final_text.find("APPEND_2_MARKER") - append3_pos = final_text.find("APPEND_3_MARKER") - - # All positions must be valid - assert initial_pos != -1 - assert append1_pos != -1 - assert append2_pos != -1 - assert append3_pos != -1 - - # STRICT ORDER CHECK: INITIAL must come first, then APPEND_1, then APPEND_2, then APPEND_3 - assert initial_pos < append1_pos, f"INITIAL ({initial_pos}) should come before APPEND_1 ({append1_pos})" - assert append1_pos < append2_pos, f"APPEND_1 ({append1_pos}) should come before APPEND_2 ({append2_pos})" - assert append2_pos < append3_pos, f"APPEND_2 ({append2_pos}) should come before APPEND_3 ({append3_pos})" - - print(f"✅ RACE CONDITION TEST PASSED: All 3 appends saved correctly IN ORDER") - print(f" ✓ Initial content preserved (INITIAL_MARKER found)") - print(f" ✓ Append 1 saved (APPEND_1_MARKER found)") - print(f" ✓ Append 2 saved (APPEND_2_MARKER found)") - print(f" ✓ Append 3 saved (APPEND_3_MARKER found)") - print(f" ✓ Correct order: INITIAL → APPEND_1 → APPEND_2 → APPEND_3") - print(f" Total blocks: {len(final_blocks)}") - - -if __name__ == "__main__": - print("Running append document tests...") - test_append_document() - test_append_json_document() - test_append_json_document_with_helpers() - test_append_multiple_times() - test_append_document_race_condition() - test_append_json_document_race_condition() - print("All tests passed! ✅") - diff --git a/tests/test_comments.py b/tests/test_comments.py index 426bc03..87eb0dc 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -135,6 +135,157 @@ def test_post_simple_text_comment(client, test_document_id): print(f"Posted simple comment ID: {comment.id}") +def test_post_comment_with_markdown(client, test_document_id): + """Test posting a comment with Markdown: converted to rich content (contentVersion 2).""" + markdown = ( + "Some **bold-marker** and *italic-marker* text\n\n" + "- list-item-one\n" + "- list-item-two\n\n" + "`code-marker` and [link-marker](https://vaiz.app)" + ) + + response = client.post_comment( + document_id=test_document_id, + markdown=markdown + ) + + # Validate response + assert isinstance(response, PostCommentResponse) + assert response.type == "PostComment" + + comment = response.comment + assert comment.id is not None + assert comment.document_id == test_document_id + + # Markdown is converted to rich content on the server + assert comment.content_version == 2 + + # Content must contain real HTML tags, not escaped markdown + assert "" in edited.content + assert "[link-marker]" not in edited.content + assert "~~strike-marker~~" not in edited.content + + print(f"Edited comment with markdown ID: {comment.id}") + + +def test_post_comment_with_markdown_mention(client, test_document_id): + """Test that a markdown mention becomes a real mention node in the comment.""" + profile = client.get_profile() + member_id = profile.profile.member_id + + response = client.post_comment( + document_id=test_document_id, + markdown=f"ping @[Reviewer](user:{member_id}), please take a look" + ) + + comment = response.comment + assert comment.content_version == 2 + # The mention must be converted into a mention span with the member id + assert f'data-user-id="{member_id}"' in comment.content + assert "@[Reviewer]" not in comment.content + + # get_comments exports the mention back in markdown syntax + comments_response = client.get_comments(test_document_id) + fetched = next((c for c in comments_response.comments if c.id == comment.id), None) + assert fetched is not None + assert f"(user:{member_id})" in fetched.content + + print(f"Posted comment with mention: {comment.id}") + + +def test_get_comments_legacy_html_fallback(client, test_document_id): + """Legacy HTML comments are returned as-is by get_comments (no markdown conversion).""" + response = client.post_comment( + document_id=test_document_id, + content="

Legacy html comment

" + ) + comment = response.comment + assert comment.content_version is None + + comments_response = client.get_comments(test_document_id) + fetched = next((c for c in comments_response.comments if c.id == comment.id), None) + assert fetched is not None + assert fetched.content_version is None + assert "" in fetched.content + + print(f"Legacy comment returned as raw HTML: {comment.id}") + + +def test_comment_markdown_content_mutually_exclusive(client, test_document_id): + """Test that content and markdown are mutually exclusive in post/edit.""" + # Both provided + with pytest.raises(ValueError): + client.post_comment( + document_id=test_document_id, + content="

html

", + markdown="**md**" + ) + + # Neither provided + with pytest.raises(ValueError): + client.post_comment(document_id=test_document_id) + + # Same for edit_comment + response = client.post_comment( + document_id=test_document_id, + markdown="comment-to-edit" + ) + with pytest.raises(ValueError): + client.edit_comment( + comment_id=response.comment.id, + content="

html

", + markdown="**md**" + ) + with pytest.raises(ValueError): + client.edit_comment(comment_id=response.comment.id) + + def test_post_comment_with_empty_file_list(client, test_document_id): """Test posting a comment with explicitly empty file list.""" response = client.post_comment( diff --git a/tests/test_complex_tables.py b/tests/test_complex_tables.py deleted file mode 100644 index 943afb5..0000000 --- a/tests/test_complex_tables.py +++ /dev/null @@ -1,578 +0,0 @@ -""" -Tests for complex table structures with colspan, rowspan, and multiple columns. -""" - -import pytest -from tests.test_config import get_test_client -from vaiz.models import CreateTaskRequest, TaskPriority -from vaiz import heading, paragraph, text, table, table_row, table_cell, table_header, horizontal_rule - - -def test_create_complex_table_with_colspan(): - """Test creating table with merged cells (colspan).""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task - task_request = CreateTaskRequest( - name="Test Complex Table with Colspan", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Build complex table with colspan - content = [ - heading(1, "Project Quarterly Report"), - - paragraph("Quarterly performance metrics:"), - - table( - # Header row with merged cell using table_header - table_row( - table_header(paragraph(text("Metric", bold=True))), - table_header(paragraph(text("Q1-Q4 Performance", bold=True)), colspan=4) # Spans 4 columns - ), - # Subheader row - table_row( - table_header(paragraph(text("Category", bold=True))), - table_header("Q1"), - table_header("Q2"), - table_header("Q3"), - table_header("Q4") - ), - # Data rows - table_row("Revenue", "$100K", "$120K", "$150K", "$180K"), - table_row("Users", "1,000", "1,500", "2,200", "3,000"), - table_row("Growth %", "20%", "25%", "30%", "35%"), - # Summary row with merged cell - table_row( - table_cell(paragraph(text("Total Revenue", bold=True))), - table_cell(paragraph(text("$550K", bold=True)), colspan=4) - ) - ) - ] - - response = client.replace_json_document(document_id, content) - assert response is not None - - # Verify table saved - saved = client.get_json_document(document_id) - saved_text = str(saved) - - assert "extension-table" in saved_text - assert "Q1-Q4 Performance" in saved_text - assert "colspan" in saved_text - - print(f"✅ Complex table with colspan created successfully") - - -def test_create_complex_table_with_rowspan(): - """Test creating table with cells spanning multiple rows.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task - task_request = CreateTaskRequest( - name="Test Complex Table with Rowspan", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Build table with rowspan - content = [ - heading(1, "Team Structure"), - - table( - # Header row using table_header - table_row( - table_header("Department"), - table_header("Role"), - table_header("Name"), - table_header("Status") - ), - # Engineering department (3 rows) - table_row( - table_cell(paragraph(text("Engineering", bold=True)), rowspan=3), # Spans 3 rows - "Backend Developer", - "John Doe", - "✅ Active" - ), - table_row( - "Frontend Developer", - "Jane Smith", - "✅ Active" - ), - table_row( - "DevOps Engineer", - "Bob Wilson", - "✅ Active" - ), - # Design department (2 rows) - table_row( - table_cell(paragraph(text("Design", bold=True)), rowspan=2), # Spans 2 rows - "UI Designer", - "Alice Brown", - "✅ Active" - ), - table_row( - "UX Researcher", - "Charlie Davis", - "⏳ Onboarding" - ) - ) - ] - - response = client.replace_json_document(document_id, content) - assert response is not None - - # Verify - saved = client.get_json_document(document_id) - saved_text = str(saved) - - assert "rowspan" in saved_text - assert "Engineering" in saved_text - assert "Design" in saved_text - - print(f"✅ Complex table with rowspan created successfully") - - -def test_create_large_table_many_columns(): - """Test creating table with many columns (10+).""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task - task_request = CreateTaskRequest( - name="Test Large Table (Many Columns)", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Build wide table with 12 columns (monthly data) - content = [ - heading(1, "Annual Performance Dashboard"), - - paragraph("Monthly metrics for 2025:"), - - table( - # Header row - 12 months using table_header - table_row( - table_header(paragraph(text("Metric", bold=True))), - table_header("Jan"), table_header("Feb"), table_header("Mar"), - table_header("Apr"), table_header("May"), table_header("Jun"), - table_header("Jul"), table_header("Aug"), table_header("Sep"), - table_header("Oct"), table_header("Nov"), table_header("Dec") - ), - # Revenue row - table_row( - table_cell(paragraph(text("Revenue ($K)", bold=True))), - "45", "48", "52", "55", "60", "65", - "70", "75", "82", "88", "95", "100" - ), - # Users row - table_row( - table_cell(paragraph(text("Active Users", bold=True))), - "1.2K", "1.3K", "1.5K", "1.7K", "2.0K", "2.3K", - "2.6K", "3.0K", "3.5K", "4.0K", "4.5K", "5.0K" - ), - # Growth row - table_row( - table_cell(paragraph(text("Growth %", bold=True))), - "5", "6", "8", "6", "9", "8", - "8", "7", "17", "14", "13", "11" - ) - ), - - paragraph( - text("Total Revenue: ", bold=True), - text("$835K", italic=True) - ) - ] - - response = client.replace_json_document(document_id, content) - assert response is not None - - # Verify - saved = client.get_json_document(document_id) - saved_blocks = saved.get("default", {}).get("content", []) - - # Find table - tables = [b for b in saved_blocks if b.get("type") == "extension-table"] - assert len(tables) > 0, "Table not found" - - # Verify table has rows - table_content = tables[0].get("content", []) - assert len(table_content) == 4, f"Expected 4 rows (header + 3 data), got {len(table_content)}" - - # Verify first row has 13 cells (Metric + 12 months) - first_row = table_content[0].get("content", []) - assert len(first_row) == 13, f"Expected 13 columns, got {len(first_row)}" - - print(f"✅ Large table with 13 columns created successfully") - - -def test_create_complex_table_with_formatting(): - """Test table with mixed colspan, rowspan, and rich formatting.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task - task_request = CreateTaskRequest( - name="Test Ultra Complex Table", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Build ultra complex table - content = [ - heading(1, "Project Status Matrix"), - - table( - # Header with merged cell using table_header - table_row( - table_header(paragraph(text("Project Dashboard", bold=True)), colspan=5) - ), - # Subheader using table_header - table_row( - table_header(paragraph(text("Phase", bold=True))), - table_header(paragraph(text("Tasks", bold=True))), - table_header(paragraph(text("Status", bold=True))), - table_header(paragraph(text("Owner", bold=True))), - table_header(paragraph(text("Due", bold=True))) - ), - # Phase 1 - 3 tasks (rowspan for phase) - table_row( - table_cell(paragraph(text("Phase 1: Design", bold=True)), rowspan=3), - "UI Mockups", - table_cell(paragraph(text("Done", italic=True))), - "John", - "Oct 15" - ), - table_row( - "Wireframes", - table_cell(paragraph(text("Done", italic=True))), - "Jane", - "Oct 18" - ), - table_row( - "Style Guide", - table_cell(paragraph(text("In Progress", italic=True))), - "Mike", - "Oct 25" - ), - # Phase 2 - 2 tasks - table_row( - table_cell(paragraph(text("Phase 2: Development", bold=True)), rowspan=2), - "API Implementation", - table_cell(paragraph(text("In Progress", italic=True))), - "Sarah", - "Nov 5" - ), - table_row( - "Frontend Integration", - table_cell(paragraph(text("Todo", italic=True))), - "Tom", - "Nov 10" - ), - # Summary row with merged cells - table_row( - table_cell(paragraph(text("Total Progress", bold=True)), colspan=2), - table_cell(paragraph(text("40% Complete", bold=True, italic=True)), colspan=3) - ) - ), - - horizontal_rule(), - - paragraph( - text("Legend: ", bold=True), - text("Done", italic=True), " | ", - text("In Progress", italic=True), " | ", - text("Todo", italic=True) - ) - ] - - response = client.replace_json_document(document_id, content) - assert response is not None - - # Verify complex structure - saved = client.get_json_document(document_id) - saved_text = str(saved) - - assert "extension-table" in saved_text - assert "colspan" in saved_text - assert "rowspan" in saved_text - assert "Project Dashboard" in saved_text - assert "Phase 1: Design" in saved_text - assert "Phase 2: Development" in saved_text - - # Verify table structure - saved_blocks = saved.get("default", {}).get("content", []) - tables = [b for b in saved_blocks if b.get("type") == "extension-table"] - assert len(tables) > 0 - - table_rows = tables[0].get("content", []) - assert len(table_rows) >= 7, f"Expected at least 7 rows, got {len(table_rows)}" - - print(f"✅ Ultra complex table with colspan, rowspan, and formatting created") - print(f" Rows: {len(table_rows)}") - print(f" Features: colspan, rowspan, bold, italic") - - -def test_append_multiple_complex_tables(): - """Test appending multiple tables to same document.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task - task_request = CreateTaskRequest( - name="Test Multiple Tables via Append", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Initial description with base info" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Append first table - Sprint metrics - table1 = [ - horizontal_rule(), - heading(2, "📊 Sprint Metrics"), - table( - table_row("Sprint", "Velocity", "Completed", "Carry Over"), - table_row("Sprint 1", "25", "23", "2"), - table_row("Sprint 2", "28", "26", "2"), - table_row("Sprint 3", "30", "30", "0") - ) - ] - client.append_json_document(document_id, table1) - - # Append second table - Team allocation - table2 = [ - horizontal_rule(), - heading(2, "👥 Team Allocation"), - table( - table_row( - table_header("Team"), - table_header("Frontend"), - table_header("Backend"), - table_header("QA"), - table_header("Total") - ), - table_row("Team A", "3", "2", "1", "6"), - table_row("Team B", "2", "3", "1", "6"), - table_row( - table_cell(paragraph(text("Total", bold=True))), - table_cell("5"), - table_cell("5"), - table_cell("2"), - table_cell(paragraph(text("12", bold=True))) - ) - ) - ] - client.append_json_document(document_id, table2) - - # Append third table - Budget breakdown with merged cells - table3 = [ - horizontal_rule(), - heading(2, "💰 Budget Breakdown"), - table( - table_row( - table_header(paragraph(text("Category", bold=True))), - table_header(paragraph(text("2025 Budget", bold=True)), colspan=2) - ), - table_row( - table_header(""), - table_header("Planned"), - table_header("Actual") - ), - table_row("Development", "$200K", "$180K"), - table_row("Marketing", "$100K", "$95K"), - table_row("Operations", "$50K", "$48K"), - table_row( - table_cell(paragraph(text("Total", bold=True))), - table_cell(paragraph(text("$350K", bold=True))), - table_cell(paragraph(text("$323K", bold=True))) - ) - ) - ] - client.append_json_document(document_id, table3) - - # Verify all 3 tables present - saved = client.get_json_document(document_id) - saved_blocks = saved.get("default", {}).get("content", []) - - tables = [b for b in saved_blocks if b.get("type") == "extension-table"] - assert len(tables) == 3, f"Expected 3 tables, found {len(tables)}" - - saved_text = str(saved) - assert "Sprint Metrics" in saved_text - assert "Team Allocation" in saved_text - assert "Budget Breakdown" in saved_text - - print(f"✅ Successfully appended 3 complex tables") - print(f" Table 1: Sprint metrics (4 columns)") - print(f" Table 2: Team allocation (5 columns)") - print(f" Table 3: Budget breakdown (3 columns, with colspan)") - - -def test_create_nested_table_structure(): - """Test creating document with table nested within other content.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create task - task_request = CreateTaskRequest( - name="Test Nested Table Structure", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Build complex nested structure - from vaiz import bullet_list, ordered_list - - content = [ - heading(1, "Project Documentation"), - - paragraph("Overview of project status and metrics."), - - heading(2, "Key Milestones"), - bullet_list( - "Phase 1: Planning - Complete", - "Phase 2: Development - In Progress", - "Phase 3: Testing - Upcoming" - ), - - heading(2, "Current Sprint Tasks"), - - table( - table_row( - table_header("ID"), - table_header("Task"), - table_header("Assignee"), - table_header("Priority"), - table_header("Status"), - table_header("Hours") - ), - table_row("T-001", "API Integration", "John", "High", "✅ Done", "8"), - table_row("T-002", "UI Polish", "Jane", "Medium", "⏳ Progress", "12"), - table_row("T-003", "Documentation", "Mike", "Low", "📋 Todo", "6"), - table_row( - table_cell(paragraph(text("Total", bold=True)), colspan=5), - table_cell(paragraph(text("26 hours", bold=True))) - ) - ), - - heading(2, "Next Steps"), - ordered_list( - "Review completed tasks", - "Update sprint board", - "Plan next iteration" - ), - - horizontal_rule(), - - paragraph( - text("Document created with ", italic=True), - text("replace_json_document()", code=True), - text(" and document structure helpers", italic=True) - ) - ] - - response = client.replace_json_document(document_id, content) - assert response is not None - - # Verify structure - saved = client.get_json_document(document_id) - saved_blocks = saved.get("default", {}).get("content", []) - - # Count different element types - headings = sum(1 for b in saved_blocks if b.get("type") == "heading") - tables = sum(1 for b in saved_blocks if b.get("type") == "extension-table") - bullet_lists = sum(1 for b in saved_blocks if b.get("type") == "bulletList") - ordered_lists = sum(1 for b in saved_blocks if b.get("type") == "orderedList") - - assert headings >= 3, "Should have at least 3 headings" - assert tables >= 1, "Should have at least 1 table" - assert bullet_lists >= 1, "Should have bullet list" - assert ordered_lists >= 1, "Should have ordered list" - - print(f"✅ Complex nested structure with table created") - print(f" Headings: {headings}") - print(f" Tables: {tables}") - print(f" Bullet lists: {bullet_lists}") - print(f" Ordered lists: {ordered_lists}") - print(f" Total blocks: {len(saved_blocks)}") - - -if __name__ == "__main__": - print("Running complex table tests...") - test_create_complex_table_with_colspan() - test_create_complex_table_with_rowspan() - test_create_large_table_many_columns() - test_append_multiple_complex_tables() - test_create_nested_table_structure() - print("All tests passed! ✅") - diff --git a/tests/test_document_blocks.py b/tests/test_document_blocks.py deleted file mode 100644 index ce77e6c..0000000 --- a/tests/test_document_blocks.py +++ /dev/null @@ -1,489 +0,0 @@ -""" -Tests for special document blocks: TOC, Anchors, Siblings, Code Block, and Task Lists. -""" - -import json -from vaiz import toc_block, anchors_block, siblings_block, code_block, heading, task_list, task_item, paragraph - - -def test_toc_block_structure(): - """Test that toc_block creates correct structure.""" - toc = toc_block() - - # Check basic structure - assert toc["type"] == "doc-siblings" - assert "attrs" in toc - assert "content" in toc - - # Check attributes - attrs = toc["attrs"] - assert attrs["custom"] == 1 - assert attrs["contenteditable"] == "false" - assert "uid" in attrs - assert len(attrs["uid"]) > 0 - - # Check content - assert len(toc["content"]) == 1 - content_item = toc["content"][0] - assert content_item["type"] == "text" - - # Parse the JSON text content - data = json.loads(content_item["text"]) - assert data["type"] == "toc" - - -def test_toc_block_serialization(): - """Test that toc_block can be serialized to JSON.""" - toc = toc_block() - - # Should be serializable without errors - json_str = json.dumps(toc, ensure_ascii=False) - assert isinstance(json_str, str) - assert '"type": "doc-siblings"' in json_str - # The inner JSON is escaped, so we check for the escaped version - assert '\\"type\\": \\"toc\\"' in json_str or '"type": "toc"' in json_str - - -def test_multiple_toc_blocks_have_unique_uids(): - """Test that multiple TOC blocks get unique UIDs.""" - toc1 = toc_block() - toc2 = toc_block() - - uid1 = toc1["attrs"]["uid"] - uid2 = toc2["attrs"]["uid"] - - assert uid1 != uid2 - assert len(uid1) > 0 - assert len(uid2) > 0 - - -def test_anchors_block_structure(): - """Test that anchors_block creates correct structure.""" - anchors = anchors_block() - - # Check basic structure - assert anchors["type"] == "doc-siblings" - assert "attrs" in anchors - assert "content" in anchors - - # Check attributes - attrs = anchors["attrs"] - assert attrs["custom"] == 1 - assert attrs["contenteditable"] == "false" - assert "uid" in attrs - assert len(attrs["uid"]) > 0 - - # Check content - assert len(anchors["content"]) == 1 - content_item = anchors["content"][0] - assert content_item["type"] == "text" - - # Parse the JSON text content - data = json.loads(content_item["text"]) - assert data["type"] == "anchors" - - -def test_anchors_block_serialization(): - """Test that anchors_block can be serialized to JSON.""" - anchors = anchors_block() - - # Should be serializable without errors - json_str = json.dumps(anchors, ensure_ascii=False) - assert isinstance(json_str, str) - assert '"type": "doc-siblings"' in json_str - # The inner JSON is escaped, so we check for the escaped version - assert '\\"type\\": \\"anchors\\"' in json_str or '"type": "anchors"' in json_str - - -def test_toc_and_anchors_blocks_can_coexist(): - """Test that TOC and Anchors blocks can be used together.""" - toc = toc_block() - anchors = anchors_block() - - # Both should have different UIDs - assert toc["attrs"]["uid"] != anchors["attrs"]["uid"] - - # Both should be doc-siblings type - assert toc["type"] == "doc-siblings" - assert anchors["type"] == "doc-siblings" - - # But different data types - toc_data = json.loads(toc["content"][0]["text"]) - anchors_data = json.loads(anchors["content"][0]["text"]) - - assert toc_data["type"] == "toc" - assert anchors_data["type"] == "anchors" - - -def test_siblings_block_structure(): - """Test that siblings_block creates correct structure.""" - siblings = siblings_block() - - # Check basic structure - assert siblings["type"] == "doc-siblings" - assert "attrs" in siblings - assert "content" in siblings - - # Check attributes - attrs = siblings["attrs"] - assert attrs["custom"] == 1 - assert attrs["contenteditable"] == "false" - assert "uid" in attrs - assert len(attrs["uid"]) > 0 - - # Check content - assert len(siblings["content"]) == 1 - content_item = siblings["content"][0] - assert content_item["type"] == "text" - - # Parse the JSON text content - data = json.loads(content_item["text"]) - assert data["type"] == "siblings" - - -def test_all_doc_siblings_blocks_have_unique_types(): - """Test that all doc-siblings blocks have correct and unique types.""" - toc = toc_block() - anchors = anchors_block() - siblings = siblings_block() - - # All should be doc-siblings type - assert toc["type"] == "doc-siblings" - assert anchors["type"] == "doc-siblings" - assert siblings["type"] == "doc-siblings" - - # All should have unique UIDs - uids = [toc["attrs"]["uid"], anchors["attrs"]["uid"], siblings["attrs"]["uid"]] - assert len(uids) == len(set(uids)) # All unique - - # But different data types - toc_data = json.loads(toc["content"][0]["text"]) - anchors_data = json.loads(anchors["content"][0]["text"]) - siblings_data = json.loads(siblings["content"][0]["text"]) - - assert toc_data["type"] == "toc" - assert anchors_data["type"] == "anchors" - assert siblings_data["type"] == "siblings" - - -def test_code_block_structure(): - """Test that code_block creates correct structure.""" - code = code_block(code='print("Hello")', language="python") - - # Check basic structure - assert code["type"] == "codeBlock" - assert "attrs" in code - assert "content" in code - - # Check attributes - attrs = code["attrs"] - assert "uid" in attrs - assert attrs["language"] == "python" - - # Check content - assert len(code["content"]) == 1 - assert code["content"][0]["type"] == "text" - assert code["content"][0]["text"] == 'print("Hello")' - - -def test_code_block_empty(): - """Test creating an empty code block.""" - code = code_block() - - # Should have type and attrs - assert code["type"] == "codeBlock" - assert "attrs" in code - assert "uid" in code["attrs"] - - # Content may or may not be present when empty - if "content" in code: - assert code["content"] == [] - - -def test_code_block_without_language(): - """Test creating a code block without specifying language.""" - code = code_block(code="const x = 1;") - - assert code["type"] == "codeBlock" - assert "content" in code - assert code["content"][0]["text"] == "const x = 1;" - # Language may not be set - if "language" in code["attrs"]: - assert isinstance(code["attrs"]["language"], str) - - -def test_code_block_multiline(): - """Test code block with multiline code.""" - multiline_code = """def hello(): - print("Hello, World!") - return True""" - - code = code_block(code=multiline_code, language="python") - - assert code["type"] == "codeBlock" - assert code["attrs"]["language"] == "python" - assert code["content"][0]["text"] == multiline_code - - -def test_code_block_serialization(): - """Test that code_block can be serialized to JSON.""" - code = code_block(code='console.log("test")', language="javascript") - - # Should be serializable without errors - json_str = json.dumps(code, ensure_ascii=False) - assert isinstance(json_str, str) - assert '"type": "codeBlock"' in json_str - assert '"language": "javascript"' in json_str - - -def test_multiple_code_blocks_have_unique_uids(): - """Test that multiple code blocks get unique UIDs.""" - code1 = code_block(code="x = 1") - code2 = code_block(code="y = 2") - - uid1 = code1["attrs"]["uid"] - uid2 = code2["attrs"]["uid"] - - assert uid1 != uid2 - assert len(uid1) > 0 - assert len(uid2) > 0 - - -def test_heading_has_uid(): - """Test that heading automatically gets UID for TOC support.""" - h1 = heading(1, "Test Heading") - - # Check that heading has UID - assert "attrs" in h1 - assert "uid" in h1["attrs"] - assert len(h1["attrs"]["uid"]) == 12 - - # UID should contain letters and digits - uid = h1["attrs"]["uid"] - assert uid.isalnum() - - -def test_multiple_headings_have_unique_uids(): - """Test that multiple headings get unique UIDs.""" - h1 = heading(1, "First") - h2 = heading(2, "Second") - h3 = heading(1, "Third") - - uid1 = h1["attrs"]["uid"] - uid2 = h2["attrs"]["uid"] - uid3 = h3["attrs"]["uid"] - - # All UIDs should be different - assert uid1 != uid2 - assert uid1 != uid3 - assert uid2 != uid3 - - # All should be 12 characters - assert len(uid1) == 12 - assert len(uid2) == 12 - assert len(uid3) == 12 - - -def test_heading_uid_format(): - """Test that heading UID has correct format (letters and digits only).""" - h = heading(1, "Test") - uid = h["attrs"]["uid"] - - # Should be exactly 12 characters - assert len(uid) == 12 - - # Should contain only alphanumeric characters - assert uid.isalnum() - - # Should have mix of cases (statistically likely with 12 chars) - # At least one test to ensure we're using both upper and lower - has_upper = any(c.isupper() for c in uid) - has_lower = any(c.islower() for c in uid) - - # With 12 random chars from [a-zA-Z0-9], very likely to have both - # But we don't strictly require it in single test - assert has_upper or has_lower # At least one letter present - - -# Task List (Checklist) Tests - -def test_task_item_structure(): - """Test that task_item creates correct structure.""" - item = task_item("Buy milk", checked=False) - - # Check basic structure - assert item["type"] == "taskItem" - assert "attrs" in item - assert "content" in item - - # Check attributes - assert item["attrs"]["checked"] == False - - # Check content - assert len(item["content"]) == 1 - assert item["content"][0]["type"] == "paragraph" - assert item["content"][0]["content"][0]["text"] == "Buy milk" - - -def test_task_item_checked(): - """Test that task_item can be marked as checked.""" - item = task_item("Completed task", checked=True) - - assert item["type"] == "taskItem" - assert item["attrs"]["checked"] == True - - -def test_task_list_structure(): - """Test that task_list creates correct structure.""" - checklist = task_list( - task_item("Task 1", checked=False), - task_item("Task 2", checked=True) - ) - - # Check basic structure - assert checklist["type"] == "taskList" - assert "attrs" in checklist - assert "content" in checklist - - # Check attributes (UID should be auto-generated) - assert "uid" in checklist["attrs"] - assert len(checklist["attrs"]["uid"]) > 0 - - # Check content - assert len(checklist["content"]) == 2 - assert checklist["content"][0]["type"] == "taskItem" - assert checklist["content"][1]["type"] == "taskItem" - assert checklist["content"][0]["attrs"]["checked"] == False - assert checklist["content"][1]["attrs"]["checked"] == True - - -def test_task_list_from_strings(): - """Test creating task list from simple strings.""" - checklist = task_list("Todo 1", "Todo 2", "Todo 3") - - assert checklist["type"] == "taskList" - assert len(checklist["content"]) == 3 - - # All should be task items with checked=False by default - for item in checklist["content"]: - assert item["type"] == "taskItem" - assert item["attrs"]["checked"] == False - - -def test_nested_task_list(): - """Test creating nested task lists (multi-level checklist).""" - nested = task_list( - task_item( - paragraph("Phase 1: Planning"), - task_list( - task_item("Define requirements", checked=True), - task_item("Create wireframes", checked=False) - ), - checked=False - ), - task_item( - paragraph("Phase 2: Development"), - task_list( - task_item("Setup project", checked=False), - task_item("Implement features", checked=False) - ), - checked=False - ) - ) - - # Check main task list structure - assert nested["type"] == "taskList" - assert len(nested["content"]) == 2 - - # Check first item has nested task list - first_item = nested["content"][0] - assert first_item["type"] == "taskItem" - assert first_item["attrs"]["checked"] == False - assert len(first_item["content"]) == 2 # paragraph + nested task list - - # Check nested task list - nested_list = first_item["content"][1] - assert nested_list["type"] == "taskList" - assert len(nested_list["content"]) == 2 - assert nested_list["content"][0]["attrs"]["checked"] == True - assert nested_list["content"][1]["attrs"]["checked"] == False - - -def test_multiple_task_lists_have_unique_uids(): - """Test that multiple task lists get unique UIDs.""" - list1 = task_list("Item 1") - list2 = task_list("Item 2") - list3 = task_list("Item 3") - - uid1 = list1["attrs"]["uid"] - uid2 = list2["attrs"]["uid"] - uid3 = list3["attrs"]["uid"] - - # All UIDs should be different - assert uid1 != uid2 - assert uid1 != uid3 - assert uid2 != uid3 - - # All should be non-empty - assert len(uid1) > 0 - assert len(uid2) > 0 - assert len(uid3) > 0 - - -def test_task_list_serialization(): - """Test that task_list can be serialized to JSON.""" - checklist = task_list( - task_item("Review PR", checked=True), - task_item("Deploy", checked=False) - ) - - # Should be serializable without errors - json_str = json.dumps(checklist, ensure_ascii=False) - assert isinstance(json_str, str) - assert '"type": "taskList"' in json_str - assert '"type": "taskItem"' in json_str - assert '"checked": true' in json_str - assert '"checked": false' in json_str - - -def test_complex_nested_task_list(): - """Test creating a complex multi-level nested task list.""" - complex_list = task_list( - task_item( - paragraph("Main Task"), - task_list( - task_item( - paragraph("Subtask 1"), - task_list( - task_item("Sub-subtask 1.1", checked=True), - task_item("Sub-subtask 1.2", checked=False) - ), - checked=False - ), - task_item("Subtask 2", checked=True) - ), - checked=False - ) - ) - - # Check structure is valid - assert complex_list["type"] == "taskList" - - # Navigate through nested structure - main_item = complex_list["content"][0] - assert main_item["type"] == "taskItem" - - # Get first level nested list - level1_list = main_item["content"][1] - assert level1_list["type"] == "taskList" - assert len(level1_list["content"]) == 2 - - # Get second level nested list - level1_item = level1_list["content"][0] - level2_list = level1_item["content"][1] - assert level2_list["type"] == "taskList" - assert len(level2_list["content"]) == 2 - - # Verify deepest level items - assert level2_list["content"][0]["attrs"]["checked"] == True - assert level2_list["content"][1]["attrs"]["checked"] == False diff --git a/tests/test_document_structure_helpers.py b/tests/test_document_structure_helpers.py deleted file mode 100644 index b92509c..0000000 --- a/tests/test_document_structure_helpers.py +++ /dev/null @@ -1,928 +0,0 @@ -""" -Tests for Document Structure helper functions. -""" - -import pytest -from tests.test_config import get_test_client -from vaiz.models import CreateTaskRequest, TaskPriority -from vaiz.helpers.document_structure import ( - text, paragraph, heading, bullet_list, ordered_list, - list_item, link_text, horizontal_rule, blockquote, details, - details_summary, details_content, table, table_row, table_cell -) - - -def test_document_structure_text_builder(): - """Test text node builder.""" - # Plain text - node = text("Hello") - assert node == {"type": "text", "text": "Hello"} - - # Bold text - node = text("Bold", bold=True) - assert node["marks"] == [{"type": "bold"}] - - # Italic text - node = text("Italic", italic=True) - assert node["marks"] == [{"type": "italic"}] - - # Code text - node = text("code", code=True) - assert node["marks"] == [{"type": "code"}] - - # Combined formatting - node = text("Bold Italic", bold=True, italic=True) - assert len(node["marks"]) == 2 - - # Link - node = text("Click here", link="https://vaiz.app") - assert node["marks"][0]["type"] == "link" - assert node["marks"][0]["attrs"]["href"] == "https://vaiz.app" - - -def test_document_structure_paragraph_builder(): - """Test paragraph node builder.""" - # Simple paragraph - node = paragraph("Hello") - assert node["type"] == "paragraph" - assert node["content"][0]["text"] == "Hello" - - # Paragraph with mixed content - node = paragraph( - "Hello ", - text("World", bold=True) - ) - assert len(node["content"]) == 2 - assert node["content"][1]["marks"][0]["type"] == "bold" - - -def test_document_structure_heading_builder(): - """Test heading node builder.""" - # H1 - node = heading(1, "Title") - assert node["type"] == "heading" - assert node["attrs"]["level"] == 1 - assert node["content"][0]["text"] == "Title" - - # H2 with formatting - node = heading(2, text("Subtitle", bold=True)) - assert node["attrs"]["level"] == 2 - - -def test_document_structure_bullet_list_builder(): - """Test bullet list builder.""" - # Simple list - node = bullet_list("First", "Second", "Third") - assert node["type"] == "bulletList" - assert len(node["content"]) == 3 - assert node["content"][0]["type"] == "listItem" - - # List with complex items - node = bullet_list( - list_item(paragraph(text("Bold item", bold=True))), - "Simple item" - ) - assert len(node["content"]) == 2 - - -def test_document_structure_ordered_list_builder(): - """Test ordered list builder.""" - # Simple numbered list - node = ordered_list("First", "Second", "Third") - assert node["type"] == "orderedList" - assert len(node["content"]) == 3 - - # List starting from 5 - node = ordered_list("Item A", "Item B", start=5) - assert node["attrs"]["start"] == 5 - - -def test_document_structure_nested_lists(): - """Test nested list structures.""" - node = bullet_list( - "Top level item", - list_item( - paragraph("Parent item"), - bullet_list( - "Nested item 1", - "Nested item 2" - ) - ) - ) - - assert node["type"] == "bulletList" - # Check nested structure exists - assert node["content"][1]["content"][1]["type"] == "bulletList" - - -def test_document_structure_link_text_builder(): - """Test link text builder.""" - node = link_text("Visit Vaiz", "https://vaiz.app") - assert node["type"] == "text" - assert node["text"] == "Visit Vaiz" - assert node["marks"][0]["type"] == "link" - assert node["marks"][0]["attrs"]["href"] == "https://vaiz.app" - - # Bold link - node = link_text("Bold Link", "https://example.com", bold=True) - assert len(node["marks"]) == 2 # link + bold - - -def test_document_structure_horizontal_rule_builder(): - """Test horizontal rule builder.""" - node = horizontal_rule() - assert node["type"] == "horizontalRule" - assert node == {"type": "horizontalRule"} - - -def test_document_structure_blockquote_builder(): - """Test blockquote builder.""" - # Simple blockquote - node = blockquote("This is a quote") - assert node["type"] == "blockquote" - assert node["content"][0]["type"] == "paragraph" - assert node["content"][0]["content"][0]["text"] == "This is a quote" - - # Blockquote with multiple paragraphs - node = blockquote( - paragraph("First line"), - paragraph("Second line") - ) - assert node["type"] == "blockquote" - assert len(node["content"]) == 2 - - # Blockquote with formatted text - node = blockquote( - paragraph( - text("Important: ", bold=True), - "This is a critical note" - ) - ) - assert node["type"] == "blockquote" - assert node["content"][0]["content"][0]["marks"][0]["type"] == "bold" - - -def test_document_structure_details_builder(): - """Test details (collapsible section) builder.""" - # Simple details with string summary - node = details("Click to expand", paragraph("Hidden content")) - assert node["type"] == "details" - assert len(node["content"]) == 2 - assert node["content"][0]["type"] == "detailsSummary" - assert node["content"][0]["content"][0]["text"] == "Click to expand" - assert node["content"][1]["type"] == "detailsContent" - - # Details with formatted summary - node = details( - details_summary(text("Additional Info", bold=True)), - details_content(paragraph("More information here")) - ) - assert node["type"] == "details" - assert node["content"][0]["type"] == "detailsSummary" - assert node["content"][0]["content"][0]["marks"][0]["type"] == "bold" - assert node["content"][1]["type"] == "detailsContent" - - # Details with multiple paragraphs in content - node = details( - "Summary", - details_content( - paragraph("First paragraph"), - paragraph("Second paragraph") - ) - ) - assert node["type"] == "details" - assert node["content"][1]["type"] == "detailsContent" - assert len(node["content"][1]["content"]) == 2 - - # Details with simple string content (auto-wrapped) - node = details("Summary", paragraph("Content")) - assert node["type"] == "details" - assert node["content"][1]["type"] == "detailsContent" - - -def test_document_structure_details_summary_builder(): - """Test details summary builder.""" - node = details_summary("Click to expand") - assert node["type"] == "detailsSummary" - assert node["content"][0]["text"] == "Click to expand" - - # With formatted text - node = details_summary(text("Important", bold=True), " info") - assert node["type"] == "detailsSummary" - assert len(node["content"]) == 2 - assert node["content"][0]["marks"][0]["type"] == "bold" - - -def test_document_structure_details_content_builder(): - """Test details content builder.""" - node = details_content(paragraph("Hidden content")) - assert node["type"] == "detailsContent" - assert node["content"][0]["type"] == "paragraph" - - # With multiple paragraphs - node = details_content( - paragraph("First"), - paragraph("Second") - ) - assert node["type"] == "detailsContent" - assert len(node["content"]) == 2 - - -def test_document_structure_table_builders(): - """Test table-related builders.""" - # Table cell - cell = table_cell("Content") - assert cell["type"] == "tableCell" - assert cell["attrs"]["colspan"] == 1 - assert cell["attrs"]["rowspan"] == 1 - assert cell["content"][0]["type"] == "paragraph" - - # Table cell with colspan/rowspan - cell_span = table_cell("Merged", colspan=2, rowspan=2) - assert cell_span["attrs"]["colspan"] == 2 - assert cell_span["attrs"]["rowspan"] == 2 - - # Table row with strings - row = table_row("Cell 1", "Cell 2", "Cell 3") - assert row["type"] == "tableRow" - assert row["attrs"]["showRowNumbers"] == False - assert len(row["content"]) == 3 - assert row["content"][0]["type"] == "tableCell" - - # Complete table - tbl = table( - table_row("Name", "Status"), # Header row (first row) - table_row("Task 1", "Done"), - table_row("Task 2", "In Progress") - ) - assert tbl["type"] == "extension-table" - assert "uid" in tbl["attrs"] - assert tbl["attrs"]["showRowNumbers"] == False - assert len(tbl["content"]) == 3 - - -def test_document_structure_table_with_formatting(): - """Test table with formatted content.""" - tbl = table( - table_row( - table_cell(paragraph(text("Name", bold=True))), - table_cell(paragraph(text("Status", bold=True))) - ), - table_row( - table_cell(paragraph(text("Task 1", bold=True))), - table_cell("✅ Done") - ), - table_row( - "Task 2", - table_cell(paragraph(text("In Progress", italic=True))) - ) - ) - - assert tbl["type"] == "extension-table" - assert len(tbl["content"]) == 3 - - -def test_document_structure_table_header(): - """Test table_header builder.""" - from vaiz import table_header - - # Simple table header - header = table_header("Column Name") - assert header["type"] == "tableHeader" - assert header["attrs"]["colspan"] == 1 - assert header["attrs"]["rowspan"] == 1 - assert header["content"][0]["type"] == "paragraph" - assert header["content"][0]["content"][0]["text"] == "Column Name" - - # Table header with colspan/rowspan - header_span = table_header("Merged Header", colspan=3, rowspan=2) - assert header_span["attrs"]["colspan"] == 3 - assert header_span["attrs"]["rowspan"] == 2 - - # Table header with formatted text - header_bold = table_header(paragraph(text("Name", bold=True))) - assert header_bold["type"] == "tableHeader" - assert header_bold["content"][0]["content"][0]["marks"][0]["type"] == "bold" - - # Complete table with table_header - tbl = table( - table_row( - table_header("Name"), - table_header("Status"), - table_header("Priority") - ), - table_row("Task 1", "Done", "High"), - table_row("Task 2", "In Progress", "Medium") - ) - - assert tbl["type"] == "extension-table" - assert len(tbl["content"]) == 3 - - # First row should have tableHeader cells - first_row = tbl["content"][0] - assert first_row["content"][0]["type"] == "tableHeader" - assert first_row["content"][1]["type"] == "tableHeader" - assert first_row["content"][2]["type"] == "tableHeader" - - # Second row should have tableCell cells - second_row = tbl["content"][1] - assert second_row["content"][0]["type"] == "tableCell" - assert second_row["content"][1]["type"] == "tableCell" - assert second_row["content"][2]["type"] == "tableCell" - - -def test_replace_json_document_with_helpers(): - """Test replace_json_document using helper functions.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create a test task - task_request = CreateTaskRequest( - name="Test Task with Helper Functions", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Will be replaced" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Build content using helper functions - content = [ - heading(1, "🎉 Created with Helpers"), - - paragraph( - "This document was created using ", - text("type-safe helper functions", bold=True), - " from the Vaiz Python SDK!" - ), - - heading(2, "✨ Features"), - bullet_list( - "Type-safe builders", - "Readable code", - list_item( - paragraph("Nested lists with ", text("formatting", italic=True)), - bullet_list( - "Subitem 1", - "Subitem 2" - ) - ) - ), - - heading(2, "🔗 Links"), - paragraph( - "Visit ", - link_text("Vaiz", "https://vaiz.app", bold=True) - ), - - horizontal_rule(), - - paragraph( - text("Built with ", italic=True), - text("document structure helpers", code=True) - ) - ] - - # Replace document - response = client.replace_json_document( - document_id=document_id, - content=content - ) - - assert response is not None - - # Verify - updated_content = client.get_json_document(document_id) - assert updated_content is not None - - print(f"✅ Successfully created document with helper functions") - print(f" Content blocks: {len(content)}") - - -def test_replace_json_document_with_table(): - """Test replace_json_document with table content.""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create a test task - task_request = CreateTaskRequest( - name="Test Task with Table", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Will be replaced with table" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Build content with table - content = [ - heading(1, "Project Status"), - paragraph("Current task status:"), - table( - table_row("Task", "Assignee", "Status"), # Header row - table_row("Design mockups", "John", "✅ Done"), - table_row("API development", "Jane", "⏳ In Progress"), - table_row("Documentation", "Mike", "📋 Todo") - ) - ] - - # Replace document - response = client.replace_json_document( - document_id=document_id, - content=content - ) - - assert response is not None - - # Verify - updated_content = client.get_json_document(document_id) - assert updated_content is not None - - print(f"✅ Successfully created document with table") - - -def test_create_comprehensive_document_with_all_features(): - """Test creating a large comprehensive document with all available features.""" - from vaiz import ( - heading, paragraph, text, - bullet_list, ordered_list, list_item, - table, table_row, table_cell, table_header, - horizontal_rule, blockquote, details, link_text - ) - - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create a test task - task_request = CreateTaskRequest( - name="📚 Comprehensive Document - All Features Test", - group=client.space_id, - board=board.id, - priority=TaskPriority.High, - description="Testing all document structure features" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Build a comprehensive document with all features - content = [ - # Header section - heading(1, "📚 Comprehensive Documentation Example"), - - paragraph( - "This document demonstrates ", - text("all available features", bold=True), - " of the Vaiz Python SDK document structure helpers. ", - "Created on: ", - text("2025-10-23", italic=True) - ), - - horizontal_rule(), - - # Text formatting section - heading(2, "✨ Text Formatting"), - - paragraph( - "The SDK supports various text formatting options: ", - text("bold text", bold=True), - ", ", - text("italic text", italic=True), - ", ", - text("inline code", code=True), - ", and ", - text("bold + italic", bold=True, italic=True), - "." - ), - - paragraph( - "You can also create ", - link_text("hyperlinks", "https://vaiz.app"), - " with custom targets." - ), - - horizontal_rule(), - - # Lists section - heading(2, "📝 Lists"), - - heading(3, "Bullet Lists"), - - paragraph("Simple unordered list:"), - - bullet_list( - "First item", - "Second item with some detail", - "Third item" - ), - - paragraph("Nested bullet list with formatting:"), - - bullet_list( - list_item( - paragraph(text("Parent item 1", bold=True)), - bullet_list( - "Nested item 1.1", - "Nested item 1.2" - ) - ), - list_item( - paragraph(text("Parent item 2", bold=True)), - bullet_list( - "Nested item 2.1", - list_item( - paragraph("Nested item 2.2 with sub-items:"), - bullet_list( - "Deep nested 2.2.1", - "Deep nested 2.2.2" - ) - ) - ) - ), - "Parent item 3 (no nesting)" - ), - - heading(3, "Ordered Lists"), - - paragraph("Numbered steps:"), - - ordered_list( - "Initialize the client", - "Create a document request", - "Build content using helpers", - "Send the request to API", - "Verify the response" - ), - - paragraph("Custom start number:"), - - ordered_list( - "Step 10", - "Step 11", - "Step 12", - start=10 - ), - - horizontal_rule(), - - # Tables section - heading(2, "📊 Tables"), - - heading(3, "Simple Table with Headers"), - - paragraph("Basic table using ", text("table_header()", code=True), ":"), - - table( - table_row( - table_header("Feature"), - table_header("Status"), - table_header("Version"), - table_header("Priority") - ), - table_row( - "Document helpers", - text("✅ Complete", bold=True), - "0.8.0", - "High" - ), - table_row( - "Table headers", - text("✅ Complete", bold=True), - "0.9.0", - "High" - ), - table_row( - "Custom fields", - text("✅ Complete", bold=True), - "0.7.0", - "Medium" - ), - table_row( - "File uploads", - text("✅ Complete", bold=True), - "0.6.0", - "Medium" - ) - ), - - heading(3, "Complex Table with Merged Cells"), - - paragraph("Table with colspan and rowspan:"), - - table( - # Main header - table_row( - table_header(paragraph(text("Q1-Q4 2025 Roadmap", bold=True)), colspan=6) - ), - # Subheaders - table_row( - table_header("Quarter"), - table_header("Month"), - table_header("Feature"), - table_header("Team"), - table_header("Status"), - table_header("Progress") - ), - # Q1 data - table_row( - table_cell(paragraph(text("Q1", bold=True)), rowspan=3), - "January", - "API v2 Migration", - "Backend", - "✅ Done", - "100%" - ), - table_row( - "February", - "UI Redesign", - "Frontend", - "✅ Done", - "100%" - ), - table_row( - "March", - "Mobile App", - "Mobile", - "✅ Done", - "100%" - ), - # Q2 data - table_row( - table_cell(paragraph(text("Q2", bold=True)), rowspan=2), - "April", - "Performance Optimization", - "DevOps", - "⏳ In Progress", - "75%" - ), - table_row( - "May", - "Analytics Dashboard", - "Data", - "📋 Planned", - "0%" - ), - # Summary - table_row( - table_cell(paragraph(text("Summary", bold=True)), colspan=4), - table_cell(paragraph(text("5 features", bold=True))), - table_cell(paragraph(text("75% complete", bold=True))) - ) - ), - - heading(3, "Wide Table (Many Columns)"), - - paragraph("Table with 8 columns:"), - - table( - table_row( - table_header("#"), - table_header("Name"), - table_header("Role"), - table_header("Team"), - table_header("Location"), - table_header("Status"), - table_header("Start Date"), - table_header("Projects") - ), - table_row("1", "John Doe", "Backend Dev", "API", "🇺🇸 USA", "Active", "2023-01", "5"), - table_row("2", "Jane Smith", "Frontend Dev", "UI", "🇬🇧 UK", "Active", "2023-03", "8"), - table_row("3", "Mike Johnson", "DevOps", "Infra", "🇩🇪 DE", "Active", "2023-06", "3"), - table_row("4", "Sarah Williams", "Designer", "UX", "🇫🇷 FR", "Active", "2024-01", "12"), - table_row("5", "Tom Brown", "QA Engineer", "Test", "🇯🇵 JP", "Active", "2024-03", "6"), - table_row( - table_cell(paragraph(text("Total", bold=True)), colspan=7), - table_cell(paragraph(text("34 projects", bold=True))) - ) - ), - - horizontal_rule(), - - # Details (collapsible) section - heading(2, "📂 Collapsible Sections (Details)"), - - paragraph("Simple collapsible section:"), - - details( - "Click to expand", - paragraph("This content is hidden by default and can be expanded.") - ), - - paragraph("Details with formatted content:"), - - details( - "Technical Details", - paragraph( - text("API Endpoint: ", bold=True), - text("/api/v1/documents", code=True) - ), - paragraph( - text("Method: ", bold=True), - "POST" - ), - paragraph( - text("Authentication: ", bold=True), - "Bearer token required" - ) - ), - - horizontal_rule(), - - # Blockquote section - heading(2, "💬 Blockquotes"), - - paragraph("Simple blockquote:"), - - blockquote( - paragraph( - text("Note: ", bold=True), - "This is an important callout or quote that stands out from regular content." - ) - ), - - paragraph("Blockquote with multiple paragraphs:"), - - blockquote( - paragraph( - text("First principle: ", bold=True), - "Always write clean, maintainable code." - ), - paragraph( - text("Second principle: ", bold=True), - "Documentation is as important as the code itself." - ), - paragraph( - "— The Clean Code Philosophy" - ) - ), - - horizontal_rule(), - - # Mixed content section - heading(2, "🎨 Mixed Content"), - - paragraph( - text("Complex paragraph", bold=True), - " with ", - text("multiple", italic=True), - " ", - text("formatting", code=True), - " options and a ", - link_text("link to documentation", "https://docs.vaiz.app", bold=True), - "." - ), - - bullet_list( - list_item( - paragraph("List item with inline ", text("bold", bold=True), " and ", text("code", code=True)), - ), - list_item( - paragraph("List item with ", link_text("a link", "https://vaiz.app")), - ), - list_item( - paragraph( - "Complex item: ", - text("bold", bold=True), - " + ", - text("italic", italic=True), - " + ", - text("code", code=True) - ) - ) - ), - - horizontal_rule(), - - # Summary section - heading(2, "📋 Summary"), - - paragraph(text("This document demonstrates:", bold=True)), - - ordered_list( - "6 heading levels (H1-H6)", - "Text formatting: bold, italic, code, links", - "Bullet lists (simple and nested)", - "Ordered lists (with custom start numbers)", - "Blockquotes (simple and multi-paragraph)", - "Details (collapsible sections)", - "Tables with header cells (table_header)", - "Complex tables with colspan and rowspan", - "Wide tables with many columns", - "Horizontal rules (dividers)", - "Mixed content with multiple formatting" - ), - - horizontal_rule(), - - # Footer - paragraph( - text("Created with: ", italic=True), - text("Vaiz Python SDK v0.9.0+", code=True) - ), - - paragraph( - text("GitHub: ", bold=True), - link_text("vaiz-python-sdk", "https://github.com/vaiz/vaiz-python-sdk") - ), - ] - - # Replace document content - response = client.replace_json_document(document_id, content) - assert response is not None - - # Verify the document was created successfully - saved = client.get_json_document(document_id) - saved_blocks = saved.get("default", {}).get("content", []) - - # Count different element types - headings = sum(1 for b in saved_blocks if b.get("type") == "heading") - paragraphs = sum(1 for b in saved_blocks if b.get("type") == "paragraph") - tables = sum(1 for b in saved_blocks if b.get("type") == "extension-table") - bullet_lists = sum(1 for b in saved_blocks if b.get("type") == "bulletList") - ordered_lists = sum(1 for b in saved_blocks if b.get("type") == "orderedList") - hrs = sum(1 for b in saved_blocks if b.get("type") == "horizontalRule") - blockquotes = sum(1 for b in saved_blocks if b.get("type") == "blockquote") - details_blocks = sum(1 for b in saved_blocks if b.get("type") == "details") - - # Verify we have a good variety of content - assert headings >= 8, f"Expected at least 8 headings, got {headings}" - assert paragraphs >= 10, f"Expected at least 10 paragraphs, got {paragraphs}" - assert tables >= 3, f"Expected at least 3 tables, got {tables}" - assert bullet_lists >= 2, f"Expected at least 2 bullet lists, got {bullet_lists}" - assert ordered_lists >= 2, f"Expected at least 2 ordered lists, got {ordered_lists}" - assert hrs >= 5, f"Expected at least 5 horizontal rules, got {hrs}" - assert blockquotes >= 2, f"Expected at least 2 blockquotes, got {blockquotes}" - assert details_blocks >= 2, f"Expected at least 2 details blocks, got {details_blocks}" - - # Verify table headers are used correctly - found_table_headers = False - for block in saved_blocks: - if block.get("type") == "extension-table": - table_content = block.get("content", []) - if table_content: - first_row = table_content[0] - first_row_cells = first_row.get("content", []) - if first_row_cells: - # Check if first cell is a header - first_cell_type = first_row_cells[0].get("type") - if first_cell_type == "tableHeader": - found_table_headers = True - break - - assert found_table_headers, "At least one table should use tableHeader cells" - - print("✅ Comprehensive document created successfully") - print(f" Total blocks: {len(saved_blocks)}") - print(f" Headings: {headings}") - print(f" Paragraphs: {paragraphs}") - print(f" Tables: {tables}") - print(f" Bullet lists: {bullet_lists}") - print(f" Ordered lists: {ordered_lists}") - print(f" Blockquotes: {blockquotes}") - print(f" Details (collapsible): {details_blocks}") - print(f" Horizontal rules: {hrs}") - print(f" Document ID: {document_id}") - - -if __name__ == "__main__": - print("Running Document Structure helpers tests...") - test_document_structure_text_builder() - test_document_structure_paragraph_builder() - test_document_structure_heading_builder() - test_document_structure_bullet_list_builder() - test_document_structure_ordered_list_builder() - test_document_structure_nested_lists() - test_document_structure_link_text_builder() - test_document_structure_horizontal_rule_builder() - test_document_structure_blockquote_builder() - test_document_structure_details_builder() - test_document_structure_details_summary_builder() - test_document_structure_details_content_builder() - test_document_structure_table_builders() - test_document_structure_table_with_formatting() - test_replace_json_document_with_helpers() - test_replace_json_document_with_table() - print("All tests passed! ✅") - diff --git a/tests/test_documents.py b/tests/test_documents.py index 530174e..bbbeb9f 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -1,7 +1,18 @@ import pytest from datetime import datetime -from vaiz.models import GetDocumentRequest, ReplaceDocumentRequest, ReplaceDocumentResponse, GetDocumentsRequest, GetDocumentsResponse, Document, Kind, CreateDocumentRequest, CreateDocumentResponse, EditDocumentRequest, EditDocumentResponse +from vaiz.models import ( + ReplaceMarkdownDocumentRequest, + ReplaceMarkdownDocumentResponse, + GetDocumentsRequest, + GetDocumentsResponse, + Document, + Kind, + CreateDocumentRequest, + CreateDocumentResponse, + EditDocumentRequest, + EditDocumentResponse, +) from tests.test_config import get_test_client @@ -87,29 +98,23 @@ def ensure_test_documents_exist(client, kind: Kind, kind_id: str, min_count: int return all_docs[:min_count] -def test_get_document_request_model_serialization(): - request = GetDocumentRequest(document_id="doc123") - data = request.model_dump() - assert data == {"documentId": "doc123"} - - -def test_replace_document_request_model_serialization(): - request = ReplaceDocumentRequest( +def test_replace_markdown_document_request_model_serialization(): + request = ReplaceMarkdownDocumentRequest( document_id="doc123", - description="

New Content

HTML description

" + markdown="# New Content\n\nMarkdown description" ) data = request.model_dump() expected = { "documentId": "doc123", - "description": "

New Content

HTML description

" + "markdown": "# New Content\n\nMarkdown description" } assert data == expected -def test_get_document_fetches_json(client): +def test_get_markdown_document_for_task(client): # Create a task to get a real document id from vaiz.models import CreateTaskRequest, TaskPriority - from tests.test_config import TEST_BOARD_ID, TEST_GROUP_ID, TEST_PROJECT_ID + from tests.test_config import TEST_BOARD_ID, TEST_GROUP_ID task = CreateTaskRequest( name="SDK Test - GetDocument", @@ -122,16 +127,16 @@ def test_get_document_fetches_json(client): task_response = client.create_task(task) document_id = task_response.task.document - doc = client.get_json_document(document_id) + markdown = client.get_markdown_document(document_id) - # API may return an empty document for newly created tasks - assert isinstance(doc, dict) + assert isinstance(markdown, str) + assert "Initial document content" in markdown -def test_replace_document_content(client): - """Test replacing document content with new JSON content.""" +def test_replace_markdown_document_content(client): + """Test replacing document content with Markdown content.""" from vaiz.models import CreateTaskRequest, TaskPriority - from tests.test_config import TEST_BOARD_ID, TEST_GROUP_ID, TEST_PROJECT_ID + from tests.test_config import TEST_BOARD_ID, TEST_GROUP_ID # Create a task with initial description task = CreateTaskRequest( @@ -141,51 +146,32 @@ def test_replace_document_content(client): priority=TaskPriority.General, description="Initial content that will be replaced" ) - - # Enable verbose mode to see request/response - client.verbose = True - + task_response = client.create_task(task) document_id = task_response.task.document - # Get initial content - initial_content = client.get_json_document(document_id) - print(f"Initial content: {initial_content}") - - # API currently supports PLAIN TEXT description only - new_description_text = ( - "🎯 Replaced Content via SDK!\n\n" - "This content was completely replaced using the replaceDocument API method.\n\n" - "- ✅ Content replacement works\n" - "- 📝 Plain text format\n" - "- 🚀 Ready to use!" + new_markdown = ( + "# Replaced Content via SDK\n\n" + "This content was completely replaced using the replaceMarkdownDocument API method.\n\n" + "- Content replacement works\n" + "- **Markdown** format\n" + "- Ready to use" ) - print(f"Sending description: {new_description_text}") - # Replace document content - replace_response = client.replace_document( + replace_response = client.replace_markdown_document( document_id=document_id, - description=new_description_text + markdown=new_markdown ) - # Verify response - assert isinstance(replace_response, ReplaceDocumentResponse) + assert isinstance(replace_response, ReplaceMarkdownDocumentResponse) - # Get updated content and verify it changed - updated_content = client.get_json_document(document_id) - print(f"Updated content: {updated_content}") + # Verify round-trip + updated_markdown = client.get_markdown_document(document_id) + assert "# Replaced Content via SDK" in updated_markdown + assert "Content replacement works" in updated_markdown + assert "Initial content that will be replaced" not in updated_markdown - # For now, just verify the API call was successful and returned proper response - # The content may not change immediately or may require different format - assert isinstance(updated_content, dict) - - # API call was successful if we got here without exception - print("✅ replace_document API call completed successfully!") - print(f"✅ Response type: {type(replace_response)}") - print(f"✅ Document ID: {document_id}") - - # Note: Content change verification may require different content format - # or there might be a delay in content update + print("✅ replace_markdown_document API call completed successfully!") def test_get_documents_request_model_serialization(): @@ -432,32 +418,29 @@ def test_space_document_content_workflow(client): print(f"Testing Space document: {test_document.title} (ID: {test_document.id})") # 3. Get document content - content = client.get_json_document(test_document.id) - assert isinstance(content, dict) - print(f"✅ Retrieved Space document content: {type(content)}") + content = client.get_markdown_document(test_document.id) + assert isinstance(content, str) + print(f"✅ Retrieved Space document content") # 4. Replace document content - new_content = f""" -# Test Content Update - Space Document + new_content = f"""# Test Content Update - Space Document Updated at: {datetime.now().isoformat()} This is a test content replacement for Space document. -Document ID: {test_document.id} -Document Title: {test_document.title} """ - replace_response = client.replace_document( + replace_response = client.replace_markdown_document( document_id=test_document.id, - description=new_content + markdown=new_content ) - assert isinstance(replace_response, ReplaceDocumentResponse) + assert isinstance(replace_response, ReplaceMarkdownDocumentResponse) print("✅ Replaced Space document content") # 5. Verify content was updated - updated_content = client.get_json_document(test_document.id) - assert isinstance(updated_content, dict) + updated_content = client.get_markdown_document(test_document.id) + assert "Test Content Update - Space Document" in updated_content print("✅ Retrieved updated Space document content") @@ -473,32 +456,29 @@ def test_member_document_content_workflow(client): print(f"Testing Member document: {test_document.title} (ID: {test_document.id})") # 3. Get document content - content = client.get_json_document(test_document.id) - assert isinstance(content, dict) - print(f"✅ Retrieved Member document content: {type(content)}") + content = client.get_markdown_document(test_document.id) + assert isinstance(content, str) + print(f"✅ Retrieved Member document content") # 4. Replace document content - new_content = f""" -# Test Content Update - Member Document + new_content = f"""# Test Content Update - Member Document Updated at: {datetime.now().isoformat()} This is a test content replacement for Member (personal) document. -Document ID: {test_document.id} -Document Title: {test_document.title} """ - replace_response = client.replace_document( + replace_response = client.replace_markdown_document( document_id=test_document.id, - description=new_content + markdown=new_content ) - assert isinstance(replace_response, ReplaceDocumentResponse) + assert isinstance(replace_response, ReplaceMarkdownDocumentResponse) print("✅ Replaced Member document content") # 5. Verify content was updated - updated_content = client.get_json_document(test_document.id) - assert isinstance(updated_content, dict) + updated_content = client.get_markdown_document(test_document.id) + assert "Test Content Update - Member Document" in updated_content print("✅ Retrieved updated Member document content") @@ -515,36 +495,33 @@ def test_project_document_content_workflow(client): print(f"Testing Project document: {test_document.title} (ID: {test_document.id})") # 3. Get document content - content = client.get_json_document(test_document.id) - assert isinstance(content, dict) - print(f"✅ Retrieved Project document content: {type(content)}") + content = client.get_markdown_document(test_document.id) + assert isinstance(content, str) + print(f"✅ Retrieved Project document content") # 4. Replace document content - new_content = f""" -# Test Content Update - Project Document + new_content = f"""# Test Content Update - Project Document Updated at: {datetime.now().isoformat()} This is a test content replacement for Project document. -Document ID: {test_document.id} -Document Title: {test_document.title} ## Test Details + - Document Size: {test_document.size} bytes -- Created At: {test_document.created_at} """ - replace_response = client.replace_document( + replace_response = client.replace_markdown_document( document_id=test_document.id, - description=new_content + markdown=new_content ) - assert isinstance(replace_response, ReplaceDocumentResponse) + assert isinstance(replace_response, ReplaceMarkdownDocumentResponse) print("✅ Replaced Project document content") # 5. Verify content was updated - updated_content = client.get_json_document(test_document.id) - assert isinstance(updated_content, dict) + updated_content = client.get_markdown_document(test_document.id) + assert "Test Content Update - Project Document" in updated_content print("✅ Retrieved updated Project document content") @@ -576,14 +553,14 @@ def test_all_scopes_document_workflow(client): print(f"Document: {doc.title} (ID: {doc.id})") # Get content - content = client.get_json_document(doc.id) - assert isinstance(content, dict) + content = client.get_markdown_document(doc.id) + assert isinstance(content, str) print(f"✅ Got {scope_name} document content") # Replace content new_content = f"Test update for {scope_name} document at {datetime.now().isoformat()}" - replace_response = client.replace_document(doc.id, new_content) - assert isinstance(replace_response, ReplaceDocumentResponse) + replace_response = client.replace_markdown_document(doc.id, new_content) + assert isinstance(replace_response, ReplaceMarkdownDocumentResponse) print(f"✅ Replaced {scope_name} document content") tested_scopes.append(scope_name) @@ -682,20 +659,19 @@ def test_create_and_update_document_workflow(client): print(f"✅ Created document: {document.id}") # 2. Add content - content = f""" -# Workflow Test Document + content = f"""# Workflow Test Document Created at: {datetime.now().isoformat()} This document was created via SDK and immediately updated with content. """ - client.replace_document(document.id, content) + client.replace_markdown_document(document.id, content) print("✅ Added content to document") # 3. Verify content - retrieved_content = client.get_json_document(document.id) - assert isinstance(retrieved_content, dict) + retrieved_content = client.get_markdown_document(document.id) + assert "Workflow Test Document" in retrieved_content print("✅ Retrieved and verified document content") # 4. Verify document appears in list diff --git a/tests/test_embed_blocks.py b/tests/test_embed_blocks.py deleted file mode 100644 index f3fcc11..0000000 --- a/tests/test_embed_blocks.py +++ /dev/null @@ -1,277 +0,0 @@ -""" -Tests for embed block helpers. -""" - -import json -import pytest -from vaiz import embed_block, EmbedType - - -class TestEmbedBlock: - """Test suite for embed_block helper function.""" - - def test_youtube_embed_basic(self): - """Test creating a basic YouTube embed.""" - result = embed_block( - url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", - embed_type=EmbedType.YOUTUBE - ) - - assert result["type"] == "embed" - assert result["attrs"]["custom"] == 1 - assert result["attrs"]["contenteditable"] == "false" - assert result["attrs"]["size"] == "medium" # default - assert result["attrs"]["isContentHidden"] is False # default - assert "uid" in result["attrs"] - - # Check content - assert len(result["content"]) == 1 - assert result["content"][0]["type"] == "text" - - # Parse embed data - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["type"] == "YouTube" - assert embed_data["url"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" - # YouTube should extract embed URL - assert embed_data["extractedUrl"] == "https://www.youtube.com/embed/dQw4w9WgXcQ" - - def test_figma_embed_with_hidden_content(self): - """Test creating a Figma embed.""" - result = embed_block( - url="https://www.figma.com/design/sNL3kbNERHQvP9cqB3tbSG/Figma-File-Template--Community-?node-id=0-1&p=f&t=uIEJLQJDmYT74g6r-0", - embed_type=EmbedType.FIGMA, - size="large", - is_content_hidden=True - ) - - assert result["type"] == "embed" - assert result["attrs"]["size"] == "large" - assert result["attrs"]["isContentHidden"] is True - - # Parse embed data - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["type"] == "Figma" - assert embed_data["url"] == "https://www.figma.com/design/sNL3kbNERHQvP9cqB3tbSG/Figma-File-Template--Community-?node-id=0-1&p=f&t=uIEJLQJDmYT74g6r-0" - # Figma always has isContentHidden=True in data - assert embed_data["isContentHidden"] is True - # Check that extractedUrl is in embed format with 'file' instead of 'design' - assert "www.figma.com/embed?embed_host=share&url=https://www.figma.com/file/" in embed_data["extractedUrl"] - assert "/design/" not in embed_data["extractedUrl"] - - def test_vimeo_embed(self): - """Test creating a Vimeo embed.""" - result = embed_block( - url="https://vimeo.com/123456789", - embed_type=EmbedType.VIMEO - ) - - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["type"] == "Vimeo" - assert embed_data["url"] == "https://vimeo.com/123456789" - - def test_codesandbox_embed(self): - """Test creating a CodeSandbox embed.""" - result = embed_block( - url="https://codesandbox.io/s/example", - embed_type=EmbedType.CODESANDBOX, - size="small" - ) - - assert result["attrs"]["size"] == "small" - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["type"] == "CodeSandbox" - - def test_github_gist_embed(self): - """Test creating a GitHub Gist embed.""" - result = embed_block( - url="https://gist.github.com/user/abc123", - embed_type=EmbedType.GITHUB_GIST - ) - - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["type"] == "GitHub Gist" - assert embed_data["url"] == "https://gist.github.com/user/abc123" - # GitHub Gist should be transformed to data URI - assert embed_data["extractedUrl"].startswith("data:text/html;charset=utf-8,") - assert ".js'>" in embed_data["extractedUrl"] - - def test_miro_embed_with_hidden_content(self): - """Test creating a Miro embed with hidden content.""" - result = embed_block( - url="https://miro.com/app/board/example", - embed_type=EmbedType.MIRO, - is_content_hidden=True - ) - - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["type"] == "Miro" - assert embed_data["isContentHidden"] is True - - def test_iframe_embed(self): - """Test creating a generic Iframe embed.""" - result = embed_block(url="https://example.com/embed") - - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["type"] == "Iframe" - assert embed_data["url"] == "https://example.com/embed" - - def test_embed_with_empty_url(self): - """Test creating an embed with empty URL (valid use case).""" - # For Iframe (default), empty URL should work - result = embed_block(url="") - - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["url"] == "" - assert embed_data["extractedUrl"] == "" - assert embed_data["type"] == "Iframe" - - def test_embed_sizes(self): - """Test all size options.""" - for size in ["small", "medium", "large"]: - result = embed_block( - url="https://youtube.com/example", - embed_type=EmbedType.YOUTUBE, - size=size - ) - assert result["attrs"]["size"] == size - - def test_unique_uids(self): - """Test that each embed gets a unique UID.""" - embed1 = embed_block(url="https://youtube.com/1", embed_type=EmbedType.YOUTUBE) - embed2 = embed_block(url="https://youtube.com/2", embed_type=EmbedType.YOUTUBE) - - assert embed1["attrs"]["uid"] != embed2["attrs"]["uid"] - - def test_hidden_content_only_for_figma_miro(self): - """Test that isContentHidden in data is only set for Figma and Miro.""" - # For Figma, it should ALWAYS be True in data - figma = embed_block( - url="https://www.figma.com/design/test/example", - embed_type=EmbedType.FIGMA, - is_content_hidden=False # Even when False in attrs - ) - figma_data = json.loads(figma["content"][0]["text"]) - assert "isContentHidden" in figma_data - assert figma_data["isContentHidden"] is True # Always True for Figma - - # For Miro, it should be in data only when specified - miro = embed_block( - url="https://miro.com/test", - embed_type=EmbedType.MIRO, - is_content_hidden=True - ) - miro_data = json.loads(miro["content"][0]["text"]) - assert "isContentHidden" in miro_data - assert miro_data["isContentHidden"] is True - - # For YouTube, it should NOT be in data (only in attrs) - youtube = embed_block( - url="https://youtube.com/watch?v=test", - embed_type=EmbedType.YOUTUBE, - is_content_hidden=True - ) - youtube_data = json.loads(youtube["content"][0]["text"]) - assert "isContentHidden" not in youtube_data - - def test_embed_data_json_serialization(self): - """Test that embed data is properly JSON serialized.""" - result = embed_block( - url="https://youtube.com/тест", # Non-ASCII characters - embed_type=EmbedType.YOUTUBE - ) - - # Should not raise an exception - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["url"] == "https://youtube.com/тест" - - def test_default_embed_type_is_iframe(self): - """Test that default embed type is Iframe when not specified.""" - result = embed_block(url="https://example.com/embed") - - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["type"] == "Iframe" - assert embed_data["url"] == "https://example.com/embed" - - def test_embed_type_enum_values(self): - """Test that EmbedType enum has correct values.""" - assert EmbedType.YOUTUBE.value == "YouTube" - assert EmbedType.FIGMA.value == "Figma" - assert EmbedType.VIMEO.value == "Vimeo" - assert EmbedType.CODESANDBOX.value == "CodeSandbox" - assert EmbedType.GITHUB_GIST.value == "GitHub Gist" - assert EmbedType.MIRO.value == "Miro" - assert EmbedType.IFRAME.value == "Iframe" - - def test_youtube_url_extraction(self): - """Test that YouTube URLs are properly extracted to embed format.""" - # Test standard watch URL - result1 = embed_block( - url="https://www.youtube.com/watch?v=aY9M6MKXX7Y", - embed_type=EmbedType.YOUTUBE - ) - embed_data1 = json.loads(result1["content"][0]["text"]) - assert embed_data1["url"] == "https://www.youtube.com/watch?v=aY9M6MKXX7Y" - assert embed_data1["extractedUrl"] == "https://www.youtube.com/embed/aY9M6MKXX7Y" - - # Test youtu.be short URL - result2 = embed_block( - url="https://youtu.be/dQw4w9WgXcQ", - embed_type=EmbedType.YOUTUBE - ) - embed_data2 = json.loads(result2["content"][0]["text"]) - assert embed_data2["url"] == "https://youtu.be/dQw4w9WgXcQ" - assert embed_data2["extractedUrl"] == "https://www.youtube.com/embed/dQw4w9WgXcQ" - - def test_figma_url_extraction(self): - """Test that Figma URLs are properly extracted to embed format.""" - result = embed_block( - url="https://www.figma.com/design/sNL3kbNERHQvP9cqB3tbSG/Figma-File-Template--Community-?node-id=0-1&p=f&t=uIEJLQJDmYT74g6r-0", - embed_type=EmbedType.FIGMA - ) - embed_data = json.loads(result["content"][0]["text"]) - - # Original URL should be preserved - assert embed_data["url"] == "https://www.figma.com/design/sNL3kbNERHQvP9cqB3tbSG/Figma-File-Template--Community-?node-id=0-1&p=f&t=uIEJLQJDmYT74g6r-0" - - # ExtractedUrl should be in embed format with 'file' instead of 'design' - assert embed_data["extractedUrl"].startswith("https://www.figma.com/embed?embed_host=share&url=") - assert "https://www.figma.com/file/" in embed_data["extractedUrl"] - assert "/design/" not in embed_data["extractedUrl"] - - # Figma always has isContentHidden=True - assert embed_data["isContentHidden"] is True - - def test_github_gist_url_extraction(self): - """Test that GitHub Gist URLs are properly extracted to data URI format.""" - result = embed_block( - url="https://gist.github.com/gdb/b6365e79be6052e7531e7ba6ea8caf23", - embed_type=EmbedType.GITHUB_GIST - ) - embed_data = json.loads(result["content"][0]["text"]) - - # Original URL should be preserved - assert embed_data["url"] == "https://gist.github.com/gdb/b6365e79be6052e7531e7ba6ea8caf23" - - # ExtractedUrl should be data URI with script tag - assert embed_data["extractedUrl"].startswith("data:text/html;charset=utf-8,") - assert "" in embed_data["extractedUrl"] - assert "" in embed_data["extractedUrl"] - - def test_non_transformed_urls(self): - """Test that other embed types keep original URL as extractedUrl.""" - # Miro should keep original URL - result = embed_block( - url="https://miro.com/app/board/test", - embed_type=EmbedType.MIRO - ) - embed_data = json.loads(result["content"][0]["text"]) - assert embed_data["extractedUrl"] == embed_data["url"] - - # CodeSandbox should keep original URL - result2 = embed_block( - url="https://codesandbox.io/s/test", - embed_type=EmbedType.CODESANDBOX - ) - embed_data2 = json.loads(result2["content"][0]["text"]) - assert embed_data2["extractedUrl"] == embed_data2["url"] - diff --git a/tests/test_embed_blocks_integration.py b/tests/test_embed_blocks_integration.py deleted file mode 100644 index b32cb7c..0000000 --- a/tests/test_embed_blocks_integration.py +++ /dev/null @@ -1,365 +0,0 @@ -""" -Integration test for embed blocks - creates a real document with various embed types. -""" - -from tests.test_config import get_test_client -from vaiz.models import CreateDocumentRequest, Kind -from vaiz import ( - heading, paragraph, text, - embed_block, EmbedType, - horizontal_rule, bullet_list -) - - -def test_create_document_with_embed_blocks(): - """Create a document with various embed block types.""" - client = get_test_client() - - # Create Space document - doc_response = client.create_document( - CreateDocumentRequest( - kind=Kind.Space, - kind_id=client.space_id, - title="🎬 SDK Test - Embed Blocks Demo", - index=0 - ) - ) - - document_id = doc_response.payload.document.id - assert document_id is not None - - # Create content with various embed types - content = [ - heading(1, "🎬 Embed Blocks Demo"), - - paragraph( - "This document demonstrates all ", - text("embed block types", bold=True), - " supported by the Vaiz Python SDK." - ), - - horizontal_rule(), - - # YouTube example - heading(2, "📺 YouTube Video"), - paragraph("Example of embedded YouTube video:"), - embed_block( - url="https://www.youtube.com/watch?v=aY9M6MKXX7Y", - embed_type=EmbedType.YOUTUBE, - size="large" - ), - - horizontal_rule(), - - # Figma example - heading(2, "🎨 Figma Design"), - paragraph("Example of embedded Figma design file:"), - embed_block( - url="https://www.figma.com/design/sNL3kbNERHQvP9cqB3tbSG/Figma-File-Template--Community-?node-id=0-1&p=f&t=uIEJLQJDmYT74g6r-0", - embed_type=EmbedType.FIGMA, - size="large" - ), - - horizontal_rule(), - - # CodeSandbox example - heading(2, "💻 CodeSandbox"), - paragraph("Example of embedded CodeSandbox:"), - embed_block( - url="https://codesandbox.io/p/sandbox/define-api-post-request-forked-rvbj1", - embed_type=EmbedType.CODESANDBOX, - size="large" - ), - - horizontal_rule(), - - # GitHub Gist example - heading(2, "📝 GitHub Gist"), - paragraph("Example of embedded GitHub Gist:"), - embed_block( - url="https://gist.github.com/gdb/b6365e79be6052e7531e7ba6ea8caf23", - embed_type=EmbedType.GITHUB_GIST - ), - - horizontal_rule(), - - # Miro example - heading(2, "📋 Miro Board"), - paragraph("Example of embedded Miro board:"), - embed_block( - url="https://miro.com/app/board/uXjVO0TkGLc=/", - embed_type=EmbedType.MIRO, - size="large" - ), - - horizontal_rule(), - - # Generic Iframe example (Google Docs) - heading(2, "🌐 Google Docs (Iframe)"), - paragraph("Example of generic iframe embed with Google Docs:"), - embed_block( - url="https://docs.google.com/document/d/1nnI3fxDeMlUsmK7CZpQXWcpdfHMgB9XcFjRq18ceiZY/edit?tab=t.0", - size="medium" - ), - - horizontal_rule(), - - # Summary - heading(2, "📋 Summary"), - - paragraph(text("Supported Embed Types:", bold=True)), - - bullet_list( - "YouTube - Video hosting platform (real example)", - "Figma - Design and prototyping tool (Figma File Template)", - "CodeSandbox - Online code editor and sandbox (API POST request example)", - "GitHub Gist - Code snippet hosting", - "Miro - Collaborative whiteboard (real board)", - "Iframe - Generic iframe embeds (Google Docs example)" - ), - - horizontal_rule(), - - paragraph( - text("Created with: ", italic=True), - text("Vaiz Python SDK - embed_block() helper", code=True) - ) - ] - - # Add content to document - response = client.replace_json_document(document_id, content) - assert response is not None - - # Verify content was saved - saved = client.get_json_document(document_id) - saved_blocks = saved.get("default", {}).get("content", []) - - assert len(saved_blocks) > 0, "Document should have content blocks" - - # Count embed blocks - embed_blocks = sum(1 for b in saved_blocks if b.get("type") == "embed") - headings = sum(1 for b in saved_blocks if b.get("type") == "heading") - - assert embed_blocks >= 6, f"Should have at least 6 embed blocks, found {embed_blocks}" - assert headings >= 7, f"Should have at least 7 headings, found {headings}" - - # Verify embed block structure - first_embed = next((b for b in saved_blocks if b.get("type") == "embed"), None) - assert first_embed is not None, "Should find at least one embed block" - assert "content" in first_embed, "Embed block should have content" - - # Verify embed data is in content - embed_content = first_embed["content"] - assert len(embed_content) > 0, "Embed should have content" - assert embed_content[0]["type"] == "text", "Embed content should be text node" - - # Parse embed data JSON - import json - embed_data = json.loads(embed_content[0]["text"]) - assert "type" in embed_data, "Embed data should have type" - assert "url" in embed_data, "Embed data should have url" - assert "extractedUrl" in embed_data, "Embed data should have extractedUrl" - - print(f"\n✅ First embed block structure:") - print(f" Type: {embed_data['type']}") - print(f" URL: {embed_data['url']}") - - print(f"\n✅ Document with embed blocks created successfully!") - print(f" Document ID: {document_id}") - print(f" Title: {doc_response.payload.document.title}") - print(f" Total blocks: {len(saved_blocks)}") - print(f" Embed blocks: {embed_blocks}") - print(f" Headings: {headings}") - print(f"\n📍 Location: Space docs section") - print(f"🔗 Check your Vaiz interface to see all embed blocks:") - print(f" - YouTube video") - print(f" - Figma design") - print(f" - CodeSandbox") - print(f" - GitHub Gist") - print(f" - Miro board") - print(f" - Google Docs (iframe)") - - -def test_create_tasks_with_embeds_and_move_between_groups(): - """Create tasks with embed blocks in descriptions and move them between board groups.""" - from vaiz.models import CreateTaskRequest, TaskPriority, MoveTaskItem, MoveTasksRequest - - client = get_test_client() - - # Get boards to find a board to work with - print("\n📋 Fetching boards...") - boards_response = client.get_boards() - assert len(boards_response.boards) > 0, "No boards found in space" - - # Use the first board - test_board = boards_response.boards[0] - board_id = test_board.id - print(f"✅ Using board: {test_board.name} (ID: {board_id})") - - # Get full board details to access groups - print("\n📊 Fetching board details with groups...") - board_response = client.get_board(board_id) - board = board_response.board - - # Check if board has groups - assert board.groups is not None, f"Board '{board.name}' has no groups" - assert len(board.groups) >= 2, f"Board '{board.name}' needs at least 2 groups (found {len(board.groups)})" - - print(f"✅ Board has {len(board.groups)} groups:") - for i, group in enumerate(board.groups, 1): - print(f" {i}. {group.name} (ID: {group.id})") - - # Get first two groups - group_1 = board.groups[0] - group_2 = board.groups[1] - - # Create tasks with embed blocks in descriptions - print(f"\n📝 Creating tasks with embed blocks in group '{group_1.name}'...") - - task_configs = [ - { - "name": "🎬 SDK Test - YouTube Embed Task", - "embed_url": "https://www.youtube.com/watch?v=aY9M6MKXX7Y", - "embed_type": EmbedType.YOUTUBE, - "description": "Task with YouTube video embed" - }, - { - "name": "🎨 SDK Test - Figma Embed Task", - "embed_url": "https://www.figma.com/design/sNL3kbNERHQvP9cqB3tbSG/Figma-File-Template", - "embed_type": EmbedType.FIGMA, - "description": "Task with Figma design embed" - }, - { - "name": "💻 SDK Test - CodeSandbox Embed Task", - "embed_url": "https://codesandbox.io/p/sandbox/define-api-post-request-forked-rvbj1", - "embed_type": EmbedType.CODESANDBOX, - "description": "Task with CodeSandbox embed" - } - ] - - task_ids = [] - - for config in task_configs: - # Create task description with embed block - description_content = [ - heading(2, config["description"]), - paragraph(f"This task demonstrates {config['embed_type'].value} embed in task description:"), - embed_block( - url=config["embed_url"], - embed_type=config["embed_type"], - size="medium" - ), - horizontal_rule(), - paragraph( - text("Created with: ", italic=True), - text("Vaiz Python SDK - embed_block() helper", code=True) - ) - ] - - # Create task - task_request = CreateTaskRequest( - name=config["name"], - board=board_id, - group=group_1.id, - priority=TaskPriority.General - ) - - response = client.create_task(task_request) - task_id = response.task.id - task_ids.append(task_id) - - # Add embed blocks to task description - document_id = response.task.document - client.replace_json_document(document_id, description_content) - - print(f" ✅ Created task: {config['name']} (ID: {task_id})") - - print(f"\n✅ Created {len(task_ids)} tasks with embed blocks in group '{group_1.name}'") - - # Verify tasks are in group 1 - print(f"\n🔍 Verifying tasks are in group '{group_1.name}'...") - for i, task_id in enumerate(task_ids, 1): - task_response = client.get_task(task_id) - task = task_response.task - assert task.group == group_1.id, f"Task {i} is not in expected group" - print(f" ✅ Task {i} confirmed in group '{group_1.name}'") - - # Move tasks to group 2 using moveTasks API - print(f"\n🔄 Moving tasks to group '{group_2.name}'...") - move_request = MoveTasksRequest( - moves=[ - MoveTaskItem(task_id=tid, to_group_id=group_2.id, to_index=i) - for i, tid in enumerate(task_ids) - ] - ) - move_response = client.move_tasks(move_request) - assert len(move_response.payload.success_ids) == len(task_ids), ( - f"Expected {len(task_ids)} successful moves, got {len(move_response.payload.success_ids)}. " - f"Failed: {move_response.payload.failed_ids}" - ) - print(f" ✅ Moved {len(move_response.payload.success_ids)} tasks to group '{group_2.name}'") - - print(f"\n✅ All tasks moved to group '{group_2.name}'") - - # Verify tasks are now in group 2 - print(f"\n🔍 Verifying tasks are now in group '{group_2.name}'...") - for i, task_id in enumerate(task_ids, 1): - task_response = client.get_task(task_id) - task = task_response.task - assert task.group == group_2.id, f"Task {i} is not in group '{group_2.name}'" - print(f" ✅ Task {i} confirmed in group '{group_2.name}'") - - # Move tasks back to group 1 - print(f"\n🔄 Moving tasks back to group '{group_1.name}'...") - move_back_request = MoveTasksRequest( - moves=[ - MoveTaskItem(task_id=tid, to_group_id=group_1.id, to_index=i) - for i, tid in enumerate(task_ids) - ] - ) - move_back_response = client.move_tasks(move_back_request) - assert len(move_back_response.payload.success_ids) == len(task_ids), ( - f"Expected {len(task_ids)} successful moves back, got {len(move_back_response.payload.success_ids)}. " - f"Failed: {move_back_response.payload.failed_ids}" - ) - print(f" ✅ Moved {len(move_back_response.payload.success_ids)} tasks back to group '{group_1.name}'") - - print(f"\n✅ All tasks moved back to group '{group_1.name}'") - - # Verify tasks are back in group 1 - print(f"\n🔍 Verifying tasks are back in group '{group_1.name}'...") - for i, task_id in enumerate(task_ids, 1): - task_response = client.get_task(task_id) - task = task_response.task - assert task.group == group_1.id, f"Task {i} is not back in group '{group_1.name}'" - print(f" ✅ Task {i} confirmed in group '{group_1.name}'") - - print("\n" + "="*60) - print("✅ Tasks with embed blocks group movement test completed!") - print("="*60) - print(f"\n📊 Summary:") - print(f" Board: {board.name}") - print(f" Groups tested: {group_1.name} ↔ {group_2.name}") - print(f" Tasks created: {len(task_ids)}") - print(f" Each task has embed blocks in description") - print(f" Movements performed: {len(task_ids) * 2}") - print(f"\n🔗 Check your Vaiz interface to see the tasks with embeds in '{group_1.name}'") - - -if __name__ == "__main__": - print("\n" + "="*60) - print("Creating document with embed blocks...") - print("="*60 + "\n") - - test_create_document_with_embed_blocks() - - print("\n" + "="*60) - print("Creating tasks with embed blocks and moving between groups...") - print("="*60 + "\n") - - test_create_tasks_with_embeds_and_move_between_groups() - - print("\n" + "="*60) - print("✅ All embed blocks integration tests completed!") - print("="*60) - diff --git a/tests/test_file_blocks.py b/tests/test_file_blocks.py deleted file mode 100644 index a6f2f4c..0000000 --- a/tests/test_file_blocks.py +++ /dev/null @@ -1,452 +0,0 @@ -""" -Tests for file and image block helper functions. -""" - -import pytest -from vaiz.helpers import ( - image_block, - files_block, - paragraph, - text, - heading, -) -from vaiz.models import CreateDocumentRequest, Kind -from vaiz.models.enums import UploadFileType -from tests.test_config import get_test_client, TEST_SPACE_ID -import json - - -# Mock file class for testing -class MockUploadedFile: - """Mock uploaded file for testing.""" - def __init__(self, id, url, name, size, ext, mime=None, dimension=None, dominant_color=None): - self.id = id - self.url = url - self.name = name - self.size = size - self.ext = ext - self.mime = mime - self.dimension = dimension - self.dominant_color = dominant_color - - -def test_image_block_structure(): - """Test creating an image block with new simple API.""" - mock_file = MockUploadedFile( - id="test_file_id", - url="http://example.com/image.png", - name="test.png", - size=12345, - ext="png", - dimension=[800, 600] - ) - - result = image_block(file=mock_file) - - assert result["type"] == "image-block" - assert result["attrs"]["custom"] == 1 - assert result["attrs"]["contenteditable"] == "false" - assert result["attrs"]["widthPercent"] == 100 - assert "uid" in result["attrs"] - - # Parse JSON content - content_text = result["content"][0]["text"] - image_data = json.loads(content_text) - - assert image_data["fileId"] == "test_file_id" - assert image_data["src"] == "http://example.com/image.png" - assert image_data["fileName"] == "test.png" - assert image_data["fileSize"] == 12345 - assert image_data["dimensions"] == [800, 600] - assert image_data["aspectRatio"] == 800 / 600 - - -def test_image_block_with_caption(): - """Test creating an image block with caption.""" - mock_file = MockUploadedFile( - id="test_id", - url="http://example.com/image.png", - name="test.png", - size=1000, - ext="png" - ) - - result = image_block(file=mock_file, caption="Test caption") - - content_text = result["content"][0]["text"] - image_data = json.loads(content_text) - - assert image_data["caption"] == "Test caption" - - -def test_image_block_custom_width(): - """Test creating an image block with custom width.""" - mock_file = MockUploadedFile( - id="test_id", - url="http://example.com/image.png", - name="test.png", - size=1000, - ext="png" - ) - - result = image_block(file=mock_file, width_percent=50) - - assert result["attrs"]["widthPercent"] == 50 - - -def test_image_block_auto_mime_detection(): - """Test that MIME type is automatically detected from extension.""" - # Test PNG - mock_png = MockUploadedFile( - id="test_id", - url="http://example.com/image.png", - name="test.png", - size=1000, - ext="png" - ) - result_png = image_block(file=mock_png) - content_png = json.loads(result_png["content"][0]["text"]) - assert content_png["fileType"] == "image/png" - - # Test JPEG - mock_jpg = MockUploadedFile( - id="test_id", - url="http://example.com/image.jpg", - name="test.jpg", - size=1000, - ext="jpg" - ) - result_jpg = image_block(file=mock_jpg) - content_jpg = json.loads(result_jpg["content"][0]["text"]) - assert content_jpg["fileType"] == "image/jpeg" - - # Test GIF - mock_gif = MockUploadedFile( - id="test_id", - url="http://example.com/image.gif", - name="test.gif", - size=1000, - ext="gif" - ) - result_gif = image_block(file=mock_gif) - content_gif = json.loads(result_gif["content"][0]["text"]) - assert content_gif["fileType"] == "image/gif" - - # Test WEBP - mock_webp = MockUploadedFile( - id="test_id", - url="http://example.com/image.webp", - name="test.webp", - size=1000, - ext="webp" - ) - result_webp = image_block(file=mock_webp) - content_webp = json.loads(result_webp["content"][0]["text"]) - assert content_webp["fileType"] == "image/webp" - - # Test with explicit MIME from file - mock_with_mime = MockUploadedFile( - id="test_id", - url="http://example.com/image.png", - name="test.png", - size=1000, - ext="png", - mime="image/custom" - ) - result_with_mime = image_block(file=mock_with_mime) - content_with_mime = json.loads(result_with_mime["content"][0]["text"]) - assert content_with_mime["fileType"] == "image/custom" - - -def test_files_block_single_file(): - """Test creating a files block with one file.""" - file_item = { - "fileId": "test_file_id", - "url": "http://example.com/file.pdf", - "name": "document.pdf", - "size": 54321, - "extension": "pdf", - "type": "Pdf" - } - - result = files_block(file_item) - - assert result["type"] == "files" - assert result["attrs"]["custom"] == 1 - assert result["attrs"]["contenteditable"] == "false" - assert "uid" in result["attrs"] - - # Parse JSON content - content_text = result["content"][0]["text"] - files_data = json.loads(content_text) - - assert "files" in files_data - assert len(files_data["files"]) == 1 - - file = files_data["files"][0] - assert file["fileId"] == "test_file_id" - assert file["url"] == "http://example.com/file.pdf" - assert file["name"] == "document.pdf" - assert file["size"] == 54321 - assert file["extension"] == "pdf" - assert file["type"] == "Pdf" - assert "id" in file - assert "createAt" in file - - -def test_files_block_multiple_files(): - """Test creating a files block with multiple files.""" - file1 = { - "fileId": "file1", - "url": "http://example.com/doc1.pdf", - "name": "doc1.pdf", - "size": 1000, - "extension": "pdf", - "type": "Pdf" - } - - file2 = { - "fileId": "file2", - "url": "http://example.com/image.png", - "name": "image.png", - "size": 2000, - "extension": "png", - "type": "Image" - } - - result = files_block(file1, file2) - - content_text = result["content"][0]["text"] - files_data = json.loads(content_text) - - assert len(files_data["files"]) == 2 - assert files_data["files"][0]["fileId"] == "file1" - assert files_data["files"][1]["fileId"] == "file2" - - -def test_files_block_unique_ids(): - """Test that each file in files block gets unique ID.""" - file1 = { - "fileId": "same_file_id", - "url": "http://example.com/file.pdf", - "name": "file.pdf", - "size": 1000, - "extension": "pdf", - "type": "Pdf" - } - - file2 = { - "fileId": "same_file_id", - "url": "http://example.com/file.pdf", - "name": "file.pdf", - "size": 1000, - "extension": "pdf", - "type": "Pdf" - } - - result = files_block(file1, file2) - - content_text = result["content"][0]["text"] - files_data = json.loads(content_text) - - file_ids = [f["id"] for f in files_data["files"]] - assert len(file_ids) == 2 - assert file_ids[0] != file_ids[1] # Each should have unique internal ID - - -def test_create_document_with_file_blocks(): - """Integration test: Create document with real uploaded files.""" - client = get_test_client() - - # Upload test files - image_path = "assets/example.png" - pdf_path = "assets/example.pdf" - - # Upload image - image_uploaded = client.upload_file(image_path, file_type=UploadFileType.Image) - assert image_uploaded.file.id is not None - - # Upload PDF - pdf_uploaded = client.upload_file(pdf_path, file_type=UploadFileType.Pdf) - assert pdf_uploaded.file.id is not None - - # Create document - create_request = CreateDocumentRequest( - kind=Kind.Space, - kind_id=TEST_SPACE_ID, - title="Test: File and Image Blocks", - index=0 - ) - - doc_response = client.create_document(create_request) - test_doc_id = doc_response.payload.document.id - - try: - # Build content with file blocks - content = [ - heading(1, "File Blocks Test"), - - paragraph(text("Image block:")), - - # New simple API - just pass the file object! - image_block(file=image_uploaded.file), - - paragraph(text("Files block:")), - - files_block({ - "fileId": pdf_uploaded.file.id, - "url": pdf_uploaded.file.url, - "name": pdf_uploaded.file.name, - "size": pdf_uploaded.file.size, - "extension": pdf_uploaded.file.ext, - "type": "Pdf" - }) - ] - - # Replace document content - client.replace_json_document(test_doc_id, content) - - # Verify blocks were created - doc_content = client.get_json_document(test_doc_id) - - assert doc_content is not None - assert "default" in doc_content - assert "content" in doc_content["default"] - - doc_nodes = doc_content["default"]["content"] - - # Find file blocks - image_blocks = [n for n in doc_nodes if n.get("type") == "image-block"] - files_blocks = [n for n in doc_nodes if n.get("type") == "files"] - - assert len(image_blocks) == 1, "Image block not found" - assert len(files_blocks) == 1, "Files block not found" - - # Verify image block structure - image_block_node = image_blocks[0] - assert "content" in image_block_node - image_content = image_block_node["content"][0]["text"] - image_data = json.loads(image_content) - assert image_data["fileId"] == image_uploaded.file.id - - # Verify files block structure - files_block_node = files_blocks[0] - assert "content" in files_block_node - files_content = files_block_node["content"][0]["text"] - files_data = json.loads(files_content) - assert "files" in files_data - assert len(files_data["files"]) == 1 - assert files_data["files"][0]["fileId"] == pdf_uploaded.file.id - - print(f"\n✅ Successfully created and verified document with file blocks") - print(f" Document ID: {test_doc_id}") - print(f" Image block: {image_uploaded.file.name}") - print(f" Files block: {pdf_uploaded.file.name}") - print(f" View at: https://vaiz.app/document/{test_doc_id}") - - finally: - pass # Could add cleanup here - - -@pytest.mark.integration -def test_add_images_with_captions_to_existing_document(): - """Integration test: Add images with captions to existing document.""" - client = get_test_client() - - # Create a new document for testing - create_request = CreateDocumentRequest( - kind=Kind.Space, - kind_id=TEST_SPACE_ID, - title="Test: Images with Captions", - index=0 - ) - - doc_response = client.create_document(create_request) - document_id = doc_response.payload.document.id - - # Upload test image - image_path = "assets/example.png" - - print(f"\n📤 Uploading image: {image_path}") - image_uploaded = client.upload_file(image_path, file_type=UploadFileType.Image) - assert image_uploaded.file.id is not None - print(f"✅ Uploaded: {image_uploaded.file.name} (ID: {image_uploaded.file.id})") - - # Build content with images that have captions - content = [ - heading(1, "Images with Captions Test"), - - paragraph(text("This document demonstrates image blocks with captions.")), - - heading(2, "Image 1: With Caption"), - - # New simple API - just pass file and optional parameters! - image_block( - file=image_uploaded.file, - caption="This is a test image with a caption" - ), - - paragraph(text("The image above has a caption.", italic=True)), - - heading(2, "Image 2: With Different Caption"), - - image_block( - file=image_uploaded.file, - caption="Another caption example with description", - width_percent=75 - ), - - paragraph(text("The image above has a different caption and 75% width.", italic=True)), - - heading(2, "Image 3: Full Width with Long Caption"), - - image_block( - file=image_uploaded.file, - caption="This is a longer caption that demonstrates how captions work with full-width images. Captions provide context and descriptions for images.", - width_percent=100 - ), - ] - - # Replace document content - print(f"\n📝 Updating document: {document_id}") - client.replace_json_document(document_id, content) - - # Verify blocks were created with captions - doc_content = client.get_json_document(document_id) - - assert doc_content is not None - assert "default" in doc_content - assert "content" in doc_content["default"] - - doc_nodes = doc_content["default"]["content"] - - # Find image blocks - image_blocks = [n for n in doc_nodes if n.get("type") == "image-block"] - - assert len(image_blocks) == 3, f"Expected 3 image blocks, found {len(image_blocks)}" - - # Verify all images have captions - captions_found = 0 - for idx, image_block_node in enumerate(image_blocks): - assert "content" in image_block_node - image_content = image_block_node["content"][0]["text"] - image_data = json.loads(image_content) - - assert image_data["fileId"] == image_uploaded.file.id - - # Check caption exists - if "caption" in image_data: - captions_found += 1 - print(f" Image {idx + 1} caption: {image_data['caption'][:50]}...") - - assert captions_found == 3, f"Expected 3 captions, found {captions_found}" - - print(f"\n✅ Successfully created 3 images with captions in document!") - print(f" Document ID: {document_id}") - print(f" View at: https://vaiz.app/document/{document_id}") - - -if __name__ == "__main__": - # test_create_document_with_file_blocks() - test_add_images_with_captions_to_existing_document() - diff --git a/tests/test_markdown_documents.py b/tests/test_markdown_documents.py index 6d37c89..959fdd5 100644 --- a/tests/test_markdown_documents.py +++ b/tests/test_markdown_documents.py @@ -245,6 +245,39 @@ def test_markdown_complex_document_roundtrip(): print(f"✅ Complex document round-trip preserved all structures for document {document_id}") +def test_markdown_mentions_roundtrip(): + """Test that mentions written as @[label](kind:id) survive a markdown round-trip.""" + client = get_test_client() + document_id = _create_test_document(client, "Test Markdown Mentions") + + # Use the current member as a real mention target + profile = client.get_profile() + member_id = profile.profile.member_id + + markdown = ( + f"Please review, @[Reviewer](user:{member_id})\n\n" + f"Related doc: @[Spec](document:{document_id})" + ) + + client.replace_markdown_document(document_id, markdown) + + result = client.get_markdown_document(document_id) + + # Mentions are exported back with a generic label but the same kind:id + assert f"(user:{member_id})" in result + assert f"(document:{document_id})" in result + # The raw syntax must not be flattened into plain text + assert "@[Reviewer]" not in result or f"(user:{member_id})" in result + + # Write the exported markdown back: mentions must survive a second pass + client.replace_markdown_document(document_id, result) + second = client.get_markdown_document(document_id) + assert f"(user:{member_id})" in second + assert f"(document:{document_id})" in second + + print(f"✅ Mentions survived markdown round-trip in document {document_id}") + + def test_markdown_append_table_to_existing_content(): """Test appending a table after existing rich content keeps both parts intact.""" client = get_test_client() diff --git a/tests/test_mention_blocks.py b/tests/test_mention_blocks.py deleted file mode 100644 index 838af9b..0000000 --- a/tests/test_mention_blocks.py +++ /dev/null @@ -1,263 +0,0 @@ -""" -Tests for mention block helper functions. -""" - -import pytest -from vaiz.helpers import ( - mention, - mention_user, - mention_document, - mention_task, - mention_milestone, - paragraph, - text, - heading, -) -from vaiz.models import CreateDocumentRequest, Kind, GetDocumentsRequest, GetTasksRequest -from tests.test_config import get_test_client, TEST_SPACE_ID - - -def test_mention_user(): - """Test creating a user mention with member_id.""" - member_id = "68fa5e14cdb30e1c96755975" - result = mention_user(member_id) - - assert result["type"] == "custom-mention" - assert result["attrs"]["custom"] == 1 - assert result["attrs"]["inline"] is True - assert result["attrs"]["data"]["item"]["id"] == member_id - assert result["attrs"]["data"]["item"]["kind"] == "User" - assert "uid" in result["attrs"] - assert len(result["content"]) == 1 - assert result["content"][0]["type"] == "text" - - -def test_mention_document(): - """Test creating a document mention.""" - document_id = "68fa6c7b62f676bcd1bcecae" - result = mention_document(document_id) - - assert result["type"] == "custom-mention" - assert result["attrs"]["data"]["item"]["id"] == document_id - assert result["attrs"]["data"]["item"]["kind"] == "Document" - - -def test_mention_task(): - """Test creating a task mention.""" - task_id = "68fa67d262f676bcd1bc162f" - result = mention_task(task_id) - - assert result["type"] == "custom-mention" - assert result["attrs"]["data"]["item"]["id"] == task_id - assert result["attrs"]["data"]["item"]["kind"] == "Task" - - -def test_mention_milestone(): - """Test creating a milestone mention.""" - milestone_id = "68fa650bcdb30e1c9677562e" - result = mention_milestone(milestone_id) - - assert result["type"] == "custom-mention" - assert result["attrs"]["data"]["item"]["id"] == milestone_id - assert result["attrs"]["data"]["item"]["kind"] == "Milestone" - - -def test_mention_generic(): - """Test creating a generic mention with explicit kind.""" - item_id = "test123" - result = mention(item_id, "User") - - assert result["type"] == "custom-mention" - assert result["attrs"]["data"]["item"]["id"] == item_id - assert result["attrs"]["data"]["item"]["kind"] == "User" - - -def test_mention_in_paragraph(): - """Test using mentions in a paragraph.""" - member_id = "68fa5e14cdb30e1c96755975" - task_id = "68fa67d262f676bcd1bc162f" - - para = paragraph( - text("User "), - mention_user(member_id), - text(" is assigned to "), - mention_task(task_id) - ) - - assert para["type"] == "paragraph" - assert len(para["content"]) == 4 - assert para["content"][1]["type"] == "custom-mention" - assert para["content"][3]["type"] == "custom-mention" - - -def test_mention_unique_uids(): - """Test that each mention gets a unique UID.""" - member_id = "68fa5e14cdb30e1c96755975" - mention1 = mention_user(member_id) - mention2 = mention_user(member_id) - - assert mention1["attrs"]["uid"] != mention2["attrs"]["uid"] - - -def test_mention_structure_complete(): - """Test that mention structure has all required fields.""" - result = mention_user("test123") - - # Check all required attributes exist - assert "type" in result - assert "attrs" in result - assert "content" in result - - attrs = result["attrs"] - assert "uid" in attrs - assert "custom" in attrs - assert "inline" in attrs - assert "data" in attrs - - data = attrs["data"] - assert "item" in data - - item = data["item"] - assert "id" in item - assert "kind" in item - - -def test_all_mention_kinds(): - """Test all supported mention kinds.""" - kinds = ["User", "Document", "Task", "Milestone"] - - for kind in kinds: - result = mention("test_id", kind) - assert result["attrs"]["data"]["item"]["kind"] == kind - - -def test_create_document_with_real_mentions(): - """Integration test: Create document with real mentions and verify they exist.""" - client = get_test_client() - - # Get real IDs from API - profile = client.get_profile() - member_id = profile.profile.member_id - - # Get real document ID - docs_response = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=TEST_SPACE_ID) - ) - document_id = None - if docs_response.payload.documents: - document_id = docs_response.payload.documents[0].id - - # Get real task ID - tasks_response = client.get_tasks(GetTasksRequest()) - task_id = None - if tasks_response.payload.tasks: - task_id = tasks_response.payload.tasks[0].id - - # Get real milestone ID - milestones_response = client.get_milestones() - milestone_id = None - if milestones_response.milestones: - milestone_id = milestones_response.milestones[0].id - - # Create document in Space with mentions - create_request = CreateDocumentRequest( - kind=Kind.Space, - kind_id=TEST_SPACE_ID, - title="Test: Mention Blocks", - index=0 - ) - - doc_response = client.create_document(create_request) - test_doc_id = doc_response.payload.document.id - - try: - # Build content with real mentions - content = [ - heading(1, "Test Mention Blocks"), - paragraph( - text("User mention: "), - mention_user(member_id) - ) - ] - - # Add document mention if available - if document_id: - content.append(paragraph( - text("Document mention: "), - mention_document(document_id) - )) - - # Add task mention if available - if task_id: - content.append(paragraph( - text("Task mention: "), - mention_task(task_id) - )) - - # Add milestone mention if available - if milestone_id: - content.append(paragraph( - text("Milestone mention: "), - mention_milestone(milestone_id) - )) - - # Replace document content with mentions - client.replace_json_document(test_doc_id, content) - - # Verify mentions were created by reading document - doc_content = client.get_json_document(test_doc_id) - - assert doc_content is not None - assert "default" in doc_content - assert "content" in doc_content["default"] - - doc_nodes = doc_content["default"]["content"] - - # Find mention nodes in content - mention_nodes = [] - for node in doc_nodes: - if node.get("type") == "paragraph" and "content" in node: - for child in node["content"]: - if child.get("type") == "custom-mention": - mention_nodes.append(child) - - # Verify at least user mention was created - assert len(mention_nodes) > 0, "No mention blocks found in document" - - # Verify all are mention nodes with full structure - for mention_node in mention_nodes: - assert mention_node["type"] == "custom-mention", f"Expected custom-mention, got {mention_node.get('type')}" - assert "attrs" in mention_node, "Mention node missing 'attrs' field" - assert "data" in mention_node["attrs"], "Mention attrs missing 'data' field" - assert "item" in mention_node["attrs"]["data"], "Mention data missing 'item' field" - assert "id" in mention_node["attrs"]["data"]["item"], "Mention item missing 'id' field" - assert "kind" in mention_node["attrs"]["data"]["item"], "Mention item missing 'kind' field" - - # Verify specific mention IDs and kinds match - mention_items = [m["attrs"]["data"]["item"] for m in mention_nodes] - mention_ids = [item["id"] for item in mention_items] - mention_kinds = [item["kind"] for item in mention_items] - - assert member_id in mention_ids, "User mention not found in document" - assert "User" in mention_kinds, "User kind not found" - - if document_id: - assert document_id in mention_ids, "Document mention not found" - assert "Document" in mention_kinds, "Document kind not found" - if task_id: - assert task_id in mention_ids, "Task mention not found" - assert "Task" in mention_kinds, "Task kind not found" - if milestone_id: - assert milestone_id in mention_ids, "Milestone mention not found" - assert "Milestone" in mention_kinds, "Milestone kind not found" - - print(f"\n✅ Successfully created and verified document with {len(mention_nodes)} mention block(s)") - print(f" Document ID: {test_doc_id}") - print(f" All mentions have full structure with attrs, data, and item fields") - print(f" View at: https://vaiz.app/document/{test_doc_id}") - - finally: - # Cleanup: delete test document - # Note: If there's a delete_document method, use it here - pass - diff --git a/tests/test_mention_integration.py b/tests/test_mention_integration.py deleted file mode 100644 index 4a46bd8..0000000 --- a/tests/test_mention_integration.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Integration tests for mention blocks with main vaiz module imports. -""" - -import pytest - - -def test_mention_imports_from_vaiz(): - """Test that mention helpers can be imported from vaiz module.""" - from vaiz import ( - mention, - mention_user, - mention_document, - mention_task, - mention_milestone, - ) - - # Test that functions are callable - assert callable(mention) - assert callable(mention_user) - assert callable(mention_document) - assert callable(mention_task) - assert callable(mention_milestone) - - -def test_mention_with_other_document_helpers(): - """Test using mentions with other document structure helpers.""" - from vaiz import ( - paragraph, - text, - heading, - mention_user, - mention_task, - bullet_list, - list_item, - ) - - member_id = "test_member_123" - task_id = "test_task_456" - - # Create document with mentions mixed with other elements - content = [ - heading(1, "Task Assignment"), - paragraph( - text("User "), - mention_user(member_id), - text(" is assigned to "), - mention_task(task_id) - ), - bullet_list( - list_item(paragraph(text("Priority: High"))), - list_item(paragraph( - text("Assignee: "), - mention_user(member_id) - )) - ) - ] - - assert len(content) == 3 - assert content[0]["type"] == "heading" - assert content[1]["type"] == "paragraph" - assert content[2]["type"] == "bulletList" - - -def test_mention_helpers_create_valid_structure(): - """Test that all mention helper functions create valid structures.""" - from vaiz import ( - mention_user, - mention_document, - mention_task, - mention_milestone, - ) - - helpers = [ - (mention_user, "User"), - (mention_document, "Document"), - (mention_task, "Task"), - (mention_milestone, "Milestone"), - ] - - for helper_func, expected_kind in helpers: - result = helper_func("test_id_123") - - assert result["type"] == "custom-mention" - assert result["attrs"]["custom"] == 1 - assert result["attrs"]["inline"] is True - assert result["attrs"]["data"]["item"]["kind"] == expected_kind - assert result["attrs"]["data"]["item"]["id"] == "test_id_123" - diff --git a/tests/test_mention_verification.py b/tests/test_mention_verification.py deleted file mode 100644 index cc4a082..0000000 --- a/tests/test_mention_verification.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -Test to verify mention blocks structure after creation. -""" - -import pytest -from vaiz.helpers import mention_user, mention_document, mention_task, mention_milestone, paragraph, text, heading -from vaiz.models import CreateDocumentRequest, Kind, GetDocumentsRequest, GetTasksRequest -from tests.test_config import get_test_client, TEST_SPACE_ID -import json - - -def test_verify_mention_structure_after_creation(): - """Verify that mentions are correctly saved and retrieved from API.""" - client = get_test_client() - - # Get real IDs (member_id for user mentions) - profile = client.get_profile() - member_id = profile.profile.member_id - - docs_response = client.get_documents( - GetDocumentsRequest(kind=Kind.Space, kind_id=TEST_SPACE_ID) - ) - doc_id = docs_response.payload.documents[0].id if docs_response.payload.documents else None - - tasks_response = client.get_tasks(GetTasksRequest()) - task_id = tasks_response.payload.tasks[0].id if tasks_response.payload.tasks else None - - milestones_response = client.get_milestones() - milestone_id = milestones_response.milestones[0].id if milestones_response.milestones else None - - # Create document - create_request = CreateDocumentRequest( - kind=Kind.Space, - kind_id=TEST_SPACE_ID, - title="Test: Mention Structure Verification", - index=0 - ) - - doc_response = client.create_document(create_request) - test_doc_id = doc_response.payload.document.id - - print(f"\n📄 Created test document: {test_doc_id}") - print(f"🔗 View at: https://vaiz.app/document/{test_doc_id}") - - try: - # Create mentions with known structure - sent_mentions = [] - - # User mention - user_mention = mention_user(member_id) - sent_mentions.append(("User", member_id, user_mention)) - - content = [ - heading(1, "Mention Structure Test"), - paragraph(text("User: "), user_mention) - ] - - # Add other mentions if available - if doc_id: - doc_mention = mention_document(doc_id) - sent_mentions.append(("Document", doc_id, doc_mention)) - content.append(paragraph(text("Document: "), doc_mention)) - - if task_id: - task_mention = mention_task(task_id) - sent_mentions.append(("Task", task_id, task_mention)) - content.append(paragraph(text("Task: "), task_mention)) - - if milestone_id: - milestone_mention = mention_milestone(milestone_id) - sent_mentions.append(("Milestone", milestone_id, milestone_mention)) - content.append(paragraph(text("Milestone: "), milestone_mention)) - - print(f"\n📤 Sending {len(sent_mentions)} mention(s) to API...") - - # Print what we're sending - for kind, entity_id, mention_data in sent_mentions: - print(f"\n {kind} mention:") - print(f" Entity ID: {entity_id}") - print(f" Sent structure: {json.dumps(mention_data, indent=6)}") - - # Send to API - client.replace_json_document(test_doc_id, content) - - # Retrieve and verify - print(f"\n📥 Retrieving document from API...") - doc_content = client.get_json_document(test_doc_id) - - print(f"\n📋 Retrieved document structure:") - print(json.dumps(doc_content, indent=2)) - - # Extract mentions from response - received_mentions = [] - for node in doc_content.get("default", {}).get("content", []): - if node.get("type") == "paragraph" and "content" in node: - for child in node["content"]: - if child.get("type") == "custom-mention": - received_mentions.append(child) - - print(f"\n✅ Found {len(received_mentions)} mention(s) in response") - - # Detailed verification - print(f"\n🔍 Detailed verification:") - - attrs_present = 0 - for i, mention in enumerate(received_mentions, 1): - print(f"\n Mention {i}:") - print(f" Type: {mention.get('type')}") - print(f" Has 'attrs': {('attrs' in mention)}") - print(f" Has 'content': {('content' in mention)}") - - if 'attrs' in mention: - attrs_present += 1 - attrs = mention['attrs'] - print(f" Attributes present: {list(attrs.keys())}") - - if 'data' in attrs and 'item' in attrs['data']: - item = attrs['data']['item'] - print(f" Item ID: {item.get('id')}") - print(f" Item Kind: {item.get('kind')}") - - # Assertions - assert len(received_mentions) == len(sent_mentions), \ - f"Expected {len(sent_mentions)} mentions, got {len(received_mentions)}" - - # Check that all are mention types - for mention in received_mentions: - assert mention.get("type") == "custom-mention", \ - f"Expected type 'custom-mention', got {mention.get('type')}" - - print(f"\n📊 Summary:") - print(f" ✅ Sent: {len(sent_mentions)} mentions with full structure") - print(f" ✅ Received: {len(received_mentions)} mentions with full structure") - print(f" ✅ All mentions have 'attrs' field: {attrs_present}/{len(received_mentions)}") - print(f"\n🔗 Verify visually in browser: https://vaiz.app/document/{test_doc_id}") - print(f" Mentions should display with avatars/icons and be clickable") - - # Additional assertion - verify attrs present - assert attrs_present == len(received_mentions), \ - f"Expected all {len(received_mentions)} mentions to have attrs, but only {attrs_present} have it" - - finally: - pass # Could add cleanup here - - -if __name__ == "__main__": - test_verify_mention_structure_after_creation() - diff --git a/tests/test_milestones.py b/tests/test_milestones.py index e4c3728..43c5961 100644 --- a/tests/test_milestones.py +++ b/tests/test_milestones.py @@ -189,7 +189,8 @@ def test_toggle_milestone(client): assert response.type == "ToggleMilestone" assert response.task.id == task_id assert milestone_id in response.task.milestones - assert response.task.milestone == milestone_id # Should set the main milestone field too + # Note: the API no longer sets the main `milestone` field on toggle, + # only the `milestones` list is updated assert hasattr(response.task, "name") assert hasattr(response.task, "board") assert hasattr(response.task, "project") diff --git a/tests/test_replace_json_document.py b/tests/test_replace_json_document.py deleted file mode 100644 index 1b65361..0000000 --- a/tests/test_replace_json_document.py +++ /dev/null @@ -1,864 +0,0 @@ -""" -Tests for replaceJSONDocument API endpoint. -""" - -import pytest -from tests.test_config import get_test_client - - -def test_replace_json_document(): - """Test replacing document content with JSON content.""" - client = get_test_client() - - # Create a simple JSON content structure - json_content = [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Test content from replace_json_document"} - ] - } - ] - - # Use a known document ID from your test environment - # or create a task first to get a document ID - from vaiz.models import CreateTaskRequest, TaskPriority, Kind - - # Get first board to use - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create a test task - task_request = CreateTaskRequest( - name="Test Task for JSON Document Replacement", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Initial description" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - assert document_id, "Task should have a document ID" - - # Replace document content with JSON - response = client.replace_json_document( - document_id=document_id, - content=json_content - ) - - # Response should be successful (empty response object) - assert response is not None - - # Verify content was updated by fetching it - updated_content = client.get_json_document(document_id) - assert updated_content is not None - - print(f"✅ Successfully replaced document {document_id} with JSON content") - - -def test_replace_json_document_with_rich_content(): - """Test replacing document with rich formatted JSON content.""" - client = get_test_client() - - # Create rich JSON content with various formatting - rich_content = [ - { - "type": "heading", - "attrs": {"level": 1}, - "content": [ - {"type": "text", "text": "Rich Content Test"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "This is "}, - { - "type": "text", - "marks": [{"type": "bold"}], - "text": "bold text" - }, - {"type": "text", "text": " and this is "}, - { - "type": "text", - "marks": [{"type": "italic"}], - "text": "italic text" - }, - {"type": "text", "text": "."} - ] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "First item"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Second item"} - ] - } - ] - } - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Link: "}, - { - "type": "text", - "marks": [ - { - "type": "link", - "attrs": { - "href": "https://vaiz.app", - "target": "_blank" - } - } - ], - "text": "Vaiz" - } - ] - } - ] - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create a test task - from vaiz.models import CreateTaskRequest, TaskPriority - - task_request = CreateTaskRequest( - name="Test Task for Rich JSON Content", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Will be replaced" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Replace with rich content - response = client.replace_json_document( - document_id=document_id, - content=rich_content - ) - - assert response is not None - - # Verify content was updated - updated_content = client.get_json_document(document_id) - assert updated_content is not None - - print(f"✅ Successfully replaced document {document_id} with rich JSON content") - - -def test_replace_json_document_empty_content(): - """Test replacing document with empty JSON content.""" - client = get_test_client() - - # Empty content array - empty_content = [] - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create a test task - from vaiz.models import CreateTaskRequest, TaskPriority - - task_request = CreateTaskRequest( - name="Test Task for Empty JSON Content", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Original content" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Replace with empty content - response = client.replace_json_document( - document_id=document_id, - content=empty_content - ) - - assert response is not None - - print(f"✅ Successfully cleared document {document_id} content") - - -def test_replace_json_document_complex_structure(): - """Test replacing document with complex multi-level structure using only supported document elements.""" - client = get_test_client() - - # Complex document structure with ONLY validated working document elements: - # - paragraph, heading, text, bulletList, orderedList, listItem - # - marks: bold, italic, code, link - complex_content = [ - # Title - { - "type": "heading", - "attrs": {"level": 1}, - "content": [ - {"type": "text", "text": "📚 Comprehensive Project Documentation"} - ] - }, - # Subtitle with formatting - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "italic"}], "text": "Last updated: 2025-10-22 | "}, - {"type": "text", "marks": [{"type": "bold"}], "text": "Status: Active"} - ] - }, - # Separator paragraph - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"} - ] - }, - # Overview section - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "📋 Project Overview"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "This document provides a "}, - {"type": "text", "marks": [{"type": "bold"}], "text": "comprehensive overview"}, - {"type": "text", "text": " of the project architecture, implementation details, and "}, - { - "type": "text", - "marks": [ - {"type": "link", "attrs": {"href": "https://vaiz.app/roadmap", "target": "_blank"}} - ], - "text": "roadmap" - }, - {"type": "text", "text": "."} - ] - }, - # Key Features with bullet list - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "✨ Key Features"} - ] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Real-time collaboration: "}, - {"type": "text", "text": "Multiple users can work simultaneously"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Rich text editing: "}, - {"type": "text", "text": "Support for "}, - {"type": "text", "marks": [{"type": "italic"}], "text": "formatting"}, - {"type": "text", "text": ", "}, - {"type": "text", "marks": [{"type": "code"}], "text": "code"}, - {"type": "text", "text": ", and more"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "API Integration: "}, - {"type": "text", "text": "Comprehensive REST API with Python SDK"} - ] - }, - # Nested list - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Task management"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Document collaboration"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Custom field management"} - ] - } - ] - } - ] - } - ] - } - ] - }, - # Technical Stack section - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "🛠 Technical Stack"} - ] - }, - # Ordered list - { - "type": "orderedList", - "attrs": {"start": 1}, - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Backend: "}, - {"type": "text", "text": "Node.js, MongoDB, Redis"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Frontend: "}, - {"type": "text", "text": "React, TypeScript, Rich Editor"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "SDK: "}, - {"type": "text", "text": "Python 3.8+"} - ] - } - ] - } - ] - }, - # Code example section (using inline code instead of codeBlock) - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "💻 Quick Start"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Install the SDK: "}, - {"type": "text", "marks": [{"type": "code"}], "text": "pip install vaiz-sdk"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Initialize the client:"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "code"}], "text": "from vaiz import VaizClient"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "code"}], "text": "client = VaizClient(token=\"your_token\")"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "code"}], "text": "task = client.get_task(\"PRJ-123\")"} - ] - }, - # Important notes section (using bold paragraph instead of blockquote) - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "💡 Important Notes"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Security Notice: "}, - {"type": "text", "text": "Always store your API token securely. Never commit tokens to version control. Use environment variables or secure vault solutions."} - ] - }, - # Implementation timeline (removed strikethrough mark) - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "📅 Implementation Timeline"} - ] - }, - { - "type": "orderedList", - "attrs": {"start": 1}, - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Phase 1: "}, - {"type": "text", "text": "Core API development "}, - {"type": "text", "marks": [{"type": "code"}], "text": "✓ Completed"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Phase 2: "}, - {"type": "text", "text": "Python SDK implementation "}, - {"type": "text", "marks": [{"type": "code"}], "text": "⏳ In Progress"} - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "bold"}], "text": "Phase 3: "}, - {"type": "text", "text": "Advanced features (webhooks, real-time) "}, - {"type": "text", "marks": [{"type": "code"}], "text": "📋 Planned"} - ] - } - ] - } - ] - }, - # Resources section with mixed formatting - { - "type": "heading", - "attrs": {"level": 2}, - "content": [ - {"type": "text", "text": "🔗 Resources"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Official links:"} - ] - }, - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [{"type": "link", "attrs": {"href": "https://docs.vaiz.app", "target": "_blank"}}], - "text": "Documentation" - } - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [{"type": "link", "attrs": {"href": "https://github.com/vaizcom/vaiz-python-sdk", "target": "_blank"}}], - "text": "GitHub Repository" - } - ] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [{"type": "link", "attrs": {"href": "https://vaiz.app/community", "target": "_blank"}}], - "text": "Community Forum" - } - ] - } - ] - } - ] - }, - # Footer (using separator paragraph instead of horizontalRule) - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"} - ] - }, - { - "type": "paragraph", - "content": [ - {"type": "text", "marks": [{"type": "italic"}], "text": "This document was generated programmatically using the "}, - {"type": "text", "marks": [{"type": "code"}], "text": "replace_json_document"}, - {"type": "text", "marks": [{"type": "italic"}], "text": " API method."} - ] - } - ] - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create a test task - from vaiz.models import CreateTaskRequest, TaskPriority - - task_request = CreateTaskRequest( - name="Test Task with Complex Document Structure", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="Will be replaced with complex content" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Replace with complex structure - response = client.replace_json_document( - document_id=document_id, - content=complex_content - ) - - assert response is not None - - # Verify content was updated - updated_content = client.get_json_document(document_id) - assert updated_content is not None - - print(f"✅ Successfully replaced document {document_id} with complex structure") - print(f" Structure includes: headings, lists (nested), inline code, links, and formatting") - - -def test_replace_json_document_complete_replacement(): - """Test that replace_json_document COMPLETELY replaces old content (strict test).""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create a test task with SPECIFIC initial content - from vaiz.models import CreateTaskRequest, TaskPriority - - task_request = CreateTaskRequest( - name="Test Complete JSON Replacement", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="OLD_CONTENT_MARKER - This text should be completely removed after replacement" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Verify initial content exists - initial_content = client.get_json_document(document_id) - initial_text = str(initial_content) - assert "OLD_CONTENT_MARKER" in initial_text, "Initial content should contain marker" - - # Replace with completely NEW content - new_content = [ - # Heading with NEW marker - { - "type": "heading", - "attrs": {"level": 1}, - "content": [ - {"type": "text", "text": "NEW_CONTENT_MARKER - Verified Structure"} - ] - }, - # Paragraph with formatting - { - "type": "paragraph", - "content": [ - {"type": "text", "text": "Text with "}, - {"type": "text", "marks": [{"type": "bold"}], "text": "bold"}, - {"type": "text", "text": ", "}, - {"type": "text", "marks": [{"type": "italic"}], "text": "italic"}, - {"type": "text", "text": ", and "}, - {"type": "text", "marks": [{"type": "code"}], "text": "code"} - ] - }, - # Link - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [ - {"type": "link", "attrs": {"href": "https://vaiz.app", "target": "_blank"}} - ], - "text": "Link to Vaiz" - } - ] - }, - # Bullet list - { - "type": "bulletList", - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Bullet item 1"}] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Bullet item 2"}] - } - ] - } - ] - }, - # Ordered list - { - "type": "orderedList", - "attrs": {"start": 1}, - "content": [ - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Ordered step 1"}] - } - ] - }, - { - "type": "listItem", - "content": [ - { - "type": "paragraph", - "content": [{"type": "text", "text": "Ordered step 2"}] - } - ] - } - ] - } - ] - - # Replace - response = client.replace_json_document( - document_id=document_id, - content=new_content - ) - - assert response is not None - - # Retrieve and STRICTLY verify replacement - saved_content = client.get_json_document(document_id) - saved_text = str(saved_content) - - # CRITICAL: Old content MUST be completely gone - assert "OLD_CONTENT_MARKER" not in saved_text, "OLD CONTENT STILL EXISTS! API did not replace, it appended!" - - # CRITICAL: New content MUST be present - assert "NEW_CONTENT_MARKER" in saved_text, "New content not found" - assert "Verified Structure" in saved_text, "New heading not found" - - # Verify structure elements are saved correctly - saved_blocks = saved_content.get("default", {}).get("content", []) - - # Verify we have all expected block types - block_types = [block.get("type") for block in saved_blocks] - assert "heading" in block_types, "Heading not saved" - assert "paragraph" in block_types, "Paragraph not saved" - assert "bulletList" in block_types, "Bullet list not saved" - assert "orderedList" in block_types, "Ordered list not saved" - - # Verify marks are preserved - assert "bold" in saved_text, "Bold formatting not preserved" - assert "italic" in saved_text, "Italic formatting not preserved" - assert "code" in saved_text, "Code formatting not preserved" - assert "vaiz.app" in saved_text, "Link not preserved" - - print(f"✅ STRICT TEST PASSED: Complete replacement verified") - print(f" ✓ Old content removed (OLD_CONTENT_MARKER not found)") - print(f" ✓ New content present (NEW_CONTENT_MARKER found)") - print(f" ✓ All structure elements preserved") - print(f" ✓ All formatting marks preserved") - - -def test_replace_document_complete_replacement(): - """Test that replace_document COMPLETELY replaces old content (strict test).""" - client = get_test_client() - - # Get first board - boards = client.get_boards() - if not boards or not boards.boards: - pytest.skip("No boards available for testing") - - board = boards.boards[0] - - # Create a test task with SPECIFIC initial content - from vaiz.models import CreateTaskRequest, TaskPriority - - task_request = CreateTaskRequest( - name="Test Complete Plain Text Replacement", - group=client.space_id, - board=board.id, - priority=TaskPriority.General, - description="PLAIN_OLD_MARKER - This plain text should be completely removed" - ) - - task_response = client.create_task(task_request) - document_id = task_response.task.document - - # Verify initial content exists - initial_content = client.get_json_document(document_id) - initial_text = str(initial_content) - assert "PLAIN_OLD_MARKER" in initial_text, "Initial content should contain marker" - - # Replace with completely NEW plain text - new_description = "PLAIN_NEW_MARKER - This is the new content. The old content should be gone." - - response = client.replace_document( - document_id=document_id, - description=new_description - ) - - assert response is not None - - # Retrieve and STRICTLY verify replacement - saved_content = client.get_json_document(document_id) - saved_text = str(saved_content) - - # CRITICAL: Old content MUST be completely gone - assert "PLAIN_OLD_MARKER" not in saved_text, "OLD CONTENT STILL EXISTS! API did not replace, it appended!" - - # CRITICAL: New content MUST be present - assert "PLAIN_NEW_MARKER" in saved_text, "New content not found" - assert "new content" in saved_text, "New description text not found" - - print(f"✅ STRICT TEST PASSED: Complete plain text replacement verified") - print(f" ✓ Old content removed (PLAIN_OLD_MARKER not found)") - print(f" ✓ New content present (PLAIN_NEW_MARKER found)") - - -if __name__ == "__main__": - print("Running replace_json_document tests...") - test_replace_json_document() - test_replace_json_document_with_rich_content() - test_replace_json_document_empty_content() - test_replace_json_document_complex_structure() - test_replace_json_document_complete_replacement() - test_replace_document_complete_replacement() - print("All tests passed! ✅") - diff --git a/tests/test_standalone_documents.py b/tests/test_standalone_documents.py index bfbdca1..ce7316b 100644 --- a/tests/test_standalone_documents.py +++ b/tests/test_standalone_documents.py @@ -6,19 +6,12 @@ import pytest from tests.test_config import get_test_client from vaiz.models import CreateDocumentRequest, Kind -from vaiz import ( - heading, paragraph, text, - bullet_list, ordered_list, list_item, - table, table_row, table_cell, table_header, - horizontal_rule, link_text -) def test_create_space_document_with_content(): """Create a standalone Space document (appears in Space docs section).""" client = get_test_client() - - # Create Space document + doc_response = client.create_document( CreateDocumentRequest( kind=Kind.Space, @@ -27,89 +20,55 @@ def test_create_space_document_with_content(): index=0 ) ) - + document_id = doc_response.payload.document.id assert document_id is not None - - # Add comprehensive content - content = [ - heading(1, "📄 SDK Test - Space Document"), - - paragraph( - "This is a ", - text("standalone Space document", bold=True), - " created by the Vaiz Python SDK test suite. ", - "It should appear in the ", - text("Space docs", italic=True), - " section." - ), - - horizontal_rule(), - - heading(2, "✨ Features Demonstrated"), - - bullet_list( - "Text formatting (bold, italic, code)", - "Lists (bullet and ordered)", - "Tables with headers", - "Horizontal rules", - "Links" - ), - - heading(2, "📊 Sample Table"), - - table( - table_row( - table_header("Feature"), - table_header("Status"), - table_header("Version") - ), - table_row("Document creation", "✅ Working", "0.9.0"), - table_row("Content helpers", "✅ Working", "0.8.0"), - table_row("Table headers", "✅ Working", "0.9.0") - ), - - horizontal_rule(), - - paragraph( - text("Learn more: ", bold=True), - link_text("Vaiz Documentation", "https://docs.vaiz.app") - ) - ] - - # Add content to document - response = client.replace_json_document(document_id, content) + + markdown = ( + "# SDK Test - Space Document\n\n" + "This is a **standalone Space document** created by the Vaiz Python SDK " + "test suite. It should appear in the *Space docs* section.\n\n" + "---\n\n" + "## Features Demonstrated\n\n" + "- Text formatting (bold, italic, code)\n" + "- Lists (bullet and ordered)\n" + "- Tables with headers\n" + "- Horizontal rules\n" + "- Links\n\n" + "## Sample Table\n\n" + "| Feature | Status | Version |\n" + "| --- | --- | --- |\n" + "| Document creation | Working | 1.0.0 |\n" + "| Markdown content | Working | 1.0.0 |\n\n" + "---\n\n" + "**Learn more:** [Vaiz Documentation](https://docs.vaiz.app)" + ) + + response = client.replace_markdown_document(document_id, markdown) assert response is not None - - # Verify content was saved - saved = client.get_json_document(document_id) - saved_blocks = saved.get("default", {}).get("content", []) - - assert len(saved_blocks) > 0, "Document should have content blocks" - - # Verify we have headings and tables - headings = sum(1 for b in saved_blocks if b.get("type") == "heading") - tables = sum(1 for b in saved_blocks if b.get("type") == "extension-table") - + + # Verify content was saved via markdown round-trip + saved = client.get_markdown_document(document_id) + lines = saved.splitlines() + + headings = sum(1 for l in lines if l.lstrip().startswith("#")) + table_separators = sum(1 for l in lines if l.strip().startswith("|") and "---" in l) + assert headings >= 2, "Should have at least 2 headings" - assert tables >= 1, "Should have at least 1 table" - + assert table_separators >= 1, "Should have at least 1 table" + print(f"✅ Space document created successfully") print(f" Document ID: {document_id}") print(f" Title: {doc_response.payload.document.title}") - print(f" Content blocks: {len(saved_blocks)}") - print(f" Location: Space docs section") def test_create_personal_document_with_content(): """Create a standalone Member (Personal) document.""" client = get_test_client() - - # Get member ID for personal documents + profile = client.get_profile() member_id = profile.profile.member_id - - # Create Personal document + doc_response = client.create_document( CreateDocumentRequest( kind=Kind.Member, @@ -118,127 +77,56 @@ def test_create_personal_document_with_content(): index=0 ) ) - + document_id = doc_response.payload.document.id assert document_id is not None - - # Add comprehensive content - content = [ - heading(1, "📝 Personal Notes - SDK Test"), - - paragraph( - "This is a ", - text("personal document", bold=True), - " created by the SDK. ", - "It should appear in the ", - text("Personal docs", italic=True), - " section." - ), - - horizontal_rule(), - - heading(2, "🎯 My Tasks Today"), - - ordered_list( - "Review SDK test results", - "Update documentation", - "Test new table_header feature", - "Commit changes to repository" - ), - - heading(2, "💡 Ideas"), - - bullet_list( - list_item( - paragraph(text("Document Structure", bold=True)), - bullet_list( - "Add more examples", - "Create video tutorials" - ) - ), - list_item( - paragraph(text("SDK Improvements", bold=True)), - bullet_list( - "Better error messages", - "More helper functions" - ) - ) - ), - - heading(2, "📈 Progress Tracking"), - - table( - table_row( - table_header("Week"), - table_header("Tasks Completed"), - table_header("Status") - ), - table_row("Week 1", "12", "✅ Good"), - table_row("Week 2", "15", "✅ Excellent"), - table_row("Week 3", "10", "⚠️ Need improvement"), - table_row( - table_cell(text("Total", bold=True)), - table_cell(text("37", bold=True)), - table_cell("📊") - ) - ), - - horizontal_rule(), - - heading(2, "🔗 Useful Links"), - - paragraph( - "• ", link_text("Vaiz Docs", "https://docs.vaiz.app"), "\n", - "• ", link_text("Python SDK", "https://github.com/vaiz/vaiz-python-sdk"), "\n", - "• ", link_text("API Reference", "https://api.vaiz.app") - ), - - horizontal_rule(), - - paragraph( - text("Created with: ", italic=True), - text("Vaiz Python SDK", code=True) - ) - ] - - # Add content to document - response = client.replace_json_document(document_id, content) + + markdown = ( + "# Personal Notes - SDK Test\n\n" + "This is a **personal document** created by the SDK. " + "It should appear in the *Personal docs* section.\n\n" + "---\n\n" + "## My Tasks Today\n\n" + "1. Review SDK test results\n" + "2. Update documentation\n" + "3. Commit changes to repository\n\n" + "## Ideas\n\n" + "- **Documentation**\n" + " - Add more examples\n" + " - Create video tutorials\n" + "- **SDK Improvements**\n" + " - Better error messages\n" + " - More helper functions\n\n" + "## Progress Tracking\n\n" + "| Week | Tasks Completed | Status |\n" + "| --- | --- | --- |\n" + "| Week 1 | 12 | Good |\n" + "| Week 2 | 15 | Excellent |\n" + "| Week 3 | 10 | Needs improvement |\n\n" + "---\n\n" + "## Useful Links\n\n" + "- [Vaiz Docs](https://docs.vaiz.app)\n" + "- [Python SDK](https://github.com/vaizcom/vaiz-python-sdk)\n\n" + "*Created with* `Vaiz Python SDK`" + ) + + response = client.replace_markdown_document(document_id, markdown) assert response is not None - - # Verify content was saved - saved = client.get_json_document(document_id) - saved_blocks = saved.get("default", {}).get("content", []) - - assert len(saved_blocks) > 0, "Document should have content blocks" - - # Count elements - headings = sum(1 for b in saved_blocks if b.get("type") == "heading") - tables = sum(1 for b in saved_blocks if b.get("type") == "extension-table") - bullet_lists = sum(1 for b in saved_blocks if b.get("type") == "bulletList") - ordered_lists = sum(1 for b in saved_blocks if b.get("type") == "orderedList") - + + saved = client.get_markdown_document(document_id) + lines = saved.splitlines() + + headings = sum(1 for l in lines if l.lstrip().startswith("#")) + table_separators = sum(1 for l in lines if l.strip().startswith("|") and "---" in l) + bullet_items = sum(1 for l in lines if l.lstrip().startswith("- ")) + ordered_items = sum(1 for l in lines if l.lstrip()[:3] in ("1. ", "2. ", "3. ")) + assert headings >= 4, "Should have at least 4 headings" - assert tables >= 1, "Should have at least 1 table" - assert bullet_lists >= 1, "Should have bullet lists" - assert ordered_lists >= 1, "Should have ordered list" - + assert table_separators >= 1, "Should have at least 1 table" + assert bullet_items >= 2, "Should have bullet lists" + assert ordered_items >= 3, "Should have ordered list" + print(f"✅ Personal document created successfully") print(f" Document ID: {document_id}") print(f" Title: {doc_response.payload.document.title}") - print(f" Content blocks: {len(saved_blocks)}") - print(f" Headings: {headings}, Tables: {tables}") - print(f" Location: Personal docs section") - - -if __name__ == "__main__": - print("Creating standalone documents...") - print("\n" + "="*60) - test_create_space_document_with_content() - print("\n" + "="*60) - test_create_personal_document_with_content() - print("\n" + "="*60) - print("\n✅ All standalone documents created!") - print("Check your Vaiz interface:") - print(" - Space docs: Should see 'SDK Test - Space Document'") - print(" - Personal docs: Should see 'SDK Test Document - Member (Personal)'") - + print(f" Headings: {headings}, Tables: {table_separators}") diff --git a/tests/test_task_list_integration.py b/tests/test_task_list_integration.py deleted file mode 100644 index edd0647..0000000 --- a/tests/test_task_list_integration.py +++ /dev/null @@ -1,338 +0,0 @@ -""" -Integration tests for task lists (checklists) in real documents and tasks. -""" - -import pytest -import sys -import os - -# Add tests directory to path for test_config import -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from test_config import get_test_client, TEST_BOARD_ID, TEST_GROUP_ID -from vaiz import ( - CreateTaskRequest, - heading, - paragraph, - task_list, - task_item, -) - - -def get_client(): - """Get test client.""" - return get_test_client() - - -def get_board_id(): - """Get test board ID.""" - if not TEST_BOARD_ID: - pytest.skip("TEST_BOARD_ID not set in environment") - return TEST_BOARD_ID - - -def get_group_id(): - """Get test group ID.""" - if not TEST_GROUP_ID: - pytest.skip("TEST_GROUP_ID not set in environment") - return TEST_GROUP_ID - - -@pytest.mark.integration -def test_create_task_with_simple_checklist(): - """Test creating a real task with a simple checklist in description.""" - client = get_client() - board_id = get_board_id() - group_id = get_group_id() - - # Create task first - request = CreateTaskRequest( - name="Test Task with Checklist", - board=board_id, - group=group_id, - ) - - response = client.create_task(request) - task_id = response.task.id - document_id = response.task.document - - # Verify task was created - assert task_id is not None - assert response.task.name == "Test Task with Checklist" - - # Now add checklist content to the task description - content = [ - heading(1, "Sprint Tasks"), - paragraph("Here's what needs to be done:"), - task_list( - task_item("Review pull requests", checked=True), - task_item("Update documentation", checked=False), - task_item("Deploy to production", checked=False), - ) - ] - - client.replace_json_document(document_id, content) - - # Fetch task and verify content - task = client.get_task(task_id) - doc_content = client.get_json_document(task.task.document) - - # Verify structure - assert "default" in doc_content - content_blocks = doc_content["default"]["content"] - - # Find task list block - task_list_block = None - for block in content_blocks: - if block.get("type") == "taskList": - task_list_block = block - break - - assert task_list_block is not None, "Task list block not found in document" - assert len(task_list_block["content"]) == 3 - - # Verify task items - items = task_list_block["content"] - assert items[0]["attrs"]["checked"] == True - assert items[1]["attrs"]["checked"] == False - assert items[2]["attrs"]["checked"] == False - - print(f"✅ Created task with checklist: {task_id}") - return task_id - - -@pytest.mark.integration -def test_create_task_with_nested_checklist(): - """Test creating a task with nested multi-level checklist.""" - client = get_client() - board_id = get_board_id() - group_id = get_group_id() - - # Create task first - request = CreateTaskRequest( - name="Project with Nested Checklist", - board=board_id, - group=group_id, - ) - - response = client.create_task(request) - task_id = response.task.id - document_id = response.task.document - - # Verify task was created - assert task_id is not None - - # Create nested checklist content - content = [ - heading(1, "Project Milestones"), - task_list( - task_item( - paragraph("Phase 1: Planning"), - task_list( - task_item("Define requirements", checked=True), - task_item("Create wireframes", checked=True), - task_item("Review with stakeholders", checked=False) - ), - checked=True - ), - task_item( - paragraph("Phase 2: Development"), - task_list( - task_item("Setup project structure", checked=True), - task_item("Implement features", checked=False), - task_item( - paragraph("Testing"), - task_list( - task_item("Unit tests", checked=False), - task_item("Integration tests", checked=False), - ), - checked=False - ) - ), - checked=False - ), - ) - ] - - # Update task description with nested checklist - client.replace_json_document(document_id, content) - - # Fetch and verify structure - task = client.get_task(task_id) - doc_content = client.get_json_document(task.task.document) - - # Verify nested structure exists - assert "default" in doc_content - content_blocks = doc_content["default"]["content"] - - # Find main task list - main_task_list = None - for block in content_blocks: - if block.get("type") == "taskList": - main_task_list = block - break - - assert main_task_list is not None - assert len(main_task_list["content"]) == 2 - - # Verify first item has nested list - first_item = main_task_list["content"][0] - assert first_item["attrs"]["checked"] == True - - # Find nested task list in first item - nested_list = None - for item in first_item["content"]: - if isinstance(item, dict) and item.get("type") == "taskList": - nested_list = item - break - - assert nested_list is not None - assert len(nested_list["content"]) == 3 - - print(f"✅ Created task with nested checklist: {task_id}") - return task_id - - -@pytest.mark.integration -def test_update_task_with_checklist(): - """Test updating an existing task to add a checklist.""" - client = get_client() - board_id = get_board_id() - group_id = get_group_id() - - # Create simple task first - request = CreateTaskRequest( - name="Task to Update with Checklist", - board=board_id, - group=group_id - ) - - response = client.create_task(request) - task_id = response.task.id - document_id = response.task.document - - # Update with checklist - new_content = [ - heading(1, "Updated with Checklist"), - paragraph("Action items:"), - task_list( - task_item("First action", checked=False), - task_item("Second action", checked=False), - task_item("Third action", checked=False), - ) - ] - - client.replace_json_document(document_id, new_content) - - # Verify update - doc_content = client.get_json_document(document_id) - content_blocks = doc_content["default"]["content"] - - # Find task list - has_task_list = any(block.get("type") == "taskList" for block in content_blocks) - assert has_task_list, "Task list not found after update" - - print(f"✅ Updated task with checklist: {task_id}") - return task_id - - -@pytest.mark.integration -def test_task_list_with_mixed_content(): - """Test creating a task with mixed content including checklist.""" - client = get_client() - board_id = get_board_id() - group_id = get_group_id() - - # Create task first - request = CreateTaskRequest( - name="Sprint Planning with Checklists", - board=board_id, - group=group_id, - ) - - response = client.create_task(request) - task_id = response.task.id - document_id = response.task.document - - # Verify task was created - assert task_id is not None - - # Create mixed content with checklists - content = [ - heading(1, "Sprint Planning"), - paragraph("This sprint focuses on user authentication."), - - heading(2, "Backend Tasks"), - task_list( - task_item("Design database schema", checked=True), - task_item("Implement authentication API", checked=False), - task_item("Add JWT tokens", checked=False), - ), - - heading(2, "Frontend Tasks"), - task_list( - task_item("Create login page", checked=True), - task_item("Create registration page", checked=False), - task_item("Add form validation", checked=False), - ), - - paragraph("Review meeting scheduled for Friday."), - ] - - # Update task description with mixed content - client.replace_json_document(document_id, content) - - # Verify content - task = client.get_task(task_id) - doc_content = client.get_json_document(task.task.document) - content_blocks = doc_content["default"]["content"] - - # Count task lists - task_lists = [block for block in content_blocks if block.get("type") == "taskList"] - assert len(task_lists) == 2, "Should have 2 task lists" - - # Count headings - headings = [block for block in content_blocks if block.get("type") == "heading"] - assert len(headings) >= 2, "Should have at least 2 headings" - - print(f"✅ Created task with mixed content and checklists: {task_id}") - return task_id - - -if __name__ == "__main__": - print("Running task list integration tests...") - print() - - try: - task_id_1 = test_create_task_with_simple_checklist() - print(f"Test 1 passed: Simple checklist - Task ID: {task_id_1}") - print() - - task_id_2 = test_create_task_with_nested_checklist() - print(f"Test 2 passed: Nested checklist - Task ID: {task_id_2}") - print() - - task_id_3 = test_update_task_with_checklist() - print(f"Test 3 passed: Update with checklist - Task ID: {task_id_3}") - print() - - task_id_4 = test_task_list_with_mixed_content() - print(f"Test 4 passed: Mixed content - Task ID: {task_id_4}") - print() - - print("=" * 60) - print("✅ All integration tests passed!") - print() - print("Created tasks with task lists:") - print(f" 1. Simple checklist: {task_id_1}") - print(f" 2. Nested checklist: {task_id_2}") - print(f" 3. Updated checklist: {task_id_3}") - print(f" 4. Mixed content: {task_id_4}") - print() - print("You can view these tasks in your Vaiz workspace to see the checklists!") - - except Exception as e: - print(f"❌ Test failed: {e}") - import traceback - traceback.print_exc() - diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 2fbc7e3..4a33845 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -1,7 +1,7 @@ import pytest from datetime import datetime from tests.test_config import get_test_client, TEST_BOARD_ID, TEST_GROUP_ID, TEST_PROJECT_ID, TEST_ASSIGNEE_ID -from vaiz.models import CreateTaskRequest, EditTaskRequest, TaskPriority, GetHistoryRequest, GetHistoryResponse, HistoryItem, ReplaceDocumentResponse, GetTasksRequest, GetTasksResponse, MoveTaskItem, MoveTasksRequest, MoveTasksResponse +from vaiz.models import CreateTaskRequest, EditTaskRequest, TaskPriority, GetHistoryRequest, GetHistoryResponse, HistoryItem, ReplaceMarkdownDocumentResponse, GetTasksRequest, GetTasksResponse, MoveTaskItem, MoveTasksRequest, MoveTasksResponse from vaiz.models.enums import Kind @pytest.fixture(scope="module") @@ -219,19 +219,13 @@ def test_task_get_description_method_with_initial_content(client): task_instance = task_response.task assert task_instance.document is not None - # Use the convenience method to get description - description_body = task_instance.get_task_description(client) - assert isinstance(description_body, dict) - - print(f"Initial description body keys: {list(description_body.keys())}") - print(f"Full description body: {description_body}") + # Use the convenience method to get description as Markdown + description_markdown = task_instance.get_task_description(client) + assert isinstance(description_markdown, str) + assert "Initial task description content" in description_markdown + print(f"Task document ID: {task_instance.document}") - - # Check if there's content in the default key - if 'default' in description_body: - default_content = description_body['default'] - print(f"Default content type: {type(default_content)}") - print(f"Default content: {default_content}") + print(f"Description markdown: {description_markdown}") @@ -255,17 +249,18 @@ def test_task_update_description_method(client): task_instance = task_response.task assert task_instance.document is not None - # Update description via convenience method + # Update description via convenience method (Markdown) new_description = ( - "Updated via Task.update_task_description()\n\n" - "This replaces the existing description content." + "## Updated Description\n\n" + "Updated via `Task.update_task_description()` with **markdown** content." ) update_response = task_instance.update_task_description(client, new_description) - assert isinstance(update_response, ReplaceDocumentResponse) + assert isinstance(update_response, ReplaceMarkdownDocumentResponse) - # Fetch description body to ensure API call succeeded - updated_body = task_instance.get_task_description(client) - assert isinstance(updated_body, dict) + # Verify markdown round-trip + updated_markdown = task_instance.get_task_description(client) + assert "## Updated Description" in updated_markdown + assert "Initial description" not in updated_markdown def test_get_tasks_default_parameters(client): diff --git a/vaiz/__init__.py b/vaiz/__init__.py index 75708e2..6741aaa 100644 --- a/vaiz/__init__.py +++ b/vaiz/__init__.py @@ -37,14 +37,6 @@ UploadedFile, UploadFileResponse, Document, - ReplaceDocumentRequest, - ReplaceDocumentResponse, - ReplaceJSONDocumentRequest, - ReplaceJSONDocumentResponse, - AppendDocumentRequest, - AppendDocumentResponse, - AppendJSONDocumentRequest, - AppendJSONDocumentResponse, ReplaceMarkdownDocumentRequest, ReplaceMarkdownDocumentResponse, AppendMarkdownDocumentRequest, @@ -127,39 +119,6 @@ make_number_value, make_checkbox_value, make_url_value, - - # Document structure builders - text, - paragraph, - heading, - list_item, - bullet_list, - ordered_list, - task_item, - task_list, - link_text, - horizontal_rule, - blockquote, - details, - details_summary, - details_content, - table, - table_row, - table_cell, - table_header, - mention, - mention_user, - mention_document, - mention_task, - mention_milestone, - image_block, - files_block, - toc_block, - anchors_block, - siblings_block, - code_block, - embed_block, - EmbedType, ) __all__ = [ @@ -193,14 +152,6 @@ 'UploadedFile', 'UploadFileResponse', 'Document', - 'ReplaceDocumentRequest', - 'ReplaceDocumentResponse', - 'ReplaceJSONDocumentRequest', - 'ReplaceJSONDocumentResponse', - 'AppendDocumentRequest', - 'AppendDocumentResponse', - 'AppendJSONDocumentRequest', - 'AppendJSONDocumentResponse', 'ReplaceMarkdownDocumentRequest', 'ReplaceMarkdownDocumentResponse', 'AppendMarkdownDocumentRequest', @@ -278,37 +229,4 @@ 'make_number_value', 'make_checkbox_value', 'make_url_value', - - # Document structure builders - 'text', - 'paragraph', - 'heading', - 'list_item', - 'bullet_list', - 'ordered_list', - 'task_item', - 'task_list', - 'link_text', - 'horizontal_rule', - 'blockquote', - 'details', - 'details_summary', - 'details_content', - 'table', - 'table_row', - 'table_cell', - 'table_header', - 'mention', - 'mention_user', - 'mention_document', - 'mention_task', - 'mention_milestone', - 'image_block', - 'files_block', - 'toc_block', - 'anchors_block', - 'siblings_block', - 'code_block', - 'embed_block', - 'EmbedType', ] \ No newline at end of file diff --git a/vaiz/api/comments.py b/vaiz/api/comments.py index 1659fa7..6407d20 100644 --- a/vaiz/api/comments.py +++ b/vaiz/api/comments.py @@ -10,28 +10,46 @@ class CommentsAPIClient(BaseAPIClient): def post_comment( self, document_id: str, - content: str, + content: Optional[str] = None, file_ids: Optional[List[str]] = None, - reply_to: Optional[str] = None + reply_to: Optional[str] = None, + markdown: Optional[str] = None ) -> PostCommentResponse: """ Post a comment to a document. - + + Markdown is the recommended content format: it is converted to rich + comment content on the server. HTML `content` remains supported as + the legacy format. Exactly one of `content` or `markdown` must be + provided. + Args: document_id (str): The ID of the document to comment on - content (str): The comment content (can include HTML) + content (Optional[str]): The comment content as HTML (legacy format) file_ids (Optional[List[str]]): List of file IDs to attach to the comment reply_to (Optional[str]): ID of the comment to reply to (for threaded replies) - + markdown (Optional[str]): The comment content as Markdown (recommended) + Returns: PostCommentResponse: The created comment information - + Raises: + ValueError: If both or neither of `content` and `markdown` are provided VaizSDKError: If the API request fails + + Example: + >>> client.post_comment( + ... document_id="doc_id", + ... markdown="Some **bold** text\\n\\n- item 1\\n- item 2" + ... ) """ + if (content is None) == (markdown is None): + raise ValueError("Provide exactly one of 'content' or 'markdown'") + request = PostCommentRequest( document_id=document_id, content=content, + markdown=markdown, file_ids=file_ids or [], reply_to=reply_to ) @@ -113,17 +131,22 @@ def add_reaction( def get_comments(self, document_id: str) -> GetCommentsResponse: """ Get all comments for a document. - + + Comment content is returned as Markdown (the same format `post_comment` + accepts), including mentions as `@[label](kind:id)`. Legacy comments + (`content_version` other than 2) are returned as raw HTML; check + `Comment.content_version` to tell the formats apart. + Args: document_id (str): The ID of the document to get comments for - + Returns: GetCommentsResponse: The list of comments for the document - + Raises: VaizSDKError: If the API request fails """ - request = GetCommentsRequest(document_id=document_id) + request = GetCommentsRequest(document_id=document_id, format="markdown") response_data = self._make_request("getComments", json_data=request.model_dump()) return GetCommentsResponse(**response_data) @@ -131,29 +154,47 @@ def get_comments(self, document_id: str) -> GetCommentsResponse: def edit_comment( self, comment_id: str, - content: str, + content: Optional[str] = None, add_file_ids: Optional[List[str]] = None, order_file_ids: Optional[List[str]] = None, - remove_file_ids: Optional[List[str]] = None + remove_file_ids: Optional[List[str]] = None, + markdown: Optional[str] = None ) -> EditCommentResponse: """ Edit an existing comment. - + + Markdown is the recommended content format: it is converted to rich + comment content on the server. HTML `content` remains supported as + the legacy format. Exactly one of `content` or `markdown` must be + provided. + Args: comment_id (str): The ID of the comment to edit - content (str): The new content for the comment (HTML supported) + content (Optional[str]): The new content as HTML (legacy format) add_file_ids (Optional[List[str]]): File IDs to add to the comment order_file_ids (Optional[List[str]]): File IDs in new order remove_file_ids (Optional[List[str]]): File IDs to remove from the comment - + markdown (Optional[str]): The new content as Markdown (recommended) + Returns: EditCommentResponse: The updated comment - + Raises: + ValueError: If both or neither of `content` and `markdown` are provided VaizSDKError: If the API request fails + + Example: + >>> client.edit_comment( + ... comment_id="comment_id", + ... markdown="Updated with a [link](https://vaiz.app)" + ... ) """ + if (content is None) == (markdown is None): + raise ValueError("Provide exactly one of 'content' or 'markdown'") + request = EditCommentRequest( content=content, + markdown=markdown, comment_id=comment_id, add_file_ids=add_file_ids or [], order_file_ids=order_file_ids or [], diff --git a/vaiz/api/documents.py b/vaiz/api/documents.py index 9910674..7c0c151 100644 --- a/vaiz/api/documents.py +++ b/vaiz/api/documents.py @@ -1,17 +1,5 @@ -from typing import Any, Dict, List, Union, Optional -import json - from vaiz.api.base import BaseAPIClient from vaiz.models.documents import ( - GetDocumentRequest, - ReplaceDocumentRequest, - ReplaceDocumentResponse, - ReplaceJSONDocumentRequest, - ReplaceJSONDocumentResponse, - AppendDocumentRequest, - AppendDocumentResponse, - AppendJSONDocumentRequest, - AppendJSONDocumentResponse, ReplaceMarkdownDocumentRequest, ReplaceMarkdownDocumentResponse, AppendMarkdownDocumentRequest, @@ -26,199 +14,10 @@ EditDocumentResponse ) -# Import Document Structure types for better type hints -try: - from vaiz.helpers.document_structure import DocumentNode -except ImportError: - DocumentNode = Dict[str, Any] # Fallback if typing_extensions not available class DocumentsAPIClient(BaseAPIClient): """API client for document content operations.""" - def get_json_document(self, document_id: str) -> Dict[str, Any]: - """ - Fetch JSON document content by document ID. - - This universal method allows retrieving the content of a task description - or a standalone document body. - - Args: - document_id: The document ID to fetch - - Returns: - Dict[str, Any]: The JSON document as returned by the API (unmodeled) - """ - request = GetDocumentRequest(document_id=document_id) - response_data = self._make_request("getJSONDocument", json_data=request.model_dump()) - # API returns shape: { payload: { json: "{...}" }, type: "GetJSONDocument" } - payload = response_data.get("payload", {}) - json_str = payload.get("json", "{}") - try: - parsed = json.loads(json_str) - except (TypeError, json.JSONDecodeError): - parsed = {} - return parsed - - def replace_document(self, document_id: str, description: str) -> ReplaceDocumentResponse: - """ - Replace document content completely. - - Args: - document_id: The document ID to replace content for - description: New description content as plain text string - - Returns: - ReplaceDocumentResponse: Empty response object on success - - Raises: - VaizSDKError: If the API request fails - """ - request = ReplaceDocumentRequest( - document_id=document_id, - description=description - ) - - response_data = self._make_request("replaceDocument", json_data=request.model_dump()) - return ReplaceDocumentResponse(**response_data) - - def replace_json_document( - self, - document_id: str, - content: Union[List[DocumentNode], List[Dict[str, Any]]] - ) -> ReplaceJSONDocumentResponse: - """ - Replace document content with structured JSON content. - - This method allows you to replace document content with structured JSON content - using the document editor format. This is useful when you need to set rich content with - specific formatting, links, lists, etc. - - **Tip:** Use document structure builder functions from `vaiz` for type-safe content creation: - `text()`, `paragraph()`, `heading()`, `bullet_list()`, `ordered_list()`, `link_text()` - - Args: - document_id: The document ID to replace content for - content: JSONContent array in document structure format, or use helper functions - - Returns: - ReplaceJSONDocumentResponse: Empty response object on success - - Raises: - VaizSDKError: If the API request fails - - Example with raw JSON: - >>> content = [ - ... { - ... "type": "paragraph", - ... "content": [ - ... {"type": "text", "text": "Hello, "}, - ... {"type": "text", "marks": [{"type": "bold"}], "text": "World"} - ... ] - ... } - ... ] - >>> client.replace_json_document(document_id, content) - - Example with helper functions: - >>> from vaiz import paragraph, text, heading - >>> content = [ - ... heading(1, "Welcome"), - ... paragraph( - ... "Hello, ", - ... text("World", bold=True) - ... ) - ... ] - >>> client.replace_json_document(document_id, content) - """ - request = ReplaceJSONDocumentRequest( - document_id=document_id, - content=content - ) - - response_data = self._make_request("replaceJSONDocument", json_data=request.model_dump()) - return ReplaceJSONDocumentResponse(**response_data) - - def append_document( - self, - document_id: str, - description: Optional[str] = None, - files: Optional[List[Any]] = None - ) -> AppendDocumentResponse: - """ - Append plain text content to an existing document. - - This method adds content to the end of the document without removing existing content. - - Args: - document_id: The document ID to append content to - description: Plain text content to append (optional) - files: Files to attach (optional) - - Returns: - AppendDocumentResponse: Empty response object on success - - Raises: - VaizSDKError: If the API request fails - - Example: - >>> client.append_document( - ... document_id="doc_id", - ... description="Additional content to add" - ... ) - """ - request = AppendDocumentRequest( - document_id=document_id, - description=description, - files=files - ) - - response_data = self._make_request("appendDocument", json_data=request.model_dump()) - return AppendDocumentResponse(**response_data) - - def append_json_document( - self, - document_id: str, - content: Union[List[DocumentNode], List[Dict[str, Any]]] - ) -> AppendJSONDocumentResponse: - """ - Append structured JSON content to an existing document. - - This method adds content to the end of the document without removing existing content. - Use document structure builder functions for type-safe content creation. - - Args: - document_id: The document ID to append content to - content: JSONContent array in document structure format - - Returns: - AppendJSONDocumentResponse: Empty response object on success - - Raises: - VaizSDKError: If the API request fails - - Example with raw JSON: - >>> content = [ - ... { - ... "type": "paragraph", - ... "content": [{"type": "text", "text": "Added content"}] - ... } - ... ] - >>> client.append_json_document(document_id, content) - - Example with helpers: - >>> from vaiz import paragraph, text - >>> content = [ - ... paragraph("Added ", text("content", bold=True)) - ... ] - >>> client.append_json_document(document_id, content) - """ - request = AppendJSONDocumentRequest( - document_id=document_id, - content=content - ) - - response_data = self._make_request("appendJSONDocument", json_data=request.model_dump()) - return AppendJSONDocumentResponse(**response_data) - def replace_markdown_document(self, document_id: str, markdown: str) -> ReplaceMarkdownDocumentResponse: """ Replace document content with Markdown content. @@ -357,5 +156,3 @@ def edit_document(self, request: EditDocumentRequest) -> EditDocumentResponse: """ response_data = self._make_request("editDocument", json_data=request.model_dump()) return EditDocumentResponse(**response_data) - - diff --git a/vaiz/api/members.py b/vaiz/api/members.py index 13ae0ec..11b1949 100644 --- a/vaiz/api/members.py +++ b/vaiz/api/members.py @@ -6,10 +6,16 @@ class MembersAPIClient(BaseAPIClient): def get_space_members(self) -> GetSpaceMembersResponse: """ Get all members in the current space. - + + Bot members (AI, automation, integration bots) are excluded from the result. + Returns: GetSpaceMembersResponse: The list of space members """ response_data = self._make_request("getSpaceMembers", method="POST", json_data={}) + members = response_data.get("payload", {}).get("members", []) + response_data["payload"]["members"] = [ + m for m in members if not str(m.get("kind", "")).endswith("Bot") + ] return GetSpaceMembersResponse(**response_data) diff --git a/vaiz/helpers/__init__.py b/vaiz/helpers/__init__.py index 47b7633..f5995f6 100644 --- a/vaiz/helpers/__init__.py +++ b/vaiz/helpers/__init__.py @@ -50,162 +50,7 @@ make_url_value, ) -from .document_structure import ( - # Document structure node builders - text, - paragraph, - heading, - list_item, - bullet_list, - ordered_list, - task_item, - task_list, - link_text, - horizontal_rule, - blockquote, - details, - details_summary, - details_content, - table, - table_row, - table_cell, - table_header, - mention, - mention_user, - mention_document, - mention_task, - mention_milestone, - image_block, - files_block, - toc_block, - anchors_block, - siblings_block, - code_block, - embed_block, - - # Document structure types - DocumentNode, - TextNode, - ParagraphNode, - HeadingNode, - BulletListNode, - OrderedListNode, - ListItemNode, - TaskListNode, - TaskItemNode, - TaskListAttrs, - TaskItemAttrs, - TableNode, - TableRowNode, - TableCellNode, - TableHeaderNode, - TableCellOrHeader, - HorizontalRuleNode, - BlockquoteNode, - DetailsNode, - DetailsSummaryNode, - DetailsContentNode, - Mark, - MentionNode, - MentionAttrs, - MentionData, - MentionItem, - ImageBlockNode, - ImageBlockAttrs, - ImageBlockData, - FilesBlockNode, - FilesBlockAttrs, - FilesBlockData, - FileItem, - DocSiblingsNode, - DocSiblingsAttrs, - DocSiblingsData, - CodeBlockNode, - CodeBlockAttrs, - EmbedBlockNode, - EmbedBlockAttrs, - EmbedBlockData, - EmbedType, -) - __all__ = [ - # Document structure builders - 'text', - 'paragraph', - 'heading', - 'list_item', - 'bullet_list', - 'ordered_list', - 'task_item', - 'task_list', - 'link_text', - 'horizontal_rule', - 'blockquote', - 'details', - 'details_summary', - 'details_content', - 'table', - 'table_row', - 'table_cell', - 'table_header', - 'mention', - 'mention_user', - 'mention_document', - 'mention_task', - 'mention_milestone', - 'image_block', - 'files_block', - 'toc_block', - 'anchors_block', - 'siblings_block', - 'code_block', - 'embed_block', - - # Document structure types - 'DocumentNode', - 'TextNode', - 'ParagraphNode', - 'HeadingNode', - 'BulletListNode', - 'OrderedListNode', - 'ListItemNode', - 'TaskListNode', - 'TaskItemNode', - 'TaskListAttrs', - 'TaskItemAttrs', - 'TableNode', - 'TableRowNode', - 'TableCellNode', - 'TableHeaderNode', - 'TableCellOrHeader', - 'HorizontalRuleNode', - 'BlockquoteNode', - 'DetailsNode', - 'DetailsSummaryNode', - 'DetailsContentNode', - 'Mark', - 'MentionNode', - 'MentionAttrs', - 'MentionData', - 'MentionItem', - 'ImageBlockNode', - 'ImageBlockAttrs', - 'ImageBlockData', - 'FilesBlockNode', - 'FilesBlockAttrs', - 'FilesBlockData', - 'FileItem', - 'DocSiblingsNode', - 'DocSiblingsAttrs', - 'DocSiblingsData', - 'CodeBlockNode', - 'CodeBlockAttrs', - 'EmbedBlockNode', - 'EmbedBlockAttrs', - 'EmbedBlockData', - 'EmbedType', - - # Custom fields (existing) # Field creation helpers 'make_text_field', 'make_number_field', diff --git a/vaiz/helpers/document_structure.py b/vaiz/helpers/document_structure.py deleted file mode 100644 index 37b4115..0000000 --- a/vaiz/helpers/document_structure.py +++ /dev/null @@ -1,1572 +0,0 @@ -""" -Helper functions for building valid Document Structure JSON content. - -This module provides type-safe builders for document nodes and marks, -ensuring only validated elements are used with the replace_json_document API. -""" - -from typing import List, Optional, Literal, Union -from typing_extensions import TypedDict, NotRequired -from enum import Enum - - -# Embed block type enum -class EmbedType(str, Enum): - """Supported embed types for embed blocks.""" - YOUTUBE = "YouTube" - FIGMA = "Figma" - VIMEO = "Vimeo" - CODESANDBOX = "CodeSandbox" - GITHUB_GIST = "GitHub Gist" - MIRO = "Miro" - IFRAME = "Iframe" - - -# Type definitions for text formatting marks -class BoldMark(TypedDict): - """Bold text formatting mark.""" - type: Literal["bold"] - - -class ItalicMark(TypedDict): - """Italic text formatting mark.""" - type: Literal["italic"] - - -class CodeMark(TypedDict): - """Inline code formatting mark.""" - type: Literal["code"] - - -class LinkMarkAttrs(TypedDict): - """Attributes for link mark.""" - href: str - target: NotRequired[str] - - -class LinkMark(TypedDict): - """Link mark with href and optional target.""" - type: Literal["link"] - attrs: LinkMarkAttrs - - -# Union type for all supported marks -Mark = Union[BoldMark, ItalicMark, CodeMark, LinkMark] - - -# Type definitions for text node -class TextNode(TypedDict): - """Text node with optional formatting marks.""" - type: Literal["text"] - text: str - marks: NotRequired[List[Mark]] - - -# Type definitions for heading attributes -class HeadingAttrs(TypedDict): - """Attributes for heading node.""" - level: Literal[1, 2, 3, 4, 5, 6] - uid: NotRequired[str] - - -# Type definitions for ordered list attributes -class OrderedListAttrs(TypedDict): - """Attributes for ordered list node.""" - start: NotRequired[int] - - -# Type definitions for task list (checklist) attributes -class TaskListAttrs(TypedDict): - """Attributes for task list node.""" - uid: NotRequired[str] - - -class TaskItemAttrs(TypedDict): - """Attributes for task item node.""" - checked: bool - - -# Type definitions for file and image blocks -class ImageBlockData(TypedDict): - """Data for image block (stored as JSON string in content).""" - id: str - src: str - thumbSrc: NotRequired[str] - fileName: str - caption: NotRequired[str] - fileType: str - extension: str - title: str - aspectRatio: NotRequired[float] - fileSize: int - dimensions: NotRequired[List[int]] - fileId: str - dominantColor: NotRequired[dict] - - -class ImageBlockAttrs(TypedDict): - """Attributes for image block.""" - uid: str - custom: Literal[1] - contenteditable: Literal["false"] - widthPercent: NotRequired[int] - - -class ImageBlockNode(TypedDict): - """Image block node for displaying images.""" - type: Literal["image-block"] - attrs: ImageBlockAttrs - content: List[TextNode] - - -class FileItem(TypedDict): - """Individual file item in files block.""" - id: str - fileId: str - createAt: int - url: str - extension: str - name: str - size: int - type: str # Pdf, Image, Video, etc. - dominantColor: NotRequired[dict] - - -class FilesBlockData(TypedDict): - """Data for files block (stored as JSON string in content).""" - files: List[FileItem] - - -class FilesBlockAttrs(TypedDict): - """Attributes for files block.""" - uid: str - custom: Literal[1] - contenteditable: Literal["false"] - - -class FilesBlockNode(TypedDict): - """Files block node for displaying file attachments.""" - type: Literal["files"] - attrs: FilesBlockAttrs - content: List[TextNode] - - -# Type definitions for mention blocks -class MentionItem(TypedDict): - """Item reference in mention block.""" - id: str - kind: Literal["User", "Document", "Task", "Milestone"] - - -class MentionData(TypedDict): - """Data for mention block.""" - item: MentionItem - - -class MentionAttrs(TypedDict): - """Attributes for mention block.""" - uid: str - custom: Literal[1] - inline: Literal[True] - data: MentionData - - -class MentionNode(TypedDict): - """Mention node for referencing users, documents, tasks, or milestones.""" - type: Literal["custom-mention"] - attrs: MentionAttrs - content: List[TextNode] - - -# Type definitions for doc-siblings block (TOC, Anchors, Siblings, etc.) -class DocSiblingsData(TypedDict): - """Data for doc-siblings block.""" - type: Literal["toc", "anchors", "siblings"] - - -class DocSiblingsAttrs(TypedDict): - """Attributes for doc-siblings block.""" - uid: str - custom: Literal[1] - contenteditable: Literal["false"] - - -class DocSiblingsNode(TypedDict): - """Doc-siblings node for special document elements like TOC, Anchors, and Siblings.""" - type: Literal["doc-siblings"] - attrs: DocSiblingsAttrs - content: List[TextNode] - - -# Type definitions for code block -class CodeBlockAttrs(TypedDict): - """Attributes for code block.""" - uid: NotRequired[str] - language: NotRequired[str] - - -class CodeBlockNode(TypedDict): - """Code block node for displaying code with syntax highlighting.""" - type: Literal["codeBlock"] - attrs: NotRequired[CodeBlockAttrs] - content: NotRequired[List[TextNode]] - - -# Type definitions for embed block -class EmbedBlockData(TypedDict): - """Data for embed block (stored as JSON string in content).""" - type: str # EmbedType value - url: str - extractedUrl: str - isContentHidden: NotRequired[bool] - - -class EmbedBlockAttrs(TypedDict): - """Attributes for embed block.""" - uid: str - custom: Literal[1] - contenteditable: Literal["false"] - size: NotRequired[Literal["small", "medium", "large"]] - isContentHidden: NotRequired[bool] - - -class EmbedBlockNode(TypedDict): - """Embed block node for displaying embedded content (YouTube, Figma, etc.).""" - type: Literal["embed"] - attrs: EmbedBlockAttrs - content: List[TextNode] - - -# Forward references for recursive types -DocumentContent = Union[TextNode, 'ParagraphNode', 'HeadingNode', 'BulletListNode', 'OrderedListNode', 'ListItemNode', 'TaskListNode', 'TaskItemNode', 'TableNode', 'HorizontalRuleNode', 'BlockquoteNode', 'DetailsNode', 'DetailsSummaryNode', 'DetailsContentNode', MentionNode, ImageBlockNode, FilesBlockNode, DocSiblingsNode, CodeBlockNode, EmbedBlockNode] - - -class ParagraphNode(TypedDict): - """Paragraph node containing text and other inline content.""" - type: Literal["paragraph"] - content: NotRequired[List[DocumentContent]] - - -class HeadingNode(TypedDict): - """Heading node with level 1-6.""" - type: Literal["heading"] - attrs: HeadingAttrs - content: NotRequired[List[DocumentContent]] - - -class ListItemNode(TypedDict): - """List item node, can contain paragraphs and nested lists.""" - type: Literal["listItem"] - content: NotRequired[List[DocumentContent]] - - -class BulletListNode(TypedDict): - """Bullet (unordered) list node.""" - type: Literal["bulletList"] - content: List[ListItemNode] - - -class OrderedListNode(TypedDict): - """Ordered (numbered) list node.""" - type: Literal["orderedList"] - attrs: NotRequired[OrderedListAttrs] - content: List[ListItemNode] - - -class TaskItemNode(TypedDict): - """Task item node for checklist items with checked status.""" - type: Literal["taskItem"] - attrs: TaskItemAttrs - content: NotRequired[List[DocumentContent]] - - -class TaskListNode(TypedDict): - """Task list (checklist) node containing task items.""" - type: Literal["taskList"] - attrs: NotRequired[TaskListAttrs] - content: List[TaskItemNode] - - -# Table-related attributes -class TableCellAttrs(TypedDict): - """Attributes for table cell.""" - colspan: NotRequired[int] - rowspan: NotRequired[int] - - -class TableRowAttrs(TypedDict): - """Attributes for table row.""" - showRowNumbers: NotRequired[bool] - - -class ExtensionTableAttrs(TypedDict): - """Attributes for extension-table.""" - uid: NotRequired[str] - showRowNumbers: NotRequired[bool] - - -# Table-related nodes -class TableCellNode(TypedDict): - """Table cell node.""" - type: Literal["tableCell"] - attrs: NotRequired[TableCellAttrs] - content: NotRequired[List[DocumentContent]] - - -class TableHeaderNode(TypedDict): - """Table header cell node (th).""" - type: Literal["tableHeader"] - attrs: NotRequired[TableCellAttrs] - content: NotRequired[List[DocumentContent]] - - -# Union type for table cells (both data and header cells) -TableCellOrHeader = Union[TableCellNode, TableHeaderNode] - - -class TableRowNode(TypedDict): - """Table row node containing cells or headers.""" - type: Literal["tableRow"] - attrs: NotRequired[TableRowAttrs] - content: List[TableCellOrHeader] - - -class TableNode(TypedDict): - """Extension table node containing rows.""" - type: Literal["extension-table"] - attrs: NotRequired[ExtensionTableAttrs] - content: List[TableRowNode] - - -# Horizontal rule node -class HorizontalRuleNode(TypedDict): - """Horizontal rule (divider line).""" - type: Literal["horizontalRule"] - - -# Blockquote node -class BlockquoteNode(TypedDict): - """Blockquote node for quoted text.""" - type: Literal["blockquote"] - content: NotRequired[List[DocumentContent]] - - -# Details (collapsible) nodes -class DetailsSummaryNode(TypedDict): - """Details summary node (always visible header).""" - type: Literal["detailsSummary"] - content: NotRequired[List[DocumentContent]] - - -class DetailsContentNode(TypedDict): - """Details content node (collapsible body).""" - type: Literal["detailsContent"] - content: NotRequired[List[DocumentContent]] - - -class DetailsNode(TypedDict): - """Details node (collapsible section with summary and content).""" - type: Literal["details"] - content: List[Union[DetailsSummaryNode, DetailsContentNode]] - - -# Main content type -DocumentNode = Union[ParagraphNode, HeadingNode, BulletListNode, OrderedListNode, ListItemNode, TaskListNode, TaskItemNode, TableNode, HorizontalRuleNode, BlockquoteNode, DetailsNode, ImageBlockNode, FilesBlockNode, MentionNode, DocSiblingsNode, CodeBlockNode, EmbedBlockNode] - - -# Helper functions - -def text(content: str, bold: bool = False, italic: bool = False, - code: bool = False, link: Optional[str] = None, - link_target: str = "_blank") -> TextNode: - """ - Create a text node with optional formatting. - - Args: - content: The text content - bold: Apply bold formatting - italic: Apply italic formatting - code: Apply inline code formatting - link: URL for link (if provided, makes text a hyperlink) - link_target: Link target attribute (default: "_blank") - - Returns: - TextNode: A valid document text node - - Example: - >>> text("Hello World", bold=True) - {'type': 'text', 'text': 'Hello World', 'marks': [{'type': 'bold'}]} - """ - # Normalize empty text to a single space to avoid server validation errors - normalized_content = " " if content == "" else content - node: TextNode = {"type": "text", "text": normalized_content} - marks: List[Mark] = [] - - if bold: - marks.append({"type": "bold"}) - if italic: - marks.append({"type": "italic"}) - if code: - marks.append({"type": "code"}) - if link: - marks.append({"type": "link", "attrs": {"href": link, "target": link_target}}) - - if marks: - node["marks"] = marks - - return node - - -def paragraph(*content: Union[TextNode, str]) -> ParagraphNode: - """ - Create a paragraph node. - - Args: - *content: Text nodes or strings (strings will be converted to text nodes) - - Returns: - ParagraphNode: A valid document paragraph node - - Example: - >>> paragraph("Hello ", text("World", bold=True)) - {'type': 'paragraph', 'content': [{'type': 'text', 'text': 'Hello '}, ...]} - """ - node: ParagraphNode = {"type": "paragraph"} - if content: - node["content"] = [ - item if isinstance(item, dict) else text(item) - for item in content - ] - return node - - -def _generate_uid() -> str: - """ - Generate a unique identifier for document elements. - Format: 12 characters, mix of uppercase, lowercase letters and digits. - Example: "sEeaN9ddIDsL" - - Returns: - str: A unique identifier string - """ - import random - import string - - # Generate random string with letters (upper + lower) and digits - chars = string.ascii_letters + string.digits # a-z, A-Z, 0-9 - uid = ''.join(random.choice(chars) for _ in range(12)) - return uid - - -def heading(level: Literal[1, 2, 3, 4, 5, 6], *content: Union[TextNode, str]) -> HeadingNode: - """ - Create a heading node with automatic UID generation for TOC support. - - Args: - level: Heading level (1-6) - *content: Text nodes or strings - - Returns: - HeadingNode: A valid document heading node with UID - - Example: - >>> heading(1, "Title") - {'type': 'heading', 'attrs': {'level': 1, 'uid': '...'}, 'content': [{'type': 'text', 'text': 'Title'}]} - """ - node: HeadingNode = { - "type": "heading", - "attrs": { - "level": level, - "uid": _generate_uid() - } - } - if content: - node["content"] = [ - item if isinstance(item, dict) else text(item) - for item in content - ] - return node - - -def list_item(*content: Union[ParagraphNode, BulletListNode, OrderedListNode]) -> ListItemNode: - """ - Create a list item node. - - Args: - *content: Paragraphs or nested lists - - Returns: - ListItemNode: A valid document list item node - - Example: - >>> list_item(paragraph("First item")) - {'type': 'listItem', 'content': [{'type': 'paragraph', ...}]} - """ - node: ListItemNode = {"type": "listItem"} - if content: - node["content"] = list(content) - return node - - -def bullet_list(*items: Union[ListItemNode, str]) -> BulletListNode: - """ - Create a bullet (unordered) list. - - Args: - *items: List item nodes or strings (strings will be wrapped in list items) - - Returns: - BulletListNode: A valid document bullet list node - - Example: - >>> bullet_list("First", "Second") - {'type': 'bulletList', 'content': [{'type': 'listItem', ...}, ...]} - """ - content: List[ListItemNode] = [] - for item in items: - if isinstance(item, str): - content.append(list_item(paragraph(item))) - else: - content.append(item) - - return {"type": "bulletList", "content": content} - - -def ordered_list(*items: Union[ListItemNode, str], start: int = 1) -> OrderedListNode: - """ - Create an ordered (numbered) list. - - Args: - *items: List item nodes or strings (strings will be wrapped in list items) - start: Starting number for the list (default: 1) - - Returns: - OrderedListNode: A valid document ordered list node - - Example: - >>> ordered_list("First", "Second", start=1) - {'type': 'orderedList', 'attrs': {'start': 1}, 'content': [...]} - """ - content: List[ListItemNode] = [] - for item in items: - if isinstance(item, str): - content.append(list_item(paragraph(item))) - else: - content.append(item) - - node: OrderedListNode = {"type": "orderedList", "content": content} - if start != 1: - node["attrs"] = {"start": start} - - return node - - -def task_item(*content: Union[ParagraphNode, TaskListNode, str], checked: bool = False) -> TaskItemNode: - """ - Create a task item node for checklists. - - Args: - *content: Paragraphs, nested task lists, or strings (strings will be wrapped in paragraphs) - checked: Whether the task item is completed (default: False) - - Returns: - TaskItemNode: A valid task item node - - Example: - >>> task_item("Buy milk", checked=False) - {'type': 'taskItem', 'attrs': {'checked': False}, 'content': [{'type': 'paragraph', ...}]} - - >>> task_item(paragraph("Review PR"), checked=True) - {'type': 'taskItem', 'attrs': {'checked': True}, 'content': [...]} - """ - node: TaskItemNode = { - "type": "taskItem", - "attrs": {"checked": checked} - } - - if content: - node["content"] = [ - item if isinstance(item, dict) else paragraph(item) - for item in content - ] - - return node - - -def task_list(*items: Union[TaskItemNode, str], checked: bool = False) -> TaskListNode: - """ - Create a task list (checklist) node. - - Args: - *items: Task item nodes or strings (strings will be wrapped in task items) - checked: Default checked state for string items (default: False) - - Returns: - TaskListNode: A valid task list node - - Example: - >>> task_list("Todo 1", "Todo 2") - {'type': 'taskList', 'attrs': {'uid': '...'}, 'content': [...]} - - >>> task_list( - ... task_item("First task", checked=True), - ... task_item("Second task", checked=False) - ... ) - {'type': 'taskList', 'content': [...]} - """ - import uuid - - content: List[TaskItemNode] = [] - for item in items: - if isinstance(item, str): - content.append(task_item(item, checked=checked)) - else: - content.append(item) - - return { - "type": "taskList", - "attrs": {"uid": str(uuid.uuid4())[:12].replace('-', '')}, - "content": content - } - - -def link_text(content: str, href: str, target: str = "_blank", - bold: bool = False, italic: bool = False) -> TextNode: - """ - Create a hyperlink text node. - - Args: - content: The link text - href: The URL - target: Link target (default: "_blank") - bold: Apply bold formatting - italic: Apply italic formatting - - Returns: - TextNode: A text node with link mark - - Example: - >>> link_text("Visit Vaiz", "https://vaiz.app") - {'type': 'text', 'text': 'Visit Vaiz', 'marks': [{'type': 'link', ...}]} - """ - return text(content, bold=bold, italic=italic, link=href, link_target=target) - - -def horizontal_rule() -> HorizontalRuleNode: - """ - Create a horizontal rule (divider line). - - Returns: - HorizontalRuleNode: A horizontal rule node - - Example: - >>> horizontal_rule() - {'type': 'horizontalRule'} - """ - return {"type": "horizontalRule"} - - -def blockquote(*content: Union[ParagraphNode, str]) -> BlockquoteNode: - """ - Create a blockquote node for quoted text. - - Args: - *content: Paragraphs or strings (strings will be wrapped in paragraphs) - - Returns: - BlockquoteNode: A valid blockquote node - - Example: - >>> blockquote("This is a quote") - {'type': 'blockquote', 'content': [{'type': 'paragraph', 'content': [{'type': 'text', 'text': 'This is a quote'}]}]} - - >>> blockquote(paragraph("First line"), paragraph("Second line")) - {'type': 'blockquote', 'content': [{'type': 'paragraph', ...}, {'type': 'paragraph', ...}]} - """ - node: BlockquoteNode = {"type": "blockquote"} - if content: - node["content"] = [ - item if isinstance(item, dict) else paragraph(item) - for item in content - ] - return node - - -def details_summary(*content: Union[TextNode, str]) -> DetailsSummaryNode: - """ - Create a details summary node (always visible header of collapsible section). - - Args: - *content: Text nodes or strings (strings will be converted to text nodes) - - Returns: - DetailsSummaryNode: A valid details summary node - - Example: - >>> details_summary("Click to expand") - {'type': 'detailsSummary', 'content': [{'type': 'text', 'text': 'Click to expand'}]} - """ - node: DetailsSummaryNode = {"type": "detailsSummary"} - if content: - node["content"] = [ - item if isinstance(item, dict) else text(item) - for item in content - ] - return node - - -def details_content(*content: Union[ParagraphNode, str]) -> DetailsContentNode: - """ - Create a details content node (collapsible body). - - Args: - *content: Paragraphs or strings (strings will be wrapped in paragraphs) - - Returns: - DetailsContentNode: A valid details content node - - Example: - >>> details_content(paragraph("Hidden content")) - {'type': 'detailsContent', 'content': [{'type': 'paragraph', ...}]} - """ - node: DetailsContentNode = {"type": "detailsContent"} - if content: - node["content"] = [ - item if isinstance(item, dict) else paragraph(item) - for item in content - ] - return node - - -def details(summary: Union[DetailsSummaryNode, str], *content: Union[DetailsContentNode, ParagraphNode, str]) -> DetailsNode: - """ - Create a collapsible details section (HTML
element). - - Args: - summary: Summary node or string (always visible header) - *content: Content nodes, paragraphs, or strings (collapsible body) - - Returns: - DetailsNode: A valid details node - - Example: - >>> details("Click to expand", paragraph("Hidden content")) - {'type': 'details', 'content': [{'type': 'detailsSummary', ...}, {'type': 'detailsContent', ...}]} - - >>> details( - ... details_summary(text("Additional Info", bold=True)), - ... details_content( - ... paragraph("Here is more information"), - ... paragraph("With multiple paragraphs") - ... ) - ... ) - """ - # Create summary node if string provided - summary_node: DetailsSummaryNode - if isinstance(summary, str): - summary_node = details_summary(summary) - else: - summary_node = summary - - # Create content node - content_items: List[DocumentContent] = [] - for item in content: - if isinstance(item, dict): - # If it's already a DetailsContentNode, use it; otherwise wrap in paragraph - if item.get("type") == "detailsContent": - content_items.append(item) - else: - content_items.append(item) - else: - # String - wrap in paragraph - content_items.append(paragraph(item)) - - # Wrap content in detailsContent if not already wrapped - if content_items and all(item.get("type") != "detailsContent" for item in content_items): - content_node = details_content(*content_items) - result_content: List[Union[DetailsSummaryNode, DetailsContentNode]] = [summary_node, content_node] - else: - result_content = [summary_node] + [item for item in content_items if item.get("type") == "detailsContent"] - - return {"type": "details", "content": result_content} - - -def table_cell(*content: Union[ParagraphNode, str], colspan: int = 1, rowspan: int = 1) -> TableCellNode: - """ - Create a table cell node. - - Args: - *content: Paragraphs or strings (strings will be wrapped in paragraphs) - colspan: Number of columns to span (default: 1) - rowspan: Number of rows to span (default: 1) - - Returns: - TableCellNode: A valid table cell node - - Example: - >>> table_cell("Cell content") - {'type': 'tableCell', 'attrs': {'colspan': 1, 'rowspan': 1}, 'content': [...]} - """ - node: TableCellNode = { - "type": "tableCell", - "attrs": {"colspan": colspan, "rowspan": rowspan} - } - if content: - node["content"] = [ - item if isinstance(item, dict) else paragraph(item) - for item in content - ] - return node - - -def table_header(*content: Union[ParagraphNode, str], colspan: int = 1, rowspan: int = 1) -> TableHeaderNode: - """ - Create a table header cell node (th). - - Args: - *content: Paragraphs or strings (strings will be wrapped in paragraphs) - colspan: Number of columns to span (default: 1) - rowspan: Number of rows to span (default: 1) - - Returns: - TableHeaderNode: A valid table header cell node - - Example: - >>> table_header("Column Name") - {'type': 'tableHeader', 'attrs': {'colspan': 1, 'rowspan': 1}, 'content': [...]} - """ - node: TableHeaderNode = { - "type": "tableHeader", - "attrs": {"colspan": colspan, "rowspan": rowspan} - } - if content: - node["content"] = [ - item if isinstance(item, dict) else paragraph(item) - for item in content - ] - return node - - -def table_row(*cells: Union[TableCellNode, TableHeaderNode, str], show_row_numbers: bool = False) -> TableRowNode: - """ - Create a table row node. - - Args: - *cells: Table cell nodes, table header nodes, or strings (strings will be wrapped in cells) - show_row_numbers: Show row numbers (default: False) - - Returns: - TableRowNode: A valid table row node - - Example: - >>> table_row("Cell 1", "Cell 2", "Cell 3") - {'type': 'tableRow', 'attrs': {'showRowNumbers': False}, 'content': [...]} - - >>> table_row(table_header("Name"), table_header("Status")) - {'type': 'tableRow', 'attrs': {'showRowNumbers': False}, 'content': []} - """ - content: List[TableCellOrHeader] = [] - for cell in cells: - if isinstance(cell, str): - content.append(table_cell(cell)) - else: - content.append(cell) - - return { - "type": "tableRow", - "attrs": {"showRowNumbers": show_row_numbers}, - "content": content - } - - -def table(*rows: TableRowNode, show_row_numbers: bool = False) -> TableNode: - """ - Create an extension-table node. - - Args: - *rows: Table row nodes - show_row_numbers: Show row numbers (default: False) - - Returns: - TableNode: A valid extension-table node - - Example: - >>> table( - ... table_row("Name", "Status"), - ... table_row("Task 1", "Done"), - ... table_row("Task 2", "In Progress") - ... ) - {'type': 'extension-table', 'attrs': {'showRowNumbers': False}, 'content': [...]} - """ - import uuid - return { - "type": "extension-table", - "attrs": { - "uid": str(uuid.uuid4())[:12].replace('-', ''), - "showRowNumbers": show_row_numbers - }, - "content": list(rows) - } - - -def mention(item_id: str, kind: Literal["User", "Document", "Task", "Milestone"]) -> MentionNode: - """ - Create a mention node for referencing users, documents, tasks, or milestones. - - Args: - item_id: The ID of the item to mention - kind: The type of item ("User", "Document", "Task", or "Milestone") - - Returns: - MentionNode: A valid mention node - - Example: - >>> mention("68fa5e14cdb30e1c96755975", "User") - {'type': 'custom-mention', 'attrs': {'uid': ..., 'custom': 1, 'inline': True, 'data': {'item': {'id': '68fa5e14cdb30e1c96755975', 'kind': 'User'}}}, 'content': [{'type': 'text', 'text': ' '}]} - - >>> mention("68fa67d262f676bcd1bc162f", "Task") - {'type': 'custom-mention', 'attrs': {..., 'data': {'item': {'id': '68fa67d262f676bcd1bc162f', 'kind': 'Task'}}}, ...} - """ - import uuid - return { - "type": "custom-mention", - "attrs": { - "uid": str(uuid.uuid4())[:12].replace('-', ''), - "custom": 1, - "inline": True, - "data": { - "item": { - "id": item_id, - "kind": kind - } - } - }, - "content": [{"type": "text", "text": " "}] - } - - -def mention_user(member_id: str) -> MentionNode: - """ - Create a mention node for a user (team member). - - Args: - member_id: The member ID to mention - - Returns: - MentionNode: A valid user mention node - - Example: - >>> mention_user("68fa5e14cdb30e1c96755975") - {'type': 'custom-mention', 'attrs': {..., 'data': {'item': {'id': '68fa5e14cdb30e1c96755975', 'kind': 'User'}}}, ...} - """ - return mention(member_id, "User") - - -def mention_document(document_id: str) -> MentionNode: - """ - Create a mention node for a document. - - Args: - document_id: The document ID to mention - - Returns: - MentionNode: A valid document mention node - - Example: - >>> mention_document("68fa6c7b62f676bcd1bcecae") - {'type': 'custom-mention', 'attrs': {..., 'data': {'item': {'id': '68fa6c7b62f676bcd1bcecae', 'kind': 'Document'}}}, ...} - """ - return mention(document_id, "Document") - - -def mention_task(task_id: str) -> MentionNode: - """ - Create a mention node for a task. - - Args: - task_id: The task ID to mention - - Returns: - MentionNode: A valid task mention node - - Example: - >>> mention_task("68fa67d262f676bcd1bc162f") - {'type': 'custom-mention', 'attrs': {..., 'data': {'item': {'id': '68fa67d262f676bcd1bc162f', 'kind': 'Task'}}}, ...} - """ - return mention(task_id, "Task") - - -def mention_milestone(milestone_id: str) -> MentionNode: - """ - Create a mention node for a milestone. - - Args: - milestone_id: The milestone ID to mention - - Returns: - MentionNode: A valid milestone mention node - - Example: - >>> mention_milestone("68fa650bcdb30e1c9677562e") - {'type': 'custom-mention', 'attrs': {..., 'data': {'item': {'id': '68fa650bcdb30e1c9677562e', 'kind': 'Milestone'}}}, ...} - """ - return mention(milestone_id, "Milestone") - - -def image_block( - file, - width_percent: int = 100, - caption: str = "" -) -> ImageBlockNode: - """ - Create an image block node from an uploaded file. - - Args: - file: UploadedFile object from client.upload_file() response (use uploaded.file) - width_percent: Width as percentage (default: 100) - caption: Optional image caption - - Returns: - ImageBlockNode: A valid image block node - - Example: - >>> # Upload image and create block in one flow - >>> uploaded = client.upload_file("path/to/image.png") - >>> - >>> # Simple usage - just pass the file object! - >>> image_block(file=uploaded.file) - >>> - >>> # With optional parameters - >>> image_block( - ... file=uploaded.file, - ... caption="Product photo", - ... width_percent=75 - ... ) - """ - import uuid - import json - import mimetypes - - # Extract data from uploaded file object - file_id = file.id - src = file.url - file_name = file.name - file_size = file.size - extension = file.ext - dimensions = file.dimension if hasattr(file, 'dimension') and file.dimension else None - dominant_color = file.dominant_color if hasattr(file, 'dominant_color') and file.dominant_color else None - - # Auto-detect MIME type from extension or use mime from file - if hasattr(file, 'mime') and file.mime: - file_type = file.mime - else: - mime_type = mimetypes.guess_type(f"file.{extension}")[0] - file_type = mime_type if mime_type else "image/png" - - # Generate unique IDs - block_uid = str(uuid.uuid4())[:12].replace('-', '') - image_id = str(uuid.uuid4())[:12].replace('-', '') - - # Build image data - image_data: ImageBlockData = { - "id": image_id, - "src": src, - "fileName": file_name, - "fileType": file_type, - "extension": extension, - "title": file_name, - "fileSize": file_size, - "fileId": file_id, - } - - # Auto-calculate aspect ratio from dimensions - if dimensions: - image_data["dimensions"] = dimensions - if len(dimensions) == 2 and dimensions[1] > 0: - image_data["aspectRatio"] = dimensions[0] / dimensions[1] - - if caption: - image_data["caption"] = caption - - if dominant_color: - image_data["dominantColor"] = dominant_color - - # Create image block node - return { - "type": "image-block", - "attrs": { - "uid": block_uid, - "custom": 1, - "contenteditable": "false", - "widthPercent": width_percent - }, - "content": [ - {"type": "text", "text": json.dumps(image_data, ensure_ascii=False)} - ] - } - - -def files_block(*file_items: dict) -> FilesBlockNode: - """ - Create a files block node with one or more file attachments. - - Args: - *file_items: File item dictionaries with file metadata - - Returns: - FilesBlockNode: A valid files block node - - Example: - >>> # First, upload files - >>> uploaded1 = client.upload_file("document.pdf") - >>> uploaded2 = client.upload_file("image.png") - >>> - >>> # Create file items - >>> file1 = { - ... "fileId": uploaded1.file.id, - ... "url": uploaded1.file.url, - ... "name": "document.pdf", - ... "size": uploaded1.file.size, - ... "extension": "pdf", - ... "type": "Pdf" - ... } - >>> - >>> file2 = { - ... "fileId": uploaded2.file.id, - ... "url": uploaded2.file.url, - ... "name": "image.png", - ... "size": uploaded2.file.size, - ... "extension": "png", - ... "type": "Image" - ... } - >>> - >>> # Create files block - >>> files_block(file1, file2) - """ - import uuid - import json - import time - - block_uid = str(uuid.uuid4())[:12].replace('-', '') - current_timestamp = int(time.time() * 1000) - - # Build file items list - files_list = [] - for item in file_items: - file_item: FileItem = { - "id": str(uuid.uuid4())[:12].replace('-', ''), - "fileId": item["fileId"], - "createAt": current_timestamp, - "url": item["url"], - "extension": item["extension"], - "name": item["name"], - "size": item["size"], - "type": item["type"], - } - - if "dominantColor" in item: - file_item["dominantColor"] = item["dominantColor"] - - files_list.append(file_item) - - # Create files block node - files_data: FilesBlockData = {"files": files_list} - - return { - "type": "files", - "attrs": { - "uid": block_uid, - "custom": 1, - "contenteditable": "false" - }, - "content": [ - {"type": "text", "text": json.dumps(files_data, ensure_ascii=False)} - ] - } - - -def toc_block() -> DocSiblingsNode: - """ - Create a Table of Contents (TOC) block that automatically generates document outline. - - Returns: - DocSiblingsNode: A valid TOC block node - - Example: - >>> from vaiz import toc_block, heading, paragraph - >>> content = [ - ... toc_block(), - ... heading(1, "Introduction"), - ... paragraph("Welcome to our document"), - ... heading(2, "Getting Started"), - ... paragraph("First steps...") - ... ] - >>> client.replace_json_document(document_id, content) - """ - import uuid - import json - - block_uid = str(uuid.uuid4())[:12].replace('-', '') - - toc_data: DocSiblingsData = {"type": "toc"} - - return { - "type": "doc-siblings", - "attrs": { - "uid": block_uid, - "custom": 1, - "contenteditable": "false" - }, - "content": [ - {"type": "text", "text": json.dumps(toc_data, ensure_ascii=False)} - ] - } - - -def anchors_block() -> DocSiblingsNode: - """ - Create an Anchors block that displays related documents and backlinks. - - This block automatically shows: - - Documents linked to this document - - Documents that link to this document (backlinks) - - Related documents from the same space - - Returns: - DocSiblingsNode: A valid Anchors block node - - Example: - >>> from vaiz import anchors_block, toc_block, heading, paragraph - >>> content = [ - ... toc_block(), - ... anchors_block(), - ... heading(1, "Main Content"), - ... paragraph("Document with TOC and related links") - ... ] - >>> client.replace_json_document(document_id, content) - """ - import uuid - import json - - block_uid = str(uuid.uuid4())[:12].replace('-', '') - - anchors_data: DocSiblingsData = {"type": "anchors"} - - return { - "type": "doc-siblings", - "attrs": { - "uid": block_uid, - "custom": 1, - "contenteditable": "false" - }, - "content": [ - {"type": "text", "text": json.dumps(anchors_data, ensure_ascii=False)} - ] - } - - -def siblings_block() -> DocSiblingsNode: - """ - Create a Siblings block for Previous/Next document navigation. - - This block provides navigation between documents in the same branch/sequence: - - Shows Previous and Next documents in the sequence - - Typically placed at the bottom of the page - - Perfect for tutorial series, guides, and documentation chapters - - Creates "Back" and "Forward" navigation buttons - - Returns: - DocSiblingsNode: A valid Siblings block node - - Example: - >>> from vaiz import siblings_block, heading, paragraph - >>> content = [ - ... heading(1, "Tutorial Part 2"), - ... paragraph("Main content here..."), - ... siblings_block() # Navigation at the bottom - ... ] - >>> client.replace_json_document(document_id, content) - """ - import uuid - import json - - block_uid = str(uuid.uuid4())[:12].replace('-', '') - - siblings_data: DocSiblingsData = {"type": "siblings"} - - return { - "type": "doc-siblings", - "attrs": { - "uid": block_uid, - "custom": 1, - "contenteditable": "false" - }, - "content": [ - {"type": "text", "text": json.dumps(siblings_data, ensure_ascii=False)} - ] - } - - -def code_block(code: str = "", language: str = "") -> CodeBlockNode: - """ - Create a code block for displaying code with syntax highlighting. - - Args: - code: The code content to display - language: Programming language for syntax highlighting (e.g., "python", "javascript", "typescript", etc.) - - Returns: - CodeBlockNode: A valid code block node - - Example: - >>> from vaiz import code_block, heading, paragraph - >>> content = [ - ... heading(1, "Code Example"), - ... paragraph("Here's a Python function:"), - ... code_block( - ... code='def hello():\\n print("Hello, World!")', - ... language="python" - ... ) - ... ] - >>> client.replace_json_document(document_id, content) - - >>> # Or create an empty code block - >>> empty_code = code_block() - """ - import uuid - - block_uid = str(uuid.uuid4())[:12].replace('-', '') - - node: CodeBlockNode = { - "type": "codeBlock", - } - - # Add attrs if we have uid or language - attrs: CodeBlockAttrs = {"uid": block_uid} - if language: - attrs["language"] = language - node["attrs"] = attrs - - # Add content if code is provided - if code: - node["content"] = [{"type": "text", "text": code}] - - return node - - -def _extract_embed_url(url: str, embed_type: EmbedType) -> str: - """ - Extract the proper embed URL for different platforms. - - Args: - url: Original URL - embed_type: Type of embed - - Returns: - str: Extracted embed URL - """ - import re - - if embed_type == EmbedType.YOUTUBE: - # Convert YouTube watch URL to embed URL - # https://www.youtube.com/watch?v=VIDEO_ID -> https://www.youtube.com/embed/VIDEO_ID - match = re.search(r'(?:youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]+)', url) - if match: - video_id = match.group(1) - return f"https://www.youtube.com/embed/{video_id}" - - elif embed_type == EmbedType.FIGMA: - # Convert Figma design/file URL to embed format - # https://www.figma.com/design/FILE_ID/... -> - # https://www.figma.com/embed?embed_host=share&url=https://www.figma.com/file/FILE_ID/... - # Note: 'design' should be changed to 'file' in the embedded URL - figma_url = url.replace('/design/', '/file/') - return f"https://www.figma.com/embed?embed_host=share&url={figma_url}" - - elif embed_type == EmbedType.GITHUB_GIST: - # Convert GitHub Gist URL to data URI with script tag - # https://gist.github.com/user/gist_id -> data:text/html with script tag - return f"""data:text/html;charset=utf-8, - - - """ - - # For other types, return the original URL - return url - - -def embed_block( - url: str = "", - embed_type: Optional[EmbedType] = None, - size: Literal["small", "medium", "large"] = "medium", - is_content_hidden: bool = False -) -> EmbedBlockNode: - """ - Create an embed block for displaying embedded content (YouTube, Figma, etc.). - - Args: - url: URL of the content to embed - embed_type: Type of embed (EmbedType enum). Default is EmbedType.IFRAME - size: Display size ("small", "medium", "large"), default is "medium" - is_content_hidden: Whether to hide the content by default (used for Figma, Miro), default is False - - Returns: - EmbedBlockNode: A valid embed block node - - Example: - >>> from vaiz import embed_block, EmbedType, heading, paragraph - >>> - >>> # YouTube video - >>> content = [ - ... heading(1, "Project Demo"), - ... paragraph("Check out our demo video:"), - ... embed_block( - ... url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", - ... embed_type=EmbedType.YOUTUBE - ... ) - ... ] - >>> client.replace_json_document(document_id, content) - - >>> # Figma design with content hidden - >>> figma_embed = embed_block( - ... url="https://www.figma.com/file/...", - ... embed_type=EmbedType.FIGMA, - ... size="large", - ... is_content_hidden=True - ... ) - - >>> # CodeSandbox - >>> codesandbox = embed_block( - ... url="https://codesandbox.io/s/...", - ... embed_type=EmbedType.CODESANDBOX - ... ) - - >>> # Generic iframe embed (default type) - >>> iframe = embed_block(url="https://example.com/embed") - """ - import json - - # Default to Iframe if not specified - if embed_type is None: - embed_type = EmbedType.IFRAME - - # Generate unique ID (same format as heading UIDs) - block_uid = _generate_uid() - - # Extract proper embed URL - extracted_url = _extract_embed_url(url, embed_type) - - # Build embed data - embed_data: EmbedBlockData = { - "type": embed_type.value, - "url": url, - "extractedUrl": extracted_url - } - - # Add isContentHidden for Figma and Miro - # Figma always has isContentHidden=True in data (UI behavior) - if embed_type == EmbedType.FIGMA: - embed_data["isContentHidden"] = True - elif embed_type == EmbedType.MIRO and is_content_hidden: - embed_data["isContentHidden"] = True - - # Build node - node: EmbedBlockNode = { - "type": "embed", - "attrs": { - "uid": block_uid, - "custom": 1, - "contenteditable": "false", - "size": size, - "isContentHidden": is_content_hidden - }, - "content": [ - { - "type": "text", - "text": json.dumps(embed_data, ensure_ascii=False, separators=(',', ':')) - } - ] - } - - return node - - -# Convenience exports -__all__ = [ - # Types - 'DocumentNode', - 'TextNode', - 'ParagraphNode', - 'HeadingNode', - 'BulletListNode', - 'OrderedListNode', - 'ListItemNode', - 'TaskListNode', - 'TaskItemNode', - 'TaskListAttrs', - 'TaskItemAttrs', - 'TableNode', - 'TableRowNode', - 'TableCellNode', - 'TableHeaderNode', - 'TableCellOrHeader', - 'HorizontalRuleNode', - 'BlockquoteNode', - 'DetailsNode', - 'DetailsSummaryNode', - 'DetailsContentNode', - 'Mark', - 'MentionNode', - 'MentionAttrs', - 'MentionData', - 'MentionItem', - 'ImageBlockNode', - 'ImageBlockAttrs', - 'ImageBlockData', - 'FilesBlockNode', - 'FilesBlockAttrs', - 'FilesBlockData', - 'FileItem', - 'DocSiblingsNode', - 'DocSiblingsAttrs', - 'DocSiblingsData', - 'CodeBlockNode', - 'CodeBlockAttrs', - 'EmbedBlockNode', - 'EmbedBlockAttrs', - 'EmbedBlockData', - 'EmbedType', - - # Builders - 'text', - 'paragraph', - 'heading', - 'list_item', - 'bullet_list', - 'ordered_list', - 'task_item', - 'task_list', - 'link_text', - 'horizontal_rule', - 'blockquote', - 'details', - 'details_summary', - 'details_content', - 'table', - 'table_row', - 'table_cell', - 'table_header', - 'mention', - 'mention_user', - 'mention_document', - 'mention_task', - 'mention_milestone', - 'image_block', - 'files_block', - 'toc_block', - 'anchors_block', - 'siblings_block', - 'code_block', - 'embed_block', -] - diff --git a/vaiz/models/__init__.py b/vaiz/models/__init__.py index a374e43..142ad13 100644 --- a/vaiz/models/__init__.py +++ b/vaiz/models/__init__.py @@ -5,7 +5,7 @@ from .projects import Project, ProjectsResponse, ProjectResponse from .milestones import Milestone, MilestonesResponse, CreateMilestoneRequest, CreateMilestoneResponse, GetMilestoneResponse, EditMilestoneRequest, EditMilestoneResponse, ToggleMilestoneRequest, ToggleMilestoneResponse from .upload import UploadedFile, UploadFileResponse -from .documents import GetDocumentRequest, ReplaceDocumentRequest, ReplaceDocumentResponse, ReplaceJSONDocumentRequest, ReplaceJSONDocumentResponse, AppendDocumentRequest, AppendDocumentResponse, AppendJSONDocumentRequest, AppendJSONDocumentResponse, ReplaceMarkdownDocumentRequest, ReplaceMarkdownDocumentResponse, AppendMarkdownDocumentRequest, AppendMarkdownDocumentResponse, GetMarkdownDocumentRequest, GetMarkdownDocumentPayload, GetMarkdownDocumentResponse, Document, GetDocumentsRequest, GetDocumentsResponse, GetDocumentsPayload, CreateDocumentRequest, CreateDocumentResponse, CreateDocumentPayload, EditDocumentRequest, EditDocumentResponse, EditDocument, EditDocumentPayload +from .documents import ReplaceMarkdownDocumentRequest, ReplaceMarkdownDocumentResponse, AppendMarkdownDocumentRequest, AppendMarkdownDocumentResponse, GetMarkdownDocumentRequest, GetMarkdownDocumentPayload, GetMarkdownDocumentResponse, Document, GetDocumentsRequest, GetDocumentsResponse, GetDocumentsPayload, CreateDocumentRequest, CreateDocumentResponse, CreateDocumentPayload, EditDocumentRequest, EditDocumentResponse, EditDocument, EditDocumentPayload from .comments import Comment, CommentReaction, PostCommentRequest, PostCommentResponse, ReactToCommentRequest, ReactToCommentResponse, GetCommentsRequest, GetCommentsResponse, EditCommentRequest, EditCommentResponse, DeleteCommentRequest, DeleteCommentResponse from .spaces import Space, GetSpaceRequest, GetSpaceResponse, GetSpacePayload from .members import Member, GetSpaceMembersResponse, GetSpaceMembersPayload @@ -84,15 +84,6 @@ # Document models 'Document', - 'GetDocumentRequest', - 'ReplaceDocumentRequest', - 'ReplaceDocumentResponse', - 'ReplaceJSONDocumentRequest', - 'ReplaceJSONDocumentResponse', - 'AppendDocumentRequest', - 'AppendDocumentResponse', - 'AppendJSONDocumentRequest', - 'AppendJSONDocumentResponse', 'ReplaceMarkdownDocumentRequest', 'ReplaceMarkdownDocumentResponse', 'AppendMarkdownDocumentRequest', diff --git a/vaiz/models/comments.py b/vaiz/models/comments.py index 40b02c7..b4197e6 100644 --- a/vaiz/models/comments.py +++ b/vaiz/models/comments.py @@ -23,6 +23,7 @@ class Comment(VaizBaseModel): document_id: str = Field(..., alias="documentId") author_id: str = Field(..., alias="authorId") content: str + content_version: Optional[int] = Field(default=None, alias="contentVersion") files: List[UploadedFile] = [] reactions: List["CommentReaction"] = [] reply_to: Optional[str] = Field(default=None, alias="replyTo") @@ -36,9 +37,14 @@ class Comment(VaizBaseModel): class PostCommentRequest(BaseModel): - """Request model for creating a comment.""" + """Request model for creating a comment. - content: str + Either `content` (HTML, legacy) or `markdown` must be provided. + Markdown is converted to rich comment content on the server. + """ + + content: Optional[str] = None + markdown: Optional[str] = None file_ids: List[str] = Field(default_factory=list, alias="fileIds") document_id: str = Field(..., alias="documentId") reply_to: Optional[str] = Field(default=None, alias="replyTo") @@ -95,9 +101,14 @@ def reactions(self) -> List[CommentReaction]: class GetCommentsRequest(BaseModel): - """Request model for getting comments.""" + """Request model for getting comments. + + The SDK always requests markdown: Lexical comments (`content_version == 2`) + are returned with `content` as markdown; legacy comments fall back to raw HTML. + """ document_id: str = Field(..., alias="documentId") + format: Optional[str] = None model_config = ConfigDict(populate_by_name=True) @@ -120,9 +131,14 @@ def comments(self) -> List[Comment]: class EditCommentRequest(BaseModel): - """Request model for editing a comment.""" + """Request model for editing a comment. - content: str + Either `content` (HTML, legacy) or `markdown` may be provided. + Markdown is converted to rich comment content on the server. + """ + + content: Optional[str] = None + markdown: Optional[str] = None comment_id: str = Field(..., alias="commentId") add_file_ids: List[str] = Field(default_factory=list, alias="addFileIds") order_file_ids: List[str] = Field(default_factory=list, alias="orderFileIds") diff --git a/vaiz/models/documents.py b/vaiz/models/documents.py index e51acd2..30e87c1 100644 --- a/vaiz/models/documents.py +++ b/vaiz/models/documents.py @@ -5,86 +5,6 @@ from .enums import Kind -class GetDocumentRequest(BaseModel): - """Request model for fetching a JSON document by its ID.""" - document_id: str = Field(..., alias="documentId") - - model_config = ConfigDict(populate_by_name=True) - - def model_dump(self, **kwargs): # type: ignore[override] - data = super().model_dump(by_alias=True, **kwargs) - return {k: v for k, v in data.items() if v is not None} - - -class ReplaceDocumentRequest(BaseModel): - """Request model for replacing document content.""" - document_id: str = Field(..., alias="documentId") - description: str - - model_config = ConfigDict(populate_by_name=True) - - def model_dump(self, **kwargs): # type: ignore[override] - data = super().model_dump(by_alias=True, **kwargs) - return {k: v for k, v in data.items() if v is not None} - - -class ReplaceDocumentResponse(BaseModel): - """Response model for document replacement - returns empty object on success.""" - pass - - -class ReplaceJSONDocumentRequest(BaseModel): - """Request model for replacing document content with JSON content.""" - document_id: str = Field(..., alias="documentId") - content: List[Dict[str, Any]] = Field(..., description="JSONContent array in document structure format") - - model_config = ConfigDict(populate_by_name=True) - - def model_dump(self, **kwargs): # type: ignore[override] - data = super().model_dump(by_alias=True, **kwargs) - return {k: v for k, v in data.items() if v is not None} - - -class ReplaceJSONDocumentResponse(BaseModel): - """Response model for JSON document replacement - returns empty object on success.""" - pass - - -class AppendDocumentRequest(BaseModel): - """Request model for appending plain text content to a document.""" - document_id: str = Field(..., alias="documentId") - description: Optional[str] = None - files: Optional[List[Any]] = Field(default=None, description="Optional array of IFile") - - model_config = ConfigDict(populate_by_name=True) - - def model_dump(self, **kwargs): # type: ignore[override] - data = super().model_dump(by_alias=True, **kwargs) - return {k: v for k, v in data.items() if v is not None} - - -class AppendDocumentResponse(BaseModel): - """Response model for document append - returns empty object on success.""" - pass - - -class AppendJSONDocumentRequest(BaseModel): - """Request model for appending JSON content to a document.""" - document_id: str = Field(..., alias="documentId") - content: List[Dict[str, Any]] = Field(..., description="JSONContent array in document structure format") - - model_config = ConfigDict(populate_by_name=True) - - def model_dump(self, **kwargs): # type: ignore[override] - data = super().model_dump(by_alias=True, **kwargs) - return {k: v for k, v in data.items() if v is not None} - - -class AppendJSONDocumentResponse(BaseModel): - """Response model for JSON document append - returns empty object on success.""" - pass - - class ReplaceMarkdownDocumentRequest(BaseModel): """Request model for replacing document content with Markdown content.""" document_id: str = Field(..., alias="documentId") diff --git a/vaiz/models/enums.py b/vaiz/models/enums.py index aa62133..c8f7d2b 100644 --- a/vaiz/models/enums.py +++ b/vaiz/models/enums.py @@ -78,6 +78,7 @@ class Icon(str, Enum): Snow = 'Snow' Fire = 'Fire' Drop = 'Drop' + Tree = 'Tree' DoctorsBag = 'DoctorsBag' Hospital = 'Hospital' MedicalDoctor = 'MedicalDoctor' diff --git a/vaiz/models/tasks.py b/vaiz/models/tasks.py index 795c6b4..cc3d539 100644 --- a/vaiz/models/tasks.py +++ b/vaiz/models/tasks.py @@ -1,8 +1,8 @@ -from pydantic import BaseModel, Field, ConfigDict +from pydantic import BaseModel, Field, ConfigDict, field_validator from typing import Dict, Any, List, Optional, TYPE_CHECKING from datetime import datetime from .base import TaskPriority, CustomField, VaizBaseModel -from .documents import ReplaceDocumentResponse +from .documents import ReplaceMarkdownDocumentResponse from .enums import UploadFileType, Kind if TYPE_CHECKING: @@ -45,6 +45,12 @@ class Task(VaizBaseModel): types: List[str] = [] priority: TaskPriority hrid: str + + @field_validator("types", mode="before") + @classmethod + def _none_types_to_empty_list(cls, v): + """API may return null for tasks without types.""" + return v if v is not None else [] followers: Dict[str, str] archiver: Optional[str] = None completed: bool @@ -69,35 +75,34 @@ class Task(VaizBaseModel): model_config = ConfigDict(populate_by_name=True) - def get_task_description(self, client: 'VaizClient') -> Dict[str, Any]: - """Convenience method to fetch this task's description document body. + def get_task_description(self, client: 'VaizClient') -> str: + """Fetch this task's description as a Markdown string. Args: client (VaizClient): An initialized Vaiz client instance Returns: - Dict[str, Any]: Parsed JSON document body for this task's description + str: The task description rendered as Markdown """ - return client.get_json_document(self.document) + return client.get_markdown_document(self.document) def update_task_description( self, client: 'VaizClient', - description: str, - ) -> ReplaceDocumentResponse: - """Replace this task's description content. + markdown: str, + ) -> ReplaceMarkdownDocumentResponse: + """Replace this task's description with Markdown content. - Uses the document API to completely replace the description content for - the document associated with this task. + Markdown is converted to native document blocks on the server. Args: client (VaizClient): An initialized Vaiz client instance - description (str): New description content as plain text + markdown (str): New description content as a Markdown string Returns: - ReplaceDocumentResponse: Response object from the replace operation + ReplaceMarkdownDocumentResponse: Response object from the replace operation """ - return client.replace_document(self.document, description) + return client.replace_markdown_document(self.document, markdown) class TaskResponse(BaseModel):