mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e94d4722e4 | |||
| e30829e68a | |||
| 0b27487c61 | |||
| aa5bf19134 | |||
| c0c36bacbb | |||
| 7bd1ea7c40 | |||
| 5ebb1e8a61 | |||
| 96d6987858 | |||
| 3602198cd8 |
@@ -321,6 +321,7 @@ const Container = styled(Flex)<ContainerProps>`
|
||||
z-index: ${depths.mobileSidebar};
|
||||
max-width: 80%;
|
||||
min-width: 280px;
|
||||
padding-left: var(--sal);
|
||||
${fadeOnDesktopBackgrounded()}
|
||||
|
||||
@media print {
|
||||
|
||||
@@ -38,10 +38,10 @@ function StarredLink({ star }: Props) {
|
||||
const { ui, collections, documents } = useStores();
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
const { documentId, collectionId } = star;
|
||||
const collection = collections.get(collectionId);
|
||||
const collection = collectionId ? collections.get(collectionId) : undefined;
|
||||
const locationSidebarContext = useLocationSidebarContext();
|
||||
const sidebarContext = starredSidebarContext(
|
||||
star.documentId ?? star.collectionId
|
||||
star.documentId ?? star.collectionId ?? ""
|
||||
);
|
||||
const [expanded, setExpanded] = useState(
|
||||
(star.documentId
|
||||
|
||||
@@ -7,6 +7,7 @@ import { isCode } from "@shared/editor/lib/isCode";
|
||||
import { findParentNode } from "@shared/editor/queries/findParentNode";
|
||||
import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
|
||||
import { depths, s } from "@shared/styles";
|
||||
import { getSafeAreaInsets } from "@shared/utils/browser";
|
||||
import { HEADER_HEIGHT } from "~/components/Header";
|
||||
import { Portal } from "~/components/Portal";
|
||||
import useEventListener from "~/hooks/useEventListener";
|
||||
@@ -241,12 +242,16 @@ const FloatingToolbar = React.forwardRef(function FloatingToolbar_(
|
||||
|
||||
if (props.active) {
|
||||
const rect = document.body.getBoundingClientRect();
|
||||
const safeAreaInsets = getSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<ReactPortal>
|
||||
<MobileWrapper
|
||||
ref={menuRef}
|
||||
style={{
|
||||
bottom: `calc(100% - ${height - rect.y}px)`,
|
||||
bottom: `calc(100% - ${
|
||||
height - rect.y - safeAreaInsets.bottom
|
||||
}px)`,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -2,7 +2,8 @@ import Extension from "@shared/editor/lib/Extension";
|
||||
import { InputRule } from "@shared/editor/lib/InputRule";
|
||||
|
||||
const rightArrow = new InputRule(/->$/, "→");
|
||||
const emdash = new InputRule(/--$/, "—");
|
||||
// Note that the suppression of pipe here prevents conflict with table creation rule.
|
||||
const emdash = new InputRule(/(?:^|[^\|])(--)$/, "—");
|
||||
const oneHalf = new InputRule(/(?:^|\s)(1\/2)$/, "½");
|
||||
const threeQuarters = new InputRule(/(?:^|\s)(3\/4)$/, "¾");
|
||||
const copyright = new InputRule(/\(c\)$/, "©️");
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ class Star extends Model {
|
||||
document?: Document;
|
||||
|
||||
/** The collection ID that is starred. */
|
||||
collectionId: string;
|
||||
collectionId?: string;
|
||||
|
||||
/** The collection that is starred. */
|
||||
@Relation(() => Collection, { onDelete: "cascade" })
|
||||
|
||||
@@ -66,7 +66,6 @@ const CollectionScene = observer(function _CollectionScene() {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const { documents, collections, ui } = useStores();
|
||||
const [isFetching, setFetching] = React.useState(false);
|
||||
const [error, setError] = React.useState<Error | undefined>();
|
||||
const currentPath = location.pathname;
|
||||
const [, setLastVisitedPath] = useLastVisitedPath();
|
||||
@@ -120,21 +119,16 @@ const CollectionScene = observer(function _CollectionScene() {
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchData() {
|
||||
if ((!can || !collection) && !error && !isFetching) {
|
||||
try {
|
||||
setError(undefined);
|
||||
setFetching(true);
|
||||
await collections.fetch(id);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setFetching(false);
|
||||
}
|
||||
try {
|
||||
setError(undefined);
|
||||
await collections.fetch(id);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
}
|
||||
|
||||
void fetchData();
|
||||
}, [collections, isFetching, collection, error, id, can]);
|
||||
}, []);
|
||||
|
||||
useCommandBarActions([editCollection], [ui.activeCollectionId ?? "none"]);
|
||||
|
||||
|
||||
@@ -186,6 +186,13 @@ export default class CollectionsStore extends Store<Collection> {
|
||||
statusFilter: [CollectionStatusFilter.Archived],
|
||||
});
|
||||
|
||||
get(id: string): Collection | undefined {
|
||||
return (
|
||||
this.data.get(id) ??
|
||||
this.orderedData.find((collection) => id.endsWith(collection.urlId))
|
||||
);
|
||||
}
|
||||
|
||||
@computed
|
||||
get archived(): Collection[] {
|
||||
return orderBy(this.orderedData, "archivedAt", "desc").filter(
|
||||
|
||||
+20
-36
@@ -13,7 +13,6 @@ import {
|
||||
Transaction,
|
||||
Op,
|
||||
FindOptions,
|
||||
ScopeOptions,
|
||||
WhereOptions,
|
||||
EmptyResultError,
|
||||
} from "sequelize";
|
||||
@@ -106,20 +105,6 @@ type AdditionalFindOptions = {
|
||||
},
|
||||
}))
|
||||
@Scopes(() => ({
|
||||
withCollectionPermissions: (userId: string, paranoid = true) => ({
|
||||
include: [
|
||||
{
|
||||
attributes: ["id", "permission", "sharing", "teamId", "deletedAt"],
|
||||
model: userId
|
||||
? Collection.scope({
|
||||
method: ["withMembership", userId],
|
||||
})
|
||||
: Collection,
|
||||
as: "collection",
|
||||
paranoid,
|
||||
},
|
||||
],
|
||||
}),
|
||||
withoutState: {
|
||||
attributes: {
|
||||
exclude: ["state"],
|
||||
@@ -169,13 +154,23 @@ type AdditionalFindOptions = {
|
||||
],
|
||||
};
|
||||
},
|
||||
withMembership: (userId: string) => {
|
||||
withMembership: (userId: string, paranoid = true) => {
|
||||
if (!userId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
include: [
|
||||
{
|
||||
attributes: ["id", "permission", "sharing", "teamId", "deletedAt"],
|
||||
model: userId
|
||||
? Collection.scope({
|
||||
method: ["withMembership", userId],
|
||||
})
|
||||
: Collection,
|
||||
as: "collection",
|
||||
paranoid,
|
||||
},
|
||||
{
|
||||
association: "memberships",
|
||||
where: {
|
||||
@@ -637,21 +632,15 @@ class Document extends ArchivableModel<
|
||||
return uniq(membershipUserIds);
|
||||
}
|
||||
|
||||
static defaultScopeWithUser(userId: string) {
|
||||
const collectionScope: Readonly<ScopeOptions> = {
|
||||
method: ["withCollectionPermissions", userId],
|
||||
};
|
||||
const viewScope: Readonly<ScopeOptions> = {
|
||||
method: ["withViews", userId],
|
||||
};
|
||||
const membershipScope: Readonly<ScopeOptions> = {
|
||||
method: ["withMembership", userId],
|
||||
};
|
||||
static withMembershipScope(userId: string, options?: FindOptions<Document>) {
|
||||
return this.scope([
|
||||
"defaultScope",
|
||||
collectionScope,
|
||||
viewScope,
|
||||
membershipScope,
|
||||
{
|
||||
method: ["withViews", userId],
|
||||
},
|
||||
{
|
||||
method: ["withMembership", userId, options?.paranoid],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -685,14 +674,12 @@ class Document extends ArchivableModel<
|
||||
// almost every endpoint needs the collection membership to determine policy permissions.
|
||||
const scope = this.scope([
|
||||
"withDrafts",
|
||||
{
|
||||
method: ["withCollectionPermissions", userId, rest.paranoid],
|
||||
},
|
||||
options.includeState ? "withState" : "withoutState",
|
||||
{
|
||||
method: ["withViews", userId],
|
||||
},
|
||||
{
|
||||
method: ["withMembership", userId],
|
||||
method: ["withMembership", userId, rest.paranoid],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -750,9 +737,6 @@ class Document extends ArchivableModel<
|
||||
const user = userId ? await User.findByPk(userId) : null;
|
||||
const documents = await this.scope([
|
||||
"withDrafts",
|
||||
{
|
||||
method: ["withCollectionPermissions", userId, rest.paranoid],
|
||||
},
|
||||
{
|
||||
method: ["withViews", userId],
|
||||
},
|
||||
|
||||
@@ -182,25 +182,16 @@ export default class SearchHelper {
|
||||
},
|
||||
];
|
||||
|
||||
return Document.scope([
|
||||
"withDrafts",
|
||||
{
|
||||
method: ["withViews", user.id],
|
||||
},
|
||||
{
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
},
|
||||
{
|
||||
method: ["withMembership", user.id],
|
||||
},
|
||||
]).findAll({
|
||||
where,
|
||||
subQuery: false,
|
||||
order: [["updatedAt", "DESC"]],
|
||||
include,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
return Document.withMembershipScope(user.id)
|
||||
.scope("withDrafts")
|
||||
.findAll({
|
||||
where,
|
||||
subQuery: false,
|
||||
order: [["updatedAt", "DESC"]],
|
||||
include,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
public static async searchCollectionsForUser(
|
||||
@@ -273,23 +264,14 @@ export default class SearchHelper {
|
||||
|
||||
// Final query to get associated document data
|
||||
const [documents, count] = await Promise.all([
|
||||
Document.scope([
|
||||
"withDrafts",
|
||||
{
|
||||
method: ["withViews", user.id],
|
||||
},
|
||||
{
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
},
|
||||
{
|
||||
method: ["withMembership", user.id],
|
||||
},
|
||||
]).findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
id: map(results, "id"),
|
||||
},
|
||||
}),
|
||||
Document.withMembershipScope(user.id)
|
||||
.scope("withDrafts")
|
||||
.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
id: map(results, "id"),
|
||||
},
|
||||
}),
|
||||
results.length < limit && offset === 0
|
||||
? Promise.resolve(results.length)
|
||||
: countQuery,
|
||||
|
||||
@@ -268,7 +268,7 @@ router.post(
|
||||
}
|
||||
|
||||
const [documents, total] = await Promise.all([
|
||||
Document.defaultScopeWithUser(user.id).findAll({
|
||||
Document.withMembershipScope(user.id).findAll({
|
||||
where,
|
||||
order: [
|
||||
[
|
||||
@@ -348,7 +348,7 @@ router.post(
|
||||
};
|
||||
}
|
||||
|
||||
const documents = await Document.defaultScopeWithUser(user.id).findAll({
|
||||
const documents = await Document.withMembershipScope(user.id).findAll({
|
||||
where,
|
||||
order: [
|
||||
[
|
||||
@@ -397,15 +397,11 @@ router.post(
|
||||
const membershipScope: Readonly<ScopeOptions> = {
|
||||
method: ["withMembership", user.id],
|
||||
};
|
||||
const collectionScope: Readonly<ScopeOptions> = {
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
};
|
||||
const viewScope: Readonly<ScopeOptions> = {
|
||||
method: ["withViews", user.id],
|
||||
};
|
||||
const documents = await Document.scope([
|
||||
membershipScope,
|
||||
collectionScope,
|
||||
viewScope,
|
||||
"withDrafts",
|
||||
]).findAll({
|
||||
@@ -539,12 +535,14 @@ router.post(
|
||||
delete where.updatedAt;
|
||||
}
|
||||
|
||||
const documents = await Document.defaultScopeWithUser(user.id).findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const documents = await Document.withMembershipScope(user.id)
|
||||
.scope("withDrafts")
|
||||
.findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
documents.map((document) => presentDocument(ctx, document))
|
||||
);
|
||||
@@ -2033,13 +2031,7 @@ router.post(
|
||||
const collectionIds = await user.collectionIds({
|
||||
paranoid: false,
|
||||
});
|
||||
const collectionScope: Readonly<ScopeOptions> = {
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
};
|
||||
const documents = await Document.scope([
|
||||
collectionScope,
|
||||
"withDrafts",
|
||||
]).findAll({
|
||||
const documents = await Document.scope("withDrafts").findAll({
|
||||
attributes: ["id"],
|
||||
where: {
|
||||
deletedAt: {
|
||||
|
||||
@@ -24,6 +24,7 @@ router.post(
|
||||
async (ctx: APIContext<T.GroupMembershipsListReq>) => {
|
||||
const { groupId } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
const userId = user.id;
|
||||
|
||||
const memberships = await GroupMembership.findAll({
|
||||
where: {
|
||||
@@ -44,7 +45,7 @@ router.post(
|
||||
association: "groupUsers",
|
||||
required: true,
|
||||
where: {
|
||||
userId: user.id,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -57,15 +58,13 @@ router.post(
|
||||
const documentIds = memberships
|
||||
.map((p) => p.documentId)
|
||||
.filter(Boolean) as string[];
|
||||
const documents = await Document.scope([
|
||||
"withDrafts",
|
||||
{ method: ["withMembership", user.id] },
|
||||
{ method: ["withCollectionPermissions", user.id] },
|
||||
]).findAll({
|
||||
where: {
|
||||
id: documentIds,
|
||||
},
|
||||
});
|
||||
const documents = await Document.withMembershipScope(userId)
|
||||
.scope("withDrafts")
|
||||
.findAll({
|
||||
where: {
|
||||
id: documentIds,
|
||||
},
|
||||
});
|
||||
|
||||
const groups = uniqBy(
|
||||
memberships.map((membership) => membership.group),
|
||||
|
||||
@@ -113,7 +113,7 @@ router.post(
|
||||
user.collectionIds(),
|
||||
]);
|
||||
|
||||
const documents = await Document.defaultScopeWithUser(user.id).findAll({
|
||||
const documents = await Document.withMembershipScope(user.id).findAll({
|
||||
where: {
|
||||
id: pins.map((pin) => pin.documentId),
|
||||
collectionId: collectionIds,
|
||||
|
||||
@@ -94,7 +94,7 @@ router.post(
|
||||
.map((star) => star.documentId)
|
||||
.filter(Boolean) as string[];
|
||||
const documents = documentIds.length
|
||||
? await Document.defaultScopeWithUser(user.id).findAll({
|
||||
? await Document.withMembershipScope(user.id).findAll({
|
||||
where: {
|
||||
id: documentIds,
|
||||
collectionId: collectionIds,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Router from "koa-router";
|
||||
|
||||
import { Op, Sequelize } from "sequelize";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { transaction } from "@server/middlewares/transaction";
|
||||
@@ -46,14 +45,8 @@ router.post(
|
||||
const documentIds = memberships
|
||||
.map((p) => p.documentId)
|
||||
.filter(Boolean) as string[];
|
||||
const documents = await Document.scope([
|
||||
"withDrafts",
|
||||
{ method: ["withMembership", user.id] },
|
||||
{ method: ["withCollectionPermissions", user.id] },
|
||||
]).findAll({
|
||||
where: {
|
||||
id: documentIds,
|
||||
},
|
||||
const documents = await Document.findByIds(documentIds, {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const policies = presentPolicies(user, [...documents, ...memberships]);
|
||||
|
||||
@@ -99,10 +99,7 @@ export const getDocumentPermission = async ({
|
||||
documentId: string;
|
||||
skipMembershipId?: string;
|
||||
}): Promise<DocumentPermission | undefined> => {
|
||||
const document = await Document.scope({
|
||||
method: ["withCollectionPermissions", userId],
|
||||
}).findOne({ where: { id: documentId } });
|
||||
|
||||
const document = await Document.findByPk(documentId, { userId });
|
||||
const permissions: DocumentPermission[] = [];
|
||||
|
||||
const collection = document?.collection;
|
||||
|
||||
@@ -44,11 +44,11 @@ export function createTable({
|
||||
};
|
||||
}
|
||||
|
||||
function createTableInner(
|
||||
export function createTableInner(
|
||||
state: EditorState,
|
||||
rowsCount: number,
|
||||
colsCount: number,
|
||||
colWidth: number,
|
||||
colWidth?: number,
|
||||
withHeaderRow = true,
|
||||
cellContent?: Node
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { chainCommands } from "prosemirror-commands";
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { NodeSpec, Node as ProsemirrorNode } from "prosemirror-model";
|
||||
import { TextSelection } from "prosemirror-state";
|
||||
import {
|
||||
addColumnAfter,
|
||||
addRowAfter,
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
deleteColSelection,
|
||||
deleteRowSelection,
|
||||
moveOutOfTable,
|
||||
createTableInner,
|
||||
} from "../commands/table";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import { FixTablesPlugin } from "../plugins/FixTables";
|
||||
@@ -101,6 +104,18 @@ export default class Table extends Node {
|
||||
};
|
||||
}
|
||||
|
||||
inputRules() {
|
||||
return [
|
||||
new InputRule(/^(\|--)$/, (state, _, start, end) => {
|
||||
const nodes = createTableInner(state, 2, 2);
|
||||
const tr = state.tr.replaceWith(start - 1, end, nodes).scrollIntoView();
|
||||
const resolvedPos = tr.doc.resolve(start + 1);
|
||||
tr.setSelection(TextSelection.near(resolvedPos));
|
||||
return tr;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
|
||||
state.renderTable(node);
|
||||
state.closeBlock(node);
|
||||
|
||||
@@ -113,4 +113,11 @@ export default createGlobalStyle<Props>`
|
||||
outline-offset: -1px;
|
||||
outline-width: initial;
|
||||
}
|
||||
|
||||
:root {
|
||||
--sat: env(safe-area-inset-top);
|
||||
--sar: env(safe-area-inset-right);
|
||||
--sab: env(safe-area-inset-bottom);
|
||||
--sal: env(safe-area-inset-left);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -13,6 +13,32 @@ export function isTouchDevice(): boolean {
|
||||
return window.matchMedia?.("(hover: none) and (pointer: coarse)")?.matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the safe area insets for the current device.
|
||||
*/
|
||||
export function getSafeAreaInsets(): {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
} {
|
||||
// Check if CSS environment variables are supported
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const supportsEnv = window.CSS?.supports?.("top", "env(safe-area-inset-top)");
|
||||
|
||||
if (supportsEnv) {
|
||||
return {
|
||||
top: parseFloat(style.getPropertyValue("--sat") || "0"),
|
||||
right: parseFloat(style.getPropertyValue("--sar") || "0"),
|
||||
bottom: parseFloat(style.getPropertyValue("--sab") || "0"),
|
||||
left: parseFloat(style.getPropertyValue("--sal") || "0"),
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to zero if not supported
|
||||
return { top: 0, right: 0, bottom: 0, left: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the client is running on a Mac.
|
||||
*/
|
||||
|
||||
@@ -94,6 +94,9 @@ export default () =>
|
||||
modifyURLPrefix: {
|
||||
"": `${environment.CDN_URL ?? ""}/static/`,
|
||||
},
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
cleanupOutdatedCaches: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /api\/urls\.unfurl$/,
|
||||
@@ -109,6 +112,34 @@ export default () =>
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /api\/attachments\.redirect/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "attachments-redirect-cache",
|
||||
expiration: {
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 120, // 120 seconds
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200, 302], // Include redirects
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /api\/files\.get/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "files-cache",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 604800, // 7 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200, 206], // Include partial content for range requests
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: {
|
||||
|
||||
Reference in New Issue
Block a user