|
|
@@ -1,25 +1,58 @@
|
|
|
(ns microtables-frontend.events
|
|
|
(:require
|
|
|
+ [clojure.string :as string]
|
|
|
[microtables-frontend.db :as db]
|
|
|
[microtables-frontend.evaluation :as eval]
|
|
|
[microtables-frontend.utils.coordinates :as coords]
|
|
|
[microtables-frontend.utils.data :as data-utils]
|
|
|
+ [microtables-frontend.utils.storage :as storage]
|
|
|
[re-frame.core :as re-frame]))
|
|
|
|
|
|
+(defn rebuild-table-data
|
|
|
+ "Rebuilds :refs/:inbound/:dirty/:display for a table-data map from :value alone.
|
|
|
+ Used both at boot and when opening a saved table, so persisted state never needs
|
|
|
+ to carry (or trust) the derived fields - only :value round-trips through storage."
|
|
|
+ [table-data]
|
|
|
+ (-> table-data
|
|
|
+ (data-utils/walk-modify-data
|
|
|
+ (fn [_c _r datum]
|
|
|
+ (if (data-utils/formula? (:value datum))
|
|
|
+ (assoc datum :dirty true)
|
|
|
+ datum)))
|
|
|
+ data-utils/create-all-references
|
|
|
+ eval/create-all-back-references
|
|
|
+ eval/evaluate-all))
|
|
|
+
|
|
|
+(defn- fresh-active-table
|
|
|
+ []
|
|
|
+ {:id (storage/new-table-id) :name (storage/default-table-name)})
|
|
|
+
|
|
|
+; The read side of local storage: kept here (rather than as reg-cofx, which this
|
|
|
+; app has no other precedent for) since these are simple one-shot reads, matching
|
|
|
+; the existing reg-fx-only style used for :focus-on-cell below.
|
|
|
+(defn- read-index
|
|
|
+ []
|
|
|
+ (or (storage/deserialize (.getItem js/localStorage storage/index-key)) []))
|
|
|
+
|
|
|
+(defn- write-index!
|
|
|
+ [index]
|
|
|
+ (.setItem js/localStorage storage/index-key (storage/serialize index)))
|
|
|
+
|
|
|
+(defn- read-table-data
|
|
|
+ [id]
|
|
|
+ (:table-data (storage/deserialize (.getItem js/localStorage (storage/table-key id)))))
|
|
|
+
|
|
|
(re-frame/reg-event-db
|
|
|
::initialize-db
|
|
|
- (fn [_ _]
|
|
|
+ (fn [_ [_ restored]]
|
|
|
(println "initializing db")
|
|
|
- (-> db/default-db
|
|
|
- (update-in [:table-data] #(data-utils/walk-modify-data
|
|
|
- %
|
|
|
- (fn [_c _r datum]
|
|
|
- (if (= (first (:value datum)) "=")
|
|
|
- (assoc datum :dirty true)
|
|
|
- datum))))
|
|
|
- (update-in [:table-data] data-utils/create-all-references)
|
|
|
- (update-in [:table-data] eval/create-all-back-references)
|
|
|
- (update-in [:table-data] eval/evaluate-all))))
|
|
|
+ (if restored
|
|
|
+ (-> db/default-db
|
|
|
+ (assoc :table-data (rebuild-table-data (:table-data restored)))
|
|
|
+ (assoc :active-table {:id (:id restored) :name (:name restored)}))
|
|
|
+ (-> db/default-db
|
|
|
+ (update :table-data rebuild-table-data)
|
|
|
+ (assoc :active-table (fresh-active-table))))))
|
|
|
|
|
|
(re-frame/reg-event-db
|
|
|
::movement-enter-cell
|
|
|
@@ -27,15 +60,17 @@
|
|
|
(println "::movement-enter-cell" c r)
|
|
|
(assoc-in db [:position :cursor] {:col c :row r})))
|
|
|
|
|
|
-(re-frame/reg-event-db
|
|
|
+(re-frame/reg-event-fx
|
|
|
::movement-leave-cell
|
|
|
- (fn [db [_ c r]]
|
|
|
+ (fn [{:keys [db]} [_ c r]]
|
|
|
(println "::movement-leave-cell" c r)
|
|
|
- (-> db
|
|
|
- (assoc-in [:position :cursor] nil)
|
|
|
- (assoc-in [:position :selection] nil)
|
|
|
- (update-in [:table-data] #(eval/reset-references % c r))
|
|
|
- (update-in [:table-data] #(eval/evaluate-from-cell % c r)))))
|
|
|
+ (let [new-db (-> db
|
|
|
+ (assoc-in [:position :cursor] nil)
|
|
|
+ (assoc-in [:position :selection] nil)
|
|
|
+ (update-in [:table-data] #(eval/reset-references % c r))
|
|
|
+ (update-in [:table-data] #(eval/evaluate-from-cell % c r)))]
|
|
|
+ {:db new-db
|
|
|
+ :save-table! {:table (:active-table new-db) :table-data (:table-data new-db)}})))
|
|
|
|
|
|
(re-frame/reg-event-db
|
|
|
::edit-cell-value
|
|
|
@@ -95,3 +130,150 @@
|
|
|
::cycle-totals-mode
|
|
|
(fn [db _]
|
|
|
(update db :totals-mode totals-mode-cycle)))
|
|
|
+
|
|
|
+;; ── Local storage table persistence (todo 11.1) ──
|
|
|
+
|
|
|
+; Writes a table's data (skipping tables that have never been saved and are
|
|
|
+; still empty) and touches its index entry's :last-opened-at. Registered under
|
|
|
+; two effect keys (:save-table! and :touch-table!) so a single event can fire
|
|
|
+; it twice with different args - this re-frame version has no built-in :fx
|
|
|
+; vector-of-effects to repeat one key.
|
|
|
+(defn- save-table-fx!
|
|
|
+ [{:keys [table table-data]}]
|
|
|
+ (let [index (read-index)
|
|
|
+ already-saved? (some #(= (:id %) (:id table)) index)]
|
|
|
+ (when (or already-saved? (not (storage/table-empty? table-data)))
|
|
|
+ (write-index! (storage/save-entry index (:id table) (:name table) (storage/now-ms)))
|
|
|
+ (.setItem js/localStorage (storage/table-key (:id table)) (storage/serialize {:table-data table-data})))))
|
|
|
+
|
|
|
+(re-frame/reg-fx :save-table! save-table-fx!)
|
|
|
+(re-frame/reg-fx :touch-table! save-table-fx!)
|
|
|
+
|
|
|
+(re-frame/reg-fx
|
|
|
+ :save-tables-index!
|
|
|
+ (fn [index]
|
|
|
+ (write-index! index)))
|
|
|
+
|
|
|
+(re-frame/reg-fx
|
|
|
+ :remove-table!
|
|
|
+ (fn [id]
|
|
|
+ (.removeItem js/localStorage (storage/table-key id))
|
|
|
+ (write-index! (storage/remove-entry (read-index) id))))
|
|
|
+
|
|
|
+(re-frame/reg-event-db
|
|
|
+ ::open-table-modal
|
|
|
+ (fn [db _]
|
|
|
+ (-> db
|
|
|
+ (assoc-in [:table-modal :open?] true)
|
|
|
+ (assoc-in [:table-modal :tab] :tables)
|
|
|
+ (assoc-in [:table-modal :tables] (storage/sort-by-recency (read-index))))))
|
|
|
+
|
|
|
+(re-frame/reg-event-db
|
|
|
+ ::close-table-modal
|
|
|
+ (fn [db _]
|
|
|
+ (-> db
|
|
|
+ (assoc-in [:table-modal :open?] false)
|
|
|
+ (assoc-in [:table-modal :menu-open] nil)
|
|
|
+ (assoc-in [:table-modal :editing] nil)
|
|
|
+ (assoc-in [:table-modal :pending-delete] nil))))
|
|
|
+
|
|
|
+(re-frame/reg-event-db
|
|
|
+ ::set-table-modal-tab
|
|
|
+ (fn [db [_ tab]]
|
|
|
+ (assoc-in db [:table-modal :tab] tab)))
|
|
|
+
|
|
|
+(re-frame/reg-event-db
|
|
|
+ ::toggle-table-menu
|
|
|
+ (fn [db [_ table-id]]
|
|
|
+ (update-in db [:table-modal :menu-open] #(if (= % table-id) nil table-id))))
|
|
|
+
|
|
|
+(re-frame/reg-event-fx
|
|
|
+ ::new-table
|
|
|
+ (fn [{:keys [db]} _]
|
|
|
+ {:db (assoc db
|
|
|
+ :table-data nil
|
|
|
+ :position (:position db/default-db)
|
|
|
+ :active-table (fresh-active-table))
|
|
|
+ :save-table! {:table (:active-table db) :table-data (:table-data db)}}))
|
|
|
+
|
|
|
+(re-frame/reg-event-fx
|
|
|
+ ::switch-to-table
|
|
|
+ (fn [{:keys [db]} [_ table-id]]
|
|
|
+ (let [entries (get-in db [:table-modal :tables])
|
|
|
+ entry (first (filter #(= (:id %) table-id) entries))
|
|
|
+ restored (read-table-data table-id)
|
|
|
+ rebuilt (rebuild-table-data restored)
|
|
|
+ new-active {:id table-id :name (:name entry)}]
|
|
|
+ {:db (-> db
|
|
|
+ (assoc :table-data rebuilt)
|
|
|
+ (assoc :position (:position db/default-db))
|
|
|
+ (assoc :active-table new-active)
|
|
|
+ (assoc-in [:table-modal :open?] false)
|
|
|
+ (assoc-in [:table-modal :menu-open] nil))
|
|
|
+ ; save the outgoing table (if it warrants saving), then touch the incoming
|
|
|
+ ; table's :last-opened-at so it sorts to the top next time the modal opens
|
|
|
+ ; (this re-frame version has no built-in :fx vector-of-effects, so these
|
|
|
+ ; are two distinct effect keys sharing one handler, not one repeated key)
|
|
|
+ :save-table! {:table (:active-table db) :table-data (:table-data db)}
|
|
|
+ :touch-table! {:table new-active :table-data restored}})))
|
|
|
+
|
|
|
+(re-frame/reg-event-db
|
|
|
+ ::start-rename-table
|
|
|
+ (fn [db [_ table-id current-name]]
|
|
|
+ (-> db
|
|
|
+ (assoc-in [:table-modal :editing] {:id table-id :value current-name})
|
|
|
+ (assoc-in [:table-modal :menu-open] nil))))
|
|
|
+
|
|
|
+(re-frame/reg-event-db
|
|
|
+ ::update-rename-value
|
|
|
+ (fn [db [_ value]]
|
|
|
+ (assoc-in db [:table-modal :editing :value] value)))
|
|
|
+
|
|
|
+(re-frame/reg-event-db
|
|
|
+ ::cancel-rename-table
|
|
|
+ (fn [db _]
|
|
|
+ (assoc-in db [:table-modal :editing] nil)))
|
|
|
+
|
|
|
+(re-frame/reg-event-fx
|
|
|
+ ::confirm-rename-table
|
|
|
+ (fn [{:keys [db]} _]
|
|
|
+ (let [{:keys [id value]} (get-in db [:table-modal :editing])
|
|
|
+ trimmed (string/trim (or value ""))]
|
|
|
+ (if (or (nil? id) (string/blank? trimmed))
|
|
|
+ {:db (assoc-in db [:table-modal :editing] nil)}
|
|
|
+ (let [updated-tables (storage/rename-entry (get-in db [:table-modal :tables]) id trimmed)
|
|
|
+ active? (= id (get-in db [:active-table :id]))
|
|
|
+ new-db (cond-> (-> db
|
|
|
+ (assoc-in [:table-modal :tables] updated-tables)
|
|
|
+ (assoc-in [:table-modal :editing] nil))
|
|
|
+ active? (assoc-in [:active-table :name] trimmed))]
|
|
|
+ {:db new-db
|
|
|
+ :save-tables-index! updated-tables})))))
|
|
|
+
|
|
|
+(re-frame/reg-event-db
|
|
|
+ ::request-delete-table
|
|
|
+ (fn [db [_ table-id]]
|
|
|
+ (-> db
|
|
|
+ (assoc-in [:table-modal :pending-delete] table-id)
|
|
|
+ (assoc-in [:table-modal :menu-open] nil))))
|
|
|
+
|
|
|
+(re-frame/reg-event-db
|
|
|
+ ::cancel-delete-table
|
|
|
+ (fn [db _]
|
|
|
+ (assoc-in db [:table-modal :pending-delete] nil)))
|
|
|
+
|
|
|
+(re-frame/reg-event-fx
|
|
|
+ ::confirm-delete-table
|
|
|
+ (fn [{:keys [db]} [_ table-id]]
|
|
|
+ (let [was-active? (= table-id (get-in db [:active-table :id]))
|
|
|
+ updated-tables (storage/remove-entry (get-in db [:table-modal :tables]) table-id)
|
|
|
+ base-db (-> db
|
|
|
+ (assoc-in [:table-modal :tables] updated-tables)
|
|
|
+ (assoc-in [:table-modal :pending-delete] nil))]
|
|
|
+ {:db (if was-active?
|
|
|
+ (assoc base-db
|
|
|
+ :table-data nil
|
|
|
+ :position (:position db/default-db)
|
|
|
+ :active-table (fresh-active-table))
|
|
|
+ base-db)
|
|
|
+ :remove-table! table-id})))
|