mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-03 06:09:02 +02:00
Merge pull request #22979 from overleaf/dp-file-tree-in-editor
Add file tree to editor GitOrigin-RevId: 493ecf88d632bed92c6b2b5ae2e5c0b7eef968cc
This commit is contained in:
BIN
Binary file not shown.
@@ -3,14 +3,17 @@
|
||||
// to update the font file with the latest icons.
|
||||
|
||||
export default /** @type {const} */ ([
|
||||
'book_5',
|
||||
'code',
|
||||
'description',
|
||||
'forum',
|
||||
'help',
|
||||
'image',
|
||||
'integration_instructions',
|
||||
'picture_as_pdf',
|
||||
'rate_review',
|
||||
'report',
|
||||
'settings',
|
||||
'table_chart',
|
||||
'web_asset',
|
||||
])
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { useSelectableEntity } from '../contexts/file-tree-selectable'
|
||||
|
||||
import FileTreeIcon from './file-tree-icon'
|
||||
import FileTreeItemInner from './file-tree-item/file-tree-item-inner'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import iconTypeFromName from '../util/icon-type-from-name'
|
||||
import classnames from 'classnames'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
function FileTreeDoc({
|
||||
name,
|
||||
@@ -46,52 +40,4 @@ function FileTreeDoc({
|
||||
)
|
||||
}
|
||||
|
||||
export const FileTreeIcon = ({
|
||||
isLinkedFile,
|
||||
name,
|
||||
}: {
|
||||
name: string
|
||||
isLinkedFile?: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const className = classnames('file-tree-icon', {
|
||||
'linked-file-icon': isLinkedFile,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<>
|
||||
<Icon type={iconTypeFromName(name)} fw className={className} />
|
||||
{isLinkedFile && (
|
||||
<Icon
|
||||
type="external-link-square"
|
||||
modifier="rotate-180"
|
||||
className="linked-file-highlight"
|
||||
accessibilityLabel={t('linked_file')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
bs5={
|
||||
<>
|
||||
<MaterialIcon type={iconTypeFromName(name)} className={className} />
|
||||
{isLinkedFile && (
|
||||
<MaterialIcon
|
||||
type="open_in_new"
|
||||
modifier="rotate-180"
|
||||
className="linked-file-highlight"
|
||||
accessibilityLabel={t('linked_file')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileTreeDoc
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
function FileTreeFolderIcons({
|
||||
expanded,
|
||||
onExpandCollapseClick,
|
||||
}: {
|
||||
expanded: boolean
|
||||
onExpandCollapseClick: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const newEditor = useFeatureFlag('editor-redesign')
|
||||
|
||||
if (newEditor) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="folder-expand-collapse-button"
|
||||
onClick={onExpandCollapseClick}
|
||||
aria-label={expanded ? t('collapse') : t('expand')}
|
||||
>
|
||||
<MaterialIcon
|
||||
type={expanded ? 'expand_more' : 'chevron_right'}
|
||||
className="file-tree-expand-icon"
|
||||
/>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<>
|
||||
<button
|
||||
onClick={onExpandCollapseClick}
|
||||
aria-label={expanded ? t('collapse') : t('expand')}
|
||||
>
|
||||
<Icon
|
||||
type={expanded ? 'angle-down' : 'angle-right'}
|
||||
fw
|
||||
className="file-tree-expand-icon"
|
||||
/>
|
||||
</button>
|
||||
<Icon
|
||||
type={expanded ? 'folder-open' : 'folder'}
|
||||
fw
|
||||
className="file-tree-folder-icon"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
bs5={
|
||||
<>
|
||||
<button
|
||||
onClick={onExpandCollapseClick}
|
||||
aria-label={expanded ? t('collapse') : t('expand')}
|
||||
>
|
||||
<MaterialIcon
|
||||
type={expanded ? 'expand_more' : 'chevron_right'}
|
||||
className="file-tree-expand-icon"
|
||||
/>
|
||||
</button>
|
||||
<MaterialIcon
|
||||
type={expanded ? 'folder_open' : 'folder'}
|
||||
className="file-tree-folder-icon"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileTreeFolderIcons
|
||||
@@ -32,39 +32,47 @@ function FileTreeFolderList({
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={classNames('list-unstyled', classes.root)}
|
||||
className={classNames(
|
||||
'list-unstyled',
|
||||
'file-tree-folder-list',
|
||||
classes.root
|
||||
)}
|
||||
role="tree"
|
||||
ref={dropRef}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{folders.sort(compareFunction).map(folder => {
|
||||
return (
|
||||
<FileTreeFolder
|
||||
key={folder._id}
|
||||
name={folder.name}
|
||||
id={folder._id}
|
||||
folders={folder.folders}
|
||||
docs={folder.docs}
|
||||
files={folder.fileRefs}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{docsAndFiles.sort(compareFunction).map(doc => {
|
||||
if ('isFile' in doc) {
|
||||
<div className="file-tree-folder-list-inner">
|
||||
{folders.sort(compareFunction).map(folder => {
|
||||
return (
|
||||
<FileTreeDoc
|
||||
key={doc._id}
|
||||
name={doc.name}
|
||||
id={doc._id}
|
||||
isFile={doc.isFile}
|
||||
isLinkedFile={doc.linkedFileData && !!doc.linkedFileData.provider}
|
||||
<FileTreeFolder
|
||||
key={folder._id}
|
||||
name={folder.name}
|
||||
id={folder._id}
|
||||
folders={folder.folders}
|
||||
docs={folder.docs}
|
||||
files={folder.fileRefs}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
{docsAndFiles.sort(compareFunction).map(doc => {
|
||||
if ('isFile' in doc) {
|
||||
return (
|
||||
<FileTreeDoc
|
||||
key={doc._id}
|
||||
name={doc.name}
|
||||
id={doc._id}
|
||||
isFile={doc.isFile}
|
||||
isLinkedFile={
|
||||
doc.linkedFileData && !!doc.linkedFileData.provider
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <FileTreeDoc key={doc._id} name={doc.name} id={doc._id} />
|
||||
})}
|
||||
{children}
|
||||
return <FileTreeDoc key={doc._id} name={doc.name} id={doc._id} />
|
||||
})}
|
||||
{children}
|
||||
</div>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import {
|
||||
useFileTreeSelectable,
|
||||
useSelectableEntity,
|
||||
@@ -12,11 +10,10 @@ import { useDroppable } from '../contexts/file-tree-draggable'
|
||||
import FileTreeItemInner from './file-tree-item/file-tree-item-inner'
|
||||
import FileTreeFolderList from './file-tree-folder-list'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { Doc } from '../../../../../types/doc'
|
||||
import { FileRef } from '../../../../../types/file-ref'
|
||||
import FileTreeFolderIcons from './file-tree-folder-icons'
|
||||
|
||||
function FileTreeFolder({
|
||||
name,
|
||||
@@ -31,8 +28,6 @@ function FileTreeFolder({
|
||||
docs: Doc[]
|
||||
files: FileRef[]
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { isSelected, props: selectableEntityProps } = useSelectableEntity(
|
||||
id,
|
||||
'folder'
|
||||
@@ -58,47 +53,6 @@ function FileTreeFolder({
|
||||
const { isOver: isOverRoot, dropRef: dropRefRoot } = useDroppable(id)
|
||||
const { isOver: isOverList, dropRef: dropRefList } = useDroppable(id)
|
||||
|
||||
const icons = (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<>
|
||||
<button
|
||||
onClick={handleExpandCollapseClick}
|
||||
aria-label={expanded ? t('collapse') : t('expand')}
|
||||
>
|
||||
<Icon
|
||||
type={expanded ? 'angle-down' : 'angle-right'}
|
||||
fw
|
||||
className="file-tree-expand-icon"
|
||||
/>
|
||||
</button>
|
||||
<Icon
|
||||
type={expanded ? 'folder-open' : 'folder'}
|
||||
fw
|
||||
className="file-tree-folder-icon"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
bs5={
|
||||
<>
|
||||
<button
|
||||
onClick={handleExpandCollapseClick}
|
||||
aria-label={expanded ? t('collapse') : t('expand')}
|
||||
>
|
||||
<MaterialIcon
|
||||
type={expanded ? 'expand_more' : 'chevron_right'}
|
||||
className="file-tree-expand-icon"
|
||||
/>
|
||||
</button>
|
||||
<MaterialIcon
|
||||
type={expanded ? 'folder_open' : 'folder'}
|
||||
className="file-tree-folder-icon"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<li
|
||||
@@ -119,7 +73,12 @@ function FileTreeFolder({
|
||||
name={name}
|
||||
type="folder"
|
||||
isSelected={isSelected}
|
||||
icons={icons}
|
||||
icons={
|
||||
<FileTreeFolderIcons
|
||||
expanded={expanded}
|
||||
onExpandCollapseClick={handleExpandCollapseClick}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
{expanded ? (
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import iconTypeFromName, {
|
||||
newEditorIconTypeFromName,
|
||||
} from '../util/icon-type-from-name'
|
||||
import classnames from 'classnames'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
function FileTreeIcon({
|
||||
isLinkedFile,
|
||||
name,
|
||||
}: {
|
||||
name: string
|
||||
isLinkedFile?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const className = classnames('file-tree-icon', {
|
||||
'linked-file-icon': isLinkedFile,
|
||||
})
|
||||
|
||||
const newEditor = useFeatureFlag('editor-redesign')
|
||||
|
||||
if (newEditor) {
|
||||
return (
|
||||
<>
|
||||
<MaterialIcon
|
||||
unfilled
|
||||
type={newEditorIconTypeFromName(name)}
|
||||
className={className}
|
||||
/>
|
||||
{isLinkedFile && (
|
||||
<MaterialIcon
|
||||
type="open_in_new"
|
||||
modifier="rotate-180"
|
||||
className="linked-file-highlight"
|
||||
accessibilityLabel={t('linked_file')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<>
|
||||
<Icon type={iconTypeFromName(name)} fw className={className} />
|
||||
{isLinkedFile && (
|
||||
<Icon
|
||||
type="external-link-square"
|
||||
modifier="rotate-180"
|
||||
className="linked-file-highlight"
|
||||
accessibilityLabel={t('linked_file')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
bs5={
|
||||
<>
|
||||
<MaterialIcon type={iconTypeFromName(name)} className={className} />
|
||||
{isLinkedFile && (
|
||||
<MaterialIcon
|
||||
type="open_in_new"
|
||||
modifier="rotate-180"
|
||||
className="linked-file-highlight"
|
||||
accessibilityLabel={t('linked_file')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileTreeIcon
|
||||
@@ -1,4 +1,24 @@
|
||||
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
|
||||
import { AvailableUnfilledIcon } from '@/shared/components/material-icon'
|
||||
|
||||
// TODO ide-redesign-cleanup: Make this the default export and remove the legacy version
|
||||
export const newEditorIconTypeFromName = (
|
||||
name: string
|
||||
): AvailableUnfilledIcon => {
|
||||
let ext = name.split('.').pop()
|
||||
ext = ext ? ext.toLowerCase() : ext
|
||||
|
||||
if (ext && ['png', 'pdf', 'jpg', 'jpeg', 'gif'].includes(ext)) {
|
||||
return 'image'
|
||||
} else if (ext && ['csv', 'xls', 'xlsx'].includes(ext)) {
|
||||
return 'table_chart'
|
||||
} else if (ext && ['py', 'r'].includes(ext)) {
|
||||
return 'code'
|
||||
} else if (ext && ['bib'].includes(ext)) {
|
||||
return 'book_5'
|
||||
}
|
||||
return 'description'
|
||||
}
|
||||
|
||||
export default function iconTypeFromName(name: string): string {
|
||||
let ext = name.split('.').pop()
|
||||
|
||||
@@ -5,6 +5,7 @@ import MaterialIcon, {
|
||||
} from '@/shared/components/material-icon'
|
||||
import { Panel } from 'react-resizable-panels'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { FileTree } from '@/features/ide-react/components/file-tree'
|
||||
|
||||
type RailElement = {
|
||||
icon: AvailableUnfilledIcon
|
||||
@@ -26,7 +27,12 @@ const RAIL_TABS: RailElement[] = [
|
||||
{
|
||||
key: 'file-tree',
|
||||
icon: 'description',
|
||||
component: <>File tree</>,
|
||||
component: (
|
||||
<>
|
||||
{/* TODO: add panel for file outline */}
|
||||
<FileTree />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'integrations',
|
||||
@@ -127,11 +133,11 @@ const RailTab = ({
|
||||
}) => {
|
||||
return (
|
||||
<NavLink eventKey={eventKey} className="ide-rail-tab-link">
|
||||
<MaterialIcon
|
||||
className="ide-rail-tab-link-icon"
|
||||
type={icon}
|
||||
unfilled={!active}
|
||||
/>
|
||||
{active ? (
|
||||
<MaterialIcon className="ide-rail-tab-link-icon" type={icon} />
|
||||
) : (
|
||||
<MaterialIcon className="ide-rail-tab-link-icon" type={icon} unfilled />
|
||||
)}
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
--file-tree-bg: var(--bg-dark-tertiary);
|
||||
--file-tree-item-selected-color: var(--content-primary-dark);
|
||||
--file-tree-item-dragging-bg: #{rgb($bg-dark-secondary, 0.9)};
|
||||
--file-tree-item-dragging-color: var(--content-primary-dark);
|
||||
--file-tree-item-dragging-preview-bg: #{rgb($bg-accent-01, 0.6)};
|
||||
--file-tree-item-dragging-preview-colour: var(--content-primary-dark);
|
||||
--file-tree-line-height: 2.05;
|
||||
--file-tree-icon-colour: var(--content-disabled);
|
||||
}
|
||||
|
||||
@include theme('light') {
|
||||
@@ -15,6 +18,96 @@
|
||||
--file-tree-bg: var(--bg-light-primary);
|
||||
--file-tree-item-selected-color: var(--bg-light-primary);
|
||||
--file-tree-item-dragging-bg: #{rgb($bg-light-tertiary, 0.9)};
|
||||
--file-tree-item-dragging-color: var(--content-secondary);
|
||||
--file-tree-item-dragging-preview-colour: var(--bg-light-primary);
|
||||
}
|
||||
|
||||
// TODO ide-redesign-cleanup: Replace the existing styling with these overrides.
|
||||
.ide-redesign-main {
|
||||
--file-tree-item-hover-bg: var(--bg-light-secondary);
|
||||
--file-tree-item-selected-bg: var(--bg-dark-primary);
|
||||
--file-tree-item-selected-color: var(--white);
|
||||
--file-tree-item-color: var(--content-primary);
|
||||
--file-tree-bg: var(--white);
|
||||
--file-tree-icon-colour: var(--content-primary);
|
||||
--file-tree-item-dragging-bg: #{rgb($bg-dark-primary, 0.9)};
|
||||
--file-tree-item-dragging-color: var(--white);
|
||||
--file-tree-item-dragging-preview-bg: #{rgb($bg-light-secondary, 0.6)};
|
||||
--file-tree-item-dragging-preview-colour: #{rgb($content-primary, 0.6)};
|
||||
|
||||
.file-tree {
|
||||
background-color: var(--file-tree-bg);
|
||||
}
|
||||
|
||||
.file-tree ul.file-tree-list {
|
||||
margin: var(--spacing-02);
|
||||
}
|
||||
|
||||
.file-tree-folder-list {
|
||||
border-left: 1px solid
|
||||
color-mix(in srgb, var(--border-primary) 24%, transparent);
|
||||
margin-left: 14px !important;
|
||||
|
||||
&.file-tree-list {
|
||||
border-left: none;
|
||||
margin-left: var(--spacing-02) !important;
|
||||
|
||||
> .file-tree-folder-list-inner {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-tree-folder-list-inner {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-02);
|
||||
}
|
||||
|
||||
.item-name-button,
|
||||
.folder-expand-collapse-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
.file-tree ul.file-tree-list li .material-symbols.file-tree-expand-icon {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.file-tree ul.file-tree-list li .material-symbols.file-tree-icon {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
// TODO ide-redesign-cleanup: Remove the !important overrides once
|
||||
// we have replaced the default styling
|
||||
.linked-file-highlight {
|
||||
background-color: var(--file-tree-bg) !important;
|
||||
color: var(--file-tree-icon-colour) !important;
|
||||
left: 14px !important;
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
color: var(--file-tree-item-color);
|
||||
border-radius: var(--border-radius-base);
|
||||
padding: var(--spacing-02);
|
||||
|
||||
// TODO ide-redesign-cleanup: This is here to override the fake-full-width-bg
|
||||
// mixin. We can just remove that mixin when we clean this up.
|
||||
&::before {
|
||||
content: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.item-name-button {
|
||||
margin-left: var(--spacing-02);
|
||||
}
|
||||
|
||||
.dnd-draggable-preview-item {
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
}
|
||||
|
||||
.ide-react-file-tree-panel {
|
||||
@@ -214,7 +307,7 @@
|
||||
}
|
||||
|
||||
.material-symbols {
|
||||
color: var(--content-disabled);
|
||||
color: var(--file-tree-icon-colour);
|
||||
|
||||
&.file-tree-icon {
|
||||
margin-right: var(--spacing-02);
|
||||
@@ -252,7 +345,7 @@
|
||||
width: 24px;
|
||||
padding: var(--spacing-03);
|
||||
font-size: var(--font-size-03);
|
||||
color: var(--content-disabled);
|
||||
color: var(--file-tree-icon-colour);
|
||||
}
|
||||
|
||||
.file-tree-dropdown-toggle {
|
||||
@@ -410,7 +503,7 @@
|
||||
|
||||
@include fake-full-width-bg(var(--file-tree-item-dragging-bg));
|
||||
|
||||
color: var(--file-tree-item-color);
|
||||
color: var(--file-tree-item-dragging-color);
|
||||
|
||||
.material-symbols {
|
||||
color: var(--content-disabled) !important;
|
||||
@@ -447,7 +540,7 @@
|
||||
}
|
||||
|
||||
.dnd-draggable-preview-item {
|
||||
color: var(--file-tree-item-selected-color);
|
||||
color: var(--file-tree-item-dragging-preview-colour);
|
||||
background-color: var(--file-tree-item-dragging-preview-bg);
|
||||
width: 75%;
|
||||
padding-left: var(--spacing-08);
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
--toolbar-btn-hover-bg-color: var(--neutral-80);
|
||||
--toolbar-btn-hover-color: var(--white);
|
||||
--editor-toolbar-bg: var(--white);
|
||||
--toolbar-filetree-bg-color: var(--white);
|
||||
|
||||
.toolbar {
|
||||
border-bottom: none;
|
||||
|
||||
@@ -372,7 +372,7 @@ describe('<FileTreeRoot/>', function () {
|
||||
|
||||
cy.findByRole('treeitem', { name: 'abcdef.tex' }).then($itemEl => {
|
||||
cy.findByTestId('file-tree-list-root').then($rootEl => {
|
||||
expect($itemEl.get(0).parentNode).to.equal($rootEl.get(0))
|
||||
expect($itemEl.get(0).parentNode?.parentNode).to.equal($rootEl.get(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user