-
-
Notifications
You must be signed in to change notification settings - Fork 337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#1030 add 'Duplicate document' action in menu #1040
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,12 +6,14 @@ | |
import {loadUserManager} from 'app/client/lib/imports'; | ||
import {getTimeFromNow} from 'app/client/lib/timeUtils'; | ||
import {reportError} from 'app/client/models/AppModel'; | ||
import {docUrl, urlState} from 'app/client/models/gristUrlState'; | ||
import {docUrl, getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState'; | ||
import {DocPageModel} from 'app/client/models/DocPageModel'; | ||
import {HomeModel, makeLocalViewSettings, ViewSettings} from 'app/client/models/HomeModel'; | ||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo'; | ||
import {attachAddNewTip} from 'app/client/ui/AddNewTip'; | ||
import * as css from 'app/client/ui/DocMenuCss'; | ||
import {buildHomeIntro, buildWorkspaceIntro} from 'app/client/ui/HomeIntro'; | ||
import {makeCopy} from 'app/client/ui/MakeCopyMenu'; | ||
import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades'; | ||
import {buildTutorialCard} from 'app/client/ui/TutorialCard'; | ||
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs'; | ||
|
@@ -49,13 +51,13 @@ const testId = makeTestId('test-dm-'); | |
* Usage: | ||
* dom('div', createDocMenu(homeModel)) | ||
*/ | ||
export function createDocMenu(home: HomeModel): DomElementArg[] { | ||
export function createDocMenu(home: HomeModel, page: DocPageModel): DomElementArg[] { | ||
return [ | ||
attachWelcomePopups(home), | ||
dom.domComputed(home.loading, loading => ( | ||
loading === 'slow' ? css.spinner(loadingSpinner()) : | ||
loading ? null : | ||
dom.create(createLoadedDocMenu, home) | ||
dom.create(createLoadedDocMenu, home, page) | ||
)) | ||
]; | ||
} | ||
|
@@ -71,7 +73,7 @@ function attachWelcomePopups(home: HomeModel): (el: Element) => void { | |
}; | ||
} | ||
|
||
function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) { | ||
function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel, pageModel: DocPageModel) { | ||
const flashDocId = observable<string|null>(null); | ||
const upgradeButton = buildUpgradeButton(owner, home.app); | ||
return css.docList( /* vbox */ | ||
|
@@ -112,7 +114,7 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) { | |
// removes all pinned docs when on trash page. | ||
dom.maybe((use) => use(home.currentWSPinnedDocs).length > 0, () => [ | ||
css.docListHeader(css.pinnedDocsIcon('PinBig'), t("Pinned Documents")), | ||
createPinnedDocs(home, home.currentWSPinnedDocs), | ||
createPinnedDocs(home, pageModel, home.currentWSPinnedDocs), | ||
]), | ||
|
||
// Build the featured templates dom if on the Examples & Templates page. | ||
|
@@ -122,7 +124,7 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) { | |
t("Featured"), | ||
testId('featured-templates-header') | ||
), | ||
createPinnedDocs(home, home.featuredTemplates, true), | ||
createPinnedDocs(home, pageModel, home.featuredTemplates, true), | ||
]), | ||
|
||
dom.maybe(home.available, () => [ | ||
|
@@ -146,24 +148,24 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) { | |
(page === 'all') ? | ||
dom('div', | ||
showIntro ? buildHomeIntro(home) : null, | ||
buildAllDocsBlock(home, home.workspaces, showIntro, flashDocId, viewSettings), | ||
shouldShowTemplates(home, showIntro) ? buildAllDocsTemplates(home, viewSettings) : null, | ||
buildAllDocsBlock(home, pageModel, home.workspaces, showIntro, flashDocId, viewSettings), | ||
shouldShowTemplates(home, showIntro) ? buildAllDocsTemplates(home, pageModel, viewSettings) : null, | ||
) : | ||
(page === 'trash') ? | ||
dom('div', | ||
css.docBlock(t("Documents stay in Trash for 30 days, after which they get deleted permanently.")), | ||
dom.maybe((use) => use(home.trashWorkspaces).length === 0, () => | ||
css.docBlock(t("Trash is empty.")) | ||
), | ||
buildAllDocsBlock(home, home.trashWorkspaces, false, flashDocId, viewSettings), | ||
buildAllDocsBlock(home, pageModel, home.trashWorkspaces, false, flashDocId, viewSettings), | ||
) : | ||
(page === 'templates') ? | ||
dom('div', | ||
buildAllTemplates(home, home.templateWorkspaces, viewSettings) | ||
buildAllTemplates(home, pageModel, home.templateWorkspaces, viewSettings) | ||
) : | ||
workspace && !workspace.isSupportWorkspace && workspace.docs?.length ? | ||
css.docBlock( | ||
buildWorkspaceDocBlock(home, workspace, flashDocId, viewSettings), | ||
buildWorkspaceDocBlock(home, pageModel, workspace, flashDocId, viewSettings), | ||
testId('doc-block') | ||
) : | ||
workspace && !workspace.isSupportWorkspace && workspace.docs?.length === 0 ? | ||
|
@@ -188,7 +190,7 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) { | |
} | ||
|
||
function buildAllDocsBlock( | ||
home: HomeModel, workspaces: Observable<Workspace[]>, | ||
home: HomeModel, page: DocPageModel, workspaces: Observable<Workspace[]>, | ||
showIntro: boolean, flashDocId: Observable<string|null>, viewSettings: ViewSettings, | ||
) { | ||
return dom.forEach(workspaces, (ws) => { | ||
|
@@ -220,7 +222,7 @@ function buildAllDocsBlock( | |
|
||
testId('ws-header'), | ||
), | ||
buildWorkspaceDocBlock(home, ws, flashDocId, viewSettings), | ||
buildWorkspaceDocBlock(home, page, ws, flashDocId, viewSettings), | ||
testId('doc-block') | ||
); | ||
}); | ||
|
@@ -232,7 +234,7 @@ function buildAllDocsBlock( | |
* | ||
* If there are no featured templates, builds nothing. | ||
*/ | ||
function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) { | ||
function buildAllDocsTemplates(home: HomeModel, page: DocPageModel, viewSettings: ViewSettings) { | ||
return dom.domComputed(home.featuredTemplates, templates => { | ||
if (templates.length === 0) { return null; } | ||
|
||
|
@@ -251,7 +253,7 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) { | |
createVideoTourTextButton(), | ||
), | ||
dom.maybe((use) => !use(hideTemplatesObs), () => [ | ||
buildTemplateDocs(home, templates, viewSettings), | ||
buildTemplateDocs(home, page, templates, viewSettings), | ||
bigBasicButton( | ||
t("Discover More Templates"), | ||
urlState().setLinkUrl({homePage: 'templates'}), | ||
|
@@ -273,7 +275,8 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) { | |
* | ||
* Used on the Examples & Templates below the featured templates. | ||
*/ | ||
function buildAllTemplates(home: HomeModel, templateWorkspaces: Observable<Workspace[]>, viewSettings: ViewSettings) { | ||
function buildAllTemplates(home: HomeModel, page: DocPageModel, templateWorkspaces: Observable<Workspace[]>, | ||
viewSettings: ViewSettings) { | ||
return dom.forEach(templateWorkspaces, workspace => { | ||
return css.templatesDocBlock( | ||
css.templateBlockHeader( | ||
|
@@ -283,7 +286,7 @@ function buildAllTemplates(home: HomeModel, templateWorkspaces: Observable<Works | |
), | ||
testId('templates-header'), | ||
), | ||
buildTemplateDocs(home, workspace.docs, viewSettings), | ||
buildTemplateDocs(home, page, workspace.docs, viewSettings), | ||
css.docBlock.cls((use) => '-' + use(viewSettings.currentView)), | ||
testId('templates'), | ||
); | ||
|
@@ -371,8 +374,8 @@ function buildPrefs( | |
} | ||
|
||
|
||
function buildWorkspaceDocBlock(home: HomeModel, workspace: Workspace, flashDocId: Observable<string|null>, | ||
viewSettings: ViewSettings) { | ||
function buildWorkspaceDocBlock(home: HomeModel, page: DocPageModel, workspace: Workspace, | ||
flashDocId: Observable<string|null>, viewSettings: ViewSettings) { | ||
const renaming = observable<Document|null>(null); | ||
|
||
function renderDocs(sort: 'date'|'name', view: "list"|"icons") { | ||
|
@@ -385,7 +388,7 @@ function buildWorkspaceDocBlock(home: HomeModel, workspace: Workspace, flashDocI | |
return dom.forEach(docs, doc => { | ||
if (view === 'icons') { | ||
return dom.update( | ||
buildPinnedDoc(home, doc, workspace), | ||
buildPinnedDoc(home, page, doc, workspace), | ||
testId('doc'), | ||
); | ||
} | ||
|
@@ -420,7 +423,7 @@ function buildWorkspaceDocBlock(home: HomeModel, workspace: Workspace, flashDocI | |
css.docMenuTrigger(icon('Dots'), testId('doc-options')), | ||
] : | ||
css.docMenuTrigger(icon('Dots'), | ||
menu(() => makeDocOptionsMenu(home, doc, renaming), | ||
menu(() => makeDocOptionsMenu(home, page, doc, renaming), | ||
{placement: 'bottom-start', parentSelectorToMark: '.' + css.docRowWrapper.className}), | ||
// Clicks on the menu trigger shouldn't follow the link that it's contained in. | ||
dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }), | ||
|
@@ -478,7 +481,8 @@ async function doRename(home: HomeModel, doc: Document, val: string, flashDocId: | |
// losing the doc that was e.g. just renamed. | ||
|
||
// Exported because also used by the PinnedDocs component. | ||
export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Observable<Document|null>) { | ||
export function makeDocOptionsMenu(home: HomeModel, page: DocPageModel, doc: Document, | ||
renaming: Observable<Document|null>) { | ||
const org = home.app.currentOrg; | ||
const orgAccess: roles.Role|null = org ? org.access : null; | ||
|
||
|
@@ -529,6 +533,16 @@ export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Obs | |
dom.cls('disabled', !roles.canEdit(orgAccess)), | ||
testId('pin-doc') | ||
), | ||
menuItem(() => { | ||
const {appModel} = page; | ||
if (!appModel.currentValidUser) { | ||
page.clearUnsavedChanges(); | ||
window.location.href = getLoginOrSignupUrl({srcDocId: urlState().state.get().doc}); | ||
return; | ||
} | ||
Comment on lines
+537
to
+541
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wondered if there could be a case where an anonymous user would have access to this menu item, and there is: when they are shared a workspace or an organisation. In such a case, the workflow is relevant: they are invited to login or to logon. So good catch, that's perfect! 👌 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you point me to a situation when this might happen? |
||
return makeCopy({pageModel: page, doc, modalTitle: t("Duplicate Document")}); | ||
}, t("Duplicate Document") | ||
), | ||
menuItem(manageUsers, roles.canEditAccess(doc.access) ? t("Manage Users"): t("Access Details"), | ||
testId('doc-access') | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is not needed, is it? DocPageModel should be only created if we've opened a document. Here we are somehow "lucky" that this
constructor
isn't doing anything destructive, and is disposed when we open a document (thanks to the internal implementation ingristUrlState.ts/needPageLoad
). But still it does create some listeners and computed properties (that do something when url is changed), which is not ideal, and can lead to unexpected bugs later - as, probably, the absence ofdoc
inDocPageModel
is rather perceived as temporary state, but here it is enforced.I think the main culprit here is that
clearUnsavedChanges
is hold in theDocPageModel
and not on theAppModel
level, the ideal situation for me would be to refactor it:clearUnsavedChanges
method to theAppModel
orTopAppModel
DocPageModelImpl
at allmakeCopy
so that uses onlyAppModel
instance (I looked into its code, and it only needs thisclearUnsavedChanges
method