feat: Add event subscription option to webhooks (#4540)

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Pranav Raj S
2022-04-25 17:44:42 +05:30
committed by GitHub
parent fa51fd1d73
commit 899176a793
25 changed files with 552 additions and 359 deletions

View File

@@ -2,6 +2,10 @@
margin-right: var(--space-small);
}
.margin-bottom-small {
margin-bottom: var(--space-small);
}
.margin-right-smaller {
margin-right: var(--space-smaller);
}

View File

@@ -0,0 +1,50 @@
<template>
<span>
{{ textToBeDisplayed }}
<button class="show-more--button" @click="toggleShowMore">
{{ buttonLabel }}
</button>
</span>
</template>
<script>
export default {
props: {
text: {
type: String,
default: '',
},
limit: {
type: Number,
default: 120,
},
},
data() {
return {
showMore: false,
};
},
computed: {
textToBeDisplayed() {
if (this.showMore) {
return this.text;
}
return this.text.slice(0, this.limit) + '...';
},
buttonLabel() {
const i18nKey = !this.showMore ? 'SHOW_MORE' : 'SHOW_LESS';
return this.$t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`);
},
},
methods: {
toggleShowMore() {
this.showMore = !this.showMore;
},
},
};
</script>
<style scoped>
.show-more--button {
color: var(--w-500);
}
</style>

View File

@@ -2,6 +2,29 @@
"INTEGRATION_SETTINGS": {
"HEADER": "Integrations",
"WEBHOOK": {
"SUBSCRIBED_EVENTS": "Subscribed Events",
"FORM": {
"CANCEL": "Cancel",
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
"SUBSCRIPTIONS": {
"LABEL": "Events",
"EVENTS": {
"CONVERSATION_CREATED": "Conversation Created",
"CONVERSATION_STATUS_CHANGED": "Conversation Status Changed",
"CONVERSATION_UPDATED": "Conversation Updated",
"MESSAGE_CREATED": "Message created",
"MESSAGE_UPDATED": "Message updated",
"WEBWIDGET_TRIGGERED": "Live chat widget opened by the user"
}
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: https://example/api/webhook",
"ERROR": "Please enter a valid URL"
},
"EDIT_SUBMIT": "Update webhook",
"ADD_SUBMIT": "Create webhook"
},
"TITLE": "Webhook",
"CONFIGURE": "Configure",
"HEADER": "Webhook settings",
@@ -17,35 +40,16 @@
"EDIT": {
"BUTTON_TEXT": "Edit",
"TITLE": "Edit webhook",
"CANCEL": "Cancel",
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
"FORM": {
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: https://example/api/webhook",
"ERROR": "Please enter a valid URL"
},
"SUBMIT": "Edit webhook"
},
"API": {
"SUCCESS_MESSAGE": "Webhook URL updated successfully",
"SUCCESS_MESSAGE": "Webhook configuration updated successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
"ADD": {
"CANCEL": "Cancel",
"TITLE": "Add new webhook",
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
"FORM": {
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: https://example/api/webhook",
"ERROR": "Please enter a valid URL"
},
"SUBMIT": "Create webhook"
},
"API": {
"SUCCESS_MESSAGE": "Webhook added successfully",
"SUCCESS_MESSAGE": "Webhook configuration added successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
@@ -57,16 +61,16 @@
},
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"MESSAGE": "Are you sure to delete the webhook? (%{webhookURL})",
"YES": "Yes, Delete ",
"NO": "No, Keep it"
}
}
},
"SLACK": {
"HELP_TEXT" : {
"HELP_TEXT" : {
"TITLE": "Using Slack Integration",
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
}
},
"DELETE": {

View File

@@ -127,6 +127,10 @@
"BUTTON_TEXT": "Copy",
"COPY_SUCCESSFUL": "Code copied to clipboard successfully"
},
"SHOW_MORE_BLOCK": {
"SHOW_MORE": "Show More",
"SHOW_LESS": "Show Less"
},
"FILE_BUBBLE": {
"DOWNLOAD": "Download",
"UPLOADING": "Uploading..."

View File

@@ -1,108 +0,0 @@
<template>
<div class="column content-box">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')"
/>
<form class="row" @submit.prevent="editWebhook">
<div class="medium-12 columns">
<label :class="{ error: $v.endPoint.$error }">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.LABEL') }}
<input
v-model.trim="endPoint"
type="text"
name="endPoint"
:placeholder="
$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.PLACEHOLDER')
"
@input="$v.endPoint.$touch"
/>
<span v-if="$v.endPoint.$error" class="message">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.ERROR') }}
</span>
</label>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-button
:is-disabled="
$v.endPoint.$invalid || uiFlags.updatingItem || endPoint === url
"
:is-loading="uiFlags.updatingItem"
>
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.SUBMIT') }}
</woot-button>
<woot-button class="button clear" @click.prevent="onClose">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.CANCEL') }}
</woot-button>
</div>
</div>
</form>
</div>
</template>
<script>
import { required, url, minLength } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
export default {
mixins: [alertMixin],
props: {
id: {
type: Number,
required: true,
},
url: {
type: String,
required: true,
},
onClose: {
type: Function,
required: true,
},
},
data() {
return {
alertMessage: '',
endPoint: this.url,
webhookId: this.id,
};
},
validations: {
endPoint: {
required,
minLength: minLength(7),
url,
},
},
computed: {
...mapGetters({ uiFlags: 'webhooks/getUIFlags' }),
},
methods: {
resetForm() {
this.endPoint = '';
this.$v.endPoint.$reset();
},
async editWebhook() {
try {
await this.$store.dispatch('webhooks/update', {
webhook: { url: this.endPoint },
id: this.webhookId,
});
this.alertMessage = this.$t(
'INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.SUCCESS_MESSAGE'
);
this.resetForm();
this.onClose();
} catch (error) {
this.alertMessage =
error.response.data.message ||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
}
},
},
};
</script>

View File

@@ -1,121 +0,0 @@
<template>
<modal :show.sync="show" :on-close="onClose" :close-on-backdrop-click="false">
<div class="column content-box">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.TITLE')"
:header-content="
useInstallationName(
$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.DESC'),
globalConfig.installationName
)
"
/>
<form class="row" @submit.prevent="addWebhook">
<div class="medium-12 columns">
<label :class="{ error: $v.endPoint.$error }">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.LABEL') }}
<input
v-model.trim="endPoint"
type="text"
name="endPoint"
:placeholder="
$t(
'INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.PLACEHOLDER'
)
"
@input="$v.endPoint.$touch"
/>
<span v-if="$v.endPoint.$error" class="message">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.ERROR') }}
</span>
</label>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-button
:disabled="$v.endPoint.$invalid || addWebHook.showLoading"
:is-loading="addWebHook.showLoading"
>
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.SUBMIT') }}
</woot-button>
<woot-button class="button clear" @click.prevent="onClose">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.CANCEL') }}
</woot-button>
</div>
</div>
</form>
</div>
</modal>
</template>
<script>
import { required, url, minLength } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
import Modal from '../../../../components/Modal';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { mapGetters } from 'vuex';
export default {
components: {
Modal,
},
mixins: [alertMixin, globalConfigMixin],
props: {
onClose: {
type: Function,
required: true,
},
},
data() {
return {
endPoint: '',
addWebHook: {
showAlert: false,
showLoading: false,
},
show: true,
};
},
computed: {
...mapGetters({ globalConfig: 'globalConfig/get' }),
},
validations: {
endPoint: {
required,
minLength: minLength(7),
url,
},
},
methods: {
resetForm() {
this.endPoint = '';
this.$v.endPoint.$reset();
},
async addWebhook() {
this.addWebHook.showLoading = true;
try {
await this.$store.dispatch('webhooks/create', {
webhook: { url: this.endPoint },
});
this.addWebHook.showLoading = false;
this.addWebHook.message = this.$t(
'INTEGRATION_SETTINGS.WEBHOOK.ADD.API.SUCCESS_MESSAGE'
);
this.resetForm();
this.onClose();
} catch (error) {
this.addWebHook.showLoading = false;
this.addWebHook.message =
error.response.data.message ||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
} finally {
this.addWebHook.showLoading = false;
this.showAlert(this.addWebHook.message);
}
},
},
};
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div class="column content-box">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')"
/>
<webhook-form
:value="value"
:is-submitting="uiFlags.updatingItem"
:submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.EDIT_SUBMIT')"
@submit="onSubmit"
@cancel="onClose"
/>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
import WebhookForm from './WebhookForm.vue';
export default {
components: { WebhookForm },
mixins: [alertMixin],
props: {
value: {
type: Object,
required: true,
},
id: {
type: [Number, String],
required: true,
},
onClose: {
type: Function,
required: true,
},
},
computed: {
...mapGetters({ uiFlags: 'webhooks/getUIFlags' }),
},
methods: {
async onSubmit(webhook) {
try {
await this.$store.dispatch('webhooks/update', {
webhook,
id: this.id,
});
this.showAlert(
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.SUCCESS_MESSAGE')
);
this.onClose();
} catch (error) {
const alertMessage =
error.response.data.message ||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
this.showAlert(alertMessage);
}
},
},
};
</script>

View File

@@ -4,7 +4,7 @@
color-scheme="success"
class-names="button--fixed-right-top"
icon="add-circle"
@click="openAddPopup()"
@click="openAddPopup"
>
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }}
</woot-button>
@@ -37,35 +37,14 @@
</th>
</thead>
<tbody>
<tr v-for="(webHookItem, index) in records" :key="webHookItem.id">
<td class="webhook-link">
{{ webHookItem.url }}
</td>
<td class="button-wrapper">
<woot-button
v-tooltip.top="
$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')
"
variant="smooth"
size="tiny"
color-scheme="secondary"
icon="edit"
@click="openEditPopup(webHookItem)"
>
</woot-button>
<woot-button
v-tooltip.top="
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
"
variant="smooth"
color-scheme="alert"
size="tiny"
icon="dismiss-circle"
@click="openDeletePopup(webHookItem, index)"
>
</woot-button>
</td>
</tr>
<webhook-row
v-for="(webHookItem, index) in records"
:key="webHookItem.id"
:index="index"
:webhook="webHookItem"
@edit="openEditPopup"
@delete="openDeletePopup"
/>
</tbody>
</table>
</div>
@@ -83,24 +62,27 @@
</div>
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<new-webhook :on-close="hideAddPopup" />
<new-webhook v-if="showAddPopup" :on-close="hideAddPopup" />
</woot-modal>
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
<edit-webhook
v-if="showEditPopup"
:id="selectedWebHook.id"
:url="selectedWebHook.url"
:value="selectedWebHook"
:on-close="hideEditPopup"
/>
</woot-modal>
<woot-delete-modal
:show.sync="showDeleteConfirmationPopup"
:on-close="closeDeletePopup"
:on-confirm="confirmDeletion"
:title="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')"
:message="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE')"
:message="
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE', {
webhookURL: selectedWebHook.url,
})
"
:confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')"
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
/>
@@ -112,11 +94,13 @@ import NewWebhook from './NewWebHook';
import EditWebhook from './EditWebHook';
import alertMixin from 'shared/mixins/alertMixin';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import WebhookRow from './WebhookRow';
export default {
components: {
NewWebhook,
EditWebhook,
WebhookRow,
},
mixins: [alertMixin, globalConfigMixin],
data() {
@@ -179,11 +163,3 @@ export default {
},
};
</script>
<style scoped lang="scss">
.webhook-link {
word-break: break-word;
}
.button-wrapper button:nth-child(2) {
margin-left: var(--space-normal);
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<div class="column content-box">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.TITLE')"
:header-content="
useInstallationName(
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.DESC'),
globalConfig.installationName
)
"
/>
<webhook-form
:is-submitting="uiFlags.creatingItem"
:submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.ADD_SUBMIT')"
@submit="onSubmit"
@cancel="onClose"
/>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { mapGetters } from 'vuex';
import WebhookForm from './WebhookForm.vue';
export default {
components: { WebhookForm },
mixins: [alertMixin, globalConfigMixin],
props: {
onClose: {
type: Function,
required: true,
},
},
computed: {
...mapGetters({
globalConfig: 'globalConfig/get',
uiFlags: 'webhooks/getUIFlags',
}),
},
methods: {
async onSubmit(webhook) {
try {
await this.$store.dispatch('webhooks/create', { webhook });
this.showAlert(
this.$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.API.SUCCESS_MESSAGE')
);
this.onClose();
} catch (error) {
const message =
error.response.data.message ||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
this.showAlert(message);
}
},
},
};
</script>

View File

@@ -0,0 +1,108 @@
<template>
<form class="row" @submit.prevent="onSubmit">
<div class="medium-12 columns">
<label :class="{ error: $v.url.$error }">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.LABEL') }}
<input
v-model.trim="url"
type="text"
name="url"
:placeholder="
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.PLACEHOLDER')
"
@input="$v.url.$touch"
/>
<span v-if="$v.url.$error" class="message">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }}
</span>
</label>
<label :class="{ error: $v.url.$error }" class="margin-bottom-small">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.LABEL') }}
</label>
<div v-for="event in supportedWebhookEvents" :key="event">
<input
:id="event"
v-model="subscriptions"
type="checkbox"
:value="event"
name="subscriptions"
class="margin-right-small"
/>
<span class="fs-small">
{{ `${getEventLabel(event)} (${event})` }}
</span>
</div>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-button
:disabled="$v.$invalid || isSubmitting"
:is-loading="isSubmitting"
>
{{ submitLabel }}
</woot-button>
<woot-button class="button clear" @click.prevent="$emit('cancel')">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.CANCEL') }}
</woot-button>
</div>
</div>
</form>
</template>
<script>
import { required, url, minLength } from 'vuelidate/lib/validators';
import webhookMixin from './webhookMixin';
const SUPPORTED_WEBHOOK_EVENTS = [
'conversation_created',
'conversation_status_changed',
'conversation_updated',
'message_created',
'message_updated',
'webwidget_triggered',
];
export default {
mixins: [webhookMixin],
props: {
value: {
type: Object,
default: () => ({}),
},
isSubmitting: {
type: Boolean,
default: false,
},
submitLabel: {
type: String,
required: true,
},
},
validations: {
url: {
required,
minLength: minLength(7),
url,
},
subscriptions: {
required,
},
},
data() {
return {
url: this.value.url || '',
subscriptions: this.value.subscriptions || [],
supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS,
};
},
methods: {
onSubmit() {
this.$emit('submit', {
url: this.url,
subscriptions: this.subscriptions,
});
},
},
};
</script>

View File

@@ -0,0 +1,83 @@
<template>
<tr>
<td>
<div class="webhook--link">{{ webhook.url }}</div>
<span class="webhook--subscribed-events">
<span class="webhook--subscribed-label">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.SUBSCRIBED_EVENTS') }}:
</span>
<show-more :text="subscribedEvents" :limit="60" />
</span>
</td>
<td class="button-wrapper">
<woot-button
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')"
variant="smooth"
size="tiny"
color-scheme="secondary"
icon="edit"
@click="$emit('edit', webhook)"
>
</woot-button>
<woot-button
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')"
variant="smooth"
color-scheme="alert"
size="tiny"
icon="dismiss-circle"
@click="$emit('delete', webhook, index)"
>
</woot-button>
</td>
</tr>
</template>
<script>
import webhookMixin from './webhookMixin';
import ShowMore from 'dashboard/components/widgets/ShowMore';
export default {
components: { ShowMore },
mixins: [webhookMixin],
props: {
webhook: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
subscribedEvents() {
const { subscriptions } = this.webhook;
return subscriptions.map(event => this.getEventLabel(event)).join(', ');
},
},
};
</script>
<style scoped lang="scss">
.webhook--link {
color: var(--s-700);
font-weight: var(--font-weight-medium);
word-break: break-word;
}
.webhook--subscribed-events {
color: var(--s-500);
font-size: var(--font-size-mini);
}
.webhook--subscribed-label {
font-weight: var(--font-weight-medium);
}
.button-wrapper {
max-width: var(--space-mega);
min-width: auto;
button:nth-child(2) {
margin-left: var(--space-normal);
}
}
</style>

View File

@@ -0,0 +1,26 @@
import { createWrapper } from '@vue/test-utils';
import webhookMixin from '../webhookMixin';
import Vue from 'vue';
describe('webhookMixin', () => {
describe('#getEventLabel', () => {
it('returns correct i18n translation:', () => {
const Component = {
render() {},
title: 'WebhookComponent',
mixins: [webhookMixin],
methods: {
$t(text) {
return text;
},
},
};
const Constructor = Vue.extend(Component);
const vm = new Constructor().$mount();
const wrapper = createWrapper(vm);
expect(wrapper.vm.getEventLabel('message_created')).toEqual(
`INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.MESSAGE_CREATED`
);
});
});
});

View File

@@ -0,0 +1,10 @@
export default {
methods: {
getEventLabel(event) {
const eventName = event.toUpperCase();
return this.$t(
`INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.${eventName}`
);
},
},
};

View File

@@ -1,6 +1,6 @@
import Index from './Index';
import SettingsContent from '../Wrapper';
import Webhook from './Webhook';
import Webhook from './Webhooks/Index';
import ShowIntegration from './ShowIntegration';
import { frontendURL } from '../../../../helper/URLHelper';

View File

@@ -14,7 +14,7 @@ const state = {
export const getters = {
getWebhooks(_state) {
return _state.records;
return _state.records.sort((w1, w2) => w1.id - w2.id);
},
getUIFlags(_state) {
return _state.uiFlags;