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', () => {