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.")]))