Skip to content

Commit 258d7b5

Browse files
authored
Merge pull request #268 from codex-team/note-history
feat(note-history): implement history version page
2 parents c84e77a + f9eb34c commit 258d7b5

File tree

15 files changed

+342
-24
lines changed

15 files changed

+342
-24
lines changed

codex-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"vue-tsc": "latest"
4949
},
5050
"dependencies": {
51-
"@editorjs/editorjs": "2.30.2-rc.0",
51+
"@editorjs/editorjs": "2.30.2",
5252
"vue": "^3.4.16"
5353
}
5454
}

codex-ui/src/vue/components/avatar/Avatar.vue

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
<img
33
:src="src"
44
:alt="`Avatar of ${username}`"
5-
:class="$style.avatar"
5+
:class="$style[`avatar--${size}`]"
66
referrerpolicy="no-referrer"
77
>
88
</template>
99

1010
<script setup lang="ts">
1111
import { defineProps } from 'vue';
1212
13-
defineProps<{
13+
withDefaults(defineProps<{
1414
/**
1515
* Path to the image
1616
*/
@@ -21,13 +21,33 @@ defineProps<{
2121
* In future, we can use this to generate initials
2222
*/
2323
username: string;
24-
}>();
24+
25+
/**
26+
* Size of the avatar image
27+
* medium by default
28+
*/
29+
size: 'medium' | 'small';
30+
}>(),
31+
{
32+
src: undefined,
33+
username: undefined,
34+
size: 'medium',
35+
});
36+
2537
</script>
2638

2739
<style module>
2840
.avatar {
29-
width: var(--size-avatar);
30-
height: var(--size-avatar);
31-
border-radius: var(--radius-m);
41+
&--small {
42+
width: var(--size-icon);
43+
height: var(--size-icon);
44+
border-radius: var(--radius-s);
45+
}
46+
47+
&--medium {
48+
width: var(--size-avatar);
49+
height: var(--size-avatar);
50+
border-radius: var(--radius-m);
51+
}
3252
}
3353
</style>

src/application/i18n/messages/en.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@
8383
},
8484
"history": {
8585
"title": "Versions history",
86-
"view": "View"
86+
"view": "View",
87+
"useVersion": "Use this version",
88+
"confirmVersionRestore": "Do you really want to use this version?",
89+
"editedTime": "edited on"
8790
},
8891
"noteList": {
8992
"emptyNoteList": "No Notes yet. Make your first note",
@@ -175,6 +178,7 @@
175178
"notFound": "Not found",
176179
"joinTeam": "Join",
177180
"authorization": "Authorize",
178-
"history": "History"
181+
"history": "History",
182+
"historyVersion": "Version"
179183
}
180184
}

src/application/router/routes.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { RouteRecordRaw } from 'vue-router';
1010
import AddTool from '@/presentation/pages/marketplace/AddTool.vue';
1111
import MarketplacePage from '@/presentation/pages/marketplace/MarketplacePage.vue';
1212
import History from '@/presentation/pages/History.vue';
13+
import HistoryVersion from '@/presentation/pages/HistoryVersion.vue';
1314
import MarketplaceTools from '@/presentation/pages/marketplace/MarketplaceTools.vue';
1415

