@apollo-annotation/jbrowse-plugin-apollo 0.1.0 → 0.1.1

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.
Files changed (61) hide show
  1. package/dist/index.esm.js +3096 -2525
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +3095 -2524
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +2974 -2103
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +7 -7
  12. package/src/ApolloInternetAccount/model.ts +9 -9
  13. package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +5 -2
  14. package/src/BackendDrivers/BackendDriver.ts +6 -3
  15. package/src/BackendDrivers/CollaborationServerDriver.ts +12 -6
  16. package/src/BackendDrivers/DesktopFileDriver.ts +13 -15
  17. package/src/BackendDrivers/InMemoryFileDriver.ts +9 -3
  18. package/src/ChangeManager.ts +6 -3
  19. package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +86 -0
  20. package/src/FeatureDetailsWidget/Attributes.tsx +374 -0
  21. package/src/FeatureDetailsWidget/BasicInformation.tsx +178 -0
  22. package/src/FeatureDetailsWidget/NumberTextField.tsx +75 -0
  23. package/src/FeatureDetailsWidget/RelatedFeature.tsx +87 -0
  24. package/src/FeatureDetailsWidget/Sequence.tsx +88 -0
  25. package/src/FeatureDetailsWidget/StringTextField.tsx +60 -0
  26. package/src/FeatureDetailsWidget/index.ts +2 -0
  27. package/src/FeatureDetailsWidget/model.ts +67 -0
  28. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +5 -2
  29. package/src/LinearApolloDisplay/glyphs/CanonicalGeneGlyph.ts +12 -4
  30. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +1 -1
  31. package/src/LinearApolloDisplay/glyphs/Glyph.ts +21 -2
  32. package/src/LinearApolloDisplay/glyphs/ImplicitExonGeneGlyph.ts +18 -5
  33. package/src/LinearApolloDisplay/stateModel/base.ts +1 -1
  34. package/src/LinearApolloDisplay/stateModel/getGlyph.ts +1 -1
  35. package/src/LinearApolloDisplay/stateModel/glyphs.ts +1 -1
  36. package/src/LinearApolloDisplay/stateModel/layouts.ts +1 -1
  37. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +1 -1
  38. package/src/LinearApolloDisplay/stateModel/rendering.ts +35 -3
  39. package/src/OntologyManager/util.ts +33 -0
  40. package/src/SixFrameFeatureDisplay/stateModel.ts +1 -1
  41. package/src/TabularEditor/HybridGrid/ChangeHandling.ts +2 -2
  42. package/src/TabularEditor/HybridGrid/Feature.tsx +3 -3
  43. package/src/TabularEditor/HybridGrid/FeatureAttributes.tsx +1 -1
  44. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +1 -1
  45. package/src/components/AddAssembly.tsx +5 -5
  46. package/src/components/AddChildFeature.tsx +13 -29
  47. package/src/components/AddFeature.tsx +1 -1
  48. package/src/components/CopyFeature.tsx +5 -2
  49. package/src/components/DeleteAssembly.tsx +1 -1
  50. package/src/components/DeleteFeature.tsx +2 -2
  51. package/src/components/DownloadGFF3.tsx +2 -2
  52. package/src/components/ImportFeatures.tsx +1 -1
  53. package/src/components/ManageUsers.tsx +1 -1
  54. package/src/components/ModifyFeatureAttribute.tsx +2 -2
  55. package/src/components/ViewChangeLog.tsx +7 -5
  56. package/src/extensions/annotationFromPileup.ts +2 -2
  57. package/src/index.ts +26 -8
  58. package/src/makeDisplayComponent.tsx +1 -2
  59. package/src/session/ClientDataStore.ts +6 -6
  60. package/src/session/session.ts +6 -3
  61. package/src/util/loadAssemblyIntoClient.ts +6 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.1.0",
3
2
  "name": "@apollo-annotation/jbrowse-plugin-apollo",
3
+ "version": "0.1.1",
4
4
  "description": "Apollo plugin for JBrowse 2",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,6 +51,9 @@
51
51
  "name": "Apollo"
52
52
  },
