Skip to content

Commit 98fe91a

Browse files
authored
add canvases (#33)
* add xy-flow * add live canvas features * add free-hand drawing * fix stroke impl, add more modes * add undo/redo * add color picker and selection improvements * add better zoom controls * refactor * selection fixes + minimap adjustment * improve multi select indicator * fix sidebar layout issue * improve color picker * add basic embedding and dnd * add resizing * add embedded content * add better prveiews with client side image generation for notes * add convex eslint plugin * fix ssr issue * add preview generation * fixes * improve canvas embeds * address comments * fixes * explicitly pass auth secret * fixes * more fixes * improvements * improvements * fix pre-mature throw * fix test * add resize awareness and fix canvas external file drop * address comments * fix flakey test * address comments again * fix claim token in tests
1 parent d35b5d0 commit 98fe91a

150 files changed

Lines changed: 10109 additions & 1574 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

convex/_generated/api.d.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ import type * as campaigns_mutations from "../campaigns/mutations.js";
5151
import type * as campaigns_queries from "../campaigns/queries.js";
5252
import type * as campaigns_types from "../campaigns/types.js";
5353
import type * as campaigns_validation from "../campaigns/validation.js";
54+
import type * as canvases_baseSchema from "../canvases/baseSchema.js";
55+
import type * as canvases_functions_createCanvas from "../canvases/functions/createCanvas.js";
56+
import type * as canvases_functions_enhanceCanvas from "../canvases/functions/enhanceCanvas.js";
57+
import type * as canvases_functions_updateCanvas from "../canvases/functions/updateCanvas.js";
58+
import type * as canvases_mutations from "../canvases/mutations.js";
59+
import type * as canvases_types from "../canvases/types.js";
5460
import type * as common_constants from "../common/constants.js";
5561
import type * as common_logger from "../common/logger.js";
5662
import type * as common_slug from "../common/slug.js";
@@ -101,7 +107,6 @@ import type * as notes_functions_createNote from "../notes/functions/createNote.
101107
import type * as notes_functions_enhanceNote from "../notes/functions/enhanceNote.js";
102108
import type * as notes_functions_getNote from "../notes/functions/getNote.js";
103109
import type * as notes_functions_updateNote from "../notes/functions/updateNote.js";
104-
import type * as notes_functions_updateNoteContent from "../notes/functions/updateNoteContent.js";
105110
import type * as notes_mutations from "../notes/mutations.js";
106111
import type * as notes_queries from "../notes/queries.js";
107112
import type * as notes_types from "../notes/types.js";
@@ -119,6 +124,7 @@ import type * as sessions_queries from "../sessions/queries.js";
119124
import type * as sessions_types from "../sessions/types.js";
120125
import type * as sidebarItems_functions_applyToDependents from "../sidebarItems/functions/applyToDependents.js";
121126
import type * as sidebarItems_functions_applyToTree from "../sidebarItems/functions/applyToTree.js";
127+
import type * as sidebarItems_functions_claimPreviewGeneration from "../sidebarItems/functions/claimPreviewGeneration.js";
122128
import type * as sidebarItems_functions_collectDescendants from "../sidebarItems/functions/collectDescendants.js";
123129
import type * as sidebarItems_functions_defaultItemName from "../sidebarItems/functions/defaultItemName.js";
124130
import type * as sidebarItems_functions_emptyTrashBin from "../sidebarItems/functions/emptyTrashBin.js";
@@ -131,6 +137,7 @@ import type * as sidebarItems_functions_hardDeleteItem from "../sidebarItems/fun
131137
import type * as sidebarItems_functions_moveSidebarItem from "../sidebarItems/functions/moveSidebarItem.js";
132138
import type * as sidebarItems_functions_permanentlyDeleteSidebarItem from "../sidebarItems/functions/permanentlyDeleteSidebarItem.js";
133139
import type * as sidebarItems_functions_purgeExpiredTrash from "../sidebarItems/functions/purgeExpiredTrash.js";
140+
import type * as sidebarItems_functions_setPreviewImage from "../sidebarItems/functions/setPreviewImage.js";
134141
import type * as sidebarItems_internalMutations from "../sidebarItems/internalMutations.js";
135142
import type * as sidebarItems_mutations from "../sidebarItems/mutations.js";
136143
import type * as sidebarItems_queries from "../sidebarItems/queries.js";
@@ -178,6 +185,7 @@ import type * as yjsSync_functions_compactUpdates from "../yjsSync/functions/com
178185
import type * as yjsSync_functions_createYjsDocument from "../yjsSync/functions/createYjsDocument.js";
179186
import type * as yjsSync_functions_deleteYjsDocument from "../yjsSync/functions/deleteYjsDocument.js";
180187
import type * as yjsSync_functions_reconstructYDoc from "../yjsSync/functions/reconstructYDoc.js";
188+
import type * as yjsSync_functions_types from "../yjsSync/functions/types.js";
181189
import type * as yjsSync_functions_uint8ToArrayBuffer from "../yjsSync/functions/uint8ToArrayBuffer.js";
182190
import type * as yjsSync_internalMutations from "../yjsSync/internalMutations.js";
183191
import type * as yjsSync_mutations from "../yjsSync/mutations.js";
@@ -233,6 +241,12 @@ declare const fullApi: ApiFromModules<{
233241
"campaigns/queries": typeof campaigns_queries;
234242
"campaigns/types": typeof campaigns_types;
235243
"campaigns/validation": typeof campaigns_validation;
244+
"canvases/baseSchema": typeof canvases_baseSchema;
245+
"canvases/functions/createCanvas": typeof canvases_functions_createCanvas;
246+
"canvases/functions/enhanceCanvas": typeof canvases_functions_enhanceCanvas;
247+
"canvases/functions/updateCanvas": typeof canvases_functions_updateCanvas;
248+
"canvases/mutations": typeof canvases_mutations;
249+
"canvases/types": typeof canvases_types;
236250
"common/constants": typeof common_constants;
237251
"common/logger": typeof common_logger;
238252
"common/slug": typeof common_slug;
@@ -283,7 +297,6 @@ declare const fullApi: ApiFromModules<{
283297
"notes/functions/enhanceNote": typeof notes_functions_enhanceNote;
284298
"notes/functions/getNote": typeof notes_functions_getNote;
285299
"notes/functions/updateNote": typeof notes_functions_updateNote;
286-
"notes/functions/updateNoteContent": typeof notes_functions_updateNoteContent;
287300
"notes/mutations": typeof notes_mutations;
288301
"notes/queries": typeof notes_queries;
289302
"notes/types": typeof notes_types;
@@ -301,6 +314,7 @@ declare const fullApi: ApiFromModules<{
301314
"sessions/types": typeof sessions_types;
302315
"sidebarItems/functions/applyToDependents": typeof sidebarItems_functions_applyToDependents;
303316
"sidebarItems/functions/applyToTree": typeof sidebarItems_functions_applyToTree;
317+
"sidebarItems/functions/claimPreviewGeneration": typeof sidebarItems_functions_claimPreviewGeneration;
304318
"sidebarItems/functions/collectDescendants": typeof sidebarItems_functions_collectDescendants;
305319
"sidebarItems/functions/defaultItemName": typeof sidebarItems_functions_defaultItemName;
306320
"sidebarItems/functions/emptyTrashBin": typeof sidebarItems_functions_emptyTrashBin;
@@ -313,6 +327,7 @@ declare const fullApi: ApiFromModules<{
313327
"sidebarItems/functions/moveSidebarItem": typeof sidebarItems_functions_moveSidebarItem;
314328
"sidebarItems/functions/permanentlyDeleteSidebarItem": typeof sidebarItems_functions_permanentlyDeleteSidebarItem;
315329
"sidebarItems/functions/purgeExpiredTrash": typeof sidebarItems_functions_purgeExpiredTrash;
330+
"sidebarItems/functions/setPreviewImage": typeof sidebarItems_functions_setPreviewImage;
316331
"sidebarItems/internalMutations": typeof sidebarItems_internalMutations;
317332
"sidebarItems/mutations": typeof sidebarItems_mutations;
318333
"sidebarItems/queries": typeof sidebarItems_queries;
@@ -360,6 +375,7 @@ declare const fullApi: ApiFromModules<{
360375
"yjsSync/functions/createYjsDocument": typeof yjsSync_functions_createYjsDocument;
361376
"yjsSync/functions/deleteYjsDocument": typeof yjsSync_functions_deleteYjsDocument;
362377
"yjsSync/functions/reconstructYDoc": typeof yjsSync_functions_reconstructYDoc;
378+
"yjsSync/functions/types": typeof yjsSync_functions_types;
363379
"yjsSync/functions/uint8ToArrayBuffer": typeof yjsSync_functions_uint8ToArrayBuffer;
364380
"yjsSync/internalMutations": typeof yjsSync_internalMutations;
365381
"yjsSync/mutations": typeof yjsSync_mutations;

convex/_test/factories.helper.ts

Lines changed: 103 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type schema from '../schema'
1414
import type {
1515
SidebarItemId,
1616
SidebarItemLocation,
17+
SidebarItemTable,
1718
SidebarItemType,
1819
} from '../sidebarItems/types/baseTypes'
1920
import type { PermissionLevel } from '../permissions/types'
@@ -141,6 +142,10 @@ const sidebarItemBase = (
141142
parentId: null
142143
allPermissionLevel: PermissionLevel | null
143144
location: SidebarItemLocation
145+
previewStorageId: null
146+
previewLockedUntil: null
147+
previewClaimToken: null
148+
previewUpdatedAt: null
144149
} & ReturnType<typeof commonFields> => ({
145150
name,
146151
slug: slugify(name),
@@ -150,147 +155,143 @@ const sidebarItemBase = (
150155
parentId: null,
151156
allPermissionLevel: null,
152157
location: SIDEBAR_ITEM_LOCATION.sidebar,
158+
previewStorageId: null,
159+
previewLockedUntil: null,
160+
previewClaimToken: null,
161+
previewUpdatedAt: null,
153162
...commonFields(creatorProfileId),
154163
})
155164

156-
export async function createNote(
165+
type CommonSidebarItemOverrides = Partial<{
166+
name: string
167+
slug: string
168+
parentId: Id<'folders'> | null
169+
allPermissionLevel: PermissionLevel | null
170+
location: SidebarItemLocation
171+
iconName: string | null
172+
color: string | null
173+
deletionTime: number | null
174+
deletedBy: Id<'userProfiles'> | null
175+
previewStorageId: Id<'_storage'> | null
176+
previewLockedUntil: number | null
177+
previewClaimToken: string | null
178+
previewUpdatedAt: number | null
179+
}>
180+
181+
async function insertSidebarItem<TTable extends SidebarItemTable>(
157182
t: T,
183+
table: TTable,
158184
campaignId: Id<'campaigns'>,
159185
creatorProfileId: Id<'userProfiles'>,
160-
overrides?: Partial<{
161-
name: string
162-
slug: string
163-
parentId: Id<'folders'> | null
164-
allPermissionLevel: PermissionLevel | null
165-
location: SidebarItemLocation
166-
iconName: string | null
167-
color: string | null
168-
deletionTime: number | null
169-
deletedBy: Id<'userProfiles'> | null
170-
}>,
186+
label: string,
187+
extraDefaults: Record<string, unknown>,
188+
overrides?: CommonSidebarItemOverrides & Record<string, unknown>,
171189
) {
172190
const n = nextId()
173-
const name = overrides?.name ?? `Note ${n}`
174-
const defaults = {
175-
...sidebarItemBase(campaignId, creatorProfileId, name),
176-
type: SIDEBAR_ITEM_TYPES.notes,
177-
}
191+
const name = overrides?.name ?? `${label} ${n}`
178192
const data = {
179-
...defaults,
193+
...sidebarItemBase(campaignId, creatorProfileId, name),
194+
...extraDefaults,
180195
...overrides,
181196
slug: overrides?.slug ?? slugify(name),
182197
}
183-
const noteId = await t.run(async (ctx) => {
184-
return await ctx.db.insert('notes', data)
185-
})
198+
199+
const id = await t.run(async (ctx) => ctx.db.insert(table, data as any))
200+
return { id: id as Id<TTable>, ...data }
201+
}
202+
203+
export async function createNote(
204+
t: T,
205+
campaignId: Id<'campaigns'>,
206+
creatorProfileId: Id<'userProfiles'>,
207+
overrides?: CommonSidebarItemOverrides,
208+
) {
209+
const { id: noteId, ...data } = await insertSidebarItem(
210+
t,
211+
'notes',
212+
campaignId,
213+
creatorProfileId,
214+
'Note',
215+
{ type: SIDEBAR_ITEM_TYPES.notes },
216+
overrides,
217+
)
186218
return { noteId, ...data }
187219
}
188220

189221
export async function createFolder(
190222
t: T,
191223
campaignId: Id<'campaigns'>,
192224
creatorProfileId: Id<'userProfiles'>,
193-
overrides?: Partial<{
194-
name: string
195-
slug: string
196-
parentId: Id<'folders'> | null
197-
inheritShares: boolean
198-
allPermissionLevel: PermissionLevel | null
199-
location: SidebarItemLocation
200-
iconName: string | null
201-
color: string | null
202-
deletionTime: number | null
203-
deletedBy: Id<'userProfiles'> | null
204-
}>,
225+
overrides?: CommonSidebarItemOverrides & Partial<{ inheritShares: boolean }>,
205226
) {
206-
const n = nextId()
207-
const name = overrides?.name ?? `Folder ${n}`
208-
const defaults = {
209-
...sidebarItemBase(campaignId, creatorProfileId, name),
210-
type: SIDEBAR_ITEM_TYPES.folders,
211-
inheritShares: false,
212-
}
213-
const data = {
214-
...defaults,
215-
...overrides,
216-
slug: overrides?.slug ?? slugify(name),
217-
}
218-
const folderId = await t.run(async (ctx) => {
219-
return await ctx.db.insert('folders', data)
220-
})
227+
const { id: folderId, ...data } = await insertSidebarItem(
228+
t,
229+
'folders',
230+
campaignId,
231+
creatorProfileId,
232+
'Folder',
233+
{ type: SIDEBAR_ITEM_TYPES.folders, inheritShares: false },
234+
overrides,
235+
)
221236
return { folderId, ...data }
222237
}
223238

224239
export async function createFile(
225240
t: T,
226241
campaignId: Id<'campaigns'>,
227242
creatorProfileId: Id<'userProfiles'>,
228-
overrides?: Partial<{
229-
name: string
230-
slug: string
231-
parentId: Id<'folders'> | null
232-
storageId: Id<'_storage'> | null
233-
allPermissionLevel: PermissionLevel | null
234-
location: SidebarItemLocation
235-
iconName: string | null
236-
color: string | null
237-
deletionTime: number | null
238-
deletedBy: Id<'userProfiles'> | null
239-
}>,
243+
overrides?: CommonSidebarItemOverrides &
244+
Partial<{ storageId: Id<'_storage'> | null }>,
240245
) {
241-
const n = nextId()
242-
const name = overrides?.name ?? `File ${n}`
243-
const defaults = {
244-
...sidebarItemBase(campaignId, creatorProfileId, name),
245-
type: SIDEBAR_ITEM_TYPES.files,
246-
storageId: null,
247-
}
248-
const data = {
249-
...defaults,
250-
...overrides,
251-
slug: overrides?.slug ?? slugify(name),
252-
}
253-
const fileId = await t.run(async (ctx) => {
254-
return await ctx.db.insert('files', data)
255-
})
246+
const { id: fileId, ...data } = await insertSidebarItem(
247+
t,
248+
'files',
249+
campaignId,
250+
creatorProfileId,
251+
'File',
252+
{ type: SIDEBAR_ITEM_TYPES.files, storageId: null },
253+
overrides,
254+
)
256255
return { fileId, ...data }
257256
}
258257

259258
export async function createGameMap(
260259
t: T,
261260
campaignId: Id<'campaigns'>,
262261
creatorProfileId: Id<'userProfiles'>,
263-
overrides?: Partial<{
264-
name: string
265-
slug: string
266-
parentId: Id<'folders'> | null
267-
imageStorageId: Id<'_storage'> | null
268-
allPermissionLevel: PermissionLevel | null
269-
location: SidebarItemLocation
270-
iconName: string | null
271-
color: string | null
272-
deletionTime: number | null
273-
deletedBy: Id<'userProfiles'> | null
274-
}>,
262+
overrides?: CommonSidebarItemOverrides &
263+
Partial<{ imageStorageId: Id<'_storage'> | null }>,
275264
) {
276-
const n = nextId()
277-
const name = overrides?.name ?? `Map ${n}`
278-
const defaults = {
279-
...sidebarItemBase(campaignId, creatorProfileId, name),
280-
type: SIDEBAR_ITEM_TYPES.gameMaps,
281-
imageStorageId: null,
282-
}
283-
const data = {
284-
...defaults,
285-
...overrides,
286-
slug: overrides?.slug ?? slugify(name),
287-
}
288-
const mapId = await t.run(async (ctx) => {
289-
return await ctx.db.insert('gameMaps', data)
290-
})
265+
const { id: mapId, ...data } = await insertSidebarItem(
266+
t,
267+
'gameMaps',
268+
campaignId,
269+
creatorProfileId,
270+
'Map',
271+
{ type: SIDEBAR_ITEM_TYPES.gameMaps, imageStorageId: null },
272+
overrides,
273+
)
291274
return { mapId, ...data }
292275
}
293276

277+
export async function createCanvas(
278+
t: T,
279+
campaignId: Id<'campaigns'>,
280+
creatorProfileId: Id<'userProfiles'>,
281+
overrides?: CommonSidebarItemOverrides,
282+
) {
283+
const { id: canvasId, ...data } = await insertSidebarItem(
284+
t,
285+
'canvases',
286+
campaignId,
287+
creatorProfileId,
288+
'Canvas',
289+
{ type: SIDEBAR_ITEM_TYPES.canvases },
290+
overrides,
291+
)
292+
return { canvasId, ...data }
293+
}
294+
294295
export async function createSession(
295296
t: T,
296297
campaignId: Id<'campaigns'>,

convex/auth/component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const createAuth = (ctx: GenericCtx<DataModel>) => {
4747
: {}
4848

4949
return betterAuth({
50+
secret: process.env.BETTER_AUTH_SECRET,
5051
baseURL: siteUrl,
5152
database: authComponent.adapter(ctx),
5253
session: {

convex/canvases/baseSchema.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { v } from 'convex/values'
2+
import { defineTable } from 'convex/server'
3+
import {
4+
commonSidebarItemTableFields,
5+
commonSidebarItemValidatorFields,
6+
} from '../sidebarItems/schema/baseFields'
7+
import { commonValidatorFields } from '../common/schema'
8+
import { SIDEBAR_ITEM_TYPES } from '../sidebarItems/types/baseTypes'
9+
10+
export const canvasTableFields = {
11+
...commonSidebarItemTableFields,
12+
type: v.literal(SIDEBAR_ITEM_TYPES.canvases),
13+
}
14+
15+
export const canvasValidatorFields = {
16+
...commonValidatorFields('canvases'),
17+
...commonSidebarItemValidatorFields,
18+
type: v.literal(SIDEBAR_ITEM_TYPES.canvases),
19+
}
20+
21+
export const canvasValidator = v.object(canvasValidatorFields)
22+
23+
export const canvasesTables = {
24+
canvases: defineTable({
25+
...canvasTableFields,
26+
})
27+
.index('by_campaign_location_parent_name', [
28+
'campaignId',
29+
'location',
30+
'parentId',
31+
'name',
32+
])
33+
.index('by_campaign_slug', ['campaignId', 'slug'])
34+
.index('by_campaign_deletionTime', ['campaignId', 'deletionTime']),
35+
}

0 commit comments

Comments
 (0)