Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions intro/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ slug: changelog

Listed below are all the changes to the SeaTable API. Each date corresponds to a new version of SeaTable Server Enterprise Edition. If you’re looking for changes beyond the API, see the SeaTable [Changelog](https://seatable.com/changelog) or check out the [SeaTable Blog](https://seatable.com/blog) for detailed release notes.

## Version 7.0

> 📘 New requests
>
> **Account Operations - Team Admin**
>
> - [List Managed Apps](/reference/listmanagedapps) <span class="APIMethod APIMethod_fixedWidth APIMethod_get">get</span>
> - [List Usable Apps](/reference/listusableapps) <span class="APIMethod APIMethod_fixedWidth APIMethod_get">get</span>
> - [List Bases Shared to User](/reference/listbasessharedtouser-1) <span class="APIMethod APIMethod_fixedWidth APIMethod_get">get</span>

## Version 6.2 (21.07.2026)

> 🚧 Breaking changes
Expand Down
125 changes: 125 additions & 0 deletions team_admin_account_operations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ paths:
type: string
last_login:
type: string
nullable: true
self_usage:
type: integer
quota:
Expand Down Expand Up @@ -715,6 +716,88 @@ paths:
example:
success: true

/api/v2.1/org/{org_id}/admin/users/{user_id}/managed-external-apps/:
get:
tags:
- Users
summary: List Managed Apps
operationId: listManagedApps
description: >-
List all the universal apps a certain user manages, i.e. the apps in
which the user has the `admin` role. Inactive apps and apps of deleted
bases are not included.
security:
- AccountTokenAuth: []
parameters:
- $ref: "#/components/parameters/org_id"
- $ref: "#/components/parameters/user_id"
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
example:
managed_apps:
- app_user_id: 12
app_id: 4
app_name: Customer Portal
app_uuid: 25ed3722-7988-4389-8095-9a46373cd8a5
app_config: '{"app_type": "universal-app", "app_name": "Customer Portal", "settings": {"pages": [], "navigation": []}}'
role_name: admin
permission: rw
dtable_uuid: c56624fd757f4f4bae7de76235aeb1f0
dtable_name: CRM
workspace_id: 2
link: https://cloud.seatable.io/external-apps/25ed3722-7988-4389-8095-9a46373cd8a5/
edit_link: https://cloud.seatable.io/dtable/external-apps-edit/25ed3722-7988-4389-8095-9a46373cd8a5/
joined_at: '2025-11-28T14:16:30+01:00'
count: 1

/api/v2.1/org/{org_id}/admin/users/{user_id}/can-use-external-apps/:
get:
tags:
- Users
summary: List Usable Apps
operationId: listUsableApps
Comment on lines +764 to +765

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Naming?

description: >-
List all the universal apps a certain user can use, i.e. the apps in
which the user has a role other than `admin`. Inactive apps and apps
of deleted bases are not included.
security:
- AccountTokenAuth: []
parameters:
- $ref: "#/components/parameters/org_id"
- $ref: "#/components/parameters/user_id"
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
example:
can_use_apps:
- app_user_id: 13
app_id: 4
app_name: Customer Portal
app_uuid: 25ed3722-7988-4389-8095-9a46373cd8a5
app_config: '{"app_type": "universal-app", "app_name": "Customer Portal", "settings": {"pages": [], "navigation": []}}'
role_name: default
permission: rw
dtable_uuid: c56624fd757f4f4bae7de76235aeb1f0
dtable_name: CRM
workspace_id: 2
link: https://cloud.seatable.io/external-apps/25ed3722-7988-4389-8095-9a46373cd8a5/
edit_link: https://cloud.seatable.io/dtable/external-apps-edit/25ed3722-7988-4389-8095-9a46373cd8a5/
joined_at: '2025-11-28T14:16:30+01:00'
count: 1

# Bases
/api/v2.1/org/{org_id}/admin/dtables/:
get:
Expand Down Expand Up @@ -837,6 +920,48 @@ paths:
owner_deleted: false
rows_count: 0
count: 2
/api/v2.1/org/{org_id}/admin/users/{user_id}/shared-dtables/:
get:
tags:
- Bases
summary: List Bases Shared to User
operationId: listBasesSharedToUser
description: List all the bases shared to a certain user by the user's ID.
security:
- AccountTokenAuth: []
parameters:
- $ref: "#/components/parameters/org_id"
- $ref: "#/components/parameters/user_id"
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
example:
dtable_list:
- id: 8
workspace_id: 2
uuid: 3a6e34b7-c9e1-4c2f-8b0e-2f5d3f21c7a4
name: CRM
created_at: '2025-11-28T14:16:30+01:00'
updated_at: '2025-11-28T14:17:04+01:00'
color: "#E91E63"
text_color: null
icon: icon-user-interview
is_encrypted: false
in_storage: true
backend: js
creator: org-1-admin
modifier: org-1-admin
file_size: 1389
rows_count: 7
from_user: e38487b4357e40c8b1359223ba87a1af@auth.local
from_user_name: org-1-admin
count: 1
/api/v2.1/org/{org_id}/admin/dtables/{base_uuid}/:
get:
tags:
Expand Down
20 changes: 20 additions & 0 deletions tests/__snapshots__/test_team_apps/test_listManagedApps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"count": 1,
"managed_apps": [
{
"app_config": "{\"app_type\": \"universal-app\", \"app_name\": \"Test App\", \"settings\": {\"pages\": [], \"navigation\": []}}",
"app_id": "int",
"app_name": "Test App",
"app_user_id": "int",
"app_uuid": "str",
"dtable_name": "ManagedAppsTest",
"dtable_uuid": "str",
"edit_link": "str",
"joined_at": "str",
"link": "str",
"permission": "rw",
"role_name": "admin",
"workspace_id": "int"
}
]
}
20 changes: 20 additions & 0 deletions tests/__snapshots__/test_team_apps/test_listUsableApps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"can_use_apps": [
{
"app_config": "{\"app_type\": \"universal-app\", \"app_name\": \"Test App\", \"settings\": {\"pages\": [], \"navigation\": []}}",
"app_id": "int",
"app_name": "Test App",
"app_user_id": "int",
"app_uuid": "str",
"dtable_name": "UsableAppsTest",
"dtable_uuid": "str",
"edit_link": "str",
"joined_at": "str",
"link": "str",
"permission": "rw",
"role_name": "default",
"workspace_id": "int"
}
],
"count": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"count": 1,
"dtable_list": [
{
"backend": "js",
"color": null,
"created_at": "str",
"creator": "str",
"file_size": "int",
"from_user": "str",
"from_user_name": "str",
"icon": null,
"id": "int",
"in_storage": true,
"is_encrypted": false,
"modifier": "str",
"name": "SharedBasesTest",
"rows_count": 0,
"text_color": null,
"updated_at": "str",
"uuid": "str",
"workspace_id": "int"
}
]
}
161 changes: 161 additions & 0 deletions tests/test_team_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import json
import pytest
import requests
from conftest import (
BASE_URL, Secret, TeamAdmin, system_admin_account_operations, team_admin_account_operations,
user_account_operations,
)
from schemathesis import Case
from syrupy.assertion import SnapshotAssertion
from syrupy.matchers import path_type

