-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
330 additions
and
220 deletions.
There are no files selected for viewing
308 changes: 308 additions & 0 deletions
308
packages/insomnia/src/ui/components/modals/project-edit-modal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,308 @@ | ||
import React, { | ||
type FC, | ||
useMemo, | ||
useState, | ||
} from 'react'; | ||
import { | ||
Button, | ||
Dialog, | ||
Heading, | ||
Input, | ||
Label, | ||
Modal, | ||
ModalOverlay, | ||
Radio, | ||
RadioGroup, | ||
TextField, | ||
} from 'react-aria-components'; | ||
import { | ||
useNavigate, | ||
} from 'react-router-dom'; | ||
|
||
import { getAppWebsiteBaseURL } from '../../../common/constants'; | ||
import * as models from '../../../models'; | ||
import { invariant } from '../../../utils/invariant'; | ||
import { insomniaFetch } from '../../insomniaFetch'; | ||
import { ORG_STORAGE_RULE } from '../../routes/organization'; | ||
import { Icon } from '../icon'; | ||
import { showAlert, showModal } from '.'; | ||
import { AskModal } from './ask-modal'; | ||
|
||
export enum PROJECT_EDIT_MODAL_TYPE { | ||
NEW = 'new', | ||
EDIT = 'edit', | ||
}; | ||
|
||
type ProjectType = 'local' | 'remote'; | ||
|
||
const ProjectEditModal: FC<{ | ||
modalType: PROJECT_EDIT_MODAL_TYPE; | ||
isOpen: boolean; | ||
setIsOpen: (isOpen: boolean) => void; | ||
orgStorageRule: ORG_STORAGE_RULE; | ||
organizationId: string; | ||
showStorageRestrictionMessage: boolean; | ||
}> = ({ | ||
modalType, | ||
isOpen, | ||
setIsOpen, | ||
orgStorageRule, | ||
organizationId, | ||
showStorageRestrictionMessage, | ||
}) => { | ||
const [doesShowChangeStorageTypeConfirmation, setDoesShowChangeStorageTypeConfirmation] = useState(false); | ||
const selectedStorageTypeRef | ||
const title = useMemo(() => { | ||
if (modalType === PROJECT_EDIT_MODAL_TYPE.NEW) { | ||
return 'Create a New Project'; | ||
} else if (modalType === PROJECT_EDIT_MODAL_TYPE.EDIT) { | ||
if (!doesShowChangeStorageTypeConfirmation) { | ||
return 'Project Settings'; | ||
} else { | ||
return 'haha'; | ||
} | ||
} else { | ||
throw new Error(`Invalid modal type: ${modalType}`); | ||
} | ||
}, [modalType, doesShowChangeStorageTypeConfirmation]); | ||
const navigate = useNavigate(); | ||
const defaultStorageSelection = orgStorageRule === ORG_STORAGE_RULE.LOCAL_ONLY ? 'local' : 'remote'; | ||
return (<ModalOverlay | ||
isOpen={isOpen} | ||
onOpenChange={isOpen => setIsOpen(isOpen)} | ||
isDismissable | ||
className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-center justify-center bg-black/30" | ||
> | ||
<Modal className="max-w-2xl w-full rounded-md border border-solid border-[--hl-sm] p-[--padding-lg] max-h-full bg-[--color-bg] text-[--color-font]"> | ||
<Dialog className="outline-none" aria-label='Create or update dialog'> | ||
{({ close }) => ( | ||
<div className='flex flex-col gap-4'> | ||
<div className='flex gap-2 items-center justify-between'> | ||
<Heading slot="title" className='text-2xl'>{title}</Heading> | ||
<Button | ||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm" | ||
onPress={close} | ||
> | ||
<Icon icon="x" /> | ||
</Button> | ||
</div> | ||
<form | ||
className='flex flex-col gap-4' | ||
onSubmit={e => { | ||
e.preventDefault(); | ||
const formData = new FormData(e.currentTarget); | ||
const type = formData.get('type'); | ||
|
||
if (!type) { | ||
showAlert({ | ||
title: 'Project type not selected', | ||
message: 'Please select a project type before continuing', | ||
}); | ||
return; | ||
} | ||
|
||
const name = formData.get('name'); | ||
|
||
createNewProject({ | ||
organizationId, | ||
name: (typeof name === 'string') ? name : 'My project', | ||
projectType: type as ProjectType, | ||
}).then( | ||
newProjectId => { | ||
navigate(`/organization/${organizationId}/project/${newProjectId}`); | ||
}, | ||
err => { | ||
const errMsg = err.message; | ||
if (errMsg === 'NEEDS_TO_UPGRADE') { | ||
showModal(AskModal, { | ||
title: 'Upgrade your plan', | ||
message: 'You are currently on the Free plan where you can invite as many collaborators as you want as long as you don\'t have more than one project. Since you have more than one project, you need to upgrade to "Individual" or above to continue.', | ||
yesText: 'Upgrade', | ||
noText: 'Cancel', | ||
onDone: async (isYes: boolean) => { | ||
if (isYes) { | ||
window.main.openInBrowser(`${getAppWebsiteBaseURL()}/app/subscription/update?plan=individual`); | ||
} | ||
}, | ||
}); | ||
} else if (errMsg === 'FORBIDDEN') { | ||
showAlert({ | ||
title: 'Could not create project.', | ||
message: 'You do not have permission to create a project in this organization.', | ||
}); | ||
} else { | ||
showAlert({ | ||
title: 'Could not create project.', | ||
message: errMsg, | ||
}); | ||
} | ||
}, | ||
); | ||
|
||
close(); | ||
}} | ||
> | ||
<TextField | ||
autoFocus | ||
name="name" | ||
defaultValue="My project" | ||
className="group relative flex-1 flex flex-col gap-2" | ||
> | ||
<Label className='text-sm text-[--hl]'> | ||
Project name | ||
</Label> | ||
<Input | ||
placeholder="My project" | ||
className="py-1 placeholder:italic w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors" | ||
/> | ||
</TextField> | ||
<RadioGroup name="type" defaultValue={defaultStorageSelection} className="flex flex-col gap-2"> | ||
<Label className="text-sm text-[--hl]"> | ||
Project type | ||
</Label> | ||
<div className="flex gap-2"> | ||
<Radio | ||
isDisabled={orgStorageRule === ORG_STORAGE_RULE.LOCAL_ONLY} | ||
value="remote" | ||
className="flex-1 data-[selected]:border-[--color-surprise] data-[selected]:ring-2 data-[selected]:ring-[--color-surprise] data-[disabled]:opacity-25 hover:bg-[--hl-xs] focus:bg-[--hl-sm] border border-solid border-[--hl-md] rounded p-4 focus:outline-none transition-colors" | ||
> | ||
<div className='flex items-center gap-2'> | ||
<Icon icon="globe" /> | ||
<Heading className="text-lg font-bold">Cloud Sync</Heading> | ||
</div> | ||
<p className='pt-2'> | ||
Encrypted and synced securely to the cloud, ideal for out of the box collaboration. | ||
</p> | ||
</Radio> | ||
<Radio | ||
isDisabled={orgStorageRule === ORG_STORAGE_RULE.CLOUD_ONLY} | ||
value="local" | ||
className="flex-1 data-[selected]:border-[--color-surprise] data-[selected]:ring-2 data-[selected]:ring-[--color-surprise] data-[disabled]:opacity-25 hover:bg-[--hl-xs] focus:bg-[--hl-sm] border border-solid border-[--hl-md] rounded p-4 focus:outline-none transition-colors" | ||
> | ||
<div className="flex items-center gap-2"> | ||
<Icon icon="laptop" /> | ||
<Heading className="text-lg font-bold">Local Vault</Heading> | ||
</div> | ||
<p className="pt-2"> | ||
Stored locally only with no cloud. Ideal when collaboration is not needed. | ||
</p> | ||
</Radio> | ||
<Radio | ||
isDisabled={orgStorageRule === ORG_STORAGE_RULE.CLOUD_ONLY} | ||
value="local" | ||
className="flex-1 data-[selected]:border-[--color-surprise] data-[selected]:ring-2 data-[selected]:ring-[--color-surprise] data-[disabled]:opacity-25 hover:bg-[--hl-xs] focus:bg-[--hl-sm] border border-solid border-[--hl-md] rounded p-4 focus:outline-none transition-colors" | ||
> | ||
<div className="flex items-center gap-2"> | ||
<Icon icon="laptop" /> | ||
<Heading className="text-lg font-bold">Local Vault</Heading> | ||
</div> | ||
<p className="pt-2"> | ||
Stored locally only with no cloud. Ideal when collaboration is not needed. | ||
</p> | ||
</Radio> | ||
</div> | ||
</RadioGroup> | ||
<div className="flex justify-between gap-2 items-center"> | ||
<div className="flex items-center gap-2 text-sm"> | ||
<Icon icon="info-circle" /> | ||
<span> | ||
{showStorageRestrictionMessage && `The organization owner mandates that projects must be created and stored ${orgStorageRule.split('_').join(' ')}.`} You can optionally enable Git Sync | ||
</span> | ||
</div> | ||
<div className='flex items-center gap-2'> | ||
<Button | ||
onPress={close} | ||
className="hover:no-underline hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font] transition-colors rounded-sm" | ||
> | ||
Cancel | ||
</Button> | ||
<Button | ||
type="submit" | ||
className="hover:no-underline bg-[--color-surprise] hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font-surprise] transition-colors rounded-sm" | ||
> | ||
Create | ||
</Button> | ||
</div> | ||
</div> | ||
</form> | ||
</div> | ||
)} | ||
</Dialog> | ||
</Modal> | ||
</ModalOverlay>); | ||
}; | ||
|
||
ProjectEditModal.displayName = 'NewProjectModal'; | ||
|
||
async function createNewProject({ | ||
organizationId, | ||
name, | ||
projectType, | ||
}: { | ||
organizationId: string; | ||
name: string; | ||
projectType: ProjectType; | ||
}) { | ||
invariant(organizationId, 'Organization ID is required'); | ||
invariant(typeof name === 'string', 'Name is required'); | ||
invariant(projectType === 'local' || projectType === 'remote', 'Project type is required'); | ||
|
||
const user = await models.userSession.getOrCreate(); | ||
const sessionId = user.id; | ||
invariant(sessionId, 'User must be logged in to create a project'); | ||
|
||
if (projectType === 'local') { | ||
const project = await models.project.create({ | ||
name, | ||
parentId: organizationId, | ||
}); | ||
return project._id; | ||
} | ||
|
||
try { | ||
const newCloudProject = await insomniaFetch<{ | ||
id: string; | ||
name: string; | ||
} | { | ||
error: string; | ||
message?: string; | ||
}>({ | ||
path: `/v1/organizations/${organizationId}/team-projects`, | ||
method: 'POST', | ||
data: { | ||
name, | ||
}, | ||
sessionId, | ||
}); | ||
|
||
if (!newCloudProject || 'error' in newCloudProject) { | ||
let error = 'An unexpected error occurred while creating the project. Please try again.'; | ||
if (newCloudProject.error === 'FORBIDDEN') { | ||
error = newCloudProject.error; | ||
} | ||
|
||
if (newCloudProject.error === 'NEEDS_TO_UPGRADE') { | ||
error = 'Upgrade your account in order to create new Cloud Projects.'; | ||
} | ||
|
||
if (newCloudProject.error === 'PROJECT_STORAGE_RESTRICTION') { | ||
error = newCloudProject.message ?? 'The owner of the organization allows only Local Vault project creation.'; | ||
} | ||
|
||
throw new Error(error); | ||
} | ||
|
||
const project = await models.project.create({ | ||
_id: newCloudProject.id, | ||
name: newCloudProject.name, | ||
remoteId: newCloudProject.id, | ||
parentId: organizationId, | ||
}); | ||
|
||
return project._id; | ||
} catch (err) { | ||
throw new Error(err instanceof Error ? err.message : `An unexpected error occurred while creating the project. Please try again. ${err}`); | ||
} | ||
} | ||
|
||
export default ProjectEditModal; |
Oops, something went wrong.