import { Grid as MUIGrid } from '@mui/material'; import { FC, forwardRef, ReactNode, useCallback, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import INPUT_TYPES from '../lib/consts/INPUT_TYPES'; import { REP_DOMAIN } from '../lib/consts/REG_EXP_PATTERNS'; import FlexBox, { FlexBoxProps } from './FlexBox'; import InputWithRef, { InputForwardedRefContent } from './InputWithRef'; import isEmpty from '../lib/isEmpty'; import MessageBox, { Message } from './MessageBox'; import MessageGroup, { MessageGroupForwardedRefContent } from './MessageGroup'; import OutlinedInputWithLabel, { OutlinedInputWithLabelProps, } from './OutlinedInputWithLabel'; import pad from '../lib/pad'; import SuggestButton from './SuggestButton'; import { createTestInputFunction, testNotBlank } from '../lib/test_input'; import { InputTestBatches, InputTestInputs } from '../types/TestInputFunction'; import { BodyText } from './Text'; type GeneralInitFormValues = { adminPassword?: string; domainName?: string; hostName?: string; hostNumber?: number; organizationName?: string; organizationPrefix?: string; }; type GeneralInitFormForwardRefContent = { get?: () => GeneralInitFormValues; }; type OutlinedInputWithLabelOnBlur = Exclude< OutlinedInputWithLabelProps['inputProps'], undefined >['onBlur']; const MAX_ORGANIZATION_PREFIX_LENGTH = 5; const MIN_ORGANIZATION_PREFIX_LENGTH = 1; const MAX_HOST_NUMBER_LENGTH = 2; const IT_IDS = { adminPassword: 'adminPassword', confirmAdminPassword: 'confirmAdminPassword', domainName: 'domainName', hostName: 'hostName', hostNumber: 'hostNumber', organizationName: 'organizationName', organizationPrefix: 'organizationPrefix', }; const MAP_TO_ORGANIZATION_PREFIX_BUILDER: Record< number, (words: string[]) => string > = { 0: () => '', 1: ([word]) => word.substring(0, MIN_ORGANIZATION_PREFIX_LENGTH).toLocaleLowerCase(), 2: (words) => words.map((word) => word.substring(0, 1).toLocaleLowerCase()).join(''), }; const buildOrganizationPrefix = (organizationName = '') => { const words: string[] = organizationName .split(/\s/) .filter((word) => !/and|of/.test(word)) .slice(0, MAX_ORGANIZATION_PREFIX_LENGTH); const builderKey: number = words.length > 1 ? 2 : words.length; return MAP_TO_ORGANIZATION_PREFIX_BUILDER[builderKey](words); }; const buildHostName = ({ organizationPrefix, hostNumber, domainName, }: { organizationPrefix?: string; hostNumber?: number; domainName?: string; }) => isEmpty([organizationPrefix, hostNumber, domainName], { not: true }) ? `${organizationPrefix}-striker${pad(hostNumber)}.${domainName}` : ''; const ms = (text: ReactNode) => ( ); const MessageChildren: FC = ({ ...flexBoxProps }) => ( .inline-monospace-text': { marginTop: '-.2em', }, }} /> ); const GeneralInitForm = forwardRef< GeneralInitFormForwardRefContent, { toggleSubmitDisabled?: ToggleSubmitDisabledFunction } >(({ toggleSubmitDisabled }, ref) => { const [helpMessage, setHelpMessage] = useState(); const [isShowOrganizationPrefixSuggest, setIsShowOrganizationPrefixSuggest] = useState(false); const [isShowHostNameSuggest, setIsShowHostNameSuggest] = useState(false); const [isConfirmAdminPassword, setIsConfirmAdminPassword] = useState(true); const adminPasswordInputRef = useRef>({}); const confirmAdminPasswordInputRef = useRef< InputForwardedRefContent<'string'> >({}); const organizationNameInputRef = useRef>( {}, ); const organizationPrefixInputRef = useRef>( {}, ); const domainNameInputRef = useRef>({}); const hostNumberInputRef = useRef>({}); const hostNameInputRef = useRef>({}); const messageGroupRef = useRef({}); const setOrganizationPrefixInputMessage = useCallback( (message?: Message) => messageGroupRef.current.setMessage?.call( null, IT_IDS.organizationPrefix, message, ), [], ); const setHostNumberInputMessage = useCallback( (message?: Message) => messageGroupRef.current.setMessage?.call( null, IT_IDS.hostNumber, message, ), [], ); const setDomainNameInputMessage = useCallback( (message?: Message) => messageGroupRef.current.setMessage?.call( null, IT_IDS.domainName, message, ), [], ); const setHostNameInputMessage = useCallback( (message?: Message) => messageGroupRef.current.setMessage?.call(null, IT_IDS.hostName, message), [], ); const setAdminPasswordInputMessage = useCallback( (message?: Message) => messageGroupRef.current.setMessage?.call( null, IT_IDS.adminPassword, message, ), [], ); const setConfirmAdminPasswordInputMessage = useCallback( (message?: Message) => messageGroupRef.current.setMessage?.call( null, IT_IDS.confirmAdminPassword, message, ), [], ); const inputTests: InputTestBatches = useMemo( () => ({ [IT_IDS.adminPassword]: { defaults: { getValue: () => adminPasswordInputRef.current.getValue?.call(null), onSuccess: () => { setAdminPasswordInputMessage(undefined); }, }, tests: [ { onFailure: () => { setAdminPasswordInputMessage({ children: ( Admin password cannot contain single-quote ({ms("'")}), double-quote ({ms('"')}), slash ({ms('/')}), backslash ( {ms('\\')}), angle brackets ({ms('<>')}), curly brackets ( {ms('{}')}). ), }); }, test: ({ value }) => !/['"/\\><}{]/g.test(value as string), }, { test: testNotBlank }, ], }, [IT_IDS.confirmAdminPassword]: { defaults: { getValue: () => isConfirmAdminPassword ? confirmAdminPasswordInputRef.current.getValue?.call(null) : adminPasswordInputRef.current.getValue?.call(null), onSuccess: () => { setConfirmAdminPasswordInputMessage(undefined); }, }, tests: [ { onFailure: () => { setConfirmAdminPasswordInputMessage({ children: 'Admin password confirmation failed.', }); }, test: ({ value }) => value === adminPasswordInputRef.current.getValue?.call(null), }, { test: testNotBlank }, ], }, [IT_IDS.domainName]: { defaults: { getValue: () => domainNameInputRef.current.getValue?.call(null), onSuccess: () => { setDomainNameInputMessage(undefined); }, }, tests: [ { onFailure: () => { setDomainNameInputMessage({ children: ( Domain name can only contain lowercase alphanumeric, hyphen ({ms('-')}), and dot ({ms('.')}) characters. ), }); }, test: ({ value }) => REP_DOMAIN.test(value as string), }, { test: testNotBlank }, ], }, [IT_IDS.hostName]: { defaults: { getValue: () => hostNameInputRef.current.getValue?.call(null), onSuccess: () => { setHostNameInputMessage(undefined); }, }, tests: [ { onFailure: () => { setHostNameInputMessage({ children: ( Host name can only contain lowercase alphanumeric, hyphen ( {ms('-')}), and dot ({ms('.')}) characters. ), }); }, test: ({ value }) => REP_DOMAIN.test(value as string), }, { test: testNotBlank }, ], }, [IT_IDS.hostNumber]: { defaults: { getValue: () => hostNumberInputRef.current.getValue?.call(null), onSuccess: () => { setHostNumberInputMessage(undefined); }, }, tests: [ { onFailure: () => { setHostNumberInputMessage({ children: 'Host number can only contain digits.', }); }, test: ({ value }) => /^\d+$/.test(value as string), }, { test: testNotBlank }, ], }, [IT_IDS.organizationName]: { defaults: { getValue: () => organizationNameInputRef.current.getValue?.call(null), }, tests: [{ test: testNotBlank }], }, [IT_IDS.organizationPrefix]: { defaults: { getValue: () => organizationPrefixInputRef.current.getValue?.call(null), max: MAX_ORGANIZATION_PREFIX_LENGTH, min: MIN_ORGANIZATION_PREFIX_LENGTH, onSuccess: () => { setOrganizationPrefixInputMessage(undefined); }, }, tests: [ { onFailure: ({ max, min }) => { setOrganizationPrefixInputMessage({ children: `Organization prefix must be ${min} to ${max} lowercase alphanumeric characters.`, }); }, test: ({ max, min, value }) => RegExp(`^[a-z0-9]{${min},${max}}$`).test(value as string), }, ], }, }), [ isConfirmAdminPassword, setAdminPasswordInputMessage, setConfirmAdminPasswordInputMessage, setDomainNameInputMessage, setHostNameInputMessage, setHostNumberInputMessage, setOrganizationPrefixInputMessage, ], ); const testInput = useMemo( () => createTestInputFunction(inputTests), [inputTests], ); const testAllInputs = useCallback( (...excludeTestIds: string[]) => testInput({ excludeTestIds, isIgnoreOnCallbacks: true }), [testInput], ); const testInputSeparate = useCallback( (id: string, input: InputTestInputs[string]) => { const isLocalValid = testInput({ inputs: { [id]: input }, }); toggleSubmitDisabled?.call(null, isLocalValid && testAllInputs(id)); }, [testInput, testAllInputs, toggleSubmitDisabled], ); const populateOrganizationPrefixInput = useCallback( ({ organizationName = organizationNameInputRef.current.getValue?.call(null), } = {}) => { const organizationPrefix = buildOrganizationPrefix(organizationName); organizationPrefixInputRef.current.setValue?.call( null, organizationPrefix, ); return organizationPrefix; }, [], ); const populateHostNameInput = useCallback( ({ organizationPrefix = organizationPrefixInputRef.current.getValue?.call( null, ), hostNumber = hostNumberInputRef.current.getValue?.call(null), domainName = domainNameInputRef.current.getValue?.call(null), } = {}) => { const hostName = buildHostName({ organizationPrefix, hostNumber, domainName, }); hostNameInputRef.current.setValue?.call(null, hostName); return hostName; }, [], ); const isOrganizationPrefixPrereqFilled = useCallback( () => isEmpty([organizationNameInputRef.current.getValue?.call(null)], { not: true, }), [], ); const isHostNamePrereqFilled = useCallback( () => isEmpty( [ organizationPrefixInputRef.current.getValue?.call(null), hostNumberInputRef.current.getValue?.call(null), domainNameInputRef.current.getValue?.call(null), ], { not: true, }, ), [], ); const populateOrganizationPrefixInputOnBlur: OutlinedInputWithLabelOnBlur = useCallback(() => { if (organizationPrefixInputRef.current.getIsChangedByUser?.call(null)) { setIsShowOrganizationPrefixSuggest(isOrganizationPrefixPrereqFilled()); } else { populateOrganizationPrefixInput(); } }, [isOrganizationPrefixPrereqFilled, populateOrganizationPrefixInput]); const populateHostNameInputOnBlur: OutlinedInputWithLabelOnBlur = useCallback(() => { if (hostNameInputRef.current.getIsChangedByUser?.call(null)) { setIsShowHostNameSuggest(isHostNamePrereqFilled()); } else { populateHostNameInput(); } }, [isHostNamePrereqFilled, populateHostNameInput]); const handleOrganizationPrefixSuggest = useCallback(() => { const organizationPrefix = populateOrganizationPrefixInput(); if (!hostNameInputRef.current.getIsChangedByUser?.call(null)) { populateHostNameInput({ organizationPrefix }); } }, [populateHostNameInput, populateOrganizationPrefixInput]); const handlerHostNameSuggest = useCallback(() => { populateHostNameInput(); }, [populateHostNameInput]); const buildHelpMessage = useCallback( (text: string) => (previous?: string) => previous === text ? undefined : text, [], ); useImperativeHandle(ref, () => ({ get: () => ({ adminPassword: adminPasswordInputRef.current.getValue?.call(null), organizationName: organizationNameInputRef.current.getValue?.call(null), organizationPrefix: organizationPrefixInputRef.current.getValue?.call(null), domainName: domainNameInputRef.current.getValue?.call(null), hostNumber: hostNumberInputRef.current.getValue?.call(null), hostName: hostNameInputRef.current.getValue?.call(null), }), })); return ( { toggleSubmitDisabled?.call(null, testAllInputs()); }} onHelp={() => { setHelpMessage( buildHelpMessage( 'Name of the organization that maintains this Anvil! system. You can enter anything that makes sense to you.', ), ); }} /> } ref={organizationNameInputRef} /> :first-child': { flexGrow: 1, }, }} > ), inputProps: { maxLength: MAX_ORGANIZATION_PREFIX_LENGTH, sx: { minWidth: '2.5em', }, }, onBlur: populateHostNameInputOnBlur, }} inputLabelProps={{ isNotifyRequired: true }} label="Prefix" onChange={({ target: { value } }) => { testInputSeparate(IT_IDS.organizationPrefix, { [IT_IDS.organizationPrefix]: { max: MAX_ORGANIZATION_PREFIX_LENGTH, min: MIN_ORGANIZATION_PREFIX_LENGTH, value, }, }); setIsShowOrganizationPrefixSuggest( isOrganizationPrefixPrereqFilled(), ); }} onHelp={() => { setHelpMessage( buildHelpMessage( "Alphanumberic short-form of the organization name. It's used as the prefix for host names.", ), ); }} /> } ref={organizationPrefixInputRef} /> { testInputSeparate(IT_IDS.hostNumber, { value }); }} onHelp={() => { setHelpMessage( buildHelpMessage( "Number or count of this striker; this should be '1' for the first striker, '2' for the second striker, and such.", ), ); }} /> } ref={hostNumberInputRef} valueType="number" /> { testInputSeparate(IT_IDS.domainName, { value }); }} onHelp={() => { setHelpMessage( buildHelpMessage( "Domain name for this striker. It's also the default domain used when creating new install manifests.", ), ); }} /> } ref={domainNameInputRef} /> ), }} inputLabelProps={{ isNotifyRequired: true }} label="Host name" onChange={({ target: { value } }) => { testInputSeparate(IT_IDS.hostName, { value }); setIsShowHostNameSuggest(isHostNamePrereqFilled()); }} onHelp={() => { setHelpMessage( buildHelpMessage( "Host name for this striker. It's usually a good idea to use the auto-generated value.", ), ); }} /> } ref={hostNameInputRef} /> * > *': { width: '100%', }, }} > { setIsConfirmAdminPassword( inputType === INPUT_TYPES.password, ); }, }} inputLabelProps={{ isNotifyRequired: true }} label="Admin password" onChange={({ target: { value } }) => { testInputSeparate(IT_IDS.adminPassword, { value, }); }} onHelp={() => { setHelpMessage( buildHelpMessage( "Password use to login to this Striker and connect to its database. Don't provide an used password here because it'll be stored as plaintext.", ), ); }} /> } ref={adminPasswordInputRef} /> {isConfirmAdminPassword && ( { testInputSeparate(IT_IDS.confirmAdminPassword, { value, }); }} /> } ref={confirmAdminPasswordInputRef} /> )} {helpMessage && ( { setHelpMessage(undefined); }} > {helpMessage} )} ); }); GeneralInitForm.defaultProps = { toggleSubmitDisabled: undefined }; GeneralInitForm.displayName = 'GeneralInitForm'; export type { GeneralInitFormForwardRefContent, GeneralInitFormValues }; export default GeneralInitForm;