Skip to content

Commit

Permalink
feat: implement auto save feature (#112)
Browse files Browse the repository at this point in the history
chore(test): remove save button click in enterSettings to test auto save

chore: add Gitlocalize in readme
  • Loading branch information
ReidyT authored Feb 19, 2024
1 parent 695cd91 commit 3428862
Show file tree
Hide file tree
Showing 13 changed files with 437 additions and 126 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@
VITE_API_HOST=<request address for the backend>
```

![GitHub package.json version](https://img.shields.io/github/package-json/v/graasp/graasp-app-text-analysis?color=green&style=flat-square)

[![GitHub package.json version](https://img.shields.io/github/package-json/v/graasp/graasp-app-text-analysis?color=green&style=flat-square)](https://github.com/graasp/graasp-app-text-analysis)
[![GitLocalize](https://gitlocalize.com/repo/9343/whole_project/badge.svg)](https://gitlocalize.com/repo/9343?utm_source=badge)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->


## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
Expand Down
6 changes: 3 additions & 3 deletions cypress/e2e/builder/enterSettings.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ describe('Enter Settings', () => {
cy.get(buildDataCy(TITLE_INPUT_FIELD_CY))
.should('be.visible')
.type('Title');
cy.get(buildDataCy(SETTINGS_SAVE_BUTTON_CY)).click();
// should be disabled automatically by auto save
cy.get(buildDataCy(SETTINGS_SAVE_BUTTON_CY)).should('be.disabled');

cy.get(buildDataCy(TITLE_INPUT_FIELD_CY)).type('New Title');
cy.get(buildDataCy(SETTINGS_SAVE_BUTTON_CY)).should('not.be.disabled');
cy.get(buildDataCy(SETTINGS_SAVE_BUTTON_CY)).click();
// should be disabled automatically by auto save
cy.get(buildDataCy(SETTINGS_SAVE_BUTTON_CY)).should('be.disabled');

// test that multiline is disabled, because it is rendered inline in player
Expand All @@ -69,7 +69,7 @@ describe('Enter Settings', () => {
'Lorem ipsum dolor sit amet. Ut optio laborum qui ducimus rerum eum illum possimus non quidem facere.',
);

cy.get(buildDataCy(SETTINGS_SAVE_BUTTON_CY)).click();
// should be disabled automatically by auto save

cy.get(buildDataCy(SETTINGS_SAVE_BUTTON_CY)).should('be.disabled');

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@testing-library/react": "14.1.2",
"cypress-vite": "^1.5.0",
"i18next": "^23.8.2",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.isobject": "3.0.2",
"lodash.isstring": "4.0.1",
Expand Down Expand Up @@ -77,6 +78,7 @@
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"@types/i18n": "^0.13.10",
"@types/jest": "29.5.11",
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.isobject": "3.0.7",
"@types/lodash.isstring": "4.0.7",
Expand Down
57 changes: 42 additions & 15 deletions src/components/context/AppSettingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { FC, PropsWithChildren, createContext, useMemo } from 'react';

import { Data } from '@graasp/apps-query-client';
import { AppSetting } from '@graasp/sdk';

import { hooks, mutations } from '../../config/queryClient';
Expand All @@ -15,22 +16,24 @@ type PatchAppSettingType = {
id: string;
};

type DeleteAppSettingType = {
id: string;
};

export type AppSettingContextType = {
postAppSetting: (payload: PostAppSettingType) => void;
patchAppSetting: (payload: PatchAppSettingType) => void;
deleteAppSetting: (payload: DeleteAppSettingType) => void;
postAppSetting: (payload: PostAppSettingType) => Promise<AppSetting<Data>>;
patchAppSetting: (payload: PatchAppSettingType) => Promise<AppSetting<Data>>;
appSettingArray: AppSetting[];
isPatchError: boolean;
isPostError: boolean;
isLoading: boolean;
isSuccess: boolean;
};

const defaultContextValue = {
postAppSetting: () => null,
patchAppSetting: () => null,
deleteAppSetting: () => null,
postAppSetting: () => Promise.reject(),
patchAppSetting: () => Promise.reject(),
appSettingArray: [],
isPatchError: false,
isPostError: false,
isLoading: false,
isSuccess: false,
};

const AppSettingContext =
Expand All @@ -39,17 +42,41 @@ const AppSettingContext =
export const AppSettingProvider: FC<PropsWithChildren> = ({ children }) => {
const appSetting = hooks.useAppSettings();

const { mutate: postAppSetting } = mutations.usePostAppSetting();
const { mutate: patchAppSetting } = mutations.usePatchAppSetting();
const { mutate: deleteAppSetting } = mutations.useDeleteAppSetting();
const {
mutateAsync: postAppSetting,
isError: isPostError,
isLoading: postLoading,
isSuccess: postSuccess,
} = mutations.usePostAppSetting();
const {
mutateAsync: patchAppSetting,
isError: isPatchError,
isLoading: patchLoading,
isSuccess: patchSuccess,
} = mutations.usePatchAppSetting();

const isLoading = postLoading || patchLoading;
const isSuccess = postSuccess || patchSuccess;

const contextValue: AppSettingContextType = useMemo(
() => ({
postAppSetting,
patchAppSetting,
deleteAppSetting,
isPatchError,
isPostError,
isLoading,
isSuccess,
appSettingArray: appSetting.data || [],
}),
[appSetting.data, deleteAppSetting, patchAppSetting, postAppSetting],
[
appSetting.data,
isLoading,
isSuccess,
patchAppSetting,
isPatchError,
postAppSetting,
isPostError,
],
);

if (appSetting.isLoading) {
Expand Down
69 changes: 69 additions & 0 deletions src/components/hooks/useAutoSave.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import debounce from 'lodash.debounce';

import { useEffect, useRef } from 'react';

const AUTO_SAVE_DEBOUNCE_MS = 700;
const REFRESH_SAVE_TIME_MS = 20_000;

export interface DebouncedFunction {
(): void;
cancel(): void;
}

type UseAutoSaveType = {
updateSaveTimeToNow: () => void;
debounceSaveSetting: ({
settingKey,
saveCallBack,
}: {
settingKey: string;
saveCallBack: () => void;
}) => void;
};

type UseAutoSaveProps = {
onRefreshLastSaved?: (lastTime: Date) => void;
};

export const useAutoSave = ({
onRefreshLastSaved,
}: UseAutoSaveProps): UseAutoSaveType => {
const lastSavedTime = useRef<Date>();

// This map is used to manage the auto save of each setting.
const autoSaveDebounceMap = useRef(new Map<string, DebouncedFunction>());

// This useEffect is used to refresh the last saved time periodically.
useEffect(() => {
const interval = setInterval(() => {
const lastTime = lastSavedTime.current;
if (lastTime) {
onRefreshLastSaved?.(lastTime);
}
}, REFRESH_SAVE_TIME_MS);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastSavedTime.current]);

const debounceSaveSetting = ({
settingKey,
saveCallBack,
}: {
settingKey: string;
saveCallBack: () => void;
}): void => {
autoSaveDebounceMap.current.get(settingKey)?.cancel();
const newDebounce = debounce(() => saveCallBack(), AUTO_SAVE_DEBOUNCE_MS);
autoSaveDebounceMap.current.set(settingKey, newDebounce);
newDebounce();
};

const updateSaveTimeToNow = (): void => {
lastSavedTime.current = new Date();
};

return {
debounceSaveSetting,
updateSaveTimeToNow,
};
};
24 changes: 24 additions & 0 deletions src/components/hooks/useOnlineStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';

// This effect allows to detect when the client is offline.
export const useOnlineStatus = (): boolean => {
const [online, setOnline] = useState(
typeof window !== 'undefined' ? window.navigator.onLine : true,
);

useEffect(() => {
const handleStatusChange = (): void => {
setOnline(navigator.onLine);
};

window.addEventListener('online', handleStatusChange);
window.addEventListener('offline', handleStatusChange);

return () => {
window.removeEventListener('online', handleStatusChange);
window.removeEventListener('offline', handleStatusChange);
};
}, []);

return online;
};
Loading

0 comments on commit 3428862

Please sign in to comment.