From 9a2d2bfb6cc86457eaeec396f01d7a43f02ad159 Mon Sep 17 00:00:00 2001 From: yaoweiprc <6896642+yaoweiprc@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:17:21 +0800 Subject: [PATCH] Refine the modal for creating and editing project (#9423) * tmp * split creat and edit project form * Update style * tmp * Create button wording * feat: project type select (#9436) * feat: project type select * update * Use ProjectTypeSelect component * fix: edit form ts * Extract git-repo-form * feat: git connection info (#9441) * feat: git connection info * update * fix: lint * fix: ts * fix: delete load git repo * comment out branch * tmp * git repo scan result * feat: update git repo name directly * feat: update git repo name directly (#9456) * fix lint issue * remove unused props * fix: smoke test * Disable connected git repo options when creating new git project. * refactor: improve layout and styling in project modal and create form * refactor: adjust spacing and layout in project forms and type select component --------- Co-authored-by: Curry Yang <163384738+CurryYangxx@users.noreply.github.com> Co-authored-by: Curry Yang <1019yanglu@gmail.com> Co-authored-by: James Gatz --- .../smoke/dashboard-interactions.test.ts | 1 + .../insomnia/src/basic-components/banner.tsx | 21 +- .../insomnia/src/basic-components/divider.tsx | 9 + .../insomnia/src/basic-components/link.tsx | 13 +- .../insomnia/src/basic-components/utils.ts | 2 +- packages/insomnia/src/models/project.ts | 16 -- .../src/routes/git.all-connected-repos.tsx | 31 +++ ...ganizationId.project.$projectId._index.tsx | 2 +- .../dropdowns/git-project-sync-dropdown.tsx | 7 - .../github-repository-select.tsx | 38 ++- .../github-repository-settings-form.tsx | 26 +- .../src/ui/components/git/connection-info.tsx | 32 +++ .../ui/components/git/git-provider-tag.tsx | 18 ++ .../git-project-repository-settings-modal.tsx | 2 +- .../ui/components/modals/project-modal.tsx | 42 ++- .../ui/components/panes/no-project-view.tsx | 22 +- .../ui/components/project/git-repo-form.tsx | 136 ++++++++++ .../project/git-repo-scan-result.tsx | 102 +++++++ .../project/project-create-form.tsx | 255 ++++++++++++++++++ .../project/project-settings-form.tsx | 188 +++---------- .../project/project-type-select.tsx | 113 ++++++++ .../project/project-type-warning.tsx | 83 ++++++ .../src/ui/components/project/utils.tsx | 26 ++ 23 files changed, 957 insertions(+), 228 deletions(-) create mode 100644 packages/insomnia/src/basic-components/divider.tsx create mode 100644 packages/insomnia/src/routes/git.all-connected-repos.tsx create mode 100644 packages/insomnia/src/ui/components/git/connection-info.tsx create mode 100644 packages/insomnia/src/ui/components/git/git-provider-tag.tsx create mode 100644 packages/insomnia/src/ui/components/project/git-repo-form.tsx create mode 100644 packages/insomnia/src/ui/components/project/git-repo-scan-result.tsx create mode 100644 packages/insomnia/src/ui/components/project/project-create-form.tsx create mode 100644 packages/insomnia/src/ui/components/project/project-type-select.tsx create mode 100644 packages/insomnia/src/ui/components/project/project-type-warning.tsx create mode 100644 packages/insomnia/src/ui/components/project/utils.tsx diff --git a/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts index 751d8a1941..31768dbb67 100644 --- a/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts @@ -12,6 +12,7 @@ test.describe('Dashboard', () => { // Create new project await page.getByRole('button', { name: 'Create new Project' }).click(); + await page.getByLabel('Project Type: local').click(); await page.getByRole('button', { name: 'Create', exact: true }).click(); // Check empty project diff --git a/packages/insomnia/src/basic-components/banner.tsx b/packages/insomnia/src/basic-components/banner.tsx index 95aa788c9d..b6e69be98a 100644 --- a/packages/insomnia/src/basic-components/banner.tsx +++ b/packages/insomnia/src/basic-components/banner.tsx @@ -6,6 +6,7 @@ import { Icon } from './icon'; interface BannerProps { type: 'info' | 'warning'; message: React.ReactNode; + footer?: React.ReactNode; title?: string; className?: string; } @@ -15,20 +16,16 @@ const bannerTypeToIconName: Record = { }; const bannerTypeToBgColor: Record = { info: 'bg-(--color-surprise)', - warning: 'bg-(--color-warning)', + warning: 'bg-(--color-warning)/50', }; -export const Banner = ({ type, title, message, className }: BannerProps) => { +export const Banner = ({ type, title, message, footer, className }: BannerProps) => { return ( -
- -
- {title &&
{title}
} -
{message}
+
+ +
+ {title &&
{title}
} +
{message}
+ {footer &&
{footer}
}
); diff --git a/packages/insomnia/src/basic-components/divider.tsx b/packages/insomnia/src/basic-components/divider.tsx new file mode 100644 index 0000000000..53ded77dfb --- /dev/null +++ b/packages/insomnia/src/basic-components/divider.tsx @@ -0,0 +1,9 @@ +import { twMerge } from 'tailwind-merge'; + +interface DividerProps { + className?: string; +} + +export const Divider = ({ className }: DividerProps) => { + return
; +}; diff --git a/packages/insomnia/src/basic-components/link.tsx b/packages/insomnia/src/basic-components/link.tsx index 889f4bfc24..03df03f0d2 100644 --- a/packages/insomnia/src/basic-components/link.tsx +++ b/packages/insomnia/src/basic-components/link.tsx @@ -1,25 +1,18 @@ import { twMerge } from 'tailwind-merge'; -import { Icon } from './icon'; - interface LearnMoreLinkProps { href: string; - children: React.ReactNode; + children?: React.ReactNode; className?: string; } -export const LearnMoreLink = ({ href, children, className }: LearnMoreLinkProps) => { +export const LearnMoreLink = ({ href, children = 'Learn more ↗', className }: LearnMoreLinkProps) => { return ( {children} - ); }; diff --git a/packages/insomnia/src/basic-components/utils.ts b/packages/insomnia/src/basic-components/utils.ts index 25422ff0ae..17ce0d1b1b 100644 --- a/packages/insomnia/src/basic-components/utils.ts +++ b/packages/insomnia/src/basic-components/utils.ts @@ -25,7 +25,7 @@ export function getBorderColorClasses(color: ButtonColor) { return { primary: '', danger: '', - default: 'border border-[--hl]', + default: 'border border-(--hl-md)', }[color]; } diff --git a/packages/insomnia/src/models/project.ts b/packages/insomnia/src/models/project.ts index 79c7cf0da4..6a954d5c67 100644 --- a/packages/insomnia/src/models/project.ts +++ b/packages/insomnia/src/models/project.ts @@ -155,22 +155,6 @@ export function getDefaultProjectStorageType( return 'local'; } -export function isSwitchingStorageType(project: Project, storageType: 'local' | 'remote' | 'git') { - if (storageType === 'git' && !isGitProject(project)) { - return true; - } - - if (storageType === 'local' && (isRemoteProject(project) || isGitProject(project))) { - return true; - } - - if (storageType === 'remote' && !isRemoteProject(project)) { - return true; - } - - return false; -} - export function getProjectStorageTypeLabel(storageRules: StorageRules): string { const storageTypes = { 'Cloud Sync': storageRules.enableCloudSync, diff --git a/packages/insomnia/src/routes/git.all-connected-repos.tsx b/packages/insomnia/src/routes/git.all-connected-repos.tsx new file mode 100644 index 0000000000..7d8aa35af7 --- /dev/null +++ b/packages/insomnia/src/routes/git.all-connected-repos.tsx @@ -0,0 +1,31 @@ +import { href } from 'react-router'; + +import * as models from '~/models'; +import { isEmptyGitProject } from '~/models/project'; +import { createFetcherLoadHook } from '~/utils/router'; + +export async function clientLoader() { + const allProjects = await models.project.all(); + const allConnectedGitProjects = allProjects.filter( + project => models.project.isGitProject(project) && !isEmptyGitProject(project), + ); + const gitRepoURIProjectNameMap: Record = {}; + await Promise.all( + allConnectedGitProjects.map(async ({ gitRepositoryId, name }) => { + if (gitRepositoryId) { + const gitRepository = await models.gitRepository.getById(gitRepositoryId); + if (gitRepository) { + gitRepoURIProjectNameMap[gitRepository.uri] = name; + } + } + }), + ); + return gitRepoURIProjectNameMap; +} + +export const useAllConnectedReposLoaderFetcher = createFetcherLoadHook( + load => () => { + return load(`${href('/git/all-connected-repos')}`); + }, + clientLoader, +); diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx index 25f442c956..09c7669297 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx @@ -84,7 +84,7 @@ import { insomniaFetch } from '~/ui/insomnia-fetch'; import { DEFAULT_STORAGE_RULES } from '~/ui/organization-utils'; import { invariant } from '~/utils/invariant'; -type ProjectScopeKeys = WorkspaceScope | 'unsynced'; +export type ProjectScopeKeys = WorkspaceScope | 'unsynced'; export const scopeToLabelMap: Record< ProjectScopeKeys, 'Document' | 'Collection' | 'Mock Server' | 'Unsynced' | 'Environment' | 'MCP Client' diff --git a/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx index e35472a6d6..3399c4e47c 100644 --- a/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx @@ -422,13 +422,6 @@ export const GitProjectSyncDropdown: FC = ({ gitRepository }) => { isDisabled?: boolean; action: () => void; }[] = [ - { - id: 'repository-settings', - label: 'Repository Settings', - isDisabled: false, - icon: 'wrench', - action: () => setIsGitRepoSettingsModalOpen(true), - }, { id: 'branches', label: 'Branches', diff --git a/packages/insomnia/src/ui/components/git-credentials/github-repository-select.tsx b/packages/insomnia/src/ui/components/git-credentials/github-repository-select.tsx index 8013f8d57f..339fbaf32d 100644 --- a/packages/insomnia/src/ui/components/git-credentials/github-repository-select.tsx +++ b/packages/insomnia/src/ui/components/git-credentials/github-repository-select.tsx @@ -8,7 +8,15 @@ import { GitRemoteBranchSelect } from './git-remote-branch-select'; type GitHubRepository = Awaited>['repos'][number]; -export const GitHubRepositorySelect = ({ uri, token }: { uri?: string; token: string }) => { +export const GitHubRepositorySelect = ({ + uri, + token, + allConnectedRepoURIProjectNameMap, +}: { + uri?: string; + token: string; + allConnectedRepoURIProjectNameMap?: Record | undefined; +}) => { const [loading, setLoading] = useState(false); const [repositories, setRepositories] = useState([]); const [selectedRepository, setSelectedRepository] = useState(null); @@ -118,14 +126,26 @@ export const GitHubRepositorySelect = ({ uri, token }: { uri?: string; token: st id: string; name: string; }> className="flex min-w-max flex-col p-2 text-sm select-none focus:outline-hidden"> - {item => ( - - {item.name} - - )} + {item => { + const isDisabled = + allConnectedRepoURIProjectNameMap && + Object.prototype.hasOwnProperty.call(allConnectedRepoURIProjectNameMap, item.id); + return ( + + {isDisabled && } + {item.name} + {isDisabled && ( + + Already connected to: {allConnectedRepoURIProjectNameMap[item.id]} + + )} + + ); + }} diff --git a/packages/insomnia/src/ui/components/git-credentials/github-repository-settings-form.tsx b/packages/insomnia/src/ui/components/git-credentials/github-repository-settings-form.tsx index f5915a1d83..b2b1e360ee 100644 --- a/packages/insomnia/src/ui/components/git-credentials/github-repository-settings-form.tsx +++ b/packages/insomnia/src/ui/components/git-credentials/github-repository-settings-form.tsx @@ -15,10 +15,11 @@ import { GitHubRepositorySelect } from './github-repository-select'; interface Props { uri?: string; onSubmit: (args: Partial) => void; + allConnectedRepoURIProjectNameMap?: Record | undefined; } export const GitHubRepositorySetupFormGroup = (props: Props) => { - const { onSubmit, uri } = props; + const { onSubmit, uri, allConnectedRepoURIProjectNameMap } = props; const githubTokenLoader = useGitHubCredentialsFetcher(); useEffect(() => { @@ -33,7 +34,14 @@ export const GitHubRepositorySetupFormGroup = (props: Props) => { return ; } - return ; + return ( + + ); }; const Avatar = ({ src }: { src: string }) => { @@ -68,9 +76,15 @@ interface GitHubRepositoryFormProps { uri?: string; onSubmit: (args: Partial) => void; credentials: GitCredentials; + allConnectedRepoURIProjectNameMap?: Record | undefined; } -const GitHubRepositoryForm = ({ uri, credentials, onSubmit }: GitHubRepositoryFormProps) => { +const GitHubRepositoryForm = ({ + uri, + credentials, + onSubmit, + allConnectedRepoURIProjectNameMap, +}: GitHubRepositoryFormProps) => { const [error, setError] = useState(''); const signOutFetcher = useGithubSignOutFetcher(); @@ -116,7 +130,11 @@ const GitHubRepositoryForm = ({ uri, credentials, onSubmit }: GitHubRepositoryFo Disconnect
- + {error && (

- onOpenChange(false)} - /> + {project ? ( + + ) : ( + + )} )} diff --git a/packages/insomnia/src/ui/components/panes/no-project-view.tsx b/packages/insomnia/src/ui/components/panes/no-project-view.tsx index 5aa62902b9..269ae76949 100644 --- a/packages/insomnia/src/ui/components/panes/no-project-view.tsx +++ b/packages/insomnia/src/ui/components/panes/no-project-view.tsx @@ -2,7 +2,7 @@ import React, { type FC } from 'react'; import type { StorageRules } from '~/models/organization'; -import { ProjectSettingsForm } from '../project/project-settings-form'; +import { ProjectCreateForm } from '../project/project-create-form'; interface Props { storageRules: StorageRules; @@ -11,14 +11,18 @@ interface Props { export const NoProjectView: FC = ({ storageRules, isGitSyncEnabled }) => { return ( -
- Welcome to your organization! - Create a new project to get started - +
+
+

Welcome to your organization!

+

Create a new project to get started

+
+ +
+
); }; diff --git a/packages/insomnia/src/ui/components/project/git-repo-form.tsx b/packages/insomnia/src/ui/components/project/git-repo-form.tsx new file mode 100644 index 0000000000..6771c2ae38 --- /dev/null +++ b/packages/insomnia/src/ui/components/project/git-repo-form.tsx @@ -0,0 +1,136 @@ +import { type FC, useEffect } from 'react'; +import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; + +import { isGitCredentialsOAuth } from '~/models/git-repository'; +import { useAllConnectedReposLoaderFetcher } from '~/routes/git.all-connected-repos'; +import type { useGitProjectInitCloneActionFetcher } from '~/routes/git.init-clone'; + +import type { OauthProviderName } from '../../../models/git-credentials'; +import type { GitRepository } from '../../../models/git-repository'; +import { ErrorBoundary } from '../error-boundary'; +import { CustomRepositorySettingsFormGroup } from '../git-credentials/custom-repository-settings-form'; +import { GitHubRepositorySetupFormGroup } from '../git-credentials/github-repository-settings-form'; +import { GitLabRepositorySetupFormGroup } from '../git-credentials/gitlab-repository-settings-form'; +import type { ActiveView, ProjectData } from './utils'; + +interface Props { + setProjectData: React.Dispatch>; + projectData: ProjectData; + initCloneGitRepositoryFetcher: ReturnType; + organizationId: string; + setActiveView: React.Dispatch>; + selectedTab: OauthProviderName; + setTab: React.Dispatch>; +} + +export const GitRepoForm: FC = ({ + setProjectData, + projectData, + initCloneGitRepositoryFetcher, + organizationId, + setActiveView, + selectedTab, + setTab, +}) => { + const onGitRepoFormSubmit = (gitRepositoryPatch: Partial) => { + const { author, credentials, created, modified, isPrivate, needsFullClone, uriNeedsMigration, ...repoPatch } = + gitRepositoryPatch; + + setProjectData({ + ...projectData, + ...credentials, + authorName: author?.name || '', + authorEmail: author?.email || '', + uri: repoPatch.uri, + ref: repoPatch.ref, + }); + + initCloneGitRepositoryFetcher.submit({ + ...repoPatch, + authorName: author?.name || '', + authorEmail: author?.email || '', + ...(credentials + ? isGitCredentialsOAuth(credentials) + ? { + credentials: { + token: credentials.token || '', + oauth2format: credentials.oauth2format || 'github', + username: credentials.username || '', + }, + } + : { + credentials, + } + : { + credentials: { + password: '', + username: '', + }, + }), + uri: repoPatch.uri || '', + organizationId, + }); + + setActiveView('git-results'); + }; + const allConnectedReposLoaderFetcher = useAllConnectedReposLoaderFetcher(); + const allConnectedReposLoaderFetcherLoad = allConnectedReposLoaderFetcher.load; + useEffect(() => { + allConnectedReposLoaderFetcherLoad(); + }, [allConnectedReposLoaderFetcherLoad]); + const allConnectedRepoURIProjectNameMap = allConnectedReposLoaderFetcher.data; + return ( + + { + setTab(key as OauthProviderName); + }} + aria-label="Git repository settings tabs" + className="flex h-full w-full flex-col" + > + + +
+ GitHub +
+
+ +
+ GitLab +
+
+ +
+ Git +
+
+
+ + + + + + + + + +
+
+ ); +}; diff --git a/packages/insomnia/src/ui/components/project/git-repo-scan-result.tsx b/packages/insomnia/src/ui/components/project/git-repo-scan-result.tsx new file mode 100644 index 0000000000..67dea647f3 --- /dev/null +++ b/packages/insomnia/src/ui/components/project/git-repo-scan-result.tsx @@ -0,0 +1,102 @@ +import classNames from 'classnames'; +import type { FC } from 'react'; + +import { Banner } from '~/basic-components/banner'; +import { Icon } from '~/basic-components/icon'; +import { LearnMoreLink } from '~/basic-components/link'; +import type { useGitProjectInitCloneActionFetcher } from '~/routes/git.init-clone'; +import { + type ProjectScopeKeys, + scopeToIconMap, + scopeToLabelMap, +} from '~/routes/organization.$organizationId.project.$projectId._index'; + +interface Props { + initCloneGitRepositoryFetcher: ReturnType; + insomniaFiles: + | Extract['data'], { files: any }>['files'] + | undefined; + repoURI?: string; +} + +export const GitRepoScanResult: FC = ({ initCloneGitRepositoryFetcher, insomniaFiles, repoURI }) => { + const fileTypeCountMap: Partial> = {}; + insomniaFiles?.forEach(({ scope }) => { + if (!fileTypeCountMap[scope]) { + fileTypeCountMap[scope] = 0; + } + fileTypeCountMap[scope]++; + }); + return ( + <> +
+

Insomnia files in repo

+

{repoURI}

+ {initCloneGitRepositoryFetcher.state !== 'idle' ? ( +
+

+ + Scanning remote repo for Insomnia files... +

+
+ ) : insomniaFiles?.length === 0 ? ( +
+

+ + No Insomnia files found − let’s start something new! + + There were no Insomnia files in the selected repo or branch, so you’ll begin with a blank project locally. + When you commit and push changes, they will be available on the remote repo selected. +

+
+ ) : ( +
+ + + + + + + + + {Object.keys(fileTypeCountMap) + .sort() + .map((scope, idx) => ( + + + + + ))} + +
CountFile type
+ {fileTypeCountMap[scope as ProjectScopeKeys]} + + + {scopeToLabelMap[scope as ProjectScopeKeys]} +
+
+ )} +
+ {/* TODO: update learn more link */} + {insomniaFiles && insomniaFiles?.some(file => file.path === '.insomnia') && ( + + There are out of date Insomnia project files in the selected repo. By cloning this project, outdated files + will automatically be migrated to the latest version. + + } + title="Migrate legacy files?" + className="text-left" + /> + )} + + ); +}; diff --git a/packages/insomnia/src/ui/components/project/project-create-form.tsx b/packages/insomnia/src/ui/components/project/project-create-form.tsx new file mode 100644 index 0000000000..12f1ce9000 --- /dev/null +++ b/packages/insomnia/src/ui/components/project/project-create-form.tsx @@ -0,0 +1,255 @@ +import classNames from 'classnames'; +import type { FC } from 'react'; +import React, { useEffect, useState } from 'react'; +import { Button, Checkbox, Input, Label, TextField } from 'react-aria-components'; +import { useParams } from 'react-router'; + +import { type StorageRules } from '~/models/organization'; +import { useGitProjectInitCloneActionFetcher } from '~/routes/git.init-clone'; +import { + fallbackFeatures, + useOrganizationPermissionsLoaderFetcher, +} from '~/routes/organization.$organizationId.permissions'; +import { useProjectNewActionFetcher } from '~/routes/organization.$organizationId.project.new'; +import { GitRepoForm } from '~/ui/components/project/git-repo-form'; +import { GitRepoScanResult } from '~/ui/components/project/git-repo-scan-result'; +import { ProjectTypeSelect } from '~/ui/components/project/project-type-select'; +import { ProjectTypeWarning } from '~/ui/components/project/project-type-warning'; +import { type ProjectData, type ProjectType, useActiveView } from '~/ui/components/project/utils'; +import { useLoaderDeferData } from '~/ui/hooks/use-loader-defer-data'; + +import type { OauthProviderName } from '../../../models/git-credentials'; +import { Icon } from '../icon'; + +interface Props { + storageRules: StorageRules; + isGitSyncEnabled: boolean; + defaultProjectName?: string; + onCancel?(): void; + activeViewObj?: ReturnType; +} + +export const ProjectCreateForm: FC = ({ + storageRules, + isGitSyncEnabled, + defaultProjectName = 'My Project', + onCancel, + activeViewObj, +}) => { + const { organizationId } = useParams() as { organizationId: string }; + + // Reload isGitSyncEnabled everytime this component is mounted + const permissionsFetcher = useOrganizationPermissionsLoaderFetcher({ key: `permissions:${organizationId}` }); + const permissionsFetcherLoad = permissionsFetcher.load; + useEffect(() => { + permissionsFetcherLoad({ + organizationId, + }); + }, [organizationId, permissionsFetcherLoad]); + const { featuresPromise } = permissionsFetcher.data || {}; + const [features = fallbackFeatures] = useLoaderDeferData(featuresPromise, organizationId); + isGitSyncEnabled = features.gitSync.enabled; + + const [storageType, setStorageType] = useState(); + + let { activeView, setActiveView } = useActiveView(); + if (activeViewObj) { + activeView = activeViewObj.activeView; + setActiveView = activeViewObj.setActiveView; + } + + const [selectedTab, setTab] = useState('github'); + + const [error, setError] = useState(null); + + const [projectData, setProjectData] = useState({ + name: defaultProjectName, + authorName: '', + authorEmail: '', + uri: '', + username: '', + password: '', + token: '', + oauth2format: undefined, + connectRepositoryLater: false, + }); + + const initCloneGitRepositoryFetcher = useGitProjectInitCloneActionFetcher(); + const newProjectFetcher = useProjectNewActionFetcher(); + + const insomniaFiles = + initCloneGitRepositoryFetcher.data && 'files' in initCloneGitRepositoryFetcher.data + ? initCloneGitRepositoryFetcher.data.files + : []; + + useEffect(() => { + if (newProjectFetcher.state === 'idle' && newProjectFetcher.data && newProjectFetcher.data?.error) { + setError(newProjectFetcher.data.error); + } + }, [newProjectFetcher.data, newProjectFetcher.state]); + + const onUpsertProject = () => { + if (!storageType) { + return; + } + newProjectFetcher.submit({ + organizationId, + projectData: { + ...projectData, + storageType, + }, + }); + }; + + return ( +
+ {error && ( +
+ + {error} +
+ )} + +
+
+ setProjectData({ ...projectData, name })} + className="group relative flex flex-col gap-2 px-0.5" + > + + + + setStorageType(v as ProjectType)} + /> + + {storageType === 'git' && ( + <> + setProjectData(prev => ({ ...prev, connectRepositoryLater: isSelected }))} + className="group mt-4 flex h-full items-center gap-2 p-0 pl-[1px]" + > +
+ +
+ Connect repository later +
+ {!projectData.connectRepositoryLater && ( + + )} + + )} +
+
+
+ {onCancel && ( + + )} + {storageType !== 'git' || projectData.connectRepositoryLater ? ( + + ) : ( + + )} +
+
+
+ +
+ +
+ + + {initCloneGitRepositoryFetcher.state !== 'idle' ? ( + + ) : ( + + )} +
+
+
+ ); +}; diff --git a/packages/insomnia/src/ui/components/project/project-settings-form.tsx b/packages/insomnia/src/ui/components/project/project-settings-form.tsx index e0af75e18e..84780ca3e4 100644 --- a/packages/insomnia/src/ui/components/project/project-settings-form.tsx +++ b/packages/insomnia/src/ui/components/project/project-settings-form.tsx @@ -1,4 +1,3 @@ -import classNames from 'classnames'; import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; import { @@ -9,8 +8,6 @@ import { Heading, Input, Label, - Radio, - RadioGroup, Row, Tab, Table, @@ -23,30 +20,24 @@ import { } from 'react-aria-components'; import { useParams } from 'react-router'; -import { getAppWebsiteBaseURL } from '~/common/constants'; -import { docsPricingLearnMoreLink } from '~/common/documentation'; +import { Divider } from '~/basic-components/divider'; import { isGitCredentialsOAuth } from '~/models/git-repository'; -import { isOwnerOfOrganization, type StorageRules } from '~/models/organization'; -import { useRootLoaderData } from '~/root'; +import type { StorageRules } from '~/models/organization'; import { useGitProjectInitCloneActionFetcher } from '~/routes/git.init-clone'; -import { useOrganizationLoaderData } from '~/routes/organization'; import { fallbackFeatures, useOrganizationPermissionsLoaderFetcher, } from '~/routes/organization.$organizationId.permissions'; import { useProjectNewActionFetcher } from '~/routes/organization.$organizationId.project.new'; -import { useIsLightTheme } from '~/ui/hooks/theme'; +import { GitConnectionInfo } from '~/ui/components/git/connection-info'; +import { ProjectTypeSelect } from '~/ui/components/project/project-type-select'; +import { ProjectTypeWarning } from '~/ui/components/project/project-type-warning'; +import { useActiveView } from '~/ui/components/project/utils'; import { useLoaderDeferData } from '~/ui/hooks/use-loader-defer-data'; import type { OauthProviderName } from '../../../models/git-credentials'; import type { GitRepository } from '../../../models/git-repository'; -import { - getDefaultProjectStorageType, - getProjectStorageTypeLabel, - isGitProject, - isRemoteProject, - type Project, -} from '../../../models/project'; +import { getDefaultProjectStorageType, isGitProject, isRemoteProject, type Project } from '../../../models/project'; import { scopeToBgColorMap, scopeToIconMap, @@ -112,9 +103,9 @@ export const ProjectSettingsForm: FC = ({ const [storageType, setStorageType] = useState<'local' | 'remote' | 'git'>( getDefaultProjectStorageType(storageRules, project), ); - const [activeView, setActiveView] = useState<'project' | 'git-clone' | 'git-results' | 'switch-storage-type'>( - 'project', - ); + + const { activeView, setActiveView } = useActiveView(); + const [selectedTab, setTab] = useState('github'); const [error, setError] = useState(null); @@ -150,8 +141,6 @@ export const ProjectSettingsForm: FC = ({ const updateProjectFetcher = useProjectUpdateActionFetcher(); const newProjectFetcher = useProjectNewActionFetcher(); - const showStorageRestrictionMessage = - !storageRules.enableCloudSync || !storageRules.enableLocalVault || !storageRules.enableGitSync; const insomniaFiles = initCloneGitRepositoryFetcher.data && 'files' in initCloneGitRepositoryFetcher.data ? initCloneGitRepositoryFetcher.data.files @@ -232,27 +221,11 @@ export const ProjectSettingsForm: FC = ({ storageType, }, }); - } else { - newProjectFetcher.submit({ - organizationId, - projectData: { - ...projectData, - storageType, - }, - }); } }; - const organizationData = useOrganizationLoaderData(); - const { userSession } = useRootLoaderData()!; - const organization = organizationData?.organizations.find(o => o.id === organizationId); - const isUserOwner = - organization && userSession.accountId && isOwnerOfOrganization({ organization, accountId: userSession.accountId }); - - const isLightTheme = useIsLightTheme(); - return ( -
+
{error && (
@@ -262,7 +235,7 @@ export const ProjectSettingsForm: FC = ({ {activeView === 'project' && ( <> -
+
= ({ onChange={name => setProjectData({ ...projectData, name })} className="group relative flex flex-col gap-2 px-0.5" > - + - { - error && setError(null); - setStorageType(value as 'local' | 'remote' | 'git'); - }} + - -
- -
- - Local Vault -
-

Stored locally only, with no cloud. Ideal when collaboration is not needed.

-
- - -
- - Cloud Sync -
-

- Encrypted and synced securely to the cloud, ideal for out of the box collaboration. -

-
- -
- - Git Sync -
-

- Stored locally and synced to a Git repository. Ideal for version control and collaboration. -

-
-
- {storageType === 'git' && !isGitSyncEnabled && ( -
- -
- - Git Sync limited to organizations of 3 or fewer users - - {isUserOwner ? ( - <> -

- Git Sync is included on your plan for up to 3 users. Since your team is larger, you’ll need to - upgrade your plan to use it.{' '} - - Learn more ↗ - -

- - Upgrade - - - ) : ( - <> -

- Git Sync is included on your plan for up to 3 users. Because your team is larger, your admin - will need to upgrade the plan for you to access it. -

- - Learn More ↗ - - - )} -
-
- )} -
- {showStorageRestrictionMessage && ( -
- - - The organization owner mandates that projects must be created and stored using{' '} - {getProjectStorageTypeLabel(storageRules)}. - -
- )} + onChange={v => setStorageType(v as 'local' | 'remote' | 'git')} + /> +
+ {storageType === 'git' && ( + <> + + + + )}
{onCancel && ( @@ -392,7 +276,7 @@ export const ProjectSettingsForm: FC = ({ Cancel )} - {storageType === 'git' && ( + {storageType === 'git' && isSwitchingStorageType(project!, storageType) && ( )} + {storageType === 'git' && !isSwitchingStorageType(project!, storageType) && ( + + )} {storageType !== 'git' && ( )}
diff --git a/packages/insomnia/src/ui/components/project/project-type-select.tsx b/packages/insomnia/src/ui/components/project/project-type-select.tsx new file mode 100644 index 0000000000..c575acc0cf --- /dev/null +++ b/packages/insomnia/src/ui/components/project/project-type-select.tsx @@ -0,0 +1,113 @@ +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { useState } from 'react'; +import { Label, Radio, RadioGroup } from 'react-aria-components'; + +import { Icon } from '~/basic-components/icon'; +import type { StorageRules } from '~/models/organization'; +import type { ProjectType } from '~/ui/components/project/utils'; + +interface ProjectTypeItem { + type: ProjectType; + icon: IconProp; + name: string; + description: string; + isDisabled: boolean; +} + +const TypeItem = ({ icon, name, description }: Omit) => { + return ( +
+ +
+
{name}
+
{description}
+
+
+ ); +}; + +interface Props { + value?: ProjectTypeItem['type']; + onChange: (value: string) => void; + storageRules: StorageRules; +} +export const ProjectTypeSelect = ({ value, onChange, storageRules }: Props) => { + const [listOpen, setListOpen] = useState(false); + const typeList: ProjectTypeItem[] = [ + { + type: 'local', + icon: 'laptop', + name: 'Local Vault', + description: 'For working alone with data stored on your machine.', + isDisabled: !storageRules.enableLocalVault, + }, + { + type: 'remote', + icon: 'globe', + name: 'Cloud Sync', + description: 'Out of the box collaboration with data stored securely to the cloud.', + isDisabled: !storageRules.enableCloudSync, + }, + { + type: 'git', + icon: ['fab', 'git-alt'], + name: 'Git Sync', + description: 'Collaborate with others securely using your existing git provider.', + isDisabled: !storageRules.enableGitSync, + }, + ]; + + const currentType = typeList.find(item => item.type === value); + + const handleChange = (v: string) => { + setListOpen(false); + onChange(v); + }; + + return ( +
+ + {listOpen || !currentType ? ( + +
+ {typeList.map(item => ( + + {({ isHovered, isSelected }) => ( +
+ +
+ )} +
+ ))} +
+
+ ) : ( +
setListOpen(true)} + > +
+ + {currentType?.name} +
+
Change
+
+ )} +
+ ); +}; diff --git a/packages/insomnia/src/ui/components/project/project-type-warning.tsx b/packages/insomnia/src/ui/components/project/project-type-warning.tsx new file mode 100644 index 0000000000..42a274f939 --- /dev/null +++ b/packages/insomnia/src/ui/components/project/project-type-warning.tsx @@ -0,0 +1,83 @@ +import { useParams } from 'react-router'; + +import { Banner } from '~/basic-components/banner'; +import { Button } from '~/basic-components/button'; +import { LearnMoreLink } from '~/basic-components/link'; +import { getAppWebsiteBaseURL } from '~/common/constants'; +import { docsPricingLearnMoreLink } from '~/common/documentation'; +import { isOwnerOfOrganization, type StorageRules } from '~/models/organization'; +import { getProjectStorageTypeLabel } from '~/models/project'; +import { useRootLoaderData } from '~/root'; +import { useOrganizationLoaderData } from '~/routes/organization'; +import type { ProjectType } from '~/ui/components/project/utils'; +import { useIsLightTheme } from '~/ui/hooks/theme'; + +interface Props { + isGitSyncEnabled: boolean; + storageType?: ProjectType; + storageRules: StorageRules; +} +export const ProjectTypeWarning = ({ isGitSyncEnabled, storageType, storageRules }: Props) => { + const isLightTheme = useIsLightTheme(); + const showStorageRestrictionMessage = + !storageRules.enableCloudSync || !storageRules.enableLocalVault || !storageRules.enableGitSync; + const organizationData = useOrganizationLoaderData(); + const { userSession } = useRootLoaderData()!; + const { organizationId } = useParams() as { organizationId: string }; + const organization = organizationData?.organizations.find(o => o.id === organizationId); + // TODO: extract to a hook later + const isUserOwner = + organization && userSession.accountId && isOwnerOfOrganization({ organization, accountId: userSession.accountId }); + return ( + <> + {storageType === 'git' && + !isGitSyncEnabled && + (isUserOwner ? ( + + Git Sync is included on your plan for up to 3 users. Since your team is larger, you’ll need to upgrade + your plan to use it. +
+ } + footer={ + + } + /> + ) : ( + + Git Sync is included on your plan for up to 3 users. Because your team is larger, your admin will need + to upgrade the plan for you to access it. +
+ } + footer={} + /> + ))} + {showStorageRestrictionMessage && ( + + The organization owner mandates that projects must be created and stored using{' '} + {getProjectStorageTypeLabel(storageRules)}. + + } + /> + )} + + ); +}; diff --git a/packages/insomnia/src/ui/components/project/utils.tsx b/packages/insomnia/src/ui/components/project/utils.tsx new file mode 100644 index 0000000000..9d705b7a7d --- /dev/null +++ b/packages/insomnia/src/ui/components/project/utils.tsx @@ -0,0 +1,26 @@ +import { useState } from 'react'; + +import type { OauthProviderName } from '../../../models/git-credentials'; + +// TODO: remove unused view value +export type ActiveView = 'project' | 'git-results' | 'git-clone' | 'switch-storage-type'; + +export function useActiveView() { + const [activeView, setActiveView] = useState('project'); + return { activeView, setActiveView }; +} + +export interface ProjectData { + name: string; + authorName?: string; + authorEmail?: string; + uri?: string; + ref?: string; + username?: string; + password?: string; + token?: string; + oauth2format?: OauthProviderName; + connectRepositoryLater?: boolean; +} + +export type ProjectType = 'local' | 'remote' | 'git';