53
53
  "dependencies": {
54
+ "@apollo-annotation/apollo-common": "^0.1.1",
55
+ "@apollo-annotation/apollo-mst": "^0.1.1",
56
+ "@apollo-annotation/apollo-shared": "^0.1.1",
54
57
  "@emotion/react": "^11.10.6",
55
58
  "@emotion/styled": "^11.10.6",
56
59
  "@gmod/gff": "1.2.0",
@@ -58,9 +61,6 @@
58
61
  "@jbrowse/plugin-linear-genome-view": "^2.0.1",
59
62
  "@mui/icons-material": "^5.8.4",
60
63
  "@types/jsonpath": "^0.2.0",
61
- "apollo-common": "^1.0.0-alpha.0",
62
- "apollo-mst": "^1.0.0-alpha.0",
63
- "apollo-shared": "^1.0.0-alpha.0",
64
64
  "autosuggest-highlight": "^3.3.4",
65
65
  "bson-objectid": "^2.0.4",
66
66
  "clsx": "^1.1.1",
@@ -76,7 +76,7 @@
76
76
  },
77
77
  "peerDependencies": {
78
78
  "@jbrowse/core": "^2.7.0",
79
- "@mui/material": "^5.11.10",
79
+ "@mui/material": "^5.11.14",
80
80
  "mobx": "^6.6.1",
81
81
  "mobx-react": "^7.2.1",
82
82
  "mobx-state-tree": "^5.1.7",
@@ -91,8 +91,8 @@
91
91
  "@jbrowse/core": "^2.7.0",
92
92
  "@jbrowse/development-tools": "^2.1.1",
93
93
  "@jest/globals": "^29.0.3",
94
- "@mui/material": "^5.11.10",
95
- "@mui/x-data-grid": "^6.0.1",
94
+ "@mui/material": "^5.11.14",
95
+ "@mui/x-data-grid": "^7.0.0",
96
96
  "@types/autosuggest-highlight": "^3",
97
97
  "@types/file-saver": "^2",
98
98
  "@types/node": "^18.14.2",
@@ -1,11 +1,4 @@
1
- import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'
2
- import { InternetAccount } from '@jbrowse/core/pluggableElementTypes'
3
- import {
4
- AbstractSessionModel,
5
- isAbstractMenuManager,
6
- isElectron,
7
- } from '@jbrowse/core/util'
8
- import { Change } from 'apollo-common'
1
+ import { Change } from '@apollo-annotation/apollo-common'
9
2
  import {
10
3
  ChangeMessage,
11
4
  CheckResultUpdate,
@@ -14,7 +7,14 @@ import {
14
7
  UserLocationMessage,
15
8
  getDecodedToken,
16
9
  makeUserSessionId,
17
- } from 'apollo-shared'
10
+ } from '@apollo-annotation/apollo-shared'
11
+ import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'
12
+ import { InternetAccount } from '@jbrowse/core/pluggableElementTypes'
13
+ import {
14
+ AbstractSessionModel,
15
+ isAbstractMenuManager,
16
+ isElectron,
17
+ } from '@jbrowse/core/util'
18
18
  import { autorun } from 'mobx'
19
19
  import { Instance, flow, getRoot, types } from 'mobx-state-tree'
20
20
  import { io } from 'socket.io-client'
@@ -1,8 +1,11 @@
1
+ import { AnnotationFeatureI } from '@apollo-annotation/apollo-mst'
2
+ import {
3
+ LocationEndChange,
4
+ LocationStartChange,
5
+ } from '@apollo-annotation/apollo-shared'
1
6
  import { getConf } from '@jbrowse/core/configuration'
2
7
  import { AbstractSessionModel, Region, getSession } from '@jbrowse/core/util'
3
8
  import { Menu, MenuItem } from '@mui/material'
4
- import { AnnotationFeatureI } from 'apollo-mst'
5
- import { LocationEndChange, LocationStartChange } from 'apollo-shared'
6
9
  import { autorun, toJS } from 'mobx'
7
10
  import { observer } from 'mobx-react'
8
11
  import { getRoot, getSnapshot } from 'mobx-state-tree'
@@ -1,8 +1,11 @@
1
+ import { Change, ClientDataStore } from '@apollo-annotation/apollo-common'
2
+ import {
3
+ AnnotationFeatureSnapshot,
4
+ CheckResultSnapshot,
5
+ } from '@apollo-annotation/apollo-mst'
6
+ import { ValidationResultSet } from '@apollo-annotation/apollo-shared'
1
7
  import { Assembly } from '@jbrowse/core/assemblyManager/assembly'
2
8
  import { Region } from '@jbrowse/core/util'
3
- import { Change, ClientDataStore } from 'apollo-common'
4
- import { AnnotationFeatureSnapshot, CheckResultSnapshot } from 'apollo-mst'
5
- import { ValidationResultSet } from 'apollo-shared'
6
9
 
7
10
  import { SubmitOpts } from '../ChangeManager'
8
11
 
@@ -1,13 +1,19 @@
1
- import { getConf } from '@jbrowse/core/configuration'
2
- import { BaseInternetAccountModel } from '@jbrowse/core/pluggableElementTypes'
3
- import { Region, getSession } from '@jbrowse/core/util'
4
- import { AssemblySpecificChange, Change } from 'apollo-common'
1
+ import {
2
+ AssemblySpecificChange,
3
+ Change,
4
+ } from '@apollo-annotation/apollo-common'
5
5
  import {
6
6
  AnnotationFeatureSnapshot,
7
7
  ApolloRefSeqI,
8
8
  CheckResultSnapshot,
9
- } from 'apollo-mst'
10
- import { ChangeMessage, ValidationResultSet } from 'apollo-shared'
9
+ } from '@apollo-annotation/apollo-mst'
10
+ import {
11
+ ChangeMessage,
12
+ ValidationResultSet,
13
+ } from '@apollo-annotation/apollo-shared'
14
+ import { getConf } from '@jbrowse/core/configuration'
15
+ import { BaseInternetAccountModel } from '@jbrowse/core/pluggableElementTypes'
16
+ import { Region, getSession } from '@jbrowse/core/util'
11
17
  import { Socket } from 'socket.io-client'
