1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-11 20:01:10 +00:00

change button and style

This commit is contained in:
Michael An
2019-05-30 15:44:48 +08:00
parent 82cbbe2d06
commit a06a551ac8
10 changed files with 428 additions and 367 deletions

View File

@@ -0,0 +1,57 @@
.dir-content-main {
width: 75%;
overflow-y: hidden;
padding-right: 1rem;
}
.dir-content-main:hover {
overflow-y: auto;
}
.dir-content-main table td {
line-height: 2rem;
}
.dir-content-nav {
width: 24%;
overflow: hidden;
}
.dir-content-nav:hover {
overflow: auto;
}
.dir-content-resize {
width: 1%;
border-left: 1px solid #eee;
}
.department-children {
padding-left: 1rem;
position: relative;
}
.tree-node-inner {
position: relative;
}
.tree-node-inner i {
position: absolute;
top: 20%;
left: 0.3rem;
}
.tree-node-inner:hover {
background-color: #FFEFB2;
border-radius: 0.25rem;
cursor: pointer;
}
.tree-node-hight-light {
color: #fff;
border-radius: 4px;
background-color: #feac74 !important;
}
.tree-node-text {
padding-left: 1.3rem;
width: calc(100% - 1.3rem);
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 24px;
}
.tree-view {
padding: 12px 12px 12px 0;
flex: 1 1;
}

View File

