mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-12 03:01:16 +00:00
feat(skeleton): Add TS defs for modules & optimize Tabs display with array contents
This commit is contained in:
parent
711a5f66b9
commit
16713f4b84
@ -266,15 +266,28 @@ export class PanelView extends Component<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class TabsPanelView extends Component<{ container: WidgetContainer<Panel> }> {
|
export class TabsPanelView extends Component<{
|
||||||
|
container: WidgetContainer<Panel>;
|
||||||
|
// shouldHideSingleTab: 一个布尔值,用于控制当 Tabs 组件只有一个标签时是否隐藏该标签。
|
||||||
|
shouldHideSingleTab?: boolean;
|
||||||
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const { container } = this.props;
|
const { container } = this.props;
|
||||||
const titles: ReactElement[] = [];
|
const titles: ReactElement[] = [];
|
||||||
const contents: ReactElement[] = [];
|
const contents: ReactElement[] = [];
|
||||||
container.items.forEach((item: any) => {
|
// 如果只有一个标签且 shouldHideSingleTab 为 true,则不显示 Tabs
|
||||||
titles.push(<PanelTitle key={item.id} panel={item} className="lc-tab-title" />);
|
if (this.props.shouldHideSingleTab && container.items.length === 1) {
|
||||||
contents.push(<PanelView key={item.id} panel={item} hideOperationRow hideDragLine />);
|
contents.push(<PanelView key={container.items[0].id} panel={container.items[0]} hideOperationRow hideDragLine />);
|
||||||
});
|
} else {
|
||||||
|
container.items.forEach((item: any) => {
|
||||||
|
titles.push(<PanelTitle key={item.id} panel={item} className="lc-tab-title" />);
|
||||||
|
contents.push(<PanelView key={item.id} panel={item} hideOperationRow hideDragLine />);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!titles.length) {
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lc-tabs">
|
<div className="lc-tabs">
|
||||||
|
|||||||
@ -2,17 +2,16 @@ import { Component, Fragment } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { observer } from '@alilc/lowcode-editor-core';
|
import { observer } from '@alilc/lowcode-editor-core';
|
||||||
import { Area } from '../area';
|
import { Area } from '../area';
|
||||||
import { PanelConfig } from '../types';
|
|
||||||
import { Panel } from '../widget/panel';
|
import { Panel } from '../widget/panel';
|
||||||
|
import { IPublicTypePanelConfig } from '@alilc/lowcode-types';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export default class LeftFixedPane extends Component<{ area: Area<PanelConfig, Panel> }> {
|
export default class LeftFixedPane extends Component<{ area: Area<IPublicTypePanelConfig, Panel> }> {
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
// FIXME: dirty fix, need deep think
|
// FIXME: dirty fix, need deep think
|
||||||
this.props.area.skeleton.editor.get('designer')?.touchOffsetObserver();
|
this.props.area.skeleton.editor.get('designer')?.touchOffsetObserver();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { area } = this.props;
|
const { area } = this.props;
|
||||||
const width = area.current?.config.props?.width;
|
const width = area.current?.config.props?.width;
|
||||||
@ -36,7 +35,7 @@ export default class LeftFixedPane extends Component<{ area: Area<PanelConfig, P
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Contents extends Component<{ area: Area<PanelConfig, Panel> }> {
|
class Contents extends Component<{ area: Area<IPublicTypePanelConfig, Panel> }> {
|
||||||
render() {
|
render() {
|
||||||
const { area } = this.props;
|
const { area } = this.props;
|
||||||
return <Fragment>{area.container.items.map((panel) => panel.content)}</Fragment>;
|
return <Fragment>{area.container.items.map((panel) => panel.content)}</Fragment>;
|
||||||
|
|||||||
@ -3,11 +3,10 @@ import classNames from 'classnames';
|
|||||||
import { observer, Focusable } from '@alilc/lowcode-editor-core';
|
import { observer, Focusable } from '@alilc/lowcode-editor-core';
|
||||||
import { Area } from '../area';
|
import { Area } from '../area';
|
||||||
import { Panel } from '../widget/panel';
|
import { Panel } from '../widget/panel';
|
||||||
import { PanelConfig } from '../types';
|
import { IPublicApiProject, IPublicTypePanelConfig } from '@alilc/lowcode-types';
|
||||||
import { IPublicApiProject } from '@alilc/lowcode-types';
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export default class LeftFloatPane extends Component<{ area: Area<PanelConfig, Panel> }> {
|
export default class LeftFloatPane extends Component<{ area: Area<IPublicTypePanelConfig, Panel> }> {
|
||||||
private dispose?: () => void;
|
private dispose?: () => void;
|
||||||
|
|
||||||
private focusing?: Focusable;
|
private focusing?: Focusable;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { action, makeObservable, obx, engineConfig, IEditor, FocusTracker } from '@alilc/lowcode-editor-core';
|
import { action, makeObservable, obx, engineConfig, IEditor, FocusTracker } from '@alilc/lowcode-editor-core';
|
||||||
import {
|
import {
|
||||||
DockConfig,
|
DockConfig,
|
||||||
PanelConfig,
|
|
||||||
WidgetConfig,
|
WidgetConfig,
|
||||||
PanelDockConfig,
|
PanelDockConfig,
|
||||||
DialogDockConfig,
|
DialogDockConfig,
|
||||||
@ -29,6 +28,7 @@ import {
|
|||||||
IPublicTypeSkeletonConfig,
|
IPublicTypeSkeletonConfig,
|
||||||
IPublicApiSkeleton,
|
IPublicApiSkeleton,
|
||||||
IPublicTypeConfigTransducer,
|
IPublicTypeConfigTransducer,
|
||||||
|
IPublicTypePanelConfig,
|
||||||
} from '@alilc/lowcode-types';
|
} from '@alilc/lowcode-types';
|
||||||
|
|
||||||
const logger = new Logger({ level: 'warn', bizName: 'skeleton' });
|
const logger = new Logger({ level: 'warn', bizName: 'skeleton' });
|
||||||
@ -70,15 +70,15 @@ export interface ISkeleton extends Omit<IPublicApiSkeleton,
|
|||||||
|
|
||||||
readonly toolbar: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>;
|
readonly toolbar: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>;
|
||||||
|
|
||||||
readonly leftFixedArea: Area<PanelConfig, Panel>;
|
readonly leftFixedArea: Area<IPublicTypePanelConfig, Panel>;
|
||||||
|
|
||||||
readonly leftFloatArea: Area<PanelConfig, Panel>;
|
readonly leftFloatArea: Area<IPublicTypePanelConfig, Panel>;
|
||||||
|
|
||||||
readonly rightArea: Area<PanelConfig, Panel>;
|
readonly rightArea: Area<IPublicTypePanelConfig, Panel>;
|
||||||
|
|
||||||
readonly mainArea: Area<WidgetConfig | PanelConfig, Widget | Panel>;
|
readonly mainArea: Area<WidgetConfig | IPublicTypePanelConfig, Widget | Panel>;
|
||||||
|
|
||||||
readonly bottomArea: Area<PanelConfig, Panel>;
|
readonly bottomArea: Area<IPublicTypePanelConfig, Panel>;
|
||||||
|
|
||||||
readonly stages: Area<StageConfig, Stage>;
|
readonly stages: Area<StageConfig, Stage>;
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ export interface ISkeleton extends Omit<IPublicApiSkeleton,
|
|||||||
defaultSetCurrent?: boolean,
|
defaultSetCurrent?: boolean,
|
||||||
): WidgetContainer;
|
): WidgetContainer;
|
||||||
|
|
||||||
createPanel(config: PanelConfig): Panel;
|
createPanel(config: IPublicTypePanelConfig): Panel;
|
||||||
|
|
||||||
add(config: IPublicTypeSkeletonConfig, extraConfig?: Record<string, any>): IWidget | Widget | Panel | Stage | Dock | PanelDock | undefined;
|
add(config: IPublicTypeSkeletonConfig, extraConfig?: Record<string, any>): IWidget | Widget | Panel | Stage | Dock | PanelDock | undefined;
|
||||||
}
|
}
|
||||||
@ -124,15 +124,15 @@ export class Skeleton implements ISkeleton {
|
|||||||
|
|
||||||
readonly toolbar: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>;
|
readonly toolbar: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>;
|
||||||
|
|
||||||
readonly leftFixedArea: Area<PanelConfig, Panel>;
|
readonly leftFixedArea: Area<IPublicTypePanelConfig, Panel>;
|
||||||
|
|
||||||
readonly leftFloatArea: Area<PanelConfig, Panel>;
|
readonly leftFloatArea: Area<IPublicTypePanelConfig, Panel>;
|
||||||
|
|
||||||
readonly rightArea: Area<PanelConfig, Panel>;
|
readonly rightArea: Area<IPublicTypePanelConfig, Panel>;
|
||||||
|
|
||||||
@obx readonly mainArea: Area<WidgetConfig | PanelConfig, Widget | Panel>;
|
@obx readonly mainArea: Area<WidgetConfig | IPublicTypePanelConfig, Widget | Panel>;
|
||||||
|
|
||||||
readonly bottomArea: Area<PanelConfig, Panel>;
|
readonly bottomArea: Area<IPublicTypePanelConfig, Panel>;
|
||||||
|
|
||||||
readonly stages: Area<StageConfig, Stage>;
|
readonly stages: Area<StageConfig, Stage>;
|
||||||
|
|
||||||
@ -388,9 +388,9 @@ export class Skeleton implements ISkeleton {
|
|||||||
return this.widgets.find(widget => widget.name === name);
|
return this.widgets.find(widget => widget.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPanel(config: PanelConfig) {
|
createPanel(config: IPublicTypePanelConfig) {
|
||||||
const parsedConfig = this.parseConfig(config);
|
const parsedConfig = this.parseConfig(config);
|
||||||
const panel = new Panel(this, parsedConfig as PanelConfig);
|
const panel = new Panel(this, parsedConfig as IPublicTypePanelConfig);
|
||||||
this.panels.set(panel.name, panel);
|
this.panels.set(panel.name, panel);
|
||||||
logger.debug(`Panel created with name: ${panel.name} \nconfig:`, config, '\n current panels: ', this.panels);
|
logger.debug(`Panel created with name: ${panel.name} \nconfig:`, config, '\n current panels: ', this.panels);
|
||||||
return panel;
|
return panel;
|
||||||
@ -496,7 +496,7 @@ export class Skeleton implements ISkeleton {
|
|||||||
return this.leftArea.add(parsedConfig as PanelDockConfig);
|
return this.leftArea.add(parsedConfig as PanelDockConfig);
|
||||||
case 'rightArea':
|
case 'rightArea':
|
||||||
case 'right':
|
case 'right':
|
||||||
return this.rightArea.add(parsedConfig as PanelConfig);
|
return this.rightArea.add(parsedConfig as IPublicTypePanelConfig);
|
||||||
case 'topArea':
|
case 'topArea':
|
||||||
case 'top':
|
case 'top':
|
||||||
return this.topArea.add(parsedConfig as PanelDockConfig);
|
return this.topArea.add(parsedConfig as PanelDockConfig);
|
||||||
@ -508,14 +508,14 @@ export class Skeleton implements ISkeleton {
|
|||||||
case 'main':
|
case 'main':
|
||||||
case 'center':
|
case 'center':
|
||||||
case 'centerArea':
|
case 'centerArea':
|
||||||
return this.mainArea.add(parsedConfig as PanelConfig);
|
return this.mainArea.add(parsedConfig as IPublicTypePanelConfig);
|
||||||
case 'bottomArea':
|
case 'bottomArea':
|
||||||
case 'bottom':
|
case 'bottom':
|
||||||
return this.bottomArea.add(parsedConfig as PanelConfig);
|
return this.bottomArea.add(parsedConfig as IPublicTypePanelConfig);
|
||||||
case 'leftFixedArea':
|
case 'leftFixedArea':
|
||||||
return this.leftFixedArea.add(parsedConfig as PanelConfig);
|
return this.leftFixedArea.add(parsedConfig as IPublicTypePanelConfig);
|
||||||
case 'leftFloatArea':
|
case 'leftFloatArea':
|
||||||
return this.leftFloatArea.add(parsedConfig as PanelConfig);
|
return this.leftFloatArea.add(parsedConfig as IPublicTypePanelConfig);
|
||||||
case 'stages':
|
case 'stages':
|
||||||
return this.stages.add(parsedConfig as StageConfig);
|
return this.stages.add(parsedConfig as StageConfig);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { ReactElement, ComponentType } from 'react';
|
import { ReactElement, ComponentType } from 'react';
|
||||||
import {
|
import {
|
||||||
IPublicTypeTitleContent,
|
IPublicTypeTitleContent,
|
||||||
IPublicTypeI18nData,
|
|
||||||
IPublicTypeWidgetConfigArea,
|
IPublicTypeWidgetConfigArea,
|
||||||
IPublicTypeWidgetBaseConfig,
|
IPublicTypeWidgetBaseConfig,
|
||||||
IPublicTypePanelDockPanelProps,
|
|
||||||
IPublicTypePanelDockProps,
|
IPublicTypePanelDockProps,
|
||||||
|
IPublicTypePanelConfigProps,
|
||||||
|
IPublicTypePanelConfig,
|
||||||
} from '@alilc/lowcode-types';
|
} from '@alilc/lowcode-types';
|
||||||
import { IWidget } from './widget/widget';
|
import { IWidget } from './widget/widget';
|
||||||
|
|
||||||
@ -66,40 +66,17 @@ export function isDialogDockConfig(obj: any): obj is DialogDockConfig {
|
|||||||
return obj && obj.type === 'DialogDock';
|
return obj && obj.type === 'DialogDock';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 窗格扩展
|
export function isPanelConfig(obj: any): obj is IPublicTypePanelConfig {
|
||||||
export interface PanelConfig extends IPublicTypeWidgetBaseConfig {
|
|
||||||
type: 'Panel';
|
|
||||||
content?: string | ReactElement | ComponentType<any> | PanelConfig[]; // as children
|
|
||||||
props?: PanelProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPanelConfig(obj: any): obj is PanelConfig {
|
|
||||||
return obj && obj.type === 'Panel';
|
return obj && obj.type === 'Panel';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HelpTipConfig = string | { url?: string; content?: string | ReactElement };
|
|
||||||
|
|
||||||
export interface PanelProps extends IPublicTypePanelDockPanelProps {
|
|
||||||
title?: IPublicTypeTitleContent;
|
|
||||||
icon?: any; // 冗余字段
|
|
||||||
description?: string | IPublicTypeI18nData;
|
|
||||||
help?: HelpTipConfig; // 显示问号帮助
|
|
||||||
hiddenWhenInit?: boolean; // when this is true, by default will be hidden
|
|
||||||
condition?: (widget: IWidget) => any;
|
|
||||||
onInit?: (widget: IWidget) => any;
|
|
||||||
onDestroy?: () => any;
|
|
||||||
shortcut?: string; // 只有在特定位置,可触发 toggle show
|
|
||||||
enableDrag?: boolean; // 是否开启通过 drag 调整 宽度
|
|
||||||
keepVisibleWhileDragging?: boolean; // 是否在该 panel 范围内拖拽时保持 visible 状态
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PanelDockConfig extends IDockBaseConfig {
|
export interface PanelDockConfig extends IDockBaseConfig {
|
||||||
type: 'PanelDock';
|
type: 'PanelDock';
|
||||||
panelName?: string;
|
panelName?: string;
|
||||||
panelProps?: PanelProps & {
|
panelProps?: IPublicTypePanelConfigProps & {
|
||||||
area?: IPublicTypeWidgetConfigArea;
|
area?: IPublicTypeWidgetConfigArea;
|
||||||
};
|
};
|
||||||
content?: string | ReactElement | ComponentType<any> | PanelConfig[]; // content for pane
|
content?: string | ReactElement | ComponentType<any> | IPublicTypePanelConfig[]; // content for pane
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPanelDockConfig(obj: any): obj is PanelDockConfig {
|
export function isPanelDockConfig(obj: any): obj is PanelDockConfig {
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { createElement, ReactNode } from 'react';
|
import { createElement, ReactNode } from 'react';
|
||||||
import { obx, computed, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
|
import { obx, computed, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
|
||||||
import { uniqueId, createContent } from '@alilc/lowcode-utils';
|
import { uniqueId, createContent } from '@alilc/lowcode-utils';
|
||||||
import { IPublicTypeTitleContent } from '@alilc/lowcode-types';
|
import { IPublicTypeHelpTipConfig, IPublicTypePanelConfig, IPublicTypeTitleContent } from '@alilc/lowcode-types';
|
||||||
import { WidgetContainer } from './widget-container';
|
import { WidgetContainer } from './widget-container';
|
||||||
import { getEvent } from '@alilc/lowcode-shell';
|
import { getEvent } from '@alilc/lowcode-shell';
|
||||||
import { PanelConfig, HelpTipConfig } from '../types';
|
|
||||||
import { TitledPanelView, TabsPanelView, PanelView } from '../components/widget-views';
|
import { TitledPanelView, TabsPanelView, PanelView } from '../components/widget-views';
|
||||||
import { ISkeleton } from '../skeleton';
|
import { ISkeleton } from '../skeleton';
|
||||||
import { composeTitle } from './utils';
|
import { composeTitle } from './utils';
|
||||||
@ -45,6 +44,7 @@ export class Panel implements IWidget {
|
|||||||
if (this.container) {
|
if (this.container) {
|
||||||
return createElement(TabsPanelView, {
|
return createElement(TabsPanelView, {
|
||||||
container: this.container,
|
container: this.container,
|
||||||
|
shouldHideSingleTab: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,15 +72,15 @@ export class Panel implements IWidget {
|
|||||||
|
|
||||||
readonly title: IPublicTypeTitleContent;
|
readonly title: IPublicTypeTitleContent;
|
||||||
|
|
||||||
readonly help?: HelpTipConfig;
|
readonly help?: IPublicTypeHelpTipConfig;
|
||||||
|
|
||||||
private plain = false;
|
private plain = false;
|
||||||
|
|
||||||
private container?: WidgetContainer<Panel, PanelConfig>;
|
private container?: WidgetContainer<Panel, IPublicTypePanelConfig>;
|
||||||
|
|
||||||
@obx.ref public parent?: WidgetContainer;
|
@obx.ref public parent?: WidgetContainer;
|
||||||
|
|
||||||
constructor(readonly skeleton: ISkeleton, readonly config: PanelConfig) {
|
constructor(readonly skeleton: ISkeleton, readonly config: IPublicTypePanelConfig) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
const { name, content, props = {} } = config;
|
const { name, content, props = {} } = config;
|
||||||
const { hideTitleBar, title, icon, description, help } = props;
|
const { hideTitleBar, title, icon, description, help } = props;
|
||||||
@ -90,9 +90,6 @@ export class Panel implements IWidget {
|
|||||||
this.plain = hideTitleBar || !title;
|
this.plain = hideTitleBar || !title;
|
||||||
this.help = help;
|
this.help = help;
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
if (content.length === 1) {
|
|
||||||
// todo: not show tabs
|
|
||||||
}
|
|
||||||
this.container = this.skeleton.createContainer(
|
this.container = this.skeleton.createContainer(
|
||||||
name,
|
name,
|
||||||
(item) => {
|
(item) => {
|
||||||
@ -127,7 +124,7 @@ export class Panel implements IWidget {
|
|||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(item: Panel | PanelConfig) {
|
add(item: Panel | IPublicTypePanelConfig) {
|
||||||
return this.container?.add(item);
|
return this.container?.add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,27 @@
|
|||||||
import { IPublicTypeIconType, IPublicTypeTitleContent, IPublicTypeWidgetConfigArea, TipContent } from './';
|
import { ReactElement, ComponentType } from 'react';
|
||||||
|
import { IPublicTypeI18nData, IPublicTypeIconType, IPublicTypeTitleContent, IPublicTypeWidgetConfigArea, TipContent } from './';
|
||||||
|
|
||||||
|
export type IPublicTypeHelpTipConfig = string | { url?: string; content?: string | ReactElement };
|
||||||
|
|
||||||
|
export interface IPublicTypePanelConfigProps extends IPublicTypePanelDockPanelProps {
|
||||||
|
title?: IPublicTypeTitleContent;
|
||||||
|
icon?: any; // 冗余字段
|
||||||
|
description?: string | IPublicTypeI18nData;
|
||||||
|
help?: IPublicTypeHelpTipConfig; // 显示问号帮助
|
||||||
|
hiddenWhenInit?: boolean; // when this is true, by default will be hidden
|
||||||
|
condition?: (widget: any) => any;
|
||||||
|
onInit?: (widget: any) => any;
|
||||||
|
onDestroy?: () => any;
|
||||||
|
shortcut?: string; // 只有在特定位置,可触发 toggle show
|
||||||
|
enableDrag?: boolean; // 是否开启通过 drag 调整 宽度
|
||||||
|
keepVisibleWhileDragging?: boolean; // 是否在该 panel 范围内拖拽时保持 visible 状态
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicTypePanelConfig extends IPublicTypeWidgetBaseConfig {
|
||||||
|
type: 'Panel';
|
||||||
|
content?: string | ReactElement | ComponentType<any> | IPublicTypePanelConfig[]; // as children
|
||||||
|
props?: IPublicTypePanelConfigProps;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IPublicTypeWidgetBaseConfig {
|
export interface IPublicTypeWidgetBaseConfig {
|
||||||
[extra: string]: any;
|
[extra: string]: any;
|
||||||
@ -13,7 +36,7 @@ export interface IPublicTypeWidgetBaseConfig {
|
|||||||
*/
|
*/
|
||||||
area?: IPublicTypeWidgetConfigArea;
|
area?: IPublicTypeWidgetConfigArea;
|
||||||
props?: Record<string, any>;
|
props?: Record<string, any>;
|
||||||
content?: any;
|
content?: string | ReactElement | ComponentType<any> | IPublicTypePanelConfig[];
|
||||||
contentProps?: Record<string, any>;
|
contentProps?: Record<string, any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user