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
82da633e
Commit
82da633e
authored
Apr 14, 2021
by
Paul Kaplan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add title, description and following editors
parent
c789a270
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
394 additions
and
15 deletions
+394
-15
src/redux/studio-mutations.js
src/redux/studio-mutations.js
+174
-0
src/redux/studio.js
src/redux/studio.js
+25
-4
src/views/studio/studio-description.jsx
src/views/studio/studio-description.jsx
+56
-0
src/views/studio/studio-follow.jsx
src/views/studio/studio-follow.jsx
+59
-0
src/views/studio/studio-info.jsx
src/views/studio/studio-info.jsx
+13
-10
src/views/studio/studio-title.jsx
src/views/studio/studio-title.jsx
+52
-0
src/views/studio/studio.jsx
src/views/studio/studio.jsx
+4
-1
test/unit/redux/studio.test.js
test/unit/redux/studio.test.js
+11
-0
No files found.
src/redux/studio-mutations.js
0 → 100644
View file @
82da633e
/**
* Studio Mutation Reducer - Responsible for client-side modifications
* to studio info / roles. Stores in-progress and error states for updates,
* and handles the network requests.
*
* This reducer DOES NOT store the value of the field being mutated,
* or deal with loading that value initially from the server. That is
* handled by the studio info and roles reducer.
*/
const
keyMirror
=
require
(
'
keymirror
'
);
const
api
=
require
(
'
../lib/api
'
);
const
{
selectUsername
}
=
require
(
'
./session
'
);
const
{
selectStudioId
}
=
require
(
'
./studio
'
);
const
Errors
=
keyMirror
({
NETWORK
:
null
,
SERVER
:
null
,
INAPPROPRIATE
:
null
,
PERMISSION
:
null
,
UNHANDLED
:
null
});
const
getInitialState
=
()
=>
({
mutationErrors
:
{},
// { [field]: <error>, ... }
isMutating
:
{}
// { [field]: <boolean>, ... }
});
const
studioMutationsReducer
=
(
state
,
action
)
=>
{
if
(
typeof
state
===
'
undefined
'
)
{
state
=
getInitialState
();
}
switch
(
action
.
type
)
{
case
'
START_STUDIO_MUTATION
'
:
return
{
...
state
,
isMutating
:
{
...
state
.
isMutating
,
[
action
.
field
]:
true
},
mutationErrors
:
{
...
state
.
mutationErrors
,
[
action
.
field
]:
null
}
};
case
'
COMPLETE_STUDIO_MUTATION
'
:
return
{
...
state
,
isMutating
:
{
...
state
.
isMutating
,
[
action
.
field
]:
false
},
mutationErrors
:
{
...
state
.
mutationErrors
,
[
action
.
field
]:
action
.
error
}
};
default
:
return
state
;
}
};
// Action Creators
const
startMutation
=
field
=>
({
type
:
'
START_STUDIO_MUTATION
'
,
field
});
const
completeMutation
=
(
field
,
value
,
error
=
null
)
=>
({
type
:
'
COMPLETE_STUDIO_MUTATION
'
,
field
,
value
,
// Value is used by other reducers listening for this action
error
});
// Selectors
const
selectIsMutatingTitle
=
state
=>
state
.
studioMutations
.
isMutating
.
title
;
const
selectIsMutatingDescription
=
state
=>
state
.
studioMutations
.
isMutating
.
description
;
const
selectIsMutatingFollowing
=
state
=>
state
.
studioMutations
.
isMutating
.
following
;
const
selectTitleMutationError
=
state
=>
state
.
studioMutations
.
mutationErrors
.
title
;
const
selectDescriptionMutationError
=
state
=>
state
.
studioMutations
.
mutationErrors
.
description
;
const
selectFollowingMutationError
=
state
=>
state
.
studioMutations
.
mutationErrors
.
following
;
// Thunks
/**
* Given a response from `api.js`, normalize the possible
* error conditions using the `Errors` object.
* @param {object} err - error from api.js
* @param {object} body - parsed body
* @param {object} res - raw response from api.js
* @returns {string} one of Errors.<TYPE> or 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
;
try
{
if
(
body
.
errors
.
length
>
0
)
{
if
(
body
.
errors
[
0
]
===
'
inappropriate-generic
'
)
return
Errors
.
INAPPROPRIATE
;
return
Errors
.
UNHANDLED
;
}
}
catch
(
_
)
{
/* No body.errors[], continue */
}
return
null
;
};
const
mutateStudioTitle
=
value
=>
((
dispatch
,
getState
)
=>
{
dispatch
(
startMutation
(
'
title
'
));
api
({
host
:
''
,
uri
:
`/site-api/galleries/all/
${
selectStudioId
(
getState
())}
/`
,
method
:
'
PUT
'
,
useCsrf
:
true
,
json
:
{
title
:
value
}
},
(
err
,
body
,
res
)
=>
{
const
error
=
normalizeError
(
err
,
body
,
res
);
dispatch
(
completeMutation
(
'
title
'
,
value
,
error
));
});
});
const
mutateStudioDescription
=
value
=>
((
dispatch
,
getState
)
=>
{
dispatch
(
startMutation
(
'
description
'
));
api
({
host
:
''
,
uri
:
`/site-api/galleries/all/
${
selectStudioId
(
getState
())}
/`
,
method
:
'
PUT
'
,
useCsrf
:
true
,
json
:
{
description
:
value
}
},
(
err
,
body
,
res
)
=>
{
const
error
=
normalizeError
(
err
,
body
,
res
);
dispatch
(
completeMutation
(
'
description
'
,
value
,
error
));
});
});
const
mutateFollowingStudio
=
shouldFollow
=>
((
dispatch
,
getState
)
=>
{
dispatch
(
startMutation
(
'
following
'
));
const
state
=
getState
();
const
studioId
=
selectStudioId
(
state
);
const
username
=
selectUsername
(
state
);
let
uri
=
`/site-api/users/bookmarkers/
${
studioId
}
/`
;
uri
+=
shouldFollow
?
'
add/
'
:
'
remove/
'
;
uri
+=
`?usernames=
${
username
}
`
;
api
({
host
:
''
,
uri
:
uri
,
method
:
'
PUT
'
,
useCsrf
:
true
},
(
err
,
body
,
res
)
=>
{
const
error
=
normalizeError
(
err
,
body
,
res
);
dispatch
(
completeMutation
(
'
following
'
,
error
?
!
shouldFollow
:
shouldFollow
,
error
));
});
});
module
.
exports
=
{
getInitialState
,
studioMutationsReducer
,
Errors
,
// Thunks
mutateStudioTitle
,
mutateStudioDescription
,
mutateFollowingStudio
,
// Selectors
selectIsMutatingTitle
,
selectIsMutatingDescription
,
selectIsMutatingFollowing
,
selectTitleMutationError
,
selectDescriptionMutationError
,
selectFollowingMutationError
};
src/redux/studio.js
View file @
82da633e
...
...
@@ -3,7 +3,10 @@ const keyMirror = require('keymirror');
const
api
=
require
(
'
../lib/api
'
);
const
log
=
require
(
'
../lib/log
'
);
const
{
selectUserId
,
selectIsAdmin
,
selectIsSocial
,
selectUsername
,
selectToken
}
=
require
(
'
./session
'
);
const
{
selectUserId
,
selectIsAdmin
,
selectIsSocial
,
selectUsername
,
selectToken
,
selectIsLoggedIn
}
=
require
(
'
./session
'
);
const
Status
=
keyMirror
({
FETCHED
:
null
,
...
...
@@ -25,7 +28,7 @@ const getInitialState = () => ({
rolesStatus
:
Status
.
NOT_FETCHED
,
manager
:
false
,
curator
:
false
,
follow
er
:
false
,
follow
ing
:
false
,
invited
:
false
});
...
...
@@ -53,6 +56,12 @@ const studioReducer = (state, action) => {
...
state
,
[
action
.
fetchType
]:
action
.
fetchStatus
};
case
'
COMPLETE_STUDIO_MUTATION
'
:
if
(
typeof
state
[
action
.
field
]
===
'
undefined
'
)
return
state
;
return
{
...
state
,
[
action
.
field
]:
action
.
value
};
default
:
return
state
;
}
...
...
@@ -102,7 +111,12 @@ const selectCanDeleteCommentWithoutConfirm = state => selectIsAdmin(state);
// Data selectors
const
selectStudioId
=
state
=>
state
.
studio
.
id
;
const
selectStudioTitle
=
state
=>
state
.
studio
.
title
;
const
selectStudioDescription
=
state
=>
state
.
studio
.
description
;
const
selectIsLoadingInfo
=
state
=>
state
.
studio
.
infoStatus
===
Status
.
FETCHING
;
const
selectIsFollowing
=
state
=>
state
.
studio
.
following
;
const
selectCanFollowStudio
=
state
=>
selectIsLoggedIn
(
state
);
const
selectIsLoadingRoles
=
state
=>
state
.
studio
.
rolesStatus
===
Status
.
FETCHING
;
// Thunks
const
getInfo
=
()
=>
((
dispatch
,
getState
)
=>
{
...
...
@@ -158,14 +172,21 @@ module.exports = {
// Thunks
getInfo
,
getRoles
,
setInfo
,
// Selectors
selectStudioId
,
selectStudioTitle
,
selectStudioDescription
,
selectIsLoadingInfo
,
selectIsLoadingRoles
,
selectIsFollowing
,
selectCanEditInfo
,
selectCanAddProjects
,
selectShowCommentComposer
,
selectCanDeleteComment
,
selectCanDeleteCommentWithoutConfirm
,
selectCanReportComment
,
selectCanRestoreComment
selectCanRestoreComment
,
selectCanFollowStudio
};
src/views/studio/studio-description.jsx
0 → 100644
View file @
82da633e
/* eslint-disable react/jsx-no-bind */
import
React
from
'
react
'
;
import
PropTypes
from
'
prop-types
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
selectStudioDescription
,
selectIsLoadingInfo
,
selectCanEditInfo
}
from
'
../../redux/studio
'
;
import
{
mutateStudioDescription
,
selectIsMutatingDescription
,
selectDescriptionMutationError
}
from
'
../../redux/studio-mutations
'
;
const
StudioDescription
=
({
descriptionError
,
isLoading
,
isMutating
,
description
,
canEditInfo
,
handleUpdate
})
=>
(
<
div
>
<
h3
>
Description
</
h3
>
{
isLoading
?
(
<
h4
>
Loading...
</
h4
>
)
:
(
canEditInfo
?
(
<
label
>
<
textarea
rows=
"5"
cols=
"100"
disabled=
{
isMutating
}
defaultValue=
{
description
}
onBlur=
{
e
=>
e
.
target
.
value
!==
description
&&
handleUpdate
(
e
.
target
.
value
)
}
/>
{
descriptionError
&&
<
div
>
Error mutating description:
{
descriptionError
}
</
div
>
}
</
label
>
)
:
(
<
div
>
{
description
}
</
div
>
))
}
</
div
>
);
StudioDescription
.
propTypes
=
{
descriptionError
:
PropTypes
.
string
,
canEditInfo
:
PropTypes
.
bool
,
isLoading
:
PropTypes
.
bool
,
isMutating
:
PropTypes
.
bool
,
description
:
PropTypes
.
string
,
handleUpdate
:
PropTypes
.
func
};
export
default
connect
(
state
=>
({
description
:
selectStudioDescription
(
state
),
canEditInfo
:
selectCanEditInfo
(
state
),
isLoading
:
selectIsLoadingInfo
(
state
),
isMutating
:
selectIsMutatingDescription
(
state
),
descriptionError
:
selectDescriptionMutationError
(
state
)
}),
{
handleUpdate
:
mutateStudioDescription
}
)(
StudioDescription
);
src/views/studio/studio-follow.jsx
0 → 100644
View file @
82da633e
/* eslint-disable react/jsx-no-bind */
import
React
from
'
react
'
;
import
PropTypes
from
'
prop-types
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
selectIsFollowing
,
selectCanFollowStudio
,
selectIsLoadingRoles
}
from
'
../../redux/studio
'
;
import
{
mutateFollowingStudio
,
selectIsMutatingFollowing
,
selectFollowingMutationError
}
from
'
../../redux/studio-mutations
'
;
const
StudioFollow
=
({
canFollow
,
isLoading
,
isFollowing
,
isMutating
,
followingError
,
handleFollow
})
=>
(
<
div
>
<
h3
>
Following
</
h3
>
<
div
>
<
button
disabled=
{
isLoading
||
isMutating
||
!
canFollow
}
onClick=
{
()
=>
handleFollow
(
!
isFollowing
)
}
>
{
isLoading
?
(
'
Loading...
'
)
:
(
isFollowing
?
'
Unfollow
'
:
'
Follow
'
)
}
</
button
>
{
followingError
&&
<
div
>
Error mutating following:
{
followingError
}
</
div
>
}
{
!
canFollow
&&
<
div
>
Must be logged in to follow
</
div
>
}
</
div
>
</
div
>
);
StudioFollow
.
propTypes
=
{
canFollow
:
PropTypes
.
bool
,
isLoading
:
PropTypes
.
bool
,
isFollowing
:
PropTypes
.
bool
,
isMutating
:
PropTypes
.
bool
,
followingError
:
PropTypes
.
string
,
handleFollow
:
PropTypes
.
func
};
export
default
connect
(
state
=>
({
canFollow
:
selectCanFollowStudio
(
state
),
isLoading
:
selectIsLoadingRoles
(
state
),
isMutating
:
selectIsMutatingFollowing
(
state
),
isFollowing
:
selectIsFollowing
(
state
),
followingError
:
selectFollowingMutationError
(
state
)
}),
{
handleFollow
:
mutateFollowingStudio
}
)(
StudioFollow
);
src/views/studio/studio-info.jsx
View file @
82da633e
...
...
@@ -2,11 +2,16 @@ import React, {useEffect} from 'react';
import
PropTypes
from
'
prop-types
'
;
import
{
connect
}
from
'
react-redux
'
;
import
Debug
from
'
./debug.jsx
'
;
import
StudioDescription
from
'
./studio-description.jsx
'
;
import
StudioFollow
from
'
./studio-follow.jsx
'
;
import
StudioTitle
from
'
./studio-title.jsx
'
;
import
{
selectIsLoggedIn
}
from
'
../../redux/session
'
;
import
{
getInfo
,
getRoles
,
selectCanEditInfo
}
from
'
../../redux/studio
'
;
import
{
getInfo
,
getRoles
}
from
'
../../redux/studio
'
;
const
StudioInfo
=
({
isLoggedIn
,
studio
,
canEditInfo
,
onLoadInfo
,
onLoadRoles
})
=>
{
const
StudioInfo
=
({
isLoggedIn
,
studio
,
onLoadInfo
,
onLoadRoles
})
=>
{
useEffect
(()
=>
{
// Load studio info after first render
onLoadInfo
();
},
[]);
...
...
@@ -18,23 +23,22 @@ const StudioInfo = ({isLoggedIn, studio, canEditInfo, onLoadInfo, onLoadRoles})
return
(
<
div
>
<
h2
>
Studio Info
</
h2
>
<
StudioTitle
/>
<
StudioDescription
/>
<
StudioFollow
/>
<
Debug
label=
"Studio Info"
data=
{
studio
}
/>
<
Debug
label=
"Studio Info Permissions"
data=
{
{
canEditInfo
}
}
/>
</
div
>
);
};
StudioInfo
.
propTypes
=
{
canEditInfo
:
PropTypes
.
bool
,
isLoggedIn
:
PropTypes
.
bool
,
studio
:
PropTypes
.
shape
({
// Fill this in as the data is used, just for demo now
title
:
PropTypes
.
string
,
description
:
PropTypes
.
description
}),
onLoadInfo
:
PropTypes
.
func
,
onLoadRoles
:
PropTypes
.
func
...
...
@@ -43,8 +47,7 @@ StudioInfo.propTypes = {
export
default
connect
(
state
=>
({
studio
:
state
.
studio
,
isLoggedIn
:
selectIsLoggedIn
(
state
),
canEditInfo
:
selectCanEditInfo
(
state
)
isLoggedIn
:
selectIsLoggedIn
(
state
)
}),
{
onLoadInfo
:
getInfo
,
...
...
src/views/studio/studio-title.jsx
0 → 100644
View file @
82da633e
/* eslint-disable react/jsx-no-bind */
import
React
from
'
react
'
;
import
PropTypes
from
'
prop-types
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
selectStudioTitle
,
selectIsLoadingInfo
,
selectCanEditInfo
}
from
'
../../redux/studio
'
;
import
{
mutateStudioTitle
,
selectIsMutatingTitle
,
selectTitleMutationError
}
from
'
../../redux/studio-mutations
'
;
const
StudioTitle
=
({
titleError
,
isLoading
,
isMutating
,
title
,
canEditInfo
,
handleUpdate
})
=>
(
<
div
>
<
h3
>
Title
</
h3
>
{
isLoading
?
(
<
h4
>
Loading...
</
h4
>
)
:
(
canEditInfo
?
(
<
label
>
<
input
disabled=
{
isMutating
}
defaultValue=
{
title
}
onBlur=
{
e
=>
e
.
target
.
value
!==
title
&&
handleUpdate
(
e
.
target
.
value
)
}
/>
{
titleError
&&
<
div
>
Error mutating title:
{
titleError
}
</
div
>
}
</
label
>
)
:
(
<
div
>
{
title
}
</
div
>
))
}
</
div
>
);
StudioTitle
.
propTypes
=
{
titleError
:
PropTypes
.
string
,
canEditInfo
:
PropTypes
.
bool
,
isLoading
:
PropTypes
.
bool
,
isMutating
:
PropTypes
.
bool
,
title
:
PropTypes
.
string
,
handleUpdate
:
PropTypes
.
func
};
export
default
connect
(
state
=>
({
title
:
selectStudioTitle
(
state
),
canEditInfo
:
selectCanEditInfo
(
state
),
isLoading
:
selectIsLoadingInfo
(
state
),
isMutating
:
selectIsMutatingTitle
(
state
),
titleError
:
selectTitleMutationError
(
state
)
}),
{
handleUpdate
:
mutateStudioTitle
}
)(
StudioTitle
);
src/views/studio/studio.jsx
View file @
82da633e
...
...
@@ -24,8 +24,9 @@ import {
activity
}
from
'
./lib/redux-modules
'
;
const
{
studioReducer
}
=
require
(
'
../../redux/studio
'
);
const
{
getInitialState
,
studioReducer
}
=
require
(
'
../../redux/studio
'
);
const
{
commentsReducer
}
=
require
(
'
../../redux/comments
'
);
const
{
studioMutationsReducer
}
=
require
(
'
../../redux/studio-mutations
'
);
const
StudioShell
=
()
=>
{
const
match
=
useRouteMatch
();
...
...
@@ -77,10 +78,12 @@ render(
[
managers
.
key
]:
managers
.
reducer
,
[
activity
.
key
]:
activity
.
reducer
,
studio
:
studioReducer
,
studioMutations
:
studioMutationsReducer
,
comments
:
commentsReducer
},
{
studio
:
{
...
getInitialState
(),
// Include the studio id in the initial state to allow us
// to stop passing around the studio id in components
// when it is only needed for data fetching, not for rendering.
...
...
test/unit/redux/studio.test.js
View file @
82da633e
...
...
@@ -167,4 +167,15 @@ describe('studio comments', () => {
expect
(
selectCanRestoreComment
(
state
)).
toBe
(
expected
);
});
});
describe
(
'
can follow a studio
'
,
()
=>
{
test
.
each
([
[
'
logged in
'
,
true
],
[
'
unconfirmed
'
,
true
],
[
'
logged out
'
,
false
]
])(
'
%s: %s
'
,
(
role
,
expected
)
=>
{
setStateByRole
(
role
);
expect
(
selectCanFollowStudio
(
state
)).
toBe
(
expected
);
});
});
});
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