Adopt Step 7 language features across SX codebase
112 conversions across 19 .sx files using match, let-match, and pipe operators: match (17): type/value dispatch replacing cond/if chains - lib/vm.sx: HO form dispatch (for-each/map/filter/reduce/some/every?) - lib/tree-tools.sx: node-display, node-matches?, rename, count, replace, free-symbols - lib/types.sx: narrow-type, substitute-in-type, infer-type, resolve-type - web/engine.sx: default-trigger, resolve-target, classify-trigger - web/deps.sx: scan-refs-walk, scan-io-refs-walk let-match (89): dict destructuring replacing (get d "key") patterns - shared/page-functions.sx (20), blog/admin.sx (17), pub-api.sx (13) - events/ layouts/page/tickets/entries/forms (27 total) - specs-explorer.sx (7), federation/social.sx (3), lib/ small files (3) -> pipes (6): replacing triple-chained gets in lib/vm.sx - frame-closure → closure-code → code-bytecode chains Also: lib/vm.sx accessor upgrades (get vm "sp" → vm-sp vm throughout) 2650/2650 tests pass, zero regressions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
703
blog/sx/admin.sx
703
blog/sx/admin.sx
@@ -144,78 +144,140 @@
|
||||
edit-form delete-form))
|
||||
|
||||
;; Data-driven snippets list (replaces Python _snippets_sx loop)
|
||||
(defcomp ~admin/snippets-from-data (&key snippets user-id is-admin csrf badge-colours)
|
||||
(defcomp
|
||||
~admin/snippets-from-data
|
||||
(&key snippets user-id is-admin csrf badge-colours)
|
||||
(~admin/snippets-list
|
||||
:rows (<> (map (lambda (s)
|
||||
(let* ((s-id (get s "id"))
|
||||
(s-name (get s "name"))
|
||||
(s-uid (get s "user_id"))
|
||||
(s-vis (get s "visibility"))
|
||||
(owner (if (= s-uid user-id) "You" (str "User #" s-uid)))
|
||||
(badge-cls (or (get badge-colours s-vis) "bg-stone-200 text-stone-700"))
|
||||
(extra (<>
|
||||
(when is-admin
|
||||
(~admin/snippet-visibility-select
|
||||
:patch-url (get s "patch_url")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:options (<>
|
||||
(~admin/snippet-option :value "private" :selected (= s-vis "private") :label "private")
|
||||
(~admin/snippet-option :value "shared" :selected (= s-vis "shared") :label "shared")
|
||||
(~admin/snippet-option :value "admin" :selected (= s-vis "admin") :label "admin"))
|
||||
:cls "text-sm border border-stone-300 rounded px-2 py-1"))
|
||||
(when (or (= s-uid user-id) is-admin)
|
||||
(~shared:misc/delete-btn :url (get s "delete_url") :trigger-target "#snippets-list"
|
||||
:title "Delete snippet?"
|
||||
:text (str "Delete \u201c" s-name "\u201d?")
|
||||
:sx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0")))))
|
||||
(~admin/snippet-row :name s-name :owner owner :badge-cls badge-cls
|
||||
:visibility s-vis :extra extra)))
|
||||
(or snippets (list))))))
|
||||
:rows (<>
|
||||
(map
|
||||
(lambda
|
||||
(s)
|
||||
(let-match
|
||||
{:visibility s-vis :delete_url delete-url :patch_url patch-url :id s-id :user_id s-uid :name s-name}
|
||||
s
|
||||
(let*
|
||||
((owner (if (= s-uid user-id) "You" (str "User #" s-uid)))
|
||||
(badge-cls
|
||||
(or (get badge-colours s-vis) "bg-stone-200 text-stone-700"))
|
||||
(extra
|
||||
(<>
|
||||
(when
|
||||
is-admin
|
||||
(~admin/snippet-visibility-select
|
||||
:patch-url patch-url
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:options (<>
|
||||
(~admin/snippet-option
|
||||
:value "private"
|
||||
:selected (= s-vis "private")
|
||||
:label "private")
|
||||
(~admin/snippet-option
|
||||
:value "shared"
|
||||
:selected (= s-vis "shared")
|
||||
:label "shared")
|
||||
(~admin/snippet-option
|
||||
:value "admin"
|
||||
:selected (= s-vis "admin")
|
||||
:label "admin"))
|
||||
:cls "text-sm border border-stone-300 rounded px-2 py-0.5"))
|
||||
(when
|
||||
(or (= s-uid user-id) is-admin)
|
||||
(~shared:misc/delete-btn
|
||||
:url delete-url
|
||||
:trigger-target "#snippets-list"
|
||||
:title "Delete snippet?"
|
||||
:text (str "Delete \"" s-name "\"?")
|
||||
:sx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 text-red-800 rounded-full")))))
|
||||
(~admin/snippet-row
|
||||
:name s-name
|
||||
:owner owner
|
||||
:badge-cls badge-cls
|
||||
:visibility s-vis
|
||||
:extra extra))))
|
||||
(or snippets (list))))))
|
||||
|
||||
;; Data-driven menu items list (replaces Python _menu_items_list_sx loop)
|
||||
(defcomp ~admin/menu-items-from-data (&key items csrf)
|
||||
(defcomp
|
||||
~admin/menu-items-from-data
|
||||
(&key items csrf)
|
||||
(~admin/menu-items-list
|
||||
:rows (<> (map (lambda (item)
|
||||
(let* ((img (~shared:misc/img-or-placeholder :src (get item "feature_image") :alt (get item "label")
|
||||
:size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")))
|
||||
(~admin/menu-item-row
|
||||
:img img :label (get item "label") :slug (get item "slug")
|
||||
:sort-order (get item "sort_order") :edit-url (get item "edit_url")
|
||||
:delete-url (get item "delete_url")
|
||||
:confirm-text (str "Remove " (get item "label") " from the menu?")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}"))))
|
||||
(or items (list))))))
|
||||
:rows (<>
|
||||
(map
|
||||
(lambda
|
||||
(item)
|
||||
(let-match
|
||||
{:delete_url delete-url :sort_order sort-order :edit_url edit-url :feature_image feature-image :label label :slug slug}
|
||||
item
|
||||
(let
|
||||
((img (~shared:misc/img-or-placeholder :src feature-image :alt label :size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")))
|
||||
(~admin/menu-item-row
|
||||
:img img
|
||||
:label label
|
||||
:slug slug
|
||||
:sort-order sort-order
|
||||
:edit-url edit-url
|
||||
:delete-url delete-url
|
||||
:confirm-text (str "Remove " label " from the menu?")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")))))
|
||||
(or items (list))))))
|
||||
|
||||
;; Data-driven tag groups main (replaces Python _tag_groups_main_panel_sx loops)
|
||||
(defcomp ~admin/tag-groups-from-data (&key groups unassigned-tags csrf create-url)
|
||||
(defcomp
|
||||
~admin/tag-groups-from-data
|
||||
(&key groups unassigned-tags csrf create-url)
|
||||
(~admin/tag-groups-main
|
||||
:form (~admin/tag-groups-create-form :create-url create-url :csrf csrf)
|
||||
:groups (if (empty? (or groups (list)))
|
||||
(~shared:misc/empty-state :message "No tag groups yet." :cls "text-stone-500 text-sm")
|
||||
:groups (if
|
||||
(empty? (or groups (list)))
|
||||
(~shared:misc/empty-state
|
||||
:message "No tag groups yet."
|
||||
:cls "text-stone-500 text-sm")
|
||||
(~admin/tag-groups-list
|
||||
:items (<> (map (lambda (g)
|
||||
(let* ((icon (if (get g "feature_image")
|
||||
(~admin/tag-group-icon-image :src (get g "feature_image") :name (get g "name"))
|
||||
(~admin/tag-group-icon-color :style (get g "style") :initial (get g "initial")))))
|
||||
(~admin/tag-group-li :icon icon :edit-href (get g "edit_href")
|
||||
:name (get g "name") :slug (get g "slug") :sort-order (get g "sort_order"))))
|
||||
groups))))
|
||||
:unassigned (when (not (empty? (or unassigned-tags (list))))
|
||||
:items (<>
|
||||
(map
|
||||
(lambda
|
||||
(g)
|
||||
(let-match
|
||||
{:sort_order sort-order :feature_image feature-image :slug slug :edit_href edit-href :initial initial :name name :style style}
|
||||
g
|
||||
(let
|
||||
((icon (if feature-image (~admin/tag-group-icon-image :src feature-image :name name) (~admin/tag-group-icon-color :style style :initial initial))))
|
||||
(~admin/tag-group-li
|
||||
:icon icon
|
||||
:edit-href edit-href
|
||||
:name name
|
||||
:slug slug
|
||||
:sort-order sort-order))))
|
||||
groups))))
|
||||
:unassigned (when
|
||||
(not (empty? (or unassigned-tags (list))))
|
||||
(~admin/unassigned-tags
|
||||
:heading (str "Unassigned Tags (" (len unassigned-tags) ")")
|
||||
:spans (<> (map (lambda (t)
|
||||
(~admin/unassigned-tag :name (get t "name")))
|
||||
unassigned-tags))))))
|
||||
:spans (<>
|
||||
(map
|
||||
(lambda (t) (~admin/unassigned-tag :name (get t "name")))
|
||||
unassigned-tags))))))
|
||||
|
||||
;; Data-driven tag group edit (replaces Python _tag_groups_edit_main_panel_sx loop)
|
||||
(defcomp ~admin/tag-checkboxes-from-data (&key tags)
|
||||
(<> (map (lambda (t)
|
||||
(~admin/tag-checkbox
|
||||
:tag-id (get t "tag_id") :checked (get t "checked")
|
||||
:img (when (get t "feature_image") (~admin/tag-checkbox-image :src (get t "feature_image")))
|
||||
:name (get t "name")))
|
||||
(or tags (list)))))
|
||||
(defcomp
|
||||
~admin/tag-checkboxes-from-data
|
||||
(&key tags)
|
||||
(<>
|
||||
(map
|
||||
(lambda
|
||||
(t)
|
||||
(let-match
|
||||
{:tag_id tag-id :checked checked :feature_image feature-image :name name}
|
||||
t
|
||||
(~admin/tag-checkbox
|
||||
:tag-id tag-id
|
||||
:checked checked
|
||||
:img (when
|
||||
feature-image
|
||||
(~admin/tag-checkbox-image :src feature-image))
|
||||
:name name)))
|
||||
(or tags (list)))))
|
||||
|
||||
;; Preview panel components
|
||||
|
||||
@@ -258,113 +320,175 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Snippets — receives serialized snippet dicts from service
|
||||
(defcomp ~admin/snippets-content (&key snippets is-admin csrf)
|
||||
(defcomp
|
||||
~admin/snippets-content
|
||||
(&key snippets is-admin csrf)
|
||||
(~admin/snippets-panel
|
||||
:list (if (empty? (or snippets (list)))
|
||||
(~shared:misc/empty-state :icon "fa fa-puzzle-piece"
|
||||
:list (if
|
||||
(empty? (or snippets (list)))
|
||||
(~shared:misc/empty-state
|
||||
:icon "fa fa-puzzle-piece"
|
||||
:message "No snippets yet. Create one from the blog editor.")
|
||||
(~admin/snippets-list
|
||||
:rows (map (lambda (s)
|
||||
(let* ((badge-colours (dict
|
||||
"private" "bg-stone-200 text-stone-700"
|
||||
"shared" "bg-blue-100 text-blue-700"
|
||||
"admin" "bg-amber-100 text-amber-700"))
|
||||
(vis (or (get s "visibility") "private"))
|
||||
(badge-cls (or (get badge-colours vis) "bg-stone-200 text-stone-700"))
|
||||
(name (get s "name"))
|
||||
(owner (get s "owner"))
|
||||
(can-delete (get s "can_delete")))
|
||||
(~admin/snippet-row
|
||||
:name name :owner owner :badge-cls badge-cls :visibility vis
|
||||
:extra (<>
|
||||
(when is-admin
|
||||
(~admin/snippet-visibility-select
|
||||
:patch-url (get s "patch_url")
|
||||
:hx-headers {:X-CSRFToken csrf}
|
||||
:options (<>
|
||||
(~admin/snippet-option :value "private" :selected (= vis "private") :label "private")
|
||||
(~admin/snippet-option :value "shared" :selected (= vis "shared") :label "shared")
|
||||
(~admin/snippet-option :value "admin" :selected (= vis "admin") :label "admin"))))
|
||||
(when can-delete
|
||||
(~shared:misc/delete-btn
|
||||
:url (get s "delete_url")
|
||||
:trigger-target "#snippets-list"
|
||||
:title "Delete snippet?"
|
||||
:text (str "Delete \u201c" name "\u201d?")
|
||||
:sx-headers {:X-CSRFToken csrf}
|
||||
:cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0"))))))
|
||||
:rows (map
|
||||
(lambda
|
||||
(s)
|
||||
(let-match
|
||||
{:visibility vis* :delete_url delete-url :owner owner :can_delete can-delete :patch_url patch-url :name name}
|
||||
s
|
||||
(let*
|
||||
((vis (or vis* "private"))
|
||||
(badge-colours
|
||||
(dict
|
||||
"private"
|
||||
"bg-stone-200 text-stone-700"
|
||||
"shared"
|
||||
"bg-blue-100 text-blue-700"
|
||||
"admin"
|
||||
"bg-amber-100 text-amber-700"))
|
||||
(badge-cls
|
||||
(or (get badge-colours vis) "bg-stone-200 text-stone-700")))
|
||||
(~admin/snippet-row
|
||||
:name name
|
||||
:owner owner
|
||||
:badge-cls badge-cls
|
||||
:visibility vis
|
||||
:extra (<>
|
||||
(when
|
||||
is-admin
|
||||
(~admin/snippet-visibility-select
|
||||
:patch-url patch-url
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:options (<>
|
||||
(~admin/snippet-option
|
||||
:value "private"
|
||||
:selected (= vis "private")
|
||||
:label "private")
|
||||
(~admin/snippet-option
|
||||
:value "shared"
|
||||
:selected (= vis "shared")
|
||||
:label "shared")
|
||||
(~admin/snippet-option
|
||||
:value "admin"
|
||||
:selected (= vis "admin")
|
||||
:label "admin"))))
|
||||
(when
|
||||
can-delete
|
||||
(~shared:misc/delete-btn
|
||||
:url delete-url
|
||||
:trigger-target "#snippets-list"
|
||||
:title "Delete snippet?"
|
||||
:text (str "Delete \"" name "\"?")
|
||||
:sx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 text-red-800 rounded-full")))))))
|
||||
(or snippets (list)))))))
|
||||
|
||||
;; Menu Items — receives serialized menu item dicts from service
|
||||
(defcomp ~admin/menu-items-content (&key menu-items new-url csrf)
|
||||
(defcomp
|
||||
~admin/menu-items-content
|
||||
(&key menu-items new-url csrf)
|
||||
(~admin/menu-items-panel
|
||||
:new-url new-url
|
||||
:list (if (empty? (or menu-items (list)))
|
||||
(~shared:misc/empty-state :icon "fa fa-inbox"
|
||||
:list (if
|
||||
(empty? (or menu-items (list)))
|
||||
(~shared:misc/empty-state
|
||||
:icon "fa fa-inbox"
|
||||
:message "No menu items yet. Add one to get started!")
|
||||
(~admin/menu-items-list
|
||||
:rows (map (lambda (mi)
|
||||
(~admin/menu-item-row
|
||||
:img (~shared:misc/img-or-placeholder
|
||||
:src (get mi "feature_image") :alt (get mi "label")
|
||||
:size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")
|
||||
:label (get mi "label")
|
||||
:slug (get mi "url")
|
||||
:sort-order (str (or (get mi "sort_order") 0))
|
||||
:edit-url (get mi "edit_url")
|
||||
:delete-url (get mi "delete_url")
|
||||
:confirm-text (str "Remove " (get mi "label") " from the menu?")
|
||||
:hx-headers {:X-CSRFToken csrf}))
|
||||
:rows (map
|
||||
(lambda
|
||||
(mi)
|
||||
(let-match
|
||||
{:delete_url delete-url :url url :sort_order sort-order :edit_url edit-url :feature_image feature-image :label label}
|
||||
mi
|
||||
(~admin/menu-item-row
|
||||
:img (~shared:misc/img-or-placeholder
|
||||
:src feature-image
|
||||
:alt label
|
||||
:size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")
|
||||
:label label
|
||||
:slug url
|
||||
:sort-order (str (or sort-order 0))
|
||||
:edit-url edit-url
|
||||
:delete-url delete-url
|
||||
:confirm-text (str "Remove " label " from the menu?")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}"))))
|
||||
(or menu-items (list)))))))
|
||||
|
||||
;; Tag Groups — receives serialized tag group data from service
|
||||
(defcomp ~admin/tag-groups-content (&key groups unassigned-tags create-url csrf)
|
||||
(defcomp
|
||||
~admin/tag-groups-content
|
||||
(&key groups unassigned-tags create-url csrf)
|
||||
(~admin/tag-groups-main
|
||||
:form (~admin/tag-groups-create-form :create-url create-url :csrf csrf)
|
||||
:groups (if (empty? (or groups (list)))
|
||||
(~shared:misc/empty-state :icon "fa fa-tags" :message "No tag groups yet.")
|
||||
:groups (if
|
||||
(empty? (or groups (list)))
|
||||
(~shared:misc/empty-state
|
||||
:icon "fa fa-tags"
|
||||
:message "No tag groups yet.")
|
||||
(~admin/tag-groups-list
|
||||
:items (map (lambda (g)
|
||||
(let* ((fi (get g "feature_image"))
|
||||
(colour (get g "colour"))
|
||||
(name (get g "name"))
|
||||
(initial (slice (or name "?") 0 1))
|
||||
(icon (if fi
|
||||
(~admin/tag-group-icon-image :src fi :name name)
|
||||
(~admin/tag-group-icon-color
|
||||
:style (if colour (str "background:" colour) "background:#e7e5e4")
|
||||
:initial initial))))
|
||||
(~admin/tag-group-li
|
||||
:icon icon
|
||||
:edit-href (get g "edit_href")
|
||||
:name name
|
||||
:slug (or (get g "slug") "")
|
||||
:sort-order (or (get g "sort_order") 0))))
|
||||
:items (map
|
||||
(lambda
|
||||
(g)
|
||||
(let-match
|
||||
{:colour colour :sort_order sort-order* :feature_image fi :edit_href edit-href :slug slug* :name name}
|
||||
g
|
||||
(let*
|
||||
((initial (slice (or name "?") 0 1))
|
||||
(icon
|
||||
(if
|
||||
fi
|
||||
(~admin/tag-group-icon-image :src fi :name name)
|
||||
(~admin/tag-group-icon-color
|
||||
:style (if
|
||||
colour
|
||||
(str "background:" colour)
|
||||
"background:#e7e5e4")
|
||||
:initial initial))))
|
||||
(~admin/tag-group-li
|
||||
:icon icon
|
||||
:edit-href edit-href
|
||||
:name name
|
||||
:slug (or slug* "")
|
||||
:sort-order (or sort-order* 0)))))
|
||||
(or groups (list)))))
|
||||
:unassigned (when (not (empty? (or unassigned-tags (list))))
|
||||
:unassigned (when
|
||||
(not (empty? (or unassigned-tags (list))))
|
||||
(~admin/unassigned-tags
|
||||
:heading (str (len (or unassigned-tags (list))) " Unassigned Tags")
|
||||
:spans (map (lambda (t)
|
||||
(~admin/unassigned-tag :name (get t "name")))
|
||||
:spans (map
|
||||
(lambda (t) (~admin/unassigned-tag :name (get t "name")))
|
||||
(or unassigned-tags (list)))))))
|
||||
|
||||
;; Tag Group Edit — receives serialized tag group + tags from service
|
||||
(defcomp ~admin/tag-group-edit-content (&key group all-tags save-url delete-url csrf)
|
||||
(defcomp
|
||||
~admin/tag-group-edit-content
|
||||
(&key group all-tags save-url delete-url csrf)
|
||||
(~admin/tag-group-edit-main
|
||||
:edit-form (~admin/tag-group-edit-form
|
||||
:save-url save-url :csrf csrf
|
||||
:name (get group "name")
|
||||
:colour (get group "colour")
|
||||
:sort-order (get group "sort_order")
|
||||
:feature-image (get group "feature_image")
|
||||
:tags (map (lambda (t)
|
||||
(~admin/tag-checkbox
|
||||
:tag-id (get t "id")
|
||||
:checked (get t "checked")
|
||||
:img (when (get t "feature_image")
|
||||
(~admin/tag-checkbox-image :src (get t "feature_image")))
|
||||
:name (get t "name")))
|
||||
(or all-tags (list))))
|
||||
:edit-form (let-match
|
||||
{:colour colour :sort_order sort-order :feature_image feature-image :name name}
|
||||
group
|
||||
(~admin/tag-group-edit-form
|
||||
:save-url save-url
|
||||
:csrf csrf
|
||||
:name name
|
||||
:colour colour
|
||||
:sort-order sort-order
|
||||
:feature-image feature-image
|
||||
:tags (map
|
||||
(lambda
|
||||
(t)
|
||||
(let-match
|
||||
{:checked checked :feature_image t-feature-image :id tag-id :name t-name}
|
||||
t
|
||||
(~admin/tag-checkbox
|
||||
:tag-id tag-id
|
||||
:checked checked
|
||||
:img (when
|
||||
t-feature-image
|
||||
(~admin/tag-checkbox-image :src t-feature-image))
|
||||
:name t-name)))
|
||||
(or all-tags (list)))))
|
||||
:delete-form (~admin/tag-group-delete-form :delete-url delete-url :csrf csrf)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -400,31 +524,54 @@
|
||||
(code value)
|
||||
value))))
|
||||
|
||||
(defcomp ~admin/data-scalar-table (&key columns)
|
||||
(div :class "w-full overflow-x-auto sm:overflow-visible"
|
||||
(table :class "w-full table-fixed text-sm border border-neutral-200 rounded-xl overflow-hidden"
|
||||
(thead :class "bg-neutral-50/70"
|
||||
(tr (th :class "px-3 py-2 text-left font-medium w-40 sm:w-56" "Field")
|
||||
(th :class "px-3 py-2 text-left font-medium" "Value")))
|
||||
(defcomp
|
||||
~admin/data-scalar-table
|
||||
(&key columns)
|
||||
(div
|
||||
:class "w-full overflow-x-auto sm:overflow-visible"
|
||||
(table
|
||||
:class "w-full table-fixed text-sm border border-neutral-200 rounded-xl overflow-hidden"
|
||||
(thead
|
||||
:class "bg-neutral-50/70"
|
||||
(tr
|
||||
(th :class "px-3 py-2 text-left font-medium w-40 sm:w-56" "Field")
|
||||
(th :class "px-3 py-2 text-left font-medium" "Value")))
|
||||
(tbody
|
||||
(map (lambda (col)
|
||||
(tr :class "border-t border-neutral-200 align-top"
|
||||
(td :class "px-3 py-2 whitespace-nowrap text-neutral-600 align-top" (get col "key"))
|
||||
(td :class "px-3 py-2 align-top"
|
||||
(~admin/data-value-cell :value (get col "value") :value-type (get col "type")))))
|
||||
(map
|
||||
(lambda
|
||||
(col)
|
||||
(let-match
|
||||
{:value value :key key :type type}
|
||||
col
|
||||
(tr
|
||||
:class "border-t border-neutral-200 align-top"
|
||||
(td :class "px-3 py-2 whitespace-nowrap text-neutral-600" key)
|
||||
(td
|
||||
:class "px-3 py-2 align-top"
|
||||
(~admin/data-value-cell :value value :value-type type)))))
|
||||
(or columns (list)))))))
|
||||
|
||||
(defcomp ~admin/data-relationship-item (&key index summary children)
|
||||
(tr :class "border-t border-neutral-200 align-top"
|
||||
(defcomp
|
||||
~admin/data-relationship-item
|
||||
(&key index summary children)
|
||||
(tr
|
||||
:class "border-t border-neutral-200 align-top"
|
||||
(td :class "px-2 py-1 whitespace-nowrap align-top" (str index))
|
||||
(td :class "px-2 py-1 align-top"
|
||||
(pre :class "whitespace-pre-wrap break-words break-all text-xs"
|
||||
(td
|
||||
:class "px-2 py-1 align-top"
|
||||
(pre
|
||||
:class "whitespace-pre-wrap break-words break-all text-xs"
|
||||
(code summary))
|
||||
(when children
|
||||
(div :class "mt-2 pl-3 border-l border-neutral-200"
|
||||
(~admin/data-model-content
|
||||
:columns (get children "columns")
|
||||
:relationships (get children "relationships")))))))
|
||||
(when
|
||||
children
|
||||
(div
|
||||
:class "mt-2 pl-3 border-l border-neutral-200"
|
||||
(let-match
|
||||
{:relationships relationships :columns columns}
|
||||
children
|
||||
(~admin/data-model-content
|
||||
:columns columns
|
||||
:relationships relationships)))))))
|
||||
|
||||
(defcomp ~admin/data-relationship (&key name cardinality class-name loaded value)
|
||||
(div :class "rounded-xl border border-neutral-200"
|
||||
@@ -463,29 +610,50 @@
|
||||
:columns (get (get value "children") "columns")
|
||||
:relationships (get (get value "children") "relationships"))))))))))
|
||||
|
||||
(defcomp ~admin/data-model-content (&key columns relationships)
|
||||
(div :class "space-y-4"
|
||||
(defcomp
|
||||
~admin/data-model-content
|
||||
(&key columns relationships)
|
||||
(div
|
||||
:class "space-y-4"
|
||||
(~admin/data-scalar-table :columns columns)
|
||||
(when (not (empty? (or relationships (list))))
|
||||
(div :class "space-y-3"
|
||||
(map (lambda (rel)
|
||||
(~admin/data-relationship
|
||||
:name (get rel "name")
|
||||
:cardinality (get rel "cardinality")
|
||||
:class-name (get rel "class_name")
|
||||
:loaded (get rel "loaded")
|
||||
:value (get rel "value")))
|
||||
(when
|
||||
(not (empty? (or relationships (list))))
|
||||
(div
|
||||
:class "space-y-3"
|
||||
(map
|
||||
(lambda
|
||||
(rel)
|
||||
(let-match
|
||||
{:cardinality cardinality :class_name class-name :loaded loaded :value value :name name}
|
||||
rel
|
||||
(~admin/data-relationship
|
||||
:name name
|
||||
:cardinality cardinality
|
||||
:class-name class-name
|
||||
:loaded loaded
|
||||
:value value)))
|
||||
relationships)))))
|
||||
|
||||
(defcomp ~admin/data-table-content (&key tablename model-data)
|
||||
(if (not model-data)
|
||||
(defcomp
|
||||
~admin/data-table-content
|
||||
(&key tablename model-data)
|
||||
(if
|
||||
(not model-data)
|
||||
(div :class "px-4 py-8 text-stone-400" "No post data available.")
|
||||
(div :class "px-4 py-8"
|
||||
(div :class "mb-6 text-sm text-neutral-500"
|
||||
"Model: " (code "Post") " \u2022 Table: " (code tablename))
|
||||
(~admin/data-model-content
|
||||
:columns (get model-data "columns")
|
||||
:relationships (get model-data "relationships")))))
|
||||
(div
|
||||
:class "px-4 py-8"
|
||||
(div
|
||||
:class "mb-6 text-sm text-neutral-500"
|
||||
"Model: "
|
||||
(code "Post")
|
||||
" • Table: "
|
||||
(code tablename))
|
||||
(let-match
|
||||
{:relationships relationships :columns columns}
|
||||
model-data
|
||||
(~admin/data-model-content
|
||||
:columns columns
|
||||
:relationships relationships)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Calendar month view for browsing/toggling entries (B1)
|
||||
@@ -518,59 +686,117 @@
|
||||
:sx-on:afterSwap "document.body.dispatchEvent(new CustomEvent('entryToggled'))"
|
||||
(span :class "truncate block" name)))
|
||||
|
||||
(defcomp ~admin/calendar-view (&key cal-id year month-name
|
||||
current-url prev-month-url prev-year-url
|
||||
next-month-url next-year-url
|
||||
weekday-names days csrf)
|
||||
(let* ((target (str "#calendar-view-" cal-id)))
|
||||
(div :id (str "calendar-view-" cal-id)
|
||||
:sx-get current-url :sx-trigger "entryToggled from:body" :sx-swap "outerHTML"
|
||||
(header :class "flex items-center justify-center mb-4"
|
||||
(nav :class "flex items-center gap-2 text-xl"
|
||||
(a :class "px-2 py-1 hover:bg-stone-100 rounded"
|
||||
:sx-get prev-year-url :sx-target target :sx-swap "outerHTML"
|
||||
(defcomp
|
||||
~admin/calendar-view
|
||||
(&key
|
||||
cal-id
|
||||
year
|
||||
month-name
|
||||
current-url
|
||||
prev-month-url
|
||||
prev-year-url
|
||||
next-month-url
|
||||
next-year-url
|
||||
weekday-names
|
||||
days
|
||||
csrf)
|
||||
(let*
|
||||
((target (str "#calendar-view-" cal-id)))
|
||||
(div
|
||||
:id (str "calendar-view-" cal-id)
|
||||
:sx-get current-url
|
||||
:sx-trigger "entryToggled from:body"
|
||||
:sx-swap "outerHTML"
|
||||
(header
|
||||
:class "flex items-center justify-center mb-4"
|
||||
(nav
|
||||
:class "flex items-center gap-2 text-xl"
|
||||
(a
|
||||
:class "px-2 py-1 hover:bg-stone-100 rounded"
|
||||
:sx-get prev-year-url
|
||||
:sx-target target
|
||||
:sx-swap "outerHTML"
|
||||
(raw! "«"))
|
||||
(a :class "px-2 py-1 hover:bg-stone-100 rounded"
|
||||
:sx-get prev-month-url :sx-target target :sx-swap "outerHTML"
|
||||
(a
|
||||
:class "px-2 py-1 hover:bg-stone-100 rounded"
|
||||
:sx-get prev-month-url
|
||||
:sx-target target
|
||||
:sx-swap "outerHTML"
|
||||
(raw! "‹"))
|
||||
(div :class "px-3 font-medium" (str month-name " " year))
|
||||
(a :class "px-2 py-1 hover:bg-stone-100 rounded"
|
||||
:sx-get next-month-url :sx-target target :sx-swap "outerHTML"
|
||||
(a
|
||||
:class "px-2 py-1 hover:bg-stone-100 rounded"
|
||||
:sx-get next-month-url
|
||||
:sx-target target
|
||||
:sx-swap "outerHTML"
|
||||
(raw! "›"))
|
||||
(a :class "px-2 py-1 hover:bg-stone-100 rounded"
|
||||
:sx-get next-year-url :sx-target target :sx-swap "outerHTML"
|
||||
(a
|
||||
:class "px-2 py-1 hover:bg-stone-100 rounded"
|
||||
:sx-get next-year-url
|
||||
:sx-target target
|
||||
:sx-swap "outerHTML"
|
||||
(raw! "»"))))
|
||||
(div :class "rounded border bg-white"
|
||||
(div :class "hidden sm:grid grid-cols-7 text-center text-xs font-semibold text-stone-700 bg-stone-50 border-b"
|
||||
(map (lambda (wd) (div :class "py-2" wd)) (or weekday-names (list))))
|
||||
(div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200"
|
||||
(map (lambda (day)
|
||||
(let* ((extra-cls (if (get day "in_month") "" " bg-stone-50 text-stone-400"))
|
||||
(entries (or (get day "entries") (list))))
|
||||
(div :class (str "min-h-20 bg-white px-2 py-2 text-xs" extra-cls)
|
||||
(div :class "font-medium mb-1" (str (get day "day")))
|
||||
(when (not (empty? entries))
|
||||
(div :class "space-y-0.5"
|
||||
(map (lambda (e)
|
||||
(if (get e "is_associated")
|
||||
(~admin/cal-entry-associated
|
||||
:name (get e "name") :toggle-url (get e "toggle_url") :csrf csrf)
|
||||
(~admin/cal-entry-unassociated
|
||||
:name (get e "name") :toggle-url (get e "toggle_url") :csrf csrf)))
|
||||
entries))))))
|
||||
(div
|
||||
:class "rounded border bg-white"
|
||||
(div
|
||||
:class "hidden sm:grid grid-cols-7 text-center text-xs font-semibold text-stone-700 bg-stone-50 border-b"
|
||||
(map
|
||||
(lambda (wd) (div :class "py-2" wd))
|
||||
(or weekday-names (list))))
|
||||
(div
|
||||
:class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200"
|
||||
(map
|
||||
(lambda
|
||||
(day)
|
||||
(let-match
|
||||
{:entries entries* :in_month in-month :day day-num}
|
||||
day
|
||||
(let*
|
||||
((extra-cls (if in-month "" " bg-stone-50 text-stone-400"))
|
||||
(entries (or entries* (list))))
|
||||
(div
|
||||
:class (str "min-h-20 bg-white px-2 py-2 text-xs" extra-cls)
|
||||
(div :class "font-medium mb-1" (str day-num))
|
||||
(when
|
||||
(not (empty? entries))
|
||||
(div
|
||||
:class "space-y-0.5"
|
||||
(map
|
||||
(lambda
|
||||
(e)
|
||||
(let-match
|
||||
{:is_associated is-associated :toggle_url toggle-url :name name}
|
||||
e
|
||||
(if
|
||||
is-associated
|
||||
(~admin/cal-entry-associated
|
||||
:name name
|
||||
:toggle-url toggle-url
|
||||
:csrf csrf)
|
||||
(~admin/cal-entry-unassociated
|
||||
:name name
|
||||
:toggle-url toggle-url
|
||||
:csrf csrf))))
|
||||
entries)))))))
|
||||
(or days (list))))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Nav entries OOB — renders associated entry/calendar items in scroll wrapper (B2)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~admin/nav-entries-oob (&key entries calendars)
|
||||
(let* ((entry-list (or entries (list)))
|
||||
(cal-list (or calendars (list)))
|
||||
(has-items (or (not (empty? entry-list)) (not (empty? cal-list))))
|
||||
(nav-cls "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black [.hover-capable_&]:hover:bg-yellow-300 aria-selected:bg-stone-500 aria-selected:text-white [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500 p-2")
|
||||
(scroll-hs "on load or scroll if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end"))
|
||||
(if (not has-items)
|
||||
(defcomp
|
||||
~admin/nav-entries-oob
|
||||
(&key entries calendars)
|
||||
(let*
|
||||
((entry-list (or entries (list)))
|
||||
(cal-list (or calendars (list)))
|
||||
(has-items (or (not (empty? entry-list)) (not (empty? cal-list))))
|
||||
(nav-cls
|
||||
"justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black [.hover-capable_&]:hover:bg-yellow-300 aria-selected:bg-stone-500 aria-selected:text-white [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500 p-2")
|
||||
(scroll-hs
|
||||
"on load or scroll if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end"))
|
||||
(if
|
||||
(not has-items)
|
||||
(~shared:nav/blog-nav-entries-empty)
|
||||
(~shared:misc/scroll-nav-wrapper
|
||||
:wrapper-id "entries-calendars-nav-wrapper"
|
||||
@@ -580,14 +806,27 @@
|
||||
:scroll-hs scroll-hs
|
||||
:right-hs "on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft + 200"
|
||||
:items (<>
|
||||
(map (lambda (e)
|
||||
(~shared:navigation/calendar-entry-nav
|
||||
:href (get e "href") :nav-class nav-cls
|
||||
:name (get e "name") :date-str (get e "date_str")))
|
||||
(map
|
||||
(lambda
|
||||
(e)
|
||||
(let-match
|
||||
{:href href :date_str date-str :name name}
|
||||
e
|
||||
(~shared:navigation/calendar-entry-nav
|
||||
:href href
|
||||
:nav-class nav-cls
|
||||
:name name
|
||||
:date-str date-str)))
|
||||
entry-list)
|
||||
(map (lambda (c)
|
||||
(~shared:nav/blog-nav-calendar-item
|
||||
:href (get c "href") :nav-cls nav-cls
|
||||
:name (get c "name")))
|
||||
(map
|
||||
(lambda
|
||||
(c)
|
||||
(let-match
|
||||
{:href href :name name}
|
||||
c
|
||||
(~shared:nav/blog-nav-calendar-item
|
||||
:href href
|
||||
:nav-cls nav-cls
|
||||
:name name)))
|
||||
cal-list))
|
||||
:oob true))))
|
||||
|
||||
Reference in New Issue
Block a user