stash visibility

This commit is contained in:
j3d1 2026-09-01 02:10:50 +02:00
parent 23164af04f
commit 1853577ff5
26 changed files with 276 additions and 39 deletions

View file

@ -98,6 +98,7 @@
<label class="form-label fw-bold">{{ field.label }}</label>
<field-input :type="field.type" v-model="presetValues[field.key]"
:owner-groups="ownerGroups" :availability-policies="availability_policies"
:visibility-policies="visibility_policies"
:storage-locations="storage_locations"/>
</div>
</div>
@ -187,6 +188,7 @@
<field-input :type="field.type" v-model="currentItemDetails[field.key]"
:owner-groups="ownerGroups"
:availability-policies="availability_policies"
:visibility-policies="visibility_policies"
:storage-locations="storage_locations"/>
</div>
</form>
@ -293,6 +295,7 @@ const PRESET_FIELD_DEFS = [
{key: 'owner_group', label: 'Owner', type: 'owner_group'},
{key: 'owned_quantity', label: 'Quantity', type: 'number'},
{key: 'availability_policy', label: 'Availability Policy', type: 'availability_policy'},
{key: 'visibility_policy', label: 'Visibility Policy', type: 'visibility_policy'},
{key: 'storage_location', label: 'Storage Location', type: 'storage_location'}
];
@ -307,6 +310,7 @@ function defaultValueFor(key) {
case 'owned_quantity':
return 1;
case 'availability_policy':
case 'visibility_policy':
return 'private';
default:
return '';
@ -390,7 +394,7 @@ export default {
}
},
computed: {
...mapState(['user', 'availability_policies', 'storage_locations', 'groups', 'groupMemberships']),
...mapState(['user', 'availability_policies', 'visibility_policies', 'storage_locations', 'groups', 'groupMemberships']),
ownerGroups() {
const hostedHandles = new Set(this.groups.map(group => group.handle));
const foreign = this.groupMemberships.filter(m => !hostedHandles.has(m.handle));
@ -619,6 +623,7 @@ export default {
properties: details.properties,
owned_quantity: details.owned_quantity,
availability_policy: details.availability_policy,
visibility_policy: details.visibility_policy,
storage_location: details.storage_location,
owner_group: details.owner_group
});

View file

@ -20,6 +20,12 @@
</option>
</select>
<select v-else-if="type === 'visibility_policy'" class="form-select" v-model="localValue">
<option v-for="policy in visibilityPolicies" :key="policy.slug" :value="policy.slug">
{{ policy.text }}
</option>
</select>
<select v-else-if="type === 'storage_location'" class="form-select" v-model="localValue">
<option :value="null">No storage location</option>
<option v-for="location in storageLocations" :key="location.id" :value="location.id">
@ -53,6 +59,10 @@ export default {
type: Array,
default: () => []
},
visibilityPolicies: {
type: Array,
default: () => []
},
storageLocations: {
type: Array,
default: () => []

View file

@ -42,6 +42,7 @@ export default createStore({
files: [],
categories: [],
availability_policies: [],
visibility_policies: [],
domains: [],
storage_locations: [],
active_workflows: [],
@ -100,6 +101,9 @@ export default createStore({
setAvailabilityPolicies(state, availability_policies) {
state.availability_policies = availability_policies;
},
setVisibilityPolicies(state, visibility_policies) {
state.visibility_policies = visibility_policies;
},
setDomains(state, domains) {
state.domains = domains;
},
@ -343,7 +347,7 @@ export default createStore({
? await dispatch('getFriendServers', {username: 'x@' + splitGroupHandle(item.owner_group).domain})
: await dispatch('getHomeServers')
const owner = item.owner_group ? encodeHandleForUrl(item.owner_group) : state.user
const data = {availability_policy: 'private', ...item}
const data = {availability_policy: 'private', visibility_policy: 'private', ...item}
delete data.owner_group
const reply = await servers.post(getters.signAuth, '/api/v1/inventory_items/' + owner + '/', data)
state.last_load.files = 0
@ -353,7 +357,7 @@ export default createStore({
const servers = item.owner_group
? await dispatch('getFriendServers', {username: 'x@' + splitGroupHandle(item.owner_group).domain})
: await dispatch('getHomeServers')
const data = {availability_policy: 'friends', ...item}
const data = {availability_policy: 'friends', visibility_policy: 'friends', ...item}
data.files = data.files.map(file => file.id)
// Path is scoped by the owner's handle, not the item's own id domain - see docs/implementation.md#owner-handle-scoped-routes.
const path = '/api/v1/inventory_items/' + encodeHandleForUrl(item.owner_group || item.owner) + '/' + item.id + '/'
@ -661,6 +665,16 @@ export default createStore({
state.last_load.availability_policies = Date.now()
return data
},
async fetchVisibilityPolicies({state, commit, dispatch, getters}) {
if (state.last_load.visibility_policies > Date.now() - 1000 * 60 * 60 * 24) {
return state.visibility_policies
}
const servers = await dispatch('getHomeServers')
const data = await servers.get(getters.signAuth, '/api/v1/visibility_policies/')
commit('setVisibilityPolicies', data.map(policy => ({slug: policy[0], text: policy[1]})))
state.last_load.visibility_policies = Date.now()
return data
},
async fetchStorageLocations({state, commit, dispatch, getters}) {
if (state.last_load.storage_locations > Date.now() - 1000 * 60 * 60 * 24) {
return state.storage_locations
@ -734,7 +748,8 @@ export default createStore({
state.last_load.tags,
state.last_load.properties,
state.last_load.categories,
state.last_load.availability_policies)
state.last_load.availability_policies,
state.last_load.visibility_policies)
if (last_load_info > Date.now() - 1000 * 60 * 60 * 24) {
return state.info
}
@ -747,11 +762,16 @@ export default createStore({
slug: policy[0],
text: policy[1]
})))
commit('setVisibilityPolicies', data.visibility_policies.map(policy => ({
slug: policy[0],
text: policy[1]
})))
commit('setDomains', data.domains)
state.last_load.tags = Date.now()
state.last_load.properties = Date.now()
state.last_load.categories = Date.now()
state.last_load.availability_policies = Date.now()
state.last_load.visibility_policies = Date.now()
return data
},
async userIdentityRecord({state}, {password}) {

View file

@ -67,6 +67,18 @@
</ul>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title">Visibility Policies</h5>
</div>
<div class="card-body">
<ul>
<li v-for="policy in visibility_policies.sort()" :key="policy.id">
{{ policy.text }}
</li>
</ul>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title">Storage Locations</h5>
@ -98,7 +110,7 @@ export default {
...BIcons
},
computed: {
...mapState(["tags", "properties", "categories", "availability_policies", "domains", "storage_locations"])
...mapState(["tags", "properties", "categories", "availability_policies", "visibility_policies", "domains", "storage_locations"])
},
methods: {
...mapActions(["fetchInfo", "fetchStorageLocations"])

View file

@ -35,8 +35,9 @@
<table class="table table-striped" v-if="layout === 'table'">
<thead>
<tr>
<th style="width:40%;">Name</th>
<th style="width:25%">Availability Policy</th>
<th style="width:35%;">Name</th>
<th style="width:20%">Availability Policy</th>
<th style="width:20%">Visibility Policy</th>
<th class="d-none d-md-table-cell" style="width:25%">Amount</th>
<th>Actions</th>
</tr>
@ -49,6 +50,9 @@
<td class="d-none d-md-table-cell">
<span class="badge bg-secondary text-white">{{ item.availability_policy }}</span>
</td>
<td class="d-none d-md-table-cell">
<span class="badge bg-secondary text-white">{{ item.visibility_policy }}</span>
</td>
<td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
<td class="table-action">
<router-link v-if="canEdit" :to="`${itemRoute(item)}/edit`">
@ -84,6 +88,7 @@
</router-link></h5>
<div class="card-text text-black-50">
<span class="badge bg-secondary text-white">{{ item.availability_policy }}</span>
<span class="badge bg-secondary text-white">{{ item.visibility_policy }}</span>
<span class="float-right">{{ item.owned_quantity }}</span>
</div>
<div class="btn-group">

View file

@ -39,6 +39,16 @@
</option>
</select>
</div>
<div class="mb-3">
<label for="visibility_policy" class="form-label">Visibility Policy</label>
<select class="form-select" id="visibility_policy" name="visibility_policy"
v-model="item.visibility_policy">
<option v-for="policy in visibility_policies" :value="policy.slug"
:selected="policy.slug === item.visibility_policy">
{{ policy.text }}
</option>
</select>
</div>
<div class="mb-3">
<label for="storage_location" class="form-label">Storage Location</label>
<select class="form-select" id="storage_location" name="storage_location"
@ -115,7 +125,7 @@ export default {
}
},
computed: {
...mapState(["availability_policies", "storage_locations"]),
...mapState(["availability_policies", "visibility_policies", "storage_locations"]),
decodedHandle() {
return decodeHandleFromUrl(this.handle)
}

View file

@ -48,6 +48,16 @@
</option>
</select>
</div>
<div class="mb-3">
<label for="visibility_policy" class="form-label">Visibility Policy</label>
<select class="form-select" id="visibility_policy" name="visibility_policy"
v-model="item.visibility_policy">
<option v-for="policy in visibility_policies" :value="policy.slug"
:selected="policy.slug === item.visibility_policy">
{{ policy.text }}
</option>
</select>
</div>
<div class="mb-3">
<label for="storage_location" class="form-label">Storage Location</label>
<select class="form-select" id="storage_location" name="storage_location"
@ -109,6 +119,7 @@ export default {
description: "",
owned_quantity: 0,
availability_policy: "",
visibility_policy: "",
image: "",
tags: [],
properties: [],
@ -126,7 +137,7 @@ export default {
'fetchGroupMemberships'])
},
computed: {
...mapState(["availability_policies", "storage_locations", "groups", "groupMemberships"]),
...mapState(["availability_policies", "visibility_policies", "storage_locations", "groups", "groupMemberships"]),
// Groups hosted here plus groups only known via a GroupMembership pointer (see
// Groups.vue's allGroups for the same merge/dedupe).
ownerGroups() {

View file

@ -32,9 +32,10 @@
<table class="table table-striped" v-if="layout === 'table'">
<thead>
<tr>
<th style="width:40%;">Name</th>
<th style="width:25%">Path</th>
<th class="d-none d-md-table-cell" style="width:25%">Category</th>
<th style="width:35%;">Name</th>
<th style="width:20%">Path</th>
<th class="d-none d-md-table-cell" style="width:20%">Category</th>
<th class="d-none d-md-table-cell" style="width:20%">Visibility Policy</th>
<th>Actions</th>
</tr>
</thead>
@ -50,6 +51,9 @@
<span class="badge bg-info text-white" v-if="location.category">{{ location.category }}</span>
<span class="text-muted" v-else>-</span>
</td>
<td class="d-none d-md-table-cell">
<span class="badge bg-secondary text-white">{{ location.visibility_policy }}</span>
</td>
<td class="table-action">
<router-link :to="`${locationRoute(location)}/edit`">
<b-icon-pencil-square></b-icon-pencil-square>
@ -81,6 +85,7 @@
<div class="card-text text-black-50">
<small class="text-muted d-block">{{ location.path }}</small>
<span class="badge bg-info text-white" v-if="location.category">{{ location.category }}</span>
<span class="badge bg-secondary text-white">{{ location.visibility_policy }}</span>
</div>
<div class="card-text" v-if="location.description">
<small>{{ location.description }}</small>

View file

@ -25,6 +25,12 @@
<label for="owner" class="form-label">Owner</label>
<div>{{ location.owner || location.owner_group || '-' }}</div>
</div>
<div class="mb-3">
<label for="visibility_policy" class="form-label">Visibility Policy</label>
<div>
<span class="badge bg-secondary text-white">{{ location.visibility_policy }}</span>
</div>
</div>
</div>
</div>
<div class="card" v-if="canEdit">

View file

@ -38,6 +38,16 @@
</option>
</select>
</div>
<div class="mb-3">
<label for="visibility_policy" class="form-label">Visibility Policy</label>
<select class="form-select" id="visibility_policy" name="visibility_policy"
v-model="location.visibility_policy">
<option v-for="policy in visibility_policies" :value="policy.slug"
:selected="policy.slug === location.visibility_policy">
{{ policy.text }}
</option>
</select>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" style="width: 100%"
@click="submitForm()">Update
@ -80,6 +90,7 @@ export default {
description: "",
category: null,
parent: null,
visibility_policy: "private",
},
// The location's own owner's full location list, for the parent dropdown - own
// personal locations, or the owning group's own (see loadLocation).
@ -87,7 +98,7 @@ export default {
}
},
computed: {
...mapState(["categories"]),
...mapState(["categories", "visibility_policies"]),
decodedHandle() {
return decodeHandleFromUrl(this.handle)
},

View file

@ -45,6 +45,16 @@
</option>
</select>
</div>
<div class="mb-3">
<label for="visibility_policy" class="form-label">Visibility Policy</label>
<select class="form-select" id="visibility_policy" name="visibility_policy"
v-model="location.visibility_policy">
<option v-for="policy in visibility_policies" :value="policy.slug"
:selected="policy.slug === location.visibility_policy">
{{ policy.text }}
</option>
</select>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" style="width: 100%"
@click="submitForm()">Add
@ -84,6 +94,7 @@ export default {
description: "",
category: null,
parent: null,
visibility_policy: "private",
// The group's own "#name@domain" handle once picked, or null for a personal
// location - createStorageLocation resolves the target domain from it directly.
owner_group: null
@ -115,7 +126,7 @@ export default {
}
},
computed: {
...mapState(["categories", "storage_locations", "groups", "groupMemberships"]),
...mapState(["categories", "visibility_policies", "storage_locations", "groups", "groupMemberships"]),
// Groups hosted here plus groups only known via a GroupMembership pointer (see
// Groups.vue's allGroups for the same merge/dedupe).
ownerGroups() {