if(!window.docAppData) window.docAppData = {
lastAppUrl: '',
originalDocumentTitle: document.title,
documentTitleSuffix: ' - ' + document.title.split(' - ').pop(),
currentUser: null,
currentSpaceType: '',
userPrivs: {},
};
const docAppData = window.docAppData;
let savingDocData = {};
let docBasicModal = {};
let currentDocApp = null;
window.setZentaoSlashMenu = function(menus, title, position)
{
if(!menus?.length) return;
const slashMenus = menus.map(menu =>
{
if(menu.subMenu)
{
menu.subMenu = menu.subMenu.map(subMenu => {
subMenu.action = function (context) {
zui.Modal.open({
size: subMenu.modalSize ? subMenu.modalSize : 'lg',
url: $.createLink(subMenu.module, subMenu.method, subMenu.params)
});
}
return subMenu;
});
}
else
{
if(menu.key == 'gantt' && !menu.isModal)
{
menu.action = function (context) {
const doc = getDocApp().doc;
$.ajaxSubmit({
url: $.createLink(menu.module, menu.method, menu.params),
data: {templateID: doc.data.id}
});
}
}
else
{
menu.action = function (context) {
zui.Modal.open({
size: menu.modalSize ? menu.modalSize : 'lg',
url: $.createLink(menu.module, menu.method, menu.params)
});
}
}
}
return menu;
});
slashMenus.unshift({ groupName: title});
zui.Editor.setSlashMenu(slashMenus, {before: position, reset: true});
zui.Editor.setGlobalConfig({slashMenuStyles: [`link:${$.libRoot}zui.zentao.css`]});
}
/**
* 更多菜单操作。
* More menu action.
*
* @param {MoreMenuItem} menu
*/
window.moreMenuAction = function(menu)
{
if (menu.module && menu.method && menu.params)
{
if(menu.key == 'gantt' && !menu.isModal)
{
const doc = getDocApp().doc;
$.ajaxSubmit({
url: $.createLink(menu.module, menu.method, menu.params),
data: {templateID: doc.data.id}
});
}
else {
zui.Modal.open({
size: menu.modalSize ? menu.modalSize : 'lg',
url: $.createLink(menu.module, menu.method, menu.params)
});
}
}
}
/* 获取 ZentaoList 块属性。 */
function getZentaoListProps(type, blockID, props)
{
if(props && props.isTemplate) blockID = '__TML_ZENTAOLIST__' + blockID;
const zentaoListLang = getLang('zentaoList');
const exportUrl = `${location.origin}${$.createLink('doc', 'ajaxExportZentaoList', `blockID=${blockID}`)}`;
const title = [(zentaoListLang ? zentaoListLang[type] : type), getLang('list')].join(config.clientLang.includes('zh') ? '' : ' ');
const fetcher = `${location.origin}${$.createLink('doc', 'zentaoList', `type=${type}&blockID=${blockID}`)}`;
return $.extend({}, props,
{
title: title,
fetcher: fetcher,
clearBeforeLoad: false
}, type === 'gantt' ? {
fetcher: fetcher,
export: [{type: 'link', props: {text: title, href: exportUrl}}]
} : {
exportUrl: exportUrl,
});
}
/* 更新禅道列表。*/
window.updateZentaoList = function(blockID, props)
{
const $block = $('#docApp').find(`.zentao-list[data-id="${blockID}"]`);
if(!$block.length) return false;
getDocApp().editor.$.updateBlock($block[0], props);
};
/* 替换禅道列表。*/
window.replaceZentaoList = function(oldBlockID, newType, newblockID, props)
{
return updateZentaoList(oldBlockID, {content: getZentaoListProps(newType, newblockID, props), renderID: $.guid++});
};
/* 插入禅道列表。*/
window.insertZentaoList = function(type, blockID, props, replaceBlockID)
{
if(Number(replaceBlockID)) return replaceZentaoList(replaceBlockID, type, blockID, props);
getDocApp().editor.$.insertCustom(getZentaoListProps(type, blockID, props));
};
/* 从编辑器删除禅道列表。*/
window.deleteZentaoList = function(blockID)
{
const $block = $('#docApp').find(`.zentao-list[data-id="${blockID}"]`);
if(!$block.length) return false;
getDocApp().editor.$.deleteBlock($block[0]);
};
/* 重新载入禅道列表。*/
window.reloadZentaoList = function(blockID)
{
$('#docApp').find(`.zentao-list[data-id="${blockID}"]`)
.closest('.lazy-content')
.trigger('loadContent');
};
/**
* Get doc app instance.
* 获取文档应用组件实例。
*
* @returns {object}
*/
window.getDocApp = function()
{
if(!currentDocApp)
{
const $docApp = $('#docApp');
const docApp = $docApp.data('zui.DocApp') || $docApp.data('zui.DocViewer');
currentDocApp = docApp ? docApp.$ : null;
}
return currentDocApp;
}
/**
* 获取语言项,如果没有设置 key,则获取整个语言项对象,语言项定义在 module/doc/ui/app.html.php 的 $langData 中。
* Get language item, if key is not set, return the whole language object, language items are defined in $langData in module/doc/ui/app.html.php.
*
* @param {string} key
* @param {array|string|object} args
*/
window.getLang = function(key, args)
{
if(!docAppData.docLangData)
{
const docApp = getDocApp();
docAppData.docLangData = docApp ? docApp.props.langData : {};
}
if(typeof key !== 'string') return docAppData.docLangData;
const value = docAppData.docLangData[key];
if(value === undefined) return;
if(args)
{
args = Array.isArray(args) ? args : [args];
args.unshift(value);
return zui.formatString.apply(null, args);
}
return value;
}
/**
* 检查是否有指定的权限,权限定义在 module/doc/ui/app.html.php 的 $privs 中。
* Check if the specified privilege exists, privileges are defined in $privs in module/doc/ui/app.html.php.
* @param {string} priv
*/
window.hasPriv = function(priv, value)
{
const setting = docAppData.userPrivs[priv];
return value !== undefined ? setting === value : !!setting;
}
/**
* 处理文档应用销毁事件,销毁文档基本信息对话框。
* Handle the doc app destroy event, destroy the doc basic info dialog.
*/
function handleDocAppDestroy()
{
if(docBasicModal.current)
{
docBasicModal.current.destroy();
docBasicModal = {};
}
}
function isApiLib(lib, docApp)
{
if(docApp && docApp.spaceType === 'api') return true;
if(typeof lib === 'number') lib = getDocApp().getLib(lib);
if(!lib)
{
docApp = docApp || getDocApp();
if(!docApp) return false;
if(docApp.spaceType === 'api') return true;
lib = docApp.lib;
}
return lib && lib.data.type === 'api';
}
function isApiDoc(doc)
{
const docApp = getDocApp();
if(docApp && docApp.spaceType === 'api') return true;
if(typeof doc !== 'object' && docApp) doc = docApp.getDoc(doc);
if(doc && doc.api !== undefined) return doc.api;
const id = typeof doc === 'object' ? doc.id : doc;
return String(id).startsWith('api.');
}
/**
* 处理对话框的操作,执行文档应用实例上的方法。
* Process dialog actions, execute methods on the doc app instance.
*
* @param {string|array|object} action
* @param {object} docApp
*/
function processDocAppAction(action, docApp)
{
docApp = docApp || getDocApp();
if(typeof action === 'string')
{
action = {call: action};
}
if(Array.isArray(action))
{
if(typeof action[0] !== 'string')
{
action.forEach((act) => processDocAppAction(act, docApp));
return;
}
action = {call: action[0], args: action.slice(1)};
}
if(!action.mode && action.call.includes(':'))
{
const parts = action.call.split(':');
action.mode = parts[0];
action.call = parts[1];
}
if(action.mode && action.mode !== docApp.mode) return;
const method = zui.deepGet(docApp, action.call);
if(typeof method === 'function') method.apply(docApp, action.args);
}
/**
* 检查空间是否可以进行修改操作,如果不指定空间则判断当前空间。
* Check if the space can be modified, if the space is not specified, check the current space.
*
* @param {object|number} space
*/
window.canModifySpace = function(space)
{
const docApp = getDocApp();
if(!docApp && !space) return false;
space = typeof space !== 'object' ? docApp.getSpace(space) : space;
return space && space.canModify !== false;
}
/*
* 修改 Ajax 表单提交后的默认处理方法,改为处理文档应用的操作。
* Modify the default processing method after Ajax form submission to process the actions of the doc app.
*/
zui.AjaxForm.DEFAULT.onResult = function(res)
{
const docApp = getDocApp();
if(docApp && res.docApp)
{
/* 请求响应对象中的 docApp 属性定义了如何进行后续操作。 */
processDocAppAction(res.docApp, docApp);
delete res.docApp;
delete res.load;
delete res.locate;
delete res.callback;
}
};
let lastViewedSpaceHome = '';
let lastViewedSpace = '';
let lastViewedLib = '';
/**
* 向服务器提交最新访问的文档对象。
* Post last viewed data to server.
*
* @param {object} location Doc App location info.
* @returns void
*/
function postLastViewedToServer(location)
{
if(!location) return;
const postData = {};
const {spaceType, spaceID, libID} = location;
if(spaceType && spaceType !== lastViewedSpaceHome)
{
lastViewedSpaceHome = spaceType;
postData.lastViewedSpaceHome = spaceType;
}
if(spaceID && spaceID !== lastViewedSpace)
{
const realSpaceID = typeof spaceID === 'number'
? spaceID
: spaceID.split('.')[1];
if(typeof realSpaceID === 'number')
{
lastViewedSpace = spaceID;
postData.lastViewedSpace = spaceID;
}
}
if(libID && libID !== lastViewedLib)
{
lastViewedLib = libID;
postData.lastViewedLib = libID;
}
if(Object.keys(postData).length) $.post($.createLink('doc', 'ajaxSetLastViewed'), postData);
}
/**
* 处理文档应用界面切换事件,修改浏览器地址栏 URL 和标题。
* Handle the doc app view switch event, modify the browser address bar URL and title.
*
* @param {string} mode Doc App ui mode.
* @param {object} location Doc App location info.
* @param {object} info Doc App view info.
* @returns
*/
function handleSwitchView(mode, location, info)
{
if(!this.element || this.element.closest('.modal')) return;
const rawModule = config.rawModule.toLowerCase();
const rawMethod = config.rawMethod.toLowerCase();
const formatUrl = this.props.viewModeUrl;
const formatOptions = $.extend({recTotal: 0, recPerPage: 20, page: 1}, location, location.pager,
{
mode : mode,
rawModule : rawModule,
rawMethod : rawMethod,
search : encodeURIComponent(location.search || ''),
noSpace : location.noSpace || 0,
filterType: encodeURIComponent(location.filterType || '')
});
postLastViewedToServer(location);
if(typeof formatUrl === 'function')
{
url = formatUrl.call(this, formatOptions);
}
else if(mode === 'view' || mode === 'diff')
{
url = $.createLink('doc', 'view', `docID=${location.docID}` + (info.data && location.docVersion && location.docVersion !== info.data.version ? `&version=${location.docVersion}` : ''));
}
else
{
url = zui.formatString(formatUrl, formatOptions).replace('&libID=0&moduleID=0&docID=0&mode=home&orderBy=id_desc&recTotal=0&recPerPage=0&pageID=1&filterType=&search=&noSpace=false', '').replace('&libID=0&moduleID=0&browseType=all¶m=&orderBy=&recTotal=0&recPerPage=20&pageID=1&mode=home&docID=0&search=', '');
}
if(url === docAppData.lastAppUrl) return;
if(docAppData.lastAppUrl && !$.apps.getAppUrl().endsWith(url)) $.apps.updateAppUrl(url, info.title ? (info.title + docAppData.documentTitleSuffix) : docAppData.originalDocumentTitle);
docAppData.lastAppUrl = url;
if(docBasicModal.current)
{
docBasicModal.current.destroy();
docBasicModal = {};
}
}
function showDocBasicModal(parentID, docID, isDraft, modalType = 'doc', doc = null)
{
const docApp = getDocApp();
const spaceType = docApp.spaceType;
const spaceID = docApp.spaceID;
const libID = docApp.libID;
const moduleID = docApp.moduleID;
const url = $.createLink('doc', 'setDocBasic', `objectType=${spaceType}&objectID=${spaceID}&libID=${libID}&moduleID=${moduleID}&parentID=${parentID || 0}&docID=${docID || 0}&isDraft=${isDraft ? 'yes' : 'no'}&modalType=${modalType}`);
if(docBasicModal.doc === docID && docBasicModal.current)
{
docBasicModal.current.show();
}
else
{
if(docBasicModal.current) docBasicModal.current.destroy();
docBasicModal.doc = docID;
zui.Modal.open(
{
key : `${config.rawModule}-${config.rawMethod}-${spaceType}-${docID}`,
ref : docBasicModal,
url : url,
destroyOnHide: !docID,
cache : true,
request: {
method: "POST",
data: {
title: doc?.title,
},
error: error => {
if(docBasicModal.current) docBasicModal.current.hide();
showSaveFailedAlert(error);
}
},
onHidden: () => {
if(!docBasicModal.keepOnHide && docBasicModal.current) docBasicModal.current.destroy();
},
$onDestroy: () => {
if(docBasicModal.current) docBasicModal = {};
}
});
}
return new Promise((resolve) => {window.docBasicModalResolver = resolve;});
}
window.beforeSetDocBasicInfo = function(_, form)
{
docBasicModal.keepOnHide = true;
if (window.docBasicModalResolver) window.docBasicModalResolver(new FormData(form));
zui.Modal.query('#setDocBasicForm').hide();
return false;
};
function mergeDocFormData(doc, formData)
{
if(!doc || !formData) return;
const keys = new Set(formData.keys());
for(const key of keys)
{
const values = formData.getAll(key);
if(!values.length) continue;
if(key === 'module' || key === 'lib') doc[key] = +values[0];
else doc[key] = values.length > 1 ? values : values[0];
}
return doc;
}
window.showSaveFailedAlert = function(error)
{
zui.Modal.alert(
{
title: getLang('saveDocFailed'),
content: error instanceof Error ? error.message : String(error),
actions: [
{key: 'close'}
]
});
console.error('Save doc failed:', error);
}
/**
* 向服务器提交新文档。
* Submit new doc to server.
*
* @param {object} doc
* @param {number} spaceID
* @param {number} libID
* @param {number} moduleID
* @param {FormData} formData
* @returns
*/
function submitNewDoc(doc, spaceID, libID, moduleID, formData, afterCreate)
{
const docApp = getDocApp();
if(docApp.isSavingDoc) return;
docApp.isSavingDoc = true;
$(docApp.element).find('[zui-command^="saveDoc"],[zui-command^="saveNewDoc"]').attr('disabled', true);
const spaceType = docApp.signals.spaceType.value;
const url = $.createLink('doc', 'create', `objectType=${spaceType}&objectID=${Math.max(spaceID, 0)}&libID=${libID}&moduleID=${moduleID}`);
const docData =
{
rawContent : doc.content,
content : doc.html,
status : doc.status || 'normal',
contentType: doc.contentType,
type : doc.type || 'text',
lib : libID,
module : moduleID,
parent : doc.parent,
title : doc.title,
keywords : doc.keywords,
acl : 'private',
space : spaceType,
uid : (doc.uid || `doc${doc.id}`),
template : doc.template
};
if(formData) mergeDocFormData(docData, formData);
const getErrorMessage = (res) => {
if(typeof res.message === 'string') return res.message;
if(typeof res.message === 'object' && res.message && Object.values(res.message).length)
{
for(const x of Object.values(res.message))
{
if(typeof x === 'string') return x;
if(Array.isArray(x) && x.length) return x[0];
}
}
if(typeof data.error === 'string') return data.error;
return getLang('errorOccurred');
};
return new Promise((resolve, reject) =>
{
$.post(url, docData, (res) =>
{
try
{
const data = JSON.parse(res);
if(!checkResponse(data)) return;
if(typeof data !== 'object' || data.result === 'fail')
{
throw new Error(getErrorMessage(data));
}
const newDoc = $.extend(doc, {id: data.id}, docData, data.doc, {status: doc.status || data.status});
if (!newDoc.space.includes('.')) {
newDoc.space = `${spaceType}.${docData[spaceType]}`;
}
resolve(newDoc);
if(afterCreate)
{
docApp.load(null, null, null, {noLoading: true, picks: 'doc'}).then(() => {
afterCreate(newDoc);
});
}
}
catch (error)
{
showSaveFailedAlert(error);
reject(error);
}
}).fail(error => {
resolve(false);
showSaveFailedAlert(error);
}).complete(() => {
docApp.isSavingDoc = false;
$(docApp.element).find('[zui-command^="saveDoc"],[zui-command^="saveNewDoc"]').removeAttr('disabled');
});
});
}
function submitEditDoc(doc, formData)
{
const url = $.createLink('doc', 'edit', `docID=${doc.id}`);
const docApp = getDocApp();
const docData = {status: 'normal', type: 'chapter'};
if(formData) mergeDocFormData(docData, formData);
return new Promise((resolve) => {
$.post(url, docData, (res) =>
{
try
{
const data = JSON.parse(res);
if(!checkResponse(data)) return;
if(typeof data !== 'object' || data.result === 'fail')
{
let message = data.message || data.error || getLang('errorOccurred');
if(typeof message === 'object') message = Object.values(message).map(x => Array.isArray(x) ? x.join('\n') : x).join('\n');
throw new Error(message);
}
resolve(doc);
}
catch (error)
{
resolve(false);
showSaveFailedAlert(error);
if(!error.message) return;
}
}).fail(error => {
resolve(false);
showSaveFailedAlert(error);
}).complete(() => {
docApp.load(null, null, null, {noLoading: true, picks: 'doc'});
});
});
}
/**
* 处理创建文档的操作请求,向服务器发送请求并返回创建的文档对象。
* Handle the create doc operation request, send a request to the server and return the created doc object.
*/
function handleCreateDoc(doc, spaceID, libID, moduleID)
{
return showDocBasicModal(0, 0, doc.status === 'draft', 'doc', doc).then((formData) => {
return submitNewDoc(doc, spaceID, libID, moduleID, formData);
});
}
/**
* 处理保存文档的操作请求,向服务器发送请求并返回保存的文档对象。
* Handle the save doc operation request, send a request to the server and return the saved doc object.
*/
function handleSaveDoc(doc)
{
const docApp = getDocApp();
if(docApp.isSavingDoc) return;
docApp.isSavingDoc = true;
$(docApp.element).find('[zui-command^="saveDoc"],[zui-command^="saveNewDoc"]').attr('disabled', true);
const spaceType = doc.object ? doc.object.type : (doc.objectType || docApp.signals.spaceType.value);
const libID = doc.libInfo ? doc.libInfo.id : (doc.lib || docApp.signals.libID.value);
const moduleID = doc.module || docApp.signals.moduleID.value;
const url = $.createLink('doc', 'edit', `docID=${doc.id}`);
const docData = {
rawContent : doc.content,
status : doc.status || 'normal',
contentType: doc.contentType,
type : 'text',
lib : libID,
module : moduleID,
parent : doc.parent,
title : doc.title,
keywords : doc.keywords,
acl : doc.acl,
content : doc.html,
space : spaceType,
uid : (doc.uid || `doc${doc.id}`),
};
if(Array.isArray(doc.files)) docData.files = doc.files.map(f => typeof f === 'object' ? f.id : f).join(',');
if(doc.fromVersion) docData.fromVersion = doc.fromVersion;
if(savingDocData[doc.id])
{
mergeDocFormData(docData, savingDocData[doc.id]);
delete savingDocData[doc.id];
}
return new Promise((resolve) => {
$.post(url, docData, (res) =>
{
try
{
const data = JSON.parse(res);
if(!checkResponse(data)) return;
if(typeof data !== 'object' || data.result === 'fail')
{
let message = data.message || data.error || getLang('errorOccurred');
if(typeof message === 'object') message = Object.values(message).map(x => Array.isArray(x) ? x.join('\n') : x).join('\n');
throw new Error(message);
}
doc = $.extend({}, doc, docData, data.doc);
if(docApp.spaceType == 'quick')
{
resolve(false);
const libType = docApp.lib.data.quickType;
return loadPage($.createLink('doc', 'quick', `type=${libType}&docID=${doc.id}`));
}
const libID = +doc.lib;
const lib = docApp.getLib(libID);
if(!lib)
{
resolve(false);
return loadPage($.createLink('doc', 'view', `docID=${doc.id}`));
}
delete doc.title;
delete doc.keywords;
delete doc.content;
resolve(doc);
}
catch (error)
{
resolve(false);
showSaveFailedAlert(error);
if(!error.message) return;
}
}).fail(error => {
resolve(false);
showSaveFailedAlert(error);
}).complete(() => {
docApp.isSavingDoc = false;
$(docApp.element).find('[zui-command^="saveDoc"],[zui-command^="saveNewDoc"]').removeAttr('disabled');
const {id, acl, users, groups, addedBy} = doc;
let isInEditGroups = false;
for(let groupID of Object.values(userGroups))
{
if(groups.includes(groupID)) isInEditGroups = true;
}
if(acl == 'private' && !users.includes(docAppData.currentUser) && addedBy !== docAppData.currentUser && !isInEditGroups)
{
if(docApp.hasEditingDoc)
{
docApp.cancelEditDoc().then(() => {
docApp.delete('doc', id);
docApp.load(null, null, null, {noLoading: true, picks: 'doc'});
});
}
else
{
docApp.delete('doc', id);
docApp.load(null, null, null, {noLoading: true, picks: 'doc'});
}
}
});
});
}
/**
* 检查给定的文档是否可以进行移动操作。
* Check if the given doc can be moved.
*/
function canMoveDoc(doc)
{
return !String(doc.id).startsWith('api.') && getDocApp().props.privs.moveDoc;
}
/**
* 处理删除文档文件的请求。
* Handle the request to delete the doc file.
* @param {object} file File info object.
* @param {object} doc Doc info object.
*/
function deleteDocFile(file, doc)
{
zui.Modal.confirm(getLang('fileConfirmDelete')).then(result =>
{
if(!result) return;
$.ajaxSubmit(
{
url: $.createLink('doc', 'deleteFile', `docID=${doc.id}&fileID=${file.id}&confirm=yes`),
load: false,
callback: null,
onSuccess: (res) =>
{
if(res && typeof res === 'object' && res.result === 'success')
{
getDocApp().update('doc', $.extend({}, doc, {files: doc.files.filter(f => f.id !== file.id)}));
zui.HistoryPanel.update({objectType: 'doc', objectID: doc.id});
}
}
})
});
}
/**
* 处理重命名文件的请求。
* Handle the request to rename the file.
*
* @param {object} file
* @param {object} doc
*/
function renameDocFile(file, doc)
{
let fileName = file.title;
let extension = file.extension;
if(extension && fileName.endsWith(`.${extension}`)) fileName = fileName.substring(0, fileName.length - extension.length - 1);
zui.Modal.prompt({message: getLang('fileRename'), defaultValue: fileName, placeholder: fileName}).then(newName =>
{
if(!newName) return;
fileName = newName;
if(extension && fileName.endsWith(`.${extension}`))
{
fileName = fileName.substring(0, fileName.length - extension.length - 1);
}
else if(!extension && fileName.includes('.'))
{
extension = fileName.split('.').pop();
fileName = fileName.substring(0, fileName.length - extension.length - 1);
}
$.ajaxSubmit(
{
url: $.createLink('file', 'edit', `fileID=${file.id}`),
data: {fileName: newName, extension : file.extension},
load: false,
onComplete: (res) =>
{
if(!res || typeof res !== 'object') return;
const updatedFile = $.extend({}, file, {title: extension ? `${fileName}.${extension}` : fileName, extension}, res.id ? res : {});
getDocApp().update('doc', $.extend({}, doc, {files: doc.files.map(f => f.id === file.id ? updatedFile : f)}));
}
});
});
}
async function importContentToEditor(type)
{
const docApp = getDocApp();
if(!docApp) return;
const editor = docApp.editor.$;
if(!editor) return;
let content = '';
if(navigator.clipboard) content = await navigator.clipboard.readText();
content = await zui.Modal.prompt({size: 'lg', message: `Import ${type}`, defaultValue: content, placeholder: 'Paste content here...', multiline: true});
return getDocApp().importDoc(content, type);
}
/* Import markdown or json file to editor. */
function importFileToEditor(type, options)
{
if(options && options.fromText) return importContentToEditor(type);
const accept = type === 'markdown' ? '.md' : (type === 'confluence' ? '.html,.txt' : '.json');
return zui.selectFile(accept).then(file => {
if(!file) return;
zui.readFile(file).then(content => {
if(typeof content !== 'string' || !content.length) return;
return getDocApp().importDoc(content, type, file.name.split('.').shift());
});
});
}
function isOpenVersion(version) {
return /^\d/.test(version || $('#zuiCSS').attr('href').split('?v=').pop());
}
/**
* 定义文档各个试图和 UI 元素的上的操作方法。
* Define the operation methods on the views and UI elements of the doc.
*/
const actionsMap =
{
/**
* 定义首页的操作按钮。
* Define the actions on the home page.
*
* @returns {array}
*/
home: function()
{
return [
hasPriv('createSpace') ? {icon: 'plus', btnType: 'primary', text: getLang('createSpace'), command: 'createSpace'} : null,
];
},
/**
* 定义空间的操作按钮。
* Define the actions of space.
*/
space: function(info)
{
const lang = getLang();
const items = [];
const space = info.data; // Space info object.
/* 获取侧边栏没有文档库时的操作按钮。 Get actions when sidebar no lib. */
if(info.ui === 'sidebar-no-lib')
{
if(!canModifySpace(space) || !hasPriv('createLib')) return null;
return [{icon: 'plus', text: lang.createLib, command: 'createLib', btnType: 'primary-pale'}];
}
if(hasPriv('editSpace')) items.push({text: lang.actions.editSpace, command: `editSpace/${space.id}`});
if(hasPriv('deleteSpace')) items.push({text: lang.actions.deleteSpace, command: `deleteSpace/${space.id}`});
if(!items.length) return;
return [
{type: 'dropdown', icon: 'cog-outline', square: true, caret: false, placement: info.ui === 'space-card' ? 'bottom-end' : 'top-end', items: items},
];
},
/**
* 定义文档的操作按钮。
* Define the actions of doc.
*/
doc: function(info)
{
const lang = getLang();
const doc = info.data;
const canModify = canModifySpace();
const isApi = isApiDoc(doc);
const isTemplate = doc.templateType !== '';
const canEditDoc = canModify && hasPriv(isApi ? 'editApi' : 'edit');
const isChapter = doc.type === 'chapter';
/* 侧边栏上的文档操作按钮。Doc actions in sidebar. */
if(info.ui === 'sidebar')
{
if(!canModify) return [];
const moreItems =
[
hasPriv('addChapter') ? {icon: 'plus', text: lang.addSubChapter, command: `addChapter/${doc.id}`} : null,
hasPriv('addChapter') && doc.parent != 0 ? {icon: 'plus', text: lang.addSameChapter, command: `addChapter/${doc.parent}`} : null,
!isTemplate && hasPriv('create') ? {icon: 'plus', text: lang.addSubDoc, command: `startCreateDoc/${doc.id}`} : null,
!isChapter && canMoveDoc(doc) ? {icon: 'folder-move', text: lang.moveDoc, command: `moveDoc/${doc.id}`} : null,
!doc.frozen && !isChapter && hasPriv('delete') ? {icon: 'trash', text: lang.delete, command: `deleteDoc/${doc.id}`} : null,
isChapter && hasPriv('editChapter') ? {icon: 'pencil', text: lang.editChapter, command: `editChapter/${doc.id}`} : null,
isChapter && hasPriv('deleteChapter') ? {icon: 'trash', text: lang.deleteChapter, command: `deleteDoc/${doc.id}`} : null,
];
return [
(!isChapter && hasPriv('sortDoc')) || (isChapter && hasPriv('sortChapter')) ? {icon: 'move cursor-move', className: 'sort-handler', hint: lang.sortDoc, size: 'xs'} : null,
moreItems.every(item => item === null) ? null : {icon: 'ellipsis-v', caret: false, placement: 'bottom-end', size: 'xs', items: moreItems},
];
}
const moreItems = [];
if(canModify && canMoveDoc(doc)) moreItems.push({icon: 'folder-move', text: lang.moveDoc, command: `moveDoc/${doc.id}`});
if(!doc.frozen && canModify && hasPriv('delete')) moreItems.push({icon: 'trash', text: lang.delete, command: `deleteDoc/${doc.id}`});
if(!isApi && canModify && hasPriv('effort')) moreItems.push({icon: 'time', text: lang.effort, command: `effortDoc/${doc.id}`});
if(!isApi && !isTemplate)
{
const canExportWord = hasPriv('exportDoc');
if(canExportWord && (doc.contentType === 'doc' || doc.contentType === 'markdown'))
{
const exportItems = [
{text: lang.exportWord, command: 'exportWord'},
config.debug > 5 ? {text: lang.exportPdf, command: 'exportDoc/pdf'} : null,
config.debug > 5 ? {text: lang.exportImage, command: 'exportDoc/png'} : null,
{text: lang.exportMarkdown, command: 'exportDoc/markdown'},
{text: lang.exportHtml, command: 'exportDoc/html'},
{text: lang.exportJSON, command: 'exportDoc/json'},
].filter(Boolean);
moreItems.push({icon: 'export', text: lang.export, items: exportItems});
}
else if(canExportWord)
{
moreItems.push({icon: 'export', text: lang.export, command: 'exportWord'});
}
}
if (isApi) {
const canExportApi = hasPriv('exportApi');
if (canExportApi) {
moreItems.push({icon: 'export', text: lang.export, command: 'exportApi'});
}
}
const docApp = this;
const isOpen = isOpenVersion(config.version);
if(!isOpen && docApp && docApp.mode === 'view' && doc.status != 'draft' && !doc.api && !isTemplate)
{
moreItems.push({icon: 'link', text: getLang('relateObject'), className: 'relateObject-btn', command: 'toggleViewSideTab/relateObject'})
}
const isToolbar = info.ui === 'toolbar';
const isInModal = $(this.element).closest('.modal').length;
let aiAction = null;
let aiTeammateAction = null;
const docAIPrompts = window.docAIPrompts;
if(!isApi && docAIPrompts && docAIPrompts.length)
{
const aiLang = window.docAIPromptLang;
const aiActionItems = docAIPrompts.map(prompt => ({icon: 'magic', text: prompt.status !== 'active' ? zui.jsx`${prompt.name}${aiLang.statuses[prompt.status]}` : prompt.name, command: `executeAIPrompt/${prompt.id}/${doc.id}`, hint: prompt.desc}));
if(aiActionItems.length === 1) aiAction = aiActionItems[0];
else aiAction = {icon: 'lightning', text: aiLang.dropdownTitle || 'AI', type: 'dropdown', btnType: 'ai-styled', size: 'sm', caret: 'down', placement: 'bottom-end', items: aiActionItems};
const aiTeammateLang = window.docAITeammateLang;
const aiTeammates = window.docAITeammates;
if(aiTeammates.length)
{
const aiTeammateItems = aiTeammates.map(teammate => {
const name = aiTeammateLang.nameLabel.replace('%s', teammate.name);
return {text: zui.jsx` ${name}`, hint: name, command: `assignAITeammate/${teammate.id}/${doc.id}`,};
});
aiTeammateAction = {icon: 'hand-right', text: aiTeammateLang.dropdownTitle, type: 'dropdown', btnType: 'ai-styled', size: 'sm', caret: 'down', placement: 'bottom-end', items: aiTeammateItems};
}
}
const actions = isInModal ? [] : [
aiAction,
aiTeammateAction,
!isApi && canViewAITask ? {hint: lang.aiTask, icon: 'task-list', command: `viewAITasks/${doc.id}`} : null,
isToolbar ? {hint: docApp.fullscreen ? lang.exitFullscreen : lang.enterFullscreen, icon: docApp.fullscreen ? 'fullscreen-exit' : 'fullscreen', command: 'toggleFullscreen'} : null,
canEditDoc ? {icon: 'edit', class: doc.editable ? null : 'disabled', type: doc.editable ? 'ghost text-primary' : 'ghost text-gray', hint: doc.editable ? lang.edit : lang.needEditable, rounded: 'lg', command: doc.editable ? 'startEditDoc' : null} : null,
(isToolbar && docApp.props.showDocOutline !== false) ? {hint: lang.docOutline, icon: 'list-box', command: 'toggleViewSideTab/outline'} : null,
(isToolbar && docApp.props.showDocHistory !== false) ? {hint: lang.history, icon: 'file-log', command: 'toggleViewSideTab/history'} : null,
moreItems.length ? {icon: 'icon-ellipsis-v', type: 'dropdown', rounded: 'lg', placement: 'bottom-end', caret: false, items: moreItems} : null,
];
return actions;
},
/**
* 定义文档库的操作按钮。
* Define the actions of modules.
*/
lib: function(info)
{
const docApp = getDocApp();
const lang = getLang();
const items = [];
const lib = info.data;
const canModify = canModifySpace() || docApp === null;
const canAddModule = canModify && hasPriv('addModule');
/* 获取侧边栏没有模块时的操作按钮。 Get actions when sidebar no module. */
if(info.ui === 'sidebar-no-module')
{
if(!canAddModule) return;
return [{text: lang.actions.addModule, command: `addModule/${lib.id}/0/${lib.id}/child`, icon: 'plus', type: 'primary-pale'}];
}
if(canAddModule && info.ui !== 'space-card' && info.ui !== 'sidebar') items.push({text: lang.actions.addModule, command: `addModule/${lib.id}/0/${lib.id}/child`});
if(canModify && hasPriv('editLib')) items.push({text: lang.actions.editLib, command: `editLib/${lib.id}`});
if(canModify && lib.type !== 'api' && hasPriv('moveLib') && info.ui !== 'space-card' && info.ui !== 'sidebar-toolbar') items.push({text: lang.moveTo, command: `moveLib/${lib.id}`});
if(canModify && hasPriv('deleteLib')) items.push({text: lang.actions.deleteLib, command: `deleteLib/${lib.id}`});
if(!items.length) return;
if(info.ui === 'sidebar')
{
return [
hasPriv('sortDoclib') ? {icon: 'move cursor-move', className: 'sort-handler', hint: lang.sortDoclib, size: 'xs'} : null,
items.length ? {icon: 'ellipsis-v', caret: false, placement: 'bottom-end', size: 'xs', items: items} : null,
];
}
return [
{type: 'dropdown', icon: info.ui === 'space-card' ? 'ellipsis-v' : 'cog-outline', square: true, caret: false, placement: info.ui === 'space-card' ? 'bottom-end' : 'top-end', items: items},
];
},
/**
* 定义目录的操作按钮。
* Define the actions on the lib page.
*/
module: function(info)
{
const lang = getLang();
const items = [];
const module = info.data;
if(hasPriv('addModule')) items.push({text: lang.actions.addSameModule, command: `addModule/${module.lib}/${module.parent || module.lib}/${module.id}/same`}, {text: lang.actions.addSubModule, command: `addModule/${module.lib}/${module.id}/${module.id}/child`});
if(hasPriv('editModule')) items.push({text: lang.actions.editModule, command: `editModule/${module.id}`});
if(hasPriv('deleteModule')) items.push({text: lang.actions.delModule, command: `deleteModule/${module.id}`});
if(info.ui === 'sidebar')
{
return [
hasPriv('sortModule') ? {icon: 'move cursor-move', className: 'sort-handler', hint: lang.sortLib, size: 'xs'} : null,
items.length ? {icon: 'ellipsis-v', caret: false, placement: 'bottom-end', size: 'xs', items: items} : null,
];
}
return items;
},
/**
* 定义文档表格操作栏的操作按钮。
* Define the actions on the doc table footer.
*/
'doc-table': function(info)
{
const docApp = this;
return [hasPriv('batchMoveDoc') ? {
text: getLang('batchMove'),
onClick: function()
{
/* Get all selected doc id list. */
const selections = info.dtable.getChecks();
if(!selections.length) return;
const docIdList = window.btoa(JSON.stringify(selections));
const spaceType = docApp.spaceType;
const spaceID = docApp.spaceID;
const libID = docApp.libID;
const moduleID = docApp.moduleID;
const url = $.createLink('doc', 'batchMoveDoc', 'type=' + spaceType + '&docIdList=' + docIdList + '&spaceID=' + spaceID + '&libID=' + libID + '&moduleID=' + moduleID);
zui.Modal.open({size: 'sm', url: url});
}
} : null];
},
/**
* 定义文档列表的操作按钮。
* Define the actions on the doc list.
*/
'doc-list': function(info)
{
const docApp = this;
const lang = getLang();
const canModify = canModifySpace();
const libID = docApp.libID;
const docID = docApp.docID;
if(isApiLib(docApp.lib))
{
if(!canModify) return;
const canCreateApi = hasPriv('createApi');
const command = 'startCreateDoc/' + docID + '/api';
if(info.ui === 'doc-list-empty')
{
return canCreateApi ? [{icon: 'plus', text: lang.createApi, btnType: 'primary', command}] : null;
}
const canCreateLib = hasPriv('createLib');
const items =
[
canCreateApi ? {icon: 'plus', text: lang.createApi, command} : null,
(canCreateApi && canCreateLib) ? {type: 'divider'} : null,
canCreateLib ? {icon: 'wiki-lib', text: lang.createLib, command: 'createLib'} : null,
].filter(Boolean);
const canExportApi = hasPriv('exportApi') && libID;
return [
canExportApi ? {type: 'divider', style: {margin: '6px 0'}} : null,
canExportApi ? {icon: 'export', text: lang.export, command: 'exportApi'} : null,
items.length ? {icon: 'plus', type: 'dropdown', btnType: 'primary', size: 'md', text: lang.create, items: items} : null,
];
}
const canCreateDoc = canModify && hasPriv('create');
const canCreateLib = canModify && hasPriv('createLib');
const canCreateSpace = canModify && hasPriv('createSpace');
const canCreateOffice = canModify && hasPriv('createOffice');
const canUploadDoc = canModify && hasPriv('uploadFile');
const canExportDoc = hasPriv('exportDoc') && docApp.spaceType !== 'quick';
/* 文档列表没有文档时的按钮。Actions for empty doc list. */
if(info.ui === 'doc-list-empty')
{
return canCreateDoc ? [{icon: 'plus', text: lang.createDoc, btnType: 'primary', command: `startCreateDoc/${docID}`}] : null;
}
const items = canModify ?
[
canCreateDoc ? {icon: 'plus', text: lang.createDoc, command: `startCreateDoc/${docID}`} : null,
canUploadDoc ? {icon: 'import', text: lang.uploadDoc, command: 'uploadDoc'} : null,
canCreateDoc ? {type: 'divider'} : null,
canCreateOffice ? {icon: 'file-word', text: lang.createList.word, command: 'startCreateOffice/word'} : null,
canCreateOffice ? {icon: 'file-powerpoint', text: lang.createList.ppt, command: 'startCreateOffice/ppt'} : null,
canCreateOffice ? {icon: 'file-excel', text: lang.createList.excel, command: 'startCreateOffice/excel'} : null,
(canCreateOffice && (canCreateLib || canCreateSpace)) ? {type: 'divider'} : null,
canCreateSpace ? {icon: 'cube', text: lang.createSpace, command: 'createSpace'} : null,
canCreateLib ? {icon: 'wiki-lib', text: lang.createLib, command: 'createLib'} : null,
].filter(Boolean) : [];
return [
((canExportDoc && libID > 0) || canCreateDoc) ? {type: 'divider', style: {margin: '6px 0'}} : null,
canExportDoc && libID > 0 ? {icon: 'export', text: lang.export, command: 'exportWord'} : null, // 不在库中,无法导出
items.length ? {icon: 'plus', type: 'dropdown', btnType: 'primary', size: 'md', text: lang.create, items: items} : null,
];
},
/**
* 定义文档编辑时的操作按钮。
* Define the actions on toolbar of the doc editing page.
*/
'doc-edit': function(info)
{
const doc = info.data;
if(!doc) return;
const lang = getLang();
if(isApiLib())
{
return [
{text: lang.save, size: 'md', className: 'btn-wide', type: 'primary', command: 'saveApiDoc'},
{text: lang.cancel, size: 'md', className: 'btn-wide', type: 'primary-outline', command: 'cancelEditDoc'},
];
}
const moreItems = [];
if(!enableHocuspocus && (doc.contentType === 'doc' || doc.contentType === 'markdown') && !isApiDoc(doc))
{
if(config.debug > 5) moreItems.push({text: lang.importConfluence, command: 'importFile/confluence'});
moreItems.push(
{text: lang.importMarkdown, command: 'importFile/markdown'},
{text: lang.importJSON, command: 'importFile/json'},
{type: 'divider'},
{text: lang.exportJSON, command: 'exportDoc/json'},
);
}
const isToolbar = info.ui === 'toolbar';
const docApp = this;
return [
isToolbar ? {hint: docApp.fullscreen ? lang.exitFullscreen : lang.enterFullscreen, icon: docApp.fullscreen ? 'fullscreen-exit' : 'fullscreen', command: 'toggleFullscreen'} : null,
(isToolbar && docApp.props.showDocOutline !== false) ? {hint: lang.docOutline, icon: 'list-box', command: 'toggleViewSideTab/outline'} : null,
(isToolbar && docApp.props.showDocHistory !== false) ? {hint: lang.history, icon: 'file-log', command: 'toggleViewSideTab/history'} : null,
doc.status === 'draft' ? {text: lang.saveDraft, size: 'md', className: 'btn-wide', type: 'secondary', command: 'saveDoc/draft'} : null,
{text: lang.release, size: 'md', className: 'btn-wide', type: 'primary', command: 'saveDoc'},
{text: lang.cancel, size: 'md', className: 'btn-wide', type: 'primary-outline', command: 'cancelEditDoc'},
{text: lang.settings, size: 'md', type: 'ghost', command: `showDocSettingModal/${doc.id}/${doc.contentType}/1`, icon: 'cog-outline'},
moreItems.length ? {icon: 'icon-ellipsis-v', type: 'dropdown', rounded: 'lg', placement: 'bottom-end', caret: false, items: moreItems} : null,
].filter(Boolean);
},
/**
* 定义文档创建时的操作按钮。
* Define the actions on toolbar of the doc creating page.
*/
'doc-create': function(info)
{
const lang = getLang();
if(isApiLib())
{
return [
{text: lang.save, size: 'md', className: 'btn-wide', type: 'primary', command: 'saveApiDoc'},
{text: lang.cancel, size: 'md', className: 'btn-wide', type: 'primary-outline', command: 'cancelCreateDoc'},
];
}
const moreItems = [];
const creatingDoc = this.signals.creatingDoc.value;
if(creatingDoc && creatingDoc.contentType === 'doc')
{
moreItems.push(
{text: lang.importMarkdown, command: 'importFile/markdown'},
{text: lang.importJSON, command: 'importFile/json'},
{type: 'divider'},
{text: lang.exportJSON, command: 'exportDoc/json'},
);
}
const isToolbar = info.ui === 'toolbar';
const docApp = this;
return [
isToolbar ? {hint: docApp.fullscreen ? lang.exitFullscreen : lang.enterFullscreen, icon: docApp.fullscreen ? 'fullscreen-exit' : 'fullscreen', command: 'toggleFullscreen'} : null,
(isToolbar && docApp.props.showDocOutline !== false) ? {hint: lang.docOutline, icon: 'list-box', command: 'toggleViewSideTab/outline'} : null,
{text: lang.saveDraft, size: 'md', className: 'btn-wide', type: 'secondary', command: 'saveNewDoc/draft'},
{text: lang.release, size: 'md', className: 'btn-wide', type: 'primary', command: 'saveNewDoc'},
{text: lang.cancel, size: 'md', className: 'btn-wide', type: 'primary-outline', command: 'cancelCreateDoc'},
moreItems.length ? {icon: 'icon-ellipsis-v', type: 'dropdown', rounded: 'lg', placement: 'bottom-end', caret: false, items: moreItems} : null,
];
},
/**
* 定义文件的操作按钮。
* Define the actions of file.
*/
file: function(info)
{
const doc = info.doc;
const file = info.data;
const docApp = getDocApp();
const isReadonly = docApp.mode === 'view';
const privs = docApp.props.privs;
const lang = getLang();
const canEdit = !isReadonly && privs.edit && (!doc.privs || doc.privs.edit !== false) && privs.editFile;
const canDelete = !isReadonly && privs.edit && (!doc.privs || doc.privs.edit !== false) && privs.deleteFile;
const canDownload = privs.downloadFile;
let previewAction = null;
let isOfficeFile = 'doc|docx|xls|xlsx|ppt|pptx|pdf'.includes(file.extension);
let canPreview = privs.previewFile && 'txt|jpg|jpeg|gif|png|bmp|mp4'.includes(file.extension);
if(privs.previewFile && typeof libreOfficeTurnon !== 'undefined' && libreOfficeTurnon && isOfficeFile) canPreview = true;
if(canPreview)
{
previewAction = {url: $.createLink('file', 'download', `fileID=${file.id}&mouse=left`), hint: lang.filePreview, icon: 'eye', target: '_blank'};
if(!isOfficeFile)
{
previewAction['data-toggle'] = 'modal';
previewAction['data-size'] = 'lg';
delete previewAction.target;
}
}
return [
previewAction,
canDownload ? {target: '_blank', url: zui.formatString(docApp.props.fileUrl, file), hint: lang.fileDownload, icon: 'download'} : null,
canEdit ? {hint: lang.fileRename, icon: 'pencil-alt', onClick: renameDocFile.bind(this, file, doc)} : null,
canDelete ? {hint: lang.fileDelete, icon: 'trash', onClick: deleteDocFile.bind(this, file, doc)} : null,
].filter(Boolean);
},
'file-list': function(info)
{
return [hasPriv('exportFiles') ? {icon: 'export', text: getLang('export'), command: 'exportFiles'} : null];
}
};
function getActions(type, info)
{
const builder = actionsMap[type];
if(builder) return builder.call(this, info);
}
/**
* 定义界面操作命令。文档中的大部分操作按钮都会调用这里定义的命令。
* Define the UI commands. Most of the action buttons in the doc will call the commands defined here.
*/
const commands =
{
/* 创建文档。 Create doc. */
startCreateDoc: function(_, args)
{
const docApp = getDocApp();
if(!docApp.libList.length) return zui.Modal.alert(getLang('createLibFirst'));
const docType = (args && args[1]) || 'doc';
const {spaceType} = docApp;
if(spaceType === 'api' || docType === 'api') return docApp.startCreateDoc();
const templateID = args?.[1] ?? 0;
const {spaceID, libID, moduleID} = docApp;
return showDocBasicModal(args?.[0] ?? 0, 0, true, docType).then((formData) => {
zui.Editor.loadModule().then(() => {
const emptyDoc = zui.Editor.createEmptyDoc();
const snapshot = zui.Editor.toSnapshot(emptyDoc);
const doc = {contentType: 'doc', status: 'draft', content: JSON.stringify(snapshot), template: templateID};
return submitNewDoc(doc, spaceID, libID, moduleID, formData, (newDoc) => {
docApp.selectDoc(newDoc.id);
docApp.startEditDoc(newDoc.id);
});
});
});
},
startEditDoc: function(_, [doc], options)
{
const docApp = getDocApp();
doc = doc || docApp._doc.value?.data;
if (typeof doc !== 'object') {
doc = docApp._treeMap.value.docs.get(doc)?.data;
}
if(doc.frozen) return zui.Modal.alert(getLang('frozenTips').replace('%s', getLang('edit')));
/** 如果是附件类型并且没有内容的文档,则跳转到上传文档页面进行编辑。 */
if(doc.type === 'attachment' && !doc.hasContent) {
const type = docApp.lib ? docApp.lib.data.type : docApp.spaceType;
const url = $.createLink('doc', 'uploadDocs', `docID=${doc.id}&objectType=${type}&objectID=${docApp.spaceID}&libID=${doc.lib}&moduleID=${doc.module}&type=attachment`);
zui.Modal.open({url: url});
return false;
}
return docApp.startEditDoc(doc, options);
},
/** 上传文档。Upload Doc. */
uploadDoc: function(_, args)
{
const docApp = getDocApp();
const docID = args[0] || docApp.docID
const type = docApp.lib ? docApp.lib.data.type : docApp.spaceType;
const url = $.createLink('doc', 'uploadDocs', `docID=${docID}&objectType=${type}&objectID=${docApp.spaceID}&libID=${docApp.libID}&moduleID=${docApp.moduleID}&type=attachment`);
zui.Modal.open({url: url});
},
/** 创建 Office 文件。 Start create office file. */
startCreateOffice: function(_, args)
{
const type = args[0];
const docApp = getDocApp();
const url = $.createLink('doc', 'create', `objectType=${docApp.spaceType}&objectID=${docApp.spaceID}&libID=${docApp.libID}&moduleID=${docApp.moduleID}&type=${type}`);
zui.Modal.open({url: url});
},
createSpace: function()
{
const docApp = getDocApp();
const params = docApp.spaceType == 'mine' ? 'type=mine' : '';
const url = $.createLink('doc', 'createSpace', params);
zui.Modal.open({size: 'sm', url: url});
},
editSpace: function(_, args)
{
const docApp = getDocApp();
const spaceID = args[0] || docApp.spaceID;
const url = $.createLink('doc', 'editSpace', `libID=${spaceID}`);
zui.Modal.open({size: 'sm', url: url});
},
deleteSpace: function(_, args)
{
const docApp = getDocApp();
const spaceID = args[0] || docApp.spaceID;
$.ajaxSubmit(
{
confirm: getLang('confirmDeleteSpace'),
url: $.createLink('doc', 'deleteSpace', `libID=${spaceID}`),
load: false,
onSuccess: function(res)
{
if(res && res.result === 'success')
{
const docApp = getDocApp();
if(spaceID === docApp.spaceID && docApp.mode !== 'home')
{
const spaceList = docApp.spaceList.filter(x => x.id !== spaceID);
if(spaceList.length)
{
docApp.selectSpace(spaceList[0].id);
return;
}
docApp.switchView({spaceID: 0, libID: 0, moduleID: 0, docID: 0}, 'home');
}
docApp.delete('space', spaceID);
}
}
});
},
createLib: function(_, args)
{
const docApp = getDocApp();
const spaceID = args[0] || docApp.spaceID;
const url = $.createLink('doc', 'createLib', spaceID ? `type=${docApp.spaceType}&objectID=${spaceID}` : `type=${docApp.spaceType}`);
zui.Modal.open({size: 'sm', url: url});
},
moveLib: function(_, args)
{
const docApp = getDocApp();
const libID = args[0] || docApp.libID;
const url = $.createLink('doc', 'moveLib', `libID=${libID}`);
zui.Modal.open({size: 'sm', url: url});
},
editLib: function(_, args)
{
const docApp = getDocApp();
const libID = args[0] || docApp.libID;
const url = $.createLink(isApiLib(libID) ? 'api' : 'doc', 'editLib', `libID=${libID}`);
zui.Modal.open({size: 'sm', url: url});
},
deleteLib: function(_, args)
{
const docApp = getDocApp();
const libID = args[0] || docApp.libID;
$.ajaxSubmit(
{
confirm: getLang('confirmDeleteLib'),
url: $.createLink('doc', 'deleteLib', `libID=${libID}`),
load: false,
onSuccess: function()
{
const docApp = getDocApp();
docApp.delete('lib', libID);
if(docApp.mode === 'home') docApp.loadHomeLibs();
}
});
},
addModule: function(_, args)
{
zui.Modal.prompt({message: getLang('moduleName')}).then(newMoudleName =>
{
if(!newMoudleName) return;
const docApp = getDocApp();
const libID = args[0] || docApp.libID;
const parentID = args[1] || 0;
const objectID = args[2] || 0;
const createType = args[3] || 'child';
const data =
{
name: newMoudleName,
libID: libID,
parentID: parentID,
objectID: objectID,
moduleType: isApiLib(libID) ? 'api' : 'doc',
isUpdate: false,
createType: createType,
};
$.ajaxSubmit(
{
load: false,
url: $.createLink('tree', 'ajaxCreateModule'),
data: data,
onSuccess: (res) =>
{
if(!res.module) return;
const docApp = getDocApp();
docApp.update('module', res.module);
if(!docApp.hasEditingDoc) docApp.selectModule(res.module.id);
}
})
});
},
editModule: function(_, args)
{
const docApp = getDocApp();
const moduleID = args[0] || docApp.moduleID;
const url = $.createLink('doc', 'editCatalog', `moduleID=${moduleID}&type=doc`);
zui.Modal.open({size: 'sm', url: url});
},
deleteModule: function(_, args)
{
const docApp = getDocApp();
const moduleID = args[0] || docApp.moduleID;
const moduleInfo = docApp.treeMap.modules.get(+moduleID);
$.ajaxSubmit(
{
confirm: getLang(moduleInfo.modules.length > 0 || moduleInfo.docs.length > 0 ? 'confirmDeleteModuleWithSub' : 'confirmDeleteModule'),
url: $.createLink('doc', 'deleteCatalog', `moduleID=${moduleID}`),
load: false,
onSuccess: function(res)
{
if(!res) return;
if(res.result === 'success')
{
const docApp = getDocApp();
const moduleInfo = docApp.treeMap.modules.get(+moduleID);
if(moduleInfo)
{
const parent = moduleInfo.parent;
docApp.changeState(
{
libID: parent.type === 'lib' ? parent.data.id : docApp.libID,
module: parent.type === 'lib' ? 0 : parent.data.id,
})
}
else if(docApp.mode === 'list')
{
docApp.selectLib(docApp.libID, 0);
}
docApp.delete('module', moduleID);
}
else if(res.message)
{
zui.Modal.alert(res.message);
}
}
});
},
collectDoc: function(_, args)
{
const docApp = getDocApp();
const docID = args[0] || docApp.docID;
$.post($.createLink('doc', 'collect', `objectID=${docID}&objectType=doc`), {}, function(res)
{
if(typeof res == 'string') res = JSON.parse(res);
if(!checkResponse(res)) return;
const collected = res.status === 'yes';
const doc = docApp.getDoc(docID);
getDocApp().update('doc', $.extend(doc, {isCollector: collected, collects: collected ? doc.collects + 1 : doc.collects - 1}));
});
},
addChapter: function(_, args)
{
const docApp = getDocApp();
const {spaceID, libID, moduleID} = docApp;
return showDocBasicModal(args[0], 0, false, 'chapter').then((formData) => {
const doc = {type: 'chapter', status: 'normal', parent: args[0]};
return submitNewDoc(doc, spaceID, libID, moduleID, formData).then(() => {
docApp.load(null, null, null, {noLoading: false, picks: 'doc'});
if(docBasicModal.current)
{
docBasicModal.current.destroy();
docBasicModal = {};
}
});
});
},
editChapter: function(_, args)
{
const docApp = getDocApp();
const {spaceID, libID, moduleID} = docApp;
return showDocBasicModal(0, args[0], false, 'chapter').then((formData) => {
return submitEditDoc({id: args[0]}, formData);
});
},
moveDoc: function(_, args)
{
const docApp = getDocApp();
const docID = args[0] || docApp.docID;
const libID = docApp.libID;
const spaceType = docApp.spaceType;
const spaceID = docApp.spaceID;
const url = $.createLink('doc', 'moveDoc', `docID=${docID}&libID=${libID}&spaceType=${spaceType}&space=${spaceID}`);
zui.Modal.open({size: 'sm', url: url});
},
handleMovedDoc(_, args)
{
const docID = +args[0];
const space = args[1];
const spaceID = +(space.includes('.') ? space.split('.').pop() : space);
const docApp = getDocApp();
if(docApp.mode === 'view')
{
if(spaceID === docApp.spaceID) docApp.loadDoc(docID).then(() => docApp.selectDoc(docID));
else if(space.startsWith(docApp.spaceType)) docApp.loadDoc(docID).then(() => docApp.selectLib(docApp.libID));
else loadPage($.createLink('doc', 'view', `docID=${docID}`));
}
else
{
docApp.load(null, null, null, {noLoading: true, picks: 'doc'});
}
},
startConvertDoc: function(_, args)
{
const docApp = getDocApp();
const docID = args[0] || docApp.docID;
const lang = getLang();
zui.Modal.confirm({title: lang.convertToNewDoc, content: lang.convertToNewDocConfirm}).then(result =>
{
if(!result) return;
docApp.startEditDoc(docID, {convert: true});
});
},
deleteDoc: function(_, args)
{
const docApp = getDocApp();
const docID = args[0] || docApp.docID;
const isApi = isApiDoc(docID);
const docInfo = docApp.treeMap.docs.get(docID)
const doc = docInfo.data;
const isChapter = doc?.type === 'chapter';
$.ajaxSubmit(
{
confirm: getLang(isChapter ? (docInfo.docs.length > 0 ? 'confirmDeleteChapterWithSub' : 'confirmDeleteChapter') : (docInfo.docs.length > 0 ? 'confirmDeleteWithSub' : 'confirmDelete')),
url: $.createLink(isApi ? 'api' : 'doc', 'delete', `${isApi ? 'apiID' : 'docID'}=${isApi ? getApiID(docID) : docID}`),
load: false,
onSuccess: function()
{
getDocApp().delete('doc', docID);
}
});
},
effortDoc: function(_,args)
{
const docApp = getDocApp();
const docID = args[0] || docApp.docID;
const url = $.createLink('effort', 'createForObject', `objectType=doc&objectID=${docID}`);
zui.Modal.open({url: url, size: 'sm'});
},
exportWord: function(_, args)
{
const docApp = getDocApp();
const type = docApp.spaceType;
const libID = args[0] || docApp.libID;
const moduleID = args[1] || docApp.moduleID;
const docID = args[2] || docApp.docID;
const url = $.createLink('doc', `${type}2export`, `libID=${libID}&moduleID=${moduleID}&docID=${docID}&objectID=${docApp.spaceID}`);
if(docID)
{
window.open(url, '_self');
return;
}
zui.Modal.open({url: url, size: 'sm'});
},
exportApi: function(_, args)
{
const docApp = getDocApp();
const libID = args[0] || docApp.libID;
const release = args[1] || docApp.signals.libReleaseMap.value[libID] || 0;
const moduleID = args[2] || docApp.moduleID;
let docID = args[3] || docApp.docID;
if(typeof docID === 'string') docID = getApiID(docID);
const url = $.createLink('api', 'export', `libID=${libID}&version=0&release=${release}&moduleID=${moduleID}${docID ? `&apiID=${docID}` : ''}`);
if(docID)
{
window.open(url, '_self');
return;
}
zui.Modal.open({url: url, size: 'sm'});
},
importFile: function(info, args)
{
const type = args[0] || 'markdown';
const docApp = getDocApp();
const editor = docApp.editor.$;
const altKey = info.event.altKey;
if(editor.isEmpty) return importFileToEditor(type, {fromText: altKey});
zui.Modal.confirm({message: getLang('importConfirm')})
.then((result) => result && importFileToEditor(type, {fromText: altKey}));
},
showDocSettingModal: function(_, args)
{
const docApp = getDocApp();
const doc = docApp.doc;
const docID = args[0] || doc.id;
const docType = args[1] || doc.contentType;
const saveEdited = args[2] || 0;
showDocBasicModal(0, docID).then(formData => {
savingDocData[docID] = formData;
if(saveEdited == 1)
{
const docData = {
content : doc.data.content,
status : doc.data.status,
contentType: doc.data.contentType,
type : doc.data.type,
lib : doc.data.lib,
module : doc.data.module,
title : doc.data.title,
keywords : doc.data.keywords,
acl : doc.data.acl,
space : doc.data.space,
uid : doc.data.uid,
};
mergeDocFormData(docData, formData);
}
});
},
/**
* 保存 API 文档数据。
* Save the API doc data.
*/
saveApiDoc: function()
{
/* 触发 API 表单的提交事件。 */
$('#docApp .doc-view form').trigger('submit');
},
/**
* 加载指定的 API 文档。
* Load the specified API doc.
*/
loadApi: function(_, args)
{
const apiID = getApiID(args[0] || this.docID);
const version = args[1] || 0;
const release = args[2] || 0;
const select = !!args[3];
$.getJSON($.createLink('api', 'ajaxGetApi', `apiID=${apiID}&version=${version}&release=${release}`), function(result)
{
if(!result || typeof result !== 'object') return;
result.id = `api.${apiID}`;
const docApp = getDocApp();
docApp.update('doc', result);
if(select) docApp.selectDoc(result.id);
});
},
/**
* 选择 API 文档。
* Select the specified API doc.
*/
selectApi: function(_, args)
{
const apiID = args[0];
getDocApp().selectDoc(`api.${getApiID(apiID)}`);
},
/**
* 显示结构列表。
* Show the struct list.
*/
showStructs: function()
{
getDocApp().changeState(
{
mode: 'list',
moduleID: 0,
docID: 0,
listType: 'structs',
});
},
/**
* 显示版本列表。
* Show the release list.
*/
showReleases: function()
{
getDocApp().changeState(
{
mode: 'list',
moduleID: 0,
docID: 0,
listType: 'releases',
});
},
/**
* 显示API 目录。
* Show the api catalog.
*/
showModules: function()
{
getDocApp().changeState(
{
mode: 'list',
moduleID: 0,
docID: 0,
listType: '',
});
},
/**
* 获取库的 API 列表。
* Get the API list of the library.
*/
loadLibApi: function(_, args)
{
const docApp = getDocApp();
const libReleaseMap = docApp.signals.libReleaseMap.value;
const libID = args[0] || docApp.libID;
if(!libID) return;
const releaseID = args[1] || libReleaseMap[libID] || 0;
const url = $.createLink('api', 'ajaxGetLibApiList', `libID=${libID}&releaseID=${releaseID}`);
$.getJSON(url, function(data)
{
const lib = docApp.getLib(libID);
if(lib && lib.docs && lib.docs.length) docApp.delete('doc', lib.docs.map(x => x.data.id));
data.forEach((x) => x.id = `api.${x.id}`);
docApp.update('doc', data);
});
},
/**
* 更改当前库的发布版本。
* Change the release of the current library.
*/
changeLibRelease: function(_, args)
{
const docApp = getDocApp();
const libID = args[0];
const release = args[1];
const libReleaseMap = docApp.signals.libReleaseMap.value;
if(libReleaseMap[libID] === release) return;
libReleaseMap[libID] = release;
docApp.signals.libReleaseMap.value = $.extend({}, libReleaseMap);
docApp.executeCommand('loadLibApi', [libID, release]);
const doc = docApp.doc;
if(doc && doc.data.lib === libID) docApp.executeCommand('loadApi', [doc.data.id, 0, release]);
},
/**
* 加载懒加载内容。
* Load the lazy content.
*/
loadLazyContent: function(_, args)
{
const selector = args[0];
if(!selector) return;
$(selector).closest('.lazy-content').trigger('loadContent');
},
/**
* 更新懒加载内容。
* Update the lazy content.
*/
updateLazyContent: function(context, args)
{
const event = context.event;
const $element = $(event.currentTarget);
const selector = args[0] || $element.data('lazyTarget');
const $lazy = (selector ? $(selector) : $element).closest('.lazy-content');
const url = $element.data('url') || $element.attr('href');
if(url) $lazy.trigger('loadContent', url);
event.preventDefault();
event.stopPropagation();
},
/**
* 执行禅道智能体。
* Execute the zentao AI prompt.
*/
executeAIPrompt: function(_, args)
{
const promptID = args[0];
const docID = args[1];
const url = $.createLink('ai', 'promptExecute', `promptId=${promptID}&objectId=${docID}&auto=0`);
$.ajaxSubmit({url: url, method: 'POST'});
},
/**
* 批量导出附件库中的附件。
* Batch export attachments in the attachment library.
*/
exportFiles: function(_, args)
{
const docApp = getDocApp();
const objectID = docApp.spaceID;
const objectType = docApp.spaceType;
const url = $.createLink('doc', 'exportfiles', `objectID=${objectID}&objectType=${objectType}`);
zui.Modal.open({url: url, size: 'sm'});
},
/*
* 委派数字员工任务。
* Assign the ai task to the ai teammate.
*/
assignAITeammate: function(_, args)
{
const teammateID = args[0];
const docID = args[1];
const pageInfo = 'doc,view';
loadModal($.createLink('aiteammate', 'assignagent', `teammateID=${teammateID}&objectType=doc&objectID=${docID}&pageInfo=${pageInfo}`), null, {size: 'sm'});
},
/**
* 查看数字员工任务。
* View the ai tasks.
*/
viewAITasks: function(_, args)
{
const docID = args[0];
loadModal($.createLink('aitask', 'browse', `teammate=0&browseType=&involved=0&orderBy=id_desc¶m=0&recTotal=0&recPerPage=20&pageID=1&objectType=doc&objectID=${docID}`));
}
};
function getApiID(id)
{
if(typeof id === 'number') return id;
if(String(+id) === id) return +id;
return +id.replace('api.', '');
}
/**
* 获取列表类型。
* Get the list type.
*
* @param {object} docApp
* @returns {string}
*/
function getListType(docApp)
{
docApp = docApp || getDocApp();
if(docApp.mode !== 'list' || docApp.module) return '';
const listType = docApp.signals.listType.value;
return listType;
}
/**
* 定义页面上的自定义渲染器。
* Define the custom renderers on the page.
*/
const customRenders =
{
/**
* 定义 API 文档编辑器渲染,包括查看、编辑和创建。
* Define the API doc editor render, including view, edit and create.
*/
editor: function()
{
const mode = this.mode;
if(!isApiLib())
{
const docInfo = this.doc;
const doc = docInfo ? docInfo.data : null;
if (mode === 'diff') {
const {
diffVersions,
spaceType,
} = this.signals;
const versions = diffVersions ? diffVersions.value : [];
if (versions.length === 2) {
return {
fetcher: $.createLink('doc', 'ajaxDiff', `&objectType=${spaceType.value}&docID=${doc.id}&newVersion=${versions[0]}&oldVersion=${versions[1]}`),
};
}
}
if(mode === 'view' && !hasPriv('view')) return {html: `
${getDocAppLang('accessDenied')}
`}; return; } if(mode === 'create') { const libID = this.libID; const moduleID = this.moduleID; return {fetcher: $.createLink('api', 'create', `libID=${libID}&moduleID=${moduleID}&space=api`), loadingText: getDocAppLang('loading')}; } if(mode === 'edit') { const doc = this.doc.data; return {fetcher: $.createLink('api', 'edit', `apiID=${getApiID(doc.id)}`), loadingText: getDocAppLang('loading')}; } const doc = this.doc.data; if(!hasPriv('viewApi')) return {html: `${getDocAppLang('accessDenied')}
`}; const release = this.signals.libReleaseMap.value[doc.lib] || 0; const version = this.signals.docVersion.value || 0; return {fetcher: $.createLink('api', 'view', `libID=${doc.lib}&apiID=${getApiID(doc.id)}&moduleID=${doc.module}&version=${version}&release=${release}`), loadingText: getDocAppLang('loadingDocsData'), className: 'doc-editor-content'}; }, /** * 定义 API 文档列表渲染,包括结构和版本。 * Define the API doc list render, including structs and releases. */ list: function() { if(!isApiLib()) return; const listType = getListType(this); const libID = getApiID(this.libID); const releaseID = this.signals.libReleaseMap.value[libID] || 0; if(listType === 'structs') return {fetcher: $.createLink('api', 'struct', `libID=${this.libID}&releaseID=${releaseID}`), loadingText: getDocAppLang('loading'), className: 'api-struct-list'}; if(listType === 'releases') return {fetcher: $.createLink('api', 'releases', `libID=${this.libID}`), loadingText: getDocAppLang('loading'), className: 'api-release-list'}; }, /** * 定义 API 文档列表筛选菜单渲染,当展示结构和版本时不显示筛选菜单。 * Define the API doc list filter menu render, not show the filter menu when showing structs and releases. */ filters: function() { if(!isApiLib(null, this)) return; const listType = getListType(this); if(this.mode !== 'list' || !listType) return; return null; }, /** * 定义 API 文档列表工具栏渲染。 * Define the API doc list toolbar render. */ toolbar: function() { if(!isApiLib()) return; const listType = getListType(this); if(this.mode === 'list' && listType) { const libID = getApiID(this.libID); const items = []; if(hasPriv('createStruct') && listType === 'structs') items.push({text: getDocAppLang('createStruct'), icon: 'plus', btnType: 'primary', 'data-toggle': 'modal', 'data-size': 'lg', url: $.createLink('api', 'createStruct', `libID=${libID}`)}); if(hasPriv('createRelease') && listType === 'releases') items.push({text: getDocAppLang('createRelease'), icon: 'plus', btnType: 'primary', 'data-toggle': 'modal', url: $.createLink('api', 'createRelease', `libID=${libID}`)}); if(!items.length) return null; return {component: 'toolbar', props: {items: items}}; } }, /** * 定义顶部面包屑导航渲染。 * Define the top breadcrumb render. */ 'app-nav': function(items) { const lib = this.lib; if(!lib || !isApiLib(lib)) return items; const versions = lib.data.versions; if(versions && versions.length) { const viewIndex = items.findIndex(item => item[0] === 'lib'); if(viewIndex >= 0) { const libView = items[viewIndex][1]; const libID = getApiID(lib.data.id); const release = this.signals.libReleaseMap.value[libID] || 0; const options = [{text: getDocAppLang('latestVersion'), value: 0, selected: !release, command: `changeLibRelease/${libID}/0`}]; versions.forEach(version => options.push({selected: release === version.id, text: `v${version.version}`, value: version.id, command: `changeLibRelease/${libID}/${version.id}`})); const currentVersion = versions.find(x => x.id === release); const versionPicker = zui.renderCustomContent( { content: { component: 'DropdownButton', props: { text : currentVersion ? currentVersion.version : getDocAppLang('latestVersion'), size : 'xs', type : 'gray-pale', rounded : 'full', className: 'h-4 gap-1 mr-2', items : options, } } }); items[viewIndex] = ['lib', [libView, versionPicker]]; } } const listType = getListType(this); if(this.mode !== 'list' || !listType) return items; if(listType === 'structs') items.push([listType, zui.renderCustomContent({className: 'mx-2', content: getDocAppLang('struct')})]); if(listType === 'releases') items.push([listType, zui.renderCustomContent({className: 'mx-2', content: getDocAppLang('releases')})]); return items; }, /** * 定义侧边栏渲染,显示结构、版本和模块。 * Define the sidebar render, show structs, releases and modules. */ 'sidebar-before': function() { if(!isApiLib()) return; const canViewStructs = docAppHasPriv('struct'); const canViewReleases = docAppHasPriv('releases'); if(!canViewStructs && !canViewReleases) return; const isListMode = this.mode === 'list'; const lang = getDocAppLang(); const listType = getListType(this); const items = []; if(canViewStructs) items.push({text: lang.struct, selected: listType === 'structs' && isListMode, icon: 'treemap muted', command: 'showStructs'}, {type: 'divider', className: 'my-1'}); if(canViewReleases) items.push({text: lang.releases, selected: listType === 'releases' && isListMode, icon: 'version muted', command: 'showReleases'}, {type: 'divider', className: 'my-1'}); items.push({text: lang.module, icon: 'list muted', command: 'showModules'}); return { component: 'tree', className: 'p-2 pb-0 api-lib-menu', props: {items: items, itemProps: {className: 'state'}} }; } }; /** * 获取文档界面上的表格初始化选项。 * Get the table initialization options on the doc UI. * * @param {object} options * @param {object} info * @returns {object} */ function getTableOptions(options, info) { const lang = getLang(); if(info.type === 'doc-list') { const tableCols = lang.tableCols; const canModify = canModifySpace(this.space); options.cols.forEach(col => { if(typeof tableCols[col.name] === 'string') col.title = tableCols[col.name]; if(!config.clientLang.includes('zh')) { if(col.name === 'addedDate' || col.name == 'editedDate') col.width = '120'; } if(col.name === 'actions' && col.actionsMap) { if(!canModify) col.actions = []; col.actions = col.actions.filter(action => hasPriv(action == 'move' ? 'moveDoc' : action)); const actionHints = {edit: lang.editDoc, move: lang.moveDoc, delete: lang.deleteDoc}; const privMap = {edit: hasPriv('edit'), move: hasPriv('moveDoc'), delete: hasPriv('delete')}; $.each(col.actionsMap, (key, value) => { if(typeof actionHints[key] === 'string') value.hint = actionHints[key]; value.disabled = !privMap[key]; }); col.actionItemCreator = function(item, info) { const rowData = info.row.data; item.command = item.command.replace('{id}', String(rowData.id)); if(rowData.frozen && (item.key === 'edit' || item.key === 'trash')) { const actionHint = item.key == 'trash' ? actionHints['delete'] : actionHints[item.key]; item.hint = lang.frozenTips.replace('%s', actionHint); item.disabled = true; } return item; } } }); options.onCheckChange = function(changes) { const checkedList = this.getChecks(); $.cookie.set('checkedItem', checkedList, {expires:config.cookieLife, path:config.webRoot}); } } if(info.type === 'file-list') { const tableCols = lang.fileTableCols; const canExportFiles = hasPriv('exportFiles'); options.cols.forEach(col => { if(typeof tableCols[col.name] === 'string') col.title = tableCols[col.name]; if(col.name == 'objectName') { const onRenderCellBackup = col.onRenderCell; col.onRenderCell = function(result, info) { const file = info.row.data; if(file.rawObjectType == 'stepResult') { const url = $.createLink('testtask', 'results', `runID=0&caseID=${file.objectID}`); result[0] = {html: `${file.objectName}#${file.objectID} ${file.sourceName}`}; return result; } return onRenderCellBackup.call(this,result,info); } } if(col.name === 'id' && canExportFiles) col.checkbox = true; }); if(canExportFiles) { options.checkable = true; options.onCheckChange = function(changes) { const checkedList = this.getChecks(); $.cookie.set('checkedFileItem', checkedList, {expires:config.cookieLife, path:config.webRoot}); } } return options; } if(!hasPriv('batchMoveDoc') && !hasPriv('exportDoc')) { options.checkable = false; options.footer = options.footer.filter(f => f !== 'checkbox' && f !== 'checkedInfo'); } return options; } /** * 获取文档界面上的列表筛选菜单。 * Get the filter menu on the doc UI. * * @param {string} mode * @returns {[key: string, text: string][]} */ function getFilterTypes(mode) { if(mode === 'home') { if(docAppData.currentSpaceType === 'project') return getLang('projectFilterTypes'); if(docAppData.currentSpaceType === 'product') return getLang('productFilterTypes'); return getLang('spaceFilterTypes'); } if(mode === 'list') { const lib = this.lib; if(lib && lib.data.type === 'api') return getLang('apifilterTypes'); return getLang('filterTypes'); } if(mode === 'files') return getLang('fileFilterTypes'); } /** * 检查文档是否符合筛选条件。 * Check if the doc matches the filter condition. * * @param {string} type */ function isMatchFilter(type, filterType, item) { if(type === 'doc') { if(filterType === 'createdByMe') return item.addedBy === docAppData.currentUser; if(filterType === 'editedByMe') return item.editedBy === docAppData.currentUser; if(filterType === 'draft') return item.status === 'draft'; if(filterType === 'collect') return item.isCollector; if(filterType === 'released') return item.status === 'normal'; } if(type === 'project' || type === 'product') { return filterType !== 'mine' || item.isMine; } return true; } /** * 将 [key, ...] 形式的顺序列表转换为 {key: order, ...} 形式。 * Convert the order list in the form of [key, ...] to {key: order, ...}. * * @param {number[]} orders * @returns {object} */ function formatOrders(orders, useIndex) { const orderedKeys = useIndex ? [] : Array.from(orders).sort((a, b) => a - b); return orders.reduce((map, key, index) => { map[key] = useIndex ? index : orderedKeys[index]; return map; }, {}); } /** * 对文档条目进行排序。 * Sort the doc items. * * @param {string} type 'doc' | 'module' | 'lib' | 'space' * @param {object|array} orders {key: order, ...} | [key, ...] */ function sortItems(type, orders) { if(Array.isArray(orders)) orders = formatOrders(orders, type === 'lib'); const sortMethods = {doc: 'sortDoc', module: 'sortCatalog', lib: 'sortDoclib'}; if(!sortMethods[type]) return console.error(`[DocApp] Invalid sort type: ${type}`); const url = $.createLink('doc', sortMethods[type]); return $.ajaxSubmit({ url, data: {orders: JSON.stringify(orders)}, onSuccess: function() { const items = Object.keys(orders).map(id => ({id: +id, order: orders[id]})); getDocApp().update(type, items); } }); } /** * 获取界面上条目排序选项。 * Get the sortable options on the doc UI. * * @param {string} type 'doc' | 'doc-module' | 'module' | 'lib' | 'space' */ function getSortableOptions(type) { const canSortDocModule = hasPriv('sortModule') || hasPriv('sortDoc'); if((type === 'doc-module' && canSortDocModule) || (type === 'lib' && hasPriv('sortDoclib'))) { const getItemType = (key) => key[0] === 'm' ? 'module' : (key[0] === 'l' ? 'lib' : 'doc'); return { sortable: {handle: '.sort-handler'}, canSortTo: function(event) { let fromKey = event.dragged.getAttribute('z-key'); let toKey = event.related.getAttribute('z-key'); if(!fromKey || !toKey) return false; const fromType = getItemType(fromKey); const toType = getItemType(toKey); if (fromType !== toType) return false; return true; }, onSort: function(event, orders) { const fromKey = event.item.getAttribute('z-key'); if(!fromKey || !orders.length) return; const fromType = getItemType(fromKey); const orderedList = []; orders.forEach((key, index) => { const keyType = getItemType(key); if(keyType !== fromType) return; if(fromType !== 'doc') key = key.substring(1); orderedList.push(+key); }); if(fromType === 'doc') { const docApp = getDocApp(); const doc = docApp.getDoc(orderedList[0]); const lib = doc ? docApp.getLib(doc.lib) : null; if(lib && typeof lib.data.orderBy === 'string' && lib.data.orderBy.endsWith('desc')) orderedList.reverse(); } sortItems(fromType, orderedList); } } } } window.checkResponse = function(res) { if(typeof res === 'string') res = JSON.parse(res); if(typeof res !== 'object' || !res) return true; if(res.load === 'login') { if(res.message) zui.Modal.alert(res.message).then(() => $.apps.logout()); else $.apps.logout(); return false; } return true; } /** * 获取文档详情侧边栏标签页定义。 * Get the doc view sidebar tabs. * * @param {object} doc */ window.getDocViewSidebarTabs = function(doc, info) { if(isApiLib()) { const lang = getDocAppLang(); if(info.isNewDoc || info.mode === 'create') return []; return [ {key: 'info', icon: 'info', title: lang.docInfo}, info.mode === 'edit' ? null : {key: 'outline', icon: 'list-box', title: lang.docOutline}, {key: 'history', icon: 'file-log', title: lang.history}, ].filter(Boolean); } let tabList = []; if(info.isNewDoc) { tabList.push({key: 'outline', icon: 'list-box', title: getLang('docOutline')}); } else { tabList.push( {key: 'outline', icon: 'list-box', title: getLang('docOutline')}, {key: 'history', icon: 'file-log', title: getLang('history')} ); } if(info.mode == 'view' && doc.status != 'draft' && !doc.api) { tabList.push( { key : 'relateObject', icon : 'link', title : getLang('relateObject'), render: function(doc){return {fetcher: $.createLink('doc', 'ajaxGetRelatedObjects', `docID=${doc.id}`)}} }); } return tabList; } function getSpaceFetcher(spaceType, spaceID = 0) { if(spaceType === 'product') { return $.createLink('product', 'ajaxGetDropMenu', `productID=${spaceID}&module=doc&method=app&extra=&from=&useLink=0`); } if(spaceType === 'project') { return $.createLink('project', 'ajaxGetDropMenu', `projectID=${spaceID}&module=doc&method=app&extra=&useLink=0`); } } function initOfficePreview(options) { if(options.canPreviewOffice && options.fileInfoUrl) { const officeTypes = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf']); zui.Editor.setGlobalConfig( { canPreviewFile: (file) => { const extension = file.name.split('.').pop(); return officeTypes.has(extension); }, onPreviewFile: (file) => { const doc = getDocApp().doc || {data: {id: 0}}; $.getJSON(zui.formatString(options.fileInfoUrl, { gid: btoa(`g-${file.key}`), objectType: 'doc', objectID: doc.data.id, title: '', extra: '', })).then(res => { if(res.result === 'success' && res.file) { const editable = doc.data.editable !== false; window.open($.createLink('file', 'download', `fileID=${res.file.id}&mouse=left&edit=${editable ? '0' : 'never'}`), '_blank'); } }); } }); } if(options.privs.downloadFile === false) zui.Editor.setGlobalConfig({canDownloadFile: (file) => false}); } /** * 处理导入的文档快照。 * Handle the imported doc snapshot. */ function handleImportDocSnapshot(snapshot) { const forEachBlock = (blocks, callback) => { blocks.forEach(block => { callback(block); if(block.children) forEachBlock(block.children, callback); }); }; const requestType = config.requestType.toUpperCase(); const fixUrl = (url, method, paramList) => { const replaceOrign = url => { try { const urlObj = new URL(url); if(urlObj.origin != location.origin) { urlObj.protocol = location.protocol; urlObj.host = location.host; return urlObj.toString(); } return url; } catch { return url; } }; const link = $.parseLink(replaceOrign(url), 'auto'); if(link.requestType.toUpperCase() === requestType || method.toLowerCase() !== link.methodName.toLowerCase()) return url return `${location.origin}${$.createLink('doc', method, paramList.map((p, i) => [p, link.vars[i][1]]))}`; }; /* Fix custom block with fetcher in difference request type. */ forEachBlock(snapshot.blocks.children, block => { if(block.flavour !== 'affine:embed-zui-custom') return; const content = block.props.content; if(!content || typeof content !== 'object') return; if(content.fetcher) content.fetcher = fixUrl(content.fetcher, 'zentaoList', ['type', 'blockID']); if(content.exportUrl) content.exportUrl = fixUrl(content.exportUrl, 'ajaxExportZentaoList', ['blockID']); }); return snapshot; } /** * 设置文档应用组件选项。 * Set the doc app options. */ window.setDocAppOptions = function(_, options) { docAppData.currentUser = options.currentUser; docAppData.currentSpaceType = options.spaceType; docAppData.userPrivs = options.privs; docAppData.docLangData = options.langData; initOfficePreview(options); return $.extend( { editorProps : {onImportSnapshot: handleImportDocSnapshot}, defaultState : {listType: '', libReleaseMap: {}}, commands : commands, customRenders : customRenders, onCreateDoc : docAppData.userPrivs.create ? handleCreateDoc : null, onSaveDoc : docAppData.userPrivs.edit ? handleSaveDoc : null, canMoveDoc : canMoveDoc, onSwitchView : handleSwitchView, getActions : getActions, getTableOptions : options.docViewer ? null : getTableOptions, getFilterTypes : getFilterTypes, isMatchFilter : isMatchFilter, getSortableOptions : getSortableOptions, getDocViewSidebarTabs: getDocViewSidebarTabs, getSpaceFetcher : getSpaceFetcher, showDocOutline : true, showDocHistory : true, $onDestroy : handleDocAppDestroy, }, window.docAppOptions, options); }; window.docAppActions = actionsMap; window.docAppCommands = commands; window.docAppCustomRenders = customRenders; window.getDocApp = getDocApp; window.getDocAppLang = getLang; window.docAppHasPriv = hasPriv; window.getFileActions = function(file) { let fileActions = []; let canPreview = false; let officeTypes = 'doc|docx|xls|xlsx|ppt|pptx|pdf'; let isOfficeFile = officeTypes.includes(file.extension); let previewExtension = 'txt|jpg|jpeg|gif|png|bmp|mp4'; if(previewExtension.includes(file.extension)) canPreview = true; if(libreOfficeTurnon && isOfficeFile) canPreview = true; if(canPreview) { let previewAction = {icon: 'eye', title: previewLang, url: previewLink.replace('{id}', file.id).replace('\\', ''), className: 'text-primary', target: '_blank'}; if(!isOfficeFile) { previewAction['data-toggle'] = 'modal'; previewAction['data-size'] = 'lg'; delete previewAction.target; } fileActions.push(previewAction); } fileActions.push({icon: 'download', title: downloadLang, url: downloadLink.replace('{id}', file.id).replace('\\', ''), className: 'text-primary', target: '_blank'}); return fileActions; }; window.useHocuspocus = function(app) { if(config.vision !== 'rnd') return false; return enableHocuspocus && app.spaceType !== 'mine'; }; let lastDisconnectMessager = null; window.onHocuspocusConnect = function(app) { if(app.spaceType === 'mine') return; lastDisconnectMessager && lastDisconnectMessager.hide(); lastDisconnectMessager = null; zui.Messager.show({ content: hocuspocusConnect, type: 'success', time: 3000, }); }; window.onHocuspocusDisconnect = function(app) { if(app.spaceType === 'mine') return; lastDisconnectMessager && lastDisconnectMessager.hide(); lastDisconnectMessager = zui.Messager.show({ content: hocuspocusDisconnect, type: 'danger', time: 0, }); }; window.onAuthenticationFailed = function(app) { if(app.spaceType === 'mine') return; zui.Messager.show({ content: needReadable, type: 'danger', time: 3000, }); }; window.onDocAppModeChange = function() { lastDisconnectMessager && lastDisconnectMessager.hide(); lastDisconnectMessager = null; }; window.getPageFormHelper = function() { const docApp = getDocApp(); if(docApp.mode === 'edit' && !isApiDoc(docApp.doc)) { const fields = { title: (_, $scope) => $scope.find('.doc-editor-title'), content: (_, $scope) => $scope.find('[z-use-editor]'), }; return zui.formHelper($(docApp.element).find('.doc-editor'), {fields: fields}); } };