Skip to content
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

Merged
merged 1 commit into from
Jul 29, 2024

Conversation

andresgutgon
Copy link
Contributor

@andresgutgon andresgutgon commented Jul 25, 2024

What

☝️ That

TODO

  • Add file IMPLEMENT server action
  • When creating a file reseting tmpFolders zustand store
  • Implement destroyOrSoftDeleteDocuments (used by both destroy document or destroy folder)
  • Destroy document server action
  • DRY File node display in File tree

Next PR

  • Destroy folder server action
  • Resizable sidebar
  • Make the sidebar files tree vertically scrollable. Custom scrollbar styles
  • Sidebar header with File + | Folder +
  • Review constraint in document version by commit and path. Now that documents can be soft deleted it will be possible to have same path in 2 different docs

@andresgutgon andresgutgon added the 🚧 wip Work in progress label Jul 25, 2024
@@ -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()
Copy link
Contributor Author

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

Copy link
Contributor Author

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

@andresgutgon andresgutgon force-pushed the feature/add-files-to-sidebar-and-make-resizable branch 3 times, most recently from 38c000d to 7a41443 Compare July 26, 2024 13:14
<div className='flex flex-row gap-4 w-full items-center'>
inputs.map((param, idx) => (
<div
key={idx}
Copy link
Contributor Author

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

@andresgutgon andresgutgon force-pushed the feature/add-files-to-sidebar-and-make-resizable branch from 7a41443 to 0a34227 Compare July 27, 2024 12:08
@@ -54,20 +54,28 @@ export const getDocumentByUuid = cache(
documentUuid: string
commit: Commit
}) => {
const workspace = await findWorkspaceFromCommit(commit)
Copy link
Contributor Author

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()
Copy link
Contributor Author

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 })
Copy link
Contributor Author

@andresgutgon andresgutgon Jul 27, 2024

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],
Copy link
Contributor Author

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

Copy link
Contributor Author

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 () => {
Copy link
Contributor Author

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),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we use scopes we always have to use the scope for picking the columns of the table. Otherwise it doesn't work

image

Copy link
Collaborator

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 }) {
Copy link
Contributor Author

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)
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor Author

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))
}

/**
Copy link
Contributor Author

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
Copy link
Contributor Author

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>
)
})
Copy link
Contributor Author

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

@andresgutgon andresgutgon changed the title Save documents in DB from sidebar and make sidebar resizable Create and Delete documents (not folders yet) in the sidebar Jul 27, 2024
@andresgutgon andresgutgon removed the 🚧 wip Work in progress label Jul 27, 2024
@andresgutgon andresgutgon force-pushed the feature/add-files-to-sidebar-and-make-resizable branch from c4cc0b5 to eea2fb9 Compare July 27, 2024 15:25
@andresgutgon andresgutgon requested review from csansoon and geclos July 27, 2024 15:36
@andresgutgon andresgutgon force-pushed the feature/add-files-to-sidebar-and-make-resizable branch from eea2fb9 to 1d03f61 Compare July 27, 2024 16:04
await database
.update(documentVersions)
.set({
resolvedContent: '[CHACHED] Doc 1 (version 1)',
Copy link
Contributor

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 }
Copy link
Collaborator

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.

Copy link
Contributor Author

@andresgutgon andresgutgon Jul 29, 2024

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 }
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fantastic

resolvedContent: 'VERSION_1',
})
})
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Comment on lines +147 to +152
await Promise.all([
hardDestroyDocuments({ documents, existingUuids, tx }),
createDocumentsAsSoftDeleted({ toBeSoftDeleted, commitId, tx }),
updateDocumetsAsSoftDeleted({ toBeSoftDeleted, commitId, tx }),
invalidateDocumentsCacheInCommit(commitId, tx),
])
Copy link
Contributor

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.

Copy link
Collaborator

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
Copy link
Collaborator

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

Comment on lines +21 to +27
export async function markAsSoftDelete(documentUuid: string, tx = database) {
return tx
.update(documentVersions)
.set({ deletedAt: new Date() })
.where(eq(documentVersions.documentUuid, documentUuid))
}

Copy link
Contributor

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 (?)

Comment on lines +84 to +95
// 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])
Copy link
Contributor

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 () => {
Copy link
Collaborator

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

@geclos geclos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome

@andresgutgon andresgutgon merged commit 1477638 into main Jul 29, 2024
2 checks passed
@andresgutgon andresgutgon deleted the feature/add-files-to-sidebar-and-make-resizable branch July 29, 2024 07:58
andresgutgon added a commit that referenced this pull request Jul 29, 2024
andresgutgon added a commit that referenced this pull request Jul 29, 2024
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants