| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- import {
- createContext,
- useContext,
- useEffect,
- FC,
- useCallback,
- useMemo,
- useState,
- useRef,
- } from 'react'
- import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
- import { useConnectionContext } from '@/features/ide-react/context/connection-context'
- import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
- import _ from 'lodash'
- import { getJSON, postJSON } from '@/infrastructure/fetch-json'
- import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
- import { useEditorContext } from '@/shared/context/editor-context'
- import { useIdeContext } from '@/shared/context/ide-context'
- import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
- import useEventListener from '@/shared/hooks/use-event-listener'
- import { FileTreeFindResult } from '@/features/ide-react/types/file-tree'
- import { Project } from '../../../../../types/project'
- import { useModalsContext } from '@/features/ide-react/context/modals-context'
- import { useTranslation } from 'react-i18next'
- type DocumentMetadata = {
- labels: string[]
- packages: Record<string, any>
- }
- type DocumentsMetadata = Record<string, DocumentMetadata>
- type MetadataContextValue = {
- metadata: {
- state: {
- documents: DocumentsMetadata
- }
- getAllLabels: () => DocumentMetadata['labels']
- getAllPackages: () => DocumentMetadata['packages']
- }
- }
- type DocMetadataResponse = { docId: string; meta: DocumentMetadata }
- const MetadataContext = createContext<MetadataContextValue | undefined>(
- undefined
- )
- export const MetadataProvider: FC = ({ children }) => {
- const { t } = useTranslation()
- const ide = useIdeContext()
- const { eventEmitter, projectId } = useIdeReactContext()
- const { socket } = useConnectionContext()
- const { onlineUsersCount } = useOnlineUsersContext()
- const { permissionsLevel } = useEditorContext()
- const { currentDocument } = useEditorManagerContext()
- const { showGenericMessageModal } = useModalsContext()
- const [documents, setDocuments] = useState<DocumentsMetadata>({})
- const debouncerRef = useRef<Map<string, number>>(new Map()) // DocId => Timeout
- useEffect(() => {
- const handleEntityDeleted = (entity: FileTreeFindResult) => {
- if (entity.type === 'doc') {
- setDocuments(documents => {
- return _.omit(documents, entity.entity._id)
- })
- }
- }
- eventEmitter.on('entity:deleted', handleEntityDeleted)
- return () => {
- eventEmitter.off('entity:deleted', handleEntityDeleted)
- }
- }, [eventEmitter])
- useEffect(() => {
- window.dispatchEvent(
- new CustomEvent('project:metadata', { detail: documents })
- )
- }, [documents])
- const onBroadcastDocMeta = useCallback((data: DocMetadataResponse) => {
- const { docId, meta } = data
- if (docId != null && meta != null) {
- setDocuments(documents => ({ ...documents, [docId]: meta }))
- }
- }, [])
- const getAllLabels = useCallback(
- () => _.flattenDeep(Object.values(documents).map(meta => meta.labels)),
- [documents]
- )
- const getAllPackages = useCallback(() => {
- const packageCommandMapping: Record<string, any> = {}
- for (const meta of Object.values(documents)) {
- for (const [packageName, commandSnippets] of Object.entries(
- meta.packages
- )) {
- packageCommandMapping[packageName] = commandSnippets
- }
- }
- return packageCommandMapping
- }, [documents])
- const loadProjectMetaFromServer = useCallback(() => {
- getJSON(`/project/${projectId}/metadata`).then(
- (response: { projectMeta: DocumentsMetadata }) => {
- const { projectMeta } = response
- if (projectMeta) {
- setDocuments(projectMeta)
- }
- }
- )
- }, [projectId])
- const loadDocMetaFromServer = useCallback(
- (docId: string) => {
- // Don't broadcast metadata when there are no other users in the
- // project.
- const broadcast = onlineUsersCount > 0
- postJSON(`/project/${projectId}/doc/${docId}/metadata`, {
- body: {
- broadcast,
- },
- }).then((response: DocMetadataResponse) => {
- if (!broadcast && response) {
- // handle the POST response like a broadcast event when there are no
- // other users in the project.
- onBroadcastDocMeta(response)
- }
- })
- },
- [onBroadcastDocMeta, onlineUsersCount, projectId]
- )
- const scheduleLoadDocMetaFromServer = useCallback(
- (docId: string) => {
- if (permissionsLevel === 'readOnly') {
- // The POST request is blocked for users without write permission.
- // The user will not be able to consume the metadata for edits anyway.
- return
- }
- // Debounce loading labels with a timeout
- const existingTimeout = debouncerRef.current.get(docId)
- if (existingTimeout != null) {
- window.clearTimeout(existingTimeout)
- debouncerRef.current.delete(docId)
- }
- debouncerRef.current.set(
- docId,
- window.setTimeout(() => {
- // TODO: wait for the document to be saved?
- loadDocMetaFromServer(docId)
- debouncerRef.current.delete(docId)
- }, 2000)
- )
- },
- [loadDocMetaFromServer, permissionsLevel]
- )
- const handleBroadcastDocMeta = useCallback(
- (data: DocMetadataResponse) => {
- onBroadcastDocMeta(data)
- },
- [onBroadcastDocMeta]
- )
- useSocketListener(socket, 'broadcastDocMeta', handleBroadcastDocMeta)
- const handleMetadataOutdated = useCallback(() => {
- if (currentDocument) {
- scheduleLoadDocMetaFromServer(currentDocument.doc_id)
- }
- }, [currentDocument, scheduleLoadDocMetaFromServer])
- useEventListener('editor:metadata-outdated', handleMetadataOutdated)
- useEffect(() => {
- const handleProjectJoined = ({ project }: { project: Project }) => {
- if (project.deletedByExternalDataSource) {
- showGenericMessageModal(
- t('project_renamed_or_deleted'),
- t('project_renamed_or_deleted_detail')
- )
- }
- window.setTimeout(() => {
- if (permissionsLevel !== 'readOnly') {
- loadProjectMetaFromServer()
- }
- }, 200)
- }
- eventEmitter.once('project:joined', handleProjectJoined)
- return () => {
- eventEmitter.off('project:joined', handleProjectJoined)
- }
- }, [
- eventEmitter,
- loadProjectMetaFromServer,
- permissionsLevel,
- showGenericMessageModal,
- t,
- ])
- const value = useMemo<MetadataContextValue>(
- () => ({
- metadata: {
- state: { documents },
- getAllLabels,
- getAllPackages,
- },
- }),
- [documents, getAllLabels, getAllPackages]
- )
- // Expose metadataManager via ide object because useCodeMirrorScope relies on
- // it, for now
- ide.metadataManager = value
- return (
- <MetadataContext.Provider value={value}>
- {children}
- </MetadataContext.Provider>
- )
- }
- export function useMetadataContext(): MetadataContextValue {
- const context = useContext(MetadataContext)
- if (!context) {
- throw new Error(
- 'useMetadataContext is only available inside MetadataProvider'
- )
- }
- return context
- }
|