Skip to content

Commit 0687d4c

Browse files
committed
wip
1 parent b5a3471 commit 0687d4c

File tree

17 files changed

+547
-242
lines changed

17 files changed

+547
-242
lines changed

auto-imports.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ declare global {
178178
const useGamepad: typeof import('@vueuse/core')['useGamepad']
179179
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
180180
const useHead: typeof import('@vueuse/head')['useHead']
181+
const useHistory: typeof import('./composables/useHistory')['useHistory']
181182
const useHooks: typeof import('./composables/useHooks')['useHooks']
182183
const useIdle: typeof import('@vueuse/core')['useIdle']
183184
const useImage: typeof import('@vueuse/core')['useImage']
@@ -478,6 +479,7 @@ declare module 'vue' {
478479
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
479480
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
480481
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
482+
readonly useHistory: UnwrapRef<typeof import('./composables/useHistory')['useHistory']>
481483
readonly useHooks: UnwrapRef<typeof import('./composables/useHooks')['useHooks']>
482484
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
483485
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>

components/DocMeta.vue

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
<template>
2+
<CoreScrollable class="meta p-4 md:p-2">
3+
<div class="flex flex-col flex-grow">
4+
<CoreLink v-if="hasHistory" :to="{ path: `/docs/${doc.id}/versions` }" class="sidebar-link w-full">
5+
<HistoryIcon class="w-5" />
6+
<span class="ml-6 md:ml-3 flex-grow text-left">History ({{ docVersions.length }})</span>
7+
</CoreLink>
8+
<div v-if="docVersion" class="flex flex-col flex-grow">
9+
<CoreLink @click="restoreDocVersion" :to="{ path: `/docs/${doc.id}` }" class="sidebar-link w-full">
10+
<HistoryIcon class="w-5" />
11+
<span class="ml-6 md:ml-3 flex-grow text-left">Restore Version</span>
12+
</CoreLink>
13+
</div>
14+
<div v-else-if="doc" class="flex flex-col flex-grow">
15+
<div>
16+
<button @click.stop="duplicateDocument" class="sidebar-link w-full">
17+
<DuplicateIcon class="w-5" />
18+
<span class="ml-6 md:ml-3 flex-grow text-left">Duplicate</span>
19+
</button>
20+
<DiscardableAction v-if="doc.id" :discardedAt="doc.discardedAt" :onDiscard="discardDocument" :onRestore="restoreDocument" class="sidebar-link w-full"></DiscardableAction>
21+
<button v-if="hasCodeblocks" @click="openSandbox" class="sidebar-link w-full">
22+
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
23+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
24+
</svg>
25+
<span class="ml-6 md:ml-3 flex-grow text-left">Create Sandbox</span>
26+
</button>
27+
<div>
28+
<div v-if="doc.public">
29+
<button @click="restrictDocument" class="sidebar-link w-full">
30+
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
31+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
32+
</svg>
33+
<span class="ml-6 md:ml-3 flex-grow text-left">Make Private</span>
34+
</button>
35+
<button @click="copyPublicUrl" class="sidebar-link w-full">
36+
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
37+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
38+
</svg>
39+
<span class="ml-6 md:ml-3 flex-grow text-left">Copy Link</span>
40+
</button>
41+
<input ref="link" :value="publicUrl" type="text" class="form-text w-full mb-2" readonly data-test-public-url>
42+
</div>
43+
<div v-else class="mb-2">
44+
<button @click="shareDocument" class="sidebar-link w-full" data-test-share-doc>
45+
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" />
47+
</svg>
48+
<span class="ml-6 md:ml-3 flex-grow text-left">Make Public</span>
49+
</button>
50+
</div>
51+
</div>
52+
</div>
53+
<div class="mt-4">
54+
<TagLink v-for="tag in doc.tags" :key="tag" :tag="tag" class="sidebar-link" />
55+
</div>
56+
<div class="mt-4">
57+
<DocLink v-for="reference in references" :key="reference.id" :doc="reference" class="sidebar-link" />
58+
</div>
59+
<div class="mt-4">
60+
<div v-for="task in doc.tasks" class="flex items-center px-3 py-2 my-1 md:px-2 md:py-1">
61+
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
62+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
63+
</svg>
64+
<span class="flex-grow overflow-hidden truncate ml-3">{{ task }}</span>
65+
</div>
66+
</div>
67+
<div class="flex flex-col justify-end flex-grow px-3 md:p-2 mt-4 mb-3 md:mb-1">
68+
<div v-if="doc.updatedAt">
69+
<small class="text-gray-700">Last Saved</small>
70+
<div class="capitalize pt-2 md:pt-1">{{ savedAt }}</div>
71+
</div>
72+
<div v-if="doc.createdAt" class="mt-3 md:mt-2">
73+
<small class="text-gray-700">Created</small>
74+
<div class="pt-2 md:pt-1">{{ createdAt }}</div>
75+
</div>
76+
<div v-if="doc.updatedAt" class="mt-3 md:mt-2">
77+
<small class="text-gray-700">Updated</small>
78+
<div class="pt-2 md:pt-1">{{ updatedAt }}</div>
79+
</div>
80+
<div v-if="doc.discardedAt" class="mt-3 md:mt-2">
81+
<small class="text-gray-700">Discarded</small>
82+
<div class="pt-2 md:pt-1">{{ discardedAt }}</div>
83+
</div>
84+
</div>
85+
</div>
86+
</div>
87+
</CoreScrollable>
88+
</template>
89+
90+
<script>
91+
import { TrashIcon as DiscardIcon, DocumentDuplicateIcon as DuplicateIcon, ClockIcon as HistoryIcon, LockClosedIcon as PrivateIcon, LockOpenIcon as PublicIcon } from '@heroicons/vue/24/outline'
92+
import moment from 'moment'
93+
import { useStore } from 'vuex'
94+
import DiscardableAction from '/components/DiscardableAction.vue'
95+
import DocLink from '/components/DocLink.vue'
96+
import TagLink from '/components/TagLink.vue'
97+
import CodeSandbox from '/src/common/code_sandbox.js'
98+
import { parseCodeblocks, parseReferences } from '/src/common/parsers'
99+
import Doc from '/src/models/doc'
100+
import { open } from '/src/router.js'
101+
102+
import {
103+
DISCARD_DOCUMENT,
104+
DUPLICATE_DOCUMENT,
105+
RESTORE_DOCUMENT,
106+
RESTRICT_DOCUMENT,
107+
SHARE_DOCUMENT,
108+
SET_RIGHT_SIDEBAR_VISIBILITY,
109+
} from '/src/store/actions.js'
110+
111+
export default {
112+
components: {
113+
DiscardIcon,
114+
DiscardableAction,
115+
DocLink,
116+
DuplicateIcon,
117+
HistoryIcon,
118+
PrivateIcon,
119+
PublicIcon,
120+
TagLink,
121+
},
122+
setup() {
123+
const store = useStore()
124+
const { doc } = useDocs()
125+
const { docVersion, docVersions } = useDocVersions(doc)
126+
const hasHistory = computed(() => docVersions.value.length > 0)
127+
128+
const restoreDocVersion = () => {
129+
store.commit('EDIT_DOCUMENT', new Doc({ ...doc.value, text: docVersion.value.text }))
130+
}
131+
132+
return {
133+
docVersion,
134+
docVersions,
135+
hasHistory,
136+
restoreDocVersion,
137+
}
138+
},
139+
data() {
140+
return {
141+
now: moment(),
142+
ticker: null,
143+
}
144+
},
145+
computed: {
146+
codeblocks() {
147+
return parseCodeblocks(this.doc.text)
148+
},
149+
createdAt() {
150+
if (this.$route.params.docId) {
151+
return moment(this.doc.createdAt).format('ddd, MMM Do, YYYY [at] h:mm A')
152+
}
153+
154+
return 'Not yet created'
155+
},
156+
discardedAt() {
157+
return moment(this.doc.discardedAt).format('ddd, MMM Do, YYYY [at] h:mm A')
158+
},
159+
doc() {
160+
return this.$store.getters.decrypted.find((doc) => doc.id === this.$route.params.docId)
161+
},
162+
hasCodeblocks() {
163+
return this.codeblocks.length > 0
164+
},
165+
publicUrl() {
166+
const path = this.$router.resolve({ path: `/public/${this.doc.id}` }).href
167+
168+
return `${location.protocol}//${location.host}${path}`
169+
},
170+
references() {
171+
const references = parseReferences(this.doc.text)
172+
173+
return this.$store.getters.kept.filter((doc) => {
174+
return references.includes(doc.id)
175+
})
176+
},
177+
savedAt() {
178+
if (this.$route.params.docId) {
179+
if (this.now.diff(this.doc.updatedAt, 'seconds') < 5) {
180+
return 'just now'
181+
}
182+
else {
183+
return `${moment(this.doc.updatedAt).from(this.now, true)} ago`
184+
}
185+
}
186+
187+
return 'Not yet saved'
188+
},
189+
updatedAt() {
190+
if (this.$route.params.docId) {
191+
return moment(this.doc.updatedAt).format('ddd, MMM Do, YYYY [at] h:mm A')
192+
}
193+
194+
return 'Not yet updated'
195+
},
196+
},
197+
methods: {
198+
async copyPublicUrl() {
199+
// copy link to clipboard
200+
this.$refs.link.select()
201+
document.execCommand('copy')
202+
},
203+
async discardDocument() {
204+
this.$store.dispatch(DISCARD_DOCUMENT, { id: this.doc.id })
205+
206+
open({ path: '/docs/new' })
207+
},
208+
async duplicateDocument() {
209+
const newDocId = await this.$store.dispatch(DUPLICATE_DOCUMENT, { id: this.doc.id })
210+
211+
open({ path: `/docs/${newDocId}` })
212+
},
213+
async openSandbox() {
214+
const files = this.codeblocks.reduce((agg, codeblock, index) => {
215+
const filename = codeblock.filename || [index, (codeblock.language || 'txt')].join('.')
216+
217+
return {
218+
...agg,
219+
[filename]: {
220+
content: codeblock.code,
221+
},
222+
}
223+
}, {})
224+
225+
CodeSandbox.create(files).then(sandbox_id => CodeSandbox.open(sandbox_id))
226+
},
227+
async restoreDocument() {
228+
this.$store.dispatch(RESTORE_DOCUMENT, { id: this.doc.id })
229+
},
230+
async restrictDocument() {
231+
this.$store.dispatch(RESTRICT_DOCUMENT, { id: this.doc.id })
232+
},
233+
async shareDocument() {
234+
this.$store.dispatch(SHARE_DOCUMENT, { id: this.doc.id })
235+
},
236+
async toggleMeta() {
237+
this.$store.dispatch(SET_RIGHT_SIDEBAR_VISIBILITY, !this.$store.state.showRightSidebar)
238+
},
239+
},
240+
async beforeUnmount() {
241+
clearInterval(this.ticker)
242+
},
243+
async mounted() {
244+
this.mounted = true
245+
246+
this.ticker = setInterval(() => {
247+
this.now = moment()
248+
}, 5000)
249+
},
250+
}
251+
</script>

composables/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './useAppearance'
22
export * from './useAuth'
3+
export * from './useDatabase'
34
export * from './useLayout'
45
export * from './usePinnedDocs'
56
export * from './useTiers'

composables/useDatabase.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { tryOnScopeDispose } from '@vueuse/core'
2+
import { liveQuery } from 'dexie'
3+
import { type Observable, type Subscription } from 'rxjs'
4+
import { type Ref, type UnwrapRef, ref } from 'vue'
5+
import { db } from '/src/database'
6+
7+
export const useDatabase = () => {
8+
const observe = <T>(callback: () => T) => {
9+
return useObservable<T>(liveQuery<T>(callback) as any)
10+
}
11+
12+
return {
13+
db,
14+
observe,
15+
}
16+
}
17+
18+
type QueryReturnType<T, I = undefined> = { result: Ref<T | I> }
19+
20+
export function useQuery<T>(callback: () => Promise<T>): QueryReturnType<T>
21+
export function useQuery<T>(callback: () => Promise<T>, initialValue: T): QueryReturnType<T, T>
22+
export function useQuery<T>(callback: () => Promise<T>, initialValue?: T) {
23+
const result = initialValue ? ref<T>(initialValue) : ref<T>()
24+
const subscription = ref<Subscription>()
25+
26+
watch(callback, () => {
27+
const observable = liveQuery<T>(callback)
28+
29+
subscription.value?.unsubscribe()
30+
subscription.value = observable.subscribe({
31+
next: (value) => {
32+
result.value = value
33+
},
34+
error: (error) => {
35+
console.error(error)
36+
},
37+
}) as any
38+
}, { immediate: true })
39+
40+
tryOnScopeDispose(() => subscription.value?.unsubscribe())
41+
42+
return {
43+
result,
44+
}
45+
}

composables/useDocVersions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const useDocVersions = () => {
2+
const router = useRouter()
3+
const { db } = useDatabase()
4+
const { doc } = useDocs()
5+
// Todo: Sort versions.
6+
const { result: docVersions } = useQuery(() => db.docVersions.where({ docId: doc.value?.id }).reverse().sortBy('updatedAt'), [])
7+
const docVersion = computed(() => docVersions.value.find(version => version.id === router.currentRoute.value.params.versionId))
8+
9+
return {
10+
docVersion,
11+
docVersions,
12+
}
13+
}

composables/useDocs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import type Doc from '/src/models/doc'
44
export const useDocs = () => {
55
const store = useStore()
66
const router = useRouter()
7-
const docs = computed(() => store.getters.decrypted)
8-
const doc = computed(() => docs.value.find((doc: Doc) => doc.id === router.currentRoute.value.params.docId))
7+
const docs = computed<Doc[]>(() => store.getters.decrypted)
8+
const doc = computed<Doc | undefined>(() => docs.value.find((doc: Doc) => doc.id === router.currentRoute.value.params.docId))
99

1010
return {
1111
doc,

0 commit comments

Comments
 (0)