12
18
 
13
19
  import { ChangeManager, SubmitOpts } from '../ChangeManager'
@@ -1,27 +1,25 @@
1
- import gff, { GFF3Item } from '@gmod/gff'
2
- import { getConf } from '@jbrowse/core/configuration'
3
- import { Region, getSession } from '@jbrowse/core/util'
4
1
  import {
5
2
  AssemblySpecificChange,
6
3
  Change,
7
4
  isAssemblySpecificChange,
8
- } from 'apollo-common'
9
- import { AnnotationFeatureSnapshot, CheckResultSnapshot } from 'apollo-mst'
10
- import { ValidationResultSet, makeGFF3Feature } from 'apollo-shared'
5
+ } from '@apollo-annotation/apollo-common'
6
+ import {
7
+ AnnotationFeatureSnapshot,
8
+ CheckResultSnapshot,
9
+ } from '@apollo-annotation/apollo-mst'
10
+ import {
11
+ ValidationResultSet,
12
+ makeGFF3Feature,
13
+ splitStringIntoChunks,
14
+ } from '@apollo-annotation/apollo-shared'
15
+ import gff, { GFF3Item } from '@gmod/gff'
16
+ import { getConf } from '@jbrowse/core/configuration'
17
+ import { Region, getSession } from '@jbrowse/core/util'
11
18
  import { getSnapshot } from 'mobx-state-tree'
12
19
 
13
20
  import { checkFeatures, loadAssemblyIntoClient } from '../util'
14
21
  import { BackendDriver } from './BackendDriver'
15
22
 