pytestmark = pytest.mark.needs_large_license


def test_listManagedApps(team: TeamAdmin, system_admin_account_token: Secret, snapshot_json: SnapshotAssertion):
"""Create a universal app, make the team admin its admin and verify it appears in listManagedApps."""
headers = {'Authorization': f'Bearer {team.account_token}'}
path_parameters = {'org_id': team.team_id}

# Custom app roles require the "advanced customization" permission, which the default team role lacks
case: Case = system_admin_account_operations.find_operation_by_id('updateTeam') \
.Case(path_parameters=path_parameters, body={'role': 'org_enterprise'})
response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'})
assert response.status_code == 200

# The team admin will be the app's admin, so get their own user ID
case: Case = user_account_operations.find_operation_by_id('getAccountInfo').Case()
response = case.call(headers=headers)
assert response.status_code == 200
user_id = response.json()['email']

# Create a base in the team admin's personal workspace
case: Case = user_account_operations.find_operation_by_id('listWorkspaces').Case()
response = case.call(headers=headers)
assert response.status_code == 200
ws_id = next(w for w in response.json()['workspace_list'] if w.get('type') == 'personal')['id']

body = {'workspace_id': ws_id, 'name': 'ManagedAppsTest'}
case: Case = user_account_operations.find_operation_by_id('createBase').Case(body=body)
response = case.call(headers=headers)
assert response.status_code == 201
base_uuid = response.json()['table']['uuid']

# Create a universal app, an "admin" role and add the team admin with that role (endpoints not in the spec)
app_config = json.dumps({'app_type': 'universal-app', 'app_name': 'Test App', 'settings': {'pages': [], 'navigation': []}})
response = requests.post(
f'{BASE_URL}/api/v2.1/workspace/{ws_id}/dtable/ManagedAppsTest/external-apps/',
headers=headers, data={'app_type': 'universal-app', 'app_config': app_config},
)
assert response.status_code == 201
app_uuid = response.json()['external_app']['app_uuid']

