Chore/kill yalc with fire (#629)
* init * more shifting * Move @babel dependencies up to workspace root * Begin updating dependent libraries to use sync'd webpack builds w/ watches * Commit changes before a long weekend * Split packages into platform and extensions * Consolidate how/where we specify file/module type entrypoints * Make it possible to pass in extensions as App props * Resolve viewer's modules * Target for UMD bundle * Add Webpack Stylus loader * Provide WebPack build options for microscopy, vtk, and ui * Better entrypoint for extensions * Push changes up to switch PCs * Fix ESM symlink build for Viewers * Shift docs to root * Specify additional lerna config props * Begin providing basic readme info * Rename example extension folder * Shift links to bottom of doc * First attempt at a netlify deploy preview * Attempt to fix deploy preview build * push workspace enabled to initial command; remove second yarn install; use npx to call gitbook cli commands * Try to use global gitbook-cli * Export node_modules path * More agressive with modifying PATH * Follow deploy log output advice * try again to set bin path * Try swapping bash with a new shell * Try bash -l instead of exec bash * Annd let's try again * and again * We may have figured it out johnny, boy * We need to pull cornerstone-wado-image-loader files from hoisted node_modules * Attempt to fix ticks/escapes * Change copy syntax; try running tests on viewers from root for circleci * Lower version to match circleci image * Try alternative jest-canvas-mock location and version file syntax * Run version command instead of calling node directly * Try alternative jest transform * Set path and enable workspaces * Add codecov flags * Use cpx so our copying finishes? * Get all platform unit tests to run * Run and report individually and in parallel * Try once relying on codecov yaml to split w/ flags * Add projects to split by flags * Generate separate reports * Trigger codecov after everything has finished running; these may not support flags * Use recommended flags from issue comments for codecov * Fix typo * Remove clear flag * Shift back to calling codecov from root * Generate example for codecov issue * Try fixing paths * Simplify * Try using codecov orb * Bump circleci version * Upload core and viewer * Use full string paths * Remove individual codecov calls * Fix dir * Combine files before upload * Escape anchors * tryfix syntax * Escape string literal * Combine lines to reduce path * long paths * Use home alias * Fix filename * Try to see the contents of our cat'd file * Also upload `core` * Try running with aliased folder * Fix path; split PR and Merge unit tests into separate jobs * Set default threshold * Fix typo * Changing to scoped package names * Adding note * Catch more updates * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.0 - @ohif/extension-dicom-html@0.0.4-alpha.0 - @ohif/extension-dicom-microscopy@0.0.9-alpha.0 - @ohif/extension-dicom-pdf@0.0.8-alpha.0 - @ohif/extension-vtk@0.1.4-alpha.0 - @ohif/core@0.11.1-alpha.0 - @ohif/i18n@0.2.3-alpha.0 - @ohif/ui@0.2.18-alpha.0 - @ohif/viewer@0.0.22-alpha.0 * Clean up primary readme * Support for dev and dev:* commands * Shift build command; satisfy default PWA build * Remove old scripts * Clean up UI to set webpack scripts * Tidy up project links * Clean duplicate code in UI project * Update UI project's docs * Clean up ui and i18n config * Reduce duplicate code in `@ohif/core` * Fix @ohif/ui versions * bump @ohif/core versions * Redux testkit dep * Remove unused/duplicate config files for projects/packages * ohif-core --> @ohif/core * Safer publish command * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.1 - @ohif/extension-dicom-html@0.0.4-alpha.1 - @ohif/extension-dicom-microscopy@0.0.9-alpha.1 - @ohif/extension-dicom-pdf@0.0.8-alpha.1 - @ohif/extension-vtk@0.1.4-alpha.1 - @ohif/core@0.11.1-alpha.1 - @ohif/i18n@0.2.3-alpha.1 - @ohif/ui@0.2.18-alpha.1 - @ohif/viewer@0.0.22-alpha.1 * test version bump * Add global prettier config * chore(release): publish %s [skip ci] - @ohif/viewer@0.0.22-alpha.2 * test lerna publish * Update package.jsons * Fix build scripts * catch changes * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.2 - @ohif/extension-dicom-html@0.0.4-alpha.2 - @ohif/extension-dicom-microscopy@0.0.9-alpha.2 - @ohif/extension-dicom-pdf@0.0.8-alpha.2 - @ohif/extension-vtk@0.1.4-alpha.2 - @ohif/core@0.11.1-alpha.2 - @ohif/i18n@0.2.3-alpha.2 - @ohif/ui@0.2.18-alpha.2 - @ohif/viewer@0.0.22-alpha.3 * fix eslint issues caused by eslint 6 and our react-app config * Publish with canary tag * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.3 - @ohif/extension-dicom-html@0.0.4-alpha.3 - @ohif/extension-dicom-microscopy@0.0.9-alpha.3 - @ohif/extension-dicom-pdf@0.0.8-alpha.3 - @ohif/extension-vtk@0.1.4-alpha.3 - @ohif/core@0.11.1-alpha.3 - @ohif/i18n@0.2.3-alpha.3 - @ohif/ui@0.2.18-alpha.3 - @ohif/viewer@0.0.22-alpha.4 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.4 - @ohif/extension-dicom-html@0.0.4-alpha.4 - @ohif/extension-dicom-microscopy@0.0.9-alpha.4 - @ohif/extension-dicom-pdf@0.0.8-alpha.4 - @ohif/extension-vtk@0.1.4-alpha.4 - @ohif/core@0.11.1-alpha.4 - @ohif/i18n@0.2.3-alpha.4 - @ohif/ui@0.2.18-alpha.4 - @ohif/viewer@0.0.22-alpha.5 * Simplify build * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.5 - @ohif/extension-dicom-html@0.0.4-alpha.5 - @ohif/extension-dicom-microscopy@0.0.9-alpha.5 - @ohif/extension-dicom-pdf@0.0.8-alpha.5 - @ohif/extension-vtk@0.1.4-alpha.5 - @ohif/core@0.11.1-alpha.5 - @ohif/i18n@0.2.3-alpha.5 - @ohif/ui@0.2.18-alpha.5 - @ohif/viewer@0.0.22-alpha.6 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.6 - @ohif/extension-dicom-html@0.0.4-alpha.6 - @ohif/extension-dicom-microscopy@0.0.9-alpha.6 - @ohif/extension-dicom-pdf@0.0.8-alpha.6 - @ohif/extension-vtk@0.1.4-alpha.6 - @ohif/core@0.11.1-alpha.6 - @ohif/i18n@0.2.3-alpha.6 - @ohif/ui@0.2.18-alpha.6 - @ohif/viewer@0.0.22-alpha.7 * Split version and publish * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.7 - @ohif/extension-dicom-html@0.0.4-alpha.7 - @ohif/extension-dicom-microscopy@0.0.9-alpha.7 - @ohif/extension-dicom-pdf@0.0.8-alpha.7 - @ohif/extension-vtk@0.1.4-alpha.7 - @ohif/core@0.11.1-alpha.7 - @ohif/i18n@0.2.3-alpha.7 - @ohif/ui@0.2.18-alpha.7 - @ohif/viewer@0.0.22-alpha.8 * Try simplify netlify build * react-viewerbase --> @ohif/ui * add test for stylus loader * Try new way to specify publish path * Use an output folder that won't be ignored * Try move command * fiddle with paths * Dig for some information * Make sure we're in root for our commands * Remove discovery * Setup script-tag example * Fix typo * Move config folder * Update cornerstone extension to use shared/root webpack configs * capture changes * Update extension webpack configs * Don't check in docz output * Fix babel runtime resolution issue * Simplify ui build steps * Push in progress * Shift directories * Get viewer dev path to work * Fix viewer commonjs build * Fix build pwa config for viewers * Make sure our package specific command can be called from root * Update notes * Fix imports and misc. merge notes * Bump wado-image-loader version * Switch up commands to remove webworkers copy * Fix CI config * Fix more broken package references * Add react-dropzone * Persist so e2e test can use * Fix lockfile * Use cypress orb to simplify CI * Fix config * Try again * Install yarn latest for orb * Install yarn a different way * Wait on correct port * Run tests in viewer project * Fix cypress config * Make Cypress happier - use default location * Simplify e2e testing * Capture UI changes since fork * Catch core up * Grab latest deps after merge * Fix broken import * Make sure cornerstone-wado-image-loader resolves to 3+ * Note regarding orbs * Fix refresh on 404 routes * Fix odd error for cornerstone-wado-image-loader * Different target for UMD * Simplify dev script * Try to get fancy with netlify output * Let's do it again * Try to fix measurementTools issue * See if we can get our lerna magic to work on merge to monorepo branch
This commit is contained in:
1 parent
cac911f08e
commit
08ff214ea6
919 files changed
+45652
-52097
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import './AboutModal.styl';
|
||||
|
||||
import 'react-bootstrap-modal/lib/css/rbm-patch.css';
|
||||
|
||||
// TODO: Is this the only component importing these?
|
||||
import './../../design/styles/common/modal.styl';
|
||||
|
||||
export class AboutModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
// TODO: Make this component more generic to allow things other than W/L and hotkeys...
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
|
||||
static itemsPreset = [
|
||||
{
|
||||
name: 'Repository URL',
|
||||
value: 'https://github.com/OHIF/Viewers/',
|
||||
},
|
||||
{
|
||||
name: 'Latest Master Commits',
|
||||
value: 'https://github.com/OHIF/Viewers/commits/master',
|
||||
},
|
||||
];
|
||||
|
||||
static defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
renderTableRow(item) {
|
||||
return (
|
||||
<tr key={item.name}>
|
||||
<td>{item.name}</td>
|
||||
<td>
|
||||
<a target="_blank" href={item.value}>
|
||||
{item.value}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.isOpen}
|
||||
onHide={this.props.onCancel}
|
||||
aria-labelledby="ModalHeader"
|
||||
className="AboutModal modal fade themed in"
|
||||
backdrop={false}
|
||||
large={true}
|
||||
keyboard={false}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>About</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="btn-group">
|
||||
<a
|
||||
className="btn btn-default"
|
||||
target="_blank"
|
||||
href="https://groups.google.com/forum/#!forum/cornerstone-platform"
|
||||
>
|
||||
Visit the forum
|
||||
</a>
|
||||
<a
|
||||
className="btn btn-default"
|
||||
target="_blank"
|
||||
href="https://github.com/OHIF/Viewers/issues"
|
||||
>
|
||||
Report an issue
|
||||
</a>
|
||||
<a
|
||||
className="btn btn-default"
|
||||
target="_blank"
|
||||
href="http://ohif.org"
|
||||
>
|
||||
More details
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Version Information</h3>
|
||||
<table className="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{AboutModal.itemsPreset.map(item => this.renderTableRow(item))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
@import './../../design/styles/common/button.styl'
|
||||
@import './../../design/styles/common/table.styl'
|
||||
|
||||
.AboutModal
|
||||
.btn
|
||||
border-color: #ccc;
|
||||
|
||||
.table
|
||||
thead:first-child
|
||||
tr:first-child
|
||||
th, td
|
||||
border-top: 0;
|
||||
|
||||
thead, tbody, tfoot
|
||||
tr
|
||||
th, td
|
||||
padding: 8px;
|
||||
line-height: 1.42857143;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid #ddd;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { Component } from 'react';
|
||||
import LanguageSwitcher from '../languageSwitcher';
|
||||
|
||||
export class GeneralPreferences extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="general-preferences-wrapper">
|
||||
<div className="col-sm-3">
|
||||
<label htmlFor="language-select" className="p-r-1">
|
||||
Language
|
||||
</label>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import './HotKeysPreferences.styl';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
allowedKeys,
|
||||
disallowedCombinations,
|
||||
specialKeys,
|
||||
} from './hotKeysConfig.js';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export class HotKeysPreferences extends Component {
|
||||
static propTypes = {
|
||||
hotKeysData: PropTypes.objectOf(
|
||||
PropTypes.shape({
|
||||
keys: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const hotkeyCommands = Object.keys(this.props.hotKeysData);
|
||||
const localHotKeys = hotkeyCommands.map(commandName => {
|
||||
const definition = this.props.hotKeysData[commandName];
|
||||
|
||||
return {
|
||||
commandName,
|
||||
keys: definition.keys,
|
||||
label: definition.label,
|
||||
};
|
||||
});
|
||||
|
||||
this.state = {
|
||||
hotKeys: localHotKeys,
|
||||
errorMessages: {},
|
||||
};
|
||||
|
||||
this.onInputKeyDown = this.onInputKeyDown.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the keys used in a KeyPress event and returns an array of the
|
||||
* keys pressed
|
||||
*
|
||||
* @param {KeyDownEvent} keyDownEvent
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getKeysPressedArray(keyDownEvent) {
|
||||
const keysPressedArray = [];
|
||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
||||
|
||||
if (ctrlKey && !altKey) {
|
||||
keysPressedArray.push('CTRL');
|
||||
}
|
||||
|
||||
if (shiftKey && !altKey) {
|
||||
keysPressedArray.push('SHIFT');
|
||||
}
|
||||
|
||||
if (altKey && !ctrlKey) {
|
||||
keysPressedArray.push('ALT');
|
||||
}
|
||||
|
||||
return keysPressedArray;
|
||||
}
|
||||
|
||||
getConflictingCommand(currentToolKey, hotKeyCommand) {
|
||||
return Object.keys(this.state.hotKeys).find(tool => {
|
||||
const value = this.state.hotKeys[tool].command;
|
||||
return value && value === hotKeyCommand && tool !== currentToolKey;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} commandName
|
||||
* @param {KeyDownEvent} keyDownEvent
|
||||
* @param {Boolean} [displayPressedKey=false]
|
||||
*/
|
||||
updateInputText(commandName, keyDownEvent, displayPressedKey = false) {
|
||||
const pressedKeys = this.getKeysPressedArray(keyDownEvent);
|
||||
|
||||
if (displayPressedKey) {
|
||||
const specialKeyName = specialKeys[keyDownEvent.which];
|
||||
const keyName =
|
||||
specialKeyName ||
|
||||
keyDownEvent.key ||
|
||||
String.fromCharCode(keyDownEvent.keyCode);
|
||||
pressedKeys.push(keyName.toUpperCase());
|
||||
}
|
||||
|
||||
this.updateHotKeysState(commandName, pressedKeys.join('+'));
|
||||
}
|
||||
|
||||
updateHotKeysState(commandName, keys) {
|
||||
const hotKeys = this.state.hotKeys;
|
||||
const hotKeyIndex = this.state.hotKeys.findIndex(
|
||||
x => x.commandName === commandName
|
||||
);
|
||||
hotKeys[hotKeyIndex].keys[0] = keys;
|
||||
this.setState({ hotKeys });
|
||||
}
|
||||
|
||||
updateErrorsState(toolKey, errorMessage) {
|
||||
const errorMessages = this.state.errorMessages;
|
||||
errorMessages[toolKey] = errorMessage;
|
||||
this.setState({ errorMessages });
|
||||
}
|
||||
|
||||
onInputKeyDown(event, commandName) {
|
||||
// Prevent ESC key from propagating and closing the modal
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (allowedKeys.includes(event.keyCode)) {
|
||||
this.updateInputText(commandName, event, true);
|
||||
} else {
|
||||
this.updateInputText(commandName, event, false);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
onChange(event, commandName) {
|
||||
if (event.ctrlKey || event.altKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hotKeyIndex = this.state.hotKeys.findIndex(
|
||||
x => x.commandName === commandName
|
||||
);
|
||||
const hotKey = this.state.hotKeys[hotKeyIndex];
|
||||
const keys = hotKey.keys[0];
|
||||
const pressedKeys = keys.split('+');
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1].toUpperCase();
|
||||
|
||||
// clear the prior errors
|
||||
this.setState({ errorMessages: {} }, () => {
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = ['CTRL', 'ALT', 'SHIFT'].includes(lastPressedKey);
|
||||
if (isModifier) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
"It's not possible to define only modifier keys (CTRL, ALT and SHIFT) as a shortcut"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if it has some conflict
|
||||
*/
|
||||
const conflictedCommandKey = this.getConflictingCommand(
|
||||
commandName,
|
||||
keys
|
||||
);
|
||||
if (conflictedCommandKey) {
|
||||
const conflictedCommand = this.state.hotKeys[conflictedCommandKey];
|
||||
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
`"${conflictedCommand.label}" is already using the "${
|
||||
conflictedCommand.command
|
||||
}" shortcut.`
|
||||
);
|
||||
this.updateErrorsState(conflictedCommandKey, '');
|
||||
this.updateHotKeysState(commandName, '');
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if is a valid combination
|
||||
*/
|
||||
const modifierCommand = pressedKeys
|
||||
.slice(0, pressedKeys.length - 1)
|
||||
.join('+')
|
||||
.toUpperCase();
|
||||
|
||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
? disallowedCombination.includes(lastPressedKey)
|
||||
: false;
|
||||
|
||||
if (hasDisallowedCombinations) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
"It's not possible to define only modifier keys (CTRL, ALT and SHIFT) as a shortcut"
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderRow({ commandName, label, keys }) {
|
||||
return (
|
||||
<tr key={commandName}>
|
||||
<td className="text-right p-r-1">{label}</td>
|
||||
<td width="200">
|
||||
<label
|
||||
className={`wrapperLabel ${
|
||||
this.state.errorMessages[commandName] !== undefined
|
||||
? 'state-error'
|
||||
: ''
|
||||
} `}
|
||||
ref={input => (this[commandName] = input)}
|
||||
data-key="defaultTool"
|
||||
>
|
||||
<input
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value={keys[0]}
|
||||
vali="true"
|
||||
className="form-control hotkey text-center"
|
||||
onKeyDown={event => this.onInputKeyDown(event, commandName)}
|
||||
onKeyUp={event => this.onChange(event, commandName)}
|
||||
/>
|
||||
<span className="wrapperText" />
|
||||
<span className="errorMessage">
|
||||
{this.state.errorMessages[commandName]}
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const halfWayThough = Math.floor(this.state.hotKeys.length / 2);
|
||||
const firstHalfHotkeys = this.state.hotKeys.slice(0, halfWayThough);
|
||||
const secondHalfHotkeys = this.state.hotKeys.slice(
|
||||
halfWayThough,
|
||||
this.state.hotKeys.length
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="HotKeysPreferences">
|
||||
{/* <!-- Column 1 --> */}
|
||||
<div className="column">
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-right p-r-1">Function</th>
|
||||
<th className="text-center">Shortcut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{firstHalfHotkeys.map(hotkeyDefinition =>
|
||||
this.renderRow(hotkeyDefinition)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* <!-- Column 2 --> */}
|
||||
<div className="column">
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-right p-r-1">Function</th>
|
||||
<th className="text-center">Shortcut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{secondHalfHotkeys.map(hotkeyDefinition =>
|
||||
this.renderRow(hotkeyDefinition)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.HotKeysPreferences
|
||||
display: flex;
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
|
||||
.column
|
||||
width: 50%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
|
||||
.column-full
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { HotKeysPreferences } from './HotKeysPreferences';
|
||||
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
||||
import { GeneralPreferences } from './GeneralPreferences';
|
||||
import './UserPreferences.styl';
|
||||
|
||||
export class UserPreferences extends Component {
|
||||
static defaultProps = {
|
||||
hotKeysData: {},
|
||||
windowLevelData: {},
|
||||
generalData: {},
|
||||
};
|
||||
|
||||
// TODO: Make this more generic. Tabs should not be restricted to these entries
|
||||
static propTypes = {
|
||||
hotKeysData: PropTypes.object.isRequired,
|
||||
windowLevelData: PropTypes.object.isRequired,
|
||||
generalData: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
tabIndex: 0,
|
||||
};
|
||||
|
||||
tabClick(tabIndex) {
|
||||
this.setState({ tabIndex });
|
||||
}
|
||||
|
||||
renderHotkeysTab() {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<HotKeysPreferences hotKeysData={this.props.hotKeysData} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
renderWindowLevelTab() {
|
||||
if (this.props.windowLevelData) {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<WindowLevelPreferences
|
||||
windowLevelData={this.props.windowLevelData}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderGeneralTab() {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<GeneralPreferences generalData={this.props.generalData} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
renderTabs(tabIndex) {
|
||||
switch (tabIndex) {
|
||||
case 0:
|
||||
return this.renderHotkeysTab();
|
||||
case 1:
|
||||
return this.renderWindowLevelTab();
|
||||
case 2:
|
||||
return this.renderGeneralTab();
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getTabClass(tabIndex) {
|
||||
return tabIndex === this.state.tabIndex ? 'nav-link active' : 'nav-link';
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="dialog-separator-after">
|
||||
<ul className="nav nav-tabs">
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(0);
|
||||
}}
|
||||
className={this.getTabClass(0)}
|
||||
>
|
||||
<button>Hotkeys</button>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(1);
|
||||
}}
|
||||
className={this.getTabClass(1)}
|
||||
>
|
||||
<button>Window Level</button>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(2);
|
||||
}}
|
||||
className={this.getTabClass(2)}
|
||||
>
|
||||
<button>General</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{this.renderTabs(this.state.tabIndex)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@import './../../design/styles/common/form.styl'
|
||||
@import './../../design/styles/common/navbar.styl'
|
||||
@import './../../design/styles/common/state.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.modal-body
|
||||
overflow: hidden
|
||||
|
||||
.errorMessage
|
||||
color: var(--state-error-text)
|
||||
font-size: 10px
|
||||
text-transform: uppercase;
|
||||
|
||||
.form-content
|
||||
border-bottom: 3px solid var(--primary-background-color)
|
||||
margin-bottom: 20px
|
||||
margin-left: -22px
|
||||
margin-right: -22px
|
||||
max-height: 70vh
|
||||
overflow-y: auto
|
||||
padding: 22px
|
||||
min-height: 500px
|
||||
|
||||
.popover
|
||||
width: 300px
|
||||
@@ -0,0 +1,107 @@
|
||||
import './UserPreferencesModal.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import 'react-bootstrap-modal/lib/css/rbm-patch.css';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { UserPreferences } from './UserPreferences';
|
||||
|
||||
// TODO: Is this the only component importing these?
|
||||
import './../../design/styles/common/modal.styl';
|
||||
|
||||
class UserPreferencesModal extends Component {
|
||||
// TODO: Make this component more generic to allow things other than W/L and hotkeys...
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onResetToDefaults: PropTypes.func,
|
||||
windowLevelData: PropTypes.object,
|
||||
hotKeysData: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
windowLevelData: cloneDeep(props.windowLevelData),
|
||||
hotKeysData: cloneDeep(props.hotKeysData),
|
||||
};
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
save = () => {
|
||||
this.props.onSave({
|
||||
windowLevelData: this.state.windowLevelData,
|
||||
hotKeysData: this.state.hotKeysData,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prev, next) {
|
||||
const newStateData = {};
|
||||
|
||||
if (!isEqual(prev.windowLevelData, next.windowLevelData)) {
|
||||
newStateData.windowLevelData = prev.windowLevelData;
|
||||
}
|
||||
|
||||
if (!isEqual(prev.hotKeysData, next.hotKeysData)) {
|
||||
newStateData.hotKeysData = prev.hotKeysData;
|
||||
}
|
||||
|
||||
if (newStateData.hotKeysData || newStateData.windowLevelData) {
|
||||
this.setState(newStateData);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.isOpen}
|
||||
onHide={this.props.onCancel}
|
||||
aria-labelledby="ModalHeader"
|
||||
className="ModalHeader modal fade themed in"
|
||||
backdrop={false}
|
||||
large={true}
|
||||
keyboard={false}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{this.props.t('User Preferences')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<UserPreferences
|
||||
windowLevelData={this.state.windowLevelData}
|
||||
hotKeysData={this.state.hotKeysData}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
onClick={this.props.onResetToDefaults}
|
||||
>
|
||||
{this.props.t('Reset to Defaults')}
|
||||
</button>
|
||||
<Modal.Dismiss className="btn btn-default">
|
||||
{this.props.t('Cancel')}
|
||||
</Modal.Dismiss>
|
||||
<button className="btn btn-primary" onClick={this.save}>
|
||||
{this.props.t('Save')}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('UserPreferencesModal')(
|
||||
UserPreferencesModal
|
||||
);
|
||||
export { connectedComponent as UserPreferencesModal };
|
||||
export default connectedComponent;
|
||||
@@ -0,0 +1,17 @@
|
||||
@import './../../design/styles/common/navbar.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
@import './../../design/styles/common/modal.styl'
|
||||
@import './../../design/styles/common/button.styl'
|
||||
|
||||
.close
|
||||
float: right;
|
||||
font-size: 21px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #000;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
opacity: .2;
|
||||
|
||||
.ModalHeader
|
||||
ol, ul
|
||||
margin-top: 0;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './WindowLevelPreferences.styl';
|
||||
|
||||
export class WindowLevelPreferences extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
data: this.props.windowLevelData,
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
windowLevelData: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
onChange(event, key, field) {
|
||||
const data = this.state.data;
|
||||
const entry = data[key];
|
||||
entry[field] = event.target.value;
|
||||
this.setState({ data });
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(data);
|
||||
}
|
||||
}
|
||||
|
||||
getWLPreferencesRows(key) {
|
||||
const entry = this.state.data[key];
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td className="p-r-1 text-center">{key}</td>
|
||||
<td className="p-r-1">
|
||||
<label className="wrapperLabel">
|
||||
<input
|
||||
value={entry.description}
|
||||
type="text"
|
||||
className="form-control"
|
||||
onChange={event => {
|
||||
this.onChange(event, key, 'description');
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
<td className="p-r-1">
|
||||
<label className="wrapperLabel">
|
||||
<input
|
||||
value={entry.window}
|
||||
type="number"
|
||||
className="form-control"
|
||||
onChange={event => {
|
||||
this.onChange(event, key, 'window');
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
<td className="p-r-1">
|
||||
<label className="wrapperLabel">
|
||||
<input
|
||||
value={entry.level}
|
||||
type="number"
|
||||
className="form-control"
|
||||
onChange={event => {
|
||||
this.onChange(event, key, 'level');
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-x-1 text-center presetIndex">Preset</th>
|
||||
<th className="p-x-1">Description</th>
|
||||
<th className="p-x-1">Window</th>
|
||||
<th className="p-x-1">Level</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(this.state.data).map(key => {
|
||||
return this.getWLPreferencesRows(key);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@import './UserPreferences.styl'
|
||||
|
||||
.presetIndex
|
||||
padding: 0px 10px 0px 10px
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: About Modal
|
||||
menu: Components
|
||||
route: /components/about-modal
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { AboutModal } from './../index.js'
|
||||
|
||||
# About Modal
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
isOpen: false
|
||||
}}>
|
||||
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
onClick={ () => setState({ isOpen: true }) }
|
||||
>
|
||||
Open about modal
|
||||
</button>
|
||||
<AboutModal
|
||||
{...state}
|
||||
onCancel={() => setState({ isOpen: false })}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={AboutModal} />
|
||||
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
currentLanguage: 'en',
|
||||
languages: [
|
||||
{
|
||||
value: 'en',
|
||||
label: 'English',
|
||||
},
|
||||
{
|
||||
value: 'es',
|
||||
label: 'Spanish',
|
||||
},
|
||||
],
|
||||
onChange: language => {},
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
export default {
|
||||
defaultTool: { label: 'Default Tool', keys: ['ESC'], column: 0 },
|
||||
zoom: { label: 'Zoom', keys: ['Z'], column: 0 },
|
||||
wwwc: { label: 'W/L', keys: ['W'], column: 0 },
|
||||
pan: { label: 'Pan', keys: ['P'], column: 0 },
|
||||
angle: { label: 'Angle measurement', keys: ['A'], column: 0 },
|
||||
stackScroll: { label: 'Scroll stack', keys: ['S'], column: 0 },
|
||||
magnify: { label: 'Magnify', keys: ['M'], column: 0 },
|
||||
length: { label: 'Length measurement', keys: [''], column: 0 },
|
||||
annotate: { label: 'Annotate', keys: [''], column: 0 },
|
||||
dragProbe: { label: 'Pixel probe', keys: [''], column: 0 },
|
||||
ellipticalRoi: { label: 'Elliptical ROI', keys: [''], column: 0 },
|
||||
rectangleRoi: { label: 'Rectangle ROI', keys: [''], column: 0 },
|
||||
|
||||
// Viewport hotkeys
|
||||
flipH: { label: 'Flip Horizontally', keys: ['H'], column: 0 },
|
||||
flipV: { label: 'Flip Vertically', keys: ['V'], column: 0 },
|
||||
rotateR: { label: 'Rotate Right', keys: ['R'], column: 0 },
|
||||
rotateL: { label: 'Rotate Left', keys: ['L'], column: 0 },
|
||||
invert: { label: 'Invert', keys: ['I'], column: 0 },
|
||||
zoomIn: { label: 'Zoom In', keys: [''], column: 0 },
|
||||
zoomOut: { label: 'Zoom Out', keys: [''], column: 0 },
|
||||
zoomToFit: { label: 'Zoom to Fit', keys: [''], column: 0 },
|
||||
resetViewport: { label: 'Reset', keys: [''], column: 0 },
|
||||
clearTools: { label: 'Clear Tools', keys: [''], column: 0 },
|
||||
|
||||
// 2nd column
|
||||
|
||||
// Viewport navigation hotkeys
|
||||
scrollDown: { label: 'Scroll Down', keys: ['DOWN'], column: 1 },
|
||||
scrollUp: { label: 'Scroll Up', keys: ['UP'], column: 1 },
|
||||
scrollLastImage: { label: 'Scroll to Last Image', keys: ['END'], column: 1 },
|
||||
scrollFirstImage: {
|
||||
label: 'Scroll to First Image',
|
||||
keys: ['HOME'],
|
||||
column: 1,
|
||||
},
|
||||
previousDisplaySet: {
|
||||
label: 'Previous Series',
|
||||
keys: ['PAGEUP'],
|
||||
column: 1,
|
||||
},
|
||||
nextDisplaySet: { label: 'Next Series', keys: ['PAGEDOWN'], column: 1 },
|
||||
nextPanel: { label: 'Next Image Viewport', keys: ['RIGHT'], column: 1 },
|
||||
previousPanel: {
|
||||
label: 'Previous Image Viewport',
|
||||
keys: ['LEFT'],
|
||||
column: 1,
|
||||
},
|
||||
|
||||
// Miscellaneous hotkeys
|
||||
toggleOverlayTags: {
|
||||
label: 'Toggle Image Info Overlay',
|
||||
keys: ['O'],
|
||||
column: 1,
|
||||
},
|
||||
toggleCinePlay: { label: 'Play/Pause Cine', keys: ['SPACE'], column: 1 },
|
||||
toggleCineDialog: {
|
||||
label: 'Show/Hide Cine Controls',
|
||||
keys: [''],
|
||||
column: 1,
|
||||
},
|
||||
toggleDownloadDialog: {
|
||||
label: 'Show/Hide Download Dialog',
|
||||
keys: [''],
|
||||
column: 1,
|
||||
},
|
||||
|
||||
// Preset hotkeys
|
||||
WLPreset0: { label: 'W/L Preset 0 (Soft Tissue)', keys: ['1'], column: 1 },
|
||||
WLPreset1: { label: 'W/L Preset 1 (Lung)', keys: ['2'], column: 1 },
|
||||
WLPreset2: { label: 'W/L Preset 2 (Liver)', keys: ['3'], column: 1 },
|
||||
WLPreset3: { label: 'W/L Preset 3 (Bone)', keys: ['4'], column: 1 },
|
||||
WLPreset4: { label: 'W/L Preset 4 (Brain)', keys: ['5'], column: 1 },
|
||||
WLPreset5: { label: 'W/L Preset 5', keys: ['6'], column: 1 },
|
||||
WLPreset6: { label: 'W/L Preset 6', keys: ['7'], column: 1 },
|
||||
WLPreset7: { label: 'W/L Preset 7', keys: ['8'], column: 1 },
|
||||
WLPreset8: { label: 'W/L Preset 8', keys: ['9'], column: 1 },
|
||||
WLPreset9: { label: 'W/L Preset 0', keys: ['0'], column: 1 },
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: User Preferences Modal
|
||||
menu: Components
|
||||
route: /components/user-preferences-modal
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { UserPreferencesModal } from './../index.js'
|
||||
import NameSpace from '../../../__docs__/NameSpace'
|
||||
//
|
||||
import windowLevelDefaults from './windowLevelDefaults.js'
|
||||
import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
|
||||
# User Preferences Modal
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
isOpen: false,
|
||||
windowLevelData: windowLevelDefaults,
|
||||
hotKeysData: hotkeyDefaults,
|
||||
}}>
|
||||
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
onClick={ () => setState({ isOpen: true }) }
|
||||
>
|
||||
Open user preferences
|
||||
</button>
|
||||
<UserPreferencesModal
|
||||
{...state}
|
||||
onCancel={() => setState({ isOpen: false })}
|
||||
onSave={() => alert('on save')}
|
||||
onResetToDefaults={() => alert('on reset')}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={UserPreferencesModal} />
|
||||
|
||||
## Translation Namespace
|
||||
|
||||
<NameSpace name="UserPreferencesModal" />
|
||||
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
0: { description: 'Soft tissue', window: 400, level: 40 },
|
||||
1: { description: 'Lung', window: 1500, level: -600 },
|
||||
2: { description: 'Liver', window: 150, level: 90 },
|
||||
3: { description: 'Bone', window: 2500, level: 480 },
|
||||
4: { description: 'Brain', window: 80, level: 40 },
|
||||
5: { description: '', window: '', level: '' },
|
||||
6: { description: '', window: '', level: '' },
|
||||
7: { description: '', window: '', level: '' },
|
||||
8: { description: '', window: '', level: '' },
|
||||
9: { description: '', window: '', level: '' },
|
||||
10: { description: '', window: '', level: '' },
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
const range = (start, end) => {
|
||||
return new Array(end - start).fill().map((d, i) => i + start);
|
||||
};
|
||||
|
||||
export const disallowedCombinations = {
|
||||
'': [],
|
||||
ALT: ['SPACE'],
|
||||
SHIFT: [],
|
||||
CTRL: [
|
||||
'F4',
|
||||
'F5',
|
||||
'F11',
|
||||
'W',
|
||||
'R',
|
||||
'T',
|
||||
'O',
|
||||
'P',
|
||||
'A',
|
||||
'D',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'J',
|
||||
'L',
|
||||
'Z',
|
||||
'X',
|
||||
'C',
|
||||
'V',
|
||||
'B',
|
||||
'N',
|
||||
'PAGEDOWN',
|
||||
'PAGEUP',
|
||||
],
|
||||
'CTRL+SHIFT': ['Q', 'W', 'R', 'T', 'P', 'A', 'H', 'V', 'B', 'N'],
|
||||
};
|
||||
|
||||
export const allowedKeys = [
|
||||
...[8, 13, 27, 32, 46], // BACKSPACE, ENTER, ESCAPE, SPACE, DELETE
|
||||
...[12, 106, 107, 109, 110, 111], // Numpad keys
|
||||
...range(218, 220), // [\]
|
||||
...range(185, 190), // ;=,-./
|
||||
...range(111, 131), // F1-F19
|
||||
...range(32, 41), // arrow keys, home/end, pg dn/up
|
||||
...range(47, 58), // 0-9
|
||||
...range(64, 91), // A-Z
|
||||
];
|
||||
|
||||
export const specialKeys = {
|
||||
8: 'backspace',
|
||||
9: 'tab',
|
||||
13: 'return',
|
||||
16: 'shift',
|
||||
17: 'ctrl',
|
||||
18: 'alt',
|
||||
19: 'pause',
|
||||
20: 'capslock',
|
||||
27: 'esc',
|
||||
32: 'space',
|
||||
33: 'pageup',
|
||||
34: 'pagedown',
|
||||
35: 'end',
|
||||
36: 'home',
|
||||
37: 'left',
|
||||
38: 'up',
|
||||
39: 'right',
|
||||
40: 'down',
|
||||
45: 'insert',
|
||||
46: 'del',
|
||||
96: '0',
|
||||
97: '1',
|
||||
98: '2',
|
||||
99: '3',
|
||||
100: '4',
|
||||
101: '5',
|
||||
102: '6',
|
||||
103: '7',
|
||||
104: '8',
|
||||
105: '9',
|
||||
106: '*',
|
||||
107: '+',
|
||||
109: '-',
|
||||
110: '.',
|
||||
111: '/',
|
||||
112: 'f1',
|
||||
113: 'f2',
|
||||
114: 'f3',
|
||||
115: 'f4',
|
||||
116: 'f5',
|
||||
117: 'f6',
|
||||
118: 'f7',
|
||||
119: 'f8',
|
||||
120: 'f9',
|
||||
121: 'f10',
|
||||
122: 'f11',
|
||||
123: 'f12',
|
||||
144: 'numlock',
|
||||
145: 'scroll',
|
||||
191: '/',
|
||||
224: 'meta',
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export { UserPreferences } from './UserPreferences.js';
|
||||
export { AboutModal } from './AboutModal.js';
|
||||
export { UserPreferencesModal } from './UserPreferencesModal.js';
|
||||
export { GeneralPreferences } from './GeneralPreferences.js';
|
||||
Reference in new issue
Block a user