Skip to content

Commit

Permalink
#865 Write projects overview in react/javascript
Browse files Browse the repository at this point in the history
* add SearchField
* add deleteConfig function
* implement backend search
* add filter by user condition to configReducer for site admins
  • Loading branch information
CalamityC committed May 17, 2024
1 parent d2fd488 commit a3b4ad0
Show file tree
Hide file tree
Showing 12 changed files with 140 additions and 107 deletions.
55 changes: 54 additions & 1 deletion rdmo/core/assets/js/components/SearchAndFilter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,58 @@
import React from 'react'
import PropTypes from 'prop-types'
import { debounce } from 'lodash'

const SearchField = ({ value, onChange, onSearch, placeholder, delay }) => {
const handleSearch = debounce(() => {
onSearch(value)
}, delay ?? 300)

const handleChange = (newValue) => {
onChange(newValue)
}

const handleButtonClick = () => {
onChange('')
handleSearch()
}

const handleKeyDown = (event) => {
if (event.key === 'Enter') {
handleSearch()
}
}

return (
<div className="form-group mb-0">
<div className="input-group">
<input
type="text"
className="form-control"
placeholder={placeholder}
value={value}
onChange={(e) => handleChange(e.target.value)}
onKeyDown={handleKeyDown}
/>
<span className="input-group-btn">
<button className="btn btn-default" onClick={handleButtonClick}>
<span className="fa fa-times"></span>
</button>
<button className="btn btn-primary" onClick={handleSearch}>
Search
</button>
</span>
</div>
</div>
)
}

SearchField.propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onSearch: PropTypes.func.isRequired,
placeholder: PropTypes.string.isRequired,
delay: PropTypes.number, // Optional: Specify the delay time in milliseconds
}

