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 16 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'
188 changes: 160 additions & 28 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
@@ -1,4 +1,4 @@
import { isEmpty, isNil } from 'lodash'
import { first, isEmpty, isNil } from 'lodash'

import PageApi from '../api/PageApi'
import ProjectApi from '../api/ProjectApi'
Expand Down 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 @@ -363,26 +366,73 @@ export function updateValue(value, attrs, store = true) {
}
}

export function copyValue(value) {
export function copyValue(...originalValues) {
const firstValue = first(originalValues)
const pendingId = `copyValue/${firstValue.attribute}/${firstValue.set_prefix}/${firstValue.set_index}`

return (dispatch, getState) => {
const sets = getState().interview.sets
const values = getState().interview.values

sets.filter((set) => (
(set.set_prefix == value.set_prefix) &&
(set.set_index != value.set_index)
)).forEach((set) => {
const sibling = values.find((v) => (
(v.attribute == value.attribute) &&
(v.set_prefix == set.set_prefix) &&
(v.set_index == set.set_index) &&
(v.collection_index == value.collection_index)
))

if (isNil(sibling)) {
dispatch(storeValue(ValueFactory.create({ ...value, set_index: set.set_index })))
} else if (isEmptyValue(sibling)) {
dispatch(storeValue(ValueFactory.update(sibling, value)))
dispatch(addToPending(pendingId))

const { sets, values } = getState().interview

// create copies for each value for all it's empty siblings
const copies = originalValues.reduce((copies, value) => {
return [
...copies,
...sets.filter((set) => (
(set.set_prefix == value.set_prefix) &&
(set.set_index != value.set_index)
)).map((set) => {
const siblingIndex = values.findIndex((v) => (
(v.attribute == value.attribute) &&
(v.set_prefix == set.set_prefix) &&
(v.set_index == set.set_index) &&
(v.collection_index == value.collection_index)
))

const sibling = siblingIndex > 0 ? values[siblingIndex] : null

if (isNil(sibling)) {
return [ValueFactory.create({ ...value, set_index: set.set_index }), siblingIndex]
} else if (isEmptyValue(sibling)) {
// the spread operator { ...sibling } does prevent an update in place
return [ValueFactory.update({ ...sibling }, value), siblingIndex]
} else {
return null
}
}).filter((value) => !isNil(value))
]
}, [])

// dispatch storeValueInit for each of the updated values,
// created values have valueIndex -1 and will be skipped
// eslint-disable-next-line no-unused-vars
copies.forEach(([value, valueIndex]) => dispatch(storeValueInit(valueIndex)))

// loop over all copies and store the values on the server
// afterwards fetchNavigation, updateProgress and check refresh once
return Promise.all(
copies.map(([value, valueIndex]) => {
return ValueApi.storeValue(projectId, value)
.then((value) => dispatch(storeValueSuccess(value, valueIndex)))
})
).then(() => {
dispatch(removeFromPending(pendingId))

const page = getState().interview.page
const sets = getState().interview.sets
const question = page.questions.find((question) => question.attribute === firstValue.attribute)
const refresh = question && question.optionsets.some((optionset) => optionset.has_refresh)

dispatch(fetchNavigation(page))
dispatch(updateProgress())

if (refresh) {
// if the refresh flag is set, reload all values for the page,
// resolveConditions will be called in fetchValues
dispatch(fetchValues(page))
} else {
dispatch(resolveConditions(page, sets))
}
})
}
Expand Down Expand Up @@ -459,8 +509,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 +526,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 @@ -524,10 +574,11 @@ export function deleteSet(set, setValue) {

if (sets.length > 1) {
const index = sets.indexOf(set)
if (index > 0) {
if (index < sets.length - 1) {
dispatch(activateSet(sets[index + 1]))
} else {
// If it's the last set, activate the new last set
dispatch(activateSet(sets[index - 1]))
} else if (index == 0) {
dispatch(activateSet(sets[1]))
}
}

Expand Down Expand Up @@ -559,3 +610,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 {
value.set_prefix = value.set_prefix.split('|').map((sp, idx) => {
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.

// 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
Loading
Loading