From 3c004fd36d078bdd8474fa287ee780e3730defe3 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Mon, 28 Sep 2026 19:09:29 -0500 Subject: [PATCH] feat(dashboard): a 3D button beside every property link fmt.property(id, name) links a property's page and adds a small 3D button straight to its 3D view. Used everywhere a property is linked: the properties list and top slots, moderation, character pages' related properties, UGC search and the UGC page's placements, the home page, the chat log, player reports and report notes. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/static/js/chat-log.js | 2 +- dDashboardServer/static/js/common.js | 6 ++++++ dDashboardServer/static/js/player-reports.js | 2 +- dDashboardServer/static/js/related.js | 2 +- dDashboardServer/static/js/reports.js | 2 +- dDashboardServer/static/js/ugc-search.js | 2 +- dDashboardServer/static/js/ugc.js | 2 +- dDashboardServer/templates/index.jinja2 | 2 +- dDashboardServer/templates/moderation.jinja2 | 2 +- dDashboardServer/templates/properties.jinja2 | 4 ++-- 10 files changed, 16 insertions(+), 10 deletions(-) diff --git a/dDashboardServer/static/js/chat-log.js b/dDashboardServer/static/js/chat-log.js index ad7bc2002..82960dff3 100644 --- a/dDashboardServer/static/js/chat-log.js +++ b/dDashboardServer/static/js/chat-log.js @@ -92,7 +92,7 @@ document.getElementById('worldSubtitle').innerHTML = 'Zone ' + esc(world.mapID) + ' · instance ' + esc(world.instanceID) + (world.cloneID ? ' · clone ' + esc(world.cloneID) : '') + (world.ip ? ' · ' + esc(world.ip + ':' + world.port) + '' : '') + (world.ownerId ? ' · owned by ' + fmt.character(world.ownerId, world.ownerName) : '') + - (world.propertyId ? ' · ' + fmt.link('/properties/' + world.propertyId, 'property') : ''); + (world.propertyId ? ' · ' + fmt.property(world.propertyId, 'property') : ''); document.getElementById('worldActions').innerHTML = can.worlds ? '' : ''; loadStats(); diff --git a/dDashboardServer/static/js/common.js b/dDashboardServer/static/js/common.js index 506088330..568342582 100644 --- a/dDashboardServer/static/js/common.js +++ b/dDashboardServer/static/js/common.js @@ -145,6 +145,12 @@ }, badge: function (text, type) { return '' + esc(text) + ''; }, link: function (href, text) { return '' + esc(text) + ''; }, + // A property by name, linked to its page, with a button straight to its 3D view + property: function (id, name) { + if (!id || id === '0') return '-'; + return '' + fmt.link('/properties/' + id, name || '(unnamed)') + + ' 3D'; + }, // A character by name, linked; the ID only when the name is unknown (e.g. a deleted character) character: function (id, name) { if (!id || id === '0') return '-'; diff --git a/dDashboardServer/static/js/player-reports.js b/dDashboardServer/static/js/player-reports.js index 7c1cbabfc..565faca33 100644 --- a/dDashboardServer/static/js/player-reports.js +++ b/dDashboardServer/static/js/player-reports.js @@ -27,7 +27,7 @@ var parts = []; if (r.target_character_id !== '0') parts.push(fmt.character(r.target_character_id, r.target_name)); if (r.target_account_id) parts.push('' + esc(r.target_account_name || ('Account ' + r.target_account_id)) + ''); - if (r.property_id !== '0') parts.push('' + esc(r.property_name || 'Property') + ''); + if (r.property_id !== '0') parts.push('' + fmt.property(r.property_id, r.property_name || 'Property') + ''); if (r.kind !== 'PLAYER' && r.object_lot) parts.push('' + esc(r.object_name || ('LOT ' + r.object_lot)) + ' (' + esc(r.object_id) + ')'); return parts.join('
') || 'Unknown'; } diff --git a/dDashboardServer/static/js/related.js b/dDashboardServer/static/js/related.js index 4bb9a3341..b0335229a 100644 --- a/dDashboardServer/static/js/related.js +++ b/dDashboardServer/static/js/related.js @@ -35,7 +35,7 @@ var SECTIONS = [ ['properties', 'Properties', function (rows) { return table(kind === 'account' ? ['Property', 'Owner', 'Zone', 'Models', 'Privacy', 'Status', 'Updated'] : ['Property', 'Zone', 'Models', 'Privacy', 'Status', 'Updated'], rows.map(function (p) { - var cells = [fmt.link('/properties/' + p.id, p.name || '(unnamed)')]; + var cells = [fmt.property(p.id, p.name)]; if (kind === 'account') cells.push(fmt.character(p.owner_id, p.owner_name)); return cells.concat([fmt.zone(p.zone_id, p.zone_name), esc(nf.format(p.models)), esc(Labels.name('privacy', p.privacy_option) || p.privacy_option), p.mod_approved ? fmt.badge('Approved', 'success') : fmt.badge(p.rejection_reason ? 'Rejected' : 'Not approved yet', p.rejection_reason ? 'danger' : 'warning') + diff --git a/dDashboardServer/static/js/reports.js b/dDashboardServer/static/js/reports.js index 9c2cd5937..30c922617 100644 --- a/dDashboardServer/static/js/reports.js +++ b/dDashboardServer/static/js/reports.js @@ -526,7 +526,7 @@ var note = ''; if (current && current.unknown) note = 'Recorded before properties were told apart, so which property is not known.'; else if (activity.place === 'properties' || (current && current.property && current.place.indexOf(':') < 0)) note = 'Every property together (different builds on the same ground). Pick one property to see its own data.'; - if (current && current.property_id) note += (note ? ' ' : '') + 'Property page'; + if (current && current.property_id) note += (note ? ' ' : '') + fmt.property(current.property_id, 'Property page'); if (current && current.owner_id) note += (note ? ' · ' : '') + 'Owner ' + charLink(current.owner_id, current.owner_name); if (current && current.place && current.place.indexOf(':') > 0 && current.property) note += (note ? ' · ' : '') + 'World map'; document.getElementById('activityPlaceNote').innerHTML = note; diff --git a/dDashboardServer/static/js/ugc-search.js b/dDashboardServer/static/js/ugc-search.js index 0a36f8ce2..28259f0e4 100644 --- a/dDashboardServer/static/js/ugc-search.js +++ b/dDashboardServer/static/js/ugc-search.js @@ -20,7 +20,7 @@ // A model the player didn't name has the client's placeholder (Objects__name) var given = w.modelName && !/^Objects_\d+_name$/.test(w.modelName); var name = given ? ' as ' + esc(w.modelName) + '' : ''; - return '
On ' + fmt.link('/properties/' + w.propertyId, w.propertyName || 'Property ' + w.propertyId) + name + + return '
On ' + fmt.property(w.propertyId, w.propertyName || 'Property ' + w.propertyId) + name + ' (' + fmt.character(w.ownerId, w.ownerName) + ', ' + esc(w.zoneId) + ')' + ' 3D
'; } diff --git a/dDashboardServer/static/js/ugc.js b/dDashboardServer/static/js/ugc.js index aa90f9b53..4f4b93479 100644 --- a/dDashboardServer/static/js/ugc.js +++ b/dDashboardServer/static/js/ugc.js @@ -474,7 +474,7 @@ var refs = null, refTimer = null; function whereText(w) { return (w || []).map(function (x) { - if (x.type === 'property') return 'On ' + esc(x.propertyName || 'property ' + x.propertyId) + '' + (x.ownerName ? ' of ' + esc(x.ownerName) : ''); + if (x.type === 'property') return 'On ' + fmt.property(x.propertyId, x.propertyName || 'property ' + x.propertyId) + (x.ownerName ? ' of ' + esc(x.ownerName) : ''); if (x.type === 'mail') return 'In a mail to ' + esc(x.characterName || x.characterId) + ''; return 'In the creator\'s ' + esc(x.inventory || 'inventory'); }).join('
') || 'Not found placed, mailed or with its creator'; diff --git a/dDashboardServer/templates/index.jinja2 b/dDashboardServer/templates/index.jinja2 index 2517bb571..b5e5d1bfb 100644 --- a/dDashboardServer/templates/index.jinja2 +++ b/dDashboardServer/templates/index.jinja2 @@ -235,7 +235,7 @@ $(function () { { data: 'instanceID', render: function (d, t, row) { return row.properties ? esc(row.properties) + ' propert' + (row.properties === 1 ? 'y' : 'ies') : esc(d); } } ].concat(detailedWorlds ? [{ data: 'cloneID' }, { data: 'propertyName', defaultContent: '', render: function (d, t, row) { if (!row.cloneID) return '-'; - return (row.propertyId ? fmt.link('/properties/' + row.propertyId, d || '(unnamed)') : esc(d || '')) + (row.ownerId ? '
by ' + fmt.character(row.ownerId, row.ownerName) + '
' : ''); + return (row.propertyId ? fmt.property(row.propertyId, d) : esc(d || '')) + (row.ownerId ? '
by ' + fmt.character(row.ownerId, row.ownerName) + '
' : ''); } }] : []).concat([ { data: 'players', render: function (d) { return fmt.badge(d, 'primary'); } } ]).concat(detailedWorlds ? [{ data: 'isPrivate', render: function (d) { return d ? fmt.badge('Private', 'warning') : fmt.badge('Public', 'success'); } }, { data: 'port', defaultContent: '', render: function (d, t, row) { return '' + esc((row.ip || '') + ':' + d) + ''; } }] : []).concat(DASH.can('worlds_manage') ? [{ data: null, orderable: false, className: 'text-end', render: function (d, t, row) { diff --git a/dDashboardServer/templates/moderation.jinja2 b/dDashboardServer/templates/moderation.jinja2 index 382f57e22..365bd8187 100644 --- a/dDashboardServer/templates/moderation.jinja2 +++ b/dDashboardServer/templates/moderation.jinja2 @@ -88,7 +88,7 @@ $(function () { if (DASH.can('moderate_properties')) { serverTable('#pendingPropsTable', '/api/tables/pending_properties', [ { data: 'id', orderable: false }, - { data: 'name', orderable: false, render: function (d, t, row) { return fmt.link('/properties/' + row.id, d || '(unnamed)'); } }, + { data: 'name', orderable: false, render: function (d, t, row) { return fmt.property(row.id, d); } }, { data: 'owner_id', orderable: false, render: function (d, t, row) { return fmt.character(d, row.owner_name); } }, // Nothing built yet is worth knowing before approving it for everyone to visit { data: 'models', orderable: false, render: function (d) { return d ? esc(d) : fmt.badge('Empty', 'secondary'); } }, diff --git a/dDashboardServer/templates/properties.jinja2 b/dDashboardServer/templates/properties.jinja2 index 0cacab41a..32d3ede19 100644 --- a/dDashboardServer/templates/properties.jinja2 +++ b/dDashboardServer/templates/properties.jinja2 @@ -62,7 +62,7 @@ $(function () { var pending = document.getElementById('pendingOnly'); var table = serverTable('#propertiesTable', '/api/tables/properties', [ { data: 'id' }, - { data: 'name', render: function (d, t, row) { return fmt.link('/properties/' + row.id, d || '(unnamed)'); } }, + { data: 'name', render: function (d, t, row) { return t === 'display' ? fmt.property(row.id, d) : d; } }, { data: 'owner_id', render: function (d, t, row) { return fmt.character(d, row.owner_name); } }, { data: 'mod_approved', render: function (d) { return d ? fmt.badge('Approved', 'success') : fmt.badge('Unapproved', 'warning'); } }, { data: 'reputation' }, @@ -95,7 +95,7 @@ $(function () { if (!slots.length) { slotsEl.innerHTML = '
No property worlds found in the client data.
'; return; } slotsEl.innerHTML = slots.map(function (s) { var showing = s.showing - ? fmt.link('/properties/' + s.showing.id, s.showing.name || '(unnamed)') + ' by ' + esc(s.showing.owner_name) + '' + ? fmt.property(s.showing.id, s.showing.name) + ' by ' + esc(s.showing.owner_name) + '' : 'Nothing (locked slot)'; var warn = fullAuto ? '' : s.picked_unavailable ? '
The picked property isn\'t an approved public property of ' + esc(s.zone_name) + ' (any more), so the slot falls back to Auto.
' : s.picked_shown_elsewhere ? '
The picked property is already shown in an earlier slot, so this one falls back to Auto.
' : '';