diff --git a/packages/insomnia-smoke-test/tests/critical/app-start.test.ts b/packages/insomnia-smoke-test/tests/critical/app-start.test.ts index b839ead39..f4698d0bf 100644 --- a/packages/insomnia-smoke-test/tests/critical/app-start.test.ts +++ b/packages/insomnia-smoke-test/tests/critical/app-start.test.ts @@ -8,11 +8,11 @@ test('can send requests', async ({ app, page }) => { const responseBody = page.locator('[data-testid="CodeEditor"]:visible', { has: page.locator('.CodeMirror-activeline'), }); - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('smoke-test-collection.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/prerelease/cookie-editor-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/cookie-editor-interactions.test.ts index cb59c0a80..17ce9e3e7 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/cookie-editor-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/cookie-editor-interactions.test.ts @@ -4,10 +4,10 @@ import { test } from '../../playwright/test'; test.describe('Cookie editor', async () => { test.beforeEach(async ({ app, page }) => { - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('simple.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/prerelease/dashboard-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/dashboard-interactions.test.ts index 90ed77ff5..77591102b 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/dashboard-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/dashboard-interactions.test.ts @@ -7,20 +7,20 @@ test.describe('Dashboard', async () => { test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); test.describe('Projects', async () => { test('Can create, rename and delete new project', async ({ page }) => { - await expect(page.locator('.app')).toContainText('All Files (0)'); + await page.getByLabel('All Files (0)').click(); await expect(page.locator('.app')).not.toContainText('Git Sync'); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); // Create new project - await page.click('[data-testid="CreateProjectButton"]'); + await page.getByRole('button', { name: 'Create new Project' }).click(); await page.locator('text=Create').nth(1).click(); // Check empty project await expect(page.locator('.app')).toContainText('This is an empty project, to get started create your first resource:'); // Rename Project - await page.click('[data-testid="ProjectDropDown-My-Project"] button'); - await page.getByRole('menuitem', { name: 'Project Settings' }).click(); + await page.getByRole('row', { name: 'My Project' }).getByRole('button', { name: 'Project Actions' }).click(); + await page.getByRole('menuitemradio', { name: 'Settings' }).click(); await page.getByPlaceholder('My Project').click(); await page.getByPlaceholder('My Project').fill('My Project123'); @@ -32,8 +32,8 @@ test.describe('Dashboard', async () => { await expect(page.locator('.app')).toContainText('My Project123'); // Delete project - await page.click('[data-testid="ProjectDropDown-My-Project123"] button'); - await page.getByRole('menuitem', { name: 'Project Settings' }).click(); + await page.getByRole('row', { name: 'My Project' }).getByRole('button', { name: 'Project Actions' }).click(); + await page.getByRole('menuitemradio', { name: 'Settings' }).click(); // Click text=NameActions Delete >> button await page.click('text=NameActions Delete >> button'); await page.getByRole('button', { name: 'Click to confirm' }).click(); @@ -42,27 +42,27 @@ test.describe('Dashboard', async () => { await expect(page.locator('.app')).toContainText('Insomnia'); await expect(page.locator('.app')).not.toContainText('My Project123'); await expect(page.locator('.app')).toContainText('New Document'); - await expect(page.locator('.app')).toContainText('All Files (0)'); + await page.getByLabel('All Files (0)').click(); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); }); }); test.describe('Interactions', async () => { // Not sure about the name here // TODO(INS-2504) - we don't support importing multiple collections at this time test.skip('Can filter through multiple collections', async ({ app, page }) => { - await expect(page.locator('.app')).toContainText('All Files (0)'); + await page.getByLabel('All Files (0)').click(); await expect(page.locator('.app')).not.toContainText('Git Sync'); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('multiple-workspaces.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionSmoke testsjust now').click(); // Check that 10 new workspaces are imported besides the default one - const workspaceCards = page.locator('.card-badge'); + const workspaceCards = page.getByLabel('Workspaces').getByRole('gridcell'); await expect(workspaceCards).toHaveCount(11); await expect(page.locator('.app')).toContainText('New Document'); await expect(page.locator('.app')).toContainText('collection 1'); @@ -87,13 +87,13 @@ test.describe('Dashboard', async () => { }); test('Can create, rename and delete a document', async ({ page }) => { - await expect(page.locator('.app')).toContainText('All Files (0)'); + await page.getByLabel('All Files (0)').click(); await expect(page.locator('.app')).not.toContainText('Git Sync'); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); // Create new document - await page.getByRole('button', { name: 'Create' }).click(); - await page.getByRole('menuitem', { name: 'Design Document' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); + await page.getByRole('menuitemradio', { name: 'Design Document' }).click(); await page.locator('text=Create').nth(1).click(); await page.getByTestId('project').click(); @@ -113,7 +113,7 @@ test.describe('Dashboard', async () => { await page.getByTestId('project').click(); - const workspaceCards = page.locator('.card-badge'); + const workspaceCards = page.getByLabel('Workspaces').getByRole('gridcell'); await expect(workspaceCards).toHaveCount(2); // Delete document @@ -124,13 +124,13 @@ test.describe('Dashboard', async () => { }); test('Can create, rename and delete a collection', async ({ page }) => { - await expect(page.locator('.app')).toContainText('All Files (0)'); + await page.getByLabel('All Files (0)').click(); await expect(page.locator('.app')).not.toContainText('Git Sync'); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); // Create new collection - await page.getByRole('button', { name: 'Create' }).click(); - await page.getByRole('menuitem', { name: 'Request Collection' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); + await page.getByRole('menuitemradio', { name: 'Request Collection' }).click(); await page.locator('text=Create').nth(1).click(); await page.getByTestId('project').click(); @@ -149,7 +149,7 @@ test.describe('Dashboard', async () => { await page.click('[role="dialog"] button:has-text("Duplicate")'); await page.getByTestId('project').click(); - const workspaceCards = page.locator('.card-badge'); + const workspaceCards = page.getByLabel('Workspaces').getByRole('gridcell'); await expect(workspaceCards).toHaveCount(2); // Delete collection diff --git a/packages/insomnia-smoke-test/tests/prerelease/debug-sidebar-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/debug-sidebar-interactions.test.ts index 9e90492f9..f0dbaf7c7 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/debug-sidebar-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/debug-sidebar-interactions.test.ts @@ -6,10 +6,10 @@ import { test } from '../../playwright/test'; test.describe('Debug-Sidebar', async () => { test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); test.beforeEach(async ({ app, page }) => { - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('simple.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); @@ -71,8 +71,8 @@ test.describe('Debug-Sidebar', async () => { }); test('Filter by request name', async ({ page }) => { - await page.locator('[placeholder="Filter"]').click(); - await page.locator('[placeholder="Filter"]').fill('example http'); + await page.getByLabel('Collection filter').click(); + await page.getByLabel('Collection filter').fill('example http'); await page.getByLabel('Request Collection').getByRole('row', { name: 'example http' }).click(); }); diff --git a/packages/insomnia-smoke-test/tests/prerelease/design-document-naming.test.ts b/packages/insomnia-smoke-test/tests/prerelease/design-document-naming.test.ts new file mode 100644 index 000000000..6528c672e --- /dev/null +++ b/packages/insomnia-smoke-test/tests/prerelease/design-document-naming.test.ts @@ -0,0 +1,11 @@ +import { test } from '../../playwright/test'; + +test('can name design documents', async ({ page }) => { + await page.getByRole('button', { name: ' New Document' }).click(); + await page.getByPlaceholder('my-spec.yaml').fill('jurassic park'); + await page.getByPlaceholder('my-spec.yaml').press('Enter'); + await page.getByLabel('jurassic park').click(); + await page.getByRole('button', { name: 'jurassic park ' }).press('Escape'); + await page.getByTestId('project').click(); + await page.getByLabel('jurassic park').click(); +}); diff --git a/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts index 90fbefb63..f5c5d5d5a 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts @@ -8,10 +8,10 @@ test.describe('Design interactions', async () => { test('Unit Test interactions', async ({ app, page }) => { // Setup - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('unit-test.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/prerelease/environment-editor-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/environment-editor-interactions.test.ts index c0f496c4f..b7d7785e1 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/environment-editor-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/environment-editor-interactions.test.ts @@ -4,10 +4,10 @@ import { test } from '../../playwright/test'; test.describe('Environment Editor', async () => { test.beforeEach(async ({ app, page }) => { - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('environments.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/prerelease/git-sync-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/git-sync-interactions.test.ts index edb8b047c..8aad780fa 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/git-sync-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/git-sync-interactions.test.ts @@ -52,8 +52,8 @@ test.fixme('Clone Repo with bad values @failing', async ({ page }) => { }); test.fixme('Clone Gitlab Repo with bad values', async ({ page }) => { - await page.getByRole('button', { name: 'Create' }).click(); - await page.getByRole('menuitem', { name: 'Git Clone' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); + await page.getByRole('menuitemradio', { name: 'Git Clone' }).click(); await page.getByRole('tab', { name: 'Git' }).nth(2).click(); // Fill in Git Sync details and clone repository diff --git a/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts index e97ef2f0b..7562a6696 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts @@ -11,12 +11,12 @@ test.describe('gRPC interactions', () => { let streamMessage: Locator; test.beforeEach(async ({ app, page }) => { - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('grpc.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/prerelease/preferences-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/preferences-interactions.test.ts index b0db372af..22e821a0a 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/preferences-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/preferences-interactions.test.ts @@ -17,10 +17,10 @@ test('Preferences through keyboard shortcut', async ({ page }) => { // Quick reproduction for Kong/insomnia#5664 and INS-2267 test('Check filter responses by environment preference', async ({ app, page }) => { - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('simple.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/app.test.ts b/packages/insomnia-smoke-test/tests/smoke/app.test.ts index 755f0d8f8..7600b25ef 100644 --- a/packages/insomnia-smoke-test/tests/smoke/app.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/app.test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { loadFixture } from '../../playwright/paths'; -import { test } from '../../playwright/test'; +import { test } from '../../playwright/test';; test('can send requests', async ({ app, page }) => { test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); @@ -10,17 +10,16 @@ test('can send requests', async ({ app, page }) => { has: page.locator('.CodeMirror-activeline'), }); - await page.getByRole('button', { name: 'Create' }).click(); - const text = await loadFixture('smoke-test-collection.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); - await page.locator('.card-menu').click(); + await page.getByRole('button', { name: 'Workspace actions menu button' }).click(); await page.getByRole('menuitem', { name: 'Export' }).click(); await page.getByRole('dialog').getByRole('checkbox').nth(1).uncheck(); await page.getByRole('button', { name: 'Export' }).click(); @@ -88,12 +87,12 @@ test('can send requests', async ({ app, page }) => { // This feature is unsafe to place beside other tests, cancelling a request can cause network code to block // related to https://linear.app/insomnia/issue/INS-973 test('can cancel requests', async ({ app, page }) => { - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('smoke-test-collection.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts b/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts index 62dab5f41..28bd0c4a3 100644 --- a/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts @@ -6,14 +6,14 @@ import { test } from '../../playwright/test'; test('can render schema and send GraphQL requests', async ({ app, page }) => { test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); // Copy the collection with the graphql query to clipboard const text = await loadFixture('graphql.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); // Import from clipboard - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); @@ -46,11 +46,11 @@ test('can render schema and send GraphQL requests', async ({ app, page }) => { test('can send GraphQL requests after editing and prettifying query', async ({ app, page }) => { test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('graphql.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts b/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts index 396e9ed0a..1cf5af27e 100644 --- a/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts @@ -10,12 +10,12 @@ test('can send gRPC requests with reflection', async ({ app, page }) => { has: page.locator('.CodeMirror-activeline'), }); - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('grpc.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts b/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts index 9e1b0d7f3..333531298 100644 --- a/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts @@ -17,12 +17,12 @@ test('can make oauth2 requests', async ({ app, page }) => { }); const projectView = page.locator('#wrapper'); - await projectView.getByRole('button', { name: 'Create' }).click(); + await projectView.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('oauth.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/websocket.test.ts b/packages/insomnia-smoke-test/tests/smoke/websocket.test.ts index a17537eb0..0c979e611 100644 --- a/packages/insomnia-smoke-test/tests/smoke/websocket.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/websocket.test.ts @@ -10,12 +10,12 @@ test('can make websocket connection', async ({ app, page }) => { has: page.locator('.CodeMirror-activeline'), }); - await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); const text = await loadFixture('websockets.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('menuitem', { name: 'Import' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); diff --git a/packages/insomnia/src/ui/components/__tests__/workspace-card.test.ts b/packages/insomnia/src/ui/components/__tests__/workspace-card.test.ts deleted file mode 100644 index 97348943e..000000000 --- a/packages/insomnia/src/ui/components/__tests__/workspace-card.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; - -import { getVersionDisplayment } from '../workspace-card'; - -describe('getVersionDisplayment', () => { - it('returns null, undefined, and empty string as-is', () => { - expect(getVersionDisplayment(null)).toEqual(null); - expect(getVersionDisplayment(undefined)).toEqual(undefined); - expect(getVersionDisplayment('')).toEqual(''); - }); - - it('returns numbers as strings', () => { - expect(getVersionDisplayment(0)).toEqual('v0'); // important to make sure we handle, since `0` is falsey - expect(getVersionDisplayment(1)).toEqual('v1'); - expect(getVersionDisplayment(1.1)).toEqual('v1.1'); - }); - - it('does not add a `v` if the string starts with one', () => { - expect(getVersionDisplayment('v1')).toEqual('v1'); - expect(getVersionDisplayment('victor')).toEqual('victor'); - }); - - it("adds a `v` to all strings that don't start with a v", () => { - expect(getVersionDisplayment('1')).toEqual('v1'); - expect(getVersionDisplayment('1.0.0')).toEqual('v1.0.0'); - expect(getVersionDisplayment('alpha1')).toEqual('valpha1'); // yes, we know this is non-ideal, see INS-1320. - }); -}); diff --git a/packages/insomnia/src/ui/components/card.tsx b/packages/insomnia/src/ui/components/card.tsx deleted file mode 100644 index fec1c06cf..000000000 --- a/packages/insomnia/src/ui/components/card.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import React, { FC, ReactNode, useState } from 'react'; -import styled from 'styled-components'; - -import { IconEnum, SvgIcon } from './svg-icon'; - -const StyledCard = styled.div({ - '&&': { - transition: 'all 0.1s ease-out', - }, - height: '196px', - width: '204px', - border: '1px solid var(--hl-sm)', - display: 'flex', - flexDirection: 'column', - flexGrow: '0', - flexShrink: '0', - color: 'var(--font-dark)', - borderRadius: 'var(--radius-md)', - - '&:hover': { - borderColor: 'var(--color-surprise)', - boxShadow: 'var(--padding-sm) var(--padding-sm) calc(var(--padding-xl) * 1.5) calc(0px - var(--padding-xl)) rgba(0, 0, 0, 0.2)', - cursor: 'pointer', - '.title': { - color: 'var(--color-surprise)', - }, - }, - - '&.selected': { - backgroundColor: 'rgba(var(--color-surprise-rgb), 0.05)', - borderColor: 'var(--color-surprise)', - '.title': { - color: 'var(--color-surprise)', - }, - cursor: 'default', - '&:hover': { - boxShadow: 'none', - }, - }, - - '&.deselected': { - backgroundColor: 'transparent', - border: '1px solid var(--hl-sm)', - cursor: 'default', - '&:hover': { - borderColor: 'var(--color-surprise)', - boxShadow: '3px 3px 20px -10px rgba(0, 0, 0, 0.2)', - }, - }, -}); - -const CardHeader = styled.div({ - textAlign: 'left', - padding: 'var(--padding-md) var(--padding-xs) 0 var(--padding-xs)', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - - '.header-item': { - fontSize: 'var(--font-size-xs)', - padding: 'var(--padding-xs)', - }, - - '.card-badge': { - marginLeft: 'var(--padding-sm)', - backgroundColor: 'var(--hl-xs)', - borderRadius: 'var(--radius-sm)', - display: 'flex', - alignItems: 'center', - padding: 0, - overflow: 'hidden', - - '&.card-icon': { - padding: 0, - }, - - '&:empty': { - visibility: 'hidden', - }, - }, - - '.card-menu': { - margin: 'calc(-1 * var(--padding-sm))', - marginRight: 'var(--padding-xs)', - height: '100%', - display: 'flex', - alignItems: 'center', - fontWeight: '900', - fontSize: 'var(--font-size-lg)', - color: 'var(--font-color)', - - button: { - padding: 'var(--padding-xs) var(--padding-sm)', - - '&:hover': { - backgroundColor: 'var(--hl-xxs)', - }, - }, - }, - - '.card-checkbox-label': { - display: 'block', - position: 'relative', - margin: 'auto', - cursor: 'default', - fontSize: 'var(--font-size-xl)', - lineHeight: 'var(--font-size-xl)', - height: 'var(--font-size-xl)', - width: 'var(--font-size-xl)', - clear: 'both', - - '.card-checkbox': { - position: 'absolute', - top: '0', - left: '0', - height: 'var(--font-size-xl)', - width: 'var(--font-size-xl)', - backgroundColor: 'rgba(var(--color-surprise-rgb), 0.1)', - borderRadius: 'var(--radius-md)', - border: '2px solid var(--color-surprise)', - - '&::after': { - position: 'absolute', - content: '""', - height: '0', - width: '0', - borderRadius: 'var(--radius-md)', - border: 'solid var(--color-font-info)', - borderWidth: '0 var(--padding-sm) var(--padding-sm) 0', - transform: 'rotate(0deg) scale(0)', - opacity: '1', - }, - }, - - input :{ - position: 'absolute', - opacity: '0', - cursor: 'default', - - '&:checked ~ .card-checkbox': { - backgroundColor: 'var(--color-surprise)', - borderRadius: 'var(--radius-md)', - transform: 'rotate(0deg) scale(1)', - opacity: '1', - - '&::after': { - transform: 'rotate(45deg) scale(1)', - opacity: '1', - left: '0.375rem', - top: '0.125rem', - width: '0.3125rem', - height: '0.625rem', - border: 'solid var(--color-bg)', - borderWidth: '0 2px 2px 0', - backgroundColor: 'transparent', - borderRadius: '0', - }, - }, - }, - }, -}); - -const CardBody = styled.div({ - justifyContent: 'normal', - fontWeight: '400', - color: 'var(--font-color)', - marginTop: 'var(--padding-md)', - paddingLeft: 'var(--padding-md)', - overflowY: 'auto', - - '.title': { - fontSize: 'var(--font-size-md)', - paddingRight: 'var(--padding-md)', - overflowX: 'hidden', - textOverflow: 'ellipsis', - }, - - '.version': { - fontSize: 'var(--font-size-xs)', - paddingTop: 'var(--padding-xs)', - }, -}); - -const CardFooter = styled.div({ - marginTop: 'auto', - paddingLeft: 'var(--padding-md)', - paddingTop: 'var(--padding-sm)', - paddingBottom: 'var(--padding-sm)', - color: 'var(--hl-xl)', - - 'span': { - display: 'flex', - justifyContent: 'left', - flexDirection: 'row', - marginBottom: 'var(--padding-xs)', - svg: { - width: '1em', - height: '1em', - }, - }, - - '.icoLabel': { - paddingLeft: 'var(--padding-xs)', - fontSize: 'var(--font-size-sm)', - '*': { - display: 'inline', - }, - }, -}); - -export interface CardProps { - docBranch?: ReactNode; - docLog?: ReactNode; - docMenu?: ReactNode; - docTitle?: ReactNode; - docVersion?: ReactNode; - tagLabel: ReactNode; - docFormat?: ReactNode; - onChange?: (event: React.SyntheticEvent) => any; - onClick?: (event: React.SyntheticEvent) => any; - selectable?: boolean; -} - -export const Card: FC = props => { - const [state, setState] = useState({ - selected: false, - selectable: false, - }); - - const { - tagLabel, - docTitle, - docVersion, - docBranch, - docLog, - docMenu, - docFormat, - selectable, - onClick, - } = props; - - return ( - - -
{tagLabel}
- {selectable ? ( -
- -
- ) : ( -
{docMenu}
- )} -
- - {docTitle && ( -
- {docTitle} -
- )} - {docVersion &&
{docVersion}
} -
- - {docFormat && ( - - -
{docFormat}
-
- )} - {docBranch && ( - - -
{docBranch}
-
- )} - {docLog && ( - - -
{docLog}
-
- )} -
-
- ); -}; diff --git a/packages/insomnia/src/ui/components/dropdowns/project-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/project-dropdown.tsx index cd4e9201f..cd33cad84 100644 --- a/packages/insomnia/src/ui/components/dropdowns/project-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/project-dropdown.tsx @@ -1,12 +1,18 @@ +import { IconName } from '@fortawesome/fontawesome-svg-core'; import React, { FC, Fragment, useState } from 'react'; +import { + Button, + Item, + Menu, + MenuTrigger, + Popover, +} from 'react-aria-components'; import { useFetcher } from 'react-router-dom'; -import { toKebabCase } from '../../../common/misc'; -import { strings } from '../../../common/strings'; import { Project, } from '../../../models/project'; -import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../base/dropdown'; +import { Icon } from '../icon'; import ProjectSettingsModal from '../modals/project-settings-modal'; interface Props { @@ -15,46 +21,72 @@ interface Props { } export const ProjectDropdown: FC = ({ project, organizationId }) => { - const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); + const [isProjectSettingsModalOpen, setIsProjectSettingsModalOpen] = + useState(false); const deleteProjectFetcher = useFetcher(); + + const projectActionList: { + id: string; + name: string; + icon: IconName; + action: (projectId: string) => void; + }[] = [ + { + id: 'settings', + name: 'Settings', + icon: 'gear', + action: () => setIsProjectSettingsModalOpen(true), + }, + { + id: 'delete', + name: 'Delete', + icon: 'trash', + action: projectId => + deleteProjectFetcher.submit( + {}, + { + method: 'post', + action: `/organization/${organizationId}/project/${projectId}/delete`, + } + ), + }, + ]; return ( - - - - } - > - - setIsSettingsModalOpen(true)} - /> - - - - deleteProjectFetcher.submit( - {}, - { method: 'post', action: `/organization/${organizationId}/project/${project._id}/delete` } - ) - } - /> - - - {isSettingsModalOpen && ( + + + + { + projectActionList.find(({ id }) => key === id)?.action(project._id); + }} + items={projectActionList} + className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none" + > + {item => ( + + + {item.name} + + )} + + + + {isProjectSettingsModalOpen && ( setIsSettingsModalOpen(false)} + onHide={() => setIsProjectSettingsModalOpen(false)} project={project} /> )} diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-card-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-card-dropdown.tsx index fc8f63595..7b2312c46 100644 --- a/packages/insomnia/src/ui/components/dropdowns/workspace-card-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/workspace-card-dropdown.tsx @@ -104,7 +104,7 @@ export const WorkspaceCardDropdown: FC = props => { aria-label='Workspace Actions Dropdown' onOpen={refresh} triggerButton={ - + } diff --git a/packages/insomnia/src/ui/components/workspace-card.tsx b/packages/insomnia/src/ui/components/workspace-card.tsx deleted file mode 100644 index 498c4cc9f..000000000 --- a/packages/insomnia/src/ui/components/workspace-card.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React, { Fragment } from 'react'; -import { FC } from 'react'; -import styled from 'styled-components'; - -import { - ACTIVITY_DEBUG, - ACTIVITY_SPEC, - GlobalActivity, -} from '../../common/constants'; -import { fuzzyMatchAll } from '../../common/misc'; -import { strings } from '../../common/strings'; -import { Project } from '../../models/project'; -import { isDesign } from '../../models/workspace'; -import { WorkspaceWithMetadata } from '../routes/project'; -import { Highlight } from './base/highlight'; -import { Card } from './card'; -import { WorkspaceCardDropdown } from './dropdowns/workspace-card-dropdown'; -import { TimeFromNow } from './time-from-now'; - -const Label = styled.div({ - display: 'flex', - alignItems: 'center', - overflow: 'hidden', - gap: 'var(--padding-sm)', - height: '1.5rem', - paddingRight: 'var(--padding-sm)', -}); - -const LabelIcon = styled.div({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: '0.2rem', - height: '1rem', -}); - -export interface WorkspaceCardProps { - workspaceWithMetadata: WorkspaceWithMetadata; - filter: string; - activeProject: Project; - onSelect: (workspaceId: string, activity: GlobalActivity) => void; - projects: Project[]; -} - -/** note: numbers are not technically valid (and, indeed, we throw a lint error), but we need to handle this case otherwise a user will not be able to import a spec with a malformed version and even _see_ that it's got the error. */ -export const getVersionDisplayment = (version?: string | number | null) => { - if (version === null || version === undefined || version === '') { - return version; - } - - if (typeof version === 'number') { - console.warn( - `OpenAPI documents must not use number data types for $.info.version, found ${version}` - ); - version = String(version); - } else if (typeof version !== 'string') { - console.error('unable to parse spec version'); - return ''; - } - - if (!version.startsWith('v')) { - return `v${version}`; - } - - return version; -}; - -export const WorkspaceCard: FC = ({ - workspaceWithMetadata, - filter, - activeProject, - projects, - onSelect, -}) => { - const { - apiSpec, - lastActiveBranch, - lastModifiedTimestamp, - workspace, - lastCommitTime, - modifiedLocally, - lastCommitAuthor, - spec, - specFormat, - specFormatVersion, - hasUnsavedChanges, - workspaceMeta, - clientCertificates, - caCertificate, - } = workspaceWithMetadata; - let branch = lastActiveBranch; - - let log = ; - - if (hasUnsavedChanges) { - // Show locally unsaved changes for spec - // NOTE: this doesn't work for non-spec workspaces - branch = lastActiveBranch + '*'; - if (modifiedLocally) { - log = ( - - {' '} - (unsaved) - - ); - } - } else if (lastCommitTime) { - // Show last commit time and author - log = ( - - {' '} - {lastCommitAuthor && `by ${lastCommitAuthor}`} - - ); - } - - const version = getVersionDisplayment(spec?.info?.version); - let label: string = strings.collection.singular; - let format = ''; - let labelIcon = ; - let defaultActivity = ACTIVITY_DEBUG; - let title = workspace.name; - - if (isDesign(workspace)) { - label = strings.document.singular; - labelIcon = ; - - if (specFormat === 'openapi') { - format = `OpenAPI ${specFormatVersion}`; - } else if (specFormat === 'swagger') { - // NOTE: This is not a typo, we're labeling Swagger as OpenAPI also - format = `OpenAPI ${specFormatVersion}`; - } - - defaultActivity = ACTIVITY_SPEC; - title = apiSpec?.fileName || title; - } - - // Filter the card by multiple different properties - const matchResults = fuzzyMatchAll( - filter, - [title, label, branch || '', version || ''], - { - splitSpace: true, - loose: true, - } - ); - - // Return null if we don't match the filter - if (filter && !matchResults) { - return null; - } - - return ( - : undefined - } - docTitle={title ? : undefined} - docVersion={ - version ? : undefined - } - tagLabel={ - label ? ( - - ) : undefined - } - docLog={log} - docMenu={ - - } - docFormat={format} - onClick={() => onSelect(workspace._id, defaultActivity)} - /> - ); -}; diff --git a/packages/insomnia/src/ui/routes/actions.tsx b/packages/insomnia/src/ui/routes/actions.tsx index 80777e8e4..db3e4df5c 100644 --- a/packages/insomnia/src/ui/routes/actions.tsx +++ b/packages/insomnia/src/ui/routes/actions.tsx @@ -107,6 +107,8 @@ export const createNewWorkspaceAction: ActionFunction = async ({ parentId: projectId, }); + console.log({ workspace, name }); + if (scope === 'design') { await models.apiSpec.getOrCreateForParentId(workspace._id); } diff --git a/packages/insomnia/src/ui/routes/project.tsx b/packages/insomnia/src/ui/routes/project.tsx index dc18d0e4b..d093365d4 100644 --- a/packages/insomnia/src/ui/routes/project.tsx +++ b/packages/insomnia/src/ui/routes/project.tsx @@ -1,14 +1,19 @@ -import React, { FC, Fragment, useRef, useState } from 'react'; +import { IconName } from '@fortawesome/fontawesome-svg-core'; +import React, { FC, Fragment, useState } from 'react'; import { - AriaButtonProps, - AriaGridListItemOptions, - AriaGridListOptions, - mergeProps, - useButton, - useFocusRing, - useGridList, - useGridListItem, -} from 'react-aria'; + Button, + GridList, + Heading, + Input, + Item, + ListBox, + Menu, + MenuTrigger, + Popover, + SearchField, + Select, + SelectValue, +} from 'react-aria-components'; import { LoaderFunction, matchPath, @@ -19,16 +24,13 @@ import { useParams, useRouteLoaderData, useSearchParams, - useSubmit, } from 'react-router-dom'; -import { Item, ListProps, ListState, useListState } from 'react-stately'; -import styled from 'styled-components'; import { parseApiSpec, ParsedApiSpec } from '../../common/api-specs'; import { - ACTIVITY_DEBUG, - ACTIVITY_SPEC, + DASHBOARD_SORT_ORDERS, DashboardSortOrder, + dashboardSortOrderName, getProductName, } from '../../common/constants'; import { fuzzyMatchAll, isNotNullOrUndefined } from '../../common/misc'; @@ -46,546 +48,25 @@ import { } from '../../models/organization'; import { DEFAULT_PROJECT_ID, - isDefaultProject, isRemoteProject, Project, } from '../../models/project'; import { isDesign, Workspace } from '../../models/workspace'; import { WorkspaceMeta } from '../../models/workspace-meta'; import { invariant } from '../../utils/invariant'; -import { - Dropdown, - DropdownButton, - DropdownItem, - DropdownSection, - ItemContent, -} from '../components/base/dropdown'; -import { DashboardSortDropdown } from '../components/dropdowns/dashboard-sort-dropdown'; import { ProjectDropdown } from '../components/dropdowns/project-dropdown'; import { RemoteWorkspacesDropdown } from '../components/dropdowns/remote-workspaces-dropdown'; +import { WorkspaceCardDropdown } from '../components/dropdowns/workspace-card-dropdown'; import { ErrorBoundary } from '../components/error-boundary'; +import { Icon } from '../components/icon'; import { showAlert, showPrompt } from '../components/modals'; import { GitRepositoryCloneModal } from '../components/modals/git-repository-settings-modal/git-repo-clone-modal'; import { ImportModal } from '../components/modals/import-modal'; import { EmptyStatePane } from '../components/panes/project-empty-state-pane'; import { SidebarLayout } from '../components/sidebar-layout'; -import { Button } from '../components/themed-button/button'; -import { WorkspaceCard } from '../components/workspace-card'; +import { TimeFromNow } from '../components/time-from-now'; import { RootLoaderData } from './root'; -const StyledDropdownButton = styled(DropdownButton).attrs({ - variant: 'outlined', -})({ - '&&': { - marginLeft: 'var(--padding-md)', - gap: 'var(--padding-sm)', - }, -}); - -const SearchFormControl = styled.div({ - position: 'relative', - fontSize: 'var(--font-size-md)', - maxWidth: '400px', - minWidth: '200px', -}); - -const SearchInput = styled.input({ - '&&': { - paddingRight: 'var(--padding-lg)!important', - fontSize: 'var(--font-size-sm)', - }, -}); - -const PaneHeaderToolbar = styled.div({ - display: 'flex', - boxSizing: 'border-box', - justifyContent: 'space-between', - flex: 0, - gridColumn: '1 / -1', - width: '100%', - position: 'sticky', - top: 0, - zIndex: 1, - paddingTop: 'var(--padding-md)', - paddingBottom: 'var(--padding-sm)', - backgroundColor: 'var(--color-bg)', -}); - -const Pane = styled.div({ - position: 'relative', - height: '100%', - width: '100%', - boxSizing: 'border-box', - background: 'var(--color-bg)', - display: 'grid', - gridAutoColumns: 'minmax(204px, auto)', - gridTemplateColumns: 'repeat(auto-fit, 208px)', - gridAutoRows: 'min-content', - placeContent: 'start', - overflow: 'hidden auto', - padding: '0 var(--padding-md) var(--padding-md) var(--padding-md)', - gap: '1rem', - flex: '1 0 auto', - overflowY: 'auto', -}); - -const SidebarTitle = styled.h2({ - display: 'flex', - alignItems: 'center', - gap: 'var(--padding-sm)', - padding: 'var(--padding-sm)', - fontSize: 'var(--font-size-md)', - margin: 0, - paddingLeft: 'var(--padding-md)', - borderBottom: '1px solid var(--hl-md)', - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', -}); - -const SidebarSection = styled.div({ - boxSizing: 'border-box', - width: '100%', - display: 'flex', - alignItems: 'center', - flexDirection: 'row', - flexWrap: 'wrap', - fontSize: 'var(--font-size-sm)', - color: 'var(--hl)', - paddingRight: 'var(--padding-xs)', - height: 'var(--height-nav)', - - // Make it scroll when too skinny - overflow: 'auto', - - '&::-webkit-scrollbar': { - display: 'none', - }, - - '& > *': { - flex: '1', - marginTop: 'var(--padding-xs)', - marginBottom: 'var(--padding-xs)', - maxWidth: '100%', - }, - - '& > .dropdown > *': { - width: '100%', - }, - - '& > *:first-child': { - marginRight: 'var(--padding-xxs)', - }, - - '& > *:last-child': { - marginLeft: 'var(--padding-xxs)', - }, - - '.sidebar__menu__thing': { - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - - '& > .sidebar__menu__thing__text': { - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - - 'i.fa': { - // Bump the drop down caret down a bit - position: 'relative', - top: '1px', - }, - }, - - '.btn': { - borderRadius: '900px', - height: 'var(--line-height-xxs)', - paddingLeft: 'var(--padding-xxs)', - paddingRight: 'var(--padding-xxs)', - color: 'var(--color-font)', - - '&: hover, &:focus': { - opacity: '1', - }, - }, -}); - -const SidebarSectionTitle = styled.h3({ - paddingLeft: 'var(--padding-md)', - textTransform: 'uppercase', - color: 'var(--color-font)', - fontSize: 'var(--font-size-xs)', -}); - -const Sidebar = styled.div({ - height: '100%', - overflow: 'hidden', - display: 'flex', - flexDirection: 'column', -}); - -const SidebarDivider = styled.span({ - width: '100%', - borderTop: '1px solid var(--hl-md)', - paddingBottom: 'var(--padding-md)', -}); - -const SearchFormContainer = styled.div({ - padding: 'var(--padding-xs)', - paddingTop: 0, -}); - -const ProjectListContainer = styled.ul({ - flex: 1, -}); - -const OrganizationProjectsSidebar: FC<{ - title: string; - projects: Project[]; - workspaces: Workspace[]; - activeProject: Project; - organizationId: string; - allFilesCount: number; - documentsCount: number; - collectionsCount: number; - createNewCollection: () => void; - createNewDocument: () => void; -}> = ({ - activeProject, - projects, - title, - organizationId, - collectionsCount, - documentsCount, - allFilesCount, - createNewCollection, - createNewDocument, -}) => { - const createNewProjectFetcher = useFetcher(); - const { organizations } = useRouteLoaderData('root') as RootLoaderData; - const navigate = useNavigate(); - const submit = useSubmit(); - const [searchParams] = useSearchParams(); - const [isSearchOpen, setSearchOpen] = useState(false); - - // Only show the search in the default organization (local data) since other organizations (remote data) only have one project - const shouldShowSearch = organizationId === DEFAULT_ORGANIZATION_ID; - - return ( - - - - {title} - - } - > - - {organization => ( - - { - navigate(`/organization/${organization._id}`); - }} - /> - - )} - - - - - Projects ({projects.length}) - {shouldShowSearch && ( - - )} - - - {isSearchOpen && shouldShowSearch && ( - - - - submit({ - ...Object.fromEntries(searchParams.entries()), - projectName: event.target.value, - }) - } - className="no-margin" - /> - - - )} - - {projects.map(proj => { - return ( -
  • -
    - - {!isDefaultProject(proj) && ( -
    - -
    - )} -
    -
  • - ); - })} -
    - - - - - { - const scope = [...selection]?.[0]?.toString(); - - if (scope) { - submit({ - ...Object.fromEntries(searchParams.entries()), - scope, - }); - } - }} - > - - - - - - - - - { - createNewDocument(); - }} - > - - - - - - - - { - createNewCollection(); - }} - > - - - - - - - - - - { - window.main.openInBrowser(key.toString()); - }} - > - - - - - - - - ); - }; - -const ButtonWithoutHoverBackground = styled(Button)({ - '&&:hover': { - backgroundColor: 'unset', - }, -}); - -const ListItemButton = ( - props: AriaButtonProps & { - children: React.ReactNode; - } -) => { - const ref = useRef(null); - const { buttonProps } = useButton(props, ref); - - return ( - - {props.children} - - ); -}; - -const SidebarListItemContent = styled.div<{ - level: number; -}>(props => ({ - display: 'flex', - width: '100%', - alignItems: 'center', - justifyContent: 'space-between', - padding: 'var(--padding-sm)', - paddingLeft: `calc(var(--padding-md) * ${props.level || 1})`, - boxSizing: 'border-box', - position: 'relative', -})); - -const StyledSidebarListItemTitle = styled.div({ - display: 'flex', - alignItems: 'center', - gap: 'var(--padding-sm)', -}); - -interface SidebarListItemTitleProps { - icon: string; - label: string; -} - -const SidebarListItemTitle = ({ icon, label }: SidebarListItemTitleProps) => { - return ( - - {label} - - ); -}; - export interface WorkspaceWithMetadata { _id: string; hasUnsavedChanges: boolean; @@ -610,7 +91,9 @@ export const indexLoader: LoaderFunction = async ({ params }) => { const { organizationId } = params; invariant(organizationId, 'Organization ID is required'); - const prevOrganizationLocation = localStorage.getItem(`locationHistoryEntry:${organizationId}`); + const prevOrganizationLocation = localStorage.getItem( + `locationHistoryEntry:${organizationId}` + ); if (prevOrganizationLocation) { const match = matchPath( @@ -622,7 +105,9 @@ export const indexLoader: LoaderFunction = async ({ params }) => { ); if (match && match.params.organizationId && match.params.projectId) { - return redirect(`/organization/${match?.params.organizationId}/project/${match?.params.projectId}`); + return redirect( + `/organization/${match?.params.organizationId}/project/${match?.params.projectId}` + ); } } @@ -648,6 +133,7 @@ export interface ProjectLoaderData { allFilesCount: number; documentsCount: number; collectionsCount: number; + projectsCount: number; activeProject: Project; projects: Project[]; organization: Organization; @@ -668,14 +154,22 @@ export const loader: LoaderFunction = async ({ let project = await models.project.getById(projectId); if (!project) { - const defaultproject = await models.project.getById(DEFAULT_PROJECT_ID); - project = defaultproject || await models.project.create({ _id: DEFAULT_PROJECT_ID, name: getProductName(), remoteId: null }); + const defaultProject = await models.project.getById(DEFAULT_PROJECT_ID); + project = + defaultProject || + (await models.project.create({ + _id: DEFAULT_PROJECT_ID, + name: getProductName(), + remoteId: null, + })); } invariant(project, 'Project was not found'); const projectWorkspaces = await models.workspace.findByParentId(project._id); - const getWorkspaceMetaData = async (workspace: Workspace): Promise => { + const getWorkspaceMetaData = async ( + workspace: Workspace + ): Promise => { const apiSpec = await models.apiSpec.getByParentId(workspace._id); let spec: ParsedApiSpec['contents'] = null; @@ -692,7 +186,9 @@ export const loader: LoaderFunction = async ({ // TODO: Check for parse errors if it's an invalid spec } } - const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); + const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId( + workspace._id + ); invariant(workspaceMeta, 'WorkspaceMeta was not found'); const lastActiveBranch = workspaceMeta?.cachedGitRepositoryBranch; @@ -702,7 +198,7 @@ export const loader: LoaderFunction = async ({ const workspaceModified = workspaceMeta?.modified || workspace.modified; const modifiedLocally = isDesign(workspace) - ? (apiSpec?.modified || 0) + ? apiSpec?.modified || 0 : workspaceModified; // Span spec, workspace and sync related timestamps for card last modified label and sort order @@ -719,11 +215,13 @@ export const loader: LoaderFunction = async ({ const hasUnsavedChanges = Boolean( isDesign(workspace) && - workspaceMeta?.cachedGitLastCommitTime && - modifiedLocally > workspaceMeta?.cachedGitLastCommitTime + workspaceMeta?.cachedGitLastCommitTime && + modifiedLocally > workspaceMeta?.cachedGitLastCommitTime + ); + + const clientCertificates = await models.clientCertificate.findByParentId( + workspace._id ); - const name = isDesign(workspace) ? (apiSpec?.fileName || '') : workspace.name; - const clientCertificates = await models.clientCertificate.findByParentId(workspace._id); return { _id: workspace._id, @@ -736,16 +234,13 @@ export const loader: LoaderFunction = async ({ lastActiveBranch, spec, specFormat, - name, + name: workspace.name, apiSpec, specFormatVersion, workspaceMeta, clientCertificates, caCertificate: await models.caCertificate.findByParentId(workspace._id), - workspace: { - ...workspace, - name, - }, + workspace, }; }; @@ -757,16 +252,25 @@ export const loader: LoaderFunction = async ({ const workspaces = workspacesWithMetaData .filter(w => (scope !== 'all' ? w.workspace.scope === scope : true)) // @TODO - Figure out if the database has a way to sort/filter items that could replace this logic. - .filter(workspace => filter ? Boolean(fuzzyMatchAll(filter, - // Use the filter string to match against these properties - [ - workspace.name, - workspace.workspace.scope === 'design' ? 'document' : 'collection', - workspace.lastActiveBranch || '', - workspace.specFormatVersion || '', - ], - { splitSpace: true, loose: true } - )?.indexes) : true) + .filter(workspace => + filter + ? Boolean( + fuzzyMatchAll( + filter, + // Use the filter string to match against these properties + [ + workspace.name, + workspace.workspace.scope === 'design' + ? 'document' + : 'collection', + workspace.lastActiveBranch || '', + workspace.specFormatVersion || '', + ], + { splitSpace: true, loose: true } + )?.indexes + ) + : true + ) .sort((a, b) => sortMethodMap[sortOrder as DashboardSortOrder](a, b)); const allProjects = await models.project.all(); @@ -785,11 +289,12 @@ export const loader: LoaderFunction = async ({ organizationId === DEFAULT_ORGANIZATION_ID ? defaultOrganization : { - _id: organizationId, - name: projects[0].name, - }, + _id: organizationId, + name: projects[0].name, + }, workspaces, projects, + projectsCount: organizationProjects.length, activeProject: project, allFilesCount: workspacesWithMetaData.length, documentsCount: workspacesWithMetaData.filter( @@ -810,14 +315,21 @@ const ProjectRoute: FC = () => { allFilesCount, collectionsCount, documentsCount, + projectsCount, } = useLoaderData() as ProjectLoaderData; - const { organizationId } = useParams() as { organizationId: string }; - const [searchParams] = useSearchParams(); + + const { organizationId, projectId } = useParams() as { + organizationId: string; + projectId: string; + }; + + const { organizations } = useRouteLoaderData('root') as RootLoaderData; + + const [searchParams, setSearchParams] = useSearchParams(); const [isGitRepositoryCloneModalOpen, setIsGitRepositoryCloneModalOpen] = useState(false); const fetcher = useFetcher(); - const submit = useSubmit(); const navigate = useNavigate(); const filter = searchParams.get('filter') || ''; const sortOrder = @@ -869,168 +381,553 @@ const ProjectRoute: FC = () => { }); }; + const createNewProjectFetcher = useFetcher(); + const importFromGit = () => { setIsGitRepositoryCloneModalOpen(true); }; - const hasWorkspaces = workspaces?.length > 0; + const createInProjectActionList: { + id: string; + name: string; + icon: IconName; + action: () => void; + }[] = [ + { + id: 'new-collection', + name: 'Request collection', + icon: 'bars', + action: createNewCollection, + }, + { + id: 'new-document', + name: 'Design document', + icon: 'file', + action: createNewDocument, + }, + { + id: 'import', + name: 'Import', + icon: 'file-import', + action: () => { + setImportModalType('file'); + }, + }, + { + id: 'git-clone', + name: 'Git Clone', + icon: 'code-fork', + action: importFromGit, + }, + ]; + + const scopeActionList: { + id: string; + label: string; + icon: IconName; + level: number; + action?: { + icon: IconName; + label: string; + run: () => void; + }; + }[] = [ + { + id: 'all', + label: `All files (${allFilesCount})`, + icon: 'folder', + level: 0, + }, + { + id: 'document', + label: `Documents (${documentsCount})`, + level: 1, + icon: 'file', + action: { + icon: 'plus', + label: 'New design document', + run: createNewDocument, + }, + }, + { + id: 'collection', + label: `Collections (${collectionsCount})`, + level: 1, + icon: 'bars', + action: { + icon: 'plus', + label: 'New request collection', + run: createNewCollection, + }, + }, + ]; return ( w.workspace)} - activeProject={activeProject} - allFilesCount={allFilesCount} - collectionsCount={collectionsCount} - documentsCount={documentsCount} - createNewCollection={createNewCollection} - createNewDocument={createNewDocument} - /> +
    +
    + +
    +
    + + Projects ({projectsCount}) + + {organizationId === DEFAULT_ORGANIZATION_ID && ( +
    + { + setSearchParams({ + ...Object.fromEntries(searchParams.entries()), + projectName, + }); + }} + > + +
    + +
    +
    + + +
    + )} + + { + if (keys !== 'all') { + const value = keys.values().next().value; + navigate({ + pathname: `/organization/${organizationId}/project/${value}`, + search: searchParams.toString(), + }); + } + }} + > + {item => { + return ( + +
    + + + {item.name} + + {item._id !== DEFAULT_PROJECT_ID && } +
    +
    + ); + }} +
    +
    + { + if (keys !== 'all') { + const value = keys.values().next().value; + setSearchParams({ + ...Object.fromEntries(searchParams.entries()), + scope: value, + }); + } + }} + > + {item => { + return ( + +
    + + + + {item.label} + + + {item.action && ( + + )} +
    +
    + ); + }} +
    + +
    } renderPaneOne={ - +
    + { + setSearchParams({ + ...Object.fromEntries(searchParams.entries()), + filter, + }); + }} + > + +
    + +
    +
    + + + + + + { + const item = createInProjectActionList.find( + item => item.id === key + ); + if (item) { + item.action(); + } + }} + items={createInProjectActionList} + className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none" + > + {item => ( + + + {item.name} + + )} + + + + {isRemoteProject(activeProject) && ( + - ))} - {filter && !hasWorkspaces && ( -
    -

    - No documents found for {filter} -

    -
    - )} - {!filter && !hasWorkspaces && ( - setImportModalType('file')} - cloneFromGit={importFromGit} - /> - )} - + )} +
    + + { + navigate( + `/organization/${organizationId}/project/${projectId}/workspace/${key}/debug` + ); + }} + className="flex-1 overflow-y-auto data-[empty]:flex data-[empty]:justify-center grid [grid-template-columns:repeat(auto-fit,200px)] [grid-template-rows:repeat(auto-fit,200px)] gap-4 p-[--padding-md]" + renderEmptyState={() => { + if (filter) { + return ( +
    +

    + No documents found for {filter} +

    +
    + ); + } + + return ( + setImportModalType('file')} + cloneFromGit={importFromGit} + /> + ); + }} + > + {item => { + return ( + +
    +
    + {isDesign(item.workspace) ? ( +
    + +
    + ) : ( +
    + +
    + )} + + {isDesign(item.workspace) + ? 'Document' + : 'Collection'} + +
    + + +
    + + {item.workspace.name} + +
    + {item.spec?.info?.version && ( +
    + {item.spec.info.version.startsWith('v') ? '' : 'v'} + {item.spec.info.version} +
    + )} + {item.specFormat && ( +
    + + + {item.specFormat === 'openapi' + ? 'OpenAPI' + : 'Swagger'}{' '} + {item.specFormatVersion} + +
    + )} + {item.lastActiveBranch && ( +
    + + + {item.lastActiveBranch} + +
    + )} + {item.lastModifiedTimestamp && ( +
    + + + + {!item.hasUnsavedChanges && + item.lastCommitTime && + item.lastCommitAuthor && + `by ${item.lastCommitAuthor}`} + +
    + )} +
    +
    + ); + }} +
    + } /> {isGitRepositoryCloneModalOpen && ( @@ -1052,81 +949,4 @@ const ProjectRoute: FC = () => { ); }; -const ListContent = styled.ul({ - listStyle: 'none', - margin: 0, - padding: 0, - width: '100%', -}); - -const List = ( - props: ListProps & AriaGridListOptions -) => { - const state = useListState(props); - const ref = useRef(null); - const { gridProps } = useGridList(props, state, ref); - - return ( - - {[...state.collection].map(item => ( - - ))} - - ); -}; - -const ListItemContent = styled.li<{ - isSelected: boolean; - nestingLevel?: number; -}>(props => ({ - display: 'flex', - alignItems: 'center', - flexWrap: 'nowrap', - width: '100%', - outline: 'none', - padding: 0, - gap: 'var(--padding-sm)', - color: props.isSelected ? 'var(--color-font)' : 'var(--hl)', - backgroundColor: props.isSelected ? 'var(--hl-xs)' : undefined, - boxSizing: 'border-box', - '&:hover': { - backgroundColor: 'var(--hl-xs)', - }, -})); - -interface ItemProps { - item: AriaGridListItemOptions['node']; - state: ListState; -} - -const ListItem = ({ item, state }: ItemProps) => { - const ref = React.useRef(null); - const { rowProps, gridCellProps } = useGridListItem( - { node: item }, - state, - ref - ); - - const { focusProps } = useFocusRing(); - const isSelected = state.selectionManager.selectedKeys.has(item.key); - - return ( - -
    - {item.rendered} -
    -
    - ); -}; - export default ProjectRoute; diff --git a/packages/insomnia/src/ui/routes/root.tsx b/packages/insomnia/src/ui/routes/root.tsx index e8f2291ee..1be33d192 100644 --- a/packages/insomnia/src/ui/routes/root.tsx +++ b/packages/insomnia/src/ui/routes/root.tsx @@ -428,14 +428,14 @@ const Root = () => { )} -
    +