Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"author": "",
"license": "MIT",
"dependencies": {
"@archipel/ui": "^1.0",
"@archipel/common": "^0.1",
"@archipel/ui": "^1.0",
"filereader-stream": "^2.0.0",
"pretty-bytes": "^5.1.0",
"proptypes": "^1.1.0",
Expand All @@ -21,6 +21,7 @@
"react-icons": "^3.2.2",
"react-toggle-button": "^2.2.0",
"react-values": "^0.3.0",
"simpl-schema": "^1.5.5",
"speedometer": "^1.1.0",
"through2": "^2.0.3",
"wayfarer": "^6.6.4"
Expand Down
15 changes: 1 addition & 14 deletions packages/app/src/features/archive/netStatsStore.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import { getApi } from '../../lib/api'
import deepEqual from '@archipel/common/util/deepEqual'

let stats = {}
const subscribers = new Set()
Expand Down Expand Up @@ -66,17 +67,3 @@ export function useArchiveStats (archive) {
}, [archive])
return state
}

function deepEqual (a, b) {
// if (a !== b) return false // unequal by reference

// according to http://www.mattzeunert.com/2016/01/28/javascript-deep-equal.html
// the following is faster than module deep-equal https://github.com/substack/node-deep-equal/
// so subject to high quality optimization
let sa = JSON.stringify(a)
let sb = JSON.stringify(b)

if (sa !== sb) return false

return true
}
28 changes: 28 additions & 0 deletions packages/app/src/features/metadata/EditSubmetadataOverlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import { MetadataEditor } from './MetadataEditor'
import { Modal } from '@archipel/ui'

export function EditMetadataOverlay (props) {
const { archive, type, literal } = props

/*
TODO: find good UID for arbitrary resources
include discovery key? maybe not since,
Max Mustermann is Max Mustermann with respect
to arbitrary archives.
include literal name, such as 'Max Mustermann' or 'Kiel'
yes, but there are several Max Mustermanns and Kiel's around
include type? yes, at least provides more context,
but does not resolve above ambiguity
*/
const ID = archive + type + literal
console.log('Submeta called with', props)

return (
<>
<Modal toggle={literal}>
<MetadataEditor ID={ID} {...props} />
</Modal>
</>
)
}
58 changes: 58 additions & 0 deletions packages/app/src/features/metadata/MemorizingTripleStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getApi } from '../../lib/api'
import deepEqual from '@archipel/common/util/deepEqual'

export default function MemorizingTripleStore () {
this.store = [['queries'], ['results']]
this.tripleStore = null
this.init()
this.activateCleaner()
}

MemorizingTripleStore.prototype.init = async function () {
this.tripleStore = await getApi().then((apis) => apis.hypergraph)
}

MemorizingTripleStore.prototype.activateCleaner = function (interval, cleaningSize) {
if (!interval) interval = 300000
if (!cleaningSize) cleaningSize = 100
let reductionSize = Math.floor(cleaningSize * 3 / 4)

this.cleaningInterval = setInterval(() => {
let length = this.store.length
if (length >= cleaningSize) {
this.store[0] = this.store[0].slice(length - reductionSize)
this.store[1] = this.store[1].slice(length - reductionSize)
}
}, interval)
}

MemorizingTripleStore.prototype.clearCleaner = function () {
clearInterval(this.cleaningInterval)
}

MemorizingTripleStore.prototype.storeQuery = function (query, result) {
this.store[0].push(query)
this.store[1].push(result)
}

MemorizingTripleStore.prototype.restoreQuery = function (query) {
let index = this.store[0].findIndex((elem) => deepEqual(elem, query))
if (index >= 1) return this.store[1][index]
return null
}

MemorizingTripleStore.prototype.get = async function (archive, query, opts) {
let result = this.restoreQuery({ archive, query })
if (result) return result
result = await this.tripleStore.get(archive, query, opts)
this.storeQuery({ archive, query }, result)
return result
}

