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

feat(auto-save): Auto-save on custom / page type & slice builder #1258

Merged
merged 2 commits into from
Jan 18, 2024

Conversation

xrutayisire
Copy link
Collaborator

@xrutayisire xrutayisire commented Dec 28, 2023

Context

The Solution

Auto-save for page / custom type:

  • Remove selectedCustomTypeReducer reducer
  • Remove watchSelectedCustomTypeSagas saga
  • Remove old redux sectors
  • Remove old redux actions
  • Make the custom type builder working without redux with it's own data locally
  • Support Enter key to submit a new field
  • Ensure that when renaming a tab, the existing name is already filled
  • Create a local context for custom type builder
  • Ensure you cannot add a field if the form is already displayed
  • Persist save to local custom type builder context
  • Create a custom hook useAutoSave to ensure we have the proper way to handle multiple changes
  • Create a component to display saving status
  • Create domain functions to manage custom type model
  • Add unit tests for custom type domain functions
  • Add e2e tests for custom / page type builder
  • Add option to delay response for e2e mocking procedures management

TO DO

  • Discussions with Benjamin on the design of toaster on error
  • Discussions with Benjamin for the delay on error retry

Impact / Dependencies

  • The local custom type builder will not be updated automatically on window focus or anything else

Checklist before requesting a review

  • I hereby declare my code ready for review.
  • If it is a critical feature, I have added tests.
  • The CI is successful.
  • If there could backward compatibility issues, it has been discussed and planned.

Preview

Screen.Recording.2023-12-29.at.18.53.35.mov

Copy link

vercel bot commented Dec 28, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
slice-machine ✅ Ready (Inspect) Visit Preview Jan 18, 2024 2:49pm

@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from 9e71dfa to 4c92702 Compare December 28, 2023 20:40
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from 4c92702 to b412472 Compare December 28, 2023 21:42
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from b412472 to 9a85c8a Compare December 29, 2023 15:55
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from 9a85c8a to 8aa625c Compare December 29, 2023 18:23
@xrutayisire xrutayisire marked this pull request as ready for review December 29, 2023 18:25
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from 8aa625c to 2f17bed Compare December 29, 2023 18:37
@xrutayisire xrutayisire marked this pull request as draft December 29, 2023 18:38
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from 2f17bed to eafae8c Compare December 29, 2023 18:56
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from eafae8c to 8b7d010 Compare December 29, 2023 23:29
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from 8b7d010 to 621123a Compare December 29, 2023 23:38
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from 621123a to bc680c1 Compare January 2, 2024 15:12
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from bc680c1 to c130f36 Compare January 2, 2024 18:50
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from c130f36 to cc3118f Compare January 2, 2024 19:20
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from cc3118f to 6449ea8 Compare January 3, 2024 11:50
@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from 6449ea8 to 6beab0a Compare January 3, 2024 12:22
Copy link
Collaborator

@bapmrl bapmrl left a comment

Choose a reason for hiding this comment

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

Nice useAutoSave hook 😊 Just have some questions and comments as I don't understand all the intricacies of it.

nextSave: (() => Promise<unknown>) | null;
};

export type AutoSaveStatusType = "saving" | "saved" | "error";
Copy link
Collaborator

Choose a reason for hiding this comment

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

⚠️ In my opinion, there's no need to append Type to a type.

💡 Maybe failed would be better than error? That way, you'd be using past tense like saved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I did that because I have a visual component named AutoSaveStatus that also refer to the type. So I wanted to be clear about the difference. WDYT?

Good idea for failed, I will rename it 🙂

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, what do you think about naming the component AutoSaveStatusIndicator then? 😊

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's a good idea, yes!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

Copy link
Collaborator

Choose a reason for hiding this comment

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

💡 I wonder if a new core/autoSave folder would be a better fit. Indeed, I feel like we're putting too many things into features. For me, features should ideally be "optional" parts/pages/modules and core should contain the shared domain bits that allow the features to work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh, I see. I really like it, we kind of discussed a bit that at the beginning I remember. But maybe it will also create a problem with too many first level folder in src with a dev never knowing where to put stuff?
For me, I "feel" it's ok, but I wanted to warn about that.

If we create a core folder, we could put:

  • autoSave
  • documentation
  • environments
  • inAppGuide
  • legacyState
  • slicesTemplates

