mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
37eed2d3c2
Summary: Staging tests were using an organization named Test Grist, while local was using one named test-grist. Both are now named Test Grist, which should help keep things more consistent between local and deployment test runs. The inherited line height of template docs in icon view was different on Windows, cutting off part of the last line of the description. The description line height should now be fixed to a reasonable value. Test Plan: Manually tested CSS fix on a Windows machine. Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D2948
271 lines
7.4 KiB
TypeScript
271 lines
7.4 KiB
TypeScript
import {docUrl, urlState} from 'app/client/models/gristUrlState';
|
|
import {getTimeFromNow, HomeModel} from 'app/client/models/HomeModel';
|
|
import {makeDocOptionsMenu, makeRemovedDocOptionsMenu} from 'app/client/ui/DocMenu';
|
|
import {transientInput} from 'app/client/ui/transientInput';
|
|
import {colors, vars} from 'app/client/ui2018/cssVars';
|
|
import {icon} from 'app/client/ui2018/icons';
|
|
import {menu} from 'app/client/ui2018/menus';
|
|
import * as roles from 'app/common/roles';
|
|
import {Document, Workspace} from 'app/common/UserAPI';
|
|
import {computed, dom, makeTestId, Observable, observable, styled} from 'grainjs';
|
|
|
|
const testId = makeTestId('test-dm-');
|
|
|
|
/**
|
|
* PinnedDocs builds the dom at the top of the doclist showing all the pinned docs in the
|
|
* selectedOrg. Builds nothing if there are no pinned docs.
|
|
*
|
|
* Used only by DocMenu.
|
|
*/
|
|
export function createPinnedDocs(home: HomeModel, docs: Observable<Document[]>, isExample = false) {
|
|
return pinnedDocList(
|
|
dom.forEach(docs, doc => buildPinnedDoc(home, doc, doc.workspace, isExample)),
|
|
testId('pinned-doc-list'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build a single doc card with a preview and name. A misnomer because it's now used not only for
|
|
* pinned docs, but also for the thumnbails (aka "icons") view mode.
|
|
*/
|
|
export function buildPinnedDoc(home: HomeModel, doc: Document, workspace: Workspace, isExample = false): HTMLElement {
|
|
const renaming = observable<Document|null>(null);
|
|
const isRenamingDoc = computed((use) => use(renaming) === doc);
|
|
return pinnedDocWrapper(
|
|
dom.autoDispose(isRenamingDoc),
|
|
dom.domComputed(isRenamingDoc, (isRenaming) =>
|
|
pinnedDoc(
|
|
isRenaming || doc.removedAt ?
|
|
null :
|
|
urlState().setLinkUrl(docUrl(doc, isExample ? {org: workspace.orgDomain} : undefined)),
|
|
pinnedDoc.cls('-no-access', !roles.canView(doc.access)),
|
|
pinnedDocPreview(
|
|
(doc.options?.icon ?
|
|
cssImage({src: doc.options.icon}) :
|
|
[docInitials(doc.name), pinnedDocThumbnail()]
|
|
),
|
|
(doc.public && !isExample ? cssPublicIcon('PublicFilled', testId('public')) : null),
|
|
pinnedDocPreview.cls('-with-icon', Boolean(doc.options?.icon)),
|
|
),
|
|
pinnedDocFooter(
|
|
(isRenaming ?
|
|
pinnedDocEditorInput({
|
|
initialValue: doc.name || '',
|
|
save: async (val) => (val !== doc.name) ? home.renameDoc(doc.id, val) : undefined,
|
|
close: () => renaming.set(null),
|
|
}, testId('doc-name-editor'))
|
|
:
|
|
pinnedDocTitle(
|
|
dom.text(doc.name),
|
|
testId('pinned-doc-name'),
|
|
// Mostly for the sake of tests, allow .test-dm-pinned-doc-name to find documents in
|
|
// either 'list' or 'icons' views.
|
|
testId('doc-name')
|
|
)
|
|
),
|
|
doc.options?.description ?
|
|
cssPinnedDocDesc(doc.options.description, testId('pinned-doc-desc')) :
|
|
cssPinnedDocTimestamp(
|
|
capitalizeFirst(getTimeFromNow(doc.removedAt || doc.updatedAt)),
|
|
testId('pinned-doc-desc')
|
|
)
|
|
)
|
|
)
|
|
),
|
|
isExample ? null : (doc.removedAt ?
|
|
[
|
|
// For deleted documents, attach the menu to the entire doc icon, and include the
|
|
// "Dots" icon just to clarify that there are options.
|
|
menu(() => makeRemovedDocOptionsMenu(home, doc, workspace),
|
|
{placement: 'right-start'}),
|
|
pinnedDocOptions(icon('Dots'), testId('pinned-doc-options')),
|
|
] :
|
|
pinnedDocOptions(icon('Dots'),
|
|
menu(() => makeDocOptionsMenu(home, doc, renaming),
|
|
{placement: 'bottom-start'}),
|
|
// Clicks on the menu trigger shouldn't follow the link that it's contained in.
|
|
dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }),
|
|
testId('pinned-doc-options'),
|
|
)
|
|
),
|
|
testId('pinned-doc')
|
|
);
|
|
}
|
|
|
|
function docInitials(docTitle: string) {
|
|
return cssDocInitials(docTitle.slice(0, 2), testId('pinned-initials'));
|
|
}
|
|
|
|
// Capitalizes the first letter in the given string.
|
|
function capitalizeFirst(str: string): string {
|
|
return str.replace(/^[a-z]/gi, c => c.toUpperCase());
|
|
}
|
|
|
|
const pinnedDocList = styled('div', `
|
|
display: flex;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
padding-bottom: 16px;
|
|
margin: 0 0 28px 0;
|
|
`);
|
|
|
|
const pinnedDocWrapper = styled('div', `
|
|
display: inline-block;
|
|
flex: 0 0 auto;
|
|
position: relative;
|
|
width: 210px;
|
|
margin: 16px 24px 16px 0;
|
|
border: 1px solid ${colors.mediumGrey};
|
|
border-radius: 1px;
|
|
vertical-align: top;
|
|
&:hover {
|
|
border: 1px solid ${colors.slate};
|
|
}
|
|
`);
|
|
|
|
const pinnedDoc = styled('a', `
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
color: black;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
|
|
&:hover {
|
|
color: black;
|
|
text-decoration: none;
|
|
}
|
|
&-no-access, &-no-access:hover {
|
|
color: ${colors.slate};
|
|
cursor: not-allowed;
|
|
}
|
|
`);
|
|
|
|
const pinnedDocPreview = styled('div', `
|
|
position: relative;
|
|
flex: none;
|
|
width: 100%;
|
|
height: 131px;
|
|
background-color: ${colors.dark};
|
|
min-height: 0;
|
|
|
|
padding: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
.${pinnedDoc.className}-no-access > & {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
&-with-icon {
|
|
padding: 0;
|
|
}
|
|
`);
|
|
|
|
const pinnedDocThumbnail = styled('div', `
|
|
position: absolute;
|
|
right: 20px;
|
|
bottom: 20px;
|
|
height: 48px;
|
|
width: 48px;
|
|
background-image: var(--icon-ThumbPreview);
|
|
background-size: 48px 48px;
|
|
background-repeat: no-repeat;
|
|
background-position: center;
|
|
`);
|
|
|
|
const cssDocInitials = styled('div', `
|
|
position: absolute;
|
|
left: 20px;
|
|
bottom: 20px;
|
|
font-size: 32px;
|
|
border: 1px solid ${colors.lightGreen};
|
|
color: ${colors.mediumGreyOpaque};
|
|
border-radius: 3px;
|
|
padding: 4px 0;
|
|
width: 48px;
|
|
height: 48px;
|
|
text-align: center;
|
|
`);
|
|
|
|
const pinnedDocOptions = styled('div', `
|
|
position: absolute;
|
|
top: 12px;
|
|
right: 12px;
|
|
height: 24px;
|
|
width: 24px;
|
|
padding: 4px;
|
|
line-height: 0px;
|
|
border-radius: 3px;
|
|
cursor: default;
|
|
visibility: hidden;
|
|
background-color: ${colors.mediumGrey};
|
|
--icon-color: ${colors.light};
|
|
|
|
.${pinnedDocWrapper.className}:hover &, &.weasel-popup-open {
|
|
visibility: visible;
|
|
}
|
|
`);
|
|
|
|
const pinnedDocFooter = styled('div', `
|
|
width: 100%;
|
|
font-size: ${vars.mediumFontSize};
|
|
`);
|
|
|
|
const pinnedDocTitle = styled('div', `
|
|
margin: 16px 16px 0px 16px;
|
|
font-weight: bold;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
`);
|
|
|
|
const pinnedDocEditorInput = styled(transientInput, `
|
|
margin: 16px 16px 0px 16px;
|
|
font-weight: bold;
|
|
min-width: 0px;
|
|
color: initial;
|
|
font-size: inherit;
|
|
line-height: inherit;
|
|
appearance: none;
|
|
-moz-appearance: none;
|
|
padding: 0;
|
|
border: none;
|
|
outline: none;
|
|
background-color: ${colors.mediumGrey};
|
|
`);
|
|
|
|
const cssPinnedDocTimestamp = styled('div', `
|
|
margin: 8px 16px 16px 16px;
|
|
color: ${colors.slate};
|
|
`);
|
|
|
|
const cssPinnedDocDesc = styled(cssPinnedDocTimestamp, `
|
|
margin: 8px 16px 16px 16px;
|
|
color: ${colors.slate};
|
|
height: 48px;
|
|
line-height: 16px;
|
|
-webkit-box-orient: vertical;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
word-break: break-word;
|
|
`);
|
|
|
|
const cssImage = styled('img', `
|
|
position: relative;
|
|
background-color: ${colors.light};
|
|
height: 100%;
|
|
width: 100%;
|
|
object-fit: scale-down;
|
|
`);
|
|
|
|
const cssPublicIcon = styled(icon, `
|
|
position: absolute;
|
|
top: 16px;
|
|
left: 16px;
|
|
--icon-color: ${colors.lightGreen};
|
|
`);
|