stash
This commit is contained in:
parent
7c91661be2
commit
796fef81be
37 changed files with 3404 additions and 108 deletions
|
|
@ -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