1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-02 23:48:47 +00:00

feat: set icon

This commit is contained in:
liuhongbo
2024-06-19 16:52:27 +08:00
parent 149aab4a2c
commit a3ed7f298d
8 changed files with 707 additions and 38 deletions

View File

@@ -0,0 +1,165 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { Input, Popover, PopoverBody } from 'reactstrap';
import classnames from 'classnames';
import data from '@emoji-mart/data';
import Picker from '@emoji-mart/react';
import { init } from 'emoji-mart';
import { gettext } from '../../../utils/constants';
init({ data }); // init data for emoji-mart, used in Picker and `getRandomEmoji` method
const propTypes = {
currentPageConfig: PropTypes.object.isRequired,
onUpdatePage: PropTypes.func.isRequired,
};
const PageHeader = ({ currentPageConfig, onUpdatePage }) => {
const [isShowController, setIsShowController] = useState(false);
const [isShowIconPanel, setIsShowIconPanel] = useState(false);
const iconPanelPopoverRef = useRef(null);
console.log('currentPageConfig', currentPageConfig);
const handleRenameDocument = useCallback((e) => {
const { nativeEvent: { isComposing } } = e;
if (isComposing) return;
const newName = e.target.value.trim();
const { id, name, icon } = currentPageConfig;
if (newName === name) return;
const pageConfig = { name: newName, icon };
onUpdatePage && onUpdatePage(id, pageConfig);
}, [currentPageConfig, onUpdatePage]);
const changeControllerDisplayed = useCallback(() => {
setIsShowController(!isShowController);
}, [isShowController]);
const handleSetIcon = useCallback((emoji, cb) => {
onUpdatePage(currentPageConfig.id, { name: currentPageConfig.name, icon: emoji });
cb && cb();
}, [currentPageConfig.id, currentPageConfig.name, onUpdatePage]);
const setRandomEmoji = useCallback(() => {
const nativeEmojis = Reflect.ownKeys(data.natives);
const emojiCount = nativeEmojis.length;
const emoji = nativeEmojis[Math.floor(Math.random() * emojiCount)];
handleSetIcon(emoji);
}, [handleSetIcon]);
const handleIconPanelDisplayedChange = useCallback(() => {
setIsShowIconPanel(!isShowIconPanel);
}, [isShowIconPanel]);
const handleClickAddIcon = useCallback(() => {
setRandomEmoji();
handleIconPanelDisplayedChange();
}, [handleIconPanelDisplayedChange, setRandomEmoji]);
const handleIconPanelListener = useCallback((e) => {
const isClickInPopover = iconPanelPopoverRef.current.contains(e.target);
if (!isClickInPopover) handleIconPanelDisplayedChange();
}, [handleIconPanelDisplayedChange]);
// Update current page favicon
useEffect(() => {
let faviconUrl = '';
if (currentPageConfig.icon) {
faviconUrl = `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${currentPageConfig.icon}</text></svg>`;
} else {
const { serviceUrl, mediaUrl, faviconPath } = window.seafile;
faviconUrl = `${serviceUrl}${mediaUrl}${faviconPath}`;
}
document.getElementById('favicon').href = faviconUrl;
}, [currentPageConfig.icon]);
useEffect(() => {
if (isShowIconPanel) {
setTimeout(() => {
// Avoid open behavior closing popover
addEventListener('click', handleIconPanelListener);
}, 0);
}
if (!isShowIconPanel) removeEventListener('click', handleIconPanelListener);
return () => {
removeEventListener('click', handleIconPanelListener);
};
}, [handleIconPanelListener, isShowIconPanel]);
const handleIconRemove = () => {
handleSetIcon('');
handleIconPanelDisplayedChange();
};
const handleAddCover = useCallback(() => { }, []);
return (
<div
className='wiki-page-header-wrapper'
>
<div className='wiki-cover'></div>
<div className='wiki-page-gap-container'>
<div
className='wiki-editor-header'
onMouseEnter={changeControllerDisplayed}
onMouseLeave={changeControllerDisplayed}
>
<div className='wiki-icon-container'>
<div className={classnames('wiki-icon-wrapper', { show: currentPageConfig.icon })} id='wiki-icon-wrapper' onClick={handleIconPanelDisplayedChange}>
<span>{currentPageConfig.icon}</span>
</div>
</div>
<Popover
flip
target="wiki-icon-wrapper"
toggle={() => void 0}
placement="bottom"
isOpen={currentPageConfig.icon && isShowIconPanel}
innerClassName='wiki-icon-panel'
hideArrow={true}
>
<div ref={iconPanelPopoverRef}>
<div className='wiki-icon-panel-header popover-header'>
<span>{gettext('Emojis')}</span>
<span onClick={handleIconRemove} className='wiki-remove-icon-btn'>{gettext('Remove')}</span>
</div>
<PopoverBody className='wiki-icon-panel-body'>
<Picker
data={data}
onEmojiSelect={(emoji) => handleSetIcon(emoji.native, handleIconPanelDisplayedChange)}
previewPosition="none"
skinTonePosition="none"
locale={window.seafile.lang.slice(0, 2)}
maxFrequentRows={2}
/>
</PopoverBody>
</div>
</Popover>
<div className={classnames('wiki-page-controller', { show: isShowController })}>
{!currentPageConfig.icon && (
<div className='wiki-page-controller-item' onClick={handleClickAddIcon}>
<i className='fa fa-save'></i>
<span className='text'>{gettext('Add icon')}</span>
</div>
)}
<div className='wiki-page-controller-item'>
<i className='fa fa-save'></i>
<span className='text'>{gettext('Add cover')}</span>
</div>
</div>
<Input className='sf-wiki-title' onCompositionEnd={handleRenameDocument} bsSize="lg" onChange={handleRenameDocument} defaultValue={currentPageConfig.name} />
</div>
</div>
</div>
);
};
PageHeader.propTypes = propTypes;
export default PageHeader;

