Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Open sidebar
ensembl-web
ensembl-client
Commits
ed8cb5cf
Unverified
Commit
ed8cb5cf
authored
Sep 15, 2021
by
Andrey Azov
Committed by
GitHub
Sep 15, 2021
Browse files
Save contact us form state (#553)
parent
afaaebb2
Pipeline
#193805
passed with stages
in 4 minutes and 38 seconds
Changes
7
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
253 additions
and
8 deletions
+253
-8
src/ensembl/package-lock.json
src/ensembl/package-lock.json
+11
-0
src/ensembl/package.json
src/ensembl/package.json
+1
-0
src/ensembl/src/index.tsx
src/ensembl/src/index.tsx
+11
-7
src/ensembl/src/services/indexeddb-service.ts
src/ensembl/src/services/indexeddb-service.ts
+68
-0
src/ensembl/src/shared/components/communication-framework/contact-us/contact-us-form/hooks/useSavedForm.ts
...ramework/contact-us/contact-us-form/hooks/useSavedForm.ts
+112
-0
src/ensembl/src/shared/components/communication-framework/contact-us/contact-us-form/initial/ContactUsInitialForm.tsx
...ntact-us/contact-us-form/initial/ContactUsInitialForm.tsx
+18
-1
src/ensembl/src/shared/contexts/IndexedDBContext.tsx
src/ensembl/src/shared/contexts/IndexedDBContext.tsx
+32
-0
No files found.
src/ensembl/package-lock.json
View file @
ed8cb5cf
...
...
@@ -27,6 +27,7 @@
"graphql": "15.5.1",
"graphql-request": "3.5.0",
"http-proxy-middleware": "2.0.1",
"idb": "6.1.2",
"lodash": "4.17.21",
"query-string": "7.0.1",
"react": "17.0.2",
...
...
@@ -22718,6 +22719,11 @@
"postcss": "^8.1.0"
}
},
"node_modules/idb": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/idb/-/idb-6.1.2.tgz",
"integrity": "sha512-1DNDVu3yDhAZkFDlJf0t7r+GLZ248F5pTAtA7V0oVG3yjmV125qZOx3g0XpAEkGZVYQiFDAsSOnGet2bhugc3w=="
},
"node_modules/identity-obj-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
...
...
@@ -60241,6 +60247,11 @@
"dev": true,
"requires": {}
},
"idb": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/idb/-/idb-6.1.2.tgz",
"integrity": "sha512-1DNDVu3yDhAZkFDlJf0t7r+GLZ248F5pTAtA7V0oVG3yjmV125qZOx3g0XpAEkGZVYQiFDAsSOnGet2bhugc3w=="
},
"identity-obj-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
src/ensembl/package.json
View file @
ed8cb5cf
...
...
@@ -49,6 +49,7 @@
"graphql"
:
"15.5.1"
,
"graphql-request"
:
"3.5.0"
,
"http-proxy-middleware"
:
"2.0.1"
,
"idb"
:
"6.1.2"
,
"lodash"
:
"4.17.21"
,
"query-string"
:
"7.0.1"
,
"react"
:
"17.0.2"
,
...
...
src/ensembl/src/index.tsx
View file @
ed8cb5cf
...
...
@@ -20,6 +20,8 @@ import { Provider } from 'react-redux';
import
{
CookiesProvider
}
from
'
react-cookie
'
;
import
{
ConnectedRouter
}
from
'
connected-react-router
'
;
import
{
HelmetProvider
}
from
'
react-helmet-async
'
;
import
{
Provider
as
IndexedDBProvider
}
from
'
src/shared/contexts/IndexedDBContext
'
;
import
configureStore
,
{
history
}
from
'
./store
'
;
import
Root
from
'
./root/Root
'
;
...
...
@@ -32,13 +34,15 @@ const store = configureStore();
hydrate
(
<
StrictMode
>
<
CookiesProvider
>
<
Provider
store
=
{
store
}
>
<
ConnectedRouter
history
=
{
history
}
>
<
HelmetProvider
>
<
Root
/>
</
HelmetProvider
>
</
ConnectedRouter
>
</
Provider
>
<
IndexedDBProvider
>
<
Provider
store
=
{
store
}
>
<
ConnectedRouter
history
=
{
history
}
>
<
HelmetProvider
>
<
Root
/>
</
HelmetProvider
>
</
ConnectedRouter
>
</
Provider
>
</
IndexedDBProvider
>
</
CookiesProvider
>
</
StrictMode
>,
document
.
getElementById
(
'
ens-app
'
)
...
...
src/ensembl/src/services/indexeddb-service.ts
0 → 100644
View file @
ed8cb5cf
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import
{
openDB
,
IDBPDatabase
}
from
'
idb
'
;
const
DB_NAME
=
'
ensembl-website
'
;
const
DB_VERSION
=
1
;
const
getDbPromise
=
()
=>
{
return
openDB
(
DB_NAME
,
DB_VERSION
,
{
upgrade
(
db
)
{
if
(
!
db
.
objectStoreNames
.
contains
(
'
contact-forms
'
))
{
db
.
createObjectStore
(
'
contact-forms
'
);
}
}
});
};
class
IndexedDB
{
static
db
:
IDBPDatabase
|
null
=
null
;
private
static
async
getDB
()
{
if
(
!
this
.
db
)
{
this
.
db
=
await
getDbPromise
();
}
return
this
.
db
;
}
static
async
get
(
store
:
string
,
key
:
string
)
{
const
db
=
await
this
.
getDB
();
return
db
.
get
(
store
,
key
);
}
static
async
set
(
store
:
string
,
key
:
string
,
value
:
any
)
{
const
db
=
await
this
.
getDB
();
return
db
.
put
(
store
,
value
,
key
);
}
static
async
delete
(
store
:
string
,
key
:
string
)
{
const
db
=
await
this
.
getDB
();
return
db
.
delete
(
store
,
key
);
}
static
async
clear
(
store
:
string
)
{
const
db
=
await
this
.
getDB
();
return
db
.
clear
(
store
);
}
static
async
keys
(
store
:
string
)
{
const
db
=
await
this
.
getDB
();
return
db
.
getAllKeys
(
store
);
}
}
export
default
IndexedDB
;
src/ensembl/src/shared/components/communication-framework/contact-us/contact-us-form/hooks/useSavedForm.ts
0 → 100644
View file @
ed8cb5cf
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import
{
useEffect
,
useRef
,
useContext
}
from
'
react
'
;
import
IndexedDBContext
from
'
src/shared/contexts/IndexedDBContext
'
;
type
FormFieldType
=
string
|
File
|
File
[]
|
null
;
type
Form
=
Record
<
string
,
FormFieldType
>
;
type
Params
<
S
>
=
{
formName
:
string
;
currentState
:
S
;
updateState
:
(
state
:
S
)
=>
void
;
};
type
UseSavedForm
=
<
State
extends
Form
>
(
params
:
Params
<
State
>
)
=>
{
clearSavedForm
:
()
=>
void
;
};
const
STORE_NAME
=
'
contact-forms
'
;
// storage used to save data of different contact forms
const
useSavedForm
:
UseSavedForm
=
(
params
)
=>
{
const
{
formName
,
currentState
,
updateState
}
=
params
;
const
indexedDB
=
useContext
(
IndexedDBContext
);
const
stateRef
=
useRef
<
typeof
currentState
>
(
currentState
);
stateRef
.
current
=
currentState
;
// <-- to be able to save the latest state despite the closures
useEffect
(()
=>
{
// recover form state from the storage
indexedDB
.
get
(
STORE_NAME
,
formName
).
then
((
savedState
)
=>
{
if
(
savedState
)
{
updateState
(
savedState
);
}
});
},
[]);
useEffect
(()
=>
{
// save the form before user refreshes or closes the tab
window
.
addEventListener
(
'
beforeunload
'
,
saveFormState
);
return
()
=>
{
window
.
removeEventListener
(
'
beforeunload
'
,
saveFormState
);
saveFormState
();
// <-- also save the form on every unmount
};
},
[]);
const
isEmptyForm
=
(
state
:
typeof
currentState
)
=>
{
let
isEmpty
=
true
;
for
(
const
value
of
Object
.
values
(
state
))
{
if
(
Array
.
isArray
(
value
)
&&
value
.
length
)
{
isEmpty
=
false
;
}
else
if
(
value
!==
null
&&
value
!==
''
)
{
isEmpty
=
false
;
}
}
return
isEmpty
;
};
const
saveFormState
=
()
=>
{
// only save the form if it is not empty
if
(
isEmptyForm
(
stateRef
.
current
))
{
return
;
}
const
formWithoutHugeFiles
=
withoutHugeFiles
(
stateRef
.
current
);
indexedDB
.
set
(
STORE_NAME
,
params
.
formName
,
formWithoutHugeFiles
);
};
const
withoutHugeFiles
=
(
state
:
Form
)
=>
{
// the user isn't allowed to submit files larger than 10MB anyway;
// so there is no point is saving such files to IndexedDB
const
fileSizeLimit
=
10
e6
;
const
clonedState
=
{
...
state
};
for
(
const
[
fieldName
,
fieldValue
]
of
Object
.
entries
(
clonedState
))
{
if
(
fieldValue
instanceof
File
&&
fieldValue
.
size
>
fileSizeLimit
)
{
clonedState
[
fieldName
]
=
null
;
}
else
if
(
Array
.
isArray
(
fieldValue
))
{
clonedState
[
fieldName
]
=
fieldValue
.
filter
(
(
file
)
=>
file
.
size
<=
fileSizeLimit
);
}
}
return
clonedState
;
};
const
clearSavedForm
=
()
=>
{
indexedDB
.
delete
(
STORE_NAME
,
formName
);
};
return
{
clearSavedForm
};
};
export
default
useSavedForm
;
src/ensembl/src/shared/components/communication-framework/contact-us/contact-us-form/initial/ContactUsInitialForm.tsx
View file @
ed8cb5cf
...
...
@@ -26,6 +26,7 @@ import noop from 'lodash/noop';
import
{
submitForm
}
from
'
../submitForm
'
;
import
noEarlierThan
from
'
src/shared/utils/noEarlierThan
'
;
import
useSavedForm
from
'
../hooks/useSavedForm
'
;
import
SubmissionSuccess
from
'
../submission-success/SubmissionSuccess
'
;
import
ShadedInput
from
'
src/shared/components/input/ShadedInput
'
;
...
...
@@ -85,13 +86,19 @@ type RemoveFileAction = {
payload
:
number
;
// index of the file in the array of files
};
type
ReplaceStateAction
=
{
type
:
'
replace-state
'
;
payload
:
State
;
};
type
Action
=
|
UpdateNameAction
|
UpdateEmailAction
|
UpdateSubjectAction
|
UpdateMessageAction
|
AddFileAction
|
RemoveFileAction
;
|
RemoveFileAction
|
ReplaceStateAction
;
const
reducer
=
(
state
:
State
,
action
:
Action
):
State
=>
{
switch
(
action
.
type
)
{
...
...
@@ -109,6 +116,8 @@ const reducer = (state: State, action: Action): State => {
const
newFiles
=
[...
state
.
files
];
newFiles
.
splice
(
action
.
payload
,
1
);
return
{
...
state
,
files
:
newFiles
};
case
'
replace-state
'
:
return
action
.
payload
;
default
:
return
state
;
}
...
...
@@ -130,6 +139,13 @@ const ContactUsInitialForm = () => {
const
stateRef
=
useRef
<
typeof
state
>
();
stateRef
.
current
=
state
;
const
{
clearSavedForm
}
=
useSavedForm
({
formName
:
FORM_NAME
,
currentState
:
state
,
updateState
:
(
savedState
)
=>
dispatch
({
type
:
'
replace-state
'
,
payload
:
savedState
})
});
useEffect
(()
=>
{
// TODO: this useEffect will be unnecessary when the Input is refactored to include forwardRef
const
emailInput
=
formRef
.
current
?.
querySelector
(
'
#email
'
);
...
...
@@ -194,6 +210,7 @@ const ContactUsInitialForm = () => {
noEarlierThan
(
submitPromise
,
1000
)
.
then
(()
=>
{
clearSavedForm
();
setSubmissionState
(
LoadingState
.
SUCCESS
);
})
.
catch
(()
=>
{
...
...
src/ensembl/src/shared/contexts/IndexedDBContext.tsx
0 → 100644
View file @
ed8cb5cf
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import
React
,
{
createContext
,
ReactNode
}
from
'
react
'
;
import
IndexedDB
from
'
src/services/indexeddb-service
'
;
const
IndexedDBContext
=
createContext
<
typeof
IndexedDB
>
(
IndexedDB
);
const
Provider
=
({
children
}:
{
children
:
ReactNode
})
=>
{
return
(
<
IndexedDBContext
.
Provider
value
=
{
IndexedDB
}
>
{
children
}
</
IndexedDBContext
.
Provider
>
);
};
export
{
Provider
};
export
default
IndexedDBContext
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment