feat: connect environments to redux store

This commit is contained in:
Anoop M D 2022-10-16 05:46:49 +05:30
parent c6ac90a9f8
commit 7ca6270f2b
18 changed files with 307 additions and 115 deletions

View File

@ -1,25 +0,0 @@
import React, { useEffect, useState } from "react";
import { IconEdit, IconTrash } from "@tabler/icons";
import RenameEnvironment from "../../RenameEnvironment";
import DeleteEnvironment from "../../DeleteEnvironment";
// import StyledWrapper from "./StyledWrapper";
const EnvironmentDetails = ({selected}) => {
const [ openEditModal, setOpenEditModal] = useState(false);
const [ openDeleteModal, setOpenDeleteModal] = useState(false);
return (
<div className="ml-10 flex-grow flex pt-4" style={{maxWidth: '700px'}}>
<span>{selected.name}</span>
<div className="flex gap-x-4 pl-4" >
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)}/>
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)}/>
</div>
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={selected} />}
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={selected} />}
</div>
);
};
export default EnvironmentDetails;

View File

@ -1,43 +0,0 @@
import Modal from "components/Modal/index";
import React, { useState } from "react";
import CreateEnvironment from "./CreateEnvironment";
import Layout from "./Layout";
const EnvironmentSettings = ({onClose}) => {
const environments = [
{name: "My env", uid: 123},
{name: "hjdgfh dj", uid: 3876},
];
const [openCreateModal, setOpenCreateModal] = useState(false)
if(!environments.length) {
return (
<Modal
size="lg"
title="Environment"
confirmText={"Close"}
handleConfirm={onClose}
hideCancel={true}
>
<p>No environment found!</p>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={() => setOpenCreateModal(true)}>+ Create Environment</button>
{openCreateModal && <CreateEnvironment onClose={() => setOpenCreateModal(false)}/>}
</Modal>
)
}
return (
<Modal
size="lg"
title="Environment"
confirmText={"Close"}
handleCancel={onClose}
hideFooter={true}
>
<Layout />
</Modal>
)
}
export default EnvironmentSettings;

View File

@ -1,10 +1,10 @@
import React, { useRef, forwardRef, useState } from 'react'; import React, { useRef, forwardRef, useState } from 'react';
import Dropdown from 'components/Dropdown'; import Dropdown from 'components/Dropdown';
import { IconAdjustmentsHorizontal, IconCaretDown } from '@tabler/icons'; import { IconAdjustmentsHorizontal, IconCaretDown } from '@tabler/icons';
import EnvironmentSettings from "./EnvironmentSettings"; import EnvironmentSettings from "../EnvironmentSettings";
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
const EnvironmentSelector = () => { const EnvironmentSelector = ({collection}) => {
const dropdownTippyRef = useRef(); const dropdownTippyRef = useRef();
const [openSettingsModal, setOpenSettingsModal] = useState(false); const [openSettingsModal, setOpenSettingsModal] = useState(false);
@ -46,7 +46,7 @@ const EnvironmentSelector = () => {
</div> </div>
</Dropdown> </Dropdown>
</div> </div>
{openSettingsModal && <EnvironmentSettings onClose={() => setOpenSettingsModal(false)}/>} {openSettingsModal && <EnvironmentSettings collection={collection} onClose={() => setOpenSettingsModal(false)}/>}
</StyledWrapper> </StyledWrapper>
); );
}; };

View File

@ -1,12 +1,13 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import Portal from "components/Portal/index"; import Portal from "components/Portal/index";
import Modal from "components/Modal/index"; import Modal from "components/Modal/index";
import toast from 'react-hot-toast';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { addWorkspace } from 'providers/ReduxStore/slices/workspaces'; import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
const CreateEnvironment = ({onClose}) => { const CreateEnvironment = ({collection, onClose}) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const inputRef = useRef(); const inputRef = useRef();
const formik = useFormik({ const formik = useFormik({
@ -17,12 +18,16 @@ const CreateEnvironment = ({onClose}) => {
validationSchema: Yup.object({ validationSchema: Yup.object({
name: Yup.string() name: Yup.string()
.min(1, 'must be atleast 1 characters') .min(1, 'must be atleast 1 characters')
.max(30, 'must be 30 characters or less') .max(50, 'must be 50 characters or less')
.required('name is required') .required('name is required')
}), }),
onSubmit: (values) => { onSubmit: (values) => {
// dispatch(addWorkspace({name: values.name})); dispatch(addEnvironment(values.name, collection.uid))
// onClose(); .then(() => {
toast.success("Environment created in collection");
onClose();
})
.catch(() => toast.error("An error occured while created the environment"));
} }
}); });

