Compare commits

..

1 Commits

Author SHA1 Message Date
Tom Moor 7c8ba94551 fix: Subtle collection loading bug 2025-05-01 23:20:28 -04:00
18 changed files with 123 additions and 156 deletions
-1
View File
@@ -321,7 +321,6 @@ 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 = collectionId ? collections.get(collectionId) : undefined;
const collection = collections.get(collectionId);
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = starredSidebarContext(
star.documentId ?? star.collectionId ?? ""
star.documentId ?? star.collectionId
);
const [expanded, setExpanded] = useState(
(star.documentId
+1 -6
View File
@@ -7,7 +7,6 @@ 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";
@@ -242,16 +241,12 @@ 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 - safeAreaInsets.bottom
}px)`,
bottom: `calc(100% - ${height - rect.y}px)`,
}}
>
{props.children}
+1 -2
View File
@@ -2,8 +2,7 @@ import Extension from "@shared/editor/lib/Extension";
import { InputRule } from "@shared/editor/lib/InputRule";
const rightArrow = new InputRule(/->$/, "→");
// Note that the suppression of pipe here prevents conflict with table creation rule.
const emdash = new InputRule(/(?:^|[^\|])(--)$/, "—");
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
View File
@@ -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" })
+36 -20
View File
@@ -13,6 +13,7 @@ import {
Transaction,
Op,
FindOptions,
ScopeOptions,
WhereOptions,
EmptyResultError,
} from "sequelize";
@@ -105,6 +106,20 @@ 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"],
@@ -154,23 +169,13 @@ type AdditionalFindOptions = {
],
};
},
withMembership: (userId: string, paranoid = true) => {
withMembership: (userId: string) => {
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: {
@@ -632,15 +637,21 @@ class Document extends ArchivableModel<
return uniq(membershipUserIds);
}
static withMembershipScope(userId: string, options?: FindOptions<Document>) {
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],
};
return this.scope([
"defaultScope",
{
method: ["withViews", userId],
},
{
method: ["withMembership", userId, options?.paranoid],
},
collectionScope,
viewScope,
membershipScope,
]);
}
@@ -674,12 +685,14 @@ class Document extends ArchivableModel<
// almost every endpoint needs the collection membership to determine policy permissions.
const scope = this.scope([
"withDrafts",
options.includeState ? "withState" : "withoutState",
{
method: ["withCollectionPermissions", userId, rest.paranoid],
},
{
method: ["withViews", userId],
},
{
method: ["withMembership", userId, rest.paranoid],
method: ["withMembership", userId],
},
]);
@@ -737,6 +750,9 @@ 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],
},
+36 -18
View File
@@ -182,16 +182,25 @@ export default class SearchHelper {
},
];
return Document.withMembershipScope(user.id)
.scope("withDrafts")
.findAll({
where,
subQuery: false,
order: [["updatedAt", "DESC"]],
include,
offset,
limit,
});
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,
});
}
public static async searchCollectionsForUser(
@@ -264,14 +273,23 @@ export default class SearchHelper {
// Final query to get associated document data
const [documents, count] = await Promise.all([
Document.withMembershipScope(user.id)
.scope("withDrafts")
.findAll({
where: {
teamId: user.teamId,
id: map(results, "id"),
},
}),
Document.scope([
"withDrafts",
{
method: ["withViews", user.id],
},
{
method: ["withCollectionPermissions", user.id],
},
{
method: ["withMembership", user.id],
},
]).findAll({
where: {
teamId: user.teamId,
id: map(results, "id"),
},
}),
results.length < limit && offset === 0
? Promise.resolve(results.length)
: countQuery,
+19 -11
View File
@@ -268,7 +268,7 @@ router.post(
}
const [documents, total] = await Promise.all([
Document.withMembershipScope(user.id).findAll({
Document.defaultScopeWithUser(user.id).findAll({
where,
order: [
[
@@ -348,7 +348,7 @@ router.post(
};
}
const documents = await Document.withMembershipScope(user.id).findAll({
const documents = await Document.defaultScopeWithUser(user.id).findAll({
where,
order: [
[
@@ -397,11 +397,15 @@ 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({
@@ -535,14 +539,12 @@ router.post(
delete where.updatedAt;
}
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 documents = await Document.defaultScopeWithUser(user.id).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))
);
@@ -2031,7 +2033,13 @@ router.post(
const collectionIds = await user.collectionIds({
paranoid: false,
});
const documents = await Document.scope("withDrafts").findAll({
const collectionScope: Readonly<ScopeOptions> = {
method: ["withCollectionPermissions", user.id],
};
const documents = await Document.scope([
collectionScope,
"withDrafts",
]).findAll({
attributes: ["id"],
where: {
deletedAt: {
@@ -24,7 +24,6 @@ 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: {
@@ -45,7 +44,7 @@ router.post(
association: "groupUsers",
required: true,
where: {
userId,
userId: user.id,
},
},
],
@@ -58,13 +57,15 @@ router.post(
const documentIds = memberships
.map((p) => p.documentId)
.filter(Boolean) as string[];
const documents = await Document.withMembershipScope(userId)
.scope("withDrafts")
.findAll({
where: {
id: documentIds,
},
});
const documents = await Document.scope([
"withDrafts",
{ method: ["withMembership", user.id] },
{ method: ["withCollectionPermissions", user.id] },
]).findAll({
where: {
id: documentIds,
},
});
const groups = uniqBy(
memberships.map((membership) => membership.group),
+1 -1
View File
@@ -113,7 +113,7 @@ router.post(
user.collectionIds(),
]);
const documents = await Document.withMembershipScope(user.id).findAll({
const documents = await Document.defaultScopeWithUser(user.id).findAll({
where: {
id: pins.map((pin) => pin.documentId),
collectionId: collectionIds,
+1 -1
View File
@@ -94,7 +94,7 @@ router.post(
.map((star) => star.documentId)
.filter(Boolean) as string[];
const documents = documentIds.length
? await Document.withMembershipScope(user.id).findAll({
? await Document.defaultScopeWithUser(user.id).findAll({
where: {
id: documentIds,
collectionId: collectionIds,
@@ -1,4 +1,5 @@
import Router from "koa-router";
import { Op, Sequelize } from "sequelize";
import auth from "@server/middlewares/authentication";
import { transaction } from "@server/middlewares/transaction";
@@ -45,8 +46,14 @@ router.post(
const documentIds = memberships
.map((p) => p.documentId)
.filter(Boolean) as string[];
const documents = await Document.findByIds(documentIds, {
userId: user.id,
const documents = await Document.scope([
"withDrafts",
{ method: ["withMembership", user.id] },
{ method: ["withCollectionPermissions", user.id] },
]).findAll({
where: {
id: documentIds,
},
});
const policies = presentPolicies(user, [...documents, ...memberships]);
+4 -1
View File
@@ -99,7 +99,10 @@ export const getDocumentPermission = async ({
documentId: string;
skipMembershipId?: string;
}): Promise<DocumentPermission | undefined> => {
const document = await Document.findByPk(documentId, { userId });
const document = await Document.scope({
method: ["withCollectionPermissions", userId],
}).findOne({ where: { id: documentId } });
const permissions: DocumentPermission[] = [];
const collection = document?.collection;
+2 -2
View File
@@ -44,11 +44,11 @@ export function createTable({
};
}
export function createTableInner(
function createTableInner(
state: EditorState,
rowsCount: number,
colsCount: number,
colWidth?: number,
colWidth: number,
withHeaderRow = true,
cellContent?: Node
) {
-15
View File
@@ -1,7 +1,5 @@
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,
@@ -25,7 +23,6 @@ import {
deleteColSelection,
deleteRowSelection,
moveOutOfTable,
createTableInner,
} from "../commands/table";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
import { FixTablesPlugin } from "../plugins/FixTables";
@@ -104,18 +101,6 @@ 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);
-7
View File
@@ -113,11 +113,4 @@ 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);
}
`;
-26
View File
@@ -13,32 +13,6 @@ 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.
*/
-31
View File
@@ -94,9 +94,6 @@ export default () =>
modifyURLPrefix: {
"": `${environment.CDN_URL ?? ""}/static/`,
},
skipWaiting: true,
clientsClaim: true,
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /api\/urls\.unfurl$/,
@@ -112,34 +109,6 @@ 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: {