Files
habitica/website/client/src/pages/settings/siteDataRows/webhooksRow.vue
T
2026-06-04 15:47:50 -05:00

328 lines
8.3 KiB
Vue

<template>
<div>
<h2
v-once
>
{{ $t("webhooks") }}
</h2>
<div
v-once
class="webhooks-info mb-3"
v-html="$t('webhooksInfo', hookLinks)"
>
</div>
<div
class="d-flex justify-content-center webhooks-list"
:class="{'webhooks-exists': Boolean(webhooks.length)}"
>
<table class="table table-striped">
<tr v-if="webhooks.length">
<th>{{ $t('webhookURL') }}</th>
<th>{{ $t('enabled') }}</th>
<th></th>
</tr>
<tr
v-for="(webhook, index) in webhooks"
:key="webhook.id"
>
<td style="width: 588px">
<div class="d-flex align-items-center">
<div style="width: 440px">
<validated-text-input
v-model="webhook.url"
:placeholder="$t('webhookURL')"
:is-valid="isValidUrl(webhook.url)"
:readonly="!unsaved.includes(index)"
/>
</div>
<template v-if="unsaved.includes(index)">
<button
class="btn btn-primary ml-2"
:disabled="!isValidUrl(webhook.url)"
@click="saveWebhook(webhook, index)"
>
Save
</button>
<a
class="edit-link ml-3"
@click.prevent="cancelWebhookChanges(webhook, index)"
>
{{ $t('cancel') }}
</a>
</template>
</div>
</td>
<td style="vertical-align: middle;">
<toggle-switch
v-if="!unsaved.includes(index)"
v-model="webhook.enabled"
@change="updateWebhookEnabled(webhook, index)"
/>
</td>
<td class="menu-column">
<b-dropdown
v-if="!unsaved.includes(index)"
right="right"
toggle-class="with-icon"
class="ml-2"
:no-caret="true"
>
<template #button-content>
<span
v-once
class="svg-icon inline menuIcon"
v-html="icons.menuIcon"
>
</span>
</template>
<b-dropdown-item
class="selectListItem"
@click="editWebhook(webhook, index)"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.editIcon"
></span>
<span v-once>
{{ $t('edit') }}
</span>
</span>
</b-dropdown-item>
<b-dropdown-item
class="selectListItem custom-hover--delete"
@click="deleteWebhook(webhook, index)"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.deleteIcon"
></span>
<span v-once>
{{ $t('delete') }}
</span>
</span>
</b-dropdown-item>
</b-dropdown>
</td>
</tr>
<tr>
<td
colspan="3"
:class="{'webhooks-empty': !Boolean(webhooks.length)}"
>
<button
class="btn btn-secondary d-flex align-items-center new-webhook-btn"
:class="{'webhooks-exists': Boolean(webhooks.length)}"
tabindex="0"
@click="newUnsavedWebhook()"
>
<div
class="svg-icon icon-10 color"
v-html="icons.positive"
></div>
<div class="ml-75 mr-1">
{{ $t('addWebhook') }}
</div>
</button>
</td>
</tr>
</table>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.webhooks-info {
line-height: 1.71;
color: $gray-50;
}
.svg-icon.icon-10 {
color: $green-10;
}
.menuIcon {
width: 4px;
height: 1rem;
object-fit: contain;
}
.custom-hover--delete {
--hover-color: #{$maroon-50};
--hover-background: #ffb6b83F;
}
.webhooks-list {
margin-bottom: 0.5rem;
tr:first-of-type {
th {
padding: 0.25rem;
border-top: 0;
}
}
td {
padding: 0.5rem !important;
&:first-of-type {
text-align: end;
vertical-align: middle;
padding-right: 1rem !important;
line-height: 1.71;
color: $gray-50;
}
&:not(:first-of-type) {
padding-right: 0 !important;
padding-left: 0 !important;
}
}
}
td.webhooks-empty {
border-top-color: transparent;
}
td.menu-column {
width: 2rem;
padding-left: 0 !important;
padding-right: 0 !important;
}
.new-webhook-btn:not(.webhooks-exists) {
margin: 0 auto;
}
table {
margin-bottom: 0 !important;
}
</style>
<script>
import isURL from 'validator/es/lib/isURL';
import uuid from '@/../../common/script/libs/uuid';
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import positiveIcon from '@/assets/svg/positive.svg?raw';
import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
import menuIcon from '@/assets/svg/menu.svg?raw';
import deleteIcon from '@/assets/svg/delete.svg?raw';
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
import editIcon from '@/assets/svg/edit.svg?raw';
export default {
components: { ValidatedTextInput, ToggleSwitch },
mixins: [InlineSettingMixin],
data () {
return {
icons: Object.freeze({
positive: positiveIcon,
menuIcon,
deleteIcon,
editIcon,
}),
webhooks: [], // view copy of state
unsaved: [],
hookLinks: {
linkOpen: '<a target="_blank" rel="noreferrer noopener" href="https://apidoc.habitica.com/#api-Webhook-AddWebhook">',
linkClose: '</a>',
},
};
},
mounted () {
this.setWebhooksViewCopy();
},
computed: {
...mapState({
user: 'user.data',
credentials: 'credentials',
}),
},
methods: {
isValidUrl (url) {
return isURL(url, {
require_tld: true,
require_protocol: true,
protocols: ['http', 'https'],
});
},
async newUnsavedWebhook () {
const webhookInfo = {
id: uuid(),
type: 'taskActivity',
options: {
created: false,
updated: false,
deleted: false,
scored: true,
},
url: '',
enabled: true,
};
this.unsaved.push(
this.webhooks.push(webhookInfo) - 1,
);
},
cancelWebhookChanges (webhook, index) {
if (this.unsaved.includes(index)) {
this.unsaved = this.unsaved.filter(i => i !== index);
}
if (this.user.webhooks[index]) {
this.webhooks[index] = this.user.webhooks[index];
} else {
this.webhooks.splice(index, 1);
}
},
async saveWebhook (webhook, index) {
if (!this.isValidUrl(webhook.url)) {
return;
}
const webhookId = webhook.id;
if (this.user.webhooks.every(w => w.id !== webhookId)) {
const createdWebhook = await this.$store.dispatch('user:addWebhook', { webhook });
this.user.webhooks[index] = createdWebhook;
} else {
const updatedWebhook = await this.$store.dispatch('user:updateWebhook', { webhook });
this.user.webhooks[index] = updatedWebhook;
}
this.cancelWebhookChanges(webhook, index);
},
async updateWebhookEnabled (webhook, index) {
if (this.unsaved.includes(index)) {
return;
}
const updatedWebhook = await this.$store.dispatch('user:updateWebhook', { webhook });
this.user.webhooks[index] = updatedWebhook;
},
async editWebhook (webhook, index) {
this.unsaved.push(index);
},
async deleteWebhook (webhook, index) {
await this.$store.dispatch('user:deleteWebhook', { webhook });
this.user.webhooks.splice(index, 1);
this.setWebhooksViewCopy();
},
setWebhooksViewCopy () {
this.webhooks = [...this.user.webhooks];
},
},
};
</script>