View File

@ -1,15 +1,20 @@
import React from 'react'; import React from 'react';
import Portal from "components/Portal/index"; import Portal from "components/Portal/index";
import toast from 'react-hot-toast';
import Modal from "components/Modal/index"; import Modal from "components/Modal/index";
// import { deleteWorkspace } from 'providers/ReduxStore/slices/workspaces'; import { deleteEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
const DeleteEnvironment = ({onClose, environment}) => { const DeleteEnvironment = ({onClose, environment, collection}) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const onConfirm = () =>{ const onConfirm = () =>{
// dispatch(deleteWorkspace({workspaceUid: workspace.uid})) dispatch(deleteEnvironment(environment.uid, collection.uid))
onClose(); .then(() => {
toast.success("Environment deleted successfully");
onClose();
})
.catch(() => toast.error("An error occured while deleting the environment"));
}; };
return ( return (

View File

@ -0,0 +1,26 @@
import React, {useState } from "react";
import { IconEdit, IconTrash } from "@tabler/icons";
import RenameEnvironment from "../../RenameEnvironment";
import DeleteEnvironment from "../../DeleteEnvironment";
const EnvironmentDetails = ({environment, collection}) => {
const [ openEditModal, setOpenEditModal] = useState(false);
const [ openDeleteModal, setOpenDeleteModal] = useState(false);
return (
<div className="ml-6 flex-grow flex pt-6" style={{maxWidth: '700px'}}>
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection}/>}
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection}/>}
<div className="flex flex-grow">
<div className="flex-grow font-medium">{environment.name}</div>
<div className="flex gap-x-4 pl-4 pr-6">
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)}/>
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)}/>
</div>
</div>
</div>
);
};
export default EnvironmentDetails;

View File