And so in features we leave:

  • changes
  • cusotmTypes
  • labs
  • slices

WDYT?

Btw I guess since it's already a +3000/-3000 PR I can do that right after my PRs are merged and only do the autoSave now. So I ease the review for Hugo. Ok for you?

Copy link
Collaborator

@bapmrl bapmrl Jan 16, 2024

Choose a reason for hiding this comment

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

Okay for me, but I'm not sure about the partitioning you propose. We'll discuss it again ^^

Also, what would you think of moving all the "old" code into a separate top-level old directory? 😜

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What old code you refer to? I'm not a fan of this, then you start to have again more confusion and folders. The old folders and old code. I feel It's just more pain to maintain.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Following our discussion, will remove 2 folders after my PRs

};

type AutoSaveState = {
pendingSave: (() => Promise<unknown>) | null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

⚠️ Let's favor undefined over null for consistency.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep my bad, I prefer also to use undefined, was too fast here. Good catch, thanks 🙂
I will also add it to the best practices notion page.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

errorMessage = "An error happened while saving",
retryDelay = 2000,
retryMessage = "Retry",
} = args;
Copy link
Collaborator

Choose a reason for hiding this comment

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

💡 A line break here would improve readability.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True! 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done


export type AutoSaveStatusType = "saving" | "saved" | "error";

export const useAutoSave = (args: UseAutoSaveArgs = {}) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

⚠️ I think it's important to always define the return type of top-level functions in order to help developers quickly understand what the function does.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep good catch, I will do it, and also it's already in the best practices 🎉

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

await autoSaveState.pendingSave();

