diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx
index 60bf609ec..0541841af 100644
--- a/app/actions/definitions/documents.tsx
+++ b/app/actions/definitions/documents.tsx
@@ -29,6 +29,7 @@ import { getEventFiles } from "@shared/utils/files";
import DocumentDelete from "~/scenes/DocumentDelete";
import DocumentMove from "~/scenes/DocumentMove";
import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
+import DocumentPublish from "~/scenes/DocumentPublish";
import DocumentTemplatizeDialog from "~/components/DocumentTemplatizeDialog";
import { createAction } from "~/actions";
import { DocumentSection } from "~/actions/sections";
@@ -71,11 +72,9 @@ export const createDocument = createAction({
section: DocumentSection,
icon: ,
keywords: "create",
- visible: ({ activeCollectionId, stores }) =>
- !!activeCollectionId &&
- stores.policies.abilities(activeCollectionId).update,
+ visible: ({ currentTeamId, stores }) =>
+ !!currentTeamId && stores.policies.abilities(currentTeamId).createDocument,
perform: ({ activeCollectionId, inStarredSection }) =>
- activeCollectionId &&
history.push(newDocumentPath(activeCollectionId), {
starred: inStarredSection,
}),
@@ -143,20 +142,30 @@ export const publishDocument = createAction({
!!document?.isDraft && stores.policies.abilities(activeDocumentId).update
);
},
- perform: ({ activeDocumentId, stores, t }) => {
+ perform: async ({ activeDocumentId, stores, t }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
+ if (document?.publishedAt) {
+ return;
+ }
- document?.save({
- publish: true,
- });
-
- stores.toasts.showToast(t("Document published"), {
- type: "success",
- });
+ if (document?.collectionId) {
+ await document.save({
+ publish: true,
+ });
+ stores.toasts.showToast(t("Document published"), {
+ type: "success",
+ });
+ } else if (document) {
+ stores.dialogs.openModal({
+ title: t("Publish document"),
+ isCentered: true,
+ content: ,
+ });
+ }
},
});
diff --git a/app/components/ActionButton.tsx b/app/components/ActionButton.tsx
index f3fd358de..ae4b50bb9 100644
--- a/app/components/ActionButton.tsx
+++ b/app/components/ActionButton.tsx
@@ -23,31 +23,44 @@ const ActionButton = React.forwardRef(
{ action, context, tooltip, hideOnActionDisabled, ...rest }: Props,
ref: React.Ref
) => {
+ const [executing, setExecuting] = React.useState(false);
const disabled = rest.disabled;
if (!context || !action) {
return ;
}
- if (action?.visible && !action.visible(context) && hideOnActionDisabled) {
+ const actionContext = { ...context, isButton: true };
+
+ if (
+ action?.visible &&
+ !action.visible(actionContext) &&
+ hideOnActionDisabled
+ ) {
return null;
}
const label =
- typeof action.name === "function" ? action.name(context) : action.name;
+ typeof action.name === "function"
+ ? action.name(actionContext)
+ : action.name;
const button = (
- )}
-
-
- {t("Choose a collection")}
-
-
- >
+
+ }>
+ {t("New doc")}
+
+
);
}
-const CollectionName = styled.div`
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
-`;
-
export default observer(NewDocumentMenu);
diff --git a/app/routes/authenticated.tsx b/app/routes/authenticated.tsx
index c270c9ce2..de5044715 100644
--- a/app/routes/authenticated.tsx
+++ b/app/routes/authenticated.tsx
@@ -100,6 +100,7 @@ function AuthenticatedRoutes() {
+
{
onPublish = (ev: React.MouseEvent | KeyboardEvent) => {
ev.preventDefault();
- const { document } = this.props;
+ const { document, dialogs, t } = this.props;
if (document.publishedAt) {
return;
}
- this.onSave({
- publish: true,
- done: true,
- });
+
+ if (document?.collectionId) {
+ this.onSave({
+ publish: true,
+ done: true,
+ });
+ } else {
+ dialogs.openModal({
+ title: t("Publish document"),
+ isCentered: true,
+ content: ,
+ });
+ }
};
onToggleTableOfContents = (ev: KeyboardEvent) => {
diff --git a/app/scenes/Document/components/Header.tsx b/app/scenes/Document/components/Header.tsx
index 11cea992b..e487182d7 100644
--- a/app/scenes/Document/components/Header.tsx
+++ b/app/scenes/Document/components/Header.tsx
@@ -20,6 +20,7 @@ import Collaborators from "~/components/Collaborators";
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
import Header from "~/components/Header";
import Tooltip from "~/components/Tooltip";
+import { publishDocument } from "~/actions/definitions/documents";
import { restoreRevision } from "~/actions/definitions/revisions";
import useActionContext from "~/hooks/useActionContext";
import useMobile from "~/hooks/useMobile";
@@ -94,13 +95,6 @@ function DocumentHeader({
});
}, [onSave]);
- const handlePublish = React.useCallback(() => {
- onSave({
- done: true,
- publish: true,
- });
- }, [onSave]);
-
const context = useActionContext({
activeDocumentId: document?.id,
});
@@ -312,23 +306,17 @@ function DocumentHeader({
)}
- {can.update && isDraft && !isRevision && (
-
-
-
- {isPublishing ? `${t("Publishing")}…` : t("Publish")}
-
-
-
- )}
+
+
+ {document.collectionId ? t("Publish") : `${t("Publish")}…`}
+
+
{!isEditing && (
<>
{!isDeleted && }
diff --git a/app/scenes/DocumentNew.tsx b/app/scenes/DocumentNew.tsx
index 5b1100f1f..a36e5442a 100644
--- a/app/scenes/DocumentNew.tsx
+++ b/app/scenes/DocumentNew.tsx
@@ -23,11 +23,14 @@ function DocumentNew() {
useEffect(() => {
async function createDocument() {
const params = queryString.parse(location.search);
+ let collection;
try {
- const collection = await collections.fetch(id);
+ if (id) {
+ collection = await collections.fetch(id);
+ }
const document = await documents.create({
- collectionId: collection.id,
+ collectionId: collection?.id,
parentDocumentId: params.parentDocumentId?.toString(),
templateId: params.templateId?.toString(),
template: params.template === "true" ? true : false,
diff --git a/app/scenes/DocumentPublish.tsx b/app/scenes/DocumentPublish.tsx
new file mode 100644
index 000000000..1fc761fcb
--- /dev/null
+++ b/app/scenes/DocumentPublish.tsx
@@ -0,0 +1,428 @@
+import FuzzySearch from "fuzzy-search";
+import { includes, difference, concat, filter } from "lodash";
+import { observer } from "mobx-react";
+import { StarredIcon, DocumentIcon } from "outline-icons";
+import * as React from "react";
+import { useTranslation, Trans } from "react-i18next";
+import AutoSizer from "react-virtualized-auto-sizer";
+import { FixedSizeList as List } from "react-window";
+import styled, { useTheme } from "styled-components";
+import breakpoint from "styled-components-breakpoint";
+import Document from "~/models/Document";
+import Button from "~/components/Button";
+import Flex from "~/components/Flex";
+import CollectionIcon from "~/components/Icons/CollectionIcon";
+import EmojiIcon from "~/components/Icons/EmojiIcon";
+import { Outline } from "~/components/Input";
+import InputSearch from "~/components/InputSearch";
+import PublishLocation from "~/components/PublishLocation";
+import Text from "~/components/Text";
+import useMobile from "~/hooks/useMobile";
+import useStores from "~/hooks/useStores";
+import useToasts from "~/hooks/useToasts";
+import { isModKey } from "~/utils/keyboard";
+import { flattenTree, descendants } from "~/utils/tree";
+
+type Props = {
+ /** Document to publish */
+ document: Document;
+};
+
+function DocumentPublish({ document }: Props) {
+ const isMobile = useMobile();
+ const [searchTerm, setSearchTerm] = React.useState();
+ const [selectedLocation, setLocation] = React.useState();
+ const [initialScrollOffset, setInitialScrollOffset] = React.useState(
+ 0
+ );
+ const { collections, documents } = useStores();
+ const { showToast } = useToasts();
+ const theme = useTheme();
+ const [items, setItems] = React.useState(
+ flattenTree(collections.tree.root).slice(1)
+ );
+ const [activeItem, setActiveItem] = React.useState(0);
+ const [expandedItems, setExpandedItems] = React.useState([]);
+ const inputSearchRef = React.useRef(
+ null
+ );
+ const { t } = useTranslation();
+ const { dialogs } = useStores();
+ const listRef = React.useRef>(null);
+ const VERTICAL_PADDING = 6;
+ const HORIZONTAL_PADDING = 24;
+
+ const nextItem = () => {
+ return Math.min(activeItem + 1, items.length - 1);
+ };
+
+ const prevItem = () => {
+ return Math.max(activeItem - 1, 0);
+ };
+
+ const searchIndex = React.useMemo(() => {
+ const data = flattenTree(collections.tree.root).slice(1);
+
+ return new FuzzySearch(data, ["data.title"], {
+ caseSensitive: false,
+ });
+ }, [collections.tree]);
+
+ React.useEffect(() => {
+ if (searchTerm) {
+ setLocation(null);
+ setExpandedItems([]);
+ }
+ setActiveItem(0);
+ }, [searchTerm]);
+
+ React.useEffect(() => {
+ let results = flattenTree(collections.tree.root).slice(1);
+
+ if (collections.isLoaded) {
+ if (searchTerm) {
+ results = searchIndex.search(searchTerm);
+ } else {
+ results = results.filter((r) => r.data.type === "collection");
+ }
+ }
+
+ setInitialScrollOffset(0);
+ setItems(results);
+ }, [document, collections, searchTerm, searchIndex]);
+
+ const handleSearch = (ev: React.ChangeEvent) => {
+ setSearchTerm(ev.target.value);
+ };
+
+ const isExpanded = (index: number) => {
+ const item = items[index];
+ return includes(expandedItems, item.data.id);
+ };
+
+ const calculateInitialScrollOffset = (itemCount: number) => {
+ if (listRef.current) {
+ const { height, itemSize } = listRef.current.props;
+ const { scrollOffset } = listRef.current.state as {
+ scrollOffset: number;
+ };
+ const itemsHeight = itemCount * itemSize;
+ return itemsHeight < height ? 0 : scrollOffset;
+ }
+ return 0;
+ };
+
+ const collapse = (item: number) => {
+ const descendantIds = descendants(items[item]).map((des) => des.data.id);
+ setExpandedItems(
+ difference(expandedItems, [...descendantIds, items[item].data.id])
+ );
+
+ // remove children
+ const newItems = filter(
+ items,
+ (item: any) => !includes(descendantIds, item.data.id)
+ );
+ const scrollOffset = calculateInitialScrollOffset(newItems.length);
+ setInitialScrollOffset(scrollOffset);
+ setItems(newItems);
+ };
+
+ const expand = (item: number) => {
+ setExpandedItems(concat(expandedItems, items[item].data.id));
+
+ // add children
+ const newItems = items.slice();
+ newItems.splice(item + 1, 0, ...descendants(items[item], 1));
+ const scrollOffset = calculateInitialScrollOffset(newItems.length);
+ setInitialScrollOffset(scrollOffset);
+ setItems(newItems);
+ };
+
+ const isSelected = (item: number) => {
+ if (!selectedLocation) {
+ return false;
+ }
+ const selectedItemId = selectedLocation.data.id;
+ const itemId = items[item].data.id;
+
+ return selectedItemId === itemId;
+ };
+
+ const toggleCollapse = (item: number) => {
+ if (isExpanded(item)) {
+ collapse(item);
+ } else {
+ expand(item);
+ }
+ };
+
+ const toggleSelect = (item: number) => {
+ if (isSelected(item)) {
+ setLocation(null);
+ } else {
+ setLocation(items[item]);
+ }
+ };
+
+ const publish = async () => {
+ if (!selectedLocation) {
+ showToast(t("Select a location to publish"), {
+ type: "info",
+ });
+ return;
+ }
+
+ try {
+ const {
+ collectionId,
+ type,
+ id: parentDocumentId,
+ } = selectedLocation.data;
+
+ // Also move it under if selected path corresponds to another doc
+ if (type === "document") {
+ await document.move(collectionId, parentDocumentId);
+ }
+
+ document.collectionId = collectionId;
+ await document.save({ publish: true });
+
+ showToast(t("Document published"), {
+ type: "success",
+ });
+
+ dialogs.closeAllModals();
+ } catch (err) {
+ showToast(t("Couldn’t publish the document, try again?"), {
+ type: "error",
+ });
+ }
+ };
+
+ const row = ({
+ index,
+ data,
+ style,
+ }: {
+ index: number;
+ data: any[];
+ style: React.CSSProperties;
+ }) => {
+ const result = data[index];
+ const isCollection = result.data.type === "collection";
+ let icon;
+
+ if (isCollection) {
+ const col = collections.get(result.data.collectionId);
+ icon = col && (
+
+ );
+ } else {
+ const doc = documents.get(result.data.id);
+ const { emoji } = result.data;
+ if (emoji) {
+ icon = ;
+ } else if (doc?.isStarred) {
+ icon = ;
+ } else {
+ icon = ;
+ }
+ }
+
+ return (
+ setActiveItem(index)}
+ onClick={() => toggleSelect(index)}
+ onDisclosureClick={(ev) => {
+ ev.stopPropagation();
+ toggleCollapse(index);
+ }}
+ location={result}
+ selected={isSelected(index)}
+ active={activeItem === index}
+ expanded={isExpanded(index)}
+ icon={icon}
+ isSearchResult={!!searchTerm}
+ />
+ );
+ };
+
+ if (!document || !collections.isLoaded) {
+ return null;
+ }
+
+ const focusSearchInput = () => {
+ inputSearchRef.current?.focus();
+ };
+
+ const handleKeyDown = (ev: React.KeyboardEvent) => {
+ switch (ev.key) {
+ case "ArrowDown": {
+ ev.preventDefault();
+ setActiveItem(nextItem());
+ break;
+ }
+ case "ArrowUp": {
+ ev.preventDefault();
+ if (activeItem === 0) {
+ focusSearchInput();
+ } else {
+ setActiveItem(prevItem());
+ }
+ break;
+ }
+ case "ArrowLeft": {
+ if (!searchTerm && isExpanded(activeItem)) {
+ toggleCollapse(activeItem);
+ }
+ break;
+ }
+ case "ArrowRight": {
+ if (!searchTerm) {
+ toggleCollapse(activeItem);
+ }
+ break;
+ }
+ case "Enter": {
+ if (isModKey(ev)) {
+ publish();
+ } else {
+ toggleSelect(activeItem);
+ }
+ break;
+ }
+ }
+ };
+
+ const innerElementType = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+ >(({ style, ...rest }, ref) => (
+
+ ));
+
+ return (
+
+
+ {items.length ? (
+
+
+ {({ width, height }: { width: number; height: number }) => (
+
+ results[index].data.id}
+ >
+ {row}
+
+
+ )}
+
+
+ ) : (
+
+ {t("No results found")}.
+
+ )}
+
+
+ );
+}
+
+const NoResults = styled(Flex)`
+ align-items: center;
+ justify-content: center;
+ height: 65vh;
+
+ ${breakpoint("tablet")`
+ height: 40vh;
+ `}
+`;
+
+const Search = styled(InputSearch)`
+ ${Outline} {
+ border-radius: 16px;
+ }
+ margin-bottom: 4px;
+ padding-left: 24px;
+ padding-right: 24px;
+`;
+
+const FlexContainer = styled(Flex)`
+ margin-left: -24px;
+ margin-right: -24px;
+ margin-bottom: -24px;
+ outline: none;
+`;
+
+const Results = styled.div`
+ height: 65vh;
+
+ ${breakpoint("tablet")`
+ height: 40vh;
+ `}
+`;
+
+const Footer = styled(Flex)`
+ height: 64px;
+ border-top: 1px solid ${(props) => props.theme.horizontalRule};
+ padding-left: 24px;
+ padding-right: 24px;
+`;
+
+const SelectedLocation = styled(Text)`
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 0;
+`;
+
+export default observer(DocumentPublish);
diff --git a/app/stores/CollectionsStore.ts b/app/stores/CollectionsStore.ts
index 82aedbf12..ea4fbc26b 100644
--- a/app/stores/CollectionsStore.ts
+++ b/app/stores/CollectionsStore.ts
@@ -1,7 +1,8 @@
import invariant from "invariant";
-import { concat, find, last } from "lodash";
+import { concat, find, last, isEmpty } from "lodash";
import { computed, action } from "mobx";
import { CollectionPermission, FileOperationFormat } from "@shared/types";
+import parseTitle from "@shared/utils/parseTitle";
import Collection from "~/models/Collection";
import { NavigationNode } from "~/types";
import { client } from "~/utils/ApiClient";
@@ -99,6 +100,76 @@ export default class CollectionsStore extends BaseStore {
});
}
+ @computed
+ get tree() {
+ const subtree = (node: any) => {
+ const isDocument = node.data.type === DocumentPathItemType.Document;
+ if (isDocument) {
+ const { strippedTitle, emoji } = parseTitle(node.data.title);
+ node.data.title = strippedTitle;
+ if (emoji) {
+ node.data.emoji = emoji;
+ }
+ }
+ const root: any = {
+ data: {
+ id: node.data.id,
+ title: node.data.name || node.data.title,
+ type: node.data.type,
+ collectionId:
+ node.data.type === DocumentPathItemType.Collection
+ ? node.data.id
+ : node.data.collectionId,
+ emoji: node.data.emoji,
+ },
+ children: [],
+ parent: node.parent,
+ depth: node.depth,
+ };
+ !isEmpty(node.children) &&
+ node.children.forEach((child: any) => {
+ root.children.push(
+ subtree({
+ data: {
+ ...child,
+ type: DocumentPathItemType.Document,
+ collectionId: root.data.collectionId,
+ },
+ parent: root,
+ children: child.children || [],
+ depth: root.depth + 1,
+ }).root
+ );
+ });
+ return { root };
+ };
+
+ const root: any = {
+ data: null,
+ parent: null,
+ children: [],
+ depth: -1,
+ };
+
+ if (this.isLoaded) {
+ this.data.forEach((collection) => {
+ root.children.push(
+ subtree({
+ data: {
+ ...collection,
+ type: DocumentPathItemType.Collection,
+ },
+ children: collection.documents || [],
+ parent: root,
+ depth: root.depth + 1,
+ }).root
+ );
+ });
+ }
+
+ return { root };
+ }
+
@action
import = async (attachmentId: string, format?: string) => {
await client.post("/collections.import", {
diff --git a/app/types.ts b/app/types.ts
index 9b7ab8130..45bf2e12b 100644
--- a/app/types.ts
+++ b/app/types.ts
@@ -99,7 +99,7 @@ export type Action = {
placeholder?: ((context: ActionContext) => string) | string;
selected?: (context: ActionContext) => boolean;
visible?: (context: ActionContext) => boolean;
- perform?: (context: ActionContext) => void;
+ perform?: (context: ActionContext) => Promise | any;
children?: ((context: ActionContext) => Action[]) | Action[];
};
diff --git a/app/utils/routeHelpers.ts b/app/utils/routeHelpers.ts
index 7d2024635..904d4d8ca 100644
--- a/app/utils/routeHelpers.ts
+++ b/app/utils/routeHelpers.ts
@@ -97,14 +97,16 @@ export function updateDocumentUrl(oldUrl: string, document: Document): string {
}
export function newDocumentPath(
- collectionId: string,
+ collectionId?: string,
params: {
parentDocumentId?: string;
templateId?: string;
template?: boolean;
} = {}
): string {
- return `/collection/${collectionId}/new?${queryString.stringify(params)}`;
+ return collectionId
+ ? `/collection/${collectionId}/new?${queryString.stringify(params)}`
+ : `/doc/new`;
}
export function searchPath(
diff --git a/app/utils/tree.ts b/app/utils/tree.ts
new file mode 100644
index 000000000..313c376e0
--- /dev/null
+++ b/app/utils/tree.ts
@@ -0,0 +1,32 @@
+import { flatten } from "lodash";
+
+export const flattenTree = (root: any) => {
+ const flattened: any[] = [];
+ if (!root) {
+ return flattened;
+ }
+
+ flattened.push(root);
+
+ root.children.forEach((child: any) => {
+ flattened.push(flattenTree(child));
+ });
+
+ return flatten(flattened);
+};
+
+export const ancestors = (node: any) => {
+ const ancestors: any[] = [];
+ while (node.parent !== null) {
+ ancestors.unshift(node);
+ node = node.parent;
+ }
+ return ancestors;
+};
+
+export const descendants = (node: any, depth = 0) => {
+ const allDescendants = flattenTree(node).slice(1);
+ return depth === 0
+ ? allDescendants
+ : allDescendants.filter((d) => d.depth <= node.depth + depth);
+};
diff --git a/server/commands/documentCreator.ts b/server/commands/documentCreator.ts
index 6cb097b54..88ec04be4 100644
--- a/server/commands/documentCreator.ts
+++ b/server/commands/documentCreator.ts
@@ -7,7 +7,7 @@ type Props = {
title: string;
text: string;
publish?: boolean;
- collectionId?: string;
+ collectionId?: string | null;
parentDocumentId?: string | null;
importId?: string;
templateDocument?: Document | null;
diff --git a/server/commands/documentUpdater.ts b/server/commands/documentUpdater.ts
index 2253fbbcb..ab167f177 100644
--- a/server/commands/documentUpdater.ts
+++ b/server/commands/documentUpdater.ts
@@ -22,7 +22,7 @@ type Props = {
/** Whether the document should be published to the collection */
publish?: boolean;
/** The ID of the collection to publish the document to */
- collectionId?: string;
+ collectionId?: string | null;
/** The IP address of the user creating the document */
ip: string;
/** The database transaction to run within */
diff --git a/server/routes/api/documents/documents.test.ts b/server/routes/api/documents/documents.test.ts
index 3e580ca9f..e8c512a43 100644
--- a/server/routes/api/documents/documents.test.ts
+++ b/server/routes/api/documents/documents.test.ts
@@ -2238,6 +2238,19 @@ describe("#documents.create", () => {
expect(body.message).toEqual("collectionId: Invalid uuid");
});
+ it("should succeed if collectionId is null", async () => {
+ const { user } = await seed();
+ const res = await server.post("/api/documents.create", {
+ body: {
+ token: user.getJwtToken(),
+ collectionId: null,
+ title: "new document",
+ text: "hello",
+ },
+ });
+ expect(res.status).toEqual(200);
+ });
+
it("should fail for invalid parentDocumentId", async () => {
const { user, collection } = await seed();
const res = await server.post("/api/documents.create", {
diff --git a/server/routes/api/documents/schema.ts b/server/routes/api/documents/schema.ts
index db84bd48d..2d87baaf6 100644
--- a/server/routes/api/documents/schema.ts
+++ b/server/routes/api/documents/schema.ts
@@ -197,7 +197,7 @@ export const DocumentsUpdateSchema = BaseSchema.extend({
templateId: z.string().uuid().nullish(),
/** Doc collection Id */
- collectionId: z.string().uuid().optional(),
+ collectionId: z.string().uuid().nullish(),
/** Boolean to denote if text should be appended */
append: z.boolean().optional(),
@@ -279,7 +279,7 @@ export const DocumentsCreateSchema = BaseSchema.extend({
publish: z.boolean().optional(),
/** Create Doc under this collection */
- collectionId: z.string().uuid().optional(),
+ collectionId: z.string().uuid().nullish(),
/** Create Doc under this parent */
parentDocumentId: z.string().uuid().nullish(),
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index de4d63250..bd1611d1b 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -15,6 +15,7 @@
"New document": "New document",
"Publish": "Publish",
"Document published": "Document published",
+ "Publish document": "Publish document",
"Unpublish": "Unpublish",
"Document unpublished": "Document unpublished",
"Subscribe": "Subscribe",
@@ -198,13 +199,13 @@
"Click to retry": "Click to retry",
"Back": "Back",
"Documents": "Documents",
+ "Untitled": "Untitled",
"Results": "Results",
"No results for {{query}}": "No results for {{query}}",
"Logo": "Logo",
"Move document": "Move document",
"You can't reorder documents in an alphabetically sorted collection": "You can't reorder documents in an alphabetically sorted collection",
"Collections": "Collections",
- "Untitled": "Untitled",
"New nested document": "New nested document",
"Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word",
"Empty": "Empty",
@@ -443,7 +444,6 @@
"Done Editing": "Done Editing",
"New from template": "New from template",
"Restore version": "Restore version",
- "Publishing": "Publishing",
"No history yet": "No history yet",
"Stats": "Stats",
"{{ count }} minute read": "{{ count }} minute read",
@@ -512,6 +512,10 @@
"Couldn’t create the document, try again?": "Couldn’t create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the {{ documentTitle }} document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the {{ documentTitle }} document? This action is immediate and cannot be undone.",
+ "Select a location to publish": "Select a location to publish",
+ "Couldn’t publish the document, try again?": "Couldn’t publish the document, try again?",
+ "No results found": "No results found",
+ "Publish in {{ location }}": "Publish in {{ location }}",
"view and edit access": "view and edit access",
"view only access": "view only access",
"no access": "no access",
diff --git a/shared/utils/parseTitle.ts b/shared/utils/parseTitle.ts
index 27e4a5e29..e3bed8d65 100644
--- a/shared/utils/parseTitle.ts
+++ b/shared/utils/parseTitle.ts
@@ -17,8 +17,14 @@ export default function parseTitle(text = "") {
const startsWithEmoji = firstEmoji && title.startsWith(`${firstEmoji} `);
const emoji = startsWithEmoji ? firstEmoji : undefined;
+ // title with first leading emoji stripped
+ const strippedTitle = startsWithEmoji
+ ? title.replace(firstEmoji, "").trim()
+ : title;
+
return {
title,
emoji,
+ strippedTitle,
};
}
diff --git a/yarn.lock b/yarn.lock
index ec2496927..766ae3a55 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -158,7 +158,7 @@
dependencies:
"@babel/types" "^7.16.0"
-"@babel/helper-function-name@^7.16.0", "@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0":
+"@babel/helper-function-name@^7.16.0", "@babel/helper-function-name@^7.19.0":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c"
integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==
@@ -173,7 +173,7 @@
dependencies:
"@babel/types" "^7.18.6"
-"@babel/helper-member-expression-to-functions@^7.18.9", "@babel/helper-member-expression-to-functions@^7.20.7":
+"@babel/helper-member-expression-to-functions@^7.20.7":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz#a6f26e919582275a93c3aa6594756d71b0bb7f05"
integrity sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==
@@ -298,7 +298,7 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.11", "@babel/parser@^7.20.7", "@babel/parser@^7.7.0":
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.7", "@babel/parser@^7.7.0":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b"
integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==