-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement template engine for message [On Hold for #760] #751
Changes from all commits
8e49eb9
caf53b9
5a2c826
938adc4
ae03c5b
3b15a1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,4 +1,10 @@ | ||||||||||||||||||||||||||||||
const { R } = require("redbean-node"); | ||||||||||||||||||||||||||||||
const { Liquid } = require( "liquidjs"); | ||||||||||||||||||||||||||||||
const { UP } = require("../src/util"); | ||||||||||||||||||||||||||||||
const dayjs = require("dayjs"); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const engine = new Liquid(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const Apprise = require("./notification-providers/apprise"); | ||||||||||||||||||||||||||||||
const Discord = require("./notification-providers/discord"); | ||||||||||||||||||||||||||||||
const Gotify = require("./notification-providers/gotify"); | ||||||||||||||||||||||||||||||
|
@@ -22,8 +28,99 @@ const Feishu = require("./notification-providers/feishu"); | |||||||||||||||||||||||||||||
const AliyunSms = require("./notification-providers/aliyun-sms"); | ||||||||||||||||||||||||||||||
const DingDing = require("./notification-providers/dingding"); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const MinimalDetailTemplate = "{{monitor.name}}: {{monitor.health}}"; | ||||||||||||||||||||||||||||||
const LowDetailTemplate = "[{{monitor.name}}] [{{monitor.health}}] {{heartbeat.msg}}"; | ||||||||||||||||||||||||||||||
const MediumDetailTemplate = `Monitor: {{monitor.name}} | ||||||||||||||||||||||||||||||
Health: {{monitor.health}} | ||||||||||||||||||||||||||||||
Address: {{monitor.url}} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
{% if heartbeat.status == 1 and monitor.upsideDown -%} | ||||||||||||||||||||||||||||||
Your Upside down {{monitor.type}} monitor is unexpectedly connected | ||||||||||||||||||||||||||||||
{%- elsif heartbeat.status == 1 and monitor.upsideDown==false -%} | ||||||||||||||||||||||||||||||
Your {{monitor.type}} monitor is up | ||||||||||||||||||||||||||||||
{%- elsif heartbeat.status == 0 and monitor.upsideDown -%} | ||||||||||||||||||||||||||||||
Your Upside down {{monitor.type}} monitor is no longer connected | ||||||||||||||||||||||||||||||
{%- elsif heartbeat.status == 0 and monitor.upsideDown == false -%} | ||||||||||||||||||||||||||||||
Your {{monitor.type}} monitor is unexpectedly down. | ||||||||||||||||||||||||||||||
{%- endif %} | ||||||||||||||||||||||||||||||
Time: {{heartbeat.time}} | ||||||||||||||||||||||||||||||
Uptime Message: {{heartbeat.msg}}`; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const FullDetailTemplate = `Monitor: {{monitor.name}} | ||||||||||||||||||||||||||||||
Health: {{monitor.health}} | ||||||||||||||||||||||||||||||
Address: {{monitor.url}} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
{% if heartbeat.status == 1 and monitor.upsideDown -%} | ||||||||||||||||||||||||||||||
Your Upside down {{monitor.type}} monitor is unexpectedly connected | ||||||||||||||||||||||||||||||
{%- elsif heartbeat.status == 1 and monitor.upsideDown==false -%} | ||||||||||||||||||||||||||||||
Your {{monitor.type}} monitor is up | ||||||||||||||||||||||||||||||
{%- elsif heartbeat.status == 0 and monitor.upsideDown -%} | ||||||||||||||||||||||||||||||
Your Upside down {{monitor.type}} monitor is no longer connected | ||||||||||||||||||||||||||||||
{%- elsif heartbeat.status == 0 and monitor.upsideDown == false -%} | ||||||||||||||||||||||||||||||
Your {{monitor.type}} monitor is unexpectedly down. | ||||||||||||||||||||||||||||||
{%- endif %} | ||||||||||||||||||||||||||||||
Time: {{heartbeat.time}} | ||||||||||||||||||||||||||||||
Uptime Message: {{heartbeat.msg}} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Tags | ||||||||||||||||||||||||||||||
---------------------------------------- | ||||||||||||||||||||||||||||||
{% for tag in monitor.tags -%} | ||||||||||||||||||||||||||||||
{{ tag.name }} | ||||||||||||||||||||||||||||||
{%- if tag.value and tag.value != "" -%} | ||||||||||||||||||||||||||||||
: {{tag.value}} | ||||||||||||||||||||||||||||||
{%- endif %} | ||||||||||||||||||||||||||||||
{% endfor -%}`; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
class Notification { | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
static generateTestHeartbeat() { | ||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||
monitorID: 5, | ||||||||||||||||||||||||||||||
status: 1, | ||||||||||||||||||||||||||||||
time: R.isoDateTime(dayjs.utc()), | ||||||||||||||||||||||||||||||
msg: "TEST NOTIFICATION MESSAGE", | ||||||||||||||||||||||||||||||
ping: 278, | ||||||||||||||||||||||||||||||
important: true, | ||||||||||||||||||||||||||||||
duration: 8, | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
static generateTestMonitor() { | ||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||
id: 5, | ||||||||||||||||||||||||||||||
name: "Test Notification Monitor", | ||||||||||||||||||||||||||||||
url: "https://www.example.com", | ||||||||||||||||||||||||||||||
method: "Get", | ||||||||||||||||||||||||||||||
body: "OK", | ||||||||||||||||||||||||||||||
headers: null, | ||||||||||||||||||||||||||||||
hostname: "www.example.com", | ||||||||||||||||||||||||||||||
port: 443, | ||||||||||||||||||||||||||||||
maxretries: 2, | ||||||||||||||||||||||||||||||
weight: 2000, | ||||||||||||||||||||||||||||||
active: 1, | ||||||||||||||||||||||||||||||
type: "HTTP", | ||||||||||||||||||||||||||||||
interval: 60, | ||||||||||||||||||||||||||||||
retryInterval: this.retryInterval, | ||||||||||||||||||||||||||||||
keyword: null, | ||||||||||||||||||||||||||||||
ignoreTls: false, | ||||||||||||||||||||||||||||||
upsideDown: false, | ||||||||||||||||||||||||||||||
maxredirects: 10, | ||||||||||||||||||||||||||||||
accepted_statuscodes: ["200-299"], | ||||||||||||||||||||||||||||||
dns_resolve_type: "A", | ||||||||||||||||||||||||||||||
dns_resolve_server: "1.1.1.1", | ||||||||||||||||||||||||||||||
dns_last_result: null, | ||||||||||||||||||||||||||||||
pushToken: null, | ||||||||||||||||||||||||||||||
notificationIDList: { "1": true, | ||||||||||||||||||||||||||||||
"5": true }, | ||||||||||||||||||||||||||||||
tags: [{ "id": 21, | ||||||||||||||||||||||||||||||
"monitor_id": 16, | ||||||||||||||||||||||||||||||
"tag_id": 2, | ||||||||||||||||||||||||||||||
"value": "", | ||||||||||||||||||||||||||||||
"name": "Internal", | ||||||||||||||||||||||||||||||
"color": "#059669" }], | ||||||||||||||||||||||||||||||
Comment on lines
+115
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
providerList = {}; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
static init() { | ||||||||||||||||||||||||||||||
|
@@ -77,36 +174,78 @@ class Notification { | |||||||||||||||||||||||||||||
* @returns {Promise<string>} Successful msg | ||||||||||||||||||||||||||||||
* Throw Error with fail msg | ||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { | ||||||||||||||||||||||||||||||
static async send(notification, msg, monitorJSON, heartbeatJSON) { | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The method signature should not be changed. As I said in: #646 (comment) This is used for general message.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have been wondering about that. There were only two calls to I wonder especially given the number of special cases that pop up around the more complicated notifiers, if it would not be better to split out those calls explicitly instead of having one function with logic for both cases.
It would make it a larger change, but given I am already going to have to go through basically every notification type to verify/test the templates and notifications anyway, I don't think that it would be all that much more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Planning for more notification such as domain renewal, cert renewal remind. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO if the function name is |
||||||||||||||||||||||||||||||
if (this.providerList[notification.type]) { | ||||||||||||||||||||||||||||||
return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
monitorJSON.health = ((heartbeatJSON.status == 1) !== monitorJSON.upsideDown) ? "✅ Healthy" : "❌ Unhealthy"; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
let parseData = { | ||||||||||||||||||||||||||||||
// I actually dont think that it is necessary to put the notification in the data sent to the template. | ||||||||||||||||||||||||||||||
// notification: notification, | ||||||||||||||||||||||||||||||
monitor: monitorJSON, | ||||||||||||||||||||||||||||||
heartbeat: heartbeatJSON, | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
let template = this.getTemplateFromNotification(notification); | ||||||||||||||||||||||||||||||
console.log(`Template: (${template})`); | ||||||||||||||||||||||||||||||
let message = await engine.parseAndRender(template, parseData); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
return this.providerList[notification.type].send(notification, message, monitorJSON, heartbeatJSON); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
//Removed try-catch here. I am not sure what the default should be in the case of a broken template. | ||||||||||||||||||||||||||||||
//switch to manually building the message? | ||||||||||||||||||||||||||||||
//the problem is that it would still need to send a message after the template fails of if it failed to send completely.. | ||||||||||||||||||||||||||||||
//im not sure if that is the desired result on a template fail. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||
throw new Error("Notification type is not supported"); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
static getTemplateFromNotification(notification) { | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Comment on lines
+204
to
+205
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
let template = notification.template; | ||||||||||||||||||||||||||||||
let detail = notification.detail; | ||||||||||||||||||||||||||||||
console.log(`Detail: (${detail}) Template: (${template})`); | ||||||||||||||||||||||||||||||
switch (detail) { | ||||||||||||||||||||||||||||||
case "Minimal Detail": | ||||||||||||||||||||||||||||||
return MinimalDetailTemplate; | ||||||||||||||||||||||||||||||
case "Low Detail": | ||||||||||||||||||||||||||||||
return LowDetailTemplate; | ||||||||||||||||||||||||||||||
case "Medium Detail": | ||||||||||||||||||||||||||||||
return MediumDetailTemplate; | ||||||||||||||||||||||||||||||
case "Full Detail": | ||||||||||||||||||||||||||||||
return FullDetailTemplate; | ||||||||||||||||||||||||||||||
case "Custom Template": | ||||||||||||||||||||||||||||||
if (template) { | ||||||||||||||||||||||||||||||
return template; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
//returns low in the case of a template being empty string or undefined. | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
return LowDetailTemplate; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
static async save(notification, notificationID, userID) { | ||||||||||||||||||||||||||||||
let bean | ||||||||||||||||||||||||||||||
let bean; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if (notificationID) { | ||||||||||||||||||||||||||||||
bean = await R.findOne("notification", " id = ? AND user_id = ? ", [ | ||||||||||||||||||||||||||||||
notificationID, | ||||||||||||||||||||||||||||||
userID, | ||||||||||||||||||||||||||||||
]) | ||||||||||||||||||||||||||||||
]); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if (! bean) { | ||||||||||||||||||||||||||||||
throw new Error("notification not found") | ||||||||||||||||||||||||||||||
throw new Error("notification not found"); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||
bean = R.dispense("notification") | ||||||||||||||||||||||||||||||
bean = R.dispense("notification"); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
bean.name = notification.name; | ||||||||||||||||||||||||||||||
bean.user_id = userID; | ||||||||||||||||||||||||||||||
bean.config = JSON.stringify(notification); | ||||||||||||||||||||||||||||||
bean.is_default = notification.isDefault || false; | ||||||||||||||||||||||||||||||
await R.store(bean) | ||||||||||||||||||||||||||||||
await R.store(bean); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if (notification.applyExisting) { | ||||||||||||||||||||||||||||||
await applyNotificationEveryMonitor(bean.id, userID); | ||||||||||||||||||||||||||||||
|
@@ -119,13 +258,13 @@ class Notification { | |||||||||||||||||||||||||||||
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [ | ||||||||||||||||||||||||||||||
notificationID, | ||||||||||||||||||||||||||||||
userID, | ||||||||||||||||||||||||||||||
]) | ||||||||||||||||||||||||||||||
]); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if (! bean) { | ||||||||||||||||||||||||||||||
throw new Error("notification not found") | ||||||||||||||||||||||||||||||
throw new Error("notification not found"); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
await R.trash(bean) | ||||||||||||||||||||||||||||||
await R.trash(bean); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
static checkApprise() { | ||||||||||||||||||||||||||||||
|
@@ -145,17 +284,17 @@ async function applyNotificationEveryMonitor(notificationID, userID) { | |||||||||||||||||||||||||||||
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [ | ||||||||||||||||||||||||||||||
monitors[i].id, | ||||||||||||||||||||||||||||||
notificationID, | ||||||||||||||||||||||||||||||
]) | ||||||||||||||||||||||||||||||
]); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if (! checkNotification) { | ||||||||||||||||||||||||||||||
let relation = R.dispense("monitor_notification"); | ||||||||||||||||||||||||||||||
relation.monitor_id = monitors[i].id; | ||||||||||||||||||||||||||||||
relation.notification_id = notificationID; | ||||||||||||||||||||||||||||||
await R.store(relation) | ||||||||||||||||||||||||||||||
await R.store(relation); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
module.exports = { | ||||||||||||||||||||||||||||||
Notification, | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
}; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -22,6 +22,23 @@ | |||||
<input id="notification-name" v-model="notification.name" type="text" class="form-control" required> | ||||||
</div> | ||||||
|
||||||
<div v-show="enableTemplateOptions" class="mb-3"> | ||||||
<label for="notification-detail" class="form-label">{{ $t("Notification Message Detail") }}</label> | ||||||
<select id="notification-detail" v-model="notification.detail" class="form-select"> | ||||||
<option v-for="detail in detailLevels" :key="detail" :value="detail">{{ $t(detail) }}</option> | ||||||
</select> | ||||||
</div> | ||||||
|
||||||
<!-- using show so that if the user toggels to a different template level, they dont loose what is in the field. --> | ||||||
<div v-show="notification.detail === 'Custom Template' && enableTemplateOptions" class="mb-3"> | ||||||
<label for="notification-text" class="form-label">{{ $t("Custom Message Template") }}</label> | ||||||
<textarea id="notification-text" v-model="notification.template" type="text" class="form-control"></textarea> | ||||||
<div v-pre class="form-text"> | ||||||
Uses Liquid templates Via LiquidJS.<br /> | ||||||
See <a href="https://github.com/louislam/uptime-kuma/wiki">the Uptime Kuma Wiki</a> for full detauls.<br /> | ||||||
</div> | ||||||
</div> | ||||||
|
||||||
<!-- form body --> | ||||||
<component :is="currentForm" /> | ||||||
|
||||||
|
@@ -72,7 +89,8 @@ import { Modal } from "bootstrap"; | |||||
import { ucfirst } from "../util.ts"; | ||||||
|
||||||
import Confirm from "./Confirm.vue"; | ||||||
import NotificationFormList from "./notifications"; | ||||||
import { NotificationFormList, NotificationDetailList, TemplateEnabledList } from "./notifications"; | ||||||
// import from "./notifications"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
export default { | ||||||
components: { | ||||||
|
@@ -92,7 +110,9 @@ export default { | |||||
type: null, | ||||||
isDefault: false, | ||||||
// Do not set default value here, please scroll to show() | ||||||
} | ||||||
}, | ||||||
detailLevels: NotificationDetailList, | ||||||
|
||||||
Comment on lines
+114
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
}; | ||||||
}, | ||||||
|
||||||
|
@@ -102,6 +122,9 @@ export default { | |||||
return null; | ||||||
} | ||||||
return NotificationFormList[this.notification.type]; | ||||||
}, | ||||||
enableTemplateOptions() { | ||||||
return (TemplateEnabledList.includes(this.notification.type)); | ||||||
} | ||||||
}, | ||||||
|
||||||
|
@@ -118,6 +141,9 @@ export default { | |||||
this.notification.name = this.getUniqueDefaultName(to); | ||||||
} | ||||||
}, | ||||||
"notification.detail"(to, from) { | ||||||
this.notification.detail = to; | ||||||
} | ||||||
}, | ||||||
mounted() { | ||||||
this.modal = new Modal(this.$refs.modal); | ||||||
|
@@ -149,6 +175,9 @@ export default { | |||||
|
||||||
// Set Default value here | ||||||
this.notification.type = this.notificationTypes[0]; | ||||||
this.notification.detail = this.detailLevels[1]; | ||||||
this.notification.template = "[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}"; | ||||||
|
||||||
Comment on lines
+179
to
+180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
this.modal.show(); | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -306,4 +306,16 @@ export default { | |||||||||||||||||||||||||||||
"One record": "One record", | ||||||||||||||||||||||||||||||
"Showing {from} to {to} of {count} records": "Showing {from} to {to} of {count} records", | ||||||||||||||||||||||||||||||
steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ", | ||||||||||||||||||||||||||||||
"Message Template":"Message Template", | ||||||||||||||||||||||||||||||
"Default Template":"[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}", | ||||||||||||||||||||||||||||||
HealthyStatus:"✅ Healthy", | ||||||||||||||||||||||||||||||
UnhealthyStatus:"❌ Unhealthy", | ||||||||||||||||||||||||||||||
Comment on lines
+309
to
+312
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
//template levels | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
"Notification Message Detail":"Notification Message Detail", | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
"Minimal Detail":"Minimal Detail", | ||||||||||||||||||||||||||||||
"Low Detail":"Low Detail", | ||||||||||||||||||||||||||||||
"Medium Detail":"Medium Detail", | ||||||||||||||||||||||||||||||
"Full Detail":"Full Detail", | ||||||||||||||||||||||||||||||
"Custom Template":"Custom Template", | ||||||||||||||||||||||||||||||
Comment on lines
+314
to
+320
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.