feat(core): added support for detecting lifecycle events based on interfaces
This commit is contained in:
parent
2b6a653050
commit
30b6542fc8
|
@ -1,5 +1,5 @@
|
||||||
import {CONST, normalizeBlank, isPresent, CONST_EXPR} from 'angular2/src/facade/lang';
|
import {CONST, CONST_EXPR} from 'angular2/src/facade/lang';
|
||||||
import {ListWrapper, List} from 'angular2/src/facade/collection';
|
import {List} from 'angular2/src/facade/collection';
|
||||||
import {Injectable} from 'angular2/src/di/annotations_impl';
|
import {Injectable} from 'angular2/src/di/annotations_impl';
|
||||||
import {DEFAULT} from 'angular2/change_detection';
|
import {DEFAULT} from 'angular2/change_detection';
|
||||||
|
|
||||||
|
@ -778,15 +778,6 @@ export class Directive extends Injectable {
|
||||||
this.compileChildren = compileChildren;
|
this.compileChildren = compileChildren;
|
||||||
this.hostInjector = hostInjector;
|
this.hostInjector = hostInjector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if a directive participates in a given `LifecycleEvent`.
|
|
||||||
*
|
|
||||||
* See {@link onChange}, {@link onDestroy}, {@link onAllChangesDone} for details.
|
|
||||||
*/
|
|
||||||
hasLifecycleHook(hook: LifecycleEvent): boolean {
|
|
||||||
return isPresent(this.lifecycle) ? ListWrapper.contains(this.lifecycle, hook) : false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import 'package:angular2/src/core/annotations_impl/annotations.dart';
|
||||||
|
import 'package:angular2/src/core/compiler/interfaces.dart';
|
||||||
|
import 'package:angular2/src/reflection/reflection.dart';
|
||||||
|
|
||||||
|
bool hasLifecycleHook(LifecycleEvent e, type, Directive annotation) {
|
||||||
|
if (annotation.lifecycle != null) {
|
||||||
|
return annotation.lifecycle.contains(e);
|
||||||
|
} else {
|
||||||
|
if (type is! Type) return false;
|
||||||
|
|
||||||
|
final List interfaces = reflector.interfaces(type);
|
||||||
|
var interface;
|
||||||
|
|
||||||
|
if (e == onChange) {
|
||||||
|
interface = OnChange;
|
||||||
|
|
||||||
|
} else if (e == onDestroy) {
|
||||||
|
interface = OnDestroy;
|
||||||
|
|
||||||
|
} else if (e == onAllChangesDone) {
|
||||||
|
interface = OnAllChangesDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces.contains(interface);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {Type, isPresent} from 'angular2/src/facade/lang';
|
||||||
|
import {LifecycleEvent, Directive} from 'angular2/src/core/annotations_impl/annotations';
|
||||||
|
|
||||||
|
export function hasLifecycleHook(e: LifecycleEvent, type, annotation: Directive): boolean {
|
||||||
|
if (isPresent(annotation.lifecycle)) {
|
||||||
|
return annotation.lifecycle.indexOf(e) !== -1;
|
||||||
|
} else {
|
||||||
|
if (!(type instanceof Type)) return false;
|
||||||
|
return e.name in(<any>type).prototype;
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import {
|
||||||
onDestroy,
|
onDestroy,
|
||||||
onAllChangesDone
|
onAllChangesDone
|
||||||
} from 'angular2/src/core/annotations_impl/annotations';
|
} from 'angular2/src/core/annotations_impl/annotations';
|
||||||
|
import {hasLifecycleHook} from './directive_lifecycle_reflector';
|
||||||
import {ChangeDetector, ChangeDetectorRef} from 'angular2/change_detection';
|
import {ChangeDetector, ChangeDetectorRef} from 'angular2/change_detection';
|
||||||
import {QueryList} from './query_list';
|
import {QueryList} from './query_list';
|
||||||
import {reflector} from 'angular2/src/reflection/reflection';
|
import {reflector} from 'angular2/src/reflection/reflection';
|
||||||
|
@ -282,7 +283,6 @@ export class DirectiveBinding extends ResolvedBinding {
|
||||||
var resolvedViewInjectables = ann instanceof Component && isPresent(ann.viewInjector) ?
|
var resolvedViewInjectables = ann instanceof Component && isPresent(ann.viewInjector) ?
|
||||||
resolveBindings(ann.viewInjector) :
|
resolveBindings(ann.viewInjector) :
|
||||||
[];
|
[];
|
||||||
|
|
||||||
var metadata = new DirectiveMetadata({
|
var metadata = new DirectiveMetadata({
|
||||||
id: stringify(rb.key.token),
|
id: stringify(rb.key.token),
|
||||||
type: ann instanceof
|
type: ann instanceof
|
||||||
|
@ -300,9 +300,11 @@ export class DirectiveBinding extends ResolvedBinding {
|
||||||
null,
|
null,
|
||||||
properties: isPresent(ann.properties) ? MapWrapper.createFromStringMap(ann.properties) : null,
|
properties: isPresent(ann.properties) ? MapWrapper.createFromStringMap(ann.properties) : null,
|
||||||
readAttributes: DirectiveBinding._readAttributes(deps),
|
readAttributes: DirectiveBinding._readAttributes(deps),
|
||||||
callOnDestroy: ann.hasLifecycleHook(onDestroy),
|
|
||||||
callOnChange: ann.hasLifecycleHook(onChange),
|
callOnDestroy: hasLifecycleHook(onDestroy, rb.key.token, ann),
|
||||||
callOnAllChangesDone: ann.hasLifecycleHook(onAllChangesDone),
|
callOnChange: hasLifecycleHook(onChange, rb.key.token, ann),
|
||||||
|
callOnAllChangesDone: hasLifecycleHook(onAllChangesDone, rb.key.token, ann),
|
||||||
|
|
||||||
changeDetection: ann instanceof
|
changeDetection: ann instanceof
|
||||||
Component ? ann.changeDetection : null
|
Component ? ann.changeDetection : null
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,6 +12,7 @@ export interface OnChange { onChange(changes: StringMap<string, any>): void; }
|
||||||
export interface OnDestroy { onDestroy(): void; }
|
export interface OnDestroy { onDestroy(): void; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines lifecycle method [onAllChangesDone ] called when the bindings of all its children have been changed.
|
* Defines lifecycle method [onAllChangesDone ] called when the bindings of all its children have
|
||||||
|
* been changed.
|
||||||
*/
|
*/
|
||||||
export interface OnAllChangesDone { onAllChangesDone (): void; }
|
export interface OnAllChangesDone { onAllChangesDone(): void; }
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
library reflection.reflection_capabilities;
|
library reflection.reflection_capabilities;
|
||||||
|
|
||||||
import 'reflection.dart';
|
|
||||||
import 'package:angular2/src/facade/lang.dart';
|
import 'package:angular2/src/facade/lang.dart';
|
||||||
import 'types.dart';
|
import 'types.dart';
|
||||||
import 'dart:mirrors';
|
import 'dart:mirrors';
|
||||||
|
|
|
@ -97,9 +97,7 @@ export class ReflectionCapabilities {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
interfaces(type): List<any> {
|
interfaces(type): List<any> { throw new BaseException("JavaScript does not support interfaces"); }
|
||||||
throw new BaseException("JavaScript does not support interfaces");
|
|
||||||
}
|
|
||||||
|
|
||||||
getter(name: string): GetterFn { return new Function('o', 'return o.' + name + ';'); }
|
getter(name: string): GetterFn { return new Function('o', 'return o.' + name + ';'); }
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import {Type, isPresent, stringify, BaseException} from 'angular2/src/facade/lang';
|
import {Type, isPresent, stringify, BaseException} from 'angular2/src/facade/lang';
|
||||||
import {List, ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
import {
|
||||||
|
List,
|
||||||
|
ListWrapper,
|
||||||
|
Map,
|
||||||
|
MapWrapper,
|
||||||
|
StringMap,
|
||||||
|
StringMapWrapper
|
||||||
|
} from 'angular2/src/facade/collection';
|
||||||
import {SetterFn, GetterFn, MethodFn} from './types';
|
import {SetterFn, GetterFn, MethodFn} from './types';
|
||||||
export {SetterFn, GetterFn, MethodFn} from './types';
|
|
||||||
|
|
||||||
export class Reflector {
|
export class Reflector {
|
||||||
_typeInfo: Map<Type, any>;
|
_typeInfo: Map<Type, any>;
|
||||||
|
@ -18,7 +24,7 @@ export class Reflector {
|
||||||
this.reflectionCapabilities = reflectionCapabilities;
|
this.reflectionCapabilities = reflectionCapabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerType(type: Type, typeInfo: Map<Type, any>): void {
|
registerType(type: Type, typeInfo: StringMap<string, any>): void {
|
||||||
MapWrapper.set(this._typeInfo, type, typeInfo);
|
MapWrapper.set(this._typeInfo, type, typeInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,32 +35,32 @@ export class Reflector {
|
||||||
registerMethods(methods: Map<string, MethodFn>): void { _mergeMaps(this._methods, methods); }
|
registerMethods(methods: Map<string, MethodFn>): void { _mergeMaps(this._methods, methods); }
|
||||||
|
|
||||||
factory(type: Type): Function {
|
factory(type: Type): Function {
|
||||||
if (MapWrapper.contains(this._typeInfo, type)) {
|
if (this._containsTypeInfo(type)) {
|
||||||
return MapWrapper.get(this._typeInfo, type)["factory"];
|
return this._getTypeInfoField(type, "factory", null);
|
||||||
} else {
|
} else {
|
||||||
return this.reflectionCapabilities.factory(type);
|
return this.reflectionCapabilities.factory(type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parameters(typeOfFunc): List<any> {
|
parameters(typeOrFunc): List<any> {
|
||||||
if (MapWrapper.contains(this._typeInfo, typeOfFunc)) {
|
if (MapWrapper.contains(this._typeInfo, typeOrFunc)) {
|
||||||
return MapWrapper.get(this._typeInfo, typeOfFunc)["parameters"];
|
return this._getTypeInfoField(typeOrFunc, "parameters", []);
|
||||||
} else {
|
} else {
|
||||||
return this.reflectionCapabilities.parameters(typeOfFunc);
|
return this.reflectionCapabilities.parameters(typeOrFunc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
annotations(typeOfFunc): List<any> {
|
annotations(typeOrFunc): List<any> {
|
||||||
if (MapWrapper.contains(this._typeInfo, typeOfFunc)) {
|
if (MapWrapper.contains(this._typeInfo, typeOrFunc)) {
|
||||||
return MapWrapper.get(this._typeInfo, typeOfFunc)["annotations"];
|
return this._getTypeInfoField(typeOrFunc, "annotations", []);
|
||||||
} else {
|
} else {
|
||||||
return this.reflectionCapabilities.annotations(typeOfFunc);
|
return this.reflectionCapabilities.annotations(typeOrFunc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interfaces(type): List<any> {
|
interfaces(type): List<any> {
|
||||||
if (MapWrapper.contains(this._typeInfo, type)) {
|
if (MapWrapper.contains(this._typeInfo, type)) {
|
||||||
return MapWrapper.get(this._typeInfo, type)["interfaces"];
|
return this._getTypeInfoField(type, "interfaces", []);
|
||||||
} else {
|
} else {
|
||||||
return this.reflectionCapabilities.interfaces(type);
|
return this.reflectionCapabilities.interfaces(type);
|
||||||
}
|
}
|
||||||
|
@ -83,6 +89,13 @@ export class Reflector {
|
||||||
return this.reflectionCapabilities.method(name);
|
return this.reflectionCapabilities.method(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getTypeInfoField(typeOrFunc, key, defaultValue) {
|
||||||
|
var res = MapWrapper.get(this._typeInfo, typeOrFunc)[key];
|
||||||
|
return isPresent(res) ? res : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_containsTypeInfo(typeOrFunc) { return MapWrapper.contains(this._typeInfo, typeOrFunc); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function _mergeMaps(target: Map<any, any>, config: Map<string, Function>): void {
|
function _mergeMaps(target: Map<any, any>, config: Map<string, Function>): void {
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/test_lib';
|
|
||||||
import {Directive, onChange} from 'angular2/src/core/annotations_impl/annotations';
|
|
||||||
|
|
||||||
export function main() {
|
|
||||||
describe("Directive", () => {
|
|
||||||
describe("lifecycle", () => {
|
|
||||||
it("should be false when no lifecycle specified", () => {
|
|
||||||
var d = new Directive();
|
|
||||||
expect(d.hasLifecycleHook(onChange)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be false when the lifecycle does not contain the hook", () => {
|
|
||||||
var d = new Directive({lifecycle: []});
|
|
||||||
expect(d.hasLifecycleHook(onChange)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be true otherwise", () => {
|
|
||||||
var d = new Directive({lifecycle: [onChange]});
|
|
||||||
expect(d.hasLifecycleHook(onChange)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
library angular2.test.core.compiler.directive_lifecycle_spec;
|
||||||
|
|
||||||
|
import 'package:angular2/test_lib.dart';
|
||||||
|
import 'package:angular2/angular2.dart';
|
||||||
|
import 'package:angular2/src/core/compiler/element_injector.dart';
|
||||||
|
|
||||||
|
|
||||||
|
main() {
|
||||||
|
describe('Create DirectiveMetadata', () {
|
||||||
|
describe('lifecycle', () {
|
||||||
|
metadata(type, annotation) => DirectiveBinding.createFromType(type, annotation).metadata;
|
||||||
|
|
||||||
|
describe("onChange", () {
|
||||||
|
it("should be true when the directive implements OnChange", () {
|
||||||
|
expect(metadata(DirectiveImplementingOnChange, new Directive()).callOnChange).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true when the lifecycle includes onChange", () {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive(lifecycle: [onChange])).callOnChange).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false otherwise", () {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive()).callOnChange).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false when empty lifecycle", () {
|
||||||
|
expect(metadata(DirectiveImplementingOnChange, new Directive(lifecycle: [])).callOnChange).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("onDestroy", () {
|
||||||
|
it("should be true when the directive implements OnDestroy", () {
|
||||||
|
expect(metadata(DirectiveImplementingOnDestroy, new Directive()).callOnDestroy).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true when the lifecycle includes onDestroy", () {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive(lifecycle: [onDestroy])).callOnDestroy).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false otherwise", () {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive()).callOnDestroy).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("onAllChangesDone", () {
|
||||||
|
it("should be true when the directive implements OnAllChangesDone", () {
|
||||||
|
expect(metadata(DirectiveImplementingOnAllChangesDone, new Directive()).callOnAllChangesDone).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true when the lifecycle includes onAllChangesDone", () {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive(lifecycle: [onAllChangesDone])).callOnAllChangesDone).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false otherwise", () {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive()).callOnAllChangesDone).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectiveNoHooks {
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectiveImplementingOnChange implements OnChange {
|
||||||
|
onChange(_){}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectiveImplementingOnDestroy implements OnDestroy {
|
||||||
|
onDestroy(){}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectiveImplementingOnAllChangesDone implements OnAllChangesDone {
|
||||||
|
onAllChangesDone(){}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
import {
|
||||||
|
AsyncTestCompleter,
|
||||||
|
beforeEach,
|
||||||
|
xdescribe,
|
||||||
|
ddescribe,
|
||||||
|
describe,
|
||||||
|
el,
|
||||||
|
expect,
|
||||||
|
iit,
|
||||||
|
inject,
|
||||||
|
IS_DARTIUM,
|
||||||
|
it,
|
||||||
|
SpyObject,
|
||||||
|
proxy
|
||||||
|
} from 'angular2/test_lib';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Directive,
|
||||||
|
onChange,
|
||||||
|
onDestroy,
|
||||||
|
onAllChangesDone
|
||||||
|
} from 'angular2/src/core/annotations_impl/annotations';
|
||||||
|
import {DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
describe('Create DirectiveMetadata', () => {
|
||||||
|
describe('lifecycle', () => {
|
||||||
|
function metadata(type, annotation) {
|
||||||
|
return DirectiveBinding.createFromType(type, annotation).metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("onChange", () => {
|
||||||
|
it("should be true when the directive has the onChange method", () => {
|
||||||
|
expect(metadata(DirectiveWithOnChangeMethod, new Directive({})).callOnChange).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true when the lifecycle includes onChange", () => {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive({lifecycle: [onChange]})).callOnChange)
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false otherwise",
|
||||||
|
() => { expect(metadata(DirectiveNoHooks, new Directive()).callOnChange).toBe(false); });
|
||||||
|
|
||||||
|
it("should be false when empty lifecycle", () => {
|
||||||
|
expect(metadata(DirectiveWithOnChangeMethod, new Directive({lifecycle: []})).callOnChange)
|
||||||
|
.toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("onDestroy", () => {
|
||||||
|
it("should be true when the directive has the onDestroy method", () => {
|
||||||
|
expect(metadata(DirectiveWithOnDestroyMethod, new Directive({})).callOnDestroy)
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true when the lifecycle includes onDestroy", () => {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive({lifecycle: [onDestroy]})).callOnDestroy)
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false otherwise", () => {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive()).callOnDestroy).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("onAllChangesDone", () => {
|
||||||
|
it("should be true when the directive has the onAllChangesDone method", () => {
|
||||||
|
expect(
|
||||||
|
metadata(DirectiveWithOnAllChangesDoneMethod, new Directive({})).callOnAllChangesDone)
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true when the lifecycle includes onAllChangesDone", () => {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive({lifecycle: [onAllChangesDone]}))
|
||||||
|
.callOnAllChangesDone)
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false otherwise", () => {
|
||||||
|
expect(metadata(DirectiveNoHooks, new Directive()).callOnAllChangesDone).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectiveNoHooks {}
|
||||||
|
|
||||||
|
class DirectiveWithOnChangeMethod {
|
||||||
|
onChange(_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectiveWithOnDestroyMethod {
|
||||||
|
onDestroy(_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectiveWithOnAllChangesDoneMethod {
|
||||||
|
onAllChangesDone(_) {}
|
||||||
|
}
|
|
@ -42,8 +42,7 @@ class TestObj {
|
||||||
|
|
||||||
class Interface {}
|
class Interface {}
|
||||||
|
|
||||||
class ClassImplementingInterface implements Interface {
|
class ClassImplementingInterface implements Interface {}
|
||||||
}
|
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('Reflector', () => {
|
describe('Reflector', () => {
|
||||||
|
@ -85,6 +84,11 @@ export function main() {
|
||||||
reflector.registerType(TestObj, {"parameters": [1, 2]});
|
reflector.registerType(TestObj, {"parameters": [1, 2]});
|
||||||
expect(reflector.parameters(TestObj)).toEqual([1, 2]);
|
expect(reflector.parameters(TestObj)).toEqual([1, 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return an empty list when no paramters field in the stored type info", () => {
|
||||||
|
reflector.registerType(TestObj, {});
|
||||||
|
expect(reflector.parameters(TestObj)).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("annotations", () => {
|
describe("annotations", () => {
|
||||||
|
|
Loading…
Reference in New Issue