2020-02-21 11:03:39 +01:00
|
|
|
import React, { useEffect, useState, useRef } from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
2020-02-26 14:53:32 +01:00
|
|
|
import { Sync } from '@buffetjs/icons';
|
2020-02-28 11:51:57 +01:00
|
|
|
import { ErrorMessage, Description } from '@buffetjs/styles';
|
2020-02-21 11:03:39 +01:00
|
|
|
import { Label, Error } from '@buffetjs/core';
|
2020-02-26 17:19:53 +01:00
|
|
|
import { useDebounce, useClickAwayListener } from '@buffetjs/hooks';
|
2020-02-21 11:03:39 +01:00
|
|
|
import styled from 'styled-components';
|
2020-02-26 17:19:53 +01:00
|
|
|
import { request, LoadingIndicator } from 'strapi-helper-plugin';
|
2020-02-21 11:03:39 +01:00
|
|
|
import { FormattedMessage } from 'react-intl';
|
2020-02-26 17:19:53 +01:00
|
|
|
import { isEmpty } from 'lodash';
|
2020-02-21 11:03:39 +01:00
|
|
|
|
|
|
|
import pluginId from '../../pluginId';
|
2020-02-26 14:53:32 +01:00
|
|
|
import getRequestUrl from '../../utils/getRequestUrl';
|
2020-02-21 11:03:39 +01:00
|
|
|
import useDataManager from '../../hooks/useDataManager';
|
|
|
|
import RightLabel from './RightLabel';
|
|
|
|
import Options from './Options';
|
|
|
|
import RegenerateButton from './RegenerateButton';
|
|
|
|
import RightContent from './RightContent';
|
|
|
|
import Input from './InputUID';
|
2020-02-28 11:51:57 +01:00
|
|
|
import Wrapper from './Wrapper';
|
|
|
|
import SubLabel from './SubLabel';
|
|
|
|
import UID_REGEX from './regex';
|
2020-02-21 11:03:39 +01:00
|
|
|
|
|
|
|
const InputContainer = styled.div`
|
|
|
|
position: relative;
|
|
|
|
`;
|
|
|
|
const Name = styled(Label)`
|
|
|
|
display: block;
|
|
|
|
text-transform: capitalize;
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
`;
|
|
|
|
|
|
|
|
// This component should be in buffetjs. It will be used in the media lib.
|
|
|
|
// This component will be the strapi custom dropdown component.
|
|
|
|
// TODO : Make this component generic -> InputDropdown.
|
|
|
|
// TODO : Use the Compounds components pattern
|
|
|
|
// https://blog.bitsrc.io/understanding-compound-components-in-react-23c4b84535b5
|
|
|
|
const InputUID = ({
|
2020-02-26 17:19:53 +01:00
|
|
|
attribute,
|
|
|
|
contentTypeUID,
|
2020-02-28 11:51:57 +01:00
|
|
|
description,
|
2020-02-21 11:03:39 +01:00
|
|
|
error: inputError,
|
|
|
|
name,
|
|
|
|
onChange,
|
2020-02-26 17:19:53 +01:00
|
|
|
required,
|
|
|
|
validations,
|
|
|
|
value,
|
2020-02-28 11:51:57 +01:00
|
|
|
editable,
|
|
|
|
...inputProps
|
2020-02-21 11:03:39 +01:00
|
|
|
}) => {
|
|
|
|
const { modifiedData, initialData } = useDataManager();
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const [availability, setAvailability] = useState(null);
|
|
|
|
const [isSuggestionOpen, setIsSuggestionOpen] = useState(true);
|
2020-02-26 17:19:53 +01:00
|
|
|
const [isCustomized, setIsCustomized] = useState(false);
|
2020-02-21 11:03:39 +01:00
|
|
|
const [label, setLabel] = useState();
|
|
|
|
const debouncedValue = useDebounce(value, 300);
|
2020-02-26 17:19:53 +01:00
|
|
|
const debouncedTargetFieldValue = useDebounce(modifiedData[attribute.targetField], 300);
|
2020-02-21 11:03:39 +01:00
|
|
|
const wrapperRef = useRef(null);
|
2020-02-26 14:53:32 +01:00
|
|
|
const generateUid = useRef();
|
2020-02-21 11:03:39 +01:00
|
|
|
const initialValue = initialData[name];
|
2020-02-26 17:19:53 +01:00
|
|
|
const isCreation = isEmpty(initialData);
|
2020-02-21 11:03:39 +01:00
|
|
|
|
2020-04-15 11:41:08 +02:00
|
|
|
generateUid.current = async (changeInitialData = false) => {
|
2020-02-21 11:03:39 +01:00
|
|
|
setIsLoading(true);
|
2020-02-26 14:53:32 +01:00
|
|
|
const requestURL = getRequestUrl('explorer/uid/generate');
|
2020-02-21 11:03:39 +01:00
|
|
|
try {
|
2020-02-26 14:53:32 +01:00
|
|
|
const { data } = await request(requestURL, {
|
2020-02-21 11:03:39 +01:00
|
|
|
method: 'POST',
|
|
|
|
body: {
|
|
|
|
contentTypeUID,
|
|
|
|
field: name,
|
2020-02-26 14:53:32 +01:00
|
|
|
data: modifiedData,
|
2020-02-21 11:03:39 +01:00
|
|
|
},
|
|
|
|
});
|
2020-04-15 11:41:08 +02:00
|
|
|
|
|
|
|
onChange({ target: { name, value: data, type: 'text' } }, changeInitialData);
|
2020-02-21 11:03:39 +01:00
|
|
|
setIsLoading(false);
|
|
|
|
} catch (err) {
|
|
|
|
console.error({ err });
|
|
|
|
setIsLoading(false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-02-26 14:53:32 +01:00
|
|
|
const checkAvailability = async () => {
|
2020-02-21 11:03:39 +01:00
|
|
|
setIsLoading(true);
|
2020-02-26 14:53:32 +01:00
|
|
|
const requestURL = getRequestUrl('explorer/uid/check-availability');
|
2020-02-21 11:03:39 +01:00
|
|
|
try {
|
2020-02-26 14:53:32 +01:00
|
|
|
const data = await request(requestURL, {
|
2020-02-21 11:03:39 +01:00
|
|
|
method: 'POST',
|
|
|
|
body: {
|
|
|
|
contentTypeUID,
|
|
|
|
field: name,
|
2020-02-28 11:51:57 +01:00
|
|
|
value: value ? value.trim() : null,
|
2020-02-21 11:03:39 +01:00
|
|
|
},
|
|
|
|
});
|
2020-02-26 14:53:32 +01:00
|
|
|
setAvailability(data);
|
|
|
|
|
|
|
|
if (data.suggestion) {
|
|
|
|
setIsSuggestionOpen(true);
|
|
|
|
}
|
2020-02-21 11:03:39 +01:00
|
|
|
setIsLoading(false);
|
|
|
|
} catch (err) {
|
|
|
|
console.error({ err });
|
|
|
|
setIsLoading(false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!value && required) {
|
2020-04-15 11:41:08 +02:00
|
|
|
generateUid.current(true);
|
2020-02-21 11:03:39 +01:00
|
|
|
}
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
useEffect(() => {
|
2020-02-28 11:51:57 +01:00
|
|
|
if (
|
|
|
|
debouncedValue &&
|
|
|
|
debouncedValue.trim().match(UID_REGEX) &&
|
|
|
|
debouncedValue !== initialValue
|
|
|
|
) {
|
2020-02-21 11:03:39 +01:00
|
|
|
checkAvailability();
|
|
|
|
}
|
|
|
|
if (!debouncedValue) {
|
|
|
|
setAvailability(null);
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2020-02-26 14:53:32 +01:00
|
|
|
}, [debouncedValue, initialValue]);
|
2020-02-21 11:03:39 +01:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
let timer;
|
|
|
|
|
|
|
|
if (availability && availability.isAvailable) {
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
setAvailability(null);
|
|
|
|
}, 4000);
|
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
if (timer) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [availability]);
|
|
|
|
|
2020-02-26 17:19:53 +01:00
|
|
|
useEffect(() => {
|
2020-04-15 11:41:08 +02:00
|
|
|
if (!isCustomized && isCreation && debouncedTargetFieldValue) {
|
2020-02-26 17:19:53 +01:00
|
|
|
generateUid.current();
|
|
|
|
}
|
|
|
|
}, [debouncedTargetFieldValue, isCustomized, isCreation]);
|
|
|
|
|
2020-02-21 11:03:39 +01:00
|
|
|
useClickAwayListener(wrapperRef, () => setIsSuggestionOpen(false));
|
|
|
|
|
|
|
|
const handleFocus = () => {
|
|
|
|
if (availability && availability.suggestion) {
|
|
|
|
setIsSuggestionOpen(true);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSuggestionClick = () => {
|
|
|
|
setIsSuggestionOpen(false);
|
|
|
|
onChange({ target: { name, value: availability.suggestion, type: 'text' } });
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleGenerateMouseEnter = () => {
|
|
|
|
setLabel('regenerate');
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleGenerateMouseLeave = () => {
|
|
|
|
setLabel(null);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleChange = (e, canCheck, dispatch) => {
|
|
|
|
if (!canCheck) {
|
|
|
|
dispatch({
|
|
|
|
type: 'SET_CHECK',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: 'SET_ERROR',
|
|
|
|
error: null,
|
|
|
|
});
|
|
|
|
|
2020-02-26 17:19:53 +01:00
|
|
|
if (e.target.value && isCreation) {
|
|
|
|
setIsCustomized(true);
|
|
|
|
}
|
|
|
|
|
2020-02-21 11:03:39 +01:00
|
|
|
onChange(e);
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
2020-02-28 11:51:57 +01:00
|
|
|
<Error
|
|
|
|
name={name}
|
|
|
|
inputError={inputError}
|
|
|
|
type="text"
|
|
|
|
validations={{ ...validations, regex: UID_REGEX }}
|
|
|
|
>
|
2020-02-21 11:03:39 +01:00
|
|
|
{({ canCheck, onBlur, error, dispatch }) => {
|
|
|
|
const hasError = error && error !== null;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Wrapper ref={wrapperRef}>
|
|
|
|
<Name htmlFor={name}>{name}</Name>
|
|
|
|
<InputContainer>
|
|
|
|
<Input
|
2020-02-28 11:51:57 +01:00
|
|
|
{...inputProps}
|
|
|
|
editable={editable}
|
2020-02-21 11:03:39 +01:00
|
|
|
error={hasError}
|
|
|
|
onFocus={handleFocus}
|
|
|
|
name={name}
|
|
|
|
onChange={e => handleChange(e, canCheck, dispatch)}
|
|
|
|
type="text"
|
|
|
|
onBlur={onBlur}
|
2020-02-26 17:19:53 +01:00
|
|
|
// eslint-disable-next-line no-irregular-whitespace
|
|
|
|
value={value || ''}
|
2020-02-21 11:03:39 +01:00
|
|
|
/>
|
|
|
|
<RightContent>
|
|
|
|
<RightLabel availability={availability} label={label} />
|
2020-02-28 11:51:57 +01:00
|
|
|
{editable && (
|
|
|
|
<RegenerateButton
|
|
|
|
onMouseEnter={handleGenerateMouseEnter}
|
|
|
|
onMouseLeave={handleGenerateMouseLeave}
|
|
|
|
onClick={generateUid.current}
|
|
|
|
>
|
|
|
|
{isLoading ? (
|
|
|
|
<LoadingIndicator small />
|
|
|
|
) : (
|
|
|
|
<Sync fill={label ? '#007EFF' : '#B5B7BB'} width="11px" height="11px" />
|
|
|
|
)}
|
|
|
|
</RegenerateButton>
|
|
|
|
)}
|
2020-02-21 11:03:39 +01:00
|
|
|
</RightContent>
|
|
|
|
{availability && availability.suggestion && isSuggestionOpen && (
|
|
|
|
<FormattedMessage id={`${pluginId}.components.uid.suggested`}>
|
|
|
|
{msg => (
|
|
|
|
<Options
|
|
|
|
title={msg}
|
|
|
|
options={[
|
|
|
|
{
|
|
|
|
id: 'suggestion',
|
|
|
|
label: availability.suggestion,
|
|
|
|
onClick: handleSuggestionClick,
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</FormattedMessage>
|
|
|
|
)}
|
|
|
|
</InputContainer>
|
2020-02-28 11:51:57 +01:00
|
|
|
{!hasError && description && <SubLabel as={Description}>{description}</SubLabel>}
|
|
|
|
{hasError && <SubLabel as={ErrorMessage}>{error}</SubLabel>}
|
2020-02-21 11:03:39 +01:00
|
|
|
</Wrapper>
|
|
|
|
);
|
|
|
|
}}
|
|
|
|
</Error>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
InputUID.propTypes = {
|
2020-02-26 17:19:53 +01:00
|
|
|
attribute: PropTypes.object.isRequired,
|
2020-02-21 11:03:39 +01:00
|
|
|
contentTypeUID: PropTypes.string.isRequired,
|
2020-02-28 11:51:57 +01:00
|
|
|
description: PropTypes.string,
|
|
|
|
editable: PropTypes.bool,
|
2020-02-21 11:03:39 +01:00
|
|
|
error: PropTypes.string,
|
|
|
|
name: PropTypes.string.isRequired,
|
|
|
|
onChange: PropTypes.func.isRequired,
|
|
|
|
required: PropTypes.bool,
|
|
|
|
validations: PropTypes.object,
|
|
|
|
value: PropTypes.string,
|
|
|
|
};
|
|
|
|
|
|
|
|
InputUID.defaultProps = {
|
2020-02-28 11:51:57 +01:00
|
|
|
description: '',
|
|
|
|
editable: false,
|
2020-02-21 11:03:39 +01:00
|
|
|
error: null,
|
|
|
|
required: false,
|
|
|
|
validations: {},
|
|
|
|
value: '',
|
|
|
|
};
|
|
|
|
|
|
|
|
export default InputUID;
|