mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Add loading spinner component (#1580)
This commit is contained in:
@@ -74,9 +74,9 @@ export default class MainController implements vscode.Disposable {
|
|||||||
dialog.cancelButton.onClick(() => console.log('cancel clicked!'));
|
dialog.cancelButton.onClick(() => console.log('cancel clicked!'));
|
||||||
dialog.okButton.label = 'ok';
|
dialog.okButton.label = 'ok';
|
||||||
dialog.cancelButton.label = 'no';
|
dialog.cancelButton.label = 'no';
|
||||||
let customButton1 = sqlops.window.modelviewdialog.createButton('Test button 1');
|
let customButton1 = sqlops.window.modelviewdialog.createButton('Load name');
|
||||||
customButton1.onClick(() => console.log('button 1 clicked!'));
|
customButton1.onClick(() => console.log('button 1 clicked!'));
|
||||||
let customButton2 = sqlops.window.modelviewdialog.createButton('Test button 2');
|
let customButton2 = sqlops.window.modelviewdialog.createButton('Load all');
|
||||||
customButton2.onClick(() => console.log('button 2 clicked!'));
|
customButton2.onClick(() => console.log('button 2 clicked!'));
|
||||||
dialog.customButtons = [customButton1, customButton2];
|
dialog.customButtons = [customButton1, customButton2];
|
||||||
tab1.registerContent(async (view) => {
|
tab1.registerContent(async (view) => {
|
||||||
@@ -84,7 +84,14 @@ export default class MainController implements vscode.Disposable {
|
|||||||
.withProperties({
|
.withProperties({
|
||||||
//width: 300
|
//width: 300
|
||||||
}).component();
|
}).component();
|
||||||
|
let inputBoxWrapper = view.modelBuilder.loadingComponent().withItem(inputBox).component();
|
||||||
|
inputBoxWrapper.loading = false;
|
||||||
|
customButton1.onClick(() => {
|
||||||
|
inputBoxWrapper.loading = true;
|
||||||
|
setTimeout(() => inputBoxWrapper.loading = false, 5000);
|
||||||
|
});
|
||||||
let inputBox2 = view.modelBuilder.inputBox().component();
|
let inputBox2 = view.modelBuilder.inputBox().component();
|
||||||
|
let backupFilesInputBox = view.modelBuilder.inputBox().component();
|
||||||
|
|
||||||
let checkbox = view.modelBuilder.checkBox()
|
let checkbox = view.modelBuilder.checkBox()
|
||||||
.withProperties({
|
.withProperties({
|
||||||
@@ -107,7 +114,7 @@ export default class MainController implements vscode.Disposable {
|
|||||||
let button2 = view.modelBuilder.button()
|
let button2 = view.modelBuilder.button()
|
||||||
.component();
|
.component();
|
||||||
button.onDidClick(e => {
|
button.onDidClick(e => {
|
||||||
inputBox2.value = 'Button clicked';
|
backupFilesInputBox.value = 'Button clicked';
|
||||||
});
|
});
|
||||||
let dropdown = view.modelBuilder.dropDown()
|
let dropdown = view.modelBuilder.dropDown()
|
||||||
.withProperties({
|
.withProperties({
|
||||||
@@ -175,7 +182,7 @@ export default class MainController implements vscode.Disposable {
|
|||||||
, { flex: '1 1 50%' }).component();
|
, { flex: '1 1 50%' }).component();
|
||||||
let formModel = view.modelBuilder.formContainer()
|
let formModel = view.modelBuilder.formContainer()
|
||||||
.withFormItems([{
|
.withFormItems([{
|
||||||
component: inputBox,
|
component: inputBoxWrapper,
|
||||||
title: 'Backup name'
|
title: 'Backup name'
|
||||||
}, {
|
}, {
|
||||||
component: inputBox2,
|
component: inputBox2,
|
||||||
@@ -187,7 +194,7 @@ export default class MainController implements vscode.Disposable {
|
|||||||
component: checkbox,
|
component: checkbox,
|
||||||
title: ''
|
title: ''
|
||||||
}, {
|
}, {
|
||||||
component: inputBox2,
|
component: backupFilesInputBox,
|
||||||
title: 'Backup files',
|
title: 'Backup files',
|
||||||
actions: [button, button3]
|
actions: [button, button3]
|
||||||
}, {
|
}, {
|
||||||
@@ -197,7 +204,13 @@ export default class MainController implements vscode.Disposable {
|
|||||||
horizontal: false,
|
horizontal: false,
|
||||||
componentWidth: 400
|
componentWidth: 400
|
||||||
}).component();
|
}).component();
|
||||||
await view.initializeModel(formModel);
|
let formWrapper = view.modelBuilder.loadingComponent().withItem(formModel).component();
|
||||||
|
formWrapper.loading = false;
|
||||||
|
customButton2.onClick(() => {
|
||||||
|
formWrapper.loading = true;
|
||||||
|
setTimeout(() => formWrapper.loading = false, 5000);
|
||||||
|
});
|
||||||
|
await view.initializeModel(formWrapper);
|
||||||
});
|
});
|
||||||
|
|
||||||
sqlops.window.modelviewdialog.openDialog(dialog);
|
sqlops.window.modelviewdialog.openDialog(dialog);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import RadioButtonComponent from './radioButton.component';
|
|||||||
import WebViewComponent from './webview.component';
|
import WebViewComponent from './webview.component';
|
||||||
import TableComponent from './table.component';
|
import TableComponent from './table.component';
|
||||||
import TextComponent from './text.component';
|
import TextComponent from './text.component';
|
||||||
|
import LoadingComponent from './loadingComponent.component';
|
||||||
import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry';
|
import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry';
|
||||||
import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes';
|
import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||||
|
|
||||||
@@ -58,3 +59,6 @@ registerComponentType(TEXT_COMPONENT, ModelComponentTypes.Text, TextComponent);
|
|||||||
|
|
||||||
export const TABLE_COMPONENT = 'table-component';
|
export const TABLE_COMPONENT = 'table-component';
|
||||||
registerComponentType(TABLE_COMPONENT, ModelComponentTypes.Table, TableComponent);
|
registerComponentType(TABLE_COMPONENT, ModelComponentTypes.Table, TableComponent);
|
||||||
|
|
||||||
|
export const LOADING_COMPONENT = 'loading-component';
|
||||||
|
registerComponentType(LOADING_COMPONENT, ModelComponentTypes.LoadingComponent, LoadingComponent);
|
||||||
|
|||||||
31
src/sql/parts/modelComponents/loading.svg
Normal file
31
src/sql/parts/modelComponents/loading.svg
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version='1.0' standalone='no' ?>
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||||
|
<style>
|
||||||
|
circle {
|
||||||
|
animation: ball 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||||
|
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||||
|
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||||
|
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||||
|
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||||
|
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||||
|
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||||
|
|
||||||
|
@keyframes ball {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0.3; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
88
src/sql/parts/modelComponents/loadingComponent.component.ts
Normal file
88
src/sql/parts/modelComponents/loadingComponent.component.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import 'vs/css!./loadingComponent';
|
||||||
|
import {
|
||||||
|
Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ViewChild, ElementRef
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import * as sqlops from 'sqlops';
|
||||||
|
|
||||||
|
import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
|
||||||
|
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/parts/modelComponents/interfaces';
|
||||||
|
import * as nls from 'vs/nls';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'modelview-loadingComponent',
|
||||||
|
template: `
|
||||||
|
<div class="modelview-loadingComponent-container" *ngIf="loading">
|
||||||
|
<div class="modelview-loadingComponent-spinner" *ngIf="loading" [title]=_loadingTitle #spinnerElement></div>
|
||||||
|
</div>
|
||||||
|
<model-component-wrapper #childElement [descriptor]="_component" [modelStore]="modelStore" *ngIf="_component" [ngClass]="{'modelview-loadingComponent-content-loading': loading}">
|
||||||
|
</model-component-wrapper>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export default class LoadingComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
|
||||||
|
private readonly _loadingTitle = nls.localize('loadingMessage', 'Loading');
|
||||||
|
private _component: IComponentDescriptor;
|
||||||
|
|
||||||
|
@Input() descriptor: IComponentDescriptor;
|
||||||
|
@Input() modelStore: IModelStore;
|
||||||
|
|
||||||
|
@ViewChild('spinnerElement', { read: ElementRef }) private _spinnerElement: ElementRef;
|
||||||
|
@ViewChild('childElement', { read: ElementRef }) private _childElement: ElementRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) {
|
||||||
|
super(changeRef);
|
||||||
|
this._validations.push(() => {
|
||||||
|
if (!this._component) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.modelStore.getComponent(this._component.id).validate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.baseInit();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.setLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.baseDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IComponent implementation
|
||||||
|
|
||||||
|
public layout(): void {
|
||||||
|
this._changeRef.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLayout(): void {
|
||||||
|
this.layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setProperties(properties: { [key: string]: any; }): void {
|
||||||
|
super.setProperties(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get loading(): boolean {
|
||||||
|
return this.getPropertyOrDefault<sqlops.LoadingComponentProperties, boolean>((props) => props.loading, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public set loading(newValue: boolean) {
|
||||||
|
this.setPropertyFromUI<sqlops.LoadingComponentProperties, boolean>((properties, value) => { properties.loading = value; }, newValue);
|
||||||
|
this.layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public addToContainer(componentDescriptor: IComponentDescriptor): void {
|
||||||
|
this._component = componentDescriptor;
|
||||||
|
this.layout();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/sql/parts/modelComponents/loadingComponent.css
Normal file
24
src/sql/parts/modelComponents/loadingComponent.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
.modelview-loadingComponent-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs .modelview-loadingComponent-spinner {
|
||||||
|
content: url("loading.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-dark .modelview-loadingComponent-spinner,
|
||||||
|
.hc-black .modelview-loadingComponent-spinner {
|
||||||
|
content: url("loading_inverse.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.modelview-loadingComponent-spinner {
|
||||||
|
height: 20px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modelview-loadingComponent-content-loading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
31
src/sql/parts/modelComponents/loading_inverse.svg
Normal file
31
src/sql/parts/modelComponents/loading_inverse.svg
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version='1.0' standalone='no' ?>
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||||
|
<style>
|
||||||
|
circle {
|
||||||
|
animation: ball 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||||
|
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||||
|
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||||
|
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||||
|
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||||
|
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||||
|
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||||
|
|
||||||
|
@keyframes ball {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0.3; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<g style="fill:white;">
|
||||||
|
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||||
|
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
29
src/sql/sqlops.proposed.d.ts
vendored
29
src/sql/sqlops.proposed.d.ts
vendored
@@ -32,6 +32,7 @@ declare module 'sqlops' {
|
|||||||
formContainer(): FormBuilder;
|
formContainer(): FormBuilder;
|
||||||
groupContainer(): GroupBuilder;
|
groupContainer(): GroupBuilder;
|
||||||
toolbarContainer(): ToolbarBuilder;
|
toolbarContainer(): ToolbarBuilder;
|
||||||
|
loadingComponent(): LoadingComponentBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentBuilder<T extends Component> {
|
export interface ComponentBuilder<T extends Component> {
|
||||||
@@ -69,6 +70,14 @@ declare module 'sqlops' {
|
|||||||
addToolbarItem(toolbarComponent: ToolbarComponent): void;
|
addToolbarItem(toolbarComponent: ToolbarComponent): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoadingComponentBuilder extends ComponentBuilder<LoadingComponent> {
|
||||||
|
/**
|
||||||
|
* Set the component wrapped by the LoadingComponent
|
||||||
|
* @param component The component to wrap
|
||||||
|
*/
|
||||||
|
withItem(component: Component): LoadingComponentBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FormBuilder extends ContainerBuilder<FormContainer, FormLayout, FormItemLayout> {
|
export interface FormBuilder extends ContainerBuilder<FormContainer, FormLayout, FormItemLayout> {
|
||||||
withFormItems(components: FormComponent[], itemLayout?: FormItemLayout): ContainerBuilder<FormContainer, FormLayout, FormItemLayout>;
|
withFormItems(components: FormComponent[], itemLayout?: FormItemLayout): ContainerBuilder<FormContainer, FormLayout, FormItemLayout>;
|
||||||
|
|
||||||
@@ -335,6 +344,10 @@ declare module 'sqlops' {
|
|||||||
iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri };
|
iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoadingComponentProperties {
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CardComponent extends Component {
|
export interface CardComponent extends Component {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -390,6 +403,22 @@ declare module 'sqlops' {
|
|||||||
webviewId: string;
|
webviewId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used to wrap another component that needs to be loaded, and show a loading spinner
|
||||||
|
* while the contained component is loading
|
||||||
|
*/
|
||||||
|
export interface LoadingComponent extends Component {
|
||||||
|
/**
|
||||||
|
* Whether to show the loading spinner instead of the contained component. True by default
|
||||||
|
*/
|
||||||
|
loading: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component displayed when the loading property is false
|
||||||
|
*/
|
||||||
|
component: Component;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view backed by a model provided by an extension.
|
* A view backed by a model provided by an extension.
|
||||||
* This model contains enough information to lay out the view
|
* This model contains enough information to lay out the view
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ export enum ModelComponentTypes {
|
|||||||
DashboardWebview,
|
DashboardWebview,
|
||||||
Form,
|
Form,
|
||||||
Group,
|
Group,
|
||||||
Toolbar
|
Toolbar,
|
||||||
|
LoadingComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IComponentShape {
|
export interface IComponentShape {
|
||||||
|
|||||||
@@ -137,6 +137,13 @@ class ModelBuilderImpl implements sqlops.ModelBuilder {
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadingComponent(): sqlops.LoadingComponentBuilder {
|
||||||
|
let id = this.getNextComponentId();
|
||||||
|
let builder = new LoadingComponentBuilder(new LoadingComponentWrapper(this._proxy, this._handle, id));
|
||||||
|
this._componentBuilders.set(id, builder);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
getComponentBuilder<T extends sqlops.Component>(component: ComponentWrapper, id: string): ComponentBuilderImpl<T> {
|
getComponentBuilder<T extends sqlops.Component>(component: ComponentWrapper, id: string): ComponentBuilderImpl<T> {
|
||||||
let componentBuilder: ComponentBuilderImpl<T> = new ComponentBuilderImpl<T>(component);
|
let componentBuilder: ComponentBuilderImpl<T> = new ComponentBuilderImpl<T>(component);
|
||||||
this._componentBuilders.set(id, componentBuilder);
|
this._componentBuilders.set(id, componentBuilder);
|
||||||
@@ -299,6 +306,13 @@ class ToolbarContainerBuilder extends ContainerBuilderImpl<sqlops.ToolbarContain
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LoadingComponentBuilder extends ComponentBuilderImpl<sqlops.LoadingComponent> implements sqlops.LoadingComponentBuilder {
|
||||||
|
withItem(component: sqlops.Component) {
|
||||||
|
this.component().component = component;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class InternalItemConfig {
|
class InternalItemConfig {
|
||||||
constructor(private _component: ComponentWrapper, public config: any) { }
|
constructor(private _component: ComponentWrapper, public config: any) { }
|
||||||
|
|
||||||
@@ -761,6 +775,30 @@ class ButtonWrapper extends ComponentWrapper implements sqlops.ButtonComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LoadingComponentWrapper extends ComponentWrapper implements sqlops.LoadingComponent {
|
||||||
|
constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {
|
||||||
|
super(proxy, handle, ModelComponentTypes.LoadingComponent, id);
|
||||||
|
this.properties = {};
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get loading(): boolean {
|
||||||
|
return this.properties['loading'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public set loading(value: boolean) {
|
||||||
|
this.setProperty('loading', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get component(): sqlops.Component {
|
||||||
|
return this.items[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public set component(value: sqlops.Component) {
|
||||||
|
this.addItem(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ModelViewImpl implements sqlops.ModelView {
|
class ModelViewImpl implements sqlops.ModelView {
|
||||||
|
|
||||||
public onClosedEmitter = new Emitter<any>();
|
public onClosedEmitter = new Emitter<any>();
|
||||||
|
|||||||
Reference in New Issue
Block a user