From 527a1814d8b6f0368115b7b9f39fab87046782e0 Mon Sep 17 00:00:00 2001 From: RuthShryock Date: Wed, 6 Dec 2023 08:40:16 -0500 Subject: [PATCH 01/42] changing max filname length as a bug fix --- kpi/models/import_export_task.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kpi/models/import_export_task.py b/kpi/models/import_export_task.py index 6d9a3cf7d8..5d22ea086f 100644 --- a/kpi/models/import_export_task.py +++ b/kpi/models/import_export_task.py @@ -570,8 +570,9 @@ class ExportTaskBase(ImportExportTask): } TIMESTAMP_KEY = '_submission_time' - # Above 244 seems to cause 'Download error' in Chrome 64/Linux - MAXIMUM_FILENAME_LENGTH = 240 + # Above 244 seems to cause 'Download error' in Chrome 64/Linux and above + # 207 causes a 'Filename too long' error in Excel + MAXIMUM_FILENAME_LENGTH = 207 class InaccessibleData(Exception): def __str__(self): From 46b67ef8ee8b82f245173bb89d7004566d1c346c Mon Sep 17 00:00:00 2001 From: Leszek Date: Wed, 15 May 2024 22:10:08 +0200 Subject: [PATCH 02/42] typescriptize pageState store --- .../security/mfa/mfaSection.component.tsx | 8 +- jsapp/js/app.jsx | 3 +- jsapp/js/assetQuickActions.tsx | 13 ++-- .../RESTServices/RESTServiceLogs.es6 | 4 +- .../RESTServices/RESTServicesForm.es6 | 4 +- .../RESTServices/RESTServicesList.es6 | 6 +- jsapp/js/components/bigModal/bigModal.es6 | 7 +- .../dataAttachments/connectProjects.es6 | 5 +- jsapp/js/components/drawer.es6 | 10 +-- jsapp/js/components/formLanding.js | 18 ++--- jsapp/js/components/formSummary.js | 6 +- .../header/mainHeader.component.tsx | 4 +- .../js/components/library/librarySidebar.es6 | 4 +- jsapp/js/components/library/myLibraryRoute.js | 4 +- jsapp/js/components/list.es6 | 5 +- jsapp/js/components/map.es6 | 4 +- .../components/modalForms/assetTagsForm.es6 | 4 +- .../js/components/modalForms/encryptForm.es6 | 3 +- .../modalForms/libraryAssetForm.es6 | 6 +- .../modalForms/libraryNewItemForm.es6 | 10 +-- .../js/components/modalForms/modalHelpers.es6 | 6 +- .../components/modalForms/projectSettings.es6 | 8 +- .../modalForms/translationSettings.es6 | 5 +- .../modalForms/translationTable.es6 | 3 +- jsapp/js/dropzone.utils.tsx | 5 +- jsapp/js/editorMixins/assetNavigator.es6 | 7 +- jsapp/js/lists/sidebarForms.es6 | 4 +- jsapp/js/mixins.tsx | 5 +- jsapp/js/pageState.store.ts | 75 +++++++++++++++++++ jsapp/js/stores.d.ts | 18 ----- jsapp/js/stores.es6 | 53 +------------ 31 files changed, 164 insertions(+), 153 deletions(-) create mode 100644 jsapp/js/pageState.store.ts diff --git a/jsapp/js/account/security/mfa/mfaSection.component.tsx b/jsapp/js/account/security/mfa/mfaSection.component.tsx index c06ae0d2a2..41bd589eb3 100644 --- a/jsapp/js/account/security/mfa/mfaSection.component.tsx +++ b/jsapp/js/account/security/mfa/mfaSection.component.tsx @@ -5,7 +5,6 @@ import ToggleSwitch from 'js/components/common/toggleSwitch'; import Icon from 'js/components/common/icon'; import InlineMessage from 'js/components/common/inlineMessage'; import LoadingSpinner from 'js/components/common/loadingSpinner'; -import {stores} from 'js/stores'; import type { MfaUserMethodsResponse, MfaActivatedResponse, @@ -15,6 +14,7 @@ import {MODAL_TYPES} from 'jsapp/js/constants'; import envStore from 'js/envStore'; import './mfaSection.scss'; import {formatTime, formatDate} from 'js/utils'; +import pageState from 'js/pageState.store'; bem.SecurityRow = makeBem(null, 'security-row'); bem.SecurityRow__header = makeBem(bem.SecurityRow, 'header'); @@ -103,7 +103,7 @@ export default class SecurityRoute extends React.Component<{}, SecurityState> { mfaActivating(response: MfaActivatedResponse) { if (response && !response.inModal) { - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.MFA_MODALS, qrCode: response.details, modalType: 'qr', @@ -133,7 +133,7 @@ export default class SecurityRoute extends React.Component<{}, SecurityState> { if (isActive) { mfaActions.activate(); } else { - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.MFA_MODALS, modalType: 'deactivate', customModalHeader: this.renderCustomHeader(), @@ -149,7 +149,7 @@ export default class SecurityRoute extends React.Component<{}, SecurityState> { ) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.MFA_MODALS, modalType: type, customModalHeader: this.renderCustomHeader(), diff --git a/jsapp/js/app.jsx b/jsapp/js/app.jsx index 1ea3ad73bc..159ecd8e71 100644 --- a/jsapp/js/app.jsx +++ b/jsapp/js/app.jsx @@ -29,12 +29,13 @@ import { isTOSAgreementRouteBlockerActive, } from 'js/router/routerUtils'; import {isAnyProcessingRouteActive} from 'js/components/processing/routes.utils'; +import pageState from 'js/pageState.store'; class App extends React.Component { constructor(props) { super(props); this.state = Object.assign({ - pageState: stores.pageState.state, + pageState: pageState.state, }); } diff --git a/jsapp/js/assetQuickActions.tsx b/jsapp/js/assetQuickActions.tsx index d954650a0e..eda41055ce 100644 --- a/jsapp/js/assetQuickActions.tsx +++ b/jsapp/js/assetQuickActions.tsx @@ -30,6 +30,7 @@ import permConfig from './components/permissions/permConfig'; import toast from 'react-hot-toast'; import {userCan} from './components/permissions/utils'; import {renderJSXMessage} from './alertify'; +import pageState from 'js/pageState.store'; export function openInFormBuilder(uid: string) { if (routerIsActive(ROUTES.LIBRARY)) { @@ -533,12 +534,12 @@ export function deployAsset( /** Opens a modal for sharing asset. */ export function manageAssetSharing(uid: string) { - stores.pageState.showModal({type: MODAL_TYPES.SHARING, uid: uid}); + pageState.showModal({type: MODAL_TYPES.SHARING, uid: uid}); } /** Opens a modal for replacing an asset using a file. */ export function replaceAssetForm(asset: AssetResponse | ProjectViewAsset) { - stores.pageState.showModal({type: MODAL_TYPES.REPLACE_PROJECT, asset: asset}); + pageState.showModal({type: MODAL_TYPES.REPLACE_PROJECT, asset: asset}); } /** @@ -547,7 +548,7 @@ export function replaceAssetForm(asset: AssetResponse | ProjectViewAsset) { * up front via `asset` parameter. */ export function manageAssetLanguages(uid: string, asset?: AssetResponse) { - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.FORM_LANGUAGES, assetUid: uid, asset: asset, @@ -555,12 +556,12 @@ export function manageAssetLanguages(uid: string, asset?: AssetResponse) { } export function manageAssetEncryption(uid: string) { - stores.pageState.showModal({type: MODAL_TYPES.ENCRYPT_FORM, assetUid: uid}); + pageState.showModal({type: MODAL_TYPES.ENCRYPT_FORM, assetUid: uid}); } /** Opens a modal for modifying asset tags (also editable in Details Modal). */ export function modifyAssetTags(asset: AssetResponse | ProjectViewAsset) { - stores.pageState.showModal({type: MODAL_TYPES.ASSET_TAGS, asset: asset}); + pageState.showModal({type: MODAL_TYPES.ASSET_TAGS, asset: asset}); } /** @@ -575,7 +576,7 @@ export function manageAssetSettings(asset: AssetResponse) { modalType = MODAL_TYPES.LIBRARY_COLLECTION; } if (modalType) { - stores.pageState.showModal({ + pageState.showModal({ type: modalType, asset: asset, }); diff --git a/jsapp/js/components/RESTServices/RESTServiceLogs.es6 b/jsapp/js/components/RESTServices/RESTServiceLogs.es6 index 4dc754de81..7c5133198d 100644 --- a/jsapp/js/components/RESTServices/RESTServiceLogs.es6 +++ b/jsapp/js/components/RESTServices/RESTServiceLogs.es6 @@ -4,7 +4,7 @@ import autoBind from 'react-autobind'; import reactMixin from 'react-mixin'; import Reflux from 'reflux'; import alertify from 'alertifyjs'; -import {stores} from '../../stores'; +import pageState from 'js/pageState.store'; import bem from 'js/bem'; import LoadingSpinner from 'js/components/common/loadingSpinner'; import {actions} from '../../actions'; @@ -174,7 +174,7 @@ export default class RESTServiceLogs extends React.Component { openSubmissionModal(log) { const currentAsset = this.currentAsset(); - stores.pageState.switchModal({ + pageState.switchModal({ type: MODAL_TYPES.SUBMISSION, sid: log.submission_id, asset: currentAsset, diff --git a/jsapp/js/components/RESTServices/RESTServicesForm.es6 b/jsapp/js/components/RESTServices/RESTServicesForm.es6 index 85c3ed4505..3d966631cf 100644 --- a/jsapp/js/components/RESTServices/RESTServicesForm.es6 +++ b/jsapp/js/components/RESTServices/RESTServicesForm.es6 @@ -6,7 +6,6 @@ import bem from 'js/bem'; import LoadingSpinner from 'js/components/common/loadingSpinner'; import {dataInterface} from '../../dataInterface'; import {actions} from '../../actions'; -import {stores} from '../../stores'; import WrappedSelect from 'js/components/common/wrappedSelect'; import Checkbox from 'js/components/common/checkbox'; import Radio from 'js/components/common/radio'; @@ -14,6 +13,7 @@ import TextBox from 'js/components/common/textBox'; import {KEY_CODES} from 'js/constants'; import envStore from 'js/envStore'; import {notify} from 'js/utils'; +import pageState from 'js/pageState.store'; const EXPORT_TYPES = { json: { @@ -252,7 +252,7 @@ export default class RESTServicesForm extends React.Component { const callbacks = { onComplete: () => { - stores.pageState.hideModal(); + pageState.hideModal(); actions.resources.loadAsset({id: this.state.assetUid}); }, onFail: (data) => { diff --git a/jsapp/js/components/RESTServices/RESTServicesList.es6 b/jsapp/js/components/RESTServices/RESTServicesList.es6 index e206918859..c2dd03caa0 100644 --- a/jsapp/js/components/RESTServices/RESTServicesList.es6 +++ b/jsapp/js/components/RESTServices/RESTServicesList.es6 @@ -3,7 +3,6 @@ import autoBind from 'react-autobind'; import reactMixin from 'react-mixin'; import Reflux from 'reflux'; import alertify from 'alertifyjs'; -import {stores} from '../../stores'; import {actions} from '../../actions'; import bem from 'js/bem'; import LoadingSpinner from 'js/components/common/loadingSpinner'; @@ -13,6 +12,7 @@ import { notify, escapeHtml, } from 'js/utils'; +import pageState from 'js/pageState.store'; const REST_SERVICES_SUPPORT_URL = 'rest_services.html'; @@ -60,7 +60,7 @@ export default class RESTServicesList extends React.Component { } editHook(evt) { - stores.pageState.showModal({ + pageState.showModal({ assetUid: this.state.assetUid, type: MODAL_TYPES.REST_SERVICES, hookUid: evt.currentTarget.dataset.hookUid @@ -92,7 +92,7 @@ export default class RESTServicesList extends React.Component { } openNewRESTServiceModal() { - stores.pageState.showModal({ + pageState.showModal({ assetUid: this.state.assetUid, // hookUid: not provided intentionally type: MODAL_TYPES.REST_SERVICES diff --git a/jsapp/js/components/bigModal/bigModal.es6 b/jsapp/js/components/bigModal/bigModal.es6 index 0e33ac6a2b..eb8d8eed04 100644 --- a/jsapp/js/components/bigModal/bigModal.es6 +++ b/jsapp/js/components/bigModal/bigModal.es6 @@ -29,6 +29,7 @@ import TranslationSettings from 'js/components/modalForms/translationSettings'; import TranslationTable from 'js/components/modalForms/translationTable'; // This should either be more generic or else be it's own component in the account directory. import MFAModals from './mfaModals'; +import pageState from 'js/pageState.store'; function getSubmissionTitle(props) { let title = t('Success!'); @@ -62,7 +63,7 @@ function getSubmissionTitle(props) { * To display a modal, you need to use `pageState` store with `showModal` method: * * ``` - * stores.pageState.showModal({ + * pageState.showModal({ * type: MODAL_TYPES.NEW_FORM * }); * ``` @@ -273,7 +274,7 @@ class BigModal extends React.Component { title: title, message: message, labels: {ok: t('Close'), cancel: t('Cancel')}, - onok: stores.pageState.hideModal, + onok: pageState.hideModal, oncancel: dialog.destroy, }; dialog.set(opts).show(); @@ -289,7 +290,7 @@ class BigModal extends React.Component { t('You will lose all unsaved changes.') ); } else { - stores.pageState.hideModal(); + pageState.hideModal(); } } diff --git a/jsapp/js/components/dataAttachments/connectProjects.es6 b/jsapp/js/components/dataAttachments/connectProjects.es6 index 4b7ca25c5e..af374bcc5c 100644 --- a/jsapp/js/components/dataAttachments/connectProjects.es6 +++ b/jsapp/js/components/dataAttachments/connectProjects.es6 @@ -9,7 +9,6 @@ import TextBox from 'js/components/common/textBox'; import Button from 'js/components/common/button'; import MultiCheckbox from 'js/components/common/multiCheckbox'; import {actions} from 'js/actions'; -import {stores} from 'js/stores'; import bem from 'js/bem'; import LoadingSpinner from 'js/components/common/loadingSpinner'; import envStore from 'js/envStore'; @@ -21,7 +20,7 @@ import { MODAL_TYPES, MAX_DISPLAYED_STRING_LENGTH, } from 'js/constants'; - +import pageState from 'js/pageState.store'; import './connect-projects.scss'; const DYNAMIC_DATA_ATTACHMENTS_SUPPORT_URL = 'dynamic_data_attachment.html'; @@ -333,7 +332,7 @@ class ConnectProjects extends React.Component { } showColumnFilterModal(asset, source, filename, fields, attachmentUrl) { - stores.pageState.showModal( + pageState.showModal( { type: MODAL_TYPES.DATA_ATTACHMENT_COLUMNS, asset: asset, diff --git a/jsapp/js/components/drawer.es6 b/jsapp/js/components/drawer.es6 index b29b8a1ae7..b1a1093c6a 100644 --- a/jsapp/js/components/drawer.es6 +++ b/jsapp/js/components/drawer.es6 @@ -5,7 +5,6 @@ import autoBind from 'react-autobind'; import {observer} from 'mobx-react'; import Reflux from 'reflux'; import {NavLink} from 'react-router-dom'; -import {stores} from '../stores'; import sessionStore from '../stores/session'; import bem from 'js/bem'; import {searches} from '../searches'; @@ -17,6 +16,7 @@ import {ROUTES, PROJECTS_ROUTES} from 'js/router/routerConstants'; import SidebarFormsList from '../lists/sidebarForms'; import envStore from 'js/envStore'; import {router, routerIsActive, withRouter} from '../router/legacy'; +import pageState from 'js/pageState.store'; const AccountSidebar = lazy(() => import('js/account/accountSidebar')); @@ -39,12 +39,12 @@ const FormSidebar = observer( currentAssetId: false, files: [], }, - stores.pageState.state + pageState.state ); this.state = Object.assign(INITIAL_STATE, this.state); this.unlisteners = []; - this.stores = [stores.pageState]; + this.stores = [pageState]; autoBind(this); } componentDidMount() { @@ -61,7 +61,7 @@ const FormSidebar = observer( } newFormModal(evt) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.NEW_FORM, }); } @@ -141,7 +141,7 @@ const Drawer = observer( constructor(props) { super(props); autoBind(this); - this.stores = [stores.pageState]; + this.stores = [pageState]; } isAccount() { diff --git a/jsapp/js/components/formLanding.js b/jsapp/js/components/formLanding.js index eb589787bd..5c1a09fd5c 100644 --- a/jsapp/js/components/formLanding.js +++ b/jsapp/js/components/formLanding.js @@ -5,7 +5,6 @@ import autoBind from 'react-autobind'; import Reflux from 'reflux'; import bem from 'js/bem'; import {dataInterface} from '../dataInterface'; -import {stores} from '../stores'; import sessionStore from 'js/stores/session'; import PopoverMenu from 'js/popoverMenu'; import LoadingSpinner from 'js/components/common/loadingSpinner'; @@ -30,6 +29,7 @@ import {PERMISSIONS_CODENAMES} from 'js/components/permissions/permConstants'; import {HELP_ARTICLE_ANON_SUBMISSIONS_URL} from 'js/constants'; import AnonymousSubmission from './anonymousSubmission.component'; import NewFeatureDialog from './newFeatureDialog.component'; +import pageState from 'js/pageState.store'; const DVCOUNT_LIMIT_MINIMUM = 20; const ANON_CAN_ADD_PERM_URL = permConfig.getPermissionByCodename( @@ -46,7 +46,6 @@ class FormLanding extends React.Component { nextPagesVersions: [], anonymousSubmissions: false, anonymousPermissions: [], - shouldShowNewFeatureDialog: false, }; autoBind(this); } @@ -113,7 +112,7 @@ class FormLanding extends React.Component { } enketoPreviewModal(evt) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.ENKETO_PREVIEW, assetid: this.state.uid, }); @@ -179,17 +178,14 @@ class FormLanding extends React.Component { } showSharingModal(evt) { evt.preventDefault(); - stores.pageState._onHideModal = () => { - this.setState({shouldShowNewFeatureDialog: true}); - }; - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.SHARING, assetid: this.state.uid, }); } showReplaceProjectModal(evt) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.REPLACE_PROJECT, asset: this.state, }); @@ -221,14 +217,14 @@ class FormLanding extends React.Component { } showLanguagesModal(evt) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.FORM_LANGUAGES, asset: this.state, }); } showEncryptionModal(evt) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.ENCRYPT_FORM, asset: this.state, }); @@ -446,7 +442,7 @@ class FormLanding extends React.Component { envStore.data.support_url + HELP_ARTICLE_ANON_SUBMISSIONS_URL } featureKey='anonymousSubmissions' - disabled={stores.pageState.state?.modal} + disabled={pageState.state?.modal} pointerClass='anonymousSubmissionPointer' dialogClass='anonymousSubmissionDialog' > diff --git a/jsapp/js/components/formSummary.js b/jsapp/js/components/formSummary.js index d9c81e5dd2..a4301c24bf 100644 --- a/jsapp/js/components/formSummary.js +++ b/jsapp/js/components/formSummary.js @@ -3,7 +3,6 @@ import reactMixin from 'react-mixin'; import autoBind from 'react-autobind'; import Reflux from 'reflux'; import { Link, NavLink } from 'react-router-dom'; -import {stores} from 'js/stores'; import mixins from 'js/mixins'; import bem from 'js/bem'; import DocumentTitle from 'react-document-title'; @@ -18,6 +17,7 @@ import './formSummary.scss'; import {userCan} from 'js/components/permissions/utils'; import FormSummaryProjectInfo from './formSummaryProjectInfo'; import SubmissionsCountGraph from 'js/project/submissionsCountGraph.component'; +import pageState from 'js/pageState.store'; class FormSummary extends React.Component { constructor(props) { @@ -110,7 +110,7 @@ class FormSummary extends React.Component { sharingModal (evt) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.SHARING, assetid: this.state.uid, }); @@ -118,7 +118,7 @@ class FormSummary extends React.Component { enketoPreviewModal (evt) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.ENKETO_PREVIEW, assetid: this.state.uid, }); diff --git a/jsapp/js/components/header/mainHeader.component.tsx b/jsapp/js/components/header/mainHeader.component.tsx index 1fb58a3a54..332785960f 100644 --- a/jsapp/js/components/header/mainHeader.component.tsx +++ b/jsapp/js/components/header/mainHeader.component.tsx @@ -1,6 +1,5 @@ import React from 'react'; import {observer} from 'mobx-react'; -import {stores} from 'js/stores'; import sessionStore from 'js/stores/session'; import assetStore from 'js/assetStore'; import bem from 'js/bem'; @@ -25,6 +24,7 @@ import type {IconName} from 'jsapp/fonts/k-icons'; import MainHeaderBase from './mainHeaderBase.component'; import MainHeaderLogo from './mainHeaderLogo.component'; import GitRev from './gitRev.component'; +import pageState from 'js/pageState.store'; interface MainHeaderProps extends WithRouterProps { assetUid: string | null; @@ -78,7 +78,7 @@ const MainHeader = class MainHeader extends React.Component { } toggleFixedDrawer() { - stores.pageState.toggleFixedDrawer(); + pageState.toggleFixedDrawer(); } render() { diff --git a/jsapp/js/components/library/librarySidebar.es6 b/jsapp/js/components/library/librarySidebar.es6 index 28622d2feb..32ccb43280 100644 --- a/jsapp/js/components/library/librarySidebar.es6 +++ b/jsapp/js/components/library/librarySidebar.es6 @@ -3,7 +3,6 @@ import Reflux from 'reflux'; import reactMixin from 'react-mixin'; import PropTypes from 'prop-types'; import autoBind from 'react-autobind'; -import {stores} from 'js/stores'; import sessionStore from 'js/stores/session'; import bem from 'js/bem'; import {MODAL_TYPES} from 'js/constants'; @@ -11,6 +10,7 @@ import myLibraryStore from './myLibraryStore'; import { routerIsActive } from '../../router/legacy'; import {ROUTES} from '../../router/routerConstants'; import {NavLink} from 'react-router-dom'; +import pageState from 'js/pageState.store'; class LibrarySidebar extends Reflux.Component { constructor(props){ @@ -39,7 +39,7 @@ class LibrarySidebar extends Reflux.Component { showLibraryNewModal(evt) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.LIBRARY_NEW_ITEM }); } diff --git a/jsapp/js/components/library/myLibraryRoute.js b/jsapp/js/components/library/myLibraryRoute.js index 49aaccb022..ae5edecc8d 100644 --- a/jsapp/js/components/library/myLibraryRoute.js +++ b/jsapp/js/components/library/myLibraryRoute.js @@ -7,13 +7,13 @@ import DocumentTitle from 'react-document-title'; import Dropzone from 'react-dropzone'; import mixins from 'js/mixins'; import bem from 'js/bem'; -import {stores} from 'js/stores'; import {validFileTypes} from 'utils'; import myLibraryStore from './myLibraryStore'; import AssetsTable from 'js/components/assetsTable/assetsTable'; import {MODAL_TYPES} from 'js/constants'; import {ROOT_BREADCRUMBS} from 'js/components/library/libraryConstants'; import {ASSETS_TABLE_CONTEXTS} from 'js/components/assetsTable/assetsTableConstants'; +import pageState from 'js/pageState.store'; class MyLibraryRoute extends React.Component { constructor(props) { @@ -70,7 +70,7 @@ class MyLibraryRoute extends React.Component { */ onFileDrop(files, rejectedFiles, evt) { if (files.length === 1) { - stores.pageState.switchModal({ + pageState.switchModal({ type: MODAL_TYPES.LIBRARY_UPLOAD, file: files[0], }); diff --git a/jsapp/js/components/list.es6 b/jsapp/js/components/list.es6 index ac61469845..1902262a6a 100644 --- a/jsapp/js/components/list.es6 +++ b/jsapp/js/components/list.es6 @@ -13,6 +13,7 @@ import { ASSET_TYPES, ACCESS_TYPES, } from 'js/constants'; +import pageState from 'js/pageState.store'; export class ListSearch extends React.Component { constructor(props) { @@ -243,7 +244,7 @@ export class ListExpandToggle extends React.Component { constructor(props) { super(props); this.state = { - assetNavExpanded: stores.pageState.state.assetNavExpanded, + assetNavExpanded: pageState.state.assetNavExpanded, }; autoBind(this); } @@ -257,7 +258,7 @@ export class ListExpandToggle extends React.Component { } onExpandedToggleChange(isChecked) { - stores.pageState.setState({assetNavExpanded: isChecked}); + pageState.setState({assetNavExpanded: isChecked}); this.setState({assetNavExpanded: isChecked}); } diff --git a/jsapp/js/components/map.es6 b/jsapp/js/components/map.es6 index 6d99a21fbb..ca3f434b8c 100644 --- a/jsapp/js/components/map.es6 +++ b/jsapp/js/components/map.es6 @@ -4,7 +4,6 @@ import autoBind from 'react-autobind'; import Reflux from 'reflux'; import {dataInterface} from '../dataInterface'; import bem from 'js/bem'; -import {stores} from '../stores'; import {actions} from '../actions'; import PopoverMenu from 'js/popoverMenu'; import Modal from 'js/components/common/modal'; @@ -19,6 +18,7 @@ import 'leaflet/dist/leaflet.css'; import 'leaflet.heat/dist/leaflet-heat'; import 'leaflet.markercluster/dist/leaflet.markercluster'; import 'leaflet.markercluster/dist/MarkerCluster.css'; +import pageState from 'js/pageState.store'; import { ASSET_FILE_TYPES, @@ -672,7 +672,7 @@ export class FormMap extends React.Component { ids.push(r._id); }); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.SUBMISSION, sid: evt.layer.options.sId, asset: this.props.asset, diff --git a/jsapp/js/components/modalForms/assetTagsForm.es6 b/jsapp/js/components/modalForms/assetTagsForm.es6 index 06825f9f02..3b4f3bb766 100644 --- a/jsapp/js/components/modalForms/assetTagsForm.es6 +++ b/jsapp/js/components/modalForms/assetTagsForm.es6 @@ -3,11 +3,11 @@ import autoBind from 'react-autobind'; import {observer} from 'mobx-react'; import KoboTagsInput from 'js/components/common/koboTagsInput'; import bem from 'js/bem'; -import {stores} from 'js/stores'; import sessionStore from 'js/stores/session'; import {actions} from 'js/actions'; import {notify} from 'utils'; import LoadingSpinner from 'js/components/common/loadingSpinner'; +import pageState from 'js/pageState.store'; /** * @param {Object} asset - Modal asset. @@ -39,7 +39,7 @@ export const AssetTagsForm = observer(class AssetTagsForm extends React.Componen onUpdateAssetCompleted() { this.setState({isPending: false}); - stores.pageState.hideModal(); + pageState.hideModal(); } onUpdateAssetFailed() { diff --git a/jsapp/js/components/modalForms/encryptForm.es6 b/jsapp/js/components/modalForms/encryptForm.es6 index 0a40bc195e..db6f530945 100644 --- a/jsapp/js/components/modalForms/encryptForm.es6 +++ b/jsapp/js/components/modalForms/encryptForm.es6 @@ -8,6 +8,7 @@ import {actions} from 'js/actions'; import bem from 'js/bem'; import {MODAL_TYPES} from 'js/constants'; import {stores} from 'js/stores'; +import pageState from 'js/pageState.store'; class EncryptForm extends React.Component { constructor(props) { @@ -51,7 +52,7 @@ class EncryptForm extends React.Component { publicKey: asset.content.settings.public_key }); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.ENCRYPT_FORM, asset: asset }); diff --git a/jsapp/js/components/modalForms/libraryAssetForm.es6 b/jsapp/js/components/modalForms/libraryAssetForm.es6 index 5c31ef7d45..5daf745048 100644 --- a/jsapp/js/components/modalForms/libraryAssetForm.es6 +++ b/jsapp/js/components/modalForms/libraryAssetForm.es6 @@ -10,7 +10,6 @@ import PropTypes from 'prop-types'; import TextBox from 'js/components/common/textBox'; import bem from 'js/bem'; import LoadingSpinner from 'js/components/common/loadingSpinner'; -import {stores} from 'js/stores'; import sessionStore from 'js/stores/session'; import {actions} from 'js/actions'; import {notify} from 'utils'; @@ -21,6 +20,7 @@ import mixins from 'js/mixins'; import managedCollectionsStore from 'js/components/library/managedCollectionsStore'; import envStore from 'js/envStore'; import {withRouter} from 'js/router/legacy'; +import pageState from 'js/pageState.store'; /** * Modal for creating or updating library asset (collection or template) @@ -94,7 +94,7 @@ export class LibraryAssetFormComponent extends React.Component { onCreateResourceCompleted(response) { this.setState({isPending: false}); notify(t('##type## ##name## created').replace('##type##', this.getFormAssetType()).replace('##name##', response.name)); - stores.pageState.hideModal(); + pageState.hideModal(); if (this.getFormAssetType() === ASSET_TYPES.collection.id) { this.props.router.navigate(`/library/asset/${response.uid}`); } else if (this.getFormAssetType() === ASSET_TYPES.template.id) { @@ -109,7 +109,7 @@ export class LibraryAssetFormComponent extends React.Component { onUpdateAssetCompleted() { this.setState({isPending: false}); - stores.pageState.hideModal(); + pageState.hideModal(); } onUpdateAssetFailed() { diff --git a/jsapp/js/components/modalForms/libraryNewItemForm.es6 b/jsapp/js/components/modalForms/libraryNewItemForm.es6 index 30fc3e0abf..86956887ef 100644 --- a/jsapp/js/components/modalForms/libraryNewItemForm.es6 +++ b/jsapp/js/components/modalForms/libraryNewItemForm.es6 @@ -5,7 +5,6 @@ import Reflux from 'reflux'; import PropTypes from 'prop-types'; import bem from 'js/bem'; import LoadingSpinner from 'js/components/common/loadingSpinner'; -import {stores} from 'js/stores'; import sessionStore from 'js/stores/session'; import { MODAL_TYPES, @@ -16,6 +15,7 @@ import mixins from 'js/mixins'; import managedCollectionsStore from 'js/components/library/managedCollectionsStore'; import {withRouter} from 'js/router/legacy'; import {when} from 'mobx'; +import pageState from 'js/pageState.store'; class LibraryNewItemForm extends React.Component { constructor(props) { @@ -34,7 +34,7 @@ class LibraryNewItemForm extends React.Component { } goToAssetCreator() { - stores.pageState.hideModal(); + pageState.hideModal(); let targetPath = ROUTES.NEW_LIBRARY_ITEM; if (this.isLibrarySingle()) { @@ -50,21 +50,21 @@ class LibraryNewItemForm extends React.Component { } goToCollection() { - stores.pageState.switchModal({ + pageState.switchModal({ type: MODAL_TYPES.LIBRARY_COLLECTION, previousType: MODAL_TYPES.LIBRARY_NEW_ITEM }); } goToTemplate() { - stores.pageState.switchModal({ + pageState.switchModal({ type: MODAL_TYPES.LIBRARY_TEMPLATE, previousType: MODAL_TYPES.LIBRARY_NEW_ITEM }); } goToUpload() { - stores.pageState.switchModal({ + pageState.switchModal({ type: MODAL_TYPES.LIBRARY_UPLOAD, previousType: MODAL_TYPES.LIBRARY_NEW_ITEM }); diff --git a/jsapp/js/components/modalForms/modalHelpers.es6 b/jsapp/js/components/modalForms/modalHelpers.es6 index 683610d99e..65448d99f5 100644 --- a/jsapp/js/components/modalForms/modalHelpers.es6 +++ b/jsapp/js/components/modalForms/modalHelpers.es6 @@ -1,14 +1,14 @@ import React from 'react'; import bem from 'js/bem'; -import {stores} from 'js/stores'; +import pageState from 'js/pageState.store'; export function renderBackButton(isDisabled = false) { - if (stores.pageState.hasPreviousModal()) { + if (pageState.hasPreviousModal()) { return ( {t('Back')} diff --git a/jsapp/js/components/modalForms/projectSettings.es6 b/jsapp/js/components/modalForms/projectSettings.es6 index e150ec638d..c9d40a5496 100644 --- a/jsapp/js/components/modalForms/projectSettings.es6 +++ b/jsapp/js/components/modalForms/projectSettings.es6 @@ -13,7 +13,7 @@ import bem from 'js/bem'; import LoadingSpinner from 'js/components/common/loadingSpinner'; import InlineMessage from 'js/components/common/inlineMessage'; import assetUtils from 'js/assetUtils'; -import {stores} from 'js/stores'; +import pageState from 'js/pageState.store'; import sessionStore from 'js/stores/session'; import mixins from 'js/mixins'; import TemplatesList from 'js/components/templatesList'; @@ -345,12 +345,12 @@ class ProjectSettings extends React.Component { */ goToFormBuilder(assetUid) { - stores.pageState.hideModal(); + pageState.hideModal(); this.props.router.navigate(`/forms/${assetUid}/edit`); } goToFormLanding() { - stores.pageState.hideModal(); + pageState.hideModal(); let targetUid; if (this.state.formAsset) { @@ -369,7 +369,7 @@ class ProjectSettings extends React.Component { } goToProjectsList() { - stores.pageState.hideModal(); + pageState.hideModal(); this.props.router.navigate(ROUTES.FORMS); } diff --git a/jsapp/js/components/modalForms/translationSettings.es6 b/jsapp/js/components/modalForms/translationSettings.es6 index f576e3c972..78d9b255f6 100644 --- a/jsapp/js/components/modalForms/translationSettings.es6 +++ b/jsapp/js/components/modalForms/translationSettings.es6 @@ -19,6 +19,7 @@ import { notify, escapeHtml, } from 'utils'; +import pageState from 'js/pageState.store'; const LANGUAGE_SUPPORT_URL = 'language_dashboard.html'; @@ -70,7 +71,7 @@ export class TranslationSettings extends React.Component { renameLanguageIndex: -1, }); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.FORM_LANGUAGES, asset: asset, }); @@ -101,7 +102,7 @@ export class TranslationSettings extends React.Component { launchTranslationTableModal(evt) { const index = evt.currentTarget.dataset.index; const langString = evt.currentTarget.dataset.string; - stores.pageState.switchModal({ + pageState.switchModal({ type: MODAL_TYPES.FORM_TRANSLATIONS_TABLE, asset: this.state.asset, langString: langString, diff --git a/jsapp/js/components/modalForms/translationTable.es6 b/jsapp/js/components/modalForms/translationTable.es6 index 28a829d9cf..9493ef3173 100644 --- a/jsapp/js/components/modalForms/translationTable.es6 +++ b/jsapp/js/components/modalForms/translationTable.es6 @@ -18,6 +18,7 @@ import { hasRowRestriction, hasAssetRestriction, } from 'js/components/locking/lockingUtils'; +import pageState from 'js/pageState.store'; const SAVE_BUTTON_TEXT = { DEFAULT: t('Save Changes'), @@ -226,7 +227,7 @@ export class TranslationTable extends React.Component { } showManageLanguagesModal() { - stores.pageState.switchModal({ + pageState.switchModal({ type: MODAL_TYPES.FORM_LANGUAGES, asset: this.props.asset, }); diff --git a/jsapp/js/dropzone.utils.tsx b/jsapp/js/dropzone.utils.tsx index 89a1f60888..9c73ad06f8 100644 --- a/jsapp/js/dropzone.utils.tsx +++ b/jsapp/js/dropzone.utils.tsx @@ -8,6 +8,7 @@ import {router, routerIsActive} from 'js/router/legacy'; import {ROUTES} from './router/routerConstants'; import {stores} from './stores'; import {getExponentialDelayTime} from 'js/components/projectDownloads/exportFetcher'; +import pageState from 'js/pageState.store'; const IMPORT_FAILED_GENERIC_MESSAGE = t('Import failed'); @@ -153,7 +154,7 @@ function onImportOneAmongMany( const isLastFileInBatch = fileIndex + 1 === totalFilesInBatch; // We open the modal that displays the message with total files count. - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.UPLOADING_XLS, filename: t('## files').replace('##', String(totalFilesInBatch)), }); @@ -179,7 +180,7 @@ function onImportOneAmongMany( // the edges). if (isLastFileInBatch) { // After the last import is created, we hide the modal… - stores.pageState.hideModal(); + pageState.hideModal(); // …and display a helpful toast notify.warning( t( diff --git a/jsapp/js/editorMixins/assetNavigator.es6 b/jsapp/js/editorMixins/assetNavigator.es6 index 2d60af4cab..b5a9a9d662 100644 --- a/jsapp/js/editorMixins/assetNavigator.es6 +++ b/jsapp/js/editorMixins/assetNavigator.es6 @@ -17,6 +17,7 @@ import { ListCollectionFilter, ListExpandToggle, } from 'js/components/list'; +import pageState from 'js/pageState.store'; class AssetNavigatorListView extends React.Component { constructor(props) { @@ -125,7 +126,7 @@ class AssetNavigatorListView extends React.Component { } - { (stores.pageState.state.assetNavExpanded && item.asset_type === 'block') && + { (pageState.state.assetNavExpanded && item.asset_type === 'block') &&
    {summ.labels.map((lbl, i) => (
  1. {lbl}
  2. @@ -133,7 +134,7 @@ class AssetNavigatorListView extends React.Component {
} - { stores.pageState.state.assetNavExpanded && + { pageState.state.assetNavExpanded && {(item.tags || []).map((tg, i) => ( {tg} @@ -170,7 +171,7 @@ class AssetNavigator extends Reflux.Component { } componentDidMount() { - this.listenTo(stores.pageState, this.handlePageStateStore); + this.listenTo(pageState, this.handlePageStateStore); this.state.searchContext.mixin.searchDefault(); } diff --git a/jsapp/js/lists/sidebarForms.es6 b/jsapp/js/lists/sidebarForms.es6 index 591ed2a61c..c413a3c99a 100644 --- a/jsapp/js/lists/sidebarForms.es6 +++ b/jsapp/js/lists/sidebarForms.es6 @@ -8,7 +8,7 @@ import mixins from '../mixins'; import bem from 'js/bem'; import LoadingSpinner from 'js/components/common/loadingSpinner'; import {searches} from '../searches'; -import {stores} from '../stores'; +import pageState from 'js/pageState.store'; import {COMMON_QUERIES, DEPLOYMENT_CATEGORIES} from 'js/constants'; import AssetName from 'js/components/common/assetName'; import {userCan} from 'js/components/permissions/utils'; @@ -30,7 +30,7 @@ class SidebarFormsList extends Reflux.Component { filterTags: COMMON_QUERIES.s, }), }; - this.store = stores.pageState; + this.store = pageState; autoBind(this); } componentDidMount() { diff --git a/jsapp/js/mixins.tsx b/jsapp/js/mixins.tsx index 23ffd28884..b199bf277a 100644 --- a/jsapp/js/mixins.tsx +++ b/jsapp/js/mixins.tsx @@ -39,6 +39,7 @@ import { deployAsset, } from 'js/assetQuickActions'; import type {DropFilesEventHandler} from 'react-dropzone'; +import pageState from 'js/pageState.store'; const IMPORT_CHECK_INTERVAL = 1000; @@ -357,7 +358,7 @@ const mixins: MixinsObject = { params = Object.assign({library: isLibrary}, params); if (params.base64Encoded) { - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.UPLOADING_XLS, filename: multipleFiles ? t('## files').replace('##', String(totalFiles)) @@ -455,7 +456,7 @@ const mixins: MixinsObject = { notify.error(t('Import Failed!')); log('import failed', failData); }); - stores.pageState.hideModal(); + pageState.hideModal(); }, 2500); }, (jqxhr: string) => { diff --git a/jsapp/js/pageState.store.ts b/jsapp/js/pageState.store.ts new file mode 100644 index 0000000000..7d4cb10061 --- /dev/null +++ b/jsapp/js/pageState.store.ts @@ -0,0 +1,75 @@ +import Reflux from 'reflux'; + +interface PageStateModalParams { + type: string; // one of MODAL_TYPES + [name: string]: any; +} + +interface PageStateStoreState { + assetNavExpanded?: boolean; + showFixedDrawer?: boolean; + modal?: PageStateModalParams | false; +} + +// DEPRECATED +// This is some old weird store that is responsible for two things: +// 1. toggling mobile menu +// 2. handling modal from `bigModal.es6` +class PageStateStore extends Reflux.Store { + state: PageStateStoreState = { + assetNavExpanded: false, + showFixedDrawer: false, + modal: false, + } + + setState(newState: PageStateStoreState) { + Object.assign(this.state, newState); + this.trigger(this.state); + } + + toggleFixedDrawer() { + const _changes: PageStateStoreState = {}; + const newval = !this.state.showFixedDrawer; + _changes.showFixedDrawer = newval; + Object.assign(this.state, _changes); + this.trigger(_changes); + } + + showModal(params: PageStateModalParams) { + this.setState({ + modal: params + }); + } + + hideModal() { + this.setState({ + modal: false + }); + } + + // use it when you have one modal opened and want to display different one + // because just calling showModal has weird outcome + switchModal(params: PageStateModalParams) { + this.hideModal(); + // HACK switch to setState callback after updating to React 16+ + window.setTimeout(() => { + this.showModal(params); + }, 0); + } + + switchToPreviousModal() { + if (this.state.modal) { + this.switchModal({ + type: this.state.modal.previousType + }); + } + } + + hasPreviousModal() { + return this.state.modal && this.state.modal?.previousType; + } +} + +const pageState = new PageStateStore(); + +export default pageState; diff --git a/jsapp/js/stores.d.ts b/jsapp/js/stores.d.ts index 12489cbc7d..dad02c3af5 100644 --- a/jsapp/js/stores.d.ts +++ b/jsapp/js/stores.d.ts @@ -1,8 +1,3 @@ -interface PageStateModalParams { - type: string; // one of MODAL_TYPES.NEW_FORM - [name: string]: any; -} - // TODO: either change whole `stores.es6` to `stores.ts` or crete a type // definition for a store you need. export namespace stores { @@ -10,19 +5,6 @@ export namespace stores { const surveyState: any; const assetSearch: any; const translations: any; - const pageState: { - state: { - assetNavExpanded: boolean; - showFixedDrawer: boolean; - modal?: {} | false; - }; - toggleFixedDrawer: () => void; - showModal: (params: PageStateModalParams) => void; - hideModal: () => void; - switchModal: (params: PageStateModalParams) => void; - switchToPreviousModal: () => void; - hasPreviousModal: () => boolean; - }; const snapshots: any; const session: { listen: (clb: Function) => void; diff --git a/jsapp/js/stores.es6 b/jsapp/js/stores.es6 index 67296ab153..dbfc3cc837 100644 --- a/jsapp/js/stores.es6 +++ b/jsapp/js/stores.es6 @@ -112,58 +112,7 @@ stores.translations = Reflux.createStore({ }, }); -stores.pageState = Reflux.createStore({ - init () { - this.state = { - assetNavExpanded: false, - showFixedDrawer: false - }; - }, - setState (chz) { - var changed = changes(this.state, chz); - if (changed) { - Object.assign(this.state, changed); - this.trigger(changed); - } - }, - toggleFixedDrawer () { - var _changes = {}; - var newval = !this.state.showFixedDrawer; - _changes.showFixedDrawer = newval; - Object.assign(this.state, _changes); - this.trigger(_changes); - }, - showModal (params) { - this.setState({ - modal: params - }); - }, - hideModal () { - if (this._onHideModal) { - this._onHideModal(); - } - this.setState({ - modal: false - }); - }, - // use it when you have one modal opened and want to display different one - // because just calling showModal has weird outcome - switchModal (params) { - this.hideModal(); - // HACK switch to setState callback after updating to React 16+ - window.setTimeout(() => { - this.showModal(params); - }, 0); - }, - switchToPreviousModal() { - this.switchModal({ - type: this.state.modal.previousType - }); - }, - hasPreviousModal() { - return this.state.modal && this.state.modal.previousType; - } -}); + stores.snapshots = Reflux.createStore({ init () { From 99785f95fe5cf65065ab6a7f6da6a0e53836b23b Mon Sep 17 00:00:00 2001 From: Leszek Date: Wed, 15 May 2024 22:12:30 +0200 Subject: [PATCH 03/42] transcriptize enketoHandler --- jsapp/js/components/submissions/mediaCell.tsx | 4 +- .../submissions/submissionModal.es6 | 18 ++--- jsapp/js/constants.ts | 5 +- jsapp/js/dataInterface.ts | 12 +++- .../{enketoHandler.es6 => enketoHandler.tsx} | 68 +++++++++++-------- 5 files changed, 63 insertions(+), 44 deletions(-) rename jsapp/js/{enketoHandler.es6 => enketoHandler.tsx} (58%) diff --git a/jsapp/js/components/submissions/mediaCell.tsx b/jsapp/js/components/submissions/mediaCell.tsx index a5d65a83ed..11c0014fde 100644 --- a/jsapp/js/components/submissions/mediaCell.tsx +++ b/jsapp/js/components/submissions/mediaCell.tsx @@ -1,7 +1,6 @@ import autoBind from 'react-autobind'; import React from 'react'; import bem, {makeBem} from 'js/bem'; -import {stores} from 'js/stores'; import { MODAL_TYPES, QUESTION_TYPES, @@ -19,6 +18,7 @@ import type {SubmissionAttachment} from 'js/dataInterface'; import './mediaCell.scss'; import Icon from 'js/components/common/icon'; import type {IconName} from 'jsapp/fonts/k-icons'; +import pageState from 'js/pageState.store'; bem.TableMediaPreviewHeader = makeBem(null, 'table-media-preview-header'); bem.TableMediaPreviewHeader__title = makeBem(bem.TableMediaPreviewHeader, 'title', 'div'); @@ -82,7 +82,7 @@ class MediaCell extends React.Component { launchMediaModal(evt: MouseEvent | TouchEvent) { evt.preventDefault(); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.TABLE_MEDIA_PREVIEW, questionType: this.props.questionType, mediaAttachment: this.props.mediaAttachment, diff --git a/jsapp/js/components/submissions/submissionModal.es6 b/jsapp/js/components/submissions/submissionModal.es6 index bd4efcc3a4..d2294a1630 100644 --- a/jsapp/js/components/submissions/submissionModal.es6 +++ b/jsapp/js/components/submissions/submissionModal.es6 @@ -10,12 +10,12 @@ import {actions} from 'js/actions'; import bem from 'js/bem'; import LoadingSpinner from 'js/components/common/loadingSpinner'; import {launchPrinting} from 'utils'; -import {stores} from 'js/stores'; +import pageState from 'js/pageState.store'; import { VALIDATION_STATUSES_LIST, MODAL_TYPES, META_QUESTION_TYPES, - ENKETO_ACTIONS, + EnketoActions, } from 'js/constants'; import SubmissionDataTable from 'js/components/submissions/submissionDataTable'; import Checkbox from 'js/components/common/checkbox'; @@ -167,7 +167,7 @@ class SubmissionModal extends React.Component { } onDeletedSubmissionCompleted() { - stores.pageState.hideModal(); + pageState.hideModal(); } launchEditSubmission() { @@ -179,7 +179,7 @@ class SubmissionModal extends React.Component { enketoHandler.openSubmission( this.props.asset.uid, this.state.sid, - ENKETO_ACTIONS.edit + EnketoActions.edit ).then( () => {this.setState({isEditLoading: false});}, () => {this.setState({isEditLoading: false});} @@ -193,7 +193,7 @@ class SubmissionModal extends React.Component { enketoHandler.openSubmission( this.props.asset.uid, this.state.sid, - ENKETO_ACTIONS.view + EnketoActions.view ).then( () => {this.setState({isViewLoading: false});} ); @@ -203,7 +203,7 @@ class SubmissionModal extends React.Component { // Due to how modals are created, we must close this modal and recreate // an almost identical one to display the new submission with a different // title bar - stores.pageState.hideModal(); + pageState.hideModal(); actions.resources.duplicateSubmission(this.props.asset.uid, this.state.sid, this.state.submission); } @@ -218,7 +218,7 @@ class SubmissionModal extends React.Component { switchSubmission(sid) { this.setState({ loading: true}); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.SUBMISSION, sid: sid, asset: this.props.asset, @@ -230,7 +230,7 @@ class SubmissionModal extends React.Component { prevTablePage() { this.setState({ loading: true}); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.SUBMISSION, sid: false, page: 'prev', @@ -240,7 +240,7 @@ class SubmissionModal extends React.Component { nextTablePage() { this.setState({ loading: true}); - stores.pageState.showModal({ + pageState.showModal({ type: MODAL_TYPES.SUBMISSION, sid: false, page: 'next', diff --git a/jsapp/js/constants.ts b/jsapp/js/constants.ts index bc4c8a8504..eeb3c100ae 100644 --- a/jsapp/js/constants.ts +++ b/jsapp/js/constants.ts @@ -37,7 +37,10 @@ export const ROOT_URL = (() => { return `${window.location.protocol}//${window.location.host}${rootPath}`; })(); -export const ENKETO_ACTIONS = createEnum(['edit', 'view']); +export enum EnketoActions { + edit = 'edit', + view = 'view' +} export const HOOK_LOG_STATUSES = { SUCCESS: 2, diff --git a/jsapp/js/dataInterface.ts b/jsapp/js/dataInterface.ts index dd26e6e766..4020a37b79 100644 --- a/jsapp/js/dataInterface.ts +++ b/jsapp/js/dataInterface.ts @@ -863,6 +863,14 @@ interface DataInterface { [key: string]: Function; } +export interface EnketoLinkResponse { + url: string; + version_id: string; + responseJSON?: { + detail?: string; + } +} + const $ajax = (o: {}) => $.ajax(Object.assign({}, {dataType: 'json', method: 'GET'}, o)); @@ -1757,13 +1765,13 @@ export const dataInterface: DataInterface = { }); }, - getEnketoEditLink(uid: string, sid: string): JQuery.jqXHR { + getEnketoEditLink(uid: string, sid: string): JQuery.jqXHR { return $ajax({ url: `${ROOT_URL}/api/v2/assets/${uid}/data/${sid}/enketo/edit/?return_url=false`, method: 'GET', }); }, - getEnketoViewLink(uid: string, sid: string): JQuery.jqXHR { + getEnketoViewLink(uid: string, sid: string): JQuery.jqXHR { return $ajax({ url: `${ROOT_URL}/api/v2/assets/${uid}/data/${sid}/enketo/view/`, method: 'GET', diff --git a/jsapp/js/enketoHandler.es6 b/jsapp/js/enketoHandler.tsx similarity index 58% rename from jsapp/js/enketoHandler.es6 rename to jsapp/js/enketoHandler.tsx index 8717754c4a..1cdf119bf0 100644 --- a/jsapp/js/enketoHandler.es6 +++ b/jsapp/js/enketoHandler.tsx @@ -1,70 +1,76 @@ import React from 'react'; -import {ENKETO_ACTIONS} from 'js/constants' +import {EnketoActions} from 'js/constants' import {dataInterface} from 'js/dataInterface'; +import type {EnketoLinkResponse} from 'js/dataInterface'; import {notify} from 'js/utils'; /** * For handling Enketo in DRY way. */ -const enketoHandler = { - enketoUrls: new Map(), - winTab: null, +class EnketoHandler { + /** Map of `urlId`s (see `_getUrlId`) pointing to urls */ + enketoUrls: Map = new Map(); + winTab: null | WindowProxy = null; /** * Builds unique url id. */ - _getUrlId(aid, sid, action) { - return `${aid}-${sid}-${action}`; - }, + _getUrlId(assetUid: string, submissionUid: string, action: EnketoActions) { + return `${assetUid}-${submissionUid}-${action}`; + } - _hasEnketoUrl(urlId) { + _hasEnketoUrl(urlId: string) { return this.enketoUrls.has(urlId); - }, + } /** * Opens submission in new window. */ - _openEnketoUrl(urlId) { - this.winTab.location.href = this.enketoUrls.get(urlId); - }, + _openEnketoUrl(urlId: string) { + const enketoUrl = this.enketoUrls.get(urlId); + if (this.winTab !== null && enketoUrl !== undefined) { + this.winTab.location.href = enketoUrl; + } else { + notify.error(t('Could not open window for "##url##"').replace('##url##', String(enketoUrl))); + } + } - _saveEnketoUrl(urlId, url) { + _saveEnketoUrl(urlId: string, url: string) { this.enketoUrls.set(urlId, url); // store url for 30 seconds as configured in Enketo setTimeout(this._removeEnketoUrl.bind(this, urlId), 30 * 1000); - }, + } - _removeEnketoUrl(urlId) { + _removeEnketoUrl(urlId: string) { this.enketoUrls.delete(urlId); - }, + } /** * Opens submission url from cache or after getting it from endpoint. - * - * @param {string} aid - Asset id. - * @param {string} sid - Submission id. - * * @returns {Promise} Promise that resolves when url is being opened. */ - openSubmission(aid, sid, action) { + openSubmission(assetUid: string, submissionUid: string, action: EnketoActions) { // we create the tab immediately to avoid browser popup blocker killing it this.winTab = window.open('', '_blank'); + let dataIntMethod = dataInterface.getEnketoEditLink; - if ( action === ENKETO_ACTIONS.view ) { + if (action === EnketoActions.view) { dataIntMethod = dataInterface.getEnketoViewLink; } - const urlId = this._getUrlId(aid, sid, action); + + const urlId = this._getUrlId(assetUid, submissionUid, action); + const enketoPromise = new Promise((resolve, reject) => { if (this._hasEnketoUrl(urlId)) { this._openEnketoUrl(urlId); - resolve(); + resolve(false); } else { - dataIntMethod(aid, sid) - .always((enketoData) => { + dataIntMethod(assetUid, submissionUid) + .always((enketoData: EnketoLinkResponse) => { if (enketoData.url) { this._saveEnketoUrl(urlId, enketoData.url); this._openEnketoUrl(urlId); - resolve(); + resolve(false); } else { const errorMsg = (
@@ -79,17 +85,19 @@ const enketoHandler = { ); notify.error(errorMsg); - reject(); + reject(false); } }); } }).catch(() => { // close the blank tab since it will never load anything 😢 // (and it obscures the error message) - this.winTab.close(); + this.winTab?.close(); }); + return enketoPromise; - }, + } }; +const enketoHandler = new EnketoHandler(); export default enketoHandler; From 44c7b15cc3a60dc8a777b919671e435be7658d72 Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 20 May 2024 18:23:28 +0200 Subject: [PATCH 04/42] small post pageState typescriptization fixes --- jsapp/js/app.jsx | 6 +++--- jsapp/js/pageState.store.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jsapp/js/app.jsx b/jsapp/js/app.jsx index 159ecd8e71..f68fe3c6a2 100644 --- a/jsapp/js/app.jsx +++ b/jsapp/js/app.jsx @@ -46,12 +46,12 @@ class App extends React.Component { onRouteChange() { // slide out drawer overlay on every page change (better mobile experience) if (this.state.pageState.showFixedDrawer) { - stores.pageState.setState({showFixedDrawer: false}); + pageState.setState({showFixedDrawer: false}); } // hide modal on every page change if (this.state.pageState.modal) { - stores.pageState.hideModal(); + pageState.hideModal(); } } @@ -149,7 +149,7 @@ class App extends React.Component { App.contextTypes = {router: PropTypes.object}; -reactMixin(App.prototype, Reflux.connect(stores.pageState, 'pageState')); +reactMixin(App.prototype, Reflux.connect(pageState, 'pageState')); reactMixin(App.prototype, mixins.contextRouter); export default withRouter(App); diff --git a/jsapp/js/pageState.store.ts b/jsapp/js/pageState.store.ts index 7d4cb10061..029ae80584 100644 --- a/jsapp/js/pageState.store.ts +++ b/jsapp/js/pageState.store.ts @@ -5,7 +5,7 @@ interface PageStateModalParams { [name: string]: any; } -interface PageStateStoreState { +export interface PageStateStoreState { assetNavExpanded?: boolean; showFixedDrawer?: boolean; modal?: PageStateModalParams | false; From 5f0e3b5ae7e5ca57d611ef96fab7b00e06a12a1d Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 20 May 2024 18:24:18 +0200 Subject: [PATCH 05/42] change Icon component to functional one and fix KoboDropdown prop optionality --- jsapp/js/components/common/icon.tsx | 42 ++++++++++----------- jsapp/js/components/common/koboDropdown.tsx | 3 +- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/jsapp/js/components/common/icon.tsx b/jsapp/js/components/common/icon.tsx index 74625730c4..18e1ff0add 100644 --- a/jsapp/js/components/common/icon.tsx +++ b/jsapp/js/components/common/icon.tsx @@ -25,28 +25,24 @@ interface IconProps { /** * An icon component. */ -class Icon extends React.Component { - render() { - let classNames: string[] = []; - if ( - Array.isArray(this.props.classNames) && - typeof this.props.classNames[0] === 'string' - ) { - classNames = this.props.classNames; - } - - const size = this.props.size || DefaultSize; - classNames.push(`k-icon--size-${size}`); - - if (this.props.color) { - classNames.push(`k-icon--color-${this.props.color}`); - } - - classNames.push('k-icon'); - classNames.push(`k-icon-${this.props.name}`); - - return ; +export default function Icon(props: IconProps) { + let classNames: string[] = []; + if ( + Array.isArray(props.classNames) && + typeof props.classNames[0] === 'string' + ) { + classNames = props.classNames; } -} -export default Icon; + const size = props.size || DefaultSize; + classNames.push(`k-icon--size-${size}`); + + if (props.color) { + classNames.push(`k-icon--color-${props.color}`); + } + + classNames.push('k-icon'); + classNames.push(`k-icon-${props.name}`); + + return ; +} diff --git a/jsapp/js/components/common/koboDropdown.tsx b/jsapp/js/components/common/koboDropdown.tsx index 6aaa82ddd4..af10e029f1 100644 --- a/jsapp/js/components/common/koboDropdown.tsx +++ b/jsapp/js/components/common/koboDropdown.tsx @@ -16,7 +16,8 @@ export type KoboDropdownPlacement = const DEFAULT_PLACEMENT: KoboDropdownPlacement = 'down-center'; interface KoboDropdownProps { - placement: KoboDropdownPlacement; + /** Defaults to DEFAULT_PLACEMENT :wink: */ + placement?: KoboDropdownPlacement; isRequired?: boolean; /** Disables the dropdowns trigger, thus disallowing opening dropdown. */ isDisabled?: boolean; From 3d45bd6870dec4da4ed35f3a6bab7d0eb8e2bb37 Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 20 May 2024 18:25:29 +0200 Subject: [PATCH 06/42] AudioCell and MediaCell now correctly support error handling --- jsapp/js/components/submissions/audioCell.tsx | 57 ++++++++++--------- jsapp/js/components/submissions/mediaCell.tsx | 55 +++++++++++------- 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/jsapp/js/components/submissions/audioCell.tsx b/jsapp/js/components/submissions/audioCell.tsx index dadacfd8ea..62d7471f21 100644 --- a/jsapp/js/components/submissions/audioCell.tsx +++ b/jsapp/js/components/submissions/audioCell.tsx @@ -1,6 +1,7 @@ import React from 'react'; import bem, {makeBem} from 'js/bem'; import Button from 'js/components/common/button'; +import Icon from 'js/components/common/icon'; import MiniAudioPlayer from 'js/components/common/miniAudioPlayer'; import {goToProcessing} from 'js/components/processing/routes.utils'; import type {SubmissionAttachment} from 'js/dataInterface'; @@ -13,38 +14,40 @@ interface AudioCellProps { qpath: string; /* submissionEditId is meta/rootUuid || _uuid */ submissionEditId: string; - /** Required by the mini player. */ - mediaAttachment: SubmissionAttachment; + /** Required by the mini player. String passed is an error message */ + mediaAttachment: SubmissionAttachment | string; } /** * An alternative component to MediaCell for audio columns. It's a transitional * component created with Processing View in mind. It omits the modal. */ -export default class AudioCell extends React.Component { - render() { - return ( - - {this.props.mediaAttachment?.download_url && - - } - ); } @@ -113,30 +122,29 @@ export default function TableColumnSortDropdown(props: TableColumnSortDropdownPr {renderSortButton(SortValues.ASCENDING)} {renderSortButton(SortValues.DESCENDING)} - {userCan(PERMISSIONS_CODENAMES.change_asset, props.asset) && - - } - {userCan(PERMISSIONS_CODENAMES.change_asset, props.asset) && + )} + {userCan(PERMISSIONS_CODENAMES.change_asset, props.asset) && ( - } + )} } /> diff --git a/jsapp/js/dataInterface.ts b/jsapp/js/dataInterface.ts index ca73c2b6f4..c225ebdf4a 100644 --- a/jsapp/js/dataInterface.ts +++ b/jsapp/js/dataInterface.ts @@ -481,16 +481,22 @@ export interface TableSortBySetting { * None of these are actually stored as `null`s, but we use this interface for * a new settings draft too and it's simpler that way. */ -export interface AssetTableSettings { +interface AssetTableSettingsObject { 'selected-columns'?: string[] | null; 'frozen-column'?: string | null; 'show-group-name'?: boolean | null; 'translation-index'?: number | null; 'show-hxl-tags'?: boolean | null; 'sort-by'?: TableSortBySetting | null; - 'data-table'?: { - // TODO - } +} + +/** + * This interface consists of properties from `AssetTableSettingsObject` and one + * more property that holds a temporary copy of `AssetTableSettingsObject` + */ +export interface AssetTableSettings extends AssetTableSettingsObject { + /** This is the same object as AssetTableSettings */ + 'data-table'?: AssetTableSettingsObject } export interface AssetSettings { diff --git a/jsapp/js/pageState.store.ts b/jsapp/js/pageState.store.ts index 029ae80584..d5dc334564 100644 --- a/jsapp/js/pageState.store.ts +++ b/jsapp/js/pageState.store.ts @@ -11,10 +11,10 @@ export interface PageStateStoreState { modal?: PageStateModalParams | false; } -// DEPRECATED +// TODO: // This is some old weird store that is responsible for two things: -// 1. toggling mobile menu -// 2. handling modal from `bigModal.es6` +// 1. toggling mobile menu - should be moved to some other place +// 2. handling modal from `bigModal.es6` - should be moved somewhere near the modal files class PageStateStore extends Reflux.Store { state: PageStateStoreState = { assetNavExpanded: false, @@ -47,8 +47,10 @@ class PageStateStore extends Reflux.Store { }); } - // use it when you have one modal opened and want to display different one - // because just calling showModal has weird outcome + /** + * Use it when you have one modal opened and want to display different one + * (because just calling showModal has weird outcome). + */ switchModal(params: PageStateModalParams) { this.hideModal(); // HACK switch to setState callback after updating to React 16+ @@ -57,6 +59,9 @@ class PageStateStore extends Reflux.Store { }, 0); } + /** + * Use it when you have modal opened and want to go back to previous one. + */ switchToPreviousModal() { if (this.state.modal) { this.switchModal({ From 3f93bc6a1c1ee61ddf7dc9603934171cdb5687c1 Mon Sep 17 00:00:00 2001 From: Anji Tong Date: Thu, 23 May 2024 15:48:39 -0400 Subject: [PATCH 15/42] WIP expoential backoff function used instead of 5s --- .../processing/singleProcessingStore.ts | 19 ++++++++++++++----- .../processing/transxAutomaticButton.tsx | 10 +++++----- .../projectDownloads/exportFetcher.ts | 7 ++++++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/jsapp/js/components/processing/singleProcessingStore.ts b/jsapp/js/components/processing/singleProcessingStore.ts index 5e668f8c32..fe4b89713f 100644 --- a/jsapp/js/components/processing/singleProcessingStore.ts +++ b/jsapp/js/components/processing/singleProcessingStore.ts @@ -34,6 +34,7 @@ import { getCurrentProcessingRouteParts, ProcessingTab, } from 'js/components/processing/routes.utils'; +import {getExponentialDelayTime} from '../projectDownloads/exportFetcher'; export enum StaticDisplays { Data = 'Data', @@ -125,6 +126,7 @@ interface SingleProcessingStoreData { isFetchingData: boolean; isPollingForTranscript: boolean; hiddenSidebarQuestions: string[]; + exponentialBackoffCount: number; } class SingleProcessingStore extends Reflux.Store { @@ -155,6 +157,7 @@ class SingleProcessingStore extends Reflux.Store { isFetchingData: false, isPollingForTranscript: false, hiddenSidebarQuestions: [], + exponentialBackoffCount: 1, }; /** Clears all data - useful before making initialisation call */ @@ -167,6 +170,7 @@ class SingleProcessingStore extends Reflux.Store { this.data.translationDraft = undefined; this.data.source = undefined; this.data.isPristine = true; + this.data.exponentialBackoffCount = 1; } public get currentAssetUid() { @@ -680,13 +684,16 @@ class SingleProcessingStore extends Reflux.Store { setTimeout(() => { // make sure to check for applicability *after* the timeout fires, not // before. someone can do a lot of navigating in 5 seconds + console.log('this.data befoer the if', this.data); if (this.isAutoTranscriptionEventApplicable(event)) { + this.incrementExponentialBackoffCount(); this.data.isPollingForTranscript = true; this.requestAutoTranscription(); } else { this.data.isPollingForTranscript = false; + console.log('done?'); } - }, 5000); + }, getExponentialDelayTime(this.data.exponentialBackoffCount)); } private onSetTranslationCompleted(newTranslations: Transx[]) { @@ -717,6 +724,11 @@ class SingleProcessingStore extends Reflux.Store { this.trigger(this.data); } + private incrementExponentialBackoffCount() { + this.data.exponentialBackoffCount = this.data.exponentialBackoffCount + 1; + this.trigger(this.data); + } + /** * Returns a list of selectable language codes. * Omits the one currently being edited. @@ -967,10 +979,7 @@ class SingleProcessingStore extends Reflux.Store { /** Returns available displays for given tab */ getAvailableDisplays(tabName: ProcessingTab) { - const outcome: DisplaysList = [ - StaticDisplays.Audio, - StaticDisplays.Data, - ]; + const outcome: DisplaysList = [StaticDisplays.Audio, StaticDisplays.Data]; if (tabName !== ProcessingTab.Transcript && this.data.transcript) { outcome.push(StaticDisplays.Transcript); } diff --git a/jsapp/js/components/processing/transxAutomaticButton.tsx b/jsapp/js/components/processing/transxAutomaticButton.tsx index 2956793492..fe3f9bd218 100644 --- a/jsapp/js/components/processing/transxAutomaticButton.tsx +++ b/jsapp/js/components/processing/transxAutomaticButton.tsx @@ -109,10 +109,10 @@ export default class TransxAutomaticButton extends React.Component< } render() { - if (!envStore.data.asr_mt_features_enabled) { - // We hide button for users that don't have access to the feature. - return null; - } else { + //if (!envStore.data.asr_mt_features_enabled) { + // // We hide button for users that don't have access to the feature. + // return null; + //} else { return ( @@ -97,5 +107,3 @@ class AssetContentSummary extends React.Component { ); } } - -export default AssetContentSummary; diff --git a/jsapp/js/components/library/assetInfoBox.es6 b/jsapp/js/components/library/assetInfoBox.tsx similarity index 81% rename from jsapp/js/components/library/assetInfoBox.es6 rename to jsapp/js/components/library/assetInfoBox.tsx index 2069729cf6..0c6a2ffa1f 100644 --- a/jsapp/js/components/library/assetInfoBox.es6 +++ b/jsapp/js/components/library/assetInfoBox.tsx @@ -1,7 +1,4 @@ import React from 'react'; -import reactMixin from 'react-mixin'; -import autoBind from 'react-autobind'; -import Reflux from 'reflux'; import bem from 'js/bem'; import {actions} from 'js/actions'; import sessionStore from 'js/stores/session'; @@ -10,37 +7,57 @@ import {ASSET_TYPES} from 'js/constants'; import { notify, formatTime, -} from 'utils'; +} from 'js/utils'; import './assetInfoBox.scss'; +import type {AssetResponse, AccountResponse} from 'js/dataInterface'; + +interface AssetInfoBoxProps { + asset: AssetResponse; +} + +interface AssetInfoBoxState { + areDetailsVisible: boolean; + ownerData: AccountResponse | {username: string; date_joined: string} | null; +} /** - * @prop asset + * Displays some meta information about given asset. */ -class AssetInfoBox extends React.Component { - constructor(props){ +export default class AssetInfoBox extends React.Component< + AssetInfoBoxProps, + AssetInfoBoxState +> { + private unlisteners: Function[] = []; + + constructor(props: AssetInfoBoxProps){ super(props); this.state = { areDetailsVisible: false, ownerData: null, }; - autoBind(this); } componentDidMount() { if (!assetUtils.isSelfOwned(this.props.asset)) { - this.listenTo(actions.misc.getUser.completed, this.onGetUserCompleted); - this.listenTo(actions.misc.getUser.failed, this.onGetUserFailed); + this.unlisteners.push( + actions.misc.getUser.completed.listen(this.onGetUserCompleted.bind(this)), + actions.misc.getUser.failed.listen(this.onGetUserFailed.bind(this)) + ) actions.misc.getUser(this.props.asset.owner); } else { this.setState({ownerData: sessionStore.currentAccount}); } } + componentWillUnmount() { + this.unlisteners.forEach((clb) => {clb();}); + } + toggleDetails() { this.setState({areDetailsVisible: !this.state.areDetailsVisible}); } - onGetUserCompleted(userData) { + onGetUserCompleted(userData: AccountResponse) { this.setState({ownerData: userData}); } @@ -136,7 +153,7 @@ class AssetInfoBox extends React.Component { - + {this.state.areDetailsVisible ? : } {this.state.areDetailsVisible ? t('Hide full details') : t('Show full details')} @@ -145,7 +162,3 @@ class AssetInfoBox extends React.Component { ); } } - -reactMixin(AssetInfoBox.prototype, Reflux.ListenerMixin); - -export default AssetInfoBox; diff --git a/jsapp/js/components/library/assetPublicButton.es6 b/jsapp/js/components/library/assetPublicButton.tsx similarity index 70% rename from jsapp/js/components/library/assetPublicButton.es6 rename to jsapp/js/components/library/assetPublicButton.tsx index 1482177e26..6268af9672 100644 --- a/jsapp/js/components/library/assetPublicButton.es6 +++ b/jsapp/js/components/library/assetPublicButton.tsx @@ -1,36 +1,53 @@ import React from 'react'; -import autoBind from 'react-autobind'; import bem from 'js/bem'; import {actions} from 'js/actions'; import assetUtils from 'js/assetUtils'; import {ASSET_TYPES} from 'js/constants'; -import { - notify -} from 'js/utils'; +import {notify} from 'js/utils'; +import type {AssetResponse} from 'js/dataInterface'; + +interface AssetPublicButtonProps { + asset: AssetResponse; +} + +interface AssetPublicButtonState { + isPublicPending: boolean; + isAwaitingFreshPermissions: boolean; +} /** - * @prop asset + * Button for making asset (works only for `collection` type) public or non-public. */ -class AssetPublicButton extends React.Component { - constructor(props){ +export default class AssetPublicButton extends React.Component< + AssetPublicButtonProps, + AssetPublicButtonState +> { + private unlisteners: Function[] = []; + + constructor(props: AssetPublicButtonProps) { super(props); this.state = { isPublicPending: false, isAwaitingFreshPermissions: false }; - autoBind(this); } componentDidMount() { - actions.permissions.setAssetPublic.completed.listen(this.onSetAssetPublicCompleted); - actions.permissions.setAssetPublic.failed.listen(this.onSetAssetPublicFailed); + this.unlisteners.push( + actions.permissions.setAssetPublic.completed.listen(this.onSetAssetPublicCompleted.bind(this)), + actions.permissions.setAssetPublic.failed.listen(this.onSetAssetPublicFailed.bind(this)) + ); + } + + componentWillUnmount() { + this.unlisteners.forEach((clb) => {clb();}); } componentWillReceiveProps() { this.setState({isAwaitingFreshPermissions: false}); } - onSetAssetPublicCompleted(assetUid) { + onSetAssetPublicCompleted(assetUid: string) { if (this.props.asset.uid === assetUid) { this.setState({ isPublicPending: false, @@ -39,7 +56,7 @@ class AssetPublicButton extends React.Component { } } - onSetAssetPublicFailed(assetUid) { + onSetAssetPublicFailed(assetUid: string) { if (this.props.asset.uid === assetUid) { this.setState({isPublicPending: false}); notify(t('Failed to change asset public status.'), 'error'); @@ -86,7 +103,7 @@ class AssetPublicButton extends React.Component { {!isPublic && @@ -96,7 +113,7 @@ class AssetPublicButton extends React.Component { {isPublic && @@ -107,5 +124,3 @@ class AssetPublicButton extends React.Component { ); } } - -export default AssetPublicButton; diff --git a/jsapp/js/components/library/assetRoute.js b/jsapp/js/components/library/assetRoute.tsx similarity index 65% rename from jsapp/js/components/library/assetRoute.js rename to jsapp/js/components/library/assetRoute.tsx index 5ef78b8d8f..6b19aa321f 100644 --- a/jsapp/js/components/library/assetRoute.js +++ b/jsapp/js/components/library/assetRoute.tsx @@ -1,11 +1,7 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import autoBind from 'react-autobind'; -import reactMixin from 'react-mixin'; -import Reflux from 'reflux'; +import clonedeep from 'lodash.clonedeep'; import DocumentTitle from 'react-document-title'; import bem from 'js/bem'; -import mixins from 'js/mixins'; import {actions} from 'js/actions'; import assetUtils from 'js/assetUtils'; import {ASSET_TYPES, ACCESS_TYPES} from 'js/constants'; @@ -16,24 +12,42 @@ import AssetBreadcrumbs from './assetBreadcrumbs'; import AssetContentSummary from './assetContentSummary'; import CollectionAssetsTable from 'js/components/library/collectionAssetsTable'; import LoadingSpinner from 'js/components/common/loadingSpinner'; +import {getRouteAssetUid} from 'js/router/routerUtils'; +import type {AssetResponse} from 'js/dataInterface'; -class AssetRoute extends React.Component { - constructor(props) { +interface AssetRouteProps { + params: { + uid: string; + } +} + +interface AssetRouteState { + asset: AssetResponse | undefined; +} + +export default class AssetRoute extends React.Component< + AssetRouteProps, + AssetRouteState +> { + private unlisteners: Function[] = []; + + constructor(props: AssetRouteProps) { super(props); - this.state = {asset: false}; - this.unlisteners = []; - autoBind(this); + + this.state = { + asset: undefined, + }; } componentDidMount() { this.unlisteners.push( - actions.library.moveToCollection.completed.listen(this.onMoveToCollectionCompleted), - actions.library.subscribeToCollection.completed.listen(this.onSubscribeToCollectionCompleted), - actions.library.unsubscribeFromCollection.completed.listen(this.onUnsubscribeFromCollectionCompleted), - actions.resources.loadAsset.completed.listen(this.onAssetChanged), - actions.resources.updateAsset.completed.listen(this.onAssetChanged), - actions.resources.cloneAsset.completed.listen(this.onAssetChanged), - actions.resources.createResource.completed.listen(this.onAssetChanged), + actions.library.moveToCollection.completed.listen(this.onAssetChanged.bind(this)), + actions.library.subscribeToCollection.completed.listen(this.onSubscribeToCollectionCompleted.bind(this)), + actions.library.unsubscribeFromCollection.completed.listen(this.onUnsubscribeFromCollectionCompleted.bind(this)), + actions.resources.loadAsset.completed.listen(this.onAssetChanged.bind(this)), + actions.resources.updateAsset.completed.listen(this.onAssetChanged.bind(this)), + actions.resources.cloneAsset.completed.listen(this.onAssetChanged.bind(this)), + actions.resources.createResource.completed.listen(this.onAssetChanged.bind(this)), ); this.loadCurrentAsset(); } @@ -42,16 +56,16 @@ class AssetRoute extends React.Component { this.unlisteners.forEach((clb) => {clb();}); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: AssetRouteProps) { // trigger loading when switching assets if (nextProps.params.uid !== this.props.params.uid) { - this.setState({asset: false}); + this.setState({asset: undefined}); this.loadCurrentAsset(); } } loadCurrentAsset() { - const uid = this.currentAssetID(); + const uid = getRouteAssetUid(); if (uid) { actions.resources.loadAsset({id: uid}); } @@ -65,35 +79,40 @@ class AssetRoute extends React.Component { this.onAssetAccessTypeChanged(false); } - onAssetAccessTypeChanged(setSubscribed) { - let newAsset = this.state.asset; - if (setSubscribed) { - newAsset.access_types.push(ACCESS_TYPES.subscribed); - } else { - newAsset.access_types.splice( - newAsset.access_types.indexOf(ACCESS_TYPES.subscribed), - 1 - ); + /** + * This updates the local asset object, avoiding the need to fetch whole thing + * from Back End. + */ + onAssetAccessTypeChanged(setSubscribed: boolean) { + let newAsset = clonedeep(this.state.asset); + if (newAsset) { + if (setSubscribed && newAsset.access_types === null) { + newAsset.access_types = [ACCESS_TYPES.subscribed]; + } else if (setSubscribed && newAsset.access_types !== null) { + newAsset.access_types.push(ACCESS_TYPES.subscribed); + } else if (!setSubscribed && newAsset.access_types !== null) { + newAsset.access_types.splice( + newAsset.access_types.indexOf(ACCESS_TYPES.subscribed), + 1 + ); + // Cleanup if empty array is left + if (newAsset.access_types.length === 0) { + newAsset.access_types = null; + } + } + + this.setState({asset: newAsset}); } - this.setState({asset: newAsset}); } - onMoveToCollectionCompleted(asset) { - if (asset.parent === null) { - this.onAssetRemoved(asset.uid); - } else { - this.onAssetChanged(asset); - } - } - - onAssetChanged(asset) { - if (asset.uid === this.currentAssetID()) { + onAssetChanged(asset: AssetResponse) { + if (asset.uid === getRouteAssetUid()) { this.setState({asset: asset}); } } render() { - if (this.state.asset === false) { + if (!this.state.asset) { return (); } @@ -148,12 +167,3 @@ class AssetRoute extends React.Component { ); } } - -reactMixin(AssetRoute.prototype, mixins.contextRouter); -reactMixin(AssetRoute.prototype, Reflux.ListenerMixin); - -AssetRoute.contextTypes = { - router: PropTypes.object -}; - -export default AssetRoute; diff --git a/jsapp/js/components/library/librarySidebar.es6 b/jsapp/js/components/library/librarySidebar.tsx similarity index 78% rename from jsapp/js/components/library/librarySidebar.es6 rename to jsapp/js/components/library/librarySidebar.tsx index 28622d2feb..40f9382b86 100644 --- a/jsapp/js/components/library/librarySidebar.es6 +++ b/jsapp/js/components/library/librarySidebar.tsx @@ -1,29 +1,38 @@ import React from 'react'; -import Reflux from 'reflux'; -import reactMixin from 'react-mixin'; -import PropTypes from 'prop-types'; -import autoBind from 'react-autobind'; import {stores} from 'js/stores'; import sessionStore from 'js/stores/session'; import bem from 'js/bem'; import {MODAL_TYPES} from 'js/constants'; import myLibraryStore from './myLibraryStore'; -import { routerIsActive } from '../../router/legacy'; -import {ROUTES} from '../../router/routerConstants'; +import {routerIsActive} from 'js/router/legacy'; +import {ROUTES} from 'js/router/routerConstants'; import {NavLink} from 'react-router-dom'; -class LibrarySidebar extends Reflux.Component { - constructor(props){ +interface LibrarySidebarProps {} + +interface LibrarySidebarState { + myLibraryCount: number; + isLoading: boolean; +} + +/** + * Displays "NEW" button (for adding a Library item) and two navigation links + * pointing to "My Library" and "Public Collections". + */ +export default class LibrarySidebar extends React.Component< + LibrarySidebarProps, + LibrarySidebarState +> { + constructor(props: LibrarySidebarProps) { super(props); this.state = { myLibraryCount: 0, isLoading: true }; - autoBind(this); } componentDidMount() { - this.listenTo(myLibraryStore, this.myLibraryStoreChanged); + myLibraryStore.listen(this.myLibraryStoreChanged.bind(this)); this.setState({ isLoading: false, myLibraryCount: myLibraryStore.getCurrentUserTotalAssets() @@ -59,11 +68,11 @@ class LibrarySidebar extends Reflux.Component { } return ( - + <> {t('new')} @@ -94,15 +103,7 @@ class LibrarySidebar extends Reflux.Component { - + ); } } - -LibrarySidebar.contextTypes = { - router: PropTypes.object -}; - -reactMixin(LibrarySidebar.prototype, Reflux.ListenerMixin); - -export default LibrarySidebar; From 4f3ea76bb2b3432490b13208b6e5ee5804d86040 Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 3 Jun 2024 15:16:37 +0200 Subject: [PATCH 20/42] typescriptize MyLibraryRoute and PublicCollectionsRoute components --- .../js/components/assetsTable/assetsTable.tsx | 16 ++-- .../{myLibraryRoute.js => myLibraryRoute.tsx} | 83 +++++++++---------- jsapp/js/components/library/myLibraryStore.ts | 2 +- ...onsRoute.js => publicCollectionsRoute.tsx} | 56 +++++-------- .../library/publicCollectionsStore.ts | 17 ++-- 5 files changed, 86 insertions(+), 88 deletions(-) rename jsapp/js/components/library/{myLibraryRoute.js => myLibraryRoute.tsx} (62%) rename jsapp/js/components/library/{publicCollectionsRoute.js => publicCollectionsRoute.tsx} (62%) diff --git a/jsapp/js/components/assetsTable/assetsTable.tsx b/jsapp/js/components/assetsTable/assetsTable.tsx index 6161c4eb5a..f3a2c5bd71 100644 --- a/jsapp/js/components/assetsTable/assetsTable.tsx +++ b/jsapp/js/components/assetsTable/assetsTable.tsx @@ -48,17 +48,17 @@ interface AssetsTableProps { /** Displays a spinner */ isLoading?: boolean; /** To display contextual empty message when zero assets. */ - emptyMessage?: string; + emptyMessage?: React.ReactNode; /** List of assets to be displayed. */ assets: AssetResponse[]; /** Number of assets on all pages. */ - totalAssets: number; + totalAssets: number | null; /** List of available filters values. */ metadata?: MetadataResponse; // this type ?? /** Seleceted order column id, one of ASSETS_TABLE_COLUMNS. */ orderColumnId: string; - /** Seleceted order column value. */ - orderValue: string; + /** Seleceted order column value. Defaults to "ascending" */ + orderValue: OrderDirection | null; /** Called when user selects a column for odering. */ onOrderChange: OrderChangeCallback; /** Seleceted filter column, one of ASSETS_TABLE_COLUMNS. */ @@ -85,7 +85,13 @@ interface AssetsTableState { } /** - * Displays a table of assets. + * DEPRECATED-ish (see below) + * Displays a table of assets. This old-ish component is handling three routes: + * - My Library + * - Public Collections + * - Single Collection + * The new and shiny component that handles Projects List is `ProjectsTable`, + * and in the future it should become (if possible) the only one to be used. */ export default class AssetsTable extends React.Component< AssetsTableProps, diff --git a/jsapp/js/components/library/myLibraryRoute.js b/jsapp/js/components/library/myLibraryRoute.tsx similarity index 62% rename from jsapp/js/components/library/myLibraryRoute.js rename to jsapp/js/components/library/myLibraryRoute.tsx index 49aaccb022..d317b00e1d 100644 --- a/jsapp/js/components/library/myLibraryRoute.js +++ b/jsapp/js/components/library/myLibraryRoute.tsx @@ -1,34 +1,35 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import reactMixin from 'react-mixin'; -import autoBind from 'react-autobind'; -import Reflux from 'reflux'; import DocumentTitle from 'react-document-title'; import Dropzone from 'react-dropzone'; -import mixins from 'js/mixins'; import bem from 'js/bem'; +import mixins from 'js/mixins'; import {stores} from 'js/stores'; -import {validFileTypes} from 'utils'; +import {validFileTypes} from 'js/utils'; import myLibraryStore from './myLibraryStore'; import AssetsTable from 'js/components/assetsTable/assetsTable'; import {MODAL_TYPES} from 'js/constants'; import {ROOT_BREADCRUMBS} from 'js/components/library/libraryConstants'; -import {ASSETS_TABLE_CONTEXTS} from 'js/components/assetsTable/assetsTableConstants'; +import {AssetsTableContextName} from 'js/components/assetsTable/assetsTableConstants'; +import type {MyLibraryStoreData} from './myLibraryStore'; +import type {OrderDirection} from 'js/projects/projectViews/constants'; +import type {FileWithPreview} from 'react-dropzone'; +import type {DragEvent} from 'react'; -class MyLibraryRoute extends React.Component { - constructor(props) { - super(props); - this.state = this.getFreshState(); - this.unlisteners = []; - autoBind(this); - } +export default class MyLibraryRoute extends React.Component< + {}, + MyLibraryStoreData +> { + private unlisteners: Function[] = []; + + state = this.getFreshState(); - getFreshState() { + getFreshState(): MyLibraryStoreData { return { - isLoading: myLibraryStore.data.isFetchingData, + isFetchingData: myLibraryStore.data.isFetchingData, assets: myLibraryStore.data.assets, metadata: myLibraryStore.data.metadata, - totalAssets: myLibraryStore.data.totalSearchAssets, + totalUserAssets: myLibraryStore.data.totalUserAssets, + totalSearchAssets: myLibraryStore.data.totalSearchAssets, orderColumnId: myLibraryStore.data.orderColumnId, orderValue: myLibraryStore.data.orderValue, filterColumnId: myLibraryStore.data.filterColumnId, @@ -40,7 +41,7 @@ class MyLibraryRoute extends React.Component { componentDidMount() { this.unlisteners.push( - myLibraryStore.listen(this.myLibraryStoreChanged) + myLibraryStore.listen(this.myLibraryStoreChanged.bind(this), this) ); } @@ -52,15 +53,15 @@ class MyLibraryRoute extends React.Component { this.setState(this.getFreshState()); } - onAssetsTableOrderChange(orderColumnId, orderValue) { - myLibraryStore.setOrder(orderColumnId, orderValue); + onAssetsTableOrderChange(columnId: string, value: OrderDirection) { + myLibraryStore.setOrder(columnId, value); } - onAssetsTableFilterChange(filterColumnId, filterValue) { - myLibraryStore.setFilter(filterColumnId, filterValue); + onAssetsTableFilterChange(columnId: string | null, value: string | null) { + myLibraryStore.setFilter(columnId, value); } - onAssetsTableSwitchPage(pageNumber) { + onAssetsTableSwitchPage(pageNumber: number) { myLibraryStore.setCurrentPage(pageNumber); } @@ -68,19 +69,24 @@ class MyLibraryRoute extends React.Component { * If only one file was passed, then open a modal for selecting the type. * Otherwise just start uploading all files. */ - onFileDrop(files, rejectedFiles, evt) { - if (files.length === 1) { + onFileDrop( + acceptedFiles: FileWithPreview[], + rejectedFiles: FileWithPreview[], + evt: DragEvent + ) { + if (acceptedFiles.length === 1) { stores.pageState.switchModal({ type: MODAL_TYPES.LIBRARY_UPLOAD, - file: files[0], + file: acceptedFiles[0], }); } else { - this.dropFiles(files, rejectedFiles, evt); + // TODO comes from mixin + mixins.droppable.dropFiles(acceptedFiles, rejectedFiles, evt); } } render() { - let contextualEmptyMessage = t('Your search returned no results.'); + let contextualEmptyMessage: React.ReactNode = t('Your search returned no results.'); if (myLibraryStore.data.totalUserAssets === 0) { contextualEmptyMessage = ( @@ -96,7 +102,7 @@ class MyLibraryRoute extends React.Component { return ( @@ -134,12 +140,3 @@ class MyLibraryRoute extends React.Component { ); } } - -MyLibraryRoute.contextTypes = { - router: PropTypes.object, -}; - -reactMixin(MyLibraryRoute.prototype, mixins.droppable); -reactMixin(MyLibraryRoute.prototype, Reflux.ListenerMixin); - -export default MyLibraryRoute; diff --git a/jsapp/js/components/library/myLibraryStore.ts b/jsapp/js/components/library/myLibraryStore.ts index 392fa9acd6..52ae6a5ad3 100644 --- a/jsapp/js/components/library/myLibraryStore.ts +++ b/jsapp/js/components/library/myLibraryStore.ts @@ -21,7 +21,7 @@ import type { import type {AssetTypeName} from 'js/constants'; import {reaction} from 'mobx'; -interface MyLibraryStoreData { +export interface MyLibraryStoreData { isFetchingData: boolean; currentPage: number; totalPages: number | null; diff --git a/jsapp/js/components/library/publicCollectionsRoute.js b/jsapp/js/components/library/publicCollectionsRoute.tsx similarity index 62% rename from jsapp/js/components/library/publicCollectionsRoute.js rename to jsapp/js/components/library/publicCollectionsRoute.tsx index 32668c0653..05b1bf872a 100644 --- a/jsapp/js/components/library/publicCollectionsRoute.js +++ b/jsapp/js/components/library/publicCollectionsRoute.tsx @@ -1,29 +1,27 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import reactMixin from 'react-mixin'; -import autoBind from 'react-autobind'; -import Reflux from 'reflux'; import DocumentTitle from 'react-document-title'; import bem from 'js/bem'; import publicCollectionsStore from './publicCollectionsStore'; import AssetsTable from 'js/components/assetsTable/assetsTable'; import {ROOT_BREADCRUMBS} from 'js/components/library/libraryConstants'; -import {ASSETS_TABLE_CONTEXTS} from 'js/components/assetsTable/assetsTableConstants'; +import {AssetsTableContextName} from 'js/components/assetsTable/assetsTableConstants'; +import type {PublicCollectionsStoreData} from './publicCollectionsStore'; +import type {OrderDirection} from 'js/projects/projectViews/constants'; -class PublicCollectionsRoute extends React.Component { - constructor(props) { - super(props); - this.state = this.getFreshState(); - this.unlisteners = []; - autoBind(this); - } +export default class PublicCollectionsRoute extends React.Component< + {}, + PublicCollectionsStoreData +> { + private unlisteners: Function[] = []; + + state = this.getFreshState(); getFreshState() { return { - isLoading: publicCollectionsStore.data.isFetchingData, + isFetchingData: publicCollectionsStore.data.isFetchingData, assets: publicCollectionsStore.data.assets, metadata: publicCollectionsStore.data.metadata, - totalAssets: publicCollectionsStore.data.totalSearchAssets, + totalSearchAssets: publicCollectionsStore.data.totalSearchAssets, orderColumnId: publicCollectionsStore.data.orderColumnId, orderValue: publicCollectionsStore.data.orderValue, filterColumnId: publicCollectionsStore.data.filterColumnId, @@ -35,7 +33,7 @@ class PublicCollectionsRoute extends React.Component { componentDidMount() { this.unlisteners.push( - publicCollectionsStore.listen(this.publicCollectionsStoreChanged) + publicCollectionsStore.listen(this.publicCollectionsStoreChanged.bind(this), this) ); } @@ -47,15 +45,15 @@ class PublicCollectionsRoute extends React.Component { this.setState(this.getFreshState()); } - onAssetsTableOrderChange(orderColumnId, orderValue) { - publicCollectionsStore.setOrder(orderColumnId, orderValue); + onAssetsTableOrderChange(columnId: string, value: OrderDirection) { + publicCollectionsStore.setOrder(columnId, value); } - onAssetsTableFilterChange(filterColumnId, filterValue) { - publicCollectionsStore.setFilter(filterColumnId, filterValue); + onAssetsTableFilterChange(columnId: string | null, value: string | null) { + publicCollectionsStore.setFilter(columnId, value); } - onAssetsTableSwitchPage(pageNumber) { + onAssetsTableSwitchPage(pageNumber: number) { publicCollectionsStore.setCurrentPage(pageNumber); } @@ -68,19 +66,19 @@ class PublicCollectionsRoute extends React.Component {
@@ -88,11 +86,3 @@ class PublicCollectionsRoute extends React.Component { ); } } - -PublicCollectionsRoute.contextTypes = { - router: PropTypes.object -}; - -reactMixin(PublicCollectionsRoute.prototype, Reflux.ListenerMixin); - -export default PublicCollectionsRoute; diff --git a/jsapp/js/components/library/publicCollectionsStore.ts b/jsapp/js/components/library/publicCollectionsStore.ts index 51b1ca7c4e..b9b451557e 100644 --- a/jsapp/js/components/library/publicCollectionsStore.ts +++ b/jsapp/js/components/library/publicCollectionsStore.ts @@ -8,6 +8,7 @@ import { ORDER_DIRECTIONS, ASSETS_TABLE_COLUMNS, } from 'js/components/assetsTable/assetsTableConstants'; +import type {OrderDirection} from 'js/projects/projectViews/constants'; import type {AssetsTableColumn} from 'js/components/assetsTable/assetsTableConstants'; import {ASSET_TYPES, ACCESS_TYPES} from 'js/constants'; import {ROUTES} from 'js/router/routerConstants'; @@ -22,17 +23,17 @@ import type { } from 'js/dataInterface'; import {reaction} from 'mobx'; -interface PublicCollectionsStoreData { +export interface PublicCollectionsStoreData { isFetchingData: boolean; currentPage: number; totalPages: number | null; totalSearchAssets: number | null; assets: AssetResponse[]; metadata: MetadataResponse; - orderColumnId?: string; - orderValue?: string | null; - filterColumnId?: string | null; - filterValue?: string | null; + orderColumnId: string; + orderValue: OrderDirection | null | undefined; + filterColumnId: string | null; + filterValue: string | null; } class PublicCollectionsStore extends Reflux.Store { @@ -60,6 +61,10 @@ class PublicCollectionsStore extends Reflux.Store { sectors: [], organizations: [], }, + orderColumnId: this.DEFAULT_ORDER_COLUMN.id, + orderValue: this.DEFAULT_ORDER_COLUMN.defaultValue, + filterColumnId: null, + filterValue: null, }; init() { @@ -339,7 +344,7 @@ class PublicCollectionsStore extends Reflux.Store { this.fetchData(); } - setOrder(orderColumnId: string, orderValue: string) { + setOrder(orderColumnId: string, orderValue: OrderDirection) { if ( this.data.orderColumnId !== orderColumnId || this.data.orderValue !== orderValue From bc9df2e89cb62c6cb2bdd2ea01a517330871b34e Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 3 Jun 2024 18:05:08 +0200 Subject: [PATCH 21/42] fix Make Public button not unlocking itself --- jsapp/js/actions.es6 | 3 - jsapp/js/components/common/button.scss | 14 ++++ jsapp/js/components/common/button.tsx | 1 + .../components/library/assetPublicButton.tsx | 76 +++++++++++-------- 4 files changed, 61 insertions(+), 33 deletions(-) diff --git a/jsapp/js/actions.es6 b/jsapp/js/actions.es6 index 6c8ffd8edc..9eeabf0aa6 100644 --- a/jsapp/js/actions.es6 +++ b/jsapp/js/actions.es6 @@ -103,9 +103,6 @@ permissionsActions.assignAssetPermission.completed.listen((uid) => { permissionsActions.copyPermissionsFrom.completed.listen((sourceUid, targetUid) => { actions.resources.loadAsset({id: targetUid}); }); -permissionsActions.setAssetPublic.completed.listen((uid) => { - actions.resources.loadAsset({id: uid}); -}); permissionsActions.removeAssetPermission.completed.listen((uid, isNonOwner) => { // Avoid this call if a non-owner removed their own permissions as it will fail if (!isNonOwner) { diff --git a/jsapp/js/components/common/button.scss b/jsapp/js/components/common/button.scss index b243efdeb3..07e62dd235 100644 --- a/jsapp/js/components/common/button.scss +++ b/jsapp/js/components/common/button.scss @@ -219,6 +219,20 @@ $button-border-radius: sizes.$x6; justify-content: center; } +// teal button ↓ + +.k-button.k-button--color-teal.k-button--type-bare { + @include button-bare(colors.$kobo-teal, colors.$kobo-hover-teal); +} + +.k-button.k-button--color-teal.k-button--type-frame { + @include button-frame(colors.$kobo-teal); +} + +.k-button.k-button--color-teal.k-button--type-full { + @include button-full(colors.$kobo-teal, colors.$kobo-hover-teal); +} + // blue button ↓ .k-button.k-button--color-blue.k-button--type-bare { diff --git a/jsapp/js/components/common/button.tsx b/jsapp/js/components/common/button.tsx index 917e200106..b079e113b6 100644 --- a/jsapp/js/components/common/button.tsx +++ b/jsapp/js/components/common/button.tsx @@ -20,6 +20,7 @@ import {useId} from 'js/hooks/useId.hook'; */ export type ButtonType = 'bare' | 'frame' | 'full'; export type ButtonColor = + | 'teal' | 'blue' | 'light-blue' | 'red' diff --git a/jsapp/js/components/library/assetPublicButton.tsx b/jsapp/js/components/library/assetPublicButton.tsx index 6268af9672..2e50405a18 100644 --- a/jsapp/js/components/library/assetPublicButton.tsx +++ b/jsapp/js/components/library/assetPublicButton.tsx @@ -4,6 +4,7 @@ import {actions} from 'js/actions'; import assetUtils from 'js/assetUtils'; import {ASSET_TYPES} from 'js/constants'; import {notify} from 'js/utils'; +import Button from 'js/components/common/button'; import type {AssetResponse} from 'js/dataInterface'; interface AssetPublicButtonProps { @@ -12,6 +13,10 @@ interface AssetPublicButtonProps { interface AssetPublicButtonState { isPublicPending: boolean; + /** + * After asset public state is changed, we wait for the asset to be loaded + * again, so that we know from the permissions `assetUtils.isAssetPublic`. + */ isAwaitingFreshPermissions: boolean; } @@ -51,8 +56,20 @@ export default class AssetPublicButton extends React.Component< if (this.props.asset.uid === assetUid) { this.setState({ isPublicPending: false, + // Public state of asset changed, now we await fresh permissions isAwaitingFreshPermissions: true, }); + + // We need to get fresh asset here, so that new permissions would be + // available for the button code. We rely on the fact that new asset + // would be passed through `props` and `componentWillReceiveProps` will + // unlock the button again. + // + // TODO: this flow should be improved, but it might require some more + // thought, as the asset data flow in the whole app should be redone + // (after very thorough planning). Unfortunately many places in the app + // have this problem. + actions.resources.loadAsset({id: this.props.asset.uid}, true); } } @@ -79,10 +96,6 @@ export default class AssetPublicButton extends React.Component< actions.permissions.setAssetPublic(this.props.asset, false); } - isSetPublicButtonDisabled() { - return this.state.isPublicPending || this.state.isAwaitingFreshPermissions; - } - render() { if (!this.props.asset) { return null; @@ -91,36 +104,39 @@ export default class AssetPublicButton extends React.Component< const isPublicable = this.props.asset.asset_type === ASSET_TYPES.collection.id; const isPublic = isPublicable && assetUtils.isAssetPublic(this.props.asset.permissions); const isSelfOwned = assetUtils.isSelfOwned(this.props.asset); + const isButtonPending = this.state.isPublicPending || this.state.isAwaitingFreshPermissions; if (!isPublicable || !isSelfOwned) { return null; } - return ( - - {/* NOTE: this button is purposely available for not ready - collections as a means to teach users (via error notifications). */} - {!isPublic && - - - {t('Make public')} - - } - {isPublic && - - - {t('Make private')} - - } - - ); + // NOTE: this button is purposely made available for collections that are + // not ready yet (i.e. the required metadata of the collection is empty), + // as we display an error notification that teaches users what to do. + if (!isPublic) { + return ( +