mirror of
https://github.com/MetinSeylan/Vue-Socket.io.git
synced 2025-04-16 15:21:28 +02:00
Rolled back changes from 8988085bef
due to numerous breaking changes.
This commit is contained in:
parent
bcd466a22e
commit
977f56102b
7 changed files with 321 additions and 461 deletions
124
README.md
124
README.md
|
@ -16,6 +16,7 @@
|
|||
<p>Vue-Socket.io is a socket.io integration for Vuejs, easy to use, supporting Vuex and component level socket consumer managements.<p>
|
||||
|
||||
###### Demo
|
||||
|
||||
- <a href="http://metinseylan.com/vuesocketio/" target="_blank">Chat Application</a>
|
||||
- <a href="http://metinseylan.com" target="_blank">Car Tracking Application</a>
|
||||
|
||||
|
@ -24,18 +25,21 @@ are you looking for old documentation? <a href="https://github.com/MetinSeylan/V
|
|||
</p>
|
||||
|
||||
#### 🚀 Installation
|
||||
|
||||
```bash
|
||||
npm install vue-socket.io --save
|
||||
```
|
||||
##### Using Connection String
|
||||
``` javascript
|
||||
import Vue from 'vue'
|
||||
import store from './store'
|
||||
import App from './App.vue'
|
||||
import VueSocketIO from 'vue-socket.io'
|
||||
import SocketIO from "socket.io-client"
|
||||
|
||||
Vue.use(new VueSocketIO({
|
||||
##### Using Connection String
|
||||
|
||||
```javascript
|
||||
import Vue from 'vue';
|
||||
import store from './store';
|
||||
import App from './App.vue';
|
||||
import VueSocketIO from 'vue-socket.io';
|
||||
|
||||
Vue.use(
|
||||
new VueSocketIO({
|
||||
debug: true,
|
||||
connection: 'http://metinseylan.com:1992',
|
||||
vuex: {
|
||||
|
@ -43,33 +47,36 @@ Vue.use(new VueSocketIO({
|
|||
actionPrefix: 'SOCKET_',
|
||||
mutationPrefix: 'SOCKET_'
|
||||
},
|
||||
options: { path: "/my-app/" } //Optional options
|
||||
}))
|
||||
options: { path: '/my-app/' } //Optional options
|
||||
})
|
||||
);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
}).$mount('#app');
|
||||
```
|
||||
|
||||
##### Using socket.io-client Instance
|
||||
|
||||
```javascript
|
||||
import Vue from 'vue'
|
||||
import store from './store'
|
||||
import App from './App.vue'
|
||||
import VueSocketIO from 'vue-socket.io'
|
||||
import SocketIO from "socket.io-client"
|
||||
import Vue from 'vue';
|
||||
import store from './store';
|
||||
import App from './App.vue';
|
||||
import VueSocketIO from 'vue-socket.io';
|
||||
import SocketIO from 'socket.io-client';
|
||||
|
||||
const options = { path: '/my-app/' }; //Options object to pass into SocketIO
|
||||
|
||||
Vue.use(new VueSocketIO({
|
||||
Vue.use(
|
||||
new VueSocketIO({
|
||||
debug: true,
|
||||
connection: SocketIO('http://metinseylan.com:1992', options), //options object is Optional
|
||||
vuex: {
|
||||
store,
|
||||
actionPrefix: "SOCKET_",
|
||||
mutationPrefix: "SOCKET_"
|
||||
actionPrefix: 'SOCKET_',
|
||||
mutationPrefix: 'SOCKET_'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -78,17 +85,17 @@ new Vue({
|
|||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
}).$mount('#app');
|
||||
```
|
||||
|
||||
**Parameters**|**Type's**|**Default**|**Required**|**Description**
|
||||
-----|-----|-----|-----|-----
|
||||
debug|Boolean|`false`|Optional|Enable logging for debug
|
||||
connection|String/Socket.io-client|`null`|Required|Websocket server url or socket.io-client instance
|
||||
vuex.store|Vuex|`null`|Optional|Vuex store instance
|
||||
vuex.actionPrefix|String|`null`|Optional|Prefix for emitting server side vuex actions
|
||||
vuex.mutationPrefix|String |`null`|Optional|Prefix for emitting server side vuex mutations
|
||||
vuex.options.useConnectionNamespace |Boolean|`false`|Optional|Use more than one connection namespace
|
||||
| **Parameters** | **Type's** | **Default** | **Required** | **Description** |
|
||||
| ----------------------------------- | ----------------------- | ----------- | ------------ | ------------------------------------------------- |
|
||||
| debug | Boolean | `false` | Optional | Enable logging for debug |
|
||||
| connection | String/Socket.io-client | `null` | Required | Websocket server url or socket.io-client instance |
|
||||
| vuex.store | Vuex | `null` | Optional | Vuex store instance |
|
||||
| vuex.actionPrefix | String | `null` | Optional | Prefix for emitting server side vuex actions |
|
||||
| vuex.mutationPrefix | String | `null` | Optional | Prefix for emitting server side vuex mutations |
|
||||
| vuex.options.useConnectionNamespace | Boolean | `false` | Optional | Use more than one connection namespace |
|
||||
|
||||
#### 🌈 Component Level Usage
|
||||
|
||||
|
@ -98,19 +105,19 @@ vuex.options.useConnectionNamespace |Boolean|`false`|Optional|Use more than one
|
|||
new Vue({
|
||||
sockets: {
|
||||
connect: function() {
|
||||
console.log('socket connected')
|
||||
console.log('socket connected');
|
||||
},
|
||||
customEmit: function(data) {
|
||||
console.log('this method was fired by the socket server. eg: io.emit("customEmit", data)')
|
||||
console.log('this method was fired by the socket server. eg: io.emit("customEmit", data)');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickButton: function(data) {
|
||||
// $socket is socket.io-client instance
|
||||
this.$socket.emit('emit_method', data)
|
||||
this.$socket.emit('emit_method', data);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
##### Dynamic Listeners
|
||||
|
@ -118,7 +125,7 @@ new Vue({
|
|||
<p>If you need consuming events dynamically in runtime, you can use `subscribe` and `unsubscribe` methods in Vue component</p>
|
||||
|
||||
```javascript
|
||||
this.sockets.subscribe('EVENT_NAME', (data) => {
|
||||
this.sockets.subscribe('EVENT_NAME', data => {
|
||||
this.msg = data.message;
|
||||
});
|
||||
|
||||
|
@ -126,73 +133,38 @@ this.sockets.unsubscribe('EVENT_NAME');
|
|||
```
|
||||
|
||||
#### 🏆 Vuex Integration
|
||||
|
||||
<p>When you set store parameter in installation, `Vue-Socket.io` will start sending events to Vuex store. The prefix for mutations and actions are both optional and will default to none. The prefix may be useful in differentiating what type of event should be sent to the store in the case of an action and mutation with the same name.</p>
|
||||
|
||||
```javascript
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
Vue.use(Vuex)
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {},
|
||||
mutations: {
|
||||
"<MUTATION_PREFIX><EVENT_NAME>"() {
|
||||
'<MUTATION_PREFIX><EVENT_NAME>'() {
|
||||
// do something
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
"<ACTION_PREFIX><EVENT_NAME>"() {
|
||||
'<ACTION_PREFIX><EVENT_NAME>'() {
|
||||
// do something
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
##### Vuex Namespaced Modules
|
||||
|
||||
<p>When using namespaced modules, the prefix, if one exists, will be parsed into the action name. For example, if the emitted event it `jobs/runTask` and the action prefix is `SOCKET_`, then the vuex action should be `SOCKET_runTask` in the jobs namespace. In this example the vuex store will receive the dispatched action: `jobs/SOCKET_runTask`.</p>
|
||||
|
||||
|
||||
#### 🏆 Connection Namespace
|
||||
<p>When you need to handle more than one namespaced connection, you need to set the `useConnectionNamespace` property of
|
||||
the options object to true. What this does is telling the plugin that you are going to be using more than one namespaced connection and you want to put every connection in their own `$socket` key.</p>
|
||||
|
||||
``` javascript
|
||||
import Vue from 'vue'
|
||||
import store from './store'
|
||||
import App from './App.vue'
|
||||
import VueSocketIO from 'vue-socket.io'
|
||||
|
||||
Vue.use(new VueSocketIO({
|
||||
debug: true,
|
||||
connection: 'http://metinseylan.com:1992/mynamespace',
|
||||
vuex: {
|
||||
store,
|
||||
options: {
|
||||
useConnectionNamespace: true
|
||||
}
|
||||
},
|
||||
options: { path: "/my-app/" } //Optional options
|
||||
}))
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
```
|
||||
|
||||
Then use it like this:
|
||||
|
||||
``` javascript
|
||||
Vue.$socket.mynamespace.emit('emit_method', data)
|
||||
```
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starcharts.herokuapp.com/MetinSeylan/Vue-Socket.io)
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/MetinSeylan/Vue-Socket.io" target="_blank">
|
||||
<img src="https://media.giphy.com/media/11jlnltQgUi2mQ/giphy.gif">
|
||||
|
|
6
dist/vue-socketio.js
vendored
6
dist/vue-socketio.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,6 @@
|
|||
import Logger from './logger';
|
||||
|
||||
export default class EventEmitter {
|
||||
|
||||
constructor(vuex = {}) {
|
||||
Logger.info(vuex ? `Vuex adapter enabled` : `Vuex adapter disabled`);
|
||||
this.store = vuex.store;
|
||||
|
@ -17,20 +16,14 @@ export default class EventEmitter{
|
|||
* @param component
|
||||
*/
|
||||
addListener(event, callback, component) {
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
|
||||
if (!this.listeners.has(event)) this.listeners.set(event, []);
|
||||
this.listeners.get(event).push({ callback, component });
|
||||
|
||||
Logger.info(`#${event} subscribe, component: ${component.$options.name}`);
|
||||
|
||||
} else {
|
||||
|
||||
throw new Error(`callback must be a function`);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,12 +32,8 @@ export default class EventEmitter{
|
|||
* @param component
|
||||
*/
|
||||
removeListener(event, component) {
|
||||
|
||||
if (this.listeners.has(event)) {
|
||||
|
||||
const listeners = this.listeners.get(event).filter(listener => (
|
||||
listener.component !== component
|
||||
));
|
||||
const listeners = this.listeners.get(event).filter(listener => listener.component !== component);
|
||||
|
||||
if (listeners.length > 0) {
|
||||
this.listeners.set(event, listeners);
|
||||
|
@ -53,9 +42,7 @@ export default class EventEmitter{
|
|||
}
|
||||
|
||||
Logger.info(`#${event} unsubscribe, component: ${component.$options.name}`);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,82 +51,67 @@ export default class EventEmitter{
|
|||
* @param args
|
||||
*/
|
||||
emit(event, args) {
|
||||
|
||||
if (this.listeners.has(event)) {
|
||||
|
||||
Logger.info(`Broadcasting: #${event}, Data:`, args);
|
||||
|
||||
this.listeners.get(event).forEach((listener) => {
|
||||
this.listeners.get(event).forEach(listener => {
|
||||
listener.callback.call(listener.component, args);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (event !== 'ping' && event !== 'pong') {
|
||||
this.dispatchStore(event, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* dispatching vuex actions
|
||||
* @param event
|
||||
* @param args
|
||||
*/
|
||||
dispatchStore(event, args) {
|
||||
|
||||
if (this.store) {
|
||||
|
||||
if (this.store._actions) {
|
||||
|
||||
let action_name = event;
|
||||
|
||||
if (!!this.actionPrefix) {
|
||||
let namespace_sep_pos = event.lastIndexOf('/');
|
||||
action_name = (namespace_sep_pos !== -1) ? [event.slice(0, namespace_sep_pos+1), this.actionPrefix, event.slice(namespace_sep_pos+1)].join('') : this.actionPrefix + event;
|
||||
action_name =
|
||||
namespace_sep_pos !== -1
|
||||
? [event.slice(0, namespace_sep_pos + 1), this.actionPrefix, event.slice(namespace_sep_pos + 1)].join('')
|
||||
: this.actionPrefix + event;
|
||||
}
|
||||
|
||||
|
||||
for (let key in this.store._actions) {
|
||||
|
||||
if (key === action_name) {
|
||||
|
||||
Logger.info(`Dispatching Action: ${key}, Data:`, args);
|
||||
|
||||
this.store.dispatch(key, args);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (this.mutationPrefix) {
|
||||
|
||||
let mutation_name = event;
|
||||
|
||||
if (!!this.mutationPrefix) {
|
||||
let namespace_sep_pos = event.lastIndexOf('/');
|
||||
mutation_name = (namespace_sep_pos !== -1) ? [event.slice(0, namespace_sep_pos+1), this.mutationPrefix, event.slice(namespace_sep_pos+1)].join('') : this.mutationPrefix + event;
|
||||
mutation_name =
|
||||
namespace_sep_pos !== -1
|
||||
? [event.slice(0, namespace_sep_pos + 1), this.mutationPrefix, event.slice(namespace_sep_pos + 1)].join(
|
||||
''
|
||||
)
|
||||
: this.mutationPrefix + event;
|
||||
}
|
||||
|
||||
for (let key in this.store._mutations) {
|
||||
|
||||
if (key === mutation_name) {
|
||||
|
||||
Logger.info(`Committing Mutation: ${key}, Data:`, args);
|
||||
|
||||
this.store.commit(key, args);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
51
src/index.js
51
src/index.js
|
@ -1,11 +1,10 @@
|
|||
import Mixin from "./mixin";
|
||||
import Logger from "./logger";
|
||||
import Listener from "./listener";
|
||||
import Emitter from "./emitter";
|
||||
import SocketIO from "socket.io-client";
|
||||
import Mixin from './mixin';
|
||||
import Logger from './logger';
|
||||
import Listener from './listener';
|
||||
import Emitter from './emitter';
|
||||
import SocketIO from 'socket.io-client';
|
||||
|
||||
export default class VueSocketIO {
|
||||
|
||||
/**
|
||||
* lets take all resource
|
||||
* @param io
|
||||
|
@ -14,14 +13,10 @@ export default class VueSocketIO {
|
|||
* @param options
|
||||
*/
|
||||
constructor({ connection, vuex, debug, options }) {
|
||||
|
||||
Logger.debug = debug;
|
||||
this.io = this.connect(connection, options);
|
||||
this.useConnectionNamespace = (options && options.useConnectionNamespace);
|
||||
this.namespaceName = (options && options.namespaceName);
|
||||
this.emitter = new Emitter(vuex);
|
||||
this.listener = new Listener(this.io, this.emitter);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,32 +24,11 @@ export default class VueSocketIO {
|
|||
* @param Vue
|
||||
*/
|
||||
install(Vue) {
|
||||
|
||||
const namespace = this.namespaceName || this.io.nsp.replace("/", "");
|
||||
|
||||
if (this.useConnectionNamespace) {
|
||||
if (typeof Vue.prototype.$socket === "object") {
|
||||
Vue.prototype.$socket = {
|
||||
...Vue.prototype.$socket,
|
||||
[namespace]: this.io
|
||||
};
|
||||
|
||||
Vue.prototype.$vueSocketIo = {...Vue.prototype.$vueSocketIo, [namespace]: this};
|
||||
} else {
|
||||
Vue.prototype.$socket = {
|
||||
[namespace]: this.io
|
||||
};
|
||||
Vue.prototype.$vueSocketIo = { [namespace]: this};
|
||||
}
|
||||
} else {
|
||||
Vue.prototype.$socket = this.io;
|
||||
Vue.prototype.$vueSocketIo = this;
|
||||
}
|
||||
|
||||
Vue.mixin(Mixin);
|
||||
|
||||
Logger.info('Vue-Socket.io plugin enabled');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,15 +37,16 @@ export default class VueSocketIO {
|
|||
* @param options
|
||||
*/
|
||||
connect(connection, options) {
|
||||
if (connection && typeof connection === "object") {
|
||||
Logger.info(`Received socket.io-client instance`);
|
||||
if (connection && typeof connection === 'object') {
|
||||
Logger.info('Received socket.io-client instance');
|
||||
|
||||
return connection;
|
||||
} else if (typeof connection === "string") {
|
||||
const io = SocketIO(connection, options);
|
||||
Logger.info(`Received connection string`);
|
||||
return (this.io = io);
|
||||
} else if (typeof connection === 'string') {
|
||||
Logger.info('Received connection string');
|
||||
|
||||
return (this.io = SocketIO(connection, options));
|
||||
} else {
|
||||
throw new Error("Unsupported connection type");
|
||||
throw new Error('Unsupported connection type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
|
||||
export default class VueSocketIOListener {
|
||||
|
||||
/**
|
||||
* socket.io-client reserved event keywords
|
||||
* @type {string[]}
|
||||
|
@ -31,14 +29,14 @@ export default class VueSocketIOListener {
|
|||
* Listening all socket.io events
|
||||
*/
|
||||
register() {
|
||||
this.io.onevent = (packet) => {
|
||||
this.io.onevent = packet => {
|
||||
let [event, ...args] = packet.data;
|
||||
|
||||
if (args.length === 1) args = args[0];
|
||||
|
||||
this.onEvent(event, args)
|
||||
this.onEvent(event, args);
|
||||
};
|
||||
VueSocketIOListener.staticEvents.forEach(event => this.io.on(event, args => this.onEvent(event, args)))
|
||||
VueSocketIOListener.staticEvents.forEach(event => this.io.on(event, args => this.onEvent(event, args)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,5 +45,4 @@ export default class VueSocketIOListener {
|
|||
onEvent(event, args) {
|
||||
this.emitter.emit(event, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,35 +1,27 @@
|
|||
/**
|
||||
* shitty logger class
|
||||
*/
|
||||
export default new class VueSocketIOLogger {
|
||||
|
||||
export default new (class VueSocketIOLogger {
|
||||
constructor() {
|
||||
this.debug = false;
|
||||
this.prefix = '%cVue-Socket.io: ';
|
||||
}
|
||||
|
||||
info(text, data = '') {
|
||||
|
||||
if(this.debug) window.console.info(this.prefix+`%c${text}`, 'color: blue; font-weight: 600', 'color: #333333', data);
|
||||
|
||||
if (this.debug)
|
||||
window.console.info(this.prefix + `%c${text}`, 'color: blue; font-weight: 600', 'color: #333333', data);
|
||||
}
|
||||
|
||||
error() {
|
||||
|
||||
if (this.debug) window.console.error(this.prefix, ...arguments);
|
||||
|
||||
}
|
||||
|
||||
warn() {
|
||||
|
||||
if (this.debug) window.console.warn(this.prefix, ...arguments);
|
||||
|
||||
}
|
||||
|
||||
event(text, data = '') {
|
||||
|
||||
if(this.debug) window.console.info(this.prefix+`%c${text}`, 'color: blue; font-weight: 600', 'color: #333333', data);
|
||||
|
||||
}
|
||||
|
||||
if (this.debug)
|
||||
window.console.info(this.prefix + `%c${text}`, 'color: blue; font-weight: 600', 'color: #333333', data);
|
||||
}
|
||||
})();
|
||||
|
|
60
src/mixin.js
60
src/mixin.js
|
@ -1,88 +1,40 @@
|
|||
export default {
|
||||
|
||||
/**
|
||||
* Assign runtime callbacks
|
||||
*/
|
||||
beforeCreate() {
|
||||
|
||||
if (!this.sockets) this.sockets = {};
|
||||
|
||||
if (typeof this.$vueSocketIo === 'object') {
|
||||
for (const namespace of Object.keys(this.$vueSocketIo)) {
|
||||
this.sockets[namespace] = {
|
||||
subscribe: (event, callback) => {
|
||||
this.$vueSocketIo[namespace].emitter.addListener(event, callback, this);
|
||||
},
|
||||
unsubscribe: (event) => {
|
||||
this.$vueSocketIo[namespace].emitter.removeListener(event, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.sockets.subscribe = (event, callback) => {
|
||||
this.$vueSocketIo.emitter.addListener(event, callback, this);
|
||||
};
|
||||
|
||||
this.sockets.unsubscribe = event => {
|
||||
this.$vueSocketIo.emitter.removeListener(event, this);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Register all socket events
|
||||
*/
|
||||
mounted() {
|
||||
|
||||
if (this.$options.sockets) {
|
||||
|
||||
if (typeof this.$vueSocketIo === 'object') {
|
||||
for (const namespace of Object.keys(this.$vueSocketIo)) {
|
||||
if (this.$options.sockets[namespace]) {
|
||||
Object.keys(this.$options.sockets[namespace]).forEach(event => {
|
||||
|
||||
if(event !== 'subscribe' && event !== 'unsubscribe') {
|
||||
this.$vueSocketIo[namespace].emitter.addListener(event, this.$options.sockets[namespace][event], this);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object.keys(this.$options.sockets).forEach(event => {
|
||||
|
||||
if (event !== 'subscribe' && event !== 'unsubscribe') {
|
||||
this.$vueSocketIo.emitter.addListener(event, this.$options.sockets[event], this);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* unsubscribe when component unmounting
|
||||
*/
|
||||
beforeDestroy() {
|
||||
|
||||
if (this.$options.sockets) {
|
||||
|
||||
if (typeof this.$vueSocketIo === 'object') {
|
||||
for (const namespace of Object.keys(this.$vueSocketIo)) {
|
||||
if (this.$options.sockets[namespace]) {
|
||||
Object.keys(this.$options.sockets[namespace]).forEach(event => {
|
||||
|
||||
this.$vueSocketIo[namespace].emitter.removeListener(event, this);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object.keys(this.$options.sockets).forEach(event => {
|
||||
|
||||
this.$vueSocketIo.emitter.removeListener(event, this);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue