Project route UI improvements (#6400)

* project tailwind/aria-components

* update tests

* add failing test

* update e2e tests

* fix workspace name issue

* fix scroll issue

* bye test

---------

Co-authored-by: Filipe Freire <livrofubia@gmail.com>
This commit is contained in:
James Gatz 2023-08-28 15:53:37 +02:00 committed by GitHub
parent 7e3e44c50a
commit 81039277af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 754 additions and 1407 deletions

View File

@ -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();

View File

@ -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();

View File

@ -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

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();

View File

@ -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();

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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.
});
});

View File

@ -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<HTMLInputElement>) => any;
onClick?: (event: React.SyntheticEvent<HTMLDivElement>) => any;
selectable?: boolean;
}
export const Card: FC<CardProps> = props => {
const [state, setState] = useState({
selected: false,
selectable: false,
});
const {
tagLabel,
docTitle,
docVersion,
docBranch,
docLog,
docMenu,
docFormat,
selectable,
onClick,
} = props;
return (
<StyledCard className={state.selected ? 'selected' : 'deselected'} onClick={onClick}>
<CardHeader>
<div className="header-item card-badge">{tagLabel}</div>
{selectable ? (
<div className="header-item card-menu">
<label className="card-checkbox-label">
<input
type="checkbox"
onChange={event => {
setState(state => ({
...state,
selected: !state.selected,
}));
props.onChange?.(event);
}}
/>
<span className="card-checkbox" />
</label>
</div>
) : (
<div className="header-item card-menu">{docMenu}</div>
)}
</CardHeader>
<CardBody>
{docTitle && (
<div className="title">
<strong>{docTitle}</strong>
</div>
)}
{docVersion && <div className="version">{docVersion}</div>}
</CardBody>
<CardFooter>
{docFormat && (
<span>
<SvgIcon icon={IconEnum.file} />
<div className="icoLabel">{docFormat}</div>
</span>
)}
{docBranch && (
<span>
<SvgIcon icon={IconEnum.gitBranch} />
<div className="icoLabel">{docBranch}</div>
</span>
)}
{docLog && (
<span>
<SvgIcon icon={IconEnum.clock} />
<div className="icoLabel">{docLog}</div>
</span>
)}
</CardFooter>
</StyledCard>
);
};

View File

