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
UIRS-33 Major rework using react-query
#68
Changes from 32 commits
e17ba09
ef6acee
c1b091e
07ca20e
840c1fd
9612c6a
4ca890a
39b2f5c
a732703
8c6dc21
cfdfd62
d28705b
6ac8418
e969c0a
f68503b
e25ca80
552ba2b
d9621f3
ef4bc91
ce06d56
c7fa8a4
a0cef89
e291ad7
761014e
dd1a1a2
637e49a
1de2592
2990ad8
5d7c2ea
33fec73
f3cc2b3
6bdd972
2c11ea8
304fb17
5d0360c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,16 @@ | ||
{ | ||
"parser": "babel-eslint", | ||
"extends": ["@folio/eslint-config-stripes/acquisitions"] | ||
"extends": ["@folio/eslint-config-stripes/acquisitions"], | ||
"rules": { | ||
"no-multiple-empty-lines": "off" | ||
}, | ||
"overrides": [ | ||
{ | ||
"files": ["*test.*", "test/**"], | ||
"rules": { | ||
"react/prop-types": "off", | ||
"padding-line-between-statements": "off" | ||
} | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1 @@ | ||
const commonCofig = require('@folio/stripes-acq-components/jest.config'); | ||
|
||
module.exports = { | ||
...commonCofig, | ||
coverageDirectory: './artifacts/coverage-jest/', | ||
}; | ||
module.exports = require('@folio/stripes-acq-components/jest.config'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { useQueryClient } from 'react-query'; | ||
|
||
import { useOkapiQuery } from './useOkapiQuery'; | ||
import { useOkapiMutation } from './useOkapiMutation'; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it new style to have 2 empty lines? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. General stripes lint rules added this possibility a while ago. I guess visual dividing logically separated portions of code is a good thing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's enough to have 1 line to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if portions to be separated have empty lines inside? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean, these are mostly just functions and not various heterogenous chunks of code, so I don't really think that applies here. But 🤷, I don't really have a stake in this. |
||
|
||
export const useListQuery = options => { | ||
const query = useOkapiQuery({ | ||
path: 'remote-storage/configurations', | ||
queryKey: 'remote-storage/configurations', | ||
...options, | ||
}); | ||
|
||
return { | ||
configurations: query.data?.configurations ?? [], | ||
...query, | ||
}; | ||
}; | ||
|
||
|
||
export const useSingleQuery = ({ id, onError, ...rest }) => { | ||
const queryClient = useQueryClient(); | ||
|
||
const query = useOkapiQuery({ | ||
path: `remote-storage/configurations/${id}`, | ||
queryKey: ['remote-storage/configurations', id], | ||
axelhunn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
onError: () => { | ||
// One of the most possible sources of an error here | ||
// is navigating to the item that was already deleted (from another workplace). | ||
// Refreshing the list couldn't hurt in this case. | ||
queryClient.invalidateQueries(['remote-storage/configurations'], { exact: true }); | ||
|
||
return onError?.(); | ||
}, | ||
...rest, | ||
}); | ||
|
||
return { | ||
configuration: query.data ?? {}, | ||
...query, | ||
}; | ||
}; | ||
|
||
|
||
const useMutation = ({ onSettled, ...rest }) => { | ||
const queryClient = useQueryClient(); | ||
|
||
return useOkapiMutation({ | ||
// We refresh the list on any result of item mutation, success or error: | ||
// One of the most possible sources of an error here | ||
// is trying to mutate the item that was already deleted (from another workplace). | ||
// Refreshing the list couldn't hurt in this case. | ||
onSettled: () => { | ||
queryClient.invalidateQueries(['remote-storage/configurations'], { exact: true }); | ||
|
||
return onSettled?.(); | ||
}, | ||
...rest, | ||
}); | ||
}; | ||
|
||
|
||
export const useCreateMutation = options => useMutation({ | ||
method: 'post', | ||
path: 'remote-storage/configurations', | ||
...options, | ||
}); | ||
|
||
|
||
export const useUpdateMutation = ({ id, onSuccess, ...rest }) => { | ||
const queryClient = useQueryClient(); | ||
|
||
return useMutation({ | ||
method: 'put', | ||
path: `remote-storage/configurations/${id}`, | ||
onSuccess: () => { | ||
queryClient.invalidateQueries(['remote-storage/configurations', id], { exact: true }); | ||
|
||
return onSuccess?.(); | ||
}, | ||
...rest, | ||
}); | ||
}; | ||
|
||
|
||
export const useDeleteMutation = ({ id, ...options }) => useMutation({ | ||
method: 'delete', | ||
path: `remote-storage/configurations/${id}`, | ||
...options, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { useOkapiQuery } from './useOkapiQuery'; | ||
|
||
export const useListQuery = options => { | ||
const query = useOkapiQuery({ | ||
path: 'remote-storage/mappings', | ||
...options, | ||
}); | ||
|
||
return { | ||
mappings: query?.data?.mappings ?? [], | ||
...query, | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * as Mappings from './Mappings'; | ||
export * as Configurations from './Configurations'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { server, rest, API_BASE } from '../../../test/net'; | ||
import { renderAPIHook, ERROR_RESPONSE } from '../setup'; // must be imported before the tested hooks | ||
|
||
import { useCreateMutation, useListQuery } from '../../Configurations'; | ||
|
||
|
||
const data = { | ||
name: 'RS1', | ||
providerName: 'DEMATIC_EMS', | ||
}; | ||
|
||
const url = { | ||
create: `${API_BASE}/configurations`, | ||
list: `${API_BASE}/configurations`, | ||
}; | ||
|
||
let request; | ||
|
||
beforeEach(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. outside describe? is it legal? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, perfectly so. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO describe provides more readable message in case of fail There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the filename clearly tells what the test is about, the message is informative enough There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is nice about using |
||
request = undefined; | ||
server.use(rest.post( | ||
url.create, | ||
(req, res, ctx) => { | ||
request = req; | ||
|
||
return res(ctx.status(201)); // "Created" | ||
}, | ||
)); | ||
}); | ||
|
||
|
||
it('POSTs data to server', async () => { | ||
const { result, waitFor } = renderAPIHook(useCreateMutation); | ||
|
||
result.current.mutate(data); | ||
|
||
await waitFor(() => result.current.isSuccess); | ||
expect(request?.body).toEqual(data); | ||
}); | ||
|
||
describe('Invalidation of List query', () => { | ||
const checkListInvalidatedOn = async (status) => { | ||
const { result, waitFor } = renderAPIHook(useCreateMutation); | ||
|
||
const listQueryHook = renderAPIHook(useListQuery); | ||
|
||
await waitFor(() => listQueryHook.result.current.isFetching); | ||
await waitFor(() => !listQueryHook.result.current.isFetching && listQueryHook.result.current.isSuccess); | ||
|
||
result.current.mutate(data); | ||
|
||
// await waitFor(() => result.current.isSuccess); | ||
await waitFor(() => result.current.status === status); | ||
|
||
return listQueryHook.result.current.isFetching; | ||
}; | ||
|
||
beforeEach(() => { | ||
server.use(rest.get(url.list, (req, res, ctx) => res(ctx.json({ | ||
configurations: [1, 2, 3], | ||
})))); | ||
}); | ||
|
||
it('is made on success', async () => { | ||
expect(await checkListInvalidatedOn('success')).toBeTruthy(); | ||
}); | ||
|
||
it('is made on error', async () => { | ||
server.use(rest.post(url.create, ERROR_RESPONSE)); | ||
|
||
expect(await checkListInvalidatedOn('error')).toBeTruthy(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { server, rest, API_BASE } from '../../../test/net'; | ||
import { renderAPIHook, ERROR_RESPONSE } from '../setup'; // must be imported before the tested hooks | ||
|
||
import { useDeleteMutation, useListQuery } from '../../Configurations'; | ||
|
||
|
||
const id = 42; | ||
const url = { | ||
delete: `${API_BASE}/configurations/${id}`, | ||
list: `${API_BASE}/configurations`, | ||
}; | ||
|
||
|
||
beforeEach(() => { | ||
server.use(rest.delete( | ||
url.delete, | ||
(req, res, ctx) => res(ctx.status(204)), // "No Content" | ||
)); | ||
}); | ||
|
||
|
||
it('DELETESs data from server', async () => { | ||
const { result, waitFor } = renderAPIHook(() => useDeleteMutation({ id })); | ||
|
||
result.current.mutate(); | ||
|
||
await expect(waitFor(() => result.current.isSuccess)).resolves.not.toThrow(); | ||
}); | ||
|
||
|
||
describe('Invalidation of List query', () => { | ||
const checkListInvalidatedOn = async (status) => { | ||
const { result, waitFor } = renderAPIHook(() => useDeleteMutation({ id })); | ||
|
||
const listQueryHook = renderAPIHook(useListQuery); | ||
|
||
await waitFor(() => listQueryHook.result.current.isFetching); | ||
await waitFor(() => !listQueryHook.result.current.isFetching && listQueryHook.result.current.isSuccess); | ||
|
||
result.current.mutate(); | ||
|
||
await waitFor(() => result.current.status === status); | ||
|
||
return listQueryHook.result.current.isFetching; | ||
}; | ||
|
||
beforeEach(() => { | ||
server.use(rest.get(url.list, (req, res, ctx) => res(ctx.json({ | ||
configurations: [1, 2, 3], | ||
})))); | ||
}); | ||
|
||
it('is made on success', async () => { | ||
expect(await checkListInvalidatedOn('success')).toBeTruthy(); | ||
}); | ||
|
||
it('is made on error', async () => { | ||
server.use(rest.delete(url.delete, ERROR_RESPONSE)); | ||
|
||
expect(await checkListInvalidatedOn('error')).toBeTruthy(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// This should be imported before the tested hooks | ||
import { server, rest, API_BASE } from '../../../test/net'; | ||
import { renderAPIHook, ERROR_RESPONSE } from '../setup'; // must be imported before the tested hooks | ||
|
||
import { useListQuery } from '../../Configurations'; | ||
|
||
|
||
const url = `${API_BASE}/configurations`; | ||
|
||
beforeEach(() => { | ||
server.use(rest.get(url, (req, res, ctx) => res(ctx.json({ | ||
configurations: [1, 2, 3], | ||
})))); | ||
}); | ||
|
||
|
||
it('returns list of configurations when loaded', async () => { | ||
const { result, waitFor } = renderAPIHook(useListQuery); | ||
|
||
expect(result.current.isLoading).toBeTruthy(); | ||
|
||
await waitFor(() => result.current.isSuccess); | ||
expect(result.current.configurations).toEqual([1, 2, 3]); | ||
}); | ||
|
||
it('returns empty list while loading', () => { | ||
const { result } = renderAPIHook(useListQuery); | ||
|
||
expect(result.current.configurations).toEqual([]); | ||
expect(result.current.isLoading).toBeTruthy(); | ||
}); | ||
|
||
it('returns empty list on error', async () => { | ||
server.use(rest.get(url, ERROR_RESPONSE)); | ||
|
||
const { result, waitFor } = renderAPIHook(useListQuery); | ||
|
||
await waitFor(() => result.current.isError); | ||
expect(result.current.configurations).toEqual([]); | ||
}); |
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.
why do your files start from capital? are they classes or components?
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.
it's a composite module, with several exports
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.
usually it follows camelCase, PascalCase is used by classes or components
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 guess there's no strict rule for modules.
As a general rule we use camelCase for something that is insnance and can be multiplied,
and PascalCase for something that can be instantiated OR is used as namespace for static members, like
React
inThey also use the same convention in MDN for module names, see
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#creating_a_module_object
As for naming the files, in React world we use same case for filename as for it content - same PascalCase for components and classes. So I used to name such modules in PascalCase too.
But again, no strict rules for that, it's an opinionated thing.
So if you point me to the rule on this here in FOLIO, I'll gladly abide it.
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.
Here is elaborate article on the subject https://badgerbadgerbadgerbadger.medium.com/letter-casing-in-names-of-imported-nodejs-modules-707194c6a003