better upload experience, without dropzone
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -328,91 +328,99 @@
|
|||||||
[:force-location {:optional true}
|
[:force-location {:optional true}
|
||||||
[:maybe [:string {:decode/string strip :min 2 :max 2}]]]])
|
[:maybe [:string {:decode/string strip :min 2 :max 2}]]]])
|
||||||
|
|
||||||
|
(defn upload-form [{:keys [form-params form-errors] :as request}]
|
||||||
|
(com/content-card {}
|
||||||
|
|
||||||
|
[:div.px-4.py-3.space-y-4
|
||||||
|
|
||||||
|
[:div.flex.justify-between.items-center [:h1.text-2xl.mb-3.font-bold "Import new invoices"]
|
||||||
|
|
||||||
|
[:div.flex.gap-2.items-baseline "Trouble with the new upload experience?"
|
||||||
|
[:a {:href (bidi/path-for client-routes/routes :import-invoices)}
|
||||||
|
(com/pill {:color :secondary}
|
||||||
|
"Go back to previous version")]]]
|
||||||
|
[:div#page-notification.notification.block {:style {:display "none"}}]
|
||||||
|
|
||||||
|
|
||||||
|
[:form
|
||||||
|
{:hx-post (bidi/path-for ssr-routes/only-routes
|
||||||
|
::route/import-file)
|
||||||
|
:hx-encoding "multipart/form-data"
|
||||||
|
:hx-target "#page-notification"
|
||||||
|
:hx-swap "outerHTML"
|
||||||
|
:id "upload"}
|
||||||
|
(fc/start-form
|
||||||
|
form-params form-errors
|
||||||
|
[:div.flex.gap-4
|
||||||
|
|
||||||
|
(fc/with-field :force-client
|
||||||
|
(com/validated-field {:label "Force client"
|
||||||
|
:errors (fc/field-errors)}
|
||||||
|
[:div.w-96
|
||||||
|
(com/typeahead {:name (fc/field-name)
|
||||||
|
:error? (fc/error?)
|
||||||
|
:class "w-96"
|
||||||
|
:placeholder "Search..."
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
||||||
|
:value (fc/field-value)
|
||||||
|
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))
|
||||||
|
(fc/with-field :force-location
|
||||||
|
(com/validated-field {:label "Force location"
|
||||||
|
:errors (fc/field-errors)}
|
||||||
|
|
||||||
|
(com/text-input {:name (fc/field-name)
|
||||||
|
:value (fc/field-value)
|
||||||
|
:size 2})))
|
||||||
|
(fc/with-field :force-vendor
|
||||||
|
(com/validated-field {:label "Force vendor"
|
||||||
|
:errors (fc/field-errors)}
|
||||||
|
[:div.w-96
|
||||||
|
(com/typeahead {:name (fc/field-name)
|
||||||
|
:error? (fc/error?)
|
||||||
|
:class "w-96"
|
||||||
|
:placeholder "Search..."
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||||
|
:value (fc/field-value)
|
||||||
|
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))])
|
||||||
|
|
||||||
|
[:div.border-2.border-dashed.rounded-lg.p-4.w-full.text-center.cursor-pointer.h-64.flex.items-center.justify-center.text-lg.relative
|
||||||
|
{ :x-data (hx/json {"files" nil
|
||||||
|
"hovering" false})
|
||||||
|
":class" "{'bg-blue-100': !hovering,
|
||||||
|
'border-blue-300': !hovering,
|
||||||
|
'text-blue-700': !hovering,
|
||||||
|
'bg-green-100': hovering,
|
||||||
|
'border-green-300': hovering,
|
||||||
|
'text-green-700': hovering
|
||||||
|
}"
|
||||||
|
:x-ref "box"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[:input {:type "file"
|
||||||
|
:name "file"
|
||||||
|
:multiple "multiple"
|
||||||
|
:class "absolute inset-0 m-0 p-0 w-full h-full outline-none opacity-0",
|
||||||
|
:x-on:change "files = $event.target.files; console.log($event.target.files);",
|
||||||
|
:x-on:dragover "hovering = true",
|
||||||
|
:x-on:dragleave "hovering = false",
|
||||||
|
:x-on:drop "hovering = false"}]
|
||||||
|
[:div.flex.flex-col.space-2
|
||||||
|
[:div
|
||||||
|
[:ul {:x-show "files != null"}
|
||||||
|
[:template {:x-for "f in files" }
|
||||||
|
[:li (com/pill {:color :primary :x-text "f.name"}) ]
|
||||||
|
]]]
|
||||||
|
|
||||||
|
[:div.htmx-indicator-hidden "Drop files to upload here"]]]
|
||||||
|
(com/button {:color :primary :class "w-32 mt-3"} "Upload")]]))
|
||||||
|
|
||||||
(def grid-page
|
(def grid-page
|
||||||
(helper/build {:id "entity-table"
|
(helper/build {:id "entity-table"
|
||||||
:nav com/main-aside-nav
|
:nav com/main-aside-nav
|
||||||
:check-boxes? true
|
:check-boxes? true
|
||||||
:page-specific-nav filters
|
:page-specific-nav filters
|
||||||
:above-grid (fn [{:keys [form-params form-errors] :as request}]
|
:above-grid upload-form
|
||||||
(com/content-card {}
|
|
||||||
|
|
||||||
[:div.px-4.py-3.space-y-4
|
|
||||||
{:x-data (hx/json {"a" false})
|
|
||||||
"@started" "a=true"
|
|
||||||
"@completed" "a=false"}
|
|
||||||
|
|
||||||
#_[:div.bg-red-50.p-8 {:x-show "a"} "HELLO THERE"]
|
|
||||||
|
|
||||||
[:div.flex.justify-between.items-center [:h1.text-2xl.mb-3.font-bold "Import new invoices"]
|
|
||||||
|
|
||||||
[:div.flex.gap-2.items-baseline "Trouble with the new upload experience?"
|
|
||||||
[:a {:href (bidi/path-for client-routes/routes :import-invoices)}
|
|
||||||
(com/pill {:color :secondary}
|
|
||||||
"Go back to previous version")]]]
|
|
||||||
[:div#page-notification.notification.block {:style {:display "none"}}]
|
|
||||||
|
|
||||||
|
|
||||||
[:form
|
|
||||||
{:action (bidi/path-for ssr-routes/only-routes
|
|
||||||
::route/import-file)
|
|
||||||
:method "POST"
|
|
||||||
:id "upload"}
|
|
||||||
(fc/start-form
|
|
||||||
form-params form-errors
|
|
||||||
[:div.flex.gap-4
|
|
||||||
|
|
||||||
(fc/with-field :force-client
|
|
||||||
(com/validated-field {:label "Force client"
|
|
||||||
:errors (fc/field-errors)}
|
|
||||||
[:div.w-96
|
|
||||||
(com/typeahead {:name (fc/field-name)
|
|
||||||
:error? (fc/error?)
|
|
||||||
:class "w-96"
|
|
||||||
:placeholder "Search..."
|
|
||||||
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
|
||||||
:value (fc/field-value)
|
|
||||||
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))
|
|
||||||
(fc/with-field :force-location
|
|
||||||
(com/validated-field {:label "Force location"
|
|
||||||
:errors (fc/field-errors)}
|
|
||||||
|
|
||||||
(com/text-input {:name (fc/field-name)
|
|
||||||
:value (fc/field-value)
|
|
||||||
:size 2})))
|
|
||||||
(fc/with-field :force-vendor
|
|
||||||
(com/validated-field {:label "Force vendor"
|
|
||||||
:errors (fc/field-errors)}
|
|
||||||
[:div.w-96
|
|
||||||
(com/typeahead {:name (fc/field-name)
|
|
||||||
:error? (fc/error?)
|
|
||||||
:class "w-96"
|
|
||||||
:placeholder "Search..."
|
|
||||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
|
||||||
:value (fc/field-value)
|
|
||||||
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))])
|
|
||||||
[:div {":class" "a ? 'htmx-request' : ''"}
|
|
||||||
[:div.bg-blue-100.border-2.border-dashed.rounded-lg.border-blue-300.p-4.w-full.text-center.cursor-pointer.h-64.flex.items-center.justify-center.text-blue-700.text-lg
|
|
||||||
[:div.htmx-indicator.flex.items-center
|
|
||||||
(svg/spinner {:class "inline w-4 h-4 text-blue-700"})
|
|
||||||
[:div.ml-3 "Loading..."]]
|
|
||||||
[:div.htmx-indicator-hidden "Drop files to upload here"]]
|
|
||||||
[:script
|
|
||||||
(hiccup/raw
|
|
||||||
"ezcater_dropzone = new Dropzone (\"#upload\", {
|
|
||||||
init: function() {
|
|
||||||
this.on('addedfile', () => { document.getElementById('upload').dispatchEvent(new Event('started', {bubbles:true})) });
|
|
||||||
},
|
|
||||||
|
|
||||||
success: function (file, response) {
|
|
||||||
document.getElementById(\"page-notification\").innerHTML = response;
|
|
||||||
document.getElementById(\"page-notification\").style[\"display\"] = \"block\";
|
|
||||||
document.getElementById('upload').dispatchEvent(new Event('completed', {bubbles:true}))
|
|
||||||
document.body.dispatchEvent(new Event('invalidated'));
|
|
||||||
},
|
|
||||||
acceptedFiles: '.xls,.xlsx,.pdf,.csv',
|
|
||||||
disablePreviews: true
|
|
||||||
})
|
|
||||||
")]]]]))
|
|
||||||
|
|
||||||
:fetch-page fetch-page
|
:fetch-page fetch-page
|
||||||
:oob-render
|
:oob-render
|
||||||
@@ -550,7 +558,7 @@
|
|||||||
(count ids))
|
(count ids))
|
||||||
:invalidated "invalidated"})})))
|
:invalidated "invalidated"})})))
|
||||||
|
|
||||||
(defn upload-invoices [{{files :file
|
#_(defn upload-invoices [{{files :file
|
||||||
files-2 "file"
|
files-2 "file"
|
||||||
client :client
|
client :client
|
||||||
client-2 "client"
|
client-2 "client"
|
||||||
@@ -650,7 +658,8 @@
|
|||||||
|
|
||||||
|
|
||||||
(defn import-uploaded-invoice [user imports]
|
(defn import-uploaded-invoice [user imports]
|
||||||
(alog/info ::importing-uploaded :count (count imports))
|
(alog/info ::importing-uploaded :count (count imports)
|
||||||
|
:bc (or user "NOO"))
|
||||||
(let [potential-invoices (->> imports
|
(let [potential-invoices (->> imports
|
||||||
(map import->invoice)
|
(map import->invoice)
|
||||||
(map #(validate-invoice % user))
|
(map #(validate-invoice % user))
|
||||||
@@ -669,40 +678,71 @@
|
|||||||
{})))
|
{})))
|
||||||
tx)))
|
tx)))
|
||||||
|
|
||||||
(defn import-file [request]
|
(defn import-internal [tempfile filename force-client force-location force-vendor identity]
|
||||||
(let [{:keys [file force-client force-vendor force-location]} (:multipart-params request)
|
(mu/with-context {:parsing-file filename}
|
||||||
{:keys [filename tempfile]} file]
|
(try
|
||||||
|
(let [extension (last (str/split (.getName (io/file filename)) #"\."))
|
||||||
|
s3-location (str "invoice-files/" (str (UUID/randomUUID)) "." extension)
|
||||||
|
_ (s3/put-object (:data-bucket env)
|
||||||
|
s3-location
|
||||||
|
(io/input-stream tempfile)
|
||||||
|
{:content-type (if (= "csv" extension)
|
||||||
|
"text/csv"
|
||||||
|
"application/pdf")
|
||||||
|
:content-length (.length tempfile)})
|
||||||
|
imports (->> (parse/parse-file (.getPath tempfile) filename)
|
||||||
|
(map #(assoc %
|
||||||
|
:client-override force-client
|
||||||
|
:location-override force-location
|
||||||
|
:vendor-override force-vendor
|
||||||
|
:source-url (str "https://" (:data-bucket env)
|
||||||
|
"/"
|
||||||
|
s3-location))))]
|
||||||
|
(import-uploaded-invoice identity imports))
|
||||||
|
(catch Exception e
|
||||||
|
(alog/warn ::couldnt-import-upload
|
||||||
|
:error e)
|
||||||
|
(throw e)))))
|
||||||
|
|
||||||
(mu/with-context {:parsing-file filename}
|
(defn import-file [request]
|
||||||
(try
|
#_(html-response [:div])
|
||||||
(let [extension (last (str/split (.getName (io/file filename)) #"\."))
|
(let [{:keys [file force-client force-vendor force-location]} (:multipart-params request)
|
||||||
s3-location (str "invoice-files/" (str (UUID/randomUUID)) "." extension)
|
file (if (vector? file)
|
||||||
_ (s3/put-object (:data-bucket env)
|
file
|
||||||
s3-location
|
[file])
|
||||||
(io/input-stream tempfile)
|
results (reduce
|
||||||
{:content-type (if (= "csv" extension)
|
(fn [result {:keys [filename tempfile]}]
|
||||||
"text/csv"
|
(try
|
||||||
"application/pdf")
|
(import-internal tempfile filename force-client force-location force-vendor (:identity request) )
|
||||||
:content-length (.length tempfile)})
|
(update result :results conj {:filename filename
|
||||||
imports (->> (parse/parse-file (.getPath tempfile) filename)
|
:response "success!"})
|
||||||
(map #(assoc %
|
(catch Exception e
|
||||||
:client-override force-client
|
(-> result
|
||||||
:location-override force-location
|
(assoc :error? true)
|
||||||
:vendor-override force-vendor
|
(update :results conj {:filename filename
|
||||||
:source-url (str "https://" (:data-bucket env)
|
:response (.getMessage e)})))))
|
||||||
"/"
|
{:error? false :results []}
|
||||||
s3-location))))]
|
file)]
|
||||||
(import-uploaded-invoice (:identity request) imports))
|
(html-response [:div#page-notification.p-4.rounded-lg
|
||||||
(html-response [:div.bg-primary-50.p-4.text-primary-700.rounded-lg "Invoices have been successfully uploaded."])
|
{:class (if (:error? results)
|
||||||
(catch Exception e
|
"bg-red-50 text-red-700"
|
||||||
(alog/warn ::couldnt-import-upload
|
"bg-primary-50 text-primary-700")}
|
||||||
:error e)
|
[:table
|
||||||
(html-response [:div.bg-red-50.p-4.text-red-700.rounded-lg (.getMessage e)])
|
(for [r (:results results)]
|
||||||
#_{:status 400
|
[:tr
|
||||||
:body (pr-str {:message (.getMessage e)
|
[:td.p-2.border
|
||||||
:error (.toString e)
|
{:class (if (:error? results)
|
||||||
:data (ex-data e)})
|
"bg-red-50 text-red-700 border-red-300"
|
||||||
:headers {"Content-Type" "application/edn"}})))))
|
"bg-primary-50 text-primary-700 border-green-500")}
|
||||||
|
(:filename r)]
|
||||||
|
[:td.p-2.border
|
||||||
|
{:class (if (:error? results)
|
||||||
|
"bg-red-50 text-red-700 border-red-300"
|
||||||
|
"bg-primary-50 text-primary-700 border-green-500")}
|
||||||
|
(:response r)]])]]
|
||||||
|
:headers
|
||||||
|
{"hx-trigger" "invalidated"})
|
||||||
|
))
|
||||||
|
|
||||||
#_(defn wrap-test [handler]
|
#_(defn wrap-test [handler]
|
||||||
(fn [request]
|
(fn [request]
|
||||||
|
|||||||
Reference in New Issue
Block a user