@ -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<Props> = ({ 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 (
<Fragment>
<Dropdown
aria-label='Project Dropdown'
dataTestId={toKebabCase(`ProjectDropDown-${project.name}`)}
triggerButton={
<DropdownButton className="row" title={project.name}>
<i className="fa fa-ellipsis space-left" />
</DropdownButton>
}
>
<DropdownItem aria-label={`${strings.project.singular} Settings`}>
<ItemContent
icon="gear"
style={{ gap: 'var(--padding-sm)' }}
iconStyle={{ width: 'unset', fill: 'var(--hl)' }}
label={`${strings.project.singular} Settings`}
onClick={() => setIsSettingsModalOpen(true)}
/>
</DropdownItem>
<DropdownItem aria-label='Delete'>
<ItemContent
icon="trash-o"
label="Delete"
className="danger"
withPrompt
onClick={() =>
deleteProjectFetcher.submit(
{},
{ method: 'post', action: `/organization/${organizationId}/project/${project._id}/delete` }
)
}
/>
</DropdownItem>
</Dropdown>
{isSettingsModalOpen && (
<MenuTrigger>
<Button
aria-label="Project Actions"
className="opacity-0 items-center hover:opacity-100 focus:opacity-100 data-[pressed]:opacity-100 flex group-focus:opacity-100 group-hover:opacity-100 justify-center h-6 aspect-square 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"
>
<Icon icon="caret-down" />
</Button>
<Popover className="min-w-max">
<Menu
aria-label="Project Actions Menu"
selectionMode="single"
onAction={key => {
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
key={item.id}
id={item.id}
className="flex gap-2 px-[--padding-md] aria-selected:font-bold items-center text-[--color-font] h-[--line-height-xs] w-full text-md whitespace-nowrap bg-transparent hover:bg-[--hl-sm] disabled:cursor-not-allowed focus:bg-[--hl-xs] focus:outline-none transition-colors"
aria-label={item.name}
>
<Icon icon={item.icon} />
<span>{item.name}</span>
</Item>
)}
</Menu>
</Popover>
</MenuTrigger>
{isProjectSettingsModalOpen && (
<ProjectSettingsModal
onHide={() => setIsSettingsModalOpen(false)}
onHide={() => setIsProjectSettingsModalOpen(false)}
project={project}
/>
)}

View File

@ -104,7 +104,7 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
aria-label='Workspace Actions Dropdown'
onOpen={refresh}
triggerButton={
<DropdownButton>
<DropdownButton aria-label='Workspace actions menu button' className="px-4 py-1 flex flex-1 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">
<SvgIcon icon="ellipsis" />
</DropdownButton>
}

View File

@ -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<WorkspaceCardProps> = ({
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 = <TimeFromNow timestamp={lastModifiedTimestamp} />;
if (hasUnsavedChanges) {
// Show locally unsaved changes for spec
// NOTE: this doesn't work for non-spec workspaces
branch = lastActiveBranch + '*';
if (modifiedLocally) {
log = (
<Fragment>
<TimeFromNow className="text-danger" timestamp={modifiedLocally} />{' '}
(unsaved)
</Fragment>
);
}
} else if (lastCommitTime) {
// Show last commit time and author
log = (
<Fragment>
<TimeFromNow timestamp={lastCommitTime} />{' '}
{lastCommitAuthor && `by ${lastCommitAuthor}`}
</Fragment>
);
}
const version = getVersionDisplayment(spec?.info?.version);
let label: string = strings.collection.singular;
let format = '';
let labelIcon = <i className="fa fa-bars" />;
let defaultActivity = ACTIVITY_DEBUG;
let title = workspace.name;
if (isDesign(workspace)) {
label = strings.document.singular;
labelIcon = <i className="fa fa-file-o" />;
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 (
<Card
docBranch={
branch ? <Highlight search={filter} text={branch} /> : undefined
}
docTitle={title ? <Highlight search={filter} text={title} /> : undefined}
docVersion={
version ? <Highlight search={filter} text={version} /> : undefined
}
tagLabel={
label ? (
<Label>
<LabelIcon
style={{
color: isDesign(workspace) ? 'var(--color-font-info)' : 'var(--color-font-surprise)',
backgroundColor: isDesign(workspace) ? 'var(--color-info)' : 'var(--color-surprise)',
}}
>{labelIcon}</LabelIcon>
<Highlight search={filter} text={label} />
</Label>
) : undefined
}
docLog={log}
docMenu={
<WorkspaceCardDropdown
apiSpec={apiSpec}
workspace={workspace}
workspaceMeta={workspaceMeta}
project={activeProject}
projects={projects}
clientCertificates={clientCertificates}
caCertificate={caCertificate}
/>
}
docFormat={format}
onClick={() => onSelect(workspace._id, defaultActivity)}
/>
);
};

View File

@ -107,6 +107,8 @@ export const createNewWorkspaceAction: ActionFunction = async ({
parentId: projectId,
});
console.log({ workspace, name });
if (scope === 'design') {
await models.apiSpec.getOrCreateForParentId(workspace._id);
}

File diff suppressed because it is too large Load Diff

View File

@ -428,14 +428,14 @@ const Root = () => {
)}
</div>
</header>
<div className="[grid-area:Navbar]">
<div className="[grid-area:Navbar] overflow-hidden">
<nav className="flex flex-col items-center place-content-stretch gap-[--padding-md] w-full h-full overflow-y-auto py-[--padding-md]">
{organizations.map(organization => (
<TooltipTrigger key={organization._id}>
<Link>
<NavLink
className={({ isActive }) =>
`select-none text-[--color-font-surprise] hover:no-underline transition-all duration-150 bg-gradient-to-br box-border from-[#4000BF] to-[#154B62] p-[--padding-sm] font-bold outline-[3px] rounded-md w-[28px] h-[28px] flex items-center justify-center active:outline overflow-hidden outline-offset-[3px] outline ${
`select-none text-[--color-font-surprise] flex-shrink-0 hover:no-underline transition-all duration-150 bg-gradient-to-br box-border from-[#4000BF] to-[#154B62] p-[--padding-sm] font-bold outline-[3px] rounded-md w-[28px] h-[28px] flex items-center justify-center active:outline overflow-hidden outline-offset-[3px] outline ${
isActive
? 'outline-[--color-font]'
: 'outline-transparent focus:outline-[--hl-md] hover:outline-[--hl-md]'