Skip to content

Commit 11037f7

Browse files
rainandbareczi-github-helper[bot]
andauthored
feat: Add Object Id Prefix (#1743)
* rough draft add object id prefix * chore: Updated [rdev] values.yaml image tags to sha-b8b2dd2 * add placeholder and object id logic * chore: Updated [rdev] values.yaml image tags to sha-584c623 * remove console logs * adjust to match designs * chore: Updated [rdev] values.yaml image tags to sha-0d77058 * off by 2 px * chore: Updated [rdev] values.yaml image tags to sha-10566ed --------- Co-authored-by: rainandbare <[email protected]> Co-authored-by: czi-github-helper[bot] <+czi-github-helper[bot]@users.noreply.github.com>
1 parent d5c084c commit 11037f7

File tree

17 files changed

+379
-45
lines changed

17 files changed

+379
-45
lines changed

.infra/rdev/values.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ stack:
22
services:
33
frontend:
44
image:
5-
tag: sha-aede296
5+
tag: sha-10566ed
66
replicaCount: 1
77
env:
88
- name: API_URL_V2

frontend/packages/data-portal/app/components/AnnotationFilter/AnnotationFilter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useFeatureFlag } from 'app/utils/featureFlags'
1212

1313
import { AnnotationSoftwareFilter } from './AnnotationSoftwareFilter'
1414
import { MethodTypeFilter } from './MethodTypeFilter'
15-
import { ObjectIdFilter } from './ObjectIdFilter'
15+
import { ObjectIdFilter } from './ObjectIdFilter/ObjectIdFilter'
1616

