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 it 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.",