diff --git a/frontend/src/pages/sdoc-file-history/history-version.js b/frontend/src/pages/sdoc-file-history/history-version.js
index 2831a72863..2c51f3a30e 100644
--- a/frontend/src/pages/sdoc-file-history/history-version.js
+++ b/frontend/src/pages/sdoc-file-history/history-version.js
@@ -1,11 +1,15 @@
+import moment from 'moment';
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap';
import { gettext, filePath } from '../../utils/constants';
import URLDecorator from '../../utils/url-decorator';
+import Rename from '../../components/rename';
import '../../css/history-record-item.css';
+moment.locale(window.app.config.lang);
+
class HistoryVersion extends React.Component {
constructor(props) {
@@ -13,18 +17,19 @@ class HistoryVersion extends React.Component {
this.state = {
isShowOperationIcon: false,
isMenuShow: false,
+ isRenameShow: false,
};
}
onMouseEnter = () => {
const { currentVersion, historyVersion } = this.props;
- if (currentVersion.commitId === historyVersion.commitId) return;
+ if (currentVersion.commit_id === historyVersion.commit_id) return;
this.setState({ isShowOperationIcon: true });
}
onMouseLeave = () => {
const { currentVersion, historyVersion } = this.props;
- if (currentVersion.commitId === historyVersion.commitId) return;
+ if (currentVersion.commit_id === historyVersion.commit_id) return;
this.setState({ isShowOperationIcon: false });
}
@@ -35,7 +40,7 @@ class HistoryVersion extends React.Component {
onClick = () => {
this.setState({ isShowOperationIcon: false });
const { currentVersion, historyVersion } = this.props;
- if (currentVersion.commitId === historyVersion.commitId) return;
+ if (currentVersion.commit_id === historyVersion.commit_id) return;
this.props.onSelectHistoryVersion(historyVersion);
}
@@ -50,15 +55,30 @@ class HistoryVersion extends React.Component {
onItemCopy = () => {
const { historyVersion } = this.props;
+ historyVersion.ctime_format = moment(historyVersion.ctime).format('YYYY-MM-DD HH:mm');
this.props.onCopy(historyVersion);
}
+ toggleRename = () => {
+ this.setState({isRenameShow: !this.state.isRenameShow});
+ }
+
+ onRenameConfirm = (newName) => {
+ const { obj_id } = this.props.historyVersion;
+ this.props.renameHistoryVersion(obj_id, newName);
+ this.toggleRename();
+ }
+
+ onRenameCancel = () => {
+ this.toggleRename();
+ }
+
render() {
const { currentVersion, historyVersion } = this.props;
if (!currentVersion || !historyVersion) return null;
- const { ctime, commitId, creatorName, revFileId } = historyVersion;
- const isHighlightItem = commitId === currentVersion.commitId;
- const url = URLDecorator.getUrl({ type: 'download_historic_file', filePath: filePath, objID: revFileId });
+ const { ctime, commit_id, creator_name, obj_id, name} = historyVersion;
+ const isHighlightItem = commit_id === currentVersion.commit_id;
+ const url = URLDecorator.getUrl({ type: 'download_historic_file', filePath: filePath, objID: obj_id });
return (
-
{ctime}
+ {this.state.isRenameShow ?
+
+ :{name}
+ }
+ {moment(ctime).format('YYYY-MM-DD HH:mm')}
- {creatorName}
+ {creator_name}
@@ -86,6 +110,7 @@ class HistoryVersion extends React.Component {
{(this.props.index !== 0) && {gettext('Restore')}}
{gettext('Download')}
{(this.props.index !== 0) && {gettext('Copy')}}
+ {gettext('Rename')}
@@ -101,6 +126,7 @@ HistoryVersion.propTypes = {
onSelectHistoryVersion: PropTypes.func.isRequired,
onRestore: PropTypes.func.isRequired,
onCopy: PropTypes.func.isRequired,
+ renameHistoryVersion: PropTypes.func.isRequired,
};
export default HistoryVersion;
diff --git a/frontend/src/pages/sdoc-file-history/index.js b/frontend/src/pages/sdoc-file-history/index.js
index fddb3b787b..0d13168be5 100644
--- a/frontend/src/pages/sdoc-file-history/index.js
+++ b/frontend/src/pages/sdoc-file-history/index.js
@@ -50,12 +50,12 @@ class SdocFileHistory extends React.Component {
onSelectHistoryVersion = (currentVersion, lastVersion) => {
this.setState({ isLoading: true, currentVersion });
- seafileAPI.getFileRevision(historyRepoID, currentVersion.commitId, currentVersion.path).then(res => {
+ seafileAPI.getFileRevision(historyRepoID, currentVersion.commit_id, currentVersion.path).then(res => {
return seafileAPI.getFileContent(res.data);
}).then(res => {
const currentVersionContent = res.data;
if (lastVersion) {
- seafileAPI.getFileRevision(historyRepoID, lastVersion.commitId, lastVersion.path).then(res => {
+ seafileAPI.getFileRevision(historyRepoID, lastVersion.commit_id, lastVersion.path).then(res => {
return seafileAPI.getFileContent(res.data);
}).then(res => {
const lastVersionContent = res.data;
@@ -84,7 +84,7 @@ class SdocFileHistory extends React.Component {
const { currentVersionContent } = this.state;
this.setState({ isLoading: true }, () => {
localStorage.setItem('seahub-sdoc-history-show-changes', isShowChanges + '');
- seafileAPI.getFileRevision(historyRepoID, lastVersion.commitId, lastVersion.path).then(res => {
+ seafileAPI.getFileRevision(historyRepoID, lastVersion.commit_id, lastVersion.path).then(res => {
return seafileAPI.getFileContent(res.data);
}).then(res => {
const lastVersionContent = res.data;
diff --git a/frontend/src/pages/sdoc-file-history/side-panel.js b/frontend/src/pages/sdoc-file-history/side-panel.js
index 6befd3d4b2..392ca27a67 100644
--- a/frontend/src/pages/sdoc-file-history/side-panel.js
+++ b/frontend/src/pages/sdoc-file-history/side-panel.js
@@ -2,15 +2,16 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Loading from '../../components/loading';
-import { gettext, filePath, historyRepoID } from '../../utils/constants';
+import { gettext, historyRepoID, PER_PAGE } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
-import FileHistory from '../../models/file-history';
import { Utils } from '../../utils/utils';
import editUtilities from '../../utils/editor-utilities';
import toaster from '../../components/toast';
import HistoryVersion from './history-version';
import Switch from '../../components/common/switch';
+const { docUuid } = window.fileHistory.pageOptions;
+
class SidePanel extends Component {
constructor(props) {
@@ -19,85 +20,104 @@ class SidePanel extends Component {
isLoading: true,
historyVersions: [],
errorMessage: '',
+ currentPage: 1,
+ hasMore: false,
+ fileOwner: '',
+ isReloadingData: false,
};
- this.init();
}
componentDidMount() {
- this.listHistoryVersions(historyRepoID, filePath, this.nextCommit, (historyVersion, lastHistoryVersion) => {
- this.props.onSelectHistoryVersion(historyVersion, lastHistoryVersion);
+ seafileAPI.listSdocHistory(docUuid, 1, PER_PAGE).then(res => {
+ let historyList = res.data;
+ if (historyList.length === 0) {
+ this.setState({isLoading: false});
+ throw Error('there has an error in server');
+ }
+ this.initResultState(res.data);
});
}
- init = () => {
- this.hasMore = true;
- this.nextCommit = '';
- this.filePath = '';
- this.oldFilePath = '';
+ refershFileList() {
+ seafileAPI.listSdocHistory(docUuid, 1, PER_PAGE).then(res => {
+ this.initResultState(res.data);
+ });
}
- listHistoryVersions = (repoID, filePath, commit, callback) => {
- seafileAPI.listOldFileHistoryRecords(repoID, filePath, commit).then((res) => {
- let historyData = res.data;
- if (!historyData) {
- this.setState({ isLoading: false, errorMessage: 'There is an error in server.' });
- return;
- }
- this.updateHistoryVersions(historyData, callback);
+ initResultState(result) {
+ if (result.histories.length) {
+ this.setState({
+ historyVersions: result.histories,
+ currentPage: result.page,
+ hasMore: result.total_count > (PER_PAGE * this.state.currentPage),
+ isLoading: false,
+ fileOwner: result.histories[0].creator_email,
+ });
+ this.props.onSelectHistoryVersion(result.histories[0], result.histories[1]);
+ }
+ }
+
+ updateResultState(result) {
+ if (result.histories.length) {
+ this.setState({
+ historyVersions: [...this.state.historyVersions, ...result.histories],
+ currentPage: result.page,
+ hasMore: result.total_count > (PER_PAGE * this.state.currentPage),
+ isLoading: false,
+ fileOwner: result.histories[0].creator_email
+ });
+ }
+ }
+
+ loadMore = () => {
+ if (!this.state.isReloadingData) {
+ let currentPage = this.state.currentPage + 1;
+ this.setState({
+ currentPage: currentPage,
+ isReloadingData: true,
+ });
+ seafileAPI.listSdocHistory(docUuid, currentPage, PER_PAGE).then(res => {
+ this.updateResultState(res.data);
+ this.setState({isReloadingData: false});
+ });
+ }
+ }
+
+ renameHistoryVersion = (objID, newName) => {
+ seafileAPI.renameSdocHistory(docUuid, objID, newName).then((res) => {
+ this.setState({
+ historyVersions: this.state.historyVersions.map(item => {
+ if (item.obj_id == objID) {
+ item.name = newName;
+ }
+ return item;
+ })
+ });
}).catch(error => {
const errorMessage = Utils.getErrorMsg(error, true);
this.setState({ isLoading: false, errorMessage: errorMessage });
});
}
- updateHistoryVersions(result, callback) {
- const dataCount = result.data ? result.data.length : 0;
- this.nextCommit = result.next_start_commit || '';
- if (dataCount) {
- const addedHistoryVersions = result.data.map(item => new FileHistory(item));
- this.filePath = addedHistoryVersions[dataCount - 1].path;
- this.oldFilePath = addedHistoryVersions[dataCount - 1].revRenamedOldPath;
- const historyVersions = [ ...this.state.historyVersions, ...addedHistoryVersions ];
- this.setState({ historyVersions: historyVersions, isLoading: false, errorMessage: '' }, () => {
- callback && callback(historyVersions[0], historyVersions[1]);
- });
- return;
- }
- if (this.nextCommit) {
- this.listHistoryVersions(historyRepoID, filePath, this.nextCommit);
- return;
- }
- this.hasMore = false;
- this.setState({ isLoading: false, errorMessage: '' });
- }
-
onScrollHandler = (event) => {
const clientHeight = event.target.clientHeight;
const scrollHeight = event.target.scrollHeight;
const scrollTop = event.target.scrollTop;
const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight);
- if (isBottom && this.hasMore && this.nextCommit) {
+ if (isBottom && this.state.hasMore) {
this.loadMore();
}
}
- loadMore = () => {
- if (this.state.isLoading) return;
- this.setState({ isLoading: true }, () => {
- const currentFilePath = this.oldFilePath || this.filePath;
- this.listHistoryVersions(historyRepoID, currentFilePath, this.nextCommit);
- });
- }
-
- restoreVersion = (historyVersion) => {
- const { commitId, path } = historyVersion;
- editUtilities.revertFile(path, commitId).then(res => {
+ restoreVersion = (currentItem) => {
+ const { commit_id, path } = currentItem;
+ editUtilities.revertFile(path, commit_id).then(res => {
if (res.data.success) {
- this.init();
- this.setState({ isLoading: true, historyVersions: [], errorMessage: '' } , () => {
- this.listHistoryVersions(historyRepoID, filePath);
- });
+ this.setState({isLoading: true});
+ this.refershFileList();
}
+ let message = gettext('Successfully restored.');
+ toaster.success(message);
}).catch(error => {
const errorMessage = Utils.getErrorMsg(error, true);
toaster.danger(gettext(errorMessage));
@@ -111,13 +131,13 @@ class SidePanel extends Component {
return;
}
const { historyVersions } = this.state;
- const historyVersionIndex = historyVersions.findIndex(item => item.commitId === historyVersion.commitId);
+ const historyVersionIndex = historyVersions.findIndex(item => item.commit_id === historyVersion.commit_id);
this.props.onSelectHistoryVersion(historyVersion, historyVersions[historyVersionIndex + 1]);
}
copyHistoryFile = (historyVersion) => {
- const { path, revFileId, ctime } = historyVersion;
- seafileAPI.sdocCopyHistoryFile(historyRepoID, path, revFileId, ctime).then(res => {
+ const { path, obj_id, ctime_format } = historyVersion;
+ seafileAPI.sdocCopyHistoryFile(historyRepoID, path, obj_id, ctime_format).then(res => {
let message = gettext('Successfully copied %(name)s.');
let filename = res.data.file_name;
message = message.replace('%(name)s', filename);
@@ -157,13 +177,14 @@ class SidePanel extends Component {
{historyVersions.map((historyVersion, index) => {
return (
);
})}
@@ -179,7 +200,7 @@ class SidePanel extends Component {
onShowChanges = () => {
const { isShowChanges, currentVersion } = this.props;
const { historyVersions } = this.state;
- const historyVersionIndex = historyVersions.findIndex(item => item.commitId === currentVersion.commitId);
+ const historyVersionIndex = historyVersions.findIndex(item => item.commit_id === currentVersion.commit_id);
const lastVersion = historyVersions[historyVersionIndex + 1];
this.props.onShowChanges(!isShowChanges, lastVersion);
}
diff --git a/seahub/seadoc/apis.py b/seahub/seadoc/apis.py
index d281724d07..c26a36acac 100644
--- a/seahub/seadoc/apis.py
+++ b/seahub/seadoc/apis.py
@@ -3,6 +3,7 @@ import json
import logging
import requests
import posixpath
+from datetime import datetime
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -24,11 +25,17 @@ from seahub.seadoc.utils import is_valid_seadoc_access_token, get_seadoc_upload_
gen_seadoc_image_parent_path, get_seadoc_asset_upload_link, get_seadoc_asset_download_link, \
can_access_seadoc_asset
from seahub.utils.file_types import SEADOC, IMAGE
-from seahub.utils import get_file_type_and_ext, normalize_file_path, PREVIEW_FILEEXT, \
+from seahub.utils import get_file_type_and_ext, normalize_file_path, PREVIEW_FILEEXT, get_file_history, \
gen_inner_file_get_url, gen_inner_file_upload_url
from seahub.tags.models import FileUUIDMap
from seahub.utils.error_msg import file_type_error_msg
from seahub.utils.repo import parse_repo_perm
+from seahub.utils.file_revisions import get_file_revisions_within_limit
+from seahub.seadoc.models import SeadocHistoryName
+from seahub.avatar.templatetags.avatar_tags import api_avatar_url
+from seahub.base.templatetags.seahub_tags import email2nickname, \
+ email2contact_email
+from seahub.utils.timeutils import utc_datetime_to_isoformat_timestr, timestamp_to_isoformat_timestr
logger = logging.getLogger(__name__)
@@ -353,3 +360,147 @@ class SeadocCopyHistoryFile(APIView):
'file_name': new_file_name,
'file_path': new_file_path,
})
+
+
+class SeadocHistory(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle, )
+
+ def _get_new_file_history_info(self, ent, avatar_size, name_dict):
+ info = {}
+ creator_name = ent.op_user
+ url, is_default, date_uploaded = api_avatar_url(creator_name, avatar_size)
+ info['creator_avatar_url'] = url
+ info['creator_email'] = creator_name
+ info['creator_name'] = email2nickname(creator_name)
+ info['creator_contact_email'] = email2contact_email(creator_name)
+ info['ctime'] = utc_datetime_to_isoformat_timestr(ent.timestamp)
+ info['size'] = ent.size
+ info['obj_id'] = ent.file_id
+ info['commit_id'] = ent.commit_id
+ info['old_path'] = ent.old_path if hasattr(ent, 'old_path') else ''
+ info['path'] = ent.path
+ info['name'] = name_dict.get(ent.file_id, '')
+ return info
+
+ def get(self, request, file_uuid):
+ """list history, same as NewFileHistoryView
+ """
+ uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid)
+ if not uuid_map:
+ error_msg = 'seadoc uuid %s not found.' % file_uuid
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ repo_id = uuid_map.repo_id
+ username = request.user.username
+ path = posixpath.join(uuid_map.parent_path, uuid_map.filename)
+
+ # permission check
+ if not check_folder_permission(request, repo_id, path):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ # resource check
+ repo = seafile_api.get_repo(repo_id)
+ if not repo:
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+ commit_id = repo.head_cmmt_id
+
+ try:
+ avatar_size = int(request.GET.get('avatar_size', 32))
+ page = int(request.GET.get('page', 1))
+ per_page = int(request.GET.get('per_page', 25))
+ except ValueError:
+ avatar_size = 32
+ page = 1
+ per_page = 25
+
+ # Don't use seafile_api.get_file_id_by_path()
+ # if path parameter is `rev_renamed_old_path`.
+ # seafile_api.get_file_id_by_path() will return None.
+ file_id = seafile_api.get_file_id_by_commit_and_path(repo_id,
+ commit_id, path)
+ if not file_id:
+ error_msg = 'File %s not found.' % path
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # get repo history limit
+ try:
+ history_limit = seafile_api.get_repo_history_limit(repo_id)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ start = (page - 1) * per_page
+ count = per_page
+ try:
+ file_revisions, total_count = get_file_history(repo_id, path, start, count, history_limit)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ name_dict = {}
+ obj_id_list = [commit.file_id for commit in file_revisions]
+ if obj_id_list:
+ name_queryset = SeadocHistoryName.objects.list_by_obj_ids(
+ doc_uuid=file_uuid, obj_id_list=obj_id_list)
+ name_dict = {item.obj_id: item.name for item in name_queryset}
+ data = [self._get_new_file_history_info(ent, avatar_size, name_dict) for ent in file_revisions]
+ result = {
+ "histories": data,
+ "page": page,
+ "total_count": total_count
+ }
+ return Response(result)
+
+ def post(self, request, file_uuid):
+ """rename history
+ """
+ username = request.user.username
+ obj_id = request.data.get('obj_id', '')
+ new_name = request.data.get('new_name', '')
+ if not obj_id:
+ error_msg = 'obj_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+ if not new_name:
+ error_msg = 'new_name invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid)
+ if not uuid_map:
+ error_msg = 'seadoc uuid %s not found.' % file_uuid
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ repo_id = uuid_map.repo_id
+ username = request.user.username
+ path = posixpath.join(uuid_map.parent_path, uuid_map.filename)
+
+ # permission check
+ if not check_folder_permission(request, repo_id, path):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ # resource check
+ repo = seafile_api.get_repo(repo_id)
+ if not repo:
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ token = seafile_api.get_fileserver_access_token(repo_id,
+ obj_id, 'download', username)
+ if not token:
+ error_msg = 'history %s not found.' % obj_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # main
+ SeadocHistoryName.objects.update_name(file_uuid, obj_id, new_name)
+
+ return Response({
+ 'obj_id': obj_id,
+ 'name': new_name,
+ })
diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py
new file mode 100644
index 0000000000..f84c2bc6da
--- /dev/null
+++ b/seahub/seadoc/models.py
@@ -0,0 +1,33 @@
+from django.db import models
+
+
+class SeadocHistoryNameManager(models.Manager):
+ def update_name(self, doc_uuid, obj_id, name):
+ if self.filter(doc_uuid=doc_uuid, obj_id=obj_id).exists():
+ obj = self.filter(doc_uuid=doc_uuid, obj_id=obj_id).update(name=name)
+ else:
+ obj = self.create(doc_uuid=doc_uuid, obj_id=obj_id, name=name)
+ return obj
+
+ def list_by_obj_ids(self, doc_uuid, obj_id_list):
+ return self.filter(doc_uuid=doc_uuid, obj_id__in=obj_id_list)
+
+
+class SeadocHistoryName(models.Model):
+ doc_uuid = models.CharField(max_length=36, db_index=True)
+ obj_id = models.CharField(max_length=40)
+ name = models.CharField(max_length=255)
+
+ objects = SeadocHistoryNameManager()
+
+ class Meta:
+ db_table = 'history_name'
+ unique_together = ('doc_uuid', 'obj_id')
+
+ def to_dict(self):
+ return {
+ 'id': self.pk,
+ 'doc_uuid': self.doc_uuid,
+ 'obj_id': self.obj_id,
+ 'name': self.name,
+ }
diff --git a/seahub/seadoc/urls.py b/seahub/seadoc/urls.py
index e6d1bb5d59..956956aee4 100644
--- a/seahub/seadoc/urls.py
+++ b/seahub/seadoc/urls.py
@@ -1,6 +1,6 @@
from django.urls import re_path
from .apis import SeadocAccessToken, SeadocUploadLink, SeadocDownloadLink, SeadocUploadFile, \
- SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile
+ SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile, SeadocHistory
urlpatterns = [
re_path(r'^access-token/(?P[-0-9a-f]{36})/$', SeadocAccessToken.as_view(), name='seadoc_access_token'),
@@ -10,4 +10,5 @@ urlpatterns = [
re_path(r'^upload-image/(?P[-0-9a-f]{36})/$', SeadocUploadImage.as_view(), name='seadoc_upload_image'),
re_path(r'^download-image/(?P[-0-9a-f]{36})/(?P.*)$', SeadocDownloadImage.as_view(), name='seadoc_download_image'),
re_path(r'^copy-history-file/(?P[-0-9a-f]{36})/$', SeadocCopyHistoryFile.as_view(), name='seadoc_copy_history_file'),
+ re_path(r'^history/(?P[-0-9a-f]{36})/$', SeadocHistory.as_view(), name='seadoc_history'),
]
diff --git a/seahub/settings.py b/seahub/settings.py
index 223e3da8af..1b8f9fe551 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -275,6 +275,7 @@ INSTALLED_APPS = [
'seahub.organizations',
'seahub.krb5_auth',
'seahub.django_cas_ng',
+ 'seahub.seadoc',
]
diff --git a/sql/mysql.sql b/sql/mysql.sql
index 9274c276ee..be7c8e69b4 100644
--- a/sql/mysql.sql
+++ b/sql/mysql.sql
@@ -1375,3 +1375,13 @@ CREATE TABLE `organizations_orgadminsettings` (
UNIQUE KEY `organizations_orgadminsettings_org_id_key_a01cc7de_uniq` (`org_id`,`key`),
KEY `organizations_orgadminsettings_org_id_4f70d186` (`org_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE `history_name` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `doc_uuid` varchar(36) NOT NULL,
+ `obj_id` varchar(40) NOT NULL,
+ `name` varchar(255) NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `history_name_doc_uuid` (`doc_uuid`),
+ UNIQUE KEY `history_name_doc_uuid_obj_id` (`doc_uuid`, `obj_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;