1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-19 18:29:23 +00:00

add-seaTable-integration (#5761)

* 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>
This commit is contained in:
yinjianfei-user
2023-12-12 15:08:37 +08:00
committed by GitHub
parent a514228406
commit 21bac022cc
12 changed files with 856 additions and 2 deletions

View File

@@ -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 = '<span class="op-target">' + Utils.HTMLescape(repo.repo_name) + '</span>';
const title = gettext('{placeholder} SeaTable integration').replace('{placeholder}', itemName);
return (
<Modal isOpen={true} toggle={onSeaTableIntegrationToggle} className="account-dialog">
<ModalHeader toggle={onSeaTableIntegrationToggle}>
<p dangerouslySetInnerHTML={{__html: title}} className="m-0"></p>
</ModalHeader>
<ModalBody className="account-dialog-content">
<div className="account-dialog-main">
{status === STATUS.SEATABLE_ACCOUNT_MANAGE && (
<SeatableAccountSettingList
seatableSettings={seatableSettings}
changeStatus={() => this.changeStatus(STATUS.ADD_SETABLE_ACCOUNT)}
editSeatableSettingAccount={this.editSeatableSettingAccount}
deleteStableAccountSetting={this.deleteStableAccountSetting}
/>
)}
{status === STATUS.ADD_SETABLE_ACCOUNT && (
<AddSeatableAccountSetting
changeStatus={() => this.changeStatus(STATUS.SEATABLE_ACCOUNT_MANAGE)}
onSubmit={this.onSubmit}
/>
)}
{status === STATUS.UPDATE_SEATABLE_ACCOUNT && (
<AddSeatableAccountSetting
currentDtableInfo={currentDtableInfo}
changeStatus={() => this.changeStatus(STATUS.SEATABLE_ACCOUNT_MANAGE)}
onSubmit={this.onSubmit}
/>
)}
</div>
</ModalBody>
</Modal>
);
}
}
RepoSeaTableIntegrationDialog.propTypes = propTypes;
export default RepoSeaTableIntegrationDialog;

View File

@@ -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 (
<div className="add-account">
<div className="add-account-header d-flex align-items-center justify-content-between">
<span>
<span className="back-btn d-inline-flex align-items-center justify-content-center" onClick={this.props.changeStatus}>
<i className="link-icon icon-left sf3-font sf3-font-arrow" style={{transform: 'rotate(180deg)', color: '#999'}}></i>
</span>
<span className="add-account-header-text">{gettext('Add SeaTable Integration')}</span>
</span>
<button
onClick={stage === 'toCheck'? this.testSeatableAPIToken : this.addSeatableAccountSetting}
type="button"
className="btn btn-secondary add-account-btn"
>{stage === 'toCheck' ? gettext('Check') : gettext('Submit')}</button>
</div>
<div className="base-account">
<div className="account-name-desc">
<FormGroup>
<Label>{gettext('Base name')}</Label>
<Input value={base_name} onChange={this.onChangeBaseName}/>
</FormGroup>
<FormGroup>
<Label>{gettext('SeaTable server URL')}</Label>
<Input value={seatable_url} onChange={this.onChangeSeatableUrl}/>
</FormGroup>
<FormGroup className="base-account-password">
<Label>{gettext('SeaTable API token')}</Label>
<InputGroup>
<Input value={seatable_api_token} type={passwordType} onChange={this.onChangeSeatableApiToken}/>
<InputGroupText>
<i className={`fas ${passwordType === 'password' ? 'fa-eye-slash' : 'fa-eye'} cursor-pointer`} onClick={this.togglePasswordShow} />
</InputGroupText>
</InputGroup>
</FormGroup>
</div>
{errMessage && <Alert color="danger">{errMessage}</Alert>}
{successMessage && (
<Alert color="success">
<span className="dtable-font dtable-icon-check-circle mr-2"></span>
{gettext('Successfully connected to SeaTable')}
</Alert>
)}
</div>
</div>
);
}
}
export default AddSeatableAccountSetting;

View File

@@ -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 (
<Modal isOpen={true} toggle={closeDialog}>
<ModalHeader toggle={closeDialog}>{gettext('Delete SeaTable base')}</ModalHeader>
<ModalBody>
<div className="pb-6">{gettext('Are you sure to delete SeaTable')}{' '}{accountName}?</div>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={closeDialog}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.props.onDeleteSeatables}>{gettext('Delete')}</Button>
</ModalFooter>
</Modal>
);
}
}
export default DeleteSeatablesDialog;

View File

@@ -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 (
<tr key={`account-${base_api_token}`}>
<td width='30%' className="text-truncate" title={base_name} aria-label={base_name}>{base_name}</td>
<td id={`abc-${index}`} width='55%' className="text-truncate" title={seatable_server_url} aria-label={seatable_server_url}>
{seatable_server_url}
</td>
<td width='15%'>
<span
className="account-operation-btn"
onClick={this.props.editSeatableSettingAccount.bind(this, base_api_token)}
title={gettext('Edit')}
aria-label={gettext('Edit')}
>
<i className="sf2-icon-edit" style={{color: '#999'}}></i>
</span>
<span
className="account-operation-btn"
onClick={this.openDialog}
title={gettext('Delete')}
aria-label={gettext('Delete')}
>
<i className="sf2-icon-delete" style={{color: '#999'}}></i>
</span>
</td>
{isShowDialog &&
<DeleteSeatablesDialog
t={t}
accountName={base_name}
onDeleteSeatables={this.onDeleteSeatables}
closeDialog={this.closeDialog}
/>
}
</tr>
);
}
}
export default SeatableAccountItem;

View File

@@ -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 (
<div className="no-accounts d-flex flex-column align-items-center justify-content-center">
<img src={`${mediaUrl}img/no-items-tip.png`} alt={gettext('No SeaTable libraries')} />
<p>{gettext('No Seafile libraries')}</p>
</div>
);
}
return (
<>
<table className="accounts-list-header">
<thead>
<tr>
<th width='30%'>{gettext('Base name')}</th>
<th width='55%'>{gettext('SeaTable server URL')}</th>
<th width='15%'> </th>
</tr>
</thead>
</table>
<div className="accounts-list-body">
<table>
<tbody>
{seatableSettings.map((setting, index) => {
return (
<SeatableAccountItem
key={setting.base_api_token}
index={index}
setting={setting}
editSeatableSettingAccount={this.props.editSeatableSettingAccount}
deleteStableAccountSetting={this.props.deleteStableAccountSetting}
/>
);
})}
</tbody>
</table>
</div>
</>
);
};
render() {
return (
<div className="accounts-manage">
<div className="accounts-manage-header d-flex align-items-center justify-content-between">
<span>{gettext('SeaTable')}</span>
<Button color="primary" size="sm" outline={true} onClick={this.props.changeStatus}>{gettext('Add')}</Button>
</div>
<div className="accounts-list mt-2">
{this.renderContent()}
</div>
</div>
);
}
}
export default SeatableAccountSettingList;