This commit is contained in:
j3d1 2026-09-05 16:20:44 +02:00
parent 9814ec7ecd
commit 197f173097
14 changed files with 170 additions and 89 deletions

View file

@ -1,9 +1,9 @@
<template>
<span style="position: relative; display: inline-block;">
<div class="delete-button">
<button class="btn btn-danger btn-sm" @click="triggerDelete">
<guarded-button class="btn btn-danger btn-sm" :confirm-message="confirmMessage" @confirm="$emit('delete')">
<b-icon-trash></b-icon-trash>
</button>
</guarded-button>
</div>
<slot></slot>
</span>
@ -20,10 +20,12 @@
<script>
import * as BIcons from "bootstrap-icons-vue";
import GuardedButton from "@/components/GuardedButton.vue";
export default {
name: "DeletableWrapper",
components: {
GuardedButton,
...BIcons
},
props: {
@ -33,12 +35,6 @@ export default {
default: null
}
},
emits: ["delete"],
methods: {
triggerDelete() {
if (this.confirmMessage && !confirm(this.confirmMessage)) return;
this.$emit("delete");
}
}
emits: ["delete"]
}
</script>

View file

@ -0,0 +1,43 @@
<template>
<component :is="tag" v-bind="rootProps" :class="{unguarded: shiftHeld}" @click="handleClick">
<slot></slot>
</component>
</template>
<script>
import {reactive} from "vue"
const shiftState = reactive({held: false})
window.addEventListener("keydown", event => shiftState.held = event.shiftKey)
window.addEventListener("keyup", event => shiftState.held = event.shiftKey)
export default {
name: "GuardedButton",
props: {
confirmMessage: {
type: String,
default: null
},
tag: {
type: String,
default: "button"
}
},
emits: ["confirm"],
computed: {
rootProps() {
return this.tag === "a" ? {href: "#"} : {}
},
shiftHeld() {
return shiftState.held
}
},
methods: {
handleClick(event) {
if (this.tag === "a") event.preventDefault()
if (this.confirmMessage && !event.shiftKey && !confirm(this.confirmMessage)) return
this.$emit("confirm", event)
}
}
}
</script>

View file

