Fase B7: CalDAV/CardDAV sync with Nextcloud, and a Calendar view type

- New built-in classes Event and Contact (schema 65.37), plus a
  Calendar View view type alongside table/list/gallery, backed by a new
  fixed month-grid calendar-view component in components/views.cljs
  reusing the existing scheduled/deadline properties.
- New worker/handler/caldav.cljs: a deliberately-scoped CalDAV
  (VEVENT/VTODO) + CardDAV (VCARD) client against a single Nextcloud
  calendar/addressbook collection, with its own minimal iCalendar/vCard
  encoder-decoder and namespace-agnostic multistatus XML extraction
  (no external ical/rrule/xml dependency exists in this project).
- RRULE <-> :logseq.property.repeat/* translation covers only the
  `plus` (advance-from-scheduled) repeat mode with a bare FREQ(;INTERVAL)
  rule -- dotted-plus/double-plus and anything with BYDAY/COUNT/UNTIL
  are left untranslated on purpose rather than mistranslated.
- Credentials live in the platform keychain (save-secret-text!), never
  in the graph. Sync runs Node-only, every 15 minutes per open repo,
  started/stopped alongside the repo's lifecycle in db_core.cljs.
  Conflicts are last-write-wins via ETag: a 412 on push is skipped and
  retried after the next pull refreshes the stored ETag.
This commit is contained in:
2026-08-26 21:11:00 +02:00
parent dadeca7165
commit 0158430489
12 changed files with 896 additions and 3 deletions

View File

@@ -115,6 +115,25 @@
{:title "Area"
:properties {:logseq.property.class/extends :logseq.class/Page}}
;; :logseq.property/scheduled and :logseq.property/deadline (the actual
;; event start/end) are pre-existing baseline properties, not declared
;; here in :schema :properties -- a migrated-in class can only resolve
;; :schema :properties against properties added in that SAME migration
;; step (see `build-initial-classes*`), so a page can still be tagged
;; Event and given scheduled/deadline like any other public property.
:logseq.class/Event
{:title "Event"
:properties {:logseq.property.class/extends :logseq.class/Page
:logseq.property/icon {:type :tabler-icon :id "calendar-event"}}
:schema {:properties [:logseq.property.event/location]}}
:logseq.class/Contact
{:title "Contact"
:properties {:logseq.property.class/extends :logseq.class/Page
:logseq.property/icon {:type :tabler-icon :id "user"}}
:schema {:properties [:logseq.property.contact/email :logseq.property.contact/phone
:logseq.property.contact/organization]}}
;; TODO: Add more classes such as :book, :paper, :movie, :music)
)))

View File

@@ -422,6 +422,36 @@
;; TODO: Add more props :Estimate, :Cycle, :Project
:logseq.property.event/location {:title "Location"
:schema {:type :string
:public? true}
:queryable? true}
:logseq.property.contact/email {:title "Email"
:schema {:type :string
:public? true}
:queryable? true}
:logseq.property.contact/phone {:title "Phone"
:schema {:type :string
:public? true}
:queryable? true}
:logseq.property.contact/organization {:title "Organization"
:schema {:type :string
:public? true}
:queryable? true}
;; CalDAV/CardDAV sync bookkeeping, not user-facing
:logseq.property.caldav/uid {:title "CalDAV UID"
:schema {:type :string
:hide? true
:public? false}}
:logseq.property.caldav/href {:title "CalDAV Href"
:schema {:type :string
:hide? true
:public? false}}
:logseq.property.caldav/etag {:title "CalDAV ETag"
:schema {:type :string
:hide? true
:public? false}}
:logseq.property/icon {:title "Icon"
:schema {:type :map}}
:logseq.property/publishing-public? {:title "Publishing Public?"
@@ -455,7 +485,8 @@
:icon {:type :tabler-icon :id icon}})
[[:logseq.property.view/type.table "Table View" "table"]
[:logseq.property.view/type.list "List View" "list"]
[:logseq.property.view/type.gallery "Gallery View" "layout-grid"]])
[:logseq.property.view/type.gallery "Gallery View" "layout-grid"]
[:logseq.property.view/type.calendar "Calendar View" "calendar"]])
:properties {:logseq.property/default-value :logseq.property.view/type.table}
:queryable? true}

View File

@@ -30,7 +30,7 @@
(map (juxt :major :minor)
[(parse-schema-version x) (parse-schema-version y)])))
(def version (parse-schema-version "65.36"))
(def version (parse-schema-version "65.37"))
(defn major-version
"Return a number.

View File

@@ -16,6 +16,18 @@
(is (not (contains? db-class/private-tags :logseq.class/Project)))
(is (not (contains? db-class/private-tags :logseq.class/Area)))))
(deftest event-and-contact-are-built-in-page-classes
(testing "Event and Contact are registered as built-in classes that behave like pages"
(is (contains? db-class/built-in-classes :logseq.class/Event))
(is (contains? db-class/built-in-classes :logseq.class/Contact))
(is (contains? db-class/page-children-classes :logseq.class/Event))
(is (contains? db-class/page-children-classes :logseq.class/Contact))
(is (not (contains? db-class/private-tags :logseq.class/Event)))
(is (not (contains? db-class/private-tags :logseq.class/Contact))))
(testing "Event's schema properties only reference properties migrated in the same step"
(is (= [:logseq.property.event/location]
(get-in db-class/built-in-classes [:logseq.class/Event :schema :properties])))))
(deftest get-class-objects-dedupes-inherited-tags-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Parent {:block/title "Parent"}