diff --git a/packages/app-aco/package.json b/packages/app-aco/package.json index f87f32a5e36..b887e7b4871 100644 --- a/packages/app-aco/package.json +++ b/packages/app-aco/package.json @@ -21,6 +21,7 @@ "@webiny/app-admin": "0.0.0", "@webiny/app-headless-cms-common": "0.0.0", "@webiny/app-security": "0.0.0", + "@webiny/app-utils": "0.0.0", "@webiny/app-wcp": "0.0.0", "@webiny/form": "0.0.0", "@webiny/plugins": "0.0.0", diff --git a/packages/app-aco/src/Folders.tsx b/packages/app-aco/src/Folders.tsx deleted file mode 100644 index 170e5c72ae2..00000000000 --- a/packages/app-aco/src/Folders.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; -import { Plugin } from "@webiny/app-admin"; -import { FoldersApiProvider } from "~/contexts/FoldersApi"; - -interface FoldersApiProviderHOCProps { - children: React.ReactNode; -} - -const FoldersApiProviderHOC = (Component: React.ComponentType) => { - return function FoldersApiProviderHOC({ children }: FoldersApiProviderHOCProps) { - return ( - - {children} - - ); - }; -}; - -export const Folders = () => { - return ; -}; diff --git a/packages/app-aco/src/components/FolderTree/List/index.tsx b/packages/app-aco/src/components/FolderTree/List/index.tsx index 627035ca5ad..11fa30881cb 100644 --- a/packages/app-aco/src/components/FolderTree/List/index.tsx +++ b/packages/app-aco/src/components/FolderTree/List/index.tsx @@ -56,6 +56,8 @@ export const List = ({ newTree: NodeModel[], { dragSourceId, dropTargetId }: DropOptions ) => { + // Store the current state of the tree before the drop action + const oldTree = [...treeData]; try { const item = folders.find(folder => folder.id === dragSourceId); @@ -65,14 +67,13 @@ export const List = ({ setTreeData(newTree); - await updateFolder( - { - ...item, - parentId: dropTargetId !== ROOT_FOLDER ? (dropTargetId as string) : null - }, - { refetchFoldersList: true } - ); + await updateFolder({ + ...item, + parentId: dropTargetId !== ROOT_FOLDER ? (dropTargetId as string) : null + }); } catch (error) { + // If an error occurs, revert the tree back to its original state + setTreeData(oldTree); return showSnackbar(error.message); } }; diff --git a/packages/app-aco/src/components/FolderTree/index.tsx b/packages/app-aco/src/components/FolderTree/index.tsx index b5e6edb5f8e..2bbbbc73a36 100644 --- a/packages/app-aco/src/components/FolderTree/index.tsx +++ b/packages/app-aco/src/components/FolderTree/index.tsx @@ -29,7 +29,7 @@ export const FolderTree = ({ onFolderClick, rootFolderLabel }: FolderTreeProps) => { - const { folders, folderLevelPermissions: flp } = useFolders(); + const { folders, folderLevelPermissions: flp, loading } = useFolders(); const localFolders = useMemo(() => { if (!folders) { return []; @@ -44,7 +44,7 @@ export const FolderTree = ({ }, [folders]); const renderList = () => { - if (!folders) { + if (loading.INIT || loading.LIST) { return ; } diff --git a/packages/app-aco/src/contexts/FoldersApi/FoldersApiProvider.tsx b/packages/app-aco/src/contexts/FoldersApi/FoldersApiProvider.tsx deleted file mode 100644 index 379afcbde0a..00000000000 --- a/packages/app-aco/src/contexts/FoldersApi/FoldersApiProvider.tsx +++ /dev/null @@ -1,350 +0,0 @@ -import React, { ReactNode, useEffect, useRef, useState } from "react"; -import { useApolloClient } from "@apollo/react-hooks"; -import { - CREATE_FOLDER, - DELETE_FOLDER, - GET_FOLDER, - LIST_FOLDERS, - UPDATE_FOLDER -} from "~/graphql/folders.gql"; - -import { - CreateFolderResponse, - CreateFolderVariables, - DeleteFolderResponse, - DeleteFolderVariables, - FolderItem, - GetFolderQueryVariables, - GetFolderResponse, - ListFoldersQueryVariables, - ListFoldersResponse, - UpdateFolderResponse, - UpdateFolderVariables -} from "~/types"; -import { ROOT_FOLDER } from "~/constants"; - -interface OffCacheUpdate { - (): void; -} - -export interface OnCacheUpdate { - (folders: FolderItem[]): void; -} - -export interface FoldersApiContext { - listFolders: ( - type: string, - options?: Partial<{ invalidateCache: boolean }> - ) => Promise; - getFolder: (type: string, id: string) => Promise; - createFolder: (type: string, folder: Omit) => Promise; - updateFolder: ( - type: string, - folder: Omit< - FolderItem, - | "type" - | "canManagePermissions" - | "canManageStructure" - | "canManageContent" - | "hasNonInheritedPermissions" - | "createdOn" - | "createdBy" - | "savedOn" - | "savedBy" - | "modifiedOn" - | "modifiedBy" - > - ) => Promise; - - deleteFolder(type: string, id: string): Promise; - - invalidateCache(folderType: string): FoldersApiContext; - - getDescendantFolders(type: string, id?: string): FolderItem[]; - - onFoldersChanged(type: string, cb: OnCacheUpdate): OffCacheUpdate; -} - -export const FoldersApiContext = React.createContext(undefined); - -interface Props { - children: ReactNode; -} - -const rootFolder: FolderItem = { - id: ROOT_FOLDER, - title: "Home", - permissions: [], - parentId: "0", - slug: "", - createdOn: "", - createdBy: { - id: "", - displayName: "", - type: "" - }, - hasNonInheritedPermissions: false, - canManagePermissions: true, - canManageStructure: true, - canManageContent: true, - savedOn: "", - savedBy: { - id: "", - displayName: "", - type: "" - }, - modifiedOn: null, - modifiedBy: null, - type: "$ROOT" -}; - -interface FoldersByType { - [type: string]: FolderItem[]; -} - -export const FoldersApiProvider = ({ children }: Props) => { - const client = useApolloClient(); - const folderObservers = useRef(new Map>()); - const [cache, setCache] = useState({}); - - useEffect(() => { - folderObservers.current.forEach((observers, type) => { - observers.forEach(observer => observer(cache[type])); - }); - }, [cache]); - - useEffect(() => { - return () => { - folderObservers.current.clear(); - }; - }, []); - - const context: FoldersApiContext = { - onFoldersChanged: (type, cb) => { - if (!folderObservers.current.has(type)) { - folderObservers.current.set(type, new Set()); - } - - folderObservers.current.get(type)!.add(cb); - return () => { - folderObservers.current.get(type)?.delete(cb); - }; - }, - invalidateCache: folderType => { - setCache(cache => { - const cacheClone = structuredClone(cache); - delete cacheClone[folderType]; - return cacheClone; - }); - return context; - }, - async listFolders(type, options) { - const invalidateCache = options?.invalidateCache === true; - if (cache[type] && !invalidateCache) { - return cache[type]; - } - - const { data: response } = await client.query< - ListFoldersResponse, - ListFoldersQueryVariables - >({ - query: LIST_FOLDERS, - variables: { - type, - limit: 10000 - }, - fetchPolicy: "network-only" - }); - - if (!response) { - throw new Error("Network error while listing folders."); - } - - const { data, error } = response.aco.listFolders; - - if (!data) { - throw new Error(error?.message || "Could not fetch folders"); - } - - const foldersWithRoot = [rootFolder, ...(data || [])]; - - setCache(cache => ({ - ...cache, - [type]: foldersWithRoot - })); - - return foldersWithRoot; - }, - - async getFolder(type, id) { - if (!id) { - throw new Error("Folder `id` is mandatory"); - } - - const folder = cache[type]?.find(folder => folder.id === id); - if (folder) { - return folder; - } - - const { data: response } = await client.query< - GetFolderResponse, - GetFolderQueryVariables - >({ - query: GET_FOLDER, - variables: { id } - }); - - if (!response) { - throw new Error("Network error while fetch folder."); - } - - const { data, error } = response.aco.getFolder; - - if (!data) { - throw new Error(error?.message || `Could not fetch folder with id: ${id}`); - } - - return data; - }, - - async createFolder(type, folder) { - const { data: response } = await client.mutate< - CreateFolderResponse, - CreateFolderVariables - >({ - mutation: CREATE_FOLDER, - variables: { - data: { - ...folder, - type - } - } - }); - - if (!response) { - throw new Error("Network error while creating folder."); - } - - const { data, error } = response.aco.createFolder; - - if (!data) { - throw new Error(error?.message || "Could not create folder"); - } - - setCache(cache => ({ - ...cache, - [type]: [...cache[type], data] - })); - - return data; - }, - - async updateFolder(type, folder) { - const { id, title, slug, permissions, parentId } = folder; - - const { data: response } = await client.mutate< - UpdateFolderResponse, - UpdateFolderVariables - >({ - mutation: UPDATE_FOLDER, - variables: { - id, - data: { - title, - slug, - permissions, - parentId - } - } - }); - - if (!response) { - throw new Error("Network error while updating folder."); - } - - const { data, error } = response.aco.updateFolder; - - if (!data) { - throw new Error(error?.message || "Could not update folder"); - } - - const folderIndex = cache[type]?.findIndex(f => f.id === id); - if (folderIndex > -1) { - setCache(cache => ({ - ...cache, - [type]: [ - ...cache[type].slice(0, folderIndex), - { - ...cache[type][folderIndex], - ...data - }, - ...cache[type].slice(folderIndex + 1) - ] - })); - } - - return data; - }, - - async deleteFolder(type, id) { - const { data: response } = await client.mutate< - DeleteFolderResponse, - DeleteFolderVariables - >({ - mutation: DELETE_FOLDER, - variables: { - id - } - }); - - if (!response) { - throw new Error("Network error while deleting folder"); - } - - const { data, error } = response.aco.deleteFolder; - - if (!data) { - throw new Error(error?.message || "Could not delete folder"); - } - - setCache(cache => ({ - ...cache, - [type]: cache[type].filter(f => f.id !== id) - })); - - return true; - }, - - getDescendantFolders(type, id) { - const currentFolders = cache[type]; - - if (!id || id === ROOT_FOLDER || !currentFolders?.length) { - return []; - } - - const folderMap = new Map(currentFolders.map(folder => [folder.id, folder])); - const result: FolderItem[] = []; - - const findChildren = (folderId: string) => { - const folder = folderMap.get(folderId); - if (!folder) { - return; - } - - result.push(folder); - - currentFolders.forEach(child => { - if (child.parentId === folder.id) { - findChildren(child.id); - } - }); - }; - - findChildren(id); - - return result; - } - }; - - return {children}; -}; diff --git a/packages/app-aco/src/contexts/FoldersApi/index.ts b/packages/app-aco/src/contexts/FoldersApi/index.ts deleted file mode 100644 index 69c1b07b6b8..00000000000 --- a/packages/app-aco/src/contexts/FoldersApi/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./FoldersApiProvider"; -export * from "./useFoldersApi"; diff --git a/packages/app-aco/src/contexts/FoldersApi/useFoldersApi.ts b/packages/app-aco/src/contexts/FoldersApi/useFoldersApi.ts deleted file mode 100644 index d0ccc58a998..00000000000 --- a/packages/app-aco/src/contexts/FoldersApi/useFoldersApi.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from "react"; -import { FoldersApiContext } from "./FoldersApiProvider"; - -export function useFoldersApi() { - const context = useContext(FoldersApiContext); - if (!context) { - throw new Error(`Missing "FoldersApiProvider" in the component hierarchy!`); - } - - return context; -} diff --git a/packages/app-aco/src/contexts/acoList.tsx b/packages/app-aco/src/contexts/acoList.tsx index b598807097f..2695295319a 100644 --- a/packages/app-aco/src/contexts/acoList.tsx +++ b/packages/app-aco/src/contexts/acoList.tsx @@ -11,7 +11,7 @@ import { ListSearchRecordsSort, SearchRecordItem } from "~/types"; -import { useAcoApp, useNavigateFolder } from "~/hooks"; +import { useAcoApp, useFolders, useNavigateFolder } from "~/hooks"; import { FoldersContext } from "~/contexts/folders"; import { SearchRecordsContext } from "~/contexts/records"; import { sortTableItems, validateOrGetDefaultDbSort } from "~/sorting"; @@ -120,6 +120,11 @@ export const AcoListProvider = ({ children, ...props }: AcoListProviderProps) => const { identity } = useSecurity(); const { currentFolderId } = useNavigateFolder(); const { folderIdPath, folderIdInPath } = useAcoApp(); + const { + folders: originalFolders, + loading: foldersLoading, + getDescendantFolders + } = useFolders(); const folderContext = useContext(FoldersContext); const searchContext = useContext(SearchRecordsContext); @@ -132,12 +137,6 @@ export const AcoListProvider = ({ children, ...props }: AcoListProviderProps) => const [listTitle, setListTitle] = useStateIfMounted(undefined); const [state, setState] = useStateIfMounted>(initializeAcoListState()); - const { - folders: originalFolders, - loading: foldersLoading, - listFolders, - getDescendantFolders - } = folderContext; const { records: originalRecords, loading: recordsLoading, listRecords, meta } = searchContext; /** @@ -155,10 +154,6 @@ export const AcoListProvider = ({ children, ...props }: AcoListProviderProps) => return; } - if (!originalFolders) { - listFolders(); - } - setState(state => { return { ...state, @@ -253,6 +248,10 @@ export const AcoListProvider = ({ children, ...props }: AcoListProviderProps) => // Initialize an empty object let where = {}; + if (!state.folderId) { + return where; + } + // Check if the current folder ID is not the ROOT_FOLDER folder if (state.folderId !== ROOT_FOLDER) { // Get descendant folder IDs of the current folder @@ -331,7 +330,7 @@ export const AcoListProvider = ({ children, ...props }: AcoListProviderProps) => const { hasMoreItems } = meta; // Retrieve all descendant folders of the current folderId - const folderWithChildren = getDescendantFolders(folderId); + const folderWithChildren = folderId ? getDescendantFolders(folderId) : []; // Compute the lengths of various arrays for later comparisons const foldersLength = folders.length; diff --git a/packages/app-aco/src/contexts/folders.tsx b/packages/app-aco/src/contexts/folders.tsx index a6b355d919c..6f243c9949c 100644 --- a/packages/app-aco/src/contexts/folders.tsx +++ b/packages/app-aco/src/contexts/folders.tsx @@ -1,38 +1,8 @@ -import React, { ReactNode, useContext, useEffect, useMemo } from "react"; -import { useWcp } from "@webiny/app-wcp/hooks/useWcp"; -import { useStateIfMounted } from "@webiny/app-admin"; -import { dataLoader, loadingHandler } from "~/handlers"; -import { FolderItem, Loading, LoadingActions } from "~/types"; +import React, { ReactNode, useContext, useMemo } from "react"; import { AcoAppContext } from "~/contexts/app"; -import { useFoldersApi } from "~/hooks"; -import { ROOT_FOLDER } from "~/constants"; - -export interface FoldersContextFolderLevelPermissions { - canManageStructure(folderId: string): boolean; - - canManagePermissions(folderId: string): boolean; - - canManageContent(folderId: string): boolean; -} interface FoldersContext { - folders?: FolderItem[] | null; - loading: Loading; - listFolders: () => Promise; - getFolder: (id: string) => Promise; - createFolder: (folder: Omit) => Promise; - updateFolder: ( - folder: Omit, - options?: Partial<{ - refetchFoldersList: boolean; - }> - ) => Promise; - - deleteFolder(folder: Pick): Promise; - - getDescendantFolders(id?: string): FolderItem[]; - - folderLevelPermissions: FoldersContextFolderLevelPermissions; + type?: string | null; } export const FoldersContext = React.createContext(undefined); @@ -42,132 +12,22 @@ interface Props { children: ReactNode; } -const defaultLoading: Record = { - INIT: true, - LIST: false, - LIST_MORE: false, - GET: false, - MOVE: false, - CREATE: false, - UPDATE: false, - DELETE: false -}; - export const FoldersProvider = ({ children, ...props }: Props) => { const appContext = useContext(AcoAppContext); - const [folders, setFolders] = useStateIfMounted(null); - const [loading, setLoading] = useStateIfMounted>(defaultLoading); - const foldersApi = useFoldersApi(); - const { canUseFolderLevelPermissions } = useWcp(); const app = appContext ? appContext.app : undefined; const type = props.type ?? app?.id; + if (!type) { throw Error(`FoldersProvider requires a "type" prop or an AcoAppContext to be available!`); } - useEffect(() => { - return foldersApi.onFoldersChanged(type, folders => { - setFolders(folders); - }); - }, []); - - const folderLevelPermissions: FoldersContextFolderLevelPermissions = useMemo(() => { - const createCanManage = - (callback: (folder: FolderItem) => boolean) => (folderId: string) => { - if (!canUseFolderLevelPermissions() || folderId === ROOT_FOLDER) { - return true; - } - - const folder = folders?.find(folder => folder.id === folderId); - if (!folder) { - return false; - } - - return callback(folder); - }; - - return { - canManageStructure: createCanManage(folder => folder.canManageStructure), - canManagePermissions: createCanManage(folder => folder.canManagePermissions), - canManageContent: createCanManage(folder => folder.canManageContent) - }; - }, [folders]); - const context = useMemo(() => { return { - folders, - loading, - async listFolders() { - const folders = await dataLoader(loadingHandler("LIST", setLoading), () => - foldersApi.listFolders(type) - ); - - setFolders(() => folders); - - setLoading(prev => ({ - ...prev, - INIT: false - })); - - return folders; - }, - - async getFolder(id) { - if (!id) { - throw new Error("Folder `id` is mandatory"); - } - - return await dataLoader(loadingHandler("GET", setLoading), () => - foldersApi.getFolder(type, id) - ); - }, - - async createFolder(folder) { - return await dataLoader(loadingHandler("CREATE", setLoading), () => - foldersApi.createFolder(type, folder) - ); - }, - - async updateFolder(folder, options) { - const { id, title, slug, permissions, parentId } = folder; - - // We must omit all inherited permissions. - const filteredPermissions = permissions.filter(p => !p.inheritedFrom); - - return await dataLoader(loadingHandler("UPDATE", setLoading), async () => { - const response = await foldersApi.updateFolder(type, { - id, - title, - slug, - permissions: filteredPermissions, - parentId - }); - - if (options?.refetchFoldersList) { - foldersApi.listFolders(type, { invalidateCache: true }).then(setFolders); - } - - return response; - }); - }, - - async deleteFolder(folder) { - const { id } = folder; - - return await dataLoader(loadingHandler("DELETE", setLoading), () => - foldersApi.deleteFolder(type, id) - ); - }, - - getDescendantFolders(id) { - return foldersApi.getDescendantFolders(type, id); - }, - - folderLevelPermissions + type }; - }, [folders, loading, setLoading, setFolders]); + }, [type]); return {children}; }; diff --git a/packages/app-aco/src/dialogs/useDeleteDialog.tsx b/packages/app-aco/src/dialogs/useDeleteDialog.tsx index 0f30ea82867..a8dd8e65bf5 100644 --- a/packages/app-aco/src/dialogs/useDeleteDialog.tsx +++ b/packages/app-aco/src/dialogs/useDeleteDialog.tsx @@ -20,13 +20,8 @@ export const useDeleteDialog = (): UseDeleteDialogResponse => { const onAccept = useCallback(async (folder: FolderItem) => { try { - const result = await deleteFolder(folder); - - if (result) { - showSnackbar(`The folder "${folder.title}" was deleted successfully.`); - } else { - throw new Error(`Error while deleting folder "${folder.title}"!`); - } + await deleteFolder(folder); + showSnackbar(`The folder "${folder.title}" was deleted successfully.`); } catch (error) { showSnackbar(error.message); } diff --git a/packages/app-aco/src/dialogs/useEditDialog.tsx b/packages/app-aco/src/dialogs/useEditDialog.tsx index 39ee1e9367a..da03f9575ae 100644 --- a/packages/app-aco/src/dialogs/useEditDialog.tsx +++ b/packages/app-aco/src/dialogs/useEditDialog.tsx @@ -77,16 +77,11 @@ export const useEditDialog = (): UseEditDialogResponse => { const onAccept = useCallback(async (folder: FolderItem, data: GenericFormData) => { try { - const result = await updateFolder({ + await updateFolder({ ...folder, ...data }); - - if (result) { - showSnackbar(`The folder "${result.title}" was updated successfully!`); - } else { - throw new Error(`Error while updating folder "${folder.title}"!`); - } + showSnackbar(`The folder "${data.title}" was updated successfully!`); } catch (error) { showSnackbar(error.message); } diff --git a/packages/app-aco/src/dialogs/useSetPermissionsDialog.tsx b/packages/app-aco/src/dialogs/useSetPermissionsDialog.tsx index 352414caa24..8758d00bc74 100644 --- a/packages/app-aco/src/dialogs/useSetPermissionsDialog.tsx +++ b/packages/app-aco/src/dialogs/useSetPermissionsDialog.tsx @@ -122,7 +122,7 @@ export const useSetPermissionsDialog = (): UseSetPermissionsDialogResponse => { const updateData = { ...folder, ...data }; try { - await updateFolder(updateData, { refetchFoldersList: true }); + await updateFolder(updateData); showSnackbar("Folder permissions updated successfully!"); } catch (error) { showSnackbar(error.message); diff --git a/packages/app-aco/src/features/folder/Folder.test.ts b/packages/app-aco/src/features/folder/Folder.test.ts new file mode 100644 index 00000000000..2619ca735f0 --- /dev/null +++ b/packages/app-aco/src/features/folder/Folder.test.ts @@ -0,0 +1,55 @@ +import pick from "lodash/pick"; +import { CmsIdentity } from "@webiny/app-headless-cms-common/types"; +import { FolderGqlDto } from "~/features/folder/createFolder/FolderGqlDto"; +import { ICreateFolderGateway } from "~/features/folder/createFolder/ICreateFolderGateway"; +import { CreateFolder } from "~/features/folder/createFolder/CreateFolder"; + +const user1: CmsIdentity = { + id: "user-1", + type: "admin", + displayName: "User 1" +}; + +const type = "demo-type"; + +const folder1: FolderGqlDto = { + id: "folder-1", + title: "Folder 1", + slug: "folder-1", + permissions: [], + hasNonInheritedPermissions: true, + canManageContent: true, + canManagePermissions: true, + canManageStructure: true, + type, + parentId: null, + createdBy: user1, + createdOn: new Date().toString(), + savedBy: user1, + savedOn: new Date().toString(), + modifiedBy: null, + modifiedOn: null +}; + +const createFolderGateway = ({ execute }: ICreateFolderGateway): ICreateFolderGateway => ({ + execute +}); + +describe("Folder features", () => { + it("should be able to create a folder", async () => { + const gateway = createFolderGateway({ + execute: jest.fn().mockImplementation(() => { + return Promise.resolve(folder1); + }) + }); + + const createFolder = CreateFolder.instance(type, gateway); + + const createPromise = createFolder.execute( + pick(folder1, ["title", "slug", "type", "parentId", "permissions"]) + ); + + await createPromise; + expect(gateway.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/app-aco/src/features/folder/Folder.ts b/packages/app-aco/src/features/folder/Folder.ts new file mode 100644 index 00000000000..23abccd015d --- /dev/null +++ b/packages/app-aco/src/features/folder/Folder.ts @@ -0,0 +1,62 @@ +import { CmsIdentity, FolderPermission } from "~/types"; + +export interface FolderData { + id?: string; + title: string; + slug: string; + type: string; + parentId: string | null; + permissions: FolderPermission[]; + hasNonInheritedPermissions?: boolean; + canManagePermissions?: boolean; + canManageStructure?: boolean; + canManageContent?: boolean; + createdBy?: CmsIdentity; + createdOn?: string; + savedBy?: CmsIdentity; + savedOn?: string; + modifiedBy?: CmsIdentity | null; + modifiedOn?: string | null; +} + +export class Folder { + public id: string; + public title: string; + public slug: string; + public type: string; + public parentId: string | null; + public permissions: FolderPermission[]; + public hasNonInheritedPermissions?: boolean; + public canManagePermissions?: boolean; + public canManageStructure?: boolean; + public canManageContent?: boolean; + public createdBy?: CmsIdentity; + public createdOn?: string; + public savedBy?: CmsIdentity; + public savedOn?: string; + public modifiedBy?: CmsIdentity | null; + public modifiedOn?: string | null; + + protected constructor(folder: FolderData) { + this.id = folder.id ?? ""; + this.title = folder.title; + this.slug = folder.slug; + this.type = folder.type; + this.parentId = folder.parentId; + this.permissions = folder.permissions; + this.hasNonInheritedPermissions = folder.hasNonInheritedPermissions; + this.canManagePermissions = folder.canManagePermissions; + this.canManageStructure = folder.canManageStructure; + this.canManageContent = folder.canManageContent; + this.createdBy = folder.createdBy; + this.createdOn = folder.createdOn; + this.savedBy = folder.savedBy; + this.savedOn = folder.savedOn; + this.modifiedBy = folder.modifiedBy; + this.modifiedOn = folder.modifiedOn; + } + + static create(folder: FolderData) { + return new Folder(folder); + } +} diff --git a/packages/app-aco/src/features/folder/cache/FoldersCache.ts b/packages/app-aco/src/features/folder/cache/FoldersCache.ts new file mode 100644 index 00000000000..b0cc7da79f9 --- /dev/null +++ b/packages/app-aco/src/features/folder/cache/FoldersCache.ts @@ -0,0 +1,47 @@ +import { makeAutoObservable } from "mobx"; +import { ICache } from "./ICache"; +import { Folder } from "../Folder"; + +export class FoldersCache implements ICache { + private folders: Folder[]; + + constructor() { + this.folders = []; + makeAutoObservable(this); + } + + hasItems() { + return this.folders.length > 0; + } + + getItems() { + return this.folders; + } + + getItem(id: string) { + return this.folders.find(f => f.id === id); + } + + async set(item: Folder): Promise { + this.folders.push(item); + } + + async setMultiple(items: Folder[]): Promise { + this.folders = items; + } + + async update(id: string, item: Folder): Promise { + const folderIndex = this.folders.findIndex(f => f.id === id); + + if (folderIndex > -1) { + this.folders[folderIndex] = { + ...this.folders[folderIndex], + ...item + }; + } + } + + async remove(id: string): Promise { + this.folders = this.folders.filter(folder => folder.id !== id); + } +} diff --git a/packages/app-aco/src/features/folder/cache/FoldersCacheFactory.ts b/packages/app-aco/src/features/folder/cache/FoldersCacheFactory.ts new file mode 100644 index 00000000000..dfda95e49df --- /dev/null +++ b/packages/app-aco/src/features/folder/cache/FoldersCacheFactory.ts @@ -0,0 +1,21 @@ +import { FoldersCache } from "./FoldersCache"; + +export class FoldersCacheFactory { + private cache: Map = new Map(); + + getCache(namespace: string) { + const cacheKey = this.getCacheKey(namespace); + + if (!this.cache.has(cacheKey)) { + this.cache.set(cacheKey, new FoldersCache()); + } + + return this.cache.get(cacheKey) as FoldersCache; + } + + private getCacheKey(namespace: string) { + return namespace; + } +} + +export const folderCacheFactory = new FoldersCacheFactory(); diff --git a/packages/app-aco/src/features/folder/cache/ICache.ts b/packages/app-aco/src/features/folder/cache/ICache.ts new file mode 100644 index 00000000000..5818456bfe5 --- /dev/null +++ b/packages/app-aco/src/features/folder/cache/ICache.ts @@ -0,0 +1,9 @@ +export interface ICache { + hasItems: () => boolean; + getItems: () => TItem[]; + getItem: (id: string) => TItem | undefined; + set: (item: TItem) => Promise; + setMultiple: (items: TItem[]) => Promise; + update: (id: string, item: TItem) => Promise; + remove: (id: string) => Promise; +} diff --git a/packages/app-aco/src/features/folder/cache/index.ts b/packages/app-aco/src/features/folder/cache/index.ts new file mode 100644 index 00000000000..bc66ba6e954 --- /dev/null +++ b/packages/app-aco/src/features/folder/cache/index.ts @@ -0,0 +1,3 @@ +export * from "./FoldersCache"; +export * from "./FoldersCacheFactory"; +export * from "./ICache"; diff --git a/packages/app-aco/src/features/folder/createFolder/CreateFolder.ts b/packages/app-aco/src/features/folder/createFolder/CreateFolder.ts new file mode 100644 index 00000000000..1182098d6d4 --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/CreateFolder.ts @@ -0,0 +1,17 @@ +import { loadingRepositoryFactory } from "@webiny/app-utils"; +import { ICreateFolderUseCase } from "./ICreateFolderUseCase"; +import { ICreateFolderGateway } from "./ICreateFolderGateway"; +import { CreateFolderRepository } from "./CreateFolderRepository"; +import { CreateFolderUseCase } from "./CreateFolderUseCase"; +import { CreateFolderUseCaseWithLoading } from "./CreateFolderUseCaseWithLoading"; +import { folderCacheFactory } from "../cache"; + +export class CreateFolder { + public static instance(type: string, gateway: ICreateFolderGateway): ICreateFolderUseCase { + const foldersCache = folderCacheFactory.getCache(type); + const loadingRepository = loadingRepositoryFactory.getRepository(type); + const repository = new CreateFolderRepository(foldersCache, gateway, type); + const useCase = new CreateFolderUseCase(repository); + return new CreateFolderUseCaseWithLoading(loadingRepository, useCase); + } +} diff --git a/packages/app-aco/src/features/folder/createFolder/CreateFolderGqlGateway.ts b/packages/app-aco/src/features/folder/createFolder/CreateFolderGqlGateway.ts new file mode 100644 index 00000000000..4db71c88118 --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/CreateFolderGqlGateway.ts @@ -0,0 +1,110 @@ +import ApolloClient from "apollo-client"; +import gql from "graphql-tag"; +import { ICreateFolderGateway } from "./ICreateFolderGateway"; +import { FolderDto } from "./FolderDto"; +import { AcoError, FolderItem } from "~/types"; + +export interface CreateFolderResponse { + aco: { + createFolder: { + data: FolderItem; + error: AcoError | null; + }; + }; +} + +export interface CreateFolderVariables { + data: Omit< + FolderItem, + | "id" + | "createdOn" + | "createdBy" + | "savedOn" + | "savedBy" + | "modifiedOn" + | "modifiedBy" + | "hasNonInheritedPermissions" + | "canManageContent" + | "canManagePermissions" + | "canManageStructure" + >; +} + +export const CREATE_FOLDER = gql` + mutation CreateFolder($data: FolderCreateInput!) { + aco { + createFolder(data: $data) { + data { + id + title + slug + permissions { + target + level + inheritedFrom + } + hasNonInheritedPermissions + canManagePermissions + canManageStructure + canManageContent + parentId + type + savedOn + savedBy { + id + displayName + } + createdOn + createdBy { + id + displayName + } + modifiedOn + modifiedBy { + id + displayName + } + } + error { + code + data + message + } + } + } + } +`; + +export class CreateFolderGqlGateway implements ICreateFolderGateway { + private client: ApolloClient; + + constructor(client: ApolloClient) { + this.client = client; + } + + async execute(folder: FolderDto) { + const { data: response } = await this.client.mutate< + CreateFolderResponse, + CreateFolderVariables + >({ + mutation: CREATE_FOLDER, + variables: { + data: { + ...folder + } + } + }); + + if (!response) { + throw new Error("Network error while creating folder."); + } + + const { data, error } = response.aco.createFolder; + + if (!data) { + throw new Error(error?.message || "Could not create folder"); + } + + return data; + } +} diff --git a/packages/app-aco/src/features/folder/createFolder/CreateFolderRepository.ts b/packages/app-aco/src/features/folder/createFolder/CreateFolderRepository.ts new file mode 100644 index 00000000000..b3398b03b58 --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/CreateFolderRepository.ts @@ -0,0 +1,32 @@ +import { makeAutoObservable } from "mobx"; +import { ICreateFolderRepository } from "./ICreateFolderRepository"; +import { Folder } from "../Folder"; +import { FoldersCache } from "../cache"; +import { ICreateFolderGateway } from "./ICreateFolderGateway"; +import { FolderDto } from "./FolderDto"; + +export class CreateFolderRepository implements ICreateFolderRepository { + private cache: FoldersCache; + private gateway: ICreateFolderGateway; + private type: string; + + constructor(cache: FoldersCache, gateway: ICreateFolderGateway, type: string) { + this.cache = cache; + this.gateway = gateway; + this.type = type; + makeAutoObservable(this); + } + + async execute(folder: Folder) { + const dto: FolderDto = { + title: folder.title, + slug: folder.slug, + permissions: folder.permissions, + type: this.type, + parentId: folder.parentId + }; + + const result = await this.gateway.execute(dto); + await this.cache.set(Folder.create(result)); + } +} diff --git a/packages/app-aco/src/features/folder/createFolder/CreateFolderUseCase.ts b/packages/app-aco/src/features/folder/createFolder/CreateFolderUseCase.ts new file mode 100644 index 00000000000..fb42c3be0e5 --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/CreateFolderUseCase.ts @@ -0,0 +1,23 @@ +import { CreateFolderParams, ICreateFolderUseCase } from "./ICreateFolderUseCase"; +import { ICreateFolderRepository } from "./ICreateFolderRepository"; +import { Folder } from "../Folder"; + +export class CreateFolderUseCase implements ICreateFolderUseCase { + private repository: ICreateFolderRepository; + + constructor(repository: ICreateFolderRepository) { + this.repository = repository; + } + + async execute(params: CreateFolderParams) { + await this.repository.execute( + Folder.create({ + title: params.title, + slug: params.slug, + type: params.type, + parentId: params.parentId, + permissions: params.permissions + }) + ); + } +} diff --git a/packages/app-aco/src/features/folder/createFolder/CreateFolderUseCaseWithLoading.ts b/packages/app-aco/src/features/folder/createFolder/CreateFolderUseCaseWithLoading.ts new file mode 100644 index 00000000000..75578757903 --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/CreateFolderUseCaseWithLoading.ts @@ -0,0 +1,20 @@ +import { ILoadingRepository } from "@webiny/app-utils"; +import { CreateFolderParams, ICreateFolderUseCase } from "./ICreateFolderUseCase"; +import { LoadingActionsEnum } from "~/types"; + +export class CreateFolderUseCaseWithLoading implements ICreateFolderUseCase { + private loadingRepository: ILoadingRepository; + private useCase: ICreateFolderUseCase; + + constructor(loadingRepository: ILoadingRepository, useCase: ICreateFolderUseCase) { + this.loadingRepository = loadingRepository; + this.useCase = useCase; + } + + async execute(params: CreateFolderParams) { + await this.loadingRepository.runCallBack( + this.useCase.execute(params), + LoadingActionsEnum.create + ); + } +} diff --git a/packages/app-aco/src/features/folder/createFolder/FolderDto.ts b/packages/app-aco/src/features/folder/createFolder/FolderDto.ts new file mode 100644 index 00000000000..09733f3ede8 --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/FolderDto.ts @@ -0,0 +1,9 @@ +import { FolderPermission } from "~/types"; + +export interface FolderDto { + title: string; + slug: string; + permissions: FolderPermission[]; + type: string; + parentId: string | null; +} diff --git a/packages/app-aco/src/features/folder/createFolder/FolderGqlDto.ts b/packages/app-aco/src/features/folder/createFolder/FolderGqlDto.ts new file mode 100644 index 00000000000..0d2ff62504c --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/FolderGqlDto.ts @@ -0,0 +1,20 @@ +import { CmsIdentity, FolderPermission } from "~/types"; + +export interface FolderGqlDto { + id: string; + title: string; + slug: string; + permissions: FolderPermission[]; + hasNonInheritedPermissions: boolean; + canManagePermissions: boolean; + canManageStructure: boolean; + canManageContent: boolean; + type: string; + parentId: string | null; + createdBy: CmsIdentity; + createdOn: string; + savedBy: CmsIdentity; + savedOn: string; + modifiedBy: CmsIdentity | null; + modifiedOn: string | null; +} diff --git a/packages/app-aco/src/features/folder/createFolder/ICreateFolderGateway.ts b/packages/app-aco/src/features/folder/createFolder/ICreateFolderGateway.ts new file mode 100644 index 00000000000..ba10ce21f07 --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/ICreateFolderGateway.ts @@ -0,0 +1,6 @@ +import { FolderDto } from "./FolderDto"; +import { FolderGqlDto } from "./FolderGqlDto"; + +export interface ICreateFolderGateway { + execute: (folderDto: FolderDto) => Promise; +} diff --git a/packages/app-aco/src/features/folder/createFolder/ICreateFolderRepository.ts b/packages/app-aco/src/features/folder/createFolder/ICreateFolderRepository.ts new file mode 100644 index 00000000000..d4cf5b909b9 --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/ICreateFolderRepository.ts @@ -0,0 +1,5 @@ +import { Folder } from "../Folder"; + +export interface ICreateFolderRepository { + execute: (folder: Folder) => Promise; +} diff --git a/packages/app-aco/src/features/folder/createFolder/ICreateFolderUseCase.ts b/packages/app-aco/src/features/folder/createFolder/ICreateFolderUseCase.ts new file mode 100644 index 00000000000..45517a3200e --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/ICreateFolderUseCase.ts @@ -0,0 +1,13 @@ +import { FolderPermission } from "~/types"; + +export interface CreateFolderParams { + title: string; + slug: string; + type: string; + parentId: string | null; + permissions: FolderPermission[]; +} + +export interface ICreateFolderUseCase { + execute: (params: CreateFolderParams) => Promise; +} diff --git a/packages/app-aco/src/features/folder/createFolder/index.ts b/packages/app-aco/src/features/folder/createFolder/index.ts new file mode 100644 index 00000000000..26134b3d69d --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/index.ts @@ -0,0 +1 @@ +export * from "./useCreateFolder"; diff --git a/packages/app-aco/src/features/folder/createFolder/useCreateFolder.ts b/packages/app-aco/src/features/folder/createFolder/useCreateFolder.ts new file mode 100644 index 00000000000..99193b73097 --- /dev/null +++ b/packages/app-aco/src/features/folder/createFolder/useCreateFolder.ts @@ -0,0 +1,35 @@ +import { useCallback, useContext } from "react"; +import { useApolloClient } from "@apollo/react-hooks"; +import { CreateFolderGqlGateway } from "./CreateFolderGqlGateway"; +import { CreateFolderParams } from "./ICreateFolderUseCase"; +import { CreateFolder } from "./CreateFolder"; +import { FoldersContext } from "~/contexts/folders"; + +export const useCreateFolder = () => { + const client = useApolloClient(); + const gateway = new CreateFolderGqlGateway(client); + + const foldersContext = useContext(FoldersContext); + + if (!foldersContext) { + throw new Error("useCreateFolder must be used within a FoldersProvider"); + } + + const { type } = foldersContext; + + if (!type) { + throw Error(`FoldersProvider requires a "type" prop or an AcoAppContext to be available!`); + } + + const createFolder = useCallback( + (params: CreateFolderParams) => { + const instance = CreateFolder.instance(type, gateway); + return instance.execute(params); + }, + [type, gateway] + ); + + return { + createFolder + }; +}; diff --git a/packages/app-aco/src/features/folder/deleteFolder/DeleteFolder.ts b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolder.ts new file mode 100644 index 00000000000..41aebf02144 --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolder.ts @@ -0,0 +1,17 @@ +import { loadingRepositoryFactory } from "@webiny/app-utils"; +import { IDeleteFolderUseCase } from "./IDeleteFolderUseCase"; +import { DeleteFolderRepository } from "./DeleteFolderRepository"; +import { DeleteFolderUseCase } from "./DeleteFolderUseCase"; +import { DeleteFolderUseCaseWithLoading } from "./DeleteFolderUseCaseWithLoading"; +import { IDeleteFolderGateway } from "./IDeleteFolderGateway"; +import { folderCacheFactory } from "../cache"; + +export class DeleteFolder { + public static instance(type: string, gateway: IDeleteFolderGateway): IDeleteFolderUseCase { + const foldersCache = folderCacheFactory.getCache(type); + const loadingRepository = loadingRepositoryFactory.getRepository(type); + const repository = new DeleteFolderRepository(foldersCache, gateway); + const useCase = new DeleteFolderUseCase(repository); + return new DeleteFolderUseCaseWithLoading(loadingRepository, useCase); + } +} diff --git a/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderGqlGateway.ts b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderGqlGateway.ts new file mode 100644 index 00000000000..1de619638e6 --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderGqlGateway.ts @@ -0,0 +1,64 @@ +import ApolloClient from "apollo-client"; +import gql from "graphql-tag"; +import { IDeleteFolderGateway } from "./IDeleteFolderGateway"; +import { AcoError } from "~/types"; + +export interface DeleteFolderVariables { + id: string; +} + +export interface DeleteFolderResponse { + aco: { + deleteFolder: { + data: boolean; + error: AcoError | null; + }; + }; +} + +export const DELETE_FOLDER = gql` + mutation DeleteFolder($id: ID!) { + aco { + deleteFolder(id: $id) { + data + error { + code + data + message + } + } + } + } +`; + +export class DeleteFolderGqlGateway implements IDeleteFolderGateway { + private client: ApolloClient; + + constructor(client: ApolloClient) { + this.client = client; + } + + async execute(id: string) { + const { data: response } = await this.client.mutate< + DeleteFolderResponse, + DeleteFolderVariables + >({ + mutation: DELETE_FOLDER, + variables: { + id + } + }); + + if (!response) { + throw new Error("Network error while deleting folder"); + } + + const { data, error } = response.aco.deleteFolder; + + if (!data) { + throw new Error(error?.message || "Could not delete folder"); + } + + return; + } +} diff --git a/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderRepository.ts b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderRepository.ts new file mode 100644 index 00000000000..6b6c898b5db --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderRepository.ts @@ -0,0 +1,21 @@ +import { makeAutoObservable } from "mobx"; +import { IDeleteFolderRepository } from "./IDeleteFolderRepository"; +import { FoldersCache } from "../cache"; +import { Folder } from "../Folder"; +import { IDeleteFolderGateway } from "./IDeleteFolderGateway"; + +export class DeleteFolderRepository implements IDeleteFolderRepository { + private cache: FoldersCache; + private gateway: IDeleteFolderGateway; + + constructor(cache: FoldersCache, gateway: IDeleteFolderGateway) { + this.cache = cache; + this.gateway = gateway; + makeAutoObservable(this); + } + + async execute(folder: Folder) { + await this.gateway.execute(folder.id); + await this.cache.remove(folder.id); + } +} diff --git a/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderUseCase.ts b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderUseCase.ts new file mode 100644 index 00000000000..9dcbfb942ef --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderUseCase.ts @@ -0,0 +1,24 @@ +import { DeleteFolderParams, IDeleteFolderUseCase } from "./IDeleteFolderUseCase"; +import { IDeleteFolderRepository } from "./IDeleteFolderRepository"; +import { Folder } from "../Folder"; + +export class DeleteFolderUseCase implements IDeleteFolderUseCase { + private repository: IDeleteFolderRepository; + + constructor(repository: IDeleteFolderRepository) { + this.repository = repository; + } + + async execute(params: DeleteFolderParams) { + await this.repository.execute( + Folder.create({ + id: params.id, + title: params.title, + slug: params.slug, + type: params.type, + parentId: params.parentId, + permissions: params.permissions + }) + ); + } +} diff --git a/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderUseCaseWithLoading.ts b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderUseCaseWithLoading.ts new file mode 100644 index 00000000000..12717658c02 --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/DeleteFolderUseCaseWithLoading.ts @@ -0,0 +1,20 @@ +import { ILoadingRepository } from "@webiny/app-utils"; +import { LoadingActionsEnum } from "~/types"; +import { DeleteFolderParams, IDeleteFolderUseCase } from "./IDeleteFolderUseCase"; + +export class DeleteFolderUseCaseWithLoading implements IDeleteFolderUseCase { + private loadingRepository: ILoadingRepository; + private useCase: IDeleteFolderUseCase; + + constructor(loadingRepository: ILoadingRepository, useCase: IDeleteFolderUseCase) { + this.loadingRepository = loadingRepository; + this.useCase = useCase; + } + + async execute(params: DeleteFolderParams) { + await this.loadingRepository.runCallBack( + this.useCase.execute(params), + LoadingActionsEnum.delete + ); + } +} diff --git a/packages/app-aco/src/features/folder/deleteFolder/IDeleteFolderGateway.ts b/packages/app-aco/src/features/folder/deleteFolder/IDeleteFolderGateway.ts new file mode 100644 index 00000000000..852a065ec5e --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/IDeleteFolderGateway.ts @@ -0,0 +1,3 @@ +export interface IDeleteFolderGateway { + execute: (id: string) => Promise; +} diff --git a/packages/app-aco/src/features/folder/deleteFolder/IDeleteFolderRepository.ts b/packages/app-aco/src/features/folder/deleteFolder/IDeleteFolderRepository.ts new file mode 100644 index 00000000000..d771713a47c --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/IDeleteFolderRepository.ts @@ -0,0 +1,5 @@ +import { Folder } from "../Folder"; + +export interface IDeleteFolderRepository { + execute: (folder: Folder) => Promise; +} diff --git a/packages/app-aco/src/features/folder/deleteFolder/IDeleteFolderUseCase.ts b/packages/app-aco/src/features/folder/deleteFolder/IDeleteFolderUseCase.ts new file mode 100644 index 00000000000..a257dd5b0e0 --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/IDeleteFolderUseCase.ts @@ -0,0 +1,14 @@ +import { FolderPermission } from "~/types"; + +export interface DeleteFolderParams { + id: string; + title: string; + slug: string; + type: string; + parentId: string | null; + permissions: FolderPermission[]; +} + +export interface IDeleteFolderUseCase { + execute: (params: DeleteFolderParams) => Promise; +} diff --git a/packages/app-aco/src/features/folder/deleteFolder/index.ts b/packages/app-aco/src/features/folder/deleteFolder/index.ts new file mode 100644 index 00000000000..87fcddfbf54 --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/index.ts @@ -0,0 +1 @@ +export * from "./useDeleteFolder"; diff --git a/packages/app-aco/src/features/folder/deleteFolder/useDeleteFolder.ts b/packages/app-aco/src/features/folder/deleteFolder/useDeleteFolder.ts new file mode 100644 index 00000000000..5f36f3fb893 --- /dev/null +++ b/packages/app-aco/src/features/folder/deleteFolder/useDeleteFolder.ts @@ -0,0 +1,35 @@ +import { useCallback, useContext } from "react"; +import { useApolloClient } from "@apollo/react-hooks"; +import { DeleteFolderGqlGateway } from "./DeleteFolderGqlGateway"; +import { DeleteFolderParams } from "./IDeleteFolderUseCase"; +import { DeleteFolder } from "./DeleteFolder"; +import { FoldersContext } from "~/contexts/folders"; + +export const useDeleteFolder = () => { + const client = useApolloClient(); + const gateway = new DeleteFolderGqlGateway(client); + + const foldersContext = useContext(FoldersContext); + + if (!foldersContext) { + throw new Error("useFolders must be used within a FoldersProvider"); + } + + const { type } = foldersContext; + + if (!type) { + throw Error(`FoldersProvider requires a "type" prop or an AcoAppContext to be available!`); + } + + const deleteFolder = useCallback( + (params: DeleteFolderParams) => { + const instance = DeleteFolder.instance(type, gateway); + return instance.execute(params); + }, + [type, gateway] + ); + + return { + deleteFolder + }; +}; diff --git a/packages/app-aco/src/features/folder/getDescendantFolders/FolderDto.ts b/packages/app-aco/src/features/folder/getDescendantFolders/FolderDto.ts new file mode 100644 index 00000000000..766a6f65f29 --- /dev/null +++ b/packages/app-aco/src/features/folder/getDescendantFolders/FolderDto.ts @@ -0,0 +1,10 @@ +import { FolderPermission } from "~/types"; + +export interface FolderDto { + id: string; + title: string; + slug: string; + permissions: FolderPermission[]; + type: string; + parentId: string | null; +} diff --git a/packages/app-aco/src/features/folder/getDescendantFolders/GetDescendantFolders.ts b/packages/app-aco/src/features/folder/getDescendantFolders/GetDescendantFolders.ts new file mode 100644 index 00000000000..ac33309677f --- /dev/null +++ b/packages/app-aco/src/features/folder/getDescendantFolders/GetDescendantFolders.ts @@ -0,0 +1,12 @@ +import { IGetDescendantFoldersUseCase } from "./IGetDescendantFoldersUseCase"; +import { GetDescendantFoldersRepository } from "./GetDescendantFoldersRepository"; +import { GetDescendantFoldersUseCase } from "./GetDescendantFoldersUseCase"; +import { folderCacheFactory } from "../cache"; + +export class GetDescendantFolders { + public static instance(type: string): IGetDescendantFoldersUseCase { + const foldersCache = folderCacheFactory.getCache(type); + const repository = new GetDescendantFoldersRepository(foldersCache); + return new GetDescendantFoldersUseCase(repository); + } +} diff --git a/packages/app-aco/src/features/folder/getDescendantFolders/GetDescendantFoldersRepository.ts b/packages/app-aco/src/features/folder/getDescendantFolders/GetDescendantFoldersRepository.ts new file mode 100644 index 00000000000..5c4fb977a81 --- /dev/null +++ b/packages/app-aco/src/features/folder/getDescendantFolders/GetDescendantFoldersRepository.ts @@ -0,0 +1,44 @@ +import { makeAutoObservable } from "mobx"; +import { IGetDescendantFoldersRepository } from "./IGetDescendantFoldersRepository"; +import { FoldersCache } from "../cache"; +import { Folder } from "../Folder"; +import { ROOT_FOLDER } from "~/constants"; + +export class GetDescendantFoldersRepository implements IGetDescendantFoldersRepository { + private readonly cache: FoldersCache; + + constructor(cache: FoldersCache) { + this.cache = cache; + makeAutoObservable(this); + } + + execute(id: string): Folder[] { + const currentFolders = this.cache.getItems(); + + if (!id || id === ROOT_FOLDER || !currentFolders.length) { + return []; + } + + const folderMap = new Map(currentFolders.map(folder => [folder.id, folder])); + const result: Folder[] = []; + + const findChildren = (folderId: string) => { + const folder = folderMap.get(folderId); + if (!folder) { + return; + } + + result.push(folder); + + currentFolders.forEach(child => { + if (child.parentId === folder.id) { + findChildren(child.id); + } + }); + }; + + findChildren(id); + + return result; + } +} diff --git a/packages/app-aco/src/features/folder/getDescendantFolders/GetDescendantFoldersUseCase.ts b/packages/app-aco/src/features/folder/getDescendantFolders/GetDescendantFoldersUseCase.ts new file mode 100644 index 00000000000..af95700892d --- /dev/null +++ b/packages/app-aco/src/features/folder/getDescendantFolders/GetDescendantFoldersUseCase.ts @@ -0,0 +1,26 @@ +import { IGetDescendantFoldersRepository } from "./IGetDescendantFoldersRepository"; +import { + GetDescendantFoldersParams, + IGetDescendantFoldersUseCase +} from "./IGetDescendantFoldersUseCase"; + +export class GetDescendantFoldersUseCase implements IGetDescendantFoldersUseCase { + private repository: IGetDescendantFoldersRepository; + + constructor(repository: IGetDescendantFoldersRepository) { + this.repository = repository; + } + + execute(params: GetDescendantFoldersParams) { + const folders = this.repository.execute(params.id); + + return folders.map(folder => ({ + id: folder.id, + title: folder.title, + slug: folder.slug, + permissions: folder.permissions, + type: folder.type, + parentId: folder.parentId + })); + } +} diff --git a/packages/app-aco/src/features/folder/getDescendantFolders/IGetDescendantFoldersRepository.ts b/packages/app-aco/src/features/folder/getDescendantFolders/IGetDescendantFoldersRepository.ts new file mode 100644 index 00000000000..97cbe4c59ff --- /dev/null +++ b/packages/app-aco/src/features/folder/getDescendantFolders/IGetDescendantFoldersRepository.ts @@ -0,0 +1,5 @@ +import { Folder } from "../Folder"; + +export interface IGetDescendantFoldersRepository { + execute: (id: string) => Folder[]; +} diff --git a/packages/app-aco/src/features/folder/getDescendantFolders/IGetDescendantFoldersUseCase.ts b/packages/app-aco/src/features/folder/getDescendantFolders/IGetDescendantFoldersUseCase.ts new file mode 100644 index 00000000000..142753f7c72 --- /dev/null +++ b/packages/app-aco/src/features/folder/getDescendantFolders/IGetDescendantFoldersUseCase.ts @@ -0,0 +1,9 @@ +import { FolderDto } from "./FolderDto"; + +export interface GetDescendantFoldersParams { + id: string; +} + +export interface IGetDescendantFoldersUseCase { + execute: (params: GetDescendantFoldersParams) => FolderDto[]; +} diff --git a/packages/app-aco/src/features/folder/getDescendantFolders/index.ts b/packages/app-aco/src/features/folder/getDescendantFolders/index.ts new file mode 100644 index 00000000000..b08ebc4e8ec --- /dev/null +++ b/packages/app-aco/src/features/folder/getDescendantFolders/index.ts @@ -0,0 +1 @@ +export * from "./useGetDescendantFolders"; diff --git a/packages/app-aco/src/features/folder/getDescendantFolders/useGetDescendantFolders.ts b/packages/app-aco/src/features/folder/getDescendantFolders/useGetDescendantFolders.ts new file mode 100644 index 00000000000..10509563968 --- /dev/null +++ b/packages/app-aco/src/features/folder/getDescendantFolders/useGetDescendantFolders.ts @@ -0,0 +1,29 @@ +import { useCallback, useContext } from "react"; +import { GetDescendantFolders } from "./GetDescendantFolders"; +import { FoldersContext } from "~/contexts/folders"; + +export const useGetDescendantFolders = () => { + const foldersContext = useContext(FoldersContext); + + if (!foldersContext) { + throw new Error("useCreateFolder must be used within a FoldersProvider"); + } + + const { type } = foldersContext; + + if (!type) { + throw Error(`FoldersProvider requires a "type" prop or an AcoAppContext to be available!`); + } + + const getDescendantFolders = useCallback( + (id: string) => { + const instance = GetDescendantFolders.instance(type); + return instance.execute({ id }); + }, + [type] + ); + + return { + getDescendantFolders + }; +}; diff --git a/packages/app-aco/src/features/folder/getFolder/GetFolder.ts b/packages/app-aco/src/features/folder/getFolder/GetFolder.ts new file mode 100644 index 00000000000..ef82ff0841f --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/GetFolder.ts @@ -0,0 +1,17 @@ +import { loadingRepositoryFactory } from "@webiny/app-utils"; +import { IGetFolderUseCase } from "./IGetFolderUseCase"; +import { IGetFolderGateway } from "./IGetFolderGateway"; +import { GetFolderRepository } from "./GetFolderRepository"; +import { GetFolderUseCase } from "./GetFolderUseCase"; +import { GetFolderUseCaseWithLoading } from "./GetFolderUseCaseWithLoading"; +import { folderCacheFactory } from "../cache"; + +export class GetFolder { + public static instance(type: string, gateway: IGetFolderGateway): IGetFolderUseCase { + const foldersCache = folderCacheFactory.getCache(type); + const loadingRepository = loadingRepositoryFactory.getRepository(type); + const repository = new GetFolderRepository(foldersCache, gateway); + const useCase = new GetFolderUseCase(repository); + return new GetFolderUseCaseWithLoading(loadingRepository, useCase); + } +} diff --git a/packages/app-aco/src/features/folder/getFolder/GetFolderGqlGateway.ts b/packages/app-aco/src/features/folder/getFolder/GetFolderGqlGateway.ts new file mode 100644 index 00000000000..3439569c7de --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/GetFolderGqlGateway.ts @@ -0,0 +1,97 @@ +import ApolloClient from "apollo-client"; +import gql from "graphql-tag"; +import { IGetFolderGateway } from "./IGetFolderGateway"; +import { FolderItem, AcoError } from "~/types"; + +export interface GetFolderResponse { + aco: { + getFolder: { + data: FolderItem | null; + error: AcoError | null; + }; + }; +} + +export interface GetFolderQueryVariables { + id: string; +} + +export const GET_FOLDER = gql` + query GetFolder($id: ID!) { + aco { + getFolder(id: $id) { + data { + id + title + slug + permissions { + target + level + inheritedFrom + } + hasNonInheritedPermissions + canManagePermissions + canManageStructure + canManageContent + parentId + type + savedOn + savedBy { + id + displayName + } + createdOn + createdBy { + id + displayName + } + modifiedOn + modifiedBy { + id + displayName + } + } + error { + code + data + message + } + } + } + } +`; + +export class GetFolderGqlGateway implements IGetFolderGateway { + private client: ApolloClient; + + constructor(client: ApolloClient) { + this.client = client; + } + + async execute(id: string) { + if (!id) { + throw new Error("Folder `id` is mandatory"); + } + + const { data: response } = await this.client.query< + GetFolderResponse, + GetFolderQueryVariables + >({ + query: GET_FOLDER, + variables: { id }, + fetchPolicy: "network-only" + }); + + if (!response) { + throw new Error("Network error while fetch folder."); + } + + const { data, error } = response.aco.getFolder; + + if (!data) { + throw new Error(error?.message || `Could not fetch folder with id: ${id}`); + } + + return data; + } +} diff --git a/packages/app-aco/src/features/folder/getFolder/GetFolderRepository.ts b/packages/app-aco/src/features/folder/getFolder/GetFolderRepository.ts new file mode 100644 index 00000000000..5e863b8eef5 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/GetFolderRepository.ts @@ -0,0 +1,21 @@ +import { makeAutoObservable } from "mobx"; +import { Folder } from "../Folder"; +import { FoldersCache } from "../cache"; +import { IGetFolderRepository } from "./IGetFolderRepository"; +import { IGetFolderGateway } from "./IGetFolderGateway"; + +export class GetFolderRepository implements IGetFolderRepository { + private cache: FoldersCache; + private gateway: IGetFolderGateway; + + constructor(cache: FoldersCache, gateway: IGetFolderGateway) { + this.cache = cache; + this.gateway = gateway; + makeAutoObservable(this); + } + + async execute(id: string) { + const response = await this.gateway.execute(id); + await this.cache.set(Folder.create(response)); + } +} diff --git a/packages/app-aco/src/features/folder/getFolder/GetFolderUseCase.ts b/packages/app-aco/src/features/folder/getFolder/GetFolderUseCase.ts new file mode 100644 index 00000000000..68cd0e14f4d --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/GetFolderUseCase.ts @@ -0,0 +1,14 @@ +import { GetFolderParams, IGetFolderUseCase } from "./IGetFolderUseCase"; +import { IGetFolderRepository } from "./IGetFolderRepository"; + +export class GetFolderUseCase implements IGetFolderUseCase { + private repository: IGetFolderRepository; + + constructor(repository: IGetFolderRepository) { + this.repository = repository; + } + + async execute(params: GetFolderParams) { + await this.repository.execute(params.id); + } +} diff --git a/packages/app-aco/src/features/folder/getFolder/GetFolderUseCaseWithLoading.ts b/packages/app-aco/src/features/folder/getFolder/GetFolderUseCaseWithLoading.ts new file mode 100644 index 00000000000..a1ee2e0816d --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/GetFolderUseCaseWithLoading.ts @@ -0,0 +1,20 @@ +import { GetFolderParams, IGetFolderUseCase } from "./IGetFolderUseCase"; +import { ILoadingRepository } from "@webiny/app-utils"; +import { LoadingActionsEnum } from "~/types"; + +export class GetFolderUseCaseWithLoading implements IGetFolderUseCase { + private loadingRepository: ILoadingRepository; + private useCase: IGetFolderUseCase; + + constructor(loadingRepository: ILoadingRepository, useCase: IGetFolderUseCase) { + this.loadingRepository = loadingRepository; + this.useCase = useCase; + } + + async execute(params: GetFolderParams) { + await this.loadingRepository.runCallBack( + this.useCase.execute(params), + LoadingActionsEnum.get + ); + } +} diff --git a/packages/app-aco/src/features/folder/getFolder/IGetFolderGateway.ts b/packages/app-aco/src/features/folder/getFolder/IGetFolderGateway.ts new file mode 100644 index 00000000000..401dcb4f004 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/IGetFolderGateway.ts @@ -0,0 +1,24 @@ +import { CmsIdentity, FolderPermission } from "~/types"; + +export interface FolderDto { + id: string; + title: string; + slug: string; + permissions: FolderPermission[]; + hasNonInheritedPermissions: boolean; + canManagePermissions: boolean; + canManageStructure: boolean; + canManageContent: boolean; + type: string; + parentId: string | null; + createdBy: CmsIdentity; + createdOn: string; + savedBy: CmsIdentity; + savedOn: string; + modifiedBy: CmsIdentity | null; + modifiedOn: string | null; +} + +export interface IGetFolderGateway { + execute: (id: string) => Promise; +} diff --git a/packages/app-aco/src/features/folder/getFolder/IGetFolderRepository.ts b/packages/app-aco/src/features/folder/getFolder/IGetFolderRepository.ts new file mode 100644 index 00000000000..48bcaf7f250 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/IGetFolderRepository.ts @@ -0,0 +1,3 @@ +export interface IGetFolderRepository { + execute: (id: string) => Promise; +} diff --git a/packages/app-aco/src/features/folder/getFolder/IGetFolderUseCase.ts b/packages/app-aco/src/features/folder/getFolder/IGetFolderUseCase.ts new file mode 100644 index 00000000000..22cf8bcc44a --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/IGetFolderUseCase.ts @@ -0,0 +1,7 @@ +export interface GetFolderParams { + id: string; +} + +export interface IGetFolderUseCase { + execute: (params: GetFolderParams) => Promise; +} diff --git a/packages/app-aco/src/features/folder/getFolder/index.ts b/packages/app-aco/src/features/folder/getFolder/index.ts new file mode 100644 index 00000000000..ed6c1f0e032 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/index.ts @@ -0,0 +1 @@ +export * from "./useGetFolder"; diff --git a/packages/app-aco/src/features/folder/getFolder/useGetFolder.ts b/packages/app-aco/src/features/folder/getFolder/useGetFolder.ts new file mode 100644 index 00000000000..c0d606a38e3 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolder/useGetFolder.ts @@ -0,0 +1,35 @@ +import { useCallback, useContext } from "react"; +import { useApolloClient } from "@apollo/react-hooks"; +import { GetFolderGqlGateway } from "./GetFolderGqlGateway"; +import { GetFolderParams } from "./IGetFolderUseCase"; +import { GetFolder } from "./GetFolder"; +import { FoldersContext } from "~/contexts/folders"; + +export const useGetFolder = () => { + const client = useApolloClient(); + const gateway = new GetFolderGqlGateway(client); + + const foldersContext = useContext(FoldersContext); + + if (!foldersContext) { + throw new Error("useGetFolder must be used within a FoldersProvider"); + } + + const { type } = foldersContext; + + if (!type) { + throw Error(`FoldersProvider requires a "type" prop or an AcoAppContext to be available!`); + } + + const getFolder = useCallback( + (params: GetFolderParams) => { + const instance = GetFolder.instance(type, gateway); + return instance.execute(params); + }, + [type, gateway] + ); + + return { + getFolder + }; +}; diff --git a/packages/app-aco/src/features/folder/getFolderLevelPermission/FolderPermissionName.ts b/packages/app-aco/src/features/folder/getFolderLevelPermission/FolderPermissionName.ts new file mode 100644 index 00000000000..a9483befe9b --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolderLevelPermission/FolderPermissionName.ts @@ -0,0 +1,4 @@ +export type FolderPermissionName = + | "canManagePermissions" + | "canManageStructure" + | "canManageContent"; diff --git a/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermission.ts b/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermission.ts new file mode 100644 index 00000000000..eb7d50c208d --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermission.ts @@ -0,0 +1,23 @@ +import { IGetFolderLevelPermissionUseCase } from "./IGetFolderLevelPermissionUseCase"; +import { GetFolderLevelPermissionRepository } from "./GetFolderLevelPermissionRepository"; +import { GetFolderLevelPermissionWithFlpUseCase } from "./GetFolderLevelPermissionWithFlpUseCase"; +import { GetFolderLevelPermissionUseCase } from "./GetFolderLevelPermissionUseCase"; +import { FolderPermissionName } from "./FolderPermissionName"; +import { folderCacheFactory } from "../cache"; + +export class GetFolderLevelPermission { + public static instance( + type: string, + permissionName: FolderPermissionName, + canUseFlp: boolean + ): IGetFolderLevelPermissionUseCase { + const foldersCache = folderCacheFactory.getCache(type); + const repository = new GetFolderLevelPermissionRepository(foldersCache, permissionName); + + if (canUseFlp) { + return new GetFolderLevelPermissionWithFlpUseCase(repository); + } + + return new GetFolderLevelPermissionUseCase(); + } +} diff --git a/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermissionRepository.ts b/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermissionRepository.ts new file mode 100644 index 00000000000..e140985f6e1 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermissionRepository.ts @@ -0,0 +1,23 @@ +import { FoldersCache } from "../cache"; +import { IGetFolderLevelPermissionRepository } from "./IGetFolderLevelPermissionRepository"; +import { FolderPermissionName } from "./FolderPermissionName"; + +export class GetFolderLevelPermissionRepository implements IGetFolderLevelPermissionRepository { + private cache: FoldersCache; + private permissionName: FolderPermissionName; + + constructor(cache: FoldersCache, permissionName: FolderPermissionName) { + this.cache = cache; + this.permissionName = permissionName; + } + + execute(id: string) { + const folder = this.cache.getItem(id); + + if (!folder) { + return false; + } + + return folder[this.permissionName] ?? false; + } +} diff --git a/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermissionUseCase.ts b/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermissionUseCase.ts new file mode 100644 index 00000000000..982a2d1a442 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermissionUseCase.ts @@ -0,0 +1,7 @@ +import { IGetFolderLevelPermissionUseCase } from "./IGetFolderLevelPermissionUseCase"; + +export class GetFolderLevelPermissionUseCase implements IGetFolderLevelPermissionUseCase { + execute() { + return true; + } +} diff --git a/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermissionWithFlpUseCase.ts b/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermissionWithFlpUseCase.ts new file mode 100644 index 00000000000..0479cfbbd37 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolderLevelPermission/GetFolderLevelPermissionWithFlpUseCase.ts @@ -0,0 +1,14 @@ +import { IGetFolderLevelPermissionUseCase } from "./IGetFolderLevelPermissionUseCase"; +import { IGetFolderLevelPermissionRepository } from "./IGetFolderLevelPermissionRepository"; + +export class GetFolderLevelPermissionWithFlpUseCase implements IGetFolderLevelPermissionUseCase { + private repository: IGetFolderLevelPermissionRepository; + + constructor(repository: IGetFolderLevelPermissionRepository) { + this.repository = repository; + } + + execute(id: string) { + return this.repository.execute(id); + } +} diff --git a/packages/app-aco/src/features/folder/getFolderLevelPermission/IGetFolderLevelPermissionRepository.ts b/packages/app-aco/src/features/folder/getFolderLevelPermission/IGetFolderLevelPermissionRepository.ts new file mode 100644 index 00000000000..69585a37ed3 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolderLevelPermission/IGetFolderLevelPermissionRepository.ts @@ -0,0 +1,3 @@ +export interface IGetFolderLevelPermissionRepository { + execute: (id: string) => boolean; +} diff --git a/packages/app-aco/src/features/folder/getFolderLevelPermission/IGetFolderLevelPermissionUseCase.ts b/packages/app-aco/src/features/folder/getFolderLevelPermission/IGetFolderLevelPermissionUseCase.ts new file mode 100644 index 00000000000..9ae5dd13cc8 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolderLevelPermission/IGetFolderLevelPermissionUseCase.ts @@ -0,0 +1,3 @@ +export interface IGetFolderLevelPermissionUseCase { + execute: (id: string) => boolean; +} diff --git a/packages/app-aco/src/features/folder/getFolderLevelPermission/index.ts b/packages/app-aco/src/features/folder/getFolderLevelPermission/index.ts new file mode 100644 index 00000000000..ad2572bc388 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolderLevelPermission/index.ts @@ -0,0 +1 @@ +export * from "./useGetFolderLevelPermission"; diff --git a/packages/app-aco/src/features/folder/getFolderLevelPermission/useGetFolderLevelPermission.ts b/packages/app-aco/src/features/folder/getFolderLevelPermission/useGetFolderLevelPermission.ts new file mode 100644 index 00000000000..092aa8bf524 --- /dev/null +++ b/packages/app-aco/src/features/folder/getFolderLevelPermission/useGetFolderLevelPermission.ts @@ -0,0 +1,37 @@ +import { useCallback, useContext } from "react"; +import { useWcp } from "@webiny/app-wcp"; +import { FoldersContext } from "~/contexts/folders"; +import { GetFolderLevelPermission } from "./GetFolderLevelPermission"; +import { FolderPermissionName } from "./FolderPermissionName"; + +export const useGetFolderLevelPermission = (permissionName: FolderPermissionName) => { + const { canUseFolderLevelPermissions } = useWcp(); + + const foldersContext = useContext(FoldersContext); + + if (!foldersContext) { + throw new Error("useGetCanManageContent must be used within a FoldersProvider"); + } + + const { type } = foldersContext; + + if (!type) { + throw Error(`FoldersProvider requires a "type" prop or an AcoAppContext to be available!`); + } + + const getFolderLevelPermission = useCallback( + (id: string) => { + const instance = GetFolderLevelPermission.instance( + type, + permissionName, + canUseFolderLevelPermissions() + ); + return instance.execute(id); + }, + [type, canUseFolderLevelPermissions] + ); + + return { + getFolderLevelPermission + }; +}; diff --git a/packages/app-aco/src/features/folder/index.ts b/packages/app-aco/src/features/folder/index.ts new file mode 100644 index 00000000000..fc6678906ca --- /dev/null +++ b/packages/app-aco/src/features/folder/index.ts @@ -0,0 +1,9 @@ +export * from "./Folder"; +export * from "./cache"; +export * from "./createFolder"; +export * from "./deleteFolder"; +export * from "./getDescendantFolders"; +export * from "./getFolder"; +export * from "./getFolderLevelPermission"; +export * from "./listFolders"; +export * from "./updateFolder"; diff --git a/packages/app-aco/src/features/folder/listFolders/FolderDto.ts b/packages/app-aco/src/features/folder/listFolders/FolderDto.ts new file mode 100644 index 00000000000..053a523ebc9 --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/FolderDto.ts @@ -0,0 +1,53 @@ +import { CmsIdentity, FolderPermission } from "~/types"; +import { Folder } from "../Folder"; +import { ROOT_FOLDER } from "~/constants"; + +export interface FolderDto { + id: string; + title: string; + slug: string; + type: string; + parentId: string; + permissions: FolderPermission[]; + hasNonInheritedPermissions: boolean; + canManagePermissions: boolean; + canManageStructure: boolean; + canManageContent: boolean; + createdBy: CmsIdentity; + createdOn: string; + savedBy: CmsIdentity; + savedOn: string; + modifiedBy: CmsIdentity; + modifiedOn: string; +} + +export class FolderDtoMapper { + static toDTO(folder: Folder): FolderDto { + return { + id: folder.id, + title: folder.title, + canManageContent: folder.canManageContent ?? false, + canManagePermissions: folder.canManagePermissions ?? false, + canManageStructure: folder.canManageStructure ?? false, + createdBy: this.createIdentity(folder.createdBy), + createdOn: folder.createdOn ?? "", + hasNonInheritedPermissions: folder.hasNonInheritedPermissions ?? false, + modifiedBy: this.createIdentity(folder.modifiedBy), + modifiedOn: folder.modifiedOn ?? "", + parentId: folder.parentId ?? ROOT_FOLDER, + permissions: folder.permissions ?? [], + savedBy: this.createIdentity(folder.savedBy), + savedOn: folder.savedOn ?? "", + slug: folder.slug, + type: folder.type + }; + } + + private static createIdentity(identity?: CmsIdentity | null): CmsIdentity { + return { + id: identity?.id || "", + displayName: identity?.displayName || "", + type: identity?.type || "" + }; + } +} diff --git a/packages/app-aco/src/features/folder/listFolders/IListFoldersGateway.ts b/packages/app-aco/src/features/folder/listFolders/IListFoldersGateway.ts new file mode 100644 index 00000000000..7e010d72607 --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/IListFoldersGateway.ts @@ -0,0 +1,24 @@ +import { CmsIdentity, FolderPermission } from "~/types"; + +export interface FolderDto { + id: string; + title: string; + slug: string; + permissions: FolderPermission[]; + hasNonInheritedPermissions: boolean; + canManagePermissions: boolean; + canManageStructure: boolean; + canManageContent: boolean; + type: string; + parentId: string | null; + createdBy: CmsIdentity; + createdOn: string; + savedBy: CmsIdentity; + savedOn: string; + modifiedBy: CmsIdentity | null; + modifiedOn: string | null; +} + +export interface IListFoldersGateway { + execute: (type: string) => Promise; +} diff --git a/packages/app-aco/src/features/folder/listFolders/IListFoldersRepository.ts b/packages/app-aco/src/features/folder/listFolders/IListFoldersRepository.ts new file mode 100644 index 00000000000..f9aedf93d50 --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/IListFoldersRepository.ts @@ -0,0 +1,3 @@ +export interface IListFoldersRepository { + execute: () => Promise; +} diff --git a/packages/app-aco/src/features/folder/listFolders/IListFoldersUseCase.ts b/packages/app-aco/src/features/folder/listFolders/IListFoldersUseCase.ts new file mode 100644 index 00000000000..7024ce59ede --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/IListFoldersUseCase.ts @@ -0,0 +1,3 @@ +export interface IListFoldersUseCase { + execute: () => Promise; +} diff --git a/packages/app-aco/src/features/folder/listFolders/ListFolders.ts b/packages/app-aco/src/features/folder/listFolders/ListFolders.ts new file mode 100644 index 00000000000..71f8153e290 --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/ListFolders.ts @@ -0,0 +1,29 @@ +import { LoadingRepository, loadingRepositoryFactory } from "@webiny/app-utils"; +import { IListFoldersUseCase } from "./IListFoldersUseCase"; +import { IListFoldersGateway } from "./IListFoldersGateway"; +import { ListFoldersRepository } from "./ListFoldersRepository"; +import { ListFoldersUseCaseWithLoading } from "./ListFoldersUseCaseWithLoading"; +import { ListFoldersUseCase } from "./ListFoldersUseCase"; +import { folderCacheFactory, FoldersCache } from "../cache"; + +interface IListFoldersInstance { + useCase: IListFoldersUseCase; + folders: FoldersCache; + loading: LoadingRepository; +} + +export class ListFolders { + public static instance(type: string, gateway: IListFoldersGateway): IListFoldersInstance { + const foldersCache = folderCacheFactory.getCache(type); + const loadingRepository = loadingRepositoryFactory.getRepository(type); + const repository = new ListFoldersRepository(foldersCache, gateway, type); + const useCase = new ListFoldersUseCase(repository); + const useCaseWithLoading = new ListFoldersUseCaseWithLoading(loadingRepository, useCase); + + return { + useCase: useCaseWithLoading, + folders: foldersCache, + loading: loadingRepository + }; + } +} diff --git a/packages/app-aco/src/features/folder/listFolders/ListFoldersGqlGateway.ts b/packages/app-aco/src/features/folder/listFolders/ListFoldersGqlGateway.ts new file mode 100644 index 00000000000..5a301622a9e --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/ListFoldersGqlGateway.ts @@ -0,0 +1,129 @@ +import ApolloClient from "apollo-client"; +import gql from "graphql-tag"; +import { IListFoldersGateway } from "./IListFoldersGateway"; +import { AcoError, FolderItem } from "~/types"; +import { ROOT_FOLDER } from "~/constants"; + +export interface ListFoldersResponse { + aco: { + listFolders: { + data: FolderItem[] | null; + error: AcoError | null; + }; + }; +} + +export interface ListFoldersQueryVariables { + type: string; + limit: number; + sort?: Record; + after?: string | null; +} + +export const LIST_FOLDERS = gql` + query ListFolders($type: String!, $limit: Int!) { + aco { + listFolders(where: { type: $type }, limit: $limit) { + data { + id + title + slug + permissions { + target + level + inheritedFrom + } + hasNonInheritedPermissions + canManagePermissions + canManageStructure + canManageContent + parentId + type + savedOn + savedBy { + id + displayName + } + createdOn + createdBy { + id + displayName + } + modifiedOn + modifiedBy { + id + displayName + } + } + error { + code + data + message + } + } + } + } +`; + +export class ListFoldersGqlGateway implements IListFoldersGateway { + private client: ApolloClient; + + constructor(client: ApolloClient) { + this.client = client; + } + + async execute(type: string) { + const { data: response } = await this.client.query< + ListFoldersResponse, + ListFoldersQueryVariables + >({ + query: LIST_FOLDERS, + variables: { + type, + limit: 10000 + }, + fetchPolicy: "network-only" + }); + + if (!response) { + throw new Error("Network error while listing folders."); + } + + const { data, error } = response.aco.listFolders; + + if (!data) { + throw new Error(error?.message || "Could not fetch folders"); + } + + return [this.getRootFolder(), ...(data || [])]; + } + + private getRootFolder(): FolderItem { + return { + id: ROOT_FOLDER, + title: "Home", + permissions: [], + parentId: "0", + slug: "", + createdOn: "", + createdBy: { + id: "", + displayName: "", + type: "" + }, + hasNonInheritedPermissions: false, + canManagePermissions: true, + canManageStructure: true, + canManageContent: true, + savedOn: "", + savedBy: { + id: "", + displayName: "", + type: "" + }, + modifiedOn: null, + modifiedBy: null, + type: "$ROOT" + }; + } +} diff --git a/packages/app-aco/src/features/folder/listFolders/ListFoldersRepository.ts b/packages/app-aco/src/features/folder/listFolders/ListFoldersRepository.ts new file mode 100644 index 00000000000..1afe0c9536e --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/ListFoldersRepository.ts @@ -0,0 +1,23 @@ +import { makeAutoObservable } from "mobx"; +import { FoldersCache } from "../cache"; +import { Folder } from "../Folder"; +import { IListFoldersGateway } from "./IListFoldersGateway"; +import { IListFoldersRepository } from "./IListFoldersRepository"; + +export class ListFoldersRepository implements IListFoldersRepository { + private cache: FoldersCache; + private gateway: IListFoldersGateway; + private type: string; + + constructor(cache: FoldersCache, gateway: IListFoldersGateway, type: string) { + this.cache = cache; + this.gateway = gateway; + this.type = type; + makeAutoObservable(this); + } + + async execute() { + const items = await this.gateway.execute(this.type); + await this.cache.setMultiple(items.map(item => Folder.create(item))); + } +} diff --git a/packages/app-aco/src/features/folder/listFolders/ListFoldersUseCase.ts b/packages/app-aco/src/features/folder/listFolders/ListFoldersUseCase.ts new file mode 100644 index 00000000000..ac913080897 --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/ListFoldersUseCase.ts @@ -0,0 +1,14 @@ +import { IListFoldersUseCase } from "./IListFoldersUseCase"; +import { IListFoldersRepository } from "./IListFoldersRepository"; + +export class ListFoldersUseCase implements IListFoldersUseCase { + private repository: IListFoldersRepository; + + constructor(repository: IListFoldersRepository) { + this.repository = repository; + } + + async execute() { + await this.repository.execute(); + } +} diff --git a/packages/app-aco/src/features/folder/listFolders/ListFoldersUseCaseWithLoading.ts b/packages/app-aco/src/features/folder/listFolders/ListFoldersUseCaseWithLoading.ts new file mode 100644 index 00000000000..ff772fbb964 --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/ListFoldersUseCaseWithLoading.ts @@ -0,0 +1,17 @@ +import { ILoadingRepository } from "@webiny/app-utils"; +import { LoadingActionsEnum } from "~/types"; +import { IListFoldersUseCase } from "./IListFoldersUseCase"; + +export class ListFoldersUseCaseWithLoading implements IListFoldersUseCase { + private loadingRepository: ILoadingRepository; + private useCase: IListFoldersUseCase; + + constructor(loadingRepository: ILoadingRepository, useCase: IListFoldersUseCase) { + this.loadingRepository = loadingRepository; + this.useCase = useCase; + } + + async execute() { + await this.loadingRepository.runCallBack(this.useCase.execute(), LoadingActionsEnum.list); + } +} diff --git a/packages/app-aco/src/features/folder/listFolders/index.ts b/packages/app-aco/src/features/folder/listFolders/index.ts new file mode 100644 index 00000000000..92780101758 --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/index.ts @@ -0,0 +1,2 @@ +export * from "./useListFolders"; +export * from "./FolderDto"; diff --git a/packages/app-aco/src/features/folder/listFolders/useListFolders.ts b/packages/app-aco/src/features/folder/listFolders/useListFolders.ts new file mode 100644 index 00000000000..96db86776cf --- /dev/null +++ b/packages/app-aco/src/features/folder/listFolders/useListFolders.ts @@ -0,0 +1,82 @@ +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { autorun } from "mobx"; +import { useApolloClient } from "@apollo/react-hooks"; +import { ListFoldersGqlGateway } from "./ListFoldersGqlGateway"; +import { ListFolders } from "./ListFolders"; +import { FolderDtoMapper } from "./FolderDto"; +import { FoldersContext } from "~/contexts/folders"; +import { FolderItem } from "~/types"; + +export const useListFolders = () => { + const client = useApolloClient(); + const gateway = new ListFoldersGqlGateway(client); + + const [vm, setVm] = useState<{ + folders: FolderItem[]; + loading: Record; + }>({ + folders: [], + loading: { + INIT: true + } + }); + + const foldersContext = useContext(FoldersContext); + + if (!foldersContext) { + throw new Error("useCreateFolder must be used within a FoldersProvider"); + } + + const { type } = foldersContext; + + if (!type) { + throw Error(`FoldersProvider requires a "type" prop or an AcoAppContext to be available!`); + } + + const { + useCase, + folders: foldersCache, + loading + } = useMemo(() => { + return ListFolders.instance(type, gateway); + }, [type, gateway]); + + const listFolders = useCallback(() => { + return useCase.execute(); + }, [useCase]); + + useEffect(() => { + if (foldersCache.hasItems()) { + return; // Skip if we already have folders in the cache. + } + + listFolders(); + }, []); + + useEffect(() => { + return autorun(() => { + const folders = foldersCache.getItems().map(folder => FolderDtoMapper.toDTO(folder)); + + setVm(vm => ({ + ...vm, + folders + })); + }); + }, [foldersCache]); + + useEffect(() => { + return autorun(() => { + const loadingState = loading.get(); + + setVm(vm => ({ + ...vm, + loading: loadingState + })); + }); + }, [loading]); + + return { + ...vm, + listFolders + }; +}; diff --git a/packages/app-aco/src/features/folder/updateFolder/FolderDto.ts b/packages/app-aco/src/features/folder/updateFolder/FolderDto.ts new file mode 100644 index 00000000000..b162e2cc1e3 --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/FolderDto.ts @@ -0,0 +1,9 @@ +import { FolderPermission } from "~/types"; + +export interface FolderDto { + id: string; + title: string; + slug: string; + permissions: FolderPermission[]; + parentId: string | null; +} diff --git a/packages/app-aco/src/features/folder/updateFolder/FolderGqlDto.ts b/packages/app-aco/src/features/folder/updateFolder/FolderGqlDto.ts new file mode 100644 index 00000000000..0d2ff62504c --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/FolderGqlDto.ts @@ -0,0 +1,20 @@ +import { CmsIdentity, FolderPermission } from "~/types"; + +export interface FolderGqlDto { + id: string; + title: string; + slug: string; + permissions: FolderPermission[]; + hasNonInheritedPermissions: boolean; + canManagePermissions: boolean; + canManageStructure: boolean; + canManageContent: boolean; + type: string; + parentId: string | null; + createdBy: CmsIdentity; + createdOn: string; + savedBy: CmsIdentity; + savedOn: string; + modifiedBy: CmsIdentity | null; + modifiedOn: string | null; +} diff --git a/packages/app-aco/src/features/folder/updateFolder/IUpdateFolderGateway.ts b/packages/app-aco/src/features/folder/updateFolder/IUpdateFolderGateway.ts new file mode 100644 index 00000000000..f6003e70efc --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/IUpdateFolderGateway.ts @@ -0,0 +1,6 @@ +import { FolderDto } from "./FolderDto"; +import { FolderGqlDto } from "./FolderGqlDto"; + +export interface IUpdateFolderGateway { + execute: (folder: FolderDto) => Promise; +} diff --git a/packages/app-aco/src/features/folder/updateFolder/IUpdateFolderRepository.ts b/packages/app-aco/src/features/folder/updateFolder/IUpdateFolderRepository.ts new file mode 100644 index 00000000000..76c55d36275 --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/IUpdateFolderRepository.ts @@ -0,0 +1,5 @@ +import { Folder } from "../Folder"; + +export interface IUpdateFolderRepository { + execute: (folder: Folder) => Promise; +} diff --git a/packages/app-aco/src/features/folder/updateFolder/IUpdateFolderUseCase.ts b/packages/app-aco/src/features/folder/updateFolder/IUpdateFolderUseCase.ts new file mode 100644 index 00000000000..173404f49ae --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/IUpdateFolderUseCase.ts @@ -0,0 +1,14 @@ +import { FolderPermission } from "~/types"; + +export interface UpdateFolderParams { + id: string; + title: string; + slug: string; + type: string; + parentId: string | null; + permissions: FolderPermission[]; +} + +export interface IUpdateFolderUseCase { + execute: (params: UpdateFolderParams) => Promise; +} diff --git a/packages/app-aco/src/features/folder/updateFolder/UpdateFolder.ts b/packages/app-aco/src/features/folder/updateFolder/UpdateFolder.ts new file mode 100644 index 00000000000..294eee74b15 --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/UpdateFolder.ts @@ -0,0 +1,17 @@ +import { loadingRepositoryFactory } from "@webiny/app-utils"; +import { IUpdateFolderUseCase } from "./IUpdateFolderUseCase"; +import { IUpdateFolderGateway } from "./IUpdateFolderGateway"; +import { UpdateFolderRepository } from "./UpdateFolderRepository"; +import { UpdateFolderUseCase } from "./UpdateFolderUseCase"; +import { UpdateFolderUseCaseWithLoading } from "./UpdateFolderUseCaseWithLoading"; +import { folderCacheFactory } from "~/features/folder"; + +export class UpdateFolder { + public static instance(type: string, gateway: IUpdateFolderGateway): IUpdateFolderUseCase { + const foldersCache = folderCacheFactory.getCache(type); + const loadingRepository = loadingRepositoryFactory.getRepository(type); + const repository = new UpdateFolderRepository(foldersCache, gateway); + const useCase = new UpdateFolderUseCase(repository); + return new UpdateFolderUseCaseWithLoading(loadingRepository, useCase); + } +} diff --git a/packages/app-aco/src/features/folder/updateFolder/UpdateFolderGqlGateway.ts b/packages/app-aco/src/features/folder/updateFolder/UpdateFolderGqlGateway.ts new file mode 100644 index 00000000000..21855ab4b44 --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/UpdateFolderGqlGateway.ts @@ -0,0 +1,110 @@ +import ApolloClient from "apollo-client"; +import gql from "graphql-tag"; +import { IUpdateFolderGateway } from "./IUpdateFolderGateway"; +import { FolderDto } from "./FolderDto"; +import { AcoError, FolderItem } from "~/types"; +import { ROOT_FOLDER } from "~/constants"; + +export interface UpdateFolderResponse { + aco: { + updateFolder: { + data: FolderItem; + error: AcoError | null; + }; + }; +} + +export interface UpdateFolderVariables { + id: string; + data: Partial< + Omit< + FolderItem, + "id" | "createdOn" | "createdBy" | "savedOn" | "savedBy" | "modifiedOn" | "modifiedBy" + > + >; +} + +export const UPDATE_FOLDER = gql` + mutation UpdateFolder($id: ID!, $data: FolderUpdateInput!) { + aco { + updateFolder(id: $id, data: $data) { + data { + id + title + slug + permissions { + target + level + inheritedFrom + } + hasNonInheritedPermissions + canManagePermissions + canManageStructure + canManageContent + parentId + type + savedOn + savedBy { + id + displayName + } + createdOn + createdBy { + id + displayName + } + modifiedOn + modifiedBy { + id + displayName + } + } + error { + code + data + message + } + } + } + } +`; + +export class UpdateFolderGqlGateway implements IUpdateFolderGateway { + private client: ApolloClient; + + constructor(client: ApolloClient) { + this.client = client; + } + + async execute(folder: FolderDto) { + const { id, title, slug, permissions, parentId } = folder; + + const { data: response } = await this.client.mutate< + UpdateFolderResponse, + UpdateFolderVariables + >({ + mutation: UPDATE_FOLDER, + variables: { + id, + data: { + title, + slug, + parentId: parentId === ROOT_FOLDER ? null : parentId, + permissions: permissions.filter(p => !p.inheritedFrom) + } + } + }); + + if (!response) { + throw new Error("Network error while updating folder."); + } + + const { data, error } = response.aco.updateFolder; + + if (!data) { + throw new Error(error?.message || "Could not update folder"); + } + + return data; + } +} diff --git a/packages/app-aco/src/features/folder/updateFolder/UpdateFolderRepository.ts b/packages/app-aco/src/features/folder/updateFolder/UpdateFolderRepository.ts new file mode 100644 index 00000000000..7c620a5a005 --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/UpdateFolderRepository.ts @@ -0,0 +1,30 @@ +import { makeAutoObservable } from "mobx"; +import { IUpdateFolderRepository } from "./IUpdateFolderRepository"; +import { FoldersCache } from "../cache"; +import { Folder } from "../Folder"; +import { IUpdateFolderGateway } from "./IUpdateFolderGateway"; +import { FolderDto } from "./FolderDto"; + +export class UpdateFolderRepository implements IUpdateFolderRepository { + private cache: FoldersCache; + private gateway: IUpdateFolderGateway; + + constructor(cache: FoldersCache, gateway: IUpdateFolderGateway) { + this.cache = cache; + this.gateway = gateway; + makeAutoObservable(this); + } + + async execute(folder: Folder) { + const dto: FolderDto = { + id: folder.id, + title: folder.title, + slug: folder.slug, + permissions: folder.permissions, + parentId: folder.parentId + }; + + const result = await this.gateway.execute(dto); + await this.cache.update(folder.id, Folder.create(result)); + } +} diff --git a/packages/app-aco/src/features/folder/updateFolder/UpdateFolderUseCase.ts b/packages/app-aco/src/features/folder/updateFolder/UpdateFolderUseCase.ts new file mode 100644 index 00000000000..f86690f6025 --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/UpdateFolderUseCase.ts @@ -0,0 +1,24 @@ +import { UpdateFolderParams, IUpdateFolderUseCase } from "./IUpdateFolderUseCase"; +import { IUpdateFolderRepository } from "./IUpdateFolderRepository"; +import { Folder } from "../Folder"; + +export class UpdateFolderUseCase implements IUpdateFolderUseCase { + private repository: IUpdateFolderRepository; + + constructor(repository: IUpdateFolderRepository) { + this.repository = repository; + } + + async execute(params: UpdateFolderParams) { + await this.repository.execute( + Folder.create({ + id: params.id, + title: params.title, + slug: params.slug, + type: params.type, + parentId: params.parentId, + permissions: params.permissions + }) + ); + } +} diff --git a/packages/app-aco/src/features/folder/updateFolder/UpdateFolderUseCaseWithLoading.ts b/packages/app-aco/src/features/folder/updateFolder/UpdateFolderUseCaseWithLoading.ts new file mode 100644 index 00000000000..6c75936edbc --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/UpdateFolderUseCaseWithLoading.ts @@ -0,0 +1,20 @@ +import { ILoadingRepository } from "@webiny/app-utils"; +import { LoadingActionsEnum } from "~/types"; +import { IUpdateFolderUseCase, UpdateFolderParams } from "./IUpdateFolderUseCase"; + +export class UpdateFolderUseCaseWithLoading implements IUpdateFolderUseCase { + private loadingRepository: ILoadingRepository; + private useCase: IUpdateFolderUseCase; + + constructor(loadingRepository: ILoadingRepository, useCase: IUpdateFolderUseCase) { + this.loadingRepository = loadingRepository; + this.useCase = useCase; + } + + async execute(params: UpdateFolderParams) { + await this.loadingRepository.runCallBack( + this.useCase.execute(params), + LoadingActionsEnum.update + ); + } +} diff --git a/packages/app-aco/src/features/folder/updateFolder/index.ts b/packages/app-aco/src/features/folder/updateFolder/index.ts new file mode 100644 index 00000000000..776c694e3e8 --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/index.ts @@ -0,0 +1 @@ +export * from "./useUpdateFolder"; diff --git a/packages/app-aco/src/features/folder/updateFolder/useUpdateFolder.ts b/packages/app-aco/src/features/folder/updateFolder/useUpdateFolder.ts new file mode 100644 index 00000000000..962d04a0d69 --- /dev/null +++ b/packages/app-aco/src/features/folder/updateFolder/useUpdateFolder.ts @@ -0,0 +1,35 @@ +import { useCallback, useContext } from "react"; +import { useApolloClient } from "@apollo/react-hooks"; +import { UpdateFolderGqlGateway } from "./UpdateFolderGqlGateway"; +import { UpdateFolder } from "./UpdateFolder"; +import { UpdateFolderParams } from "./IUpdateFolderUseCase"; +import { FoldersContext } from "~/contexts/folders"; + +export const useUpdateFolder = () => { + const client = useApolloClient(); + const gateway = new UpdateFolderGqlGateway(client); + + const foldersContext = useContext(FoldersContext); + + if (!foldersContext) { + throw new Error("useUpdateFolder must be used within a FoldersProvider"); + } + + const { type } = foldersContext; + + if (!type) { + throw Error(`FoldersProvider requires a "type" prop or an AcoAppContext to be available!`); + } + + const updateFolder = useCallback( + (params: UpdateFolderParams) => { + const instance = UpdateFolder.instance(type, gateway); + return instance.execute(params); + }, + [type, gateway] + ); + + return { + updateFolder + }; +}; diff --git a/packages/app-aco/src/graphql/folders.gql.ts b/packages/app-aco/src/graphql/folders.gql.ts deleted file mode 100644 index add1cece388..00000000000 --- a/packages/app-aco/src/graphql/folders.gql.ts +++ /dev/null @@ -1,98 +0,0 @@ -import gql from "graphql-tag"; - -const ERROR_FIELD = /* GraphQL */ ` - { - code - data - message - } -`; - -const DATA_FIELD = /* GraphQL */ ` - { - id - title - slug - permissions { - target - level - inheritedFrom - } - hasNonInheritedPermissions - canManagePermissions - canManageStructure - canManageContent - parentId - type - savedOn - savedBy { - id - displayName - } - createdOn - createdBy { - id - displayName - } - modifiedOn - modifiedBy { - id - displayName - } - } -`; - -export const CREATE_FOLDER = gql` - mutation CreateFolder($data: FolderCreateInput!) { - aco { - createFolder(data: $data) { - data ${DATA_FIELD} - error ${ERROR_FIELD} - } - } - } -`; - -export const LIST_FOLDERS = gql` - query ListFolders ($type: String!, $limit: Int!) { - aco { - listFolders(where: { type: $type }, limit: $limit) { - data ${DATA_FIELD} - error ${ERROR_FIELD} - } - } - } -`; - -export const GET_FOLDER = gql` - query GetFolder ($id: ID!) { - aco { - getFolder(id: $id) { - data ${DATA_FIELD} - error ${ERROR_FIELD} - } - } - } -`; - -export const UPDATE_FOLDER = gql` - mutation UpdateFolder($id: ID!, $data: FolderUpdateInput!) { - aco { - updateFolder(id: $id, data: $data) { - data ${DATA_FIELD} - error ${ERROR_FIELD} - } - } - } -`; - -export const DELETE_FOLDER = gql` - mutation DeleteFolder($id: ID!) { - aco { - deleteFolder(id: $id) { - data - error ${ERROR_FIELD} - } - } - } -`; diff --git a/packages/app-aco/src/hooks/index.ts b/packages/app-aco/src/hooks/index.ts index a25f3b8d2b4..60907b72441 100644 --- a/packages/app-aco/src/hooks/index.ts +++ b/packages/app-aco/src/hooks/index.ts @@ -5,4 +5,3 @@ export * from "./useFolders"; export * from "./useRecords"; export * from "./useTags"; export * from "./useNavigateFolder"; -export { useFoldersApi } from "../contexts/FoldersApi"; diff --git a/packages/app-aco/src/hooks/useFolders.ts b/packages/app-aco/src/hooks/useFolders.ts index b8d6282a243..1a0a6c7e9ee 100644 --- a/packages/app-aco/src/hooks/useFolders.ts +++ b/packages/app-aco/src/hooks/useFolders.ts @@ -1,40 +1,40 @@ -import { useContext, useEffect, useMemo } from "react"; -import { FoldersContext } from "~/contexts/folders"; +import { + useCreateFolder, + useDeleteFolder, + useGetDescendantFolders, + useGetFolder, + useGetFolderLevelPermission, + useListFolders, + useUpdateFolder +} from "~/features/folder"; export const useFolders = () => { - const context = useContext(FoldersContext); - if (!context) { - throw new Error("useFolders must be used within a FoldersProvider"); - } + const { createFolder } = useCreateFolder(); + const { deleteFolder } = useDeleteFolder(); + const { listFolders, folders, loading } = useListFolders(); + const { updateFolder } = useUpdateFolder(); + const { getDescendantFolders } = useGetDescendantFolders(); + const { getFolder } = useGetFolder(); + const { getFolderLevelPermission: canManageStructure } = + useGetFolderLevelPermission("canManageStructure"); + const { getFolderLevelPermission: canManagePermissions } = + useGetFolderLevelPermission("canManagePermissions"); + const { getFolderLevelPermission: canManageContent } = + useGetFolderLevelPermission("canManageContent"); - const { folders, loading, listFolders, ...other } = context; - - useEffect(() => { - /** - * On first mount, call `listFolders`, which will either issue a network request, or load folders from cache. - * We don't need to store the result of it to any local state; that is managed by the context provider. - * - * IMPORTANT: we check if the folders array exists: the hook can be used from multiple components and - * fetch the outdated list from Apollo Cache. Since the state is managed locally, we fetch the folders only - * at the first mount. - */ - if (folders) { - return; + return { + folders, + loading, + listFolders, + getFolder, + getDescendantFolders, + createFolder, + updateFolder, + deleteFolder, + folderLevelPermissions: { + canManageStructure, + canManagePermissions, + canManageContent } - listFolders(); - }, []); - - return useMemo( - () => ({ - /** - * NOTE: do NOT expose listFolders from this hook, because you already have folders in the `folders` property. - * You'll never need to call `listFolders` from any component. As soon as you call `useFolders()`, you'll initiate - * fetching of `folders`, which is managed by the FoldersContext. - */ - loading, - folders, - ...other - }), - [folders, loading] - ); + }; }; diff --git a/packages/app-aco/src/index.ts b/packages/app-aco/src/index.ts index c7c297a3415..1c69c067f52 100644 --- a/packages/app-aco/src/index.ts +++ b/packages/app-aco/src/index.ts @@ -3,5 +3,4 @@ export * from "./config"; export * from "./contexts"; export * from "./hooks"; export * from "./dialogs"; -export * from "./Folders"; export * from "./sorting"; diff --git a/packages/app-aco/src/types.ts b/packages/app-aco/src/types.ts index 73ef6344160..1f3c61190fc 100644 --- a/packages/app-aco/src/types.ts +++ b/packages/app-aco/src/types.ts @@ -67,6 +67,17 @@ export type LoadingActions = | "DELETE" | "MOVE"; +export enum LoadingActionsEnum { + init = "INIT", + list = "LIST", + listMore = "LIST_MORE", + get = "GET", + create = "CREATE", + update = "UPDATE", + delete = "DELETE", + move = "MOVE" +} + export interface AcoError { code: string; message: string; @@ -142,7 +153,17 @@ export interface CreateFolderResponse { export interface CreateFolderVariables { data: Omit< FolderItem, - "id" | "createdOn" | "createdBy" | "savedOn" | "savedBy" | "modifiedOn" | "modifiedBy" + | "id" + | "createdOn" + | "createdBy" + | "savedOn" + | "savedBy" + | "modifiedOn" + | "modifiedBy" + | "hasNonInheritedPermissions" + | "canManageContent" + | "canManagePermissions" + | "canManageStructure" >; } diff --git a/packages/app-aco/tsconfig.build.json b/packages/app-aco/tsconfig.build.json index e9fd945cba2..bda102d5c14 100644 --- a/packages/app-aco/tsconfig.build.json +++ b/packages/app-aco/tsconfig.build.json @@ -6,6 +6,7 @@ { "path": "../app-admin/tsconfig.build.json" }, { "path": "../app-headless-cms-common/tsconfig.build.json" }, { "path": "../app-security/tsconfig.build.json" }, + { "path": "../app-utils/tsconfig.build.json" }, { "path": "../app-wcp/tsconfig.build.json" }, { "path": "../form/tsconfig.build.json" }, { "path": "../plugins/tsconfig.build.json" }, diff --git a/packages/app-aco/tsconfig.json b/packages/app-aco/tsconfig.json index 83c93d96212..f7cb73a64e3 100644 --- a/packages/app-aco/tsconfig.json +++ b/packages/app-aco/tsconfig.json @@ -6,6 +6,7 @@ { "path": "../app-admin" }, { "path": "../app-headless-cms-common" }, { "path": "../app-security" }, + { "path": "../app-utils" }, { "path": "../app-wcp" }, { "path": "../form" }, { "path": "../plugins" }, @@ -30,6 +31,8 @@ "@webiny/app-headless-cms-common": ["../app-headless-cms-common/src"], "@webiny/app-security/*": ["../app-security/src/*"], "@webiny/app-security": ["../app-security/src"], + "@webiny/app-utils/*": ["../app-utils/src/*"], + "@webiny/app-utils": ["../app-utils/src"], "@webiny/app-wcp/*": ["../app-wcp/src/*"], "@webiny/app-wcp": ["../app-wcp/src"], "@webiny/form/*": ["../form/src/*"], diff --git a/packages/app-serverless-cms/package.json b/packages/app-serverless-cms/package.json index aa229371b54..b2a4f323cc4 100644 --- a/packages/app-serverless-cms/package.json +++ b/packages/app-serverless-cms/package.json @@ -11,7 +11,6 @@ "dependencies": { "@emotion/react": "^11.10.6", "@webiny/app": "0.0.0", - "@webiny/app-aco": "0.0.0", "@webiny/app-admin": "0.0.0", "@webiny/app-admin-rmwc": "0.0.0", "@webiny/app-apw": "0.0.0", diff --git a/packages/app-serverless-cms/src/Admin.tsx b/packages/app-serverless-cms/src/Admin.tsx index 103c0b66434..d46d168fb34 100644 --- a/packages/app-serverless-cms/src/Admin.tsx +++ b/packages/app-serverless-cms/src/Admin.tsx @@ -28,7 +28,6 @@ import { AuditLogs } from "@webiny/app-audit-logs"; import { LexicalEditorPlugin } from "@webiny/lexical-editor-pb-element"; import { LexicalEditorActions } from "@webiny/lexical-editor-actions"; import { Module as MailerSettings } from "@webiny/app-mailer"; -import { Folders } from "@webiny/app-aco"; import { Websockets } from "@webiny/app-websockets"; import { RecordLocking } from "@webiny/app-record-locking"; import { TrashBinConfigs } from "@webiny/app-trash-bin"; @@ -51,7 +50,6 @@ const App = (props: AdminProps) => { - diff --git a/packages/app-serverless-cms/tsconfig.build.json b/packages/app-serverless-cms/tsconfig.build.json index 804755f3d06..7901e5895b5 100644 --- a/packages/app-serverless-cms/tsconfig.build.json +++ b/packages/app-serverless-cms/tsconfig.build.json @@ -3,7 +3,6 @@ "include": ["src"], "references": [ { "path": "../app/tsconfig.build.json" }, - { "path": "../app-aco/tsconfig.build.json" }, { "path": "../app-admin/tsconfig.build.json" }, { "path": "../app-admin-rmwc/tsconfig.build.json" }, { "path": "../app-apw/tsconfig.build.json" }, diff --git a/packages/app-serverless-cms/tsconfig.json b/packages/app-serverless-cms/tsconfig.json index ac59a189557..46906b9d9f9 100644 --- a/packages/app-serverless-cms/tsconfig.json +++ b/packages/app-serverless-cms/tsconfig.json @@ -3,7 +3,6 @@ "include": ["src", "__tests__"], "references": [ { "path": "../app" }, - { "path": "../app-aco" }, { "path": "../app-admin" }, { "path": "../app-admin-rmwc" }, { "path": "../app-apw" }, @@ -37,8 +36,6 @@ "~tests/*": ["./__tests__/*"], "@webiny/app/*": ["../app/src/*"], "@webiny/app": ["../app/src"], - "@webiny/app-aco/*": ["../app-aco/src/*"], - "@webiny/app-aco": ["../app-aco/src"], "@webiny/app-admin/*": ["../app-admin/src/*"], "@webiny/app-admin": ["../app-admin/src"], "@webiny/app-admin-rmwc/*": ["../app-admin-rmwc/src/*"], diff --git a/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepositoryFactory.ts b/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepositoryFactory.ts index e761c3768ae..de631f0bb14 100644 --- a/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepositoryFactory.ts +++ b/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepositoryFactory.ts @@ -3,8 +3,8 @@ import { LoadingRepository } from "./LoadingRepository"; export class LoadingRepositoryFactory { private cache: Map = new Map(); - getRepository() { - const cacheKey = this.getCacheKey(); + getRepository(namespace?: string) { + const cacheKey = this.getCacheKey(namespace); if (!this.cache.has(cacheKey)) { this.cache.set(cacheKey, new LoadingRepository()); @@ -13,8 +13,8 @@ export class LoadingRepositoryFactory { return this.cache.get(cacheKey) as LoadingRepository; } - private getCacheKey() { - return Date.now().toString(); + private getCacheKey(namespace?: string) { + return namespace ?? Date.now().toString(); } } diff --git a/yarn.lock b/yarn.lock index 8da08f99a6e..355ded6c52e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13508,6 +13508,7 @@ __metadata: "@webiny/app-admin": 0.0.0 "@webiny/app-headless-cms-common": 0.0.0 "@webiny/app-security": 0.0.0 + "@webiny/app-utils": 0.0.0 "@webiny/app-wcp": 0.0.0 "@webiny/cli": 0.0.0 "@webiny/form": 0.0.0 @@ -14468,7 +14469,6 @@ __metadata: "@emotion/babel-plugin": ^11.11.0 "@emotion/react": ^11.10.6 "@webiny/app": 0.0.0 - "@webiny/app-aco": 0.0.0 "@webiny/app-admin": 0.0.0 "@webiny/app-admin-rmwc": 0.0.0 "@webiny/app-apw": 0.0.0