@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext, loginUrl } from '../../utils/constants';
import Account from '../../components/common/account';
const tablePropTypes = {
@@ -116,6 +117,15 @@ class FileScanRecords extends Component {
render() {
return (
<React.Fragment>
<div className="main-panel-north border-left-show">
<div className="cur-view-toolbar">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
</div>
<div className="common-toolbar">
<Account isAdminPanel={true} />
</div>
</div>
<div className="main-panel-center">
<div className="cur-view-container" id="content-scan-records">
<div className="cur-view-path">
@@ -130,6 +140,7 @@ class FileScanRecords extends Component {
</div>
</div>
</div>
</React.Fragment>
);
}
}

View File

@@ -1,7 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Account from '../../components/common/account';
const propTypes = {
children: PropTypes.array.isRequired,
@@ -12,14 +10,6 @@ class MainPanel extends Component {
render() {
return (
<div className="main-panel o-hidden">
<div className="main-panel-north border-left-show">
<div className="cur-view-toolbar">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
</div>
<div className="common-toolbar">
<Account isAdminPanel={true} />
</div>
</div>
{this.props.children}
</div>
);

View File

@@ -172,7 +172,7 @@ class SidePanel extends React.Component {
{isDefaultAdmin && enableWorkWeixinDepartments &&
<li className="nav-item">
<Link className='nav-link ellipsis' to={siteRoot + 'sys/work-weixin/departments/'}>
<span className="sf2-icon-organization" aria-hidden="true"></span>
<span className="sf2-icon-msgs" aria-hidden="true"></span>
<span className="nav-text">{'企业微信集成'}</span>
</Link>
</li>

View File

@@ -1,225 +1,12 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {seafileAPI} from '../../utils/seafile-api';
import {gettext, siteRoot} from '../../utils/constants';
import React, { Component, Fragment } from 'react';
import { Button } from 'reactstrap';
import _ from 'lodash';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext, siteRoot } from '../../utils/constants';
import toaster from '../../components/toast';
import Loading from '../../components/loading';
import {Button, Table} from 'reactstrap';
class WorkWeixinDepartmentMembersList extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const membersList = this.props.membersList.map((member, index) => {
let avatar = member.avatar;
if (member.avatar.length > 0) {
// get smaller avatar
avatar = member.avatar.substring(0, member.avatar.length - 1) + '100';
} else {
avatar = siteRoot + 'media/avatars/default.png';
}
const userCheckBox = member.email ? '' :
<input
type="checkbox"
className="vam"
checked={(member.userid in this.props.newUsersTempObj) ? 'checked' : ''}
onChange={() => this.props.onUserChecked(member)}
></input>;
return (
<tr key={this.props.checkedDepartmentId.toString() + member.userid}>
<td>{userCheckBox}</td>
<td><img className="avatar" src={avatar}></img></td>
<td>{member.name}</td>
<td>{member.mobile}</td>
<td>{member.contact_email}</td>
<td>{member.email ? <i className="sf2-icon-tick"></i> : ''}</td>
</tr>
);
});
const allUsersCheckBox = !this.props.canCheckUserIds.length ? '' :
<input
type="checkbox"
className="vam"
checked={this.props.isCheckedAll}
onChange={() => this.props.onAllUsersChecked()}
></input>;
return (
<div className="dir-content-main" style={{width: '75%'}}>
<div className="table-container ">
{this.props.isMembersListLoading && <Loading/>}
{!this.props.isMembersListLoading &&
<Table hover>
<thead>
<tr>
<th width="36px">{allUsersCheckBox}</th>
<th width="56px"></th>
<th width="">{'名称'}</th>
<th width="">{'手机号'}</th>
<th width="">{'邮箱'}</th>
<th width="66px">{'已添加'}</th>
</tr>
</thead>
<tbody>
{membersList}
</tbody>
</Table>
}
{!this.props.isMembersListLoading && this.props.membersList.length === 0 &&
<div className="message empty-tip">
<h2>{'无成员'}</h2>
</div>
}
</div>
</div>
);
}
}
const WorkWeixinDepartmentMembersListPropTypes = {
isMembersListLoading: PropTypes.bool.isRequired,
membersList: PropTypes.array.isRequired,
newUsersTempObj: PropTypes.object.isRequired,
checkedDepartmentId: PropTypes.number.isRequired,
onUserChecked: PropTypes.func.isRequired,
onAllUsersChecked: PropTypes.func.isRequired,
isCheckedAll: PropTypes.bool.isRequired,
canCheckUserIds: PropTypes.array.isRequired,
};
WorkWeixinDepartmentMembersList.propTypes = WorkWeixinDepartmentMembersListPropTypes;
class WorkWeixinDepartmentsTreeNode extends Component {
constructor(props) {
super(props);
this.state = {
isChildrenShow: false,
};
}
toggleChildren = () => {
this.setState({
isChildrenShow: !this.state.isChildrenShow,
});
};
renderTreeNodes = (departmentsTree) => {
if (departmentsTree.length > 0) {
return departmentsTree.map((department) => {
return (
<WorkWeixinDepartmentsTreeNode
key={department.id}
department={department}
isChildrenShow={this.state.isChildrenShow}
onChangeDepartment={this.props.onChangeDepartment}
checkedDepartmentId={this.props.checkedDepartmentId}
/>
);
});
}
};
componentDidMount() {
if (this.props.index === 0) {
this.toggleChildren();
this.props.onChangeDepartment(this.props.department.id);
}
}
render() {
let toggleIconClass = '';
if (this.props.department.children) {
if (this.state.isChildrenShow) {
toggleIconClass = 'folder-toggle-icon fa fa-caret-down';
} else {
toggleIconClass = 'folder-toggle-icon fa fa-caret-right';
}
}
let departmentStyle = this.props.checkedDepartmentId === this.props.department.id ? {color: 'blue'} : {};
return (
<div>
{this.props.isChildrenShow &&
<div>
<i className={toggleIconClass} onClick={() => this.toggleChildren()}></i>{' '}
<i
style={departmentStyle}
onClick={() => this.props.onChangeDepartment(this.props.department.id)}
>{this.props.department.name}</i>
</div>
}
{this.state.isChildrenShow &&
<div style={{left: '30px', position: 'relative'}}>
{this.props.department.children && this.renderTreeNodes(this.props.department.children)}
</div>
}
</div>
);
}
}
const WorkWeixinDepartmentsTreeNodePropTypes = {
index: PropTypes.number,
department: PropTypes.object.isRequired,
isChildrenShow: PropTypes.bool.isRequired,
onChangeDepartment: PropTypes.func.isRequired,
checkedDepartmentId: PropTypes.number.isRequired,
};
WorkWeixinDepartmentsTreeNode.propTypes = WorkWeixinDepartmentsTreeNodePropTypes;
class WorkWeixinDepartmentsTreePanel extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const departmentsTree = this.props.departmentsTree.map((department, index) => {
return (
<WorkWeixinDepartmentsTreeNode
key={department.id}
index={index}
department={department}
isChildrenShow={true}
onChangeDepartment={this.props.onChangeDepartment}
checkedDepartmentId={this.props.checkedDepartmentId}
/>
);
});
return (
<div className="dir-content-nav" style={{width: '25%'}}>
<div className="tree-view tree">
{this.props.isTreeLoading && <Loading/>}
{!this.props.isTreeLoading &&
<div className="tree-node">
{this.props.departmentsTree.length > 0 && departmentsTree}
</div>
}
</div>
</div>
);
}
}
const WorkWeixinDepartmentsTreePanelPropTypes = {
isTreeLoading: PropTypes.bool.isRequired,
departmentsTree: PropTypes.array.isRequired,
onChangeDepartment: PropTypes.func.isRequired,
checkedDepartmentId: PropTypes.number.isRequired,
};
WorkWeixinDepartmentsTreePanel.propTypes = WorkWeixinDepartmentsTreePanelPropTypes;
import Account from '../../components/common/account';
import { WorkWeixinDepartmentMembersList, WorkWeixinDepartmentsTreePanel } from './work-weixin';
import '../../css/work-weixin-departments.css';
class WorkWeixinDepartments extends Component {
@@ -255,8 +42,7 @@ class WorkWeixinDepartments extends Component {
let rootIds = parentIds.concat(intersection).filter((v) => {
return parentIds.indexOf(v) === -1 || intersection.indexOf(v) === -1;
});
let cloneData = JSON.parse(JSON.stringify(list));
let cloneData = _.cloneDeep(list);
return cloneData.filter(father => {
let branchArr = cloneData.filter(child => father.id === child.parentid);
branchArr.length > 0 ? father.children = branchArr : '';
@@ -272,16 +58,12 @@ class WorkWeixinDepartments extends Component {
departmentsTree: departmentsTree,
});
}).catch((error) => {
this.handleError(error);
this.setState({
isTreeLoading: false,
isMembersListLoading: false,
});
if (error.response) {
toaster.danger(error.response.data.error_msg || error.response.data.detail || gettext('Error'), {duration: 3});
} else {
toaster.danger(gettext('Please check the network.'), {duration: 3});
}
if (error.response.status === 403) {
if (error.response && error.response.status === 403) {
window.location = siteRoot + 'sys/useradmin/';
}
});
@@ -291,7 +73,7 @@ class WorkWeixinDepartments extends Component {
this.setState({
isMembersListLoading: true,
});
seafileAPI.adminListWorkWeixinDepartmentMembers(department_id.toString(), {fetch_child: true}).then((res) => {
seafileAPI.adminListWorkWeixinDepartmentMembers(department_id, {fetch_child: true}).then((res) => {
let membersTempObj = this.state.membersTempObj;
membersTempObj[department_id] = res.data.userlist;
let canCheckUserIds = this.getCanCheckUserIds(res.data.userlist);
@@ -303,23 +85,16 @@ class WorkWeixinDepartments extends Component {
});
}).catch((error) => {
this.setState({isMembersListLoading: false});
if (error.response) {
toaster.danger(error.response.data.error_msg || error.response.data.detail || gettext('Error'), {duration: 3});
} else {
toaster.danger(gettext('Please check the network.'), {duration: 3});
}
this.handleError(error);
});
};
getCanCheckUserIds = (membersList) => {
let canCheckUserIds = [];
for (let i = 0; i < membersList.length; i++) {
let user = membersList[i];
if (!user.email) {
canCheckUserIds.push(user.userid);
}
}
return canCheckUserIds;
let userIds = [];
membersList.forEach((member) => {
if (!member.email) userIds.push(member.userid);
});
return userIds;
};
onChangeDepartment = (department_id) => {
@@ -345,21 +120,15 @@ class WorkWeixinDepartments extends Component {
if (user.userid in newUsersTempObj) {
delete newUsersTempObj[user.userid];
if (this.state.isCheckedAll) {
this.setState({
isCheckedAll: false,
});
this.setState({ isCheckedAll: false });
}
} else {
newUsersTempObj[user.userid] = user;
if (Object.keys(newUsersTempObj).length === this.state.canCheckUserIds.length) {
this.setState({
isCheckedAll: true,
});
this.setState({ isCheckedAll: true });
}
}
this.setState({
newUsersTempObj: newUsersTempObj,
});
this.setState({ newUsersTempObj: newUsersTempObj });
}
};
@@ -372,42 +141,51 @@ class WorkWeixinDepartments extends Component {
let newUsersTempList = this.state.membersList.filter(user => {
return this.state.canCheckUserIds.indexOf(user.userid) !== -1;
});
for (let i = 0; i < newUsersTempList.length; i++) {
newUsersTempObj[newUsersTempList[i].userid] = newUsersTempList[i];
}
this.setState({
newUsersTempObj: newUsersTempObj,
});
this.setState({ newUsersTempObj: newUsersTempObj });
} else {
this.setState({
newUsersTempObj: {},
});
this.setState({ newUsersTempObj: {} });
}
});
};
onSubmit = () => {
const { newUsersTempObj } = this.state;
if (JSON.stringify(newUsersTempObj) === '{}') return;
let userList = [];
for (let i in this.state.newUsersTempObj) {
userList.push(this.state.newUsersTempObj[i]);
for (let i in newUsersTempObj) {
userList.push(newUsersTempObj[i]);
}
if (!userList.length) {
if (userList.length === 0) {
toaster.danger('未选择成员', {duration: 3});
} else {
return;
}
seafileAPI.adminAddWorkWeixinUsersBatch(userList).then((res) => {
this.setState({
newUsersTempObj: {},
isCheckedAll: false,
});
if (res.data.success) {
let membersTempObj = this.state.membersTempObj;
let membersList = this.state.membersList;
let canCheckUserIds = this.state.canCheckUserIds;
for (let i = 0; i < res.data.success.length; i++) {
let userid = res.data.success[i].userid;
let name = res.data.success[i].name;
let email = res.data.success[i].email;
this.handleSubmitSuccess(res.data.success);
}
if (res.data.failed) {
const fails= res.data.failed;
for (let i = 0; i < fails.length; i++) {
toaster.danger(fails[i].name + ' ' + fails[i].error_msg, {duration: 3});
}
}
}).catch((error) => {
this.handleError(error);
});
};
handleSubmitSuccess = (success) => {
let { membersTempObj, membersList, canCheckUserIds } = this.state;
for (let i = 0; i < success.length; i++) {
let { userid, name, email } = success[i];
toaster.success(name + ' 成功导入', {duration: 1});
// refresh all temp
if (canCheckUserIds.indexOf(userid) !== -1) {
@@ -434,39 +212,45 @@ class WorkWeixinDepartments extends Component {
canCheckUserIds: canCheckUserIds,
});
}
if (res.data.failed) {
for (let i = 0; i < res.data.failed.length; i++) {
let name = res.data.failed[i].name;
let errorMsg = res.data.failed[i].error_msg;
toaster.danger(name + ' ' + errorMsg, {duration: 3});
}
}
}).catch((error) => {
if (error.response) {
toaster.danger(error.response.data.error_msg || error.response.data.detail || gettext('Error'), {duration: 3});
handleError = (e) => {
if (e.response) {
toaster.danger(e.response.data.error_msg || e.response.data.detail || gettext('Error'), {duration: 3});
} else {
toaster.danger(gettext('Please check the network.'), {duration: 3});
}
});
}
};
componentDidMount() {
this.getWorkWeixinDepartmentsList();
}
renderNav() {
const btnClass = 'btn btn-secondary operation-item ';
return (
<div className="main-panel-north border-left-show">
<div className="cur-view-toolbar">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
<Button className={btnClass + "my-1 d-md-none"} onClick={this.onSubmit}>{'导入用户'}</Button>
<Button className={btnClass + "hidden-md-up"} onClick={this.onSubmit}>{'导入用户'}</Button>
</div>
<div className="common-toolbar">
<Account isAdminPanel={true}/>
</div>
</div>
);
}
render() {
return (
<Fragment>
{this.renderNav()}
<div className="main-panel-center">
<div className="cur-view-container">
<div className="cur-view-path">
<h3 className="sf-heading">{'企业微信集成'}</h3>
{JSON.stringify(this.state.newUsersTempObj) !== '{}' &&
<Button className="btn btn-secondary operation-item" onClick={this.onSubmit}>{'导入用户'}</Button>
}
</div>
<div className="cur-view-content" style={{display: 'flex', flexDirection: 'row'}}>
<div className="cur-view-content d-flex flex-row">
<WorkWeixinDepartmentsTreePanel
departmentsTree={this.state.departmentsTree}
isTreeLoading={this.state.isTreeLoading}
@@ -487,9 +271,9 @@ class WorkWeixinDepartments extends Component {
</div>
</div>
</div>
</Fragment>
);
}
}
export default WorkWeixinDepartments;

View File

@@ -0,0 +1,5 @@
import WorkWeixinDepartmentMembersList from './work-weixin-department-members-list';
import WorkWeixinDepartmentsTreePanel from './work-weixin-departments-tree-panel';
import WorkWeixinDepartmentsTreeNode from './work-weixin-departments-tree-node';
export { WorkWeixinDepartmentMembersList, WorkWeixinDepartmentsTreePanel, WorkWeixinDepartmentsTreeNode };

View File

@@ -0,0 +1,84 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Table } from 'reactstrap';
import { siteRoot } from '../../../utils/constants';
import Loading from '../../../components/loading';
const WorkWeixinDepartmentMembersListPropTypes = {
isMembersListLoading: PropTypes.bool.isRequired,
membersList: PropTypes.array.isRequired,
newUsersTempObj: PropTypes.object.isRequired,
checkedDepartmentId: PropTypes.number.isRequired,
onUserChecked: PropTypes.func.isRequired,
onAllUsersChecked: PropTypes.func.isRequired,
isCheckedAll: PropTypes.bool.isRequired,
canCheckUserIds: PropTypes.array.isRequired,
};
class WorkWeixinDepartmentMembersList extends Component {
constructor(props) {
super(props);
}
render() {
const { newUsersTempObj, checkedDepartmentId, isMembersListLoading, canCheckUserIds } = this.props;
const membersList = this.props.membersList.map((member, index) => {
let avatar = member.avatar;
if (member.avatar.length > 0) {
avatar = member.avatar.substring(0, member.avatar.length - 1) + '100';// get smaller avatar
} else {
avatar = siteRoot + 'media/avatars/default.png';
}
return (
<tr key={checkedDepartmentId.toString() + member.userid}>
<td>
{!member.email &&
<input type="checkbox" className="vam" onChange={() => this.props.onUserChecked(member)}
checked={(member.userid in newUsersTempObj) ? 'checked' : ''}></input>}
</td>
<td><img className="avatar" src={avatar} alt=""></img></td>
<td>{member.name}</td>
<td>{member.mobile}</td>
<td>{member.contact_email}</td>
<td>{member.email && <i className="sf2-icon-tick"></i>}</td>
</tr>
);
});
return (
<div className="dir-content-main">
{isMembersListLoading && <Loading/>}
{!isMembersListLoading && this.props.membersList.length > 0 &&
<Table hover>
<thead>
<tr>
<th width="5%">
{canCheckUserIds.length > 0 &&
<input type="checkbox" className="vam" checked={this.props.isCheckedAll}
onChange={() => this.props.onAllUsersChecked()}></input>}
</th>
<th width="10%"></th>
<th width="20%">{'名称'}</th>
<th width="20%">{'手机号'}</th>
<th width="30%">{'邮箱'}</th>
<th width="15%">{'已添加'}</th>
</tr>
</thead>
<tbody>{membersList}</tbody>
</Table>
}
{!isMembersListLoading && this.props.membersList.length === 0 &&
<div className="message empty-tip text-center">
<img src='/media/img/member-list-empty.png'/>
<h4>{'成员列表为空'}</h4>
</div>
}
</div>
);
}
}
WorkWeixinDepartmentMembersList.propTypes = WorkWeixinDepartmentMembersListPropTypes;
export default WorkWeixinDepartmentMembersList;

View File

@@ -0,0 +1,81 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const WorkWeixinDepartmentsTreeNodePropTypes = {
index: PropTypes.number,
department: PropTypes.object.isRequired,
isChildrenShow: PropTypes.bool.isRequired,
onChangeDepartment: PropTypes.func.isRequired,
checkedDepartmentId: PropTypes.number.isRequired,
};
class WorkWeixinDepartmentsTreeNode extends Component {
constructor(props) {
super(props);
this.state = {
isChildrenShow: false,
};
}
toggleChildren = () => {
this.setState({
isChildrenShow: !this.state.isChildrenShow,
});
};
componentDidMount() {
if (this.props.index === 0) {
this.toggleChildren();
this.props.onChangeDepartment(this.props.department.id);
}
}
renderTreeNodes = (departmentsTree) => {
if (departmentsTree.length > 0) {
return departmentsTree.map((department) => {
return (
<WorkWeixinDepartmentsTreeNode
key={department.id}
department={department}
isChildrenShow={this.state.isChildrenShow}
onChangeDepartment={this.props.onChangeDepartment}
checkedDepartmentId={this.props.checkedDepartmentId}
/>
);
});
}
};
render() {
const { isChildrenShow, department, checkedDepartmentId } = this.props;
let toggleClass = classNames({
'folder-toggle-icon fa fa-caret-down': department.children && this.state.isChildrenShow,
'folder-toggle-icon fa fa-caret-right': department.children && !this.state.isChildrenShow,
})
let nodeInnerClass = classNames({
'tree-node-inner': true,
'tree-node-hight-light': checkedDepartmentId === department.id
});
return (
<Fragment>
{isChildrenShow &&
<div className={nodeInnerClass}>
<i className={toggleClass} onClick={() => this.toggleChildren()}></i>{' '}
<span className="tree-node-text" onClick={() => this.props.onChangeDepartment(department.id)}>{department.name}</span>
</div>
}
{this.state.isChildrenShow &&
<div className="department-children">
{department.children && this.renderTreeNodes(department.children)}
</div>
}
</Fragment>
);
}
}
WorkWeixinDepartmentsTreeNode.propTypes = WorkWeixinDepartmentsTreeNodePropTypes;
export default WorkWeixinDepartmentsTreeNode;

View File

@@ -0,0 +1,49 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Loading from '../../../components/loading';
import WorkWeixinDepartmentsTreeNode from './work-weixin-departments-tree-node';
const WorkWeixinDepartmentsTreePanelPropTypes = {
isTreeLoading: PropTypes.bool.isRequired,
departmentsTree: PropTypes.array.isRequired,
onChangeDepartment: PropTypes.func.isRequired,
checkedDepartmentId: PropTypes.number.isRequired,
};
class WorkWeixinDepartmentsTreePanel extends Component {
constructor(props) {
super(props);
}
render() {
const { departmentsTree } = this.props;
return (
<div className="dir-content-nav">
<div className="tree-view tree">
{this.props.isTreeLoading ?
<Loading/> :
<div className="tree-node">
{departmentsTree.length > 0 && departmentsTree.map((department, index) => {
return (
<WorkWeixinDepartmentsTreeNode
key={department.id}
index={index}
department={department}
isChildrenShow={true}
onChangeDepartment={this.props.onChangeDepartment}
checkedDepartmentId={this.props.checkedDepartmentId}
/>
);
})}
</div>
}
</div>
</div>
);
}
}
WorkWeixinDepartmentsTreePanel.propTypes = WorkWeixinDepartmentsTreePanelPropTypes;
export default WorkWeixinDepartmentsTreePanel;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB