11import * as _ from 'lodash' ;
22import * as React from 'react' ;
3- import { observer } from 'mobx-react' ;
3+ import { action , observable , reaction , runInAction } from 'mobx' ;
4+ import { disposeOnUnmount , observer } from 'mobx-react' ;
45
5- import { Headers } from '../../types' ;
6+ import { RawHeaders } from '../../types' ;
67import { HEADER_NAME_PATTERN } from '../../util/headers' ;
78
8- import { EditablePairs , Pair , PairsArray } from './editable-pairs' ;
9+ import { EditablePairs , PairsArray } from './editable-pairs' ;
910
10- export type HeadersArray = Array < Pick < Pair , 'key' | 'value' > > ;
11+ const tupleArrayToPairsArray = ( headers : RawHeaders ) : PairsArray =>
12+ headers . map ( ( [ key , value ] ) => ( { key, value } ) ) ;
1113
12- const headersToHeadersArray = ( headers : Headers ) : HeadersArray =>
13- Object . entries ( headers || { } ) . reduce (
14- ( acc : HeadersArray , [ key , value ] ) => {
15- if ( _ . isArray ( value ) ) {
16- acc = acc . concat ( value . map ( value => ( { key, value } ) ) )
17- } else {
18- acc . push ( { key, value : value || '' } ) ;
19- }
20- return acc ;
21- } , [ ]
22- ) ;
23-
24- const headersArrayToHeaders = ( headers : HeadersArray ) : Headers =>
25- headers . reduce ( ( headersObj : { [ k : string ] : string | string [ ] } , { key, value } ) => {
26- const headerName = key . toLowerCase ( ) ;
27-
28- const existingValue = headersObj [ headerName ] ;
29- if ( existingValue === undefined ) {
30- headersObj [ headerName ] = value ;
31- } else if ( _ . isString ( existingValue ) ) {
32- headersObj [ headerName ] = [ existingValue , value ] ;
33- } else {
34- existingValue . push ( value ) ;
35- }
36- return headersObj ;
37- } , { } ) ;
38-
39- const withH2HeadersDisabled = ( headers : HeadersArray ) : PairsArray =>
14+ const withH2HeadersDisabled = ( headers : PairsArray ) : PairsArray =>
4015 headers . map ( ( { key, value } ) =>
4116 key . startsWith ( ':' )
4217 ? { key, value, disabled : true }
4318 : { key, value }
4419 ) ;
4520
46- const normalizeHeaderInput = ( headers : PairsArray ) : HeadersArray =>
21+ const rawHeadersAsEditablePairs = ( rawHeaders : RawHeaders ) => {
22+ return withH2HeadersDisabled ( tupleArrayToPairsArray ( rawHeaders ) ) ;
23+ } ;
24+
25+ const outputToRawHeaders = ( output : PairsArray ) : RawHeaders =>
26+ output . map ( ( { key, value } ) => [ key , value ] ) ;
27+
28+ const stripPseudoHeaders = ( headers : PairsArray ) : PairsArray =>
29+ // Strip leading colons - HTTP/2 headers should never be entered raw (but don't lower case)
30+ headers . map ( ( { key, value, disabled } ) => ( {
31+ key : ! disabled && key . startsWith ( ':' )
32+ ? key . slice ( 1 )
33+ : key ,
34+ value,
35+ disabled
36+ } ) ) ;
37+
38+ const stripPseudoHeadersAndLowercase = ( headers : PairsArray ) : PairsArray =>
4739 // Lowercase header keys, and strip any leading colons - HTTP/2 headers should never be entered raw
4840 headers . map ( ( { key, value, disabled } ) => ( {
4941 key : ! disabled && key . startsWith ( ':' )
5042 ? key . slice ( 1 ) . toLowerCase ( )
5143 : key . toLowerCase ( ) ,
52- value
44+ value,
45+ disabled
5346 } ) ) ;
5447
55- interface EditableHeadersProps < R = Headers > {
56- headers : Headers ;
57- onChange : ( headers : R ) => void ;
58- convertResult ?: ( headers : Headers ) => R ;
48+ interface EditableRawHeadersProps {
49+ input : RawHeaders ;
50+ onChange : ( headers : RawHeaders ) => void ;
5951
6052 // It's unclear whether you're strictly allowed completely empty header values, but it's definitely
6153 // not recommended and poorly supported. By default we disable it except for special use cases.
6254 allowEmptyValues ?: boolean ;
55+
56+ // By default, we lowercase headers. This isn't strictly required, but it's generally clearer,
57+ // simpler, and semantically meaningless. HTTP/2 is actually even forced lowercase on the wire.
58+ // Nonetheless, sometimes (breakpoints) you want to ignore that and edit with case regardless
59+ // because the real raw headers have existing cases and otherwise it's weird:
60+ preserveKeyCase ?: boolean ;
6361}
6462
65- export const EditableHeaders = observer ( < R extends unknown > ( props : EditableHeadersProps < R > ) => {
66- const { headers, onChange, allowEmptyValues, convertResult } = props ;
63+ export const EditableRawHeaders = observer ( (
64+ props : EditableRawHeadersProps
65+ ) => {
66+ const { input : headers , onChange, allowEmptyValues, preserveKeyCase } = props ;
6767
68- return < EditablePairs < R >
69- pairs = { withH2HeadersDisabled ( headersToHeadersArray ( headers ) ) }
68+ return < EditablePairs < RawHeaders >
69+ pairs = { rawHeadersAsEditablePairs ( headers ) }
7070 onChange = { onChange }
71- transformInput = { normalizeHeaderInput }
72- convertResult = { ( pairs : PairsArray ) =>
73- convertResult
74- ? convertResult ( headersArrayToHeaders ( pairs ) )
75- : headersArrayToHeaders ( pairs ) as unknown as R
71+ transformInput = {
72+ preserveKeyCase
73+ ? stripPseudoHeaders
74+ : stripPseudoHeadersAndLowercase
7675 }
76+ convertResult = { outputToRawHeaders }
7777
7878 allowEmptyValues = { allowEmptyValues }
7979
@@ -83,4 +83,78 @@ export const EditableHeaders = observer(<R extends unknown>(props: EditableHeade
8383 keyPlaceholder = 'Header name'
8484 valuePlaceholder = 'Header value'
8585 /> ;
86- } ) ;
86+ } ) ;
87+
88+ interface EditableHeadersProps < T > {
89+ input : T ,
90+
91+ convertInput : ( input : T ) => RawHeaders ,
92+ convertResult : ( headers : RawHeaders ) => T ,
93+
94+ onChange : ( headers : T ) => void ;
95+ onInvalidState ?: ( ) => void ;
96+
97+ // It's unclear whether you're strictly allowed completely empty header values, but it's definitely
98+ // not recommended and poorly supported. By default we disable it except for special use cases.
99+ allowEmptyValues ?: boolean ;
100+ }
101+
102+ // Editable headers acts as a wrapper around the raw header pair modification, converting to and from other
103+ // formats (most commonly header objects, rather than arrays) whilst avoiding unnecessary updates that
104+ // cause churn in the UI due to unrepresentable states in that format (e.g. duplicate headers that aren't
105+ // directly next to each other). This allows you to edit as if the data was in raw header format, but
106+ // get different data live as it changes, without collapsing to that state until later.
107+ @observer
108+ export class EditableHeaders < T > extends React . Component < EditableHeadersProps < T > > {
109+
110+ @observable
111+ private rawHeaders : RawHeaders = this . props . convertInput ( this . props . input ) ;
112+
113+ private output : T = this . props . input ;
114+
115+ componentDidMount ( ) {
116+ // Watch the input, but only update our state if its materially different
117+ // to the last output we returned.
118+ disposeOnUnmount ( this , reaction (
119+ ( ) => this . props . input ,
120+ ( input ) => {
121+ if ( ! _ . isEqual ( input , this . output ) ) {
122+ const newInput = this . props . convertInput ( input ) ;
123+ runInAction ( ( ) => {
124+ this . rawHeaders = newInput ;
125+ } ) ;
126+ }
127+ }
128+ ) ) ;
129+ }
130+
131+ @action . bound
132+ onChangeRawHeaders ( rawHeaders : RawHeaders ) {
133+ this . rawHeaders = rawHeaders ;
134+
135+ const { allowEmptyValues, convertResult, onChange, onInvalidState } = this . props ;
136+
137+ if ( allowEmptyValues ) {
138+ this . output = convertResult ( rawHeaders ) ;
139+ onChange ( this . output ) ;
140+ } else {
141+ if ( rawHeaders . some ( ( ( [ _ , value ] ) => ! value ) ) ) return onInvalidState ?.( ) ;
142+ if ( rawHeaders . some ( ( [ key ] ) => ! key ) ) return onInvalidState ?.( ) ;
143+
144+ this . output = convertResult ( rawHeaders ) ;
145+ onChange ( this . output ) ;
146+ }
147+ }
148+
149+ render ( ) {
150+ const { allowEmptyValues } = this . props ;
151+ const { rawHeaders, onChangeRawHeaders } = this ;
152+
153+ return < EditableRawHeaders
154+ input = { rawHeaders }
155+ onChange = { onChangeRawHeaders }
156+ allowEmptyValues = { allowEmptyValues }
157+ /> ;
158+ }
159+
160+ }
0 commit comments