MemorizingTripleStore.prototype.searchSubjects = async function (archive, query, opts) {
let result = this.restoreQuery({ archive, query })
if (result) return result
result = await this.tripleStore.searchSubjects(archive, query, opts)
this.storeQuery({ archive, query }, result)
return result
}
249 changes: 249 additions & 0 deletions packages/app/src/features/metadata/MetadataEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/*
Component to allow viewing and editing of the metadat associated with a file.
Indendet to be used in a Sidebar next
- to a file
- to a folder, to allow mass editing of childs metadata by directory structure.
*/
import React, { useEffect, useState } from 'react'
import { MdExpandMore, MdExpandLess, MdAdd, MdClear, MdKeyboardReturn } from 'react-icons/md'
import { Button, TightInputForm, DeleteIcon } from '@archipel/ui'
import MetadataLink from './MetadataLink'
import { EditorController } from './editorController'
import { makeLink } from '@archipel/common/util/triples'
import { useMetadata } from './editorStore'
import { getArchive } from '../archive/archive'
import { Categories } from './schemas'
import { EditMetadataOverlay } from './EditSubmetadataOverlay'

/*
Category
*/

function ShowAndSetCategory (props) {
const { controller } = props
const [category, setCategory] = useState(controller.category())
const [expanded, setExpand] = useState(false)

const ExpandIcon = expanded ? MdExpandLess : MdExpandMore
return (
<div className='bg-grey-lighter flex-none p-1'>
<div className='inline-flex cursor-pointer' onClick={onExpand}>
<span className='flex-none font-bold mr-2'>Category: </span>
<span className='flex-1'>{Categories.getLabel(category)}</span>
<div className='w-4 h-4 flex-0'>
<ExpandIcon />
</div>
</div>
{expanded && <ul className='list-reset pl-4'>
{Categories.getLabel(-1).map(
(label) => <li className='p-1 cursor-pointer'
key={label}
onClick={() => asyncSetCategory(
Categories.getID(label))}>
{label}
</li>
)}
</ul>}
</div>
)

async function asyncSetCategory (category) {
await controller.setCategory(category)
setCategory(controller.category())
}

function onExpand (e) {
e.stopPropagation()
setExpand(state => !state)
}
}

/*
Metadata List
*/

export function ListAndEditMetadata (props) {
const { metadata } = props
const { ofCategory: category, ...rest } = metadata

let keyIndex = 0
if (typeof rest !== 'object') return rest

return (
<ul className='list-reset'>
{Object.keys(rest).map(
(entryKey) => <MetadataListEntry
key={`${entryKey}+${keyIndex++}`}
entryKey={entryKey}
metadataEntry={metadata[entryKey]}
{...props}
/>
)}
</ul>
)
}

function MetadataListEntry (props) {
const { metadataEntry, setDraftValue } = props
let { values } = metadataEntry

return (
<li className='flex flex-col mb-1 mt-1'>
<span className='font-bold'>{`${metadataEntry.label}:`}</span>
{ values &&
<ul className='list-reset mx-2'>
{Object.keys(values).map((itemKey) =>
<MetadataListEntryItem
key={`value${values[itemKey].value}state${values[itemKey].state}`}
value={values[itemKey]}
{...props} />)}
</ul>
}
{ setDraftValue &&
<InputMetadataEntry className='pl-2 self-stretch'
{...props} /> }
</li>
)
}

