mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Improve snapshot listing, and add compare snapshot links
Summary: - When viewing a snapshot, list all snapshots for a doc, highlighting the current one. - Include links in per-snapshot menu to compare-to-current and compare-to-previous. - Compare links include "beta" tags. - Set order of comparison to have older on the left, and newer on the right. Test Plan: Moved out DocHistory test from Snapshots, and added some test cases. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2681
This commit is contained in:
parent
8f023a6446
commit
92224084e4
@ -7,7 +7,8 @@ import {buildConfigContainer} from 'app/client/ui/RightPanel';
|
||||
import {buttonSelect} from 'app/client/ui2018/buttonSelect';
|
||||
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {menu, menuItemLink} from 'app/client/ui2018/menus';
|
||||
import {menu, menuAnnotate, menuItemLink} from 'app/client/ui2018/menus';
|
||||
import {buildUrlId, parseUrlId} from 'app/common/gristUrls';
|
||||
import {StringUnion} from 'app/common/StringUnion';
|
||||
import {DocSnapshot} from 'app/common/UserAPI';
|
||||
import {Disposable, dom, IDomComponent, MultiHolder, Observable, styled} from 'grainjs';
|
||||
@ -16,7 +17,7 @@ import * as moment from 'moment';
|
||||
const DocHistorySubTab = StringUnion("activity", "snapshots");
|
||||
|
||||
export class DocHistory extends Disposable implements IDomComponent {
|
||||
private _subTab = createSessionObs(this, "docHistorySubTab", "activity", DocHistorySubTab.guard);
|
||||
private _subTab = createSessionObs(this, "docHistorySubTab", "snapshots", DocHistorySubTab.guard);
|
||||
|
||||
constructor(private _docPageModel: DocPageModel, private _actionLog: IDomComponent) {
|
||||
super();
|
||||
@ -46,35 +47,58 @@ export class DocHistory extends Disposable implements IDomComponent {
|
||||
const doc = this._docPageModel.currentDoc.get();
|
||||
if (!doc) { return null; }
|
||||
|
||||
// If this is a snapshot already, say so to the user. We won't find any list of snapshots of it (though we could
|
||||
// change that to list snapshots of the trunk, and highlight this one among them).
|
||||
if (doc.idParts.snapshotId) {
|
||||
return cssSnapshot(cssSnapshotCard('You are looking at a backup snapshot.'));
|
||||
// origUrlId is the snapshot-less URL, which we use to fetch snapshot history, and for
|
||||
// snapshot comparisons.
|
||||
const origUrlId = buildUrlId({...doc.idParts, snapshotId: undefined});
|
||||
|
||||
// If comparing one snapshot to another, get the other ID, so that we can highlight it too.
|
||||
const compareUrlId = urlState().state.get().params?.compare;
|
||||
const compareSnapshotId = compareUrlId && parseUrlId(compareUrlId).snapshotId;
|
||||
|
||||
// Helper to set a link to open a snapshot, optionally comparing it with a docId.
|
||||
// We include urlState().state to preserve the currently selected page.
|
||||
function setLink(snapshot: DocSnapshot, compareDocId?: string) {
|
||||
return dom.attr('href', (use) => urlState().makeUrl({
|
||||
...use(urlState().state), doc: snapshot.docId, params: {compare: compareDocId}}));
|
||||
}
|
||||
|
||||
const snapshots = Observable.create<DocSnapshot[]>(owner, []);
|
||||
const userApi = this._docPageModel.appModel.api;
|
||||
const docApi = userApi.getDocAPI(doc.id);
|
||||
const docApi = userApi.getDocAPI(origUrlId);
|
||||
docApi.getSnapshots().then(result => snapshots.set(result.snapshots)).catch(reportError);
|
||||
return dom('div',
|
||||
dom.forEach(snapshots, (snapshot) => {
|
||||
// Note that most recent snapshots are first.
|
||||
dom.domComputed(snapshots, (snapshotList) => snapshotList.map((snapshot, index) => {
|
||||
const modified = moment(snapshot.lastModified);
|
||||
const prevSnapshot = snapshotList[index + 1] || null;
|
||||
return cssSnapshot(
|
||||
cssSnapshotTime(getTimeFromNow(snapshot.lastModified)),
|
||||
cssSnapshotCard(
|
||||
cssSnapshotCard.cls('-current', Boolean(
|
||||
snapshot.snapshotId === doc.idParts.snapshotId ||
|
||||
(compareSnapshotId && snapshot.snapshotId === compareSnapshotId)
|
||||
)),
|
||||
dom('div',
|
||||
cssDatePart(modified.format('ddd ll')), ' ',
|
||||
cssDatePart(modified.format('LT'))
|
||||
),
|
||||
cssMenuDots(icon('Dots'),
|
||||
menu(() => [menuItemLink(urlState().setLinkUrl({doc: snapshot.docId}), 'Open Snapshot')],
|
||||
{placement: 'bottom-end', parentSelectorToMark: '.' + cssSnapshotCard.className}),
|
||||
menu(() => [
|
||||
menuItemLink(setLink(snapshot), 'Open Snapshot'),
|
||||
menuItemLink(setLink(snapshot, origUrlId), 'Compare to Current',
|
||||
menuAnnotate('Beta')),
|
||||
prevSnapshot && menuItemLink(setLink(prevSnapshot, snapshot.docId), 'Compare to Previous',
|
||||
menuAnnotate('Beta')),
|
||||
],
|
||||
{placement: 'bottom-end', parentSelectorToMark: '.' + cssSnapshotCard.className}
|
||||
),
|
||||
testId('doc-history-snapshot-menu'),
|
||||
),
|
||||
testId('doc-history-card'),
|
||||
),
|
||||
testId('doc-history-snapshot'),
|
||||
);
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -102,6 +126,13 @@ const cssSnapshotCard = styled('div', `
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--icon-color: ${colors.slate};
|
||||
|
||||
&-current {
|
||||
background-color: ${colors.dark};
|
||||
color: ${colors.light};
|
||||
--icon-color: ${colors.light};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssDatePart = styled('span', `
|
||||
@ -117,9 +148,7 @@ const cssMenuDots = styled('div', `
|
||||
line-height: 0px;
|
||||
border-radius: 3px;
|
||||
cursor: default;
|
||||
--icon-color: ${colors.slate};
|
||||
&:hover, &.weasel-popup-open {
|
||||
background-color: ${colors.mediumGrey};
|
||||
--icon-color: ${colors.slate};
|
||||
}
|
||||
`);
|
||||
|
@ -7,12 +7,12 @@ import {cssHoverCircle, cssTopBarBtn} from 'app/client/ui/TopBarCss';
|
||||
import {primaryButton} from 'app/client/ui2018/buttons';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {menu, menuDivider, menuIcon, menuItem, menuItemLink, menuText} from 'app/client/ui2018/menus';
|
||||
import {menu, menuAnnotate, menuDivider, menuIcon, menuItem, menuItemLink, menuText} from 'app/client/ui2018/menus';
|
||||
import {buildUrlId, parseUrlId} from 'app/common/gristUrls';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {Document} from 'app/common/UserAPI';
|
||||
import {dom, DomContents, styled} from 'grainjs';
|
||||
import {cssMenuItem, MenuCreateFunc} from 'popweasel';
|
||||
import {MenuCreateFunc} from 'popweasel';
|
||||
|
||||
function buildOriginalUrlId(urlId: string, isSnapshot: boolean): string {
|
||||
const parts = parseUrlId(urlId);
|
||||
@ -136,9 +136,20 @@ function menuOriginal(doc: Document, appModel: AppModel, isSnapshot: boolean) {
|
||||
const termToUse = isSnapshot ? "Current Version" : "Original";
|
||||
const origUrlId = buildOriginalUrlId(doc.id, isSnapshot);
|
||||
const originalUrl = urlState().makeUrl({doc: origUrlId});
|
||||
const originalUrlComparison = urlState().makeUrl({
|
||||
doc: origUrlId, params: { compare: doc.id }
|
||||
});
|
||||
|
||||
// When comparing forks, show changes from the original to the fork. When comparing a snapshot,
|
||||
// show changes from the snapshot to the original, which seems more natural. The per-snapshot
|
||||
// comparison links in DocHistory use the same order.
|
||||
const [leftDocId, rightDocId] = isSnapshot ? [doc.id, origUrlId] : [origUrlId, doc.id];
|
||||
|
||||
// Preserve the current state in order to stay on the selected page. TODO: Should auto-switch to
|
||||
// first page when the requested page is not in the document.
|
||||
const compareHref = dom.attr('href', (use) => urlState().makeUrl({
|
||||
...use(urlState().state), doc: leftDocId, params: {compare: rightDocId}}));
|
||||
|
||||
const compareUrlId = urlState().state.get().params?.compare;
|
||||
const comparingSnapshots: boolean = isSnapshot && Boolean(compareUrlId && parseUrlId(compareUrlId).snapshotId);
|
||||
|
||||
function replaceOriginal() {
|
||||
const user = appModel.currentValidUser;
|
||||
replaceTrunkWithFork(user, doc, appModel, origUrlId).catch(reportError);
|
||||
@ -151,11 +162,13 @@ function menuOriginal(doc: Document, appModel: AppModel, isSnapshot: boolean) {
|
||||
)
|
||||
),
|
||||
menuItem(replaceOriginal, `Replace ${termToUse}...`,
|
||||
dom.cls('disabled', !roles.canEdit(doc.trunkAccess || null)),
|
||||
// Disable if original is not writable, and also when comparing snapshots (since it's
|
||||
// unclear which of the versions to use).
|
||||
dom.cls('disabled', !roles.canEdit(doc.trunkAccess || null) || comparingSnapshots),
|
||||
testId('replace-original'),
|
||||
),
|
||||
menuItemLink({href: originalUrlComparison, target: '_blank'}, `Compare to ${termToUse}`,
|
||||
cssAnnotateMenuItem('Beta'),
|
||||
menuItemLink(compareHref, {target: '_blank'}, `Compare to ${termToUse}`,
|
||||
menuAnnotate('Beta'),
|
||||
testId('compare-original'),
|
||||
),
|
||||
];
|
||||
@ -298,16 +311,3 @@ const cssMenuIcon = styled(icon, `
|
||||
display: block;
|
||||
`);
|
||||
|
||||
const cssAnnotateMenuItem = styled('span', `
|
||||
color: ${colors.lightGreen};
|
||||
text-transform: uppercase;
|
||||
font-size: 8px;
|
||||
vertical-align: super;
|
||||
margin-top: -4px;
|
||||
margin-left: 4px;
|
||||
font-weight: bold;
|
||||
|
||||
.${cssMenuItem.className}-sel > & {
|
||||
color: white;
|
||||
}
|
||||
`);
|
||||
|
@ -255,6 +255,10 @@ export function menuItemCmd(cmd: Command, label: string, ...args: DomElementArg[
|
||||
);
|
||||
}
|
||||
|
||||
export function menuAnnotate(text: string) {
|
||||
return cssAnnotateMenuItem('Beta');
|
||||
}
|
||||
|
||||
export const menuDivider = styled(weasel.cssMenuDivider, `
|
||||
margin: 8px 0;
|
||||
`);
|
||||
@ -374,3 +378,17 @@ const cssCmdKey = styled('span', `
|
||||
color: ${colors.slate};
|
||||
margin-right: -12px;
|
||||
`);
|
||||
|
||||
const cssAnnotateMenuItem = styled('span', `
|
||||
color: ${colors.lightGreen};
|
||||
text-transform: uppercase;
|
||||
font-size: 8px;
|
||||
vertical-align: super;
|
||||
margin-top: -4px;
|
||||
margin-left: 4px;
|
||||
font-weight: bold;
|
||||
|
||||
.${weasel.cssMenuItem.className}-sel > & {
|
||||
color: white;
|
||||
}
|
||||
`);
|
||||
|
Loading…
Reference in New Issue
Block a user