stash
This commit is contained in:
parent
7c91661be2
commit
796fef81be
37 changed files with 3404 additions and 108 deletions
|
|
@ -109,16 +109,21 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters(["inventory_items", "loaded_items"]),
|
||||
...mapState(["user"]),
|
||||
...mapState(["user", "storage_locations"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(["fetchInventoryItems", "deleteInventoryItem"]),
|
||||
...mapActions(["fetchInventoryItems", "deleteInventoryItem", "fetchStorageLocations"]),
|
||||
only_images(files) {
|
||||
return files.filter(file => file.mime_type.startsWith("image/"));
|
||||
},
|
||||
locationPath(item) {
|
||||
const loc = this.storage_locations.find(loc => loc.id === item.storage_location)
|
||||
return loc ? loc.path : null
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchInventoryItems()
|
||||
await this.fetchStorageLocations()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
<script>
|
||||
import * as BIcons from "bootstrap-icons-vue";
|
||||
import BaseLayout from "@/components/BaseLayout.vue";
|
||||
import {mapActions, mapGetters} from "vuex";
|
||||
import {mapActions, mapGetters, mapState} from "vuex";
|
||||
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||
|
||||
export default {
|
||||
|
|
@ -75,15 +75,20 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters(["loaded_items"]),
|
||||
...mapState(["storage_locations"]),
|
||||
item() {
|
||||
return this.loaded_items.find(item => item.id === parseInt(this.id)) || {}
|
||||
},
|
||||
location() {
|
||||
return this.storage_locations.find(loc => loc.id === this.item.storage_location) || null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(["fetchInventoryItems", "deleteInventoryItem", "fetchFilesByItem"]),
|
||||
...mapActions(["fetchInventoryItems", "deleteInventoryItem", "fetchFilesByItem", "fetchStorageLocations"]),
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchInventoryItems()
|
||||
await this.fetchStorageLocations()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
<label for="storage_location" class="form-label">Storage Location</label>
|
||||
<select class="form-select" id="storage_location" name="storage_location"
|
||||
v-model="item.storage_location">
|
||||
<option value="">No storage location</option>
|
||||
<option :value="null">No storage location</option>
|
||||
<option v-for="location in storage_locations" :value="location.id"
|
||||
:key="location.id">
|
||||
{{ location.path }}
|
||||
|
|
@ -103,12 +103,13 @@ export default {
|
|||
owned_quantity: 0,
|
||||
image: "",
|
||||
files: [],
|
||||
storage_location: null,
|
||||
...this.inventory_items.find(item => item.id === parseInt(this.id))
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(["fetchInventoryItems", "updateInventoryItem", "fetchInfo"]),
|
||||
...mapActions(["fetchInventoryItems", "updateInventoryItem", "fetchInfo", "fetchStorageLocations"]),
|
||||
changeFiles(files) {
|
||||
this.inventory_items.find(item => item.id === parseInt(this.id)).files = files
|
||||
},
|
||||
|
|
@ -116,6 +117,7 @@ export default {
|
|||
async mounted() {
|
||||
await this.fetchInfo();
|
||||
await this.fetchInventoryItems();
|
||||
await this.fetchStorageLocations();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@
|
|||
<div class="mb-3">
|
||||
<label for="storage_location" class="form-label">Storage Location</label>
|
||||
<select class="form-select" id="storage_location" name="storage_location"
|
||||
v-model="item.storage_location_id">
|
||||
<option value="">No storage location</option>
|
||||
v-model="item.storage_location">
|
||||
<option :value="null">No storage location</option>
|
||||
<option v-for="location in storage_locations" :value="location.id"
|
||||
:key="location.id">
|
||||
{{ location.path }}
|
||||
|
|
@ -95,18 +95,20 @@ export default {
|
|||
image: "",
|
||||
tags: [],
|
||||
properties: [],
|
||||
files: []
|
||||
files: [],
|
||||
storage_location: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['createInventoryItem', 'fetchInfo'])
|
||||
...mapActions(['createInventoryItem', 'fetchInfo', 'fetchStorageLocations'])
|
||||
},
|
||||
computed: {
|
||||
...mapState(["availability_policies", "storage_locations"]),
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchInfo();
|
||||
await this.fetchStorageLocations();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -12,14 +12,16 @@
|
|||
<h5 class="card-title mb-0">Profile Details</h5>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<img src="/assets/img/avatars/avatar.png"
|
||||
alt="Christina Mason" class="img-fluid rounded-circle mb-2" width="128"
|
||||
height="128"/>
|
||||
<authenticated-image :src="profilePictureSrc" :owner="userHandle"
|
||||
:alt="profile.username || userHandle"
|
||||
:title="profile.username || userHandle"
|
||||
img-class="img-fluid rounded-circle mb-2"
|
||||
:width="128" :height="128" fit="cover"/>
|
||||
<h5 class="card-title mb-0">
|
||||
{{ user.username }}
|
||||
{{ profile.username }}
|
||||
</h5>
|
||||
<div class="text-muted mb-2">
|
||||
{{ user.email }}
|
||||
{{ profile.email }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -28,12 +30,12 @@
|
|||
data-feather="message-square"></span> Message</a>
|
||||
</div>
|
||||
</div>
|
||||
<!--{% if user.bio %}-->
|
||||
<!--{% if profile.bio %}-->
|
||||
<hr class="my-0"/>
|
||||
<div class="card-body">
|
||||
<h5 class="h6 card-title">Bio</h5>
|
||||
<div class="text-muted mb-2">
|
||||
{{ user.bio }}
|
||||
{{ profile.bio }}
|
||||
</div>
|
||||
</div>
|
||||
<!--{% endif %}-->
|
||||
|
|
@ -54,9 +56,9 @@
|
|||
<div class="card-body">
|
||||
<h5 class="h6 card-title">About</h5>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<!--{% if user.location %}-->
|
||||
<!--{% if profile.location %}-->
|
||||
<li class="mb-1"><span data-feather="home" class="feather-sm mr-1"></span> Lives
|
||||
in <a href="#">{{ user.location }}</a></li>
|
||||
in <a href="#">{{ profile.location }}</a></li>
|
||||
<!--{% endif %}-->
|
||||
|
||||
<li class="mb-1"><span data-feather="briefcase" class="feather-sm mr-1"></span>
|
||||
|
|
@ -230,24 +232,33 @@
|
|||
|
||||
<script>
|
||||
import BaseLayout from "@/components/BaseLayout.vue";
|
||||
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||
import {mapActions, mapState} from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'Profile',
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
name: 'John Doe',
|
||||
avatar: '/static/assets/img/avatars/avatar.png',
|
||||
cover: '/static/assets/img/photos/unsplash-1.jpg',
|
||||
occupation: 'Frontend Developer',
|
||||
company: 'Facebook Inc.',
|
||||
email: 'foo@bar.com',
|
||||
phone: '+12 345 678 001',
|
||||
address: 'Boulevard of Broken Dreams, 1234',
|
||||
}
|
||||
components: {BaseLayout, AuthenticatedImage},
|
||||
computed: {
|
||||
...mapState(['user', 'user_profile']),
|
||||
userHandle() {
|
||||
return this.user;
|
||||
},
|
||||
profile() {
|
||||
return this.user_profile || {
|
||||
username: this.user,
|
||||
email: ''
|
||||
};
|
||||
},
|
||||
profilePictureSrc() {
|
||||
return this.user_profile?.profile_picture?.name || null;
|
||||
}
|
||||
},
|
||||
components: {BaseLayout},
|
||||
methods: {
|
||||
...mapActions(['fetchUserProfile'])
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUserProfile();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@
|
|||
<router-link class="list-group-item list-group-item-action"
|
||||
:to="{name: 'notifications'}" active-class="active">Web notifications
|
||||
</router-link>
|
||||
<router-link class="list-group-item list-group-item-action" :to="{name: 'preferences'}"
|
||||
active-class="active">Preferences
|
||||
</router-link>
|
||||
<router-link class="list-group-item list-group-item-action" :to="{name: 'data'}"
|
||||
active-class="active">Your data
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ import * as BIcons from "bootstrap-icons-vue";
|
|||
import BaseLayout from "@/components/BaseLayout.vue";
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { workflowRegistry } from '@/workflows.js';
|
||||
import { getStepComponent, hasStepComponent } from '@/components/workflow/ComponentRegistry.js';
|
||||
|
||||
export default {
|
||||
name: 'WorkflowDetail',
|
||||
|
|
@ -256,15 +257,11 @@ export default {
|
|||
},
|
||||
|
||||
stepComponent() {
|
||||
// Return step-specific component if it exists
|
||||
// This allows for custom UI per workflow type and step
|
||||
// Return step-specific component if it exists using the component registry
|
||||
const workflowType = this.workflowInstance?.workflow_type;
|
||||
if (workflowType) {
|
||||
// Try to load a step-specific component
|
||||
// e.g., FotoFirstImportStep1, BulkImportStep2, etc.
|
||||
const componentName = `${workflowType}Step${this.currentStep}`;
|
||||
// This would require registering step components
|
||||
return null; // For now, use default content
|
||||
console.log('Determining step component for workflow type:', this.workflowInstance, 'and step:', this.currentStep);
|
||||
if (workflowType && this.currentStep) {
|
||||
return getStepComponent(workflowType, this.currentStep);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
|
@ -282,6 +279,19 @@ export default {
|
|||
canNavigatePrev() {
|
||||
// Check if we can navigate to the previous step
|
||||
return this.currentStepIndex > 0;
|
||||
},
|
||||
|
||||
hasCustomComponent() {
|
||||
// Check if the current step has a custom component defined
|
||||
return this.stepComponent !== null;
|
||||
},
|
||||
|
||||
componentStatusClass() {
|
||||
return this.hasCustomComponent ? 'bg-light border-success' : 'bg-light border-muted';
|
||||
},
|
||||
|
||||
componentStatusText() {
|
||||
return this.hasCustomComponent ? 'Using custom component' : 'Using default view';
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -453,6 +463,12 @@ export default {
|
|||
formatDateTime(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString();
|
||||
},
|
||||
|
||||
hasStepComponent(stepNumber) {
|
||||
// Check if a specific step has a custom component available
|
||||
const workflowType = this.workflowInstance?.workflow_type;
|
||||
return workflowType ? hasStepComponent(workflowType, stepNumber) : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -498,4 +514,3 @@ export default {
|
|||
min-width: 120px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,18 @@
|
|||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<img alt="Charles Hall"
|
||||
src="/assets/img/avatars/avatar.png"
|
||||
class="rounded-circle img-responsive mt-2" width="128"
|
||||
height="128"/>
|
||||
<authenticated-image :src="profilePictureSrc" :owner="user"
|
||||
:refresh-key="profilePictureRefreshKey"
|
||||
:alt="user" :title="user"
|
||||
img-class="rounded-circle img-responsive mt-2"
|
||||
:width="128" :height="128" fit="cover"/>
|
||||
<div class="mt-2">
|
||||
<span class="btn btn-primary"><i
|
||||
class="fas fa-upload"></i> Upload</span>
|
||||
<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">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<small>For best results, use an image at least 128px by
|
||||
128px in .jpg format</small>
|
||||
|
|
@ -99,10 +104,38 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapState} from "vuex";
|
||||
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||
import FsFileSource from "@/components/inputs/FsFileSource.vue";
|
||||
|
||||
export default {
|
||||
name: 'Account',
|
||||
components: {}
|
||||
components: {AuthenticatedImage, FsFileSource},
|
||||
computed: {
|
||||
...mapState(['user', 'user_profile']),
|
||||
profilePictureSrc() {
|
||||
return this.user_profile?.profile_picture?.name || null;
|
||||
},
|
||||
profilePictureRefreshKey() {
|
||||
return this.user_profile?.profile_picture?.id || 'none';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchUserProfile', 'updateUserProfilePicture']),
|
||||
async uploadPicture(files) {
|
||||
const image = files.find(file => file.mime_type?.startsWith('image/'))
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
await this.updateUserProfilePicture({file: image});
|
||||
},
|
||||
async removePicture() {
|
||||
await this.updateUserProfilePicture({file: null});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUserProfile();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
234
frontend/src/views/settings/Preferences.vue
Normal file
234
frontend/src/views/settings/Preferences.vue
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Preferences</h5>
|
||||
|
||||
<div v-if="!preferencesLoaded" class="text-center py-4 text-muted">
|
||||
Loading preferences...
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 26%">Preference</th>
|
||||
<th style="width: 17%">Device</th>
|
||||
<th style="width: 17%">Account</th>
|
||||
<th style="width: 17%">Default</th>
|
||||
<th style="width: 23%">Effective</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="schema in schemas" :key="schema.key">
|
||||
<td>
|
||||
<div class="font-weight-bold">{{ schema.label }}</div>
|
||||
<small v-if="schema.description" class="text-muted">{{ schema.description }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<PreferenceInput
|
||||
:id="`device-${schema.key}`"
|
||||
:schema="schema"
|
||||
:value="effectiveDeviceValues[schema.key]"
|
||||
@input="updateDevicePreference(schema.key, $event)"
|
||||
@clear="clearDevicePreference(schema.key)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<PreferenceInput
|
||||
:id="`account-${schema.key}`"
|
||||
:schema="schema"
|
||||
:value="effectiveAccountValues[schema.key]"
|
||||
@input="updateAccountPreference(schema.key, $event)"
|
||||
@clear="clearAccountPreference(schema.key)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<PreferenceInput
|
||||
:id="`default-${schema.key}`"
|
||||
:schema="schema"
|
||||
:value="schema.default"
|
||||
:disabled="true"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-weight-bold">
|
||||
{{ formatEffectiveValue(schema, getPreference(schema.key)) }}
|
||||
<span
|
||||
v-if="hasChanges && formatEffectiveValue(schema, getEffectiveValueWithPendingChanges(schema.key)) !== formatEffectiveValue(schema, getPreference(schema.key))"
|
||||
class="text-warning ml-1"
|
||||
>
|
||||
-> {{ formatEffectiveValue(schema, getEffectiveValueWithPendingChanges(schema.key)) }}
|
||||
</span>
|
||||
</div>
|
||||
<small class="text-muted">Source: {{ getPreferenceSource(schema.key) }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex pt-3 border-top mt-3">
|
||||
<button @click="savePreferences" :disabled="!hasChanges || saving" class="btn btn-primary mr-2">
|
||||
{{ saving ? 'Saving...' : 'Save changes' }}
|
||||
</button>
|
||||
<button @click="reloadPreferences" :disabled="!hasChanges || saving" class="btn btn-secondary">
|
||||
Discard changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapGetters, mapState} from 'vuex'
|
||||
import PreferenceInput from '@/components/inputs/PreferenceInput.vue'
|
||||
|
||||
export default {
|
||||
name: 'Preferences',
|
||||
components: {PreferenceInput},
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
accountChanges: {},
|
||||
deviceChanges: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['preferencesLoaded', 'preferenceDefinitions', 'devicePreferences', 'accountPreferences']),
|
||||
...mapGetters(['getPreference']),
|
||||
schemas() {
|
||||
return this.preferenceDefinitions.map((pref) => ({
|
||||
...pref,
|
||||
label: pref.label || this.formatLabel(pref.key),
|
||||
description: pref.description || '',
|
||||
}))
|
||||
},
|
||||
hasChanges() {
|
||||
return Object.keys(this.accountChanges).length > 0 || Object.keys(this.deviceChanges).length > 0
|
||||
},
|
||||
effectiveDeviceValues() {
|
||||
return {...this.devicePreferences, ...this.deviceChanges}
|
||||
},
|
||||
effectiveAccountValues() {
|
||||
return {...this.accountPreferences, ...this.accountChanges}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'loadUserPreferences',
|
||||
'setAccountPreference',
|
||||
'resetAccountPreference',
|
||||
'setDevicePreference',
|
||||
'resetDevicePreference',
|
||||
]),
|
||||
formatLabel(key) {
|
||||
const parts = key.split('.')
|
||||
const name = parts[parts.length - 1] || ''
|
||||
return name
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
},
|
||||
getEffectiveValueWithPendingChanges(key) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.deviceChanges, key)) {
|
||||
if (this.deviceChanges[key] !== null) {
|
||||
return this.deviceChanges[key]
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(this.accountChanges, key) && this.accountChanges[key] !== null) {
|
||||
return this.accountChanges[key]
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(this.accountPreferences, key) && this.accountPreferences[key] !== null) {
|
||||
return this.accountPreferences[key]
|
||||
}
|
||||
} else if (Object.prototype.hasOwnProperty.call(this.accountChanges, key)) {
|
||||
if (this.accountChanges[key] !== null) {
|
||||
return this.accountChanges[key]
|
||||
}
|
||||
} else {
|
||||
return this.getPreference(key)
|
||||
}
|
||||
|
||||
const definition = this.preferenceDefinitions.find((p) => p.key === key)
|
||||
return definition ? definition.default : null
|
||||
},
|
||||
getPreferenceSource(key) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.deviceChanges, key)) {
|
||||
return this.deviceChanges[key] !== null
|
||||
? 'Device'
|
||||
: Object.prototype.hasOwnProperty.call(this.accountPreferences, key) && this.accountPreferences[key] !== null
|
||||
? 'Account'
|
||||
: 'Default'
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(this.accountChanges, key)) {
|
||||
return this.accountChanges[key] !== null ? 'Account' : 'Default'
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(this.devicePreferences, key) && this.devicePreferences[key] !== null) {
|
||||
return 'Device'
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(this.accountPreferences, key) && this.accountPreferences[key] !== null) {
|
||||
return 'Account'
|
||||
}
|
||||
return 'Default'
|
||||
},
|
||||
updateDevicePreference(key, value) {
|
||||
this.deviceChanges = {...this.deviceChanges, [key]: value}
|
||||
},
|
||||
updateAccountPreference(key, value) {
|
||||
this.accountChanges = {...this.accountChanges, [key]: value}
|
||||
},
|
||||
clearDevicePreference(key) {
|
||||
this.deviceChanges = {...this.deviceChanges, [key]: null}
|
||||
},
|
||||
clearAccountPreference(key) {
|
||||
this.accountChanges = {...this.accountChanges, [key]: null}
|
||||
},
|
||||
reloadPreferences() {
|
||||
this.accountChanges = {}
|
||||
this.deviceChanges = {}
|
||||
},
|
||||
formatEffectiveValue(schema, value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '(not set)'
|
||||
}
|
||||
if (schema.type === 'boolean') {
|
||||
return value ? 'Yes' : 'No'
|
||||
}
|
||||
if (schema.type === 'json') {
|
||||
return typeof value === 'object' ? JSON.stringify(value) : String(value)
|
||||
}
|
||||
return String(value)
|
||||
},
|
||||
async savePreferences() {
|
||||
this.saving = true
|
||||
try {
|
||||
for (const [key, value] of Object.entries(this.accountChanges)) {
|
||||
if (value === null) {
|
||||
await this.resetAccountPreference(key)
|
||||
} else {
|
||||
await this.setAccountPreference({key, value})
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(this.deviceChanges)) {
|
||||
if (value === null) {
|
||||
await this.resetDevicePreference(key)
|
||||
} else {
|
||||
await this.setDevicePreference({key, value})
|
||||
}
|
||||
}
|
||||
this.accountChanges = {}
|
||||
this.deviceChanges = {}
|
||||
} catch (error) {
|
||||
console.error('Error saving preferences:', error)
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadUserPreferences()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue