WIP on theme admin page (#20)
This commit is contained in:
parent
4514c4864d
commit
81fe03b8f3
|
@ -167,7 +167,7 @@ type ITagMapData =
|
||||||
/// Functions to manipulate themes
|
/// Functions to manipulate themes
|
||||||
type IThemeData =
|
type IThemeData =
|
||||||
|
|
||||||
/// Retrieve all themes (except "admin")
|
/// Retrieve all themes (except "admin") (excluding the text of templates)
|
||||||
abstract member All : unit -> Task<Theme list>
|
abstract member All : unit -> Task<Theme list>
|
||||||
|
|
||||||
/// Find a theme by its ID
|
/// Find a theme by its ID
|
||||||
|
|
|
@ -96,6 +96,10 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
let keyPrefix = $"^{ThemeId.toString themeId}/"
|
let keyPrefix = $"^{ThemeId.toString themeId}/"
|
||||||
fun (row : Ast.ReqlExpr) -> row[nameof ThemeAsset.empty.Id].Match keyPrefix :> obj
|
fun (row : Ast.ReqlExpr) -> row[nameof ThemeAsset.empty.Id].Match keyPrefix :> obj
|
||||||
|
|
||||||
|
/// Function to exclude template text from themes
|
||||||
|
let withoutTemplateText (row : Ast.ReqlExpr) : obj =
|
||||||
|
{| Templates = row[nameof Theme.empty.Templates].Without [| nameof ThemeTemplate.empty.Text |] |}
|
||||||
|
|
||||||
/// Ensure field indexes exist, as well as special indexes for selected tables
|
/// Ensure field indexes exist, as well as special indexes for selected tables
|
||||||
let ensureIndexes table fields = backgroundTask {
|
let ensureIndexes table fields = backgroundTask {
|
||||||
let! indexes = rethink<string list> { withTable table; indexList; result; withRetryOnce conn }
|
let! indexes = rethink<string list> { withTable table; indexList; result; withRetryOnce conn }
|
||||||
|
@ -711,7 +715,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
member _.All () = rethink<Theme list> {
|
member _.All () = rethink<Theme list> {
|
||||||
withTable Table.Theme
|
withTable Table.Theme
|
||||||
filter (fun row -> row[nameof Theme.empty.Id].Ne "admin" :> obj)
|
filter (fun row -> row[nameof Theme.empty.Id].Ne "admin" :> obj)
|
||||||
without [ nameof Theme.empty.Templates ]
|
merge withoutTemplateText
|
||||||
orderBy (nameof Theme.empty.Id)
|
orderBy (nameof Theme.empty.Id)
|
||||||
result; withRetryDefault conn
|
result; withRetryDefault conn
|
||||||
}
|
}
|
||||||
|
@ -725,9 +729,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
member _.FindByIdWithoutText themeId = rethink<Theme> {
|
member _.FindByIdWithoutText themeId = rethink<Theme> {
|
||||||
withTable Table.Theme
|
withTable Table.Theme
|
||||||
get themeId
|
get themeId
|
||||||
merge (fun row ->
|
merge withoutTemplateText
|
||||||
{| Templates = row[nameof Theme.empty.Templates].Without [| nameof ThemeTemplate.empty.Text |]
|
|
||||||
|})
|
|
||||||
resultOption; withRetryOptionDefault conn
|
resultOption; withRetryOptionDefault conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -242,9 +242,9 @@ module Map =
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a theme template from the current row in the given data reader
|
/// Create a theme template from the current row in the given data reader
|
||||||
let toThemeTemplate rdr : ThemeTemplate =
|
let toThemeTemplate includeText rdr : ThemeTemplate =
|
||||||
{ Name = getString "name" rdr
|
{ Name = getString "name" rdr
|
||||||
Text = getString "template" rdr
|
Text = if includeText then getString "template" rdr else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an uploaded file from the current row in the given data reader
|
/// Create an uploaded file from the current row in the given data reader
|
||||||
|
|
|
@ -8,12 +8,22 @@ open MyWebLog.Data
|
||||||
/// SQLite myWebLog theme data implementation
|
/// SQLite myWebLog theme data implementation
|
||||||
type SQLiteThemeData (conn : SqliteConnection) =
|
type SQLiteThemeData (conn : SqliteConnection) =
|
||||||
|
|
||||||
/// Retrieve all themes (except 'admin'; excludes templates)
|
/// Retrieve all themes (except 'admin'; excludes template text)
|
||||||
let all () = backgroundTask {
|
let all () = backgroundTask {
|
||||||
use cmd = conn.CreateCommand ()
|
use cmd = conn.CreateCommand ()
|
||||||
cmd.CommandText <- "SELECT * FROM theme WHERE id <> 'admin' ORDER BY id"
|
cmd.CommandText <- "SELECT * FROM theme WHERE id <> 'admin' ORDER BY id"
|
||||||
use! rdr = cmd.ExecuteReaderAsync ()
|
use! rdr = cmd.ExecuteReaderAsync ()
|
||||||
return toList Map.toTheme rdr
|
let themes = toList Map.toTheme rdr
|
||||||
|
do! rdr.CloseAsync ()
|
||||||
|
cmd.CommandText <- "SELECT name, theme_id FROM theme_template WHERE theme_id <> 'admin' ORDER BY name"
|
||||||
|
use! rdr = cmd.ExecuteReaderAsync ()
|
||||||
|
let mutable templates = []
|
||||||
|
while rdr.Read () do
|
||||||
|
templates <- (ThemeId (Map.getString "theme_id" rdr), Map.toThemeTemplate false rdr) :: templates
|
||||||
|
return
|
||||||
|
themes
|
||||||
|
|> List.map (fun t ->
|
||||||
|
{ t with Templates = templates |> List.filter (fun tt -> fst tt = t.Id) |> List.map snd })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a theme by its ID
|
/// Find a theme by its ID
|
||||||
|
@ -28,7 +38,7 @@ type SQLiteThemeData (conn : SqliteConnection) =
|
||||||
templateCmd.CommandText <- "SELECT * FROM theme_template WHERE theme_id = @id"
|
templateCmd.CommandText <- "SELECT * FROM theme_template WHERE theme_id = @id"
|
||||||
templateCmd.Parameters.Add cmd.Parameters["@id"] |> ignore
|
templateCmd.Parameters.Add cmd.Parameters["@id"] |> ignore
|
||||||
use! templateRdr = templateCmd.ExecuteReaderAsync ()
|
use! templateRdr = templateCmd.ExecuteReaderAsync ()
|
||||||
return Some { theme with Templates = toList Map.toThemeTemplate templateRdr }
|
return Some { theme with Templates = toList (Map.toThemeTemplate true) templateRdr }
|
||||||
else
|
else
|
||||||
return None
|
return None
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,6 +176,40 @@ with
|
||||||
|
|
||||||
open System.IO
|
open System.IO
|
||||||
|
|
||||||
|
/// Information about a theme used for display
|
||||||
|
[<NoComparison; NoEquality>]
|
||||||
|
type DisplayTheme =
|
||||||
|
{ /// The ID / path slug of the theme
|
||||||
|
Id : string
|
||||||
|
|
||||||
|
/// The name of the theme
|
||||||
|
Name : string
|
||||||
|
|
||||||
|
/// The version of the theme
|
||||||
|
Version : string
|
||||||
|
|
||||||
|
/// How many templates are contained in the theme
|
||||||
|
TemplateCount : int
|
||||||
|
|
||||||
|
/// Whether the theme is in use by any web logs
|
||||||
|
IsInUse : bool
|
||||||
|
|
||||||
|
/// Whether the theme .zip file exists on the filesystem
|
||||||
|
IsOnDisk : bool
|
||||||
|
}
|
||||||
|
with
|
||||||
|
|
||||||
|
/// Create a display theme from a theme
|
||||||
|
static member fromTheme inUseFunc (theme : Theme) =
|
||||||
|
{ Id = ThemeId.toString theme.Id
|
||||||
|
Name = theme.Name
|
||||||
|
Version = theme.Version
|
||||||
|
TemplateCount = List.length theme.Templates
|
||||||
|
IsInUse = inUseFunc theme.Id
|
||||||
|
IsOnDisk = File.Exists $"{ThemeId.toString theme.Id}-theme.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Information about an uploaded file used for display
|
/// Information about an uploaded file used for display
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DisplayUpload =
|
type DisplayUpload =
|
||||||
|
|
|
@ -86,6 +86,10 @@ module WebLogCache =
|
||||||
_cache <- webLogs
|
_cache <- webLogs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is the given theme in use by any web logs?
|
||||||
|
let isThemeInUse themeId =
|
||||||
|
_cache |> List.exists (fun wl -> wl.ThemeId = themeId)
|
||||||
|
|
||||||
|
|
||||||
/// A cache of page information needed to display the page list in templates
|
/// A cache of page information needed to display the page list in templates
|
||||||
module PageListCache =
|
module PageListCache =
|
||||||
|
@ -147,12 +151,12 @@ module TemplateCache =
|
||||||
let private hasInclude = Regex ("""{% include_template \"(.*)\" %}""", RegexOptions.None, TimeSpan.FromSeconds 2)
|
let private hasInclude = Regex ("""{% include_template \"(.*)\" %}""", RegexOptions.None, TimeSpan.FromSeconds 2)
|
||||||
|
|
||||||
/// Get a template for the given theme and template name
|
/// Get a template for the given theme and template name
|
||||||
let get (themeId : string) (templateName : string) (data : IData) = backgroundTask {
|
let get (themeId : ThemeId) (templateName : string) (data : IData) = backgroundTask {
|
||||||
let templatePath = $"{themeId}/{templateName}"
|
let templatePath = $"{ThemeId.toString themeId}/{templateName}"
|
||||||
match _cache.ContainsKey templatePath with
|
match _cache.ContainsKey templatePath with
|
||||||
| true -> ()
|
| true -> ()
|
||||||
| false ->
|
| false ->
|
||||||
match! data.Theme.FindById (ThemeId themeId) with
|
match! data.Theme.FindById themeId with
|
||||||
| Some theme ->
|
| Some theme ->
|
||||||
let mutable text = (theme.Templates |> List.find (fun t -> t.Name = templateName)).Text
|
let mutable text = (theme.Templates |> List.find (fun t -> t.Name = templateName)).Text
|
||||||
while hasInclude.IsMatch text do
|
while hasInclude.IsMatch text do
|
||||||
|
|
|
@ -227,12 +227,12 @@ let register () =
|
||||||
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
|
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
|
||||||
typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
||||||
// View models
|
// View models
|
||||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||||
typeof<DisplayRevision>; typeof<DisplayUpload>; typeof<DisplayUser>; typeof<EditCategoryModel>
|
typeof<DisplayRevision>; typeof<DisplayTheme>; typeof<DisplayUpload>; typeof<DisplayUser>
|
||||||
typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>; typeof<EditPageModel>; typeof<EditPostModel>
|
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>; typeof<EditPageModel>
|
||||||
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
typeof<EditPostModel>; typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>
|
||||||
typeof<ManagePermalinksModel>; typeof<ManageRevisionsModel>; typeof<PostDisplay>; typeof<PostListItem>
|
typeof<LogOnModel>; typeof<ManagePermalinksModel>; typeof<ManageRevisionsModel>; typeof<PostDisplay>
|
||||||
typeof<SettingsModel>; typeof<UserMessage>
|
typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage>
|
||||||
// Framework types
|
// Framework types
|
||||||
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
||||||
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>
|
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||||
|
|
|
@ -34,7 +34,7 @@ let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
|
|
||||||
// GET /admin/categories
|
// GET /admin/categories
|
||||||
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let! catListTemplate = TemplateCache.get "admin" "category-list-body" ctx.Data
|
let! catListTemplate = TemplateCache.get adminTheme "category-list-body" ctx.Data
|
||||||
let! hash =
|
let! hash =
|
||||||
hashForPage "Categories"
|
hashForPage "Categories"
|
||||||
|> withAntiCsrf ctx
|
|> withAntiCsrf ctx
|
||||||
|
@ -122,7 +122,7 @@ let private tagMappingHash (ctx : HttpContext) = task {
|
||||||
// GET /admin/settings/tag-mappings
|
// GET /admin/settings/tag-mappings
|
||||||
let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let! hash = tagMappingHash ctx
|
let! hash = tagMappingHash ctx
|
||||||
let! listTemplate = TemplateCache.get "admin" "tag-mapping-list-body" ctx.Data
|
let! listTemplate = TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data
|
||||||
return!
|
return!
|
||||||
addToHash "tag_mapping_list" (listTemplate.Render hash) hash
|
addToHash "tag_mapping_list" (listTemplate.Render hash) hash
|
||||||
|> adminView "tag-mapping-list" next ctx
|
|> adminView "tag-mapping-list" next ctx
|
||||||
|
@ -181,6 +181,19 @@ open System.IO.Compression
|
||||||
open System.Text.RegularExpressions
|
open System.Text.RegularExpressions
|
||||||
open MyWebLog.Data
|
open MyWebLog.Data
|
||||||
|
|
||||||
|
// GET /admin/themes
|
||||||
|
let listThemes : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||||
|
let! themes = ctx.Data.Theme.All ()
|
||||||
|
let! bodyTemplate = TemplateCache.get adminTheme "theme-list-body" ctx.Data
|
||||||
|
let hash =
|
||||||
|
hashForPage "Theme Administration"
|
||||||
|
|> withAntiCsrf ctx
|
||||||
|
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
||||||
|
return!
|
||||||
|
addToHash "theme_list" (bodyTemplate.Render hash) hash
|
||||||
|
|> adminView "theme-list" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
// GET /admin/theme/update
|
// GET /admin/theme/update
|
||||||
let themeUpdatePage : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
let themeUpdatePage : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
||||||
hashForPage "Upload Theme"
|
hashForPage "Upload Theme"
|
||||||
|
|
|
@ -221,16 +221,16 @@ let isHtmx (ctx : HttpContext) =
|
||||||
/// Render a view for the specified theme, using the specified template, layout, and hash
|
/// Render a view for the specified theme, using the specified template, layout, and hash
|
||||||
let viewForTheme themeId template next ctx (hash : Hash) = task {
|
let viewForTheme themeId template next ctx (hash : Hash) = task {
|
||||||
let! hash = addViewContext ctx hash
|
let! hash = addViewContext ctx hash
|
||||||
let (ThemeId theme) = themeId
|
|
||||||
// NOTE: DotLiquid does not support {% render %} or {% include %} in its templates, so we will do a 2-pass render;
|
// NOTE: DotLiquid does not support {% render %} or {% include %} in its templates, so we will do a 2-pass render;
|
||||||
// the net effect is a "layout" capability similar to Razor or Pug
|
// the net effect is a "layout" capability similar to Razor or Pug
|
||||||
|
|
||||||
// Render view content...
|
// Render view content...
|
||||||
let! contentTemplate = TemplateCache.get theme template ctx.Data
|
let! contentTemplate = TemplateCache.get themeId template ctx.Data
|
||||||
let _ = addToHash ViewContext.Content (contentTemplate.Render hash) hash
|
let _ = addToHash ViewContext.Content (contentTemplate.Render hash) hash
|
||||||
|
|
||||||
// ...then render that content with its layout
|
// ...then render that content with its layout
|
||||||
let! layoutTemplate = TemplateCache.get theme (if isHtmx ctx then "layout-partial" else "layout") ctx.Data
|
let! layoutTemplate = TemplateCache.get themeId (if isHtmx ctx then "layout-partial" else "layout") ctx.Data
|
||||||
|
|
||||||
return! htmlString (layoutTemplate.Render hash) next ctx
|
return! htmlString (layoutTemplate.Render hash) next ctx
|
||||||
}
|
}
|
||||||
|
@ -252,14 +252,13 @@ let messagesToHeaders (messages : UserMessage array) : HttpHandler =
|
||||||
/// Render a bare view for the specified theme, using the specified template and hash
|
/// Render a bare view for the specified theme, using the specified template and hash
|
||||||
let bareForTheme themeId template next ctx (hash : Hash) = task {
|
let bareForTheme themeId template next ctx (hash : Hash) = task {
|
||||||
let! hash = addViewContext ctx hash
|
let! hash = addViewContext ctx hash
|
||||||
let (ThemeId theme) = themeId
|
|
||||||
|
|
||||||
if not (hash.ContainsKey ViewContext.Content) then
|
if not (hash.ContainsKey ViewContext.Content) then
|
||||||
let! contentTemplate = TemplateCache.get theme template ctx.Data
|
let! contentTemplate = TemplateCache.get themeId template ctx.Data
|
||||||
addToHash ViewContext.Content (contentTemplate.Render hash) hash |> ignore
|
addToHash ViewContext.Content (contentTemplate.Render hash) hash |> ignore
|
||||||
|
|
||||||
// Bare templates are rendered with layout-bare
|
// Bare templates are rendered with layout-bare
|
||||||
let! layoutTemplate = TemplateCache.get theme "layout-bare" ctx.Data
|
let! layoutTemplate = TemplateCache.get themeId "layout-bare" ctx.Data
|
||||||
return!
|
return!
|
||||||
(messagesToHeaders (hash[ViewContext.Messages] :?> UserMessage[])
|
(messagesToHeaders (hash[ViewContext.Messages] :?> UserMessage[])
|
||||||
>=> htmlString (layoutTemplate.Render hash))
|
>=> htmlString (layoutTemplate.Render hash))
|
||||||
|
@ -272,13 +271,16 @@ let themedView template next ctx hash = task {
|
||||||
return! viewForTheme (hash[ViewContext.WebLog] :?> WebLog).ThemeId template next ctx hash
|
return! viewForTheme (hash[ViewContext.WebLog] :?> WebLog).ThemeId template next ctx hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The ID for the admin theme
|
||||||
|
let adminTheme = ThemeId "admin"
|
||||||
|
|
||||||
/// Display a view for the admin theme
|
/// Display a view for the admin theme
|
||||||
let adminView template =
|
let adminView template =
|
||||||
viewForTheme (ThemeId "admin") template
|
viewForTheme adminTheme template
|
||||||
|
|
||||||
/// Display a bare view for the admin theme
|
/// Display a bare view for the admin theme
|
||||||
let adminBareView template =
|
let adminBareView template =
|
||||||
bareForTheme (ThemeId "admin") template
|
bareForTheme adminTheme template
|
||||||
|
|
||||||
/// Redirect after doing some action; commits session and issues a temporary redirect
|
/// Redirect after doing some action; commits session and issues a temporary redirect
|
||||||
let redirectToGet url : HttpHandler = fun _ ctx -> task {
|
let redirectToGet url : HttpHandler = fun _ ctx -> task {
|
||||||
|
|
|
@ -140,7 +140,10 @@ let router : HttpHandler = choose [
|
||||||
routef "/%s/edit" Admin.editMapping
|
routef "/%s/edit" Admin.editMapping
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
route "/theme/update" >=> Admin.themeUpdatePage
|
subRoute "/theme" (choose [
|
||||||
|
route "s" >=> Admin.listThemes
|
||||||
|
route "/update" >=> Admin.themeUpdatePage
|
||||||
|
])
|
||||||
subRoute "/upload" (choose [
|
subRoute "/upload" (choose [
|
||||||
route "s" >=> Upload.list
|
route "s" >=> Upload.list
|
||||||
route "/new" >=> Upload.showNew
|
route "/new" >=> Upload.showNew
|
||||||
|
|
|
@ -90,7 +90,7 @@ let private goAway : HttpHandler = RequestErrors.BAD_REQUEST "really?"
|
||||||
// GET /admin/users
|
// GET /admin/users
|
||||||
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let! hash = userListHash ctx
|
let! hash = userListHash ctx
|
||||||
let! tmpl = TemplateCache.get "admin" "user-list-body" ctx.Data
|
let! tmpl = TemplateCache.get adminTheme "user-list-body" ctx.Data
|
||||||
return!
|
return!
|
||||||
addToHash "user_list" (tmpl.Render hash) hash
|
addToHash "user_list" (tmpl.Render hash) hash
|
||||||
|> adminView "user-list" next ctx
|
|> adminView "user-list" next ctx
|
||||||
|
|
|
@ -7,23 +7,26 @@
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarText">
|
<div class="collapse navbar-collapse" id="navbarText">
|
||||||
{% if is_logged_on -%}
|
{%- if is_logged_on %}
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
||||||
{% if is_author %}
|
{%- if is_author %}
|
||||||
{{ "admin/pages" | nav_link: "Pages" }}
|
{{ "admin/pages" | nav_link: "Pages" }}
|
||||||
{{ "admin/posts" | nav_link: "Posts" }}
|
{{ "admin/posts" | nav_link: "Posts" }}
|
||||||
{{ "admin/uploads" | nav_link: "Uploads" }}
|
{{ "admin/uploads" | nav_link: "Uploads" }}
|
||||||
{% endif %}
|
{%- endif %}
|
||||||
{% if is_web_log_admin %}
|
{%- if is_web_log_admin %}
|
||||||
{{ "admin/categories" | nav_link: "Categories" }}
|
{{ "admin/categories" | nav_link: "Categories" }}
|
||||||
{{ "admin/users" | nav_link: "Users" }}
|
{{ "admin/users" | nav_link: "Users" }}
|
||||||
{{ "admin/settings" | nav_link: "Settings" }}
|
{{ "admin/settings" | nav_link: "Settings" }}
|
||||||
{% endif %}
|
{%- endif %}
|
||||||
|
{%- if is_administrator %}
|
||||||
|
{{ "admin/themes" | nav_link: "Themes" }}
|
||||||
|
{%- endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
||||||
{% if is_logged_on -%}
|
{%- if is_logged_on %}
|
||||||
{{ "admin/user/my-info" | nav_link: "My Info" }}
|
{{ "admin/user/my-info" | nav_link: "My Info" }}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ "user/log-off" | relative_link }}" hx-boost="false">Log Off</a>
|
<a class="nav-link" href="{{ "user/log-off" | relative_link }}" hx-boost="false">Log Off</a>
|
||||||
|
|
3
src/admin-theme/_theme-list-columns.liquid
Normal file
3
src/admin-theme/_theme-list-columns.liquid
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{%- assign theme_col = "col-12 col-md-6" -%}
|
||||||
|
{%- assign slug_col = "d-none d-md-block col-md-3" -%}
|
||||||
|
{%- assign tmpl_col = "d-none d-md-block col-md-3" -%}
|
34
src/admin-theme/theme-list-body.liquid
Normal file
34
src/admin-theme/theme-list-body.liquid
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<form method="post" id="themeList" class="container" hx-target="this" hx-swap="outerHTML show:window:top">
|
||||||
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
|
<div class="row mwl-table-detail" id="theme_new"></div>
|
||||||
|
{% include_template "_theme-list-columns" %}
|
||||||
|
{% for theme in themes -%}
|
||||||
|
<div class="row mwl-table-detail" id="theme_{{ theme.id }}">
|
||||||
|
<div class="{{ theme_col }} no-wrap">
|
||||||
|
{{ theme.name }}
|
||||||
|
{%- if theme.is_in_use %}
|
||||||
|
<span class="badge bg-primary ms-2">IN USE</span>
|
||||||
|
{%- endif %}
|
||||||
|
{%- unless theme.is_on_disk %}
|
||||||
|
<span class="badge bg-warning text-dark ms-2">NOT ON DISK</span>
|
||||||
|
{%- endunless %}<br>
|
||||||
|
<small>
|
||||||
|
<span class="text-muted">v{{ theme.version }}</span>
|
||||||
|
{% unless theme.is_in_use or theme.id == "default" %}
|
||||||
|
<span class="text-muted"> • </span>
|
||||||
|
{%- assign theme_del_link = "admin/" | append: theme.id | append: "/delete" | relative_link -%}
|
||||||
|
<a href="{{ theme_del_link }}" hx-post="{{ theme_del_link }}" class="text-danger"
|
||||||
|
hx-confirm="Are you sure you want to delete the theme “{{ theme.name }}”? This action cannot be undone.">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
{% endunless %}
|
||||||
|
<span class="d-md-none text-muted">
|
||||||
|
<br>Slug: {{ theme.id }} • {{ theme.template_count }} Templates
|
||||||
|
</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="{{ slug_col }}">{{ theme.id }}</div>
|
||||||
|
<div class="{{ tmpl_col }}">{{ theme.template_count }}</div>
|
||||||
|
</div>
|
||||||
|
{%- endfor %}
|
||||||
|
</form>
|
16
src/admin-theme/theme-list.liquid
Normal file
16
src/admin-theme/theme-list.liquid
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<h2 class="my-3">{{ page_title }}</h2>
|
||||||
|
<article>
|
||||||
|
<a href="{{ "admin/theme/upload" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
||||||
|
hx-target="#theme_new">
|
||||||
|
Upload a New Theme
|
||||||
|
</a>
|
||||||
|
<div class="container">
|
||||||
|
{% include_template "_theme-list-columns" %}
|
||||||
|
<div class="row mwl-table-heading">
|
||||||
|
<div class="{{ theme_col }}">Theme</div>
|
||||||
|
<div class="{{ slug_col }} d-none d-md-inline-block">Slug</div>
|
||||||
|
<div class="{{ tmpl_col }} d-none d-md-inline-block">Templates</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ theme_list }}
|
||||||
|
</article>
|
Loading…
Reference in New Issue
Block a user