1516
// Default production hostname for homepage. If different, then custom hostname used
@@ -57,6 +58,20 @@ const routes: RouteRecordRaw[] = [
5758
noteId: String(route.params.noteId),
5859
}),
5960
},
61+
{
62+
name: 'history_version',
63+
path: '/note/:noteId/history/:historyId',
64+
component: HistoryVersion,
65+
meta: {
66+
layout: 'fullpage',
67+
pageTitleI18n: 'pages.historyVersion',
68+
authRequired: true,
69+
},
70+
props: route => ({
71+
noteId: String(route.params.noteId),
72+
historyId: Number(route.params.historyId),
73+
}),
74+
},
6075
{
6176
name: 'new',
6277
path: '/new',
Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,123 @@
11
import type { MaybeRefOrGetter, Ref } from 'vue';
2-
import { computed, onMounted, ref, toValue } from 'vue';
2+
import { computed, onMounted, ref, toValue, watch } from 'vue';
33
import type { NoteHistoryRecord, NoteHistoryMeta } from '@/domain/entities/History';
44
import type { Note } from '@/domain/entities/Note';
55
import { noteHistoryService } from '@/domain';
6+
import { notEmpty } from '@/infrastructure/utils/empty';
67

78
interface UseNoteHistoryComposableState {
89
/**
910
* Note hisotry is array of the history meta used for history preview
1011
*/
1112
noteHistory: Ref<NoteHistoryMeta[] | null>;
13+
14+
/**
15+
* Content of the certain history record
16+
*/
17+
historyContent: Ref<NoteHistoryRecord['content'] | undefined>;
18+
19+
/**
20+
* Tools that are used in current history content
21+
*/
22+
historyTools: Ref<NoteHistoryRecord['tools'] | undefined>;
23+
24+
/**
25+
* Metadata of the history record
26+
*/
27+
historyMeta: Ref<NoteHistoryMeta | undefined>;
1228
}
1329

1430
interface UseNoteHistoryComposableOptions {
1531
/**
1632
* Id of the note
1733
*/
1834
noteId: MaybeRefOrGetter<NoteHistoryRecord['noteId'] | null>;
35+
36+
/**
37+
* Id of the history record
38+
*/
39+
historyId?: Ref<NoteHistoryRecord['id'] | null>;
1940
}
2041

2142
export default function useNoteHistory(options: UseNoteHistoryComposableOptions): UseNoteHistoryComposableState {
43+
/**
44+
* Array of the note history metadata
45+
* Used fot presentation of the note history
46+
*/
2247
const noteHistory = ref<NoteHistoryMeta[] | null>(null);
2348

49+
/**
50+
* Content of the current note history record
51+
* Used for the presentation of certain history record
52+
*/
53+
const historyContent = ref<NoteHistoryRecord['content'] | undefined>(undefined);
54+
55+
/**
56+
* Note tools used in current note history content
57+
* Used for the content displaying in editor
58+
*/
59+
const historyTools = ref<NoteHistoryRecord['tools'] | undefined>(undefined);
60+
61+
/**
62+
* Meta data of the note history record
63+
* Used fot informative presnetation of the note history record
64+
*/
65+
const historyMeta = ref<NoteHistoryMeta | undefined>(undefined);
66+
2467
const currentNoteId = computed(() => toValue(options.noteId));
68+
const currentHistoryId = computed(() => toValue(options.historyId));
2569

70+
/**
71+
* Loads full note history meta for certain note
72+
* @param noteId - id of the note with history
73+
*/
2674
async function loadNoteHistory(noteId: Note['id']): Promise<void> {
2775
noteHistory.value = await noteHistoryService.loadNoteHistory(noteId);
2876
}
2977

78+
/**
79+
* Get full note history record with user info
80+
* @param noteId - id of the note with history
81+
* @param historyId - id of the history record
82+
* @returns - full note history record with user info
83+
*/
84+
async function loadNoteHistoryRecord(noteId: Note['id'], historyId: NoteHistoryRecord['id']): Promise<void> {
85+
const historyRecord = await noteHistoryService.getNoteHistoryRecordById(noteId, historyId);
86+
87+
historyContent.value = historyRecord.content;
88+
historyTools.value = historyRecord.tools;
89+
historyMeta.value = {
90+
id: historyRecord.id,
91+
userId: historyRecord.userId,
92+
createdAt: historyRecord.createdAt,
93+
user: historyRecord.user,
94+
};
95+
}
96+
3097
/**
3198
* When page is mounted, we should load note history
3299
*/
33-
onMounted(() => {
100+
onMounted(async () => {
34101
if (currentNoteId.value !== null) {
35-
void loadNoteHistory(currentNoteId.value);
102+
await loadNoteHistory(currentNoteId.value);
103+
}
104+
});
105+
106+
/**
107+
* Watch fot the history id to load new history record when it is needed
108+
*/
109+
watch(currentHistoryId, async (historyId) => {
110+
if (notEmpty(historyId) && currentNoteId.value !== null) {
111+
await loadNoteHistoryRecord(currentNoteId.value, historyId);
36112
}
113+
}, {
114+
immediate: true,
37115
});
38116

39117
return {
40-
noteHistory: noteHistory,
118+
noteHistory,
119+
historyContent,
120+
historyTools,
121+
historyMeta,
41122
};
42123
}

src/domain/entities/History.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,8 @@ export type NoteHistoryMeta = Omit<NoteHistoryRecord, 'content' | 'noteId' | 'to
5757
*/
5858
user: UserMeta;
5959
};
60+
61+
/**
62+
* Note history record with user meta data
63+
*/
64+
export type NoteHistoryView = NoteHistoryRecord & { user: UserMeta };

src/domain/noteHistory.repository.interface.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { NoteHistoryMeta } from './entities/History';
1+
import type { NoteHistoryMeta, NoteHistoryRecord, NoteHistoryView } from './entities/History';
22
import type { Note } from './entities/Note';
33

44
/**
@@ -10,4 +10,12 @@ export default interface NoteHistoryRepositoryInterface {
1010
* @param noteId - id of the note
1111
*/
1212
loadNoteHistory(noteId: Note['id']): Promise<NoteHistoryMeta[]>;
13+
14+
/**
15+
* Get full note history record with user info
16+
* @param noteId - id of the note with history
17+
* @param historyId - id of the history record
18+
* @returns - full note history record with user info
19+
*/
20+
getNoteHistoryRecordById(noteId: Note['id'], historyId: NoteHistoryRecord['id']): Promise<NoteHistoryView>;
1321
}

src/domain/noteHistory.service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { NoteHistoryMeta } from './entities/History';
1+
import type { NoteHistoryMeta, NoteHistoryRecord, NoteHistoryView } from './entities/History';
22
import type { Note } from './entities/Note';
33
import type NoteHistoryRepository from './noteHistory.repository.interface';
44

@@ -23,4 +23,14 @@ export default class NoteHistoryService {
2323
public async loadNoteHistory(noteId: Note['id']): Promise<NoteHistoryMeta[]> {
2424
return await this.noteHistoryRepository.loadNoteHistory(noteId);
2525
}
26+
27+
/**
28+
* Get full note history record with user info
29+
* @param noteId - id of the note with history
30+
* @param historyId - id of the history record
31+
* @returns - full note history record with user info
32+
*/
33+
public async getNoteHistoryRecordById(noteId: Note['id'], historyId: NoteHistoryRecord['id']): Promise<NoteHistoryView> {
34+
return await this.noteHistoryRepository.getNoteHistoryRecordById(noteId, historyId);
35+
}
2636
}

src/infrastructure/noteHistory.repository.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type NoteHistoryRepositoryInterface from '@/domain/noteHistory.repository.interface';
22
import type NotesApiTransport from './transport/notes-api';
33
import type { Note } from '@/domain/entities/Note';
4-
import type { NoteHistoryMeta } from '@/domain/entities/History';
4+
import type { NoteHistoryMeta, NoteHistoryRecord, NoteHistoryView } from '@/domain/entities/History';
55

66
/**
77
* Note history repository class used for data delivery from transport to service
@@ -25,4 +25,16 @@ export default class NoteHistoryRepository implements NoteHistoryRepositoryInter
2525

2626
return response.noteHistoryMeta;
2727
}
28+
29+
/**
30+
* Get full note history record with user info
31+
* @param noteId - id of the note with history
32+
* @param historyId - id of the history record
33+
* @returns - full note history record with user info
34+
*/
35+
public async getNoteHistoryRecordById(noteId: Note['id'], historyId: NoteHistoryRecord['id']): Promise<NoteHistoryView> {
36+
const response = await this.transport.get<{ noteHistoryRecord: NoteHistoryView }>(`/note/${noteId}/history/${historyId}`);
37+
38+
return response.noteHistoryRecord;
39+
}
2840
}

