Added compact revision overview, formatting duration. Preventing rejected revisions from being reviewed again.

This commit is contained in:
2024-10-08 21:30:55 +02:00
parent 9e87c47b24
commit 067d414cd9
8 changed files with 323 additions and 136 deletions

View File

@@ -1,7 +1,7 @@
<template>
<div class="page">
<div
v-if="interactive"
v-if="context === 'admin'"
class="revs-header"
>
<Checkbox
@@ -11,136 +11,190 @@
/>
</div>
<ul class="revs nolist">
<ul
class="revs nolist"
:class="{ [`revs-${context}`]: true }"
>
<li
v-for="rev in curatedRevisions"
:key="`rev-${rev.id}`"
class="rev"
:class="{ reviewed: reviewedRevisions.has(rev.id) }"
:class="{
reviewed: reviewedRevisions.has(rev.id),
expanded: context === 'admin' || expanded.has(rev.id)
}"
>
<div class="rev-header">
<a
:href="`/scene/${rev.sceneId}`"
target="_blank"
class="rev-link rev-scene nolink noshrink"
>{{ rev.sceneId }}@{{ rev.hash.slice(0, 6) }}</a>
<template v-if="context === 'admin' || expanded.has(rev.id)">
<div class="rev-header">
<a
:href="`/scene/${rev.sceneId}`"
target="_blank"
class="rev-link rev-scene nolink noshrink"
>{{ rev.sceneId }}@{{ rev.hash.slice(0, 6) }}</a>
<a
:href="`/scene/${rev.sceneId}`"
target="_blank"
class="rev-link rev-title nolink ellipsis"
>{{ rev.base.title }}</a>
<a
:href="`/scene/${rev.sceneId}`"
target="_blank"
class="rev-link rev-title nolink ellipsis"
>{{ rev.base.title }}</a>
<div class="rev-details noshrink">
<a
v-if="rev.user"
:href="`/user/${rev.user.username}`"
target="_blank"
class="rev-username nolink"
>{{ rev.user.username }}</a>
<time
:datetime="rev.createdAt"
class="rev-created"
>{{ format(rev.createdAt, 'yyyy-MM-dd hh:mm') }}</time>
</div>
<div class="rev-actions noshrink">
<span
v-if="rev.review && context === 'admin'"
class="approved"
:class="{ rejected: !rev.review.isApproved }"
>{{ rev.review.isApproved ? 'Approved' : 'Rejected' }} by&nbsp;<a
:href="`/user/${rev.review.username}`"
target="_blank"
class="nolink"
>{{ rev.review.username }}</a>&nbsp;{{ format(rev.review.reviewedAt, 'yyyy-MM-dd hh:mm') }}</span>
<template v-else-if="context === 'admin'">
<Icon
v-tooltip="`Ban user from submitting revisions`"
icon="user-block"
class="review-reject review-ban"
@click="banEditor(rev)"
/>
<Icon
v-tooltip="`Reject revision`"
icon="blocked"
class="review-reject"
@click="reviewRevision(rev, false)"
/>
<input
v-model="feedbacks[rev.id]"
placeholder="Feedback"
class="input"
>
<Icon
v-tooltip="`Approve and apply revision`"
icon="checkmark"
class="review-approve"
@click="reviewRevision(rev, true)"
/>
</template>
</div>
</div>
<ul class="rev-deltas">
<li
v-for="(delta, index) in rev.deltas"
:key="`delta-${rev.id}-${index}`"
class="delta"
>
<span class="delta-key ellipsis">{{ delta.key }}</span>
<div class="delta-deltas">
<span class="delta-from delta-value">
<ul
v-if="Array.isArray(rev.base[delta.key])"
class="nolist"
>[
<li
v-for="item in rev.base[delta.key]"
:key="`item-${rev.id}-${index}-${item.id}`"
class="delta-item"
:class="{ modified: item.modified }"
>{{ item.name || item.id || item }}</li>&nbsp;]
</ul>
<template v-else-if="rev.base[delta.key] instanceof Date">{{ format(rev.base[delta.key], 'yyyy-MM-dd hh:mm') }}</template>
<template v-else>{{ rev.base[delta.key] }}</template>
</span>
<span class="delta-arrow"></span>
<span class="delta-to delta-value">
<ul
v-if="Array.isArray(delta.value)"
class="nolist"
>[
<li
v-for="item in delta.value"
:key="`item-${rev.id}-${index}-${item.id}`"
class="delta-item"
:class="{ modified: item.modified }"
>{{ item.name || item.id || item }}</li>&nbsp;]
</ul>
<template v-else-if="delta.value instanceof Date">{{ format(delta.value, 'yyyy-MM-dd hh:mm') }}</template>
<template v-else>{{ delta.value }}</template>
</span>
</div>
</li>
</ul>
<div
v-if="rev.comment"
class="rev-comment"
>
{{ rev.comment }}
</div>
</template>
<div
v-else
class="rev-compact"
@click="expanded.add(rev.id)"
>
<span class="rev-scene nolink noshrink"><template v-if="context !== 'scene'">{{ rev.sceneId }}</template>@{{ rev.hash.slice(0, 6) }}</span>
<span
v-if="context !== 'scene'"
class="rev-title nolink ellipsis"
>{{ rev.base.title }}</span>
<span class="rev-summary">
<span
v-if="rev.comment"
class="summary-comment ellipsis"
>{{ rev.comment }}</span>
<span class="summary-deltas ellipsis">{{ rev.deltas.map((delta) => delta.key).join(', ') }}</span>
</span>
<div class="rev-details noshrink">
<a
v-if="rev.user"
v-if="rev.user && context !== 'user'"
:href="`/user/${rev.user.username}`"
target="_blank"
class="rev-username nolink"
class="rev-username nolink ellipsis"
@click.stop
>{{ rev.user.username }}</a>
<a
v-if="rev.user && context !== 'user'"
v-tooltip="rev.user.username"
:href="`/user/${rev.user.username}`"
target="_blank"
class="rev-avatar nolink ellipsis"
@click.stop
><Icon icon="user3-long" /></a>
<time
:datetime="rev.createdAt"
class="rev-created"
>{{ format(rev.createdAt, 'yyyy-MM-dd hh:mm') }}</time>
</div>
<div class="rev-actions noshrink">
<span
v-if="rev.review"
class="approved"
:class="{ rejected: !rev.review.isApproved }"
>{{ rev.review.isApproved ? 'Approved' : 'Rejected' }} by&nbsp;<a
:href="`/user/${rev.review.username}`"
target="_blank"
class="nolink"
>{{ rev.review.username }}</a>&nbsp;{{ format(rev.review.reviewedAt, 'yyyy-MM-dd hh:mm') }}</span>
<template v-else-if="interactive">
<Icon
v-tooltip="`Ban user from submitting revisions`"
icon="user-block"
class="review-reject review-ban"
@click="banEditor(rev)"
/>
<Icon
v-tooltip="`Reject revision`"
icon="blocked"
class="review-reject"
@click="reviewRevision(rev, false)"
/>
<input
v-model="feedbacks[rev.id]"
placeholder="Feedback"
class="input"
>
</template>
<Icon
v-if="(!rev.review || !rev.review.isApproved) && interactive"
v-tooltip="`Approve and apply revision`"
icon="checkmark"
class="review-approve"
@click="reviewRevision(rev, true)"
/>
</div>
</div>
<ul class="rev-deltas">
<li
v-for="(delta, index) in rev.deltas"
:key="`delta-${rev.id}-${index}`"
class="delta"
>
<span class="delta-key ellipsis">{{ delta.key }}</span>
<div class="delta-deltas">
<span class="delta-from delta-value">
<ul
v-if="Array.isArray(rev.base[delta.key])"
class="nolist"
>[
<li
v-for="item in rev.base[delta.key]"
:key="`item-${rev.id}-${index}-${item.id}`"
class="delta-item"
:class="{ modified: item.modified }"
>{{ item.name || item.id || item }}</li>&nbsp;]
</ul>
<template v-else-if="rev.base[delta.key] instanceof Date">{{ format(rev.base[delta.key], 'yyyy-MM-dd hh:mm') }}</template>
<template v-else>{{ rev.base[delta.key] }}</template>
</span>
<span class="delta-arrow"></span>
<span class="delta-to delta-value">
<ul
v-if="Array.isArray(delta.value)"
class="nolist"
>[
<li
v-for="item in delta.value"
:key="`item-${rev.id}-${index}-${item.id}`"
class="delta-item"
:class="{ modified: item.modified }"
>{{ item.name || item.id || item }}</li>&nbsp;]
</ul>
<template v-else-if="delta.value instanceof Date">{{ format(delta.value, 'yyyy-MM-dd hh:mm') }}</template>
<template v-else>{{ delta.value }}</template>
</span>
</div>
</li>
</ul>
<div
v-if="rev.comment"
class="rev-comment"
>
{{ rev.comment }}
</div>
</li>
</ul>
@@ -154,11 +208,12 @@ import { format } from 'date-fns';
import Checkbox from '#/components/form/checkbox.vue';
import { get, post } from '#/src/api.js';
import { formatDuration } from '#/utils/format.js';
defineProps({
interactive: {
type: Boolean,
default: false,
context: {
type: String,
default: 'admin',
},
});
@@ -176,6 +231,7 @@ const moviesById = computed(() => Object.fromEntries(movies.value.map((movie) =>
const feedbacks = ref({});
const showReviewed = ref(false);
const reviewedRevisions = ref(new Set());
const expanded = ref(new Set());
const mappedKeys = {
actors: actorsById,
@@ -189,6 +245,10 @@ const dateKeys = [
'createdAt',
];
const curatedKeys = {
duration: (duration) => formatDuration(duration),
};
const curatedRevisions = computed(() => revisions.value.map((revision) => {
const curatedBase = Object.fromEntries(Object.entries(revision.base).map(([key, value]) => {
if (Array.isArray(value) && mappedKeys[key]) {
@@ -203,6 +263,10 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
return [key, new Date(value)];
}
if (curatedKeys[key]) {
return [key, curatedKeys[key](value)];
}
return [key, value];
}));
@@ -225,6 +289,13 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
};
}
if (curatedKeys[delta.key]) {
return {
...delta,
value: curatedKeys[delta.key](delta.value),
};
}
return delta;
});
@@ -267,8 +338,6 @@ async function reviewRevision(revision, isApproved) {
}
async function banEditor(revision) {
console.log('ban!', revision);
await post('/bans', {
userId: revision.user.id,
banIp: true,
@@ -302,15 +371,19 @@ async function banEditor(revision) {
}
.rev {
min-width: 1200px;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: .25rem;
margin-bottom: .5rem;
box-shadow: 0 0 3px var(--shadow-weak-30);
margin-bottom: .25rem;
font-size: .9rem;
&.expanded {
min-width: 1200px;
margin-bottom: .5rem;
}
&.reviewed {
pointer-events: none;
opacity: .5;
@@ -322,8 +395,6 @@ async function banEditor(revision) {
}
.rev-link {
color: var(--glass-strong-10);
&:hover {
color: var(--primary);
text-decoration: underline;
@@ -343,10 +414,11 @@ async function banEditor(revision) {
box-sizing: border-box;
padding: .5rem .5rem;
align-items: center;
color: var(--glass-strong-10);
}
.rev-title {
color: inherit;
padding: .5rem 0;
}
@@ -360,7 +432,10 @@ async function banEditor(revision) {
}
.rev-username {
display: flex;
align-items: center;
font-weight: bold;
height: 100%;
}
.rev-actions {
@@ -485,4 +560,74 @@ async function banEditor(revision) {
padding: .5rem .5rem;
border-top: solid 1px var(--glass-weak-30);
}
.rev-compact {
display: flex;
align-items: stretch;
cursor: pointer;
.rev-title {
width: 15rem;
margin-right: 2rem;
}
.rev-details {
flex-grow: 0;
}
.rev-avatar {
display: none;
.icon {
fill: var(--glass);
}
}
}
.revs-scene .rev-compact .rev-scene {
width: 6rem;
}
.rev-summary {
display: flex;
align-items: center;
flex-grow: 1;
overflow: hidden;
}
.summary-comment {
padding-right: .5rem;
border-right: solid 1px var(--glass-weak-20);
margin-right: .5rem;
}
.summary-deltas {
font-style: italic;
}
@media(--compact) {
.rev-compact .rev-details {
gap: .5rem;
}
.rev-compact .rev-username {
display: none;
}
.rev-compact .rev-avatar {
display: block;
}
}
@media(--small) {
.rev-compact .summary-comment {
padding: 0;
border: none;
margin: 0;
& + .summary-deltas {
display: none;
}
}
}
</style>