Files
outline/app/scenes/Collection/components/Overview.tsx
T
Hemachandar d3eb3db7ba feat: Public sharing of collections (#9529)
* shares.info, collections.info, documents.info

* shares.list, shares.create, shares.update

* shares.sitemap

* parity with existing document shared screen

* collection share popover

* parent share and table

* collection scene

* collection link in sidebar

* sidebar and breadcrumb collection link click

* collection link click in editor

* meta

* more meta + 404 page

* map internal link, remove showLastUpdated option

* fix shares.list pagination

* show last updated

* shareLoader tests

* lint

* sidebar context for collection link

* badge in shares table

* fix existing tests

* tsc

* update failing test snapshot

* env

* signed url for collection attachments

* include collection content in SSR for screen readers

* search

* drafts can be shared

* review

* tsc, remove old shared-doc scene

* tweaks

* DRY

* refactor loader

* Remove share/collection urls

* fix: Collection overview should not be editable when viewing shared link and logged in

* Tweak public breadcrumb

* fix: Deleted documents should never be exposed through share

* empty sharedTree array where includeChildDocuments is false

* revert includeChildDocs guard for logical correctness + SSR bug fix

* fix: check document is part of share

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-03 13:07:39 -04:00

111 lines
3.2 KiB
TypeScript

import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import { useMemo, useRef, useCallback, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { richExtensions } from "@shared/editor/nodes";
import { s } from "@shared/styles";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import { CollectionValidation } from "@shared/validations";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import Editor from "~/components/Editor";
import LoadingIndicator from "~/components/LoadingIndicator";
import Text from "~/components/Text";
import { withUIExtensions } from "~/editor/extensions";
import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { Properties } from "~/types";
const extensions = withUIExtensions(richExtensions);
type Props = {
collection: Collection;
shareId?: string;
};
function Overview({ collection, shareId }: Props) {
const { documents, collections } = useStores();
const { t } = useTranslation();
const user = useCurrentUser({ rejectOnEmpty: false });
const can = usePolicy(collection);
const handleSave = useMemo(
() =>
debounce(async (getValue) => {
try {
await collection.save({
data: getValue(false),
});
} catch (err) {
toast.error(t("Sorry, an error occurred saving the collection"));
throw err;
}
}, 1000),
[collection, t]
);
const childRef = useRef<HTMLDivElement>(null);
const childOffsetHeight = childRef.current?.offsetHeight || 0;
const editorStyle = useMemo(
() => ({
padding: "0 32px",
margin: "0 -32px",
paddingBottom: `calc(50vh - ${childOffsetHeight}px)`,
}),
[childOffsetHeight]
);
const onCreateLink = useCallback(
async (params: Properties<Document>) => {
const newDocument = await documents.create(
{
collectionId: collection.id,
data: ProsemirrorHelper.getEmptyDocument(),
...params,
},
{
publish: true,
}
);
return newDocument.url;
},
[collection, documents]
);
return (
<>
{collections.isSaving && <LoadingIndicator />}
{(collection.hasDescription || can.update) && (
<Suspense fallback={<Placeholder>Loading</Placeholder>}>
<Editor
defaultValue={collection.data}
onChange={handleSave}
placeholder={`${t("Add a description")}`}
extensions={extensions}
maxLength={CollectionValidation.maxDescriptionLength}
onCreateLink={onCreateLink}
canUpdate={can.update}
readOnly={!can.update || !!shareId}
userId={user?.id}
editorStyle={editorStyle}
shareId={shareId}
/>
<div ref={childRef} />
</Suspense>
)}
</>
);
}
const Placeholder = styled(Text)`
color: ${s("placeholder")};
cursor: text;
min-height: 27px;
`;
export default observer(Overview);