Card order by date modified (#3170)

Co-authored-by: Dimitri Mitropoulos <dimitrimitropoulos@gmail.com>
Co-authored-by: Opender Singh <opender.singh@konghq.com>
This commit is contained in:
Mike Ellan 2021-03-09 23:33:38 -05:00 committed by GitHub
parent f1539e2b40
commit 84b9b84fdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 98 additions and 21 deletions

View File

@ -1,6 +1,6 @@
import * as misc from '../misc'; import * as misc from '../misc';
import { globalBeforeEach } from '../../__jest__/before-each'; import { globalBeforeEach } from '../../__jest__/before-each';
import { diffPatchObj, pluralize, snapNumberToLimits } from '../misc'; import { diffPatchObj, isNotNullOrUndefined, pluralize, snapNumberToLimits } from '../misc';
describe('hasAuthHeader()', () => { describe('hasAuthHeader()', () => {
beforeEach(globalBeforeEach); beforeEach(globalBeforeEach);
@ -271,3 +271,14 @@ describe('snapNumberToLimits()', () => {
expect(snapNumberToLimits(5, NaN, 3)).toBe(3); expect(snapNumberToLimits(5, NaN, 3)).toBe(3);
}); });
}); });
describe('isNotNullOrUndefined', () => {
it('should return correctly', () => {
expect(isNotNullOrUndefined(0)).toBe(true);
expect(isNotNullOrUndefined('')).toBe(true);
expect(isNotNullOrUndefined(false)).toBe(true);
expect(isNotNullOrUndefined(null)).toBe(false);
expect(isNotNullOrUndefined(undefined)).toBe(false);
});
});

View File

@ -1,4 +1,9 @@
import { metaSortKeySort, sortMethodMap } from '../sorting'; import {
ascendingNumberSort,
descendingNumberSort,
metaSortKeySort,
sortMethodMap,
} from '../sorting';
import { request, requestGroup, grpcRequest } from '../../models'; import { request, requestGroup, grpcRequest } from '../../models';
import { import {
METHOD_DELETE, METHOD_DELETE,
@ -340,4 +345,16 @@ describe('Sorting methods', () => {
expect(metaSortKeySort({ metaSortKey: 1, _id: 2 }, { metaSortKey: 1, _id: 1 })).toBe(-1); expect(metaSortKeySort({ metaSortKey: 1, _id: 2 }, { metaSortKey: 1, _id: 1 })).toBe(-1);
expect(metaSortKeySort({ metaSortKey: 1, _id: 1 }, { metaSortKey: 1, _id: 2 })).toBe(1); expect(metaSortKeySort({ metaSortKey: 1, _id: 1 }, { metaSortKey: 1, _id: 2 })).toBe(1);
}); });
it('sorts by number', () => {
expect(ascendingNumberSort(1, 2)).toBe(-1);
expect(ascendingNumberSort(-2, 1)).toBe(-1);
expect(ascendingNumberSort(2, 1)).toBe(1);
expect(ascendingNumberSort(1, -2)).toBe(1);
expect(descendingNumberSort(1, 2)).toBe(1);
expect(descendingNumberSort(-2, 1)).toBe(1);
expect(descendingNumberSort(2, 1)).toBe(-1);
expect(descendingNumberSort(1, -2)).toBe(-1);
});
}); });

View File

@ -453,3 +453,7 @@ export function snapNumberToLimits(value: number, min?: number, max?: number): n
return value; return value;
} }
export function isNotNullOrUndefined(obj: any): boolean {
return obj !== null && obj !== undefined;
}

View File

