-
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement client-side doc versioning
- Loading branch information
1 parent
78b9214
commit f2e702e
Showing
16 changed files
with
542 additions
and
242 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
<template> | ||
<CoreScrollable class="meta p-4 md:p-2"> | ||
<div class="flex flex-col flex-grow"> | ||
<CoreLink v-if="hasHistory" :to="{ path: `/docs/${doc.id}/versions` }" class="sidebar-link w-full"> | ||
<HistoryIcon class="w-5" /> | ||
<span class="ml-6 md:ml-3 flex-grow text-left">History ({{ docVersions.length }})</span> | ||
</CoreLink> | ||
<div v-if="docVersion" class="flex flex-col flex-grow"> | ||
<CoreLink @click="restoreDocVersion" :to="{ path: `/docs/${doc.id}` }" class="sidebar-link w-full"> | ||
<HistoryIcon class="w-5" /> | ||
<span class="ml-6 md:ml-3 flex-grow text-left">Restore Version</span> | ||
</CoreLink> | ||
</div> | ||
<div v-else-if="doc" class="flex flex-col flex-grow"> | ||
<div> | ||
<button @click.stop="duplicateDocument" class="sidebar-link w-full"> | ||
<DuplicateIcon class="w-5" /> | ||
<span class="ml-6 md:ml-3 flex-grow text-left">Duplicate</span> | ||
</button> | ||
<DiscardableAction v-if="doc.id" :discardedAt="doc.discardedAt" :onDiscard="discardDocument" :onRestore="restoreDocument" class="sidebar-link w-full"></DiscardableAction> | ||
<button v-if="hasCodeblocks" @click="openSandbox" class="sidebar-link w-full"> | ||
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /> | ||
</svg> | ||
<span class="ml-6 md:ml-3 flex-grow text-left">Create Sandbox</span> | ||
</button> | ||
<div> | ||
<div v-if="doc.public"> | ||
<button @click="restrictDocument" class="sidebar-link w-full"> | ||
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
<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" /> | ||
</svg> | ||
<span class="ml-6 md:ml-3 flex-grow text-left">Make Private</span> | ||
</button> | ||
<button @click="copyPublicUrl" class="sidebar-link w-full"> | ||
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
<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" /> | ||
</svg> | ||
<span class="ml-6 md:ml-3 flex-grow text-left">Copy Link</span> | ||
</button> | ||
<input ref="link" :value="publicUrl" type="text" class="form-text w-full mb-2" readonly data-test-public-url> | ||
</div> | ||
<div v-else class="mb-2"> | ||
<button @click="shareDocument" class="sidebar-link w-full" data-test-share-doc> | ||
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
<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" /> | ||
</svg> | ||
<span class="ml-6 md:ml-3 flex-grow text-left">Make Public</span> | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
<div class="mt-4"> | ||
<TagLink v-for="tag in doc.tags" :key="tag" :tag="tag" class="sidebar-link" /> | ||
</div> | ||
<div class="mt-4"> | ||
<DocLink v-for="reference in references" :key="reference.id" :doc="reference" class="sidebar-link" /> | ||
</div> | ||
<div class="mt-4"> | ||
<div v-for="task in doc.tasks" class="flex items-center px-3 py-2 my-1 md:px-2 md:py-1"> | ||
<svg height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | ||
</svg> | ||
<span class="flex-grow overflow-hidden truncate ml-3">{{ task }}</span> | ||
</div> | ||
</div> | ||
<div class="flex flex-col justify-end flex-grow px-3 md:p-2 mt-4 mb-3 md:mb-1"> | ||
<div v-if="doc.updatedAt"> | ||
<small class="text-gray-700">Last Saved</small> | ||
<div class="capitalize pt-2 md:pt-1">{{ savedAt }}</div> | ||
</div> | ||
<div v-if="doc.createdAt" class="mt-3 md:mt-2"> | ||
<small class="text-gray-700">Created</small> | ||
<div class="pt-2 md:pt-1">{{ createdAt }}</div> | ||
</div> | ||
<div v-if="doc.updatedAt" class="mt-3 md:mt-2"> | ||
<small class="text-gray-700">Updated</small> | ||
<div class="pt-2 md:pt-1">{{ updatedAt }}</div> | ||
</div> | ||
<div v-if="doc.discardedAt" class="mt-3 md:mt-2"> | ||
<small class="text-gray-700">Discarded</small> | ||
<div class="pt-2 md:pt-1">{{ discardedAt }}</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</CoreScrollable> | ||
</template> | ||
|
||
<script> | ||
import { TrashIcon as DiscardIcon, DocumentDuplicateIcon as DuplicateIcon, ClockIcon as HistoryIcon, LockClosedIcon as PrivateIcon, LockOpenIcon as PublicIcon } from '@heroicons/vue/24/outline' | ||
import moment from 'moment' | ||
import { useStore } from 'vuex' | ||
import DiscardableAction from '#root/components/DiscardableAction.vue' | ||
import DocLink from '#root/components/DocLink.vue' | ||
import TagLink from '#root/components/TagLink.vue' | ||
import CodeSandbox from '#root/src/common/code_sandbox' | ||
import { parseCodeblocks, parseReferences } from '#root/src/common/parsers' | ||
import Doc from '#root/src/models/doc' | ||
import { | ||
DISCARD_DOCUMENT, | ||
DUPLICATE_DOCUMENT, | ||
RESTORE_DOCUMENT, | ||
RESTRICT_DOCUMENT, | ||
SHARE_DOCUMENT, | ||
SET_RIGHT_SIDEBAR_VISIBILITY, | ||
} from '#root/src/store/actions' | ||
export default { | ||
components: { | ||
DiscardIcon, | ||
DiscardableAction, | ||
DocLink, | ||
DuplicateIcon, | ||
HistoryIcon, | ||
PrivateIcon, | ||
PublicIcon, | ||
TagLink, | ||
}, | ||
setup() { | ||
const store = useStore() | ||
const { doc } = useDocs() | ||
const { docVersion, docVersions } = useDocVersions(doc) | ||
const hasHistory = computed(() => docVersions.value.length > 0) | ||
const restoreDocVersion = () => { | ||
store.commit('EDIT_DOCUMENT', new Doc({ ...doc.value, text: docVersion.value.text })) | ||
} | ||
return { | ||
docVersion, | ||
docVersions, | ||
hasHistory, | ||
restoreDocVersion, | ||
} | ||
}, | ||
data() { | ||
return { | ||
now: moment(), | ||
ticker: null, | ||
} | ||
}, | ||
computed: { | ||
codeblocks() { | ||
return parseCodeblocks(this.doc.text) | ||
}, | ||
createdAt() { | ||
if (this.$route.params.docId) { | ||
return moment(this.doc.createdAt).format('ddd, MMM Do, YYYY [at] h:mm A') | ||
} | ||
return 'Not yet created' | ||
}, | ||
discardedAt() { | ||
return moment(this.doc.discardedAt).format('ddd, MMM Do, YYYY [at] h:mm A') | ||
}, | ||
doc() { | ||
return this.$store.getters.decrypted.find((doc) => doc.id === this.$route.params.docId) | ||
}, | ||
hasCodeblocks() { | ||
return this.codeblocks.length > 0 | ||
}, | ||
publicUrl() { | ||
const path = this.$router.resolve({ path: `/public/${this.doc.id}` }).href | ||
return `${location.protocol}//${location.host}${path}` | ||
}, | ||
references() { | ||
const references = parseReferences(this.doc.text) | ||
return this.$store.getters.kept.filter((doc) => { | ||
return references.includes(doc.id) | ||
}) | ||
}, | ||
savedAt() { | ||
if (this.$route.params.docId) { | ||
if (this.now.diff(this.doc.updatedAt, 'seconds') < 5) { | ||
return 'just now' | ||
} | ||
else { | ||
return `${moment(this.doc.updatedAt).from(this.now, true)} ago` | ||
} | ||
} | ||
return 'Not yet saved' | ||
}, | ||
updatedAt() { | ||
if (this.$route.params.docId) { | ||
return moment(this.doc.updatedAt).format('ddd, MMM Do, YYYY [at] h:mm A') | ||
} | ||
return 'Not yet updated' | ||
}, | ||
}, | ||
methods: { | ||
async copyPublicUrl() { | ||
// copy link to clipboard | ||
this.$refs.link.select() | ||
document.execCommand('copy') | ||
}, | ||
async discardDocument() { | ||
this.$store.dispatch(DISCARD_DOCUMENT, { id: this.doc.id }) | ||
this.$router.push({ path: '/docs/new' }) | ||
}, | ||
async duplicateDocument() { | ||
const newDocId = await this.$store.dispatch(DUPLICATE_DOCUMENT, { id: this.doc.id }) | ||
this.$router.push({ path: `/docs/${newDocId}` }) | ||
}, | ||
async openSandbox() { | ||
const files = this.codeblocks.reduce((agg, codeblock, index) => { | ||
const filename = codeblock.filename || [index, (codeblock.language || 'txt')].join('.') | ||
return { | ||
...agg, | ||
[filename]: { | ||
content: codeblock.code, | ||
}, | ||
} | ||
}, {}) | ||
CodeSandbox.create(files).then(sandbox_id => CodeSandbox.open(sandbox_id)) | ||
}, | ||
async restoreDocument() { | ||
this.$store.dispatch(RESTORE_DOCUMENT, { id: this.doc.id }) | ||
}, | ||
async restrictDocument() { | ||
this.$store.dispatch(RESTRICT_DOCUMENT, { id: this.doc.id }) | ||
}, | ||
async shareDocument() { | ||
this.$store.dispatch(SHARE_DOCUMENT, { id: this.doc.id }) | ||
}, | ||
async toggleMeta() { | ||
this.$store.dispatch(SET_RIGHT_SIDEBAR_VISIBILITY, !this.$store.state.showRightSidebar) | ||
}, | ||
}, | ||
async beforeUnmount() { | ||
clearInterval(this.ticker) | ||
}, | ||
async mounted() { | ||
this.mounted = true | ||
this.ticker = setInterval(() => { | ||
this.now = moment() | ||
}, 5000) | ||
}, | ||
} | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
export * from './useAppearance' | ||
export * from './useAuth' | ||
export * from './useDatabase' | ||
export * from './useLayout' | ||
export * from './usePinnedDocs' | ||
export * from './useTiers' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { tryOnScopeDispose } from '@vueuse/core' | ||
import { liveQuery } from 'dexie' | ||
import { type Observable, type Subscription } from 'rxjs' | ||
import { type Ref, type UnwrapRef, ref } from 'vue' | ||
import { db } from '#root/src/database' | ||
|
||
export const useDatabase = () => { | ||
const observe = <T>(callback: () => T) => { | ||
return useObservable<T>(liveQuery<T>(callback) as any) | ||
} | ||
|
||
return { | ||
db, | ||
observe, | ||
} | ||
} | ||
|
||
type QueryReturnType<T, I = undefined> = { result: Ref<T | I> } | ||
|
||
export function useQuery<T>(callback: () => Promise<T>): QueryReturnType<T> | ||
export function useQuery<T>(callback: () => Promise<T>, initialValue: T): QueryReturnType<T, T> | ||
export function useQuery<T>(callback: () => Promise<T>, initialValue?: T) { | ||
const result = initialValue ? ref<T>(initialValue) : ref<T>() | ||
const subscription = ref<Subscription>() | ||
|
||
watch(callback, () => { | ||
const observable = liveQuery<T>(callback) | ||
|
||
subscription.value?.unsubscribe() | ||
subscription.value = observable.subscribe({ | ||
next: (value) => { | ||
result.value = value | ||
}, | ||
error: (error) => { | ||
console.error(error) | ||
}, | ||
}) as any | ||
}, { immediate: true }) | ||
|
||
tryOnScopeDispose(() => subscription.value?.unsubscribe()) | ||
|
||
return { | ||
result, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export const useDocVersions = () => { | ||
const router = useRouter() | ||
const { db } = useDatabase() | ||
const { doc } = useDocs() | ||
// Todo: Sort versions. | ||
const { result: docVersions } = useQuery(() => db.docVersions.where({ docId: doc.value?.id }).reverse().sortBy('updatedAt'), []) | ||
const docVersion = computed(() => docVersions.value.find(version => version.id === router.currentRoute.value.params.versionId)) | ||
|
||
return { | ||
docVersion, | ||
docVersions, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.