mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-27 17:19:42 -04:00
Move insomnia-components in the insomnia package (#5259)
* Move insomnia-components in the insomnia package * remove extra tooltip components * remove button from portal plugin * remove insomniaComponent injection * move spec editor sidebar * move breadcrumb component * move toggle-switch * move table/notice-table * remove unused switch component * move header to app-header * move card * move card-container to wrapper-home * Remove unused multi-switch and radio-group * move svg icon and auto-run convert-svg command * move list-group * move button components into themed-button * move dropdown * remove auto-conversion of svg from npm scripts * track svg icon components to git * remove react-switch and framer-motion * remove sidebar panel, fix lint * remove animate blocks * fix types, remove value from dropdown item * remove button * ignore svgr config * update dropdown unit tests * allow unreachable * oauth e2e test: update locator to make the Create button specific * fix type errors Co-authored-by: jackkav <jackkav@gmail.com>
This commit is contained in:
1 parent
b5f5ad8716
commit
09cf77c4ef
263 files changed
+1588
-20060
No files matched your search
@@ -14,5 +14,4 @@ screenshots/
|
||||
**/__snapshots__/
|
||||
**/dist/
|
||||
**/.cache/
|
||||
**/svgr/
|
||||
*.md
|
||||
@@ -1,3 +0,0 @@
|
||||
dist
|
||||
assets/svgr
|
||||
src/assets/svgr
|
||||
@@ -1,15 +0,0 @@
|
||||
const { OFF, TYPESCRIPT_CONVERSION } = require('eslint-config-helpers');
|
||||
|
||||
/** @type { import('eslint').Linter.Config } */
|
||||
module.exports = {
|
||||
extends: '../../.eslintrc.js',
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': OFF(TYPESCRIPT_CONVERSION),
|
||||
'@typescript-eslint/no-unnecessary-type-constraint': OFF('by an unfortunate quirk of how TypeScript differentiates between casts and JSX, this it is necessary for generic JSX to do extend unknown'),
|
||||
},
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
.cache
|
||||
src/assets/svgr
|
||||
assets/svgr
|
||||
dist
|
||||
@@ -1,7 +0,0 @@
|
||||
# Ignore everything by default
|
||||
# NOTE: NPM publish will never ignore package.json, package-lock.json, README, LICENSE, CHANGELOG
|
||||
# https://npm.github.io/publishing-pkgs-docs/publishing/the-npmignore-file.html
|
||||
*
|
||||
|
||||
# Don't ignore dist folder
|
||||
!dist/**
|
||||
@@ -1 +0,0 @@
|
||||
16.14.2
|
||||
@@ -1,14 +0,0 @@
|
||||
# insomnia-components
|
||||
|
||||
`insomnia-components` is a React component library of UI elements needed to build [Insomnia](https://insomnia.rest).
|
||||
|
||||
## SVGs
|
||||
|
||||
We use [SVGR](https://react-svgr.com) on the source SVGs to minify and convert them to React components. This conversion happens during `npm run bootstrap`.
|
||||
|
||||
The generated icons can automatically be bound to the theme library built into Insomnia, by following a few rules. In the source SVG:
|
||||
|
||||
1. All background colors should be black (#000)
|
||||
1. All foreground colors should be white (#FFF)
|
||||
|
||||
SVGR converts these colors to `fill=''` and `fill='currentColor'` respectively. These icons are exposed via `svg-icon.js`, which sets the css `fill` and `color` of the SVG according to the required theme.
|
||||
@@ -1,14 +0,0 @@
|
||||
/** @type { import('@jest/types').Config.InitialOptions } */
|
||||
module.exports = {
|
||||
preset: '../../jest-preset.js',
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
isolatedModules: true,
|
||||
},
|
||||
},
|
||||
fakeTimers: {
|
||||
enableGlobally: true,
|
||||
},
|
||||
setupFilesAfterEnv: ['./src/jest/setup-after-env.ts'],
|
||||
testEnvironment: 'jsdom',
|
||||
};
|
||||
-17535
File diff suppressed because it is too large.
Load diff
@@ -1,78 +0,0 @@
|
||||
{
|
||||
"name": "insomnia-components",
|
||||
"version": "3.6.1-beta.0",
|
||||
"author": "Kong <office@konghq.com>",
|
||||
"description": "Insomnia UI component library",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Kong/insomnia.git",
|
||||
"directory": "packages/insomnia-components"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Kong/insomnia/issues"
|
||||
},
|
||||
"main": "./dist/commonjs/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/commonjs/index.js",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "npm run convert-svg",
|
||||
"build:source": "vite build",
|
||||
"build:types": "tsc --build tsconfig.build.json",
|
||||
"build": "concurrently --names source,types \"npm run build:source\" \"npm run build:types\"",
|
||||
"clean": "rimraf dist && npm run clean:svg",
|
||||
"clean:svg": "rimraf src/assets/svgr",
|
||||
"clean:svg:index": "rimraf src/assets/svgr/index.tsx",
|
||||
"convert-svg": "npm run clean:svg && svgr --config-file svgr.config.js --out-dir src/assets/svgr src/assets && npm run clean:svg:index",
|
||||
"jest:watch": "jest --watch",
|
||||
"prepare": "npm run convert-svg && npm run build",
|
||||
"lint": "eslint . --ext .js,.ts,.tsx --cache",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"test": "jest",
|
||||
"watch": "cross-env ESBUILD_WATCH=true esr esbuild.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^28.1.0",
|
||||
"@svgr/cli": "^5.5.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@types/ramda": "^0.27.44",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"concurrently": "^7.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.14.27",
|
||||
"esbuild-runner": "^2.2.1",
|
||||
"jest": "^28.1.0",
|
||||
"jest-environment-jsdom": "^28.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-use": "^17.2.4",
|
||||
"styled-components": "^5.3.3",
|
||||
"type-fest": "^2.12.0",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/styled-components": "^5.1.23",
|
||||
"class-autobind-decorator": "^3.0.1",
|
||||
"classnames": "^2.3.1",
|
||||
"framer-motion": "4.1.17",
|
||||
"fuzzysort": "^1.2.1",
|
||||
"ramda": "^0.27.1",
|
||||
"react-switch": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-use": "^17.2.4",
|
||||
"styled-components": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CardContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-top: var(--padding-md);
|
||||
`;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React, { FC, memo } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
const StyledDivider = styled.div<{ hasName: boolean }>`
|
||||
border-bottom: 1px solid var(--hl-sm);
|
||||
overflow: visible !important;
|
||||
height: 0;
|
||||
margin: var(--padding-md) var(--padding-md) var(--padding-md) var(--padding-md);
|
||||
${({ hasName }) =>
|
||||
!hasName &&
|
||||
css`
|
||||
margin: var(--padding-xs) 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
position: relative;
|
||||
left: calc(-1 * var(--padding-sm));
|
||||
top: -0.7rem;
|
||||
color: var(--hl);
|
||||
padding-right: 1em;
|
||||
background: var(--color-bg);
|
||||
font-size: var(--font-size-xs);
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-default);
|
||||
`;
|
||||
|
||||
export const DropdownDivider: FC = memo(({ children }) => (
|
||||
<StyledDivider hasName={!!children}>
|
||||
{children && <StyledLabel>{children}</StyledLabel>}
|
||||
</StyledDivider>
|
||||
));
|
||||
|
||||
DropdownDivider.displayName = 'DropdownDivider';
|
||||
@@ -1,157 +0,0 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
interface ControlledDropdown<T> {
|
||||
value: T;
|
||||
onClick?: (value: T, event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
|
||||
}
|
||||
|
||||
interface UncontrolledDropdown {
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const isControlledInput = <T extends unknown>(dropdown: DropdownItemProps<T>): dropdown is ControlledDropdown<T> => (
|
||||
Object.prototype.hasOwnProperty.call(dropdown, 'value')
|
||||
);
|
||||
|
||||
const isUncontrolledInput = <T extends unknown>(dropdown: DropdownItemProps<T>): dropdown is UncontrolledDropdown => (
|
||||
!isControlledInput(dropdown)
|
||||
);
|
||||
|
||||
export type DropdownItemProps<T = string> = {
|
||||
buttonClass?: string;
|
||||
stayOpenAfterClick?: boolean;
|
||||
disabled?: boolean;
|
||||
children?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
right?: ReactNode;
|
||||
className?: string;
|
||||
color?: string;
|
||||
selected?: boolean;
|
||||
} & (ControlledDropdown<T> | UncontrolledDropdown);
|
||||
|
||||
const StyledButton = styled.button<{ selected?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 15rem;
|
||||
font-size: var(--font-size-md);
|
||||
text-align: left;
|
||||
padding-right: var(--padding-md);
|
||||
padding-left: var(--padding-sm);
|
||||
height: var(--line-height-xs);
|
||||
width: 100%;
|
||||
color: var(--color-font) !important;
|
||||
background-color: var(--color-bg);
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:active:not(:disabled) {
|
||||
background: var(--hl-xs);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: var(--hl-md);
|
||||
}
|
||||
|
||||
${({ selected }) => selected && css`
|
||||
background: var(--hl-xs) !important;
|
||||
font-weight: bold;
|
||||
`};
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRightNode = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--hl-xl);
|
||||
margin-left: auto;
|
||||
padding-left: var(--padding-lg);
|
||||
`;
|
||||
|
||||
const StyledInner = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledText = styled.div`
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > *:not(:first-child) {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
input + label {
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--padding-xs);
|
||||
padding-right: var(--padding-md);
|
||||
`;
|
||||
|
||||
@autoBindMethodsForReact
|
||||
export class DropdownItem<T> extends PureComponent<DropdownItemProps<T>> {
|
||||
_handleClick(event: React.MouseEvent<HTMLButtonElement>) {
|
||||
const { stayOpenAfterClick, onClick, disabled } = this.props;
|
||||
|
||||
if (stayOpenAfterClick) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (!onClick || disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isControlledInput<T>(this.props)) {
|
||||
this.props.onClick?.(this.props.value, event);
|
||||
} else if (isUncontrolledInput(this.props)) {
|
||||
this.props.onClick?.(event);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
buttonClass,
|
||||
children,
|
||||
className,
|
||||
color,
|
||||
disabled,
|
||||
right,
|
||||
icon,
|
||||
selected,
|
||||
} = this.props;
|
||||
|
||||
const styles = color ? { color } : {};
|
||||
|
||||
const inner = (
|
||||
<StyledInner className={className}>
|
||||
<StyledText style={styles}>{children}</StyledText>
|
||||
</StyledInner>
|
||||
);
|
||||
return (
|
||||
<StyledButton
|
||||
className={classnames(className, buttonClass)}
|
||||
type="button"
|
||||
onClick={this._handleClick}
|
||||
disabled={disabled}
|
||||
selected={selected}
|
||||
>
|
||||
{icon && <StyledIconContainer>{icon}</StyledIconContainer>}
|
||||
{inner}
|
||||
{right && <StyledRightNode>{right}</StyledRightNode>}
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,594 +0,0 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import fuzzySort from 'fuzzysort';
|
||||
import { any, equals } from 'ramda';
|
||||
import React, {
|
||||
CSSProperties,
|
||||
Fragment,
|
||||
PureComponent,
|
||||
ReactChild,
|
||||
ReactFragment,
|
||||
ReactNode,
|
||||
ReactPortal,
|
||||
} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { SvgIcon } from '../svg-icon';
|
||||
import { DropdownDivider } from './dropdown-divider';
|
||||
import { DropdownItem } from './dropdown-item';
|
||||
|
||||
export interface DropdownProps {
|
||||
children: ReactNode;
|
||||
renderButton: ReactNode | ((props: { open: boolean }) => void);
|
||||
right?: boolean;
|
||||
outline?: boolean;
|
||||
wide?: boolean;
|
||||
onOpen?: () => void;
|
||||
onHide?: () => void;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
beside?: boolean;
|
||||
}
|
||||
|
||||
const StyledDropdown = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledNoResults = styled.div`
|
||||
text-align: center;
|
||||
padding: var(--padding-md);
|
||||
color: var(--color-warning);
|
||||
`;
|
||||
|
||||
const StyledBackdrop = styled.div`
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
content: ' ';
|
||||
`;
|
||||
|
||||
const StyledFilter = styled.div<{ filtering: boolean }>`
|
||||
border: 1px solid var(--hl-md);
|
||||
margin: var(--padding-xs);
|
||||
width: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: var(--padding-sm);
|
||||
color: var(--hl);
|
||||
|
||||
// Can't "display: none" this because we need to be able to type
|
||||
// in it. So, we'll just store it off screen
|
||||
${({ filtering }) =>
|
||||
filtering
|
||||
? css`
|
||||
position: static;
|
||||
left: auto;
|
||||
`
|
||||
: css`
|
||||
position: absolute;
|
||||
left: -9999999px;
|
||||
`}
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
padding: var(--padding-xs) var(--padding-sm);
|
||||
color: var(--color-font);
|
||||
outline: 0;
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
z-index: 99999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 1px solid var(--hl-sm);
|
||||
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1);
|
||||
box-sizing: border-box;
|
||||
background: var(--color-bg);
|
||||
max-height: 100px;
|
||||
|
||||
// Separate it from it's surroundings a bit
|
||||
margin: var(--padding-xxs) 3px;
|
||||
|
||||
padding-top: var(--radius-md);
|
||||
padding-bottom: var(--radius-md);
|
||||
|
||||
border-radius: var(--radius-md);
|
||||
overflow: auto;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
li button:hover:not(:disabled),
|
||||
li.active button:not(:disabled) {
|
||||
background: var(--hl-xs);
|
||||
}
|
||||
|
||||
li.active button:not(:disabled) {
|
||||
background: var(--hl-sm);
|
||||
}
|
||||
`;
|
||||
|
||||
interface State {
|
||||
open: boolean;
|
||||
dropUp: boolean;
|
||||
filter: string;
|
||||
filterVisible: boolean;
|
||||
filterItems: number[] | null;
|
||||
filterActiveIndex: number;
|
||||
forcedPosition: null | {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
uniquenessKey: number;
|
||||
}
|
||||
|
||||
const isComponent = (match: string) => (child: ReactNode) => any(equals(match), [
|
||||
// @ts-expect-error this is required by our API for Dropdown
|
||||
child.type.name,
|
||||
// @ts-expect-error this is required by our API for Dropdown
|
||||
child.type.displayName,
|
||||
]);
|
||||
|
||||
const isDropdownItem = isComponent(DropdownItem.name);
|
||||
const isDropdownDivider = isComponent(DropdownDivider.name);
|
||||
|
||||
@autoBindMethodsForReact
|
||||
export class Dropdown extends PureComponent<DropdownProps, State> {
|
||||
// Save body overflow so we can revert it when needed
|
||||
defaultBodyOverflow = document.body.style.overflow;
|
||||
dropdownsContainer: HTMLElement | null = null;
|
||||
_dropdownList: HTMLDivElement | null = null;
|
||||
_cachedMinWidth: string | null = null;
|
||||
_node: HTMLDivElement | null = null;
|
||||
_filter: HTMLInputElement | null = null;
|
||||
|
||||
state: State = {
|
||||
open: false,
|
||||
dropUp: false,
|
||||
// Filter Stuff
|
||||
filter: '',
|
||||
filterVisible: false,
|
||||
filterItems: null,
|
||||
filterActiveIndex: 0,
|
||||
// Position
|
||||
forcedPosition: null,
|
||||
// Use this to force new menu every time dropdown opens
|
||||
uniquenessKey: 0,
|
||||
};
|
||||
|
||||
constructor(props: DropdownProps) {
|
||||
super(props);
|
||||
this.dropdownsContainer = document.querySelector('#dropdowns-container');
|
||||
|
||||
if (!this.dropdownsContainer) {
|
||||
this.dropdownsContainer = document.createElement('div');
|
||||
this.dropdownsContainer.id = 'dropdowns-container';
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
this.dropdownsContainer.style = [
|
||||
// Position off-screen so we don't see it
|
||||
'position: fixed',
|
||||
'right: -90000px',
|
||||
'left: -90000px', // Make same size as viewport so we can use it to calculate the
|
||||
// position of the dropdown accurately.
|
||||
'width: 100vw',
|
||||
'height: 100vh',
|
||||
].join('; ');
|
||||
document.body.appendChild(this.dropdownsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
_handleCheckFilterSubmit(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Enter') {
|
||||
// Listen for the Enter key and "click" on the active list item
|
||||
const selector = `li[data-filter-index="${this.state.filterActiveIndex}"] button`;
|
||||
this._dropdownList?.querySelector<HTMLButtonElement>(selector)?.click();
|
||||
}
|
||||
}
|
||||
|
||||
_handleChangeFilter(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const newFilter = event.target.value;
|
||||
|
||||
// Nothing to do if the filter didn't change
|
||||
if (newFilter === this.state.filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter the list items that are filterable (have data-filter-index property)
|
||||
const filterItems = [];
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
for (const listItem of (this._dropdownList?.querySelectorAll('li') || [])) {
|
||||
if (!listItem.hasAttribute('data-filter-index')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasMatch = !!fuzzySort.single(newFilter, listItem.textContent);
|
||||
|
||||
if (!newFilter || hasMatch) {
|
||||
const filterIndex = listItem.getAttribute('data-filter-index');
|
||||
filterItems.push(parseInt(filterIndex, 10));
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
filter: newFilter,
|
||||
filterItems: newFilter ? filterItems : null,
|
||||
filterActiveIndex: filterItems[0] || -1,
|
||||
filterVisible: this.state.filterVisible ? true : newFilter.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
_handleDropdownNavigation(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
const { key, shiftKey } = event;
|
||||
// Handle tab and arrows to move up and down dropdown entries
|
||||
const { filterItems, filterActiveIndex } = this.state;
|
||||
|
||||
if (['Tab', 'ArrowDown', 'ArrowUp'].includes(key)) {
|
||||
event.preventDefault();
|
||||
const items = filterItems || [];
|
||||
|
||||
if (!filterItems) {
|
||||
for (const li of (this._dropdownList?.querySelectorAll('li') || [])) {
|
||||
if (li.hasAttribute('data-filter-index')) {
|
||||
const filterIndex = li.getAttribute('data-filter-index');
|
||||
items.push(parseInt(filterIndex, 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const i = items.indexOf(filterActiveIndex);
|
||||
|
||||
if (key === 'ArrowUp' || (key === 'Tab' && shiftKey)) {
|
||||
const nextI = i > 0 ? items[i - 1] : items[items.length - 1];
|
||||
this.setState({
|
||||
filterActiveIndex: nextI,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
filterActiveIndex: items[i + 1] || items[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._filter?.focus();
|
||||
}
|
||||
|
||||
_handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (!this.state.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Catch all key presses (like global app hotkeys) if we're open
|
||||
event.stopPropagation();
|
||||
|
||||
this._handleDropdownNavigation(event);
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
_checkSizeAndPosition() {
|
||||
if (!this.state.open || !this._dropdownList) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get dropdown menu
|
||||
const dropdownList = this._dropdownList;
|
||||
|
||||
// Compute the size of all the menus
|
||||
let dropdownBtnRect = this._node?.getBoundingClientRect();
|
||||
|
||||
const bodyRect = this.dropdownsContainer?.getBoundingClientRect();
|
||||
const dropdownListRect = dropdownList.getBoundingClientRect();
|
||||
const { forcedPosition } = this.state;
|
||||
|
||||
if (forcedPosition && bodyRect !== undefined) {
|
||||
dropdownBtnRect = {
|
||||
left: forcedPosition.x,
|
||||
right: bodyRect.width - forcedPosition.x,
|
||||
top: forcedPosition.y,
|
||||
bottom: bodyRect.height - forcedPosition.y,
|
||||
width: 100,
|
||||
height: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
// Should it drop up?
|
||||
const bodyHeight = bodyRect?.height || 0;
|
||||
const dropdownTop = dropdownBtnRect?.top || 0;
|
||||
const dropUp = dropdownTop > bodyHeight - 200;
|
||||
// Reset all the things so we can start fresh
|
||||
this._dropdownList.style.left = 'initial';
|
||||
this._dropdownList.style.right = 'initial';
|
||||
this._dropdownList.style.top = 'initial';
|
||||
this._dropdownList.style.bottom = 'initial';
|
||||
this._dropdownList.style.minWidth = this._cachedMinWidth || 'initial';
|
||||
this._dropdownList.style.maxWidth = 'initial';
|
||||
|
||||
// Cache the width so we can use it later. This is because the dropdown width
|
||||
// could shrink once we start filtering items out.
|
||||
if (!this._cachedMinWidth) {
|
||||
this._cachedMinWidth = dropdownListRect.width + 'px';
|
||||
}
|
||||
|
||||
const screenMargin = 6;
|
||||
const { right, wide } = this.props;
|
||||
|
||||
if (dropdownBtnRect === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bodyRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (right || wide) {
|
||||
const { right: originalRight } = dropdownBtnRect;
|
||||
// Prevent dropdown from squishing against left side of screen
|
||||
const right = Math.max(dropdownListRect.width + screenMargin, originalRight);
|
||||
const { beside } = this.props;
|
||||
const offset = beside ? dropdownBtnRect.width - 40 : 0;
|
||||
this._dropdownList.style.right = `${bodyRect.width - right + offset}px`;
|
||||
this._dropdownList.style.maxWidth = `${Math.min(dropdownListRect.width, right + offset)}px`;
|
||||
}
|
||||
|
||||
if (!right || wide) {
|
||||
const { left: originalLeft } = dropdownBtnRect;
|
||||
const { beside } = this.props;
|
||||
const offset = beside ? dropdownBtnRect.width - 40 : 0;
|
||||
// Prevent dropdown from squishing against right side of screen
|
||||
const left = Math.min(bodyRect.width - dropdownListRect.width - screenMargin, originalLeft);
|
||||
this._dropdownList.style.left = `${left + offset}px`;
|
||||
this._dropdownList.style.maxWidth = `${Math.min(
|
||||
dropdownListRect.width,
|
||||
bodyRect.width - left - offset,
|
||||
)}px`;
|
||||
}
|
||||
|
||||
if (dropUp) {
|
||||
const { top } = dropdownBtnRect;
|
||||
this._dropdownList.style.bottom = `${bodyRect.height - top}px`;
|
||||
this._dropdownList.style.maxHeight = `${top - screenMargin}px`;
|
||||
} else {
|
||||
const { bottom } = dropdownBtnRect;
|
||||
this._dropdownList.style.top = `${bottom}px`;
|
||||
this._dropdownList.style.maxHeight = `${bodyRect.height - bottom - screenMargin}px`;
|
||||
}
|
||||
}
|
||||
|
||||
_handleClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
static _handleMouseDown(event: React.MouseEvent<HTMLDivElement>) {
|
||||
// Intercept mouse down so that clicks don't trigger things like drag and drop.
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
_getFlattenedChildren(children: ReactNode) {
|
||||
let newChildren: (ReactChild | ReactFragment | ReactPortal)[] = [];
|
||||
|
||||
for (const child of React.Children.toArray(children)) {
|
||||
if (!child) {
|
||||
// Ignore null components
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
if (child.type === Fragment) {
|
||||
newChildren = [
|
||||
...newChildren,
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
...this._getFlattenedChildren(child.props.children),
|
||||
];
|
||||
} else if (Array.isArray(child)) {
|
||||
newChildren = [
|
||||
...newChildren,
|
||||
...this._getFlattenedChildren(child),
|
||||
];
|
||||
} else {
|
||||
newChildren.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
return newChildren;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._checkSizeAndPosition();
|
||||
}
|
||||
|
||||
hide() {
|
||||
// Focus the dropdown button after hiding
|
||||
if (this._node) {
|
||||
const button = this._node.querySelector('button');
|
||||
|
||||
button?.focus();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
open: false,
|
||||
});
|
||||
this.props.onHide?.();
|
||||
}
|
||||
|
||||
show(filterVisible = false, forcedPosition = null) {
|
||||
const bodyHeight = document.body.getBoundingClientRect().height;
|
||||
|
||||
const dropdownTop = this._node?.getBoundingClientRect().top || 0;
|
||||
|
||||
const dropUp = dropdownTop > bodyHeight - 200;
|
||||
this.setState({
|
||||
open: true,
|
||||
dropUp,
|
||||
forcedPosition,
|
||||
filterVisible,
|
||||
filter: '',
|
||||
filterItems: null,
|
||||
filterActiveIndex: -1,
|
||||
uniquenessKey: this.state.uniquenessKey + 1,
|
||||
});
|
||||
this.props.onOpen?.();
|
||||
}
|
||||
|
||||
toggle(filterVisible = false) {
|
||||
if (this.state.open) {
|
||||
this.hide();
|
||||
document.body.style.overflow = this.defaultBodyOverflow;
|
||||
} else {
|
||||
this.show(filterVisible);
|
||||
// Prevent body from scrolling when dropdown is open
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, style, children, renderButton } = this.props;
|
||||
const {
|
||||
open,
|
||||
uniquenessKey,
|
||||
filterVisible,
|
||||
filterActiveIndex,
|
||||
filterItems,
|
||||
filter,
|
||||
} = this.state;
|
||||
const menuClasses = classnames({
|
||||
'theme--dropdown__menu': true,
|
||||
});
|
||||
|
||||
const allChildren = this._getFlattenedChildren(children);
|
||||
|
||||
const visibleChildren = allChildren.filter((child, i) => {
|
||||
if (!isDropdownItem(child)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// It's visible if its index is in the filterItems
|
||||
return !filterItems || filterItems.includes(i);
|
||||
});
|
||||
const dropdownItems: ReactNode[] = [];
|
||||
|
||||
for (let i = 0; i < allChildren.length; i++) {
|
||||
const child = allChildren[i];
|
||||
|
||||
if (isDropdownItem(child)) {
|
||||
const active = i === filterActiveIndex;
|
||||
const hide = !visibleChildren.includes(child);
|
||||
|
||||
dropdownItems.push(
|
||||
<li
|
||||
key={i}
|
||||
data-filter-index={i}
|
||||
className={classnames({ active, hide })}
|
||||
>
|
||||
{child}
|
||||
</li>,
|
||||
);
|
||||
} else if (isDropdownDivider(child)) {
|
||||
const currentIndex = visibleChildren.indexOf(child);
|
||||
const nextChild = visibleChildren[currentIndex + 1];
|
||||
|
||||
// Only show the divider if the next child is a DropdownItem
|
||||
if (nextChild && isDropdownItem(nextChild)) {
|
||||
dropdownItems.push(<li key={i}>{child}</li>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const noResults = filter && filterItems && filterItems.length === 0;
|
||||
const button =
|
||||
typeof renderButton === 'function'
|
||||
? renderButton({
|
||||
open,
|
||||
})
|
||||
: renderButton;
|
||||
return (
|
||||
<StyledDropdown
|
||||
style={style}
|
||||
className={className}
|
||||
ref={ref => {
|
||||
this._node = ref;
|
||||
}}
|
||||
onClick={this._handleClick}
|
||||
onKeyDown={this._handleKeyDown}
|
||||
tabIndex={-1}
|
||||
onMouseDown={Dropdown._handleMouseDown}
|
||||
>
|
||||
<Fragment key="button">{button}</Fragment>
|
||||
{this.dropdownsContainer && ReactDOM.createPortal(
|
||||
<div
|
||||
key="item"
|
||||
className={menuClasses}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
{open && <StyledBackdrop className="theme--transparent-overlay" />}
|
||||
{open && (
|
||||
<StyledMenu
|
||||
key={uniquenessKey}
|
||||
ref={ref => {
|
||||
this._dropdownList = ref;
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<StyledFilter filtering={filterVisible}>
|
||||
<SvgIcon icon="search" />
|
||||
<input
|
||||
type="text"
|
||||
onChange={this._handleChangeFilter}
|
||||
ref={ref => {
|
||||
this._filter = ref;
|
||||
|
||||
// Automatically focus the filter element when mounted so we can start typing
|
||||
if (this._filter) {
|
||||
this._filter.focus();
|
||||
}
|
||||
}}
|
||||
onKeyPress={this._handleCheckFilterSubmit}
|
||||
/>
|
||||
</StyledFilter>
|
||||
{noResults && <StyledNoResults>No match</StyledNoResults>}
|
||||
<ul
|
||||
className={classnames({
|
||||
hide: noResults,
|
||||
})}
|
||||
>
|
||||
{dropdownItems}
|
||||
</ul>
|
||||
</StyledMenu>
|
||||
)}
|
||||
</div>,
|
||||
this.dropdownsContainer,
|
||||
)}
|
||||
</StyledDropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { Dropdown, type DropdownProps } from './dropdown';
|
||||
export { DropdownItem, type DropdownItemProps } from './dropdown-item';
|
||||
export { DropdownDivider } from './dropdown-divider';
|
||||
@@ -1,48 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { FC, memo, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface HeaderProps {
|
||||
className?: string;
|
||||
gridLeft?: ReactNode;
|
||||
gridCenter?: ReactNode;
|
||||
gridRight?: ReactNode;
|
||||
}
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
border-bottom: 1px solid var(--hl-md);
|
||||
padding: var(--padding-xxs) var(--padding-sm);
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1.5fr 2fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-areas: 'header_left header_center header_right';
|
||||
.header_left {
|
||||
grid-area: header_left;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header_center {
|
||||
grid-area: header_center;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header_right {
|
||||
grid-area: header_right;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Header: FC<HeaderProps> = memo(({ className, gridLeft, gridCenter, gridRight }) => (
|
||||
<StyledHeader className={classNames('app-header theme--app-header', className)}>
|
||||
<div className="header_left">{gridLeft}</div>
|
||||
<div className="header_center">{gridCenter}</div>
|
||||
<div className="header_right">{gridRight}</div>
|
||||
</StyledHeader>
|
||||
));
|
||||
|
||||
Header.displayName = 'Header';
|
||||
@@ -1,32 +0,0 @@
|
||||
import React, { CSSProperties, FC, ReactNode } from 'react';
|
||||
|
||||
import { SvgIcon } from './svg-icon';
|
||||
import { Tooltip } from './tooltip';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
position?: 'bottom' | 'top' | 'right' | 'left';
|
||||
delay?: number;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
info?: boolean;
|
||||
}
|
||||
|
||||
export const HelpTooltip: FC<Props> = ({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
info,
|
||||
position,
|
||||
delay,
|
||||
}) => (
|
||||
<Tooltip
|
||||
position={position}
|
||||
delay={delay}
|
||||
className={className}
|
||||
message={children}
|
||||
style={style}
|
||||
>
|
||||
{info ? <SvgIcon icon="info" /> : <SvgIcon icon="question" />}
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -1,27 +0,0 @@
|
||||
export * from './button';
|
||||
export * from './dropdown';
|
||||
export * from './sidebar';
|
||||
export * from './list-group';
|
||||
|
||||
export { Breadcrumb, type BreadcrumbProps, type CrumbProps } from './breadcrumb';
|
||||
export { CardContainer } from './card-container';
|
||||
export { Card, type CardProps } from './card';
|
||||
export { Header, type HeaderProps } from './header';
|
||||
export { MultiSwitch } from './multi-switch';
|
||||
export { NoticeTable, type NoticeTableProps, type Notice } from './notice-table';
|
||||
export { RadioButtonGroup, type RadioButtonGroupProps } from './radio-button-group';
|
||||
export { SvgIcon, type SvgIconProps, type IconEnum, type ThemeEnum, type IconId } from './svg-icon';
|
||||
export { Switch, type SwitchProps, type SwitchItem } from './switch';
|
||||
export {
|
||||
Table,
|
||||
type TableProps,
|
||||
TableBody,
|
||||
TableData,
|
||||
type TableDataProps,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
type TableHeaderProps,
|
||||
TableRow,
|
||||
} from './table';
|
||||
export { ToggleSwitch, type ToggleSwitchProps } from './toggle-switch';
|
||||
export { Tooltip, type TooltipProps } from './tooltip';
|
||||
@@ -1,2 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
@@ -1,40 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { RadioButtonGroupProps } from './radio-button-group';
|
||||
import { RadioButtonGroup } from './radio-button-group';
|
||||
|
||||
const ThemedButtonGroup = styled(RadioButtonGroup)<RadioButtonGroupProps>`
|
||||
&& {
|
||||
font-weight: 500;
|
||||
background: var(--hl-xs);
|
||||
color: var(--color-font);
|
||||
border: 0;
|
||||
border-radius: 100px;
|
||||
align-content: space-evenly;
|
||||
padding: var(--padding-xxs);
|
||||
transform: scale(0.9);
|
||||
transformOrigin: 'center';
|
||||
|
||||
label {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
text-transform: uppercase;
|
||||
padding: var(--padding-xs) var(--padding-xxs);
|
||||
color: var(--hl);
|
||||
background: transparent;
|
||||
font-size: var(--font-size-xs);
|
||||
margin: 0 auto;
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
input:checked + span {
|
||||
color: var(--color-font);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MultiSwitch: FunctionComponent<RadioButtonGroupProps> = props => <ThemedButtonGroup {...props} />;
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { FunctionComponent, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface RadioButtonGroupProps {
|
||||
name: string;
|
||||
onChange: (value: string) => any;
|
||||
choices: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
className?: string;
|
||||
selectedValue: string;
|
||||
}
|
||||
|
||||
const StyledRadioButtonGroup = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--padding-xs);
|
||||
border: 1px solid var(--hl-xs);
|
||||
border-radius: var(--radius--sm);
|
||||
|
||||
& > * :not(:last-child) {
|
||||
margin-right: var(--padding-xs);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRadioButtonBtn = styled.label`
|
||||
cursor: pointer;
|
||||
color: var(--color-font);
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
padding: var(--padding-sm) var(--padding-xs);
|
||||
display: block;
|
||||
border-radius: var(--line-height-sm);
|
||||
}
|
||||
|
||||
input:checked + span {
|
||||
background-color: var(--hl-xs);
|
||||
}
|
||||
`;
|
||||
|
||||
export const RadioButtonGroup: FunctionComponent<RadioButtonGroupProps> = ({
|
||||
name,
|
||||
choices,
|
||||
onChange,
|
||||
className,
|
||||
selectedValue,
|
||||
}) => {
|
||||
const handleChange = useCallback(event => {
|
||||
if (typeof onChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(event.currentTarget.value);
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<StyledRadioButtonGroup className={className}>
|
||||
{choices.map(({ label, value }) => (
|
||||
<StyledRadioButtonBtn key={value}>
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={value}
|
||||
checked={selectedValue === value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</StyledRadioButtonBtn>
|
||||
))}
|
||||
</StyledRadioButtonGroup>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import React, { FunctionComponent, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SidebarPanelProps {
|
||||
children: ReactNode;
|
||||
childrenVisible: boolean;
|
||||
}
|
||||
|
||||
const StyledPanel = styled(motion.div)`
|
||||
height: 0;
|
||||
`;
|
||||
|
||||
export const SidebarPanel: FunctionComponent<SidebarPanelProps> = ({ childrenVisible, children }) => (
|
||||
<StyledPanel
|
||||
initial={{
|
||||
height: childrenVisible ? '100%' : '0px',
|
||||
}}
|
||||
animate={{
|
||||
height: childrenVisible ? '100%' : '0px',
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: 'easeInOut',
|
||||
delay: 0,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledPanel>
|
||||
);
|
||||
@@ -1,94 +0,0 @@
|
||||
import React, { Fragment, FunctionComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SwitchItem {
|
||||
label: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export interface SwitchProps {
|
||||
className?: string;
|
||||
onClick?: (event: React.SyntheticEvent<HTMLInputElement>) => any;
|
||||
optionItems?: SwitchItem[];
|
||||
}
|
||||
|
||||
const StyledSwitch = styled.div`
|
||||
margin: auto;
|
||||
.switch {
|
||||
position: relative;
|
||||
height: 34px;
|
||||
width: 140px;
|
||||
background: var(--hl-xs);
|
||||
border-radius: 17px;
|
||||
}
|
||||
.switch-label {
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
z-index: 2;
|
||||
float: left;
|
||||
width: 67px;
|
||||
line-height: 34px;
|
||||
font-size: 11px;
|
||||
color: var(--hl);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding-top: 0px;
|
||||
}
|
||||
.switch-label-off {
|
||||
padding-left: 2px;
|
||||
}
|
||||
.switch-label-on {
|
||||
padding-right: 2px;
|
||||
}
|
||||
.switch-input {
|
||||
display: none;
|
||||
}
|
||||
.switch-input:checked + .switch-label {
|
||||
color: var(--color-font);
|
||||
text-shadow: 0 1px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
.switch-input:checked + .switch-label-on ~ .switch-selection {
|
||||
left: 71px;
|
||||
}
|
||||
.switch-selection {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
display: block;
|
||||
width: 65px;
|
||||
height: 26px;
|
||||
border-radius: 13px;
|
||||
background-color: var(--color-bg);
|
||||
transition: left 0.2s ease-out;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Switch: FunctionComponent<SwitchProps> = ({ className, onClick, optionItems }) => (
|
||||
<StyledSwitch className={className}>
|
||||
<div className="switch">
|
||||
{optionItems?.map((item, i) => {
|
||||
return (
|
||||
<Fragment key={item.label}>
|
||||
<input
|
||||
type="radio"
|
||||
className="switch-input"
|
||||
name="switch"
|
||||
onClick={onClick}
|
||||
value={item.label}
|
||||
id={item.label}
|
||||
defaultChecked={item.selected}
|
||||
/>
|
||||
<label
|
||||
htmlFor={item.label}
|
||||
className={`switch-label ${i === 0 ? 'switch-label-off' : 'switch-label-on'}`}
|
||||
>
|
||||
{item.label}
|
||||
</label>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
<span className="switch-selection" />
|
||||
</div>
|
||||
</StyledSwitch>
|
||||
);
|
||||
@@ -1,112 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
/***********/
|
||||
/* <table> *
|
||||
/***********/
|
||||
export interface TableProps {
|
||||
children: ReactNode;
|
||||
striped?: boolean;
|
||||
outlined?: boolean;
|
||||
compact?: boolean;
|
||||
headings?: ReactNode[];
|
||||
}
|
||||
|
||||
export const Table = styled.table<TableProps>`
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: ${({ compact }) => (compact ? 'var(--padding-xs)' : 'var(--padding-sm)')}
|
||||
${({ compact }) => (compact ? 'var(--padding-sm)' : 'var(--padding-md)')};
|
||||
}
|
||||
|
||||
${({ striped }) =>
|
||||
striped &&
|
||||
`
|
||||
tbody tr:nth-child(odd) {
|
||||
background: var(--hl-xs);
|
||||
}`}
|
||||
|
||||
${({ outlined }) =>
|
||||
outlined &&
|
||||
`
|
||||
& {
|
||||
th {
|
||||
background: var(--hl-xxs);
|
||||
}
|
||||
|
||||
&,
|
||||
td {
|
||||
border: 1px solid var(--hl-sm);
|
||||
}
|
||||
|
||||
tr.table--no-outline-row td {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
& {
|
||||
border-radius: 3px;
|
||||
border-collapse: unset;
|
||||
}
|
||||
|
||||
td {
|
||||
border-left: 0;
|
||||
border-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}`}
|
||||
`;
|
||||
|
||||
/********/
|
||||
/* <tr> */
|
||||
/********/
|
||||
export const TableRow = styled.tr``;
|
||||
|
||||
/********/
|
||||
/* <td> */
|
||||
/********/
|
||||
export interface TableDataProps {
|
||||
compact?: boolean;
|
||||
align?: 'center' | 'left';
|
||||
}
|
||||
|
||||
export const TableData = styled.td<TableDataProps>`
|
||||
vertical-align: top;
|
||||
padding: 0 var(--padding-md);
|
||||
text-align: ${({ align }) => align || 'left'};
|
||||
`;
|
||||
|
||||
/********/
|
||||
|
||||
/* <th> */
|
||||
|
||||
/********/
|
||||
export interface TableHeaderProps {
|
||||
compact?: boolean;
|
||||
align?: 'center' | 'left';
|
||||
}
|
||||
|
||||
export const TableHeader = styled.th<TableHeaderProps>`
|
||||
vertical-align: top;
|
||||
padding: 0 var(--padding-md);
|
||||
text-align: ${({ align }) => align || 'left'};
|
||||
`;
|
||||
|
||||
/***********/
|
||||
/* <thead> */
|
||||
/***********/
|
||||
export const TableHead = styled.thead``;
|
||||
|
||||
/***********/
|
||||
|
||||
/* <tbody> */
|
||||
|
||||
/***********/
|
||||
export const TableBody = styled.tbody``;
|
||||
@@ -1,85 +0,0 @@
|
||||
import React, { FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import ReactSwitch from 'react-switch';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
// @ts-expect-error Issue with commonJS export in vite
|
||||
const Switch = ReactSwitch.default;
|
||||
export interface ToggleSwitchProps {
|
||||
labelClassName?: string;
|
||||
switchClassName?: string;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange(checked: boolean, arg1: Event, arg2: string): void | Promise<void>;
|
||||
label?: ReactNode;
|
||||
}
|
||||
|
||||
const ThemedSwitch = styled(Switch)<{ checked: boolean }>`
|
||||
&& {
|
||||
.react-switch-bg {
|
||||
background: ${({ checked }) => (checked ? 'var(--color-surprise)' : 'var(--hl-xl)')} !important;
|
||||
|
||||
${({ checked }) => checked && css`
|
||||
path {
|
||||
fill: var(--color-font-surprise) !important
|
||||
}
|
||||
`}
|
||||
}
|
||||
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.label`
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-left: var(--padding-md);
|
||||
}
|
||||
`;
|
||||
|
||||
export const ToggleSwitch: FunctionComponent<ToggleSwitchProps> = ({
|
||||
labelClassName,
|
||||
switchClassName = '',
|
||||
checked: checkedProp,
|
||||
onChange,
|
||||
disabled,
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
const [checked, setChecked] = useState(Boolean(checkedProp));
|
||||
|
||||
// If prop changes and differs from state, update state
|
||||
useEffect(() => {
|
||||
setChecked(Boolean(checkedProp));
|
||||
}, [checkedProp]);
|
||||
|
||||
const callback = useCallback((c, a, b) => {
|
||||
setChecked(c);
|
||||
onChange(c, a, b);
|
||||
}, [onChange]);
|
||||
|
||||
const toggle = (
|
||||
<ThemedSwitch
|
||||
className={switchClassName}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={callback}
|
||||
height={20}
|
||||
width={40}
|
||||
{...(props as Record<string, any>)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (label) {
|
||||
return (
|
||||
<StyledLabel className={labelClassName}>
|
||||
{toggle}
|
||||
<span>{label}</span>
|
||||
</StyledLabel>
|
||||
);
|
||||
} else {
|
||||
return toggle;
|
||||
}
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import { ByRoleMatcher, ByRoleOptions, fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Tooltip } from './tooltip';
|
||||
|
||||
type QueryByRole = (
|
||||
text: ByRoleMatcher,
|
||||
options?: ByRoleOptions | undefined,
|
||||
) => HTMLElement | null;
|
||||
|
||||
const expectNoTooltip = (queryByRole: QueryByRole) => {
|
||||
expect(queryByRole('tooltip', { hidden: true })).toBeNull();
|
||||
expect(queryByRole('tooltip', {})).toBeNull();
|
||||
};
|
||||
|
||||
describe('<Tooltip />', () => {
|
||||
it('should show and hide the tooltip correctly', async () => {
|
||||
const childText = 'some child';
|
||||
const delay = 200;
|
||||
const message = 'message';
|
||||
|
||||
const { getByRole, getByText, queryByRole, unmount } = render(
|
||||
<Tooltip message={message} delay={delay}>
|
||||
{childText}
|
||||
</Tooltip>,
|
||||
);
|
||||
|
||||
expect(getByRole('tooltip', { hidden: true })).toBeTruthy();
|
||||
|
||||
fireEvent.mouseEnter(getByText(childText));
|
||||
|
||||
// Should open after the configured delay, wait a sufficient amount
|
||||
jest.advanceTimersByTime(delay * 2);
|
||||
|
||||
expect(getByRole('tooltip')).toBeTruthy();
|
||||
|
||||
fireEvent.mouseLeave(getByText(childText));
|
||||
|
||||
// Should close after 100ms default, wait a sufficient amount
|
||||
jest.advanceTimersByTime(delay);
|
||||
|
||||
expect(getByRole('tooltip', { hidden: true })).toBeTruthy();
|
||||
|
||||
unmount();
|
||||
|
||||
expectNoTooltip(queryByRole);
|
||||
});
|
||||
|
||||
it('should not render tooltip if no message exists', () => {
|
||||
const childText = 'some child';
|
||||
const message = '';
|
||||
const { queryByRole, getByText } = render(<Tooltip message={message}>{childText}</Tooltip>);
|
||||
|
||||
expectNoTooltip(queryByRole);
|
||||
expect(getByText(childText)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should unmount successfully if message is empty in the first render then and non-empty after update', async () => {
|
||||
const childText = 'some child';
|
||||
const initialMessage = '';
|
||||
const newMessage = 'message';
|
||||
|
||||
const { queryByRole, getByRole, getByText, queryByText, rerender, unmount } = render(
|
||||
<Tooltip message={initialMessage}>{childText}</Tooltip>,
|
||||
);
|
||||
|
||||
expectNoTooltip(queryByRole);
|
||||
expect(getByText(childText)).toBeTruthy();
|
||||
|
||||
rerender(<Tooltip message={newMessage}>{childText}</Tooltip>);
|
||||
|
||||
expect(getByRole('tooltip', { hidden: true })).toBeTruthy();
|
||||
expect(getByText(childText)).toBeTruthy();
|
||||
|
||||
unmount();
|
||||
|
||||
expectNoTooltip(queryByRole);
|
||||
expect(queryByText(childText)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,254 +0,0 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { CSSProperties, MouseEvent, PureComponent, ReactNode } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface TooltipProps {
|
||||
children: ReactNode;
|
||||
message: ReactNode;
|
||||
position?: 'bottom' | 'top' | 'right' | 'left';
|
||||
className?: string;
|
||||
delay?: number;
|
||||
selectable?: boolean;
|
||||
wide?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
interface State {
|
||||
visible: boolean;
|
||||
movedToBody: boolean;
|
||||
}
|
||||
|
||||
const StyledTooltip = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const StyledTooltipBubble = styled.div`
|
||||
position: fixed;
|
||||
left: -999999px;
|
||||
opacity: 0;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--hl-sm);
|
||||
box-shadow: 0 0 1em rgba(0, 0, 0, 0.1);
|
||||
color: var(--color-font);
|
||||
padding: var(--padding-sm) var(--padding-md);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
max-width: 20rem;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
white-space: normal !important;
|
||||
word-wrap: break-word;
|
||||
|
||||
&.tooltip__bubble--visible {
|
||||
opacity: 1;
|
||||
z-index: 99999;
|
||||
transition: opacity 200ms;
|
||||
|
||||
// Back to normal
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&.tooltip__bubble--wide {
|
||||
max-width: 30rem;
|
||||
}
|
||||
`;
|
||||
|
||||
@autoBindMethodsForReact
|
||||
export class Tooltip extends PureComponent<TooltipProps, State> {
|
||||
_showTimeout: NodeJS.Timeout | null = null;
|
||||
_hideTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
_tooltip: HTMLDivElement | null = null;
|
||||
_bubble: HTMLDivElement | null = null;
|
||||
_id = Math.random() + '';
|
||||
|
||||
state: State = {
|
||||
visible: false,
|
||||
movedToBody: false,
|
||||
};
|
||||
|
||||
_handleStopClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
_handleMouseEnter() {
|
||||
if (this._showTimeout !== null) {
|
||||
clearTimeout(this._showTimeout);
|
||||
}
|
||||
if (this._hideTimeout !== null) {
|
||||
clearTimeout(this._hideTimeout);
|
||||
}
|
||||
this._showTimeout = setTimeout(() => {
|
||||
const tooltip = this._tooltip;
|
||||
const bubble = this._bubble;
|
||||
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
const bubbleRect = bubble.getBoundingClientRect();
|
||||
const margin = 3;
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
|
||||
switch (this.props.position) {
|
||||
case 'right':
|
||||
top = tooltipRect.top - bubbleRect.height / 2 + tooltipRect.height / 2;
|
||||
left = tooltipRect.left + tooltipRect.width + margin;
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
top = tooltipRect.top - bubbleRect.height / 2 + tooltipRect.height / 2;
|
||||
left = tooltipRect.left - bubbleRect.width - margin;
|
||||
break;
|
||||
|
||||
case 'bottom':
|
||||
top = tooltipRect.top + tooltipRect.height + margin;
|
||||
left = tooltipRect.left - bubbleRect.width / 2 + tooltipRect.width / 2;
|
||||
break;
|
||||
|
||||
case 'top':
|
||||
default:
|
||||
top = tooltipRect.top - bubbleRect.height - margin;
|
||||
left = tooltipRect.left - bubbleRect.width / 2 + tooltipRect.width / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
bubble.style.left = `${Math.max(0, left)}px`;
|
||||
bubble.style.top = `${Math.max(0, top)}px`;
|
||||
this.setState({
|
||||
visible: true,
|
||||
});
|
||||
}, this.props.delay || 400);
|
||||
}
|
||||
|
||||
_handleMouseLeave(): void {
|
||||
if (this._showTimeout !== null) {
|
||||
clearTimeout(this._showTimeout);
|
||||
}
|
||||
if (this._hideTimeout !== null) {
|
||||
clearTimeout(this._hideTimeout);
|
||||
}
|
||||
this._hideTimeout = setTimeout(() => {
|
||||
this.setState({
|
||||
visible: false,
|
||||
});
|
||||
const bubble = this._bubble;
|
||||
|
||||
if (!bubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset positioning stuff
|
||||
bubble.style.left = '';
|
||||
bubble.style.top = '';
|
||||
bubble.style.bottom = '';
|
||||
bubble.style.right = '';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_getContainer() {
|
||||
let container = document.querySelector<HTMLElement>('#tooltips-container');
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'tooltips-container';
|
||||
container.style.zIndex = '1000000';
|
||||
container.style.position = 'relative';
|
||||
document.body && document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
_moveBubbleToBody() {
|
||||
if (this._bubble) {
|
||||
const el = ReactDOM.findDOMNode(this._bubble);
|
||||
el && this._getContainer().appendChild(el);
|
||||
this.setState({
|
||||
movedToBody: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_removeBubbleFromBody() {
|
||||
if (this._bubble) {
|
||||
const el = ReactDOM.findDOMNode(this._bubble);
|
||||
if (this._getContainer().contains(el)) {
|
||||
el && this._getContainer().removeChild(el);
|
||||
}
|
||||
this.setState({
|
||||
movedToBody: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Move the element to the body so we can position absolutely
|
||||
this._moveBubbleToBody();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// If the bubble has not been moved to body, move it.
|
||||
// This can happen if there is no message during the first mount but a message is provided during on a subsequent render.
|
||||
if (!this.state.movedToBody) {
|
||||
this._moveBubbleToBody();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Remove the element from the body
|
||||
this._removeBubbleFromBody();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, message, className, selectable, wide, style } = this.props;
|
||||
const { visible } = this.state;
|
||||
|
||||
if (!message) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const tooltipClasses = classnames(className, 'tooltip');
|
||||
const bubbleClasses = classnames('tooltip__bubble theme--tooltip', {
|
||||
'tooltip__bubble--visible': visible,
|
||||
'tooltip__bubble--wide': wide,
|
||||
selectable: selectable,
|
||||
});
|
||||
return (
|
||||
<StyledTooltip
|
||||
className={tooltipClasses}
|
||||
ref={ref => {
|
||||
this._tooltip = ref;
|
||||
}}
|
||||
id={this._id}
|
||||
onMouseEnter={this._handleMouseEnter}
|
||||
onMouseLeave={this._handleMouseLeave}
|
||||
style={style}
|
||||
>
|
||||
<StyledTooltipBubble
|
||||
className={bubbleClasses}
|
||||
onClick={this._handleStopClick}
|
||||
role="tooltip"
|
||||
aria-hidden={!visible}
|
||||
aria-describedby={this._id}
|
||||
ref={ref => {
|
||||
this._bubble = ref;
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</StyledTooltipBubble>
|
||||
{children}
|
||||
</StyledTooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"experimentalDecorators": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react",
|
||||
"lib": ["DOM"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"src/jest",
|
||||
],
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
".eslintrc.js",
|
||||
"jest.config.js",
|
||||
"src",
|
||||
"svgr.config.js",
|
||||
"vite.config.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
],
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const __DEV__ = mode !== 'production';
|
||||
|
||||
return {
|
||||
build: {
|
||||
sourcemap: __DEV__,
|
||||
lib: {
|
||||
entry: './src/index.ts',
|
||||
fileName: format => {
|
||||
if (format === 'cjs') {
|
||||
return 'commonjs/index.js';
|
||||
}
|
||||
return 'index.js';
|
||||
},
|
||||
// We use CommonJS output for running tests in jest.
|
||||
formats: ['cjs', 'es'],
|
||||
name: 'insomnia-components',
|
||||
},
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom', 'styled-components', 'react-use'],
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(mode),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -17,7 +17,8 @@ test('can make oauth2 requests', async ({ app, page }) => {
|
||||
});
|
||||
|
||||
await page.locator('[data-testid="project"]').click();
|
||||
await page.locator('text=Create').click();
|
||||
const projectView = page.locator('#wrapper');
|
||||
await projectView.locator('text=Create').click();
|
||||
|
||||
const text = await loadFixture('oauth.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
@@ -4,3 +4,5 @@ send-request
|
||||
coverage
|
||||
src/main.min.js
|
||||
src/preload.js
|
||||
**/svgr
|
||||
svgr.config.js
|
||||
@@ -37,7 +37,8 @@
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"type-check": "tsc --noEmit --project tsconfig.build.json",
|
||||
"type-check:watch": "npm run type-check -- --watch"
|
||||
"type-check:watch": "npm run type-check -- --watch",
|
||||
"convert-svg": "npm_config_yes=true npx @svgr/cli@6.4.0 --no-index --config-file svgr.config.js --out-dir src/ui/components/assets/svgr src/ui/components/assets/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getinsomnia/node-libcurl": "2.3.5-2",
|
||||
@@ -177,7 +178,6 @@
|
||||
"graphql": "^16.3.0",
|
||||
"graphql-language-service": "^4.1.5",
|
||||
"highlight.js": "^11.5.1",
|
||||
"insomnia-components": "3.6.1-beta.0",
|
||||
"jest": "^28.1.0",
|
||||
"jest-environment-jsdom": "^28.0.2",
|
||||
"jest-mock": "^28.0.2",
|
||||
|
||||
@@ -21,7 +21,6 @@ const toSchema = <T>(obj: T): Schema<T> => {
|
||||
|
||||
return output as Schema<T>;
|
||||
};
|
||||
|
||||
export const baseModelSchema: Schema<BaseModel> = {
|
||||
_id: () => 'id',
|
||||
created: () => 1234,
|
||||
|
||||
@@ -66,7 +66,6 @@ export interface PrivateProperties {
|
||||
axios: typeof axios;
|
||||
analytics: typeof analytics;
|
||||
loadRendererModules: () => Promise<{
|
||||
insomniaComponents: any;
|
||||
ReactDOM: typeof ReactDOM;
|
||||
React: typeof React;
|
||||
} | {}>;
|
||||
@@ -226,12 +225,10 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): {
|
||||
|
||||
const ReactDOM = await import('react-dom');
|
||||
const React = await import('react');
|
||||
const insomniaComponents = await import('insomnia-components');
|
||||
|
||||
return {
|
||||
ReactDOM,
|
||||
React,
|
||||
insomniaComponents,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -537,12 +537,12 @@ export function describeChanges<T extends BaseModel>(a: T, b: T): string[] {
|
||||
const bStr = deterministicStringify(bValue);
|
||||
|
||||
if (aValue === undefined && bValue !== undefined) {
|
||||
changes.push(`+${key}`);
|
||||
changes.push(`+${String(key)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (aValue !== undefined && bValue === undefined) {
|
||||
changes.push(`-${key}`);
|
||||
changes.push(`-${String(key)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Breadcrumb, BreadcrumbProps, Header as _Header } from 'insomnia-components';
|
||||
import classNames from 'classnames';
|
||||
import React, { FC, Fragment, ReactNode } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import coreLogo from '../images/insomnia-logo.svg';
|
||||
import { selectIsLoggedIn } from '../redux/selectors';
|
||||
import { Breadcrumb, BreadcrumbProps } from './breadcrumb';
|
||||
import { SettingsButton } from './buttons/settings-button';
|
||||
import { AccountDropdownButton } from './dropdowns/account-dropdown/account-dropdown';
|
||||
import { GitHubStarsButton } from './github-stars-button';
|
||||
@@ -13,12 +14,6 @@ const LogoWraper = styled.div({
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const Header = styled(_Header)({
|
||||
'&&': {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
});
|
||||
|
||||
const RightWrapper = styled.div({
|
||||
transformOrigin: 'right',
|
||||
transform: 'scale(0.85)',
|
||||
@@ -37,6 +32,55 @@ export interface AppHeaderProps {
|
||||
gridRight?: ReactNode;
|
||||
}
|
||||
|
||||
export interface HeaderProps {
|
||||
className?: string;
|
||||
gridLeft?: ReactNode;
|
||||
gridCenter?: ReactNode;
|
||||
gridRight?: ReactNode;
|
||||
}
|
||||
|
||||
const StyledHeader = styled.div({
|
||||
borderBottom: '1px solid var(--hl-md)',
|
||||
padding: 'var(--padding-xxs) var(--padding-sm)',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '2fr 1.5fr 2fr',
|
||||
gridTemplateRows: '1fr',
|
||||
gridTemplateAreas: "'header_left header_center header_right'",
|
||||
'.header_left': {
|
||||
gridArea: 'header_left',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
'.header_center': {
|
||||
gridArea: 'header_center',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
'.header_right': {
|
||||
gridArea: 'header_right',
|
||||
textAlign: 'right',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
|
||||
'&&': {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
});
|
||||
|
||||
const Header: FC<HeaderProps> = ({ className, gridLeft, gridCenter, gridRight }) => (
|
||||
<StyledHeader className={classNames('app-header theme--app-header', className)}>
|
||||
<div className="header_left">{gridLeft}</div>
|
||||
<div className="header_center">{gridCenter}</div>
|
||||
<div className="header_right">{gridRight}</div>
|
||||
</StyledHeader>
|
||||
);
|
||||
|
||||
Header.displayName = 'Header';
|
||||
|
||||
export const AppHeader: FC<AppHeaderProps> = ({
|
||||
breadcrumbProps,
|
||||
gridCenter,
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,14 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnArrowRight = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 12 12"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<circle cx={6} cy={6} r={6} />
|
||||
<path fill="currentColor" d="M3 5v2h3v2l4-3-4-3v2z" />
|
||||
</svg>
|
||||
));
|
||||
@@ -0,0 +1,20 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnBitbucketLogo = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<path fill="none" d="M0 0h14v14H0z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.078 3.111A.32.32 0 0 1 2.325 3l9.35.002a.32.32 0 0 1 .32.372l-1.36 8.357a.32.32 0 0 1-.32.269H3.79a.436.436 0 0 1-.426-.364l-1.36-8.264a.32.32 0 0 1 .074-.26Zm3.565 6.433h2.754l.667-4.091H4.897l.746 4.09Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
));
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnBrackets = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 12 12"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.504 0C3.22.07 2.312.432 1.873 1.237c-.231.424-.273.877-.26 1.268.014.376.085.765.145 1.092l.004.024c.065.356.116.645.125.903.01.254-.026.402-.077.495-.068.125-.356.467-1.805.468H0v1.5h.005c1.473 0 1.765.324 1.82.417.037.061.07.17.062.385-.009.223-.057.475-.122.801l-.006.026a6.578 6.578 0 0 0-.145 1.01c-.015.37.03.808.274 1.216.507.844 1.59 1.145 3.112 1.145l-.432-1.51c-1.105-.056-1.343-.324-1.394-.407-.036-.061-.07-.17-.061-.385.009-.223.057-.475.122-.8l.006-.027c.059-.294.131-.656.145-1.009.015-.37-.03-.809-.274-1.217-.096-.159-.212-.299-.347-.42a1.93 1.93 0 0 0 .362-.475c.231-.423.273-.877.26-1.267a7.801 7.801 0 0 0-.145-1.093l-.004-.024c-.065-.356-.116-.645-.125-.903-.01-.254.026-.402.077-.494.068-.126.357-.469 1.81-.469L4.504 0Zm2.992 0L7 1.487c1.453 0 1.742.343 1.81.469.05.092.086.24.077.494-.01.258-.06.547-.125.903l-.004.024a7.8 7.8 0 0 0-.144 1.093c-.014.39.028.844.259 1.267.098.18.219.337.362.474a1.834 1.834 0 0 0-.347.421c-.245.408-.289.847-.274 1.217.014.353.086.715.145 1.01l.006.025c.065.326.113.578.122.801.009.216-.025.324-.061.385-.05.083-.29.351-1.394.407L7 11.987c1.523 0 2.605-.301 3.112-1.145.245-.408.289-.847.274-1.217-.014-.353-.086-.715-.145-1.01l-.006-.025c-.065-.326-.113-.578-.122-.8-.009-.216.025-.325.061-.386.056-.093.349-.417 1.826-.417v-1.5c-1.453 0-1.742-.343-1.81-.468-.05-.093-.086-.24-.077-.495.01-.258.06-.547.125-.903l.004-.024c.06-.327.13-.716.144-1.092.014-.39-.028-.844-.259-1.268C9.687.432 8.78.07 7.496 0Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
||||
@@ -0,0 +1,6 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnBug = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg viewBox="0 0 111 111" width="1em" height="1em" role="img" {...props}>
|
||||
<path d="M110.081 62.515c-.102 3.716-3.245 6.63-6.962 6.63H91.32v3.412c0 4.662-1.04 9.08-2.9 13.037l12.842 12.842a6.823 6.823 0 1 1-9.649 9.649l-11.67-11.67a30.575 30.575 0 0 1-19.326 6.845V51.236a2.559 2.559 0 0 0-2.558-2.559h-5.117a2.559 2.559 0 0 0-2.559 2.559v52.024a30.575 30.575 0 0 1-19.325-6.846l-11.67 11.671a6.823 6.823 0 0 1-9.65-9.649L22.58 85.594a30.583 30.583 0 0 1-2.9-13.037v-3.411H7.882c-3.718 0-6.86-2.915-6.962-6.631A6.823 6.823 0 0 1 7.74 55.5h11.94V42.975l-9.942-9.942a6.823 6.823 0 0 1 9.65-9.65l11.647 11.648h48.93l11.648-11.647a6.823 6.823 0 1 1 9.649 9.65l-9.942 9.94V55.5h11.941a6.823 6.823 0 0 1 6.82 7.015ZM55.713.917c-13.188 0-23.88 10.691-23.88 23.88h47.76c0-13.189-10.69-23.88-23.88-23.88Z" />
|
||||
</svg>
|
||||
));
|
||||
@@ -0,0 +1,20 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnBurgerMenu = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<path fill="none" d="M0 0h14v14H0z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.5 10a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h9Zm0-4a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h9Zm0-4a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h9Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
));
|
||||
@@ -0,0 +1,13 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnCheckmark = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 14 14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<path d="m11.753 2 1.244 1.23-8.375 8.473L1 8.081l1.237-1.238L4.614 9.22 11.753 2Z" />
|
||||
</svg>
|
||||
));
|
||||
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user