@ -1,12 +1,12 @@
import styled from "styled-components"; import styled from "styled-components";
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
margin-left: -1rem; margin-inline: -1rem;
margin-block: -1.5rem; margin-block: -1.5rem;
.environments-sidebar { .environments-sidebar {
margin-bottom: 8px; background-color: #eaeaea;
background-color: #ffffff; min-height: 400px;
min-height: 300px;
} }
.environment-item { .environment-item {
@ -15,28 +15,34 @@ const StyledWrapper = styled.div`
position: relative; position: relative;
cursor: pointer; cursor: pointer;
padding: 8px 10px; padding: 8px 10px;
color: rgb(35, 35, 35); border-left: solid 2px transparent;
border-bottom: 1px solid #eaecef; text-decoration: none;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
background-color: #f6f8fa; background-color: #e4e4e4;
} }
} }
.active { .active {
background-color: #e1e4e8; background-color: #dcdcdc !important;
border-left: solid 2px var(--color-brand);
&:hover { &:hover {
text-decoration: none; background-color: #dcdcdc !important;
background-color: #e1e4e8;
} }
} }
.create-env { .btn-create-environment {
padding: 8px 10px; padding: 8px 10px;
cursor: pointer; cursor: pointer;
border-bottom: none; border-bottom: none;
color: var(--color-text-link); color: var(--color-text-link);
&:hover {
span {
text-decoration: underline;
}
}
} }
`; `;

View File

@ -3,38 +3,43 @@ import EnvironmentDetails from "./EnvironmentDetails";
import CreateEnvironment from "../CreateEnvironment/index"; import CreateEnvironment from "../CreateEnvironment/index";
import StyledWrapper from "./StyledWrapper"; import StyledWrapper from "./StyledWrapper";
const environments = [ const EnvironmentList = ({collection}) => {
{name: "My env", uid: 123}, const { environments } = collection;
{name: "hjdgfh dj", uid: 3876}, const [selectedEnvironment, setSelectedEnvironment] = useState(null);
{name: "hjdgfer dj", uid: 4678},
];
const Layout = () => {
const [selectedEnvironment, setSelectedEnvironment] = useState({});
const [openCreateModal, setOpenCreateModal] = useState(false); const [openCreateModal, setOpenCreateModal] = useState(false);
useEffect(() => { useEffect(() => {
setSelectedEnvironment(environments[0]); setSelectedEnvironment(environments[0]);
}, []); }, []);
if(!selectedEnvironment) {
return null;
}
return ( return (
<StyledWrapper> <StyledWrapper>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)}/>}
<div className="flex"> <div className="flex">
<div style={{borderRight: "1px solid #ccc"}}> <div>
<div className="environments-sidebar"> <div className="environments-sidebar">
{environments && environments.length && environments.map((env) => ( {environments && environments.length && environments.map((env) => (
<div className={selectedEnvironment.uid === env.uid ? "environment-item active": "environment-item"} onClick={() => setSelectedEnvironment(env)}> <div
key={env.uid}
className={selectedEnvironment.uid === env.uid ? "environment-item active": "environment-item"}
onClick={() => setSelectedEnvironment(env)}
>
<span>{env.name}</span> <span>{env.name}</span>
</div> </div>
))} ))}
<p className="create-env" onClick={() => setOpenCreateModal(true)}> + Create</p> <div className="btn-create-environment" onClick={() => setOpenCreateModal(true)}>
+ <span>Create</span>
</div>
</div> </div>
</div> </div>
<EnvironmentDetails selected={selectedEnvironment}/> <EnvironmentDetails environment={selectedEnvironment} collection={collection}/>
</div> </div>
{openCreateModal && <CreateEnvironment onClose={() => setOpenCreateModal(false)}/>}
</StyledWrapper> </StyledWrapper>
); );
}; };
export default Layout; export default EnvironmentList;

View File

@ -1,13 +1,14 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import Portal from "components/Portal/index"; import Portal from "components/Portal/index";
import Modal from "components/Modal/index"; import Modal from "components/Modal/index";
import toast from 'react-hot-toast';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
// import { rename } from 'providers/ReduxStore/slices/environments'; import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
const RenameEnvironment = ({onClose, environment}) => { const RenameEnvironment = ({onClose, environment, collection}) => {
// const dispatch = useDispatch(); const dispatch = useDispatch();
const inputRef = useRef(); const inputRef = useRef();
const formik = useFormik({ const formik = useFormik({
enableReinitialize: true, enableReinitialize: true,
@ -17,12 +18,16 @@ const RenameEnvironment = ({onClose, environment}) => {
validationSchema: Yup.object({ validationSchema: Yup.object({
name: Yup.string() name: Yup.string()
.min(1, 'must be atleast 1 characters') .min(1, 'must be atleast 1 characters')
.max(30, 'must be 30 characters or less') .max(50, 'must be 50 characters or less')
.required('name is required') .required('name is required')
}), }),
onSubmit: (values) => { onSubmit: (values) => {
// dispatch(rename({name: values.name, uid: environment.uid})); dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
onClose(); .then(() => {
toast.success("Environment renamed successfully");
onClose();
})
.catch(() => toast.error("An error occured while renaming the environment"));
} }
}); });

View File

@ -0,0 +1,13 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
button.btn-create-environment {
&:hover {
span {
text-decoration: underline;
}
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,50 @@
import Modal from "components/Modal/index";
import React, { useState } from "react";
import CreateEnvironment from "./CreateEnvironment";
import EnvironmentList from "./EnvironmentList";
import StyledWrapper from "./StyledWrapper";
const EnvironmentSettings = ({collection, onClose}) => {
const { environments } = collection;
const [openCreateModal, setOpenCreateModal] = useState(false)
if(!environments || !environments.length) {
return (
<StyledWrapper>
<Modal
size="md"
title="Environments"
confirmText={"Close"}
handleConfirm={onClose}
handleCancel={onClose}
hideCancel={true}
>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)}/>}
<div className="text-center">
<p>No environments found!</p>
<button
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
onClick={() => setOpenCreateModal(true)}
>
+ <span>Create Environment</span>
</button>
</div>
</Modal>
</StyledWrapper>
)
}
return (
<Modal
size="lg"
title="Environments"
handleCancel={onClose}
hideFooter={true}
>
<EnvironmentList collection={collection}/>
</Modal>
)
}
export default EnvironmentSettings;

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { IconFiles } from '@tabler/icons'; import { IconFiles } from '@tabler/icons';
import EnvironmentSelector from 'components/EnvironmentSelector'; import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
const CollectionToolBar = ({collection}) => { const CollectionToolBar = ({collection}) => {
@ -12,7 +12,7 @@ const CollectionToolBar = ({collection}) => {
<span className="ml-2 mr-4 font-semibold">{collection.name}</span> <span className="ml-2 mr-4 font-semibold">{collection.name}</span>
</div> </div>
<div className="flex flex-1 items-center justify-end"> <div className="flex flex-1 items-center justify-end">
<EnvironmentSelector /> <EnvironmentSelector collection={collection}/>
</div> </div>
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@ -1,5 +1,5 @@
import path from 'path'; import path from 'path';
import axios from 'axios'; import filter from 'lodash/filter';
import each from 'lodash/each'; import each from 'lodash/each';
import trim from 'lodash/trim'; import trim from 'lodash/trim';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@ -13,6 +13,7 @@ import {
transformRequestToSaveToFilesystem, transformRequestToSaveToFilesystem,
deleteItemInCollection, deleteItemInCollection,
findParentItemInCollection, findParentItemInCollection,
findEnvironmentInCollection,
isItemAFolder, isItemAFolder,
refreshUidsInItem refreshUidsInItem
} from 'utils/collections'; } from 'utils/collections';
@ -31,6 +32,9 @@ import {
cloneItem as _cloneItem, cloneItem as _cloneItem,
deleteItem as _deleteItem, deleteItem as _deleteItem,
saveRequest as _saveRequest, saveRequest as _saveRequest,
addEnvironment as _addEnvironment,
renameEnvironment as _renameEnvironment,
deleteEnvironment as _deleteEnvironment,
createCollection as _createCollection, createCollection as _createCollection,
renameCollection as _renameCollection, renameCollection as _renameCollection,
deleteCollection as _deleteCollection, deleteCollection as _deleteCollection,
@ -71,7 +75,8 @@ export const createCollection = (collectionName) => (dispatch, getState) => {
const newCollection = { const newCollection = {
uid: uuid(), uid: uuid(),
name: collectionName, name: collectionName,
items: [] items: [],
environments: []
}; };
const requestItem = { const requestItem = {
@ -606,6 +611,88 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
}); });
}; };
export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if(!collection) {
return reject(new Error('Collection not found'));
}
const environment = {
uid: uuid(),
name: name,
variables: []
};
const collectionCopy = cloneDeep(collection);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionToSave.environments = collectionToSave.environments || [];
collectionToSave.environments.push(environment);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_addEnvironment({environment, collectionUid})))
.then(resolve)
.catch(reject);
});
};
export const renameEnvironment = (newName, environmentUid, collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if(!collection) {
return reject(new Error('Collection not found'));
}
const collectionCopy = cloneDeep(collection);
const environment = findEnvironmentInCollection(collectionCopy, environmentUid);
if(!environment) {
return reject(new Error('Environment not found'));
}
environment.name = newName;
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_renameEnvironment({newName, environmentUid, collectionUid})))
.then(resolve)
.catch(reject);
});
};
export const deleteEnvironment = (environmentUid, collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if(!collection) {
return reject(new Error('Collection not found'));
}
const collectionCopy = cloneDeep(collection);
const environment = findEnvironmentInCollection(collectionCopy, environmentUid);
if(!environment) {
return reject(new Error('Environment not found'));
}
collectionCopy.environments = filter(collectionCopy.environments, (e) => e.uid !== environmentUid);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_deleteEnvironment({environmentUid, collectionUid})))
.then(resolve)
.catch(reject);
});
};
export const removeLocalCollection = (collectionUid) => (dispatch, getState) => { export const removeLocalCollection = (collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const state = getState(); const state = getState();

View File

@ -11,6 +11,7 @@ import splitOnFirst from 'split-on-first';
import { import {
findCollectionByUid, findCollectionByUid,
findItemInCollection, findItemInCollection,
findEnvironmentInCollection,
findItemInCollectionByPathname, findItemInCollectionByPathname,
addDepth, addDepth,
collapseCollection, collapseCollection,
@ -69,6 +70,39 @@ export const collectionsSlice = createSlice({
deleteCollection: (state, action) => { deleteCollection: (state, action) => {
state.collections = filter(state.collections, c => c.uid !== action.payload.collectionUid); state.collections = filter(state.collections, c => c.uid !== action.payload.collectionUid);
}, },
addEnvironment: (state, action) => {
const { environment, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if(collection) {
collection.environments = collection.environments || [];
collection.environments.push(environment);
}
},
renameEnvironment: (state, action) => {
const { newName, environmentUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if(collection) {
const environment = findEnvironmentInCollection(collection, environmentUid);
if(environment) {
environment.name = newName;
}
}
},
deleteEnvironment: (state, action) => {
const { environmentUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if(collection) {
const environment = findEnvironmentInCollection(collection, environmentUid);
if(environment) {
collection.environments = filter(collection.environments, (e) => e.uid !== environmentUid);
}
}
},
newItem: (state, action) => { newItem: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -690,6 +724,9 @@ export const {
renameCollection, renameCollection,
deleteCollection, deleteCollection,
loadCollections, loadCollections,
addEnvironment,
renameEnvironment,
deleteEnvironment,
newItem, newItem,
deleteItem, deleteItem,
renameItem, renameItem,

View File

@ -117,6 +117,10 @@ export const recursivelyGetAllItemUids = (items = []) => {
return map(flattenedItems, (i) => i.uid); return map(flattenedItems, (i) => i.uid);
}; };
export const findEnvironmentInCollection = (collection, envUid) => {
return find(collection.environments, (e) => e.uid === envUid);
}
export const transformCollectionToSaveToIdb = (collection, options = {}) => { export const transformCollectionToSaveToIdb = (collection, options = {}) => {
const copyHeaders = (headers) => { const copyHeaders = (headers) => {
return map(headers, (header) => { return map(headers, (header) => {
@ -233,6 +237,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
collectionToSave.name = collection.name; collectionToSave.name = collection.name;
collectionToSave.uid = collection.uid; collectionToSave.uid = collection.uid;
collectionToSave.items = []; collectionToSave.items = [];
collectionToSave.environments = collection.environments || [];
copyItems(collection.items, collectionToSave.items); copyItems(collection.items, collectionToSave.items);

View File

@ -1,6 +1,21 @@
const Yup = require('yup'); const Yup = require('yup');
const { uidSchema } = require("../common"); const { uidSchema } = require("../common");
const environmentVariablesSchema = Yup.object({
uid: uidSchema,
name: Yup.string().nullable().max(256, 'name must be 256 characters or less').defined(),
value: Yup.string().nullable().max(2048, 'value must be 2048 characters or less').defined(),
type: Yup.string().oneOf(['text']).required('type is required'),
enabled: Yup.boolean().defined()
}).noUnknown(true).strict();
const environmentSchema = Yup.object({
uid: uidSchema,
name: Yup.string().min(1).max(50, 'name must be 50 characters or less').required('name is required'),
variables: Yup.array().of(environmentVariablesSchema).required('variables are required')
}).noUnknown(true).strict();
const keyValueSchema = Yup.object({ const keyValueSchema = Yup.object({
uid: uidSchema, uid: uidSchema,
name: Yup.string().nullable().max(256, 'name must be 256 characters or less').defined(), name: Yup.string().nullable().max(256, 'name must be 256 characters or less').defined(),
@ -55,6 +70,7 @@ const collectionSchema = Yup.object({
.max(50, 'name must be 100 characters or less') .max(50, 'name must be 100 characters or less')
.required('name is required'), .required('name is required'),
items: Yup.array().of(itemSchema), items: Yup.array().of(itemSchema),
environments: Yup.array().of(environmentSchema),
pathname: Yup.string().max(1024, 'pathname cannot be more than 1024 characters').nullable() pathname: Yup.string().max(1024, 'pathname cannot be more than 1024 characters').nullable()
}).noUnknown(true).strict(); }).noUnknown(true).strict();