Skip to content

Commit

Permalink
refactor(library locking): migrate files to TypeScript (#5405)
Browse files Browse the repository at this point in the history
### 💭 Notes
Migrating files to TS introduced some non-trivial changes, like:
- `enum` introduction required some parts of the code to be rewritten
(more `if`'s etc.)
- mocks file had to be rewritten from scratch, as the asset responses
there were extremely outdated and incomplete

### 👀 Preview steps
There is a nice long [Library Locking Technical Reference
article](https://kobotoolbox.github.io/articles/library-locking-technical-reference)
that I feel would be very helpful here :)

Some useful templates for testing:
- [locking template 1
(lock_all).xls](https://github.com/user-attachments/files/18306729/locking.template.1.lock_all.xls)
- [locking template 2 (profile,
language_edit).xls](https://github.com/user-attachments/files/18306730/locking.template.2.profile.language_edit.xls)
- [locking template 3
(profile).xlsx](https://github.com/user-attachments/files/18306731/locking.template.3.profile.xlsx)

Testing:
1. Upload one of the above templates (__no drag&drop!__) using "My
Library" > "NEW" > "Upload file" with "Upload as template" checked
(__important!__ as without this, all locking features would be stripped
by BE code).
6. Open the template in Form Builder
7. 🟢 notice that locking features work as expected (see Technical
Reference linked above)
8. Go to "My Library".
9. For the locked template in the list use "Create project" button.
10. Open the newly created project in Form Builder.
11. 🟢 notice that locking features work as expected

There are three __fixes__ in this PR (_mea culpa_ for not splitting).

__First fix__: I changed how `isAssetLockable` works, by extending it to
also include `template`s (previously only allowed `survey`s). This
function works like a feature flag -ish, and I've noticed that with a
template that is fully locked (has `lock_all: true`), the Form Builder
sidebar wasn't all disabled.

__Second fix__: in the same area as above, Background audio setting was
not being disabled, and it should be with `lock_all`. I suspect we've
added locking before adding Background audio in there and forgot to
include it.

__Third fix__: I no longer disabled "Add from Library" and "Layout &
Settings" buttons for `lock_all` forms. There is an annoying tiny UX bug
that was happening with current code:
1. Open a non-locked form in Form Builder and open "Layout & Settings"
sidebar
2. The sidebar being opened is being remembered by FE code
3. Open a locked form in Form Builder and notice the sidebar is open
4. 🔴 Notice you can't close the sidebar, because the toggle button is
disabled
5. 🟢 Notice that with this PR it is now possible to close sidebar (and
everything inside the sideabar is already disabled, so no harm if user
opens it)

---------

Co-authored-by: Kalvis Kalniņš <[email protected]>
  • Loading branch information
magicznyleszek and Akuukis authored Jan 6, 2025
1 parent 7654a8b commit 522a775
Show file tree
Hide file tree
Showing 16 changed files with 1,139 additions and 920 deletions.
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import React from 'react';
import autoBind from 'react-autobind';
import bem from 'js/bem';
import {ASSET_TYPES} from 'js/constants';
import {
isAssetLocked,
isAssetAllLocked,
getFormFeatures,
} from 'js/components/locking/lockingUtils';
import type {AssetResponse} from 'jsapp/js/dataInterface';

/**
* @prop {object} asset
*/
class FormLockedMessage extends React.Component {
constructor(props){
interface FormLockedMessageProps {
asset: AssetResponse;
}
interface FormLockedMessageState {
isOpen: boolean;
}

class FormLockedMessage extends React.Component<
FormLockedMessageProps,
FormLockedMessageState
> {
constructor(props: FormLockedMessageProps){
super(props);
this.state = {
isOpen: false,
};
autoBind(this);
}

toggleMoreInfo(evt) {
toggleMoreInfo(evt: React.TouchEvent) {
evt.preventDefault();
this.setState({isOpen: !this.state.isOpen});
}

getMessageText() {
const isAllLocked = isAssetAllLocked(this.props.asset.content);
const isAllLocked = (
this.props.asset.content !== undefined &&
isAssetAllLocked(this.props.asset.content)
);
if (this.props.asset.asset_type === ASSET_TYPES.template.id) {
if (isAllLocked) {
// fully locked template
Expand All @@ -45,11 +54,14 @@ class FormLockedMessage extends React.Component {
}

renderSeeMore() {
const features = getFormFeatures(this.props.asset.content);
const features = this.props.asset.content ? getFormFeatures(this.props.asset.content) : null;
if (features === null) {
return null;
}

return (
<React.Fragment>
<bem.FormBuilderMessageBox__toggle onClick={this.toggleMoreInfo}>
<bem.FormBuilderMessageBox__toggle onClick={this.toggleMoreInfo.bind(this)}>
{t('see more')}
{this.state.isOpen && <i className='k-icon k-icon-angle-up'/>}
{!this.state.isOpen && <i className='k-icon k-icon-angle-down'/>}
Expand All @@ -63,14 +75,12 @@ class FormLockedMessage extends React.Component {
<label>
{t('Locked functionalities')}
</label>
{features.cants.map((cant) => {
return (
<li key={cant.name}>
<i className='k-icon k-icon-close'/>
{cant.label}
</li>
);
})}
{features.cants.map((cant) => (
<li key={cant.name}>
<i className='k-icon k-icon-close'/>
{cant.label}
</li>
))}
</ul>
}

Expand All @@ -79,14 +89,12 @@ class FormLockedMessage extends React.Component {
<label>
{t('Unlocked functionalities')}
</label>
{features.cans.map((can) => {
return (
<li key={can.name}>
<i className='k-icon k-icon-check'/>
{can.label}
</li>
);
})}
{features.cans.map((can) => (
<li key={can.name}>
<i className='k-icon k-icon-check'/>
{can.label}
</li>
))}
</ul>
}
</div>
Expand All @@ -97,6 +105,10 @@ class FormLockedMessage extends React.Component {
}

render() {
if (!this.props.asset.content) {
return null;
}

if (!isAssetLocked(this.props.asset.content)) {
return null;
}
Expand Down
115 changes: 0 additions & 115 deletions jsapp/js/components/locking/lockingConstants.es6

This file was deleted.

126 changes: 126 additions & 0 deletions jsapp/js/components/locking/lockingConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
export interface AssetLockingProfileDefinition {
name: string;
restrictions: LockingRestrictionName[];
}

export interface IndexedAssetLockingProfileDefinition extends AssetLockingProfileDefinition {
index: number;
}

/**
* When adding or changing restrictions, plase make sure to update all the
* arrays of restrictions below.
*
* NOTE: this list should match a list from:
* https://github.com/kobotoolbox/formpack/blob/master/src/formpack/constants.py
*/
export enum LockingRestrictionName {
choice_add = 'choice_add',
choice_delete = 'choice_delete',
choice_label_edit = 'choice_label_edit',
choice_value_edit = 'choice_value_edit',
choice_order_edit = 'choice_order_edit',
question_delete = 'question_delete',
question_label_edit = 'question_label_edit',
question_settings_edit = 'question_settings_edit',
question_skip_logic_edit = 'question_skip_logic_edit',
question_validation_edit = 'question_validation_edit',
group_delete = 'group_delete',
group_label_edit = 'group_label_edit',
group_question_add = 'group_question_add',
group_question_delete = 'group_question_delete',
group_question_order_edit = 'group_question_order_edit',
group_settings_edit = 'group_settings_edit',
group_skip_logic_edit = 'group_skip_logic_edit',
group_split = 'group_split',
form_appearance = 'form_appearance',
form_meta_edit = 'form_meta_edit',
form_replace = 'form_replace',
group_add = 'group_add',
question_add = 'question_add',
question_order_edit = 'question_order_edit',
language_edit = 'language_edit',
}

export interface LockingRestrictionDefinition {
name: LockingRestrictionName;
label: string;
}

// all restrictions for questions and choices
export const QUESTION_RESTRICTIONS = [
{name: LockingRestrictionName.choice_add, label: t('Add choice to question')},
{name: LockingRestrictionName.choice_delete, label: t('Remove choice from question')},
{name: LockingRestrictionName.choice_label_edit, label: t('Edit choice labels')},
{name: LockingRestrictionName.choice_order_edit, label: t('Change choice order')},
{name: LockingRestrictionName.choice_value_edit, label: t('Edit choice values')},
{name: LockingRestrictionName.question_delete, label: t('Delete question')},
{name: LockingRestrictionName.question_label_edit, label: t('Edit question labels')},
{name: LockingRestrictionName.question_settings_edit, label: t('Edit question settings')},
{name: LockingRestrictionName.question_skip_logic_edit, label: t('Edit skip logic')},
{name: LockingRestrictionName.question_validation_edit, label: t('Edit validation')},
];

// all restrictions for groups
export const GROUP_RESTRICTIONS = [
{name: LockingRestrictionName.group_delete, label: t('Delete entire group')},
{name: LockingRestrictionName.group_label_edit, label: t('Edit group labels')},
{name: LockingRestrictionName.group_question_add, label: t('Add question to group')},
{name: LockingRestrictionName.group_question_delete, label: t('Remove question from group')},
{name: LockingRestrictionName.group_question_order_edit, label: t('Change question order within group')},
{name: LockingRestrictionName.group_settings_edit, label: t('Edit group settings')},
{name: LockingRestrictionName.group_skip_logic_edit, label: t('Edit skip logic')},
{name: LockingRestrictionName.group_split, label: t('Split group')},
];

// all restrictions for form
export const FORM_RESTRICTIONS = [
{name: LockingRestrictionName.form_appearance, label: t('Change form appearance')},
{name: LockingRestrictionName.form_meta_edit, label: t('Change form meta questions')},
{name: LockingRestrictionName.form_replace, label: t('Replace whole form')},
{name: LockingRestrictionName.group_add, label: t('Add group')},
{name: LockingRestrictionName.question_add, label: t('Add question')},
{name: LockingRestrictionName.question_order_edit, label: t('Change question order')},
{name: LockingRestrictionName.language_edit, label: t('Change languages')},
];

// currently lock_all has all restrictions,
// but we want to be flexible, so we use an array
export const LOCK_ALL_RESTRICTION_NAMES = [
LockingRestrictionName.choice_add,
LockingRestrictionName.choice_delete,
LockingRestrictionName.choice_label_edit,
LockingRestrictionName.choice_order_edit,
LockingRestrictionName.choice_value_edit,
LockingRestrictionName.question_delete,
LockingRestrictionName.question_label_edit,
LockingRestrictionName.question_settings_edit,
LockingRestrictionName.question_skip_logic_edit,
LockingRestrictionName.question_validation_edit,
LockingRestrictionName.group_delete,
LockingRestrictionName.group_label_edit,
LockingRestrictionName.group_question_add,
LockingRestrictionName.group_question_delete,
LockingRestrictionName.group_question_order_edit,
LockingRestrictionName.group_settings_edit,
LockingRestrictionName.group_skip_logic_edit,
LockingRestrictionName.group_split,
LockingRestrictionName.form_appearance,
LockingRestrictionName.form_meta_edit,
LockingRestrictionName.form_replace,
LockingRestrictionName.group_add,
LockingRestrictionName.question_add,
LockingRestrictionName.question_order_edit,
LockingRestrictionName.language_edit,
];

export const LOCK_ALL_PROP_NAME = 'kobo--lock_all';

export const LOCKING_PROFILE_PROP_NAME = 'kobo--locking-profile';

export const LOCKING_PROFILES_PROP_NAME = 'kobo--locking-profiles';

export const LOCKING_UI_CLASSNAMES = {
HIDDEN: 'locking__ui-hidden',
DISABLED: 'locking__ui-disabled',
};
Loading

0 comments on commit 522a775

Please sign in to comment.