mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
feat: quick project wizard
This commit is contained in:
committed by
Carlos Valente
parent
27f99b4c87
commit
ed2efe183b
@@ -26,7 +26,7 @@
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"react-hook-form": "^7.49.2",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"react-qr-code": "^2.0.12",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"web-vitals": "^3.1.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse } from 'ontime-types';
|
||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||
|
||||
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
|
||||
|
||||
@@ -86,6 +86,14 @@ export async function createProject(
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to create a project file
|
||||
*/
|
||||
export async function quickProject(data: QuickStartData): Promise<MessageResponse> {
|
||||
const res = await axios.post(`${dbPath}/quick`, data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to get the list of available project files
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ import useAppSettingsNavigation from './useAppSettingsNavigation';
|
||||
import style from './AppSettings.module.scss';
|
||||
|
||||
export default function AppSettings() {
|
||||
const { close, panel, location } = useAppSettingsNavigation();
|
||||
const { close, panel, location, setLocation } = useAppSettingsNavigation();
|
||||
useKeyDown(close, 'Escape');
|
||||
|
||||
return (
|
||||
@@ -26,7 +26,7 @@ export default function AppSettings() {
|
||||
<ErrorBoundary>
|
||||
<PanelList selectedPanel={panel} location={location} />
|
||||
<PanelContent onClose={close}>
|
||||
{panel === 'project' && <ProjectPanel location={location} />}
|
||||
{panel === 'project' && <ProjectPanel location={location} setLocation={setLocation} />}
|
||||
{panel === 'general' && <GeneralPanel location={location} />}
|
||||
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
|
||||
{panel === 'sources' && <SourcesPanel />}
|
||||
|
||||
@@ -126,7 +126,7 @@ export default function ViewSettingsForm() {
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Freeze timer on end'
|
||||
description='Timer in views will stop from going negative and instead stop at 00:00:00'
|
||||
description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.'
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -139,7 +139,7 @@ export default function ViewSettingsForm() {
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='End message'
|
||||
description='Message to show in negative timers if not frozen. If not provided, timer will continue'
|
||||
description='Message for negative timers; applies only if the timer isn`t frozen on End. If no message is provided, it continues into negative time'
|
||||
/>
|
||||
<Input
|
||||
size='sm'
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import QuickStart from '../../quick-start/QuickStart';
|
||||
import type { SettingsOptionId } from '../../useAppSettingsMenu';
|
||||
|
||||
import ManageProjects from './ManageProjects';
|
||||
import ProjectData from './ProjectData';
|
||||
|
||||
export default function ProjectPanel({ location }: PanelBaseProps) {
|
||||
interface ProjectPanelProps extends PanelBaseProps {
|
||||
setLocation: (location: SettingsOptionId) => void;
|
||||
}
|
||||
|
||||
export default function ProjectPanel({ location, setLocation }: ProjectPanelProps) {
|
||||
const projectRef = useScrollIntoView<HTMLDivElement>('data', location);
|
||||
const manageRef = useScrollIntoView<HTMLDivElement>('manage', location);
|
||||
|
||||
const handleQuickClose = () => {
|
||||
setLocation('project');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Project</Panel.Header>
|
||||
<QuickStart isOpen={location === 'create'} onClose={handleQuickClose} />
|
||||
<div ref={projectRef}>
|
||||
<ProjectData />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.scrollContainer {
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Select,
|
||||
Switch,
|
||||
} from '@chakra-ui/react';
|
||||
import { QuickStartData } from 'ontime-types';
|
||||
import { parseUserTime } from 'ontime-utils';
|
||||
|
||||
import { quickProject } from '../../../common/api/db';
|
||||
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/utils';
|
||||
import TimeInput from '../../../common/components/input/time-input/TimeInput';
|
||||
import { editorSettingsDefaults, useEditorSettings } from '../../../common/stores/editorSettings';
|
||||
import * as Panel from '../panel-utils/PanelUtils';
|
||||
|
||||
import { quickStartDefaults } from './quickStart.utils';
|
||||
|
||||
import style from './QuickStart.module.scss';
|
||||
|
||||
interface QuickStartProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function QuickStart(props: QuickStartProps) {
|
||||
const { isOpen, onClose } = props;
|
||||
const { defaultWarnTime, defaultDangerTime, setDangerTime, setWarnTime } = useEditorSettings();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
setError,
|
||||
} = useForm<QuickStartData>({
|
||||
defaultValues: quickStartDefaults,
|
||||
values: quickStartDefaults,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: QuickStartData) => {
|
||||
try {
|
||||
if (formData.project.title === '') {
|
||||
formData.project.title = 'untitled';
|
||||
}
|
||||
await quickProject(formData);
|
||||
await invalidateAllCaches();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
setError('root', { message: maybeAxiosError(error) });
|
||||
}
|
||||
};
|
||||
|
||||
const warnTimeInMs = parseUserTime(defaultWarnTime);
|
||||
const dangerTimeInMs = parseUserTime(defaultDangerTime);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} closeOnOverlayClick={false} variant='ontime'>
|
||||
<ModalOverlay />
|
||||
<ModalCloseButton />
|
||||
<ModalContent maxWidth='max(640px, 40vw)'>
|
||||
<form onSubmit={handleSubmit(onSubmit)} id='quick-start'>
|
||||
<ModalHeader>Create new project...</ModalHeader>
|
||||
<ModalBody className={style.scrollContainer}>
|
||||
<ModalCloseButton />
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Project title' description='Shown as the title in some views' />
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
maxLength={150}
|
||||
placeholder='Project title'
|
||||
autoComplete='off'
|
||||
width='20rem'
|
||||
{...register('project.title')}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Time format'
|
||||
description='Default time format to show in views 12 /24 hours'
|
||||
error={errors.settings?.timeFormat?.message}
|
||||
/>
|
||||
<Select variant='ontime' size='sm' width='auto' isDisabled={false} {...register('settings.timeFormat')}>
|
||||
<option value='12'>12 hours 11:00:10 PM</option>
|
||||
<option value='24'>24 hours 23:00:10</option>
|
||||
</Select>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Views language'
|
||||
description='Language to be displayed in views'
|
||||
error={errors.settings?.language?.message}
|
||||
/>
|
||||
<Select variant='ontime' size='sm' width='auto' isDisabled={false} {...register('settings.language')}>
|
||||
<option value='en'>English</option>
|
||||
<option value='fr'>French</option>
|
||||
<option value='de'>German</option>
|
||||
<option value='hu'>Hungarian</option>
|
||||
<option value='it'>Italian</option>
|
||||
<option value='no'>Norwegian</option>
|
||||
<option value='pt'>Portuguese</option>
|
||||
<option value='es'>Spanish</option>
|
||||
<option value='sv'>Swedish</option>
|
||||
<option value='pl'>Polish</option>
|
||||
</Select>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Warning time' description='Default threshold for warning time in an event' />
|
||||
<TimeInput<'warnTime'>
|
||||
name='warnTime'
|
||||
submitHandler={(_field, value) => setWarnTime(value)}
|
||||
time={warnTimeInMs}
|
||||
placeholder={editorSettingsDefaults.warnTime}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Danger time' description='Default threshold for danger time in an event' />
|
||||
<TimeInput<'dangerTime'>
|
||||
name='dangerTime'
|
||||
submitHandler={(_field, value) => setDangerTime(value)}
|
||||
time={dangerTimeInMs}
|
||||
placeholder={editorSettingsDefaults.dangerTime}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Freeze timer on end'
|
||||
description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.'
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='viewSettings.freezeEnd'
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='End message'
|
||||
description='Message for negative timers; applies only if the timer isn`t frozen on End. If no message is provided, it continues into negative time'
|
||||
/>
|
||||
<Input
|
||||
size='sm'
|
||||
autoComplete='off'
|
||||
variant='ontime-filled'
|
||||
maxLength={150}
|
||||
width='20rem'
|
||||
placeholder='Shown when timer reaches end'
|
||||
{...register('viewSettings.endMessage')}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||
<Button variant='ontime-ghosted' size='md' onClick={onClose} isDisabled={false}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='ontime-filled' size='md' type='submit' isDisabled={!isValid} isLoading={isSubmitting}>
|
||||
Create project
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { QuickStartData } from 'ontime-types';
|
||||
|
||||
export const quickStartDefaults: QuickStartData = {
|
||||
project: {
|
||||
title: '',
|
||||
},
|
||||
settings: {
|
||||
timeFormat: '24',
|
||||
language: 'en',
|
||||
},
|
||||
viewSettings: {
|
||||
freezeEnd: false,
|
||||
endMessage: '',
|
||||
},
|
||||
};
|
||||
@@ -15,6 +15,7 @@ const staticOptions = [
|
||||
id: 'project',
|
||||
label: 'Project',
|
||||
secondary: [
|
||||
{ id: 'project__create', label: 'Create...' },
|
||||
{ id: 'project__data', label: 'Project data' },
|
||||
{ id: 'project__manage', label: 'Manage projects' },
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseModel, ErrorResponse, MessageResponse, ProjectData, ProjectFileListResponse } from 'ontime-types';
|
||||
import { DatabaseModel, ErrorResponse, MessageResponse, ProjectFileListResponse } from 'ontime-types';
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
@@ -30,17 +30,17 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
|
||||
* or a 500 status with an error message in case of an exception.
|
||||
*/
|
||||
export async function createProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
||||
const newProjectData: ProjectData = {
|
||||
title: req.body?.title ?? '',
|
||||
description: req.body?.description ?? '',
|
||||
publicUrl: req.body?.publicUrl ?? '',
|
||||
publicInfo: req.body?.publicInfo ?? '',
|
||||
backstageUrl: req.body?.backstageUrl ?? '',
|
||||
backstageInfo: req.body?.backstageInfo ?? '',
|
||||
};
|
||||
|
||||
try {
|
||||
const newFileName = await projectService.createProject(req.body.filename, newProjectData);
|
||||
const newFileName = await projectService.createProject(req.body.filename, {
|
||||
project: {
|
||||
title: req.body?.title ?? '',
|
||||
description: req.body?.description ?? '',
|
||||
publicUrl: req.body?.publicUrl ?? '',
|
||||
publicInfo: req.body?.publicInfo ?? '',
|
||||
backstageUrl: req.body?.backstageUrl ?? '',
|
||||
backstageInfo: req.body?.backstageInfo ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).send({
|
||||
filename: newFileName,
|
||||
@@ -51,6 +51,21 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and loads a new project with partial DataBase data
|
||||
*/
|
||||
export async function quickProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
||||
try {
|
||||
const filename = await projectService.createProject(req.body.project.title, req.body);
|
||||
res.status(200).send({
|
||||
filename,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(500).send({ message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows downloading of current project file
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
patchPartialProjectFile,
|
||||
postProjectFile,
|
||||
projectDownload,
|
||||
quickProjectFile,
|
||||
renameProjectFile,
|
||||
} from './db.controller.js';
|
||||
import { uploadProjectFile } from './db.middleware.js';
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
validateFilenameBody,
|
||||
validateFilenameParam,
|
||||
validateNewFilenameBody,
|
||||
validateQuickProject,
|
||||
} from './db.validation.js';
|
||||
|
||||
export const router = express.Router();
|
||||
@@ -29,6 +31,7 @@ router.post('/upload', uploadProjectFile, postProjectFile);
|
||||
|
||||
router.patch('/', validatePatchProject, patchPartialProjectFile);
|
||||
router.post('/new', validateFilenameBody, validateNewProject, createProjectFile);
|
||||
router.post('/quick', validateQuickProject, quickProjectFile);
|
||||
|
||||
router.get('/all', listProjects);
|
||||
|
||||
|
||||
@@ -22,6 +22,28 @@ export const validateNewProject = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Validates request for a quick project.
|
||||
*/
|
||||
export const validateQuickProject = [
|
||||
// Project fields
|
||||
body('project.title').isString().trim(),
|
||||
|
||||
// Settings fields
|
||||
body('settings.timeFormat').optional().isIn(['12', '24']),
|
||||
body('settings.language').optional().isString().trim(),
|
||||
|
||||
// ViewSettings fields
|
||||
body('viewSettings.freezeEnd').optional().isBoolean(),
|
||||
body('viewSettings.endMessage').optional().isString().trim(),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Validates request for pathing data in the project.
|
||||
*/
|
||||
|
||||
@@ -4,11 +4,20 @@ import { DatabaseModel } from 'ontime-types';
|
||||
* Merges a partial ontime project into a given ontime project
|
||||
*/
|
||||
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>): DatabaseModel {
|
||||
const { rundown, project, settings, viewSettings, urlPresets, customFields, osc, http } = newData || {};
|
||||
const {
|
||||
rundown = existing.rundown,
|
||||
project = {},
|
||||
settings = {},
|
||||
viewSettings = {},
|
||||
urlPresets = existing.urlPresets,
|
||||
customFields = existing.customFields,
|
||||
osc = {},
|
||||
http = {},
|
||||
} = newData;
|
||||
|
||||
return {
|
||||
...existing,
|
||||
rundown: rundown ?? existing.rundown,
|
||||
rundown,
|
||||
project: { ...existing.project, ...project },
|
||||
settings: { ...existing.settings, ...settings },
|
||||
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseModel, LogOrigin, ProjectData, ProjectFileListResponse } from 'ontime-types';
|
||||
import { DatabaseModel, LogOrigin, ProjectFileListResponse } from 'ontime-types';
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import { copyFile, rename } from 'fs/promises';
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
moveCorruptFile,
|
||||
parseJsonFile,
|
||||
} from './projectServiceUtils.js';
|
||||
import { safeMerge } from '../../classes/data-provider/DataProvider.utils.js';
|
||||
|
||||
// init dependencies
|
||||
init();
|
||||
@@ -263,14 +264,8 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
||||
/**
|
||||
* Creates a new project file and applies its result
|
||||
*/
|
||||
export async function createProject(filename: string, projectData: ProjectData) {
|
||||
const data: DatabaseModel = {
|
||||
...dbModel,
|
||||
project: {
|
||||
...dbModel.project,
|
||||
...projectData,
|
||||
},
|
||||
};
|
||||
export async function createProject(filename: string, initialData: Partial<DatabaseModel>) {
|
||||
const data = safeMerge(dbModel, initialData);
|
||||
|
||||
const uniqueFileName = generateUniqueFileName(publicDir.projectsDir, filename);
|
||||
const newFile = getPathToProject(uniqueFileName);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type DatabaseModel } from '../../definitions/DataModel.type.js';
|
||||
|
||||
export interface QuickStartData {
|
||||
project: Pick<DatabaseModel['project'], 'title'>;
|
||||
settings: Pick<DatabaseModel['settings'], 'timeFormat' | 'language'>;
|
||||
viewSettings: Pick<DatabaseModel['viewSettings'], 'freezeEnd' | 'endMessage'>;
|
||||
}
|
||||
@@ -53,6 +53,7 @@ export type {
|
||||
RundownPaginated,
|
||||
SessionStats,
|
||||
} from './api/ontime-controller/BackendResponse.type.js';
|
||||
export type { QuickStartData } from './api/db/db.type.js';
|
||||
export type { RundownCached, NormalisedRundown } from './api/rundown-controller/BackendResponse.type.js';
|
||||
|
||||
// SERVER RUNTIME
|
||||
|
||||
Generated
+34
-7
@@ -4,6 +4,33 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
catalogs:
|
||||
default:
|
||||
'@types/node':
|
||||
specifier: 20.14.10
|
||||
version: 20.14.10
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 7.16.1
|
||||
version: 7.16.1
|
||||
'@typescript-eslint/parser':
|
||||
specifier: 7.16.1
|
||||
version: 7.16.1
|
||||
eslint:
|
||||
specifier: 8.56.0
|
||||
version: 8.56.0
|
||||
eslint-config-prettier:
|
||||
specifier: 9.1.0
|
||||
version: 9.1.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3
|
||||
prettier:
|
||||
specifier: 3.3.1
|
||||
version: 3.3.1
|
||||
typescript:
|
||||
specifier: 5.5.3
|
||||
version: 5.5.3
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -123,8 +150,8 @@ importers:
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2
|
||||
react-hook-form:
|
||||
specifier: ^7.49.2
|
||||
version: 7.49.2(react@18.3.1)
|
||||
specifier: ^7.53.1
|
||||
version: 7.53.1(react@18.3.1)
|
||||
react-qr-code:
|
||||
specifier: ^2.0.12
|
||||
version: 2.0.12(react@18.3.1)
|
||||
@@ -4484,11 +4511,11 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-hook-form@7.49.2:
|
||||
resolution: {integrity: sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==}
|
||||
engines: {node: '>=18', pnpm: '8'}
|
||||
react-hook-form@7.53.1:
|
||||
resolution: {integrity: sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
@@ -10070,7 +10097,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
|
||||
react-hook-form@7.49.2(react@18.3.1):
|
||||
react-hook-form@7.53.1(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user