@ -97,6 +97,14 @@ export const metaSortKeySort: SortFunction = (a, b) => {
return a.metaSortKey < b.metaSortKey ? -1 : 1; return a.metaSortKey < b.metaSortKey ? -1 : 1;
}; };
export const ascendingNumberSort = (a: number, b: number): number => {
return a < b ? -1 : 1;
};
export const descendingNumberSort = (a: number, b: number): number => {
return ascendingNumberSort(b, a);
};
export const sortMethodMap: { [SortOrder]: SortFunction } = { export const sortMethodMap: { [SortOrder]: SortFunction } = {
[SORT_NAME_ASC]: ascendingNameSort, [SORT_NAME_ASC]: ascendingNameSort,
[SORT_NAME_DESC]: descendingNameSort, [SORT_NAME_DESC]: descendingNameSort,

View File

@ -19,6 +19,7 @@ import HelpTooltip from '../help-tooltip';
import Link from '../base/link'; import Link from '../base/link';
import { trackEvent } from '../../../common/analytics'; import { trackEvent } from '../../../common/analytics';
import { docsGitSync } from '../../../common/documentation'; import { docsGitSync } from '../../../common/documentation';
import { isNotNullOrUndefined } from '../../../common/misc';
type Props = {| type Props = {|
handleInitializeEntities: () => Promise<void>, handleInitializeEntities: () => Promise<void>,
@ -68,11 +69,20 @@ class GitSyncDropdown extends React.PureComponent<Props, State> {
// Clear cached items and return if no state // Clear cached items and return if no state
if (!vcs.isInitialized() || !workspaceMeta.gitRepositoryId) { if (!vcs.isInitialized() || !workspaceMeta.gitRepositoryId) {
await models.workspaceMeta.updateByParentId(workspace._id, { // Don't update unnecessarily
cachedGitRepositoryBranch: null, const needsUpdate = [
cachedGitLastAuthor: null, workspaceMeta.cachedGitRepositoryBranch,
cachedGitLastCommitTime: null, workspaceMeta.cachedGitLastAuthor,
}); workspaceMeta.cachedGitLastCommitTime,
].some(isNotNullOrUndefined);
if (needsUpdate) {
await models.workspaceMeta.updateByParentId(workspace._id, {
cachedGitRepositoryBranch: null,
cachedGitLastAuthor: null,
cachedGitLastCommitTime: null,
});
}
return; return;
} }

View File

@ -36,7 +36,7 @@ import TimeFromNow from './time-from-now';
import Highlight from './base/highlight'; import Highlight from './base/highlight';
import type { GlobalActivity } from '../../common/constants'; import type { GlobalActivity } from '../../common/constants';
import { fuzzyMatchAll, pluralize } from '../../common/misc'; import { fuzzyMatchAll, isNotNullOrUndefined, pluralize } from '../../common/misc';
import type { import type {
HandleImportClipboardCallback, HandleImportClipboardCallback,
HandleImportFileCallback, HandleImportFileCallback,
@ -60,6 +60,8 @@ import RemoteWorkspacesDropdown from './dropdowns/remote-workspaces-dropdown';
import SettingsButton from './buttons/settings-button'; import SettingsButton from './buttons/settings-button';
import AccountDropdown from './dropdowns/account-dropdown'; import AccountDropdown from './dropdowns/account-dropdown';
import { strings } from '../../common/strings'; import { strings } from '../../common/strings';
import { WorkspaceScopeKeys } from '../../models/workspace';
import { descendingNumberSort } from '../../common/sorting';
type Props = {| type Props = {|
wrapperProps: WrapperProps, wrapperProps: WrapperProps,
@ -316,7 +318,6 @@ class WrapperHome extends React.PureComponent<Props, State> {
const { activeActivity } = await models.workspaceMeta.getOrCreateByParentId(id); const { activeActivity } = await models.workspaceMeta.getOrCreateByParentId(id);
if (!activeActivity || !isWorkspaceActivity(activeActivity)) { if (!activeActivity || !isWorkspaceActivity(activeActivity)) {
// or migration or onboarding
handleSetActiveActivity(defaultActivity); handleSetActiveActivity(defaultActivity);
} else { } else {
handleSetActiveActivity(activeActivity); handleSetActiveActivity(activeActivity);
@ -324,7 +325,7 @@ class WrapperHome extends React.PureComponent<Props, State> {
handleSetActiveWorkspace(id); handleSetActiveWorkspace(id);
} }
renderCard(w: Workspace) { renderCard(workspace: Workspace): { card: React.Node, lastModifiedTimestamp: number } {
const { const {
apiSpecs, apiSpecs,
handleSetActiveWorkspace, handleSetActiveWorkspace,
@ -334,7 +335,7 @@ class WrapperHome extends React.PureComponent<Props, State> {
const { filter } = this.state; const { filter } = this.state;
const apiSpec = apiSpecs.find(s => s.parentId === w._id); const apiSpec = apiSpecs.find(s => s.parentId === workspace._id);
let spec = null; let spec = null;
let specFormat = null; let specFormat = null;
@ -350,18 +351,25 @@ class WrapperHome extends React.PureComponent<Props, State> {
} }
// Get cached branch from WorkspaceMeta // Get cached branch from WorkspaceMeta
const workspaceMeta = workspaceMetas.find(wm => wm.parentId === w._id); const workspaceMeta = workspaceMetas.find(wm => wm.parentId === workspace._id);
const lastActiveBranch = workspaceMeta ? workspaceMeta.cachedGitRepositoryBranch : null; const lastActiveBranch = workspaceMeta ? workspaceMeta.cachedGitRepositoryBranch : null;
const lastCommitAuthor = workspaceMeta ? workspaceMeta.cachedGitLastAuthor : null; const lastCommitAuthor = workspaceMeta ? workspaceMeta.cachedGitLastAuthor : null;
const lastCommitTime = workspaceMeta ? workspaceMeta.cachedGitLastCommitTime : null; const lastCommitTime = workspaceMeta ? workspaceMeta.cachedGitLastCommitTime : null;
// WorkspaceMeta is a good proxy for last modified time // WorkspaceMeta is a good proxy for last modified time
const workspaceModified = workspaceMeta ? workspaceMeta.modified : w.modified; const workspaceModified = workspaceMeta ? workspaceMeta.modified : workspace.modified;
const modifiedLocally = apiSpec ? apiSpec.modified : workspaceModified; const modifiedLocally =
apiSpec && workspace.scope === WorkspaceScopeKeys.designer
? apiSpec.modified
: workspaceModified;
let log = <TimeFromNow timestamp={modifiedLocally} />; let log = <TimeFromNow timestamp={modifiedLocally} />;
let branch = lastActiveBranch; let branch = lastActiveBranch;
if (w.scope === 'designer' && lastCommitTime && apiSpec?.modified > lastCommitTime) { if (
workspace.scope === WorkspaceScopeKeys.designer &&
lastCommitTime &&
apiSpec?.modified > lastCommitTime
) {
// Show locally unsaved changes for spec // Show locally unsaved changes for spec
// NOTE: this doesn't work for non-spec workspaces // NOTE: this doesn't work for non-spec workspaces
branch = lastActiveBranch + '*'; branch = lastActiveBranch + '*';
@ -383,7 +391,7 @@ class WrapperHome extends React.PureComponent<Props, State> {
const docMenu = ( const docMenu = (
<DocumentCardDropdown <DocumentCardDropdown
apiSpec={apiSpec} apiSpec={apiSpec}
workspace={w} workspace={workspace}
handleSetActiveWorkspace={handleSetActiveWorkspace} handleSetActiveWorkspace={handleSetActiveWorkspace}
isLastWorkspace={workspaces.length === 1}> isLastWorkspace={workspaces.length === 1}>
<SvgIcon icon="ellipsis" /> <SvgIcon icon="ellipsis" />
@ -394,9 +402,9 @@ class WrapperHome extends React.PureComponent<Props, State> {
let format: string = ''; let format: string = '';
let labelIcon = <i className="fa fa-bars" />; let labelIcon = <i className="fa fa-bars" />;
let defaultActivity = ACTIVITY_DEBUG; let defaultActivity = ACTIVITY_DEBUG;
let title = w.name; let title = workspace.name;
if (w.scope === 'designer') { if (workspace.scope === WorkspaceScopeKeys.designer) {
label = 'Document'; label = 'Document';
labelIcon = <i className="fa fa-file-o" />; labelIcon = <i className="fa fa-file-o" />;
if (specFormat === 'openapi') { if (specFormat === 'openapi') {
@ -421,7 +429,17 @@ class WrapperHome extends React.PureComponent<Props, State> {
return null; return null;
} }
return ( const lastModifiedFrom = [
workspace?.modified,
workspaceMeta?.modified,
apiSpec?.modified,
workspaceMeta?.cachedGitLastCommitTime,
];
const lastModifiedTimestamp = lastModifiedFrom
.filter(isNotNullOrUndefined)
.sort(descendingNumberSort)[0];
const card = (
<Card <Card
key={apiSpec._id} key={apiSpec._id}
docBranch={branch && <Highlight search={filter} text={branch} />} docBranch={branch && <Highlight search={filter} text={branch} />}
@ -438,9 +456,14 @@ class WrapperHome extends React.PureComponent<Props, State> {
docLog={log} docLog={log}
docMenu={docMenu} docMenu={docMenu}
docFormat={format} docFormat={format}
onClick={() => this._handleClickCard(w._id, defaultActivity)} onClick={() => this._handleClickCard(workspace._id, defaultActivity)}
/> />
); );
return {
card,
lastModifiedTimestamp,
};
} }
renderCreateMenu() { renderCreateMenu() {
@ -508,7 +531,11 @@ class WrapperHome extends React.PureComponent<Props, State> {
const { filter } = this.state; const { filter } = this.state;
// Render each card, removing all the ones that don't match the filter // Render each card, removing all the ones that don't match the filter
const cards = workspaces.map(this.renderCard).filter(c => c !== null); const cards = workspaces
.map(this.renderCard)
.filter(isNotNullOrUndefined)
.sort((a, b) => descendingNumberSort(a.lastModifiedTimestamp, b.lastModifiedTimestamp))
.map(c => c.card);
const countLabel = cards.length > 1 ? pluralize(strings.document) : strings.document; const countLabel = cards.length > 1 ? pluralize(strings.document) : strings.document;