@ -38,10 +38,12 @@
<div v-if="photos.length > 0" class="photo-gallery mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Captured Photos ({{ photos.length }})</h6>
<button class="btn btn-sm btn-outline-danger" @click="clearAllPhotos">
<guarded-button class="btn btn-sm btn-outline-danger"
confirm-message="Are you sure you want to remove all photos?"
@confirm="clearAllPhotos">
<b-icon-trash class="me-1"></b-icon-trash>
Clear All
</button>
</guarded-button>
</div>
<div class="row">
<div v-for="(photo, index) in photos" :key="index" class="col-sm-6 col-md-4 col-lg-3 mb-3">
@ -279,6 +281,7 @@
import * as BIcons from "bootstrap-icons-vue";
import {mapActions, mapState} from "vuex";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import DragDropFileSource from "@/components/inputs/DragDropFileSource.vue";
import CameraFileSource from "@/components/inputs/CameraFileSource.vue";
import FsFileSource from "@/components/inputs/FsFileSource.vue";
@ -350,6 +353,7 @@ export default {
components: {
WebcamFileSource,
AuthenticatedImage,
GuardedButton,
DragDropFileSource,
CameraFileSource,
FsFileSource,
@ -500,14 +504,12 @@ export default {
},
async clearAllPhotos() {
if (confirm('Are you sure you want to remove all photos?')) {
const removed = this.photos;
this.photos = [];
await Promise.all(removed.map(photo =>
this.unstageFile({lifetime_id: this.workflowInstance.id, file_hash: photo.hash})
.catch(error => console.error('Failed to unstage photo:', error))
));
}
},
proceedFromStep1() {

View file

@ -46,10 +46,12 @@
<b-icon-pencil-square></b-icon-pencil-square>
Edit
</a-->
<a href="#" class="align-middle" @click="tryDropFriend(friend)">
<guarded-button tag="a" class="align-middle"
:confirm-message='`Are you sure you want to remove "${friend.handle}" as a friend?`'
@confirm="tryDropFriend(friend)">
<b-icon-trash></b-icon-trash>
Delete
</a>
</guarded-button>
</td>
</tr>
</tbody>
@ -107,6 +109,7 @@ import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import UserNameTag from "@/components/UserNameTag.vue";
import GuardedButton from "@/components/GuardedButton.vue";
export default {
name: 'Inventory',
@ -114,6 +117,7 @@ export default {
BaseLayout,
AuthenticatedImage,
UserNameTag,
GuardedButton,
...BIcons
},
data() {
@ -173,7 +177,6 @@ export default {
})
},
tryDropFriend(friend) {
if (!confirm(`Are you sure you want to remove "${friend.handle}" as a friend?`)) return
this.dropFriend(friend).then((ok) => {
if (ok) {
delete this.friends[friend.handle]

View file

@ -40,10 +40,12 @@
<user-name-tag :user="member"/>
</td>
<td class="table-action">
<a href="#" class="align-middle" @click.prevent="tryRemoveMember(member)">
<guarded-button tag="a" class="align-middle"
:confirm-message='`Are you sure you want to remove "${member.handle}" from ${group.handle}?`'
@confirm="tryRemoveMember(member)">
<b-icon-trash></b-icon-trash>
Remove
</a>
</guarded-button>
</td>
</tr>
</tbody>
@ -61,6 +63,7 @@ import {mapActions} from "vuex";
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import UserNameTag from "@/components/UserNameTag.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import {decodeHandleFromUrl} from "@/router";
export default {
@ -68,6 +71,7 @@ export default {
components: {
BaseLayout,
UserNameTag,
GuardedButton,
...BIcons
},
props: {
@ -107,7 +111,6 @@ export default {
})
},
tryRemoveMember(member) {
if (!confirm(`Are you sure you want to remove "${member.handle}" from ${this.group.handle}?`)) return
this.removeGroupMember({groupHandle: this.group.handle, identityId: member.id})
.then(() => {
this.refresh()

View file

@ -61,9 +61,11 @@
<router-link v-if="canEdit" :to="itemEditRoute(item)" title="Edit">
<b-icon-pencil-square></b-icon-pencil-square>
</router-link>
<a v-if="canEdit" href="#" title="Delete" @click.prevent="tryDeleteItem(item)">
<guarded-button v-if="canEdit" tag="a" title="Delete"
:confirm-message='`Are you sure you want to delete "${item.name}"? This cannot be undone.`'
@confirm="tryDeleteItem(item)">
<b-icon-trash></b-icon-trash>
</a>
</guarded-button>
<router-link v-if="shortIdLink(item)" :to="shortIdLink(item)">
<b-icon-link></b-icon-link>
</router-link>
@ -93,10 +95,12 @@
<span class="float-right">{{ item.owned_quantity }}</span>
</div>
<div class="btn-group">
<button v-if="canEdit" class="btn btn-danger btn-sm"
title="Delete" @click="tryDeleteItem(item)">
<guarded-button v-if="canEdit" class="btn btn-danger btn-sm"
title="Delete"
:confirm-message='`Are you sure you want to delete "${item.name}"? This cannot be undone.`'
@confirm="tryDeleteItem(item)">
<b-icon-trash></b-icon-trash>
</button>
</guarded-button>
<router-link v-if="canEdit" :to="itemEditRoute(item)"
class="btn btn-primary btn-sm" title="Edit">
<b-icon-pencil-square></b-icon-pencil-square>
@ -135,10 +139,12 @@
<span class="float-right">{{ item.owned_quantity }}</span>
</div>
<div class="btn-group mt-auto">
<button v-if="canEdit" class="btn btn-danger btn-sm"
title="Delete" @click="tryDeleteItem(item)">
<guarded-button v-if="canEdit" class="btn btn-danger btn-sm"
title="Delete"
:confirm-message='`Are you sure you want to delete "${item.name}"? This cannot be undone.`'
@confirm="tryDeleteItem(item)">
<b-icon-trash></b-icon-trash>
</button>
</guarded-button>
<router-link v-if="canEdit" :to="itemEditRoute(item)"
class="btn btn-primary btn-sm" title="Edit">
<b-icon-pencil-square></b-icon-pencil-square>
@ -171,6 +177,7 @@ import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import AuthenticatedImage from "../components/AuthenticatedImage.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import {shortenedRoute, encodeHandleForUrl, decodeHandleFromUrl} from "@/router";
export default {
@ -185,6 +192,7 @@ export default {
components: {
AuthenticatedImage,
BaseLayout,
GuardedButton,
...BIcons
},
computed: {
@ -241,7 +249,6 @@ export default {
return this.fetchFriendInventoryItems({friendHandle: this.selectedOwner})
},
tryDeleteItem(item) {
if (!confirm(`Are you sure you want to delete "${item.name}"? This cannot be undone.`)) return
this.deleteInventoryItem(item).then(() => {
this.fetchItemsForOwner()
})
@ -331,4 +338,11 @@ export default {
column-count: 6;
}
}
.unguarded.btn-danger,
.unguarded.btn-danger:hover,
.unguarded.btn-danger:focus {
background-color: orange;
border-color: orange;
}
</style>

View file

@ -45,11 +45,12 @@
<b-icon-pencil-square></b-icon-pencil-square>
Edit
</button>
<button type="submit" class="btn btn-danger"
@click="tryDeleteItem">
<guarded-button class="btn btn-danger"
:confirm-message='`Are you sure you want to delete "${item.name}"? This cannot be undone.`'
@confirm="tryDeleteItem">
<b-icon-trash></b-icon-trash>
Delete
</button>
</guarded-button>
<button v-if="canPrint" class="btn btn-secondary"
@click="$router.push({name: 'print', query: {kind: 'item', userHandle: decodedHandle, id: id}})">
<b-icon-printer></b-icon-printer>
@ -68,6 +69,7 @@ import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import {mapActions, mapGetters, mapState} from "vuex";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import {decodeHandleFromUrl, ownerOverviewRoute} from "@/router";
export default {
@ -75,6 +77,7 @@ export default {
components: {
AuthenticatedImage,
BaseLayout,
GuardedButton,
...BIcons
},
props: {
@ -118,7 +121,6 @@ export default {
this.item = await this.fetchItemByHandle({handle: this.decodedHandle, id: this.id}) || {}
},
tryDeleteItem() {
if (!confirm(`Are you sure you want to delete "${this.item.name}"? This cannot be undone.`)) return
this.deleteInventoryItem(this.item).then(() => this.$router.push(ownerOverviewRoute(this.item)))
}
},

View file

@ -35,10 +35,11 @@
<router-link class="list-group-item list-group-item-action" :to="{name: 'data'}"
active-class="active">Your data
</router-link>
<a class="list-group-item list-group-item-action delete" @click="deleteAccount"
href="#">
<guarded-button tag="a" class="list-group-item list-group-item-action delete"
confirm-message="Are you sure you want to permanently delete your account? All your data will be deleted and you will not be able to log back in. This cannot be undone."
@confirm="deleteAccount">
Delete account
</a>
</guarded-button>
</div>
</div>
</div>
@ -55,11 +56,12 @@
<script>
import BaseLayout from "@/components/BaseLayout.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import {mapActions, mapGetters, mapMutations} from "vuex";
export default {
name: 'Settings',
components: {BaseLayout},
components: {BaseLayout, GuardedButton},
data() {
return {
deleteError: null,
@ -74,10 +76,6 @@ export default {
...mapMutations(['logout']),
async deleteAccount() {
this.deleteError = null;
if (!confirm('Are you sure you want to permanently delete your account? ' +
'All your data will be deleted and you will not be able to log back in. This cannot be undone.')) {
return;
}
try {
const servers = await this.getHomeServers();
const response = await servers.delete(this.signAuth, '/api/v1/account/');

View file

@ -58,9 +58,11 @@
<router-link :to="locationEditRoute(location)" title="Edit">
<b-icon-pencil-square></b-icon-pencil-square>
</router-link>
<a href="#" title="Delete" @click.prevent="tryDeleteLocation(location)">
<guarded-button tag="a" title="Delete"
:confirm-message='`Are you sure you want to delete "${location.name}"? This cannot be undone.`'
@confirm="tryDeleteLocation(location)">
<b-icon-trash></b-icon-trash>
</a>
</guarded-button>
<router-link v-if="shortIdLink(location)" :to="shortIdLink(location)">
<b-icon-link></b-icon-link>
</router-link>
@ -91,10 +93,12 @@
<small>{{ location.description }}</small>
</div>
<div class="btn-group mt-auto">
<button class="btn btn-danger btn-sm"
title="Delete" @click="tryDeleteLocation(location)">
<guarded-button class="btn btn-danger btn-sm"
title="Delete"
:confirm-message='`Are you sure you want to delete "${location.name}"? This cannot be undone.`'
@confirm="tryDeleteLocation(location)">
<b-icon-trash></b-icon-trash>
</button>
</guarded-button>
<router-link :to="locationEditRoute(location)"
class="btn btn-primary btn-sm" title="Edit">
<b-icon-pencil-square></b-icon-pencil-square>
@ -126,6 +130,7 @@
import {mapActions, mapGetters, mapState} from "vuex";
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import {shortenedRoute, encodeHandleForUrl, decodeHandleFromUrl} from "@/router";
export default {
@ -139,6 +144,7 @@ export default {
},
components: {
BaseLayout,
GuardedButton,
...BIcons
},
computed: {
@ -186,7 +192,6 @@ export default {
: this.fetchGroupStorageLocations({groupHandle: this.selectedOwner})
},
tryDeleteLocation(location) {
if (!confirm(`Are you sure you want to delete "${location.name}"? This cannot be undone.`)) return
this.deleteStorageLocation(location).then(() => {
this.fetchLocationsForOwner()
})
@ -242,4 +247,11 @@ export default {
.btn-group.mt-auto .btn {
flex: 1;
}
.unguarded.btn-danger,
.unguarded.btn-danger:hover,
.unguarded.btn-danger:focus {
background-color: orange;
border-color: orange;
}
</style>

View file

@ -38,11 +38,12 @@
<b-icon-pencil-square></b-icon-pencil-square>
Edit
</button>
<button type="submit" class="btn btn-danger"
@click="tryDeleteLocation">
<guarded-button class="btn btn-danger"
:confirm-message='`Are you sure you want to delete "${location.name}"? This cannot be undone.`'
@confirm="tryDeleteLocation">
<b-icon-trash></b-icon-trash>
Delete
</button>
</guarded-button>
<button class="btn btn-secondary"
@click="$router.push({name: 'print', query: {kind: 'storage-location', userHandle: decodedHandle, id: id}})">
<b-icon-printer></b-icon-printer>
@ -58,6 +59,7 @@
<script>
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import {mapActions, mapGetters, mapState} from "vuex";
import {decodeHandleFromUrl, ownerLocationOverviewRoute} from "@/router";
@ -65,6 +67,7 @@ export default {
name: "StorageLocationDetail",
components: {
BaseLayout,
GuardedButton,
...BIcons
},
props: {
@ -105,7 +108,6 @@ export default {
this.location = await this.fetchStorageLocationByHandle({handle: this.decodedHandle, id: this.id}) || {}
},
tryDeleteLocation() {
if (!confirm(`Are you sure you want to delete "${this.location.name}"? This cannot be undone.`)) return
this.deleteStorageLocation(this.location).then(() => this.$router.push(ownerLocationOverviewRoute(this.location)))
}
},

View file

@ -19,10 +19,12 @@
<b-icon-arrow-left class="me-1"></b-icon-arrow-left>
Back
</button>
<button v-if="canAbort" class="btn btn-outline-danger" @click="abortWorkflow" :disabled="loading">
<guarded-button v-if="canAbort" class="btn btn-outline-danger"
:confirm-message='`Are you sure you want to abort "${getWorkflowDisplayName || workflowInstance.title}"?`'
@confirm="abortWorkflow" :disabled="loading">
<b-icon-x-circle class="me-1"></b-icon-x-circle>
Abort
</button>
</guarded-button>
</div>
</div>
@ -199,6 +201,7 @@
<script>
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import { mapState, mapActions } from 'vuex';
import { getWorkflow, getWorkflowComponent } from '@/workflows.js';
@ -206,7 +209,8 @@ export default {
name: 'WorkflowDetail',
components: {
...BIcons,
BaseLayout
BaseLayout,
GuardedButton
},
props: {
id: {
@ -416,8 +420,6 @@ export default {
},
async abortWorkflow() {
const displayName = this.getWorkflowDisplayName || this.workflowInstance.title;
if (!confirm(`Are you sure you want to abort "${displayName}"?`)) return;
try {
this.loading = true;
await this.deleteWorkflow(this.workflowInstance.id);

View file

@ -54,11 +54,12 @@
:disabled="loading">
<b-icon-eye></b-icon-eye>
</button>
<button class="btn btn-sm btn-outline-danger"
@click="abortWorkflowInstance(workflow)"
<guarded-button class="btn btn-sm btn-outline-danger"
:confirm-message='`Are you sure you want to abort "${getWorkflowDisplayName(workflow)}"?`'
@confirm="abortWorkflowInstance(workflow)"
:disabled="loading">
<b-icon-x-circle></b-icon-x-circle>
</button>
</guarded-button>
</td>
</tr>
</tbody>
@ -132,6 +133,7 @@
<script>
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import {mapState, mapActions} from 'vuex';
import {getAllWorkflows, getWorkflow, buildWorkflowApiPayload} from '@/workflows.js';
@ -139,7 +141,8 @@ export default {
name: 'Workflows',
components: {
...BIcons,
BaseLayout
BaseLayout,
GuardedButton
},
data() {
return {
@ -264,7 +267,6 @@ export default {
},
async abortWorkflowInstance(workflow) {
const displayName = this.getWorkflowDisplayName(workflow);
if (confirm(`Are you sure you want to abort "${displayName}"?`)) {
try {
this.loading = true;
this.error = null;
@ -279,7 +281,6 @@ export default {
}
}
}
}
}
</script>

View file

@ -32,9 +32,11 @@
<fs-file-source @input="uploadPicture">
<span class="btn btn-primary"><i class="fas fa-upload"></i> Upload</span>
</fs-file-source>
<button class="btn btn-outline-secondary ml-2" type="button" @click="removePicture">
<guarded-button class="btn btn-outline-secondary ml-2" type="button"
confirm-message="Are you sure you want to remove your profile picture?"
@confirm="removePicture">
Remove
</button>
</guarded-button>
</div>
<small>For best results, use an image at least 128px by
128px in .jpg format</small>
@ -107,10 +109,11 @@
import {mapActions, mapState} from "vuex";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import FsFileSource from "@/components/inputs/FsFileSource.vue";
import GuardedButton from "@/components/GuardedButton.vue";
export default {
name: 'Account',
components: {AuthenticatedImage, FsFileSource},
components: {AuthenticatedImage, FsFileSource, GuardedButton},
computed: {
...mapState(['user', 'user_profile']),
profilePictureSrc() {
@ -130,7 +133,6 @@ export default {
await this.updateUserProfilePicture({file: image});
},
async removePicture() {
if (!confirm('Are you sure you want to remove your profile picture?')) return;
await this.updateUserProfilePicture({file: null});
}
},

View file

@ -37,9 +37,11 @@
<div class="mb-3">
<label class="form-label
d-block">Delete your data</label>
<button type="button" class="btn btn-danger" @click="deleteData">
<guarded-button type="button" class="btn btn-danger"
confirm-message="Are you sure you want to permanently delete all your data (inventory, locations, settings, friends and files)? Your account itself will stay - this cannot be undone."
@confirm="deleteData">
Delete
</button>
</guarded-button>
<br>
<small>Delete all your data including photos, videos,
comments, profile information and more</small>
@ -78,12 +80,15 @@
<script>
import {mapActions, mapGetters} from 'vuex';
import router from "@/router";
import GuardedButton from "@/components/GuardedButton.vue";
//import VueQrcode from '@chenfengyuan/vue-qrcode';
export default {
name: 'Data',
components: {},
components: {
GuardedButton
},
data: () => ({
selectedFile: null,
localUserIdentityRecord: null,
@ -106,10 +111,6 @@ export default {
async deleteData() {
this.deleteError = null;
this.deleteSuccess = null;
if (!confirm('Are you sure you want to permanently delete all your data (inventory, locations, ' +
'settings, friends and files)? Your account itself will stay - this cannot be undone.')) {
return;
}
try {
const servers = await this.getHomeServers();
const response = await servers.delete(this.signAuth, '/api/v1/account_data/');