From 21bac022ccff86fe9b399ddeb6f4064d2ebfef03 Mon Sep 17 00:00:00 2001 From: yinjianfei-user <85612278+yinjianfei-user@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:08:37 +0800 Subject: [PATCH] add-seaTable-integration (#5761) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add-seaTable-integration * update-request * update-path * update-json * update-UI * update-json * update-text * update-text * remove-space * update-UI * add-hover * add variable control --------- Co-authored-by: 杨顺强 <978987373@qq.com> --- .../repo-seatable-integration-dialog.js | 192 ++++++++++++ .../add-seatable-account-setting.js | 170 +++++++++++ .../delete-seatables-dialog.js | 32 ++ .../seatable-account-setting-item.js | 78 +++++ .../seatable-account-setting-list.js | 74 +++++ .../css/repo-seatable-integration-dialog.css | 274 ++++++++++++++++++ .../src/pages/my-libs/mylib-repo-list-item.js | 18 ++ frontend/src/pages/my-libs/mylib-repo-menu.js | 9 +- frontend/src/utils/constants.js | 3 + seahub/base/context_processors.py | 4 +- seahub/settings.py | 3 + seahub/templates/base_for_react.html | 1 + 12 files changed, 856 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/dialog/repo-seatable-integration-dialog.js create mode 100644 frontend/src/components/seatable-integration-account-setting-widgets/add-seatable-account-setting.js create mode 100644 frontend/src/components/seatable-integration-account-setting-widgets/delete-seatables-dialog.js create mode 100644 frontend/src/components/seatable-integration-account-setting-widgets/seatable-account-setting-item.js create mode 100644 frontend/src/components/seatable-integration-account-setting-widgets/seatable-account-setting-list.js create mode 100644 frontend/src/css/repo-seatable-integration-dialog.css diff --git a/frontend/src/components/dialog/repo-seatable-integration-dialog.js b/frontend/src/components/dialog/repo-seatable-integration-dialog.js new file mode 100644 index 00000000000..4b5b81c5554 --- /dev/null +++ b/frontend/src/components/dialog/repo-seatable-integration-dialog.js @@ -0,0 +1,192 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody } from 'reactstrap'; +import SeatableAccountSettingList from '../seatable-integration-account-setting-widgets/seatable-account-setting-list.js'; +import AddSeatableAccountSetting from '../../components/seatable-integration-account-setting-widgets/add-seatable-account-setting.js'; +import toaster from '../toast'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; +import { gettext, internalFilePath, dirPath } from '../../utils/constants'; + +import '../../css/repo-seatable-integration-dialog.css'; + +const propTypes = { + repo: PropTypes.object.isRequired, + onSeaTableIntegrationToggle: PropTypes.func.isRequired, +}; + +const STATUS = { + SEATABLE_ACCOUNT_MANAGE: 'seatable_account_manage', + ADD_SETABLE_ACCOUNT: 'add_seatable_account', + UPDATE_SEATABLE_ACCOUNT: 'update_seatable_account' +}; + +class RepoSeaTableIntegrationDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + activeTab: 'SeaTable', + seatableSettings: [], + baseApiToken: '', + isPasswordVisible: false, + isShowDialog: false, + currentDtableInfo: {}, + status: STATUS.SEATABLE_ACCOUNT_MANAGE, + }; + this.repo = this.props.repo; + } + + componentDidMount() { + this.getSeatableSettings(); + } + + getSeatableSettings = async (status) => { + seafileAPI.req.defaults.headers.Authorization = null; + const [downloadLinkRes] = await seafileAPI.getFileDownloadLink(this.repo.repo_id, internalFilePath).then(res => [res, null]).catch((err) => [null, err]); + if (downloadLinkRes && downloadLinkRes.data) { + const fileInfoRes = await seafileAPI.getFileContent(downloadLinkRes.data); + if (fileInfoRes?.data && fileInfoRes.data) { + this.setState({ + seatableSettings: fileInfoRes.data + }); + status && this.setState({ status }); + } + } + }; + + changeTab = (tab) => { + if (this.state.activeTab !== tab) { + this.setState({activeTab: tab}); + } + }; + + changeStatus = (status) => { + this.setState({status}); + }; + + getFile = (detail, fileList) => { + const { base_name, seatable_url, seatable_api_token } = detail; + let content = [{ + 'base_name': base_name, + 'seatable_server_url': seatable_url, + 'base_api_token': seatable_api_token + }]; + + if (fileList && fileList.length !== 0) { + const index = fileList?.findIndex((item) => item.base_api_token === seatable_api_token); + if (index !== -1) { + fileList[index] = content[0]; + } else { + fileList.push(content[0]); + } + content = fileList; + } + + const fileName = internalFilePath.split('/')[2]; + const fileContent = JSON.stringify(content); + const newFile = new File([fileContent], fileName); + return newFile; + }; + + editSeatableSettingAccount = (baseApiToken) => { + const { seatableSettings } = this.state; + this.setState({ + status: STATUS.UPDATE_SEATABLE_ACCOUNT, + currentDtableInfo: seatableSettings.find((item) => item.base_api_token === baseApiToken) + }); + }; + + deleteStableAccountSetting = async (setting, status) => { + const { base_api_token } = setting; + seafileAPI.req.defaults.headers.Authorization = null; + const [downloadLinkRes] = await seafileAPI.getFileDownloadLink(this.repo.repo_id, internalFilePath).then(res => [res, null]).catch((err) => [null, err]); + if (downloadLinkRes && downloadLinkRes.data) { + const fileInfoRes = await seafileAPI.getFileContent(downloadLinkRes.data); + if (fileInfoRes?.data) { + const fileList = fileInfoRes.data; + const index = fileList?.findIndex((item) => item.base_api_token === base_api_token); + if (index !== -1) { + fileList.splice(index, 1); + const fileContent = JSON.stringify(fileList); + const fileName = internalFilePath.split('/')[2]; + const newFile = new File([fileContent], fileName); + const updateLink = await seafileAPI.getUpdateLink(this.repo.repo_id, internalFilePath.slice(0, 10)); + await seafileAPI.updateFile(updateLink.data, internalFilePath, fileName, newFile).catch(err => {toaster.danger(gettext(err.message));}); + this.getSeatableSettings(status); + } + } + } + }; + + onSubmit = async (detail, status) => { + seafileAPI.req.defaults.headers.Authorization = null; + const [downloadLinkRes, err] = await seafileAPI.getFileDownloadLink(this.repo.repo_id, internalFilePath).then(res => [res, null]).catch((err) => [null, err]); + // Contains configuration files + if (downloadLinkRes && downloadLinkRes.data) { + const fileInfoRes = await seafileAPI.getFileContent(downloadLinkRes.data); + if (fileInfoRes?.data) { + const newFile = this.getFile(detail, fileInfoRes.data); + const updateLink = await seafileAPI.getUpdateLink(this.repo.repo_id, internalFilePath.slice(0, 10)); + const fileName = internalFilePath.split('/')[2]; + await seafileAPI.updateFile(updateLink.data, internalFilePath, fileName, newFile).catch(err => {toaster.danger(gettext(err.message));}); + this.getSeatableSettings(status); + } + } + // No configuration file + if (err) { + const uploadLink = await seafileAPI.getFileServerUploadLink(this.repo.repo_id, dirPath); + const newFile = this.getFile(detail); + const formData = new FormData(); + formData.append('file', newFile); + formData.append('relative_path', internalFilePath.split('/')[1]); + formData.append('parent_dir', dirPath); + await seafileAPI.uploadImage(uploadLink.data + '?ret-json=1', formData).catch(err => {toaster.danger(gettext(err.message));}); + this.getSeatableSettings(status); + } + }; + + render() { + const { seatableSettings, status, currentDtableInfo } = this.state; + const { onSeaTableIntegrationToggle } = this.props; + let repo = this.repo; + const itemName = '' + Utils.HTMLescape(repo.repo_name) + ''; + const title = gettext('{placeholder} SeaTable integration').replace('{placeholder}', itemName); + + return ( + + +

+
+ +
+ {status === STATUS.SEATABLE_ACCOUNT_MANAGE && ( + this.changeStatus(STATUS.ADD_SETABLE_ACCOUNT)} + editSeatableSettingAccount={this.editSeatableSettingAccount} + deleteStableAccountSetting={this.deleteStableAccountSetting} + /> + )} + {status === STATUS.ADD_SETABLE_ACCOUNT && ( + this.changeStatus(STATUS.SEATABLE_ACCOUNT_MANAGE)} + onSubmit={this.onSubmit} + /> + )} + {status === STATUS.UPDATE_SEATABLE_ACCOUNT && ( + this.changeStatus(STATUS.SEATABLE_ACCOUNT_MANAGE)} + onSubmit={this.onSubmit} + /> + )} +
+
+
+ ); + } +} + +RepoSeaTableIntegrationDialog.propTypes = propTypes; + +export default RepoSeaTableIntegrationDialog; diff --git a/frontend/src/components/seatable-integration-account-setting-widgets/add-seatable-account-setting.js b/frontend/src/components/seatable-integration-account-setting-widgets/add-seatable-account-setting.js new file mode 100644 index 00000000000..188ec83d727 --- /dev/null +++ b/frontend/src/components/seatable-integration-account-setting-widgets/add-seatable-account-setting.js @@ -0,0 +1,170 @@ +import React, { Component } from 'react'; +import { Alert, Input, FormGroup, Label, InputGroup, InputGroupText } from 'reactstrap'; +import PropTypes from 'prop-types'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext } from '../../utils/constants'; + +class AddSeatableAccountSetting extends Component { + + static propTypes = { + t: PropTypes.func, + changeStatus: PropTypes.func, + onSubmit: PropTypes.func, + currentDtableInfo: PropTypes.object, + addSeatableAccountSetting: PropTypes.func, + }; + + constructor(props) { + super(props); + const { currentDtableInfo } = props; + this.state = { + errMessage: '', + base_name: currentDtableInfo?.base_name || '', + seatable_url: currentDtableInfo?.seatable_url || 'https://dev.seatable.cn/', + seatable_api_token: currentDtableInfo?.base_api_token || '', + successMessage: null, + stage: 'toCheck', // toCheck: need to check -> toSubmit: need to submit + passwordType: 'password' + }; + } + + onChangeBaseName = (event) => { + let value = event.target.value; + if (value === this.state.base_name) { + return; + } + this.setState({ + base_name: value, + errMessage: '', + }); + }; + + onChangeSeatableUrl = (event) => { + let value = event.target.value; + if (value === this.state.seatable_url) { + return; + } + this.setState({ + seatable_url: value, + successMessage: null, + stage: 'toCheck', + errMessage: '', + }); + }; + + onChangeSeatableApiToken = (event) => { + let value = event.target.value; + if (value === this.state.seatable_api_token) { + return; + } + this.setState({ + seatable_api_token: value, + successMessage: null, + stage: 'toCheck', + errMessage: '', + }); + }; + + addSeatableAccountSetting = () => { + const { t } = this.props; + let { base_name, seatable_url, seatable_api_token } = this.state; + base_name = base_name.trim(); + seatable_url = seatable_url.trim(); + seatable_api_token = seatable_api_token.trim(); + let errMessage = ''; + if (!base_name) { + errMessage = gettext('Base name is required'); + } + else if (!seatable_url) { + errMessage = gettext('URL is required'); + } + else if (!seatable_api_token) { + errMessage = gettext('SeaTable API token is required'); + } + + this.setState({errMessage}); + if (errMessage) return; + let detail = { + base_name, + seatable_url, + seatable_api_token + }; + this.props.onSubmit(detail, 'seatable_account_manage'); + }; + + testSeatableAPIToken = async () => { + const { seatable_url, seatable_api_token } = this.state; + seafileAPI.req.defaults.headers.Authorization = `Token ${seatable_api_token}`; + const [res, err] = await seafileAPI.req.get(`${seatable_url}api/v2.1/dtable/app-access-token/`).then(res => [res, null]).catch((err) => [null, err]); + if (res) { + this.setState({ + successMessage: res.data, + stage: 'toSubmit', + }); + } + if (err) { + this.setState({ + errMessage: gettext('URL or SeaTable API token is invalid'), + }); + } + }; + + togglePasswordShow = () => { + if (this.state.passwordType === 'password') { + this.setState({passwordType: 'text'}); + } else { + this.setState({passwordType: 'password'}); + } + }; + + render() { + const { errMessage, stage, successMessage, base_name, seatable_url, seatable_api_token, passwordType } = this.state; + return ( +
+
+ + + + + {gettext('Add SeaTable Integration')} + + +
+
+
+ + + + + + + + + + + + + + + + + +
+ {errMessage && {errMessage}} + {successMessage && ( + + + {gettext('Successfully connected to SeaTable')} + + )} +
+
+ ); + } +} + +export default AddSeatableAccountSetting; diff --git a/frontend/src/components/seatable-integration-account-setting-widgets/delete-seatables-dialog.js b/frontend/src/components/seatable-integration-account-setting-widgets/delete-seatables-dialog.js new file mode 100644 index 00000000000..479872fc8a8 --- /dev/null +++ b/frontend/src/components/seatable-integration-account-setting-widgets/delete-seatables-dialog.js @@ -0,0 +1,32 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext } from '../../utils/constants'; + +class DeleteSeatablesDialog extends Component { + + static propTypes = { + t: PropTypes.func, + accountName: PropTypes.string, + onDeleteSeatables: PropTypes.func, + closeDialog: PropTypes.func, + }; + + render () { + const { accountName, closeDialog } = this.props; + return ( + + {gettext('Delete SeaTable base')} + +
{gettext('Are you sure to delete SeaTable')}{' '}{accountName}?
+
+ + + + +
+ ); + } +} + +export default DeleteSeatablesDialog; diff --git a/frontend/src/components/seatable-integration-account-setting-widgets/seatable-account-setting-item.js b/frontend/src/components/seatable-integration-account-setting-widgets/seatable-account-setting-item.js new file mode 100644 index 00000000000..861e83cd3b7 --- /dev/null +++ b/frontend/src/components/seatable-integration-account-setting-widgets/seatable-account-setting-item.js @@ -0,0 +1,78 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import DeleteSeatablesDialog from './delete-seatables-dialog'; +import { gettext } from '../../utils/constants'; + +class SeatableAccountItem extends Component { + + constructor (props) { + super(props); + this.state = { + isShowDialog: false, + }; + } + + static propTypes = { + t: PropTypes.func, + editSeatableSettingAccount: PropTypes.func, + deleteStableAccountSetting: PropTypes.func, + setting: PropTypes.object, + index: PropTypes.number, + }; + + openDialog = () => { + this.setState({isShowDialog: true}); + }; + + closeDialog = () => { + this.setState({isShowDialog: false}); + }; + + onDeleteSeatables = () => { + const { setting } = this.props; + this.props.deleteStableAccountSetting(setting, 'seatable_account_manage'); + this.closeDialog(); + }; + + render() { + const { isShowDialog } = this.state; + const { setting, t, index } = this.props; + const { base_api_token, base_name, seatable_server_url } = setting; + return ( + + {base_name} + + {seatable_server_url} + + + + + + + + + + {isShowDialog && + + } + + ); + } +} + +export default SeatableAccountItem; diff --git a/frontend/src/components/seatable-integration-account-setting-widgets/seatable-account-setting-list.js b/frontend/src/components/seatable-integration-account-setting-widgets/seatable-account-setting-list.js new file mode 100644 index 00000000000..acf384597ef --- /dev/null +++ b/frontend/src/components/seatable-integration-account-setting-widgets/seatable-account-setting-list.js @@ -0,0 +1,74 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'reactstrap'; +import { gettext, mediaUrl } from '../../utils/constants'; +import SeatableAccountItem from './seatable-account-setting-item'; + +class SeatableAccountSettingList extends Component { + + static propTypes = { + accounts: PropTypes.array, + changeStatus: PropTypes.func, + editSeatableSettingAccount: PropTypes.func, + seatableSettings: PropTypes.array, + deleteStableAccountSetting: PropTypes.func, + }; + + renderContent = () => { + const { seatableSettings } = this.props; + if (!Array.isArray(seatableSettings) || seatableSettings.length === 0) { + return ( +
+ {gettext('No +

{gettext('No Seafile libraries')}

+
+ ); + } + return ( + <> + + + + + + + + +
{gettext('Base name')}{gettext('SeaTable server URL')}
+
+ + + {seatableSettings.map((setting, index) => { + return ( + + ); + })} + +
+
+ + ); + }; + + render() { + return ( +
+
+ {gettext('SeaTable')} + +
+
+ {this.renderContent()} +
+
+ ); + } +} + +export default SeatableAccountSettingList; diff --git a/frontend/src/css/repo-seatable-integration-dialog.css b/frontend/src/css/repo-seatable-integration-dialog.css new file mode 100644 index 00000000000..a4951f1c514 --- /dev/null +++ b/frontend/src/css/repo-seatable-integration-dialog.css @@ -0,0 +1,274 @@ +/* common */ +.account-dialog .nav .nav-item { + padding: 0; +} + +.account-dialog .nav .nav-item .nav-link { + padding: 0.5rem 0; + color: #8a948f; + font-weight: normal; + transition: none; + width: 100%; +} + +.account-dialog .nav .nav-item .nav-link.active { + color: #ff8000; + text-decoration: none; + border-bottom: 0.125rem solid #ff8000; +} + +.account-dialog .nav-pills .nav-item .nav-link { + padding: .3125rem 1rem .3125rem 8px; + color: #333; +} + +.account-dialog .nav-pills .nav-item .nav-link:hover { + background-color: #f5f5f5; +} + +.account-dialog .nav-pills .nav-item .nav-link.active { + background-color: #ff8000; + color: #fff; + border: none; +} + +.account-dialog table { + width: 100%; + table-layout: fixed; +} + +.account-dialog .accounts-list { + max-height: 400px; + overflow: auto; +} + +.account-dialog .accounts-list table thead tr { + border-bottom: 1px solid #efefef; + height: 2.1875rem; +} + +.account-dialog .accounts-list table tbody tr { + border-bottom: 1px solid #efefef; + height: 2.5625rem; +} + +.account-dialog table th { + padding: 0.3125rem 0.1875rem; + font-weight: 500; +} + +.account-dialog table td { + padding: 3px 0.1875rem; + color: #333; + font-size: 14px; + word-break: break-all; +} + +.account-dialog table td a { + text-decoration: none; + color: inherit; +} + +.account-dialog .ellipsis { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.account-dialog .select-module { + font-size: 16px; +} + +.account-dialog .select-module.select-module-icon { + width: 1.5rem; + height: 1.5rem; +} + +.account-dialog .select-module.select-module-name { + margin-left: 0.5rem; +} + +.account-dialog .op-target { + color: #ea7500; + word-wrap: break-word; +} + +/* custom */ +.account-dialog { + max-width: 600px; + height: calc(100% - 56px); +} + +.account-dialog .account-dialog-content { + padding: 0; + min-height: 27rem; + display: flex; + flex-direction: row; + overflow: hidden; +} + +.account-dialog-content .account-dialog-side { + display: flex; + flex: 0 0 25%; + padding: 12px 8px; + border-right: 1px solid #eee; +} + +.account-dialog-content .account-dialog-main { + flex: 1; + padding: 0.5rem 1.5rem 2rem; +} + +.account-dialog-content .account-dialog-main .tab-content { + flex: 1; +} + +.account-dialog-content .account-dialog-main .tab-pane { + height: 100%; +} + +.account-dialog .wechat-input-content { + height: 250px; +} + +.accounts-manage .accounts-manage-header { + border-bottom: 1px solid #efefef; + padding: .375rem 0; +} + +.accounts-manage .accounts-manage-header button { + font-weight: normal; +} + +.accounts-manage .no-accounts { + height: 400px; +} + +.accounts-manage .no-accounts img { + width: 120px; +} + +.accounts-manage .no-accounts p { + margin-top: 1rem; + color: #afafaf; +} + +.accounts-manage .accounts-list-body { + max-height: 400px; + overflow-y: auto; + cursor: pointer; +} + +.accounts-manage th, +.accounts-manage td { + padding: .5rem .1875rem; + border-color: #efefef; +} + +.accounts-manage th { + color: #999; +} + +.accounts-manage .accounts-list-body tr:hover { + background-color: #f9f9f9; +} + +.accounts-manage .accounts-list-body .account-operation-btn { + display: none; + margin-right: .2rem; +} + +.accounts-manage .accounts-list-body tr:hover .account-operation-btn { + display: inline-flex; + width: 24px; + height: 24px; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.accounts-manage .account-operation-btn .dtable-font { + font-size: 14px; + color: #999; +} + +.accounts-manage .account-operation-btn:hover .dtable-font { + color: #5a5a5a; +} + +.add-account, +.edit-account { + height: 100%; +} + +.add-account .add-account-header, +.edit-account .edit-account-header { + padding: .375rem 0; + border-bottom: 1px solid #efefef; +} + +.add-account .back-btn, +.edit-account .back-btn { + width: 18px; + height: 18px; + cursor: pointer; +} + +.add-account .back-btn .dtable-icon-return, +.edit-account .back-btn .dtable-icon-return { + font-size: 14px; + color: #999; +} + +.add-account .back-btn:hover .dtable-icon-return, +.edit-account .back-btn:hover .dtable-icon-return { + color: #5a5a5a; +} + +.add-account .add-account-header-text, +.edit-account .edit-account-header-text { + margin-left: .25rem; +} + +.add-account .add-account-btn, +.edit-account .edit-account-btn { + margin: 0; + padding-top: 0; + padding-bottom: 0; + font-weight: normal; + border-color: #ff8000; + color: #ff8000; +} + +.add-account .account-loading-tip { + margin-top: 1rem; +} + +.add-account .add-account-btn:hover, +.edit-account .edit-account-btn:hover { + background-color: transparent; +} + +.base-account { + height: calc(100% - 38px); +} + +.base-account input[type="checkbox"] { + margin-top: -2px; + vertical-align: middle; +} + +.base-account .account-name-desc { + margin-top: .375rem; +} + +.base-account .base-account-password .input-group .form-control { + height: 38px; + border-right: none; +} + +.base-account .base-account-password .input-group .input-group-text { + height: 38px; + border-radius: 0 3px 3px 0; +} + diff --git a/frontend/src/pages/my-libs/mylib-repo-list-item.js b/frontend/src/pages/my-libs/mylib-repo-list-item.js index e001fdcaa1a..1198be5bc0c 100644 --- a/frontend/src/pages/my-libs/mylib-repo-list-item.js +++ b/frontend/src/pages/my-libs/mylib-repo-list-item.js @@ -19,6 +19,7 @@ import LibSubFolderPermissionDialog from '../../components/dialog/lib-sub-folder import Rename from '../../components/rename'; import MylibRepoMenu from './mylib-repo-menu'; import RepoAPITokenDialog from '../../components/dialog/repo-api-token-dialog'; +import RepoSeaTableIntegrationDialog from '../../components/dialog/repo-seatable-integration-dialog'; import RepoShareAdminDialog from '../../components/dialog/repo-share-admin-dialog'; import LibOldFilesAutoDelDialog from '../../components/dialog/lib-old-files-auto-del-dialog'; import RepoMonitoredIcon from '../../components/repo-monitored-icon'; @@ -52,6 +53,7 @@ class MylibRepoListItem extends React.Component { isLabelRepoStateDialogOpen: false, isFolderPermissionDialogShow: false, isAPITokenDialogShow: false, + isSeaTableIntegrationShow: false, isRepoShareAdminDialogOpen: false, isRepoDeleted: false, isOldFilesAutoDelDialogOpen: false, @@ -132,6 +134,9 @@ class MylibRepoListItem extends React.Component { case 'Old Files Auto Delete': this.toggleOldFilesAutoDelDialog(); break; + case 'SeaTable integration': + this.onSeaTableIntegrationToggle(); + break; default: break; } @@ -240,6 +245,10 @@ class MylibRepoListItem extends React.Component { this.setState({isAPITokenDialogShow: !this.state.isAPITokenDialogShow}); }; + onSeaTableIntegrationToggle = () => { + this.setState({isSeaTableIntegrationShow: !this.state.isSeaTableIntegrationShow}); + }; + toggleRepoShareAdminDialog = () => { this.setState({isRepoShareAdminDialogOpen: !this.state.isRepoShareAdminDialogOpen}); }; @@ -509,6 +518,15 @@ class MylibRepoListItem extends React.Component { )} + {this.state.isSeaTableIntegrationShow && ( + + + + )} + {this.state.isRepoShareAdminDialogOpen && (