Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/insomnia/src/domains/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useCallback } from 'react';
import { useFetcher } from 'react-router';
import { href } from 'react-router';

type VoidIfUndefined<T> = T extends void ? void : T;

export function useDomainAction<ActionTypes extends Record<string, any>, T extends keyof ActionTypes>(
action: T,
path: string,
) {
const fetcher = useFetcher();
const submit = useCallback(
(payload: VoidIfUndefined<Parameters<ActionTypes[T]>[0]>) => {
console.log('Submitting action', href(path, payload), payload);
return fetcher.submit(JSON.stringify({ action, payload }), {
method: 'POST',
action: href(path, payload),
encType: 'application/json',
});
},
[fetcher.submit],

Check warning on line 21 in packages/insomnia/src/domains/base.ts

View workflow job for this annotation

GitHub Actions / Test

React Hook useCallback has missing dependencies: 'action', 'fetcher', and 'path'. Either include them or remove the dependency array
);

return { ...fetcher, submit };
}

export function createDomainActionHandler<ActionTypes extends Record<string, any>>(actions: ActionTypes) {
return async function handleAction(request: Request) {
const { action, payload } = (await request.json()) as { action: keyof ActionTypes; payload: any };
if (typeof actions[action] !== 'function') {
throw new TypeError(`Action ${action as string} is not defined`);
}
return actions[action](payload);
};
}

export function createDomain<ActionTypes extends Record<string, any>>(actions: ActionTypes) {
let actionPath: string | undefined;
function createActionHandler(path: string) {
actionPath = path;
return createDomainActionHandler(actions);
}

function useAction<T extends keyof ActionTypes>(type: T) {
if (!actionPath) {
throw new Error('Please call createActionHandler to define client action first.');
}
return useDomainAction<ActionTypes, T>(type, actionPath);
}

return [createActionHandler, useAction] as const;
}
2 changes: 2 additions & 0 deletions packages/insomnia/src/domains/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as organization from './organization';
export * as project from './project';
34 changes: 34 additions & 0 deletions packages/insomnia/src/domains/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { services } from '~/insomnia-data';
import { fetchAndCacheOrganizationStorageRule as _syncStorageRule, syncOrganizations } from '~/ui/organization-utils';

import { createDomain } from './base';

async function sync() {
const { id: sessionId, accountId } = await services.userSession.getOrCreate();

if (sessionId) {
await syncOrganizations(sessionId, accountId);
}

return null;
}

async function fetchAndCacheOrganizationStorageRule({
organizationId,
force,
}: {
organizationId: string;
force: boolean;
}) {
// TODO: organization-utils should be moved to domain layer
return _syncStorageRule(organizationId, force);
}

const actions = {
sync,
fetchAndCacheOrganizationStorageRule,
};

const [createOrgActionHandler, useOrgAction] = createDomain(actions);

export { createOrgActionHandler, useOrgAction };
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { deleteTeamProject, isApiError } from 'insomnia-api';
import { href, redirect } from 'react-router';
import { deleteTeamProject, isApiError, updateGitProjectCount } from 'insomnia-api';
import { redirect } from 'react-router';

import { database } from '~/common/database';
import { isNotNullOrUndefined } from '~/common/misc';
import { projectLock } from '~/common/project';
import { services } from '~/insomnia-data';
import { reportGitProjectCount } from '~/routes/organization.$organizationId.project.new';
import { database, models, type Project, services } from '~/insomnia-data';
import { invariant } from '~/utils/invariant';
import { createFetcherSubmitHook, getInitialRouteForOrganization } from '~/utils/router';
import { getInitialRouteForOrganization } from '~/utils/router';

import type { Route } from './+types/organization.$organizationId.project.$projectId.delete';
import { createDomain } from './base';

export async function clientAction({ params }: Route.ClientActionArgs) {
const { organizationId, projectId } = params;
async function remove({ organizationId, projectId }: { organizationId: string; projectId: string }) {
invariant(organizationId, 'Organization ID is required');
invariant(projectId, 'Project ID is required');
const project = await services.project.getById(projectId);
Expand Down Expand Up @@ -68,21 +66,50 @@ export async function clientAction({ params }: Route.ClientActionArgs) {
}
}

export const useProjectDeleteActionFetcher = createFetcherSubmitHook(
submit =>
({ organizationId, projectId }: { organizationId: string; projectId: string }) => {
const url = href('/organization/:organizationId/project/:projectId/delete', {
async function move({ organizationId, projectId }: { organizationId: string; projectId: string }) {
invariant(typeof organizationId === 'string', 'Organization ID is required');

const project = await services.project.getById(projectId);
invariant(project, 'Project not found');

await services.project.update(project, {
parentId: organizationId,
// We move a project to another organization as local no matter what it was before
remoteId: null,
});

return null;
}

export const reportGitProjectCount = async (organizationId: string, sessionId: string, maxRetries = 3) => {
const projects = await database.find<Project>(models.project.type, {
parentId: organizationId,
});
const gitRepositoryIds = projects.map(p => p.gitRepositoryId).filter(isNotNullOrUndefined);
const gitProjectsCount = gitRepositoryIds.length;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await updateGitProjectCount({
organizationId,
projectId,
sessionId,
gitProjectsCount,
});
return;
} catch {
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
}
}
}

console.warn('Report git project count failed');
};

const actions = {
remove,
move,
};

const [createProjectActionHandler, useProjectAction] = createDomain(actions);

return submit(
{},
{
action: url,
method: 'POST',
},
);
},
clientAction,
);
export { createProjectActionHandler, useProjectAction };

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createProjectActionHandler } from '~/domains/project';

const handleAction = createProjectActionHandler('/organization/:organizationId/project');