View File

@@ -197,7 +197,7 @@ class Wiki extends Component {
}, () => {
callback && callback();
});
this.cacheHistoryFiles(docUuid,name,id);
this.cacheHistoryFiles(docUuid, name, id);
};
onUpdatePage = (pageId, newPage) => {

View File

@@ -1,13 +1,13 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { SdocWikiViewer } from '@seafile/sdoc-editor';
import { Input } from 'reactstrap';
import { gettext, username } from '../../utils/constants';
import Loading from '../../components/loading';
import { Utils } from '../../utils/utils';
import Account from '../../components/common/account';
import WikiTopNav from './top-nav';
import { getCurrentPageConfig } from './utils';
import PageHeader from './editor-component/page-header';
const propTypes = {
path: PropTypes.string.isRequired,
@@ -32,6 +32,7 @@ class MainPanel extends Component {
docUuid: '',
currentPageConfig: {},
};
this.scrollRef = React.createRef();
}
static getDerivedStateFromProps(props, state) {
@@ -53,20 +54,10 @@ class MainPanel extends Component {
return { ...props, docUuid: window.seafile.docUuid, currentPageConfig };
}
handleRenameDocument = (e) => {
const { nativeEvent: { isComposing } } = e;
if (isComposing) return;
const newName = e.target.value.trim();
const { currentPageConfig } = this.state;
const { id, name, icon } = currentPageConfig;
if (newName === name) return;
const pageConfig = { name: newName, icon };
this.props.onUpdatePage(id, pageConfig);
};
render() {
const { permission, pathExist, isDataLoading, isViewFile, config } = this.props;
const { permission, pathExist, isDataLoading, isViewFile, config, onUpdatePage } = this.props;
const { currentPageConfig = {}, } = this.state;
const isViewingFile = pathExist && !isDataLoading && isViewFile;
const isReadOnly = !(permission === 'rw');
@@ -77,6 +68,7 @@ class MainPanel extends Component {
<WikiTopNav
config={config}
currentPageId={this.props.currentPageId}
currentPageConfig={currentPageConfig}
/>
{username &&
<Account />
@@ -89,14 +81,17 @@ class MainPanel extends Component {
}
{this.props.pathExist && this.props.isDataLoading && <Loading />}
{isViewingFile && Utils.isSdocFile(this.props.path) && (
<>
<SdocWikiViewer
document={this.props.editorContent}
docUuid={this.state.docUuid}
isWikiReadOnly={isReadOnly}
topSlot={<Input className='sf-wiki-title' onCompositionEnd={this.handleRenameDocument} bsSize="lg" onChange={this.handleRenameDocument} defaultValue={currentPageConfig.name} />}
/>
</>
<div className='sdoc-scroll-container' id='sdoc-scroll-container' ref={this.scrollRef}>
<div className='wiki-editor-container'>
<PageHeader onUpdatePage={onUpdatePage} currentPageConfig={currentPageConfig} />
<SdocWikiViewer
document={this.props.editorContent}
docUuid={this.state.docUuid}
isWikiReadOnly={isReadOnly}
scrollRef={this.scrollRef}
/>
</div>
</div>
)}
</div>
</div>

View File

@@ -37,16 +37,26 @@ function getPaths(navigation, currentPageId, pages) {
return pathStr.split('-').map(id => idPageMap[id]);
}
function WikiTopNav({ config, currentPageId }) {
function WikiTopNav({ config, currentPageId, currentPageConfig }) {
const { navigation, pages } = config;
const paths = getPaths(navigation, currentPageId, pages);
const customIcon = currentPageConfig.icon;
return (
<div className="wiki2-top-nav d-flex">
{paths.map((item, index) => {
return (
<Fragment key={item.id}>
<div className='wiki2-top-nav-item d-flex'>
<NavItemIcon symbol={'file'} disable={true} />
{
item.type === 'folder' && (<NavItemIcon symbol={'wiki-folder'} disable={true} />)
}
{
item.type !== 'folder' && (
customIcon
? <span className='nav-item-icon nav-item-icon-disable'>{customIcon}</span>
: <NavItemIcon symbol={'file'} disable={true} />
)
}
{item.name}
</div>
{index !== paths.length - 1 && <div>/</div>}
@@ -60,6 +70,7 @@ function WikiTopNav({ config, currentPageId }) {
WikiTopNav.propTypes = {
config: PropTypes.object,
currentPageId: PropTypes.string,
currentPageConfig: PropTypes.object,
};
export default WikiTopNav;

View File

@@ -0,0 +1,352 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ViewEditPopover from './view-edit-popover';
import PageDropdownMenu from './page-dropdownmenu';
import DeleteDialog from './delete-dialog';
import { gettext } from '../../../../utils/constants';
import AddNewPageDialog from '../add-new-page-dialog';
import Icon from '../../../../components/icon';
import NavItemIcon from '../nav-item-icon';
import DraggedViewItem from '../views/dragged-view-item';
class ViewItem extends Component {
constructor(props) {
super(props);
this.state = {
isShowViewEditor: false,
isShowViewOperationDropdown: false,
isShowDeleteDialog: false,
isShowInsertPage: false,
viewName: props.view.name || '',
viewIcon: props.view.icon,
isSelected: props.currentPageId === props.view.id,
isMouseEnter: false,
};
this.viewItemRef = React.createRef();
}
onMouseEnter = () => {
this.setState({ isMouseEnter: true });
if (this.state.isSelected) return;
};
onMouseMove = () => {
if (!this.state.isMouseEnter) this.setState({ isMouseEnter: true });
};
onMouseLeave = () => {
this.setState({ isMouseEnter: false });
if (this.state.isSelected) return;
};
onCurrentPageChanged = (currentPageId) => {
const { isSelected } = this.state;
if (currentPageId === this.props.view.id && isSelected === false) {
this.setState({ isSelected: true });
} else if (currentPageId !== this.props.view.id && isSelected === true) {
this.setState({ isSelected: false });
}
};
toggleViewEditor = (e) => {
if (e) e.stopPropagation();
this.setState({ isShowViewEditor: !this.state.isShowViewEditor }, () => {
if (!this.state.isShowViewEditor) {
this.saveViewProperties();
}
});
};
toggleInsertPage = () => {
this.setState({ isShowInsertPage: !this.state.isShowInsertPage });
};
saveViewProperties = () => {
const { name, icon, id } = this.props.view;
const { viewIcon } = this.state;
let viewName = this.state.viewName.trim();
if (viewIcon !== icon || viewName !== name) {
let newView = {};
if (viewName !== name) {
newView.name = viewName;
}
if (viewIcon !== icon) {
newView.icon = viewIcon;
}
this.props.onUpdatePage(id, newView);
}
};
onChangeName = (newViewName) => {
this.setState({ viewName: newViewName });
};
onChangeIcon = (newViewIcon) => {
this.setState({ viewIcon: newViewIcon });
};
openDeleteDialog = () => {
this.setState({ isShowDeleteDialog: true });
};
closeDeleteDialog = () => {
this.setState({ isShowDeleteDialog: false });
};
onViewOperationDropdownToggle = () => {
const isShowViewOperationDropdown = !this.state.isShowViewOperationDropdown;
this.setState({ isShowViewOperationDropdown });
this.changeItemFreeze(isShowViewOperationDropdown);
};
changeItemFreeze = (isFreeze) => {
if (isFreeze) {
this.viewItemRef.classList.add('view-freezed');
} else {
this.viewItemRef.classList.remove('view-freezed');
}
};
renderIcon = (icon) => {
if (!icon) {
return null;
}
if (icon.includes('dtable-icon')) {
return <span className={`mr-2 dtable-font ${icon}`}></span>;
} else {
return <Icon className="mr-2" symbol={icon} />;
}
};
setDocUuid = (docUuid) => {
window.seafile['docUuid'] = docUuid;
};
getFolderChildrenHeight = () => {
const folded = this.props.getFoldState(this.props.view.id);
if (folded) return 0;
return 'auto';
};
onClickFolderChildren = (e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
};
renderView = (view, index, pagesLength, isOnlyOneView) => {
const { isEditMode, views, folderId, pathStr } = this.props;
const id = view.id;
if (!views.find(item => item.id === id)) return;
return (
<DraggedViewItem
key={id}
pagesLength={pagesLength}
isOnlyOneView={isOnlyOneView}
infolder={false}
view={Object.assign({}, views.find(item => item.id === id), view)}
viewIndex={index}
folderId={folderId}
isEditMode={isEditMode}
renderFolderMenuItems={this.props.renderFolderMenuItems}
duplicatePage={this.props.duplicatePage}
onSetFolderId={this.props.onSetFolderId}
onSelectView={this.props.onSelectView}
onUpdatePage={this.props.onUpdatePage}
onDeleteView={this.props.onDeleteView}
onMoveViewToFolder={(targetFolderId) => {
this.props.onMoveViewToFolder(folderId, view.id, targetFolderId);
}}
onMoveView={this.props.onMoveView}
onMoveFolder={this.props.onMoveFolder}
views={views}
pathStr={pathStr + '-' + view.id}
currentPageId={this.props.currentPageId}
addPageInside={this.props.addPageInside}
getFoldState={this.props.getFoldState}
toggleExpand={this.props.toggleExpand}
/>
);
};
toggleExpand = (e) => {
e.nativeEvent.stopImmediatePropagation();
this.props.toggleExpand(this.props.view.id);
this.forceUpdate();
};
onAddNewPage = (newPage) => {
const { view } = this.props;
this.props.addPageInside(Object.assign({ parentPageId: view.id }, newPage));
};
render() {
const {
connectDragSource, connectDragPreview, connectDropTarget, isOver, canDrop, isDragging,
infolder, view, pagesLength, isEditMode, folderId, isOnlyOneView, pathStr,
} = this.props;
const { isShowViewEditor, viewName, viewIcon, isSelected } = this.state;
const isOverView = isOver && canDrop;
if (isSelected) this.setDocUuid(view.docUuid);
let viewCanDropTop;
let viewCanDrop;
if (infolder) {
viewCanDropTop = false;
viewCanDrop = isOverView;
} else {
viewCanDropTop = isOverView && isDragging;
viewCanDrop = isOverView && !isDragging;
}
let viewEditorId = `view-editor-${view.id}`;
let fn = isEditMode ? connectDragSource : (argu) => { argu; };
let childNumber = Array.isArray(view.children) ? view.children.length : 0;
const folded = this.props.getFoldState(view.id);
return (
<div>
{
fn(connectDropTarget(
connectDragPreview(
<div
className={classnames('view-item', 'view',
{ 'selected-view': isSelected },
{ 'view-can-drop-top': viewCanDropTop },
{ 'view-can-drop': viewCanDrop },
{ 'readonly': !isEditMode },
)}
ref={ref => this.viewItemRef = ref}
onMouseEnter={this.onMouseEnter}
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
id={viewEditorId}
>
<div className="view-item-main" onClick={isShowViewEditor ? () => { } : (e) => this.props.onSelectView(view.id)}>
<div className='view-content' style={pathStr ? { marginLeft: `${(pathStr.split('-').length - 1) * 24}px` } : {}}>
{childNumber === 0 && (
view.icon
? <span className='nav-item-icon nav-item-icon-disable'>{view.icon}</span>
: <NavItemIcon symbol={'file'} disable={true} />)
}
{(!this.state.isMouseEnter && childNumber > 0) &&
<NavItemIcon symbol={'files'} disable={true} />
}
{(this.state.isMouseEnter && childNumber > 0) &&
<NavItemIcon
className="icon-expand-folder"
symbol={folded ? 'right-slide' : 'drop-down'}
onClick={this.toggleExpand}
/>
}
{/* {this.renderIcon(view.icon)} */}
<span className="view-title text-truncate" title={view.name}>{view.name}</span>
{isShowViewEditor && (
<ViewEditPopover
viewName={viewName}
viewIcon={viewIcon}
viewEditorId={viewEditorId}
onChangeName={this.onChangeName}
onChangeIcon={this.onChangeIcon}
toggleViewEditor={this.toggleViewEditor}
/>
)}
</div>
</div>
<div className="d-flex">
{isEditMode &&
<>
<div className="more-view-operation" onClick={this.onViewOperationDropdownToggle}>
<Icon symbol={'more-level'} />
{this.state.isShowViewOperationDropdown &&
<PageDropdownMenu
view={view}
views={this.props.views}
pagesLength={pagesLength}
isOnlyOneView={isOnlyOneView}
folderId={folderId}
canDelete={true}
canDuplicate={true}
toggle={this.onViewOperationDropdownToggle}
renderFolderMenuItems={this.props.renderFolderMenuItems}
toggleViewEditor={this.toggleViewEditor}
duplicatePage={this.props.duplicatePage}
onSetFolderId={this.props.onSetFolderId}
onDeleteView={this.openDeleteDialog}
onMoveViewToFolder={this.props.onMoveViewToFolder}
/>
}
</div>
<div className="ml-2" onClick={this.toggleInsertPage}>
<span className='fas fa-plus'></span>
</div>
</>
}
</div>
{this.state.isShowDeleteDialog &&
<DeleteDialog
closeDeleteDialog={this.closeDeleteDialog}
handleSubmit={this.props.onDeleteView.bind(this, view.id)}
/>
}
{this.state.isShowInsertPage &&
<AddNewPageDialog
toggle={this.toggleInsertPage}
onAddNewPage={this.onAddNewPage}
title={gettext('Add page inside')}
/>
}
</div>
)
))
}
<div
className="view-folder-children"
style={{ height: this.getFolderChildrenHeight() }}
onClick={this.onClickFolderChildren}
>
{view.children &&
view.children.map((item, index) => {
return this.renderView(item, index, pagesLength, isOnlyOneView);
})
}
</div>
</div>
);
}
}
ViewItem.propTypes = {
isOver: PropTypes.bool,
canDrop: PropTypes.bool,
isDragging: PropTypes.bool,
draggedPage: PropTypes.object,
isEditMode: PropTypes.bool,
infolder: PropTypes.bool,
view: PropTypes.object,
folder: PropTypes.object,
views: PropTypes.array,
viewIndex: PropTypes.number,
folderId: PropTypes.string,
pagesLength: PropTypes.number,
connectDragSource: PropTypes.func,
connectDragPreview: PropTypes.func,
connectDropTarget: PropTypes.func,
renderFolderMenuItems: PropTypes.func,
duplicatePage: PropTypes.func,
onSetFolderId: PropTypes.func,
onSelectView: PropTypes.func,
onUpdatePage: PropTypes.func,
onDeleteView: PropTypes.func,
onMoveViewToFolder: PropTypes.func,
onMoveView: PropTypes.func,
isOnlyOneView: PropTypes.bool,
onMoveFolder: PropTypes.func,
pathStr: PropTypes.string,
currentPageId: PropTypes.string,
addPageInside: PropTypes.func,
getFoldState: PropTypes.func,
toggleExpand: PropTypes.func,
};
export default ViewItem;

View File

@@ -70,6 +70,10 @@ img[src=""] {
color: #212529;
}
.main-panel-center .cur-view-content .sf-wiki-title:focus {
box-shadow: none;
}
/* reset article h1 */
.wiki2-main-panel .article h1 {
margin-top: 0;
@@ -141,21 +145,6 @@ img[src=""] {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sdoc-editor-container .wiki-editor-header {
display: flex;
justify-content: space-between;
padding: 10px 20px;
height: 56px;
border-bottom: 1px solid #e5e5e5;
background-color: #fff;
z-index: 1000;
}
.sdoc-editor-container .wiki-editor-header .doc-ops {
display: flex;
}
.sdoc-editor-container .wiki-viewer-container {
@@ -243,3 +232,117 @@ img[src=""] {
.sdoc-editor-container .sdoc-editor-content .article #sdoc-editor {
width: 100%;
}
/* editor layout */
.main-panel-center .cur-view-content .wiki-page-gap-container {
padding: 0 50px;
padding-left: 142px;
}
.main-panel-center .cur-view-content .sf-wiki-title {
padding-left: 0;
border: none;
font-weight: bold;
font-size: 26pt;
}
.cur-view-content .wiki-cover {
height: 30vh;
width: 100%;
background-color: aqua;
}
.cur-view-content .wiki-editor-container {
display: flex;
flex-direction: column;
width: 100%;
}
.cur-view-content .sdoc-scroll-container {
overflow-y: auto;
}
.cur-view-content .sdoc-editor-container .sdoc-editor-content {
position: static;
}
.wiki-page-controller {
display: flex;
visibility: hidden;
}
.wiki-page-controller.show {
visibility: visible;
}
.wiki-page-controller-item {
display: flex;
align-items: center;
padding: 6px;
border-radius: 4px;
cursor: pointer;
}
.wiki-icon-container {
margin-bottom: 8px;
line-height: 78px;
}
.wiki-icon-wrapper {
display: none;
margin-top: -48px;
width: 78px;
height: 78px;
font-size: 78px;
}
.wiki-icon-wrapper:hover {
cursor: pointer;
background-color: #d3c5c370;
}
.wiki-icon-wrapper.show {
display: block;
}
.wiki-page-controller-item .text {
margin-left: 6px;
}
.wiki-page-controller-item:hover {
background-color: #efefef;
border-radius: 4px;
}
.wiki-icon-panel {
width: 352px;
height: 435px;
}
.wiki-icon-panel-header {
display: flex;
justify-content: space-between;
padding: 6px 14px;
background-color: #fff;
}
.wiki-icon-panel-header>span {
padding: 2px 8px;
border-radius: 4px;
user-select: none;
}
.wiki-icon-panel-header>span:hover {
background-color: #efefef;
cursor: pointer;
}
.wiki-icon-panel-body {
padding: 0;
width: 352px;
background-color: #fff;
}
.wiki-icon-panel-body div {
width: 100%;
}