+ deleteFilter(i)} toggleFilter={{ cb: toggleFilter, index: i }} />
+
)}
+
+
+
+
+
+
+
+
+
+
+
+
+ {metadata && metadata.map((e, i) => )}
+
+
+
+
+ )
+}
diff --git a/packages/app/src/features/metadata/MetadataLink.js b/packages/app/src/features/metadata/MetadataLink.js
new file mode 100644
index 0000000..d7efd01
--- /dev/null
+++ b/packages/app/src/features/metadata/MetadataLink.js
@@ -0,0 +1,19 @@
+/*
+Component which may be inlcuded anywhere in the app, shows some metadata, and redirects to the MetadataHub, filtered for the metadata.
+*/
+import React from 'react'
+import { Link } from '../../lib/router'
+
+export default function MetadataLink (props) {
+ let { className, target } = props
+ console.log('MetadataLink', target)
+ className = className || ''
+ let cls = 'inline-block p-2 text-pink-dark font-bold ' + className
+ let link = 'archive/:archive/hub/' + target
+
+ return (
+
+
#{target}
+
+ )
+}
diff --git a/packages/app/src/features/metadata/editorController.js b/packages/app/src/features/metadata/editorController.js
new file mode 100644
index 0000000..52024af
--- /dev/null
+++ b/packages/app/src/features/metadata/editorController.js
@@ -0,0 +1,187 @@
+'use strict'
+
+import { getApi } from '../../lib/api'
+import { _initialSetMetadata, getMetadata } from './editorStore'
+import getSchema, { getCategoryFromMimeType, validCategory } from './schemas'
+import { CATEGORY, triplesToMetadata, metadataToTriples, cloneObject } from './util'
+
+export function EditorController (props) {
+ console.log('New FMC', props)
+ this._ready = false
+ this.controllerName = props.name
+ this.constants = {
+ archiveKey: props.archive,
+ ID: props.ID,
+ type: props.mimetype || props.type
+ }
+ this._schema = null
+ this.state = {
+ category: null
+ }
+ this.init()
+}
+
+EditorController.prototype.init = async function () {
+ if (this._ready) return
+ if (!this.constants.ID) throw new Error('No ID!')
+ let tripleStore = await getApi().then((apis) => apis.hypergraph)
+ if (!tripleStore) throw new Error('Can not connect to TripleStore')
+ this.constants.tripleStore = tripleStore
+
+ await this.getCategory()
+ await this.getSchema()
+ await this._getActualMetadata()
+
+ this._ready = true
+}
+
+// Define setState function to allow for easy switch
+// to using react setState or similar state controllers
+EditorController.prototype.setState = function (props) {
+ const { ID } = this.constants
+ for (let i of Object.keys(props)) {
+ if (i === 'metadata') {
+ _initialSetMetadata(ID, props[i])
+ continue
+ }
+ this.state[i] = props[i]
+ }
+}
+
+/* ### set and get Category ### */
+
+// Needs to be sync
+EditorController.prototype.category = function () {
+ return this.state.category || null
+}
+
+EditorController.prototype.getCategory = async function () {
+ if (this.state.category) return this.state.category
+ let { tripleStore, archiveKey, ID } = this.constants
+
+ let queryRes = await tripleStore.get(
+ archiveKey, { subject: ID, predicate: CATEGORY }
+ )
+
+ if (queryRes.length < 1) return this._setDefaultCategory()
+
+ if (queryRes.length === 1 && validCategory(queryRes[0].object)) {
+ this.setState({ category: queryRes[0].object })
+ return queryRes[0].object
+ }
+ if (queryRes.length === 1 && !validCategory(queryRes[0].object)) {
+ console.warn('Invalid metadata category, reset to type default')
+ return this._setDefaultCategory()
+ }
+ console.warn('Category ambiguous, reset type default')
+ await tripleStore.del(archiveKey, queryRes)
+ return this._setDefaultCategory()
+}
+
+EditorController.prototype.setCategory = async function (category) {
+ await this._setCategory(category)
+ this._newSchema()
+}
+
+EditorController.prototype._setCategory = async function (category) {
+ if (!validCategory(category)) return this._setDefaultCategory()
+ let { tripleStore, archiveKey, ID } = this.constants
+ await tripleStore.put(archiveKey,
+ { subject: ID, predicate: CATEGORY, object: category })
+ this.setState({ category })
+}
+
+EditorController.prototype._setDefaultCategory = async function () {
+ let { type } = this.constants
+ if (!type) {
+ console.warn('No mimeType, setting metadata category to "resource"')
+ type = 'resource'
+ }
+ let category
+ if (validCategory(type)) {
+ category = type
+ } else {
+ category = getCategoryFromMimeType(type)
+ }
+ await this._setCategory(category)
+ return category
+}
+
+/* Get Schema according to category */
+
+EditorController.prototype.getSchema = async function () {
+ if (this._schema) return cloneObject(this._schema)
+ if (!this.state.category) await this.getCategory()
+ this._schema = await getSchema(this.state.category)
+ return cloneObject(this._schema)
+}
+
+EditorController.prototype._newSchema = async function () {
+ if (!this.state.category) await this.getCategory()
+ this._schema = getSchema(this.state.category)
+ let metadata = await this.getSchema()
+ let oldMetadata = getMetadata(this.constants.ID)
+
+ for (let entryKey of Object.keys(oldMetadata)) {
+ if (!metadata[entryKey]) metadata[entryKey] = {}
+ metadata[entryKey].values = { ...oldMetadata[entryKey].values }
+ }
+ this.setState({ metadata })
+}
+
+/* Work on the metadata-Object */
+
+EditorController.prototype._getActualMetadata = async function () {
+ let { tripleStore, archiveKey, ID } = this.constants
+
+ let fileTriples = await tripleStore.get(archiveKey, { subject: ID })
+ let schema = await this.getSchema()
+ let metadata = triplesToMetadata(fileTriples, schema, 'actualValue')
+
+ this.setState({ metadata })
+}
+
+EditorController.prototype.setDraftValue = async function (entryKey, draftValue) {
+ if (!entryKey || !draftValue) return null
+ if (draftValue.value) draftValue = draftValue.value
+ let { ID } = this.constants
+ let metadata = { ...getMetadata(ID) }
+ let metadataEntry = metadata[entryKey]
+ if (!metadataEntry.values) metadataEntry.values = {}
+ if (metadataEntry.values[draftValue]) {
+ if (metadataEntry.values[draftValue].state === 'actual') return
+ if (metadataEntry.values[draftValue].state === 'draft') return
+ if (metadataEntry.values[draftValue].state === 'delete') {
+ metadataEntry.values[draftValue].state = 'draft'
+ this.setState({ metadata })
+ return
+ }
+ }
+ if (metadataEntry.singleType) {
+ for (let valueKey of Object.keys(metadataEntry.values)) {
+ metadataEntry.values[valueKey].state = 'delete'
+ }
+ }
+ metadataEntry.values[draftValue] = { state: 'draft', value: draftValue }
+ this.setState({ metadata })
+}
+
+EditorController.prototype.setDeleteValue = async function (entryKey, value) {
+ if (value.value) value = value.value
+ let { ID } = this.constants
+ let metadata = { ...getMetadata(ID) }
+ let metadataEntry = metadata[entryKey]
+ if (metadataEntry.values[value]) metadataEntry.values[value].state = 'delete'
+ this.setState({ metadata })
+}
+
+EditorController.prototype.writeChanges = async function (props) {
+ // TODO: Verify Metadata
+ if (!props) props = {}
+ let { onUnmount } = props
+ let { archiveKey, ID, tripleStore } = this.constants
+ let { writeTriples, deleteTriples } = metadataToTriples(ID, await getMetadata(ID), 'toBeValue', await this.getSchema())
+ await tripleStore.put(archiveKey, writeTriples)
+ await tripleStore.del(archiveKey, deleteTriples)
+ if (!onUnmount) this._getActualMetadata(true)
+}
\ No newline at end of file
diff --git a/packages/app/src/features/metadata/editorStore.js b/packages/app/src/features/metadata/editorStore.js
new file mode 100644
index 0000000..8435dce
--- /dev/null
+++ b/packages/app/src/features/metadata/editorStore.js
@@ -0,0 +1,57 @@
+import { useState, useEffect } from 'react'
+import { Store } from '../../lib/store'
+
+let metadataStore = new Store('actualMetadata')
+
+export function _initialSetMetadata (fileID, metadata) {
+ metadataStore.set(fileID, metadata)
+ metadataStore.trigger(fileID)
+}
+
+// export function _setMetadataActualValue (fileID, entryID, actualValue) {
+// if (!Array.isArray(actualValue)) throw new Error('Metadata entries have to be arrays!')
+// let metadata = metadataStore.get(fileID)
+// if (!metadata[entryID]) metadata[entryID] = {}
+// metadata[entryID].actualValue = actualValue
+// metadataStore.set(fileID, metadata)
+// }
+
+// export function _setMetadataToBeValue (fileID, entryID, toBeValue) {
+// if (!Array.isArray(toBeValue)) throw new Error('Metadata entries have to be arrays!')
+// let metadata = metadataStore.get(fileID)
+// if (!metadata[entryID]) metadata[entryID] = {}
+// // console.log('in store set:', toBeValue)
+// metadata[entryID].toBeValue = toBeValue
+// // console.log('in store set', metadata)
+// metadataStore.set(fileID, metadata)
+// }
+
+export function _setMetadataValue (fileID, entryID, value) {
+ console.log('setMetadataValue', entryID, value)
+ let metadata = metadataStore.get(fileID)
+ metadata[entryID].values[value.value] = value
+ metadataStore.set(metadata)
+}
+
+export function getMetadata (fileID) {
+ let metadata = metadataStore.get(fileID)
+ if (Object.keys(metadata).length > 0) return metadata
+ return null
+}
+
+export function watchMetadata (fileID, cb, init) {
+ metadataStore.watch(fileID, cb, init)
+}
+
+export function useMetadata (fileID) {
+ const [state, setState] = useState(() => metadataStore.get(fileID))
+ useEffect(() => {
+ metadataStore.watch(fileID, watcher, true)
+
+ function watcher (metadata) {
+ setState(metadata)
+ }
+ return () => metadataStore.unwatch(fileID, watcher)
+ }, [state])
+ return state || {}
+}
diff --git a/packages/app/src/features/metadata/hubController.js b/packages/app/src/features/metadata/hubController.js
new file mode 100644
index 0000000..db8b4a2
--- /dev/null
+++ b/packages/app/src/features/metadata/hubController.js
@@ -0,0 +1,101 @@
+'use strict'
+
+import { useEffect } from 'react'
+import getSchema, { Categories, getAllKeysAndLabels } from './schemas'
+import { CATEGORY, triplesToMetadata } from './util'
+import MemorizingTripleStore from './MemorizingTripleStore'
+
+let tripleStore = new MemorizingTripleStore()
+// initTripleStore()
+// async function initTripleStore () {
+// tripleStore = await getApi().then((apis) => apis.hypergraph)
+// }
+
+let archive = null
+let limit = 20
+
+export default function hubController (props) {
+}
+
+hubController.categories = function () {
+ return Categories.getLabel(-1)
+}
+
+hubController.setArchive = function (archiveKey) {
+ archive = archiveKey
+}
+
+hubController.getArchive = function () {
+ return archive
+}
+
+hubController.limit = function (newLimit) {
+ if (newLimit) limit = newLimit
+ return limit
+}
+
+// hubController.queryCategory = async function (category) {
+// console.log('hCqueryCategory', category, tripleStore)
+// category = Categories.getID(category)
+// console.log('hCqueryCategory', category, tripleStore)
+// let res = await tripleStore.get(archive, { predicate: CATEGORY, object: category })
+// console.log('hCqueryCategory', res)
+// let ret = []
+// res.forEach(triple => ret.push(this.getMetadataToSubject(triple.subject, category)))
+// return Promise.all(ret)
+// }
+
+// hubController.queryPredicate = async function (predicate, object) {
+// let res = await tripleStore.get(archive, { predicate: predicate || null, object: object || null })
+// console.log('queryPredicate', res)
+// let ret = []
+// res.forEach(triple => ret.push(this.getMetadataToSubject(triple.subject)))
+// return Promise.all(ret)
+// }
+
+hubController.querySubject = function (subject) {
+ return tripleStore.get(archive, { subject })
+}
+
+hubController.search = async function (filterList) {
+ let attributes = []
+ for (let filter of filterList) {
+ attributes.push(...filter.getAttributes())
+ }
+ let triples = []
+ for (let pair of attributes) {
+ triples.push({ predicate: pair.attribute, object: pair.assign })
+ }
+ let res = await tripleStore.searchSubjects(archive, triples, { limit })
+ let ret = []
+ res.forEach(triple => ret.push(this.getMetadataToSubject(triple.subject)))
+ return Promise.all(ret)
+}
+
+hubController.getMetadataToSubject = async function (subject, category) {
+ if (!category) {
+ let { object } = await tripleStore.get(archive, { subject, predicate: CATEGORY })
+ category = object
+ }
+
+ let schema
+ if (category) schema = getSchema(category)
+ let triples = await this.querySubject(subject)
+ return triplesToMetadata(triples, schema)
+}
+
+hubController.getPossibleFilters = function () {
+ return getAllKeysAndLabels()
+}
+
+// function GetTripleStore (interval) {
+// let memorizingTripleStore = new MemorizingTripleStore()
+
+// useEffect(() => {
+// memorizingTripleStore.activateCleaner(interval)
+
+// return () => memorizingTripleStore.clearCleaner()
+// }, [])
+
+// return memorizingTripleStore
+// }
diff --git a/packages/app/src/features/metadata/index.js b/packages/app/src/features/metadata/index.js
new file mode 100644
index 0000000..b74ab91
--- /dev/null
+++ b/packages/app/src/features/metadata/index.js
@@ -0,0 +1,17 @@
+import { FileMetadataEditor } from './MetadataEditor'
+import MetadataHub from './MetadataHub'
+
+import registry from '../../lib/component-registry'
+import { registerRoute, registerElement } from '../../lib/router'
+
+export default function start () {
+ registry.add('fileSidebar', FileMetadataEditor, { title: 'MetadataEditor' })
+
+ registerRoute('archive/:archive/hub', MetadataHub)
+
+ registerElement('archive/:archive', {
+ link: [
+ { name: 'Hub', href: 'archive/:archive/hub', weight: 1 }
+ ]
+ })
+}
diff --git a/packages/app/src/features/metadata/issues.md b/packages/app/src/features/metadata/issues.md
new file mode 100644
index 0000000..88db8c4
--- /dev/null
+++ b/packages/app/src/features/metadata/issues.md
@@ -0,0 +1,6 @@
+Issues in the metadata feature.
+
+## functionality
+
+1. In [MetadataHub](./MetadataHub.js), resolve files on person?
+ - what happens if one would like to not only browse for artist=homer, but for (artist|interpreter|author|photographer)=homer ?
\ No newline at end of file
diff --git a/packages/app/src/features/metadata/schemas.js b/packages/app/src/features/metadata/schemas.js
new file mode 100644
index 0000000..aabfffc
--- /dev/null
+++ b/packages/app/src/features/metadata/schemas.js
@@ -0,0 +1,233 @@
+import SimplSchema from 'simpl-schema'
+import { cloneObject } from './util'
+
+const CArray = Array
+CArray.prototype.inArray = function (item) {
+ return this.some(elem => elem === item)
+}
+CArray.prototype.pushUnique = function (item) {
+ if (!this.inArray(item)) {
+ this.push(item)
+ return true
+ }
+ return false
+}
+
+const CategoryIDs = [
+ 'resource', 'file', 'image', 'person', 'address', 'text', 'article'
+]
+const CategoryLabels = [
+ 'Resource', 'File', 'Image', 'Person', 'Address', 'Text', 'Article'
+]
+export const Categories = [CategoryIDs, CategoryLabels]
+
+Categories.getID = function (label) {
+ if (label < 0) return this[0]
+ return this[0][this[1].findIndex(i => i === label)]
+}
+
+Categories.getLabel = function (id) {
+ if (id < 0) return this[1]
+ return this[1][this[0].findIndex(i => i === id)]
+}
+
+export default function getSchema (category) {
+ return cloneObject(_getSchema(category).schema())
+}
+
+function _getSchema (category) {
+ switch (category) {
+ case 'adress':
+ return adressSchema
+ case 'article':
+ return articleSchema
+ case 'file':
+ return fileSchema
+ case 'image':
+ return imageSchema
+ case 'person':
+ return personSchema
+ case 'text':
+ return textSchema
+ default:
+ return resourceSchema
+ }
+}
+
+const allKeysAndLabels = { keys: [], labels: [] }
+allKeysAndLabels.init = false
+allKeysAndLabels.labelFromKey = function (key) {
+ return this.labels[this.keys.findIndex(elem => elem === key)]
+}
+allKeysAndLabels.keyFromLabel = function (label) {
+ return this.keys[this.labels.findIndex(elem => elem === label)]
+}
+allKeysAndLabels.set = function (keysAndLabels) {
+ let { keys, labels } = keysAndLabels
+ if (keys.length !== labels.length) throw new Error('keys and labels should be in one-to-one order and hence of euqal length')
+ this.keys = keys
+ this.labels = labels
+ allKeysAndLabels.init = true
+}
+
+export function getAllKeysAndLabels () {
+ if (allKeysAndLabels.init) return allKeysAndLabels
+ let keys = new CArray()
+ let labels = new CArray()
+ for (let id of Categories.getID(-1)) {
+ let schema = _getSchema(id)
+ schema._schemaKeys.forEach(key => {
+ let res = keys.pushUnique(key)
+ if (res) labels.push(schema.schema()[key].label)
+ })
+ }
+ allKeysAndLabels.set({ keys, labels })
+ return allKeysAndLabels
+}
+
+export function getCategoryFromMimeType (mime) {
+ switch (mime) {
+ case 'text/plain':
+ return 'text'
+ case 'application/pdf':
+ return 'text'
+ case 'image/jpeg':
+ return 'image'
+ default:
+ return 'file'
+ }
+}
+
+// SimplSchema.addValidator(singleType)
+SimplSchema.extendOptions({
+ singleType: Boolean
+})
+
+const resourceSchema = new SimplSchema({
+ // hasLabel: String, // automatically by SimplSchema
+ hasDescription: {
+ type: String,
+ label: 'Description'
+ },
+ hasTag: {
+ type: String,
+ label: 'Tag'
+ }
+})
+resourceSchema.name = 'resource'
+
+const adressSchema = new SimplSchema({
+ hasCountry: {
+ type: String,
+ label: 'Country'
+ },
+ hasCity: {
+ type: String,
+ label: 'City'
+ },
+ hasPostalCode: {
+ type: String,
+ label: 'Postal code'
+ },
+ hasStreetNumber: {
+ type: String,
+ label: 'Street and number'
+ }
+}).extend(resourceSchema)
+adressSchema.name = 'adress'
+
+const personSchema = new SimplSchema({
+ hasFirstName: {
+ type: String,
+ label: 'First name',
+ singleType: true
+ },
+ hasMiddleNames: {
+ type: String,
+ label: 'Middle names'
+ },
+ hasLastName: {
+ type: String,
+ label: 'Last Name',
+ singleType: true
+ },
+ hasAdress: {
+ type: adressSchema,
+ label: 'Adress'
+ }
+})
+personSchema.extend(resourceSchema)
+personSchema.name = 'person'
+
+const textSchema = new SimplSchema({
+ hasAbstract: {
+ type: String,
+ label: 'Abstract'
+ },
+ hasLanguage: {
+ type: String,
+ label: 'Language'
+ }
+}).extend(resourceSchema)
+textSchema.name = 'text'
+
+const articleSchema = new SimplSchema({
+ hasAuthor: {
+ type: personSchema,
+ label: 'Author'
+ },
+ hasDateOfCreation: {
+ type: Date,
+ label: 'Release Date'
+ },
+ hasPlaceOfCreation: {
+ type: adressSchema,
+ label: 'Place of Publishing'
+ }
+}).extend(textSchema)
+articleSchema.name = 'article'
+
+const imageSchema = new SimplSchema({
+ hasTitle: {
+ type: String,
+ label: 'Title',
+ singleType: true },
+ hasLocalOrigin: {
+ type: String,
+ label: 'Location' },
+ hasDateOfCreation: {
+ type: Date,
+ label: 'Date of Creation',
+ singleType: true },
+ hasCreator: {
+ type: personSchema,
+ label: 'Creator' }
+})
+imageSchema.extend(resourceSchema)
+imageSchema.name = 'image'
+
+const fileSchema = new SimplSchema({
+ hasFileName: {
+ type: String,
+ label: 'File name',
+ singleType: true },
+ hasPath: {
+ type: String,
+ label: 'File path',
+ singleType: true },
+ hasCreator: {
+ type: personSchema,
+ label: 'Creator',
+ singleType: true
+ }
+})
+fileSchema.extend(resourceSchema)
+fileSchema.name = 'file'
+
+export function validCategory (category) {
+ return CategoryIDs.includes(category)
+}
+
+export function shallowObjectClone (object) {
+ return Object.assign({}, object)
+}
\ No newline at end of file
diff --git a/packages/app/src/features/metadata/util.js b/packages/app/src/features/metadata/util.js
new file mode 100644
index 0000000..acb433f
--- /dev/null
+++ b/packages/app/src/features/metadata/util.js
@@ -0,0 +1,53 @@
+'use strict'
+
+export const CATEGORY = 'ofCategory'
+
+export function triplesToMetadata (triples, metadata) {
+ if (!metadata) metadata = {}
+ for (let triple of triples) {
+ let { predicate, object } = triple
+ if (!metadata[predicate]) {
+ metadata[predicate] = {}
+ }
+
+ let dataEntry = metadata[predicate]
+ if (!dataEntry.label) dataEntry.label = predicate
+ if (!dataEntry.values) dataEntry.values = {}
+ if (!dataEntry.values[object]) dataEntry.values[object] = {}
+ dataEntry.values[object].state = 'actual'
+ dataEntry.values[object].value = object
+ }
+
+ // if (metadata[CATEGORY]) delete metadata[CATEGORY]
+ return metadata
+}
+
+export function metadataToTriples (subject, metadata) {
+ let writeTriples = []
+ let deleteTriples = []
+ for (let predicate of Object.keys(metadata)) {
+ let values = metadata[predicate].values
+ if (!values) continue
+ for (let value of Object.keys(values)) {
+ if (values[value].state === 'actual') continue
+ if (values[value].state === 'draft') {
+ writeTriples.push({ subject, predicate, object: values[value].value })
+ continue
+ }
+ if (values[value].state === 'delete') deleteTriples.push({ subject, predicate, object: values[value].value })
+ }
+ }
+ return { writeTriples, deleteTriples }
+}
+
+export function cloneObject (obj) {
+ var clone = {}
+ for (var i in obj) {
+ if (obj[i] !== null && typeof obj[i] === 'object') {
+ clone[i] = cloneObject(obj[i])
+ } else {
+ clone[i] = obj[i]
+ }
+ }
+ return clone
+}
\ No newline at end of file
diff --git a/packages/app/src/init.js b/packages/app/src/init.js
index 99617ad..4d643ca 100644
--- a/packages/app/src/init.js
+++ b/packages/app/src/init.js
@@ -6,6 +6,7 @@ import Panels from './foo/panels'
import archiveInit from './features/archive'
import driveInit from './features/drive'
import graphInit from './features/graph'
+import metadataInit from './features/metadata'
import { ArchiveListWrapper, ArchiveTabsWrapper, NoArchive } from './features/archive/ArchiveScreen.js'
import ArchiveInfo from './features/archive/ArchiveInfo'
@@ -41,7 +42,7 @@ function defaultInit () {
}
export default function init (extensions) {
- let inits = [defaultInit, archiveInit, driveInit, graphInit]
+ let inits = [defaultInit, archiveInit, driveInit, metadataInit] // graphInit
if (extensions) inits = inits.concat(extensions)
inits.forEach(ext => {
diff --git a/packages/backend/structures/hypergraph.js b/packages/backend/structures/hypergraph.js
index 4ae0dbd..8164977 100644
--- a/packages/backend/structures/hypergraph.js
+++ b/packages/backend/structures/hypergraph.js
@@ -21,13 +21,36 @@ exports.rpc = (api, opts) => {
async get (key, query) {
const db = await getHypergraph(this.session, key)
- const res = await db.get(query)
- return res
+ return db.get(query)
},
async put (key, triples) {
const db = await getHypergraph(this.session, key)
return db.put(triples)
+ },
+
+ async del (key, triples) {
+ const db = await getHypergraph(this.session, key)
+ await db.del(triples)
+ // , (err, res) => {
+ // if (err) return console.warn('Error deleting entries:', err)
+ // console.log('Deleted Entries:', res)
+ // })
+ },
+
+ async searchSubjects (key, pattern, opts) {
+ const db = await getHypergraph(this.session, key)
+ return db.searchSubjects(pattern, opts)
+ },
+
+ async query (key, query) {
+ const db = await getHypergraph(this.session, key)
+ return db.query(query)
+ // , (err, res) => {
+ // if (err) console.warn('Error at query', query, res)
+ // console.log('queried for', query, 'and got', res)
+ // })
+ // return res
}
}
@@ -35,7 +58,7 @@ exports.rpc = (api, opts) => {
if (!session.library) throw new Error('No library open.')
const library = await api.hyperlib.get(session.library)
const archive = await library.getArchive(key)
- let structure = await archive.getStructure({ type: 'hypergraph'})
+ let structure = await archive.getStructure({ type: 'hypergraph' })
if (!structure) {
structure = await archive.createStructure('hypergraph')
}
@@ -84,6 +107,64 @@ exports.structure = (opts, api) => {
return state
},
+ async get (triple, opts) {
+ return new Promise((resolve, reject) => {
+ db.get(triple, opts, (err, res) => {
+ if (err) reject(err)
+ resolve(res)
+ })
+ })
+ },
+
+ /* AdHoc Solution for bad search of hyper-graph-db
+ Problem: Dies not work with multiple search criteria
+ TODO fork hyper-graph-db and implement working solution */
+ /* returns an array of { subject: 'xyz' } object, of matching all criteria */
+
+ async searchSubjects (triples, opts) {
+ if (!triples || !Array.isArray(triples)) return null
+ let limit = opts
+
+ // get results for all single criteria
+ let res = []
+ triples.forEach(t => res.push(self.get(t)))
+ res = await Promise.all(res)
+ res = res.flat()
+
+ // scip the rest, in case of only one criterium
+ if (triples.length <= 1) {
+ if (limit < res.length) res = res.slice(0, limit)
+ return res.map(triple => { return { subject: triple.subject } })
+ }
+
+ // find those matching all criteriy
+ let indices = new Array(res.length)
+ indices.fill(1, 0)
+ for (let i = 0; i < res.length; i++) {
+ for (let j = i + 1; j < res.length; j++) {
+ if (res[i].subject === res[j].subject) {
+ indices[i]++ // = indices[i] + 1
+ indices[j]++ // = indices[j] + 1
+ }
+ }
+ }
+
+ // construct the returned array
+ let ret = []
+ indices.forEach((n, i) => {
+ if (n === triples.length) {
+ let append = true
+ ret.forEach(triple => {
+ if (res[i].subject === triple.subject) append = false
+ })
+ if (append) ret.push({ subject: res[i].subject })
+ }
+ })
+
+ if (limit < ret.length) ret = ret.slice(0, limit)
+ return ret
+ },
+
authorized (key) {
key = Buffer.from(key, 'hex')
return new Promise((resolve, reject) => {
@@ -105,7 +186,6 @@ exports.structure = (opts, api) => {
// Hack: Do a write after the auth is complete.
// Without this, hyperdrive breaks when loading the stat
// for the root folder (/). I think this is a bug in hyperdb.
- console.log('authorized writer')
resolve(true)
}
})
@@ -125,15 +205,17 @@ exports.structure = (opts, api) => {
// }
// },
- api: {}
+ api: { }
}
// Expose methods from hypergraph as api.
// Todo: Document available api.
- const asyncFuncs = ['ready', 'put', 'get']
+ const asyncFuncs = ['ready', 'put', 'del']
asyncFuncs.forEach(func => {
self.api[func] = pify(db[func].bind(db))
})
+ self.api['get'] = self.get.bind(self)
+ self.api['searchSubjects'] = self.searchSubjects.bind(self)
return self
}
diff --git a/packages/common/util/deepEqual.js b/packages/common/util/deepEqual.js
new file mode 100644
index 0000000..ff488ff
--- /dev/null
+++ b/packages/common/util/deepEqual.js
@@ -0,0 +1,13 @@
+export default 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
+}
\ No newline at end of file
diff --git a/packages/ui/src/components/DeleteIcon.js b/packages/ui/src/components/DeleteIcon.js
new file mode 100644
index 0000000..ecb99c1
--- /dev/null
+++ b/packages/ui/src/components/DeleteIcon.js
@@ -0,0 +1,9 @@
+import React from 'react'
+import { MdClear } from 'react-icons/md'
+
+export default function DeleteIcon (props) {
+ let { size, onClick } = props
+ return
+}
diff --git a/packages/ui/src/components/TightInputForm.js b/packages/ui/src/components/TightInputForm.js
new file mode 100644
index 0000000..ceaa62a
--- /dev/null
+++ b/packages/ui/src/components/TightInputForm.js
@@ -0,0 +1,30 @@
+import React from 'react'
+import { cls } from '../util.js'
+import { MdAdd, MdKeyboardReturn } from 'react-icons/md'
+
+let clss = ['inline-flex items-center']
+
+const TightInputForm = (props) => {
+ let { type, onChange, value, placeholder, onSubmit, addForm, buttonSize, widthUnits, ...rest } = props
+ props = rest
+ if (!widthUnits) widthUnits = 7
+ if (!value) value = ''
+ return (
+
+ )
+}
+
+export default TightInputForm
diff --git a/packages/ui/src/index.js b/packages/ui/src/index.js
index 63a0cc7..33f7e4a 100644
--- a/packages/ui/src/index.js
+++ b/packages/ui/src/index.js
@@ -11,6 +11,8 @@ export { default as Button, FloatingButton } from './components/Button'
// C
export { default as Card } from './components/Card'
export { default as Checkbox } from './components/Checkbox'
+// D
+export { default as DeleteIcon } from './components/DeleteIcon'
// E
export { default as ExpandButton } from './components/ExpandButton'
// F
@@ -35,8 +37,8 @@ export { default as StructuresCheckList } from './components/StructuresCheckList
// T
export { default as Tabs } from './components/Tabs'
+export { default as TightInputForm } from './components/TightInputForm'
export { default as Tree } from './components/Tree/index.js'
-
// Other
export { proplist, cls } from './util.js'
diff --git a/packages/ui/tailwind.config.js b/packages/ui/tailwind.config.js
index ff1090a..4c13004 100644
--- a/packages/ui/tailwind.config.js
+++ b/packages/ui/tailwind.config.js
@@ -520,6 +520,12 @@ module.exports = {
'4/5': '80%',
'1/6': '16.66667%',
'5/6': '83.33333%',
+ '1/7': '14.28571%',
+ '2/7': '28.57143%',
+ '3/7': '42.85714%',
+ '4/7': '57.14288%',
+ '5/7': '71.42857%',
+ '6/7': '85.71429%',
'full': '100%',
'screen': '100vw',
'main': '50rem'
@@ -599,6 +605,21 @@ module.exports = {
minHeight: {
'0': '0',
+ '1': '0.25rem',
+ '2': '0.5rem',
+ '3': '0.75rem',
+ '4': '1rem',
+ '5': '1.25rem',
+ '6': '1.5rem',
+ '8': '2rem',
+ '10': '2.5rem',
+ '12': '3rem',
+ '16': '4rem',
+ '24': '6rem',
+ '32': '8rem',
+ '48': '12rem',
+ '64': '16rem',
+
'full': '100%',
'screen': '100vh'
},
@@ -649,6 +670,8 @@ module.exports = {
maxHeight: {
'full': '100%',
+ '48': '12rem',
+ '64': '16rem',
'screen': '100vh',
},