function MetadataListEntryItem (props) {
let { value, setDeleteValue, entryKey, metadataEntry } = props
let { type: valueType } = metadataEntry

let Submeta = null
if (valueType) {
for (let key of Object.keys(valueType.definitions)) {
if (valueType.definitions[key].type && valueType.definitions[key].type._schema) {
Submeta = <EditMetadataOverlay
archive={props.archive}
type={valueType.definitions[key].type.name}
literal={value.value} />
}
}
}

if (value.state === 'actual') {
return <li className='inline-flex items-start p-1 m-1 bg-grey-lighter rounded'>
<span className='flex-1 mr-1'>{Submeta || value.value}</span>
<DeleteButton />
</li>
}
if (value.state === 'delete') {
return <li className='inline-flex items-start p-1 m-1 bg-red-lighter rounded line-through'>
<span className='flex-1'>{value.value}</span>
</li>
}
if (value.state === 'draft') {
return <li className='inline-flex items-start p-1 m-1 bg-green-lighter rounded'>
<span className='flex-1 mr-1'>{value.value}</span>
<DeleteButton />
</li>
}
return <span className='flex-1 items-start truncate'>{JSON.stringify(value)}</span>

function DeleteButton (props) {
// return <button onClick={() => setDeleteValue(entryKey, value.value)}>{<MdClear size={14} className='text-red-light border border-red-light rounded-full' />}</button>
return <DeleteIcon size={14} onClick={() => setDeleteValue(entryKey, value.value)} />
}
}

function InputMetadataEntry (props) {
let { entryKey, metadataEntry } = props
let { singleType, type: valueType } = metadataEntry

if (valueType) {
valueType = valueType.definitions[0].type.name ? valueType.definitions[0].type.name.toLowerCase() : 'string'
} else {
valueType = 'string'
}

let [draftValue, setDraftValue] = useState(metadataEntry.toBeValue)
useEffect(() => {
setDraftValue(props.draftValue)
}, [props])

const handleKeyPress = function (e) {
if (e.keyCode === 27) setDraftValue('')
}

useEffect(() => {
document.addEventListener('keydown', handleKeyPress.bind(this), false)
return document.removeEventListener('keydown', handleKeyPress.bind(this), false)
}, [])

return (
<TightInputForm
type={valueType}
onChange={e => setDraftValue(e.target.value)}
value={draftValue || ''}
onSubmit={() => props.setDraftValue(entryKey, draftValue)}
buttonSize={20}
addForm={!singleType} />
// <form className='inline-flex items-center w-auto'>
// <input className='flex-1 ml-1 p-1 border border-solid border-grey rounded'
// type={valueType}
// onChange={(e) => setDraftValue(e.target.value)}
// value={draftValue || ''} />
// <button type='submit' onClick={() => props.setDraftValue(entryKey, draftValue)}>
// {singleType
// ? <MdKeyboardReturn className='ml-1' size={20} />
// : <MdAdd className='ml-1' size={20} />
// }
// </button>
// </form>
)
}

/*
Parent
*/

let controller = null

export function MetadataEditor (props) {
console.log('ME called with', props)
let { ID } = props

useEffect(() => {
controller = new EditorController({ ...props })

// Feature or Anti-Feature?:
return () => controller.writeChanges({ onUnmount: true })
}, [])

const metadata = useMetadata(ID)

if (isObjectEmpty(metadata)) return <span>loading...</span>

return (
<div className='flex flex-col'>
<div className='mb-2'>
<ShowAndSetCategory controller={controller} />
</div>
<div className='pl-2 mb-2'>
{<ListAndEditMetadata
metadata={metadata}
archive={props.archive}
setDraftValue={controller.setDraftValue.bind(controller)}
setDeleteValue={controller.setDeleteValue.bind(controller)} />}
</div>
<Button onClick={() => controller.writeChanges()}>Save</Button>
</div>
)
}

export function FileMetadataEditor (props) {
if (props.stat.isDirectory) return null
let archive = getArchive(props.archive)

if (!archive || !archive.structures) return null
let ID = makeLink(archive.structures[0].discoveryKey, props.path)

return <MetadataEditor {...props.stat} archive={props.archive} ID={ID} />
}

function isObjectEmpty (object) {
if (typeof object !== 'object') return false
if (Object.keys(object).length === 0) return true
return false
}
Loading