mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
67afd74817
Summary: When access rules are in effect on a document, non-owners currently don't have access to snapshots. Previously when the document history tab is opened in this situation, an error toast would appear, along with a small message that was hard to see in dark mode. This change removes the toast and improves the message somewhat. Test Plan: updated test Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4218
188 lines
6.5 KiB
TypeScript
188 lines
6.5 KiB
TypeScript
import {makeT} from 'app/client/lib/localization';
|
|
import {createSessionObs} from 'app/client/lib/sessionObs';
|
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
|
import {reportError} from 'app/client/models/errors';
|
|
import {urlState} from 'app/client/models/gristUrlState';
|
|
import {getTimeFromNow} from 'app/client/models/HomeModel';
|
|
import {buildConfigContainer} from 'app/client/ui/RightPanel';
|
|
import {buttonSelect} from 'app/client/ui2018/buttonSelect';
|
|
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
|
import {icon} from 'app/client/ui2018/icons';
|
|
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';
|
|
import moment from 'moment';
|
|
|
|
const t = makeT('DocHistory');
|
|
|
|
const DocHistorySubTab = StringUnion("activity", "snapshots");
|
|
|
|
export class DocHistory extends Disposable implements IDomComponent {
|
|
private _subTab = createSessionObs(this, "docHistorySubTab", "snapshots", DocHistorySubTab.guard);
|
|
|
|
constructor(private _docPageModel: DocPageModel, private _actionLog: IDomComponent) {
|
|
super();
|
|
}
|
|
|
|
public buildDom() {
|
|
const tabs = [
|
|
{value: 'activity', label: t("Activity")},
|
|
{value: 'snapshots', label: t("Snapshots")},
|
|
];
|
|
return [
|
|
cssSubTabs(
|
|
buttonSelect(this._subTab, tabs, {}, testId('doc-history-tabs')),
|
|
),
|
|
dom.domComputed(this._subTab, (subTab) =>
|
|
buildConfigContainer(
|
|
subTab === 'activity' ? this._actionLog.buildDom() :
|
|
subTab === 'snapshots' ? dom.create(this._buildSnapshots.bind(this)) :
|
|
null
|
|
)
|
|
),
|
|
];
|
|
}
|
|
|
|
private _buildSnapshots(owner: MultiHolder) {
|
|
// Fetch snapshots, and render.
|
|
const doc = this._docPageModel.currentDoc.get();
|
|
if (!doc) { return null; }
|
|
|
|
// 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: (compareDocId ? {compare: compareDocId} : {})
|
|
}));
|
|
}
|
|
|
|
const snapshots = Observable.create<DocSnapshot[]>(owner, []);
|
|
const snapshotsDenied = Observable.create<boolean>(owner, false);
|
|
const userApi = this._docPageModel.appModel.api;
|
|
const docApi = userApi.getDocAPI(origUrlId);
|
|
docApi.getSnapshots().then(result =>
|
|
snapshots.isDisposed() || snapshots.set(result.snapshots)).catch(err => {
|
|
snapshotsDenied.set(true);
|
|
// "cannot confirm access" is what we expect if snapshots
|
|
// are denied because of access rules.
|
|
if (!String(err).match(/cannot confirm access/)) {
|
|
reportError(err);
|
|
}
|
|
});
|
|
return dom(
|
|
'div',
|
|
{tabIndex: '-1'}, // Voodoo needed to allow copying text.
|
|
dom.maybe(snapshotsDenied, () => cssSnapshotDenied(
|
|
dom(
|
|
'p',
|
|
t("Snapshots are unavailable."),
|
|
),
|
|
dom(
|
|
'p',
|
|
t("Only owners have access to snapshots for documents with access rules."),
|
|
),
|
|
testId('doc-history-error'))),
|
|
// 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(setLink(snapshot), t("Open Snapshot")),
|
|
menuItemLink(setLink(snapshot, origUrlId), t("Compare to Current"),
|
|
menuAnnotate(t("Beta"))),
|
|
prevSnapshot && menuItemLink(setLink(prevSnapshot, snapshot.docId), t("Compare to Previous"),
|
|
menuAnnotate(t("Beta"))),
|
|
],
|
|
{placement: 'bottom-end', parentSelectorToMark: '.' + cssSnapshotCard.className}
|
|
),
|
|
testId('doc-history-snapshot-menu'),
|
|
),
|
|
testId('doc-history-card'),
|
|
),
|
|
testId('doc-history-snapshot'),
|
|
);
|
|
})),
|
|
);
|
|
}
|
|
}
|
|
|
|
const cssSubTabs = styled('div', `
|
|
padding: 16px;
|
|
border-bottom: 1px solid ${theme.pagePanelsBorder};
|
|
`);
|
|
|
|
const cssSnapshot = styled('div', `
|
|
margin: 8px 16px;
|
|
`);
|
|
|
|
const cssSnapshotDenied = styled('div', `
|
|
margin: 8px 16px;
|
|
text-align: center;
|
|
color: ${theme.text};
|
|
`);
|
|
|
|
const cssSnapshotTime = styled('div', `
|
|
text-align: right;
|
|
color: ${theme.lightText};
|
|
font-size: ${vars.smallFontSize};
|
|
`);
|
|
|
|
const cssSnapshotCard = styled('div', `
|
|
border: 1px solid ${theme.documentHistorySnapshotBorder};
|
|
padding: 8px;
|
|
color: ${theme.documentHistorySnapshotFg};
|
|
background: ${theme.documentHistorySnapshotBg};
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
--icon-color: ${theme.controlSecondaryFg};
|
|
|
|
&-current {
|
|
background-color: ${theme.documentHistorySnapshotSelectedBg};
|
|
color: ${theme.documentHistorySnapshotSelectedFg};
|
|
--icon-color: ${theme.documentHistorySnapshotSelectedFg};
|
|
}
|
|
`);
|
|
|
|
const cssDatePart = styled('span', `
|
|
display: inline-block;
|
|
`);
|
|
|
|
const cssMenuDots = styled('div', `
|
|
flex: none;
|
|
margin: 0 4px 0 auto;
|
|
height: 24px;
|
|
width: 24px;
|
|
padding: 4px;
|
|
line-height: 0px;
|
|
border-radius: 3px;
|
|
cursor: default;
|
|
&:hover, &.weasel-popup-open {
|
|
background-color: ${theme.hover};
|
|
}
|
|
`);
|