mirror of
https://github.com/Kong/insomnia.git
synced 2026-08-07 05:12:33 -04:00
Allow projects to be toggled local/remote (#6667)
* update project settings to handle remote to local and around * fix styles and remove unused file * add comments and early returns * project export rename and name fix * handle auth errors * add optional chain * fix test --------- Co-authored-by: jackkav <jackkav@gmail.com>
This commit is contained in:
1 parent
b98d1934e3
commit
d006bb0a82
11 files changed
+197
-127
No files matched your search
@@ -34,10 +34,7 @@ test.describe('Dashboard', async () => {
|
||||
|
||||
// Delete project
|
||||
await page.getByRole('row', { name: 'My Project' }).getByRole('button', { name: 'Project Actions' }).click();
|
||||
await page.getByRole('menuitemradio', { name: 'Settings' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Click to confirm' }).click();
|
||||
await page.getByRole('menuitemradio', { name: 'Delete' }).click();
|
||||
|
||||
// After deleting project, return to default Insomnia Dashboard
|
||||
await expect(page.locator('.app')).toContainText('Personal Workspace');
|
||||
|
||||
@@ -154,7 +154,6 @@ export function getCurrentSessionId() {
|
||||
try {
|
||||
const { sessionExpiry } = JSON.parse(window.localStorage.getItem(_getSessionKey(sessionId)) || '{}');
|
||||
if (typeof sessionExpiry !== 'string' || !sessionExpiry) {
|
||||
console.log('No session expiry found', sessionExpiry);
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -257,13 +256,16 @@ function _getSymmetricKey() {
|
||||
}
|
||||
|
||||
async function _whoami(sessionId: string | null = null): Promise<WhoamiResponse> {
|
||||
const response = await window.main.insomniaFetch<WhoamiResponse>({
|
||||
const response = await window.main.insomniaFetch<WhoamiResponse | string>({
|
||||
method: 'GET',
|
||||
path: '/auth/whoami',
|
||||
sessionId: sessionId || getCurrentSessionId(),
|
||||
});
|
||||
if (typeof response === 'string') {
|
||||
throw new Error('Unexpected plaintext response');
|
||||
throw new Error('Unexpected plaintext response: ' + response);
|
||||
}
|
||||
if (response && !response?.encSymmetricKey) {
|
||||
throw new Error('Unexpected response: ' + JSON.stringify(response));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ const writeExportedFileToFileSystem = (filename: string, jsonData: string, onDon
|
||||
fs.writeFile(filename, jsonData, {}, onDone);
|
||||
};
|
||||
|
||||
export const exportAllToFile = (activeProjectName: string, workspacesForActiveProject: Workspace[]) => {
|
||||
export const exportProjectToFile = (activeProjectName: string, workspacesForActiveProject: Workspace[]) => {
|
||||
if (!workspacesForActiveProject.length) {
|
||||
showAlert({
|
||||
title: 'Cannot export',
|
||||
|
||||
@@ -2,10 +2,19 @@ import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React, { FC, Fragment, useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
Heading,
|
||||
Input,
|
||||
Item,
|
||||
Label,
|
||||
Menu,
|
||||
MenuTrigger,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
} from 'react-aria-components';
|
||||
import { useFetcher } from 'react-router-dom';
|
||||
|
||||
@@ -15,7 +24,6 @@ import {
|
||||
} from '../../../models/project';
|
||||
import { Icon } from '../icon';
|
||||
import { showAlert } from '../modals';
|
||||
import ProjectSettingsModal from '../modals/project-settings-modal';
|
||||
|
||||
interface Props {
|
||||
project: Project;
|
||||
@@ -33,6 +41,7 @@ export const ProjectDropdown: FC<Props> = ({ project, organizationId }) => {
|
||||
const [isProjectSettingsModalOpen, setIsProjectSettingsModalOpen] =
|
||||
useState(false);
|
||||
const deleteProjectFetcher = useFetcher();
|
||||
const updateProjectFetcher = useFetcher();
|
||||
|
||||
const projectActionList: ProjectActionItem[] = [
|
||||
{
|
||||
@@ -98,12 +107,90 @@ export const ProjectDropdown: FC<Props> = ({ project, organizationId }) => {
|
||||
</Menu>
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
{isProjectSettingsModalOpen && (
|
||||
<ProjectSettingsModal
|
||||
onHide={() => setIsProjectSettingsModalOpen(false)}
|
||||
project={project}
|
||||
/>
|
||||
)}
|
||||
<ModalOverlay isOpen={isProjectSettingsModalOpen} onOpenChange={setIsProjectSettingsModalOpen} 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 onClose={() => setIsProjectSettingsModalOpen(false)} className="outline-none">
|
||||
{({ close }) => (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex gap-2 items-center justify-between'>
|
||||
<Heading className='text-2xl'>Project Settings</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>
|
||||
{isDefaultOrganizationProject(project) && <p>
|
||||
<Icon icon="info-circle" /> This is the default project for your organization. You can not delete it or change its type.
|
||||
</p>}
|
||||
<form
|
||||
className='flex flex-col gap-4'
|
||||
onSubmit={e => {
|
||||
updateProjectFetcher.submit(e.currentTarget, {
|
||||
action: `/organization/${organizationId}/project/${project._id}/update`,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
autoFocus
|
||||
name="name"
|
||||
defaultValue={project.name}
|
||||
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={project.remoteId ? 'remote' : 'local'} className="flex flex-col gap-2">
|
||||
<Label className="text-sm text-[--hl]">
|
||||
Project type
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Radio
|
||||
value="remote"
|
||||
className="data-[selected]:border-[--color-surprise] data-[selected]:ring-2 data-[selected]:ring-[--color-surprise] hover:bg-[--hl-xs] focus:bg-[--hl-sm] border border-solid border-[--hl-md] rounded p-4 focus:outline-none transition-colors"
|
||||
>
|
||||
<Icon icon="globe" />
|
||||
<Heading className="text-lg font-bold">Secure Cloud</Heading>
|
||||
<p className='pt-2'>
|
||||
End-to-end encrypted (E2EE) and synced securely to the cloud, ideal for collaboration.
|
||||
</p>
|
||||
</Radio>
|
||||
<Radio
|
||||
isDisabled={isDefaultOrganizationProject(project)}
|
||||
value="local"
|
||||
className="data-[selected]:border-[--color-surprise] data-[disabled]:opacity-25 data-[selected]:ring-2 data-[selected]:ring-[--color-surprise] hover:bg-[--hl-xs] focus:bg-[--hl-sm] border border-solid border-[--hl-md] rounded p-4 focus:outline-none transition-colors"
|
||||
>
|
||||
<Icon icon="laptop" />
|
||||
<Heading className="text-lg font-bold">Local Vault</Heading>
|
||||
<p className="pt-2">
|
||||
Stored locally only with no cloud. Ideal when collaboration is not needed.
|
||||
</p>
|
||||
</Radio>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
className="hover:no-underline bg-[#4000BF] hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font] transition-colors rounded-sm"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { FC, Fragment, useEffect, useRef } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
|
||||
import { strings } from '../../../common/strings';
|
||||
import { isDefaultOrganizationProject, Project } from '../../../models/project';
|
||||
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { PromptButton } from '../base/prompt-button';
|
||||
import { showAlert } from '.';
|
||||
|
||||
export interface ProjectSettingsModalProps extends ModalProps {
|
||||
project: Project;
|
||||
}
|
||||
|
||||
export const ProjectSettingsModal: FC<ProjectSettingsModalProps> = ({ project, onHide }) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const { organizationId } = useParams<{organizationId: string}>();
|
||||
const { submit, Form, data, state } = useFetcher();
|
||||
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.error && state === 'idle') {
|
||||
showAlert({
|
||||
title: 'Could not rename project',
|
||||
message: data.error,
|
||||
});
|
||||
}
|
||||
}, [data, state]);
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<Modal onHide={onHide} ref={modalRef}>
|
||||
<ModalHeader key={`header::${project._id}`}>
|
||||
{strings.project.singular} Settings{' '}
|
||||
<div className="txt-sm selectable faint monospace">{project._id}</div>
|
||||
</ModalHeader>
|
||||
<ModalBody key={`body::${project._id}`} className="pad">
|
||||
<Form
|
||||
method="post"
|
||||
action={`/organization/${organizationId}/project/${project._id}/rename`}
|
||||
className="form-control form-control--outlined"
|
||||
>
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder={`My ${strings.project.singular}`}
|
||||
defaultValue={project.name}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" className="btn btn--clicky">
|
||||
Update
|
||||
</button>
|
||||
</Form>
|
||||
{!isDefaultOrganizationProject(project) && <Fragment>
|
||||
<h2>Actions</h2>
|
||||
<div className="form-control form-control--padded">
|
||||
<PromptButton
|
||||
onClick={() =>
|
||||
submit(
|
||||
{},
|
||||
{ method: 'post', action: `/organization/${organizationId}/project/${project._id}/delete` }
|
||||
)
|
||||
}
|
||||
className="width-auto btn btn--clicky inline-block"
|
||||
>
|
||||
<i className="fa fa-trash-o" /> Delete
|
||||
</PromptButton>
|
||||
</div>
|
||||
</Fragment>}
|
||||
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectSettingsModal.displayName = 'ProjectSettingsModal';
|
||||
|
||||
export default ProjectSettingsModal;
|
||||
@@ -5,7 +5,7 @@ import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { isLoggedIn } from '../../../account/session';
|
||||
import { getProductName } from '../../../common/constants';
|
||||
import { exportAllToFile } from '../../../common/export';
|
||||
import { exportProjectToFile } from '../../../common/export';
|
||||
import { exportAllData } from '../../../common/export-all-data';
|
||||
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
|
||||
import { strings } from '../../../common/strings';
|
||||
@@ -48,7 +48,6 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
|
||||
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined;
|
||||
const activeWorkspaceName = workspaceData?.activeWorkspace.name;
|
||||
const projectName = workspaceData?.activeProject.name ?? getProductName();
|
||||
const { workspaceCount } = useRootLoaderData();
|
||||
const workspacesFetcher = useFetcher();
|
||||
useEffect(() => {
|
||||
@@ -57,13 +56,15 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
workspacesFetcher.load(`/organization/${organizationId}/project/${projectId}`);
|
||||
}
|
||||
}, [organizationId, projectId, workspacesFetcher]);
|
||||
const projectLoaderData = workspacesFetcher?.data as ProjectLoaderData;
|
||||
const projectLoaderData = workspacesFetcher?.data as ProjectLoaderData | undefined;
|
||||
const workspacesForActiveProject = projectLoaderData?.workspaces.map(w => w.workspace) || [];
|
||||
const projectName = projectLoaderData?.activeProject.name ?? getProductName();
|
||||
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
||||
|
||||
const handleExportAllToFile = () => {
|
||||
exportAllToFile(projectName, workspacesForActiveProject);
|
||||
const handleExportProjectToFile = () => {
|
||||
exportProjectToFile(projectName, workspacesForActiveProject);
|
||||
hideSettingsModal();
|
||||
};
|
||||
|
||||
@@ -75,7 +76,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
<Fragment>
|
||||
<div data-testid="import-export-tab" className='flex flex-col gap-4'>
|
||||
<div className='rounded-md border border-solid border-[--hl-md] p-4 flex flex-col gap-2'>
|
||||
<Heading className='text-lg font-bold flex items-center gap-2'><Icon icon="file-export" /> Export:</Heading>
|
||||
<Heading className='text-lg font-bold flex items-center gap-2'><Icon icon="file-export" /> Export</Heading>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{workspaceData?.activeWorkspace ?
|
||||
isScratchpad(workspaceData.activeWorkspace) ?
|
||||
@@ -105,7 +106,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
<ItemContent
|
||||
icon="empty"
|
||||
label={`Export files from the "${projectName}" ${strings.project.singular}`}
|
||||
onClick={handleExportAllToFile}
|
||||
onClick={handleExportProjectToFile}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
@@ -113,7 +114,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
) : (
|
||||
<Button
|
||||
className="px-4 py-1 font-semibold border border-solid border-[--hl-md] flex items-center justify-center gap-2 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={handleExportAllToFile}
|
||||
onPress={handleExportProjectToFile}
|
||||
>
|
||||
{`Export files from the "${projectName}" ${strings.project.singular}`}
|
||||
</Button>
|
||||
@@ -171,7 +172,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-md border border-solid border-[--hl-md] p-4 flex flex-col gap-2'>
|
||||
<Heading className='text-lg font-bold flex items-center gap-2'><Icon icon="file-import" /> Import:</Heading>
|
||||
<Heading className='text-lg font-bold flex items-center gap-2'><Icon icon="file-import" /> Import</Heading>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
className="px-4 py-1 font-semibold border border-solid border-[--hl-md] flex items-center justify-center gap-2 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"
|
||||
|
||||
@@ -61,7 +61,9 @@ html {
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.new-sidebar {
|
||||
z-index: 0;
|
||||
}
|
||||
.new-sidebar .sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -221,11 +221,11 @@ const router = createMemoryRouter(
|
||||
).moveProjectAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'rename',
|
||||
path: 'update',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/actions')
|
||||
).renameProjectAction(...args),
|
||||
).updateProjectAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'git',
|
||||
|
||||
@@ -88,7 +88,7 @@ export const createNewProjectAction: ActionFunction = async ({ request, params }
|
||||
}
|
||||
};
|
||||
|
||||
export const renameProjectAction: ActionFunction = async ({
|
||||
export const updateProjectAction: ActionFunction = async ({
|
||||
request,
|
||||
params,
|
||||
}) => {
|
||||
@@ -97,7 +97,10 @@ export const renameProjectAction: ActionFunction = async ({
|
||||
const name = formData.get('name');
|
||||
invariant(typeof name === 'string', 'Name is required');
|
||||
|
||||
const { projectId } = params;
|
||||
const type = formData.get('type');
|
||||
invariant(type === 'local' || type === 'remote', 'Project type is required');
|
||||
|
||||
const { organizationId, projectId } = params;
|
||||
invariant(projectId, 'Project ID is required');
|
||||
|
||||
const project = await models.project.getById(projectId);
|
||||
@@ -105,10 +108,10 @@ export const renameProjectAction: ActionFunction = async ({
|
||||
invariant(project, 'Project not found');
|
||||
|
||||
const sessionId = session.getCurrentSessionId();
|
||||
invariant(sessionId, 'User must be logged in to rename a project');
|
||||
|
||||
try {
|
||||
if (project.remoteId) {
|
||||
// If its a cloud project, and we are renaming, then patch
|
||||
if (sessionId && project.remoteId && type === 'remote' && name !== project.name) {
|
||||
const response = await window.main.insomniaFetch<void | {
|
||||
error: string;
|
||||
message?: string;
|
||||
@@ -126,10 +129,67 @@ export const renameProjectAction: ActionFunction = async ({
|
||||
error: response.error === 'FORBIDDEN' ? 'You do not have permission to rename this project.' : 'An unexpected error occurred while renaming the project. Please try again.',
|
||||
};
|
||||
}
|
||||
|
||||
await models.project.update(project, { name });
|
||||
return null;
|
||||
}
|
||||
|
||||
// convert from cloud to local
|
||||
if (type === 'local' && project.remoteId) {
|
||||
const response = await window.main.insomniaFetch<void | {
|
||||
error: string;
|
||||
message?: string;
|
||||
}>({
|
||||
path: `/v1/organizations/${organizationId}/team-projects/${project.remoteId}`,
|
||||
method: 'DELETE',
|
||||
sessionId,
|
||||
});
|
||||
|
||||
if (response && 'error' in response) {
|
||||
return {
|
||||
error: response.error === 'FORBIDDEN' ? 'You do not have permission to change this project.' : 'An unexpected error occurred while deleting the project. Please try again.',
|
||||
};
|
||||
}
|
||||
|
||||
await models.project.update(project, { name, remoteId: null });
|
||||
return null;
|
||||
}
|
||||
// convert from local to cloud
|
||||
if (type === 'remote' && !project.remoteId) {
|
||||
const newCloudProject = await window.main.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' || newCloudProject.error === 'NEEDS_TO_UPGRADE') {
|
||||
error = newCloudProject.error;
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
await models.project.update(project, { name, remoteId: newCloudProject.id });
|
||||
return null;
|
||||
}
|
||||
|
||||
// local project rename
|
||||
await models.project.update(project, { name });
|
||||
return null;
|
||||
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return {
|
||||
|
||||
@@ -54,7 +54,7 @@ const Authorize = () => {
|
||||
<Heading className="text-2xl font-bold text-center px-3">
|
||||
Authorizing Insomnia
|
||||
</Heading>
|
||||
{!isAuthenticating && (
|
||||
{(
|
||||
<Fragment>
|
||||
<p>
|
||||
A new page should have opened in your default web browser. Please log in.
|
||||
@@ -129,8 +129,9 @@ const Authorize = () => {
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-xs)',
|
||||
}}
|
||||
disabled={isAuthenticating}
|
||||
>
|
||||
<i className="fa fa-sign-in" aria-hidden="true" />
|
||||
<i className={`fa fa-${isAuthenticating ? 'spin' : 'sign-in'}`} aria-hidden="true" />
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
@@ -139,14 +140,6 @@ const Authorize = () => {
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
{isAuthenticating && (
|
||||
<div className="flex flex-col gap-3 rounded-md bg-[--hl-sm] p-[--padding-md]">
|
||||
<Heading className="text-lg flex items-center p-8 gap-8">
|
||||
<i className="fa fa-spinner fa-spin" aria-hidden="true" />
|
||||
Authenticating...
|
||||
</Heading>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex justify-center w-full'>
|
||||
<Button
|
||||
variant="text"
|
||||
|
||||
@@ -168,6 +168,20 @@ async function syncTeamProjects({
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Turn remote projects from the current organization that are not in the list of remote projects into local projects.
|
||||
const removedRemoteProjects = await database.find<Project>(models.project.type, {
|
||||
// filter by this organization so no legacy data can be accidentally removed, because legacy had null parentId
|
||||
parentId: organizationId,
|
||||
// Remote ID is not in the list of remote projects
|
||||
remoteId: { $nin: teamProjects.map(p => p.id) },
|
||||
});
|
||||
|
||||
await Promise.all(removedRemoteProjects.map(async prj => {
|
||||
await models.project.update(prj, {
|
||||
remoteId: null,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export const indexLoader: LoaderFunction = async ({ params }) => {
|
||||
|
||||
Reference in new issue
Block a user