Skip to content
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

feat(interview): Copy dataset [3] #1128

Merged
merged 18 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions rdmo/core/assets/js/components/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ const Modal = ({ title, show, modalProps, submitLabel, submitProps, onClose, onS
<BootstrapModal.Header closeButton>
<h2 className="modal-title">{title}</h2>
</BootstrapModal.Header>
<BootstrapModal.Body>
{ children }
</BootstrapModal.Body>
{
children && (
<BootstrapModal.Body>
{ children }
</BootstrapModal.Body>
)
}
<BootstrapModal.Footer>
<button type="button" className="btn btn-default" onClick={onClose}>
{gettext('Close')}
Expand Down
4 changes: 4 additions & 0 deletions rdmo/projects/assets/js/interview/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ export const CREATE_SET = 'CREATE_SET'
export const DELETE_SET_INIT = 'DELETE_SET_INIT'
export const DELETE_SET_SUCCESS = 'DELETE_SET_SUCCESS'
export const DELETE_SET_ERROR = 'DELETE_SET_ERROR'

export const COPY_SET_INIT = 'COPY_SET_INIT'
export const COPY_SET_SUCCESS = 'COPY_SET_SUCCESS'
export const COPY_SET_ERROR = 'COPY_SET_ERROR'
94 changes: 89 additions & 5 deletions rdmo/projects/assets/js/interview/actions/interviewActions.js
MyPyDavid marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ import {
CREATE_SET,
DELETE_SET_INIT,
DELETE_SET_SUCCESS,
DELETE_SET_ERROR
DELETE_SET_ERROR,
COPY_SET_INIT,
COPY_SET_SUCCESS,
COPY_SET_ERROR
} from './actionTypes'

import { updateConfig } from 'rdmo/core/assets/js/actions/configActions'
Expand Down Expand Up @@ -459,8 +462,8 @@ export function createSet(attrs) {
// create a value for the text if the page has an attribute
const value = isNil(attrs.attribute) ? null : ValueFactory.create(attrs)

// create an action to be called immediately or after saving the value
const createSetSuccess = (value) => {
// create a callback function to be called immediately or after saving the value
const createSetCallback = (value) => {
dispatch(activateSet(set))

const state = getState().interview
Expand All @@ -476,12 +479,12 @@ export function createSet(attrs) {
}

if (isNil(value)) {
return createSetSuccess()
return createSetCallback()
} else {
return dispatch(storeValue(value)).then(() => {
const storedValue = getState().interview.values.find((v) => compareValues(v, value))
if (!isNil(storedValue)) {
createSetSuccess(storedValue)
createSetCallback(storedValue)
}
})
}
Expand Down Expand Up @@ -559,3 +562,84 @@ export function deleteSetSuccess(set) {
export function deleteSetError(errors) {
return {type: DELETE_SET_ERROR, errors}
}

export function copySet(currentSet, currentSetValue, attrs) {
const pendingId = `copySet/${currentSet.set_prefix}/${currentSet.set_index}`

return (dispatch, getState) => {
dispatch(addToPending(pendingId))
dispatch(copySetInit())

// create a new set
const set = SetFactory.create(attrs)

// create a value for the text if the page has an attribute
const value = isNil(attrs.attribute) ? null : ValueFactory.create(attrs)

// create a callback function to be called immediately or after saving the value
const copySetCallback = (setValues) => {
dispatch(activateSet(set))

const state = getState().interview

const page = state.page
const values = [...state.values, ...setValues]
const sets = gatherSets(values)

initSets(sets, page)
initValues(sets, values, page)

return dispatch({type: COPY_SET_SUCCESS, values, sets})
}

let promise
if (isNil(value)) {
// gather all values for the currentSet and it's descendants
const currentValues = getDescendants(getState().interview.values, currentSet)

// store each value in currentSet with the new set_index
promise = Promise.all(
currentValues.filter((currentValue) => !isEmptyValue(currentValue)).map((currentValue) => {
const value = {...currentValue}
const setPrefixLength = isEmpty(set.set_prefix) ? 0 : set.set_prefix.split('|').length

if (value.set_prefix == set.set_prefix) {
value.set_index = set.set_index
} else {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MyPyDavid This is complicated, can you check if this is understandable to you. I just fixed a bug here. This front-end part is for copying blocks/questionset (and tabs/pages without attribute).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, yes, I can not make any cheese out of this but the comments help ;).
I've asked a LLM to explain it to me..
You don't want to split it a bit more into functions with meaningful names?

function adjustSetPrefix(value, set, newSetPrefixDepth) {
  value.set_prefix = value.set_prefix.split('|').map((segment, idx) => {
    return idx === newSetPrefixDepth ? set.set_index : segment;
  }).join('|');
  return value;
}
export function copySet(currentSet, currentSetValue, attrs) {
  const pendingId = `copySet/${currentSet.set_prefix}/${currentSet.set_index}`;

  return (dispatch, getState) => {
    dispatch(addToPending(pendingId));
    dispatch(copySetInit());

    const set = createNewSet(attrs); // Encapsulates set creation logic
    const value = prepareSetValue(attrs); // Encapsulates value preparation logic

    if (isNil(value)) {
      handleSetWithoutAttribute(dispatch, getState, pendingId, currentSet, set, finalizeCopySet(dispatch, getState));
    } else {
      handleSetWithAttribute(dispatch, pendingId, currentSetValue, value, finalizeCopySet(dispatch, getState));
    }
  };
}

Copy link
Member Author

@jochenklar jochenklar Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will keep it like it is. Moving dispatch into functions somehow feels wrong. To me it is actually harder to read with the functions.

value.set_prefix = value.set_prefix.split('|').map((sp, idx) => {
// for the set_prefix of the new value, set the number at the position, which is one more
// than the length of the set_prefix of the new (and old) set, to the set_index of the new set.
// since idx counts from 0, this equals setPrefixLength
return (idx == setPrefixLength) ? set.set_index : sp
}).join('|')
}

delete value.id
return ValueApi.storeValue(projectId, value)
})
)
} else {
promise = ValueApi.copySet(projectId, currentSetValue, value)
}

return promise.then((values) => {
dispatch(removeFromPending(pendingId))
dispatch(copySetCallback(values))
}).catch((errors) => {
dispatch(removeFromPending(pendingId))
dispatch(copySetError(errors))
})
}
}

export function copySetInit() {
return {type: COPY_SET_INIT}
}

export function copySetSuccess(values, sets) {
return {type: COPY_SET_SUCCESS, values, sets}
}

export function copySetError(errors) {
return {type: COPY_SET_ERROR, errors}
}
8 changes: 6 additions & 2 deletions rdmo/projects/assets/js/interview/api/ValueApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ class ValueApi extends BaseApi {
}
}

static deleteSet(projectId, value) {
return this.delete(`/api/v1/projects/projects/${projectId}/values/${value.id}/set/`)
static copySet(projectId, currentSetValue, setValue) {
return this.post(`/api/v1/projects/projects/${projectId}/values/${currentSetValue.id}/set/`, setValue)
}

static deleteSet(projectId, setValue) {
return this.delete(`/api/v1/projects/projects/${projectId}/values/${setValue.id}/set/`)
}

}
Expand Down
18 changes: 13 additions & 5 deletions rdmo/projects/assets/js/interview/components/main/page/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ import PageHead from './PageHead'

const Page = ({ config, templates, overview, page, sets, values, fetchPage,
createValue, updateValue, deleteValue, copyValue,
activateSet, createSet, updateSet, deleteSet }) => {
activateSet, createSet, updateSet, deleteSet, copySet }) => {

const currentSetPrefix = ''
const currentSetIndex = page.is_collection ? get(config, 'page.currentSetIndex', 0) : 0
const currentSet = sets.find((set) => (set.set_prefix == currentSetPrefix && set.set_index == currentSetIndex)) ||
sets.find((set) => (set.set_prefix == currentSetPrefix && set.set_index == 0)) // sanity check
let currentSetIndex = page.is_collection ? get(config, 'page.currentSetIndex', 0) : 0
let currentSet = sets.find((set) => (set.set_prefix == currentSetPrefix && set.set_index == currentSetIndex))

// sanity check
if (isNil(currentSet)) {
currentSetIndex = 0
currentSet = sets.find((set) => (set.set_prefix == currentSetPrefix && set.set_index == 0))
}

const isManager = (overview.is_superuser || overview.is_editor || overview.is_reviewer)

Expand All @@ -36,6 +41,7 @@ const Page = ({ config, templates, overview, page, sets, values, fetchPage,
createSet={createSet}
updateSet={updateSet}
deleteSet={deleteSet}
copySet={copySet}
/>
<div className="row">
{
Expand All @@ -55,6 +61,7 @@ const Page = ({ config, templates, overview, page, sets, values, fetchPage,
createSet={createSet}
updateSet={updateSet}
deleteSet={deleteSet}
copySet={copySet}
createValue={createValue}
updateValue={updateValue}
deleteValue={deleteValue}
Expand Down Expand Up @@ -111,11 +118,12 @@ Page.propTypes = {
createValue: PropTypes.func.isRequired,
updateValue: PropTypes.func.isRequired,
deleteValue: PropTypes.func.isRequired,
copyValue: PropTypes.func.isRequired,
activateSet: PropTypes.func.isRequired,
createSet: PropTypes.func.isRequired,
updateSet: PropTypes.func.isRequired,
deleteSet: PropTypes.func.isRequired,
copyValue: PropTypes.func.isRequired
copySet: PropTypes.func.isRequired
}

export default Page
43 changes: 35 additions & 8 deletions rdmo/projects/assets/js/interview/components/main/page/PageHead.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import useModal from 'rdmo/core/assets/js/hooks/useModal'
import PageHeadDeleteModal from './PageHeadDeleteModal'
import PageHeadFormModal from './PageHeadFormModal'

const PageHead = ({ templates, page, sets, values, currentSet, activateSet, createSet, updateSet, deleteSet }) => {
const PageHead = ({ templates, page, sets, values, currentSet,
activateSet, createSet, updateSet, deleteSet, copySet }) => {

const currentSetValue = isNil(currentSet) ? null : (
values.find((value) => (
Expand All @@ -20,6 +21,7 @@ const PageHead = ({ templates, page, sets, values, currentSet, activateSet, crea
const {show: showCreateModal, open: openCreateModal, close: closeCreateModal} = useModal()
const {show: showUpdateModal, open: openUpdateModal, close: closeUpdateModal} = useModal()
const {show: showDeleteModal, open: openDeleteModal, close: closeDeleteModal} = useModal()
const {show: showCopyModal, open: openCopyModal, close: closeCopyModal} = useModal()

const handleActivateSet = (event, set) => {
event.preventDefault()
Expand Down Expand Up @@ -53,6 +55,16 @@ const PageHead = ({ templates, page, sets, values, currentSet, activateSet, crea
closeDeleteModal()
}

const handleCopySet = (text) => {
copySet(currentSet, currentSetValue, {
attribute: page.attribute,
set_index: last(sets) ? last(sets).set_index + 1 : 0,
set_collection: page.is_collection,
text
})
closeCopyModal()
}

return page.is_collection && (
<div className="interview-page-tabs">
<Html html={templates.project_interview_page_help} />
Expand Down Expand Up @@ -82,12 +94,13 @@ const PageHead = ({ templates, page, sets, values, currentSet, activateSet, crea
</li>
</ul>
<div className="interview-page-tabs-buttons">
{
page.attribute && (
<button className="btn-link fa fa-pencil" title={gettext('Edit tab')} onClick={openUpdateModal} />
)
}
<button className="btn-link fa fa-trash" title={gettext('Remove tab')} onClick={openDeleteModal} />
{
page.attribute && (
<button className="btn-link fa fa-pencil" title={gettext('Edit tab')} onClick={openUpdateModal} />
)
}
<button className="btn-link fa fa-copy" title={gettext('Copy tab')} onClick={openCopyModal} />
<button className="btn-link fa fa-trash" title={gettext('Remove tab')} onClick={openDeleteModal} />
</div>
</>
) : (
Expand All @@ -99,15 +112,28 @@ const PageHead = ({ templates, page, sets, values, currentSet, activateSet, crea

<PageHeadFormModal
title={capitalize(page.verbose_name)}
submitLabel={gettext('Create')}
submitColor="success"
show={showCreateModal}
initial={isNil(page.attribute) ? null : ''}
onClose={closeCreateModal}
onSubmit={handleCreateSet}
/>
<PageHeadFormModal
title={capitalize(page.verbose_name)}
submitLabel={gettext('Copy')}
submitColor="info"
show={showCopyModal}
initial={isNil(page.attribute) ? null : ''}
onClose={closeCopyModal}
onSubmit={handleCopySet}
/>
{
currentSetValue && (
<PageHeadFormModal
title={capitalize(page.verbose_name)}
submitLabel={gettext('Update')}
submitColor="primary"
show={showUpdateModal}
initial={currentSetValue.text}
onClose={closeUpdateModal}
Expand All @@ -134,7 +160,8 @@ PageHead.propTypes = {
activateSet: PropTypes.func.isRequired,
createSet: PropTypes.func.isRequired,
updateSet: PropTypes.func.isRequired,
deleteSet: PropTypes.func.isRequired
deleteSet: PropTypes.func.isRequired,
copySet: PropTypes.func.isRequired
}

export default PageHead
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,11 @@ import Modal from 'rdmo/core/assets/js/components/Modal'
import useFocusEffect from '../../../hooks/useFocusEffect'


const PageHeadFormModal = ({ title, show, initial, onClose, onSubmit }) => {
const PageHeadFormModal = ({ title, submitLabel, submitColor, show, initial, onClose, onSubmit }) => {

const ref = useRef(null)
const [inputValue, setInputValue] = useState('')
const [hasError, setHasError] = useState(false)
const submitLabel = isEmpty(initial) ? gettext('Create') : gettext('Update')
const submitProps = {
className: classNames('btn', {
'btn-success': isEmpty(initial),
'btn-primary': !isEmpty(initial),
})
}

const handleSubmit = () => {
if (isEmpty(inputValue) && !isNil(initial)) {
Expand Down Expand Up @@ -46,7 +39,7 @@ const PageHeadFormModal = ({ title, show, initial, onClose, onSubmit }) => {
useFocusEffect(ref, show)

return (
<Modal title={title} show={show} submitLabel={submitLabel} submitProps={submitProps}
<Modal title={title} show={show} submitLabel={submitLabel} submitProps={{className: `btn btn-${submitColor}`}}
onClose={onClose} onSubmit={handleSubmit} disableSubmit={hasError}>
{
isNil(initial) ? (
Expand Down Expand Up @@ -82,6 +75,8 @@ const PageHeadFormModal = ({ title, show, initial, onClose, onSubmit }) => {

PageHeadFormModal.propTypes = {
title: PropTypes.string.isRequired,
submitLabel: PropTypes.string.isRequired,
submitColor: PropTypes.string.isRequired,
show: PropTypes.bool.isRequired,
initial: PropTypes.string,
onClose: PropTypes.func.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import Question from '../question/Question'

import QuestionSetAddSet from './QuestionSetAddSet'
import QuestionSetAddSetHelp from './QuestionSetAddSetHelp'
import QuestionSetCopySet from './QuestionSetCopySet'
import QuestionSetHelp from './QuestionSetHelp'
import QuestionSetHelpTemplate from './QuestionSetHelpTemplate'
import QuestionSetRemoveSet from './QuestionSetRemoveSet'

const QuestionSet = ({ templates, questionset, sets, values, disabled, isManager,
parentSet, createSet, updateSet, deleteSet,
parentSet, createSet, updateSet, deleteSet, copySet,
createValue, updateValue, deleteValue, copyValue }) => {

const setPrefix = getChildPrefix(parentSet)
Expand All @@ -35,7 +36,8 @@ const QuestionSet = ({ templates, questionset, sets, values, disabled, isManager
currentSets.map((set, setIndex) => (
<div key={setIndex} className="interview-block">
<div className="interview-block-options">
<QuestionSetRemoveSet questionset={questionset} set={set} deleteSet={deleteSet} />
<QuestionSetCopySet questionset={questionset} sets={sets} currentSet={set} copySet={copySet} />
<QuestionSetRemoveSet questionset={questionset} currentSet={set} deleteSet={deleteSet} />
</div>
<div className="row">
{
Expand All @@ -55,6 +57,7 @@ const QuestionSet = ({ templates, questionset, sets, values, disabled, isManager
createSet={createSet}
updateSet={updateSet}
deleteSet={deleteSet}
copySet={copySet}
createValue={createValue}
updateValue={updateValue}
deleteValue={deleteValue}
Expand Down Expand Up @@ -115,6 +118,7 @@ QuestionSet.propTypes = {
createSet: PropTypes.func.isRequired,
updateSet: PropTypes.func.isRequired,
deleteSet: PropTypes.func.isRequired,
copySet: PropTypes.func.isRequired,
createValue: PropTypes.func.isRequired,
updateValue: PropTypes.func.isRequired,
deleteValue: PropTypes.func.isRequired,
Expand Down
Loading
Loading