Skip to content

Commit 2aaa870

Browse files
committed
Integrate archived content into browse view
1 parent 429a9bc commit 2aaa870

14 files changed

Lines changed: 322 additions & 88 deletions

File tree

Tekst-API/openapi.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,24 @@
121121
"title": "Head"
122122
},
123123
"description": "Only return contents for the head location of the path"
124+
},
125+
{
126+
"name": "ts",
127+
"in": "query",
128+
"required": false,
129+
"schema": {
130+
"anyOf": [
131+
{
132+
"type": "integer"
133+
},
134+
{
135+
"type": "null"
136+
}
137+
],
138+
"description": "UTC timestamp in milliseconds to get contents that were current at that point in time",
139+
"title": "Ts"
140+
},
141+
"description": "UTC timestamp in milliseconds to get contents that were current at that point in time"
124142
}
125143
],
126144
"responses": {

Tekst-API/tekst/routers/browse.py

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
from datetime import UTC, datetime
12
from typing import Annotated, Literal
23

34
from beanie import PydanticObjectId
4-
from beanie.operators import Eq, In, NotIn
5+
from beanie.operators import LTE, Eq, In, NotIn
56
from fastapi import APIRouter, Path, Query, status
67

78
from tekst import errors
@@ -154,6 +155,16 @@ async def get_location_data(
154155
description="Only return contents for the head location of the path",
155156
),
156157
] = False,
158+
archive_ts: Annotated[
159+
int | None,
160+
Query(
161+
alias="ts",
162+
description=(
163+
"UTC timestamp in milliseconds to get contents "
164+
"that were current at that point in time"
165+
),
166+
),
167+
] = None,
157168
) -> LocationData:
158169
"""
159170
Returns the location path from the location with the given ID or text/level/position
@@ -165,6 +176,16 @@ async def get_location_data(
165176
# (internal constant to conveniently adjust it later if needed)
166177
contents_fetch_limit = 1024
167178

179+
# preprocess archive UTC TS
180+
archive_ts = (
181+
None
182+
if archive_ts is None
183+
else datetime.fromtimestamp(
184+
archive_ts / 1000,
185+
tz=UTC,
186+
)
187+
)
188+
168189
# find target location
169190
location_doc = None
170191
if location_id:
@@ -202,20 +223,39 @@ async def get_location_data(
202223
with_children=True,
203224
).to_list()
204225

205-
contents = {
206-
content.resource_id: [content]
207-
for content in await ContentBaseDocument.find(
208-
In(ContentBaseDocument.location_id, location_ids or []),
209-
In(
210-
ContentBaseDocument.resource_id,
211-
[resource.id for resource in target_resources],
212-
),
213-
Eq(ContentBaseDocument.archived, False),
214-
with_children=True,
215-
)
216-
.limit(contents_fetch_limit)
217-
.to_list()
218-
}
226+
if archive_ts is None:
227+
# just query for most recent contents (we can do that in one DB request)
228+
contents = {
229+
content.resource_id: [content]
230+
for content in await ContentBaseDocument.find(
231+
In(ContentBaseDocument.location_id, location_ids or []),
232+
In(
233+
ContentBaseDocument.resource_id,
234+
[resource.id for resource in target_resources],
235+
),
236+
Eq(ContentBaseDocument.archived, False),
237+
with_children=True,
238+
)
239+
.limit(contents_fetch_limit)
240+
.to_list()
241+
}
242+
else:
243+
# for each resource/location combination,
244+
# get the content that fits the requested timestamp
245+
contents = {}
246+
for resource in target_resources:
247+
content = (
248+
await ContentBaseDocument.find(
249+
In(ContentBaseDocument.location_id, location_ids or []),
250+
Eq(ContentBaseDocument.resource_id, resource.id),
251+
LTE(ContentBaseDocument.created_at, archive_ts),
252+
with_children=True,
253+
)
254+
.sort(-ContentBaseDocument.created_at)
255+
.first_or_none()
256+
)
257+
if content:
258+
contents[resource.id] = [content]
219259

220260
# add combined contents (content context) of resources that are on the subordinate
221261
# level of the target location (if the resources are configured to support this!)

Tekst-Web/i18n/ui/deDE.yml

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ common:
8181
years: Jahre
8282
months: Monate
8383
days: Tage
84-
today: heute
84+
latest: aktuell
8585
backToOverview: Zurück zur Übersicht
8686
downloadSaved: Datei gespeichert als {filename}.
8787
areYouSure: Sind Sie sich sicher?
@@ -608,18 +608,6 @@ contents:
608608
forComparison: '{title} (zum Vergleich)'
609609
noContent: Für diese Belegstelle liegen keine Inhalte vor.
610610
btnAddContent: Inhalt hinzufügen
611-
archiveWidgetTitle: Archivierte Versionen
612-
archiveAction: Archivieren
613-
archived: archiviert
614-
msgArchived: Der Inhalt für diese Belegstelle wurde archiviert.
615-
msgNoArchivedContents: Keine archivierten Inhalte für diese Belegstelle gefunden.
616-
confirmArchive: |
617-
Sind Sie sicher, dass Sie den Inhalt für diese Belegstelle archivieren wollen?
618-
Er wird nicht mehr in Suchergebnissen oder der Leseansicht angezeigt, kann aber
619-
weiterhin als archivierte Version aufgerufen werden.
620-
Sie können ihn später wiederherstellen.
621-
restore: Wiederherstellen
622-
msgRestored: Der Inhalt für diese Belegstelle wurde aus dem Archiv wiederhergestellt.
623611
confirmDelete: |
624612
Möchten Sie den Inhalt für diese Belegstelle wirklich löschen? Sie können ihn stattdessen auch archivieren.
625613
Wenn Sie den Inhalt löschen, werden auch alle archivierten Versionen des Inhalts gelöscht,
@@ -637,6 +625,28 @@ contents:
637625
oder Änderungen vornehmen und sie als Inhalt dieser Ressourcen-Ausbesserung speichern.
638626
msgContentNoFocusView: Dieser Inhalt kann in der Fokus-Ansicht nicht angezeigt werden.
639627
warnUrlInvalid: 'Diese URL scheint ungültig oder nicht erreichbar zu sein: {url}'
628+
archive:
629+
widgetTitle: Archivierte Versionen
630+
widgetTip: Archivierte Versionen von Inhalten anzeigen
631+
action: Archivieren
632+
archived: archiviert
633+
msgArchived: Der Inhalt für diese Belegstelle wurde archiviert.
634+
msgNoArchivedContents: Keine archivierten Inhalte für diese Belegstelle gefunden.
635+
confirmArchive: |
636+
Sind Sie sicher, dass Sie den Inhalt für diese Belegstelle archivieren wollen?
637+
Er wird nicht mehr in Suchergebnissen oder der Leseansicht angezeigt, kann aber
638+
weiterhin als archivierte Version aufgerufen werden.
639+
Sie können ihn später wiederherstellen.
640+
warnBrowseArchive: |
641+
Sie sehen Inhalte vom {date}.
642+
Einige der angezeigten Inhalte sind möglicherweise archivierte Versionen,
643+
die nicht mehr aktuell sind – diese sind mit einem gelben Symbol gekennzeichnet.
644+
Andere Inhalte fehlen möglicherweise, da sie zu diesem Zeitpunkt noch nicht existierten.
645+
Wenn Sie den aktuellen Stand der Inhalte sehen möchten, klicken Sie auf den Button unter diesem Hinweis.
646+
loadCurrentContents: Aktuelle Inhalte laden
647+
archivedContentTip: Archivierte Version von {date}
648+
restore: Wiederherstellen
649+
msgRestored: Der Inhalt für diese Belegstelle wurde aus dem Archiv wiederhergestellt.
640650
corrections:
641651
notes: Korrekturnotizen
642652
otherTitle: Andere Belegstellen

Tekst-Web/i18n/ui/enUS.yml

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ common:
8181
years: years
8282
months: months
8383
days: days
84-
today: today
84+
latest: latest
8585
backToOverview: Back to overview
8686
downloadSaved: File saved as {filename}.
8787
areYouSure: Are you sure?
@@ -231,6 +231,7 @@ browse:
231231
showNonPublicResources: Show unpublished resources
232232
activateCategory: Show all resources in this category
233233
deactivateCategory: Hide all resources in this category
234+
archiveWidgetTitle: Content Archive
234235
uncategorized: Uncategorized
235236
locationResourceNoData: No data for this location
236237
textNoLocations: This text's setup seems incomplete. There are no locations defined.
@@ -597,17 +598,6 @@ contents:
597598
forComparison: '{title} (for comparison)'
598599
noContent: There is no content for this location.
599600
btnAddContent: Add content
600-
archiveWidgetTitle: Archived Versions
601-
archiveAction: Archive
602-
archived: archived
603-
msgArchived: The content for this location has been archived.
604-
msgNoArchivedContents: No archived contents found for this location.
605-
confirmArchive: |
606-
Are you sure you want to archive the content for this location?
607-
It won't be shown in search results or the reading view anymore, but can still be
608-
accessed as an archived version. You will also be able to restore it later.
609-
restore: Restore
610-
msgRestored: The content for this location has been restored from the archive.
611601
confirmDelete: |
612602
Are you sure you want to delete the content for this location? You may archive it instead.
613603
Deleting it will also delete all archived versions of this content,
@@ -625,6 +615,27 @@ contents:
625615
or make changes and save them as content of this resource patch.
626616
msgContentNoFocusView: This content cannot be displayed in focus view.
627617
warnUrlInvalid: 'The URL seems to be invalid or unreachable: {url}'
618+
archive:
619+
widgetTitle: Archived Versions
620+
widgetTip: View archived content versions
621+
action: Archive
622+
archived: archived
623+
msgArchived: The content for this location has been archived.
624+
msgNoArchivedContents: No archived contents found for this location.
625+
confirmArchive: |
626+
Are you sure you want to archive the content for this location?
627+
It won't be shown in search results or the reading view anymore, but can still be
628+
accessed as an archived version. You will also be able to restore it later.
629+
warnBrowseArchive: |
630+
You are viewing contents from {date}.
631+
Some of the contents shown may be archived versions that are not up to date – those
632+
are marked with a yellow symbol.
633+
Other contents might be missing because they didn't exist at this date, yet.
634+
If you want to see the current state of the contents, click the button below this message.
635+
loadCurrentContents: Load current contents
636+
archivedContentTip: Archived version from {date}
637+
restore: Restore
638+
msgRestored: The content for this location has been restored from the archive.
628639
corrections:
629640
notes: Correction Notes
630641
otherTitle: Other locations

Tekst-Web/src/components/browse/ArchiveWidget.vue

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ButtonShelf from '@/components/generic/ButtonShelf.vue';
1111
import GenericModal from '@/components/generic/GenericModal.vue';
1212
import { useMessages } from '@/composables/messages';
1313
import { $t } from '@/i18n';
14-
import { ArrowBackIcon, HistoryIcon, NoContentIcon } from '@/icons';
14+
import { ArchiveIcon, ArrowBackIcon, NoContentIcon } from '@/icons';
1515
import { useStateStore } from '@/stores';
1616
import { utcToDateTimeString } from '@/utils';
1717
import { NButton, NEmpty, NFlex, NIcon, NSpin } from 'naive-ui';
@@ -40,7 +40,7 @@ const showModal = ref(false);
4040
const loading = ref(false);
4141
const selectedContent = ref<AnyContentRead>();
4242
const archiveItems = ref<AnyContentArchiveItem[]>([]);
43-
const title = ref($t('contents.archiveWidgetTitle'));
43+
const title = ref($t('contents.archive.widgetTitle'));
4444
4545
function getTsDistanceMs(utcStr1: string, utcStr2: string) {
4646
return Math.ceil(Math.abs(new Date(utcStr1).getTime() - new Date(utcStr2).getTime()));
@@ -111,7 +111,7 @@ async function handleItemClick(archiveItem: AnyContentArchiveItem) {
111111
const { data, error } = await GET('/contents/{id}', { params: { path: { id: archiveItem.id } } });
112112
if (!error) {
113113
selectedContent.value = data;
114-
title.value = archiveItem.createdAtStr ?? $t('contents.archiveWidgetTitle');
114+
title.value = archiveItem.createdAtStr ?? $t('contents.archive.widgetTitle');
115115
}
116116
loading.value = false;
117117
}
@@ -124,33 +124,37 @@ async function restoreArchivedContent(archivedContent: AnyContentRead) {
124124
});
125125
if (!error) {
126126
emit('restore', data);
127-
message.success($t('contents.msgRestored'));
127+
message.success($t('contents.archive.msgRestored'));
128128
}
129129
loading.value = false;
130130
showModal.value = false;
131131
}
132132
133133
function handleBackToOverview() {
134134
selectedContent.value = undefined;
135-
title.value = $t('contents.archiveWidgetTitle');
135+
title.value = $t('contents.archive.widgetTitle');
136136
}
137137
138138
function cleanup() {
139139
selectedContent.value = undefined;
140140
archiveItems.value = [];
141-
title.value = $t('contents.archiveWidgetTitle');
141+
title.value = $t('contents.archive.widgetTitle');
142142
}
143143
</script>
144144

145145
<template>
146-
<n-button v-bind="$attrs" @click.stop.prevent="handleWidgetClick">
147-
{{ $t('contents.archiveWidgetTitle') }}
146+
<n-button
147+
v-bind="$attrs"
148+
:title="$t('contents.archive.widgetTip')"
149+
@click.stop.prevent="handleWidgetClick"
150+
>
151+
{{ $t('contents.archive.widgetTitle') }}
148152
</n-button>
149153

150154
<generic-modal
151155
v-model:show="showModal"
152156
:title="title"
153-
:icon="HistoryIcon"
157+
:icon="ArchiveIcon"
154158
width="wide"
155159
@after-enter="loadArchiveData"
156160
@after-leave="cleanup"
@@ -177,10 +181,11 @@ function cleanup() {
177181
/>
178182
</div>
179183
<template v-else-if="!selectedContent">
180-
<n-flex v-if="archiveItems?.length > 1" vertical size="large">
184+
<n-flex vertical size="large">
181185
<template v-for="item in archiveItems" :key="item.id">
182-
<n-button v-if="!item.archived" secondary disabled>
183-
{{ $t('common.today') }}
186+
<n-button v-if="!item.archived" dashed disabled>
187+
{{ item.createdAtStr }}
188+
({{ $t('common.latest') }})
184189
</n-button>
185190
<n-button v-else secondary @click="handleItemClick(item)">
186191
{{ item.createdAtStr }}
@@ -190,11 +195,15 @@ function cleanup() {
190195
class="time-gap translucent text-tiny"
191196
:style="{ lineHeight: 12 + item.distanceRel * 120 + 'px' }"
192197
>
193-
{{ item.distanceStr }}
198+
~ {{ item.distanceStr }}
194199
</div>
195200
</template>
196201
</n-flex>
197-
<n-empty v-else-if="!loading" :description="$t('contents.msgNoArchivedContents')">
202+
<n-empty
203+
v-if="!loading"
204+
:description="$t('contents.archive.msgNoArchivedContents')"
205+
class="mt-lg"
206+
>
198207
<template #icon>
199208
<n-icon :component="NoContentIcon" />
200209
</template>
@@ -208,7 +217,7 @@ function cleanup() {
208217
type="warning"
209218
@click="restoreArchivedContent(selectedContent)"
210219
>
211-
{{ $t('contents.restore') }}
220+
{{ $t('contents.archive.restore') }}
212221
</n-button>
213222
<n-button type="primary" @click="showModal = false">
214223
{{ $t('common.close') }}

0 commit comments

Comments
 (0)