diff --git a/src/apis/html-page-api.js b/src/apis/html-page-api.js
index 2377791..84866d7 100644
--- a/src/apis/html-page-api.js
+++ b/src/apis/html-page-api.js
@@ -147,6 +147,42 @@ class HTMLPageAPI {
return this._sendDelete(url, data);
}
+ addLink(page_id, table_name, row_id, link_column_name, other_row_id, preview_table_config) {
+ const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/html-page-links/`;
+ const data = { page_id, table_name, row_id, link_column_name, other_row_id };
+ if (preview_table_config && typeof preview_table_config === 'object') {
+ data.preview_table_config = preview_table_config;
+ }
+ return this.req.post(url, data);
+ }
+
+ deleteLink(page_id, table_name, row_id, link_column_name, other_row_id, preview_table_config) {
+ const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/html-page-links/`;
+ const data = { page_id, table_name, row_id, link_column_name, other_row_id };
+ if (preview_table_config && typeof preview_table_config === 'object') {
+ data.preview_table_config = preview_table_config;
+ }
+ return this._sendDelete(url, data);
+ }
+
+ addLinks(page_id, table_name, links_data, preview_table_config) {
+ const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/html-page-links/batch/`;
+ const data = { page_id, table_name, links_data };
+ if (preview_table_config && typeof preview_table_config === 'object') {
+ data.preview_table_config = preview_table_config;
+ }
+ return this.req.post(url, data);
+ }
+
+ deleteLinks(page_id, table_name, links_data, preview_table_config) {
+ const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/html-page-links/batch/`;
+ const data = { page_id, table_name, links_data };
+ if (preview_table_config && typeof preview_table_config === 'object') {
+ data.preview_table_config = preview_table_config;
+ }
+ return this._sendDelete(url, data);
+ }
+
getUploadLink(page_id, upload_type = 'file') {
const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/html-page-upload-link/`;
return this.req.get(url, { params: { page_id, upload_type } });
diff --git a/src/sdk.js b/src/sdk.js
index 5cb788f..175d30e 100644
--- a/src/sdk.js
+++ b/src/sdk.js
@@ -129,6 +129,40 @@ export class HTMLPageSDK {
return this.htmlPageAPI.deleteRows(this.options.pageId, tableName, rowsIds, previewTableConfig);
}
+ addLink({ tableName, rowId, linkColumnName, otherRowId }) {
+ const previewTableConfig = this._getPreviewTableConfig({ tableName });
+ return this.htmlPageAPI.addLink(
+ this.options.pageId,
+ tableName,
+ rowId,
+ linkColumnName,
+ otherRowId,
+ previewTableConfig,
+ );
+ }
+
+ deleteLink({ tableName, rowId, linkColumnName, otherRowId }) {
+ const previewTableConfig = this._getPreviewTableConfig({ tableName });
+ return this.htmlPageAPI.deleteLink(
+ this.options.pageId,
+ tableName,
+ rowId,
+ linkColumnName,
+ otherRowId,
+ previewTableConfig,
+ );
+ }
+
+ batchAddLinks({ tableName, linksData }) {
+ const previewTableConfig = this._getPreviewTableConfig({ tableName });
+ return this.htmlPageAPI.addLinks(this.options.pageId, tableName, linksData, previewTableConfig);
+ }
+
+ batchDeleteLinks({ tableName, linksData }) {
+ const previewTableConfig = this._getPreviewTableConfig({ tableName });
+ return this.htmlPageAPI.deleteLinks(this.options.pageId, tableName, linksData, previewTableConfig);
+ }
+
uploadFile({ file }) {
return this.htmlPageAPI.upload(this.options.pageId, file);
}
diff --git a/tests/html-page-api.test.js b/tests/html-page-api.test.js
index 91caf8f..ce3b370 100644
--- a/tests/html-page-api.test.js
+++ b/tests/html-page-api.test.js
@@ -329,8 +329,10 @@ describe('HTMLPageAPI.updateRow(s)', () => {
const { api, put } = createApi();
const rowsData = [{ row_id: 'row-1', Name: 'Jane' }];
const response = {
- data: { success: true },
- rows: [{ row_id: 'row-1', '0000': 'Jane' }],
+ data: {
+ success: true,
+ rows: [{ _id: 'row-1', '0000': 'Jane' }],
+ },
};
put.mockReturnValue(response);
@@ -350,6 +352,90 @@ describe('HTMLPageAPI.updateRow(s)', () => {
},
);
});
+
+ it('updateRow sends link-column row ids and returns expanded link values', () => {
+ const { api, put } = createApi();
+ const rowData = {
+ Name: 'Updated task',
+ 'Related projects': ['project-row-1', 'project-row-2'],
+ };
+ const updatedRow = {
+ _id: 'task-row-1',
+ Name: 'Updated task',
+ 'Related projects': [
+ { row_id: 'project-row-1', display_value: 'Project 1' },
+ { row_id: 'project-row-2', display_value: 'Project 2' },
+ ],
+ };
+ const response = { data: { success: true, row: updatedRow } };
+ put.mockReturnValue(response);
+
+ const result = api.updateRow('page-1', 'Tasks', 'task-row-1', rowData);
+
+ expect(result.data.row).toEqual(updatedRow);
+ expect(put).toHaveBeenCalledWith(
+ 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/',
+ {
+ page_id: 'page-1',
+ table_name: 'Tasks',
+ row_id: 'task-row-1',
+ row_data: rowData,
+ },
+ {
+ headers: { 'Content-Type': 'application/json' },
+ },
+ );
+ });
+
+ it('updateRows sends link-column row ids and returns expanded link values', () => {
+ const { api, put } = createApi();
+ const rowsData = [
+ {
+ row_id: 'task-row-1',
+ row: {
+ Name: 'Updated task 1',
+ 'Related projects': ['project-row-1', 'project-row-2'],
+ },
+ },
+ {
+ row_id: 'task-row-2',
+ row: {
+ 'Related projects': [],
+ },
+ },
+ ];
+ const updatedRows = [
+ {
+ _id: 'task-row-1',
+ Name: 'Updated task 1',
+ 'Related projects': [
+ { row_id: 'project-row-1', display_value: 'Project 1' },
+ { row_id: 'project-row-2', display_value: 'Project 2' },
+ ],
+ },
+ {
+ _id: 'task-row-2',
+ 'Related projects': [],
+ },
+ ];
+ const response = { data: { success: true, rows: updatedRows } };
+ put.mockReturnValue(response);
+
+ const result = api.updateRows('page-1', 'Tasks', rowsData);
+
+ expect(result.data.rows).toEqual(updatedRows);
+ expect(put).toHaveBeenCalledWith(
+ 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/batch/',
+ {
+ page_id: 'page-1',
+ table_name: 'Tasks',
+ rows_data: rowsData,
+ },
+ {
+ headers: { 'Content-Type': 'application/json' },
+ },
+ );
+ });
});
describe('HTMLPageAPI.deleteRows(s)', () => {
@@ -423,6 +509,173 @@ describe('HTMLPageAPI.deleteRows(s)', () => {
});
});
+describe('HTMLPageAPI link operations', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('adds a link and returns the updated row', () => {
+ const { api, post } = createApi();
+ const updatedRow = {
+ _id: 'task-row-1',
+ 'Related projects': [
+ { row_id: 'project-row-1', display_value: 'Project 1' },
+ ],
+ };
+ const response = { data: { success: true, row: updatedRow } };
+ post.mockReturnValue(response);
+
+ const result = api.addLink(
+ 'page-1',
+ 'Tasks',
+ 'task-row-1',
+ 'Related projects',
+ 'project-row-1',
+ );
+
+ expect(result).toBe(response);
+ expect(result.data.row).toEqual(updatedRow);
+ expect(post).toHaveBeenCalledWith(
+ 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/',
+ {
+ page_id: 'page-1',
+ table_name: 'Tasks',
+ row_id: 'task-row-1',
+ link_column_name: 'Related projects',
+ other_row_id: 'project-row-1',
+ },
+ );
+ });
+
+ it('deletes a link and returns the updated row', () => {
+ const { api, del } = createApi();
+ const updatedRow = { _id: 'task-row-1', 'Related projects': [] };
+ const response = { data: { success: true, row: updatedRow } };
+ del.mockReturnValue(response);
+
+ const result = api.deleteLink(
+ 'page-1',
+ 'Tasks',
+ 'task-row-1',
+ 'Related projects',
+ 'project-row-1',
+ );
+
+ expect(result).toBe(response);
+ expect(result.data.row).toEqual(updatedRow);
+ expect(del).toHaveBeenCalledWith(
+ 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/',
+ {
+ headers: { 'Content-Type': 'application/json' },
+ data: {
+ page_id: 'page-1',
+ table_name: 'Tasks',
+ row_id: 'task-row-1',
+ link_column_name: 'Related projects',
+ other_row_id: 'project-row-1',
+ },
+ },
+ );
+ });
+
+ it('adds and deletes links in batch and returns the updated rows', () => {
+ const { api, post, del } = createApi();
+ const linksData = [
+ {
+ row_id: 'task-row-1',
+ links: {
+ 'Related projects': ['project-row-1', 'project-row-2'],
+ },
+ },
+ ];
+ const addedRows = [{
+ _id: 'task-row-1',
+ 'Related projects': [
+ { row_id: 'project-row-1', display_value: 'Project 1' },
+ { row_id: 'project-row-2', display_value: 'Project 2' },
+ ],
+ }];
+ const deletedRows = [{ _id: 'task-row-1', 'Related projects': [] }];
+ const addResponse = { data: { success: true, rows: addedRows } };
+ const deleteResponse = { data: { success: true, rows: deletedRows } };
+ post.mockReturnValue(addResponse);
+ del.mockReturnValue(deleteResponse);
+
+ const addResult = api.addLinks('page-1', 'Tasks', linksData);
+ const deleteResult = api.deleteLinks('page-1', 'Tasks', linksData);
+
+ expect(addResult).toBe(addResponse);
+ expect(addResult.data.rows).toEqual(addedRows);
+ expect(deleteResult).toBe(deleteResponse);
+ expect(deleteResult.data.rows).toEqual(deletedRows);
+ expect(post).toHaveBeenCalledWith(
+ 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/batch/',
+ {
+ page_id: 'page-1',
+ table_name: 'Tasks',
+ links_data: linksData,
+ },
+ );
+ expect(del).toHaveBeenCalledWith(
+ 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/batch/',
+ {
+ headers: { 'Content-Type': 'application/json' },
+ data: {
+ page_id: 'page-1',
+ table_name: 'Tasks',
+ links_data: linksData,
+ },
+ },
+ );
+ });
+
+ it('includes preview_table_config in link requests for ai_agent preview', () => {
+ const { api, post, del } = createApi();
+ const previewTableConfig = { table_id: 'table-1', permissions: {} };
+ const linksData = [{ row_id: 'task-row-1', links: { related: ['project-row-1'] } }];
+
+ api.addLink('ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', previewTableConfig);
+ api.deleteLink('ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', previewTableConfig);
+ api.addLinks('ai_agent', 'Tasks', linksData, previewTableConfig);
+ api.deleteLinks('ai_agent', 'Tasks', linksData, previewTableConfig);
+
+ expect(post).toHaveBeenNthCalledWith(1, expect.any(String), {
+ page_id: 'ai_agent',
+ table_name: 'Tasks',
+ row_id: 'task-row-1',
+ link_column_name: 'related',
+ other_row_id: 'project-row-1',
+ preview_table_config: previewTableConfig,
+ });
+ expect(del).toHaveBeenNthCalledWith(1, expect.any(String), {
+ headers: { 'Content-Type': 'application/json' },
+ data: {
+ page_id: 'ai_agent',
+ table_name: 'Tasks',
+ row_id: 'task-row-1',
+ link_column_name: 'related',
+ other_row_id: 'project-row-1',
+ preview_table_config: previewTableConfig,
+ },
+ });
+ expect(post).toHaveBeenNthCalledWith(2, expect.any(String), {
+ page_id: 'ai_agent',
+ table_name: 'Tasks',
+ links_data: linksData,
+ preview_table_config: previewTableConfig,
+ });
+ expect(del).toHaveBeenNthCalledWith(2, expect.any(String), {
+ headers: { 'Content-Type': 'application/json' },
+ data: {
+ page_id: 'ai_agent',
+ table_name: 'Tasks',
+ links_data: linksData,
+ preview_table_config: previewTableConfig,
+ },
+ });
+ });
+});
+
describe('HTMLPageAPI.upload', () => {
beforeEach(() => {
jest.clearAllMocks();
diff --git a/tests/sdk.test.js b/tests/sdk.test.js
index af45a1f..ed84328 100644
--- a/tests/sdk.test.js
+++ b/tests/sdk.test.js
@@ -10,6 +10,10 @@ const mockUpdateRow = jest.fn();
const mockDeleteRows = jest.fn();
const mockAddRows = jest.fn();
const mockUpdateRows = jest.fn();
+const mockAddLink = jest.fn();
+const mockDeleteLink = jest.fn();
+const mockAddLinks = jest.fn();
+const mockDeleteLinks = jest.fn();
const mockUpload = jest.fn();
jest.mock('../src/iframe-adapter', () => ({
@@ -36,6 +40,10 @@ jest.mock('../src/apis/html-page-api', () => {
deleteRows: mockDeleteRows,
addRows: mockAddRows,
updateRows: mockUpdateRows,
+ addLink: mockAddLink,
+ deleteLink: mockDeleteLink,
+ addLinks: mockAddLinks,
+ deleteLinks: mockDeleteLinks,
upload: mockUpload,
}));
});
@@ -215,8 +223,10 @@ describe('rows', () => {
inserted_row_count: 1,
};
const updateRowsResponse = {
- data: { success: true },
- rows: [{ row_id: 'row-1', '0000': 'Jane' }],
+ data: {
+ success: true,
+ rows: [{ _id: 'row-1', '0000': 'Jane' }],
+ },
};
mockAddRow.mockReturnValue(addRowResponse);
@@ -229,16 +239,207 @@ describe('rows', () => {
expect(sdk.updateRow({ tableName: 'TableName', rowId: 'row-1', rowData: { Name: 'Jane' } })).toEqual(updateRowResponse);
expect(sdk.deleteRow({ tableName: 'TableName', rowId: 'row-1' })).toEqual(deleteRowsResponse);
expect(sdk.batchAddRows({ tableName: 'TableName', rowsData: [{ Name: 'John' }] })).toEqual(addRowsResponse);
- expect(sdk.batchUpdateRows({ tableName: 'TableName', rowsData: [{ row_id: 'row-1', Name: 'Jane' }] })).toEqual(updateRowsResponse);
+ expect(sdk.batchUpdateRows({
+ tableName: 'TableName',
+ rowsData: [{ row_id: 'row-1', row: { Name: 'Jane' } }],
+ })).toEqual(updateRowsResponse);
expect(sdk.batchDeleteRows({ tableName: 'TableName', rowsIds: ['row-1'] })).toEqual(deleteRowsResponse);
expect(mockAddRow).toHaveBeenCalledWith('page-1', 'TableName', { Name: 'John' }, undefined);
expect(mockUpdateRow).toHaveBeenCalledWith('page-1', 'TableName', 'row-1', { Name: 'Jane' }, undefined);
expect(mockDeleteRows).toHaveBeenCalledWith('page-1', 'TableName', ['row-1'], undefined);
expect(mockAddRows).toHaveBeenCalledWith('page-1', 'TableName', [{ Name: 'John' }], undefined);
- expect(mockUpdateRows).toHaveBeenCalledWith('page-1', 'TableName', [{ row_id: 'row-1', Name: 'Jane' }], undefined);
+ expect(mockUpdateRows).toHaveBeenCalledWith(
+ 'page-1',
+ 'TableName',
+ [{ row_id: 'row-1', row: { Name: 'Jane' } }],
+ undefined,
+ );
expect(mockDeleteRows).toHaveBeenCalledTimes(2);
});
+
+ it('updateRow and batchUpdateRows support link-column replacements', () => {
+ const sdk = new HTMLPageSDK({ pageId: 'page-1' });
+ sdk.htmlPageAPI = {
+ updateRow: mockUpdateRow,
+ updateRows: mockUpdateRows,
+ };
+ const rowData = {
+ Name: 'Updated task',
+ 'Related projects': ['project-row-1', 'project-row-2'],
+ };
+ const rowsData = [
+ {
+ row_id: 'task-row-1',
+ row: rowData,
+ },
+ {
+ row_id: 'task-row-2',
+ row: { 'Related projects': [] },
+ },
+ ];
+ const updatedRow = {
+ _id: 'task-row-1',
+ Name: 'Updated task',
+ 'Related projects': [
+ { row_id: 'project-row-1', display_value: 'Project 1' },
+ { row_id: 'project-row-2', display_value: 'Project 2' },
+ ],
+ };
+ const updatedRows = [
+ updatedRow,
+ { _id: 'task-row-2', 'Related projects': [] },
+ ];
+ const updateResponse = { data: { success: true, row: updatedRow } };
+ const batchUpdateResponse = { data: { success: true, rows: updatedRows } };
+ mockUpdateRow.mockReturnValue(updateResponse);
+ mockUpdateRows.mockReturnValue(batchUpdateResponse);
+
+ const result = sdk.updateRow({ tableName: 'Tasks', rowId: 'task-row-1', rowData });
+ const batchResult = sdk.batchUpdateRows({ tableName: 'Tasks', rowsData });
+
+ expect(result.data.row).toEqual(updatedRow);
+ expect(batchResult.data.rows).toEqual(updatedRows);
+ expect(mockUpdateRow).toHaveBeenCalledWith(
+ 'page-1',
+ 'Tasks',
+ 'task-row-1',
+ rowData,
+ undefined,
+ );
+ expect(mockUpdateRows).toHaveBeenCalledWith('page-1', 'Tasks', rowsData, undefined);
+ });
+});
+
+describe('links', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('delegates single and batch link operations', () => {
+ const sdk = new HTMLPageSDK({ pageId: 'page-1' });
+ sdk.htmlPageAPI = {
+ addLink: mockAddLink,
+ deleteLink: mockDeleteLink,
+ addLinks: mockAddLinks,
+ deleteLinks: mockDeleteLinks,
+ };
+ const linksData = [
+ {
+ row_id: 'task-row-1',
+ links: { 'Related projects': ['project-row-1', 'project-row-2'] },
+ },
+ ];
+ const addedRow = {
+ _id: 'task-row-1',
+ 'Related projects': [{ row_id: 'project-row-1', display_value: 'Project 1' }],
+ };
+ const deletedRow = { _id: 'task-row-1', 'Related projects': [] };
+ const addedRows = [{
+ _id: 'task-row-1',
+ 'Related projects': [
+ { row_id: 'project-row-1', display_value: 'Project 1' },
+ { row_id: 'project-row-2', display_value: 'Project 2' },
+ ],
+ }];
+ const deletedRows = [{ _id: 'task-row-1', 'Related projects': [] }];
+ const addResponse = { data: { success: true, row: addedRow } };
+ const deleteResponse = { data: { success: true, row: deletedRow } };
+ const batchAddResponse = { data: { success: true, rows: addedRows } };
+ const batchDeleteResponse = { data: { success: true, rows: deletedRows } };
+ mockAddLink.mockReturnValue(addResponse);
+ mockDeleteLink.mockReturnValue(deleteResponse);
+ mockAddLinks.mockReturnValue(batchAddResponse);
+ mockDeleteLinks.mockReturnValue(batchDeleteResponse);
+
+ const addResult = sdk.addLink({
+ tableName: 'Tasks',
+ rowId: 'task-row-1',
+ linkColumnName: 'Related projects',
+ otherRowId: 'project-row-1',
+ });
+ const deleteResult = sdk.deleteLink({
+ tableName: 'Tasks',
+ rowId: 'task-row-1',
+ linkColumnName: 'Related projects',
+ otherRowId: 'project-row-1',
+ });
+ const batchAddResult = sdk.batchAddLinks({ tableName: 'Tasks', linksData });
+ const batchDeleteResult = sdk.batchDeleteLinks({ tableName: 'Tasks', linksData });
+
+ expect(addResult).toBe(addResponse);
+ expect(addResult.data.row).toEqual(addedRow);
+ expect(deleteResult).toBe(deleteResponse);
+ expect(deleteResult.data.row).toEqual(deletedRow);
+ expect(batchAddResult).toBe(batchAddResponse);
+ expect(batchAddResult.data.rows).toEqual(addedRows);
+ expect(batchDeleteResult).toBe(batchDeleteResponse);
+ expect(batchDeleteResult.data.rows).toEqual(deletedRows);
+ expect(mockAddLink).toHaveBeenCalledWith(
+ 'page-1',
+ 'Tasks',
+ 'task-row-1',
+ 'Related projects',
+ 'project-row-1',
+ undefined,
+ );
+ expect(mockDeleteLink).toHaveBeenCalledWith(
+ 'page-1',
+ 'Tasks',
+ 'task-row-1',
+ 'Related projects',
+ 'project-row-1',
+ undefined,
+ );
+ expect(mockAddLinks).toHaveBeenCalledWith('page-1', 'Tasks', linksData, undefined);
+ expect(mockDeleteLinks).toHaveBeenCalledWith('page-1', 'Tasks', linksData, undefined);
+ });
+
+ it('includes the matching table permissions for ai_agent preview', () => {
+ const previewTableConfig = {
+ table_id: 'table-1',
+ table_name: 'Tasks',
+ permissions: {
+ edit_rows_permission: { enabled: true, columns_keys: ['related'] },
+ },
+ };
+ const expectedConfig = {
+ table_id: 'table-1',
+ permissions: previewTableConfig.permissions,
+ };
+ const sdk = new HTMLPageSDK({ pageId: 'ai_agent', previewTableConfigs: [previewTableConfig] });
+ sdk.htmlPageAPI = {
+ addLink: mockAddLink,
+ deleteLink: mockDeleteLink,
+ addLinks: mockAddLinks,
+ deleteLinks: mockDeleteLinks,
+ };
+ const linksData = [{ row_id: 'task-row-1', links: { related: ['project-row-1'] } }];
+
+ sdk.addLink({
+ tableName: 'Tasks',
+ rowId: 'task-row-1',
+ linkColumnName: 'related',
+ otherRowId: 'project-row-1',
+ });
+ sdk.deleteLink({
+ tableName: 'Tasks',
+ rowId: 'task-row-1',
+ linkColumnName: 'related',
+ otherRowId: 'project-row-1',
+ });
+ sdk.batchAddLinks({ tableName: 'Tasks', linksData });
+ sdk.batchDeleteLinks({ tableName: 'Tasks', linksData });
+
+ expect(mockAddLink).toHaveBeenCalledWith(
+ 'ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', expectedConfig,
+ );
+ expect(mockDeleteLink).toHaveBeenCalledWith(
+ 'ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', expectedConfig,
+ );
+ expect(mockAddLinks).toHaveBeenCalledWith('ai_agent', 'Tasks', linksData, expectedConfig);
+ expect(mockDeleteLinks).toHaveBeenCalledWith('ai_agent', 'Tasks', linksData, expectedConfig);
+ });
});
describe('upload', () => {