2022-10-28 16:11:08 +00:00
|
|
|
import {makeT} from 'app/client/lib/localization';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {GristDoc} from 'app/client/components/GristDoc';
|
|
|
|
import {loadSearch} from 'app/client/lib/imports';
|
2022-11-14 14:34:50 +00:00
|
|
|
import type * as searchModule from 'app/client/ui2018/search';
|
2020-10-02 15:10:00 +00:00
|
|
|
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';
|
2022-06-03 14:58:07 +00:00
|
|
|
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {buildShareMenuButton} from 'app/client/ui/ShareMenu';
|
2022-10-19 23:06:05 +00:00
|
|
|
import {hoverTooltip} from 'app/client/ui/tooltips';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {cssHoverCircle, cssTopBarBtn} from 'app/client/ui/TopBarCss';
|
2023-01-24 13:13:18 +00:00
|
|
|
import {buildLanguageMenu} from 'app/client/ui/LanguageMenu';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {docBreadcrumbs} from 'app/client/ui2018/breadcrumbs';
|
2022-06-03 14:58:07 +00:00
|
|
|
import {basicButton} from 'app/client/ui2018/buttons';
|
2024-02-08 05:10:01 +00:00
|
|
|
import {cssHideForNarrowScreen, isNarrowScreenObs, testId, theme} from 'app/client/ui2018/cssVars';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {IconName} from 'app/client/ui2018/IconList';
|
2022-10-17 09:47:16 +00:00
|
|
|
import {menuAnnotate} from 'app/client/ui2018/menus';
|
|
|
|
import {COMMENTS} from 'app/client/models/features';
|
2022-06-03 14:58:07 +00:00
|
|
|
import * as roles from 'app/common/roles';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {Computed, dom, DomElementArg, makeTestId, MultiHolder, Observable, styled} from 'grainjs';
|
|
|
|
|
2022-10-28 16:11:08 +00:00
|
|
|
const t = makeT('TopBar');
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
export function createTopBarHome(appModel: AppModel) {
|
2023-07-26 22:31:02 +00:00
|
|
|
const isAnonymous = !appModel.currentValidUser;
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
return [
|
|
|
|
cssFlexSpace(),
|
2023-07-04 21:21:34 +00:00
|
|
|
appModel.supportGristNudge.showButton(),
|
2022-06-03 14:58:07 +00:00
|
|
|
(appModel.isTeamSite && roles.canEditAccess(appModel.currentOrg?.access || null) ?
|
|
|
|
[
|
|
|
|
basicButton(
|
2022-12-06 13:57:29 +00:00
|
|
|
t("Manage Team"),
|
2022-06-03 14:58:07 +00:00
|
|
|
dom.on('click', () => manageTeamUsersApp(appModel)),
|
|
|
|
testId('topbar-manage-team')
|
|
|
|
),
|
|
|
|
cssSpacer()
|
|
|
|
] :
|
|
|
|
null
|
|
|
|
),
|
|
|
|
|
2023-01-24 13:13:18 +00:00
|
|
|
buildLanguageMenu(appModel),
|
2023-07-26 22:31:02 +00:00
|
|
|
isAnonymous ? null : buildNotifyMenuButton(appModel.notifier, appModel),
|
2020-10-02 15:10:00 +00:00
|
|
|
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);
|
|
|
|
|
2022-11-14 14:34:50 +00:00
|
|
|
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);
|
|
|
|
});
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2024-02-08 05:10:01 +00:00
|
|
|
const isSearchOpen = Computed.create(owner, searchModelObs, (use, searchModel) => {
|
|
|
|
return Boolean(searchModel && use(searchModel.isOpen));
|
|
|
|
});
|
|
|
|
|
2023-07-19 02:27:53 +00:00
|
|
|
const isUndoRedoAvailable = Computed.create(owner, use => {
|
|
|
|
const gristDoc = use(pageModel.gristDoc);
|
|
|
|
if (!gristDoc) { return false; }
|
|
|
|
|
|
|
|
const undoStack = gristDoc.getUndoStack();
|
|
|
|
return !use(undoStack.isDisabled);
|
|
|
|
});
|
|
|
|
|
2023-07-26 22:31:02 +00:00
|
|
|
const isAnonymous = !pageModel.appModel.currentValidUser;
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
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),
|
2020-12-14 17:42:09 +00:00
|
|
|
cancelRecoveryMode: getCancelRecoveryModeFn(gristDoc),
|
2020-10-02 15:10:00 +00:00
|
|
|
isPageNameReadOnly: (use) => use(gristDoc.isReadonly) || typeof use(gristDoc.activeViewId) !== 'number',
|
|
|
|
isDocNameReadOnly: (use) => use(gristDoc.isReadonly) || use(pageModel.isFork),
|
|
|
|
isFork: pageModel.isFork,
|
2021-08-05 21:22:50 +00:00
|
|
|
isBareFork: pageModel.isBareFork,
|
2020-12-14 17:42:09 +00:00
|
|
|
isRecoveryMode: pageModel.isRecoveryMode,
|
2023-03-22 13:48:50 +00:00
|
|
|
isTutorialFork: pageModel.isTutorialFork,
|
2021-07-28 19:02:06 +00:00
|
|
|
isFiddle: Computed.create(owner, (use) => use(pageModel.isPrefork)),
|
2023-04-11 05:56:26 +00:00
|
|
|
isSnapshot: pageModel.isSnapshot,
|
2020-10-02 15:10:00 +00:00
|
|
|
isPublic: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.public)),
|
2023-07-26 22:31:02 +00:00
|
|
|
isTemplate: pageModel.isTemplate,
|
|
|
|
isAnonymous,
|
2024-02-08 05:10:01 +00:00
|
|
|
}),
|
|
|
|
dom.hide(use => use(isSearchOpen) && use(isNarrowScreenObs())),
|
2020-10-02 15:10:00 +00:00
|
|
|
)
|
|
|
|
),
|
|
|
|
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()),
|
2024-02-08 05:10:01 +00:00
|
|
|
dom.hide(use => use(isSearchOpen)),
|
2022-10-19 23:06:05 +00:00
|
|
|
hoverTooltip('Undo', {key: 'topBarBtnTooltip'}),
|
2023-07-19 02:27:53 +00:00
|
|
|
cssHoverCircle.cls('-disabled', use => use(state.isUndoDisabled) || !use(isUndoRedoAvailable)),
|
|
|
|
testId('undo'),
|
2020-10-02 15:10:00 +00:00
|
|
|
),
|
|
|
|
topBarUndoBtn('Redo',
|
|
|
|
dom.on('click', () => state.isRedoDisabled.get() || allCommands.redo.run()),
|
2024-02-08 05:10:01 +00:00
|
|
|
dom.hide(use => use(isSearchOpen)),
|
2022-10-19 23:06:05 +00:00
|
|
|
hoverTooltip('Redo', {key: 'topBarBtnTooltip'}),
|
2023-07-19 02:27:53 +00:00
|
|
|
cssHoverCircle.cls('-disabled', use => use(state.isRedoDisabled) || !use(isUndoRedoAvailable)),
|
|
|
|
testId('redo'),
|
2020-10-02 15:10:00 +00:00
|
|
|
),
|
|
|
|
cssSpacer(),
|
|
|
|
]),
|
2022-11-14 14:34:50 +00:00
|
|
|
dom.domComputed((use) => {
|
|
|
|
const model = use(searchModelObs);
|
|
|
|
return model && use(moduleObs)?.searchBar(model, makeTestId('test-tb-search-'));
|
|
|
|
}),
|
2023-07-26 22:31:02 +00:00
|
|
|
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(''),
|
2022-10-17 09:47:16 +00:00
|
|
|
),
|
2023-07-26 22:31:02 +00:00
|
|
|
]),
|
|
|
|
dom('div', dom.create(AccountWidget, appModel, pageModel)),
|
2020-10-02 15:10:00 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2022-10-17 09:47:16 +00:00
|
|
|
function buildShowDiscussionButton(pageModel: DocPageModel) {
|
|
|
|
return cssHoverCircle({ style: `margin: 5px; position: relative;` },
|
|
|
|
cssTopBarBtn('Chat', dom.cls('tour-share-icon')),
|
|
|
|
cssBeta('Beta'),
|
2022-10-25 03:10:15 +00:00
|
|
|
hoverTooltip('Comments', {key: 'topBarBtnTooltip'}),
|
2022-10-17 09:47:16 +00:00
|
|
|
testId('open-discussion'),
|
|
|
|
dom.on('click', () => pageModel.gristDoc.get()!.showTool('discussion'))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const cssBeta = styled(menuAnnotate, `
|
|
|
|
position: absolute;
|
|
|
|
top: 4px;
|
|
|
|
right: -9px;
|
|
|
|
font-weight: bold;
|
|
|
|
`);
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-12-14 17:42:09 +00:00
|
|
|
function getCancelRecoveryModeFn(gristDoc: GristDoc): () => Promise<void> {
|
|
|
|
return async () => {
|
|
|
|
await gristDoc.app.topAppModel.api.getDocAPI(gristDoc.docPageModel.currentDocId.get()!)
|
|
|
|
.recover(false);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
function topBarUndoBtn(iconName: IconName, ...domArgs: DomElementArg[]): Element {
|
|
|
|
return cssHoverCircle(
|
|
|
|
cssTopBarUndoBtn(iconName),
|
|
|
|
...domArgs
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const cssTopBarUndoBtn = styled(cssTopBarBtn, `
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.topBarButtonSecondaryFg};
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
.${cssHoverCircle.className}:hover & {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.topBarButtonPrimaryFg};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.${cssHoverCircle.className}-disabled:hover & {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.topBarButtonDisabledFg};
|
2020-10-02 15:10:00 +00:00
|
|
|
cursor: default;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssBreadcrumbContainer = styled('div', `
|
|
|
|
padding: 7px;
|
|
|
|
flex: 1 1 auto;
|
2021-02-28 05:27:34 +00:00
|
|
|
min-width: 24px;
|
2020-10-02 15:10:00 +00:00
|
|
|
overflow: hidden;
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssFlexSpace = styled('div', `
|
|
|
|
flex: 1 1 0px;
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssSpacer = styled('div', `
|
2021-02-28 05:27:34 +00:00
|
|
|
max-width: 10px;
|
|
|
|
flex: auto;
|
2020-10-02 15:10:00 +00:00
|
|
|
`);
|