mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-22 07:19:41 +00:00
Graph serialisation update - Links & Reroutes (#279)
* nit * Add LGraph state POCO Moves last_link_id, last_node_id and creates same for group and reroute fix import * Add new serialisation to LGraph Add LGraph schema 1 Remove reroute from old serialised graph Remove brittle inherited types Ensure stale links are not kept when clearing graph * Add serialisable exports * Ensure valid LGraph.state during deserialise
This commit is contained in:
168
src/LGraph.ts
168
src/LGraph.ts
@@ -1,6 +1,6 @@
|
|||||||
import type { Dictionary, IContextMenuValue, ISlotType, MethodNames, Point } from "./interfaces"
|
import type { Dictionary, IContextMenuValue, ISlotType, MethodNames, Point } from "./interfaces"
|
||||||
import type { ISerialisedGraph } from "./types/serialisation"
|
import type { ISerialisedGraph, Serialisable, SerialisableGraph } from "./types/serialisation"
|
||||||
import { LGraphEventMode, TitleMode } from "./types/globalEnums"
|
import { LGraphEventMode } from "./types/globalEnums"
|
||||||
import { LiteGraph } from "./litegraph"
|
import { LiteGraph } from "./litegraph"
|
||||||
import { LGraphCanvas } from "./LGraphCanvas"
|
import { LGraphCanvas } from "./LGraphCanvas"
|
||||||
import { LGraphGroup } from "./LGraphGroup"
|
import { LGraphGroup } from "./LGraphGroup"
|
||||||
@@ -14,6 +14,13 @@ interface IGraphInput {
|
|||||||
value?: unknown
|
value?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LGraphState {
|
||||||
|
lastGroupId: number
|
||||||
|
lastNodeId: number
|
||||||
|
lastLinkId: number
|
||||||
|
lastRerouteId: number
|
||||||
|
}
|
||||||
|
|
||||||
type ParamsArray<T extends Record<any, any>, K extends MethodNames<T>> = Parameters<T[K]>[1] extends undefined ? Parameters<T[K]> | Parameters<T[K]>[0] : Parameters<T[K]>
|
type ParamsArray<T extends Record<any, any>, K extends MethodNames<T>> = Parameters<T[K]>[1] extends undefined ? Parameters<T[K]> | Parameters<T[K]>[0] : Parameters<T[K]>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,8 +30,9 @@ type ParamsArray<T extends Record<any, any>, K extends MethodNames<T>> = Paramet
|
|||||||
+ onNodeRemoved: when a node inside this graph is removed
|
+ onNodeRemoved: when a node inside this graph is removed
|
||||||
+ onNodeConnectionChange: some connection has changed in the graph (connected or disconnected)
|
+ onNodeConnectionChange: some connection has changed in the graph (connected or disconnected)
|
||||||
*/
|
*/
|
||||||
|
export class LGraph implements Serialisable<SerialisableGraph> {
|
||||||
|
static serialisedSchemaVersion = 1 as const
|
||||||
|
|
||||||
export class LGraph {
|
|
||||||
//default supported types
|
//default supported types
|
||||||
static supported_types = ["number", "string", "boolean"]
|
static supported_types = ["number", "string", "boolean"]
|
||||||
static STATUS_STOPPED = 1
|
static STATUS_STOPPED = 1
|
||||||
@@ -47,11 +55,9 @@ export class LGraph {
|
|||||||
links: Map<LinkId, LLink> & Record<LinkId, LLink>
|
links: Map<LinkId, LLink> & Record<LinkId, LLink>
|
||||||
list_of_graphcanvas: LGraphCanvas[] | null
|
list_of_graphcanvas: LGraphCanvas[] | null
|
||||||
status: number
|
status: number
|
||||||
last_node_id: number
|
|
||||||
last_link_id: number
|
state: LGraphState
|
||||||
/** The largest ID created by this graph */
|
|
||||||
last_reroute_id: number
|
|
||||||
lastGroupId: number = -1
|
|
||||||
_nodes: LGraphNode[]
|
_nodes: LGraphNode[]
|
||||||
_nodes_by_id: Record<NodeId, LGraphNode>
|
_nodes_by_id: Record<NodeId, LGraphNode>
|
||||||
_nodes_in_order: LGraphNode[]
|
_nodes_in_order: LGraphNode[]
|
||||||
@@ -72,6 +78,7 @@ export class LGraph {
|
|||||||
_last_trigger_time?: number
|
_last_trigger_time?: number
|
||||||
filter?: string
|
filter?: string
|
||||||
_subgraph_node?: LGraphNode
|
_subgraph_node?: LGraphNode
|
||||||
|
/** Must contain serialisable values, e.g. primitive types */
|
||||||
config: { align_to_grid?: any; links_ontop?: any }
|
config: { align_to_grid?: any; links_ontop?: any }
|
||||||
vars: Dictionary<unknown>
|
vars: Dictionary<unknown>
|
||||||
nodes_executing: boolean[]
|
nodes_executing: boolean[]
|
||||||
@@ -80,6 +87,22 @@ export class LGraph {
|
|||||||
extra: Record<any, any>
|
extra: Record<any, any>
|
||||||
inputs: Dictionary<IGraphInput>
|
inputs: Dictionary<IGraphInput>
|
||||||
outputs: Dictionary<IGraphInput>
|
outputs: Dictionary<IGraphInput>
|
||||||
|
|
||||||
|
/** @deprecated See {@link state}.{@link LGraphState.lastNodeId lastNodeId} */
|
||||||
|
get last_node_id() {
|
||||||
|
return this.state.lastNodeId
|
||||||
|
}
|
||||||
|
set last_node_id(value) {
|
||||||
|
this.state.lastNodeId = value
|
||||||
|
}
|
||||||
|
/** @deprecated See {@link state}.{@link LGraphState.lastLinkId lastLinkId} */
|
||||||
|
get last_link_id() {
|
||||||
|
return this.state.lastLinkId
|
||||||
|
}
|
||||||
|
set last_link_id(value) {
|
||||||
|
this.state.lastLinkId = value
|
||||||
|
}
|
||||||
|
|
||||||
onInputsOutputsChange?(): void
|
onInputsOutputsChange?(): void
|
||||||
onInputAdded?(name: string, type: string): void
|
onInputAdded?(name: string, type: string): void
|
||||||
onAfterStep?(): void
|
onAfterStep?(): void
|
||||||
@@ -102,8 +125,8 @@ export class LGraph {
|
|||||||
onAfterChange?(graph: LGraph, info?: LGraphNode): void
|
onAfterChange?(graph: LGraph, info?: LGraphNode): void
|
||||||
onConnectionChange?(node: LGraphNode): void
|
onConnectionChange?(node: LGraphNode): void
|
||||||
on_change?(graph: LGraph): void
|
on_change?(graph: LGraph): void
|
||||||
onSerialize?(data: ISerialisedGraph): void
|
onSerialize?(data: ISerialisedGraph | SerialisableGraph): void
|
||||||
onConfigure?(data: ISerialisedGraph): void
|
onConfigure?(data: ISerialisedGraph | SerialisableGraph): void
|
||||||
onGetNodeMenuOptions?(options: IContextMenuValue[], node: LGraphNode): void
|
onGetNodeMenuOptions?(options: IContextMenuValue[], node: LGraphNode): void
|
||||||
onNodeConnectionChange?(nodeSlotType: ISlotType, targetNode: LGraphNode, slotIndex: number, sourceNode?: LGraphNode, sourceSlotIndex?: number): void
|
onNodeConnectionChange?(nodeSlotType: ISlotType, targetNode: LGraphNode, slotIndex: number, sourceNode?: LGraphNode, sourceSlotIndex?: number): void
|
||||||
|
|
||||||
@@ -113,7 +136,7 @@ export class LGraph {
|
|||||||
* See {@link LGraph}
|
* See {@link LGraph}
|
||||||
* @param o data from previous serialization [optional]
|
* @param o data from previous serialization [optional]
|
||||||
*/
|
*/
|
||||||
constructor(o?: ISerialisedGraph) {
|
constructor(o?: ISerialisedGraph | SerialisableGraph) {
|
||||||
if (LiteGraph.debug) console.log("Graph created")
|
if (LiteGraph.debug) console.log("Graph created")
|
||||||
|
|
||||||
/** @see MapProxyHandler */
|
/** @see MapProxyHandler */
|
||||||
@@ -141,8 +164,12 @@ export class LGraph {
|
|||||||
this.stop()
|
this.stop()
|
||||||
this.status = LGraph.STATUS_STOPPED
|
this.status = LGraph.STATUS_STOPPED
|
||||||
|
|
||||||
this.last_node_id = 0
|
this.state = {
|
||||||
this.last_link_id = 0
|
lastGroupId: 0,
|
||||||
|
lastNodeId: 0,
|
||||||
|
lastLinkId: 0,
|
||||||
|
lastRerouteId: 0
|
||||||
|
}
|
||||||
|
|
||||||
this._version = -1 //used to detect changes
|
this._version = -1 //used to detect changes
|
||||||
|
|
||||||
@@ -158,6 +185,7 @@ export class LGraph {
|
|||||||
this._nodes_by_id = {}
|
this._nodes_by_id = {}
|
||||||
this._nodes_in_order = [] //nodes sorted in execution order
|
this._nodes_in_order = [] //nodes sorted in execution order
|
||||||
this._nodes_executable = null //nodes that contain onExecute sorted in execution order
|
this._nodes_executable = null //nodes that contain onExecute sorted in execution order
|
||||||
|
this._links.clear()
|
||||||
|
|
||||||
//other scene stuff
|
//other scene stuff
|
||||||
this._groups = []
|
this._groups = []
|
||||||
@@ -654,13 +682,14 @@ export class LGraph {
|
|||||||
*/
|
*/
|
||||||
add(node: LGraphNode | LGraphGroup, skip_compute_order?: boolean): LGraphNode | null | undefined {
|
add(node: LGraphNode | LGraphGroup, skip_compute_order?: boolean): LGraphNode | null | undefined {
|
||||||
if (!node) return
|
if (!node) return
|
||||||
|
const { state } = this
|
||||||
|
|
||||||
// LEGACY: This was changed from constructor === LGraphGroup
|
// LEGACY: This was changed from constructor === LGraphGroup
|
||||||
//groups
|
//groups
|
||||||
if (node instanceof LGraphGroup) {
|
if (node instanceof LGraphGroup) {
|
||||||
// Assign group ID
|
// Assign group ID
|
||||||
if (node.id == null || node.id === -1) node.id = ++this.lastGroupId
|
if (node.id == null || node.id === -1) node.id = ++state.lastGroupId
|
||||||
if (node.id > this.lastGroupId) this.lastGroupId = node.id
|
if (node.id > state.lastGroupId) state.lastGroupId = node.id
|
||||||
|
|
||||||
this._groups.push(node)
|
this._groups.push(node)
|
||||||
this.setDirtyCanvas(true)
|
this.setDirtyCanvas(true)
|
||||||
@@ -677,7 +706,7 @@ export class LGraph {
|
|||||||
)
|
)
|
||||||
node.id = LiteGraph.use_uuids
|
node.id = LiteGraph.use_uuids
|
||||||
? LiteGraph.uuidv4()
|
? LiteGraph.uuidv4()
|
||||||
: ++this.last_node_id
|
: ++state.lastNodeId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
|
if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
|
||||||
@@ -691,9 +720,9 @@ export class LGraph {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (node.id == null || node.id == -1) {
|
if (node.id == null || node.id == -1) {
|
||||||
node.id = ++this.last_node_id
|
node.id = ++state.lastNodeId
|
||||||
} else if (typeof node.id === "number" && this.last_node_id < node.id) {
|
} else if (typeof node.id === "number" && state.lastNodeId < node.id) {
|
||||||
this.last_node_id = node.id
|
state.lastNodeId = node.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1190,67 +1219,106 @@ export class LGraph {
|
|||||||
//save and recover app state ***************************************
|
//save and recover app state ***************************************
|
||||||
/**
|
/**
|
||||||
* Creates a Object containing all the info about this graph, it can be serialized
|
* Creates a Object containing all the info about this graph, it can be serialized
|
||||||
|
* @deprecated Use {@link asSerialisable}, which returns the newer schema version.
|
||||||
|
*
|
||||||
* @return {Object} value of the node
|
* @return {Object} value of the node
|
||||||
*/
|
*/
|
||||||
serialize(option?: { sortNodes: boolean }): ISerialisedGraph {
|
serialize(option?: { sortNodes: boolean }): ISerialisedGraph {
|
||||||
const nodes = !LiteGraph.use_uuids && option?.sortNodes
|
const { config, state, groups, nodes, extra } = this.asSerialisable(option)
|
||||||
|
const links = [...this._links.values()].map(x => x.serialize())
|
||||||
|
|
||||||
|
return {
|
||||||
|
last_node_id: state.lastNodeId,
|
||||||
|
last_link_id: state.lastLinkId,
|
||||||
|
nodes,
|
||||||
|
links,
|
||||||
|
groups,
|
||||||
|
config,
|
||||||
|
extra,
|
||||||
|
version: LiteGraph.VERSION,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a shallow copy of this object for immediate serialisation or structuredCloning.
|
||||||
|
* The return value should be discarded immediately.
|
||||||
|
* @param options Serialise options = currently `sortNodes: boolean`, whether to sort nodes by ID.
|
||||||
|
* @returns A shallow copy of parts of this graph, with shallow copies of its serialisable objects.
|
||||||
|
* Mutating the properties of the return object may result in changes to your graph.
|
||||||
|
* It is intended for use with {@link structuredClone} or {@link JSON.stringify}.
|
||||||
|
*/
|
||||||
|
asSerialisable(options?: { sortNodes: boolean }): SerialisableGraph {
|
||||||
|
const { config, state, extra } = this
|
||||||
|
|
||||||
|
const nodeList = !LiteGraph.use_uuids && options?.sortNodes
|
||||||
// @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers.
|
// @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers.
|
||||||
? [...this._nodes].sort((a, b) => a.id - b.id)
|
? [...this._nodes].sort((a, b) => a.id - b.id)
|
||||||
: this._nodes
|
: this._nodes
|
||||||
const nodes_info = nodes.map(node => node.serialize())
|
|
||||||
|
|
||||||
//pack link info into a non-verbose format
|
const nodes = nodeList.map(node => node.serialize())
|
||||||
const links: SerialisedLLinkArray[] = []
|
const groups = this._groups.map(x => x.serialize())
|
||||||
for (const link of this._links.values()) {
|
|
||||||
links.push(link.serialize())
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups_info = []
|
const links = [...this._links.values()].map(x => x.asSerialisable())
|
||||||
for (let i = 0; i < this._groups.length; ++i) {
|
|
||||||
groups_info.push(this._groups[i].serialize())
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: ISerialisedGraph = {
|
const data: SerialisableGraph = {
|
||||||
last_node_id: this.last_node_id,
|
version: LGraph.serialisedSchemaVersion,
|
||||||
last_link_id: this.last_link_id,
|
config,
|
||||||
nodes: nodes_info,
|
state,
|
||||||
links: links,
|
groups,
|
||||||
groups: groups_info,
|
nodes,
|
||||||
config: this.config,
|
links,
|
||||||
extra: this.extra,
|
extra
|
||||||
version: LiteGraph.VERSION
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onSerialize?.(data)
|
this.onSerialize?.(data)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure a graph from a JSON string
|
* Configure a graph from a JSON string
|
||||||
* @param {String} str configure a graph from a JSON string
|
* @param {String} str configure a graph from a JSON string
|
||||||
* @param {Boolean} returns if there was any error parsing
|
* @param {Boolean} returns if there was any error parsing
|
||||||
*/
|
*/
|
||||||
configure(data: ISerialisedGraph, keep_old?: boolean): boolean | undefined {
|
configure(data: ISerialisedGraph | SerialisableGraph, keep_old?: boolean): boolean | undefined {
|
||||||
// TODO: Finish typing configure()
|
// TODO: Finish typing configure()
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
|
||||||
if (!keep_old) this.clear()
|
if (!keep_old) this.clear()
|
||||||
|
|
||||||
const nodesData = data.nodes
|
if (data.version === 0.4) {
|
||||||
|
// Deprecated - old schema version, links are arrays
|
||||||
|
if (Array.isArray(data.links)) {
|
||||||
|
for (const linkData of data.links) {
|
||||||
|
const link = LLink.createFromArray(linkData)
|
||||||
|
this._links.set(link.id, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New schema - one version so far, no check required.
|
||||||
|
|
||||||
// LEGACY: This was changed from constructor === Array
|
// State
|
||||||
//decode links info (they are very verbose)
|
if (data.state) {
|
||||||
if (Array.isArray(data.links)) {
|
const { state: { lastGroupId, lastLinkId, lastNodeId, lastRerouteId } } = data
|
||||||
this._links.clear()
|
if (lastGroupId != null) this.state.lastGroupId = lastGroupId
|
||||||
for (const link_data of data.links) {
|
if (lastLinkId != null) this.state.lastLinkId = lastLinkId
|
||||||
const link = LLink.createFromArray(link_data)
|
if (lastNodeId != null) this.state.lastNodeId = lastNodeId
|
||||||
this._links.set(link.id, link)
|
if (lastRerouteId != null) this.state.lastRerouteId = lastRerouteId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links
|
||||||
|
if (Array.isArray(data.links)) {
|
||||||
|
for (const linkData of data.links) {
|
||||||
|
const link = LLink.create(linkData)
|
||||||
|
this._links.set(link.id, link)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nodesData = data.nodes
|
||||||
|
|
||||||
//copy all stored fields
|
//copy all stored fields
|
||||||
for (const i in data) {
|
for (const i in data) {
|
||||||
//links must be accepted
|
//links must be accepted
|
||||||
if (i == "nodes" || i == "groups" || i == "links")
|
if (i == "nodes" || i == "groups" || i == "links" || i === "state")
|
||||||
continue
|
continue
|
||||||
this[i] = data[i]
|
this[i] = data[i]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1929,8 +1929,8 @@ export class LGraphNode implements Positionable, IPinnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UUID: LinkIds
|
// UUID: LinkIds
|
||||||
// const nextId = LiteGraph.use_uuids ? LiteGraph.uuidv4() : ++graph.last_link_id
|
// const nextId = LiteGraph.use_uuids ? LiteGraph.uuidv4() : ++graph.state.lastLinkId
|
||||||
const nextId = ++graph.last_link_id
|
const nextId = ++graph.state.lastLinkId
|
||||||
|
|
||||||
//create link class
|
//create link class
|
||||||
link_info = new LLink(
|
link_info = new LLink(
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export class LiteGraphGlobal {
|
|||||||
SlotType = SlotType
|
SlotType = SlotType
|
||||||
LabelPosition = LabelPosition
|
LabelPosition = LabelPosition
|
||||||
|
|
||||||
VERSION = 0.4
|
/** Used in serialised graphs at one point. */
|
||||||
|
VERSION = 0.4 as const
|
||||||
|
|
||||||
CANVAS_GRID_SIZE = 10
|
CANVAS_GRID_SIZE = 10
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export { IWidget }
|
|||||||
export { LGraphBadge, BadgePosition }
|
export { LGraphBadge, BadgePosition }
|
||||||
export { SlotShape, LabelPosition, SlotDirection, SlotType }
|
export { SlotShape, LabelPosition, SlotDirection, SlotType }
|
||||||
export { EaseFunction } from "./types/globalEnums"
|
export { EaseFunction } from "./types/globalEnums"
|
||||||
|
export type { SerialisableGraph, SerialisableLLink } from "./types/serialisation"
|
||||||
|
|
||||||
export function clamp(v: number, a: number, b: number): number {
|
export function clamp(v: number, a: number, b: number): number {
|
||||||
return a > v ? a : b < v ? b : v
|
return a > v ? a : b < v ? b : v
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ISlotType, Dictionary, INodeFlags, INodeInputSlot, INodeOutputSlot, Point, Rect, Size } from "../interfaces"
|
import type { ISlotType, Dictionary, INodeFlags, INodeInputSlot, INodeOutputSlot, Point, Rect, Size } from "../interfaces"
|
||||||
import type { LGraph } from "../LGraph"
|
import type { LGraph, LGraphState } from "../LGraph"
|
||||||
import type { IGraphGroupFlags, LGraphGroup } from "../LGraphGroup"
|
import type { IGraphGroupFlags, LGraphGroup } from "../LGraphGroup"
|
||||||
import type { LGraphNode, NodeId } from "../LGraphNode"
|
import type { LGraphNode, NodeId } from "../LGraphNode"
|
||||||
import type { LiteGraph } from "../litegraph"
|
import type { LiteGraph } from "../litegraph"
|
||||||
@@ -19,6 +19,17 @@ export interface Serialisable<SerialisableObject> {
|
|||||||
asSerialisable(): SerialisableObject
|
asSerialisable(): SerialisableObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SerialisableGraph {
|
||||||
|
/** Schema version. @remarks Version bump should add to const union, which is used to narrow type during deserialise. */
|
||||||
|
version: 0 | 1
|
||||||
|
config: LGraph["config"]
|
||||||
|
state: LGraphState
|
||||||
|
groups?: ISerialisedGroup[]
|
||||||
|
nodes?: ISerialisedNode[]
|
||||||
|
links?: SerialisableLLink[]
|
||||||
|
extra?: Record<any, any>
|
||||||
|
}
|
||||||
|
|
||||||
/** Serialised LGraphNode */
|
/** Serialised LGraphNode */
|
||||||
export interface ISerialisedNode {
|
export interface ISerialisedNode {
|
||||||
title?: string
|
title?: string
|
||||||
@@ -40,15 +51,17 @@ export interface ISerialisedNode {
|
|||||||
widgets_values?: TWidgetValue[]
|
widgets_values?: TWidgetValue[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Contains serialised graph elements */
|
/**
|
||||||
|
* Original implementation from static litegraph.d.ts
|
||||||
|
* Maintained for backwards compat
|
||||||
|
*/
|
||||||
export type ISerialisedGraph<
|
export type ISerialisedGraph<
|
||||||
TNode = ReturnType<LGraphNode["serialize"]>,
|
TNode = ReturnType<LGraphNode["serialize"]>,
|
||||||
TLink = ReturnType<LLink["serialize"]>,
|
TLink = ReturnType<LLink["serialize"]>,
|
||||||
TGroup = ReturnType<LGraphGroup["serialize"]>
|
TGroup = ReturnType<LGraphGroup["serialize"]>
|
||||||
> = {
|
> = {
|
||||||
last_node_id: LGraph["last_node_id"]
|
last_node_id: NodeId
|
||||||
last_link_id: LGraph["last_link_id"]
|
last_link_id: number
|
||||||
last_reroute_id?: LGraph["last_reroute_id"]
|
|
||||||
nodes: TNode[]
|
nodes: TNode[]
|
||||||
links: TLink[]
|
links: TLink[]
|
||||||
groups: TGroup[]
|
groups: TGroup[]
|
||||||
|
|||||||
Reference in New Issue
Block a user