Skip to content

Commit

Permalink
Merge pull request #1806 from gettakaro/feature/cron-description
Browse files Browse the repository at this point in the history
Feat: add human readable cron type to textfield
  • Loading branch information
emielvanseveren authored Nov 7, 2024
2 parents c02d3e4 + 01c0584 commit 5a80f1c
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 60 deletions.
28 changes: 21 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"class-validator-jsonschema": "5.0.1",
"concurrently": "9.0.1",
"convict": "6.2.4",
"cronstrue": "^2.51.0",
"csv": "6.3.10",
"discord-api-types": "0.37.101",
"discord.js": "14.16.3",
Expand Down
127 changes: 77 additions & 50 deletions packages/lib-components/src/components/inputs/TextField/Generic.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { cloneElement, useState, ChangeEvent, ReactElement, forwardRef } from 'react';
import { InputContainer, Input, PrefixContainer, SuffixContainer } from './style';
import {
InputWrapper,
InputContainer,
Input,
PrefixContainer,
SuffixContainer,
HumanReadableCronContainer,
} from './style';
import cronstrue from 'cronstrue';

import { Size } from '../../../styled';
import { AiOutlineEye as ShowPasswordIcon, AiOutlineEyeInvisible as HidePasswordIcon } from 'react-icons/ai';
Expand All @@ -12,7 +20,7 @@ export function isNumber(value: unknown) {
return !isNaN(number) && isFinite(number);
}

export type TextFieldType = 'text' | 'password' | 'email' | 'number';
export type TextFieldType = 'text' | 'password' | 'email' | 'number' | 'cron';

export interface TextFieldProps {
type?: TextFieldType;
Expand Down Expand Up @@ -61,55 +69,74 @@ export const GenericTextField = forwardRef<HTMLInputElement, GenericTextFieldPro
}
};

const cronOutput = type === 'cron' ? showHumanReadableCron(value) : null;

return (
<InputContainer>
{prefix && <PrefixContainer hasError={hasError}>{prefix}</PrefixContainer>}
{icon && cloneElement(icon, { size: 18, className: 'icon' })}
<Input
autoCapitalize="off"
autoComplete={type === 'password' ? 'new-password' : 'off'}
hasError={hasError}
hasIcon={!!icon}
hasPrefix={!!prefix}
hasSuffix={!!suffix}
isPassword={type === 'password'}
id={id}
name={name}
onChange={handleOnChange}
onBlur={onBlur}
onFocus={onFocus}
placeholder={placeholder}
disabled={disabled}
readOnly={readOnly}
role="presentation"
inputMode={getInputMode(type)}
type={getFieldType(type, showPassword)}
ref={ref}
value={value}
aria-readonly={readOnly}
aria-required={required}
aria-describedby={setAriaDescribedBy(name, hasDescription)}
/>
{type === 'password' &&
(showPassword ? (
<HidePasswordIcon
className="password-icon"
onClick={() => {
setShowPassword(false);
}}
size={18}
/>
) : (
<ShowPasswordIcon
className="password-icon"
onClick={() => {
setShowPassword(true);
}}
size={18}
/>
))}
{suffix && <SuffixContainer hasError={hasError}>{suffix}</SuffixContainer>}
</InputContainer>
<InputWrapper>
<InputContainer>
{prefix && <PrefixContainer hasError={hasError}>{prefix}</PrefixContainer>}
{icon && cloneElement(icon, { size: 18, className: 'icon' })}
<Input
autoCapitalize="off"
autoComplete={type === 'password' ? 'new-password' : 'off'}
hasError={hasError}
hasIcon={!!icon}
hasPrefix={!!prefix}
hasSuffix={!!suffix}
isPassword={type === 'password'}
id={id}
name={name}
onChange={handleOnChange}
onBlur={onBlur}
onFocus={onFocus}
placeholder={placeholder}
disabled={disabled}
readOnly={readOnly}
role="presentation"
inputMode={getInputMode(type)}
type={getFieldType(type, showPassword)}
ref={ref}
value={value}
aria-readonly={readOnly}
aria-required={required}
aria-describedby={setAriaDescribedBy(name, hasDescription)}
/>
{type === 'password' &&
(showPassword ? (
<HidePasswordIcon
className="password-icon"
onClick={() => {
setShowPassword(false);
}}
size={18}
/>
) : (
<ShowPasswordIcon
className="password-icon"
onClick={() => {
setShowPassword(true);
}}
size={18}
/>
))}
{suffix && <SuffixContainer hasError={hasError}>{suffix}</SuffixContainer>}
</InputContainer>
{cronOutput && (
<HumanReadableCronContainer isError={cronOutput.isError}>{cronOutput.value}</HumanReadableCronContainer>
)}
</InputWrapper>
);
},
);

function showHumanReadableCron(cron: string) {
if (cron === '') {
return { value: '', isError: false };
}

try {
return { value: cronstrue.toString(cron), isError: false };
} catch (e) {
return { value: `${e}`, isError: true };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const OnChange: StoryFn<TextFieldProps> = (args) => {
return (
<>
<TextField
type={args.type}
type="cron"
name={args.name}
description={args.description}
control={control}
Expand Down Expand Up @@ -193,3 +193,23 @@ export const Generic: StoryFn = () => {
</>
);
};

export const Cron: StoryFn = () => {
const [value, setValue] = useState<string>('');

return (
<GenericTextField
type="cron"
hasError={false}
onChange={(e) => setValue(e.target.value)}
placeholder="placeholder"
required={false}
name="name"
value={value}
id="generic-text-field"
hasDescription={false}
disabled={false}
readOnly={false}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const Container = styled.div`
position: relative;
`;

export const InputWrapper = styled.div``;

export const InputContainer = styled.div`
width: 100%;
position: relative;
Expand Down Expand Up @@ -66,6 +68,10 @@ export const SuffixContainer = styled.div<{ hasError: boolean }>`
white-space: nowrap;
`;

export const HumanReadableCronContainer = styled.div<{ isError: boolean }>`
color: ${({ theme, isError }) => (isError ? theme.colors.error : theme.colors.textAlt)};
`;

export const Input = styled.input<{
hasIcon: boolean;
hasError: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TextFieldType } from './Generic';
export const getFieldType = (type: TextFieldType, passwordVisible: boolean) => {
// we only use the type number to transform the output to number
// so that zod is happy
if (passwordVisible || type === 'number') {
if (passwordVisible || type === 'number' || type === 'cron') {
return 'text';
}

Expand All @@ -13,7 +13,7 @@ export const getFieldType = (type: TextFieldType, passwordVisible: boolean) => {
type InputModes = 'text' | 'email' | 'search' | 'tel' | 'url' | 'none' | 'numeric' | 'decimal';

export const getInputMode = (type: TextFieldType): InputModes => {
if (type === 'password') return 'text';
if (type === 'password' || type === 'cron') return 'text';
if (type === 'number') return 'numeric';
return type;
};

0 comments on commit 5a80f1c

Please sign in to comment.