1717
export function AnnotationFilter() {
1818
const { t } = useI18n()

frontend/packages/data-portal/app/components/AnnotationFilter/ObjectIdFilter.tsx

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { RegexFilter } from 'app/components/Filters'
2+
import { GO, UNIPROTKB } from 'app/constants/annotationObjectIdLinks'
3+
import { QueryParams } from 'app/constants/query'
4+
import { useI18n } from 'app/hooks/useI18n'
5+
6+
import {
7+
PrefixValueProvider,
8+
usePrefixValueContext,
9+
} from './PrefixValueContext'
10+
11+
const prefixOptions = [
12+
{
13+
id: 'go',
14+
name: 'GO ID',
15+
details: 'Gene Ontology ID',
16+
link: GO,
17+
prefix: 'GO:',
18+
placeholder: '0016020 or GO:0016020',
19+
},
20+
{
21+
id: 'uniprotkb',
22+
name: 'UniProtKB',
23+
details: 'The UniProt Knowledgebase',
24+
link: UNIPROTKB,
25+
prefix: 'UniProtKB:',
26+
placeholder: 'P01267 or UniProtKB:P01267',
27+
},
28+
]
29+
30+
const prefixes = prefixOptions.map((opt) =>
31+
opt.prefix.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
32+
) // escape any special chars
33+
const prefixPattern = prefixes.join('|') // e.g. 'GO:|UNIPROTKB:'
34+
const validationRegex = new RegExp(`^(${prefixPattern})?[A-Z0-9]+$`, 'i')
35+
36+
export function ObjectIdFilterContent() {
37+
const { t } = useI18n()
38+
const { prefixValue, setPrefixValue, setInputDropdownValue } =
39+
usePrefixValueContext()
40+
return (
41+
<RegexFilter
42+
id="object-id-input"
43+
title={`${t('filterByObjectId')}:`}
44+
label={t('objectId')}
45+
queryParam={QueryParams.ObjectId}
46+
regex={validationRegex}
47+
prefixOptions={prefixOptions}
48+
displayNormalizer={(value) => {
49+
return value
50+
}}
51+
paramNormalizer={(value) => {
52+
if (!prefixValue) {
53+
return value
54+
}
55+
// if the value has a prefix and it is the same as the selected prefix, keep it
56+
if (value.toLowerCase().startsWith(prefixValue.prefix.toLowerCase())) {
57+
const id = value.split(':')[1]
58+
if (prefixValue.prefix === 'UniProtKB:') {
59+
// if the prefix is UniProtKB, we need to make the id uppercase (e.g. P01267)
60+
return `${prefixValue.prefix}${id.toUpperCase()}`
61+
}
62+
return `${prefixValue.prefix}${id}`
63+
}
64+
65+
// If the value has a different prefix from the list, update the selected prefix
66+
const matchingPrefix = prefixOptions.find((prefixOption) =>
67+
value.toLowerCase().startsWith(prefixOption.prefix.toLowerCase()),
68+
)
69+
70+
if (matchingPrefix) {
71+
setPrefixValue(matchingPrefix)
72+
setInputDropdownValue(matchingPrefix.name)
73+
const id = value.split(':')[1]
74+
if (matchingPrefix.prefix === 'UniProtKB:') {
75+
return `${matchingPrefix.prefix}${id.toUpperCase()}`
76+
}
77+
return `${matchingPrefix.prefix}${id}`
78+
}
79+
80+
// if the value has no prefix, add the selected prefix
81+
if (value.match(validationRegex)) {
82+
return `${prefixValue.prefix}${value}`
83+
}
84+
return value
85+
}}
86+
/>
87+
)
88+
}
89+
90+
export function ObjectIdFilter() {
91+
return (
92+
<PrefixValueProvider initialOptions={prefixOptions}>
93+
<ObjectIdFilterContent />
94+
</PrefixValueProvider>
95+
)
96+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { DefaultAutocompleteOption } from '@czi-sds/components'
2+
import React, { createContext, useContext, useState } from 'react'
3+
4+
export type PrefixOption = DefaultAutocompleteOption & {
5+
link: string
6+
prefix: string
7+
placeholder: string
8+
}
9+
10+
type PrefixValueContextType = {
11+
prefixValue: PrefixOption | null
12+
setPrefixValue: (value: PrefixOption | null) => void
13+
inputDropdownValue: string | null
14+
setInputDropdownValue: (value: string | null) => void
15+
}
16+
17+
const PrefixValueContext = createContext<PrefixValueContextType | undefined>(
18+
undefined,
19+
)
20+
21+
export function PrefixValueProvider({
22+
children,
23+
initialOptions,
24+
}: {
25+
children: React.ReactNode
26+
initialOptions: PrefixOption[]
27+
}) {
28+
const [prefixValue, setPrefixValue] = useState<PrefixOption | null>(
29+
initialOptions[0] ?? null,
30+
)
31+
const [inputDropdownValue, setInputDropdownValue] = useState<string | null>(
32+
initialOptions[0]?.name ?? null,
33+
)
34+
35+
const contextValue = React.useMemo(
36+
() => ({
37+
prefixValue,
38+
setPrefixValue,
39+
inputDropdownValue,
40+
setInputDropdownValue,
41+
}),
42+
[prefixValue, inputDropdownValue],
43+
)
44+
45+
return (
46+
<PrefixValueContext.Provider value={contextValue}>
47+
{children}
48+
</PrefixValueContext.Provider>
49+
)
50+
}
51+
52+
export function usePrefixValueContext() {
53+
const ctx = useContext(PrefixValueContext)
54+
if (!ctx)
55+
throw new Error(
56+
'usePrefixValueContext must be used within PrefixValueProvider',
57+
)
58+
return ctx
59+
}

frontend/packages/data-portal/app/components/DatasetFilter/AnnotationMetadataFilterSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ObjectIdFilter } from 'app/components/AnnotationFilter/ObjectIdFilter'
1+
import { ObjectIdFilter } from 'app/components/AnnotationFilter/ObjectIdFilter/ObjectIdFilter'
22
import {
33
AnnotatedObjectNameFilter,
44
AnnotatedObjectShapeTypeFilter,

frontend/packages/data-portal/app/components/DatasetFilter/NameOrIdFilterSection.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,19 @@ export function NameOrIdFilterSection({
2525
id: 'portal-id-input',
2626
label: `${t('datasetId')}:`,
2727
queryParam: QueryParams.DatasetId,
28+
placeholder: t('datasetIdPlaceholder'),
2829
},
2930
{
3031
id: 'empiar-id-input',
3132
label: `${t('empiarID')}:`,
3233
queryParam: QueryParams.EmpiarId,
34+
placeholder: t('empiarIDPlaceholder'),
3335
},
3436
{
3537
id: 'emdb-id-input',
3638
label: `${t('emdb')}:`,
3739
queryParam: QueryParams.EmdbId,
40+
placeholder: t('emdbPlaceholder'),
3841
},
3942
],
4043
[t],

frontend/packages/data-portal/app/components/DepositionFilter/DepositionIdFilter.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ export function DepositionIdFilter() {
88
return (
99
<EntityIdFilter
1010
id="deposition-id-input"
11-
title={t('filterByDepositionId')}
11+
title={`${t('filterByDepositionId')}:`}
1212
label={t('depositionId')}
1313
queryParam={QueryParams.DepositionId}
14+
placeholder={t('depositionIdPlaceholder')}
1415
/>
1516
)
1617
}

frontend/packages/data-portal/app/components/Filters/AuthorFilter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ export function AuthorFilter({ label }: { label: string }) {
1414
id: 'author-name-input',
1515
label: `${t('authorName')}:`,
1616
queryParam: QueryParams.AuthorName,
17+
placeholder: t('authorNamePlaceholder'),
1718
},
1819
{
1920
id: 'author-orcid-input',
2021
label: `${t('authorOrcid')}:`,
2122
queryParam: QueryParams.AuthorOrcid,
23+
placeholder: t('authorOrcidPlaceholder'),
2224
},
2325
],
2426
[t],

frontend/packages/data-portal/app/components/Filters/DropdownFilterButton.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,13 @@ export function DropdownFilterButton({
122122
open={open}
123123
anchorOrigin={{ vertical: 40, horizontal: 'left' }}
124124
>
125-
<div className="px-sds-l py-sds-default min-w-[278px] max-w-[320px] flex flex-col">
125+
<div className="px-sds-l py-sds-default min-w-[278px] max-w-[350px] flex flex-col">
126126
{description}
127127
{children}
128128

129-
<div className="flex items-center gap-sds-default mt-sds-l">
129+
<div className="flex items-center justify-stretch gap-sds-default mt-sds-l">
130130
<Button
131+
className="w-full"
131132
disabled={disabled}
132133
sdsType="primary"
133134
sdsStyle="square"
@@ -140,6 +141,7 @@ export function DropdownFilterButton({
140141
</Button>
141142

142143
<Button
144+
className="w-full"
143145
sdsType="secondary"
144146
sdsStyle="square"
145147
onClick={() => {

0 commit comments

Comments
 (0)