2023-09-22 15:06:03 +12:00
|
|
|
<script>
|
2025-06-20 23:26:06 +12:00
|
|
|
import axios from "axios";
|
|
|
|
import commonMixins from "../../mixins/CommonMixins";
|
2023-09-22 15:06:03 +12:00
|
|
|
|
|
|
|
export default {
|
2025-06-20 23:26:06 +12:00
|
|
|
mixins: [commonMixins],
|
|
|
|
|
2023-09-22 15:06:03 +12:00
|
|
|
props: {
|
2025-06-20 23:26:06 +12:00
|
|
|
message: {
|
|
|
|
type: Object,
|
|
|
|
required: true,
|
|
|
|
},
|
2023-09-22 15:06:03 +12:00
|
|
|
},
|
|
|
|
|
|
|
|
emits: ["setLinkErrors"],
|
|
|
|
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
error: false,
|
|
|
|
autoScan: false,
|
|
|
|
followRedirects: false,
|
|
|
|
check: false,
|
|
|
|
loaded: false,
|
|
|
|
loading: false,
|
2025-06-20 23:26:06 +12:00
|
|
|
};
|
2023-09-22 15:06:03 +12:00
|
|
|
},
|
|
|
|
|
|
|
|
computed: {
|
2024-06-22 13:27:00 +12:00
|
|
|
groupedStatuses() {
|
2025-06-20 23:26:06 +12:00
|
|
|
const results = {};
|
2023-09-22 15:06:03 +12:00
|
|
|
|
|
|
|
if (!this.check) {
|
2025-06-20 23:26:06 +12:00
|
|
|
return results;
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
// group by status
|
2025-06-20 23:26:06 +12:00
|
|
|
this.check.Links.forEach((r) => {
|
2023-09-22 15:06:03 +12:00
|
|
|
if (!results[r.StatusCode]) {
|
2025-06-20 23:26:06 +12:00
|
|
|
let css = "";
|
2023-09-22 15:06:03 +12:00
|
|
|
if (r.StatusCode >= 400 || r.StatusCode === 0) {
|
2025-06-20 23:26:06 +12:00
|
|
|
css = "text-danger";
|
2023-09-22 15:06:03 +12:00
|
|
|
} else if (r.StatusCode >= 300) {
|
2025-06-20 23:26:06 +12:00
|
|
|
css = "text-info";
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
if (r.StatusCode === 0) {
|
2025-06-20 23:26:06 +12:00
|
|
|
r.Status = "Cannot connect to server";
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
|
|
|
results[r.StatusCode] = {
|
|
|
|
StatusCode: r.StatusCode,
|
|
|
|
Status: r.Status,
|
|
|
|
Class: css,
|
2025-06-20 23:26:06 +12:00
|
|
|
URLS: [],
|
|
|
|
};
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
2025-06-20 23:26:06 +12:00
|
|
|
results[r.StatusCode].URLS.push(r.URL);
|
|
|
|
});
|
2023-09-22 15:06:03 +12:00
|
|
|
|
2025-06-20 23:26:06 +12:00
|
|
|
const newArr = [];
|
2023-09-22 15:06:03 +12:00
|
|
|
|
|
|
|
for (const i in results) {
|
2025-06-20 23:26:06 +12:00
|
|
|
newArr.push(results[i]);
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
// sort statuses
|
2025-06-20 23:26:06 +12:00
|
|
|
const sorted = newArr.sort((a, b) => {
|
2023-09-22 15:06:03 +12:00
|
|
|
if (a.StatusCode === 0) {
|
2025-06-20 23:26:06 +12:00
|
|
|
return false;
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
2025-06-20 23:26:06 +12:00
|
|
|
return a.StatusCode < b.StatusCode;
|
|
|
|
});
|
2023-09-22 15:06:03 +12:00
|
|
|
|
2025-06-20 23:26:06 +12:00
|
|
|
return sorted;
|
|
|
|
},
|
|
|
|
},
|
2023-09-22 15:06:03 +12:00
|
|
|
|
2025-06-20 23:26:06 +12:00
|
|
|
watch: {
|
|
|
|
autoScan(v) {
|
|
|
|
if (!this.loaded) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (v) {
|
|
|
|
localStorage.setItem("LinkCheckAutoScan", true);
|
|
|
|
if (!this.check) {
|
|
|
|
this.doCheck();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
localStorage.removeItem("LinkCheckAutoScan");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
followRedirects(v) {
|
|
|
|
if (!this.loaded) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (v) {
|
|
|
|
localStorage.setItem("LinkCheckFollowRedirects", true);
|
|
|
|
} else {
|
|
|
|
localStorage.removeItem("LinkCheckFollowRedirects");
|
|
|
|
}
|
|
|
|
if (this.check) {
|
|
|
|
this.doCheck();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
created() {
|
|
|
|
this.autoScan = localStorage.getItem("LinkCheckAutoScan");
|
|
|
|
this.followRedirects = localStorage.getItem("LinkCheckFollowRedirects");
|
|
|
|
},
|
|
|
|
|
|
|
|
mounted() {
|
|
|
|
this.loaded = true;
|
|
|
|
if (this.autoScan) {
|
|
|
|
this.doCheck();
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
methods: {
|
2024-06-22 13:27:00 +12:00
|
|
|
doCheck() {
|
2025-06-20 23:26:06 +12:00
|
|
|
this.check = false;
|
|
|
|
this.loading = true;
|
|
|
|
let uri = this.resolve("/api/v1/message/" + this.message.ID + "/link-check");
|
2023-09-22 15:06:03 +12:00
|
|
|
if (this.followRedirects) {
|
2025-06-20 23:26:06 +12:00
|
|
|
uri += "?follow=true";
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
// ignore any error, do not show loader
|
2025-06-20 23:26:06 +12:00
|
|
|
axios
|
|
|
|
.get(uri, null)
|
2024-06-22 13:27:00 +12:00
|
|
|
.then((result) => {
|
2025-06-20 23:26:06 +12:00
|
|
|
this.check = result.data;
|
|
|
|
this.error = false;
|
2023-09-22 15:06:03 +12:00
|
|
|
|
2025-06-20 23:26:06 +12:00
|
|
|
this.$emit("setLinkErrors", result.data.Errors);
|
2023-09-22 15:06:03 +12:00
|
|
|
})
|
2024-06-22 13:27:00 +12:00
|
|
|
.catch((error) => {
|
2023-09-22 15:06:03 +12:00
|
|
|
// handle error
|
|
|
|
if (error.response && error.response.data) {
|
|
|
|
// The request was made and the server responded with a status code
|
|
|
|
// that falls out of the range of 2xx
|
|
|
|
if (error.response.data.Error) {
|
2025-06-20 23:26:06 +12:00
|
|
|
this.error = error.response.data.Error;
|
2023-09-22 15:06:03 +12:00
|
|
|
} else {
|
2025-06-20 23:26:06 +12:00
|
|
|
this.error = error.response.data;
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
|
|
|
} else if (error.request) {
|
|
|
|
// The request was made but no response was received
|
|
|
|
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
|
|
|
// http.ClientRequest in node.js
|
2025-06-20 23:26:06 +12:00
|
|
|
this.error = "Error sending data to the server. Please try again.";
|
2023-09-22 15:06:03 +12:00
|
|
|
} else {
|
|
|
|
// Something happened in setting up the request that triggered an Error
|
2025-06-20 23:26:06 +12:00
|
|
|
this.error = error.message;
|
2023-09-22 15:06:03 +12:00
|
|
|
}
|
|
|
|
})
|
2024-06-22 13:27:00 +12:00
|
|
|
.then((result) => {
|
2023-09-22 15:06:03 +12:00
|
|
|
// always run
|
2025-06-20 23:26:06 +12:00
|
|
|
this.loading = false;
|
|
|
|
});
|
2023-09-22 15:06:03 +12:00
|
|
|
},
|
2025-06-20 23:26:06 +12:00
|
|
|
},
|
|
|
|
};
|
2023-09-22 15:06:03 +12:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<div class="pe-3">
|
|
|
|
<div class="row mb-3 align-items-center">
|
|
|
|
<div class="col">
|
|
|
|
<h4 class="mb-0">
|
2025-06-20 23:26:06 +12:00
|
|
|
<template v-if="!check"> Link check </template>
|
2023-09-22 15:06:03 +12:00
|
|
|
<template v-else>
|
|
|
|
<template v-if="check.Links.length">
|
2025-06-20 23:26:06 +12:00
|
|
|
Scanned {{ formatNumber(check.Links.length) }} link<template v-if="check.Links.length != 1"
|
|
|
|
>s</template
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
</template>
|
2025-06-20 23:26:06 +12:00
|
|
|
<template v-else> No links detected </template>
|
2023-09-22 15:06:03 +12:00
|
|
|
</template>
|
|
|
|
</h4>
|
|
|
|
</div>
|
|
|
|
<div class="col-auto">
|
|
|
|
<div class="input-group">
|
2025-06-20 23:26:06 +12:00
|
|
|
<button
|
|
|
|
class="btn btn-outline-secondary"
|
|
|
|
data-bs-toggle="modal"
|
|
|
|
data-bs-target="#AboutLinkCheckResults"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
<i class="bi bi-info-circle-fill"></i>
|
|
|
|
Help
|
|
|
|
</button>
|
|
|
|
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#LinkCheckOptions">
|
|
|
|
<i class="bi bi-gear-fill"></i>
|
|
|
|
Settings
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="!check">
|
2025-05-18 10:27:59 +12:00
|
|
|
<p class="text-muted">
|
2025-06-20 23:26:06 +12:00
|
|
|
Link check scans your email text & HTML for unique links, testing the response status codes. This
|
|
|
|
includes links to images and remote CSS stylesheets.
|
2023-09-22 15:06:03 +12:00
|
|
|
</p>
|
|
|
|
|
|
|
|
<p class="text-center my-5">
|
2025-06-20 23:26:06 +12:00
|
|
|
<button v-if="!check" class="btn btn-primary btn-lg" :disabled="loading" @click="doCheck()">
|
2023-09-22 15:06:03 +12:00
|
|
|
<template v-if="loading">
|
|
|
|
Checking links
|
|
|
|
<div class="ms-1 spinner-border spinner-border-sm text-light" role="status">
|
|
|
|
<span class="visually-hidden">Loading...</span>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<i class="bi bi-check-square me-2"></i>
|
|
|
|
Check message links
|
|
|
|
</template>
|
|
|
|
</button>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
|
2025-06-20 23:26:06 +12:00
|
|
|
<div v-for="(s, k) in groupedStatuses" v-else :key="k">
|
2023-09-22 15:06:03 +12:00
|
|
|
<div class="card mb-3">
|
|
|
|
<div class="card-header h4" :class="s.Class">
|
|
|
|
Status {{ s.StatusCode }}
|
2025-05-18 10:27:59 +12:00
|
|
|
<small v-if="s.Status != ''" class="ms-2 small text-muted">({{ s.Status }})</small>
|
2023-09-22 15:06:03 +12:00
|
|
|
</div>
|
|
|
|
<ul class="list-group list-group-flush">
|
2025-06-20 23:26:06 +12:00
|
|
|
<li v-for="(u, i) in s.URLS" :key="'status' + i" class="list-group-item">
|
2023-09-22 15:06:03 +12:00
|
|
|
<a :href="u" target="_blank" class="no-icon">{{ u }}</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<template v-if="error">
|
|
|
|
<p>Link check failed to load:</p>
|
|
|
|
<div class="alert alert-warning">
|
|
|
|
{{ error }}
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</div>
|
|
|
|
|
2025-06-20 23:26:06 +12:00
|
|
|
<div
|
|
|
|
id="LinkCheckOptions"
|
|
|
|
class="modal fade"
|
|
|
|
tabindex="-1"
|
|
|
|
aria-labelledby="LinkCheckOptionsLabel"
|
|
|
|
aria-hidden="true"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
|
|
<div class="modal-content">
|
|
|
|
<div class="modal-header">
|
2025-06-20 23:26:06 +12:00
|
|
|
<h1 id="LinkCheckOptionsLabel" class="modal-title fs-5">Link check options</h1>
|
2023-09-22 15:06:03 +12:00
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
|
|
<h6 class="mt-4">Follow HTTP redirects (status 301 & 302)</h6>
|
|
|
|
<div class="form-check form-switch mb-4">
|
2025-06-20 23:26:06 +12:00
|
|
|
<input
|
|
|
|
id="LinkCheckFollowRedirectsSwitch"
|
|
|
|
v-model="followRedirects"
|
|
|
|
class="form-check-input"
|
|
|
|
type="checkbox"
|
|
|
|
role="switch"
|
|
|
|
/>
|
2023-09-22 15:06:03 +12:00
|
|
|
<label class="form-check-label" for="LinkCheckFollowRedirectsSwitch">
|
|
|
|
<template v-if="followRedirects">Following HTTP redirects</template>
|
|
|
|
<template v-else>Not following HTTP redirects</template>
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<h6 class="mt-4">Automatic link checking</h6>
|
|
|
|
<div class="form-check form-switch mb-3">
|
2025-06-20 23:26:06 +12:00
|
|
|
<input
|
|
|
|
id="LinkCheckAutoCheckSwitch"
|
|
|
|
v-model="autoScan"
|
|
|
|
class="form-check-input"
|
|
|
|
type="checkbox"
|
|
|
|
role="switch"
|
|
|
|
/>
|
2023-09-22 15:06:03 +12:00
|
|
|
<label class="form-check-label" for="LinkCheckAutoCheckSwitch">
|
|
|
|
<template v-if="autoScan">Automatic link checking is enabled</template>
|
|
|
|
<template v-else>Automatic link checking is disabled</template>
|
|
|
|
</label>
|
|
|
|
<div class="form-text">
|
|
|
|
Note: Enabling auto checking will scan every link & image every time a message is opened.
|
|
|
|
Only enable this if you understand the potential risks & consequences.
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2025-06-20 23:26:06 +12:00
|
|
|
<div
|
|
|
|
id="AboutLinkCheckResults"
|
|
|
|
class="modal fade"
|
|
|
|
tabindex="-1"
|
|
|
|
aria-labelledby="AboutLinkCheckResultsLabel"
|
|
|
|
aria-hidden="true"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
|
|
<div class="modal-content">
|
|
|
|
<div class="modal-header">
|
2025-06-20 23:26:06 +12:00
|
|
|
<h1 id="AboutLinkCheckResultsLabel" class="modal-title fs-5">About Link check</h1>
|
2023-09-22 15:06:03 +12:00
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
|
|
</div>
|
|
|
|
<div class="modal-body">
|
2025-06-20 23:26:06 +12:00
|
|
|
<div id="LinkCheckAboutAccordion" class="accordion">
|
2023-09-22 15:06:03 +12:00
|
|
|
<div class="accordion-item">
|
|
|
|
<h2 class="accordion-header">
|
2025-06-20 23:26:06 +12:00
|
|
|
<button
|
|
|
|
class="accordion-button collapsed"
|
|
|
|
type="button"
|
|
|
|
data-bs-toggle="collapse"
|
|
|
|
data-bs-target="#col1"
|
|
|
|
aria-expanded="false"
|
|
|
|
aria-controls="col1"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
What is Link check?
|
|
|
|
</button>
|
|
|
|
</h2>
|
2025-06-20 23:26:06 +12:00
|
|
|
<div
|
|
|
|
id="col1"
|
|
|
|
class="accordion-collapse collapse"
|
|
|
|
data-bs-parent="#LinkCheckAboutAccordion"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
<div class="accordion-body">
|
|
|
|
Link check scans your message HTML and text for all unique links, images and linked
|
2024-06-22 13:27:00 +12:00
|
|
|
stylesheets. It then does a HTTP <code>HEAD</code> request to each link, 5 at a
|
|
|
|
time, to test whether the link/image/stylesheet exists.
|
2023-09-22 15:06:03 +12:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="accordion-item">
|
|
|
|
<h2 class="accordion-header">
|
2025-06-20 23:26:06 +12:00
|
|
|
<button
|
|
|
|
class="accordion-button collapsed"
|
|
|
|
type="button"
|
|
|
|
data-bs-toggle="collapse"
|
|
|
|
data-bs-target="#col2"
|
|
|
|
aria-expanded="false"
|
|
|
|
aria-controls="col2"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
What are "301" and "302" links?
|
|
|
|
</button>
|
|
|
|
</h2>
|
2025-06-20 23:26:06 +12:00
|
|
|
<div
|
|
|
|
id="col2"
|
|
|
|
class="accordion-collapse collapse"
|
|
|
|
data-bs-parent="#LinkCheckAboutAccordion"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
<div class="accordion-body">
|
|
|
|
<p>
|
2025-06-20 23:26:06 +12:00
|
|
|
These are links that redirect you to another URL, for example newsletters often
|
|
|
|
use redirect links to track user clicks.
|
2023-09-22 15:06:03 +12:00
|
|
|
</p>
|
|
|
|
<p>
|
2024-06-22 13:27:00 +12:00
|
|
|
By default Link check will not follow these links, however you can turn this on
|
2025-06-20 23:26:06 +12:00
|
|
|
via the settings and Link check will "follow" those redirects.
|
2023-09-22 15:06:03 +12:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="accordion-item">
|
|
|
|
<h2 class="accordion-header">
|
2025-06-20 23:26:06 +12:00
|
|
|
<button
|
|
|
|
class="accordion-button collapsed"
|
|
|
|
type="button"
|
|
|
|
data-bs-toggle="collapse"
|
|
|
|
data-bs-target="#col3"
|
|
|
|
aria-expanded="false"
|
|
|
|
aria-controls="col3"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
Why are some links returning an error but work in my browser?
|
|
|
|
</button>
|
|
|
|
</h2>
|
2025-06-20 23:26:06 +12:00
|
|
|
<div
|
|
|
|
id="col3"
|
|
|
|
class="accordion-collapse collapse"
|
|
|
|
data-bs-parent="#LinkCheckAboutAccordion"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
<div class="accordion-body">
|
|
|
|
<p>This may be due to various reasons, for instance:</p>
|
|
|
|
<ul>
|
|
|
|
<li>The Mailpit server cannot resolve (DNS) the hostname of the URL.</li>
|
|
|
|
<li>Mailpit is not allowed to access the URL.</li>
|
|
|
|
<li>
|
|
|
|
The webserver is blocking requests that don't come from authenticated web
|
|
|
|
browsers.
|
|
|
|
</li>
|
2025-06-20 23:26:06 +12:00
|
|
|
<li>The webserver or doesn't allow HTTP <code>HEAD</code> requests.</li>
|
2023-09-22 15:06:03 +12:00
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="accordion-item">
|
|
|
|
<h2 class="accordion-header">
|
2025-06-20 23:26:06 +12:00
|
|
|
<button
|
|
|
|
class="accordion-button collapsed"
|
|
|
|
type="button"
|
|
|
|
data-bs-toggle="collapse"
|
|
|
|
data-bs-target="#col4"
|
|
|
|
aria-expanded="false"
|
|
|
|
aria-controls="col4"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
What are the risks of running Link check automatically?
|
|
|
|
</button>
|
|
|
|
</h2>
|
2025-06-20 23:26:06 +12:00
|
|
|
<div
|
|
|
|
id="col4"
|
|
|
|
class="accordion-collapse collapse"
|
|
|
|
data-bs-parent="#LinkCheckAboutAccordion"
|
|
|
|
>
|
2023-09-22 15:06:03 +12:00
|
|
|
<div class="accordion-body">
|
|
|
|
<p>
|
2024-06-22 13:27:00 +12:00
|
|
|
Depending on the type of messages you are testing, opening all links on all
|
|
|
|
messages may have undesired consequences:
|
2023-09-22 15:06:03 +12:00
|
|
|
</p>
|
|
|
|
<ul>
|
|
|
|
<li>If the message contains tracking links this may reveal your identity.</li>
|
|
|
|
<li>
|
|
|
|
If the message contains unsubscribe links, Link check could unintentionally
|
|
|
|
unsubscribe you.
|
|
|
|
</li>
|
|
|
|
<li>
|
2024-06-22 13:27:00 +12:00
|
|
|
To speed up the checking process, Link check will attempt 5 URLs at a time.
|
|
|
|
This could lead to temporary heady load on the remote server.
|
2023-09-22 15:06:03 +12:00
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
<p>
|
2024-06-22 13:27:00 +12:00
|
|
|
Unless you know what messages you receive, it is advised to only run the Link
|
|
|
|
check manually.
|
2023-09-22 15:06:03 +12:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|