-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create and Delete documents (not folders yet) in the sidebar #60
Create and Delete documents (not folders yet) in the sidebar #60
Conversation
@@ -3,6 +3,9 @@ import { describe, expect, it } from 'vitest' | |||
|
|||
import { mergeCommit } from '../commits/merge' | |||
import { createNewDocument } from './create' | |||
import useTestDatabase from '$core/tests/useTestDatabase' | |||
|
|||
useTestDatabase() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not happy with this. I'm not sure the rollback is working on each test. We need to investigate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we should NOT have apps/web/src/components
ever. All the UI lives in packages/web-ui
. What's in apps/web
is the glue to use server actions + SWR stores. Nothing else. NO TAILWIND allowed in apps/web
38c000d
to
7a41443
Compare
<div className='flex flex-row gap-4 w-full items-center'> | ||
inputs.map((param, idx) => ( | ||
<div | ||
key={idx} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Always put your keys in react loops kids
7a41443
to
0a34227
Compare
@@ -54,20 +54,28 @@ export const getDocumentByUuid = cache( | |||
documentUuid: string | |||
commit: Commit | |||
}) => { | |||
const workspace = await findWorkspaceFromCommit(commit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This pattern for finding the workspace on commit can be solved in a more generic way when returning from a repository .find
method. Instead of returning just the model we return the model with their relations. At least some of then.
What we're doing now is doing 1 more SQL query to get again the Workspace when we already got it to build the scope for that repository. I think this is doable. I might try in another PR.
if (result.error) { | ||
const error = result.error | ||
if (error instanceof NotFoundError) { | ||
return notFound() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When is a not found we don't raise the error. We use NextJS notFound()
method that display the not-found.tsx
page
@@ -23,7 +23,7 @@ export async function GET( | |||
const commit = await commitsScope | |||
.getCommitByUuid({ uuid: commitUuid, project }) | |||
.then((r) => r.unwrap()) | |||
const documents = await scope.getDocumentsAtCommit(commit) | |||
const documents = await scope.getDocumentsAtCommit({ commit }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We had a discussion offline about using or not positional arguments. The conclusion we arrive at is that we hate it. No, I'm joking (kind of).
The conclusion was that we're going to have methods with positional arguments when we're sure this method will never have more arguments. This is an exception because I did this refactor before arriving at that conclusion and I'm sick of having this PR open. Feel free to change this one if you want.
const { project } = useCurrentProject() | ||
const { commit } = useCurrentCommit() | ||
const { mutate, data, ...rest } = useSWR<DocumentVersion[]>( | ||
['documentVersions', project.id, commit.id], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's agree on using Arrays for keys in the stores. URL like strings are harder to manipulate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huge refactoring of these tests. Please don't touch this file before I merge this PR 🙏
Resolving conflicts was taught
expect(documents[0]!.content).toBe('VERSION_2') | ||
}) | ||
|
||
it('get docs from version 3', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before all these case was in a single test with all the initialization factories. Super hard to understand. Always try to put between 1 and 3 expectations by it
block. Ideally one.
eq(documentVersions.commitId, commit.id), | ||
eq(documentVersions.documentUuid, documentUuid), | ||
eq(this.scope.commitId, commit.id), | ||
eq(this.scope.documentUuid, documentUuid), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯 good catch
@@ -145,6 +144,32 @@ export class DocumentVersionsRepository extends Repository { | |||
return Result.ok(changedDocuments) | |||
} | |||
|
|||
private async getAllDocumentsAtCommit({ commit }: { commit: Commit }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning all documents with deleted documents included is private. The moment we want to show then to for example "Recover from trash" we can implement getAllDeletedDocumentsAtCommit
|
||
const documentsFromDraft = await this.db | ||
.select(getTableColumns(documentVersions)) | ||
.from(documentVersions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrong. We should use this.scope
. We'll fix in another PR
commit: { | ||
projectId: number | ||
title?: string | ||
mergedAt?: Date |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to be explicit about what means creating an object. This way people using don't include not permitted columns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
}: { | ||
document: DocumentVersion | ||
commit: Commit | ||
workspaceId: number |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not ideal as I commented here a commit passed here was found with a repository so the workspace was already there.
There has to be a nice way of having commit.workspace.id
so object includes relations already resolved
.where(eq(documentVersions.commitId, commitId)) | ||
} | ||
|
||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks to Carlos for helping me with this. A lot is going on here. I tried my best to make as understandable as possible and added tests
document: DocumentVersion | ||
path?: string | ||
content?: string | null | ||
deletedAt?: Date | null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed deletedAt
from here now that we specific services for deleting documents.
{children} | ||
</div> | ||
) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now ☝️ a document and a folder share all the presentation layers and they only pass the necessary props. This way design is unified. This was a leftover refactor I had pending from initial file tree implementation
c4cc0b5
to
eea2fb9
Compare
eea2fb9
to
1d03f61
Compare
await database | ||
.update(documentVersions) | ||
.set({ | ||
resolvedContent: '[CHACHED] Doc 1 (version 1)', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chached
[executeDestroyDocument, mutate, data], | ||
) | ||
|
||
return { ...rest, documents: data ?? [], createFile, destroyFile, mutate } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor nitpicks: I'd prefer to keep it consistent between stores and always return data instead of a different-named prop that contains the data for each store. Also no need for the suffix "file" in actions. create and destroy is enough and beautifully concise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll have destroyFolder
in this store
) | ||
|
||
return { ...rest, documents: data ?? [], createFile, destroyFile, mutate } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fantastic
resolvedContent: 'VERSION_1', | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
await Promise.all([ | ||
hardDestroyDocuments({ documents, existingUuids, tx }), | ||
createDocumentsAsSoftDeleted({ toBeSoftDeleted, commitId, tx }), | ||
updateDocumetsAsSoftDeleted({ toBeSoftDeleted, commitId, tx }), | ||
invalidateDocumentsCacheInCommit(commitId, tx), | ||
]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is it you're calling both createDocumentsAsSoftDeleted
and updateDocumentsAsSoftDeleted
with the same array?
I see that you're filtering them inside each method, but I think it should be more clear to filter it here instead. The create…
and update…
services just receive instructions, and if they fail that's responsibility of the caller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Also the array can be conditionally computed since some of the methods do not require it.
db = database, | ||
}: { | ||
commit: Omit<Partial<Commit>, 'id'> | ||
commit: { | ||
projectId: number |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leave a todo this should be a project instead of projectid
export async function markAsSoftDelete(documentUuid: string, tx = database) { | ||
return tx | ||
.update(documentVersions) | ||
.set({ deletedAt: new Date() }) | ||
.where(eq(documentVersions.documentUuid, documentUuid)) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a commitId, otherwise you're marking it as deleted in the whole file history. A document should not be created being deleted (?)
// Litle trick to focus the input after the component is mounted | ||
// We wait some time to focus the input to avoid the focus being stolen | ||
// by the click event in the menu item that created this node. | ||
useEffect(() => { | ||
const timeout = setTimeout(() => { | ||
inputRef.current?.focus(), 100 | ||
}) | ||
|
||
return () => { | ||
clearTimeout(timeout) | ||
} | ||
}, [inputRef]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could probably use 1
or even 0
ms and it should still work, right?
import { destroyOrSoftDeleteDocuments } from './destroyOrSoftDeleteDocuments' | ||
|
||
describe('destroyOrSoftDeleteDocuments', () => { | ||
it('hardDestroyDocuments', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we make the titles self explanatory? why does it hard destroy in this test case but it creates in the next test case? It would be awesome if i could know the rule by just reading this line
* A document can: | ||
* | ||
* 1. Not exists in previous commits. In this case, it will be hard deleted | ||
* 1. Exists in previous commits and in the commit. It will be updated the `deletedAt` field |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awesome
* One more refactor to remove complexity from external api on Sidebar node wrapper * On input tab create a new child tmp folder under it to make easier build a tree * Address some comments from this PR: #60
What
☝️ That
TODO
destroyOrSoftDeleteDocuments
(used by both destroy document or destroy folder)Next PR