Added delete option to scene edits.

This commit is contained in:
2026-03-17 01:43:49 +01:00
parent 134664095a
commit f7993a9108
6 changed files with 139 additions and 648051 deletions

View File

@@ -119,12 +119,22 @@
.button-cancel { .button-cancel {
background: none; background: none;
color: var(--glass); color: var(--error);
font-weight: normal; font-weight: normal;
box-shadow: none;
.icon {
fill: var(--error);
}
&:hover:not(:disabled) { &:hover:not(:disabled) {
color: var(--error); color: var(--text-light);
background: var(--error);
cursor: pointer; cursor: pointer;
.icon {
fill: var(--text-light);
}
} }
&:disabled { &:disabled {

View File

@@ -378,6 +378,8 @@ async function reviewRevision(revision, isApproved) {
await post(`/revisions/${domain}/${revision.id}/reviews`, { await post(`/revisions/${domain}/${revision.id}/reviews`, {
isApproved, isApproved,
feedback: feedbacks.value[revision.id], feedback: feedbacks.value[revision.id],
}, {
appendErrorMessage: true,
}); });
const updatedRevision = await get(`/revisions/${domain}/${revision.id}`, { const updatedRevision = await get(`/revisions/${domain}/${revision.id}`, {

View File

@@ -28,6 +28,10 @@ export async function onBeforeRender(pageContext) {
restriction: pageContext.restriction, restriction: pageContext.restriction,
}); });
if (!scene) {
throw render(404, `Cannot find scene '${pageContext.routeParams.sceneId}'.`);
}
const [campaigns, tagIds] = await Promise.all([ const [campaigns, tagIds] = await Promise.all([
getRandomCampaigns([ getRandomCampaigns([
{ {
@@ -44,10 +48,6 @@ export async function onBeforeRender(pageContext) {
], 'tags', true), ], 'tags', true),
]); ]);
if (!scene) {
throw render(404, `Cannot find scene '${pageContext.routeParams.sceneId}'.`);
}
return { return {
pageContext: { pageContext: {
title: getTitle(scene), title: getTitle(scene),

View File

@@ -73,7 +73,7 @@
v-if="item.note" v-if="item.note"
v-tooltip="item.note" v-tooltip="item.note"
icon="info2" icon="info2"
class="item-note" class="item-note noselect"
/> />
</div> </div>
@@ -81,6 +81,7 @@
<Icon <Icon
v-if="!item.forced" v-if="!item.forced"
icon="pencil5" icon="pencil5"
class="noselect"
:class="{ active: editing.has(item.key) }" :class="{ active: editing.has(item.key) }"
@click="toggleField(item)" @click="toggleField(item)"
/> />
@@ -134,6 +135,15 @@
:disabled="!editing.has(item.key)" :disabled="!editing.has(item.key)"
/> />
<Checkbox
v-if="item.type === 'checkbox'"
:label="item.checkboxLabel"
:checked="edits[item.key]"
:disabled="!editing.has(item.key)"
class="checkbox delete"
@change="(checked) => setDelete(checked)"
/>
<div <div
v-if="item.type === 'date'" v-if="item.type === 'date'"
class="date" class="date"
@@ -210,9 +220,10 @@
<div class="editor-actions"> <div class="editor-actions">
<Checkbox <Checkbox
v-if="user.role !== 'user'" v-if="user.role !== 'user'"
v-tooltip="isApplyDisabled && editing.has('delete') ? 'Delete must be approved by an admin' : null"
label="Approve and apply immediately" label="Approve and apply immediately"
:checked="apply" :checked="apply"
:disabled="editing.size === 0" :disabled="isApplyDisabled"
@change="(checked) => apply = checked" @change="(checked) => apply = checked"
/> />
@@ -241,10 +252,7 @@ import EditTags from '#/components/edit/tags.vue';
import EditMovies from '#/components/edit/movies.vue'; import EditMovies from '#/components/edit/movies.vue';
import Checkbox from '#/components/form/checkbox.vue'; import Checkbox from '#/components/form/checkbox.vue';
import { import { post } from '#/src/api.js';
// get,
post,
} from '#/src/api.js';
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
@@ -310,12 +318,20 @@ const fields = computed(() => [
}, },
...(user.role === 'user' ...(user.role === 'user'
? [] ? []
: [{ : [
key: 'comment', {
type: 'text', key: 'comment',
placeholder: 'Do NOT use this field to summarize and clarify your revision. This field is for permanent notes and comments regarding the scene or database entry itself.', type: 'text',
value: scene.value.comment, placeholder: 'Do NOT use this field to summarize and clarify your revision. This field is for permanent notes and comments regarding the scene or database entry itself.',
}]), value: scene.value.comment,
},
{
key: 'delete',
type: 'checkbox',
checkboxLabel: 'Remove this scene from the database',
value: false,
},
]),
]); ]);
function simplifyArray(field) { function simplifyArray(field) {
@@ -332,6 +348,9 @@ const comment = ref(null);
const apply = ref(user.role !== 'user'); const apply = ref(user.role !== 'user');
const submitted = ref(false); const submitted = ref(false);
const userCanDelete = user.abilities.some((ability) => ability.subject === 'scene' && ability.action === 'delete');
const isApplyDisabled = computed(() => editing.value.size === 0 || (edits.value.delete && !userCanDelete));
const keyMap = { const keyMap = {
date: { date: {
date: 'date', date: 'date',
@@ -359,6 +378,14 @@ function setDuration(unit, event) {
edits.value.duration[timeUnits.indexOf(unit)] = Number(event.target.value); edits.value.duration[timeUnits.indexOf(unit)] = Number(event.target.value);
} }
function setDelete(checked) {
edits.value.delete = checked;
if (!userCanDelete) {
apply.value = false;
}
}
async function submit() { async function submit() {
try { try {
await post('/revisions/scenes', { await post('/revisions/scenes', {
@@ -417,6 +444,10 @@ async function submit() {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .25rem 1rem; padding: .25rem 1rem;
.value.disabled {
color: var(--glass);
}
} }
.key { .key {
@@ -488,7 +519,7 @@ async function submit() {
} }
} }
.item-note{ .item-note {
fill: var(--glass); fill: var(--glass);
padding: .5rem .75rem; padding: .5rem .75rem;
cursor: help; cursor: help;
@@ -518,6 +549,25 @@ async function submit() {
} }
} }
.checkbox.delete {
display: inline-flex;
gap: 1rem;
align-items: center;
font-weight: bold;
}
.value.disabled .delete {
:deep(.check-checkbox) + .check {
background: var(--glass-weak-30);
}
}
.value:not(.disabled) .delete {
:deep(.check-checkbox:checked) + .check {
background: var(--error);
}
}
.editor-actions { .editor-actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -540,6 +590,27 @@ async function submit() {
line-height: 1.5; line-height: 1.5;
} }
.delete-title {
display: block;
margin-top: .5rem;
max-width: 25rem;
}
.dialog-body {
padding: 1rem;
}
.dialog-section {
margin-bottom: 1rem;
text-align: center;
}
.dialog-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
@media(--small) { @media(--small) {
.row { .row {
flex-direction: column; flex-direction: column;

View File

@@ -817,7 +817,18 @@ async function applySceneMoviesDelta(sceneId, delta, trx) {
} }
} }
async function applySceneRevision(revisionIds) { async function applySceneDeleteDelta(sceneId, _delta, trx, reqUser) {
if (!reqUser.abilities.some((ability) => ability.subject === 'scene' && ability.action === 'delete')) {
throw new HttpError('You are not privileged to delete scenes', 400);
}
await knexOwner('releases')
.where('id', sceneId)
.delete()
.transacting(trx);
}
async function applySceneRevision(revisionIds, reqUser) {
const revisions = await knexOwner('scenes_revisions') const revisions = await knexOwner('scenes_revisions')
.whereIn('id', revisionIds) .whereIn('id', revisionIds)
.whereNull('applied_at'); // should not re-apply revision that was already applied .whereNull('applied_at'); // should not re-apply revision that was already applied
@@ -827,6 +838,10 @@ async function applySceneRevision(revisionIds) {
await knexOwner.transaction(async (trx) => { await knexOwner.transaction(async (trx) => {
await Promise.all(revision.deltas.map(async (delta) => { await Promise.all(revision.deltas.map(async (delta) => {
if (delta.key === 'delete') {
return applySceneDeleteDelta(revision.scene_id, delta, trx, reqUser);
}
if ([ if ([
'title', 'title',
'description', 'description',
@@ -858,11 +873,13 @@ async function applySceneRevision(revisionIds) {
await knexOwner('scenes_revisions') await knexOwner('scenes_revisions')
.where('id', revision.id) .where('id', revision.id)
.update('applied_at', knex.fn.now()); .update('applied_at', knexOwner.fn.now())
.transacting(trx);
// await trx.commit(); // await trx.commit();
}).catch(async (error) => { }).catch(async (error) => {
logger.error(`Failed to apply revision ${revision.id} on scene ${revision.scene_id}: ${error.message}`); logger.error(`Failed to apply revision ${revision.id} on scene ${revision.scene_id}: ${error.message}`);
throw error;
}); });
}, Promise.resolve()); }, Promise.resolve());
} }
@@ -892,7 +909,19 @@ export async function reviewSceneRevision(revisionId, isApproved, { feedback },
} }
if (isApproved) { if (isApproved) {
await applySceneRevision([revisionId]); try {
await applySceneRevision([revisionId], reqUser);
} catch (error) {
await knexOwner('scenes_revisions')
.where('id', revisionId)
.update({
approved: null,
reviewed_at: null,
reviewed_by: null,
});
throw error;
}
} }
} }
@@ -946,6 +975,10 @@ export async function createSceneRevision(sceneId, { edits, comment, apply }, re
}).filter(Boolean)); }).filter(Boolean));
const deltas = Object.entries(edits).map(([key, value]) => { const deltas = Object.entries(edits).map(([key, value]) => {
if (key === 'delete') {
return { key: 'delete' };
}
if (baseScene[key] === value || typeof value === 'undefined') { if (baseScene[key] === value || typeof value === 'undefined') {
return null; return null;
} }
@@ -984,6 +1017,6 @@ export async function createSceneRevision(sceneId, { edits, comment, apply }, re
if (['admin', 'editor'].includes(reqUser.role) && apply) { if (['admin', 'editor'].includes(reqUser.role) && apply) {
// don't keep the editor waiting for the revision to apply // don't keep the editor waiting for the revision to apply
reviewSceneRevision(revisionEntry.id, true, {}, reqUser); reviewSceneRevision(revisionEntry.id, true, {}, reqUser).catch(() => {});
} }
} }

File diff suppressed because it is too large Load Diff