diff --git a/.editorconfig b/.editorconfig
index adcac07..615fbcd 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,7 +8,7 @@ indent_style = space
indent_size = 4
tab_width = 4
trim_trailing_whitespace = true
-max_line_length = 120
+max_line_length = 80
[*.clj]
-indent_size = 2
\ No newline at end of file
+indent_size = 2
diff --git a/resources/app/stylesheets/app.less b/resources/app/stylesheets/app.less
index 0667559..6321fb8 100644
--- a/resources/app/stylesheets/app.less
+++ b/resources/app/stylesheets/app.less
@@ -153,6 +153,10 @@ img, svg {
}
}
}
+
+ .quick-edits {
+ margin-top: 7rem;
+ }
}
aside {
@@ -293,3 +297,18 @@ table {
margin-right: @element-margin;
}
}
+
+.tag-list {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.tag-list li {
+ display: inline-block;
+ margin-right: .5rem;
+ border: 1px solid @ci-color;
+ border-left: .5rem solid @ci-blue;
+ border-radius: .5rem;
+ padding: .1rem .4rem;
+}
diff --git a/src/wanijo/framework/neo4j.clj b/src/wanijo/framework/neo4j.clj
index f1aa8ca..7f75383 100644
--- a/src/wanijo/framework/neo4j.clj
+++ b/src/wanijo/framework/neo4j.clj
@@ -47,15 +47,21 @@
params))
(qry session params)))
+(spec/def ::tuple-query-list
+ (spec/coll-of
+ (spec/tuple fn? map?)))
(defn exec-queries! [& tuples]
+ {:pre [(spec/assert ::tuple-query-list tuples)]}
(db/with-transaction @conn tx
(doseq [tuple tuples]
+ (println tuple)
(let [qry (first tuple)
params (second tuple)]
(devmode/send-to-bar
(str (butiful-query qry)
"
---Params---
"
params))
+ (spec/assert map? params)
(qry tx params)))))
(defn now-str []
diff --git a/src/wanijo/framework/repl.clj b/src/wanijo/framework/repl.clj
index fbb64ae..88c2054 100644
--- a/src/wanijo/framework/repl.clj
+++ b/src/wanijo/framework/repl.clj
@@ -49,6 +49,14 @@
"CREATE CONSTRAINT ON (n:link)
ASSERT n.uuid IS UNIQUE")
+(db/defquery ver-1-tag-name
+ "CREATE CONSTRAINT ON (n:tag)
+ ASSERT n.name IS UNIQUE")
+
+(db/defquery ver-1-tag-uuid
+ "CREATE CONSTRAINT ON (n:tag)
+ ASSERT n.uuid IS UNIQUE")
+
(defn init-version-0 []
(neo4j/exec-query! ver-0-schema-uuid {})
(neo4j/exec-query! ver-0-attribute-uuid {})
@@ -57,8 +65,13 @@
(neo4j/exec-query! ver-0-user-uuid {})
(neo4j/exec-query! ver-0-link-uuid {}))
+(defn init-version-1 []
+ (neo4j/exec-query! ver-1-tag-name {})
+ (neo4j/exec-query! ver-1-tag-uuid {}))
+
(def migrations
- [init-version-0])
+ [init-version-0
+ init-version-1])
(defn run-migrations! []
(neo4j/exec-query! init-config {:now (neo4j/now-str)})
diff --git a/src/wanijo/handler.clj b/src/wanijo/handler.clj
index b50ac47..14316da 100644
--- a/src/wanijo/handler.clj
+++ b/src/wanijo/handler.clj
@@ -16,6 +16,7 @@
[wanijo.attribute.routes :as attr-routes]
[wanijo.instance.routes :as instance-routes]
[wanijo.visualisation.routes :as vis-routes]
+ [wanijo.tag.routes :as tag-routes]
[wanijo.framework
[auth :as auth]
[devmode :as devmode]
@@ -35,7 +36,8 @@
user-routes/routes
attr-routes/routes
instance-routes/routes
- vis-routes/routes))
+ vis-routes/routes
+ tag-routes/routes))
(route/not-found "Not Found"))
(def standalone-app
diff --git a/src/wanijo/instance/domain.clj b/src/wanijo/instance/domain.clj
index 64bee0e..2c00308 100644
--- a/src/wanijo/instance/domain.clj
+++ b/src/wanijo/instance/domain.clj
@@ -168,8 +168,10 @@
(i)-[cb:created_by]->(:user)
OPTIONAL MATCH
(p:property)-[pc:of]->(i),
- (p)-[pac:of]->(a:attribute)
- DELETE pac, pc, cb, ic, p, i")
+ (p)-[pac:of]->(a:attribute),
+ (i)-[lt:link_to]->(),
+ (i)<-[lf:link_from]-()
+ DELETE pac, pc, cb, ic, p, lt, lf i")
(defn delete! [uuid]
(neo4j/exec-query! delete {:uuid uuid}))
diff --git a/src/wanijo/instance/view.clj b/src/wanijo/instance/view.clj
index 6ddf61d..9c5bebc 100644
--- a/src/wanijo/instance/view.clj
+++ b/src/wanijo/instance/view.clj
@@ -5,6 +5,7 @@
[ring.util.anti-forgery :refer [anti-forgery-field]]
[markdown.core :as md]
[formulare.core :as form]
+ [wanijo.tag.view :as view-tag]
[wanijo.instance.domain :as domain]
[wanijo.visualisation.viz :as viz]
[wanijo.framework
@@ -91,9 +92,7 @@
"Explore from here"]]]
(when (seq (:tags instance))
[:section.tags
- [:ul
- (for [tag (:tags instance)]
- [:li (:name tag)])]])
+ (view-tag/tag-list (:tags instance))])
(when (seq (:properties instance))
[:section.properties
[:h2 "Properties"]
@@ -159,15 +158,20 @@
{:schema-uuid (:uuid schema)})}
(h (:name schema))]]
[:td (prettify-dt (:created_at link))]])]]])
- [:section.link-instance
- [:h2 "Link Instance with Instance of Schema..."]
- [:ul
- (for [schema schemas]
- [:li
- [:a {:href (path :instance-link-selection
- {:uuid (:uuid instance)
- :schema-uuid (:uuid schema)})}
- (h (:name schema))]])]]]))
+ [:section.quick-edits
+ [:h2 "Quick edits"]
+ [:section.link-instance
+ [:h3 "Link Instance with Instance of Schema..."]
+ [:ul
+ (for [schema schemas]
+ [:li
+ [:a {:href (path :instance-link-selection
+ {:uuid (:uuid instance)
+ :schema-uuid (:uuid schema)})}
+ (h (:name schema))]])]]
+ [:section.tag-instance
+ [:h3 "Add or create Tags"]
+ (view-tag/new-tag-form instance)]]]))
(defn edit! [instance form form-data schemas req]
(view/layout!
diff --git a/src/wanijo/specs.clj b/src/wanijo/specs.clj
index d7d65ca..492d943 100644
--- a/src/wanijo/specs.clj
+++ b/src/wanijo/specs.clj
@@ -13,3 +13,5 @@
(spec/def ::attribute_uuid ::neo4j/uuid)
(spec/def ::now ::neo4j/date-str)
(spec/def ::uuid ::neo4j/uuid)
+(spec/def ::user_uuid ::neo4j/uuid)
+(spec/def ::no-whitespace #(not (re-matches #".*\s.*" %)))
diff --git a/src/wanijo/tag/domain.clj b/src/wanijo/tag/domain.clj
index 8cc291c..e46b995 100644
--- a/src/wanijo/tag/domain.clj
+++ b/src/wanijo/tag/domain.clj
@@ -3,17 +3,57 @@
[wanijo.specs :as specs]
[wanijo.framework.neo4j :as neo4j]))
-(spec/def ::name ::specs/req-name)
+(spec/def ::name
+ (spec/and ::specs/name
+ ::specs/no-whitespace))
(spec/def ::tag
(spec/keys :req-un [::specs/uuid
::specs/created_at
::name]))
(neo4j/defquery tags-by-instance
- "MATCH (i:instance {uuid:{uuid}})-[:has]->(t:tag)
+ "MATCH (i:instance {uuid:{uuid}})-[:tagged_with]->(t:tag)
RETURN t
ORDER BY t.name")
(defn tags-by-instance! [instance-uuid]
{:post [(spec/assert (spec/coll-of ::tag) %)]}
- (neo4j/exec-query! tags-by-instance
- {:uuid instance-uuid}))
+ (map :t
+ (neo4j/exec-query! tags-by-instance
+ {:uuid instance-uuid})))
+
+(neo4j/defquery merge-tag
+ "MATCH (i:instance {uuid:{instance_uuid}}),
+ (u:user {uuid:{user_uuid}})
+ MERGE (t:tag {name:{name}})-[:created_by]->(u)
+ ON CREATE SET t.uuid = {uuid},
+ t.created_at = {now}
+ MERGE (i)-[:tagged_with]->(t)")
+(spec/def ::merge-tag-tuple
+ (spec/keys :req-un [::specs/instance_uuid
+ ::name
+ ::specs/uuid
+ ::specs/now
+ ::specs/user_uuid]))
+(defn merge-tag-tuples [tags instance-uuid user-uuid]
+ {:pre [(spec/assert (spec/coll-of string?) tags)]
+ :post [(spec/assert (spec/coll-of (spec/tuple fn? ::merge-tag-tuple))
+ %)]}
+ (map (fn [tag-name]
+ [merge-tag
+ {:instance_uuid instance-uuid
+ :name tag-name
+ :uuid (neo4j/uuid)
+ :now (neo4j/now-str)
+ :user_uuid user-uuid}])
+ tags))
+(comment
+ (merge-tag-tuples (list "a" "b")
+ (neo4j/uuid)
+ (neo4j/uuid))
+ (spec/explain (spec/coll-of (spec/tuple fn? ::merge-tag-tuple))
+ (merge-tag-tuples (list "a" "b")
+ (neo4j/uuid)
+ (neo4j/uuid))))
+(defn merge-tags [tags instance-uuid user-uuid]
+ (apply neo4j/exec-queries!
+ (merge-tag-tuples tags instance-uuid user-uuid)))
diff --git a/src/wanijo/tag/forms.clj b/src/wanijo/tag/forms.clj
new file mode 100644
index 0000000..4d918c4
--- /dev/null
+++ b/src/wanijo/tag/forms.clj
@@ -0,0 +1,24 @@
+(ns wanijo.tag.forms
+ (:require [clojure.spec.alpha :as spec]
+ [clojure.string :refer [split trim]]))
+
+(defn contains-whitespace? [s]
+ (re-matches #".*\s.*" s))
+
+(defn tag-names-from-input [s]
+ (->> (split s #",")
+ (map trim)))
+
+(defn any-tag-name-contains-whitespace [s]
+ (some contains-whitespace? (tag-names-from-input s)))
+
+(spec/def ::new-names
+ (spec/and string?
+ (complement empty?)
+ (complement any-tag-name-contains-whitespace)))
+
+(def new-tag
+ {:fields {:newnames {:label "Tag Name(s)"
+ :required true
+ :spec ::new-names
+ :from-req tag-names-from-input}}})
diff --git a/src/wanijo/tag/routes.clj b/src/wanijo/tag/routes.clj
new file mode 100644
index 0000000..c11f9f9
--- /dev/null
+++ b/src/wanijo/tag/routes.clj
@@ -0,0 +1,32 @@
+(ns wanijo.tag.routes
+ (:require [compojure.core :refer [defroutes POST]]
+ [ring.util.response :as resp]
+ [formulare.core :as form]
+ [wanijo.framework.routing :refer [register! path]]
+ [wanijo.schema.domain :as domain-schema]
+ [wanijo.instance
+ [view :as view-instance]
+ [domain :as domain-instance]]
+ [wanijo.tag
+ [domain :as domain]
+ [forms :as forms]]))
+
+(defn create-tag! [instance-uuid req]
+ (let [{new-names :newnames} (form/form-data forms/new-tag req)
+ user-uuid (-> req :session :uuid)]
+ (if (form/valid? forms/new-tag req)
+ (do
+ (domain/merge-tags new-names
+ instance-uuid
+ user-uuid)
+ (resp/redirect (path :instance-show
+ {:uuid instance-uuid})))
+ (view-instance/show!
+ (domain-instance/full-instance-by-uuid! instance-uuid)
+ (domain-schema/accessible-schemas! user-uuid)
+ req))))
+
+(defroutes routes
+ (POST (register! :tag-create "/tag/:instance-uuid")
+ [instance-uuid :as req]
+ (create-tag! instance-uuid req)))
diff --git a/src/wanijo/tag/view.clj b/src/wanijo/tag/view.clj
new file mode 100644
index 0000000..a60070b
--- /dev/null
+++ b/src/wanijo/tag/view.clj
@@ -0,0 +1,21 @@
+(ns wanijo.tag.view
+ (:require [hiccup.form :as hform]
+ [hiccup.core :refer [h]]
+ [ring.util.anti-forgery :refer [anti-forgery-field]]
+ [formulare.core :as form]
+ [wanijo.tag.forms :as forms]
+ [wanijo.framework.routing :refer [path]]))
+
+(defn tag-list [tags]
+ [:ul.tag-list
+ (for [tag tags]
+ [:li
+ [:code ":" (h (:name tag))]])])
+
+(defn new-tag-form [{uuid :uuid}]
+ (list
+ (hform/form-to [:post (path :tag-create {:instance-uuid uuid})]
+ (form/render-widgets forms/new-tag {} {})
+ (hform/submit-button "Tag!"))
+ [:small (str "Comma separate each tag. "
+ "Tag names must not contain whitespace.")]))