Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
"react-bem-helper": "^1.4.1",
"react-dom": "^17.0.2",
"react-helmet": "^5.2.1",
"react-hook-form": "^7.22.3",
"react-i18next": "11.11.4",
"react-image-crop": "^6.0.18",
"react-query": "^3.25.1",
Expand Down
15 changes: 3 additions & 12 deletions src/components/Contributors/Contributors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,16 @@ import { FieldHeader } from '@ndla/forms';
import { useTranslation } from 'react-i18next';
import Contributor from './Contributor';
import { ContributorType, ContributorFieldName } from './types';
import { ContributorType as ContributorTypeName } from '../../interfaces';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Foreslår å endre navn på typene for å unngå to exports med samme navn.


const StyledFormWarningText = styled.p`
font-family: ${fonts.sans};
color: ${colors.support.red};
${fonts.sizes(14, 1.1)};
`;

enum ContributorGroups {
CREATORS = 'creators',
PROCESSORS = 'processors',
RIGHTSHOLDERS = 'rightsholders',
}

interface Props {
name: ContributorGroups;
name: ContributorTypeName;
label: string;
onChange: (event: { target: { value: ContributorType[]; name: string } }) => void;
errorMessages?: string[];
Expand Down Expand Up @@ -123,11 +118,7 @@ const Contributors = ({
};

Contributors.propTypes = {
name: PropTypes.oneOf<ContributorGroups>([
ContributorGroups.CREATORS,
ContributorGroups.PROCESSORS,
ContributorGroups.RIGHTSHOLDERS,
]).isRequired,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
errorMessages: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
Expand Down
10 changes: 2 additions & 8 deletions src/components/ControlledImageSearchAndUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import {
ImageSearchQuery,
UpdatedImageMetadata,
} from '../modules/image/imageApiInterfaces';
import EditorErrorMessage from './SlateEditor/EditorErrorMessage';
import { useLicenses } from '../modules/draft/draftQueries';

const StyledTitleDiv = styled.div`
margin-bottom: ${spacing.small};
Expand All @@ -34,7 +32,7 @@ interface Props {
searchImages: (queryObject: ImageSearchQuery) => void;
fetchImage: (id: number) => Promise<ImageApiType>;
image?: ImageApiType;
updateImage: (imageMetadata: UpdatedImageMetadata, image: string | Blob) => void;
updateImage: (imageMetadata: UpdatedImageMetadata, image: string | Blob) => Promise<ImageApiType>;
inModal?: boolean;
showCheckbox?: boolean;
checkboxAction?: (image: ImageApiType) => void;
Expand All @@ -55,7 +53,6 @@ const ImageSearchAndUploader = ({
}: Props) => {
const { t } = useTranslation();
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const { data: licenses } = useLicenses({ placeholderData: [] });
const searchImagesWithParameters = (query: string, page: number) => {
return searchImages({ query, page, 'page-size': 16 });
};
Expand Down Expand Up @@ -98,17 +95,14 @@ const ImageSearchAndUploader = ({
},
{
title: t('form.visualElement.imageUpload'),
content: licenses ? (
content: (
<ImageForm
language={locale}
inModal={inModal}
image={image}
onUpdate={updateImage}
closeModal={closeModal}
licenses={licenses}
/>
) : (
<EditorErrorMessage msg={t('errorMessage.description')} />
),
},
]}
Expand Down
56 changes: 56 additions & 0 deletions src/components/Form/FormEventProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2021-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useState } from 'react';

type EventType = 'reset';

interface FormEvent {
id: number;
type: EventType;
}
interface EventState {
events: FormEvent[];
}
const FormEventContext = createContext<
[EventState, Dispatch<SetStateAction<EventState>>] | undefined
>(undefined);

interface Props {
children: ReactNode;
}

export interface FormEventProps {
events: FormEvent[];
dispatchEvent: (type: EventType) => void;
}

export const FormEventProvider = ({ children }: Props) => {
const initialValues: EventState = { events: [] };
const eventContext = useState<EventState>(initialValues);
return <FormEventContext.Provider value={eventContext}>{children}</FormEventContext.Provider>;
};

export const useFormEvents = (): FormEventProps => {
const eventContext = useContext(FormEventContext);
if (eventContext === undefined) {
throw new Error('useFormEvents must be used within the context of a FormEventProvider!');
}
const [state, setState] = eventContext;

const dispatch = (type: EventType) => {
const newEvent: FormEvent = { id: state.events.length, type };
const newEvents = state.events.concat([newEvent]);
setState(prev => ({ ...prev, events: newEvents }));
};

return {
events: state.events,
dispatchEvent: dispatch,
};
};
107 changes: 107 additions & 0 deletions src/components/Form/FormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Copyright (c) 2021-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import styled from '@emotion/styled';
import { ReactNode } from 'react';
import {
ControllerFieldState,
FieldValues,
Noop,
RefCallBack,
useController,
UseFormStateReturn,
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Node } from 'slate';
import { classes } from './';
import FormFieldDescription from './FormFieldDescription';
import FormFieldHelp from './FormFieldHelp';
import FormFieldLabel from './FormFieldLabel';
import FormRemainingCharacters from './FormRemainingCharacters';

const StyledErrorPreLine = styled.span`
white-space: pre-line;
`;

interface ControllerRenderProps<
TFieldValues extends FieldValues,
TName extends keyof TFieldValues & string
> {
onChange: (...event: any[]) => void;
onBlur: Noop;
value: TFieldValues[TName];
name: TName;
ref: RefCallBack;
}

interface Props<TFieldValues extends FieldValues, TName extends keyof TFieldValues & string> {
noBorder?: boolean;
right?: boolean;
title?: boolean;
name: TName;
label?: string;
showError?: boolean;
obligatory?: boolean;
description?: string;
maxLength?: number;
showMaxLength?: boolean;
className?: string;
children: (
props: ControllerRenderProps<TFieldValues, TName> &
ControllerFieldState &
UseFormStateReturn<TFieldValues>,
) => ReactNode;
placeholder?: string;
}

const FormField = <TFieldValues extends FieldValues, TName extends keyof TFieldValues & string>({
children,
className,
label,
name,
maxLength,
showMaxLength,
noBorder = false,
title = false,
right = false,
description,
obligatory,
showError = true,
...rest
}: Props<TFieldValues, TName>) => {
const { t } = useTranslation();

const { field, fieldState, formState } = useController<any, any>({
name: name,
});
const isSlateValue = Node.isNodeList(field.value);

return (
<div {...classes('', { 'no-border': noBorder, right, title }, className)}>
<FormFieldLabel label={label} name={name} noBorder={noBorder} />
<FormFieldDescription description={description} obligatory={obligatory} />
{children({ ...field, ...fieldState, ...formState })}
{showMaxLength && maxLength && (
<FormRemainingCharacters
maxLength={maxLength}
getRemainingLabel={(maxLength, remaining) =>
t('form.remainingCharacters', { maxLength, remaining })
}
value={isSlateValue ? Node.string(field.value[0]) : field.value}
/>
)}
{showError && !!fieldState.error && (
<FormFieldHelp error={!!fieldState.error.message}>
<StyledErrorPreLine>{fieldState.error.message}</StyledErrorPreLine>
</FormFieldHelp>
)}
</div>
);
};

export default FormField;
49 changes: 49 additions & 0 deletions src/components/Form/FormFieldDescription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2019-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { css } from '@emotion/core';

const StyledFormDescriptionBlock = styled.span`
display: flex;
`;

const obligatoryDescriptionStyle = css`
background-color: rgba(230, 132, 154, 1);
padding: 0.2em 0.6em;
`;

const StyledFormDescription = styled.p`
margin: 0.2em 0;
font-size: 0.75em;
${(p: Props) => (p.obligatory ? obligatoryDescriptionStyle : '')};
`;

interface Props {
description?: string;
obligatory?: boolean;
}

const FormFieldDescription = ({ description, obligatory }: Props) => {
if (!description) {
return null;
}
return (
<StyledFormDescriptionBlock>
<StyledFormDescription obligatory={obligatory}>{description}</StyledFormDescription>
</StyledFormDescriptionBlock>
);
};

FormFieldDescription.propTypes = {
obligatory: PropTypes.bool,
description: PropTypes.string,
};

export default FormFieldDescription;
38 changes: 38 additions & 0 deletions src/components/Form/FormFieldHelp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2019-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import { ReactNode } from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { colors, fonts } from '@ndla/core';

interface Props {
error?: boolean;
float?: 'left' | 'right' | 'none' | 'inherit';
children: ReactNode;
}

export const StyledHelpMessage = styled.span`
display: block;
font-size: ${fonts.sizes(14, 1.2)};
color: ${(p: Props) => (p.error ? colors.support.red : 'black')};
float: ${(p: Props) => p.float || 'none'};
`;

const FormFieldHelp = ({ error, float, children }: Props) => (
<StyledHelpMessage error={error} float={float}>
{children}
</StyledHelpMessage>
);

FormFieldHelp.propTypes = {
error: PropTypes.bool,
float: PropTypes.oneOf(['left', 'right', 'none', 'inherit']),
};

export default FormFieldHelp;
39 changes: 39 additions & 0 deletions src/components/Form/FormFieldLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2019-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import PropTypes from 'prop-types';

interface Props {
name: string;
label?: string;
noBorder?: boolean;
}

const FormFieldLabel = ({ label, noBorder, name }: Props) => {
if (!label) {
return null;
}
if (!noBorder) {
return <label htmlFor={name}>{label}</label>;
}
return (
<>
<label className="u-hidden" htmlFor={name}>
{label}
</label>
</>
);
};

FormFieldLabel.propTypes = {
noBorder: PropTypes.bool,
name: PropTypes.string.isRequired,
label: PropTypes.string,
};

export default FormFieldLabel;
Loading