diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index f8e7902bed..3417948e6b 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -555,6 +555,7 @@
"have_more_days_to_try": "",
"headers": "",
"help": "",
+ "help_articles_matching": "",
"help_improve_overleaf_fill_out_this_survey": "",
"help_improve_screen_reader_fill_out_this_survey": "",
"hide_configuration": "",
@@ -686,10 +687,12 @@
"joined_team": "",
"joining": "",
"justify": "",
+ "kb_suggestions_enquiry": "",
"keep_current_plan": "",
"keep_personal_projects_separate": "",
"keep_your_account_safe_add_another_email": "",
"keybindings": "",
+ "knowledge_base": "",
"labels_help_you_to_easily_reference_your_figures": "",
"labels_help_you_to_reference_your_tables": "",
"language_feedback": "",
@@ -976,6 +979,8 @@
"please_enter_confirmation_code": "",
"please_get_in_touch": "",
"please_link_before_making_primary": "",
+ "please_provide_a_message": "",
+ "please_provide_a_subject": "",
"please_reconfirm_institutional_email": "",
"please_reconfirm_your_affiliation_before_making_this_primary": "",
"please_refresh": "",
diff --git a/services/web/frontend/js/features/algolia-search/search-wiki.js b/services/web/frontend/js/features/algolia-search/search-wiki.js
index 63b54a05cd..9e2a50a42d 100644
--- a/services/web/frontend/js/features/algolia-search/search-wiki.js
+++ b/services/web/frontend/js/features/algolia-search/search-wiki.js
@@ -24,10 +24,12 @@ export function formatWikiHit(hit) {
const pagePath = hit.kb ? 'how-to' : 'latex'
let pageAnchor = ''
- let pageName = hit._highlightResult.pageName.value
- if (hit.sectionName) {
- pageAnchor = `#${hit.sectionName.replace(/\s/g, '_')}`
- pageName += ' - ' + hit.sectionName
+ const rawPageName = hit._highlightResult.pageName.value
+ const sectionName = hit.sectionName
+ let pageName = rawPageName
+ if (sectionName) {
+ pageAnchor = `#${sectionName.replace(/\s/g, '_')}`
+ pageName += ' - ' + sectionName
}
const body = hit._highlightResult.content.value
@@ -37,5 +39,5 @@ export function formatWikiHit(hit) {
.join('\n...\n')
const url = `/learn/${pagePath}/${pageSlug}${pageAnchor}`
- return { url, pageName, content }
+ return { url, pageName, rawPageName, sectionName, content }
}
diff --git a/services/web/frontend/js/features/contact-form/search.js b/services/web/frontend/js/features/contact-form/search.js
index c41174576e..dddba6781f 100644
--- a/services/web/frontend/js/features/contact-form/search.js
+++ b/services/web/frontend/js/features/contact-form/search.js
@@ -48,7 +48,7 @@ export function setupSearch(formEl) {
const iconEl = document.createElement('i')
iconEl.className = 'fa fa-angle-right'
iconEl.setAttribute('aria-hidden', 'true')
- linkEl.append(contentEl)
+ linkEl.append(iconEl)
resultsEl.append(liEl)
}
diff --git a/services/web/frontend/js/features/settings/components/account-info-section.tsx b/services/web/frontend/js/features/settings/components/account-info-section.tsx
index 3367d8b75c..51c4dc7a58 100644
--- a/services/web/frontend/js/features/settings/components/account-info-section.tsx
+++ b/services/web/frontend/js/features/settings/components/account-info-section.tsx
@@ -118,18 +118,20 @@ function AccountInfoSection() {
) : null}
{canUpdateEmail || canUpdateNames ? (
-
- {t('update')}
-
+
+
+ {t('update')}
+
+
) : null}
>
@@ -188,7 +190,9 @@ function ReadOrWriteFormGroup({
onChange={handleChangeAndValidity}
onInvalid={handleInvalid}
/>
- {validationMessage && {validationMessage}}
+ {validationMessage && (
+ {validationMessage}
+ )}
)
}
diff --git a/services/web/frontend/js/features/settings/components/password-section.tsx b/services/web/frontend/js/features/settings/components/password-section.tsx
index 33480e1357..1b442a586f 100644
--- a/services/web/frontend/js/features/settings/components/password-section.tsx
+++ b/services/web/frontend/js/features/settings/components/password-section.tsx
@@ -196,18 +196,20 @@ function PasswordForm() {
/>
) : null}
-
- {t('change')}
-
+
+
+ {t('change')}
+
+
)
}
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx
index d9df962cae..18396d0998 100644
--- a/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx
@@ -91,9 +91,13 @@ export function DropdownToggle({ ...props }: DropdownToggleProps) {
return
}
-export function DropdownMenu({ as = 'ul', ...props }: DropdownMenuProps) {
- return
-}
+export const DropdownMenu = forwardRef<
+ typeof BS5DropdownMenu,
+ DropdownMenuProps
+>(({ as = 'ul', ...props }, ref) => {
+ return
+})
+DropdownMenu.displayName = 'DropdownMenu'
export function DropdownDivider({ as = 'li' }: DropdownDividerProps) {
return
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-control.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-control.tsx
index 2160dbebf9..6926af710a 100644
--- a/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-control.tsx
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-control.tsx
@@ -36,7 +36,7 @@ const FormControl = forwardRef(
)
}
- return
+ return
}
)
FormControl.displayName = 'FormControl'
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-feedback.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-feedback.tsx
new file mode 100644
index 0000000000..85c91031ba
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-feedback.tsx
@@ -0,0 +1,20 @@
+import { Form } from 'react-bootstrap-5'
+import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
+import { ComponentProps } from 'react'
+
+export type FormFeedbackProps = Pick<
+ ComponentProps,
+ 'type' | 'className' | 'children'
+>
+
+function FormFeedback(props: FormFeedbackProps) {
+ return (
+
+
+ {props.children}
+
+
+ )
+}
+
+export default FormFeedback
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-text.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-text.tsx
index 8b804bfd04..0178cfcb10 100644
--- a/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-text.tsx
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-text.tsx
@@ -1,71 +1,49 @@
import { Form } from 'react-bootstrap-5'
-import { MergeAndOverride } from '../../../../../../../types/utils'
import MaterialIcon from '@/shared/components/material-icon'
import classnames from 'classnames'
-type FormTextProps = MergeAndOverride<
- React.ComponentProps<(typeof Form)['Text']>,
- | {
- isInfo?: boolean
- isError?: never
- isWarning?: never
- isSuccess?: never
- }
- | {
- isInfo?: never
- isError?: boolean
- isWarning?: never
- isSuccess?: never
- }
- | {
- isInfo?: never
- isError?: never
- isWarning?: boolean
- isSuccess?: never
- }
- | {
- isInfo?: never
- isError?: never
- isWarning?: never
- isSuccess?: boolean
- }
->
+type TextType = 'default' | 'info' | 'success' | 'warning' | 'error'
-export const getFormTextColor = ({
- isError,
- isSuccess,
- isWarning,
-}: {
- isError?: boolean
- isSuccess?: boolean
- isWarning?: boolean
-}) => ({
- 'text-danger': isError,
- 'text-success': isSuccess,
- 'text-warning': isWarning,
-})
+export type FormTextProps = React.ComponentProps & {
+ type?: TextType
+}
+
+const typeClassMap: Partial> = {
+ error: 'text-danger',
+ success: 'text-success',
+ warning: 'text-warning',
+}
+
+export const getFormTextClass = (type?: TextType) =>
+ typeClassMap[type || 'default']
+
+function FormTextIcon({ type }: { type?: TextType }) {
+ switch (type) {
+ case 'info':
+ return
+ case 'success':
+ return
+ case 'warning':
+ return
+ case 'error':
+ return
+ default:
+ return null
+ }
+}
function FormText({
- isInfo,
- isError,
- isWarning,
- isSuccess,
+ type = 'default',
children,
className,
...rest
}: FormTextProps) {
return (
- {isInfo && }
- {isError && }
- {isWarning && }
- {isSuccess && }
+
{children}
)
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx
index 90ef65eff8..971e043782 100644
--- a/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx
+++ b/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx
@@ -11,7 +11,7 @@ type OLFormCheckboxProps = React.ComponentProps<(typeof Form)['Check']> & {
function OLFormCheckbox(props: OLFormCheckboxProps) {
const { bs3Props, inputRef, ...rest } = props
- const bs3FormLabelProps: React.ComponentProps = {
+ const bs3FormCheckboxProps: React.ComponentProps = {
children: rest.label,
checked: rest.checked,
required: rest.required,
@@ -34,9 +34,9 @@ function OLFormCheckbox(props: OLFormCheckboxProps) {
+
) : (
-
+
)
}
bs5={}
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx
index 7b0126b497..c145f3bde1 100644
--- a/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx
+++ b/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx
@@ -1,25 +1,29 @@
-import { forwardRef } from 'react'
+import { forwardRef, ComponentProps } from 'react'
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
-import BS3FormControl from '@/features/ui/components/bootstrap-3/form/form-control'
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
+import BS3FormControl from '@/features/ui/components/bootstrap-3/form/form-control'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-type OLFormControlProps = React.ComponentProps & {
+type OLFormControlProps = ComponentProps & {
bs3Props?: Record
'data-ol-dirty'?: unknown
}
+type BS3FormControlProps = ComponentProps
+
const OLFormControl = forwardRef(
(props, ref) => {
const { bs3Props, ...rest } = props
- let bs3FormControlProps: React.ComponentProps = {
+ let bs3FormControlProps: BS3FormControlProps = {
+ componentClass: rest.as,
id: rest.id,
name: rest.name,
className: rest.className,
style: rest.style,
type: rest.type,
value: rest.value,
+ defaultValue: rest.defaultValue,
required: rest.required,
disabled: rest.disabled,
placeholder: rest.placeholder,
@@ -28,10 +32,10 @@ const OLFormControl = forwardRef(
autoFocus: rest.autoFocus,
minLength: rest.minLength,
maxLength: rest.maxLength,
- onChange: rest.onChange as (e: React.ChangeEvent) => void,
- onKeyDown: rest.onKeyDown as (e: React.KeyboardEvent) => void,
- onFocus: rest.onFocus as (e: React.FocusEvent) => void,
- onInvalid: rest.onInvalid as (e: React.InvalidEvent) => void,
+ onChange: rest.onChange as BS3FormControlProps['onChange'],
+ onKeyDown: rest.onKeyDown as BS3FormControlProps['onKeyDown'],
+ onFocus: rest.onFocus as BS3FormControlProps['onFocus'],
+ onInvalid: rest.onInvalid as BS3FormControlProps['onInvalid'],
inputRef: (inputElement: HTMLInputElement) => {
if (typeof ref === 'function') {
ref(inputElement)
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-feedback.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-feedback.tsx
new file mode 100644
index 0000000000..2ff193e0d8
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/ol/ol-form-feedback.tsx
@@ -0,0 +1,38 @@
+import { Form } from 'react-bootstrap-5'
+import {
+ HelpBlock as BS3HelpBlock,
+ HelpBlockProps as BS3HelpBlockProps,
+} from 'react-bootstrap'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { ComponentProps } from 'react'
+import classnames from 'classnames'
+import FormFeedback from '@/features/ui/components/bootstrap-5/form/form-feedback'
+
+type OLFormFeedbackProps = Pick<
+ ComponentProps,
+ 'type' | 'className' | 'children'
+> & {
+ bs3Props?: Record
+}
+
+function OLFormFeedback(props: OLFormFeedbackProps) {
+ const { bs3Props, children, ...bs5Props } = props
+
+ const bs3HelpBlockProps: BS3HelpBlockProps = {
+ className: classnames(
+ bs5Props.className,
+ bs5Props.type === 'invalid' ? 'invalid-only' : null
+ ),
+ children,
+ ...bs3Props,
+ }
+
+ return (
+ }
+ bs5={{children}}
+ />
+ )
+}
+
+export default OLFormFeedback
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx
index 5517d5d1ed..48d4c5f5da 100644
--- a/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx
+++ b/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx
@@ -1,5 +1,8 @@
import { FormGroupProps } from 'react-bootstrap-5'
-import { FormGroup as BS3FormGroup } from 'react-bootstrap'
+import {
+ FormGroup as BS3FormGroup,
+ FormGroupProps as BS3FormGroupProps,
+} from 'react-bootstrap'
import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
@@ -8,19 +11,19 @@ type OLFormGroupProps = FormGroupProps & {
}
function OLFormGroup(props: OLFormGroupProps) {
- const { bs3Props, ...rest } = props
+ const { bs3Props, className, ...rest } = props
- const bs3FormGroupProps: React.ComponentProps = {
+ const bs3FormGroupProps: BS3FormGroupProps = {
children: rest.children,
controlId: rest.controlId,
- className: rest.className,
+ className,
...bs3Props,
}
return (
}
- bs5={}
+ bs5={}
/>
)
}
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-select.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-select.tsx
new file mode 100644
index 0000000000..278b9352d1
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/ol/ol-form-select.tsx
@@ -0,0 +1,45 @@
+import { Form, FormSelectProps } from 'react-bootstrap-5'
+import {
+ FormControl as BS3FormControl,
+ FormControlProps as BS3FormControlProps,
+} from 'react-bootstrap'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
+
+type OLFormSelectProps = FormSelectProps & {
+ bs3Props?: Record
+}
+
+function OLFormSelect(props: OLFormSelectProps) {
+ const { bs3Props, ...bs5Props } = props
+
+ const bs3FormSelectProps: BS3FormControlProps = {
+ bsSize: bs5Props.size,
+ name: bs5Props.name,
+ value: bs5Props.value,
+ disabled: bs5Props.disabled,
+ onChange: bs5Props.onChange as BS3FormControlProps['onChange'],
+ required: bs5Props.required,
+ placeholder: bs5Props.placeholder,
+ className: bs5Props.className,
+ ...bs3Props,
+ }
+
+ // Get all `aria-*` and `data-*` attributes
+ const extraProps = getAriaAndDataProps(bs5Props)
+
+ return (
+
+ }
+ bs5={}
+ />
+ )
+}
+
+export default OLFormSelect
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-text.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-text.tsx
index ab09e31b4b..e84be3423c 100644
--- a/services/web/frontend/js/features/ui/components/ol/ol-form-text.tsx
+++ b/services/web/frontend/js/features/ui/components/ol/ol-form-text.tsx
@@ -1,11 +1,12 @@
import FormText, {
- getFormTextColor,
+ FormTextProps,
+ getFormTextClass,
} from '@/features/ui/components/bootstrap-5/form/form-text'
import PolymorphicComponent from '@/shared/components/polymorphic-component'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import classnames from 'classnames'
-type OLFormTextProps = React.ComponentProps & {
+type OLFormTextProps = FormTextProps & {
bs3Props?: Record
}
@@ -14,15 +15,7 @@ function OLFormText(props: OLFormTextProps) {
const bs3HelpBlockProps = {
children: rest.children,
- className: classnames(
- 'small',
- rest.className,
- getFormTextColor({
- isError: rest.isError,
- isSuccess: rest.isSuccess,
- isWarning: rest.isWarning,
- })
- ),
+ className: classnames('small', rest.className, getFormTextClass(rest.type)),
as: 'span',
...bs3Props,
} as const satisfies React.ComponentProps
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form.tsx
index 566d1ba64d..32fd5073ea 100644
--- a/services/web/frontend/js/features/ui/components/ol/ol-form.tsx
+++ b/services/web/frontend/js/features/ui/components/ol/ol-form.tsx
@@ -1,27 +1,42 @@
import { Form } from 'react-bootstrap-5'
-import { Form as BS3Form } from 'react-bootstrap'
+import { Form as BS3Form, FormProps as BS3FormProps } from 'react-bootstrap'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { ComponentProps } from 'react'
+import classnames from 'classnames'
+import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
-type OLFormProps = React.ComponentProps & {
- bs3Props?: React.ComponentProps
+type OLFormProps = ComponentProps & {
+ bs3Props?: ComponentProps
}
function OLForm(props: OLFormProps) {
const { bs3Props, ...rest } = props
- const bs3FormProps: React.ComponentProps = {
+ const bs3FormProps: BS3FormProps = {
componentClass: rest.as,
children: rest.children,
id: rest.id,
- onSubmit: rest.onSubmit as React.FormEventHandler | undefined,
- className: rest.className,
+ onSubmit: rest.onSubmit as BS3FormProps['onSubmit'],
+ onClick: rest.onClick as BS3FormProps['onClick'],
+ name: rest.name,
+ noValidate: rest.noValidate,
role: rest.role,
...bs3Props,
}
+ const bs3ClassName = classnames(
+ rest.className,
+ rest.validated ? 'was-validated' : null
+ )
+
+ // Get all `aria-*` and `data-*` attributes
+ const extraProps = getAriaAndDataProps(rest)
+
return (
}
+ bs3={
+
+ }
bs5={}
/>
)
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx b/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx
index 987ab4ac8b..39a79ef1c8 100644
--- a/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx
+++ b/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx
@@ -102,7 +102,7 @@ export function OLModalBody({ children, ...props }: OLModalBodyProps) {
const bs3ModalProps: BS3ModalBodyProps = {
componentClass: bs5Props.as,
- bsClass: bs5Props.className,
+ className: bs5Props.className,
}
return (
@@ -118,7 +118,7 @@ export function OLModalFooter({ children, ...props }: OLModalFooterProps) {
const bs3ModalProps: BS3ModalFooterProps = {
componentClass: bs5Props.as,
- bsClass: bs5Props.className,
+ className: bs5Props.className,
}
return (
diff --git a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts
index d4ef039814..c952b0dd9f 100644
--- a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts
+++ b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts
@@ -21,7 +21,7 @@ export type DropdownProps = {
export type DropdownItemProps = PropsWithChildren<{
active?: boolean
as?: ElementType
- description?: string
+ description?: ReactNode
disabled?: boolean
eventKey?: string | number
href?: string
@@ -32,6 +32,7 @@ export type DropdownItemProps = PropsWithChildren<{
className?: string
role?: string
tabIndex?: number
+ target?: string
}>
export type DropdownToggleProps = PropsWithChildren<{
diff --git a/services/web/frontend/js/shared/hooks/use-contact-us-modal.tsx b/services/web/frontend/js/shared/hooks/use-contact-us-modal.tsx
index c4bc80b83a..9d1c647dd3 100644
--- a/services/web/frontend/js/shared/hooks/use-contact-us-modal.tsx
+++ b/services/web/frontend/js/shared/hooks/use-contact-us-modal.tsx
@@ -1,13 +1,15 @@
import importOverleafModules from '../../../macros/import-overleaf-module.macro'
import { JSXElementConstructor, useCallback, useState } from 'react'
+import { HelpSuggestionSearchProvider } from '../../../../modules/support/frontend/js/context/help-suggestion-search-context'
const [contactUsModalModules] = importOverleafModules('contactUsModal')
const ContactUsModal: JSXElementConstructor<{
show: boolean
handleHide: () => void
+ autofillProjectUrl: boolean
}> = contactUsModalModules?.import.default
-export const useContactUsModal = () => {
+export const useContactUsModal = (options = { autofillProjectUrl: true }) => {
const [show, setShow] = useState(false)
const hideModal = useCallback((event?: Event) => {
@@ -21,7 +23,13 @@ export const useContactUsModal = () => {
}, [])
const modal = ContactUsModal && (
-
+
+
+
)
return { modal, hideModal, showModal }
diff --git a/services/web/frontend/stories/contact-us-modal.stories.jsx b/services/web/frontend/stories/contact-us-modal.stories.jsx
deleted file mode 100644
index cb06c90e78..0000000000
--- a/services/web/frontend/stories/contact-us-modal.stories.jsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useState } from 'react'
-import useFetchMock from './hooks/use-fetch-mock'
-import ContactUsModal from '../../modules/support/frontend/js/components/contact-us-modal'
-import { ScopeDecorator } from './decorators/scope'
-
-export const Generic = () => {
- const [show, setShow] = useState(true)
-
- const handleHide = () => setShow(false)
-
- useFetchMock(fetchMock => {
- fetchMock.post('/support', { status: 200 }, { delay: 1000 })
- })
-
- return
-}
-
-export const RequestError = args => {
- useFetchMock(fetchMock => {
- fetchMock.post('/support', { status: 404 }, { delay: 250 })
- })
-
- return
-}
-
-export default {
- title: 'Shared / Modals / Contact Us',
- component: ContactUsModal,
- args: {
- show: true,
- handleHide: () => {},
- },
- argTypes: {
- handleHide: { action: 'close modal' },
- },
- decorators: [ScopeDecorator],
-}
diff --git a/services/web/frontend/stories/contact-us-modal.stories.tsx b/services/web/frontend/stories/contact-us-modal.stories.tsx
new file mode 100644
index 0000000000..156a3b4af8
--- /dev/null
+++ b/services/web/frontend/stories/contact-us-modal.stories.tsx
@@ -0,0 +1,93 @@
+import { ComponentProps } from 'react'
+import useFetchMock from './hooks/use-fetch-mock'
+import ContactUsModal from '../../modules/support/frontend/js/components/contact-us-modal'
+import { ScopeDecorator } from './decorators/scope'
+import { StoryObj } from '@storybook/react'
+import { FixedHelpSuggestionSearchProvider } from '../../modules/support/test/frontend/helpers/contact-us-modal-base-tests'
+
+type Story = StoryObj
+type ContactUsModalProps = ComponentProps
+
+function bootstrap3Story(render: Story['render']): Story {
+ return {
+ render,
+ decorators: [
+ story => {
+ return ScopeDecorator(story)
+ },
+ ],
+ }
+}
+
+function bootstrap5Story(render: Story['render']): Story {
+ return {
+ render,
+ decorators: [
+ story => {
+ return ScopeDecorator(story, undefined, {
+ 'ol-bootstrapVersion': 5,
+ })
+ },
+ ],
+ parameters: {
+ bootstrap5: true,
+ },
+ }
+}
+
+function GenericContactUsModal(args: ContactUsModalProps) {
+ useFetchMock(fetchMock => {
+ fetchMock.post('/support', { status: 200 }, { delay: 1000 })
+ })
+
+ return (
+
+
+
+ )
+}
+
+export const Generic: Story = bootstrap3Story(args => (
+
+))
+
+export const GenericBootstrap5: Story = bootstrap5Story(args => (
+
+))
+
+const ContactUsModalWithRequestError = (args: ContactUsModalProps) => {
+ useFetchMock(fetchMock => {
+ fetchMock.post('/support', { status: 404 }, { delay: 250 })
+ })
+
+ return (
+
+
+
+ )
+}
+
+const renderContactUsModalWithRequestError = (args: ContactUsModalProps) => (
+
+)
+
+export const RequestError: Story = bootstrap3Story(
+ renderContactUsModalWithRequestError
+)
+
+export const RequestErrorBootstrap5: Story = bootstrap5Story(
+ renderContactUsModalWithRequestError
+)
+
+export default {
+ title: 'Shared / Modals / Contact Us',
+ component: ContactUsModal,
+ args: {
+ show: true,
+ handleHide: () => {},
+ autofillProjectUrl: true,
+ },
+ argTypes: {
+ handleHide: { action: 'close modal' },
+ },
+}
diff --git a/services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx b/services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx
index e37e031f63..b0c4ef8dea 100644
--- a/services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx
+++ b/services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx
@@ -4,6 +4,7 @@ import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group'
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
import MaterialIcon from '@/shared/components/material-icon'
+import FormFeedback from '@/features/ui/components/bootstrap-5/form/form-feedback'
const meta: Meta> = {
title: 'Shared / Components / Bootstrap 5 / Form / Input',
@@ -57,7 +58,7 @@ export const Info: Story = {
size="lg"
{...args}
/>
- Info
+ Info
@@ -67,7 +68,7 @@ export const Info: Story = {
defaultValue="Regular input"
{...args}
/>
- Info
+ Info
@@ -78,7 +79,7 @@ export const Info: Story = {
size="sm"
{...args}
/>
- Info
+ Info
>
)
@@ -98,7 +99,7 @@ export const Error: Story = {
isInvalid
{...args}
/>
- Error
+ Error
@@ -109,7 +110,7 @@ export const Error: Story = {
isInvalid
{...args}
/>
- Error
+ Error
@@ -121,7 +122,7 @@ export const Error: Story = {
isInvalid
{...args}
/>
- Error
+ Error
>
)
@@ -140,7 +141,7 @@ export const Warning: Story = {
size="lg"
{...args}
/>
- Warning
+ Warning
@@ -150,7 +151,7 @@ export const Warning: Story = {
defaultValue="Regular input"
{...args}
/>
- Warning
+ Warning
@@ -161,7 +162,7 @@ export const Warning: Story = {
size="sm"
{...args}
/>
- Warning
+ Warning
>
)
@@ -180,7 +181,7 @@ export const Success: Story = {
size="lg"
{...args}
/>
- Success
+ Success
@@ -190,7 +191,7 @@ export const Success: Story = {
defaultValue="Regular input"
{...args}
/>
- Success
+ Success
@@ -201,7 +202,7 @@ export const Success: Story = {
size="sm"
{...args}
/>
- Success
+ Success
>
)
diff --git a/services/web/frontend/stories/ui/form/form-select-bs5.stories.tsx b/services/web/frontend/stories/ui/form/form-select-bs5.stories.tsx
index 148ea9597f..9f9985a4b6 100644
--- a/services/web/frontend/stories/ui/form/form-select-bs5.stories.tsx
+++ b/services/web/frontend/stories/ui/form/form-select-bs5.stories.tsx
@@ -70,7 +70,7 @@ export const Info: Story = {
- Info
+ Info
@@ -81,7 +81,7 @@ export const Info: Story = {
- Info
+ Info
@@ -92,7 +92,7 @@ export const Info: Story = {
- Info
+ Info
>
)
@@ -111,7 +111,7 @@ export const Error: Story = {
- Error
+ Error
@@ -122,7 +122,7 @@ export const Error: Story = {
- Error
+ Error
@@ -133,7 +133,7 @@ export const Error: Story = {
- Error
+ Error
>
)
@@ -152,7 +152,7 @@ export const Warning: Story = {
- Warning
+ Warning
@@ -163,7 +163,7 @@ export const Warning: Story = {
- Warning
+ Warning
@@ -174,7 +174,7 @@ export const Warning: Story = {
- Warning
+ Warning
>
)
@@ -193,7 +193,7 @@ export const Success: Story = {
- Success
+ Success
@@ -204,7 +204,7 @@ export const Success: Story = {
- Success
+ Success
@@ -215,7 +215,7 @@ export const Success: Story = {
- Success
+ Success
>
)
diff --git a/services/web/frontend/stories/ui/form/form-textarea-bs5.stories.tsx b/services/web/frontend/stories/ui/form/form-textarea-bs5.stories.tsx
index df72bc7ffa..cd95984175 100644
--- a/services/web/frontend/stories/ui/form/form-textarea-bs5.stories.tsx
+++ b/services/web/frontend/stories/ui/form/form-textarea-bs5.stories.tsx
@@ -67,7 +67,7 @@ export const Info: Story = {
size="lg"
{...args}
/>
- Info
+ Info
@@ -78,7 +78,7 @@ export const Info: Story = {
defaultValue="Regular input"
{...args}
/>
- Info
+ Info
@@ -90,7 +90,7 @@ export const Info: Story = {
size="sm"
{...args}
/>
- Info
+ Info
>
)
@@ -111,7 +111,7 @@ export const Error: Story = {
isInvalid
{...args}
/>
- Error
+ Error
@@ -123,7 +123,7 @@ export const Error: Story = {
isInvalid
{...args}
/>
- Error
+ Error
@@ -136,7 +136,7 @@ export const Error: Story = {
isInvalid
{...args}
/>
- Error
+ Error
>
)
@@ -156,7 +156,7 @@ export const Warning: Story = {
size="lg"
{...args}
/>
- Warning
+ Warning
@@ -167,7 +167,7 @@ export const Warning: Story = {
defaultValue="Regular input"
{...args}
/>
- Warning
+ Warning
@@ -179,7 +179,7 @@ export const Warning: Story = {
size="sm"
{...args}
/>
- Warning
+ Warning
>
)
@@ -199,7 +199,7 @@ export const Success: Story = {
size="lg"
{...args}
/>
- Success
+ Success
@@ -210,7 +210,7 @@ export const Success: Story = {
defaultValue="Regular input"
{...args}
/>
- Success
+ Success
@@ -222,7 +222,7 @@ export const Success: Story = {
size="sm"
{...args}
/>
- Success
+ Success
>
)
diff --git a/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss b/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss
index 5992b154a6..1a1ba3c8aa 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss
@@ -120,6 +120,7 @@ $form-check-input-disabled-opacity: 1;
// form-feedback-variables
$form-feedback-invalid-color: $bg-danger-01;
$form-feedback-icon-invalid: null;
+$form-feedback-margin-top: 0; // Our feedback component wraps Form.Text, which takes care of top margin
// form-validation-colors
$form-invalid-color: $form-feedback-invalid-color;
diff --git a/services/web/frontend/stylesheets/bootstrap-5/abstracts/variables.scss b/services/web/frontend/stylesheets/bootstrap-5/abstracts/variables.scss
index d2712b14ed..62f20c9be8 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/abstracts/variables.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/abstracts/variables.scss
@@ -3,3 +3,6 @@ $footer-height: 50px;
// Header
$header-height: 68px;
+
+// Forms
+$form-group-margin-bottom: $spacing-06;
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/form.scss b/services/web/frontend/stylesheets/bootstrap-5/components/form.scss
index ca39955a5b..6a7a4d584b 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/form.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/form.scss
@@ -90,7 +90,7 @@
}
.form-group {
- margin-bottom: var(--spacing-06);
+ margin-bottom: $form-group-margin-bottom;
}
.form-control-wrapper {
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/modal.scss b/services/web/frontend/stylesheets/bootstrap-5/components/modal.scss
index d57a9fe5e1..6689bdd0bb 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/modal.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/modal.scss
@@ -85,12 +85,3 @@
.git-bridge-optional-tokens-actions {
margin-top: var(--spacing-05);
}
-
-// Contact us modal
-.contact-us-modal-textarea {
- height: 120px;
-}
-
-.modal-form-messages .notification {
- margin-bottom: var(--spacing-05);
-}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/main-style.scss b/services/web/frontend/stylesheets/bootstrap-5/main-style.scss
index 7f9855ca96..5f4635ab9c 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/main-style.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/main-style.scss
@@ -32,3 +32,9 @@ $is-overleaf-light: false;
// Page layout that isn't related to a particular component or page
@import 'base/layout';
+
+// Modals
+@import 'modals/all';
+
+// Pages custom style
+@import 'pages/all';
diff --git a/services/web/frontend/stylesheets/bootstrap-5/modals/all.scss b/services/web/frontend/stylesheets/bootstrap-5/modals/all.scss
new file mode 100644
index 0000000000..b32e9afb40
--- /dev/null
+++ b/services/web/frontend/stylesheets/bootstrap-5/modals/all.scss
@@ -0,0 +1 @@
+@import 'contact-us-modal';
diff --git a/services/web/frontend/stylesheets/bootstrap-5/modals/contact-us-modal.scss b/services/web/frontend/stylesheets/bootstrap-5/modals/contact-us-modal.scss
new file mode 100644
index 0000000000..57711a799d
--- /dev/null
+++ b/services/web/frontend/stylesheets/bootstrap-5/modals/contact-us-modal.scss
@@ -0,0 +1,82 @@
+.contact-us-modal-textarea {
+ height: 120px;
+}
+
+.modal-form-messages .notification {
+ margin-bottom: var(--spacing-05);
+}
+
+.contact-suggestions {
+ @include body-sm;
+
+ margin: 0 calc(-1 * var(--bs-modal-padding)) var(--spacing-05);
+ padding: var(--spacing-05) 0;
+ color: var(--content-secondary);
+ background-color: var(--bg-light-secondary);
+ border-top: solid 1px var(--border-primary-dark);
+ border-bottom: solid 1px var(--border-primary-dark);
+}
+
+.contact-suggestion-label {
+ margin-bottom: var(--spacing-05);
+ padding: 0 var(--spacing-07);
+}
+
+.contact-suggestion-list {
+ padding-left: 0;
+ list-style: none;
+ background-color: var(--white);
+ border-top: solid 1px var(--border-primary-dark);
+ border-bottom: solid 1px var(--border-primary-dark);
+ margin: 0;
+
+ li:last-child .contact-suggestion-list-item {
+ border-bottom: none;
+ }
+}
+
+.contact-suggestion-list-item {
+ display: flex;
+ justify-content: space-between;
+ color: var(--content-secondary);
+ padding: var(--spacing-05) var(--spacing-07);
+ border-bottom: solid 1px var(--border-divider);
+ cursor: pointer;
+ text-decoration: none;
+
+ &:hover,
+ &:focus {
+ background-color: var(--bg-light-secondary);
+
+ span {
+ text-decoration: underline;
+ }
+
+ .fa {
+ color: inherit;
+ text-decoration: none;
+ }
+ }
+
+ .fa {
+ color: var(--neutral-30);
+ }
+}
+
+.contact-suggestions-dropdown {
+ width: calc(100% - 2 * var(--bs-modal-padding));
+
+ .dropdown-header {
+ @include body-sm;
+
+ padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);
+ }
+
+ .dropdown-item {
+ white-space: normal;
+ }
+
+ .form-group + & {
+ margin-top: $spacing-02 - $form-group-margin-bottom;
+ }
+}
diff --git a/services/web/frontend/stylesheets/components/forms.less b/services/web/frontend/stylesheets/components/forms.less
index 220aa35be8..830bf92f3c 100755
--- a/services/web/frontend/stylesheets/components/forms.less
+++ b/services/web/frontend/stylesheets/components/forms.less
@@ -357,6 +357,15 @@ input[type='checkbox'],
margin-top: 5px;
margin-bottom: 10px;
color: lighten(@text-color, 25%); // lighten the text some for contrast
+
+ // Hide help blocks used as validation messages by default
+ &.invalid-only {
+ display: none;
+
+ .has-error & {
+ display: block;
+ }
+ }
}
// Inline forms
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 62f57fda9f..f880d61cb2 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1455,6 +1455,8 @@
"please_enter_email": "Please enter your email address",
"please_get_in_touch": "Please get in touch",
"please_link_before_making_primary": "Please confirm your email by linking to your institutional account before making it the primary email.",
+ "please_provide_a_message": "Please provide a message",
+ "please_provide_a_subject": "Please provide a subject",
"please_reconfirm_institutional_email": "Please take a moment to confirm your institutional email address or <0>remove it0> from your account.",
"please_reconfirm_your_affiliation_before_making_this_primary": "Please confirm your affiliation before making this the primary.",
"please_refresh": "Please refresh the page to continue.",