const TextField = ({ value, onChange, placeholder }) => {
return (
Expand Down Expand Up @@ -43,4 +96,4 @@ Select.propTypes = {
placeholder: PropTypes.string
}

export { TextField, Select }
export { SearchField, Select, TextField }
6 changes: 5 additions & 1 deletion rdmo/projects/assets/js/actions/configActions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { UPDATE_CONFIG } from './types'
import { DELETE_CONFIG, UPDATE_CONFIG } from './types'

export function updateConfig(path, value) {
return {type: UPDATE_CONFIG, path, value}
}

export function deleteConfig(path) {
return {type: DELETE_CONFIG, path}
}
6 changes: 3 additions & 3 deletions rdmo/projects/assets/js/actions/projectsActions.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

import ProjectsApi from '../api/ProjectsApi'
import { FETCH_PROJECTS_ERROR, FETCH_PROJECTS_INIT, FETCH_PROJECTS_SUCCESS }
from './types'

export function fetchAllProjects(params) {
return function(dispatch) {
export function fetchAllProjects() {
return function(dispatch, getState) {
const params = getState().config.params
dispatch(fetchProjectsInit())
const action = (dispatch) => ProjectsApi.fetchProjects(params || {})
.then(projects => {
Expand Down
9 changes: 5 additions & 4 deletions rdmo/projects/assets/js/actions/types.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export const FETCH_PROJECTS_SUCCESS = 'projects/fetchProjectsSuccess'
export const FETCH_PROJECTS_ERROR = 'projects/fetchProjectsError'
export const FETCH_PROJECTS_INIT = 'projects/fetchProjectsInit'
export const DELETE_CONFIG = 'config/deleteConfig'
export const FETCH_CURRENT_USER_ERROR = 'currentUser/fetchCurrentUserError'
export const FETCH_CURRENT_USER_INIT = 'currentUser/fetchCurrentUserInit'
export const FETCH_CURRENT_USER_SUCCESS = 'currentUser/fetchCurrentUserSuccess'
export const FETCH_CURRENT_USER_ERROR = 'currentUser/fetchCurrentUserError'
export const FETCH_PROJECTS_ERROR = 'projects/fetchProjectsError'
export const FETCH_PROJECTS_INIT = 'projects/fetchProjectsInit'
export const FETCH_PROJECTS_SUCCESS = 'projects/fetchProjectsSuccess'
export const UPDATE_CONFIG = 'config/updateConfig'
1 change: 0 additions & 1 deletion rdmo/projects/assets/js/actions/userActions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import AccountsApi from '../api/AccountsApi'
import {
FETCH_CURRENT_USER_ERROR,
Expand Down
32 changes: 13 additions & 19 deletions rdmo/projects/assets/js/components/helper/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,23 @@ import { get } from 'lodash'
const Table = ({
cellFormatters,
columnWidths,
config,
configActions,
data,
headerFormatters,
initialRows = 20,
refetchProjects,
projectsActions,
rowsToLoad = 10,
sortableColumns,
/* order of elements in 'visibleColumns' corresponds to order of columns in table */
visibleColumns,
configActions,
config
}) => {

const displayedRows = get(config, 'table.rows', '')
const displayedRows = get(config, 'table.rows')
// console.log('displayedRows %o', displayedRows)
if (displayedRows === null || displayedRows === undefined) {
configActions.updateConfig('table.rows', initialRows.toString())
}
// console.log('displayedRows %o', displayedRows)
console.log('displayedRows %o', displayedRows)

const extractSortingParams = (params) => {
const { ordering } = params || {}
Expand All @@ -41,15 +40,11 @@ const Table = ({
const { sortColumn, sortOrder } = extractSortingParams(params)

const loadMore = () => {
console.log('Load More')
configActions.updateConfig('table.rows', (parseInt(displayedRows) + parseInt(rowsToLoad)))
console.log('AFTER load more table.rows', get(config, 'table.rows'))
}

const loadAll = () => {
console.log('Load All')
configActions.updateConfig('table.rows', data.length)
console.log('AFTER load all table.rows', get(config, 'table.rows'))
}

const handleHeaderClick = (column) => {
Expand All @@ -58,9 +53,8 @@ const Table = ({
configActions.updateConfig('params.ordering', sortOrder === 'asc' ? `-${column}` : column)
} else {
configActions.updateConfig('params.ordering', column)

}
refetchProjects(params)
projectsActions.fetchAllProjects()
}
}

Expand Down Expand Up @@ -103,7 +97,7 @@ const Table = ({
}

const renderRows = () => {
const sortedRows = data.slice(0, displayedRows)
const sortedRows = displayedRows ? data.slice(0, displayedRows) : data
return (
<tbody>
{sortedRows.map((row, index) => (
Expand All @@ -125,11 +119,11 @@ const Table = ({
{renderHeaders()}
{renderRows()}
</table>
{displayedRows < data.length && (
{displayedRows && displayedRows < data.length && (
<div className="icon-container ml-auto">
<button onClick={loadMore} className="load-more-btn">
<button onClick={loadMore} className="btn">
{gettext('Load More')}
</button><button onClick={loadAll} className="load-more-btn">
</button><button onClick={loadAll} className="btn">
{gettext('Load All')}
</button>
</div>
Expand All @@ -141,15 +135,15 @@ const Table = ({
Table.propTypes = {
cellFormatters: PropTypes.object,
columnWidths: PropTypes.arrayOf(PropTypes.string),
config: PropTypes.object,
configActions: PropTypes.object,
data: PropTypes.arrayOf(PropTypes.object).isRequired,
headerFormatters: PropTypes.object,
initialRows: PropTypes.number,
refetchProjects: PropTypes.func,
projectsActions: PropTypes.object,
rowsToLoad: PropTypes.number,
sortableColumns: PropTypes.arrayOf(PropTypes.string),
visibleColumns: PropTypes.arrayOf(PropTypes.string),
configActions: PropTypes.object,
config: PropTypes.object
}

export default Table
11 changes: 11 additions & 0 deletions rdmo/projects/assets/js/components/helper/userIsManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import siteId from 'rdmo/core/assets/js/utils/siteId'

const userIsManager = (currentUser) => {
if (currentUser.is_superuser ||
(currentUser.role && currentUser.role.manager && currentUser.role.manager.some(manager => manager.id === siteId))) {
return true
}
return false
}

export default userIsManager
88 changes: 32 additions & 56 deletions rdmo/projects/assets/js/components/main/Projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React from 'react'
import PropTypes from 'prop-types'
import Table from '../helper/Table'
import Link from 'rdmo/core/assets/js/components/Link'
import { TextField } from 'rdmo/core/assets/js/components/SearchAndFilter'
import { SearchField } from 'rdmo/core/assets/js/components/SearchAndFilter'
import language from 'rdmo/core/assets/js/utils/language'
import siteId from 'rdmo/core/assets/js/utils/siteId'
import userIsManager from '../helper/userIsManager'
import { get, isNil } from 'lodash'

const Projects = ({ config, configActions, currentUserObject, projectsActions, projectsObject }) => {
Expand All @@ -13,60 +13,32 @@ const Projects = ({ config, configActions, currentUserObject, projectsActions, p
const { myProjects } = config

const displayedRows = get(config, 'table.rows')
// const params = get(config, 'params', {})

const refetchProjects = (params) => projectsActions.fetchAllProjects(params)

const currentUserId = currentUser.id
const isManager = (currentUser && currentUser.is_superuser) ||
(currentUser.role && currentUser.role.manager && currentUser.role.manager.some(manager => manager.id === siteId))

const findCurrentUsersProjects = () => {
return projects.filter(project => {
const propertiesToCheck = ['authors', 'guests', 'managers', 'owners']

for (let prop of propertiesToCheck) {
if (project[prop].some(user => user.id === currentUserId)) {
return true
}
}
const isManager = userIsManager(currentUser)

return false
})
const searchString = get(config, 'params.search', '')
const updateSearchString = (value) => {
const normedValue = value.toLowerCase()
normedValue ? configActions.updateConfig('params.search', normedValue) : configActions.deleteConfig('params.search')
}

const contentData = (isManager && myProjects)
? findCurrentUsersProjects()
: projects

const searchString = get(config, 'filter.title', '')
const updateSearchString = (value) => configActions.updateConfig('filter.title', value)

const baseUrl = window.location.origin

const langOptions = language == 'de' ?
{ hour12: false } :
{ hour12: true }

const viewLinkText = myProjects ? gettext('View all projects') : gettext('View my projects')
const headline = myProjects ? gettext('My projects') : gettext('All projects')

const filterByTitleSearch = (projects, searchString) => {
if (searchString) {
const lowercaseSearch = searchString.toLowerCase()
return projects.filter((project) =>
getTitlePath(project.title, project).toLowerCase().includes(lowercaseSearch)
)
} else {
return projects
}
}

const handleViewClick = () => {
configActions.updateConfig('myProjects', !myProjects)
myProjects ? configActions.deleteConfig('params.user') : configActions.updateConfig('params.user', currentUserId)
projectsActions.fetchAllProjects()()
}

const handleNewClick = () => {
console.log('New button clicked')
window.location.href = `${baseUrl}/projects/create`
}

Expand All @@ -84,7 +56,7 @@ const Projects = ({ config, configActions, currentUserObject, projectsActions, p
}

const getParentPath = (parentId, pathArray = []) => {
const parent = contentData.find((project) => project.id === parentId)
const parent = projects.find((project) => project.id === parentId)
if (parent) {
const { title: parentTitle, parent: grandParentId } = parent
pathArray.unshift(parentTitle)
Expand Down Expand Up @@ -120,7 +92,7 @@ const Projects = ({ config, configActions, currentUserObject, projectsActions, p
)
}

const sortableColumns = ['created', 'owner', 'role', 'title', 'updated']
const sortableColumns = ['created', 'owner', 'progress', 'role', 'title', 'updated']

/* order of elements in 'visibleColumns' corresponds to order of columns in table */
let visibleColumns = ['title', 'progress', 'updated', 'actions']
Expand All @@ -137,9 +109,9 @@ const Projects = ({ config, configActions, currentUserObject, projectsActions, p

const headerFormatters = {
title: {render: () => gettext('Name')},
role: {render: () => gettext('Role'), sortRawContent: false},
owner: {render: () => gettext('Owner'), sortRawContent: false} ,
progress: {render: () => gettext('Progress'), sortRawContent: false},
role: {render: () => gettext('Role')},
owner: {render: () => gettext('Owner')} ,
progress: {render: () => gettext('Progress')},
created: {render: () => gettext('Created')},
updated: {render: () => gettext('Last changed')},
actions: {render: () => null},
Expand Down Expand Up @@ -185,27 +157,30 @@ const Projects = ({ config, configActions, currentUserObject, projectsActions, p
}
}

const filteredProjects = filterByTitleSearch(contentData, searchString)

return (
<>
<div className="mb-10" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 className="ml-10 mt-0">{headline}</h2>
<div className="icon-container ml-auto">
<button className="element-button mr-10" onClick={handleNewClick}>
{gettext('New project')}
<button className="btn btn-link mr-10" onClick={handleNewClick}>
<i className="fa fa-plus" aria-hidden="true"></i> {gettext('New project')}
</button>
<button className="element-button" onClick={handleImportClick}>
{gettext('Import project')}
<button className="btn btn-link" onClick={handleImportClick}>
<i className="fa fa-download" aria-hidden="true"></i> {gettext('Import project')}
</button>
</div>
</div>
<span>{displayedRows>filteredProjects.length ? filteredProjects.length : displayedRows} {gettext('of')} {filteredProjects.length} {gettext('projects are displayed')}</span>
<span>{displayedRows>projects.length ? projects.length : displayedRows} {gettext('of')} {projects.length} {gettext('projects are displayed')}</span>
{/* <div className="input-group mb-20"></div> */}
<div className="panel-body">
<div className="row">
<TextField value={searchString} onChange={updateSearchString}
placeholder={gettext('Search projects')} />
<SearchField
value={searchString}
onChange={updateSearchString}
onSearch={projectsActions.fetchAllProjects}
placeholder={gettext('Search projects')}
delay={300}
/>
</div>

</div>
Expand All @@ -219,13 +194,14 @@ const Projects = ({ config, configActions, currentUserObject, projectsActions, p
<Table
cellFormatters={cellFormatters}
columnWidths={columnWidths}
data={filteredProjects}
config={config}
configActions={configActions}
data={projects}
headerFormatters={headerFormatters}
refetchProjects={refetchProjects}
projectsActions={projectsActions}
sortableColumns={sortableColumns}
visibleColumns={visibleColumns}
configActions={configActions}
config={config}

/>
</>
)
Expand Down
Loading

0 comments on commit a3b4ad0

Please sign in to comment.