diff --git a/apps/extension/src/contentscript/multitable-panel/assets/vectors.tsx b/apps/extension/src/contentscript/multitable-panel/assets/vectors.tsx index 74d6af22..fa8d17bf 100644 --- a/apps/extension/src/contentscript/multitable-panel/assets/vectors.tsx +++ b/apps/extension/src/contentscript/multitable-panel/assets/vectors.tsx @@ -168,3 +168,52 @@ export const AvailableIcon = () => ( /> ) + +export const PlusCircle = () => ( + + + + + + + +) + +export const MinusCircle = () => ( + + + + + + +) diff --git a/apps/extension/src/contentscript/multitable-panel/components/application-card.tsx b/apps/extension/src/contentscript/multitable-panel/components/application-card.tsx index 851dc2a7..bc74fb04 100644 --- a/apps/extension/src/contentscript/multitable-panel/components/application-card.tsx +++ b/apps/extension/src/contentscript/multitable-panel/components/application-card.tsx @@ -1,15 +1,19 @@ -import { AppMetadata } from '@mweb/engine' +import { AppMetadata, Document, useAppDocuments } from '@mweb/engine' import React from 'react' import styled from 'styled-components' import { Image } from './image' +import { DocumentCard } from './document-card' +import { AppInMutation } from '@mweb/engine/lib/app/services/mutation/mutation.entity' +import { Spin } from 'antd' -const Card = styled.div` +const Card = styled.div<{ $backgroundColor?: string }>` position: relative; width: 100%; border-radius: 10px; - background: #fff; + background: ${(p) => p.$backgroundColor}; border: 1px solid #eceef0; - font-family: sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', + 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; &:hover { background: rgba(24, 121, 206, 0.1); } @@ -36,12 +40,20 @@ const CardContent = styled.div` width: 100%; ` -const TextLink = styled.div<{ bold?: boolean; small?: boolean; ellipsis?: boolean }>` +type TTextLink = { + bold?: boolean + small?: boolean + ellipsis?: boolean + $color?: string +} + +const TextLink = styled.div` display: block; margin: 0; font-size: 14px; line-height: 18px; - color: ${(p) => (p.bold ? '#11181C !important' : '#687076 !important')}; + color: ${(p) => + p.$color ? `${p.$color} !important` : p.bold ? '#11181C !important' : '#687076 !important'}; font-weight: ${(p) => (p.bold ? '600' : '400')}; font-size: ${(p) => (p.small ? '12px' : '14px')}; overflow: ${(p) => (p.ellipsis ? 'hidden' : 'visible')}; @@ -50,29 +62,13 @@ const TextLink = styled.div<{ bold?: boolean; small?: boolean; ellipsis?: boolea outline: none; ` -// const Text = styled.p<{ bold?: boolean; small?: boolean; ellipsis?: boolean }>` -// margin: 0; -// font-size: 14px; -// line-height: 20px; -// color: ${(p) => (p.bold ? '#11181C' : '#687076')}; -// font-weight: ${(p) => (p.bold ? '600' : '400')}; -// font-size: ${(p) => (p.small ? '12px' : '14px')}; -// overflow: ${(p) => (p.ellipsis ? 'hidden' : '')}; -// text-overflow: ${(p) => (p.ellipsis ? 'ellipsis' : '')}; -// white-space: nowrap; - -// i { -// margin-right: 3px; -// } -// ` - -const Thumbnail = styled.div` +const Thumbnail = styled.div<{ $shape: 'circle' | 'default' }>` display: block; width: 60px; height: 60px; flex-shrink: 0; border: 1px solid #eceef0; - border-radius: 8px; + border-radius: ${(props) => (props.$shape === 'circle' ? '99em' : '8px')}; overflow: hidden; outline: none; transition: border-color 200ms; @@ -108,6 +104,45 @@ const ButtonLink = styled.button` } ` +const DocumentsWrapper = styled.div` + display: flex; + padding-bottom: 10px; +` + +const SideLine = styled.div` + border: 1px solid #c1c6ce; + margin: 0 10px; +` + +const DocumentCardList = styled.div` + width: 100%; + margin-right: 10px; + display: flex; + flex-direction: column; + gap: 6px; +` + +const MoreIcon = () => ( + + + + +) + const UncheckedIcon = () => ( ( ) -export interface Props { +export interface ISimpleApplicationCardProps { src: string metadata: AppMetadata['metadata'] + disabled: boolean isChecked: boolean onChange: (isChecked: boolean) => void + iconShape?: 'circle' + textColor?: string + backgroundColor?: string +} + +export interface IApplicationCardWithDocsProps { + src: string + metadata: AppMetadata['metadata'] disabled: boolean + docsIds: AppInMutation['documentId'][] + onDocCheckboxChange: (docId: string | null, isChecked: boolean) => void + onOpenDocumentsModal: (docs: Document[]) => void +} + +interface IApplicationCard + extends ISimpleApplicationCardProps, + Omit { + hasDocuments: boolean + usingDocs: (Document | null)[] + allDocs: Document[] } -export const ApplicationCard: React.FC = ({ +const ApplicationCard: React.FC = ({ src, metadata, + disabled, + hasDocuments, + iconShape, + textColor, + backgroundColor, isChecked, + usingDocs, + allDocs, onChange, - disabled, + onDocCheckboxChange, + onOpenDocumentsModal, }) => { - const [accountId, , widgetName] = src.split('/') - + const [accountId, , appId] = src.split('/') return ( - + - + = ({ - - {metadata.name || widgetName} + + {metadata.name || appId} @{accountId} + onChange(!isChecked)} + onClick={hasDocuments ? () => onOpenDocumentsModal(allDocs) : () => onChange(!isChecked)} > - {isChecked ? : } + {hasDocuments ? : isChecked ? : } + + {hasDocuments && usingDocs.length ? ( + + + + {usingDocs.map((doc) => ( + onDocCheckboxChange(doc?.id ?? null, false)} + disabled={disabled} + appMetadata={metadata} + /> + ))} + + + ) : null} ) } + +export const SimpleApplicationCard: React.FC = (props) => ( + null} + onDocCheckboxChange={() => null} + usingDocs={[]} + allDocs={[]} + /> +) + +export const ApplicationCardWithDocs: React.FC = (props) => { + const { src, docsIds } = props + const { documents, isLoading } = useAppDocuments(src) + const usingDocs: (Document | null)[] = documents?.filter((doc) => docsIds.includes(doc.id)) + if (docsIds.includes(null)) usingDocs.unshift(null) + + return isLoading ? ( + + + + + + ) : ( + null} + usingDocs={usingDocs} + allDocs={documents} + /> + ) +} diff --git a/apps/extension/src/contentscript/multitable-panel/components/button.tsx b/apps/extension/src/contentscript/multitable-panel/components/button.tsx index a9d25d91..57442f05 100644 --- a/apps/extension/src/contentscript/multitable-panel/components/button.tsx +++ b/apps/extension/src/contentscript/multitable-panel/components/button.tsx @@ -1,11 +1,12 @@ import styled from 'styled-components' -export const Button = styled.button` +export const Button = styled.button<{ primary?: boolean }>` display: flex; justify-content: center; align-items: center; - border: 1px solid rgba(226, 226, 229, 1); - color: rgba(2, 25, 58, 1); + border: ${(p) => (p.primary ? 'none' : '1px solid rgba(226, 226, 229, 1)')}; + color: ${(p) => (p.primary ? '#fff' : 'rgba(2, 25, 58, 1)')}; + background: ${(p) => (p.primary ? 'rgba(56, 75, 255, 1)' : 'inherit')}; width: 175px; height: 42px; border-radius: 10px; diff --git a/apps/extension/src/contentscript/multitable-panel/components/document-card.tsx b/apps/extension/src/contentscript/multitable-panel/components/document-card.tsx new file mode 100644 index 00000000..18e63b10 --- /dev/null +++ b/apps/extension/src/contentscript/multitable-panel/components/document-card.tsx @@ -0,0 +1,186 @@ +import { AppMetadata } from '@mweb/engine' +import React from 'react' +import styled from 'styled-components' +import { Image } from './image' +import { DocumentMetadata } from '@mweb/engine/lib/app/services/document/document.entity' + +const Card = styled.div` + position: relative; + width: 100%; + border-radius: 10px; + background: #f8f9ff; + border: 1px solid #eceef0; + font-family: sans-serif; + &:hover { + background: rgba(24, 121, 206, 0.1); + } + &.disabled { + opacity: 0.7; + } + &.disabled:hover { + background: #f8f9ff; + } +` + +const CardBody = styled.div` + padding: 10px 6px; + display: flex; + gap: 6px; + align-items: center; + + > * { + min-width: 0; + } +` + +const ThumbnailGroup = styled.div` + position: relative; + width: 32px; + height: 32px; + flex-shrink: 0; +` + +const Thumbnail = styled.div` + border: 1px solid #eceef0; + border-radius: 99em; + overflow: hidden; + outline: none; + transition: border-color 200ms; + + &:focus, + &:hover { + border-color: #d0d5dd; + } + + img { + object-fit: cover; + width: 100%; + height: 100%; + } +` + +const ThumbnailMini = styled.div` + display: block; + width: 16px; + height: 16px; + flex-shrink: 0; + border: 1px solid #eceef0; + border-radius: 4px; + overflow: hidden; + outline: none; + transition: border-color 200ms; + position: absolute; + bottom: 0; + right: 0; + + &:focus, + &:hover { + border-color: #d0d5dd; + } + + img { + object-fit: cover; + vertical-align: unset; + width: 100%; + height: 100%; + } +` + +const CardContent = styled.div` + width: 100%; +` + +const TextLink = styled.div<{ bold?: boolean; small?: boolean; ellipsis?: boolean }>` + display: block; + margin: 0; + font-size: 14px; + line-height: 18px; + color: ${(p) => (p.bold ? '#11181C !important' : '#687076 !important')}; + font-weight: ${(p) => (p.bold ? '600' : '400')}; + font-size: ${(p) => (p.small ? '12px' : '14px')}; + overflow: ${(p) => (p.ellipsis ? 'hidden' : 'visible')}; + text-overflow: ${(p) => (p.ellipsis ? 'ellipsis' : 'unset')}; + white-space: nowrap; + outline: none; +` + +const ButtonLink = styled.button` + padding: 8px; + cursor: pointer; + text-decoration: none; + outline: none; + border: none; + background: inherit; + &:hover, + &:focus { + text-decoration: none; + outline: none; + border: none; + background: inherit; + } + &.disabled { + cursor: default; + } +` + +const CheckedIcon = () => ( + + + +) + +const FALLBACK_IMAGE_URL = + 'https://ipfs.near.social/ipfs/bafkreifc4burlk35hxom3klq4mysmslfirj7slueenbj7ddwg7pc6ixomu' + +export interface Props { + src: string | null + metadata: DocumentMetadata | null + onChange: () => void + disabled: boolean + appMetadata: AppMetadata['metadata'] +} + +export const DocumentCard: React.FC = ({ + src, + metadata, + onChange, + disabled, + appMetadata, +}) => { + const srcParts = src?.split('/') + + return ( + + + + + {metadata?.name} + + + {appMetadata.name} + + + + + + {metadata?.name || (srcParts && srcParts[2]) || 'New Document'} + + + + {srcParts && `@${srcParts[0]}`} + + + + + + + + ) +} diff --git a/apps/extension/src/contentscript/multitable-panel/components/documents-modal.tsx b/apps/extension/src/contentscript/multitable-panel/components/documents-modal.tsx new file mode 100644 index 00000000..5e77704f --- /dev/null +++ b/apps/extension/src/contentscript/multitable-panel/components/documents-modal.tsx @@ -0,0 +1,243 @@ +import { Document } from '@mweb/engine' +import React, { FC, useState } from 'react' +import styled from 'styled-components' +import { SimpleApplicationCard } from './application-card' +import { Button } from './button' +import { MinusCircle, PlusCircle } from '../assets/vectors' + +const Wrapper = styled.div` + position: absolute; + z-index: 3; + top: calc(50% - 10px); + transform: translateY(-50%); + left: 0; + width: calc(100% - 20px); + max-height: calc(100% - 20px); + margin: 10px; + padding: 10px; + border: 1px solid #000; + border-radius: 10px; + display: flex; + flex-direction: column; + gap: 10px; + font-family: sans-serif; + background: #f8f9ff; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', + 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; +` + +const Close = styled.span` + cursor: pointer; + svg { + margin: 0; + width: 23px; + height: 23px; + + path { + stroke: #838891; + } + } + &:hover { + opacity: 0.5; + } +` + +const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + color: rgba(2, 25, 58, 1); + font-size: 14px; + font-weight: 600; + line-height: 21.09px; + text-align: left; + gap: 20px; + + .edit { + margin-right: auto; + margin-bottom: 2px; + } +` + +const Title = styled.div` + color: #02193a; +` + +const AppsList = styled.div` + overflow: hidden; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px; + background: white; + border-radius: 10px; + overscroll-behavior: contain; + + &::-webkit-scrollbar { + cursor: pointer; + width: 4px; + } + + &::-webkit-scrollbar-track { + background: rgb(244 244 244); + background: linear-gradient( + 90deg, + rgb(244 244 244 / 0%) 10%, + rgb(227 227 227 / 100%) 50%, + rgb(244 244 244 / 0%) 90% + ); + } + + &::-webkit-scrollbar-thumb { + width: 4px; + height: 2px; + background: #384bff; + border-radius: 2px; + box-shadow: 0 2px 6px rgb(0 0 0 / 9%), 0 2px 2px rgb(38 117 209 / 4%); + } +` + +const InlineButton = styled.button` + align-self: center; + width: fit-content; + border: none; + display: flex; + gap: 5px; + background: none; + color: #384bff; + font-size: 12px; + font-weight: 400; + line-height: 150%; + text-decoration: none; + cursor: pointer; + &:hover { + opacity: 0.5; + } +` + +const ButtonsBlock = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +` + +const CloseIcon = () => ( + + + + +) + +export interface Props { + docs: Document[] | null + chosenDocumentsIds: (string | null)[] + setDocumentsIds: (ids: (string | null)[]) => void + onClose: () => void +} + +export const DocumentsModal: FC = ({ + docs, + chosenDocumentsIds, + setDocumentsIds, + onClose, +}) => { + const [chosenDocsIds, setChosenDocsIds] = useState<(string | null)[]>(chosenDocumentsIds) + + const handleDocCheckboxChange = (id: string | null) => + setChosenDocsIds((val) => + chosenDocsIds.includes(id) ? val.filter((docId) => docId !== id) : [...val, id] + ) + + return ( + +
+ Select your guide + + + +
+ + handleDocCheckboxChange(null)}> + {chosenDocsIds.includes(null) ? ( + <> + + Delete document builder + + ) : ( + <> + + Create from scratch + + )} + + + + {docs?.map((doc) => ( + handleDocCheckboxChange(doc.id)} + disabled={false} + iconShape="circle" + textColor="#4E5E76" + backgroundColor="#F8F9FF" + /> + ))} + + + + + + +
+ ) +} + +const hasArrayTheSameData = (a: (string | null)[], b: (string | null)[]) => { + if (a.length !== b.length) { + return false + } + + const aMap = new Map() + const bMap = new Map() + + for (const item of a) { + aMap.set(item, (aMap.get(item) ?? 0) + 1 || 1) + } + + for (const item of b) { + bMap.set(item, (bMap.get(item) ?? 0) + 1 || 1) + } + + for (const [key, value] of aMap) { + if (bMap.get(key) !== value) { + return false + } + } + + return true +} diff --git a/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx b/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx index ea610490..0037cc34 100644 --- a/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx +++ b/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx @@ -1,5 +1,6 @@ import { AppMetadata, + Document, Mutation, useCreateMutation, useEditMutation, @@ -19,11 +20,12 @@ import { } from '../../helpers' import { useEscape } from '../../hooks/use-escape' import { Alert, AlertProps } from './alert' -import { ApplicationCard } from './application-card' +import { ApplicationCardWithDocs, SimpleApplicationCard } from './application-card' import { Button } from './button' import { DropdownButton } from './dropdown-button' import { Input } from './input' import { InputImage } from './upload-image' +import { DocumentsModal } from './documents-modal' const SelectedMutationEditorWrapper = styled.div` display: flex; @@ -40,6 +42,8 @@ const SelectedMutationEditorWrapper = styled.div` background: #f8f9ff; width: 400px; max-height: 70vh; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', + 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; ` const Close = styled.span` @@ -80,6 +84,8 @@ const AppsList = styled.div` display: flex; flex-direction: column; gap: 5px; + overscroll-behavior: contain; + &::-webkit-scrollbar { cursor: pointer; width: 4px; @@ -112,6 +118,18 @@ const ButtonsBlock = styled.div` align-items: center; ` +const BlurredBackground = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgb(255 255 255 / 75%); + backdrop-filter: blur(5px); + border-radius: 9px; + z-index: 3; +` + const CloseIcon = () => ( = ({ baseMutation, apps, onClose }) const { editMutation, isLoading: isEditing } = useEditMutation() const { mutations } = useMutableWeb() const [isModified, setIsModified] = useState(true) + const [appIdToOpenDocsModal, setAppIdToOpenDocsModal] = useState(null) + const [docsForModal, setDocsForModal] = useState(null) // Close modal with escape key useEscape(onClose) @@ -291,7 +311,38 @@ export const MutationEditorModal: FC = ({ baseMutation, apps, onClose }) const handleAppCheckboxChange = (appId: string, checked: boolean) => { setEditingMutation((mut) => { - const apps = checked ? [...mut.apps, appId] : mut.apps.filter((app) => app !== appId) + const apps = checked + ? [...mut.apps, { appId, documentId: null }] + : mut.apps.filter((app) => app.appId !== appId) + return mergeDeep(cloneDeep(mut), { apps }) + }) + } + + const handleDocCheckboxChange = (docId: string | null, appId: string, checked: boolean) => { + setEditingMutation((mut) => { + const apps = checked + ? [...mut.apps, { appId, documentId: docId }] + : mut.apps.filter((app) => app.appId !== appId || app.documentId !== docId) + return mergeDeep(cloneDeep(mut), { apps }) + }) + } + + const handleDocCheckboxBanchChange = (docIds: (string | null)[], appId: string) => { + setEditingMutation((mut) => { + const docIdsToAdd = new Set(docIds) + const apps = mut.apps.filter((_app) => { + if (_app.appId === appId) { + if (docIdsToAdd.has(_app.documentId)) { + docIdsToAdd.delete(_app.documentId) + } else { + return false + } + } + return true + }) + docIdsToAdd.forEach((docId) => { + apps.push({ appId, documentId: docId }) + }) return mergeDeep(cloneDeep(mut), { apps }) }) } @@ -335,6 +386,11 @@ export const MutationEditorModal: FC = ({ baseMutation, apps, onClose }) setMode(itemId as MutationModalMode) } + const handleOpenDocumentsModal = (appId: string, docs: Document[]) => { + setAppIdToOpenDocsModal(appId) + setDocsForModal(docs) + } + return ( @@ -370,16 +426,32 @@ export const MutationEditorModal: FC = ({ baseMutation, apps, onClose }) /> - {apps.map((app) => ( - handleAppCheckboxChange(app.id, val)} - disabled={isFormDisabled} - /> - ))} + {apps.map((app) => + app.permissions.documents ? ( + _app.appId === app.id) + .map((_app) => _app.documentId)} + onOpenDocumentsModal={(docs: Document[]) => handleOpenDocumentsModal(app.id, docs)} + onDocCheckboxChange={(docId: string | null, isChecked: boolean) => + handleDocCheckboxChange(docId, app.id, isChecked) + } + /> + ) : ( + _app.appId === app.id)} + onChange={(val) => handleAppCheckboxChange(app.id, val)} + /> + ) + )} @@ -406,6 +478,22 @@ export const MutationEditorModal: FC = ({ baseMutation, apps, onClose }) )} + + {appIdToOpenDocsModal ? ( + <> + + setAppIdToOpenDocsModal(null)} + chosenDocumentsIds={editingMutation.apps + .filter((_app) => _app.appId === appIdToOpenDocsModal) + .map((_app) => _app.documentId)} + setDocumentsIds={(val: (string | null)[]) => + handleDocCheckboxBanchChange(val, appIdToOpenDocsModal) + } + /> + + ) : null} ) } diff --git a/apps/extension/src/contentscript/multitable-panel/mutable-overlay-container.tsx b/apps/extension/src/contentscript/multitable-panel/mutable-overlay-container.tsx index 98b4f6de..2746791e 100644 --- a/apps/extension/src/contentscript/multitable-panel/mutable-overlay-container.tsx +++ b/apps/extension/src/contentscript/multitable-panel/mutable-overlay-container.tsx @@ -1,11 +1,11 @@ -import { AppWithSettings, useMutableWeb, useMutationApp } from '@mweb/engine' +import { AppInstanceWithSettings, useMutableWeb, useMutationApp } from '@mweb/engine' import { AppSwitcher, MiniOverlay } from '@mweb/shared-components' import React from 'react' import Background from '../../common/background' import { NearNetworkId } from '../../common/networks' -function AppSwitcherContainer({ app }: { app: AppWithSettings }) { - const { enableApp, disableApp, isLoading } = useMutationApp(app.id) +function AppSwitcherContainer({ app }: { app: AppInstanceWithSettings }) { + const { enableApp, disableApp, isLoading } = useMutationApp(app.instanceId) return ( ) @@ -32,7 +32,7 @@ function MutableOverlayContainer({ > <> {mutationApps.map((app) => ( - + ))} diff --git a/apps/gateway/src/components/navigation/MutableOverlayContainer.js b/apps/gateway/src/components/navigation/MutableOverlayContainer.js index 8ed9743a..8e88ab32 100644 --- a/apps/gateway/src/components/navigation/MutableOverlayContainer.js +++ b/apps/gateway/src/components/navigation/MutableOverlayContainer.js @@ -4,7 +4,7 @@ import { MiniOverlay, AppSwitcher } from '@mweb/shared-components' function AppSwitcherContainer({ app }) { // ToDo: move to @mweb/engine - const { enableApp, disableApp, isLoading } = useMutationApp(app.id) + const { enableApp, disableApp, isLoading } = useMutationApp(app.instanceId) return ( ) diff --git a/libs/engine/src/app/common/merge-deep.ts b/libs/engine/src/app/common/merge-deep.ts new file mode 100644 index 00000000..c6000d3e --- /dev/null +++ b/libs/engine/src/app/common/merge-deep.ts @@ -0,0 +1,31 @@ +/** + * Simple object check. + * @param item + * @returns {boolean} + */ +function isObject(item: any): boolean { + return item && typeof item === 'object' && !Array.isArray(item) +} + +/** + * Deep merge two objects. + * @param target + * @param ...sources + */ +export function mergeDeep(target: any, ...sources: any[]) { + if (!sources.length) return target + const source = sources.shift() + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }) + mergeDeep(target[key], source[key]) + } else { + Object.assign(target, { [key]: source[key] }) + } + } + } + + return mergeDeep(target, ...sources) +} diff --git a/libs/engine/src/app/components/context-manager.tsx b/libs/engine/src/app/components/context-manager.tsx index 0f32aadb..248a152b 100644 --- a/libs/engine/src/app/components/context-manager.tsx +++ b/libs/engine/src/app/components/context-manager.tsx @@ -9,7 +9,12 @@ import { ContextTree } from '@mweb/react' import { useContextApps } from '../contexts/mutable-web-context/use-context-apps' import { useAppControllers } from '../contexts/mutable-web-context/use-app-controllers' import { AppId, AppMetadata } from '../services/application/application.entity' -import { BosUserLink, ControllerLink, UserLinkId } from '../services/user-link/user-link.entity' +import { + BosUserLink, + BosUserLinkWithInstance, + ControllerLink, + UserLinkId, +} from '../services/user-link/user-link.entity' import { TransferableContext, buildTransferableContext } from '../common/transferable-context' import { useModal } from '../contexts/modal-context' import { useMutableWeb } from '../contexts/mutable-web-context' @@ -22,6 +27,8 @@ import { ModalProps } from '../contexts/modal-context/modal-context' import { Portal } from '../contexts/engine-context/engine-context' import { Target } from '../services/target/target.entity' import { filterAndDiscriminate } from '../common/filter-and-discriminate' +import { Document, DocumentId, DocumentMetadata } from '../services/document/document.entity' +import { ApplicationService } from '../services/application/application.service' interface WidgetProps { context: TransferableContext @@ -43,6 +50,13 @@ interface WidgetProps { indexRules: LinkIndexRules ) => Promise } + commitDocument: ( + appDocId: DocumentId, + appDocMeta: DocumentMetadata, + ctx: TransferableContext, + dataByAccount: LinkedDataByAccount + ) => Promise + getDocument: () => Promise } interface LayoutManagerProps { @@ -87,7 +101,7 @@ const ContextHandler: FC<{ context: IContextNode; insPoints: InsertionPointWithE const { controllers } = useAppControllers(context) const { links, createUserLink, deleteUserLink } = useUserLinks(context) const { apps } = useContextApps(context) - const { engine, selectedMutation } = useMutableWeb() + const { engine, selectedMutation, refreshMutation, activeApps } = useMutableWeb() const { portals } = useEngine() const portalComponents = useMemo(() => { @@ -156,10 +170,22 @@ const ContextHandler: FC<{ context: IContextNode; insPoints: InsertionPointWithE // Move to a separate hook when App wrapper is ready const handleGetLinkDataCurry = useCallback( memoize( - (appId: AppId) => + (appInstanceId: string) => (ctx: TransferableContext, accountIds?: string[] | string, indexRules?: LinkIndexRules) => { if (!selectedMutation) throw new Error('No selected mutation') - return engine.linkDbService.get(selectedMutation.id, appId, ctx, accountIds, indexRules) + const appInstance = selectedMutation.apps.find( + (app) => ApplicationService.constructAppInstanceId(app) === appInstanceId + ) + if (!appInstance) throw new Error('The app is not active') + + return engine.linkDbService.get( + selectedMutation.id, + appInstance.appId, + appInstance.documentId, + ctx, + accountIds, + indexRules + ) } ), [engine, selectedMutation] @@ -167,16 +193,22 @@ const ContextHandler: FC<{ context: IContextNode; insPoints: InsertionPointWithE const handleSetLinkDataCurry = useCallback( memoize( - (appId: AppId) => + (appInstanceId: string) => ( ctx: TransferableContext, dataByAccount: LinkedDataByAccount, indexRules: LinkIndexRules ) => { if (!selectedMutation) throw new Error('No selected mutation') + const appInstance = selectedMutation.apps.find( + (app) => ApplicationService.constructAppInstanceId(app) === appInstanceId + ) + if (!appInstance) throw new Error('The app is not active') + return engine.linkDbService.set( selectedMutation.id, - appId, + appInstance.appId, + appInstance.documentId, ctx, dataByAccount, indexRules @@ -186,6 +218,61 @@ const ContextHandler: FC<{ context: IContextNode; insPoints: InsertionPointWithE [engine, selectedMutation] ) + const handleGetDocumentCurry = useCallback( + memoize((appInstanceId: string) => async () => { + if (!selectedMutation) throw new Error('No selected mutation') + const appInstance = selectedMutation.apps.find( + (app) => ApplicationService.constructAppInstanceId(app) === appInstanceId + ) + if (!appInstance) throw new Error('The app is not active') + + if (!appInstance.documentId) return null + + const document = await engine.documentService.getDocument(appInstance.documentId) + + return document + }), + [engine, selectedMutation, refreshMutation] + ) + + const handleCommitDocumentCurry = useCallback( + memoize( + (appInstanceId: string) => + async ( + appDocId: DocumentId, + appDocMeta: DocumentMetadata, + ctx: TransferableContext, + dataByAccount: LinkedDataByAccount + ) => { + if (!selectedMutation) throw new Error('No selected mutation') + const appInstance = selectedMutation.apps.find( + (app) => ApplicationService.constructAppInstanceId(app) === appInstanceId + ) + if (!appInstance) throw new Error('The app is not active') + + const document = { + id: appDocId, + metadata: appDocMeta, + openWith: [appInstance.appId], + } + + const { mutation } = await engine.documentService.createDocumentWithData( + selectedMutation.id, + appInstance.appId, + document, + ctx, + dataByAccount + ) + + // ToDo: workaround to wait when blockchain changes will be propagated + await new Promise((resolve) => setTimeout(resolve, 3000)) + + await refreshMutation(mutation) + } + ), + [engine, selectedMutation, refreshMutation] + ) + // ToDo: check context.element return ( @@ -210,6 +297,8 @@ const ContextHandler: FC<{ context: IContextNode; insPoints: InsertionPointWithE onAttachContextRef={attachContextRef} onGetLinkDataCurry={handleGetLinkDataCurry} onSetLinkDataCurry={handleSetLinkDataCurry} + onCommitDocumentCurry={handleCommitDocumentCurry} + onGetDocumentCurry={handleGetDocumentCurry} /> ))} @@ -230,6 +319,8 @@ const ContextHandler: FC<{ context: IContextNode; insPoints: InsertionPointWithE onAttachContextRef={attachContextRef} onGetLinkDataCurry={handleGetLinkDataCurry} onSetLinkDataCurry={handleSetLinkDataCurry} + onCommitDocumentCurry={handleCommitDocumentCurry} + onGetDocumentCurry={handleGetDocumentCurry} /> {controllers.map((c) => ( @@ -240,6 +331,8 @@ const ContextHandler: FC<{ context: IContextNode; insPoints: InsertionPointWithE onContextQuery={handleContextQuery} onGetLinkDataCurry={handleGetLinkDataCurry} onSetLinkDataCurry={handleSetLinkDataCurry} + onCommitDocumentCurry={handleCommitDocumentCurry} + onGetDocumentCurry={handleGetDocumentCurry} /> ))} @@ -262,7 +355,7 @@ const InsPointHandler: FC<{ bosLayoutManager?: string context: IContextNode transferableContext: TransferableContext - allUserLinks: BosUserLink[] + allUserLinks: BosUserLinkWithInstance[] components: Portal[] apps: AppMetadata[] isEditMode: boolean @@ -273,19 +366,28 @@ const InsPointHandler: FC<{ onDisableEditMode: () => void onAttachContextRef: (callback: (r: React.Component | Element | null | undefined) => void) => void onGetLinkDataCurry: ( - appId: string + appInstanceId: string ) => ( ctx: TransferableContext, accountIds?: string[] | string, indexRules?: LinkIndexRules ) => Promise onSetLinkDataCurry: ( - appId: string + appInstanceId: string ) => ( ctx: TransferableContext, dataByAccount: LinkedDataByAccount, indexRules: LinkIndexRules ) => Promise + onCommitDocumentCurry: ( + appInstanceId: string + ) => ( + appDocId: DocumentId, + appDocMetadata: DocumentMetadata, + ctx: TransferableContext, + dataByAccount: LinkedDataByAccount + ) => Promise + onGetDocumentCurry: (appInstanceId: string) => () => Promise }> = ({ insPointName, element, @@ -304,6 +406,8 @@ const InsPointHandler: FC<{ onAttachContextRef, onGetLinkDataCurry, onSetLinkDataCurry, + onCommitDocumentCurry, + onGetDocumentCurry, }) => { const { redirectMap, isDevServerLoading } = useEngine() const { config, engine } = useMutableWeb() @@ -379,9 +483,12 @@ const InsPointHandler: FC<{ }, notify, linkDb: { - get: onGetLinkDataCurry(link.appId), - set: onSetLinkDataCurry(link.appId), + // ToDo: which instance id should be used for user links? + get: onGetLinkDataCurry(link.appInstanceId), + set: onSetLinkDataCurry(link.appInstanceId), }, + commitDocument: onCommitDocumentCurry(link.appInstanceId), + getDocument: onGetDocumentCurry(link.appInstanceId), }, // ToDo: add props isSuitable: link.insertionPoint === insPointName, // ToDo: LM know about widgets from other LM })), @@ -432,25 +539,36 @@ const ControllerHandler: FC<{ controller: ControllerLink onContextQuery: (target: Target) => TransferableContext | null onGetLinkDataCurry: ( - appId: string + appInstanceId: string ) => ( ctx: TransferableContext, accountIds?: string[] | string, indexRules?: LinkIndexRules ) => Promise onSetLinkDataCurry: ( - appId: string + appInstanceId: string ) => ( ctx: TransferableContext, dataByAccount: LinkedDataByAccount, indexRules: LinkIndexRules ) => Promise + onCommitDocumentCurry: ( + appInstanceId: string + ) => ( + appDocId: DocumentId, + appDocMetadata: DocumentMetadata, + ctx: TransferableContext, + dataByAccount: LinkedDataByAccount + ) => Promise + onGetDocumentCurry: (appInstanceId: string) => () => Promise }> = ({ transferableContext, controller, onContextQuery, onGetLinkDataCurry, onSetLinkDataCurry, + onCommitDocumentCurry, + onGetDocumentCurry, }) => { const { redirectMap, isDevServerLoading } = useEngine() const { notify } = useModal() @@ -464,9 +582,11 @@ const ControllerHandler: FC<{ query: onContextQuery, notify, linkDb: { - get: onGetLinkDataCurry(controller.appId), - set: onSetLinkDataCurry(controller.appId), + get: onGetLinkDataCurry(controller.appInstanceId), + set: onSetLinkDataCurry(controller.appInstanceId), }, + commitDocument: onCommitDocumentCurry(controller.appInstanceId), + getDocument: onGetDocumentCurry(controller.appInstanceId), } return ( diff --git a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-context.tsx b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-context.tsx index 89eb6693..c463115f 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-context.tsx +++ b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-context.tsx @@ -1,7 +1,7 @@ import { Engine } from '../../../engine' import { createContext } from 'react' -import { AppMetadata, AppWithSettings } from '../../services/application/application.entity' -import { MutationWithSettings } from '../../services/mutation/mutation.entity' +import { AppMetadata, AppInstanceWithSettings } from '../../services/application/application.entity' +import { Mutation, MutationWithSettings } from '../../services/mutation/mutation.entity' import { NearConfig } from '../../../constants' export type MutableWebContextState = { @@ -9,16 +9,17 @@ export type MutableWebContextState = { engine: Engine mutations: MutationWithSettings[] allApps: AppMetadata[] - mutationApps: AppWithSettings[] - activeApps: AppWithSettings[] + mutationApps: AppInstanceWithSettings[] + activeApps: AppInstanceWithSettings[] selectedMutation: MutationWithSettings | null + refreshMutation: (mutation: Mutation) => Promise isLoading: boolean switchMutation: (mutationId: string | null) => void favoriteMutationId: string | null setFavoriteMutation: (mutationId: string | null) => void removeMutationFromRecents: (mutationId: string) => void setMutations: React.Dispatch> - setMutationApps: React.Dispatch> + setMutationApps: React.Dispatch> } export const contextDefaultValues: MutableWebContextState = { @@ -31,6 +32,7 @@ export const contextDefaultValues: MutableWebContextState = { isLoading: false, selectedMutation: null, switchMutation: () => undefined, + refreshMutation: () => Promise.resolve(undefined), favoriteMutationId: null, setFavoriteMutation: () => undefined, removeMutationFromRecents: () => undefined, diff --git a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx index 56f23560..fbd410d1 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx +++ b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx @@ -20,6 +20,7 @@ import { AdapterType, ParserConfig } from '../../services/parser-config/parser-c import { mutationDisabled, mutationSwitched } from './notifications' import { getNearConfig } from '../../../constants' import { ModalContextState } from '../modal-context/modal-context' +import { Mutation } from '../../services/mutation/mutation.entity' type Props = { config: EngineConfig @@ -191,6 +192,12 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch [selectedMutationId] ) + const refreshMutation = useCallback(async (mutation: Mutation) => { + const mutationWithSettings = await engine.mutationService.populateMutationWithSettings(mutation) + + setMutations((prev) => prev.map((mut) => (mut.id === mutation.id ? mutationWithSettings : mut))) + }, []) + // ToDo: move to separate hook const setFavoriteMutation = useCallback( async (mutationId: string | null) => { @@ -235,6 +242,7 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch selectedMutation, isLoading, switchMutation, + refreshMutation, setFavoriteMutation, removeMutationFromRecents, favoriteMutationId, diff --git a/libs/engine/src/app/contexts/mutable-web-context/use-app-documents.ts b/libs/engine/src/app/contexts/mutable-web-context/use-app-documents.ts new file mode 100644 index 00000000..0471517e --- /dev/null +++ b/libs/engine/src/app/contexts/mutable-web-context/use-app-documents.ts @@ -0,0 +1,38 @@ +import { useCallback, useEffect, useState } from 'react' +import { AppId } from '../../services/application/application.entity' +import { Document } from '../../services/document/document.entity' +import { useMutableWeb } from './use-mutable-web' + +export const useAppDocuments = (appId: AppId) => { + const { engine } = useMutableWeb() + const [documents, setDocuments] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const loadDocuments = useCallback(async () => { + try { + setIsLoading(true) + + const documents = await engine.documentService.getDocumentsByAppId(appId) + setDocuments(documents) + } catch (err) { + if (err instanceof Error) { + setError(err.message) + } else { + setError('Unknown error') + } + } finally { + setIsLoading(false) + } + }, [engine, appId]) + + useEffect(() => { + loadDocuments() + }, [loadDocuments]) + + return { + documents, + isLoading, + error, + } +} diff --git a/libs/engine/src/app/contexts/mutable-web-context/use-mutation-app.ts b/libs/engine/src/app/contexts/mutable-web-context/use-mutation-app.ts index 1f3bbad7..6b34282e 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/use-mutation-app.ts +++ b/libs/engine/src/app/contexts/mutable-web-context/use-mutation-app.ts @@ -1,7 +1,7 @@ import { useContext, useState } from 'react' import { MutableWebContext } from './mutable-web-context' -export function useMutationApp(appId: string) { +export function useMutationApp(appInstanceId: string) { const { engine, setMutationApps, selectedMutation } = useContext(MutableWebContext) const [isLoading, setIsLoading] = useState(false) @@ -15,11 +15,16 @@ export function useMutationApp(appId: string) { try { setIsLoading(true) - await engine.applicationService.enableAppInMutation(selectedMutation.id, appId) + await engine.applicationService.enableAppInstanceInMutation( + selectedMutation.id, + appInstanceId + ) setMutationApps((apps) => apps.map((app) => - app.id === appId ? { ...app, settings: { ...app.settings, isEnabled: true } } : app + app.instanceId === appInstanceId + ? { ...app, settings: { ...app.settings, isEnabled: true } } + : app ) ) } catch (err) { @@ -41,11 +46,16 @@ export function useMutationApp(appId: string) { try { setIsLoading(true) - await engine.applicationService.disableAppInMutation(selectedMutation.id, appId) + await engine.applicationService.disableAppInstanceInMutation( + selectedMutation.id, + appInstanceId + ) setMutationApps((apps) => apps.map((app) => - app.id === appId ? { ...app, settings: { ...app.settings, isEnabled: false } } : app + app.instanceId === appInstanceId + ? { ...app, settings: { ...app.settings, isEnabled: false } } + : app ) ) } catch (err) { diff --git a/libs/engine/src/app/contexts/mutable-web-context/use-mutation-apps.ts b/libs/engine/src/app/contexts/mutable-web-context/use-mutation-apps.ts index e68cb691..3cf18fe3 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/use-mutation-apps.ts +++ b/libs/engine/src/app/contexts/mutable-web-context/use-mutation-apps.ts @@ -1,10 +1,10 @@ import { useCallback, useEffect, useState } from 'react' -import { AppWithSettings } from '../../services/application/application.entity' +import { AppInstanceWithSettings } from '../../services/application/application.entity' import { Mutation } from '../../services/mutation/mutation.entity' import { Engine } from '../../../engine' export const useMutationApps = (engine: Engine, mutation?: Mutation | null) => { - const [mutationApps, setMutationApps] = useState([]) + const [mutationApps, setMutationApps] = useState([]) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) @@ -17,18 +17,7 @@ export const useMutationApps = (engine: Engine, mutation?: Mutation | null) => { try { setIsLoading(true) - // ToDo: move to service - const apps = await Promise.all( - mutation.apps.map((appId) => - engine.applicationService - .getApplication(appId) - .then((appMetadata) => - appMetadata - ? engine.applicationService.populateAppWithSettings(mutation.id, appMetadata) - : null - ) - ) - ).then((apps) => apps.filter((app) => app !== null) as AppWithSettings[]) + const apps = await engine.applicationService.getAppsFromMutation(mutation) setMutationApps(apps) } catch (err) { diff --git a/libs/engine/src/app/contexts/mutable-web-context/use-user-links.ts b/libs/engine/src/app/contexts/mutable-web-context/use-user-links.ts index 2f633741..4577d6a9 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/use-user-links.ts +++ b/libs/engine/src/app/contexts/mutable-web-context/use-user-links.ts @@ -1,21 +1,22 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { IContextNode } from '@mweb/core' -import { BosUserLink, UserLinkId } from '../../services/user-link/user-link.entity' +import { BosUserLinkWithInstance, UserLinkId } from '../../services/user-link/user-link.entity' import { useMutableWeb } from '.' import { AppId } from '../../services/application/application.entity' // Reuse reference to empty array to avoid unnecessary re-renders -const NoLinks: BosUserLink[] = [] +const NoLinks: BosUserLinkWithInstance[] = [] export const useUserLinks = (context: IContextNode) => { const { engine, selectedMutation, activeApps } = useMutableWeb() - const [userLinks, setUserLinks] = useState([]) + const [userLinks, setUserLinks] = useState([]) const [error, setError] = useState(null) const staticLinks = useMemo(() => { if (!engine || !selectedMutation?.id) { return [] } else { + // ToDo: the service should not know about instances return engine.userLinkService.getStaticLinksForApps(activeApps, context) } }, [engine, selectedMutation, activeApps, context.parsedContext, context.isVisible]) @@ -27,6 +28,7 @@ export const useUserLinks = (context: IContextNode) => { } try { + // ToDo: the service should not know about instances const links = await engine.userLinkService.getLinksForContext( activeApps, selectedMutation.id, @@ -46,7 +48,7 @@ export const useUserLinks = (context: IContextNode) => { fetchUserLinks() }, [fetchUserLinks]) - const links = useMemo(() => { + const links: BosUserLinkWithInstance[] = useMemo(() => { return userLinks.length || staticLinks.length ? [...userLinks, ...staticLinks] : NoLinks }, [userLinks, staticLinks]) @@ -56,6 +58,13 @@ export const useUserLinks = (context: IContextNode) => { throw new Error('No mutation selected') } + // All app instances with that id + const appInstances = activeApps.filter((app) => app.id === appId) + + if (appInstances.length === 0) { + throw new Error('The app is not active') + } + try { const createdLink = await engine.userLinkService.createLink( selectedMutation.id, @@ -63,7 +72,13 @@ export const useUserLinks = (context: IContextNode) => { context ) - setUserLinks((prev) => [...prev, createdLink]) + // ToDo: should we allow to run multiple instances for user link apps? + const linkWithInstance = appInstances.map((instance) => ({ + ...createdLink, + appInstanceId: instance.instanceId, + })) + + setUserLinks((prev) => [...prev, ...linkWithInstance]) } catch (err) { console.error(err) } diff --git a/libs/engine/src/app/services/application/application.entity.ts b/libs/engine/src/app/services/application/application.entity.ts index 5aaa8feb..b76aee95 100644 --- a/libs/engine/src/app/services/application/application.entity.ts +++ b/libs/engine/src/app/services/application/application.entity.ts @@ -1,7 +1,9 @@ +import { DocumentId } from '../document/document.entity' import { ParserConfigId } from '../parser-config/parser-config.entity' import { Target } from '../target/target.entity' export type AppId = string +export type AppInstanceId = string export const AnyParserValue = 'any' @@ -26,10 +28,20 @@ export type AppMetadata = { ipfs_cid?: string } } + permissions: { + documents: boolean + } +} + +export type AppInstanceSettings = { + isEnabled: boolean } export type AppWithSettings = AppMetadata & { - settings: { - isEnabled: boolean - } + settings: AppInstanceSettings +} + +export type AppInstanceWithSettings = AppWithSettings & { + instanceId: string + documentId: DocumentId | null } diff --git a/libs/engine/src/app/services/application/application.repository.ts b/libs/engine/src/app/services/application/application.repository.ts index 1973c1e4..38c9d112 100644 --- a/libs/engine/src/app/services/application/application.repository.ts +++ b/libs/engine/src/app/services/application/application.repository.ts @@ -1,6 +1,6 @@ import { LocalDbService } from '../local-db/local-db.service' import { SocialDbService } from '../social-db/social-db.service' -import { AppId, AppMetadata } from './application.entity' +import { AppId, AppInstanceId, AppMetadata } from './application.entity' // SocialDB const ProjectIdKey = 'dapplets.near' @@ -32,12 +32,19 @@ export class ApplicationRepository { if (!app?.[SelfKey]) return null + const parsed = JSON.parse(app[SelfKey]) + return { - ...JSON.parse(app[SelfKey]), - metadata: app.metadata, id: globalAppId, appLocalId, authorId, + metadata: app.metadata, + controller: parsed.controller, + parsers: parsed.parsers, + targets: parsed.targets, + permissions: { + documents: parsed.permissions?.documents ?? false, + }, } } @@ -60,12 +67,19 @@ export class ApplicationRepository { const [authorId, , , , appLocalId] = key.split(KeyDelimiter) const globalAppId = [authorId, AppKey, appLocalId].join(KeyDelimiter) + const parsed = JSON.parse(app[SelfKey]) + return { - ...JSON.parse(app[SelfKey]), - metadata: app.metadata, id: globalAppId, appLocalId, authorId, + metadata: app.metadata, + controller: parsed.controller, + parsers: parsed.parsers, + targets: parsed.targets, + permissions: { + documents: parsed.permissions?.documents ?? false, + }, } }) @@ -83,6 +97,7 @@ export class ApplicationRepository { [SelfKey]: JSON.stringify({ targets: appMetadata.targets, parsers: appMetadata.parsers, + permissions: appMetadata.permissions, }), metadata: appMetadata.metadata, } @@ -96,13 +111,17 @@ export class ApplicationRepository { } } - async getAppEnabledStatus(mutationId: string, appId: string): Promise { - const key = LocalDbService.makeKey(STOPPED_APPS, mutationId, appId) + async getAppEnabledStatus(mutationId: string, appInstanceId: AppInstanceId): Promise { + const key = LocalDbService.makeKey(STOPPED_APPS, mutationId, appInstanceId) return (await this.localDb.getItem(key)) ?? true // app is active by default } - async setAppEnabledStatus(mutationId: string, appId: string, isEnabled: boolean): Promise { - const key = LocalDbService.makeKey(STOPPED_APPS, mutationId, appId) + async setAppEnabledStatus( + mutationId: string, + appInstanceId: AppInstanceId, + isEnabled: boolean + ): Promise { + const key = LocalDbService.makeKey(STOPPED_APPS, mutationId, appInstanceId) return this.localDb.setItem(key, isEnabled) } } diff --git a/libs/engine/src/app/services/application/application.service.ts b/libs/engine/src/app/services/application/application.service.ts index 09c8cbf4..eeabed40 100644 --- a/libs/engine/src/app/services/application/application.service.ts +++ b/libs/engine/src/app/services/application/application.service.ts @@ -1,7 +1,14 @@ import { IContextNode } from '@mweb/core' -import { MutationId } from '../mutation/mutation.entity' +import { AppInMutation, Mutation, MutationId } from '../mutation/mutation.entity' import { TargetService } from '../target/target.service' -import { AnyParserValue, AppId, AppMetadata, AppWithSettings } from './application.entity' +import { + AppId, + AppInstanceId, + AppInstanceSettings, + AppInstanceWithSettings, + AppMetadata, + AppWithSettings, +} from './application.entity' import { ApplicationRepository } from './application.repository' export class ApplicationService { @@ -16,8 +23,10 @@ export class ApplicationService { return this.applicationRepository.getApplication(appId) } - public async getAppEnabledStatus(mutationId: MutationId, appId: AppId): Promise { - return this.applicationRepository.getAppEnabledStatus(mutationId, appId) + public async getAppsFromMutation(mutation: Mutation): Promise { + return Promise.all( + mutation.apps.map((appInstance) => this._getAppInstanceWithSettings(mutation.id, appInstance)) + ).then((apps) => apps.filter((app) => app !== null) as AppInstanceWithSettings[]) } public filterSuitableApps(appsToCheck: AppMetadata[], context: IContextNode): AppMetadata[] { @@ -36,23 +45,44 @@ export class ApplicationService { return suitableApps } - public async enableAppInMutation(mutationId: MutationId, appId: AppId) { - await this.applicationRepository.setAppEnabledStatus(mutationId, appId, true) + public async enableAppInstanceInMutation(mutationId: MutationId, appInstanceId: AppInstanceId) { + await this.applicationRepository.setAppEnabledStatus(mutationId, appInstanceId, true) } - public async disableAppInMutation(mutationId: MutationId, appId: AppId) { - await this.applicationRepository.setAppEnabledStatus(mutationId, appId, false) + public async disableAppInstanceInMutation(mutationId: MutationId, appInstanceId: AppInstanceId) { + await this.applicationRepository.setAppEnabledStatus(mutationId, appInstanceId, false) } - public async populateAppWithSettings( + public static constructAppInstanceId({ appId, documentId }: AppInMutation): AppInstanceId { + // ToDo: instance id is a concatenation of app id and document id + return documentId ? `${appId}/${documentId}` : appId + } + + private async _getAppInstanceWithSettings(mutationId: MutationId, appInstance: AppInMutation) { + const instanceId = ApplicationService.constructAppInstanceId(appInstance) + + const [app, settings] = await Promise.all([ + this.getApplication(appInstance.appId), + this._getAppInstanceSettings(mutationId, instanceId), + ]) + + if (!app) return null + + const appWithSettings: AppWithSettings = { ...app, settings } + + return { + ...appWithSettings, + instanceId, + documentId: appInstance.documentId, + } + } + + private async _getAppInstanceSettings( mutationId: MutationId, - app: AppMetadata - ): Promise { + appInstanceId: AppInstanceId + ): Promise { return { - ...app, - settings: { - isEnabled: await this.applicationRepository.getAppEnabledStatus(mutationId, app.id), - }, + isEnabled: await this.applicationRepository.getAppEnabledStatus(mutationId, appInstanceId), } } } diff --git a/libs/engine/src/app/services/document/document.entity.ts b/libs/engine/src/app/services/document/document.entity.ts new file mode 100644 index 00000000..762b8750 --- /dev/null +++ b/libs/engine/src/app/services/document/document.entity.ts @@ -0,0 +1,19 @@ +import { AppId } from '../application/application.entity' + +export type DocumentId = string + +export type DocumentMetadata = { + name?: string + description?: string + image?: { + ipfs_cid?: string + } +} + +export type Document = { + id: DocumentId + metadata: DocumentMetadata + openWith: AppId[] + authorId: string + documentLocalId: string +} diff --git a/libs/engine/src/app/services/document/document.repository.ts b/libs/engine/src/app/services/document/document.repository.ts new file mode 100644 index 00000000..1fdfd149 --- /dev/null +++ b/libs/engine/src/app/services/document/document.repository.ts @@ -0,0 +1,105 @@ +import { AppId } from '../application/application.entity' +import { SocialDbService, Value } from '../social-db/social-db.service' +import { Document, DocumentId } from './document.entity' +import { mergeDeep } from '../../common/merge-deep' + +// SocialDB +const ProjectIdKey = 'dapplets.near' +const SettingsKey = 'settings' +const AppKey = 'app' +const DocumentKey = 'document' +const OpenWithKey = 'open_with' +const WildcardKey = '*' +const RecursiveWildcardKey = '**' +const KeyDelimiter = '/' +const EmptyValue = '' + +export class DocumentRepository { + constructor(private socialDb: SocialDbService) {} + + async getDocument(globalDocumentId: DocumentId): Promise { + const [authorId, , documentLocalId] = globalDocumentId.split(KeyDelimiter) + + const keys = [authorId, SettingsKey, ProjectIdKey, DocumentKey, documentLocalId] + const queryResult = await this.socialDb.get([ + [...keys, RecursiveWildcardKey].join(KeyDelimiter), + ]) + + const document = SocialDbService.getValueByKey(keys, queryResult) + + if (!document) return null + + const compatibleApps = SocialDbService.splitObjectByDepth(document.open_with, 3) + + return { + metadata: document.metadata, + id: globalDocumentId, + openWith: Object.keys(compatibleApps), + authorId, + documentLocalId, + } + } + + async getDocumentsByAppId(globalAppId: AppId): Promise { + const [appAuthorId, , appLocalId] = globalAppId.split(KeyDelimiter) + + const keys = [ + WildcardKey, // any author id + SettingsKey, + ProjectIdKey, + DocumentKey, + WildcardKey, // any document local id + OpenWithKey, + appAuthorId, + AppKey, + appLocalId, + ] + + const foundKeys = await this.socialDb.keys([keys.join(KeyDelimiter)]) + + const documentIds = foundKeys.map((key: string) => { + const [authorId, , , , documentLocalId] = key.split(KeyDelimiter) + return [authorId, DocumentKey, documentLocalId].join(KeyDelimiter) + }) + + const documents = await Promise.all(documentIds.map((id) => this.getDocument(id))).then( + (documents) => documents.filter((x) => x !== null) + ) + + return documents + } + + async saveDocument(document: Omit): Promise { + const [authorId, , documentLocalId] = document.id.split(KeyDelimiter) // ToDo: duplicate in prepareSaveDocument + + const preparedDocument = await this.prepareSaveDocument(document) + + await this.socialDb.set(preparedDocument) + + return { + ...document, + documentLocalId, + authorId, + } + } + + async prepareSaveDocument( + document: Omit + ): Promise { + const [authorId, , documentLocalId] = document.id.split(KeyDelimiter) + + const keys = [authorId, SettingsKey, ProjectIdKey, DocumentKey, documentLocalId] + + const storedAppMetadata = { + metadata: document.metadata, + open_with: mergeDeep( + {}, + ...document.openWith.map((appId) => + SocialDbService.buildNestedData(appId.split(KeyDelimiter), EmptyValue) + ) + ), + } + + return SocialDbService.buildNestedData(keys, storedAppMetadata) + } +} diff --git a/libs/engine/src/app/services/document/document.service.ts b/libs/engine/src/app/services/document/document.service.ts new file mode 100644 index 00000000..ae739374 --- /dev/null +++ b/libs/engine/src/app/services/document/document.service.ts @@ -0,0 +1,79 @@ +import { TransferableContext } from '../../common/transferable-context' +import { AppId } from '../application/application.entity' +import { LinkedDataByAccount } from '../link-db/link-db.entity' +import { LinkDbService } from '../link-db/link-db.service' +import { MutationId } from '../mutation/mutation.entity' +import { MutationService } from '../mutation/mutation.service' +import { SocialDbService, Value } from '../social-db/social-db.service' +import { Document, DocumentId } from './document.entity' +import { DocumentRepository } from './document.repository' + +export class DocumentSerivce { + constructor( + private documentRepository: DocumentRepository, + private linkDbService: LinkDbService, + private mutationService: MutationService, + private socialDbService: SocialDbService + ) {} + + async getDocument(globalDocumentId: DocumentId): Promise { + return this.documentRepository.getDocument(globalDocumentId) + } + + async getDocumentsByAppId(globalAppId: AppId): Promise { + return this.documentRepository.getDocumentsByAppId(globalAppId) + } + + async createDocument( + document: Omit + ): Promise { + if (await this.documentRepository.getDocument(document.id)) { + throw new Error('Document with that ID already exists') + } + + return this.documentRepository.saveDocument(document) + } + + async editMutation(document: Omit): Promise { + return this.documentRepository.saveDocument(document) + } + + async createDocumentWithData( + mutationId: MutationId, + appId: AppId, + document: Omit, + ctx: TransferableContext, + dataByAccount: LinkedDataByAccount + ) { + if (await this.documentRepository.getDocument(document.id)) { + throw new Error('Document with that ID already exists') + } + + // ToDo: move to mutation service? + + const mutation = await this.mutationService.getMutation(mutationId) + + if (!mutation) { + throw new Error('No mutation with that ID') + } + + // ToDo: handle multiple app instances + const app = mutation.apps.find((app) => app.appId === appId && app.documentId === null) + + if (!app) { + throw new Error('No app in mutation with that ID and empty document') + } + + app.documentId = document.id + + const dataToCommit = await Promise.all([ + this.documentRepository.prepareSaveDocument(document), + this.mutationService.prepareSaveMutation(mutation), + this.linkDbService.prepareSet(mutationId, appId, document.id, ctx, dataByAccount), + ]) + + await this.socialDbService.setMultiple(dataToCommit) + + return { mutation } + } +} diff --git a/libs/engine/src/app/services/link-db/link-db.entity.ts b/libs/engine/src/app/services/link-db/link-db.entity.ts index 13dccb7a..0cec582c 100644 --- a/libs/engine/src/app/services/link-db/link-db.entity.ts +++ b/libs/engine/src/app/services/link-db/link-db.entity.ts @@ -16,6 +16,7 @@ export type IndexedContext = { export type IndexObject = { appId: string + documentId?: string mutationId: string context: IndexedContext } diff --git a/libs/engine/src/app/services/link-db/link-db.service.ts b/libs/engine/src/app/services/link-db/link-db.service.ts index cad61344..1fe55173 100644 --- a/libs/engine/src/app/services/link-db/link-db.service.ts +++ b/libs/engine/src/app/services/link-db/link-db.service.ts @@ -1,11 +1,12 @@ import serializeToDeterministicJson from 'json-stringify-deterministic' -import { SocialDbService } from '../social-db/social-db.service' +import { SocialDbService, Value } from '../social-db/social-db.service' import { TransferableContext } from '../../common/transferable-context' import { AppId } from '../application/application.entity' import { MutationId } from '../mutation/mutation.entity' import { UserLinkRepository } from '../user-link/user-link.repository' import { LinkIndexRules, IndexObject, LinkedDataByAccount } from './link-db.entity' +import { DocumentId } from '../document/document.entity' const DefaultIndexRules: LinkIndexRules = { namespace: true, @@ -27,40 +28,32 @@ export class LinkDbService { async set( mutationId: MutationId, appId: AppId, + docId: DocumentId | null, context: TransferableContext, // ToDo: replace with IContextNode? dataByAccount: LinkedDataByAccount, indexRules: LinkIndexRules = DefaultIndexRules ): Promise { - const accounts = Object.keys(dataByAccount) - - // ToDo: implement multiple accounts - if (accounts.length !== 1) { - throw new Error('Only one account can be written at a time') - } - - const [accountId] = accounts - - const indexObject = LinkDbService._buildLinkIndex(mutationId, appId, indexRules, context) - const index = UserLinkRepository._hashObject(indexObject) // ToDo: the dependency is not injected - - const keys = [accountId, SettingsKey, ProjectIdKey, ContextLinkKey, index] - - const dataToStore = { - [DataKey]: serializeToDeterministicJson(dataByAccount[accountId]), - [IndexKey]: indexObject, - } + const preparedValue = await this.prepareSet( + mutationId, + appId, + docId, + context, + dataByAccount, + indexRules + ) - await this._socialDb.set(SocialDbService.buildNestedData(keys, dataToStore)) + await this._socialDb.set(preparedValue) } async get( mutationId: MutationId, appId: AppId, + docId: DocumentId | null, context: TransferableContext, accountIds: string[] | string = [WildcardKey], // from any user by default indexRules: LinkIndexRules = DefaultIndexRules // use context id as index by default ): Promise { - const indexObject = LinkDbService._buildLinkIndex(mutationId, appId, indexRules, context) + const indexObject = LinkDbService._buildLinkIndex(mutationId, appId, docId, indexRules, context) const index = UserLinkRepository._hashObject(indexObject) // ToDo: the dependency is not injected accountIds = Array.isArray(accountIds) ? accountIds : [accountIds] @@ -92,20 +85,57 @@ export class LinkDbService { return dataByAuthor } + async prepareSet( + mutationId: MutationId, + appId: AppId, + docId: DocumentId | null, + context: TransferableContext, // ToDo: replace with IContextNode? + dataByAccount: LinkedDataByAccount, + indexRules: LinkIndexRules = DefaultIndexRules + ): Promise { + const accounts = Object.keys(dataByAccount) + + // ToDo: implement multiple accounts + if (accounts.length !== 1) { + throw new Error('Only one account can be written at a time') + } + + const [accountId] = accounts + + const indexObject = LinkDbService._buildLinkIndex(mutationId, appId, docId, indexRules, context) + const index = UserLinkRepository._hashObject(indexObject) // ToDo: the dependency is not injected + + const keys = [accountId, SettingsKey, ProjectIdKey, ContextLinkKey, index] + + const dataToStore = { + [DataKey]: serializeToDeterministicJson(dataByAccount[accountId]), + [IndexKey]: indexObject, + } + + return SocialDbService.buildNestedData(keys, dataToStore) + } + static _buildLinkIndex( mutationId: MutationId, appId: AppId, + documentId: DocumentId | null, indexRules: LinkIndexRules, context: TransferableContext ): IndexObject { // MutationId is a part of the index. // It means that a data of the same application is different in different mutations - return { + const index: IndexObject = { mutationId, appId, context: LinkDbService._buildIndexedContextValues(indexRules, context), } + + if (documentId) { + index.documentId = documentId + } + + return index } static _buildIndexedContextValues(indexes: any, values: any): any { diff --git a/libs/engine/src/app/services/mutation/mutation.entity.ts b/libs/engine/src/app/services/mutation/mutation.entity.ts index fbd91a48..feabc3ad 100644 --- a/libs/engine/src/app/services/mutation/mutation.entity.ts +++ b/libs/engine/src/app/services/mutation/mutation.entity.ts @@ -1,7 +1,14 @@ +import { AppId } from '../application/application.entity' +import { DocumentId } from '../document/document.entity' import { Target } from '../target/target.entity' export type MutationId = string +export type AppInMutation = { + appId: AppId + documentId: DocumentId | null +} + export type Mutation = { id: MutationId metadata: { @@ -11,7 +18,7 @@ export type Mutation = { ipfs_cid?: string } } - apps: string[] + apps: AppInMutation[] targets: Target[] } diff --git a/libs/engine/src/app/services/mutation/mutation.repository.ts b/libs/engine/src/app/services/mutation/mutation.repository.ts index 8b276bf2..a246c4fb 100644 --- a/libs/engine/src/app/services/mutation/mutation.repository.ts +++ b/libs/engine/src/app/services/mutation/mutation.repository.ts @@ -1,6 +1,6 @@ import { LocalDbService } from '../local-db/local-db.service' -import { SocialDbService } from '../social-db/social-db.service' -import { Mutation, MutationId } from './mutation.entity' +import { SocialDbService, Value } from '../social-db/social-db.service' +import { AppInMutation, Mutation, MutationId } from './mutation.entity' // ToDo: move to repository? const ProjectIdKey = 'dapplets.near' @@ -32,10 +32,16 @@ export class MutationRepository { if (!mutation) return null + const normalizedApps: AppInMutation[] = mutation.apps + ? JSON.parse(mutation.apps).map((app: any) => + typeof app === 'string' ? { appId: app, documentId: null } : app + ) + : [] + return { id: globalMutationId, metadata: mutation.metadata, - apps: mutation.apps ? JSON.parse(mutation.apps) : [], + apps: normalizedApps, targets: mutation.targets ? JSON.parse(mutation.targets) : [], } } @@ -59,10 +65,16 @@ export class MutationRepository { const [accountId, , , , localMutationId] = key.split(KeyDelimiter) const mutationId = [accountId, MutationKey, localMutationId].join(KeyDelimiter) + const normalizedApps: AppInMutation[] = mutation.apps + ? JSON.parse(mutation.apps).map((app: any) => + typeof app === 'string' ? { appId: app, documentId: null } : app + ) + : [] + return { id: mutationId, metadata: mutation.metadata, - apps: mutation.apps ? JSON.parse(mutation.apps) : [], + apps: normalizedApps, targets: mutation.targets ? JSON.parse(mutation.targets) : [], } }) @@ -71,19 +83,27 @@ export class MutationRepository { } async saveMutation(mutation: Mutation): Promise { + const preparedMutation = await this.prepareSaveMutation(mutation) + + await this.socialDb.set(preparedMutation) + + return mutation + } + + async prepareSaveMutation(mutation: Mutation): Promise { const [authorId, , mutationLocalId] = mutation.id.split(KeyDelimiter) const keys = [authorId, SettingsKey, ProjectIdKey, MutationKey, mutationLocalId] + const denormalizedApps = mutation.apps.map((app) => (app.documentId ? app : app.appId)) + const storedAppMetadata = { metadata: mutation.metadata, targets: mutation.targets ? JSON.stringify(mutation.targets) : null, - apps: mutation.apps ? JSON.stringify(mutation.apps) : null, + apps: mutation.apps ? JSON.stringify(denormalizedApps) : null, } - await this.socialDb.set(SocialDbService.buildNestedData(keys, storedAppMetadata)) - - return mutation + return SocialDbService.buildNestedData(keys, storedAppMetadata) } async getFavoriteMutation(): Promise { diff --git a/libs/engine/src/app/services/mutation/mutation.service.ts b/libs/engine/src/app/services/mutation/mutation.service.ts index 6f793063..6acb64f1 100644 --- a/libs/engine/src/app/services/mutation/mutation.service.ts +++ b/libs/engine/src/app/services/mutation/mutation.service.ts @@ -1,7 +1,8 @@ -import { IContextNode, PureContextNode } from '@mweb/core' +import { IContextNode } from '@mweb/core' import { TargetService } from '../target/target.service' import { Mutation, MutationId, MutationWithSettings } from './mutation.entity' import { MutationRepository } from './mutation.repository' +import { Value } from '../social-db/social-db.service' export class MutationService { constructor( @@ -104,4 +105,8 @@ export class MutationService { }, } } + + async prepareSaveMutation(mutation: Mutation): Promise { + return this.mutationRepository.prepareSaveMutation(mutation) + } } diff --git a/libs/engine/src/app/services/social-db/social-db.service.ts b/libs/engine/src/app/services/social-db/social-db.service.ts index 7b9f189e..6ce66642 100644 --- a/libs/engine/src/app/services/social-db/social-db.service.ts +++ b/libs/engine/src/app/services/social-db/social-db.service.ts @@ -1,5 +1,6 @@ import Big from 'big.js' import { NearSigner } from '../near-signer/near-signer.service' +import { mergeDeep } from '../../common/merge-deep' export type StorageUsage = string @@ -177,6 +178,10 @@ export class SocialDbService { ) } + async setMultiple(data: Value[]): Promise { + return this.set(mergeDeep({}, ...data)) + } + async delete(keys: string[]): Promise { const data = await this.get(keys) const nullData = SocialDbService._nullifyData(data) diff --git a/libs/engine/src/app/services/user-link/user-link.entity.ts b/libs/engine/src/app/services/user-link/user-link.entity.ts index e527b631..469541b3 100644 --- a/libs/engine/src/app/services/user-link/user-link.entity.ts +++ b/libs/engine/src/app/services/user-link/user-link.entity.ts @@ -23,7 +23,7 @@ export type LinkIndexObject = { export type BosUserLink = { id: UserLinkId - appId: string + appId: AppId namespace: string insertionPoint: string bosWidgetId: string @@ -32,8 +32,13 @@ export type BosUserLink = { // ToDo: add props } +export type BosUserLinkWithInstance = BosUserLink & { + appInstanceId: string +} + export type ControllerLink = { id: string - appId: string + appId: AppId + appInstanceId: string bosWidgetId: string } diff --git a/libs/engine/src/app/services/user-link/user-link.service.ts b/libs/engine/src/app/services/user-link/user-link.service.ts index 72c889c5..1cfe8f8c 100644 --- a/libs/engine/src/app/services/user-link/user-link.service.ts +++ b/libs/engine/src/app/services/user-link/user-link.service.ts @@ -1,10 +1,21 @@ import { IContextNode } from '@mweb/core' -import { AppId, AppMetadata, AppMetadataTarget } from '../application/application.entity' +import { + AppId, + AppInstanceWithSettings, + AppMetadata, + AppMetadataTarget, +} from '../application/application.entity' import { ApplicationService } from '../application/application.service' import { MutationId } from '../mutation/mutation.entity' import { ScalarType, TargetCondition } from '../target/target.entity' import { TargetService } from '../target/target.service' -import { BosUserLink, ControllerLink, LinkIndexObject, UserLinkId } from './user-link.entity' +import { + BosUserLink, + BosUserLinkWithInstance, + ControllerLink, + LinkIndexObject, + UserLinkId, +} from './user-link.entity' import { UserLinkRepository } from './user-link.repository' export class UserLinkSerivce { @@ -15,11 +26,11 @@ export class UserLinkSerivce { // ToDo: replace with getAppsAndLinksForContext async getLinksForContext( - appsToCheck: AppMetadata[], + appsToCheck: AppInstanceWithSettings[], mutationId: MutationId, context: IContextNode - ): Promise { - const promises: Promise[] = [] + ): Promise { + const promises: Promise[] = [] for (const app of appsToCheck) { const suitableTargets = app.targets.filter((target) => @@ -28,7 +39,11 @@ export class UserLinkSerivce { // ToDo: batch requests suitableTargets.forEach((target) => { - promises.push(this._getUserLinksForTarget(mutationId, app.id, target, context)) + promises.push( + this._getUserLinksForTarget(mutationId, app.id, target, context).then((links) => { + return links.map((link) => ({ ...link, appInstanceId: app.instanceId })) + }) + ) }) } @@ -37,7 +52,10 @@ export class UserLinkSerivce { return appLinksNested.flat(2) } - getStaticLinksForApps(appsToCheck: AppMetadata[], context: IContextNode): BosUserLink[] { + getStaticLinksForApps( + appsToCheck: AppInstanceWithSettings[], + context: IContextNode + ): BosUserLinkWithInstance[] { return appsToCheck.flatMap((app) => app.targets .filter((target) => target.static) @@ -50,19 +68,24 @@ export class UserLinkSerivce { bosWidgetId: target.componentId, authorId: app.authorId, static: true, + appInstanceId: app.instanceId, })) ) } - getControllersForApps(appsToCheck: AppMetadata[], context: IContextNode): ControllerLink[] { + getControllersForApps( + appsToCheck: AppInstanceWithSettings[], + context: IContextNode + ): ControllerLink[] { return appsToCheck .filter((app) => app.controller) .flatMap((app) => app.targets .filter((target) => TargetService.isTargetMet(target, context)) .map((_, i) => ({ - id: `${app.id}/${i}`, // ToDo: id + id: `${app.id}/${app.instanceId}/${i}`, // ToDo: id appId: app.id, + appInstanceId: app.instanceId, bosWidgetId: app.controller!, // ! - because it's filtered above })) ) diff --git a/libs/engine/src/engine.tsx b/libs/engine/src/engine.tsx index 33d72540..101dd1e3 100644 --- a/libs/engine/src/engine.tsx +++ b/libs/engine/src/engine.tsx @@ -9,11 +9,13 @@ import { MutationRepository } from './app/services/mutation/mutation.repository' import { ApplicationRepository } from './app/services/application/application.repository' import { UserLinkRepository } from './app/services/user-link/user-link.repository' import { ParserConfigRepository } from './app/services/parser-config/parser-config.repository' +import { DocumentRepository } from './app/services/document/document.repository' import { MutationService } from './app/services/mutation/mutation.service' import { ApplicationService } from './app/services/application/application.service' import { UserLinkSerivce } from './app/services/user-link/user-link.service' import { ParserConfigService } from './app/services/parser-config/parser-config.service' import { LinkDbService } from './app/services/link-db/link-db.service' +import { DocumentSerivce } from './app/services/document/document.service' export type EngineConfig = { networkId: string @@ -32,6 +34,7 @@ export class Engine { applicationService: ApplicationService userLinkService: UserLinkSerivce parserConfigService: ParserConfigService + documentService: DocumentSerivce constructor(public readonly config: EngineConfig) { if (!this.config.storage) { @@ -48,11 +51,18 @@ export class Engine { const applicationRepository = new ApplicationRepository(socialDb, localDb) const userLinkRepository = new UserLinkRepository(socialDb, nearSigner) const parserConfigRepository = new ParserConfigRepository(socialDb) + const documentRepository = new DocumentRepository(socialDb) this.linkDbService = new LinkDbService(socialDb) this.mutationService = new MutationService(mutationRepository, nearConfig) this.applicationService = new ApplicationService(applicationRepository) this.userLinkService = new UserLinkSerivce(userLinkRepository, this.applicationService) this.parserConfigService = new ParserConfigService(parserConfigRepository) + this.documentService = new DocumentSerivce( + documentRepository, + this.linkDbService, + this.mutationService, + socialDb + ) } } diff --git a/libs/engine/src/index.ts b/libs/engine/src/index.ts index 8fc583c7..a60cedd9 100644 --- a/libs/engine/src/index.ts +++ b/libs/engine/src/index.ts @@ -1,6 +1,10 @@ export * as customElements from './custom-elements' export { Mutation, MutationWithSettings } from './app/services/mutation/mutation.entity' -export { AppMetadata, AppWithSettings } from './app/services/application/application.entity' +export { + AppMetadata, + AppWithSettings, + AppInstanceWithSettings, +} from './app/services/application/application.entity' export { LocalStorage } from './app/services/local-db/local-storage' export { IStorage } from './app/services/local-db/local-storage' export { App } from './app/app' @@ -14,3 +18,5 @@ export { export { ShadowDomWrapper } from './app/components/shadow-dom-wrapper' export { EngineConfig } from './engine' export { App as MutableWebProvider } from './app/app' +export { useAppDocuments } from './app/contexts/mutable-web-context/use-app-documents' +export { Document } from './app/services/document/document.entity' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49434ea7..0c333aaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,6 @@ settings: excludeLinksFromLockfile: false importers: - .: devDependencies: '@babel/preset-env':