setAutoSaveStatusInternal("saved");
setAutoSaveState((prevState) => ({
Copy link
Collaborator

Choose a reason for hiding this comment

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

❓ Is this necessary as it's already done in the useEffect below?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hum good thinking. I will try to see if it's possible and test it. It may simplify a bit the useEffect bellow even maybe.

But I'm not sure. I will try and come back to you 🙂

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes!

So I fixed the bug of the retry, and I really took time to check that one.

When a save fail, we want to trigger a retry with the latest data we have (saw with Benjamin), meaning we don't want to just reply the pending. We may want to retry with the next, but if there is no next, meaning the user did not do any action in the meantime, we will retry with the pending.
Because of this it's important in case of a success to remove the pending, if not the useEffect will detect that there is a pending operation and will try to do it again, creating an endless loop.

I added comments for the tricky parts, btw.

toast.dismiss(toastId);
setAutoSaveStatusInternal("saving");
setTimeout(() => {
void executeSave();
Copy link
Collaborator

Choose a reason for hiding this comment

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

❓ I'm not sure I understand all the execution branches that could happen. For instance, what happens if:

  1. There's an error.
  2. The user doesn't click on the retry button, but performs another action in the UI.
  3. The user clicks on the retry button.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Haha, that's something I totally tested (NO), so it's adding a double error if you really have a problem or it's saving the second thing you're doing BUT keep the retry visible.

I need to rework this! THANKS!

Screenshot 2024-01-16 at 12 00 46

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed, I don't trigger the next save if they is an error. The retry will do it only. But any changes done after a fail will still but stacked.

autoSaveStatusInternal !== "saving" &&
(autoSaveState.pendingSave || autoSaveState.nextSave)
) {
setAutoSaveState((prevState) => ({
Copy link
Collaborator

Choose a reason for hiding this comment

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

❓ I'm not sure I understand well this side effect. It seems it will run if autoSaveStatusInternal is error and the pendingSave corresponding to that error would be replaced by nextSave. Is it intended? Should the toast be dismissed as executeSave will automatically be called?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, this one is the one that actually trigger every next save.

Meaning, when we want to perform a next save we check, is there any pendingSave or nextSave WHILE the status is not "saving" and so doing something. If yes, let's run the nextSave or just remove the pending one.

About the error, yes I think I need to dismiss somewhere to prevent the error above

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

useEffect updated with the fix on the error and it should be clearer now. With a comment explaining the code.

Copy link
Collaborator

@bapmrl bapmrl left a comment

Choose a reason for hiding this comment

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

Again, excellent work Xavier 😊


type CustomTypeContext = {
customType: CustomType;
autoSaveStatus: AutoSaveStatusType;
Copy link
Collaborator

Choose a reason for hiding this comment

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

❓ It means for now we have one autosave state for custom types. Is it going to be a different state for slices? How are we going to combine both autosave states to display the error toasts and status messages consistently/once?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For me, I was planning to have 2 providers. As the provider, don't have the save logic since it's the hook and only contain things purely related to custom types or slices.

There are some duplications, but I would say healthy one that help us maintain the project without risking crashing everything.

You can see what I'm planning to do here: https://github.com/prismicio/slice-machine/blob/xru/auto-save-slice-builder/packages/slice-machine/src/features/slices/sliceBuilder/SliceBuilderProvider.tsx
And you can see that there are difference related to slices.

So for me 2 providers that reuse the hook yes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, I just wonder how the 2 autoSaveStatus will be combined to provide a clear visual indicator to the users.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hum, they will never be combined, since it's display from within the builder you never have both in the same view.


type CustomTypeProviderProps = {
children: ReactNode | ((value: CustomTypeContext) => ReactNode);
defaultCustomType: CustomType;
Copy link
Collaborator

Choose a reason for hiding this comment

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

💡 I think initialCustomType would be more in line with the parameter name of the useState hook.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes good point! I think naming is my nemesis! 😈

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done


const CustomTypeContextValue = createContext<CustomTypeContext>({
autoSaveStatus: "saved",
customType: {} as CustomType,
Copy link
Collaborator

Choose a reason for hiding this comment

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

⚠️ I think it's a weird optimization. If the consumer of that context didn't use the CustomTypeProvider, he'll get a customType that doesn't conform to the CustomType interface. I think it's both safer and clearer to initialize the context with undefined and throw if the provider cannot be found.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I was never a fan of how it's done in React. I would just love to have a provider where I initialize with undefined but every time I'm getting the context typescript knows that it's full. And React handle the error if I didn't setup the Prodiver.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will do it 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

});

export function CustomTypeProvider(props: CustomTypeProviderProps) {
const { children, defaultCustomType } = props;
Copy link
Collaborator

Choose a reason for hiding this comment

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

💡 Adding a new line after the props spreading would make the code clearer in my opinion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I will add it, maybe it's something I will look for now. I tend to group it like that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done


export function CustomTypeProvider(props: CustomTypeProviderProps) {
const { children, defaultCustomType } = props;
const isMounted = useRef(false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

💡 Maybe you could use the useIsFirstRender hook from @prismicio/editor-support?

Copy link
Collaborator Author

@xrutayisire xrutayisire Jan 16, 2024

Choose a reason for hiding this comment

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

100%, I didn't even know about it. It's something we (expect you haha) are not doing. More looking for simple stuff in editor packages. I will use it!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

isMounted.current = true;
}
},
// Prevent saveCustomTypeSuccess to trigger infinite loop
Copy link
Collaborator

Choose a reason for hiding this comment

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

💡 Maybe "from triggering an infinite loop" would be better?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agree 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

console.error("Error in save effect:", error),
);
}
}, [autoSaveStatusInternal, autoSaveState, executeSave]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe every property of autoSaveState should be listed in here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hey, it's not necessary here since it's immutable, we are talking about a state so each time the values changed the state itself 100% change. You never modify a state sub value directly.

The linter is pretty good on that and tell us here that it's good.

export function CustomTypeProvider(props: CustomTypeProviderProps) {
const { children, initialCustomType } = props;

const isFirstRender = useIsFirstRender();
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

},
// Prevent saveCustomTypeSuccess from triggering an infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps
[customType, setNextSave],
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it better to remove it from the deps or the wrap it with a useCallback?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I could try to use useStableCallback from the editor support, good idea!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's working with useStableCallback:

Screenshot 2024-01-17 at 14 55 57

I did it in the second PR with both providers to freeze this one, since the review is done now.

@xrutayisire xrutayisire force-pushed the xru/auto-save-type-builder branch from 51a9163 to 02dc287 Compare January 18, 2024 14:40
@xrutayisire xrutayisire changed the title feat(auto-save): Auto-save on custom / page type builder feat(auto-save): Auto-save on custom / page type & slice builder Jan 18, 2024
@xrutayisire xrutayisire merged commit d531c16 into main Jan 18, 2024
35 checks passed
@xrutayisire xrutayisire deleted the xru/auto-save-type-builder branch January 18, 2024 14:56
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