16
- function splitStringIntoChunks(input: string, chunkSize: number): string[] {
17
- const chunks: string[] = []
18
- for (let i = 0; i < input.length; i += chunkSize) {
19
- const chunk = input.slice(i, i + chunkSize)
20
- chunks.push(chunk)
21
- }
22
- return chunks
23
- }
24
-
25
23
  export class DesktopFileDriver extends BackendDriver {
26
24
  async loadAssembly(assemblyName: string) {
27
25
  const { assemblyManager } = getSession(this.clientStore)
@@ -1,8 +1,14 @@
1
+ import {
2
+ AssemblySpecificChange,
3
+ Change,
4
+ } from '@apollo-annotation/apollo-common'
5
+ import {
6
+ AnnotationFeatureSnapshot,
7
+ CheckResultSnapshot,
8
+ } from '@apollo-annotation/apollo-mst'
9
+ import { ValidationResultSet } from '@apollo-annotation/apollo-shared'
1
10
  import { getConf } from '@jbrowse/core/configuration'
2
11
  import { Region, getSession } from '@jbrowse/core/util'
3
- import { AssemblySpecificChange, Change } from 'apollo-common'
4
- import { AnnotationFeatureSnapshot, CheckResultSnapshot } from 'apollo-mst'
5
- import { ValidationResultSet } from 'apollo-shared'
6
12
 
7
13
  import { SubmitOpts } from '../ChangeManager'
8
14
  import { BackendDriver } from './BackendDriver'
@@ -1,10 +1,13 @@
1
- import { getSession } from '@jbrowse/core/util'
2
1
  import {
3
2
  Change,
4
3
  ClientDataStore,
5
4
  isAssemblySpecificChange,
6
- } from 'apollo-common'
7
- import { ValidationResultSet, validationRegistry } from 'apollo-shared'
5
+ } from '@apollo-annotation/apollo-common'
6
+ import {
7
+ ValidationResultSet,
8
+ validationRegistry,
9
+ } from '@apollo-annotation/apollo-shared'
10
+ import { getSession } from '@jbrowse/core/util'
8
11
  import { IAnyStateTreeNode } from 'mobx-state-tree'
9
12
 
10
13
  import { ApolloSessionModel } from './session'
@@ -0,0 +1,86 @@
1
+ import { BaseInternetAccountModel } from '@jbrowse/core/pluggableElementTypes'
2
+ import { getSession } from '@jbrowse/core/util'
3
+ import { observer } from 'mobx-react'
4
+ import { getRoot } from 'mobx-state-tree'
5
+ import React, { useMemo } from 'react'
6
+ import { makeStyles } from 'tss-react/mui'
7
+
8
+ import { ApolloInternetAccountModel } from '../ApolloInternetAccount/model'
9
+ import { ApolloSessionModel } from '../session'
10
+ import { ApolloRootModel } from '../types'
11
+ import { Attributes } from './Attributes'
12
+ import { BasicInformation } from './BasicInformation'
13
+ import { ApolloFeatureDetailsWidget as ApolloFeatureDetails } from './model'
14
+ import { RelatedFeatures } from './RelatedFeature'
15
+ import { Sequence } from './Sequence'
16
+
17
+ const useStyles = makeStyles()((theme) => ({
18
+ root: {
19
+ padding: theme.spacing(2),
20
+ },
21
+ }))
22
+
23
+ export const ApolloFeatureDetailsWidget = observer(
24
+ function ApolloFeatureDetailsWidget(props: { model: ApolloFeatureDetails }) {
25
+ const { model } = props
26
+ const { assembly, feature, refName } = model
27
+ const session = getSession(model) as unknown as ApolloSessionModel
28
+ const currentAssembly = session.apolloDataStore.assemblies.get(assembly)
29
+ const { classes } = useStyles()
30
+ const { internetAccounts } = getRoot<ApolloRootModel>(session)
31
+ const internetAccount = useMemo(() => {
32
+ return internetAccounts.find(
33
+ (ia: BaseInternetAccountModel) => ia.type === 'ApolloInternetAccount',
34
+ ) as ApolloInternetAccountModel | undefined
35
+ }, [internetAccounts])
36
+ const role = internetAccount ? internetAccount.role : 'admin'
37
+ const editable = ['admin', 'user'].includes(role ?? '')
38
+
39
+ if (!(feature && currentAssembly)) {
40
+ return null
41
+ }
42
+ const refSeq = currentAssembly.getByRefName(refName)
43
+ if (!refSeq) {
44
+ return null
45
+ }
46
+ const { end, start } = feature
47
+ const sequence = refSeq.getSequence(start, end)
48
+ if (!sequence) {
49
+ void session.apolloDataStore.loadRefSeq([
50
+ { assemblyName: assembly, refName, start, end },
51
+ ])
52
+ }
53
+
54
+ return (
55
+ <div className={classes.root}>
56
+ <BasicInformation
57
+ feature={feature}
58
+ session={session}
59
+ assembly={currentAssembly._id}
60
+ />
61
+ <hr />
62
+ <Attributes
63
+ feature={feature}
64
+ session={session}
65
+ assembly={currentAssembly._id}
66
+ editable={editable}
67
+ />
68
+ <hr />
69
+ <Sequence
70
+ feature={feature}
71
+ session={session}
72
+ assembly={currentAssembly._id}
73
+ refName={refName}
74
+ />
75
+ <hr />
76
+ <RelatedFeatures
77
+ feature={feature}
78
+ refName={refName}
79
+ session={session}
80
+ assembly={currentAssembly._id}
81
+ />
82
+ </div>
83
+ )
84
+ },
85
+ )
86
+ export default ApolloFeatureDetailsWidget
@@ -0,0 +1,374 @@
1
+ import { AnnotationFeatureI } from '@apollo-annotation/apollo-mst'
2
+ import { FeatureAttributeChange } from '@apollo-annotation/apollo-shared'
3
+ import { AbstractSessionModel } from '@jbrowse/core/util'
4
+ import DeleteIcon from '@mui/icons-material/Delete'
5
+ import {
6
+ Button,
7
+ DialogActions,
8
+ FormControl,
9
+ FormControlLabel,
10
+ FormLabel,
11
+ Grid,
12
+ IconButton,
13
+ Paper,
14
+ Radio,
15
+ RadioGroup,
16
+ TextField,
17
+ Typography,
18
+ } from '@mui/material'
19
+ import { observer } from 'mobx-react'
20
+ import { getSnapshot } from 'mobx-state-tree'
21
+ import React, { useState } from 'react'
22
+ import { makeStyles } from 'tss-react/mui'
23
+
24
+ import { AttributeValueEditorProps } from '../components'
25
+ import { OntologyTermMultiSelect } from '../components/OntologyTermMultiSelect'
26
+ import { ApolloSessionModel } from '../session'
27
+ import { StringTextField } from './StringTextField'
28
+
29
+ const reservedKeys = new Map([
30
+ [
31
+ 'Gene Ontology',
32
+ (props: AttributeValueEditorProps) => {
33
+ return <OntologyTermMultiSelect {...props} ontologyName="Gene Ontology" />
34
+ },
35
+ ],
36
+ [
37
+ 'Sequence Ontology',
38
+ (props: AttributeValueEditorProps) => {
39
+ return (
40
+ <OntologyTermMultiSelect {...props} ontologyName="Sequence Ontology" />
41
+ )
42
+ },
43
+ ],
44
+ ])
45
+
46
+ const reservedTerms = [
47
+ 'ID',
48
+ 'Name',
49
+ 'Alias',
50
+ 'Target',
51
+ 'Gap',
52
+ 'Derives_from',
53
+ 'Note',
54
+ 'Dbxref',
55
+ 'Ontology',
56
+ 'Is_Circular',
57
+ ]
58
+
59
+ const useStyles = makeStyles()((theme) => ({
60
+ newAttributePaper: {
61
+ padding: theme.spacing(2),
62
+ },
63
+ attributeName: {
64
+ background: theme.palette.secondary.main,
65
+ color: theme.palette.secondary.contrastText,
66
+ padding: theme.spacing(1),
67
+ },
68
+ }))
69
+
70
+ function CustomAttributeValueEditor(props: AttributeValueEditorProps) {
71
+ const { onChange, value } = props
72
+ return (
73
+ <StringTextField
74
+ value={value}
75
+ onChangeCommitted={(newValue) => {
76
+ onChange(newValue.split(','))
77
+ }}
78
+ variant="outlined"
79
+ fullWidth
80
+ helperText="Separate multiple values for the attribute with commas"
81
+ />
82
+ )
83
+ }
84
+
85
+ export const Attributes = observer(function Attributes({
86
+ assembly,
87
+ editable,
88
+ feature,
89
+ session,
90
+ }: {
91
+ feature: AnnotationFeatureI
92
+ session: ApolloSessionModel
93
+ assembly: string
94
+ editable: boolean
95
+ }) {
96
+ const [errorMessage, setErrorMessage] = useState('')
97
+ const [showAddNewForm, setShowAddNewForm] = useState(false)
98
+ const { classes } = useStyles()
99
+ const [newAttributeKey, setNewAttributeKey] = useState('')
100
+ const attributes = Object.fromEntries(
101
+ [...feature.attributes.entries()].map(([key, value]) => {
102
+ if (key.startsWith('gff_')) {
103
+ const newKey = key.slice(4)
104
+ const capitalizedKey = newKey.charAt(0).toUpperCase() + newKey.slice(1)
105
+ return [capitalizedKey, getSnapshot(value)]
106
+ }
107
+ if (key === '_id') {
108
+ return ['ID', getSnapshot(value)]
109
+ }
110
+ return [key, getSnapshot(value)]
111
+ }),
112
+ )
113
+ const { notify } = session as unknown as AbstractSessionModel
114
+
115
+ const { changeManager } = session.apolloDataStore
116
+
117
+ async function onChangeCommitted(newKey: string, newValue?: string[]) {
118
+ setErrorMessage('')
119
+
120
+ const attrs: Record<string, string[]> = {}
121
+ if (attributes) {
122
+ const modifiedAttrs = Object.entries({
123
+ ...attributes,
124
+ [newKey]: newValue,
125
+ })
126
+ for (const [key, val] of modifiedAttrs) {
127
+ if (!val) {
128
+ continue
129
+ }
130
+ const newKey = key.toLowerCase()
131
+ if (newKey === 'parent') {
132
+ continue
133
+ }
134
+ if ([...reservedKeys.keys()].includes(key)) {
135
+ attrs[key] = val
136
+ continue
137
+ }
138
+ switch (key) {
139
+ case 'ID': {
140
+ attrs._id = val
141
+ break
142
+ }
143
+ case 'Name': {
144
+ attrs.gff_name = val
145
+ break
146
+ }
147
+ case 'Alias': {
148
+ attrs.gff_alias = val
149
+ break
150
+ }
151
+ case 'Target': {
152
+ attrs.gff_target = val
153
+ break
154
+ }
155
+ case 'Gap': {
156
+ attrs.gff_gap = val
157
+ break
158
+ }
159
+ case 'Derives_from': {
160
+ attrs.gff_derives_from = val
161
+ break
162
+ }
163
+ case 'Note': {
164
+ attrs.gff_note = val
165
+ break
166
+ }
167
+ case 'Dbxref': {
168
+ attrs.gff_dbxref = val
169
+ break
170
+ }
171
+ case 'Ontology_term': {
172
+ attrs.gff_ontology_term = val
173
+ break
174
+ }
175
+ case 'Is_circular': {
176
+ attrs.gff_is_circular = val
177
+ break
178
+ }
179
+ default: {
180
+ attrs[key.toLowerCase()] = val
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ const change = new FeatureAttributeChange({
187
+ changedIds: [feature._id],
188
+ typeName: 'FeatureAttributeChange',
189
+ assembly,
190
+ featureId: feature._id,
191
+ attributes: attrs,
192
+ })
193
+ await changeManager.submit?.(change)
194
+ notify('Feature attributes modified successfully', 'success')
195
+ }
196
+ function handleAddNewAttributeChange() {
197
+ setErrorMessage('')
198
+ if (newAttributeKey.trim().length === 0) {
199
+ setErrorMessage('Attribute key is mandatory')
200
+ return
201
+ }
202
+ if (newAttributeKey === 'Parent') {
203
+ setErrorMessage(
204
+ '"Parent" -key is handled internally and it cannot be modified manually',
205
+ )
206
+ return
207
+ }
208
+ if (newAttributeKey in attributes) {
209
+ setErrorMessage(`Attribute "${newAttributeKey}" already exists`)
210
+ return
211
+ }
212
+ if (
213
+ /^[A-Z]/.test(newAttributeKey) &&
214
+ !reservedTerms.includes(newAttributeKey) &&
215
+ ![...reservedKeys.keys()].includes(newAttributeKey)
216
+ ) {
217
+ setErrorMessage(
218
+ `Key cannot starts with uppercase letter unless key is one of these: ${reservedTerms.join(
219
+ ', ',
220
+ )}`,
221
+ )
222
+ return
223
+ }
224
+ void onChangeCommitted(newAttributeKey, [])
225
+ }
226
+
227
+ function handleRadioButtonChange(
228
+ event: React.ChangeEvent<HTMLInputElement>,
229
+ value: string,
230
+ ) {
231
+ if (value === 'custom') {
232
+ setNewAttributeKey('')
233
+ } else if (reservedKeys.has(value)) {
234
+ setNewAttributeKey(value)
235
+ } else {
236
+ setErrorMessage('Unknown attribute type')
237
+ }
238
+ }
239
+
240
+ return (
241
+ <>
242
+ <Typography variant="h4">Attributes</Typography>
243
+ <Grid container direction="column" spacing={1}>
244
+ {Object.entries(attributes).map(([key, value]) => {
245
+ const EditorComponent =
246
+ reservedKeys.get(key) ?? CustomAttributeValueEditor
247
+ return (
248
+ <Grid container item spacing={3} alignItems="center" key={key}>
249
+ <Grid item xs="auto">
250
+ <Paper variant="outlined" className={classes.attributeName}>
251
+ <Typography>{key}</Typography>
252
+ </Paper>
253
+ </Grid>
254
+ <Grid item flexGrow={1}>
255
+ <EditorComponent
256
+ session={session}
257
+ value={value}
258
+ onChange={(newValue) => onChangeCommitted(key, newValue)}
259
+ />
260
+ </Grid>
261
+ <Grid item xs={1}>
262
+ <IconButton
263
+ aria-label="delete"
264
+ size="medium"
265
+ disabled={!editable}
266
+ onClick={() => onChangeCommitted(key)}
267
+ >
268
+ <DeleteIcon fontSize="medium" key={key} />
269
+ </IconButton>
270
+ </Grid>
271
+ </Grid>
272
+ )
273
+ })}
274
+ <Grid item>
275
+ <Button
276
+ color="primary"
277
+ variant="contained"
278
+ disabled={showAddNewForm || !editable}
279
+ onClick={() => {
280
+ setShowAddNewForm(true)
281
+ }}
282
+ >
283
+ Add new
284
+ </Button>
285
+ </Grid>
286
+ {showAddNewForm ? (
287
+ <Grid item>
288
+ <Paper elevation={8} className={classes.newAttributePaper}>
289
+ <Grid container direction="column">
290
+ <Grid item>
291
+ <FormControl>
292
+ <FormLabel id="attribute-radio-button-group">
293
+ Select attribute type
294
+ </FormLabel>
295
+ <RadioGroup
296
+ aria-labelledby="demo-radio-buttons-group-label"
297
+ defaultValue="custom"
298
+ name="radio-buttons-group"
299
+ onChange={handleRadioButtonChange}
300
+ >
301
+ <FormControlLabel
302
+ value="custom"
303
+ control={<Radio />}
304
+ disableTypography
305
+ label={
306
+ <Grid container spacing={1} alignItems="center">
307
+ <Grid item>
308
+ <Typography>Custom</Typography>
309
+ </Grid>
310
+ <Grid item>
311
+ <TextField
312
+ label="Custom attribute key"
313
+ variant="outlined"
314
+ value={
315
+ reservedKeys.has(newAttributeKey)
316
+ ? ''
317
+ : newAttributeKey
318
+ }
319
+ disabled={reservedKeys.has(newAttributeKey)}
320
+ onChange={(event) => {
321
+ setNewAttributeKey(event.target.value)
322
+ }}
323
+ />
324
+ </Grid>
325
+ </Grid>
326
+ }
327
+ />
328
+ {[...reservedKeys.keys()].map((key) => (
329
+ <FormControlLabel
330
+ key={key}
331
+ value={key}
332
+ control={<Radio />}
333
+ label={key}
334
+ />
335
+ ))}
336
+ </RadioGroup>
337
+ </FormControl>
338
+ </Grid>
339
+ <Grid item>
340
+ <DialogActions>
341
+ <Button
342
+ key="addButton"
343
+ color="primary"
344
+ variant="contained"
345
+ onClick={handleAddNewAttributeChange}
346
+ disabled={!newAttributeKey}
347
+ >
348
+ Add
349
+ </Button>
350
+ <Button
351
+ key="cancelAddButton"
352
+ variant="outlined"
353
+ type="submit"
354
+ onClick={() => {
355
+ setShowAddNewForm(false)
356
+ setNewAttributeKey('')
357
+ setErrorMessage('')
358
+ }}
359
+ >
360
+ Cancel
361
+ </Button>
362
+ </DialogActions>
363
+ </Grid>
364
+ </Grid>
365
+ </Paper>
366
+ </Grid>
367
+ ) : null}
368
+ </Grid>
369
+ {errorMessage ? (
370
+ <Typography color="error">{errorMessage}</Typography>
371
+ ) : null}
372
+ </>
373
+ )
374
+ })