src/presentation/components/note-header/NoteHeader.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
&__right {
3131
display: flex;
3232
gap: var(--spacing-s);
33+
align-items: center;
3334
}
3435
}
3536
</style>

src/presentation/pages/AuthorizationPage.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<Button
55
@click="showGoogleAuthPopup"
66
>
7-
{{ t('auth.login') }}
7+
{{ t('auth.login') }}
88
</Button>
99
</div>
1010
</template>

src/presentation/pages/History.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
:subtitle="parseDate(new Date(historyRecord.createdAt))"
2828
:has-delimiter="noteHistory !== null && index !== noteHistory?.length - 1"
2929
:class="$style['history-items__row']"
30+
@click="router.push(`/note/${props.noteId}/history/${historyRecord.id}`)"
3031
>
3132
<template #left>
3233
<Avatar
@@ -59,7 +60,7 @@ import { parseDate } from '@/infrastructure/utils/date';
5960
import { watch } from 'vue';
6061
import { useI18n } from 'vue-i18n';
6162
import type { NoteId } from '@/domain/entities/Note';
62-
import { useRoute } from 'vue-router';
63+
import { useRoute, useRouter } from 'vue-router';
6364
6465
const props = defineProps<{
6566
/**
@@ -74,14 +75,15 @@ const { noteHistory } = useNoteHistory({ noteId: props.noteId });
7475
const { patchOpenedPageByUrl } = useHeader();
7576
7677
const route = useRoute();
78+
const router = useRouter();
7779
7880
const { noteTitle } = useNote({ id: props.noteId });
7981
8082
watch(noteTitle, (currentNoteTitle) => {
8183
patchOpenedPageByUrl(
8284
route.path,
8385
{
84-
title: `Version hisotry (${currentNoteTitle})`,
86+
title: `Version history (${currentNoteTitle})`,
8587
url: route.path,
8688
});
8789
});
@@ -120,6 +122,7 @@ watch(noteTitle, (currentNoteTitle) => {
120122
display: flex;
121123
flex-direction: column;
122124
align-items: flex-start;
125+
cursor: pointer;
123126
124127
&__row {
125128
align-self: stretch;

0 commit comments

Comments
 (0)