Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
S
scratch-www
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Analytics
Analytics
Repository
Value Stream
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Commits
Open sidebar
xpstem
scratch-www
Commits
2fe3948e
Unverified
Commit
2fe3948e
authored
May 11, 2021
by
Paul Kaplan
Committed by
GitHub
May 11, 2021
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #5371 from paulkaplan/add-studio-activity-pagination
Add studio activity pagination
parents
051fc107
f61047dd
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
66 additions
and
76 deletions
+66
-76
src/redux/infinite-list.js
src/redux/infinite-list.js
+1
-24
src/views/studio/lib/fetchers.js
src/views/studio/lib/fetchers.js
+0
-9
src/views/studio/lib/studio-activity-actions.js
src/views/studio/lib/studio-activity-actions.js
+43
-0
src/views/studio/studio-activity.jsx
src/views/studio/studio-activity.jsx
+22
-14
test/unit/redux/infinite-list.test.js
test/unit/redux/infinite-list.test.js
+0
-29
No files found.
src/redux/infinite-list.js
View file @
2fe3948e
...
@@ -16,14 +16,6 @@
...
@@ -16,14 +16,6 @@
* the next state.
* the next state.
*/
*/
/**
* @typedef {function} InfiniteListFetcher
* A function to call that returns more data for the InfiniteList
* loadMore action. It must resolve to {items: [], moreToLoad} or
* reject with the error {statusCode}.
* @returns {Promise<{items:[], moreToLoad:boolean}>}
*/
/**
/**
* A redux module to create a list of items where more items can be loaded
* A redux module to create a list of items where more items can be loaded
* using an API. Additionally, there are actions for prepending items
* using an API. Additionally, there are actions for prepending items
...
@@ -102,22 +94,7 @@ const InfiniteList = key => {
...
@@ -102,22 +94,7 @@ const InfiniteList = key => {
error
:
error
=>
({
type
:
`
${
key
}
_ERROR`
,
error
}),
error
:
error
=>
({
type
:
`
${
key
}
_ERROR`
,
error
}),
loading
:
()
=>
({
type
:
`
${
key
}
_LOADING`
}),
loading
:
()
=>
({
type
:
`
${
key
}
_LOADING`
}),
append
:
(
items
,
moreToLoad
)
=>
({
type
:
`
${
key
}
_APPEND`
,
items
,
moreToLoad
}),
append
:
(
items
,
moreToLoad
)
=>
({
type
:
`
${
key
}
_APPEND`
,
items
,
moreToLoad
}),
clear
:
()
=>
({
type
:
`
${
key
}
_CLEAR`
}),
clear
:
()
=>
({
type
:
`
${
key
}
_CLEAR`
})
/**
* Load more action returns a thunk. It takes a function to call to get more items.
* It will call the LOADING action before calling the fetcher, and call
* APPEND with the results or call ERROR.
* @param {InfiniteListFetcher} fetcher - function that returns a promise
* which must resolve to {items: [], moreToLoad}.
* @returns {function} a thunk that sequences the load and dispatches
*/
loadMore
:
fetcher
=>
(
dispatch
=>
{
dispatch
(
actions
.
loading
());
return
fetcher
()
.
then
(({
items
,
moreToLoad
})
=>
dispatch
(
actions
.
append
(
items
,
moreToLoad
)))
.
catch
(
error
=>
dispatch
(
actions
.
error
(
error
)));
})
};
};
const
selector
=
state
=>
state
[
key
];
const
selector
=
state
=>
state
[
key
];
...
...
src/views/studio/lib/fetchers.js
deleted
100644 → 0
View file @
051fc107
// TODO move this to studio-activity-actions, include pagination
const
activityFetcher
=
studioId
=>
fetch
(
`
${
process
.
env
.
API_HOST
}
/studios/
${
studioId
}
/activity`
)
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
({
items
:
data
,
moreToLoad
:
false
}));
// No pagination on the activity feed
export
{
activityFetcher
};
src/views/studio/lib/studio-activity-actions.js
0 → 100644
View file @
2fe3948e
import
keyMirror
from
'
keymirror
'
;
import
api
from
'
../../../lib/api
'
;
import
{
activity
}
from
'
./redux-modules
'
;
import
{
selectStudioId
}
from
'
../../../redux/studio
'
;
const
Errors
=
keyMirror
({
NETWORK
:
null
,
SERVER
:
null
,
PERMISSION
:
null
});
const
normalizeError
=
(
err
,
body
,
res
)
=>
{
if
(
err
)
return
Errors
.
NETWORK
;
if
(
res
.
statusCode
===
401
||
res
.
statusCode
===
403
)
return
Errors
.
PERMISSION
;
if
(
res
.
statusCode
!==
200
)
return
Errors
.
SERVER
;
return
null
;
};
const
loadActivity
=
()
=>
((
dispatch
,
getState
)
=>
{
const
state
=
getState
();
const
studioId
=
selectStudioId
(
state
);
const
items
=
activity
.
selector
(
state
).
items
;
const
params
=
{
limit
:
20
};
if
(
items
.
length
>
0
)
{
// dateLimit is the newest notification you want to get back, which is
// the date of the oldest one we've already loaded
params
.
dateLimit
=
items
[
items
.
length
-
1
].
datetime_created
;
}
api
({
uri
:
`/studios/
${
studioId
}
/activity/`
,
params
},
(
err
,
body
,
res
)
=>
{
const
error
=
normalizeError
(
err
,
body
,
res
);
if
(
error
)
return
dispatch
(
activity
.
actions
.
error
(
error
));
const
ids
=
items
.
map
(
item
=>
item
.
id
);
// Deduplication is needed because pagination based on date can contain duplicates
const
deduped
=
body
.
filter
(
item
=>
ids
.
indexOf
(
item
.
id
)
===
-
1
);
dispatch
(
activity
.
actions
.
append
(
deduped
,
body
.
length
===
params
.
limit
));
});
});
export
{
loadActivity
};
src/views/studio/studio-activity.jsx
View file @
2fe3948e
...
@@ -3,11 +3,11 @@ import PropTypes from 'prop-types';
...
@@ -3,11 +3,11 @@ import PropTypes from 'prop-types';
import
{
FormattedMessage
}
from
'
react-intl
'
;
import
{
FormattedMessage
}
from
'
react-intl
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
useParams
}
from
'
react-router
'
;
import
{
activity
}
from
'
./lib/redux-modules
'
;
import
{
activity
}
from
'
./lib/redux-modules
'
;
import
{
activityFetcher
}
from
'
./lib/fetcher
s
'
;
import
{
loadActivity
}
from
'
./lib/studio-activity-action
s
'
;
import
Debug
from
'
./debug.jsx
'
;
import
Debug
from
'
./debug.jsx
'
;
import
classNames
from
'
classnames
'
;
import
SocialMessage
from
'
../../components/social-message/social-message.jsx
'
;
import
SocialMessage
from
'
../../components/social-message/social-message.jsx
'
;
...
@@ -170,14 +170,10 @@ const getComponentForItem = item => {
...
@@ -170,14 +170,10 @@ const getComponentForItem = item => {
}
}
};
};
const
StudioActivity
=
({
items
,
loading
,
error
,
onInitialLoad
})
=>
{
const
StudioActivity
=
({
items
,
loading
,
error
,
moreToLoad
,
onLoadMore
})
=>
{
const
{
studioId
}
=
useParams
();
// Fetch the data if none has been loaded yet. This would run only once,
// since studioId doesnt change, but the component is potentially mounted
// multiple times because of tab routing, so need to check for empty items.
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
studioId
&&
items
.
length
===
0
)
onInitialLoad
(
studioId
);
if
(
items
.
length
===
0
)
onLoadMore
(
);
},
[
studioId
]);
// items.length intentionally left out
},
[
]);
return
(
return
(
<
div
className=
"studio-activity"
>
<
div
className=
"studio-activity"
>
...
@@ -194,6 +190,18 @@ const StudioActivity = ({items, loading, error, onInitialLoad}) => {
...
@@ -194,6 +190,18 @@ const StudioActivity = ({items, loading, error, onInitialLoad}) => {
getComponentForItem
(
item
)
getComponentForItem
(
item
)
)
}
)
}
</
ul
>
</
ul
>
<
div
>
{
moreToLoad
&&
<
button
className=
{
classNames
(
'
button
'
,
{
'
mod-mutating
'
:
loading
})
}
onClick=
{
onLoadMore
}
>
<
FormattedMessage
id=
"general.loadMore"
/>
</
button
>
}
</
div
>
</
div
>
</
div
>
);
);
};
};
...
@@ -202,13 +210,13 @@ StudioActivity.propTypes = {
...
@@ -202,13 +210,13 @@ StudioActivity.propTypes = {
items
:
PropTypes
.
array
,
// eslint-disable-line react/forbid-prop-types
items
:
PropTypes
.
array
,
// eslint-disable-line react/forbid-prop-types
loading
:
PropTypes
.
bool
,
loading
:
PropTypes
.
bool
,
error
:
PropTypes
.
object
,
// eslint-disable-line react/forbid-prop-types
error
:
PropTypes
.
object
,
// eslint-disable-line react/forbid-prop-types
onInitialLoad
:
PropTypes
.
func
moreToLoad
:
PropTypes
.
bool
,
onLoadMore
:
PropTypes
.
func
};
};
export
default
connect
(
export
default
connect
(
state
=>
activity
.
selector
(
state
),
state
=>
activity
.
selector
(
state
),
dispatch
=>
({
{
onInitialLoad
:
studioId
=>
dispatch
(
onLoadMore
:
loadActivity
activity
.
actions
.
loadMore
(
activityFetcher
.
bind
(
null
,
studioId
,
0
)))
}
})
)(
StudioActivity
);
)(
StudioActivity
);
test/unit/redux/infinite-list.test.js
View file @
2fe3948e
/* global Promise */
import
InfiniteList
from
'
../../../src/redux/infinite-list
'
;
import
InfiniteList
from
'
../../../src/redux/infinite-list
'
;
const
module
=
InfiniteList
(
'
test-key
'
);
const
module
=
InfiniteList
(
'
test-key
'
);
...
@@ -150,34 +149,6 @@ describe('Infinite List redux module', () => {
...
@@ -150,34 +149,6 @@ describe('Infinite List redux module', () => {
expect
(
typeof
module
.
actions
[
key
]).
toBe
(
'
function
'
);
expect
(
typeof
module
.
actions
[
key
]).
toBe
(
'
function
'
);
}
}
});
});
describe
(
'
loadMore
'
,
()
=>
{
test
(
'
returns a thunk function, rather than a standard action object
'
,
()
=>
{
expect
(
typeof
module
.
actions
.
loadMore
()).
toBe
(
'
function
'
);
});
test
(
'
calls loading and the fetcher
'
,
()
=>
{
let
dispatch
=
jest
.
fn
();
let
fetcher
=
jest
.
fn
(()
=>
new
Promise
(()
=>
{
}));
// that never resolves
module
.
actions
.
loadMore
(
fetcher
)(
dispatch
);
expect
(
dispatch
).
toHaveBeenCalledWith
(
module
.
actions
.
loading
());
expect
(
fetcher
).
toHaveBeenCalled
();
});
test
(
'
calls append with resolved result from fetcher
'
,
async
()
=>
{
let
dispatch
=
jest
.
fn
();
let
fetcher
=
jest
.
fn
(()
=>
Promise
.
resolve
({
items
:
[
'
a
'
,
'
b
'
],
moreToLoad
:
false
}));
await
module
.
actions
.
loadMore
(
fetcher
)(
dispatch
);
expect
(
dispatch
.
mock
.
calls
[
1
][
0
])
// the second call to dispatch, after LOADING
.
toEqual
(
module
.
actions
.
append
([
'
a
'
,
'
b
'
],
false
));
});
test
(
'
calls error with rejecting promise from fetcher
'
,
async
()
=>
{
let
error
=
new
Error
();
let
dispatch
=
jest
.
fn
();
let
fetcher
=
jest
.
fn
(()
=>
Promise
.
reject
(
error
));
await
module
.
actions
.
loadMore
(
fetcher
)(
dispatch
);
expect
(
dispatch
.
mock
.
calls
[
1
][
0
])
// the second call to dispatch, after LOADING
.
toEqual
(
module
.
actions
.
error
(
error
));
});
});
});
});
describe
(
'
selector
'
,
()
=>
{
describe
(
'
selector
'
,
()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment