diff --git a/packages/insomnia-smoke-test/tests/smoke/cookie-editor-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/cookie-editor-interactions.test.ts index 37fb9aa18c..74d79cc9a5 100644 --- a/packages/insomnia-smoke-test/tests/smoke/cookie-editor-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/cookie-editor-interactions.test.ts @@ -1,3 +1,5 @@ +import { expect } from '@playwright/test'; + import { loadFixture } from '../../playwright/paths'; import { test } from '../../playwright/test'; @@ -23,18 +25,18 @@ test.describe('Cookie editor', async () => { await page.click('pre[role="presentation"]:has-text("bar")'); await page.locator('[data-testid="CookieValue"] >> textarea').nth(1).fill('123'); await page.locator('text=Done').nth(1).click(); - await page.getByRole('cell', { name: 'foo=b123ar; Expires=' }).click(); + await page.getByTestId('cookie-test-iteration-0').click(); // Create a new cookie - await page.locator('.cookie-list').getByRole('button', { name: 'Actions' }).click(); - await page.getByRole('menuitem', { name: 'Add Cookie' }).click(); + await page.getByRole('button', { name: 'Add Cookie' }).click(); + await page.getByRole('button', { name: 'Edit' }).first().click(); // Try to replace text in Raw view await page.getByRole('tab', { name: 'Raw' }).click(); await page.locator('text=Raw Cookie String >> input[type="text"]').fill('foo2=bar2; Expires=Tue, 19 Jan 2038 03:14:07 GMT; Domain=localhost; Path=/'); await page.locator('text=Done').nth(1).click(); - await page.getByRole('cell', { name: 'foo2=bar2; Expires=' }).click(); + await page.getByTestId('cookie-test-iteration-0').click(); await page.click('text=Done'); @@ -44,7 +46,7 @@ test.describe('Cookie editor', async () => { // Check in the timeline that the cookie was sent await page.getByRole('tab', { name: 'Console' }).click(); - await page.click('text=foo2=bar2; foo=b123ar'); + await expect(page.getByText('foo2=bar2')).toBeVisible(); // Send ws request await page.getByLabel('Request Collection').getByTestId('example websocket').press('Enter'); @@ -53,7 +55,6 @@ test.describe('Cookie editor', async () => { // Check in the timeline that the cookie was sent await page.getByRole('tab', { name: 'Console' }).click(); - await page.click('text=foo2=bar2; foo=b123ar;'); + await expect(page.getByText('foo2=bar2')).toBeVisible(); }); - }); diff --git a/packages/insomnia/src/ui/components/cookie-list.tsx b/packages/insomnia/src/ui/components/cookie-list.tsx deleted file mode 100644 index 9839bf87df..0000000000 --- a/packages/insomnia/src/ui/components/cookie-list.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { isValid } from 'date-fns'; -import React, { type FC, useCallback, useState } from 'react'; -import { Button } from 'react-aria-components'; -import { Cookie as ToughCookie } from 'tough-cookie'; -import { v4 as uuidv4 } from 'uuid'; - -import { cookieToString } from '../../common/cookies'; -import type { Cookie } from '../../models/cookie-jar'; -import { Dropdown, DropdownItem, ItemContent } from './base/dropdown'; -import { PromptButton } from './base/prompt-button'; -import { Icon } from './icon'; -import { CookieModifyModal } from './modals/cookie-modify-modal'; -import { RenderedText } from './rendered-text'; - -export interface CookieListProps { - handleCookieAdd: (cookie: Cookie) => void; - handleCookieDelete: (cookie: Cookie) => void; - handleDeleteAll: () => void; - cookies: Cookie[]; - newCookieDomainName: string; -} - -// Use tough-cookie MAX_DATE value -// https://github.com/salesforce/tough-cookie/blob/5ae97c6a28122f3fb309adcd8428274d9b2bd795/lib/cookie.js#L77 -const MAX_TIME = 2147483647000; - -const CookieRow: FC<{ - cookie: Cookie; - deleteCookie: (cookie: Cookie) => void; -}> = ({ cookie, deleteCookie }) => { - const [isCookieModalOpen, setIsCookieModalOpen] = useState(false); - if (cookie.expires && !isValid(new Date(cookie.expires))) { - cookie.expires = null; - } - - const c = ToughCookie.fromJSON(cookie); - const cookieString = c ? cookieToString(c) : ''; - return - - {cookie.domain || ''} - - - {cookieString || ''} - - { }} className="text-right no-wrap"> - {' '} - deleteCookie(cookie)} - title="Delete cookie" - > - - - {isCookieModalOpen && ( - setIsCookieModalOpen(false)} - /> - )} - - ; - -}; - -export const CookieList: FC = ({ - cookies, - handleDeleteAll, - handleCookieAdd, - newCookieDomainName, - handleCookieDelete, -}) => { - const addCookie = useCallback(() => handleCookieAdd({ - id: uuidv4(), - key: 'foo', - value: 'bar', - domain: newCookieDomainName, - expires: MAX_TIME as unknown as Date, - path: '/', - secure: false, - httpOnly: false, - }), [newCookieDomainName, handleCookieAdd]); - - return
- - - - - - - - - - {cookies.map(cookie => ( - - ))} - -
- Domain - - Cookie - - - Actions - - } - > - - - - - - - -
- {cookies.length === 0 &&
-

I couldn't find any cookies for you.

-

- -

-
} -
; -}; diff --git a/packages/insomnia/src/ui/components/modals/cookie-modify-modal.tsx b/packages/insomnia/src/ui/components/modals/cookie-modify-modal.tsx deleted file mode 100644 index 1df756b253..0000000000 --- a/packages/insomnia/src/ui/components/modals/cookie-modify-modal.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import clone from 'clone'; -import { isValid } from 'date-fns'; -import React, { useEffect, useRef, useState } from 'react'; -import { OverlayContainer } from 'react-aria'; -import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; -import { useRouteLoaderData } from 'react-router-dom'; -import { useFetcher, useParams } from 'react-router-dom'; -import { Cookie as ToughCookie } from 'tough-cookie'; - -import { cookieToString } from '../../../common/cookies'; -import type { Cookie, CookieJar } from '../../../models/cookie-jar'; -import type { WorkspaceLoaderData } from '../../routes/workspace'; -import { Modal, type ModalHandle, type ModalProps } from '../base/modal'; -import { ModalBody } from '../base/modal-body'; -import { ModalFooter } from '../base/modal-footer'; -import { ModalHeader } from '../base/modal-header'; -import { OneLineEditor } from '../codemirror/one-line-editor'; -export interface CookieModifyModalOptions { - cookie: Cookie; -} - -export const CookieModifyModal = ((props: ModalProps & CookieModifyModalOptions) => { - const modalRef = useRef(null); - const [cookie, setCookie] = useState(props.cookie); - const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; - const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); - const updateCookieJarFetcher = useFetcher(); - useEffect(() => { - modalRef.current?.show(); - }, []); - const updateCookieJar = async (cookieJarId: string, patch: CookieJar) => { - updateCookieJarFetcher.submit(JSON.stringify({ patch, cookieJarId }), { - encType: 'application/json', - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/cookieJar/update`, - }); - }; - const handleCookieUpdate = async (nextCookie: any) => { - if (!cookie) { - return; - } - const newcookie = clone(nextCookie); - // transform to Date object or fallback to null - let dateFormat = null; - if (newcookie.expires && isValid(new Date(newcookie.expires))) { - dateFormat = new Date(newcookie.expires); - } - newcookie.expires = dateFormat; - setCookie(newcookie); - - // Clone so we don't modify the original - const cookieJar = clone(activeCookieJar); - const index = activeCookieJar.cookies.findIndex(c => c.id === cookie.id); - if (index < 0) { - console.warn(`Could not find cookie with id=${cookie.id} to edit`); - return; - } - cookieJar.cookies = [...cookieJar.cookies.slice(0, index), newcookie, ...cookieJar.cookies.slice(index + 1)]; - updateCookieJar(cookieJar._id, cookieJar); - }; - - let localDateTime; - if (cookie && cookie.expires && isValid(new Date(cookie.expires))) { - localDateTime = new Date(cookie.expires).toISOString().slice(0, 16); - } - - let rawDefaultValue; - if (!cookie) { - rawDefaultValue = ''; - } else { - try { - const c = ToughCookie.fromJSON(JSON.stringify(cookie)); - rawDefaultValue = c ? cookieToString(c) : ''; - } catch (err) { - console.warn('Failed to parse cookie string', err); - rawDefaultValue = ''; - } - } - return ( - - - Edit Cookie - - {activeCookieJar && cookie && ( - - - - Friendly - - - Raw - - - -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- -
-
- - -
-
- -
- -
-
-
- )} -
- - - -
-
- ); -}); diff --git a/packages/insomnia/src/ui/components/modals/cookies-modal.tsx b/packages/insomnia/src/ui/components/modals/cookies-modal.tsx index e3442052f4..3f971d0477 100644 --- a/packages/insomnia/src/ui/components/modals/cookies-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/cookies-modal.tsx @@ -1,111 +1,541 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { OverlayContainer } from 'react-aria'; +import clone from 'clone'; +import { isValid } from 'date-fns'; +import React, { useState } from 'react'; +import { Button, Dialog, Group, Heading, Input, ListBox, ListBoxItem, Modal, ModalOverlay, Tab, TabList, TabPanel, Tabs, TextField } from 'react-aria-components'; import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom'; +import { Cookie as ToughCookie } from 'tough-cookie'; +import { v4 as uuidv4 } from 'uuid'; +import { cookieToString } from '../../../common/cookies'; import { fuzzyMatch } from '../../../common/misc'; import type { Cookie, CookieJar } from '../../../models/cookie-jar'; import { useNunjucks } from '../../context/nunjucks/use-nunjucks'; import type { WorkspaceLoaderData } from '../../routes/workspace'; -import { Modal, type ModalHandle, type ModalProps } from '../base/modal'; -import { ModalBody } from '../base/modal-body'; -import { ModalFooter } from '../base/modal-footer'; -import { ModalHeader } from '../base/modal-header'; -import { CookieList } from '../cookie-list'; +import { PromptButton } from '../base/prompt-button'; +import { OneLineEditor } from '../codemirror/one-line-editor'; +import { Icon } from '../icon'; +import { RenderedText } from '../rendered-text'; -export const CookiesModal = ({ onHide }: ModalProps) => { - const modalRef = useRef(null); +// Use tough-cookie MAX_DATE value +// https://github.com/salesforce/tough-cookie/blob/5ae97c6a28122f3fb309adcd8428274d9b2bd795/lib/cookie.js#L77 +const MAX_TIME = 2147483647000; +const ItemsPerPage = 5; +const DefaultCookie: Cookie = { + id: uuidv4(), + key: 'foo', + value: 'bar', + domain: 'domain.com', + expires: MAX_TIME as unknown as Date, + path: '/', + secure: false, + httpOnly: false, +}; + +export function chunkArray(array: T[], chunkSize: number = ItemsPerPage): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)); + } + return chunks; +} + +interface Props { + setIsOpen: (isOpen: boolean) => void; +} + +export const CookiesModal = ({ setIsOpen }: Props) => { const { handleRender } = useNunjucks(); - const [filter, setFilter] = useState(''); - const [visibleCookieIndexes, setVisibleCookieIndexes] = useState(null); - const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; - const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); - const updateCookieJarFetcher = useFetcher(); - useEffect(() => { - modalRef.current?.show(); - }, []); - const updateCookieJar = async (cookieJarId: string, patch: CookieJar) => { + const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); + const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; + const updateCookieJarFetcher = useFetcher(); + + const [page, setPage] = useState(0); + const [filter, setFilter] = useState(''); + const [filteredCookies, setFilteredCookies] = useState(chunkArray(activeCookieJar?.cookies || [])); + + const updateCookieJar = (cookieJarId: string, patch: CookieJar) => { updateCookieJarFetcher.submit(JSON.stringify({ patch, cookieJarId }), { encType: 'application/json', method: 'post', action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/cookieJar/update`, }); + + setFilteredCookies(chunkArray(patch.cookies)); }; - const filteredCookies = visibleCookieIndexes ? (activeCookieJar?.cookies || []).filter((_, i) => visibleCookieIndexes.includes(i)) : (activeCookieJar?.cookies || []); + + const handleFilterChange = async (value: string) => { + setFilter(value); + const renderedCookies: Cookie[] = []; + + for (const cookie of (activeCookieJar?.cookies || [])) { + try { + renderedCookies.push(await handleRender(cookie)); + } catch (err) { + renderedCookies.push(cookie); + } + } + + if (!value) { + setFilteredCookies(chunkArray(renderedCookies)); + return; + } + + const filteredCookies: Cookie[] = []; + + renderedCookies.forEach(cookie => { + if (fuzzyMatch(value, JSON.stringify(cookie), { splitSpace: true })) { + filteredCookies.push(cookie); + } + }); + + setFilteredCookies(chunkArray(filteredCookies)); + }; + + const handleCookieDelete = (cookieId: string) => { + const updatedActiveCookieJar = activeCookieJar; + updatedActiveCookieJar.cookies = activeCookieJar.cookies.filter(c => c.id !== cookieId); + updateCookieJar(activeCookieJar._id, updatedActiveCookieJar); + }; + + const handleDeleteAll = () => { + const updatedActiveCookieJar = activeCookieJar; + updatedActiveCookieJar.cookies = []; + + updateCookieJar(activeCookieJar._id, updatedActiveCookieJar); + }; + + const handleAddCookie = () => { + const updatedActiveCookieJar = activeCookieJar; + updatedActiveCookieJar.cookies = [DefaultCookie, ...activeCookieJar.cookies]; + + updateCookieJar(activeCookieJar._id, updatedActiveCookieJar); + }; + + const handleCookieUpdate = (cookie: Cookie) => { + const newCookie = clone(cookie); + + // transform to Date object or fallback to null + let dateFormat = null; + + if (newCookie.expires && isValid(new Date(newCookie.expires))) { + dateFormat = new Date(newCookie.expires); + } + newCookie.expires = dateFormat; + + // Clone so we don't modify the original + const cookieJar = clone(activeCookieJar); + const index = activeCookieJar.cookies.findIndex(c => c.id === cookie.id); + + if (index < 0) { + console.warn(`Could not find cookie with id=${cookie.id} to edit`); + return; + } + + cookieJar.cookies = [...cookieJar.cookies.slice(0, index), newCookie, ...cookieJar.cookies.slice(index + 1)]; + updateCookieJar(cookieJar._id, cookieJar); + }; + return ( - - - Manage Cookies - - {activeCookieJar && ( -
-
-
- + + + + {({ close }) => ( + <> + {activeCookieJar && ( +
+ + Manage Cookies + + + )} + +
+ + + Delete All + +
+
+
+ {filteredCookies.length === 0 ? + (
+

{filter ? `No cookies match your search: "${filter}"` : 'No cookies found.'}

+
) + : ( + <> + + { + setPage(page - 1); + }} + onNextPress={() => { + setPage(page + 1); + }} + /> + + ) + }
+ )} +
+
+ * cookies are automatically sent with relevant requests +
+
-
- { - const updated = activeCookieJar; - updated.cookies = []; - updateCookieJar(activeCookieJar._id, updated); - }} - handleCookieAdd={cookie => { - const updated = activeCookieJar; - updated.cookies = [cookie, ...activeCookieJar.cookies]; - updateCookieJar(activeCookieJar._id, updated); - }} - handleCookieDelete={cookie => { - const updated = activeCookieJar; - updated.cookies = activeCookieJar.cookies.filter(c => c.id !== cookie.id); - updateCookieJar(activeCookieJar._id, updated); - }} - // Set the domain to the filter so that it shows up if we're filtering - newCookieDomainName={filter || 'domain.com'} - /> -
-
+ )} - - -
- * cookies are automatically sent with relevant requests -
- -
+ - + ); }; + +export interface CookieListProps { + cookies: Cookie[]; + onCookieDelete: (cookieId: string) => void; + onUpdateCookie: (cookie: Cookie) => void; +} + +const CookieList = ({ cookies, onCookieDelete, onUpdateCookie }: CookieListProps) => { + const [cookieToEdit, setCookieToEdit] = useState(null); + + return ( + <> + + {cookies.map((cookie, index) => { + const cookieJSON = ToughCookie.fromJSON(cookie); + const cookieString = cookieJSON ? cookieToString(cookieJSON) : ''; + + if (cookie.expires && !isValid(new Date(cookie.expires))) { + cookie.expires = null; + } + + return ( + + {cookie.domain || ''} + {cookieString || ''} +
+ + onCookieDelete(cookie.id)} + title="Delete cookie" + > + + +
+
+ ); + })} +
+ {cookieToEdit && setCookieToEdit(null)} + onUpdateCookie={onUpdateCookie} + />} + + ); +}; + +interface PaginationBarProps { + isPrevDisabled?: boolean; + isNextDisabled?: boolean; + isHidden?: boolean; + page: number; + totalPages: number; + onPrevPress?: () => void; + onNextPress?: () => void; +}; + +const PaginationBar = ({ isNextDisabled, isPrevDisabled, isHidden, page, totalPages, onPrevPress, onNextPress }: PaginationBarProps) => { + if (isHidden) { + return null; + } + + return ( +
+
+ +
+

{page}

+

of

+

{totalPages}

+
+ +
+
+ ); +}; + +interface CookieModifyModalProps { + cookie: Cookie; + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + onUpdateCookie: (cookie: Cookie) => void; +} + +const CookieModifyModal = (({ cookie, isOpen, setIsOpen, onUpdateCookie }: CookieModifyModalProps) => { + const [editCookie, setEditCookie] = useState(cookie); + + let localDateTime: string; + if (editCookie && editCookie.expires && isValid(new Date(editCookie.expires))) { + localDateTime = new Date(editCookie.expires).toISOString().slice(0, 16); + } + + let rawDefaultValue; + if (!editCookie) { + rawDefaultValue = ''; + } else { + try { + const c = ToughCookie.fromJSON(JSON.stringify(editCookie)); + rawDefaultValue = c ? cookieToString(c) : ''; + } catch (err) { + console.warn('Failed to parse cookie string', err); + rawDefaultValue = ''; + } + } + + return ( + + + + {({ close }) => ( + <> + {editCookie && ( + <> +
+ + Manage Cookies + +
+ + )} +
+ +
+ + )} +
+
+
+ ); +}); diff --git a/packages/insomnia/src/ui/components/modals/invite-modal/invite-modal.tsx b/packages/insomnia/src/ui/components/modals/invite-modal/invite-modal.tsx index 018040493b..b7d660cfa7 100644 --- a/packages/insomnia/src/ui/components/modals/invite-modal/invite-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/invite-modal/invite-modal.tsx @@ -433,8 +433,6 @@ const MemberListItem: FC<{ ); }; -export const defaultPerPage = 10; - interface PaginationBarProps { isPrevDisabled?: boolean; isNextDisabled?: boolean; diff --git a/packages/insomnia/src/ui/components/viewers/response-cookies-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-cookies-viewer.tsx index bdda7bbd82..8e957d8937 100644 --- a/packages/insomnia/src/ui/components/viewers/response-cookies-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-cookies-viewer.tsx @@ -65,9 +65,7 @@ export const ResponseCookiesViewer: FC = props => {

{isCookieModalOpen && ( - setIsCookieModalOpen(false)} - /> + )}
; }; diff --git a/packages/insomnia/src/ui/css/main.css b/packages/insomnia/src/ui/css/main.css index 8df2e47d43..3f6f1b8782 100644 --- a/packages/insomnia/src/ui/css/main.css +++ b/packages/insomnia/src/ui/css/main.css @@ -1894,47 +1894,8 @@ html { .changelog hr { margin: var(--padding-lg) 0 !important; } -.cookie-list { - height: 100%; - display: grid; - grid-template-rows: auto minmax(0, 1fr); -} -.cookie-list table input:not([type='checkbox']) { - padding: var(--padding-xs) var(--padding-xxs); - width: 100%; - background: none; -} -.cookie-list table .btn { - cursor: pointer; - margin: 0; - padding: 0 var(--padding-sm); - height: var(--line-height-xs); -} -.cookie-list .cookie-list__list { - height: 100%; - padding: 0 var(--padding-md) var(--padding-md) var(--padding-md); - position: relative; - overflow-y: auto; -} -.cookie-modify.modal__body { - overflow: visible; - display: grid; - grid-template-rows: auto minmax(0, 1fr); -} -.cookie-modify.modal__body table input:not([type='checkbox']) { - padding: var(--padding-xs) var(--padding-xxs); - width: 100%; - background: none; -} -.cookie-modify.modal__body table td { - cursor: pointer; - vertical-align: middle; -} -.cookie-modify.modal__body table .btn { - cursor: pointer; - margin: 0; - padding: 0 var(--padding-sm); - height: var(--line-height-xs); +.calendar-invert::-webkit-calendar-picker-indicator { + filter: invert(1); } .graphql-editor { position: relative; diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 0fe85d69f8..9833187029 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -1129,7 +1129,7 @@ export const Debug: FC = () => { /> )} {isCookieModalOpen && ( - setIsCookieModalOpen(false)} /> + )} {isCertificatesModalOpen && ( setCertificatesModalOpen(false)} /> diff --git a/packages/insomnia/src/ui/routes/design.tsx b/packages/insomnia/src/ui/routes/design.tsx index 3b6e6be00f..9ecad1cc1a 100644 --- a/packages/insomnia/src/ui/routes/design.tsx +++ b/packages/insomnia/src/ui/routes/design.tsx @@ -991,7 +991,7 @@ const Design: FC = () => { /> )} {isCookieModalOpen && ( - setIsCookieModalOpen(false)} /> + )} {isCertificatesModalOpen && ( setCertificatesModalOpen(false)} /> diff --git a/packages/insomnia/src/ui/routes/unit-test.tsx b/packages/insomnia/src/ui/routes/unit-test.tsx index 59d79eb25b..5eb2369bc1 100644 --- a/packages/insomnia/src/ui/routes/unit-test.tsx +++ b/packages/insomnia/src/ui/routes/unit-test.tsx @@ -464,7 +464,7 @@ const TestRoute: FC = () => { /> )} {isCookieModalOpen && ( - setIsCookieModalOpen(false)} /> + )} {isCertificatesModalOpen && ( setCertificatesModalOpen(false)} />