From f43db106d8b784de3341e0c36225cfcc890e5fb2 Mon Sep 17 00:00:00 2001 From: Calvin Lu <59149377+calvinlu3@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:02:46 -0400 Subject: [PATCH] Initial commit for exon curation --- .../oncokb/curation/service/MainService.java | 100 +++++------------- .../curation/service/TranscriptService.java | 75 ++++++++++++- .../oncokb/curation/util/AlterationUtils.java | 49 ++++++++- .../webapp/app/config/constants/regex.spec.ts | 20 +++- src/main/webapp/app/config/constants/regex.ts | 2 + .../app/hooks/useTextareaAutoHeight.tsx | 26 +++++ .../firebase/input/RealtimeBasicInput.tsx | 20 +--- .../app/shared/modal/AddMutationModal.tsx | 99 ++++++++++------- .../app/shared/modal/add-mutation-modal.scss | 6 ++ 9 files changed, 262 insertions(+), 135 deletions(-) create mode 100644 src/main/webapp/app/hooks/useTextareaAutoHeight.tsx diff --git a/src/main/java/org/mskcc/oncokb/curation/service/MainService.java b/src/main/java/org/mskcc/oncokb/curation/service/MainService.java index 46f4bf8d0..48293e64d 100644 --- a/src/main/java/org/mskcc/oncokb/curation/service/MainService.java +++ b/src/main/java/org/mskcc/oncokb/curation/service/MainService.java @@ -16,7 +16,6 @@ import org.mskcc.oncokb.curation.domain.dto.HotspotInfoDTO; import org.mskcc.oncokb.curation.domain.dto.ProteinExonDTO; import org.mskcc.oncokb.curation.domain.enumeration.*; -import org.mskcc.oncokb.curation.model.IntegerRange; import org.mskcc.oncokb.curation.service.dto.TranscriptDTO; import org.mskcc.oncokb.curation.service.mapper.TranscriptMapper; import org.mskcc.oncokb.curation.util.AlterationUtils; @@ -280,87 +279,38 @@ public AlterationAnnotationStatus annotateAlteration(ReferenceGenome referenceGe } annotationDTO.setHotspot(hotspotInfoDTO); - if ( - annotatedGenes.size() == 1 && - PROTEIN_CHANGE.equals(alteration.getType()) && - alteration.getStart() != null && - alteration.getEnd() != null - ) { - Optional transcriptOptional = transcriptService.findByGeneAndReferenceGenomeAndCanonicalIsTrue( - annotatedGenes.stream().iterator().next(), - referenceGenome - ); - if (transcriptOptional.isPresent()) { - List utrs = transcriptOptional.orElseThrow().getUtrs(); - List exons = transcriptOptional.orElseThrow().getExons(); - exons.sort((o1, o2) -> { - int diff = o1.getStart() - o2.getStart(); - if (diff == 0) { - diff = o1.getEnd() - o2.getEnd(); - } - if (diff == 0) { - diff = (int) (o1.getId() - o2.getId()); - } - return diff; - }); - - List codingExons = new ArrayList<>(); - exons.forEach(exon -> { - Integer start = exon.getStart(); - Integer end = exon.getEnd(); - for (GenomeFragment utr : utrs) { - if (utr.getStart().equals(exon.getStart())) { - start = utr.getEnd() + 1; - } - if (utr.getEnd().equals(exon.getEnd())) { - end = utr.getStart() - 1; - } - } - if (start < end) { - GenomeFragment genomeFragment = new GenomeFragment(); - genomeFragment.setType(GenomeFragmentType.EXON); - genomeFragment.setStart(start); - genomeFragment.setEnd(end); - codingExons.add(genomeFragment); - } else { - GenomeFragment genomeFragment = new GenomeFragment(); - genomeFragment.setType(GenomeFragmentType.EXON); - genomeFragment.setStart(0); - genomeFragment.setEnd(0); - codingExons.add(genomeFragment); - } - }); - - if (transcriptOptional.orElseThrow().getStrand() == -1) { - Collections.reverse(codingExons); - } - - List proteinExons = new ArrayList<>(); - int startAA = 1; - int previousExonCodonResidues = 0; - for (int i = 0; i < codingExons.size(); i++) { - GenomeFragment genomeFragment = codingExons.get(i); - if (genomeFragment.getStart() == 0) { - continue; - } - int proteinLength = (previousExonCodonResidues + (genomeFragment.getEnd() - genomeFragment.getStart() + 1)) / 3; - previousExonCodonResidues = (previousExonCodonResidues + (genomeFragment.getEnd() - genomeFragment.getStart() + 1)) % 3; - ProteinExonDTO proteinExonDTO = new ProteinExonDTO(); - proteinExonDTO.setExon(i + 1); - IntegerRange integerRange = new IntegerRange(); - integerRange.setStart(startAA); - integerRange.setEnd(startAA + proteinLength - 1 + (previousExonCodonResidues > 0 ? 1 : 0)); - proteinExonDTO.setRange(integerRange); - proteinExons.add(proteinExonDTO); - startAA += proteinLength; - } + if (annotatedGenes.size() == 1) { + List proteinExons = transcriptService.getExons(annotatedGenes.stream().iterator().next(), referenceGenome); + if (PROTEIN_CHANGE.equals(alteration.getType()) && alteration.getStart() != null && alteration.getEnd() != null) { + // Filter exons based on alteration range List overlap = proteinExons .stream() .filter(exon -> alteration.getStart() <= exon.getRange().getEnd() && alteration.getEnd() >= exon.getRange().getStart()) .collect(Collectors.toList()); annotationDTO.setExons(overlap); + } else if (AlterationUtils.isExon(alteration.getAlteration())) { + List overlap = new ArrayList<>(); + List problematicExonAlts = new ArrayList<>(); + for (String exonAlterationString : Arrays.asList(alteration.getAlteration().split("\\s*\\+\\s*"))) { + Integer exonNumber = Integer.parseInt(exonAlterationString.replaceAll("\\D*", "")); + if (exonNumber > 0 && exonNumber < proteinExons.size()) { + overlap.add(proteinExons.get(exonNumber - 1)); + } else { + problematicExonAlts.add(exonAlterationString); + } + } + if (problematicExonAlts.isEmpty()) { + annotationDTO.setExons(overlap); + } else { + StringBuilder sb = new StringBuilder(); + sb.append("The following exon(s) do not exist: "); + sb.append(problematicExonAlts.stream().collect(Collectors.joining(", "))); + alterationWithStatus.setMessage(sb.toString()); + alterationWithStatus.setType(EntityStatusType.ERROR); + } } } + alterationWithStatus.setAnnotation(annotationDTO); return alterationWithStatus; } diff --git a/src/main/java/org/mskcc/oncokb/curation/service/TranscriptService.java b/src/main/java/org/mskcc/oncokb/curation/service/TranscriptService.java index 52ced8dc9..f23998faf 100644 --- a/src/main/java/org/mskcc/oncokb/curation/service/TranscriptService.java +++ b/src/main/java/org/mskcc/oncokb/curation/service/TranscriptService.java @@ -3,8 +3,6 @@ import static org.mskcc.oncokb.curation.config.Constants.ENSEMBL_POST_THRESHOLD; import java.util.*; -import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.genome_nexus.ApiException; @@ -13,9 +11,11 @@ import org.mskcc.oncokb.curation.config.cache.CacheCategory; import org.mskcc.oncokb.curation.config.cache.CacheNameResolver; import org.mskcc.oncokb.curation.domain.*; +import org.mskcc.oncokb.curation.domain.dto.ProteinExonDTO; import org.mskcc.oncokb.curation.domain.enumeration.GenomeFragmentType; import org.mskcc.oncokb.curation.domain.enumeration.ReferenceGenome; import org.mskcc.oncokb.curation.domain.enumeration.SequenceType; +import org.mskcc.oncokb.curation.model.IntegerRange; import org.mskcc.oncokb.curation.repository.TranscriptRepository; import org.mskcc.oncokb.curation.service.dto.ClustalOResp; import org.mskcc.oncokb.curation.service.dto.TranscriptDTO; @@ -582,6 +582,77 @@ public List getAlignmentResult( } } + public List getExons(Gene gene, ReferenceGenome referenceGenome) { + Optional transcriptOptional = this.findByGeneAndReferenceGenomeAndCanonicalIsTrue(gene, referenceGenome); + if (transcriptOptional.isPresent()) { + List utrs = transcriptOptional.orElseThrow().getUtrs(); + List exons = transcriptOptional.orElseThrow().getExons(); + exons.sort((o1, o2) -> { + int diff = o1.getStart() - o2.getStart(); + if (diff == 0) { + diff = o1.getEnd() - o2.getEnd(); + } + if (diff == 0) { + diff = (int) (o1.getId() - o2.getId()); + } + return diff; + }); + + List codingExons = new ArrayList<>(); + exons.forEach(exon -> { + Integer start = exon.getStart(); + Integer end = exon.getEnd(); + for (GenomeFragment utr : utrs) { + if (utr.getStart().equals(exon.getStart())) { + start = utr.getEnd() + 1; + } + if (utr.getEnd().equals(exon.getEnd())) { + end = utr.getStart() - 1; + } + } + if (start < end) { + GenomeFragment genomeFragment = new GenomeFragment(); + genomeFragment.setType(GenomeFragmentType.EXON); + genomeFragment.setStart(start); + genomeFragment.setEnd(end); + codingExons.add(genomeFragment); + } else { + GenomeFragment genomeFragment = new GenomeFragment(); + genomeFragment.setType(GenomeFragmentType.EXON); + genomeFragment.setStart(0); + genomeFragment.setEnd(0); + codingExons.add(genomeFragment); + } + }); + + if (transcriptOptional.orElseThrow().getStrand() == -1) { + Collections.reverse(codingExons); + } + + List proteinExons = new ArrayList<>(); + int startAA = 1; + int previousExonCodonResidues = 0; + for (int i = 0; i < codingExons.size(); i++) { + GenomeFragment genomeFragment = codingExons.get(i); + if (genomeFragment.getStart() == 0) { + continue; + } + int proteinLength = (previousExonCodonResidues + (genomeFragment.getEnd() - genomeFragment.getStart() + 1)) / 3; + previousExonCodonResidues = (previousExonCodonResidues + (genomeFragment.getEnd() - genomeFragment.getStart() + 1)) % 3; + ProteinExonDTO proteinExonDTO = new ProteinExonDTO(); + proteinExonDTO.setExon(i + 1); + IntegerRange integerRange = new IntegerRange(); + integerRange.setStart(startAA); + integerRange.setEnd(startAA + proteinLength - 1 + (previousExonCodonResidues > 0 ? 1 : 0)); + proteinExonDTO.setRange(integerRange); + proteinExons.add(proteinExonDTO); + startAA += proteinLength; + } + return proteinExons; + } + return new ArrayList<>(); + } + private Optional getEnsemblTranscriptBySequence( List availableEnsemblTranscripts, EnsemblSequence sequence diff --git a/src/main/java/org/mskcc/oncokb/curation/util/AlterationUtils.java b/src/main/java/org/mskcc/oncokb/curation/util/AlterationUtils.java index 0165c61b2..8fc5a7436 100644 --- a/src/main/java/org/mskcc/oncokb/curation/util/AlterationUtils.java +++ b/src/main/java/org/mskcc/oncokb/curation/util/AlterationUtils.java @@ -8,7 +8,6 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.similarity.JaroWinklerSimilarity; -import org.checkerframework.checker.regex.qual.Regex; import org.mskcc.oncokb.curation.domain.*; import org.mskcc.oncokb.curation.domain.enumeration.*; import org.springframework.stereotype.Component; @@ -21,6 +20,10 @@ public class AlterationUtils { private static final String FUSION_REGEX = "\\s*(\\w*)" + FUSION_SEPARATOR + "(\\w*)\\s*(?i)(fusion)?\\s*"; private static final String FUSION_ALT_REGEX = "\\s*(\\w*)" + FUSION_ALTERNATIVE_SEPARATOR + "(\\w*)\\s+(?i)fusion\\s*"; + private static final String EXON_ALT_REGEX = "Exon\\s+(\\d+)(-(\\d+))?\\s+(Deletion|Insertion|Duplication)"; + + private static final String EXON_ALTS_REGEX = "(" + EXON_ALT_REGEX + ")(\\s*\\+\\s*" + EXON_ALT_REGEX + ")*"; + private Alteration parseFusion(String alteration) { Alteration alt = new Alteration(); @@ -90,6 +93,36 @@ private Alteration parseGenomicChange(String genomicChange) { return alt; } + private Alteration parseExonAlteration(String alteration) { + Alteration alt = new Alteration(); + Consequence consequence = new Consequence(); + consequence.setTerm(UNKNOWN.name()); + alt.setType(AlterationType.STRUCTURAL_VARIANT); + alt.setConsequence(consequence); + + Pattern pattern = Pattern.compile(EXON_ALT_REGEX); + Matcher matcher = pattern.matcher(alteration); + List splitResults = new ArrayList<>(); + + while (matcher.find()) { + String startExonStr = matcher.group(1); // The start exon number + String endExonStr = matcher.group(3); // The end exon number (if present) + String consequenceTerm = matcher.group(4); // consequence term + + int startExon = Integer.parseInt(startExonStr); + int endExon = (endExonStr != null) ? Integer.parseInt(endExonStr) : startExon; + + for (int exon = startExon; exon <= endExon; exon++) { + splitResults.add("Exon " + exon + " " + consequenceTerm); + } + } + + alt.setAlteration(splitResults.stream().collect(Collectors.joining(" + "))); + + alt.setName(alteration); + return alt; + } + public EntityStatus parseAlteration(String alteration) { EntityStatus entityWithStatus = new EntityStatus<>(); String message = ""; @@ -130,6 +163,14 @@ public EntityStatus parseAlteration(String alteration) { return entityWithStatus; } + if (isExon(alteration)) { + Alteration alt = parseExonAlteration(alteration); + entityWithStatus.setEntity(alt); + entityWithStatus.setType(status); + entityWithStatus.setMessage(message); + return entityWithStatus; + } + // the following is to parse the alteration as protein change MutationConsequence term = UNKNOWN; String ref = null; @@ -474,6 +515,12 @@ public static Boolean isGenomicChange(String alteration) { return m.matches(); } + public static Boolean isExon(String alteration) { + Pattern p = Pattern.compile(EXON_ALTS_REGEX); + Matcher m = p.matcher(alteration); + return m.matches(); + } + public static String removeExclusionCriteria(String proteinChange) { Matcher exclusionMatch = getExclusionCriteriaMatcher(proteinChange); if (exclusionMatch.matches()) { diff --git a/src/main/webapp/app/config/constants/regex.spec.ts b/src/main/webapp/app/config/constants/regex.spec.ts index 1a664984a..707893ed4 100644 --- a/src/main/webapp/app/config/constants/regex.spec.ts +++ b/src/main/webapp/app/config/constants/regex.spec.ts @@ -1,4 +1,4 @@ -import { REFERENCE_LINK_REGEX, FDA_SUBMISSION_REGEX } from './regex'; +import { REFERENCE_LINK_REGEX, FDA_SUBMISSION_REGEX, EXON_ALTERATION_REGEX } from './regex'; describe('Regex constants test', () => { describe('Reference link regex', () => { @@ -75,4 +75,22 @@ describe('Regex constants test', () => { expect(FDA_SUBMISSION_REGEX.test(submission)).toEqual(expected); }); }); + + describe('Exon alteration regex', () => { + test.each([ + ['Exon 14 Deletion', true], + ['Exon 14 Duplication', true], + ['Exon 4 Insertion', true], + ['Exon 4-8 Deletion', true], + ['Exon 4 InSERTion', true], + ['Exon 4 Duplication', true], + ['Exon 4 Deletion + Exon 5 Deletion + Exon 6 Deletion', true], + ['Exon 4-8 Deletion + Exon 10 Deletion', true], + ['Exon 4 Deletion+Exon 5 Deletion', true], + ['Exon 14 Del', false], + ['Exon 4 8 Insertion', false], + ])('should return %b for %s', (alteration, expected) => { + expect(EXON_ALTERATION_REGEX.test(alteration)).toEqual(expected); + }); + }); }); diff --git a/src/main/webapp/app/config/constants/regex.ts b/src/main/webapp/app/config/constants/regex.ts index 070f36ed3..04164099c 100644 --- a/src/main/webapp/app/config/constants/regex.ts +++ b/src/main/webapp/app/config/constants/regex.ts @@ -9,3 +9,5 @@ export const UUID_REGEX = new RegExp('\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}'); export const WHOLE_NUMBER_REGEX = new RegExp('^\\d+$'); export const INTEGER_REGEX = /^-?\d+$/; + +export const EXON_ALTERATION_REGEX = /(Exon\s+(\d+)(-(\d+))?\s+(Deletion|Insertion|Duplication))(\s*\+\s*(\1))*/i; diff --git a/src/main/webapp/app/hooks/useTextareaAutoHeight.tsx b/src/main/webapp/app/hooks/useTextareaAutoHeight.tsx new file mode 100644 index 000000000..922a502d4 --- /dev/null +++ b/src/main/webapp/app/hooks/useTextareaAutoHeight.tsx @@ -0,0 +1,26 @@ +import React, { useEffect } from 'react'; +import { InputType } from 'zlib'; + +export const useTextareaAutoHeight = ( + inputRef: React.MutableRefObject, + type: InputType | undefined, +) => { + useEffect(() => { + const input = inputRef.current; + if (!input || type !== 'textarea') { + return; + } + + const resizeObserver = new ResizeObserver(() => { + window.requestAnimationFrame(() => { + input.style.height = 'auto'; + input.style.height = `${input.scrollHeight}px`; + }); + }); + resizeObserver.observe(input); + + return () => { + resizeObserver.disconnect(); + }; + }, []); +}; diff --git a/src/main/webapp/app/shared/firebase/input/RealtimeBasicInput.tsx b/src/main/webapp/app/shared/firebase/input/RealtimeBasicInput.tsx index d350f6937..7ef65b06c 100644 --- a/src/main/webapp/app/shared/firebase/input/RealtimeBasicInput.tsx +++ b/src/main/webapp/app/shared/firebase/input/RealtimeBasicInput.tsx @@ -10,6 +10,7 @@ import { FormFeedback, Input, Label, LabelProps } from 'reactstrap'; import { InputType } from 'reactstrap/types/lib/Input'; import * as styles from './styles.module.scss'; import { Unsubscribe } from 'firebase/database'; +import { useTextareaAutoHeight } from 'app/hooks/useTextareaAutoHeight'; export enum RealtimeInputType { TEXT = 'text', @@ -116,24 +117,7 @@ const RealtimeBasicInput: React.FunctionComponent = (props: }; }, [firebasePath, db]); - useEffect(() => { - const input = inputRef.current; - if (!input || type !== RealtimeInputType.TEXTAREA) { - return; - } - - const resizeObserver = new ResizeObserver(() => { - window.requestAnimationFrame(() => { - input.style.height = 'auto'; - input.style.height = `${input.scrollHeight}px`; - }); - }); - resizeObserver.observe(input); - - return () => { - resizeObserver.disconnect(); - }; - }, []); + useTextareaAutoHeight(inputRef, type); const labelComponent = label && ( diff --git a/src/main/webapp/app/shared/modal/AddMutationModal.tsx b/src/main/webapp/app/shared/modal/AddMutationModal.tsx index 352999936..602a90d99 100644 --- a/src/main/webapp/app/shared/modal/AddMutationModal.tsx +++ b/src/main/webapp/app/shared/modal/AddMutationModal.tsx @@ -31,6 +31,8 @@ import InfoIcon from '../icons/InfoIcon'; import { FlagTypeEnum } from '../model/enumerations/flag-type.enum.model'; import { IFlag } from '../model/flag.model'; import { SentryError } from 'app/config/sentry-error'; +import { InputType } from 'reactstrap/types/lib/Input'; +import { useTextareaAutoHeight } from 'app/hooks/useTextareaAutoHeight'; export type AlterationData = { type: AlterationTypeEnum; @@ -742,6 +744,7 @@ function AddMutationModal({ onChange={newValue => handleFieldChange(newValue?.value, 'type', alterationIndex, excludingIndex)} /> convertAlterationDataToAlteration(ex)); + alteration.genes = alterationData.genes || []; + return alteration; + } + + async function handleConfirm() { + const newMutation = mutationToEdit ? _.cloneDeep(mutationToEdit) : new Mutation(''); + const newAlterations = tabStates.map(state => convertAlterationDataToAlteration(state)); + newMutation.name = newAlterations.map(alteration => alteration.name).join(', '); + newMutation.alterations = newAlterations; + if (stringMutationInfo) { + const finalFlagArray = await saveNewFlags(); + newMutation.string_mutation_info = stringMutationInfo; + newMutation.string_mutation_info.flags = finalFlagArray; + } + + setErrorMessagesEnabled(false); + setIsConfirmPending(true); + try { + await onConfirm(newMutation, mutationList?.length || 0); + } finally { + setErrorMessagesEnabled(true); + setIsConfirmPending(false); + } + } return ( Promoting Variant(s) to Mutation : undefined} modalBody={modalBody} onCancel={onCancel} - onConfirm={async () => { - function convertAlterationDataToAlteration(alterationData: AlterationData) { - const alteration = new Alteration(); - alteration.type = alterationData.type; - alteration.alteration = alterationData.alteration; - alteration.name = getFullAlterationName(alterationData); - alteration.proteinChange = alterationData.proteinChange || ''; - alteration.proteinStart = alterationData.proteinStart || -1; - alteration.proteinEnd = alterationData.proteinEnd || -1; - alteration.refResidues = alterationData.refResidues || ''; - alteration.varResidues = alterationData.varResidues || ''; - alteration.consequence = alterationData.consequence; - alteration.comment = alterationData.comment; - alteration.excluding = alterationData.excluding.map(ex => convertAlterationDataToAlteration(ex)); - alteration.genes = alterationData.genes || []; - return alteration; - } - - const newMutation = mutationToEdit ? _.cloneDeep(mutationToEdit) : new Mutation(''); - const newAlterations = tabStates.map(state => convertAlterationDataToAlteration(state)); - newMutation.name = newAlterations.map(alteration => alteration.name).join(', '); - newMutation.alterations = newAlterations; - const newStringMutationInfo = await handleStringMutationInfoConfirm(); - if (newStringMutationInfo) { - newMutation.string_mutation_info = newStringMutationInfo; - } - - setErrorMessagesEnabled(false); - setIsConfirmPending(true); - try { - await onConfirm(newMutation, mutationList?.length || 0); - } finally { - setErrorMessagesEnabled(true); - setIsConfirmPending(false); - } - }} + onConfirm={handleConfirm} errorMessages={modalErrorMessage && errorMessagesEnabled ? [modalErrorMessage] : undefined} warningMessages={modalWarningMessage ? [modalWarningMessage] : undefined} confirmButtonDisabled={ @@ -1274,9 +1280,14 @@ interface IAddMutationModalFieldProps { onChange: (newValue: string) => void; isLoading?: boolean; disabled?: boolean; + type?: InputType; } -function AddMutationModalField({ label, value: value, placeholder, onChange, isLoading, disabled }: IAddMutationModalFieldProps) { +function AddMutationModalField({ label, value: value, placeholder, onChange, isLoading, disabled, type }: IAddMutationModalFieldProps) { + const inputRef = useRef(null); + + useTextareaAutoHeight(inputRef, type); + return (
@@ -1287,12 +1298,15 @@ function AddMutationModalField({ label, value: value, placeholder, onChange, isL { onChange(event.target.value); }} placeholder={placeholder} + type={type} + className={classNames(type === 'textarea' ? 'alteration-modal-textarea-field' : undefined)} />
@@ -1332,7 +1346,7 @@ const AddMutationInputOverlay = () => { Add button to annotate alteration(s).
-
Examples:
+
String Mutation:
  • @@ -1343,6 +1357,15 @@ const AddMutationInputOverlay = () => {
+
Exon:
+
    +
  • + Supported consequences are Insertion, Deletion and Duplication - Exon 4 Deletion +
  • +
  • + Exon range - Exon 4-8 Deletion +
  • +
); diff --git a/src/main/webapp/app/shared/modal/add-mutation-modal.scss b/src/main/webapp/app/shared/modal/add-mutation-modal.scss index 3fcac187e..b532a46bc 100644 --- a/src/main/webapp/app/shared/modal/add-mutation-modal.scss +++ b/src/main/webapp/app/shared/modal/add-mutation-modal.scss @@ -3,3 +3,9 @@ justify-content: center; align-items: center; } + +.alteration-modal-textarea-field { + min-height: 20px !important; + overflow-y: hidden; + resize: none; +}