@teselagen/ove 0.7.3-beta.7 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,11 +18,11 @@ import { pick } from "lodash-es";
18
18
  const PropertiesContainer = Comp => props => {
19
19
  const { additionalFooterEls, additionalHeaderEls, ...rest } = props;
20
20
  return (
21
- <React.Fragment>
21
+ <>
22
22
  {additionalHeaderEls}
23
23
  <Comp {...rest} />
24
24
  {additionalFooterEls}
25
- </React.Fragment>
25
+ </>
26
26
  );
27
27
  };
28
28
  const allTabs = {
@@ -35,129 +35,130 @@ const allTabs = {
35
35
  orfs: PropertiesContainer(OrfProperties),
36
36
  genbank: PropertiesContainer(GenbankView)
37
37
  };
38
- export class PropertiesDialog extends React.Component {
39
- render() {
40
- const {
41
- propertiesTool = {},
42
- propertiesViewTabUpdate,
43
- dimensions = {},
44
- height,
45
- editorName,
46
- onSave,
47
- showReadOnly,
48
- showAvailability,
49
- isProtein,
50
- annotationsToSupport = {},
51
- disableSetReadOnly,
52
- propertiesList = [
53
- "general",
54
- "features",
55
- "parts",
56
- "primers",
57
- "translations",
58
- "cutsites",
59
- "orfs",
60
- "genbank"
61
- ],
62
- closePanelButton
63
- } = { ...this.props, ...this.props.PropertiesProps };
64
38
 
65
- const { width, height: heightFromDim } = dimensions;
39
+ export const PropertiesDialog = props => {
40
+ const {
41
+ propertiesTool = {},
42
+ propertiesViewTabUpdate,
43
+ dimensions = {},
44
+ height,
45
+ editorName,
46
+ onSave,
47
+ showReadOnly,
48
+ showAvailability,
49
+ isProtein,
50
+ annotationsToSupport = {},
51
+ disableSetReadOnly,
52
+ propertiesList = [
53
+ "general",
54
+ "features",
55
+ "parts",
56
+ "primers",
57
+ "translations",
58
+ "cutsites",
59
+ "orfs",
60
+ "genbank"
61
+ ],
62
+ closePanelButton
63
+ } = { ...props, ...props.PropertiesProps };
66
64
 
67
- let { tabId, selectedAnnotationId } = propertiesTool;
68
- if (
69
- propertiesList
70
- .map(nameOrOverride => nameOrOverride.name || nameOrOverride)
71
- .indexOf(tabId) === -1
72
- ) {
73
- tabId = propertiesList[0].name || propertiesList[0];
65
+ const { width, height: heightFromDim } = dimensions;
66
+
67
+ let { tabId, selectedAnnotationId } = propertiesTool;
68
+ if (
69
+ propertiesList
70
+ .map(nameOrOverride => nameOrOverride.name || nameOrOverride)
71
+ .indexOf(tabId) === -1
72
+ ) {
73
+ tabId = propertiesList[0].name || propertiesList[0];
74
+ }
75
+
76
+ const propertiesTabs = flatMap(propertiesList, nameOrOverride => {
77
+ if (annotationsToSupport[nameOrOverride] === false) {
78
+ return [];
74
79
  }
75
- const propertiesTabs = flatMap(propertiesList, nameOrOverride => {
76
- if (annotationsToSupport[nameOrOverride] === false) {
77
- return [];
78
- }
79
80
 
80
- const name = nameOrOverride.name || nameOrOverride;
81
- const Comp = nameOrOverride.Comp || allTabs[name];
82
- if (isProtein) {
83
- if (
84
- name === "translations" ||
85
- name === "orfs" ||
86
- name === "primers" ||
87
- name === "cutsites"
88
- ) {
89
- return null;
90
- }
81
+ const name = nameOrOverride.name || nameOrOverride;
82
+ const Comp = nameOrOverride.Comp || allTabs[name];
83
+ if (isProtein) {
84
+ if (
85
+ name === "translations" ||
86
+ name === "orfs" ||
87
+ name === "primers" ||
88
+ name === "cutsites"
89
+ ) {
90
+ return null;
91
91
  }
92
- const title = (() => {
93
- if (nameOrOverride.Comp) return name; //just use the user supplied name because this is a custom panel
94
- if (name === "orfs") return "ORFs";
95
- if (name === "cutsites") return "Cut Sites";
96
- return startCase(name);
97
- })();
98
- return (
99
- <Tab
100
- key={name}
101
- title={title}
102
- id={name}
103
- panel={
104
- <Comp
105
- {...{
106
- ...pick(this.props, userDefinedHandlersAndOpts),
107
- editorName,
108
- onSave,
109
- isProtein,
110
- showReadOnly,
111
- showAvailability,
112
- disableSetReadOnly,
113
- selectedAnnotationId,
114
- ...(nameOrOverride.name && nameOrOverride)
115
- }}
116
- />
117
- }
118
- />
119
- );
120
- });
121
- const heightToUse = Math.max(0, Number((heightFromDim || height) - 30));
92
+ }
93
+ const title = (() => {
94
+ if (nameOrOverride.Comp) return name; //just use the user supplied name because this is a custom panel
95
+ if (name === "orfs") return "ORFs";
96
+ if (name === "cutsites") return "Cut Sites";
97
+ return startCase(name);
98
+ })();
99
+
122
100
  return (
101
+ <Tab
102
+ key={name}
103
+ title={title}
104
+ id={name}
105
+ panel={
106
+ <Comp
107
+ {...{
108
+ ...pick(props, userDefinedHandlersAndOpts),
109
+ editorName,
110
+ onSave,
111
+ isProtein,
112
+ showReadOnly,
113
+ showAvailability,
114
+ disableSetReadOnly,
115
+ selectedAnnotationId,
116
+ ...(nameOrOverride.name && nameOrOverride)
117
+ }}
118
+ />
119
+ }
120
+ />
121
+ );
122
+ });
123
+ const heightToUse = Math.max(0, Number((heightFromDim || height) - 30));
124
+ return (
125
+ <div
126
+ style={{
127
+ position: "relative"
128
+ }}
129
+ >
130
+ {closePanelButton}
123
131
  <div
132
+ className="ve-propertiesPanel"
124
133
  style={{
125
- position: "relative"
134
+ display: "flex",
135
+ width,
136
+ height: heightToUse || 300,
137
+ zIndex: 10,
138
+ padding: 10
139
+ // paddingBottom: '31px',
126
140
  }}
127
141
  >
128
- {closePanelButton}
129
- <div
130
- className="ve-propertiesPanel"
131
- style={{
132
- display: "flex",
133
- width,
134
- height: heightToUse || 300,
135
- zIndex: 10,
136
- padding: 10
137
- // paddingBottom: '31px',
138
- }}
139
- >
140
- {propertiesTabs.length ? (
141
- <Tabs
142
- style={{ width }}
143
- renderActiveTabPanelOnly
144
- selectedTabId={tabId}
145
- onChange={propertiesViewTabUpdate}
146
- >
147
- <Tabs.Expander />
148
- {propertiesTabs}
149
- <Tabs.Expander />
150
- </Tabs>
151
- ) : (
152
- <div style={{ margin: 20, fontSize: 20 }}>
153
- No Properties to display
154
- </div>
155
- )}
156
- </div>
142
+ {propertiesTabs.length ? (
143
+ <Tabs
144
+ style={{ width }}
145
+ renderActiveTabPanelOnly
146
+ selectedTabId={tabId}
147
+ onChange={propertiesViewTabUpdate}
148
+ >
149
+ <Tabs.Expander />
150
+ {propertiesTabs}
151
+ <Tabs.Expander />
152
+ </Tabs>
153
+ ) : (
154
+ <div style={{ margin: 20, fontSize: 20 }}>
155
+ No Properties to display
156
+ </div>
157
+ )}
157
158
  </div>
158
- );
159
- }
160
- }
159
+ </div>
160
+ );
161
+ };
161
162
 
162
163
  export default compose(
163
164
  connectToEditor(({ propertiesTool, annotationsToSupport }) => {
@@ -1,194 +1,178 @@
1
- import React from "react";
1
+ import React, { useCallback, useMemo, useState } from "react";
2
2
  import { reduxForm } from "redux-form";
3
-
4
3
  import {
5
4
  wrapDialog,
6
5
  DataTable,
7
- withSelectedEntities,
8
6
  SwitchField,
9
- tgFormValues
7
+ useTableEntities
10
8
  } from "@teselagen/ui";
11
9
  import { compose } from "redux";
12
10
  import { Button, Classes, Popover } from "@blueprintjs/core";
13
11
  import classNames from "classnames";
14
-
15
12
  import withEditorProps from "../../withEditorProps";
16
13
  import { forEach, camelCase, startCase } from "lodash-es";
17
14
  import { sizeSchema } from "../PropertiesDialog/utils";
18
15
  import { getRangeLength } from "@teselagen/range-utils";
16
+ import { useFormValue } from "../../utils/useFormValue";
17
+
18
+ const dialogFormName = "RemoveDuplicatesDialog";
19
+ const dataTableFormName = "duplicatesToRemove";
20
+ const checkboxStyle = { marginTop: 0, marginBottom: 0 };
21
+
22
+ const RemoveDuplicatesDialog = props => {
23
+ const {
24
+ type,
25
+ sequenceData = { sequence: "" },
26
+ sequenceLength,
27
+ isProtein,
28
+ hideModal
29
+ } = props;
19
30
 
20
- class RemoveDuplicatesDialog extends React.Component {
21
- state = {
22
- dups: []
23
- };
24
- componentDidMount() {
25
- this.recomputeDups();
26
- }
31
+ const { selectedEntities } = useTableEntities(dataTableFormName);
27
32
 
28
- checkboxStyle = { marginTop: 0, marginBottom: 0 };
33
+ const ignoreName = useFormValue(dialogFormName, "ignoreName");
34
+ const ignoreStartAndEnd = useFormValue(dialogFormName, "ignoreStartAndEnd");
35
+ const ignoreStrand = useFormValue(dialogFormName, "ignoreStrand");
29
36
 
30
- delayedRecomputeDups = () => {
31
- setTimeout(() => {
32
- this.recomputeDups();
33
- });
34
- };
35
- recomputeDups = () => {
36
- const {
37
- // hideModal,
38
- type,
39
- sequenceData = { sequence: "" },
40
- // handleSubmit,
41
- sequenceLength,
42
- ignoreName,
43
- ignoreStrand,
44
- ignoreStartAndEnd
45
- // circular,
46
- // upsertFeature
47
- } = this.props;
37
+ const recomputeDups = useCallback(
38
+ values => {
39
+ const ignoreName = values?.ignoreName;
40
+ const ignoreStartAndEnd = values?.ignoreStartAndEnd;
41
+ const ignoreStrand = values?.ignoreStrand;
42
+ const annotations = sequenceData[type];
43
+ const newDups = [];
44
+ const seqsHashByStartEndStrandName = {};
45
+ forEach(annotations, a => {
46
+ const hash = `${ignoreStartAndEnd ? "" : a.start}&${
47
+ ignoreStartAndEnd ? "" : a.end
48
+ }&${ignoreStrand ? "" : a.strand}&${ignoreName ? "" : a.name}`;
49
+ if (seqsHashByStartEndStrandName[hash]) {
50
+ newDups.push({ ...a, size: getRangeLength(a, sequenceLength) });
51
+ } else {
52
+ seqsHashByStartEndStrandName[hash] = true;
53
+ }
54
+ });
55
+ return newDups;
56
+ },
57
+ [sequenceData, sequenceLength, type]
58
+ );
48
59
 
49
- const annotations = sequenceData[type];
50
- const dups = [];
51
- const seqsHashByStartEndStrandName = {};
52
- forEach(annotations, a => {
53
- const hash = `${ignoreStartAndEnd ? "" : a.start}&${
54
- ignoreStartAndEnd ? "" : a.end
55
- }&${ignoreStrand ? "" : a.strand}&${ignoreName ? "" : a.name}`;
56
- if (seqsHashByStartEndStrandName[hash]) {
57
- dups.push({ ...a, size: getRangeLength(a, sequenceLength) });
58
- } else {
59
- seqsHashByStartEndStrandName[hash] = true;
60
- }
61
- });
62
- this.setState({ dups });
63
- };
64
- render() {
65
- const { duplicatesToRemoveSelectedEntities, hideModal, type } = this.props;
60
+ const [dups, setDups] = useState(recomputeDups);
61
+ const selectedIds = useMemo(() => dups.map(d => d.id), [dups]);
66
62
 
67
- const selectedIds = this.state.dups.map(d => d.id);
63
+ const fieldSubmit = useCallback(
64
+ (newVal, field) => {
65
+ const values = {
66
+ ignoreName,
67
+ ignoreStartAndEnd,
68
+ ignoreStrand,
69
+ [field]: newVal
70
+ };
71
+ const newDups = recomputeDups(values);
72
+ setDups(newDups);
73
+ },
74
+ [ignoreName, ignoreStartAndEnd, ignoreStrand, recomputeDups]
75
+ );
68
76
 
69
- const schema = {
77
+ const schema = useMemo(
78
+ () => ({
70
79
  fields: [
71
- // ...(noColor
72
- // ? []
73
- // : [
74
- // {
75
- // path: "color",
76
- // type: "string",
77
- // render: color => {
78
- // return (
79
- // <ColorPickerPopover>
80
- // <div style={{ height: 20, width: 20, background: color }} />
81
- // </ColorPickerPopover>
82
- // );
83
- // }
84
- // }
85
- // ]),
86
80
  { path: "name", type: "string" },
87
81
  // ...(noType ? [] : [{ path: "type", type: "string" }]),
88
- sizeSchema(this.props.isProtein),
82
+ sizeSchema(isProtein),
89
83
  { path: "strand", type: "string" }
90
84
  ]
91
- };
92
- // const sequenceLength = sequenceData.sequence.length;
93
- // const isCirc = (this.state || {}).circular;
94
- return (
95
- <div className={classNames(Classes.DIALOG_BODY, "tg-min-width-dialog")}>
96
- {/* {dups.map((d) => {
97
- return <div>
85
+ }),
86
+ [isProtein]
87
+ );
98
88
 
99
- </div>
100
- })} */}
101
- <DataTable
102
- noPadding
103
- withCheckboxes
104
- noFullscreenButton
105
- // onRowSelect={this.onRowSelect}
106
- maxHeight={400}
107
- selectedIds={selectedIds}
108
- formName="duplicatesToRemove"
109
- noRouter
110
- noRowsFoundMessage="No duplicates found"
111
- compact
112
- noHeader
113
- noFooter
114
- withSearch={false}
115
- hideSelectedCount
116
- isInfinite
117
- schema={schema}
118
- entities={this.state.dups}
89
+ return (
90
+ <div className={classNames(Classes.DIALOG_BODY, "tg-min-width-dialog")}>
91
+ <DataTable
92
+ noPadding
93
+ withCheckboxes
94
+ noFullscreenButton
95
+ maxHeight={400}
96
+ selectedIds={selectedIds}
97
+ formName={dataTableFormName}
98
+ noRouter
99
+ noRowsFoundMessage="No duplicates found"
100
+ compact
101
+ noHeader
102
+ noFooter
103
+ withSearch={false}
104
+ hideSelectedCount
105
+ isInfinite
106
+ schema={schema}
107
+ entities={dups}
108
+ />
109
+ <div
110
+ style={{
111
+ marginTop: 10,
112
+ display: "flex",
113
+ justifyContent: "space-between"
114
+ }}
115
+ >
116
+ <Popover
117
+ target={<Button icon="settings" />}
118
+ content={
119
+ <div style={{ padding: 20, maxWidth: 250 }}>
120
+ <div>Ignore These Fields While Finding Duplicates:</div>
121
+ <br />
122
+ <SwitchField
123
+ containerStyle={{ marginBottom: 2 }}
124
+ //delay the call to recompute dups until redux has had time to update
125
+ onFieldSubmit={newVal => fieldSubmit(newVal, "ignoreName")}
126
+ style={checkboxStyle}
127
+ name="ignoreName"
128
+ label="Name"
129
+ />
130
+ <SwitchField
131
+ containerStyle={{ marginBottom: 2 }}
132
+ //delay the call to recompute dups until redux has had time to update
133
+ onFieldSubmit={newVal => fieldSubmit(newVal, "ignoreStrand")}
134
+ style={checkboxStyle}
135
+ name="ignoreStrand"
136
+ label="Strand"
137
+ />
138
+ <SwitchField
139
+ containerStyle={{ marginBottom: 2 }}
140
+ //delay the call to recompute dups until redux has had time to update
141
+ onFieldSubmit={newVal =>
142
+ fieldSubmit(newVal, "ignoreStartAndEnd")
143
+ }
144
+ style={checkboxStyle}
145
+ name="ignoreStartAndEnd"
146
+ label="Start and End"
147
+ />
148
+ </div>
149
+ }
119
150
  />
120
- <div
121
- style={{
122
- marginTop: 10,
123
- display: "flex",
124
- justifyContent: "space-between"
151
+
152
+ <Button
153
+ intent="primary"
154
+ onClick={() => {
155
+ props[camelCase(`delete_${type}`).slice(0, -1)](
156
+ Object.keys(selectedEntities || {})
157
+ );
158
+ window.toastr.success(
159
+ `Successfully Deleted ${
160
+ Object.keys(selectedEntities || {}).length
161
+ } ${startCase(type)}`
162
+ );
163
+ hideModal();
125
164
  }}
165
+ disabled={!Object.keys(selectedEntities || {}).length}
126
166
  >
127
- <Popover
128
- target={<Button icon="settings" />}
129
- content={
130
- <div style={{ padding: 20, maxWidth: 250 }}>
131
- <div>Ignore These Fields While Finding Duplicates:</div>
132
- <br></br>
133
- <SwitchField
134
- containerStyle={{ marginBottom: 2 }}
135
- //delay the call to recompute dups until redux has had time to update
136
- onFieldSubmit={this.delayedRecomputeDups}
137
- style={this.checkboxStyle}
138
- name="ignoreName"
139
- label="Name"
140
- ></SwitchField>
141
- <SwitchField
142
- containerStyle={{ marginBottom: 2 }}
143
- //delay the call to recompute dups until redux has had time to update
144
- onFieldSubmit={this.delayedRecomputeDups}
145
- style={this.checkboxStyle}
146
- name="ignoreStrand"
147
- label="Strand"
148
- ></SwitchField>
149
- <SwitchField
150
- containerStyle={{ marginBottom: 2 }}
151
- //delay the call to recompute dups until redux has had time to update
152
- onFieldSubmit={this.delayedRecomputeDups}
153
- style={this.checkboxStyle}
154
- name="ignoreStartAndEnd"
155
- label="Start and End"
156
- ></SwitchField>
157
- </div>
158
- }
159
- ></Popover>
160
-
161
- <Button
162
- intent="primary"
163
- onClick={() => {
164
- this.props[camelCase(`delete_${type}`).slice(0, -1)](
165
- duplicatesToRemoveSelectedEntities.map(d => d.id)
166
- );
167
- window.toastr.success(
168
- `Successfully Deleted ${
169
- duplicatesToRemoveSelectedEntities.length
170
- } ${startCase(type)}`
171
- );
172
- hideModal();
173
- }}
174
- disabled={!(duplicatesToRemoveSelectedEntities || []).length}
175
- >
176
- Remove {duplicatesToRemoveSelectedEntities.length} Duplicates
177
- </Button>
178
- </div>
167
+ Remove {Object.keys(selectedEntities || {}).length} Duplicates
168
+ </Button>
179
169
  </div>
180
- );
181
- }
182
- }
170
+ </div>
171
+ );
172
+ };
183
173
 
184
174
  export default compose(
185
175
  wrapDialog(),
186
176
  withEditorProps,
187
-
188
- withSelectedEntities("duplicatesToRemove"),
189
-
190
- reduxForm({
191
- form: "RemoveDuplicatesDialog"
192
- }),
193
- tgFormValues("ignoreName", "ignoreStrand", "ignoreStartAndEnd")
177
+ reduxForm({ form: dialogFormName })
194
178
  )(RemoveDuplicatesDialog);
@@ -0,0 +1,7 @@
1
+ /* Copyright (C) 2018 TeselaGen Biotechnology, Inc. */
2
+ import { useSelector } from "react-redux";
3
+ import { get } from "lodash";
4
+
5
+ export const useFormValue = (formName, field) => {
6
+ return useSelector(state => get(state.form?.[formName]?.values, field));
7
+ };
@@ -4,6 +4,7 @@ import {
4
4
  getAminoAcidStringFromSequenceString
5
5
  } from "@teselagen/sequence-utils";
6
6
  import { getSequenceWithinRange } from "@teselagen/range-utils";
7
+ import Clipboard from "clipboard";
7
8
  import { compose } from "redux";
8
9
  import {
9
10
  getReverseComplementSequenceAndAnnotations,
@@ -598,10 +599,16 @@ function VectorInteractionHOC(Component /* options */) {
598
599
  this.props;
599
600
  const { isProtein } = sequenceData;
600
601
  const makeTextCopyable = (transformFunc, className, action = "copy") => {
601
- document.querySelectorAll(`.${className}`).forEach(element => {
602
- element.addEventListener("click", async () => {
603
- const { selectionLayer, editorName, store } = this.props;
604
- const { sequenceData, copyOptions } =
602
+ return new Clipboard(`.${className}`, {
603
+ action: () => action,
604
+ text: () => {
605
+ if (action === "copy") {
606
+ document.body.addEventListener("copy", this.handleCopy);
607
+ } else {
608
+ document.body.addEventListener("cut", this.handleCut);
609
+ }
610
+ const { editorName, store } = this.props;
611
+ const { sequenceData, copyOptions, selectionLayer } =
605
612
  store.getState().VectorEditor[editorName];
606
613
 
607
614
  const selectedSeqData = getSequenceDataBetweenRange(
@@ -627,23 +634,12 @@ function VectorInteractionHOC(Component /* options */) {
627
634
  );
628
635
  this.sequenceDataToCopy = sequenceDataToCopy;
629
636
 
630
- const textToCopy =
631
- sequenceDataToCopy.textToCopy || sequenceDataToCopy.sequence;
632
-
633
- try {
634
- await navigator.clipboard.writeText(textToCopy);
635
- if (action === "cut") {
636
- // Optionally clear the original text or perform any "cut" related logic
637
- }
638
-
639
- if (window.Cypress) {
640
- window.Cypress.textToCopy = sequenceDataToCopy.textToCopy;
641
- window.Cypress.seqDataToCopy = sequenceDataToCopy;
642
- }
643
- } catch (err) {
644
- console.error(`${action} failed:`, err);
637
+ if (window.Cypress) {
638
+ window.Cypress.textToCopy = sequenceDataToCopy.textToCopy;
639
+ window.Cypress.seqDataToCopy = sequenceDataToCopy;
645
640
  }
646
- });
641
+ return sequenceDataToCopy.textToCopy || sequenceDataToCopy.sequence;
642
+ }
647
643
  });
648
644
  };
649
645
  const aaCopy = {
@@ -664,6 +660,8 @@ function VectorInteractionHOC(Component /* options */) {
664
660
  }, className);
665
661
  }
666
662
  };
663
+ // TODO: maybe stop using Clipboard.js and unify clipboard handling with
664
+ // a more versatile approach
667
665
  return [
668
666
  ...(readOnly || disableBpEditing
669
667
  ? []
@@ -0,0 +1 @@
1
+ export function useFormValue(formName: any, field: any): any;