mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
[feat] Sort Dashboard Cards (#3850)
* Add sort order for space items * Add ability to sort dashboard cards * Fix TS errors * Fix lint errors * Add sort options for modified time * Move workspace-card to a new file and use redux global state for persistence * Remove ordering from the space model * Use currying for the workspace mapping function and clean up types * Expand testcase for activespace initialization * Fix lint errors * Remove date defaults and check if modifedLocally is defined * Fixup: Use global selector for space order * Fixup: Decouple space order initialization from active space * Fixup: Use named exports in SpaceSortDropdown and rename handler in WrapperHome
This commit is contained in:
parent
4dda066254
commit
e683a02b16
@ -299,13 +299,15 @@ export type SortOrder =
|
||||
| 'http-method'
|
||||
| 'type-desc'
|
||||
| 'type-asc';
|
||||
export const SORT_NAME_ASC: SortOrder = 'name-asc';
|
||||
export const SORT_NAME_DESC: SortOrder = 'name-desc';
|
||||
export const SORT_CREATED_ASC: SortOrder = 'created-asc';
|
||||
export const SORT_CREATED_DESC: SortOrder = 'created-desc';
|
||||
export const SORT_HTTP_METHOD: SortOrder = 'http-method';
|
||||
export const SORT_TYPE_DESC: SortOrder = 'type-desc';
|
||||
export const SORT_TYPE_ASC: SortOrder = 'type-asc';
|
||||
export const SORT_NAME_ASC = 'name-asc';
|
||||
export const SORT_NAME_DESC = 'name-desc';
|
||||
export const SORT_CREATED_ASC = 'created-asc';
|
||||
export const SORT_CREATED_DESC = 'created-desc';
|
||||
export const SORT_MODIFIED_ASC = 'modified-asc';
|
||||
export const SORT_MODIFIED_DESC = 'modified-desc';
|
||||
export const SORT_HTTP_METHOD = 'http-method';
|
||||
export const SORT_TYPE_DESC = 'type-desc';
|
||||
export const SORT_TYPE_ASC = 'type-asc';
|
||||
export const SORT_ORDERS = [
|
||||
SORT_NAME_ASC,
|
||||
SORT_NAME_DESC,
|
||||
@ -325,6 +327,29 @@ export const sortOrderName: Record<SortOrder, string> = {
|
||||
[SORT_TYPE_ASC]: 'Requests First',
|
||||
};
|
||||
|
||||
export type SpaceSortOrder =
|
||||
| 'name-asc'
|
||||
| 'name-desc'
|
||||
| 'created-asc'
|
||||
| 'created-desc'
|
||||
| 'modified-desc'
|
||||
|
||||
export const SPACE_SORT_ORDERS = [
|
||||
SORT_MODIFIED_DESC,
|
||||
SORT_NAME_ASC,
|
||||
SORT_NAME_DESC,
|
||||
SORT_CREATED_ASC,
|
||||
SORT_CREATED_DESC,
|
||||
];
|
||||
|
||||
export const spaceSortOrderName: Record<SpaceSortOrder, string> = {
|
||||
[SORT_NAME_ASC]: 'Name Ascending',
|
||||
[SORT_NAME_DESC]: 'Name Descending',
|
||||
[SORT_CREATED_ASC]: 'Oldest First',
|
||||
[SORT_CREATED_DESC]: 'Newest First',
|
||||
[SORT_MODIFIED_DESC]: 'Last Modified',
|
||||
};
|
||||
|
||||
export function getPreviewModeName(previewMode, useLong = false) {
|
||||
if (previewModeMap.hasOwnProperty(previewMode)) {
|
||||
return useLong ? previewModeMap[previewMode][1] : previewModeMap[previewMode][0];
|
||||
|
@ -445,6 +445,10 @@ export function snapNumberToLimits(value: number, min?: number, max?: number) {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function isNotNullOrUndefined(obj: any | null | undefined) {
|
||||
return obj !== null && obj !== undefined;
|
||||
}
|
||||
export function isNotNullOrUndefined<ValueType>(
|
||||
value: ValueType | null | undefined
|
||||
): value is ValueType {
|
||||
if (value === null || value === undefined) return false;
|
||||
|
||||
return true;
|
||||
}
|
@ -6,25 +6,26 @@ import {
|
||||
SORT_CREATED_ASC,
|
||||
SORT_CREATED_DESC,
|
||||
SORT_HTTP_METHOD,
|
||||
SORT_MODIFIED_ASC,
|
||||
SORT_MODIFIED_DESC,
|
||||
SORT_NAME_ASC,
|
||||
SORT_NAME_DESC,
|
||||
SORT_TYPE_ASC,
|
||||
SORT_TYPE_DESC,
|
||||
SortOrder,
|
||||
} from './constants';
|
||||
|
||||
type SortableModel = Request | RequestGroup | GrpcRequest;
|
||||
type SortFunction = (a: SortableModel, b: SortableModel) => number;
|
||||
type SortFunction<SortableType> = (a: SortableType, b: SortableType) => number;
|
||||
|
||||
const ascendingNameSort: SortFunction = (a, b) => {
|
||||
const ascendingNameSort: SortFunction<{name: string}> = (a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
};
|
||||
|
||||
const descendingNameSort: SortFunction = (a, b) => {
|
||||
const descendingNameSort: SortFunction<{name: string}> = (a, b) => {
|
||||
return b.name.localeCompare(a.name);
|
||||
};
|
||||
|
||||
const createdFirstSort: SortFunction = (a, b) => {
|
||||
const createdFirstSort: SortFunction<{created: number}> = (a, b) => {
|
||||
if (a.created === b.created) {
|
||||
return 0;
|
||||
}
|
||||
@ -32,7 +33,7 @@ const createdFirstSort: SortFunction = (a, b) => {
|
||||
return a.created < b.created ? -1 : 1;
|
||||
};
|
||||
|
||||
const createdLastSort: SortFunction = (a, b) => {
|
||||
const createdLastSort: SortFunction<{created: number}> = (a, b) => {
|
||||
if (a.created === b.created) {
|
||||
return 0;
|
||||
}
|
||||
@ -40,7 +41,23 @@ const createdLastSort: SortFunction = (a, b) => {
|
||||
return a.created > b.created ? -1 : 1;
|
||||
};
|
||||
|
||||
const httpMethodSort: SortFunction = (a, b) => {
|
||||
const ascendingModifiedSort: SortFunction<{lastModifiedTimestamp: number}> = (a, b) => {
|
||||
if (a.lastModifiedTimestamp === b.lastModifiedTimestamp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return a.lastModifiedTimestamp < b.lastModifiedTimestamp ? -1 : 1;
|
||||
};
|
||||
|
||||
const descendingModifiedSort: SortFunction<{lastModifiedTimestamp: number}> = (a, b) => {
|
||||
if (a.lastModifiedTimestamp === b.lastModifiedTimestamp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return a.lastModifiedTimestamp > b.lastModifiedTimestamp ? -1 : 1;
|
||||
};
|
||||
|
||||
const httpMethodSort: SortFunction<Pick<SortableModel, 'type' | 'metaSortKey' | '_id'>> = (a, b) => {
|
||||
// Sort Requests and GrpcRequests to top, in that order
|
||||
if (a.type !== b.type) {
|
||||
if (isRequest(a) || isRequest(b)) {
|
||||
@ -75,7 +92,7 @@ const httpMethodSort: SortFunction = (a, b) => {
|
||||
return metaSortKeySort(a, b);
|
||||
};
|
||||
|
||||
const ascendingTypeSort: SortFunction = (a, b) => {
|
||||
const ascendingTypeSort: SortFunction<Pick<SortableModel, 'type' | 'metaSortKey' | '_id'>> = (a, b) => {
|
||||
if (a.type !== b.type && (isRequestGroup(a) || isRequestGroup(b))) {
|
||||
return isRequestGroup(b) ? -1 : 1;
|
||||
}
|
||||
@ -83,7 +100,7 @@ const ascendingTypeSort: SortFunction = (a, b) => {
|
||||
return metaSortKeySort(a, b);
|
||||
};
|
||||
|
||||
const descendingTypeSort: SortFunction = (a, b) => {
|
||||
const descendingTypeSort: SortFunction<Pick<SortableModel, 'type' | 'metaSortKey' | '_id'>> = (a, b) => {
|
||||
if (a.type !== b.type && (isRequestGroup(a) || isRequestGroup(b))) {
|
||||
return isRequestGroup(a) ? -1 : 1;
|
||||
}
|
||||
@ -91,7 +108,7 @@ const descendingTypeSort: SortFunction = (a, b) => {
|
||||
return metaSortKeySort(a, b);
|
||||
};
|
||||
|
||||
export const metaSortKeySort: SortFunction = (a, b) => {
|
||||
export const metaSortKeySort: SortFunction<Pick<SortableModel, '_id' | 'metaSortKey'>> = (a, b) => {
|
||||
if (a.metaSortKey === b.metaSortKey) {
|
||||
return a._id > b._id ? -1 : 1;
|
||||
}
|
||||
@ -99,20 +116,21 @@ export const metaSortKeySort: SortFunction = (a, b) => {
|
||||
return a.metaSortKey < b.metaSortKey ? -1 : 1;
|
||||
};
|
||||
|
||||
export const ascendingNumberSort = (a: number, b: number): number => {
|
||||
export const ascendingNumberSort: SortFunction<number> = (a, b) => {
|
||||
return a < b ? -1 : 1;
|
||||
};
|
||||
|
||||
export const descendingNumberSort = (a: number, b: number): number => {
|
||||
export const descendingNumberSort: SortFunction<number> = (a, b) => {
|
||||
return ascendingNumberSort(b, a);
|
||||
};
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION appears to be a genuine error
|
||||
export const sortMethodMap: Record<SortOrder, SortFunction> = {
|
||||
export const sortMethodMap = {
|
||||
[SORT_NAME_ASC]: ascendingNameSort,
|
||||
[SORT_NAME_DESC]: descendingNameSort,
|
||||
[SORT_CREATED_ASC]: createdFirstSort,
|
||||
[SORT_CREATED_DESC]: createdLastSort,
|
||||
[SORT_MODIFIED_ASC]: ascendingModifiedSort,
|
||||
[SORT_MODIFIED_DESC]: descendingModifiedSort,
|
||||
[SORT_HTTP_METHOD]: httpMethodSort,
|
||||
[SORT_TYPE_DESC]: descendingTypeSort,
|
||||
[SORT_TYPE_ASC]: ascendingTypeSort,
|
||||
|
@ -0,0 +1,27 @@
|
||||
import { Button, Dropdown, DropdownItem } from 'insomnia-components';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { SPACE_SORT_ORDERS, SpaceSortOrder, spaceSortOrderName } from '../../../common/constants';
|
||||
|
||||
interface SpaceSortDropdownProps {
|
||||
onSelect: (value: SpaceSortOrder) => void;
|
||||
}
|
||||
|
||||
export const SpaceSortDropdown: FC<SpaceSortDropdownProps> = ({ onSelect }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
className="margin-left"
|
||||
renderButton={
|
||||
<Button>
|
||||
<i className="fa fa-sort" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{SPACE_SORT_ORDERS.map(order => (
|
||||
<DropdownItem value={order} onClick={onSelect} key={order}>
|
||||
{spaceSortOrderName[order]}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
@ -13,7 +13,7 @@ const SidebarSortDropdown: FunctionComponent<Props> = ({ handleSort }) => (
|
||||
<i className="fa fa-sort" />
|
||||
</DropdownButton>
|
||||
{SORT_ORDERS.map(order => (
|
||||
<DropdownItem onClick={() => handleSort(order)} key={order}>
|
||||
<DropdownItem value={order} onClick={handleSort} key={order}>
|
||||
{sortOrderName[order]}
|
||||
</DropdownItem>
|
||||
))}
|
||||
|
146
packages/insomnia-app/app/ui/components/workspace-card.tsx
Normal file
146
packages/insomnia-app/app/ui/components/workspace-card.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import { Card } from 'insomnia-components';
|
||||
import React, { Fragment } from 'react';
|
||||
import { FC } from 'react';
|
||||
|
||||
import {
|
||||
ACTIVITY_DEBUG,
|
||||
ACTIVITY_SPEC,
|
||||
GlobalActivity,
|
||||
} from '../../common/constants';
|
||||
import { fuzzyMatchAll } from '../../common/misc';
|
||||
import { strings } from '../../common/strings';
|
||||
import { ApiSpec } from '../../models/api-spec';
|
||||
import { Space } from '../../models/space';
|
||||
import { isDesign, Workspace } from '../../models/workspace';
|
||||
import Highlight from './base/highlight';
|
||||
import { WorkspaceCardDropdown } from './dropdowns/workspace-card-dropdown';
|
||||
import TimeFromNow from './time-from-now';
|
||||
|
||||
export interface WorkspaceCardProps {
|
||||
apiSpec: ApiSpec;
|
||||
workspace: Workspace;
|
||||
filter: string;
|
||||
activeSpace: Space;
|
||||
lastActiveBranch?: string | null;
|
||||
lastModifiedTimestamp: number;
|
||||
lastCommitTime?: number | null;
|
||||
lastCommitAuthor?: string | null;
|
||||
modifiedLocally?: number;
|
||||
spec: Record<string, any> | null;
|
||||
specFormat: 'openapi' | 'swagger' | null;
|
||||
specFormatVersion: string | null;
|
||||
hasUnsavedChanges: boolean;
|
||||
onSelect: (workspaceId: string, activity: GlobalActivity) => void;
|
||||
}
|
||||
|
||||
const WorkspaceCard: FC<WorkspaceCardProps> = ({
|
||||
apiSpec,
|
||||
filter,
|
||||
lastActiveBranch,
|
||||
lastModifiedTimestamp,
|
||||
workspace,
|
||||
activeSpace,
|
||||
lastCommitTime,
|
||||
modifiedLocally,
|
||||
lastCommitAuthor,
|
||||
spec,
|
||||
specFormat,
|
||||
specFormatVersion,
|
||||
hasUnsavedChanges,
|
||||
onSelect,
|
||||
}) => {
|
||||
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 docMenu = (
|
||||
<WorkspaceCardDropdown
|
||||
apiSpec={apiSpec}
|
||||
workspace={workspace}
|
||||
space={activeSpace}
|
||||
/>
|
||||
);
|
||||
|
||||
const version = 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={`v${version}`} /> : undefined
|
||||
}
|
||||
tagLabel={
|
||||
label ? (
|
||||
<>
|
||||
<span className="margin-right-xs">{labelIcon}</span>
|
||||
<Highlight search={filter} text={label} />
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
docLog={log}
|
||||
docMenu={docMenu}
|
||||
docFormat={format}
|
||||
onClick={() => onSelect(workspace._id, defaultActivity)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceCard;
|
@ -4,56 +4,55 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Card,
|
||||
CardContainer,
|
||||
Dropdown,
|
||||
DropdownDivider,
|
||||
DropdownItem,
|
||||
Header,
|
||||
} from 'insomnia-components';
|
||||
import React, { Fragment, PureComponent, ReactNode } from 'react';
|
||||
import React, { Fragment, PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { unreachableCase } from 'ts-assert-unreachable';
|
||||
|
||||
import { parseApiSpec, ParsedApiSpec } from '../../common/api-specs';
|
||||
import {
|
||||
AUTOBIND_CFG,
|
||||
SpaceSortOrder,
|
||||
} from '../../common/constants';
|
||||
import { hotKeyRefs } from '../../common/hotkeys';
|
||||
import { executeHotKey } from '../../common/hotkeys-listener';
|
||||
import { fuzzyMatchAll, isNotNullOrUndefined } from '../../common/misc';
|
||||
import { descendingNumberSort } from '../../common/sorting';
|
||||
import { isNotNullOrUndefined } from '../../common/misc';
|
||||
import { descendingNumberSort, sortMethodMap } from '../../common/sorting';
|
||||
import { strings } from '../../common/strings';
|
||||
import { ApiSpec } from '../../models/api-spec';
|
||||
import { isRemoteSpace } from '../../models/space';
|
||||
import { isDesign, Workspace, WorkspaceScopeKeys } from '../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../models/workspace-meta';
|
||||
import { MemClient } from '../../sync/git/mem-client';
|
||||
import { initializeLocalProjectAndMarkForSync } from '../../sync/vcs/initialize-project';
|
||||
import coreLogo from '../images/insomnia-core-logo.png';
|
||||
import { cloneGitRepository } from '../redux/modules/git';
|
||||
import { setSpaceSortOrder } from '../redux/modules/global';
|
||||
import { ForceToWorkspace } from '../redux/modules/helpers';
|
||||
import { importClipBoard, importFile, importUri } from '../redux/modules/import';
|
||||
import { activateWorkspace, createWorkspace } from '../redux/modules/workspace';
|
||||
import Highlight from './base/highlight';
|
||||
import { selectSpaceSortOrder } from '../redux/selectors';
|
||||
import SettingsButton from './buttons/settings-button';
|
||||
import AccountDropdown from './dropdowns/account-dropdown';
|
||||
import { RemoteWorkspacesDropdown } from './dropdowns/remote-workspaces-dropdown';
|
||||
import { SpaceDropdown } from './dropdowns/space-dropdown';
|
||||
import { WorkspaceCardDropdown } from './dropdowns/workspace-card-dropdown';
|
||||
import { SpaceSortDropdown } from './dropdowns/space-sort-dropdown';
|
||||
import KeydownBinder from './keydown-binder';
|
||||
import { showPrompt } from './modals';
|
||||
import Notice from './notice';
|
||||
import PageLayout from './page-layout';
|
||||
import TimeFromNow from './time-from-now';
|
||||
import type {
|
||||
WrapperProps,
|
||||
} from './wrapper';
|
||||
import WorkspaceCard, { WorkspaceCardProps } from './workspace-card';
|
||||
import type { WrapperProps } from './wrapper';
|
||||
|
||||
interface RenderedCard {
|
||||
card: ReactNode;
|
||||
lastModifiedTimestamp?: number | null;
|
||||
}
|
||||
|
||||
interface Props extends ReturnType<typeof mapDispatchToProps> {
|
||||
interface Props
|
||||
extends ReturnType<typeof mapDispatchToProps>,
|
||||
ReturnType<typeof mapStateToProps> {
|
||||
wrapperProps: WrapperProps;
|
||||
}
|
||||
|
||||
@ -61,6 +60,100 @@ interface State {
|
||||
filter: string;
|
||||
}
|
||||
|
||||
function orderSpaceCards(orderBy: SpaceSortOrder) {
|
||||
return (cardA: Pick<WorkspaceCardProps, 'workspace' | 'lastModifiedTimestamp'>, cardB: Pick<WorkspaceCardProps, 'workspace' | 'lastModifiedTimestamp'>) => {
|
||||
switch (orderBy) {
|
||||
case 'modified-desc':
|
||||
return sortMethodMap['modified-desc'](cardA, cardB);
|
||||
case 'name-asc':
|
||||
return sortMethodMap['name-asc'](cardA.workspace, cardB.workspace);
|
||||
case 'name-desc':
|
||||
return sortMethodMap['name-desc'](cardA.workspace, cardB.workspace);
|
||||
case 'created-asc':
|
||||
return sortMethodMap['created-asc'](cardA.workspace, cardB.workspace);
|
||||
case 'created-desc':
|
||||
return sortMethodMap['created-desc'](cardA.workspace, cardB.workspace);
|
||||
default:
|
||||
return unreachableCase(orderBy, `Space Ordering "${orderBy}" is invalid`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const mapWorkspaceToWorkspaceCard = ({
|
||||
apiSpecs,
|
||||
workspaceMetas,
|
||||
}: {
|
||||
apiSpecs: ApiSpec[];
|
||||
workspaceMetas: WorkspaceMeta[];
|
||||
}) => (workspace: Workspace) => {
|
||||
const apiSpec = apiSpecs.find((s) => s.parentId === workspace._id);
|
||||
|
||||
// an apiSpec model will always exist because a migration in the workspace forces it to
|
||||
if (!apiSpec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let spec: ParsedApiSpec['contents'] = null;
|
||||
let specFormat: ParsedApiSpec['format'] = null;
|
||||
let specFormatVersion: ParsedApiSpec['formatVersion'] = null;
|
||||
|
||||
try {
|
||||
const result = parseApiSpec(apiSpec.contents);
|
||||
spec = result.contents;
|
||||
specFormat = result.format;
|
||||
specFormatVersion = result.formatVersion;
|
||||
} catch (err) {
|
||||
// Assume there is no spec
|
||||
// TODO: Check for parse errors if it's an invalid spec
|
||||
}
|
||||
|
||||
// Get cached branch from WorkspaceMeta
|
||||
const workspaceMeta = workspaceMetas?.find(
|
||||
(wm) => wm.parentId === workspace._id
|
||||
);
|
||||
|
||||
const lastActiveBranch = workspaceMeta?.cachedGitRepositoryBranch;
|
||||
|
||||
const lastCommitAuthor = workspaceMeta?.cachedGitLastAuthor;
|
||||
|
||||
// WorkspaceMeta is a good proxy for last modified time
|
||||
const workspaceModified = workspaceMeta?.modified || workspace.modified;
|
||||
|
||||
const modifiedLocally = isDesign(workspace)
|
||||
? apiSpec.modified
|
||||
: workspaceModified;
|
||||
|
||||
// Span spec, workspace and sync related timestamps for card last modified label and sort order
|
||||
const lastModifiedFrom = [
|
||||
workspace?.modified,
|
||||
workspaceMeta?.modified,
|
||||
apiSpec.modified,
|
||||
workspaceMeta?.cachedGitLastCommitTime,
|
||||
];
|
||||
|
||||
const lastModifiedTimestamp = lastModifiedFrom
|
||||
.filter(isNotNullOrUndefined)
|
||||
.sort(descendingNumberSort)[0];
|
||||
|
||||
const hasUnsavedChanges = Boolean(
|
||||
isDesign(workspace) && workspaceMeta?.cachedGitLastCommitTime && apiSpec.modified > workspaceMeta?.cachedGitLastCommitTime
|
||||
);
|
||||
|
||||
return {
|
||||
hasUnsavedChanges,
|
||||
lastModifiedTimestamp,
|
||||
modifiedLocally,
|
||||
lastCommitTime: workspaceMeta?.cachedGitLastCommitTime,
|
||||
lastCommitAuthor,
|
||||
lastActiveBranch,
|
||||
spec,
|
||||
specFormat,
|
||||
apiSpec,
|
||||
specFormatVersion,
|
||||
workspace,
|
||||
};
|
||||
};
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
class WrapperHome extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
@ -86,7 +179,10 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
_handleCollectionCreate() {
|
||||
const { handleCreateWorkspace, wrapperProps: { activeSpace, vcs, isLoggedIn } } = this.props;
|
||||
const {
|
||||
handleCreateWorkspace,
|
||||
wrapperProps: { activeSpace, vcs, isLoggedIn },
|
||||
} = this.props;
|
||||
|
||||
handleCreateWorkspace({
|
||||
scope: WorkspaceScopeKeys.collection,
|
||||
@ -117,7 +213,7 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
submitName: 'Fetch and Import',
|
||||
label: 'URL',
|
||||
placeholder: 'https://website.com/insomnia-import.json',
|
||||
onComplete: uri => {
|
||||
onComplete: (uri) => {
|
||||
this.props.handleImportUri(uri, {
|
||||
forceToWorkspace: ForceToWorkspace.new,
|
||||
});
|
||||
@ -139,140 +235,6 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
renderCard(workspace: Workspace) {
|
||||
const {
|
||||
activeSpace,
|
||||
apiSpecs,
|
||||
workspaceMetas,
|
||||
} = this.props.wrapperProps;
|
||||
const { filter } = this.state;
|
||||
const apiSpec = apiSpecs.find(s => s.parentId === workspace._id);
|
||||
|
||||
// an apiSpec model will always exist because a migration in the workspace forces it to
|
||||
if (!apiSpec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let spec: ParsedApiSpec['contents'] = null;
|
||||
let specFormat: ParsedApiSpec['format'] = null;
|
||||
let specFormatVersion: ParsedApiSpec['formatVersion'] = null;
|
||||
|
||||
try {
|
||||
const result = parseApiSpec(apiSpec.contents);
|
||||
spec = result.contents;
|
||||
specFormat = result.format;
|
||||
specFormatVersion = result.formatVersion;
|
||||
} catch (err) {
|
||||
// Assume there is no spec
|
||||
// TODO: Check for parse errors if it's an invalid spec
|
||||
}
|
||||
|
||||
// Get cached branch from WorkspaceMeta
|
||||
const workspaceMeta = workspaceMetas?.find(wm => wm.parentId === workspace._id);
|
||||
const lastActiveBranch = workspaceMeta ? workspaceMeta.cachedGitRepositoryBranch : null;
|
||||
const lastCommitAuthor = workspaceMeta ? workspaceMeta.cachedGitLastAuthor : null;
|
||||
const lastCommitTime = workspaceMeta ? workspaceMeta.cachedGitLastCommitTime : null;
|
||||
|
||||
// WorkspaceMeta is a good proxy for last modified time
|
||||
const workspaceModified = workspaceMeta ? workspaceMeta.modified : workspace.modified;
|
||||
const modifiedLocally = isDesign(workspace) ? apiSpec.modified : workspaceModified;
|
||||
|
||||
// Span spec, workspace and sync related timestamps for card last modified label and sort order
|
||||
const lastModifiedFrom = [
|
||||
workspace?.modified,
|
||||
workspaceMeta?.modified,
|
||||
apiSpec.modified,
|
||||
workspaceMeta?.cachedGitLastCommitTime,
|
||||
];
|
||||
const lastModifiedTimestamp = lastModifiedFrom
|
||||
.filter(isNotNullOrUndefined)
|
||||
.sort(descendingNumberSort)[0];
|
||||
// @ts-expect-error -- TSCONVERSION appears to be genuine
|
||||
let log = <TimeFromNow timestamp={lastModifiedTimestamp} />;
|
||||
let branch = lastActiveBranch;
|
||||
|
||||
if (
|
||||
isDesign(workspace) &&
|
||||
lastCommitTime &&
|
||||
apiSpec.modified > lastCommitTime
|
||||
) {
|
||||
// Show locally unsaved changes for spec
|
||||
// NOTE: this doesn't work for non-spec workspaces
|
||||
branch = lastActiveBranch + '*';
|
||||
log = (
|
||||
<Fragment>
|
||||
<TimeFromNow className="text-danger" timestamp={modifiedLocally} /> (unsaved)
|
||||
</Fragment>
|
||||
);
|
||||
} else if (lastCommitTime) {
|
||||
// Show last commit time and author
|
||||
branch = lastActiveBranch;
|
||||
log = (
|
||||
<Fragment>
|
||||
<TimeFromNow timestamp={lastCommitTime} /> {lastCommitAuthor && `by ${lastCommitAuthor}`}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const docMenu = <WorkspaceCardDropdown apiSpec={apiSpec} workspace={workspace} space={activeSpace} />;
|
||||
const version = spec?.info?.version || '';
|
||||
let label: string = strings.collection.singular;
|
||||
let format = '';
|
||||
let labelIcon = <i className="fa fa-bars" />;
|
||||
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}`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const card = (
|
||||
<Card
|
||||
key={apiSpec._id}
|
||||
docBranch={branch ? <Highlight search={filter} text={branch} /> : undefined}
|
||||
docTitle={title ? <Highlight search={filter} text={title} /> : undefined}
|
||||
docVersion={version ? <Highlight search={filter} text={`v${version}`} /> : undefined}
|
||||
tagLabel={
|
||||
label ? (
|
||||
<>
|
||||
<span className="margin-right-xs">{labelIcon}</span>
|
||||
<Highlight search={filter} text={label} />
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
docLog={log}
|
||||
docMenu={docMenu}
|
||||
docFormat={format}
|
||||
onClick={() => this.props.handleActivateWorkspace(workspace)}
|
||||
/>
|
||||
);
|
||||
const renderedCard: RenderedCard = {
|
||||
card,
|
||||
lastModifiedTimestamp,
|
||||
};
|
||||
return renderedCard;
|
||||
}
|
||||
|
||||
renderCreateMenu() {
|
||||
const button = (
|
||||
<Button variant="contained" bg="surprise" className="margin-left">
|
||||
@ -283,25 +245,41 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
return (
|
||||
<Dropdown renderButton={button}>
|
||||
<DropdownDivider>New</DropdownDivider>
|
||||
<DropdownItem icon={<i className="fa fa-file-o" />} onClick={this._handleDocumentCreate}>
|
||||
<DropdownItem
|
||||
icon={<i className="fa fa-file-o" />}
|
||||
onClick={this._handleDocumentCreate}
|
||||
>
|
||||
Design Document
|
||||
</DropdownItem>
|
||||
<DropdownItem icon={<i className="fa fa-bars" />} onClick={this._handleCollectionCreate}>
|
||||
<DropdownItem
|
||||
icon={<i className="fa fa-bars" />}
|
||||
onClick={this._handleCollectionCreate}
|
||||
>
|
||||
Request Collection
|
||||
</DropdownItem>
|
||||
<DropdownDivider>Import From</DropdownDivider>
|
||||
<DropdownItem icon={<i className="fa fa-plus" />} onClick={this._handleImportFile}>
|
||||
<DropdownItem
|
||||
icon={<i className="fa fa-plus" />}
|
||||
onClick={this._handleImportFile}
|
||||
>
|
||||
File
|
||||
</DropdownItem>
|
||||
<DropdownItem icon={<i className="fa fa-link" />} onClick={this._handleImportUri}>
|
||||
<DropdownItem
|
||||
icon={<i className="fa fa-link" />}
|
||||
onClick={this._handleImportUri}
|
||||
>
|
||||
URL
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
icon={<i className="fa fa-clipboard" />}
|
||||
onClick={this._handleImportClipBoard}>
|
||||
onClick={this._handleImportClipBoard}
|
||||
>
|
||||
Clipboard
|
||||
</DropdownItem>
|
||||
<DropdownItem icon={<i className="fa fa-code-fork" />} onClick={this._handleWorkspaceClone}>
|
||||
<DropdownItem
|
||||
icon={<i className="fa fa-code-fork" />}
|
||||
onClick={this._handleWorkspaceClone}
|
||||
>
|
||||
Git Clone
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
@ -309,14 +287,16 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
renderDashboardMenu() {
|
||||
const { vcs } = this.props.wrapperProps;
|
||||
const { wrapperProps, handleSetSpaceSortOrder } = this.props;
|
||||
const { vcs } = wrapperProps;
|
||||
return (
|
||||
<div className="row row--right pad-left wide">
|
||||
<div
|
||||
className="form-control form-control--outlined no-margin"
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<KeydownBinder onKeydown={this._handleKeyDown}>
|
||||
<input
|
||||
ref={this._setFilterInputRef}
|
||||
@ -328,6 +308,7 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
<span className="fa fa-search filter-icon" />
|
||||
</KeydownBinder>
|
||||
</div>
|
||||
<SpaceSortDropdown onSelect={handleSetSpaceSortOrder} />
|
||||
<RemoteWorkspacesDropdown vcs={vcs} className="margin-left" />
|
||||
{this.renderCreateMenu()}
|
||||
</div>
|
||||
@ -335,16 +316,38 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { workspaces, isLoading, vcs } = this.props.wrapperProps;
|
||||
const { sortOrder, wrapperProps, handleActivateWorkspace } = this.props;
|
||||
const {
|
||||
workspaces,
|
||||
isLoading,
|
||||
vcs,
|
||||
activeSpace,
|
||||
workspaceMetas,
|
||||
apiSpecs,
|
||||
} = wrapperProps;
|
||||
const { filter } = this.state;
|
||||
// Render each card, removing all the ones that don't match the filter
|
||||
const cards = workspaces
|
||||
.map(this.renderCard)
|
||||
.map(
|
||||
mapWorkspaceToWorkspaceCard({
|
||||
workspaceMetas,
|
||||
apiSpecs,
|
||||
})
|
||||
)
|
||||
.filter(isNotNullOrUndefined)
|
||||
// @ts-expect-error -- TSCONVERSION appears to be a genuine error
|
||||
.sort((a: RenderedCard, b: RenderedCard) => descendingNumberSort(a.lastModifiedTimestamp, b.lastModifiedTimestamp))
|
||||
.map(c => c?.card);
|
||||
const countLabel = cards.length === 1 ? strings.document.singular : strings.document.plural;
|
||||
.sort(orderSpaceCards(sortOrder))
|
||||
.map((props) => (
|
||||
<WorkspaceCard
|
||||
{...props}
|
||||
key={props.apiSpec._id}
|
||||
activeSpace={activeSpace}
|
||||
onSelect={() => handleActivateWorkspace(props.workspace)}
|
||||
filter={filter}
|
||||
/>
|
||||
));
|
||||
|
||||
const countLabel =
|
||||
cards.length === 1 ? strings.document.singular : strings.document.plural;
|
||||
return (
|
||||
<PageLayout
|
||||
wrapperProps={this.props.wrapperProps}
|
||||
@ -354,8 +357,17 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
gridLeft={
|
||||
<Fragment>
|
||||
<img src={coreLogo} alt="Insomnia" width="24" height="24" />
|
||||
<Breadcrumb crumbs={[{ id: 'space', node: <SpaceDropdown vcs={vcs || undefined} /> }]} />
|
||||
{isLoading ? <i className="fa fa-refresh fa-spin space-left" /> : null}
|
||||
<Breadcrumb
|
||||
crumbs={[
|
||||
{
|
||||
id: 'space',
|
||||
node: <SpaceDropdown vcs={vcs || undefined} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<i className="fa fa-refresh fa-spin space-left" />
|
||||
) : null}
|
||||
</Fragment>
|
||||
}
|
||||
gridRight={
|
||||
@ -392,15 +404,23 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
sortOrder: selectSpaceSortOrder(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
const bound = bindActionCreators({
|
||||
createWorkspace,
|
||||
cloneGitRepository,
|
||||
importFile,
|
||||
importClipBoard,
|
||||
importUri,
|
||||
activateWorkspace,
|
||||
}, dispatch);
|
||||
const bound = bindActionCreators(
|
||||
{
|
||||
createWorkspace,
|
||||
cloneGitRepository,
|
||||
importFile,
|
||||
importClipBoard,
|
||||
importUri,
|
||||
setSpaceSortOrder,
|
||||
activateWorkspace,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
return ({
|
||||
handleCreateWorkspace: bound.createWorkspace,
|
||||
@ -408,8 +428,9 @@ const mapDispatchToProps = (dispatch) => {
|
||||
handleImportFile: bound.importFile,
|
||||
handleImportUri: bound.importUri,
|
||||
handleImportClipboard: bound.importClipBoard,
|
||||
handleSetSpaceSortOrder: bound.setSpaceSortOrder,
|
||||
handleActivateWorkspace: bound.activateWorkspace,
|
||||
});
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(WrapperHome);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(WrapperHome);
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
ACTIVITY_UNIT_TEST,
|
||||
DEPRECATED_ACTIVITY_INSOMNIA,
|
||||
GlobalActivity,
|
||||
SORT_MODIFIED_DESC,
|
||||
} from '../../../../common/constants';
|
||||
import { getDesignerDataDir } from '../../../../common/electron-helpers';
|
||||
import * as models from '../../../../models';
|
||||
@ -23,10 +24,12 @@ import {
|
||||
initActiveActivity,
|
||||
initActiveSpace,
|
||||
initActiveWorkspace,
|
||||
initSpaceSortOrder,
|
||||
LOCALSTORAGE_PREFIX,
|
||||
SET_ACTIVE_ACTIVITY,
|
||||
SET_ACTIVE_SPACE,
|
||||
SET_ACTIVE_WORKSPACE,
|
||||
SET_SPACE_SORT_ORDER,
|
||||
setActiveActivity,
|
||||
setActiveSpace,
|
||||
setActiveWorkspace,
|
||||
@ -272,6 +275,37 @@ describe('global', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('initSpaceSortOrder', () => {
|
||||
it('should initialize from local storage', () => {
|
||||
const sortOrder = SORT_MODIFIED_DESC;
|
||||
|
||||
global.localStorage.setItem(
|
||||
`${LOCALSTORAGE_PREFIX}::space-sort-order`,
|
||||
JSON.stringify(sortOrder),
|
||||
);
|
||||
|
||||
const expectedEvent = {
|
||||
'payload': {
|
||||
sortOrder,
|
||||
},
|
||||
'type': SET_SPACE_SORT_ORDER,
|
||||
};
|
||||
|
||||
expect(initSpaceSortOrder()).toStrictEqual(expectedEvent);
|
||||
});
|
||||
|
||||
it('should default to modified-desc if not exist', async () => {
|
||||
const expectedEvent = {
|
||||
'payload': {
|
||||
sortOrder: SORT_MODIFIED_DESC,
|
||||
},
|
||||
'type': SET_SPACE_SORT_ORDER,
|
||||
};
|
||||
|
||||
expect(initSpaceSortOrder()).toStrictEqual(expectedEvent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initActiveActivity', () => {
|
||||
it.each([
|
||||
ACTIVITY_SPEC,
|
||||
|
@ -7,7 +7,7 @@ import { combineReducers, Dispatch } from 'redux';
|
||||
import { unreachableCase } from 'ts-assert-unreachable';
|
||||
|
||||
import { trackEvent } from '../../../common/analytics';
|
||||
import { GlobalActivity } from '../../../common/constants';
|
||||
import type { GlobalActivity, SpaceSortOrder } from '../../../common/constants';
|
||||
import {
|
||||
ACTIVITY_ANALYTICS,
|
||||
ACTIVITY_DEBUG,
|
||||
@ -57,6 +57,7 @@ export const LOAD_STOP = 'global/load-stop';
|
||||
const LOAD_REQUEST_START = 'global/load-request-start';
|
||||
const LOAD_REQUEST_STOP = 'global/load-request-stop';
|
||||
export const SET_ACTIVE_SPACE = 'global/activate-space';
|
||||
export const SET_SPACE_SORT_ORDER = 'global/space-sort-order';
|
||||
export const SET_ACTIVE_WORKSPACE = 'global/activate-workspace';
|
||||
export const SET_ACTIVE_ACTIVITY = 'global/activate-activity';
|
||||
const COMMAND_ALERT = 'app/alert';
|
||||
@ -89,6 +90,16 @@ function activeSpaceReducer(state: string = BASE_SPACE_ID, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function spaceSortOrderReducer(state: SpaceSortOrder = 'modified-desc', action) {
|
||||
switch (action.type) {
|
||||
case SET_SPACE_SORT_ORDER:
|
||||
return action.payload.sortOrder;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function activeWorkspaceReducer(state: string | null = null, action) {
|
||||
switch (action.type) {
|
||||
case SET_ACTIVE_WORKSPACE:
|
||||
@ -142,6 +153,7 @@ function loginStateChangeReducer(state = false, action) {
|
||||
export interface GlobalState {
|
||||
isLoading: boolean;
|
||||
activeSpaceId: string;
|
||||
spaceSortOrder: SpaceSortOrder;
|
||||
activeWorkspaceId: string | null;
|
||||
activeActivity: GlobalActivity | null,
|
||||
isLoggedIn: boolean;
|
||||
@ -150,6 +162,7 @@ export interface GlobalState {
|
||||
|
||||
export const reducer = combineReducers<GlobalState>({
|
||||
isLoading: loadingReducer,
|
||||
spaceSortOrder: spaceSortOrderReducer,
|
||||
loadingRequestIds: loadingRequestsReducer,
|
||||
activeSpaceId: activeSpaceReducer,
|
||||
activeWorkspaceId: activeWorkspaceReducer,
|
||||
@ -374,6 +387,17 @@ export const setActiveSpace = (spaceId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const setSpaceSortOrder = (sortOrder: SpaceSortOrder) => {
|
||||
const key = `${LOCALSTORAGE_PREFIX}::space-sort-order`;
|
||||
window.localStorage.setItem(key, JSON.stringify(sortOrder));
|
||||
return {
|
||||
type: SET_SPACE_SORT_ORDER,
|
||||
payload: {
|
||||
sortOrder,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const setActiveWorkspace = (workspaceId: string | null) => {
|
||||
const key = `${LOCALSTORAGE_PREFIX}::activeWorkspaceId`;
|
||||
window.localStorage.setItem(key, JSON.stringify(workspaceId));
|
||||
@ -647,6 +671,23 @@ export function initActiveSpace() {
|
||||
return setActiveSpace(spaceId || BASE_SPACE_ID);
|
||||
}
|
||||
|
||||
export function initSpaceSortOrder() {
|
||||
let spaceSortOrder: SpaceSortOrder = 'modified-desc';
|
||||
|
||||
try {
|
||||
const spaceSortOrderKey = `${LOCALSTORAGE_PREFIX}::space-sort-order`;
|
||||
const stringifiedSpaceSortOrder = window.localStorage.getItem(spaceSortOrderKey);
|
||||
|
||||
if (stringifiedSpaceSortOrder) {
|
||||
spaceSortOrder = JSON.parse(stringifiedSpaceSortOrder);
|
||||
}
|
||||
} catch (e) {
|
||||
// Nothing here...
|
||||
}
|
||||
|
||||
return setSpaceSortOrder(spaceSortOrder);
|
||||
}
|
||||
|
||||
export function initActiveWorkspace() {
|
||||
let workspaceId: string | null = null;
|
||||
|
||||
@ -728,6 +769,7 @@ export const initActiveActivity = () => (dispatch, getState) => {
|
||||
|
||||
export const init = () => [
|
||||
initActiveSpace(),
|
||||
initSpaceSortOrder(),
|
||||
initActiveWorkspace(),
|
||||
initActiveActivity(),
|
||||
];
|
||||
|
@ -43,4 +43,4 @@ export const removeSpace = (space: Space) => dispatch => {
|
||||
trackSegmentEvent('Local Space Deleted');
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
@ -84,6 +84,11 @@ export const selectActiveSpace = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const selectSpaceSortOrder = createSelector(
|
||||
selectGlobal,
|
||||
global => global.spaceSortOrder
|
||||
);
|
||||
|
||||
export const selectAllWorkspaces = createSelector(
|
||||
selectEntitiesLists,
|
||||
entities => entities.workspaces,
|
||||
|
Loading…
Reference in New Issue
Block a user