From 01c0584872e8851b518a57e98621f1be607a9efd Mon Sep 17 00:00:00 2001 From: Emiel Van Severen Date: Thu, 7 Nov 2024 23:39:17 +0100 Subject: [PATCH] Feat: add human readable cron type to textfield --- package-lock.json | 28 +++- package.json | 1 + .../components/inputs/TextField/Generic.tsx | 127 +++++++++++------- .../inputs/TextField/TextField.stories.tsx | 22 ++- .../src/components/inputs/TextField/style.ts | 6 + .../src/components/inputs/TextField/util.ts | 4 +- 6 files changed, 128 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index bda09a3cf9..a46a6c7d5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,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", @@ -1307,11 +1308,12 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.0.tgz", - "integrity": "sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.0", + "@babel/parser": "^7.26.2", "@babel/types": "^7.26.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", @@ -1563,9 +1565,10 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", - "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "license": "MIT", "dependencies": { "@babel/types": "^7.26.0" }, @@ -26624,6 +26627,8 @@ }, "node_modules/cron-parser": { "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", "license": "MIT", "dependencies": { "luxon": "^3.2.1" @@ -26639,6 +26644,15 @@ "node": ">=18.0" } }, + "node_modules/cronstrue": { + "version": "2.51.0", + "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.51.0.tgz", + "integrity": "sha512-7EG9VaZZ5SRbZ7m25dmP6xaS0qe9ay6wywMskFOU/lMDKa+3gZr2oeT5OUfXwRP/Bcj8wxdYJ65AHU70CI3tsw==", + "license": "MIT", + "bin": { + "cronstrue": "bin/cli.js" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "license": "MIT", diff --git a/package.json b/package.json index 176a916338..8c67e0eb35 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/lib-components/src/components/inputs/TextField/Generic.tsx b/packages/lib-components/src/components/inputs/TextField/Generic.tsx index 8c41eeb5c1..1be0a69b27 100644 --- a/packages/lib-components/src/components/inputs/TextField/Generic.tsx +++ b/packages/lib-components/src/components/inputs/TextField/Generic.tsx @@ -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'; @@ -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; @@ -61,55 +69,74 @@ export const GenericTextField = forwardRef - {prefix && {prefix}} - {icon && cloneElement(icon, { size: 18, className: 'icon' })} - - {type === 'password' && - (showPassword ? ( - { - setShowPassword(false); - }} - size={18} - /> - ) : ( - { - setShowPassword(true); - }} - size={18} - /> - ))} - {suffix && {suffix}} - + + + {prefix && {prefix}} + {icon && cloneElement(icon, { size: 18, className: 'icon' })} + + {type === 'password' && + (showPassword ? ( + { + setShowPassword(false); + }} + size={18} + /> + ) : ( + { + setShowPassword(true); + }} + size={18} + /> + ))} + {suffix && {suffix}} + + {cronOutput && ( + {cronOutput.value} + )} + ); }, ); + +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 }; + } +} diff --git a/packages/lib-components/src/components/inputs/TextField/TextField.stories.tsx b/packages/lib-components/src/components/inputs/TextField/TextField.stories.tsx index 72d683603c..f70ea89a93 100644 --- a/packages/lib-components/src/components/inputs/TextField/TextField.stories.tsx +++ b/packages/lib-components/src/components/inputs/TextField/TextField.stories.tsx @@ -48,7 +48,7 @@ export const OnChange: StoryFn = (args) => { return ( <> { ); }; + +export const Cron: StoryFn = () => { + const [value, setValue] = useState(''); + + return ( + setValue(e.target.value)} + placeholder="placeholder" + required={false} + name="name" + value={value} + id="generic-text-field" + hasDescription={false} + disabled={false} + readOnly={false} + /> + ); +}; diff --git a/packages/lib-components/src/components/inputs/TextField/style.ts b/packages/lib-components/src/components/inputs/TextField/style.ts index 0ca453a8a0..858c5a4499 100644 --- a/packages/lib-components/src/components/inputs/TextField/style.ts +++ b/packages/lib-components/src/components/inputs/TextField/style.ts @@ -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; @@ -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; diff --git a/packages/lib-components/src/components/inputs/TextField/util.ts b/packages/lib-components/src/components/inputs/TextField/util.ts index b4a0c802a0..04311cd922 100644 --- a/packages/lib-components/src/components/inputs/TextField/util.ts +++ b/packages/lib-components/src/components/inputs/TextField/util.ts @@ -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'; } @@ -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; };