From 5c42c3e281469f3763192534001f858a0267bdf9 Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Thu, 21 Dec 2023 12:43:17 +0100 Subject: [PATCH] feat: Content context --- .../src/components/primitives/sortable.tsx | 2 +- apps/web/src/context/cache.tsx | 32 -- apps/web/src/context/client.tsx | 5 +- apps/web/src/context/content/actions.ts | 326 ++++++++++++++++++ apps/web/src/context/content/index.tsx | 204 +++++++++++ apps/web/src/context/content/loader.ts | 111 ++++++ .../content/opened-piece.tsx} | 60 +++- apps/web/src/context/index.tsx | 2 +- apps/web/src/context/local-storage.tsx | 11 +- apps/web/src/context/shared-state.tsx | 42 ++- apps/web/src/layout/secured-layout.tsx | 96 +++--- apps/web/src/layout/side-panel.tsx | 5 +- apps/web/src/layout/toolbar/index.tsx | 37 +- .../web/src/lib/composables/content-groups.ts | 108 ------ .../web/src/lib/composables/content-pieces.ts | 148 -------- apps/web/src/lib/composables/index.ts | 3 - .../extensions/comment-menu/component.tsx | 9 +- apps/web/src/views/conflict/index.tsx | 6 +- apps/web/src/views/content-piece/index.tsx | 15 +- .../views/content-piece/sections/variants.tsx | 19 +- .../dashboard/content-groups-context.tsx | 44 --- apps/web/src/views/dashboard/index.tsx | 56 +-- .../views/kanban/content-group-column.tsx | 251 ++++++-------- .../views/kanban/content-piece-card.tsx | 16 +- .../views/dashboard/views/kanban/index.tsx | 175 +++++----- .../{list => table}/content-piece-list.tsx | 12 +- .../{list => table}/content-piece-row.tsx | 0 .../dashboard/views/{list => table}/index.tsx | 4 +- .../views/{list => table}/list-header.tsx | 0 .../{list => table}/list-view-context.tsx | 0 apps/web/src/views/editor/editor.tsx | 26 +- apps/web/src/views/editor/index.tsx | 26 +- .../src/views/editor/menus/export/index.tsx | 4 +- .../src/views/explorer/content-group-row.tsx | 94 +++-- .../src/views/explorer/content-piece-row.tsx | 50 ++- .../src/views/explorer/explorer-context.tsx | 36 +- apps/web/src/views/explorer/index.tsx | 77 ++++- apps/web/src/views/explorer/list.tsx | 225 ------------ apps/web/src/views/explorer/tree-level.tsx | 274 +++++---------- apps/web/src/views/git/sync-view/index.tsx | 6 +- apps/web/src/views/settings/view.tsx | 4 +- .../src/views/standalone-editor/editor.tsx | 4 +- 42 files changed, 1314 insertions(+), 1311 deletions(-) delete mode 100644 apps/web/src/context/cache.tsx create mode 100644 apps/web/src/context/content/actions.ts create mode 100644 apps/web/src/context/content/index.tsx create mode 100644 apps/web/src/context/content/loader.ts rename apps/web/src/{lib/composables/opened-content-piece.ts => context/content/opened-piece.tsx} (69%) delete mode 100644 apps/web/src/lib/composables/content-groups.ts delete mode 100644 apps/web/src/lib/composables/content-pieces.ts delete mode 100644 apps/web/src/lib/composables/index.ts delete mode 100644 apps/web/src/views/dashboard/content-groups-context.tsx rename apps/web/src/views/dashboard/views/{list => table}/content-piece-list.tsx (98%) rename apps/web/src/views/dashboard/views/{list => table}/content-piece-row.tsx (100%) rename apps/web/src/views/dashboard/views/{list => table}/index.tsx (98%) rename apps/web/src/views/dashboard/views/{list => table}/list-header.tsx (100%) rename apps/web/src/views/dashboard/views/{list => table}/list-view-context.tsx (100%) delete mode 100644 apps/web/src/views/explorer/list.tsx diff --git a/apps/web/src/components/primitives/sortable.tsx b/apps/web/src/components/primitives/sortable.tsx index b50a7888..27de8ba4 100644 --- a/apps/web/src/components/primitives/sortable.tsx +++ b/apps/web/src/components/primitives/sortable.tsx @@ -47,7 +47,7 @@ const SortableComponent = | keyof JSX.Intr setSortable( Sortable.create(wrapperRef, { - ...options, + ...(options || {}), delayOnTouchOnly: true, delay: 500 }) diff --git a/apps/web/src/context/cache.tsx b/apps/web/src/context/cache.tsx deleted file mode 100644 index 6bfce67e..00000000 --- a/apps/web/src/context/cache.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ParentComponent, createContext, getOwner, runWithOwner, useContext } from "solid-js"; - -type CachingFunction = (key: string, input: () => T) => T; - -const CacheContext = createContext(); -const CacheProvider: ParentComponent = (props) => { - const cacheOwner = getOwner(); - const cache = new Map(); - - return ( - { - return runWithOwner(cacheOwner, () => { - if (cache.has(key)) { - return cache.get(key); - } else { - cache.set(key, input()); - - return cache.get(key); - } - })!; - }} - > - {props.children} - - ); -}; -const useCache = (): CachingFunction => { - return useContext(CacheContext)!; -}; - -export { CacheProvider, useCache }; diff --git a/apps/web/src/context/client.tsx b/apps/web/src/context/client.tsx index b4a06f95..5a9556a8 100644 --- a/apps/web/src/context/client.tsx +++ b/apps/web/src/context/client.tsx @@ -9,6 +9,7 @@ import { import { createContext, onCleanup, ParentComponent, useContext } from "solid-js"; import { Unsubscribable, observable } from "@trpc/server/observable"; import type * as App from "@vrite/backend"; +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { navigateAndReload } from "#lib/utils"; const refreshTokenLink = (closeConnection: () => void): TRPCLink => { @@ -90,6 +91,8 @@ const refreshTokenLink = (closeConnection: () => void): TRPCLink => }; type Client = ReturnType>; +type RouterInput = inferRouterInputs; +type RouterOutput = inferRouterOutputs; const ClientContext = createContext(); const ClientProvider: ParentComponent = (props) => { @@ -129,4 +132,4 @@ const useClient = (): Client => { }; export { ClientProvider, useClient }; -export type { App }; +export type { App, RouterInput, RouterOutput }; diff --git a/apps/web/src/context/content/actions.ts b/apps/web/src/context/content/actions.ts new file mode 100644 index 00000000..f374921d --- /dev/null +++ b/apps/web/src/context/content/actions.ts @@ -0,0 +1,326 @@ +import { SetStoreFunction } from "solid-js/store"; +import { App, ContentLevel } from "#context"; + +interface ContentActionsInput { + contentGroups: Record; + contentPieces: Record | undefined>; + contentLevels: Record; + setContentGroups: SetStoreFunction>; + setContentPieces: SetStoreFunction< + Record | undefined> + >; + setContentLevels: SetStoreFunction>; +} +interface ContentActions { + createContentGroup(data: App.ContentGroup): void; + deleteContentGroup(data: Pick): void; + moveContentGroup(data: Pick): void; + updateContentGroup(data: Partial & Pick): void; + reorderContentGroup(data: Pick & { index: number }): void; + createContentPiece(data: App.ExtendedContentPieceWithAdditionalData<"order">): void; + deleteContentPiece(data: Pick): void; + updateContentPiece( + data: Partial> & + Pick + ): void; + moveContentPiece(data: { + contentPiece: App.ExtendedContentPieceWithAdditionalData<"order">; + nextReferenceId?: string; + previousReferenceId?: string; + }): void; +} + +const createContentActions = ({ + contentGroups, + contentPieces, + contentLevels, + setContentGroups, + setContentPieces, + setContentLevels +}: ContentActionsInput): ContentActions => { + const createContentGroup: ContentActions["createContentGroup"] = (data) => { + const parentId = data.ancestors.at(-1) || ""; + + setContentGroups(data.id, data); + + if (contentLevels[parentId]) { + setContentLevels(parentId, "groups", (groups) => [...groups, data.id]); + } + + if (contentGroups[parentId]) { + setContentGroups(parentId, "descendants", (descendants) => [...descendants, data.id]); + } + }; + const deleteContentGroup: ContentActions["deleteContentGroup"] = (data) => { + const parentId = contentGroups[data.id]?.ancestors.at(-1) || ""; + + // Update parent group + if (contentLevels[parentId]) { + setContentLevels(parentId, "groups", (groups) => { + return groups.filter((groupId) => groupId !== data.id); + }); + } + + if (contentGroups[parentId]) { + setContentGroups(parentId, "descendants", (descendants) => { + return descendants.filter((id) => id !== data.id); + }); + } + + if (contentGroups[data.id]) { + // Remove group + setContentGroups(data.id, undefined); + + // Remove descendants (content pieces and groups) + const removeDescendants = (groupId: string): void => { + const group = contentGroups[groupId]; + + if (!group) return; + + group.descendants.forEach((descendantId) => { + const descendant = contentGroups[descendantId]; + + if (contentLevels[descendantId]) { + setContentLevels(descendantId, undefined); + } + + if (descendant) { + setContentGroups(descendantId, undefined); + removeDescendants(descendantId); + } + }); + }; + + removeDescendants(data.id); + } + }; + const moveContentGroup: ContentActions["moveContentGroup"] = (data) => { + const currentParentId = contentGroups[data.id]?.ancestors.at(-1); + const currentParent = contentGroups[currentParentId || ""]; + const newParentId = data.ancestors.at(-1) || ""; + const newParent = contentGroups[newParentId]; + + if (contentGroups[data.id]) { + // Update ancestors of the moved group + setContentGroups(data.id, (group) => ({ + ...group, + ancestors: data.ancestors + })); + + // Update ancestors of the group's descendants + const updateDescendants = (groupId: string): void => { + const group = contentGroups[groupId]; + + if (!group) return; + + group.descendants.forEach((descendantId) => { + const descendant = contentGroups[descendantId]; + + if (!descendant) return; + + const newAncestors = [ + ...group.ancestors, + ...descendant.ancestors.slice(descendant.ancestors.indexOf(groupId) + 1) + ]; + + setContentGroups(descendantId, (descendant) => ({ + ...descendant, + ancestors: newAncestors + })); + updateDescendants(descendantId); + }); + }; + + updateDescendants(data.id); + } + + // Update old parent group + if (currentParentId === "" || (currentParentId && currentParent)) { + if (contentLevels[currentParentId]) { + setContentLevels(currentParentId, "groups", (groups) => { + return groups.filter((groupId) => groupId !== data.id); + }); + } + + if (contentGroups[currentParentId]) { + setContentGroups(currentParentId, (group) => ({ + ...group, + descendants: currentParent!.descendants.filter((id) => id !== data.id) + })); + } + } + + // Update new parent group + if (newParentId === "" || (newParentId && newParent)) { + if (contentLevels[newParentId]) { + setContentLevels(newParentId, "groups", (groups) => [...groups, data.id]); + } + + if (contentGroups[newParentId]) { + setContentGroups(newParentId, (group) => ({ + ...group, + descendants: [...(group?.descendants || []), data.id] + })); + } + } + }; + const updateContentGroup: ContentActions["updateContentGroup"] = (data) => { + const contentGroup = contentGroups[data.id]; + + if (contentGroup) { + if (data.ancestors && data.ancestors.join(":") !== contentGroup.ancestors.join(":")) { + moveContentGroup({ + ancestors: data.ancestors, + id: data.id + }); + } + + setContentGroups(data.id, data); + } + }; + const reorderContentGroup: ContentActions["reorderContentGroup"] = (data) => { + const contentGroup = contentGroups[data.id]; + const parentId = contentGroup?.ancestors.at(-1) || ""; + + if (contentGroups[parentId]) { + setContentGroups(parentId, "descendants", (descendants) => { + const newDescendants = [...descendants]; + const index = newDescendants.indexOf(data.id); + + if (index < 0) return descendants; + + newDescendants.splice(index, 1); + newDescendants.splice(data.index, 0, data.id); + + return newDescendants; + }); + } + + if (contentLevels[parentId]) { + setContentLevels(parentId, "groups", (groups) => { + const newGroups = [...groups]; + const index = newGroups.indexOf(data.id); + + if (index < 0) return groups; + + newGroups.splice(index, 1); + newGroups.splice(data.index, 0, data.id); + + return newGroups; + }); + } + }; + const createContentPiece: ContentActions["createContentPiece"] = (data) => { + setContentPieces(data.id, data); + + if (contentLevels[data.contentGroupId]) { + setContentLevels(data.contentGroupId, "pieces", (pieces) => [data.id, ...pieces]); + } + }; + const moveContentPiece: ContentActions["moveContentPiece"] = (data) => { + const currentContentPiece = contentPieces[data.contentPiece.id]; + const currentParentId = currentContentPiece?.contentGroupId || ""; + const newParentId = data.contentPiece.contentGroupId || ""; + + if (currentContentPiece || contentGroups[newParentId] || contentGroups[currentParentId]) { + setContentPieces(data.contentPiece.id, data.contentPiece); + } + + if (!newParentId && !currentParentId) return; + + if ( + newParentId && + currentParentId && + newParentId !== currentParentId && + contentLevels[currentParentId] + ) { + // Remove content piece from old parent group + setContentLevels(currentParentId, "pieces", (pieces) => { + return pieces.filter((pieceId) => pieceId !== data.contentPiece.id); + }); + } + + // Place content piece within the group + if (!contentLevels[newParentId]) return; + + if (data.nextReferenceId) { + setContentLevels(newParentId, "pieces", (pieces) => { + const newPieces = [...pieces.filter((pieceId) => pieceId !== data.contentPiece.id)]; + const index = newPieces.indexOf(data.nextReferenceId!); + + if (index < 0) return pieces; + + newPieces.splice(index + 1, 0, data.contentPiece.id); + + return newPieces; + }); + } else if (data.previousReferenceId) { + setContentLevels(newParentId, "pieces", (pieces) => { + const newPieces = [...pieces.filter((pieceId) => pieceId !== data.contentPiece.id)]; + const index = newPieces.indexOf(data.previousReferenceId!); + + if (index < 0) return pieces; + + newPieces.splice(index, 0, data.contentPiece.id); + + return newPieces; + }); + } else { + setContentLevels(newParentId, "pieces", (pieces) => { + return [ + data.contentPiece.id, + ...pieces.filter((pieceId) => pieceId !== data.contentPiece.id) + ]; + }); + } + }; + const updateContentPiece: ContentActions["updateContentPiece"] = (data) => { + const currentContentPiece = contentPieces[data.id]; + const currentParentId = currentContentPiece?.contentGroupId || ""; + const newParentId = data.contentGroupId; + + if (currentContentPiece) { + setContentPieces(data.id, data); + } + + if (!newParentId || !currentParentId) return; + + if (newParentId !== currentParentId && currentContentPiece) { + moveContentPiece({ + contentPiece: { + ...currentContentPiece, + ...data + } + }); + } + }; + const deleteContentPiece: ContentActions["deleteContentPiece"] = (data) => { + const currentContentPiece = contentPieces[data.id]; + const currentParentId = currentContentPiece?.contentGroupId || ""; + + if (contentLevels[currentParentId]) { + setContentLevels(currentParentId, "pieces", (pieces) => { + return pieces.filter((pieceId) => pieceId !== data.id); + }); + } + + if (currentContentPiece) { + setContentPieces(data.id, undefined); + } + }; + + return { + createContentGroup, + deleteContentGroup, + updateContentGroup, + moveContentGroup, + reorderContentGroup, + createContentPiece, + deleteContentPiece, + updateContentPiece, + moveContentPiece + }; +}; + +export { createContentActions }; +export type { ContentActions }; diff --git a/apps/web/src/context/content/index.tsx b/apps/web/src/context/content/index.tsx new file mode 100644 index 00000000..10627686 --- /dev/null +++ b/apps/web/src/context/content/index.tsx @@ -0,0 +1,204 @@ +import { ContentActions, createContentActions } from "./actions"; +import { ContentLoader, createContentLoader } from "./loader"; +import { createContext, ParentComponent, Setter, useContext } from "solid-js"; +import { createSignal, createEffect, on, onCleanup, Accessor } from "solid-js"; +import { createStore } from "solid-js/store"; +import { useClient, useLocalStorage, App } from "#context"; + +interface ContentLevel { + groups: string[]; + pieces: string[]; + moreToLoad: boolean; + loading: boolean; +} + +interface ContentDataContextData { + contentGroups: Record; + contentPieces: Record | undefined>; + contentLevels: Record; + contentActions: ContentActions; + contentLoader: ContentLoader; + activeDraggableContentGroupId: Accessor; + activeDraggableContentPieceId: Accessor; + setActiveDraggableContentGroupId: Setter; + setActiveDraggableContentPieceId: Setter; + activeContentGroupId(): string | null; + activeContentPieceId(): string | null; + expandedContentLevels(): string[]; + setActiveContentGroupId(contentGroupId: string | null): void; + setActiveContentPieceId(contentPieceId: string | null): void; + expandContentLevel(contentGroupId: string): void; + collapseContentLevel(contentGroupId: string): void; +} + +const ContentDataContext = createContext(); +const ContentDataProvider: ParentComponent = (props) => { + const client = useClient(); + const { storage, setStorage } = useLocalStorage(); + const [activeDraggableContentGroupId, setActiveDraggableContentGroupId] = createSignal< + string | null + >(null); + const [activeDraggableContentPieceId, setActiveDraggableContentPieceId] = createSignal< + string | null + >(null); + const [contentLevels, setContentLevels] = createStore>( + {} + ); + const [contentGroups, setContentGroups] = createStore< + Record + >({}); + const [contentPieces, setContentPieces] = createStore< + Record | undefined> + >({}); + const activeContentGroupId = (): string | null => { + return storage().activeContentGroupId || null; + }; + const activeContentPieceId = (): string | null => { + return storage().activeContentPieceId || null; + }; + const setActiveContentGroupId = (contentGroupId: string | null): void => { + setStorage((storage) => ({ + ...storage, + activeContentGroupId: contentGroupId || undefined + })); + }; + const setActiveContentPieceId = (contentPieceId: string | null): void => { + setStorage((storage) => ({ + ...storage, + activeContentPieceId: contentPieceId || undefined + })); + }; + const expandedContentLevels = (): string[] => { + return storage().expandedContentLevels || [""]; + }; + const expandContentLevel = (contentGroupId: string): void => { + setStorage((storage) => ({ + ...storage, + expandedContentLevels: [...new Set([...expandedContentLevels(), contentGroupId])] + })); + }; + const collapseContentLevel = (contentGroupId: string): void => { + const levelsToClose = [contentGroupId]; + const addLevelsToClose = (parentId: string): void => { + const level = contentLevels[parentId]; + + if (!level) { + return; + } + + levelsToClose.push(...level.groups); + level.groups.forEach((groupId) => addLevelsToClose(groupId)); + }; + + addLevelsToClose(contentGroupId); + setStorage((storage) => ({ + ...storage, + expandedContentLevels: + storage.expandedContentLevels?.filter((id) => !levelsToClose.includes(id)) || [] + })); + }; + const contentActions = createContentActions({ + contentGroups, + contentPieces, + contentLevels, + setContentGroups, + setContentPieces, + setContentLevels + }); + const contentLoader = createContentLoader({ + contentGroups, + contentPieces, + contentLevels, + setContentGroups, + setContentPieces, + setContentLevels + }); + const contentGroupsSubscription = client.contentGroups.changes.subscribe(undefined, { + onData({ action, data }) { + if (action === "move") { + contentActions.moveContentGroup(data); + } else if (action === "create") { + contentActions.createContentGroup(data); + } else if (action === "delete") { + contentActions.deleteContentGroup(data); + } else if (action === "update") { + contentActions.updateContentGroup(data); + } else if (action === "reorder") { + contentActions.reorderContentGroup(data); + } + } + }); + + createEffect(() => { + for (const contentGroupId in contentGroups) { + createEffect( + on( + () => contentGroups[contentGroupId], + () => { + const contentPiecesSubscription = client.contentPieces.changes.subscribe( + { contentGroupId }, + { + onData({ action, data }) { + if (action === "update") { + contentActions.updateContentPiece(data); + } else if (action === "create") { + contentActions.createContentPiece(data); + } else if (action === "delete") { + contentActions.deleteContentPiece(data); + } else if (action === "move") { + contentActions.moveContentPiece(data); + } + } + } + ); + + onCleanup(() => { + contentPiecesSubscription.unsubscribe(); + }); + } + ) + ); + } + }); + onCleanup(() => { + contentGroupsSubscription.unsubscribe(); + }); + expandedContentLevels().forEach((id) => { + try { + contentLoader.loadContentLevel(id); + } catch (e) { + collapseContentLevel(id); + } + }); + + return ( + + {props.children} + + ); +}; +const useContentData = (): ContentDataContextData => { + return useContext(ContentDataContext)!; +}; + +export { ContentDataProvider, useContentData }; +export type { ContentLevel }; diff --git a/apps/web/src/context/content/loader.ts b/apps/web/src/context/content/loader.ts new file mode 100644 index 00000000..5f38593f --- /dev/null +++ b/apps/web/src/context/content/loader.ts @@ -0,0 +1,111 @@ +import { SetStoreFunction } from "solid-js/store"; +import { App, ContentLevel, useClient } from "#context"; + +interface ContentActionsInput { + contentGroups: Record; + contentPieces: Record | undefined>; + contentLevels: Record; + setContentGroups: SetStoreFunction>; + setContentPieces: SetStoreFunction< + Record | undefined> + >; + setContentLevels: SetStoreFunction>; +} +interface ContentLoader { + loadContentLevel(contentGroupId?: string, preload?: boolean): Promise; +} + +const createContentLoader = ({ + contentPieces, + contentLevels, + setContentGroups, + setContentPieces, + setContentLevels +}: ContentActionsInput): ContentLoader => { + const client = useClient(); + const loadContentLevel: ContentLoader["loadContentLevel"] = async (contentGroupId, preload) => { + const existingLevel = contentLevels[contentGroupId || ""]; + + if (existingLevel && existingLevel.moreToLoad) { + if (contentGroupId) { + setContentLevels(contentGroupId, "loading", true); + + const level = { + groups: [...existingLevel.groups], + pieces: [...existingLevel.pieces], + moreToLoad: false + }; + const lastPieceId = existingLevel.pieces.at(-1); + const lastPiece = contentPieces[lastPieceId || ""]; + + if (!lastPiece) { + setContentLevels(contentGroupId, level); + + return; + } + + const newContentPieces = await client.contentPieces.list.query({ + contentGroupId, + lastOrder: lastPiece.order + }); + + level.pieces.push(...newContentPieces.map((contentPiece) => contentPiece.id)); + newContentPieces.forEach((contentPiece) => { + setContentPieces(contentPiece.id, contentPiece); + }); + + if (newContentPieces.length === 20) { + level.moreToLoad = true; + } + + setContentLevels(contentGroupId, level); + } + } else if (!existingLevel) { + const level: ContentLevel = { + groups: [], + pieces: [], + moreToLoad: false, + loading: false + }; + + setContentLevels(contentGroupId || "", { ...level, loading: true }); + + const contentGroups = await client.contentGroups.list.query({ + ancestor: contentGroupId || undefined + }); + + level.groups = contentGroups.map((contentGroup) => contentGroup.id); + contentGroups.forEach((contentGroup) => { + if (preload) { + loadContentLevel(contentGroup.id); + } + + setContentGroups(contentGroup.id, contentGroup); + }); + + if (contentGroupId) { + const contentPieces = await client.contentPieces.list.query({ + contentGroupId + }); + + level.pieces = contentPieces.map((contentPiece) => contentPiece.id); + contentPieces.forEach((contentPiece) => { + setContentPieces(contentPiece.id, contentPiece); + }); + + if (contentPieces.length === 20) { + level.moreToLoad = true; + } + } + + setContentLevels(contentGroupId || "", level); + } + }; + + return { + loadContentLevel + }; +}; + +export { createContentLoader }; +export type { ContentLoader }; diff --git a/apps/web/src/lib/composables/opened-content-piece.ts b/apps/web/src/context/content/opened-piece.tsx similarity index 69% rename from apps/web/src/lib/composables/opened-content-piece.ts rename to apps/web/src/context/content/opened-piece.tsx index ba46bf06..48b375c1 100644 --- a/apps/web/src/lib/composables/opened-content-piece.ts +++ b/apps/web/src/context/content/opened-piece.tsx @@ -1,10 +1,22 @@ -import { createSignal, createEffect, on, onCleanup, Accessor } from "solid-js"; -import { createStore } from "solid-js/store"; -import { useAuthenticatedUserData, useClient, useLocalStorage, App } from "#context"; +type ContentPiecePropertyKey = keyof App.ExtendedContentPieceWithAdditionalData<"coverWidth">; +interface UseContentGroups { + contentGroups: Accessor; + loading: Accessor; + refetch(ancestorId?: string): Promise; + setContentGroups(contentGroups: App.ContentGroup[]): void; +} + +interface UseContentPieces { + contentPieces(): Array>; + setContentPieces(contentPieces: Array>): void; + loading(): boolean; + loadMore(): void; + moreToLoad(): boolean; +} interface UseOpenedContentPiece { activeVariant: Accessor; - setContentPiece>( + setContentPiece( keyOrObject: K | Partial>, value?: App.ExtendedContentPieceWithAdditionalData<"coverWidth">[K] ): void; @@ -23,6 +35,15 @@ const useOpenedContentPiece = (): UseOpenedContentPiece => { const [state, setState] = createStore<{ contentPiece: App.ExtendedContentPieceWithAdditionalData<"coverWidth"> | null; }>({ contentPiece: null }); + const activeContentGroupId = (): string | null => { + return storage().activeContentGroupId || null; + }; + const setActiveContentGroupId = (contentGroupId: string): void => { + setStorage((storage) => ({ + ...storage, + activeContentGroupId: contentGroupId || undefined + })); + }; const fetchContentPiece = async (): Promise => { setLoading(true); @@ -142,6 +163,31 @@ const useOpenedContentPiece = (): UseOpenedContentPiece => { } }; }; - -export { useOpenedContentPiece }; -export type { UseOpenedContentPiece }; +const reorderContentGroup = (contentGroupId: string, index: number): void => { + const newContentGroups = [...state.contentGroups]; + const contentGroupIndex = newContentGroups.findIndex((contentGroup) => { + return contentGroup.id === contentGroupId; + }); + const [contentGroup] = newContentGroups.splice(contentGroupIndex, 1); + + newContentGroups.splice(index, 0, contentGroup); + setState({ + contentGroups: newContentGroups + }); +}; +const moveContentGroup = (contentGroup: App.ContentGroup): void => { + const newContentGroups = [...state.contentGroups]; + const index = newContentGroups.findIndex((newContentGroup) => { + return newContentGroup.id === contentGroup.id; + }); + + if (index >= 0 && contentGroup.ancestors.at(-1) !== ancestorId()) { + newContentGroups.splice(index, 1); + } else if (index < 0 && contentGroup.ancestors.at(-1) === ancestorId()) { + newContentGroups.push(contentGroup); + } + + setState({ + contentGroups: newContentGroups + }); +}; diff --git a/apps/web/src/context/index.tsx b/apps/web/src/context/index.tsx index ca2e795a..b01b5769 100644 --- a/apps/web/src/context/index.tsx +++ b/apps/web/src/context/index.tsx @@ -5,7 +5,7 @@ export * from "./local-storage"; export * from "./appearance"; export * from "./authenticated-user-data"; export * from "./extensions"; -export * from "./cache"; export * from "./shared-state"; export * from "./command-palette"; export * from "./host-config"; +export * from "./content"; diff --git a/apps/web/src/context/local-storage.tsx b/apps/web/src/context/local-storage.tsx index 1b30d1d3..8e9496e3 100644 --- a/apps/web/src/context/local-storage.tsx +++ b/apps/web/src/context/local-storage.tsx @@ -7,20 +7,19 @@ import { createSignal, useContext } from "solid-js"; -import { App } from "#context"; interface StorageData { sourceControlConfiguredProvider: string; + activeContentGroupId: string; + activeContentPieceId: string; + expandedContentLevels: string[]; + dashboardView: string; sidePanelView: string; - sidePanelWidth: number; toolbarView: string; - dashboardView: string; - dashboardViewAncestor: App.ContentGroup; - contentPieceId: string; + sidePanelWidth: number; settingsSection: string; zenMode: boolean; html: string; - explorerOpenedLevels: string[]; } interface LocalStorageContextData { storage: Accessor>; diff --git a/apps/web/src/context/shared-state.tsx b/apps/web/src/context/shared-state.tsx index dbfaa6be..15084768 100644 --- a/apps/web/src/context/shared-state.tsx +++ b/apps/web/src/context/shared-state.tsx @@ -1,31 +1,40 @@ import { + Accessor, ParentComponent, + Setter, createContext, createSignal, getOwner, runWithOwner, useContext } from "solid-js"; -import { createStore } from "solid-js/store"; +import { SetStoreFunction, createStore } from "solid-js/store"; interface SharedState {} type SharedSignal = ( key: K, - initialValue?: Partial[K] -) => [() => Partial[K], (value: Partial[K]) => void]; + initialValue?: SharedState[K] +) => [Accessor, Setter]; +type SharedStore = ( + key: K, + initialValue?: Extract +) => [SharedState[K], SetStoreFunction]; + +interface SharedStateContextData { + useSharedSignal: SharedSignal; + useSharedStore: SharedStore; +} -const SharedStateContext = createContext(); +const SharedStateContext = createContext(); const SharedStateProvider: ParentComponent = (props) => { const sharedSignals = new Map(); + const sharedStores = new Map(); const owner = getOwner(); - const useSharedSignal = ( - key: K, - initialValue?: Partial[K] - ): [() => Partial[K], (value: Partial[K]) => void] => { + const useSharedSignal: SharedSignal = (key, initialValue) => { return runWithOwner(owner, () => { if (!sharedSignals.has(key)) { - const [state, setState] = createSignal[K]>(initialValue); + const [state, setState] = createSignal(initialValue); sharedSignals.set(key, [state, setState]); } @@ -33,14 +42,25 @@ const SharedStateProvider: ParentComponent = (props) => { return sharedSignals.get(key); })!; }; + const useSharedStore: SharedStore = (key, initialValue) => { + return runWithOwner(owner, () => { + if (!sharedStores.has(key)) { + const [state, setState] = createStore(initialValue); + + sharedStores.set(key, [state, setState]); + } + + return sharedStores.get(key); + })!; + }; return ( - + {props.children} ); }; -const useSharedState = (): SharedSignal => { +const useSharedState = (): SharedStateContextData => { return useContext(SharedStateContext)!; }; diff --git a/apps/web/src/layout/secured-layout.tsx b/apps/web/src/layout/secured-layout.tsx index 748f11bb..ad7728b6 100644 --- a/apps/web/src/layout/secured-layout.tsx +++ b/apps/web/src/layout/secured-layout.tsx @@ -3,16 +3,16 @@ import { Toolbar } from "./toolbar"; import { SidebarMenu } from "./sidebar-menu"; import { ParentComponent, Show, createEffect } from "solid-js"; import { useLocation } from "@solidjs/router"; -import { mdiFullscreenExit } from "@mdi/js"; +import { mdiCards, mdiCardsOutline, mdiFullscreenExit, mdiSourceBranch } from "@mdi/js"; import { AppearanceProvider, AuthenticatedUserDataProvider, - CacheProvider, ExtensionsProvider, CommandPaletteProvider, + ContentDataProvider, useLocalStorage } from "#context"; -import { IconButton, Tooltip } from "#components/primitives"; +import { Card, IconButton, Tooltip } from "#components/primitives"; const SecuredLayout: ParentComponent = (props) => { const { storage, setStorage } = useLocalStorage(); @@ -28,48 +28,64 @@ const SecuredLayout: ParentComponent = (props) => { - - - + +
+
+ + { + setStorage((storage) => ({ ...storage, zenMode: false })); + }} + /> + + } > - { - setStorage((storage) => ({ ...storage, zenMode: false })); - }} - /> - - } - > - - -
-
- - + -
- - - -
{props.children}
+
+
+ + + +
+ + + +
+ {props.children} +
+
+
+
+ +
- - + + diff --git a/apps/web/src/layout/side-panel.tsx b/apps/web/src/layout/side-panel.tsx index 12a63bf3..8ec46a0f 100644 --- a/apps/web/src/layout/side-panel.tsx +++ b/apps/web/src/layout/side-panel.tsx @@ -4,7 +4,7 @@ import { createSignal, createMemo, onCleanup, Component, Show } from "solid-js"; import { Dynamic } from "solid-js/web"; import { useHostConfig, useLocalStorage } from "#context"; import { createRef } from "#lib/utils"; -import { ContentPieceView } from "#views/content-piece"; +// import { ContentPieceView } from "#views/content-piece"; import { SettingsView } from "#views/settings"; import { ExtensionsView } from "#views/extensions"; import { GettingStartedView } from "#views/getting-started"; @@ -12,7 +12,8 @@ import { GitView } from "#views/git"; import { ExplorerView } from "#views/explorer"; const sidePanelViews: Record>> = { - contentPiece: ContentPieceView, + // TODO: use content data + contentPiece: () =>
, // ContentPieceView, git: () => { const hostConfig = useHostConfig(); diff --git a/apps/web/src/layout/toolbar/index.tsx b/apps/web/src/layout/toolbar/index.tsx index f67a09ae..744fd878 100644 --- a/apps/web/src/layout/toolbar/index.tsx +++ b/apps/web/src/layout/toolbar/index.tsx @@ -19,6 +19,7 @@ import { App, useClient, useCommandPalette, + useContentData, useHostConfig, useLocalStorage, useNotifications, @@ -31,8 +32,8 @@ import { breakpoints, isAppleDevice } from "#lib/utils"; const toolbarViews: Record>> = { editorStandalone: () => { - const createSharedSignal = useSharedState(); - const [sharedEditor] = createSharedSignal("editor"); + const { useSharedSignal } = useSharedState(); + const [sharedEditor] = useSharedSignal("editor"); const { setStorage } = useLocalStorage(); const [menuOpened, setMenuOpened] = createSignal(false); @@ -155,12 +156,12 @@ const toolbarViews: Record>> = { ); }, conflict: () => { - const createSharedSignal = useSharedState(); + const { useSharedSignal } = useSharedState(); const client = useClient(); const { notify } = useNotifications(); - const [resolvedContent] = createSharedSignal("resolvedContent"); - const [conflictData, setConflictData] = createSharedSignal("conflictData"); - const [conflicts, setConflicts] = createSharedSignal("conflicts"); + const [resolvedContent] = useSharedSignal("resolvedContent"); + const [conflictData, setConflictData] = useSharedSignal("conflictData"); + const [conflicts, setConflicts] = useSharedSignal("conflicts"); const [loading, setLoading] = createSignal(false); const pathDetails = createMemo(() => { const pathParts = (conflictData()?.path || "").split("/"); @@ -215,17 +216,17 @@ const toolbarViews: Record>> = { ); }, editor: () => { - const createSharedSignal = useSharedState(); + const { activeContentPieceId, contentPieces } = useContentData(); + const { useSharedSignal } = useSharedState(); const { registerCommand } = useCommandPalette(); - const [sharedEditor] = createSharedSignal("editor"); - const [sharedProvider] = createSharedSignal("provider"); - const [sharedEditedContentPiece] = createSharedSignal("editedContentPiece"); + const [sharedEditor] = useSharedSignal("editor"); + const [sharedProvider] = useSharedSignal("provider"); const { setStorage } = useLocalStorage(); const [menuOpened, setMenuOpened] = createSignal(false); createEffect( - on(sharedEditedContentPiece, (sharedEditedContentPiece) => { - if (sharedEditedContentPiece) { + on(activeContentPieceId, (contentPieceId) => { + if (contentPieceId) { registerCommand({ name: "Zen mode", category: "editor", @@ -263,9 +264,9 @@ const toolbarViews: Record>> = { wrapperClass="w-full" /> - + setMenuOpened(false)} class="w-full justify-start" wrapperClass="w-full" @@ -289,8 +290,8 @@ const toolbarViews: Record>> = { - - + + { setStorage((storage) => ({ ...storage, zenMode: true })); @@ -308,10 +309,10 @@ const toolbarViews: Record>> = { }, default: () => { const hostConfig = useHostConfig(); - const createSharedSignal = useSharedState(); + const { useSharedSignal } = useSharedState(); const { storage, setStorage } = useLocalStorage(); const { setOpened, registerCommand } = useCommandPalette(); - const [provider] = createSharedSignal("provider"); + const [provider] = useSharedSignal("provider"); const [viewSelectorOpened, setViewSelectorOpened] = createSignal(false); const view = (): string => storage().dashboardView || "kanban"; const setView = (view: string): void => { diff --git a/apps/web/src/lib/composables/content-groups.ts b/apps/web/src/lib/composables/content-groups.ts deleted file mode 100644 index db57cb3d..00000000 --- a/apps/web/src/lib/composables/content-groups.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Accessor, createSignal, onCleanup } from "solid-js"; -import { createStore } from "solid-js/store"; -import { useClient, App } from "#context/client"; - -interface UseContentGroups { - contentGroups: Accessor; - loading: Accessor; - refetch(ancestorId?: string): Promise; - setContentGroups(contentGroups: App.ContentGroup[]): void; -} - -const useContentGroups = (initialAncestorId?: string): UseContentGroups => { - const [ancestorId, setAncestorId] = createSignal(initialAncestorId); - const [loading, setLoading] = createSignal(false); - const client = useClient(); - const [state, setState] = createStore<{ - contentGroups: App.ContentGroup[]; - }>({ - contentGroups: [] - }); - const reorderContentGroup = (contentGroupId: string, index: number): void => { - const newContentGroups = [...state.contentGroups]; - const contentGroupIndex = newContentGroups.findIndex((contentGroup) => { - return contentGroup.id === contentGroupId; - }); - const [contentGroup] = newContentGroups.splice(contentGroupIndex, 1); - - newContentGroups.splice(index, 0, contentGroup); - setState({ - contentGroups: newContentGroups - }); - }; - const moveContentGroup = (contentGroup: App.ContentGroup): void => { - const newContentGroups = [...state.contentGroups]; - const index = newContentGroups.findIndex((newContentGroup) => { - return newContentGroup.id === contentGroup.id; - }); - - if (index >= 0 && contentGroup.ancestors.at(-1) !== ancestorId()) { - newContentGroups.splice(index, 1); - } else if (index < 0 && contentGroup.ancestors.at(-1) === ancestorId()) { - newContentGroups.push(contentGroup); - } - - setState({ - contentGroups: newContentGroups - }); - }; - const refetch = async (ancestor?: string): Promise => { - setLoading(true); - - try { - const contentGroups = await client.contentGroups.list.query({ ancestor }); - - setAncestorId(ancestor); - setState("contentGroups", contentGroups); - setLoading(false); - } catch (error) { - setAncestorId(ancestor); - } - }; - const contentGroupsChanges = client.contentGroups.changes.subscribe(undefined, { - onData({ action, data }) { - switch (action) { - case "create": - if (data.ancestors[data.ancestors.length - 1] === ancestorId()) { - setState({ contentGroups: [...state.contentGroups, data] }); - } - - break; - case "update": - setState( - "contentGroups", - state.contentGroups.findIndex((column) => column.id === data.id), - (contentGroup) => ({ ...contentGroup, ...data }) - ); - break; - case "delete": - setState({ - contentGroups: state.contentGroups.filter((column) => column.id !== data.id) - }); - break; - case "reorder": - reorderContentGroup(data.id, data.index); - break; - case "move": - moveContentGroup(data); - - break; - } - } - }); - - onCleanup(() => { - contentGroupsChanges.unsubscribe(); - }); - refetch(ancestorId()); - - return { - refetch, - loading, - contentGroups: () => state.contentGroups, - setContentGroups: (contentGroups) => setState("contentGroups", contentGroups) - }; -}; - -export { useContentGroups }; -export type { UseContentGroups }; diff --git a/apps/web/src/lib/composables/content-pieces.ts b/apps/web/src/lib/composables/content-pieces.ts deleted file mode 100644 index e85d2e66..00000000 --- a/apps/web/src/lib/composables/content-pieces.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { createSignal, onCleanup } from "solid-js"; -import { createStore } from "solid-js/store"; -import { App, useClient } from "#context/client"; -import { useNotifications } from "#context/notifications"; - -interface UseContentPieces { - contentPieces(): Array>; - setContentPieces(contentPieces: Array>): void; - loading(): boolean; - loadMore(): void; - moreToLoad(): boolean; -} - -const useContentPieces = (contentGroupId: string): UseContentPieces => { - const { notify } = useNotifications(); - const client = useClient(); - const [loading, setLoading] = createSignal(false); - const [moreToLoad, setMoreToLoad] = createSignal(true); - const [state, setState] = createStore<{ - contentPieces: Array>; - }>({ - contentPieces: [] - }); - const loadMore = (): void => { - const lastOrder = state.contentPieces[state.contentPieces.length - 1]?.order; - - if (loading() || !moreToLoad()) return; - - setLoading(true); - client.contentPieces.list.query({ contentGroupId, perPage: 20, lastOrder }).then((data) => { - setLoading(false); - setState("contentPieces", (contentPieces) => [...contentPieces, ...data]); - setMoreToLoad(data.length === 20); - }); - }; - const contentPiecesChanges = client.contentPieces.changes.subscribe( - { contentGroupId }, - { - onData(value) { - const { action, data } = value; - - switch (action) { - case "delete": - setState("contentPieces", (contentPieces) => { - return contentPieces.filter((contentPiece) => { - return contentPiece.id !== data.id; - }); - }); - notify({ text: "Content piece deleted", type: "success" }); - break; - case "create": - setState("contentPieces", (contentPieces) => [data, ...contentPieces]); - break; - case "update": - if (!("variantId" in value)) { - setState( - "contentPieces", - state.contentPieces.findIndex((contentPiece) => contentPiece.id === data.id), - (contentPiece) => { - return { ...contentPiece, ...data }; - } - ); - } - - break; - case "move": - setState("contentPieces", (contentPieces) => { - const currentIndex = contentPieces.findIndex( - (contentPiece) => data.contentPiece.id === contentPiece.id - ); - - if (currentIndex >= 0) { - if (contentGroupId === data.contentPiece.contentGroupId) { - const newContentPieces = [...contentPieces]; - - newContentPieces.splice(currentIndex, 1); - - const previousReferenceIndex = newContentPieces.findIndex((contentPieces) => { - return contentPieces.id === data.previousReferenceId; - }); - - if (previousReferenceIndex >= 0) { - newContentPieces.splice(previousReferenceIndex, 0, data.contentPiece); - } else { - const nextReferenceIndex = newContentPieces.findIndex((contentPieces) => { - return contentPieces.id === data.nextReferenceId; - }); - - if (nextReferenceIndex >= 0) { - newContentPieces.splice(nextReferenceIndex + 1, 0, data.contentPiece); - } else if (!data.previousReferenceId && !data.nextReferenceId) { - newContentPieces.push(data.contentPiece); - } - } - - return newContentPieces; - } - - return contentPieces.filter((contentPiece) => { - return contentPiece.id !== data.contentPiece.id; - }); - } else if (contentGroupId === data.contentPiece.contentGroupId) { - const newContentPieces = [...contentPieces]; - const previousReferenceIndex = newContentPieces.findIndex((contentPieces) => { - return contentPieces.id === data.previousReferenceId; - }); - - if (previousReferenceIndex >= 0) { - newContentPieces.splice(previousReferenceIndex, 0, data.contentPiece); - } else { - const nextReferenceIndex = newContentPieces.findIndex((contentPieces) => { - return contentPieces.id === data.nextReferenceId; - }); - - if (nextReferenceIndex >= 0) { - newContentPieces.splice(nextReferenceIndex + 1, 0, data.contentPiece); - } else if (!data.previousReferenceId && !data.nextReferenceId) { - newContentPieces.push(data.contentPiece); - } - } - - return newContentPieces; - } - - return contentPieces; - }); - break; - } - } - } - ); - - loadMore(); - onCleanup(() => { - contentPiecesChanges.unsubscribe(); - }); - - return { - contentPieces: () => state.contentPieces, - setContentPieces: (contentPieces) => setState("contentPieces", contentPieces), - loading, - loadMore, - moreToLoad - }; -}; - -export { useContentPieces }; -export type { UseContentPieces }; diff --git a/apps/web/src/lib/composables/index.ts b/apps/web/src/lib/composables/index.ts deleted file mode 100644 index d5b9265f..00000000 --- a/apps/web/src/lib/composables/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./content-groups"; -export * from "./content-pieces"; -export * from "./opened-content-piece"; diff --git a/apps/web/src/lib/editor/extensions/comment-menu/component.tsx b/apps/web/src/lib/editor/extensions/comment-menu/component.tsx index 2b64f07b..9c55cf77 100644 --- a/apps/web/src/lib/editor/extensions/comment-menu/component.tsx +++ b/apps/web/src/lib/editor/extensions/comment-menu/component.tsx @@ -6,7 +6,7 @@ import dayjs from "dayjs"; import { Component, For, createEffect, createSignal, on, onCleanup } from "solid-js"; import RelativeTimePlugin from "dayjs/plugin/relativeTime"; import { createStore } from "solid-js/store"; -import { useLocalStorage, useSharedState } from "#context"; +import { useContentData, useLocalStorage, useSharedState } from "#context"; interface BlockActionMenuProps { state: { @@ -28,9 +28,8 @@ interface CommentFragmentData { dayjs.extend(RelativeTimePlugin); const CommentMenu: Component = (props) => { - const createSharedSignal = useSharedState(); const { storage } = useLocalStorage(); - const [editedContentPiece] = createSharedSignal("editedContentPiece"); + const { activeContentPieceId } = useContentData(); const [fragments, setFragments] = createSignal([]); const handleStateUpdate = (): void => { const container = document.getElementById("pm-container"); @@ -85,7 +84,7 @@ const CommentMenu: Component = (props) => { }); return ( - +
= (props) => { fragment={fragment} contentOverlap={props.state.contentOverlap} selectedFragmentId={props.state.fragment} - contentPieceId={editedContentPiece()?.id || ""} + contentPieceId={activeContentPieceId() || ""} editor={props.state.editor} setFragment={props.state.setFragment} /> diff --git a/apps/web/src/views/conflict/index.tsx b/apps/web/src/views/conflict/index.tsx index 309f81af..b8837184 100644 --- a/apps/web/src/views/conflict/index.tsx +++ b/apps/web/src/views/conflict/index.tsx @@ -24,10 +24,10 @@ const ConflictView: Component<{ monaco: typeof monaco }> = (props) => { const { setStorage } = useLocalStorage(); const { monaco } = props; const { codeEditorTheme = () => "dark" } = useAppearance() || {}; - const createSharedSignal = useSharedState(); + const { useSharedSignal } = useSharedState(); const [containerRef, setContainerRef] = createRef(null); - const [conflictData] = createSharedSignal("conflictData"); - const [, setResolvedContent] = createSharedSignal("resolvedContent"); + const [conflictData] = useSharedSignal("conflictData"); + const [, setResolvedContent] = useSharedSignal("resolvedContent"); const [currentContent] = createResource( () => conflictData()?.contentPieceId, async (contentPieceId) => { diff --git a/apps/web/src/views/content-piece/index.tsx b/apps/web/src/views/content-piece/index.tsx index f592b776..29e10def 100644 --- a/apps/web/src/views/content-piece/index.tsx +++ b/apps/web/src/views/content-piece/index.tsx @@ -26,12 +26,12 @@ import { useClient, useLocalStorage, hasPermission, - useCache, - useHostConfig + useHostConfig, + useSharedState, + useContentData } from "#context"; import { MiniEditor } from "#components/fragments"; import { breakpoints } from "#lib/utils"; -import { useOpenedContentPiece } from "#lib/composables"; dayjs.extend(CustomParseFormat); @@ -47,11 +47,10 @@ const ContentPieceView: Component = () => { id: string; icon: string; }>; - const cache = useCache(); - const { contentPiece, setContentPiece, loading, activeVariant, setActiveVariant } = cache( - "openedContentPiece", - useOpenedContentPiece - ); + const { useSharedSignal } = useSharedState(); + // TODO: use content data + const { contentPiece, setContentPiece, loading, activeVariant, setActiveVariant } = + useContentData().openedContentPiece; const client = useClient(); const { setStorage } = useLocalStorage(); const { confirmDelete } = useConfirmationModal(); diff --git a/apps/web/src/views/content-piece/sections/variants.tsx b/apps/web/src/views/content-piece/sections/variants.tsx index e3d5e505..c418b8f3 100644 --- a/apps/web/src/views/content-piece/sections/variants.tsx +++ b/apps/web/src/views/content-piece/sections/variants.tsx @@ -1,6 +1,6 @@ import { Accessor, Component, For, Show, createSignal, onCleanup } from "solid-js"; import { createStore } from "solid-js/store"; -import { App, useClient, useLocalStorage, useSharedState } from "#context"; +import { App, useClient } from "#context"; import { Button, Loader } from "#components/primitives"; interface VariantsSectionProps { @@ -8,12 +8,6 @@ interface VariantsSectionProps { setActiveVariant(variant: App.Variant | null): void; } -declare module "#context" { - interface SharedState { - activeVariant: App.Variant; - } -} - const useVariants = (): { loading: Accessor; variants(): Array; @@ -66,8 +60,8 @@ const useVariants = (): { return { loading, variants: () => state.variants }; }; const VariantsSection: Component = (props) => { - const createSharedSignal = useSharedState(); - const [, setActiveVariant] = createSharedSignal("activeVariant"); + /* TODO: Use ContentData + const [, setActiveVariant] = createSharedSignal("activeVariant"); */ const { loading, variants } = useVariants(); return ( @@ -103,10 +97,10 @@ const VariantsSection: Component = (props) => { onClick={() => { if (active()) { props.setActiveVariant(null); - setActiveVariant(undefined); + // setActiveVariant(undefined); } else { props.setActiveVariant(variant); - setActiveVariant(variant); + // setActiveVariant(variant); } }} > @@ -118,7 +112,8 @@ const VariantsSection: Component = (props) => { text="soft" size="small" > - {variant.name} + Test + {/* TODO: variant.name*/} ); diff --git a/apps/web/src/views/dashboard/content-groups-context.tsx b/apps/web/src/views/dashboard/content-groups-context.tsx deleted file mode 100644 index 0fc7e69b..00000000 --- a/apps/web/src/views/dashboard/content-groups-context.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { - Accessor, - createContext, - createSignal, - ParentComponent, - Setter, - useContext -} from "solid-js"; -import { App } from "#context"; - -interface ContentGroupsContextProviderProps { - ancestor: Accessor; - setAncestor(ancestor: App.ContentGroup | null | undefined): void; -} -interface ContentGroupsContextData extends ContentGroupsContextProviderProps { - activeDraggablePiece: Accessor; - setActiveDraggablePiece: Setter; -} - -const ContentGroupsContext = createContext(); -const ContentGroupsContextProvider: ParentComponent = ( - props -) => { - const [activeDraggablePiece, setActiveDraggablePiece] = - createSignal(null); - - return ( - - {props.children} - - ); -}; -const useContentGroupsContext = (): ContentGroupsContextData => { - return useContext(ContentGroupsContext) as ContentGroupsContextData; -}; - -export { ContentGroupsContextProvider, useContentGroupsContext }; diff --git a/apps/web/src/views/dashboard/index.tsx b/apps/web/src/views/dashboard/index.tsx index 7776a9e5..d413194c 100644 --- a/apps/web/src/views/dashboard/index.tsx +++ b/apps/web/src/views/dashboard/index.tsx @@ -1,35 +1,26 @@ import { DashboardKanbanView } from "./views/kanban"; -import { DashboardListView } from "./views/list"; import { Component, Match, Switch, createEffect, on, onCleanup } from "solid-js"; import { HocuspocusProvider } from "@hocuspocus/provider"; import * as Y from "yjs"; -import { App, useAuthenticatedUserData, useLocalStorage, useCache, useSharedState } from "#context"; +import { + useAuthenticatedUserData, + useContentData, + useLocalStorage, + useSharedState +} from "#context"; import { getSelectionColor } from "#lib/utils"; -import { useContentGroups } from "#lib/composables"; const DashboardView: Component = () => { - const createSharedSignal = useSharedState(); - const cache = useCache(); + const { useSharedSignal } = useSharedState(); const { workspace, profile } = useAuthenticatedUserData(); const { storage, setStorage } = useLocalStorage(); - const [provider, setProvider] = createSharedSignal("provider"); - const ancestor = (): App.ContentGroup | null => { - return storage().dashboardViewAncestor || null; - }; - const { contentGroups, setContentGroups, refetch, loading } = cache("contentGroups", () => { - return useContentGroups(ancestor()?.id); - }); + const { activeContentPieceId } = useContentData(); + const [provider, setProvider] = useSharedSignal("provider"); const ydoc = new Y.Doc(); const handleReload = async (): Promise => { await fetch("/session/refresh", { method: "POST" }); provider()?.connect(); }; - const setAncestor = (ancestor: App.ContentGroup | null): void => { - setStorage((storage) => ({ - ...storage, - dashboardViewAncestor: ancestor || undefined - })); - }; provider()?.awareness?.setLocalStateField("user", { name: profile()?.username || "", @@ -44,18 +35,11 @@ const DashboardView: Component = () => { setStorage((storage) => ({ ...storage, toolbarView: "default" })); createEffect( on(storage, (storage, previousContentPieceId) => { - if (storage.contentPieceId !== previousContentPieceId) { - provider()?.awareness?.setLocalStateField("contentPieceId", storage.contentPieceId); + if (activeContentPieceId() !== previousContentPieceId) { + provider()?.awareness?.setLocalStateField("contentPieceId", activeContentPieceId()); } - return storage.contentPieceId; - }) - ); - createEffect( - on(ancestor, (ancestor, previousAncestor) => { - if (ancestor?.id !== previousAncestor?.id) { - refetch(ancestor?.id); - } + return activeContentPieceId(); }) ); @@ -74,22 +58,10 @@ const DashboardView: Component = () => {
- +
- +
diff --git a/apps/web/src/views/dashboard/views/kanban/content-group-column.tsx b/apps/web/src/views/dashboard/views/kanban/content-group-column.tsx index 142a639c..854bf63e 100644 --- a/apps/web/src/views/dashboard/views/kanban/content-group-column.tsx +++ b/apps/web/src/views/dashboard/views/kanban/content-group-column.tsx @@ -1,5 +1,4 @@ import { ContentPieceCard } from "./content-piece-card"; -import { useContentGroupsContext } from "../../content-groups-context"; import { Component, createEffect, createMemo, createSignal, For, on, Show } from "solid-js"; import { mdiDotsVertical, @@ -19,40 +18,33 @@ import { useConfirmationModal, useLocalStorage, hasPermission, - useCache, - useSharedState, - useCommandPalette + useCommandPalette, + useContentData, + ContentLevel } from "#context"; import { breakpoints, createRef } from "#lib/utils"; -import { useContentPieces } from "#lib/composables"; interface ContentGroupColumnProps { contentGroup: App.ContentGroup; index: number; onDragStart?(): void; onDragEnd?(): void; - remove?(id: string): void; + remove?(id?: string): void; } interface AddContentGroupColumnProps { class?: string; } -declare module "#context" { - interface SharedState { - activeDraggableGroup: App.ContentGroup | null; - } -} - const AddContentGroupColumn: Component = (props) => { const client = useClient(); const { notify } = useNotifications(); - const { ancestor } = useContentGroupsContext(); + const { activeContentGroupId } = useContentData(); const { registerCommand } = useCommandPalette(); const createNewContentGroup = async (): Promise => { try { await client.contentGroups.create.mutate({ name: "", - ancestor: ancestor()?.id + ancestor: activeContentGroupId() || undefined }); notify({ text: "New content group created", type: "success" }); } catch (error) { @@ -86,22 +78,20 @@ const AddContentGroupColumn: Component = (props) => ); }; const ContentGroupColumn: Component = (props) => { - const cache = useCache(); - const createSharedSignal = useSharedState(); - const { contentPieces, setContentPieces, loadMore, loading } = cache( - `contentPieces:${props.contentGroup.id}`, - () => { - return useContentPieces(props.contentGroup.id); - } - ); + const { + contentPieces, + contentLevels, + contentActions, + contentLoader, + activeContentGroupId, + setActiveContentGroupId, + activeDraggableContentGroupId, + setActiveDraggableContentGroupId, + setActiveDraggableContentPieceId + } = useContentData(); const { notify } = useNotifications(); const { confirmDelete } = useConfirmationModal(); const { setStorage } = useLocalStorage(); - const { activeDraggablePiece, setActiveDraggablePiece, setAncestor } = useContentGroupsContext(); - const [activeDraggableGroup, setActiveDraggableGroup] = createSharedSignal( - "activeDraggableGroup", - null - ); const scrollShadowController = createScrollShadowController(); const [scrollableContainerRef, setScrollableContainerRef] = createRef(null); const [dropdownOpened, setDropdownOpened] = createSignal(false); @@ -148,12 +138,11 @@ const ContentGroupColumn: Component = (props) => { async onConfirm() { try { await client.contentGroups.delete.mutate({ id: props.contentGroup.id }); - setStorage((storage) => ({ - ...storage, - ...(contentPieces().find((contentPiece) => { - return contentPiece.contentGroupId === props.contentGroup.id; - }) && { contentPieceId: undefined }) - })); + + if (activeContentGroupId() === props.contentGroup.id) { + setActiveContentGroupId(null); + } + notify({ text: "Content group deleted", type: "success" }); } catch (error) { notify({ text: "Couldn't delete the content group", type: "success" }); @@ -167,12 +156,23 @@ const ContentGroupColumn: Component = (props) => { return menuOptions; }); const [highlight, setHighlight] = createSignal(false); + const columnContentLevel = (): ContentLevel => { + return ( + contentLevels[props.contentGroup.id || ""] || { + groups: [], + moreToLoad: false, + pieces: [], + loading: false + } + ); + }; createEffect( - on(contentPieces, () => { + on(activeContentGroupId, () => { scrollShadowController.processScrollState(); }) ); + contentLoader.loadContentLevel(props.contentGroup.id); return (
= (props) => { props.remove?.(el.dataset.contentGroupId || ""); }, onStart() { - setActiveDraggableGroup(props.contentGroup); + setActiveDraggableContentGroupId(props.contentGroup.id); }, onEnd() { - setActiveDraggableGroup(null); + setActiveDraggableContentGroupId(null); } }); }} @@ -224,51 +224,6 @@ const ContentGroupColumn: Component = (props) => {
event.preventDefault()} - onDragEnter={(event) => { - if ( - activeDraggableGroup() && - event.relatedTarget instanceof HTMLElement && - !event.target.contains(event.relatedTarget) - ) { - setHighlight(true); - } - }} - onDragLeave={(event) => { - if ( - activeDraggableGroup() && - event.relatedTarget instanceof HTMLElement && - !event.target.contains(event.relatedTarget) - ) { - setHighlight(false); - } - }} - onMouseEnter={() => { - if (activeDraggableGroup()) { - setHighlight(true); - } - }} - onMouseLeave={() => { - if (activeDraggableGroup()) { - setHighlight(false); - } - }} - onTouchMove={(event) => { - if (activeDraggableGroup()) { - const x = event.touches[0].clientX; - const y = event.touches[0].clientY; - const elementAtTouchPoint = document.elementFromPoint(x, y); - - if ( - elementAtTouchPoint === event.target || - elementAtTouchPoint?.parentNode === event.target - ) { - setHighlight(true); - } else { - setHighlight(false); - } - } - }} > = (props) => { )} content="paragraph" initialValue={props.contentGroup.name} - readOnly={Boolean(activeDraggableGroup() || !hasPermission("manageDashboard"))} + readOnly={Boolean( + activeDraggableContentGroupId() || !hasPermission("manageDashboard") + )} placeholder="Group name" onBlur={(editor) => { client.contentGroups.update.mutate({ @@ -326,7 +283,7 @@ const ContentGroupColumn: Component = (props) => { scrollableContainerRef={scrollableContainerRef} controller={scrollShadowController} onScrollEnd={() => { - loadMore(); + contentLoader.loadContentLevel(props.contentGroup.id); }} />
= (props) => { ref={setScrollableContainerRef} > 0} + when={!columnContentLevel().loading || columnContentLevel().pieces.length > 0} fallback={
@@ -344,7 +301,7 @@ const ContentGroupColumn: Component = (props) => { = (props) => { fallbackOnBody: true, onStart(event) { props.onDragStart?.(); - setActiveDraggablePiece( - contentPieces()[parseInt(event.item.dataset.index || "0")] + setActiveDraggableContentPieceId( + columnContentLevel().pieces[parseInt(event.item.dataset.index || "0")] ); }, onAdd(event) { - if (typeof event.oldIndex === "number" && typeof event.newIndex === "number") { - const id = event.item.dataset.contentPieceId || ""; - const baseReferenceContentPiece = contentPieces()[event.newIndex]; - const secondReferenceContentPiece = contentPieces()[event.newIndex - 1]; - const nextReferenceContentPiece = secondReferenceContentPiece; - const previousReferenceContentPiece = baseReferenceContentPiece; - - client.contentPieces.move.mutate({ - id, - contentGroupId: props.contentGroup.id, - nextReferenceId: nextReferenceContentPiece?.id, - previousReferenceId: previousReferenceContentPiece?.id - }); + if (typeof event.oldIndex !== "number" || typeof event.newIndex !== "number") { + return; } - const children = [...(event.to?.children || [])] as HTMLElement[]; - const newItems = children.map((value) => { - return ( - contentPieces().find( - (contentPiece) => contentPiece.id === value.dataset.contentPieceId - ) || activeDraggablePiece() - ); + const id = event.item.dataset.contentPieceId || ""; + const baseReferenceContentPieceId = columnContentLevel().pieces[event.newIndex]; + const secondReferenceContentPieceId = + columnContentLevel().pieces[event.newIndex - 1]; + const nextReferenceContentPieceId = secondReferenceContentPieceId; + const previousReferenceContentPieceId = baseReferenceContentPieceId; + + client.contentPieces.move.mutate({ + id, + contentGroupId: props.contentGroup.id, + nextReferenceId: nextReferenceContentPieceId, + previousReferenceId: previousReferenceContentPieceId }); + const children = [...(event.to?.children || [])] as HTMLElement[]; + if (typeof event.newIndex === "number") { children.splice(event.newIndex, 1); } event.to?.replaceChildren(...children); - setContentPieces( - newItems.map((item) => ({ - ...item, + contentActions.moveContentPiece({ + contentPiece: { + ...contentPieces[id]!, contentGroupId: props.contentGroup.id - })) as App.FullContentPieceWithAdditionalData[] - ); + }, + nextReferenceId: nextReferenceContentPieceId, + previousReferenceId: previousReferenceContentPieceId + }); }, onRemove(event) { const children = [...(event.from?.children || [])] as HTMLElement[]; - const newItems = children - .map((v) => { - return contentPieces().find( - (contentPiece) => contentPiece.id === v.dataset.contentPieceId - ); - }) - .filter((item) => item) as App.FullContentPieceWithAdditionalData[]; children.splice(event.oldIndex || 0, 0, event.item); event.from.replaceChildren(...children); - setContentPieces(newItems); + event.item.remove(); }, onUpdate(event) { if (typeof event.oldIndex === "number" && typeof event.newIndex === "number") { - const contentPiece = contentPieces()[event.oldIndex]; - const baseReferenceContentPiece = contentPieces()[event.newIndex]; - const secondReferenceContentPiece = - contentPieces()[ + const contentPieceId = columnContentLevel().pieces[event.oldIndex]; + const baseReferenceContentPieceId = + columnContentLevel().pieces[event.newIndex]; + const secondReferenceContentPieceId = + columnContentLevel().pieces[ event.oldIndex < event.newIndex ? event.newIndex + 1 : event.newIndex - 1 ]; - let nextReferenceContentPiece = secondReferenceContentPiece; - let previousReferenceContentPiece = baseReferenceContentPiece; + let nextReferenceContentPieceId = secondReferenceContentPieceId; + let previousReferenceContentPieceId = baseReferenceContentPieceId; if (event.oldIndex < event.newIndex) { - nextReferenceContentPiece = baseReferenceContentPiece; - previousReferenceContentPiece = secondReferenceContentPiece; + nextReferenceContentPieceId = baseReferenceContentPieceId; + previousReferenceContentPieceId = secondReferenceContentPieceId; } client.contentPieces.move.mutate({ - id: contentPiece?.id, - nextReferenceId: nextReferenceContentPiece?.id, - previousReferenceId: previousReferenceContentPiece?.id + id: contentPieceId, + nextReferenceId: nextReferenceContentPieceId, + previousReferenceId: previousReferenceContentPieceId }); - setActiveDraggablePiece(null); + + if (contentPieces[contentPieceId]) { + contentActions.moveContentPiece({ + contentPiece: { + ...contentPieces[contentPieceId]!, + contentGroupId: props.contentGroup.id + }, + nextReferenceId: nextReferenceContentPieceId, + previousReferenceId: previousReferenceContentPieceId + }); + } + + setActiveDraggableContentPieceId(null); } }, onEnd() { const children = [...(sortableRef()?.children || [])] as HTMLElement[]; - const newItems = children - .map((v) => { - return contentPieces().find((contentPiece) => { - return contentPiece.id.toString() === (v.dataset.contentPieceId || ""); - }); - }) - .filter((item) => item) as App.FullContentPieceWithAdditionalData[]; children.sort( (a, b) => parseInt(a.dataset.index || "") - parseInt(b.dataset.index || "") ); sortableRef()?.replaceChildren(...children); - setContentPieces(newItems); props.onDragEnd?.(); } }} > - {(contentPiece, index) => { - return ; + {(contentPieceId, index) => { + if (contentPieceId && contentPieces[contentPieceId]) { + return ( + + ); + } }} @@ -473,13 +433,14 @@ const ContentGroupColumn: Component = (props) => { text="soft" label="New content piece" onClick={async () => { - const { id } = await client.contentPieces.create.mutate({ + const newContentPieceData = { contentGroupId: props.contentGroup.id, - referenceId: contentPieces()[0]?.id, + referenceId: columnContentLevel().pieces[0], tags: [], members: [], title: "" - }); + }; + const { id } = await client.contentPieces.create.mutate(newContentPieceData); notify({ type: "success", text: "New content piece created" }); setStorage((storage) => ({ diff --git a/apps/web/src/views/dashboard/views/kanban/content-piece-card.tsx b/apps/web/src/views/dashboard/views/kanban/content-piece-card.tsx index 280aa4be..f5858c01 100644 --- a/apps/web/src/views/dashboard/views/kanban/content-piece-card.tsx +++ b/apps/web/src/views/dashboard/views/kanban/content-piece-card.tsx @@ -4,7 +4,13 @@ import dayjs from "dayjs"; import DOMPurify from "dompurify"; import clsx from "clsx"; import { useNavigate } from "@solidjs/router"; -import { App, hasPermission, useAuthenticatedUserData, useLocalStorage } from "#context"; +import { + App, + hasPermission, + useAuthenticatedUserData, + useContentData, + useLocalStorage +} from "#context"; import { Button, Card, Heading, Icon, IconButton, Tooltip } from "#components/primitives"; import { breakpoints, tagColorClasses } from "#lib/utils"; @@ -14,10 +20,10 @@ interface ContentPieceProps { } const ContentPieceCard: Component = (props) => { - const { setStorage, storage } = useLocalStorage(); + const { activeContentPieceId } = useContentData(); + const { setStorage } = useLocalStorage(); const { deletedTags } = useAuthenticatedUserData(); const navigate = useNavigate(); - const editedArticleId = (): string => storage().contentPieceId || ""; const displayTags = createMemo(() => { const visibleTags: App.Tag[] = []; const hiddenTags: App.Tag[] = []; @@ -130,8 +136,8 @@ const ContentPieceCard: Component = (props) => { { event.preventDefault(); diff --git a/apps/web/src/views/dashboard/views/kanban/index.tsx b/apps/web/src/views/dashboard/views/kanban/index.tsx index c536d5ad..8a9b8427 100644 --- a/apps/web/src/views/dashboard/views/kanban/index.tsx +++ b/apps/web/src/views/dashboard/views/kanban/index.tsx @@ -1,36 +1,36 @@ import { ContentGroupColumn, AddContentGroupColumn } from "./content-group-column"; -import { ContentGroupsContextProvider } from "../../content-groups-context"; import clsx from "clsx"; import { Component, createEffect, createSignal, on, Show } from "solid-js"; import { Sortable } from "#components/primitives"; import { createRef } from "#lib/utils"; import { ScrollShadow } from "#components/fragments"; -import { hasPermission, App, useClient } from "#context"; +import { hasPermission, useClient, useContentData, ContentLevel } from "#context"; -interface DashboardKanbanViewProps { - ancestor?: App.ContentGroup | null; - contentGroupsLoading?: boolean; - contentGroups: App.ContentGroup[]; - setAncestor(ancestor: App.ContentGroup | null | undefined): void; - setContentGroups(contentGroups: App.ContentGroup[]): void; -} - -const DashboardKanbanView: Component = (props) => { +const DashboardKanbanView: Component = () => { const client = useClient(); + const { activeContentGroupId, contentGroups, contentLevels, contentActions } = useContentData(); const [scrollableContainerRef, setScrollableContainerRef] = createRef(null); const [snapEnabled, setSnapEnabled] = createSignal(true); + const activeContentLevel = (): ContentLevel & { empty?: boolean } => { + return ( + contentLevels[activeContentGroupId() || ""] || { + groups: [], + moreToLoad: false, + pieces: [], + empty: true, + loading: false + } + ); + }; createEffect( - on( - () => props.contentGroups, - (_contentGroups, _previousContentGroups, previousAncestorId) => { - if (previousAncestorId === props.ancestor?.id) return; + on(activeContentGroupId, (contentGroupId, previousContentGroupId) => { + if (contentGroupId === previousContentGroupId) return; - scrollableContainerRef()!.scrollLeft = 0; + scrollableContainerRef()!.scrollLeft = 0; - return props.ancestor?.id; - } - ) + return contentGroupId; + }) ); return ( @@ -41,85 +41,76 @@ const DashboardKanbanView: Component = (props) => { direction="horizontal" offset="1.25rem" /> - props.ancestor} setAncestor={props.setAncestor}> - { - return props.contentGroups.find((contentGroup) => { - return contentGroup.id.toString() === (v.dataset.contentGroupId || ""); - }); - }) - .filter((item) => item) as App.ContentGroup[]; - - children.sort( - (a, b) => parseInt(a.dataset.index || "") - parseInt(b.dataset.index || "") - ); - scrollableContainerRef()?.replaceChildren(...children); - props.setContentGroups(newItems); - } - }} - ref={setScrollableContainerRef} - wrapperProps={{ - class: clsx( - "flex-1 h-full auto-rows-fr grid-flow-column grid-flow-col grid-template-rows grid auto-cols-[calc(100vw-2.5rem)] md:auto-cols-85 overflow-x-scroll scrollbar-sm-contrast md:mx-5", - snapEnabled() && `snap-mandatory snap-x` - ) - }} - > - {(contentGroup, index) => { - if (contentGroup) { - return ( - setSnapEnabled(false)} - onDragEnd={() => setSnapEnabled(true)} - remove={(id) => { - props.setContentGroups( - props.contentGroups.filter((contentGroup) => contentGroup.id !== id) - ); - }} - /> - ); } + }, + onStart() { + setSnapEnabled(false); + }, + onEnd() { + setSnapEnabled(true); + + const children = [...(scrollableContainerRef()?.children || [])] as HTMLElement[]; + children.sort( + (a, b) => parseInt(a.dataset.index || "") - parseInt(b.dataset.index || "") + ); + scrollableContainerRef()?.replaceChildren(...children); + } + }} + ref={setScrollableContainerRef} + wrapperProps={{ + class: clsx( + "flex-1 h-full auto-rows-fr grid-flow-column grid-flow-col grid-template-rows grid auto-cols-[calc(100vw-2.5rem)] md:auto-cols-85 overflow-x-scroll scrollbar-sm-contrast md:mx-5", + snapEnabled() && `snap-mandatory snap-x` + ) + }} + > + {(contentGroupId, index) => { + if (contentGroupId && contentGroups[contentGroupId]) { return ( - - - + setSnapEnabled(false)} + onDragEnd={() => setSnapEnabled(true)} + remove={(id) => { + if (id) { + contentActions.deleteContentGroup({ id }); + } + }} + /> ); - }} - - + } + + return ( + + + + ); + }} +
); }; diff --git a/apps/web/src/views/dashboard/views/list/content-piece-list.tsx b/apps/web/src/views/dashboard/views/table/content-piece-list.tsx similarity index 98% rename from apps/web/src/views/dashboard/views/list/content-piece-list.tsx rename to apps/web/src/views/dashboard/views/table/content-piece-list.tsx index abcf9b70..d1fe2973 100644 --- a/apps/web/src/views/dashboard/views/list/content-piece-list.tsx +++ b/apps/web/src/views/dashboard/views/table/content-piece-list.tsx @@ -21,15 +21,7 @@ import { Icon, Loader } from "#components/primitives"; -import { useContentPieces } from "#lib/composables"; -import { - App, - useCache, - useClient, - hasPermission, - useNotifications, - useConfirmationModal -} from "#context"; +import { App, useClient, hasPermission, useNotifications, useConfirmationModal } from "#context"; import { useContentGroupsContext } from "#views/dashboard/content-groups-context"; const ContentPieceList: Component<{ @@ -38,7 +30,6 @@ const ContentPieceList: Component<{ setContentPiecesLoading(loading: boolean): void; }> = (props) => { const { columns, tableWidth } = useDashboardListViewData(); - const cache = useCache(); const { notify } = useNotifications(); const [expanded, setExpanded] = createSignal(true); const [loadingAction, setLoadingAction] = createSignal(""); @@ -48,6 +39,7 @@ const ContentPieceList: Component<{ const { activeDraggablePiece, setActiveDraggablePiece, setAncestor } = useContentGroupsContext(); const [sortableRef, setSortableRef] = createRef(null); const client = useClient(); + // TODO: Use ContentData const { contentPieces, loadMore, moreToLoad, loading } = cache( `contentPieces:${props.ancestor.id}`, () => { diff --git a/apps/web/src/views/dashboard/views/list/content-piece-row.tsx b/apps/web/src/views/dashboard/views/table/content-piece-row.tsx similarity index 100% rename from apps/web/src/views/dashboard/views/list/content-piece-row.tsx rename to apps/web/src/views/dashboard/views/table/content-piece-row.tsx diff --git a/apps/web/src/views/dashboard/views/list/index.tsx b/apps/web/src/views/dashboard/views/table/index.tsx similarity index 98% rename from apps/web/src/views/dashboard/views/list/index.tsx rename to apps/web/src/views/dashboard/views/table/index.tsx index 18d96eaa..1e42844d 100644 --- a/apps/web/src/views/dashboard/views/list/index.tsx +++ b/apps/web/src/views/dashboard/views/table/index.tsx @@ -140,7 +140,7 @@ const List: Component = (props) => {
); }; -const DashboardListView: Component = (props) => { +const DashboardTableView: Component = (props) => { return (
props.ancestor} setAncestor={props.setAncestor}> @@ -152,4 +152,4 @@ const DashboardListView: Component = (props) => { ); }; -export { DashboardListView }; +export { DashboardTableView }; diff --git a/apps/web/src/views/dashboard/views/list/list-header.tsx b/apps/web/src/views/dashboard/views/table/list-header.tsx similarity index 100% rename from apps/web/src/views/dashboard/views/list/list-header.tsx rename to apps/web/src/views/dashboard/views/table/list-header.tsx diff --git a/apps/web/src/views/dashboard/views/list/list-view-context.tsx b/apps/web/src/views/dashboard/views/table/list-view-context.tsx similarity index 100% rename from apps/web/src/views/dashboard/views/list/list-view-context.tsx rename to apps/web/src/views/dashboard/views/table/list-view-context.tsx diff --git a/apps/web/src/views/editor/editor.tsx b/apps/web/src/views/editor/editor.tsx index 0e17879d..36ed6998 100644 --- a/apps/web/src/views/editor/editor.tsx +++ b/apps/web/src/views/editor/editor.tsx @@ -47,6 +47,7 @@ import { App, hasPermission, useAuthenticatedUserData, + useContentData, useHostConfig, useSharedState } from "#context"; @@ -55,9 +56,8 @@ import { BlockMenu } from "#lib/editor/extensions/slash-menu/component"; declare module "#context" { interface SharedState { - editor: SolidEditor; - provider: HocuspocusProvider; - editedContentPiece: App.ContentPieceWithAdditionalData; + editor?: SolidEditor; + provider?: HocuspocusProvider; } } @@ -70,10 +70,12 @@ interface EditorProps { const Editor: Component = (props) => { const hostConfig = useHostConfig(); - const createSharedSignal = useSharedState(); const navigate = useNavigate(); const location = useLocation<{ breadcrumb?: string[] }>(); - const [activeVariant] = createSharedSignal("activeVariant"); + const { useSharedSignal } = useSharedState(); + /* TODO: Use ContentData + const [activeVariant] = createSharedSignal("activeVariant"); */ + const activeVariant = (): null => null; const ydoc = new Y.Doc(); const [containerRef, setContainerRef] = createRef(null); const handleReload = async (): Promise => { @@ -167,12 +169,9 @@ const Editor: Component = (props) => { el = event?.relatedTarget as HTMLElement | null; } }); - const [, setSharedEditor] = createSharedSignal("editor", editor()); - const [, setSharedProvider] = createSharedSignal("provider", provider); - const [, setEditedContentPiece] = createSharedSignal( - "editedContentPiece", - props.editedContentPiece - ); + const [, setSharedEditor] = useSharedSignal("editor", editor()); + const [, setSharedProvider] = useSharedSignal("provider", provider); + const { setActiveContentPieceId } = useContentData(); const shouldShow = (editor: SolidEditor): boolean => { el = null; @@ -249,7 +248,7 @@ const Editor: Component = (props) => { provider.destroy(); setSharedEditor(undefined); setSharedProvider(undefined); - setEditedContentPiece(undefined); + setActiveContentPieceId(null); }); createEffect( on( @@ -260,8 +259,7 @@ const Editor: Component = (props) => { ) ); createEffect( - on([() => props.editedContentPiece, editor], () => { - setEditedContentPiece(props.editedContentPiece); + on(editor, () => { setSharedEditor(editor()); setSharedProvider(provider); }) diff --git a/apps/web/src/views/editor/index.tsx b/apps/web/src/views/editor/index.tsx index 421e1989..1b578479 100644 --- a/apps/web/src/views/editor/index.tsx +++ b/apps/web/src/views/editor/index.tsx @@ -2,13 +2,11 @@ import { Editor } from "./editor"; import { Component, createEffect, createSignal, on, onCleanup, Show } from "solid-js"; import clsx from "clsx"; import { Loader } from "#components/primitives"; -import { useAuthenticatedUserData, useCache, useLocalStorage } from "#context"; +import { useAuthenticatedUserData, useContentData, useLocalStorage } from "#context"; import { createRef } from "#lib/utils"; -import { useOpenedContentPiece } from "#lib/composables"; const EditorView: Component = () => { - const cache = useCache(); - const { contentPiece, loading } = cache("openedContentPiece", useOpenedContentPiece); + const { contentPieces, activeContentPieceId } = useContentData(); const { storage, setStorage } = useLocalStorage(); const { workspaceSettings } = useAuthenticatedUserData(); const [syncing, setSyncing] = createSignal(true); @@ -42,7 +40,7 @@ const EditorView: Component = () => { }); createEffect( on( - contentPiece, + activeContentPieceId, () => { setSyncing(true); }, @@ -54,14 +52,14 @@ const EditorView: Component = () => { return ( <> - - - To edit, select an article in the dashboard - - + {/* */} + + To edit, select an article in the dashboard + + {/* */}
} > @@ -78,9 +76,9 @@ const EditorView: Component = () => { storage().zenMode ? "items-center" : "items-start" )} > - + { setReloaded(true); @@ -96,7 +94,7 @@ const EditorView: Component = () => {
- +
diff --git a/apps/web/src/views/editor/menus/export/index.tsx b/apps/web/src/views/editor/menus/export/index.tsx index 71f40e20..7ccc33fb 100644 --- a/apps/web/src/views/editor/menus/export/index.tsx +++ b/apps/web/src/views/editor/menus/export/index.tsx @@ -37,9 +37,9 @@ interface ExportMenuProps { type ExportType = "html" | "json" | "md" | "mdx"; const ExportMenu: Component = (props) => { - const createSharedSignal = useSharedState(); + const { useSharedSignal } = useSharedState(); const client = useClient(); - const [editor] = createSharedSignal("editor"); + const [editor] = useSharedSignal("editor"); const { registerCommand = () => {} } = useCommandPalette() || {}; const { workspaceSettings = () => null } = useAuthenticatedUserData() || {}; const { notify } = useNotifications(); diff --git a/apps/web/src/views/explorer/content-group-row.tsx b/apps/web/src/views/explorer/content-group-row.tsx index f247e151..de1c7467 100644 --- a/apps/web/src/views/explorer/content-group-row.tsx +++ b/apps/web/src/views/explorer/content-group-row.tsx @@ -21,45 +21,35 @@ import { hasPermission, useClient, useConfirmationModal, - useNotifications, - useSharedState + useContentData, + useNotifications } from "#context"; interface ContentGroupRowProps { contentGroup: App.ContentGroup; - customLabel?: string; - menuDisabled?: boolean; - draggable?: boolean; loading?: boolean; opened?: boolean; - active?: boolean; - highlight?: boolean; - removeContentGroup(id: string): void; - removeContentPiece(id: string): void; onClick?(): void; onExpand?(forceOpen?: boolean): void; onDragEnd?(event: SortableLib.SortableEvent): void; } -declare module "#context" { - interface SharedState { - activeDraggableGroup: App.ContentGroup | null; - } -} - const ContentGroupRow: Component = (props) => { const { loading, renaming, setLoading, setRenaming } = useExplorerData(); - const createSharedSignal = useSharedState(); const client = useClient(); const location = useLocation(); const { notify } = useNotifications(); const { confirmDelete } = useConfirmationModal(); - const { setLevels, setContentGroups } = useExplorerData(); - const [activeDraggablePiece] = createSharedSignal("activeDraggablePiece", null); - const [activeDraggableGroup, setActiveDraggableGroup] = createSharedSignal( - "activeDraggableGroup", - null - ); + const { + activeContentGroupId, + activeDraggableContentGroupId, + activeDraggableContentPieceId, + setActiveDraggableContentGroupId, + expandedContentLevels, + collapseContentLevel, + contentActions + } = useContentData(); + const { highlight } = useExplorerData(); const [dropdownOpened, setDropdownOpened] = createSignal(false); const menuOptions = createMemo(() => { const menuOptions: Array<{ @@ -113,7 +103,11 @@ const ContentGroupRow: Component = (props) => { }); setDropdownOpened(false); - setRenaming(newContentPiece.id); + + if (expandedContentLevels().includes(props.contentGroup.id)) { + setRenaming(newContentPiece.id); + } + setLoading(""); notify({ text: "Content piece created", type: "success" }); } catch (error) { @@ -130,13 +124,17 @@ const ContentGroupRow: Component = (props) => { setLoading(props.contentGroup.id); try { - const newContentPiece = await client.contentGroups.create.mutate({ + const newContentGroup = await client.contentGroups.create.mutate({ ancestor: props.contentGroup.id, name: "" }); setDropdownOpened(false); - setRenaming(newContentPiece.id); + + if (expandedContentLevels().includes(props.contentGroup.id)) { + setRenaming(newContentGroup.id); + } + setLoading(""); notify({ text: "Content group created", type: "success" }); } catch (error) { @@ -167,10 +165,8 @@ const ContentGroupRow: Component = (props) => { try { setLoading(props.contentGroup.id); await client.contentGroups.delete.mutate({ id: props.contentGroup.id }); - setLevels(props.contentGroup.ancestors.at(-1) || "", "groups", (groups) => { - return groups.filter((groupId) => groupId !== props.contentGroup?.id); - }); - setContentGroups(props.contentGroup.id, undefined); + collapseContentLevel(props.contentGroup.id); + contentActions.deleteContentGroup({ id: props.contentGroup.id }); setLoading(""); notify({ text: "Content group deleted", type: "success" }); } catch (error) { @@ -186,12 +182,17 @@ const ContentGroupRow: Component = (props) => { return menuOptions; }); + const active = (): boolean => activeContentGroupId() === props.contentGroup.id; + const highlighted = (): boolean => highlight() === props.contentGroup.id; return (
{ SortableLib.create(el, { @@ -208,21 +209,18 @@ const ContentGroupRow: Component = (props) => { fallbackOnBody: true, sort: false, onStart() { - setActiveDraggableGroup(props.contentGroup); + setActiveDraggableContentGroupId(props.contentGroup.id); }, onEnd(event) { event.preventDefault(); props.onDragEnd?.(event); - setActiveDraggableGroup(null); + setActiveDraggableContentGroupId(null); } }); }} >
= (props) => { = (props) => { id: props.contentGroup.id, name }); - setContentGroups(props.contentGroup.id, { ...props.contentGroup, name }); + contentActions.updateContentGroup({ id: props.contentGroup.id, name }); setRenaming(""); }} onChange={(event) => { @@ -284,7 +282,7 @@ const ContentGroupRow: Component = (props) => { id: props.contentGroup.id, name }); - setContentGroups(props.contentGroup.id, { ...props.contentGroup, name }); + contentActions.updateContentGroup({ id: props.contentGroup.id, name }); setRenaming(""); }} /> @@ -293,16 +291,16 @@ const ContentGroupRow: Component = (props) => { - {props.customLabel || props.contentGroup?.name || ""} + {props.contentGroup?.name || ""} diff --git a/apps/web/src/views/explorer/content-piece-row.tsx b/apps/web/src/views/explorer/content-piece-row.tsx index 34b78069..5a15b3f7 100644 --- a/apps/web/src/views/explorer/content-piece-row.tsx +++ b/apps/web/src/views/explorer/content-piece-row.tsx @@ -19,32 +19,31 @@ import { hasPermission, useClient, useConfirmationModal, - useNotifications, - useSharedState + useContentData, + useNotifications } from "#context"; import { createRef } from "#lib/utils"; interface ContentPieceRowProps { contentPiece: App.ExtendedContentPieceWithAdditionalData<"order">; - active?: boolean; onDragEnd?(event: SortableLib.SortableEvent): void; onClick?(): void; } const ContentPieceRow: Component = (props) => { - const { renaming, loading, setContentPieces, setLevels, setRenaming, setLoading } = - useExplorerData(); - const createSharedSignal = useSharedState(); + const { + setActiveDraggableContentPieceId, + activeContentPieceId, + activeDraggableContentGroupId, + activeDraggableContentPieceId, + contentActions + } = useContentData(); + const { renaming, loading, setRenaming, setLoading } = useExplorerData(); const { confirmDelete } = useConfirmationModal(); const { notify } = useNotifications(); const client = useClient(); const location = useLocation(); const [sortableInstanceRef, setSortableInstanceRef] = createRef(null); - const [activeDraggablePiece, setActiveDraggablePiece] = createSharedSignal( - "activeDraggablePiece", - null - ); - const [activeDraggableGroup] = createSharedSignal("activeDraggableGroup", null); const [dropdownOpened, setDropdownOpened] = createSignal(false); const menuOptions = createMemo(() => { const menuOptions: Array<{ @@ -87,10 +86,7 @@ const ContentPieceRow: Component = (props) => { try { setLoading(props.contentPiece.id); await client.contentPieces.delete.mutate({ id: props.contentPiece.id }); - setLevels(props.contentPiece.contentGroupId, "pieces", (pieces) => { - return pieces.filter((pieceId) => pieceId !== props.contentPiece.id); - }); - setContentPieces(props.contentPiece.id, undefined); + contentActions.deleteContentPiece({ id: props.contentPiece.id }); setLoading(""); notify({ text: "Content piece deleted", type: "success" }); } catch (error) { @@ -106,6 +102,7 @@ const ContentPieceRow: Component = (props) => { return menuOptions; }); + const active = (): boolean => activeContentPieceId() === props.contentPiece.id; createEffect(() => { const sortableInstance = sortableInstanceRef(); @@ -123,7 +120,8 @@ const ContentPieceRow: Component = (props) => { class={clsx( "flex flex-1 justify-center items-center cursor-pointer overflow-hidden ml-0.5 group", !dropdownOpened() && - (location.pathname !== "/editor" || !props.active) && + !activeDraggableContentPieceId() && + (location.pathname !== "/editor" || !active()) && "@hover-bg-gray-200 dark:@hover-bg-gray-700" )} ref={(el) => { @@ -139,12 +137,12 @@ const ContentPieceRow: Component = (props) => { revertOnSpill: true, sort: false, onStart() { - setActiveDraggablePiece(props.contentPiece); + setActiveDraggableContentPieceId(props.contentPiece.id); }, onEnd(event) { event.preventDefault(); props.onDragEnd?.(event); - setActiveDraggablePiece(null); + setActiveDraggableContentPieceId(null); } }); @@ -163,9 +161,9 @@ const ContentPieceRow: Component = (props) => { = (props) => { id: props.contentPiece.id, title }); - setContentPieces(props.contentPiece.id, { ...props.contentPiece, title }); + contentActions.updateContentPiece({ id: props.contentPiece.id, title }); setRenaming(""); }} onChange={(event) => { @@ -201,7 +199,7 @@ const ContentPieceRow: Component = (props) => { id: props.contentPiece.id, title }); - setContentPieces(props.contentPiece.id, { ...props.contentPiece, title }); + contentActions.updateContentPiece({ id: props.contentPiece.id, title }); setRenaming(""); }} /> @@ -210,9 +208,9 @@ const ContentPieceRow: Component = (props) => { ; - contentGroups: Record; - contentPieces: Record | undefined>; renaming: Accessor; loading: Accessor; - setLevels: SetStoreFunction>; - setContentGroups: SetStoreFunction>; - setContentPieces: SetStoreFunction< - Record | undefined> - >; + highlight: Accessor; setRenaming: Setter; setLoading: Setter; + setHighlight: Setter; } const ExplorerDataContext = createContext(); const ExplorerDataProvider: ParentComponent = (props) => { - const [levels, setLevels] = createStore>({}); - const [contentGroups, setContentGroups] = createStore< - Record - >({}); - const [contentPieces, setContentPieces] = createStore< - Record | undefined> - >({}); const [renaming, setRenaming] = createSignal(""); const [loading, setLoading] = createSignal(""); + const [highlight, setHighlight] = createSignal(""); return ( {props.children} @@ -65,4 +42,3 @@ const useExplorerData = (): ExplorerContextData => { }; export { ExplorerDataProvider, useExplorerData }; -export type { Level }; diff --git a/apps/web/src/views/explorer/index.tsx b/apps/web/src/views/explorer/index.tsx index 40d47c45..7623bf4f 100644 --- a/apps/web/src/views/explorer/index.tsx +++ b/apps/web/src/views/explorer/index.tsx @@ -1,24 +1,73 @@ -import { DashboardListView } from "./list"; +import { TreeLevel } from "./tree-level"; import { ExplorerDataProvider } from "./explorer-context"; import { Component } from "solid-js"; -import { App, useLocalStorage } from "#context"; +import { mdiClose, mdiHexagonSlice6 } from "@mdi/js"; +import { createRef } from "@vrite/components/src/ref"; +import { Heading, IconButton } from "#components/primitives"; +import { useAuthenticatedUserData, useContentData, useLocalStorage } from "#context"; +import { ScrollShadow } from "#components/fragments"; -const ExplorerView: Component = () => { - const { storage, setStorage } = useLocalStorage(); - const ancestor = (): App.ContentGroup | null => { - return storage().dashboardViewAncestor || null; - }; - const setAncestor = (ancestor: App.ContentGroup | null): void => { - setStorage((storage) => ({ - ...storage, - dashboardViewAncestor: ancestor || undefined - })); - }; +const ExplorerTree: Component = () => { + const { activeContentGroupId } = useContentData(); + const { setStorage } = useLocalStorage(); + const { workspace } = useAuthenticatedUserData(); + const [scrollableContainerRef, setScrollableContainerRef] = createRef(null); + return ( +
+
+
+ { + setStorage((storage) => ({ + ...storage, + sidePanelWidth: 0 + })); + }} + /> + + Explorer + +
+ { + setStorage((storage) => ({ + ...storage, + activeContentGroupId: undefined + })); + }} + label={{workspace()?.name}} + /> +
+
+
+ +
+ +
+
+
+
+ ); +}; +const ExplorerView: Component = () => { return (
- +
); diff --git a/apps/web/src/views/explorer/list.tsx b/apps/web/src/views/explorer/list.tsx deleted file mode 100644 index 0cb7a06b..00000000 --- a/apps/web/src/views/explorer/list.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { TreeLevel } from "./tree-level"; -import { Level, useExplorerData } from "./explorer-context"; -import { Component, createSignal } from "solid-js"; -import { mdiClose, mdiHexagonSlice6 } from "@mdi/js"; -import { createRef } from "@vrite/components/src/ref"; -import { Heading, IconButton } from "#components/primitives"; -import { App, useAuthenticatedUserData, useClient, useLocalStorage } from "#context"; -import { ScrollShadow } from "#components/fragments"; - -interface DashboardListViewProps { - ancestor?: App.ContentGroup | null; - setAncestor(ancestor: App.ContentGroup | null | undefined): void; -} - -const [highlight, setHighlight] = createSignal(""); -const DashboardListView: Component = (props) => { - const { levels, contentGroups, contentPieces, setContentGroups, setContentPieces, setLevels } = - useExplorerData(); - const client = useClient(); - const { storage, setStorage } = useLocalStorage(); - const { workspace } = useAuthenticatedUserData(); - const [scrollableContainerRef, setScrollableContainerRef] = createRef(null); - const openedLevels = (): string[] => { - return storage().explorerOpenedLevels || []; - }; - const openLevel = (parentId: string): void => { - setStorage((storage) => ({ - ...storage, - explorerOpenedLevels: [...new Set([...openedLevels(), parentId])] - })); - }; - const closeLevel = (parentId: string): void => { - const levelsToClose = [parentId]; - const addLevelsToClose = (parentId: string): void => { - const level = levels[parentId]; - - if (!level) { - return; - } - - levelsToClose.push(...level.groups); - level.groups.forEach((groupId) => addLevelsToClose(groupId)); - }; - - addLevelsToClose(parentId); - setStorage((storage) => ({ - ...storage, - explorerOpenedLevels: - storage.explorerOpenedLevels?.filter((id) => !levelsToClose.includes(id)) || [] - })); - }; - const loadLevel = async (parentId?: string, preload?: boolean): Promise => { - const existingLevel = levels[parentId || ""]; - - if (existingLevel && existingLevel.moreToLoad) { - if (parentId) { - const level = { - groups: [...existingLevel.groups], - pieces: [...existingLevel.pieces], - moreToLoad: false - }; - const lastPieceId = existingLevel.pieces.at(-1); - const lastPiece = contentPieces[lastPieceId || ""]; - - if (!lastPiece) { - setLevels(parentId, level); - - return; - } - - const newContentPieces = await client.contentPieces.list.query({ - contentGroupId: parentId, - lastOrder: lastPiece.order - }); - - level.pieces.push(...newContentPieces.map((contentPiece) => contentPiece.id)); - newContentPieces.forEach((contentPiece) => { - setContentPieces(contentPiece.id, contentPiece); - }); - - if (newContentPieces.length === 20) { - level.moreToLoad = true; - } - - setLevels(parentId, level); - } - - return; - } - - const contentGroups = await client.contentGroups.list.query({ - ancestor: parentId || undefined - }); - const level: Level = { - groups: contentGroups.map((contentGroup) => contentGroup.id), - pieces: [], - moreToLoad: false - }; - - contentGroups.forEach((contentGroup) => { - if (preload) { - loadLevel(contentGroup.id); - } - - setContentGroups(contentGroup.id, contentGroup); - }); - - if (parentId) { - const contentPieces = await client.contentPieces.list.query({ - contentGroupId: parentId - }); - - level.pieces = contentPieces.map((contentPiece) => contentPiece.id); - contentPieces.forEach((contentPiece) => { - setContentPieces(contentPiece.id, contentPiece); - }); - - if (contentPieces.length === 20) { - level.moreToLoad = true; - } - } - - setLevels(parentId || "", level); - }; - - loadLevel("", true); - openedLevels().forEach((id) => loadLevel(id)); - client.contentGroups.changes.subscribe(undefined, { - onData({ action, data }) { - if (action === "move") { - const [currentParentId] = Object.entries(levels).find(([, level]) => { - return level?.groups.find((groupId) => groupId === data.id); - })!; - - setLevels(currentParentId, "groups", (groups) => { - return groups.filter((groupId) => groupId !== data.id); - }); - setLevels(data.ancestors[data.ancestors.length - 1] || "", "groups", (groups) => [ - ...groups, - data.id - ]); - } else if (action === "create") { - setContentGroups(data.id, data); - // eslint-disable-next-line sonarjs/no-identical-functions - setLevels(data.ancestors[data.ancestors.length - 1] || "", "groups", (groups) => [ - ...groups, - data.id - ]); - } else if (action === "delete") { - const parentId = contentGroups[data.id]?.ancestors.at(-1); - - setContentGroups(data.id, undefined); - setLevels(parentId || "", "groups", (groups) => { - return groups.filter((groupId) => groupId !== data.id); - }); - } else if (action === "update") { - const parentId = contentGroups[data.id]?.ancestors.at(-1); - - setContentGroups(data.id, data); - // update - } else if (action === "reorder") { - // reorder - } - } - }); - - return ( -
-
-
- { - setStorage((storage) => ({ - ...storage, - sidePanelWidth: 0 - })); - }} - /> - - Explorer - -
- { - setStorage((storage) => ({ - ...storage, - dashboardViewAncestor: undefined - })); - }} - label={{workspace()?.name}} - /> -
-
-
- -
- -
-
-
-
- ); -}; - -export { DashboardListView }; diff --git a/apps/web/src/views/explorer/tree-level.tsx b/apps/web/src/views/explorer/tree-level.tsx index 715dc845..422c19d2 100644 --- a/apps/web/src/views/explorer/tree-level.tsx +++ b/apps/web/src/views/explorer/tree-level.tsx @@ -2,127 +2,69 @@ import { ContentGroupRow } from "./content-group-row"; import { ContentPieceRow } from "./content-piece-row"; import { useExplorerData } from "./explorer-context"; import clsx from "clsx"; -import { Component, createEffect, Show, For, Setter, onCleanup, createSignal } from "solid-js"; +import { Component, createEffect, Show, For, createSignal } from "solid-js"; import SortableLib from "sortablejs"; -import { Button, Icon, Loader } from "@vrite/components"; +import { Icon, Loader } from "@vrite/components"; import { mdiDotsHorizontalCircleOutline } from "@mdi/js"; import { useLocation, useNavigate } from "@solidjs/router"; -import { useClient, useLocalStorage, useSharedState } from "#context"; +import { App, useClient, useContentData, useLocalStorage } from "#context"; const TreeLevel: Component<{ parentId?: string; - openedLevels: string[]; - highlight: string; - setHighlight: Setter; - openLevel(parentId: string): void; - closeLevel(parentId: string): void; - loadLevel(parentId: string, preload?: boolean): Promise; }> = (props) => { - const { contentPieces, contentGroups, levels, setContentPieces, setContentGroups, setLevels } = - useExplorerData(); - const { storage, setStorage } = useLocalStorage(); - const createSharedSignal = useSharedState(); + const { + contentPieces, + contentGroups, + contentLevels, + expandedContentLevels, + expandContentLevel, + collapseContentLevel, + activeContentGroupId, + activeDraggableContentGroupId, + activeDraggableContentPieceId, + contentLoader, + contentActions + } = useContentData(); + const { highlight, setHighlight } = useExplorerData(); + const { setStorage } = useLocalStorage(); const client = useClient(); const location = useLocation(); const navigate = useNavigate(); - const [activeDraggableGroup] = createSharedSignal("activeDraggableGroup", null); - const [activeDraggablePiece] = createSharedSignal("activeDraggablePiece", null); - const [loadingMore, setLoadingMore] = createSignal(false); const handleHighlight = (event: DragEvent | MouseEvent | TouchEvent, groupId: string): void => { - const draggablePiece = activeDraggablePiece(); + const draggablePieceId = activeDraggableContentPieceId(); + const draggableGroupId = activeDraggableContentGroupId(); - let draggableGroup = activeDraggableGroup(); + let group = contentGroups[draggableGroupId || ""]; - if (!draggableGroup && draggablePiece) { - draggableGroup = contentGroups[draggablePiece.contentGroupId]; + if (!draggableGroupId && draggablePieceId) { + const draggablePiece = contentPieces[draggablePieceId]; + + if (!draggablePiece) return; + + group = contentGroups[draggablePiece.contentGroupId]; } - if (draggableGroup) { + if (group) { if ( - groupId !== draggableGroup?.id && - (draggablePiece || !contentGroups[groupId || ""]?.ancestors.includes(draggableGroup!.id)) && - (draggablePiece || draggableGroup?.ancestors.at(-1) !== groupId) + groupId !== group.id && + (draggablePieceId || !contentGroups[groupId || ""]?.ancestors.includes(group.id)) && + (draggablePieceId || group?.ancestors.at(-1) !== groupId) ) { - props.setHighlight(groupId || ""); + setHighlight(groupId || ""); } else { - props.setHighlight(""); + setHighlight(""); } } else { - props.setHighlight((highlight) => (highlight === groupId ? "" : highlight)); + setHighlight((highlight) => (highlight === groupId ? "" : highlight)); } event.stopPropagation(); }; - const selected = (): boolean => storage().dashboardViewAncestor?.id === props.parentId; - - if (props.parentId) { - const contentPiecesSubscription = client.contentPieces.changes.subscribe( - { contentGroupId: props.parentId }, - { - onData({ action, data }) { - if (action === "update") { - setContentPieces(data.id, data); - } else if (action === "create") { - setContentPieces(data.id, data); - setLevels(props.parentId || "", "pieces", (pieces) => [data.id, ...pieces]); - } else if (action === "delete") { - setContentPieces(data.id, undefined); - setLevels(props.parentId || "", "pieces", (pieces) => { - return pieces.filter((pieceId) => pieceId !== data.id); - }); - } else if (action === "move") { - setContentPieces(data.contentPiece.id, data.contentPiece); - - if (data.contentPiece.contentGroupId === props.parentId) { - if (data.nextReferenceId) { - setLevels(props.parentId || "", "pieces", (pieces) => { - const newPieces = [ - ...pieces.filter((pieceId) => pieceId !== data.contentPiece.id) - ]; - const index = newPieces.indexOf(data.nextReferenceId!); - - if (index < 0) return pieces; - - newPieces.splice(index + 1, 0, data.contentPiece.id); - - return newPieces; - }); - } else if (data.previousReferenceId) { - setLevels(props.parentId || "", "pieces", (pieces) => { - const newPieces = [ - ...pieces.filter((pieceId) => pieceId !== data.contentPiece.id) - ]; - const index = newPieces.indexOf(data.previousReferenceId!); - - if (index < 0) return pieces; - - newPieces.splice(index, 0, data.contentPiece.id); - - return newPieces; - }); - } else { - setLevels(props.parentId || "", "pieces", (pieces) => { - return [data.contentPiece.id, ...pieces]; - }); - } - } else { - setLevels(props.parentId || "", "pieces", (pieces) => { - return pieces.filter((pieceId) => pieceId !== data.contentPiece.id); - }); - } - } - } - } - ); - - onCleanup(() => { - contentPiecesSubscription.unsubscribe(); - }); - } + const selected = (): boolean => activeContentGroupId() === props.parentId; createEffect(() => { - if (!activeDraggableGroup() && !activeDraggablePiece()) { - props.setHighlight(""); + if (!activeDraggableContentGroupId() && !activeDraggableContentPieceId()) { + setHighlight(""); } }); @@ -131,7 +73,7 @@ const TreeLevel: Component<{
@@ -139,10 +81,10 @@ const TreeLevel: Component<{ class={clsx( "h-full w-0.5 -left-[0.5px] left-0 absolute rounded-full bg-black bg-opacity-5 dark:bg-white dark:bg-opacity-10", ((selected() && - !activeDraggableGroup() && - !activeDraggablePiece() && + !activeDraggableContentGroupId() && + !activeDraggableContentPieceId() && location.pathname === "/") || - props.highlight === props.parentId) && + highlight() === props.parentId) && "!bg-gradient-to-tr" )} /> @@ -150,9 +92,9 @@ const TreeLevel: Component<{
@@ -171,7 +113,7 @@ const TreeLevel: Component<{
- + {(groupId) => { return (
(highlight === groupId ? "" : highlight)); + setHighlight((highlight) => (highlight === groupId ? "" : highlight)); } }} onTouchMove={(event) => { @@ -205,8 +147,8 @@ const TreeLevel: Component<{ } }} onPointerLeave={(event) => { - if (activeDraggableGroup()) { - props.setHighlight((highlight) => (highlight === groupId ? "" : highlight)); + if (activeDraggableContentGroupId()) { + setHighlight((highlight) => (highlight === groupId ? "" : highlight)); } }} ref={(el) => { @@ -220,39 +162,23 @@ const TreeLevel: Component<{ > {}} - removeContentPiece={() => {}} - loading={props.openedLevels.includes(groupId || "") && !levels[groupId]} - opened={props.openedLevels.includes(groupId || "")} - active={storage().dashboardViewAncestor?.id === groupId} - highlight={props.highlight === groupId} + loading={ + expandedContentLevels().includes(groupId || "") && !contentLevels[groupId] + } + opened={expandedContentLevels().includes(groupId || "")} onDragEnd={() => { const group = contentGroups[groupId]!; - const newParentId = props.highlight || ""; - const oldParentId = group.ancestors.at(-1) || ""; + const newParentId = highlight() || ""; const newParent = contentGroups[newParentId]; - const oldParent = contentGroups[oldParentId]; - - if (newParentId === oldParentId || !newParent || !oldParent) return; - if (levels[oldParentId]) { - setLevels(oldParentId, "groups", (groupIds) => { - return groupIds.filter((filteredGroupId) => filteredGroupId !== group.id); + if (newParent) { + collapseContentLevel(groupId); + contentActions.moveContentGroup({ + ancestors: [...(newParent?.ancestors || []), newParentId || ""], + id: group.id }); } - setContentGroups(group.id, { - ...group, - ancestors: [...newParent.ancestors, newParentId] - }); - setContentGroups(oldParentId, { - ...oldParent, - descendants: oldParent.descendants.filter((id) => id !== group.id) - }); - setContentGroups(newParentId, { - ...newParent, - descendants: [...newParent.descendants, group.id] - }); client.contentGroups.move.mutate({ id: group.id, ancestor: newParentId || null @@ -262,76 +188,53 @@ const TreeLevel: Component<{ navigate("/"); setStorage((storage) => ({ ...storage, - dashboardViewAncestor: contentGroups[groupId] + activeContentGroupId: groupId })); }} onExpand={(forceOpen) => { - if (props.openedLevels.includes(groupId) && !forceOpen) { - props.closeLevel(groupId); + if (expandedContentLevels().includes(groupId) && !forceOpen) { + collapseContentLevel(groupId); } else { - props.loadLevel(groupId, true); - props.openLevel(groupId); + contentLoader.loadContentLevel(groupId, true); + expandContentLevel(groupId); } }} /> - - + +
); }}
- - {(pieceId) => { + + {(contentPieceId) => { return ( { navigate("/editor"); setStorage((storage) => ({ ...storage, - contentPieceId: pieceId + contentPieceId })); }} onDragEnd={() => { - const piece = contentPieces[pieceId]!; - const newParentId = props.highlight || ""; - const oldParentId = piece.contentGroupId; - const newParent = contentGroups[newParentId]; - const oldParent = contentGroups[oldParentId]; - - if (newParentId === oldParentId || !newParent || !oldParent) return; - - if (levels[oldParentId]) { - setLevels(oldParentId, "pieces", (pieces) => { - return pieces.filter((filteredPieceId) => filteredPieceId !== piece.id); - }); - } + const contentPiece = contentPieces[contentPieceId]!; + const newParentId = highlight() || ""; + const oldParentId = contentPiece.contentGroupId; + const updatedContentPiece: App.ExtendedContentPieceWithAdditionalData<"order"> = { + ...contentPiece, + contentGroupId: newParentId + }; - if (levels[newParentId]) { - setLevels(newParentId, "pieces", (pieces) => { - return [piece.id, ...pieces]; - }); - } - - if (levels[newParentId]) { - setContentPieces(piece.id, (piece) => ({ - ...piece, - contentGroupId: newParentId - })); - } + if (!newParentId || newParentId === oldParentId) return; + contentActions.moveContentPiece({ + contentPiece: updatedContentPiece + }); client.contentPieces.move.mutate({ - id: piece.id, + id: contentPieceId, contentGroupId: newParentId || undefined }); }} @@ -339,25 +242,26 @@ const TreeLevel: Component<{ ); }} - +
diff --git a/apps/web/src/views/git/sync-view/index.tsx b/apps/web/src/views/git/sync-view/index.tsx index 343990e7..315eab23 100644 --- a/apps/web/src/views/git/sync-view/index.tsx +++ b/apps/web/src/views/git/sync-view/index.tsx @@ -215,12 +215,12 @@ const CommitCard: Component<{ changedRecords: App.GitRecord[] }> = (props) => { }; const PullCard: Component = () => { const client = useClient(); - const createSharedSignal = useSharedState(); + const { useSharedSignal } = useSharedState(); const navigate = useNavigate(); const { notify } = useNotifications(); const [loading, setLoading] = createSignal(false); - const [conflicts, setConflicts] = createSharedSignal("conflicts", []); - const [conflictData, setConflictData] = createSharedSignal("conflictData"); + const [conflicts, setConflicts] = useSharedSignal("conflicts", []); + const [conflictData, setConflictData] = useSharedSignal("conflictData"); return ( {
@@ -193,7 +193,7 @@ const SettingsView: Component = () => { animate={{ opacity: 1, x: "0%" }} exit={{ opacity: 0, x: currentSectionId() === "menu" ? "-100%" : "100%" }} transition={{ duration: 0.35 }} - class="flex justify-start flex-col min-h-[calc(100%-env(safe-area-inset-bottom,0px))] items-start w-full gap-5 absolute" + class="flex justify-start flex-col min-h-[calc(100%-env(safe-area-inset-bottom,0px))] items-start w-full gap-5 absolute pb-5" > { - const createSharedSignal = useSharedState(); + const { useSharedSignal } = useSharedState(); const { storage, setStorage } = useLocalStorage(); const [containerRef, setContainerRef] = createRef(null); const [bubbleMenuOpened, setBubbleMenuOpened] = createSignal(true); @@ -145,7 +145,7 @@ const Editor: Component = () => { })); } }); - const [, setSharedEditor] = createSharedSignal("editor", editor()); + const [, setSharedEditor] = useSharedSignal("editor", editor()); const shouldShow = (editor: SolidEditor): boolean => { el = null;