import type { Route } from './+types/organization.$organizationId.project';

export const clientAction = async ({ request }: Route.ClientActionArgs) => {
return handleAction(request);
};
28 changes: 0 additions & 28 deletions packages/insomnia/src/routes/organization.sync.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions packages/insomnia/src/routes/organization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { href, NavLink, Outlet, useLocation, useNavigate, useParams, useRouteLoa
import * as reactUse from 'react-use';

import { getAppWebsiteBaseURL } from '~/common/constants';
import { createOrgActionHandler } from '~/domains/organization';
import type { Settings } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import { isOwnerOfOrganization, isPersonalOrganization } from '~/models/organization';
Expand Down Expand Up @@ -72,6 +73,12 @@ export async function clientLoader(_args: Route.ClientLoaderArgs) {
};
}

const handleAction = createOrgActionHandler('/organization');

export const clientAction = async ({ request }: Route.ClientActionArgs) => {
return handleAction(request);
};

export interface OrganizationFeatureLoaderData {
featuresPromise: Promise<FeatureList>;
billingPromise: Promise<Billing>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { StorageRules } from 'insomnia-api';
import React, { type FC, Fragment, useEffect, useState } from 'react';
import { Button, Menu, MenuItem, MenuTrigger, Popover, Tooltip, TooltipTrigger } from 'react-aria-components';

import { useProjectAction } from '~/domains/project';
import type { GitRepository, Project } from '~/insomnia-data';
import { models } from '~/insomnia-data';
import { useProjectDeleteActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.delete';

import { Icon } from '../icon';
import { showModal } from '../modals';
Expand All @@ -28,7 +28,7 @@ interface ProjectActionItem {

export const ProjectDropdown: FC<Props> = ({ project, organizationId, storageRules }) => {
const [isProjectSettingsModalOpen, setIsProjectSettingsModalOpen] = useState(false);
const deleteProjectFetcher = useProjectDeleteActionFetcher();
const deleteProjectFetcher = useProjectAction('remove');

const isRemoteProjectInconsistent = models.project.isRemoteProject(project) && !storageRules.enableCloudSync;
const isLocalProjectInconsistent =
Expand Down
16 changes: 10 additions & 6 deletions packages/insomnia/src/ui/components/settings/import-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import React, { type FC, Fragment, useEffect, useState } from 'react';
import { Button, Heading, ListBox, ListBoxItem, Popover, Select, SelectValue } from 'react-aria-components';
import { href, useParams } from 'react-router';

import { useProjectAction } from '~/domains/project';
import type { Environment, Project, Workspace } from '~/insomnia-data';
import { useRootLoaderData } from '~/root';
import { useOrganizationLoaderData } from '~/routes/organization';
import { useProjectListWorkspacesLoaderFetcher } from '~/routes/organization.$organizationId.project.$projectId.list-workspaces';
import { useProjectMoveActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.move';
import { useProjectMoveWorkspaceActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.move-workspace';
import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
import { useUntrackedProjectsLoaderFetcher } from '~/routes/untracked-projects';
Expand Down Expand Up @@ -452,7 +452,7 @@ const UntrackedProject = ({
organizationId: string;
organizations: Organization[];
}) => {
const moveProjectFetcher = useProjectMoveActionFetcher();
const moveProjectFetcher = useProjectAction('move');
const [selectedOrganizationId, setSelectedOrganizationId] = useState<string | null>(null);

return (
Expand All @@ -467,12 +467,16 @@ const UntrackedProject = ({
</p>
</div>
<moveProjectFetcher.Form
action={href(`/organization/:organizationId/project/:projectId/move`, {
organizationId,
projectId: project._id,
})}
method="POST"
className="group flex items-center gap-2"
onSubmit={e => {
e.preventDefault();
const payload = {
organizationId: selectedOrganizationId!,
projectId: project._id,
};
moveProjectFetcher.submit(payload);
}}
>
<Select
aria-label="Select an organization"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { useFetchers, useParams, useRevalidator } from 'react-router';
import * as reactUse from 'react-use';

import { CDN_INVALIDATION_TTL } from '~/common/constants';
import { useOrgAction } from '~/domains/organization';
import { useRootLoaderData } from '~/root';
import { useClearVaultKeyFetcher } from '~/routes/auth.clear-vault-key';
import { useProjectIndexLoaderData } from '~/routes/organization.$organizationId.project.$projectId._index';
import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
import { useInsomniaSyncDataActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.sync-data';
import { useStorageRulesActionFetcher } from '~/routes/organization.$organizationId.storage-rules';
import { useOrganizationSyncProjectsActionFetcher } from '~/routes/organization.$organizationId.sync-projects';
import { useOrganizationSyncActionFetcher } from '~/routes/organization.sync';
import { VCSInstance } from '~/sync/vcs/insomnia-sync';
import { avatarImageCache } from '~/ui/hooks/image-cache';

Expand Down Expand Up @@ -98,7 +98,7 @@ export const InsomniaEventStreamProvider: FC<PropsWithChildren> = ({ children })
const remoteId = projectData?.activeProject?.remoteId || workspaceData?.activeProject.remoteId;

const [presence, setPresence] = useState<UserPresence[]>([]);
const { submit: syncOrganizationsSubmit } = useOrganizationSyncActionFetcher();
const { submit: syncOrganizationsSubmit } = useOrgAction('sync');
const { submit: syncStorageRulesSubmit } = useStorageRulesActionFetcher();
const { submit: syncProjectsSubmit } = useOrganizationSyncProjectsActionFetcher();
const { submit: syncDataSubmit } = useInsomniaSyncDataActionFetcher();
Expand Down
Loading