response = requests.post(
f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-roles/',
headers=headers, data={'role_name': 'admin', 'permission': 'rw'},
)
assert response.status_code == 200
role_id = response.json()['app_role']['id']

response = requests.post(
f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-users/',
headers=headers, data={'app_user': user_id, 'app_role_id': role_id},
)
assert response.status_code == 200

# List the user's managed apps
case: Case = team_admin_account_operations.find_operation_by_id('listManagedApps') \
.Case(path_parameters={'org_id': team.team_id, 'user_id': user_id})
response = case.call(headers=headers)

assert response.status_code == 200

data = response.json()
assert data['count'] == 1
assert len(data['managed_apps']) == 1

managed_app = data['managed_apps'][0]
assert managed_app['app_uuid'] == app_uuid
assert managed_app['dtable_uuid'] == base_uuid.replace('-', '')

matcher = path_type({
r"managed_apps\.\d+\.app_user_id": (int,),
r"managed_apps\.\d+\.app_id": (int,),
r"managed_apps\.\d+\.app_uuid": (str,),
r"managed_apps\.\d+\.dtable_uuid": (str,),
r"managed_apps\.\d+\.workspace_id": (int,),
r"managed_apps\.\d+\.link": (str,),
r"managed_apps\.\d+\.edit_link": (str,),
r"managed_apps\.\d+\.joined_at": (str,),
}, regex=True)

assert snapshot_json(matcher=matcher) == data


def test_listUsableApps(team: TeamAdmin, snapshot_json: SnapshotAssertion):
"""Create a universal app, add the team admin with the default role and verify it appears in listUsableApps."""
headers = {'Authorization': f'Bearer {team.account_token}'}

case: Case = user_account_operations.find_operation_by_id('getAccountInfo').Case()
response = case.call(headers=headers)
assert response.status_code == 200
user_id = response.json()['email']

# Create a base in the team admin's personal workspace
case: Case = user_account_operations.find_operation_by_id('listWorkspaces').Case()
response = case.call(headers=headers)
assert response.status_code == 200
ws_id = next(w for w in response.json()['workspace_list'] if w.get('type') == 'personal')['id']

body = {'workspace_id': ws_id, 'name': 'UsableAppsTest'}
case: Case = user_account_operations.find_operation_by_id('createBase').Case(body=body)
response = case.call(headers=headers)
assert response.status_code == 201
base_uuid = response.json()['table']['uuid']

# Create a universal app and add the team admin with its auto-created "default" role (endpoints not in the spec)
app_config = json.dumps({'app_type': 'universal-app', 'app_name': 'Test App', 'settings': {'pages': [], 'navigation': []}})
response = requests.post(
f'{BASE_URL}/api/v2.1/workspace/{ws_id}/dtable/UsableAppsTest/external-apps/',
headers=headers, data={'app_type': 'universal-app', 'app_config': app_config},
)
assert response.status_code == 201
app_uuid = response.json()['external_app']['app_uuid']

response = requests.get(f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-roles/', headers=headers)
assert response.status_code == 200
role_id = next(r['id'] for r in response.json()['app_roles'] if r['role_name'] == 'default')

response = requests.post(
f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-users/',
headers=headers, data={'app_user': user_id, 'app_role_id': role_id},
)
assert response.status_code == 200

# List the apps the user can use
case: Case = team_admin_account_operations.find_operation_by_id('listUsableApps') \
.Case(path_parameters={'org_id': team.team_id, 'user_id': user_id})
response = case.call(headers=headers)

assert response.status_code == 200

data = response.json()
assert data['count'] == 1
assert len(data['can_use_apps']) == 1

usable_app = data['can_use_apps'][0]
assert usable_app['app_uuid'] == app_uuid
assert usable_app['dtable_uuid'] == base_uuid.replace('-', '')

matcher = path_type({
r"can_use_apps\.\d+\.app_user_id": (int,),
r"can_use_apps\.\d+\.app_id": (int,),
r"can_use_apps\.\d+\.app_uuid": (str,),
r"can_use_apps\.\d+\.dtable_uuid": (str,),
r"can_use_apps\.\d+\.workspace_id": (int,),
r"can_use_apps\.\d+\.link": (str,),
r"can_use_apps\.\d+\.edit_link": (str,),
r"can_use_apps\.\d+\.joined_at": (str,),
}, regex=True)

assert snapshot_json(matcher=matcher) == data
Loading