mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
79e80330cf
Summary: On narrow screens, there wasn't enough room in the top bar to see the text you were typing into the search input. To make more room, items to the left of the search input are now hidden while search is open; the document title is hidden on narrow screens, and the undo and redo buttons are always hidden. Test Plan: Manual. Reviewers: JakubSerafin Reviewed By: JakubSerafin Subscribers: JakubSerafin Differential Revision: https://phab.getgrist.com/D4187
227 lines
8.4 KiB
TypeScript
227 lines
8.4 KiB
TypeScript
import {makeT} from 'app/client/lib/localization';
|
|
import {GristDoc} from 'app/client/components/GristDoc';
|
|
import {loadSearch} from 'app/client/lib/imports';
|
|
import type * as searchModule from 'app/client/ui2018/search';
|
|
import {AppModel, reportError} from 'app/client/models/AppModel';
|
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
|
import {workspaceName} from 'app/client/models/WorkspaceInfo';
|
|
import {AccountWidget} from 'app/client/ui/AccountWidget';
|
|
import {buildNotifyMenuButton} from 'app/client/ui/NotifyUI';
|
|
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
|
import {buildShareMenuButton} from 'app/client/ui/ShareMenu';
|
|
import {hoverTooltip} from 'app/client/ui/tooltips';
|
|
import {cssHoverCircle, cssTopBarBtn} from 'app/client/ui/TopBarCss';
|
|
import {buildLanguageMenu} from 'app/client/ui/LanguageMenu';
|
|
import {docBreadcrumbs} from 'app/client/ui2018/breadcrumbs';
|
|
import {basicButton} from 'app/client/ui2018/buttons';
|
|
import {cssHideForNarrowScreen, isNarrowScreenObs, testId, theme} from 'app/client/ui2018/cssVars';
|
|
import {IconName} from 'app/client/ui2018/IconList';
|
|
import {menuAnnotate} from 'app/client/ui2018/menus';
|
|
import {COMMENTS} from 'app/client/models/features';
|
|
import * as roles from 'app/common/roles';
|
|
import {Computed, dom, DomElementArg, makeTestId, MultiHolder, Observable, styled} from 'grainjs';
|
|
|
|
const t = makeT('TopBar');
|
|
|
|
export function createTopBarHome(appModel: AppModel) {
|
|
const isAnonymous = !appModel.currentValidUser;
|
|
|
|
return [
|
|
cssFlexSpace(),
|
|
appModel.supportGristNudge.showButton(),
|
|
(appModel.isTeamSite && roles.canEditAccess(appModel.currentOrg?.access || null) ?
|
|
[
|
|
basicButton(
|
|
t("Manage Team"),
|
|
dom.on('click', () => manageTeamUsersApp(appModel)),
|
|
testId('topbar-manage-team')
|
|
),
|
|
cssSpacer()
|
|
] :
|
|
null
|
|
),
|
|
|
|
buildLanguageMenu(appModel),
|
|
isAnonymous ? null : buildNotifyMenuButton(appModel.notifier, appModel),
|
|
dom('div', dom.create(AccountWidget, appModel)),
|
|
];
|
|
}
|
|
|
|
export function createTopBarDoc(owner: MultiHolder, appModel: AppModel, pageModel: DocPageModel, allCommands: any) {
|
|
const doc = pageModel.currentDoc;
|
|
const renameDoc = (val: string) => pageModel.renameDoc(val);
|
|
const displayNameWs = Computed.create(owner, pageModel.currentWorkspace,
|
|
(use, ws) => ws ? {...ws, name: workspaceName(appModel, ws)} : ws);
|
|
|
|
const moduleObs = Observable.create<typeof searchModule|null>(owner, null);
|
|
loadSearch().then(module => moduleObs.set(module)).catch(reportError);
|
|
|
|
// Observable to decide whether to include the searchBar into this page. It doesn't work on
|
|
// 'code' and 'acl' pages, so it's better to omit it, and let the browser's native search work.
|
|
const enabledObs = Computed.create(owner, pageModel.gristDoc, (use, gristDoc) => {
|
|
const viewId = gristDoc ? use(gristDoc.activeViewId) : null;
|
|
return viewId !== null && viewId !== 'code' && viewId !== 'acl';
|
|
});
|
|
|
|
const searchModelObs = Computed.create(owner,
|
|
moduleObs, pageModel.gristDoc, enabledObs,
|
|
(use, module, gristDoc, enabled) => {
|
|
if (!module || !gristDoc || !enabled) {
|
|
return null;
|
|
}
|
|
return module.SearchModelImpl.create(use.owner, gristDoc);
|
|
});
|
|
|
|
const isSearchOpen = Computed.create(owner, searchModelObs, (use, searchModel) => {
|
|
return Boolean(searchModel && use(searchModel.isOpen));
|
|
});
|
|
|
|
const isUndoRedoAvailable = Computed.create(owner, use => {
|
|
const gristDoc = use(pageModel.gristDoc);
|
|
if (!gristDoc) { return false; }
|
|
|
|
const undoStack = gristDoc.getUndoStack();
|
|
return !use(undoStack.isDisabled);
|
|
});
|
|
|
|
const isAnonymous = !pageModel.appModel.currentValidUser;
|
|
|
|
return [
|
|
// TODO Before gristDoc is loaded, we could show doc-name without the page. For now, we delay
|
|
// showing of breadcrumbs until gristDoc is loaded.
|
|
dom.maybe(pageModel.gristDoc, (gristDoc) =>
|
|
cssBreadcrumbContainer(
|
|
docBreadcrumbs(displayNameWs, pageModel.currentDocTitle, gristDoc.currentPageName, {
|
|
docNameSave: renameDoc,
|
|
pageNameSave: getRenamePageFn(gristDoc),
|
|
cancelRecoveryMode: getCancelRecoveryModeFn(gristDoc),
|
|
isPageNameReadOnly: (use) => use(gristDoc.isReadonly) || typeof use(gristDoc.activeViewId) !== 'number',
|
|
isDocNameReadOnly: (use) => use(gristDoc.isReadonly) || use(pageModel.isFork),
|
|
isFork: pageModel.isFork,
|
|
isBareFork: pageModel.isBareFork,
|
|
isRecoveryMode: pageModel.isRecoveryMode,
|
|
isTutorialFork: pageModel.isTutorialFork,
|
|
isFiddle: Computed.create(owner, (use) => use(pageModel.isPrefork)),
|
|
isSnapshot: pageModel.isSnapshot,
|
|
isPublic: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.public)),
|
|
isTemplate: pageModel.isTemplate,
|
|
isAnonymous,
|
|
}),
|
|
dom.hide(use => use(isSearchOpen) && use(isNarrowScreenObs())),
|
|
)
|
|
),
|
|
cssFlexSpace(),
|
|
|
|
// Don't show useless undo/redo buttons for sample docs, to leave more space for "Make copy".
|
|
dom.maybe(pageModel.undoState, (state) => [
|
|
topBarUndoBtn('Undo',
|
|
dom.on('click', () => state.isUndoDisabled.get() || allCommands.undo.run()),
|
|
dom.hide(use => use(isSearchOpen)),
|
|
hoverTooltip('Undo', {key: 'topBarBtnTooltip'}),
|
|
cssHoverCircle.cls('-disabled', use => use(state.isUndoDisabled) || !use(isUndoRedoAvailable)),
|
|
testId('undo'),
|
|
),
|
|
topBarUndoBtn('Redo',
|
|
dom.on('click', () => state.isRedoDisabled.get() || allCommands.redo.run()),
|
|
dom.hide(use => use(isSearchOpen)),
|
|
hoverTooltip('Redo', {key: 'topBarBtnTooltip'}),
|
|
cssHoverCircle.cls('-disabled', use => use(state.isRedoDisabled) || !use(isUndoRedoAvailable)),
|
|
testId('redo'),
|
|
),
|
|
cssSpacer(),
|
|
]),
|
|
dom.domComputed((use) => {
|
|
const model = use(searchModelObs);
|
|
return model && use(moduleObs)?.searchBar(model, makeTestId('test-tb-search-'));
|
|
}),
|
|
dom.maybe(use => !(use(pageModel.isTemplate) && isAnonymous), () => [
|
|
buildShareMenuButton(pageModel),
|
|
dom.maybe(use =>
|
|
(
|
|
use(pageModel.gristDoc)
|
|
&& !use(use(pageModel.gristDoc)!.isReadonly)
|
|
&& use(COMMENTS())
|
|
),
|
|
() => buildShowDiscussionButton(pageModel)),
|
|
dom.update(
|
|
buildNotifyMenuButton(appModel.notifier, appModel),
|
|
cssHideForNarrowScreen.cls(''),
|
|
),
|
|
]),
|
|
dom('div', dom.create(AccountWidget, appModel, pageModel)),
|
|
];
|
|
}
|
|
|
|
function buildShowDiscussionButton(pageModel: DocPageModel) {
|
|
return cssHoverCircle({ style: `margin: 5px; position: relative;` },
|
|
cssTopBarBtn('Chat', dom.cls('tour-share-icon')),
|
|
cssBeta('Beta'),
|
|
hoverTooltip('Comments', {key: 'topBarBtnTooltip'}),
|
|
testId('open-discussion'),
|
|
dom.on('click', () => pageModel.gristDoc.get()!.showTool('discussion'))
|
|
);
|
|
}
|
|
|
|
const cssBeta = styled(menuAnnotate, `
|
|
position: absolute;
|
|
top: 4px;
|
|
right: -9px;
|
|
font-weight: bold;
|
|
`);
|
|
|
|
// Given the GristDoc instance, returns a rename function for the current active page.
|
|
// If the current page is not able to be renamed or the new name is invalid, the function is a noop.
|
|
function getRenamePageFn(gristDoc: GristDoc): (val: string) => Promise<void> {
|
|
return async (val: string) => {
|
|
const views = gristDoc.docModel.views;
|
|
const viewId = gristDoc.activeViewId.get();
|
|
if (typeof viewId === 'number' && val.length > 0) {
|
|
const name = views.rowModels[viewId].name;
|
|
await name.saveOnly(val);
|
|
}
|
|
};
|
|
}
|
|
|
|
function getCancelRecoveryModeFn(gristDoc: GristDoc): () => Promise<void> {
|
|
return async () => {
|
|
await gristDoc.app.topAppModel.api.getDocAPI(gristDoc.docPageModel.currentDocId.get()!)
|
|
.recover(false);
|
|
};
|
|
}
|
|
|
|
function topBarUndoBtn(iconName: IconName, ...domArgs: DomElementArg[]): Element {
|
|
return cssHoverCircle(
|
|
cssTopBarUndoBtn(iconName),
|
|
...domArgs
|
|
);
|
|
}
|
|
|
|
const cssTopBarUndoBtn = styled(cssTopBarBtn, `
|
|
background-color: ${theme.topBarButtonSecondaryFg};
|
|
|
|
.${cssHoverCircle.className}:hover & {
|
|
background-color: ${theme.topBarButtonPrimaryFg};
|
|
}
|
|
|
|
.${cssHoverCircle.className}-disabled:hover & {
|
|
background-color: ${theme.topBarButtonDisabledFg};
|
|
cursor: default;
|
|
}
|
|
`);
|
|
|
|
const cssBreadcrumbContainer = styled('div', `
|
|
padding: 7px;
|
|
flex: 1 1 auto;
|
|
min-width: 24px;
|
|
overflow: hidden;
|
|
`);
|
|
|
|
const cssFlexSpace = styled('div', `
|
|
flex: 1 1 0px;
|
|
`);
|
|
|
|
const cssSpacer = styled('div', `
|
|
max-width: 10px;
|
|
flex: auto;
|
|
`);
|