Upload / delete themes (#20)
- Moved themes to section of installation admin page (will also implement #23 there)
This commit is contained in:
parent
0a32181e65
commit
d854178255
|
@ -170,6 +170,12 @@ type IThemeData =
|
||||||
/// Retrieve all themes (except "admin") (excluding the text of templates)
|
/// Retrieve all themes (except "admin") (excluding the text of templates)
|
||||||
abstract member All : unit -> Task<Theme list>
|
abstract member All : unit -> Task<Theme list>
|
||||||
|
|
||||||
|
/// Delete a theme
|
||||||
|
abstract member Delete : ThemeId -> Task<bool>
|
||||||
|
|
||||||
|
/// Determine if a theme exists
|
||||||
|
abstract member Exists : ThemeId -> Task<bool>
|
||||||
|
|
||||||
/// Find a theme by its ID
|
/// Find a theme by its ID
|
||||||
abstract member FindById : ThemeId -> Task<Theme option>
|
abstract member FindById : ThemeId -> Task<Theme option>
|
||||||
|
|
||||||
|
|
|
@ -180,6 +180,14 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
/// The batch size for restoration methods
|
/// The batch size for restoration methods
|
||||||
let restoreBatchSize = 100
|
let restoreBatchSize = 100
|
||||||
|
|
||||||
|
/// Delete assets for the given theme ID
|
||||||
|
let deleteAssetsByTheme themeId = rethink {
|
||||||
|
withTable Table.ThemeAsset
|
||||||
|
filter (matchAssetByThemeId themeId)
|
||||||
|
delete
|
||||||
|
write; withRetryDefault; ignoreResult conn
|
||||||
|
}
|
||||||
|
|
||||||
/// The connection for this instance
|
/// The connection for this instance
|
||||||
member _.Conn = conn
|
member _.Conn = conn
|
||||||
|
|
||||||
|
@ -720,6 +728,16 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
result; withRetryDefault conn
|
result; withRetryDefault conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
member _.Exists themeId = backgroundTask {
|
||||||
|
let! count = rethink<int> {
|
||||||
|
withTable Table.Theme
|
||||||
|
filter (nameof Theme.empty.Id) themeId
|
||||||
|
count
|
||||||
|
result; withRetryDefault conn
|
||||||
|
}
|
||||||
|
return count > 0
|
||||||
|
}
|
||||||
|
|
||||||
member _.FindById themeId = rethink<Theme> {
|
member _.FindById themeId = rethink<Theme> {
|
||||||
withTable Table.Theme
|
withTable Table.Theme
|
||||||
get themeId
|
get themeId
|
||||||
|
@ -733,6 +751,20 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
resultOption; withRetryOptionDefault conn
|
resultOption; withRetryOptionDefault conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
member this.Delete themeId = backgroundTask {
|
||||||
|
match! this.FindByIdWithoutText themeId with
|
||||||
|
| Some _ ->
|
||||||
|
do! deleteAssetsByTheme themeId
|
||||||
|
do! rethink {
|
||||||
|
withTable Table.Theme
|
||||||
|
get themeId
|
||||||
|
delete
|
||||||
|
write; withRetryDefault; ignoreResult conn
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
| None -> return false
|
||||||
|
}
|
||||||
|
|
||||||
member _.Save theme = rethink {
|
member _.Save theme = rethink {
|
||||||
withTable Table.Theme
|
withTable Table.Theme
|
||||||
get theme.Id
|
get theme.Id
|
||||||
|
@ -750,12 +782,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
result; withRetryDefault conn
|
result; withRetryDefault conn
|
||||||
}
|
}
|
||||||
|
|
||||||
member _.DeleteByTheme themeId = rethink {
|
member _.DeleteByTheme themeId = deleteAssetsByTheme themeId
|
||||||
withTable Table.ThemeAsset
|
|
||||||
filter (matchAssetByThemeId themeId)
|
|
||||||
delete
|
|
||||||
write; withRetryDefault; ignoreResult conn
|
|
||||||
}
|
|
||||||
|
|
||||||
member _.FindById assetId = rethink<ThemeAsset> {
|
member _.FindById assetId = rethink<ThemeAsset> {
|
||||||
withTable Table.ThemeAsset
|
withTable Table.ThemeAsset
|
||||||
|
|
|
@ -26,6 +26,15 @@ type SQLiteThemeData (conn : SqliteConnection) =
|
||||||
{ t with Templates = templates |> List.filter (fun tt -> fst tt = t.Id) |> List.map snd })
|
{ t with Templates = templates |> List.filter (fun tt -> fst tt = t.Id) |> List.map snd })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Does a given theme exist?
|
||||||
|
let exists themeId = backgroundTask {
|
||||||
|
use cmd = conn.CreateCommand ()
|
||||||
|
cmd.CommandText <- "SELECT COUNT(id) FROM theme WHERE id = @id"
|
||||||
|
cmd.Parameters.AddWithValue ("@id", ThemeId.toString themeId) |> ignore
|
||||||
|
let! count = count cmd
|
||||||
|
return count > 0
|
||||||
|
}
|
||||||
|
|
||||||
/// Find a theme by its ID
|
/// Find a theme by its ID
|
||||||
let findById themeId = backgroundTask {
|
let findById themeId = backgroundTask {
|
||||||
use cmd = conn.CreateCommand ()
|
use cmd = conn.CreateCommand ()
|
||||||
|
@ -53,6 +62,21 @@ type SQLiteThemeData (conn : SqliteConnection) =
|
||||||
| None -> return None
|
| None -> return None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a theme by its ID
|
||||||
|
let delete themeId = backgroundTask {
|
||||||
|
match! findByIdWithoutText themeId with
|
||||||
|
| Some _ ->
|
||||||
|
use cmd = conn.CreateCommand ()
|
||||||
|
cmd.CommandText <- """
|
||||||
|
DELETE FROM theme_asset WHERE theme_id = @id;
|
||||||
|
DELETE FROM theme_template WHERE theme_id = @id;
|
||||||
|
DELETE FROM theme WHERE id = @id"""
|
||||||
|
cmd.Parameters.AddWithValue ("@id", ThemeId.toString themeId) |> ignore
|
||||||
|
do! write cmd
|
||||||
|
return true
|
||||||
|
| None -> return false
|
||||||
|
}
|
||||||
|
|
||||||
/// Save a theme
|
/// Save a theme
|
||||||
let save (theme : Theme) = backgroundTask {
|
let save (theme : Theme) = backgroundTask {
|
||||||
use cmd = conn.CreateCommand ()
|
use cmd = conn.CreateCommand ()
|
||||||
|
@ -112,6 +136,8 @@ type SQLiteThemeData (conn : SqliteConnection) =
|
||||||
|
|
||||||
interface IThemeData with
|
interface IThemeData with
|
||||||
member _.All () = all ()
|
member _.All () = all ()
|
||||||
|
member _.Delete themeId = delete themeId
|
||||||
|
member _.Exists themeId = exists themeId
|
||||||
member _.FindById themeId = findById themeId
|
member _.FindById themeId = findById themeId
|
||||||
member _.FindByIdWithoutText themeId = findByIdWithoutText themeId
|
member _.FindByIdWithoutText themeId = findByIdWithoutText themeId
|
||||||
member _.Save theme = save theme
|
member _.Save theme = save theme
|
||||||
|
|
|
@ -6,7 +6,9 @@ open Giraffe
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
open MyWebLog.ViewModels
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
// GET /admin
|
// ~~ DASHBOARDS ~~
|
||||||
|
|
||||||
|
// GET /admin/dashboard
|
||||||
let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
let getCount (f : WebLogId -> Task<int>) = f ctx.WebLog.Id
|
let getCount (f : WebLogId -> Task<int>) = f ctx.WebLog.Id
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
|
@ -30,7 +32,24 @@ let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
|> adminView "dashboard" next ctx
|
|> adminView "dashboard" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- CATEGORIES --
|
// GET /admin/dashboard/administration
|
||||||
|
let adminDashboard : 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 "myWebLog Administration"
|
||||||
|
|> withAntiCsrf ctx
|
||||||
|
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
||||||
|
|> addViewContext ctx
|
||||||
|
return!
|
||||||
|
addToHash "theme_list" (bodyTemplate.Render hash) hash
|
||||||
|
|> adminView "admin-dashboard" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redirect the user to the admin dashboard
|
||||||
|
let toAdminDashboard : HttpHandler = redirectToGet "admin/dashboard/administration"
|
||||||
|
|
||||||
|
// ~~ CATEGORIES ~~
|
||||||
|
|
||||||
// GET /admin/categories
|
// GET /admin/categories
|
||||||
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
|
@ -106,7 +125,7 @@ let deleteCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next
|
||||||
|
|
||||||
open Microsoft.AspNetCore.Http
|
open Microsoft.AspNetCore.Http
|
||||||
|
|
||||||
// -- TAG MAPPINGS --
|
// ~~ TAG MAPPINGS ~~
|
||||||
|
|
||||||
/// Get the hash necessary to render the tag mapping list
|
/// Get the hash necessary to render the tag mapping list
|
||||||
let private tagMappingHash (ctx : HttpContext) = task {
|
let private tagMappingHash (ctx : HttpContext) = task {
|
||||||
|
@ -173,7 +192,7 @@ let deleteMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun nex
|
||||||
return! tagMappingsBare next ctx
|
return! tagMappingsBare next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- THEMES --
|
// ~~ THEMES ~~
|
||||||
|
|
||||||
open System
|
open System
|
||||||
open System.IO
|
open System.IO
|
||||||
|
@ -181,24 +200,21 @@ open System.IO.Compression
|
||||||
open System.Text.RegularExpressions
|
open System.Text.RegularExpressions
|
||||||
open MyWebLog.Data
|
open MyWebLog.Data
|
||||||
|
|
||||||
// GET /admin/themes
|
// GET /admin/theme/list
|
||||||
let listThemes : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
let listThemes : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||||
let! themes = ctx.Data.Theme.All ()
|
let! themes = ctx.Data.Theme.All ()
|
||||||
let! bodyTemplate = TemplateCache.get adminTheme "theme-list-body" ctx.Data
|
return!
|
||||||
let hash =
|
hashForPage "Themes"
|
||||||
hashForPage "Theme Administration"
|
|
||||||
|> withAntiCsrf ctx
|
|> withAntiCsrf ctx
|
||||||
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
||||||
return!
|
|> adminBareView "theme-list-body" next ctx
|
||||||
addToHash "theme_list" (bodyTemplate.Render hash) hash
|
|
||||||
|> adminView "theme-list" next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/theme/new
|
// GET /admin/theme/new
|
||||||
let addTheme : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
let addTheme : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
||||||
hashForPage "Upload a Theme File"
|
hashForPage "Upload a Theme File"
|
||||||
|> withAntiCsrf ctx
|
|> withAntiCsrf ctx
|
||||||
|> adminView "theme-upload" next ctx
|
|> adminBareView "theme-upload" next ctx
|
||||||
|
|
||||||
/// Update the name and version for a theme based on the version.txt file, if present
|
/// Update the name and version for a theme based on the version.txt file, if present
|
||||||
let private updateNameAndVersion (theme : Theme) (zip : ZipArchive) = backgroundTask {
|
let private updateNameAndVersion (theme : Theme) (zip : ZipArchive) = backgroundTask {
|
||||||
|
@ -279,8 +295,8 @@ let saveTheme : HttpHandler = requireAccess Administrator >=> fun next ctx -> ta
|
||||||
match getThemeIdFromFileName themeFile.FileName with
|
match getThemeIdFromFileName themeFile.FileName with
|
||||||
| Ok themeId when themeId <> adminTheme ->
|
| Ok themeId when themeId <> adminTheme ->
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
let! theme = data.Theme.FindByIdWithoutText themeId
|
let! exists = data.Theme.Exists themeId
|
||||||
let isNew = Option.isNone theme
|
let isNew = not exists
|
||||||
let! model = ctx.BindFormAsync<UploadThemeModel> ()
|
let! model = ctx.BindFormAsync<UploadThemeModel> ()
|
||||||
if isNew || model.DoOverwrite then
|
if isNew || model.DoOverwrite then
|
||||||
// Load the theme to the database
|
// Load the theme to the database
|
||||||
|
@ -290,26 +306,45 @@ let saveTheme : HttpHandler = requireAccess Administrator >=> fun next ctx -> ta
|
||||||
do! ThemeAssetCache.refreshTheme themeId data
|
do! ThemeAssetCache.refreshTheme themeId data
|
||||||
TemplateCache.invalidateTheme themeId
|
TemplateCache.invalidateTheme themeId
|
||||||
// Save the .zip file
|
// Save the .zip file
|
||||||
use file = new FileStream ($"{themeId}-theme.zip", FileMode.Create)
|
use file = new FileStream ($"{ThemeId.toString themeId}-theme.zip", FileMode.Create)
|
||||||
do! stream.CopyToAsync file
|
do! themeFile.CopyToAsync file
|
||||||
do! addMessage ctx { UserMessage.success with Message = "Theme updated successfully" }
|
do! addMessage ctx
|
||||||
return! redirectToGet "admin/dashboard" next ctx
|
{ UserMessage.success with
|
||||||
|
Message = $"""Theme {if isNew then "add" else "updat"}ed successfully"""
|
||||||
|
}
|
||||||
|
return! toAdminDashboard next ctx
|
||||||
else
|
else
|
||||||
do! addMessage ctx
|
do! addMessage ctx
|
||||||
{ UserMessage.error with
|
{ UserMessage.error with
|
||||||
Message = "Theme exists and overwriting was not requested; nothing saved"
|
Message = "Theme exists and overwriting was not requested; nothing saved"
|
||||||
}
|
}
|
||||||
return! redirectToGet "admin/theme/new" next ctx
|
return! toAdminDashboard next ctx
|
||||||
| Ok _ ->
|
| Ok _ ->
|
||||||
do! addMessage ctx { UserMessage.error with Message = "You may not replace the admin theme" }
|
do! addMessage ctx { UserMessage.error with Message = "You may not replace the admin theme" }
|
||||||
return! redirectToGet "admin/theme/new" next ctx
|
return! toAdminDashboard next ctx
|
||||||
| Error message ->
|
| Error message ->
|
||||||
do! addMessage ctx { UserMessage.error with Message = message }
|
do! addMessage ctx { UserMessage.error with Message = message }
|
||||||
return! redirectToGet "admin/theme/update" next ctx
|
return! toAdminDashboard next ctx
|
||||||
else return! RequestErrors.BAD_REQUEST "Bad request" next ctx
|
else return! RequestErrors.BAD_REQUEST "Bad request" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- WEB LOG SETTINGS --
|
// POST /admin/theme/{id}/delete
|
||||||
|
let deleteTheme themeId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||||
|
let data = ctx.Data
|
||||||
|
if themeId = "admin" || themeId = "default" then
|
||||||
|
do! addMessage ctx { UserMessage.error with Message = $"You may not delete the {themeId} theme" }
|
||||||
|
return! listThemes next ctx
|
||||||
|
else
|
||||||
|
match! data.Theme.Delete (ThemeId themeId) with
|
||||||
|
| true ->
|
||||||
|
let zippedTheme = $"{themeId}-theme.zip"
|
||||||
|
if File.Exists zippedTheme then File.Delete zippedTheme
|
||||||
|
do! addMessage ctx { UserMessage.success with Message = $"Theme ID {themeId} deleted successfully" }
|
||||||
|
return! listThemes next ctx
|
||||||
|
| false -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~~ WEB LOG SETTINGS ~~
|
||||||
|
|
||||||
open System.Collections.Generic
|
open System.Collections.Generic
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,7 @@ let router : HttpHandler = choose [
|
||||||
routef "y/%s/edit" Admin.editCategory
|
routef "y/%s/edit" Admin.editCategory
|
||||||
])
|
])
|
||||||
route "/dashboard" >=> Admin.dashboard
|
route "/dashboard" >=> Admin.dashboard
|
||||||
|
route "/dashboard/administration" >=> Admin.adminDashboard
|
||||||
subRoute "/page" (choose [
|
subRoute "/page" (choose [
|
||||||
route "s" >=> Page.all 1
|
route "s" >=> Page.all 1
|
||||||
routef "s/page/%i" Page.all
|
routef "s/page/%i" Page.all
|
||||||
|
@ -141,7 +142,7 @@ let router : HttpHandler = choose [
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
subRoute "/theme" (choose [
|
subRoute "/theme" (choose [
|
||||||
route "s" >=> Admin.listThemes
|
route "/list" >=> Admin.listThemes
|
||||||
route "/new" >=> Admin.addTheme
|
route "/new" >=> Admin.addTheme
|
||||||
])
|
])
|
||||||
subRoute "/upload" (choose [
|
subRoute "/upload" (choose [
|
||||||
|
@ -188,7 +189,10 @@ let router : HttpHandler = choose [
|
||||||
routef "/%s/delete" Admin.deleteMapping
|
routef "/%s/delete" Admin.deleteMapping
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
route "/theme/new" >=> Admin.saveTheme
|
subRoute "/theme" (choose [
|
||||||
|
route "/new" >=> Admin.saveTheme
|
||||||
|
routef "/%s/delete" Admin.deleteTheme
|
||||||
|
])
|
||||||
subRoute "/upload" (choose [
|
subRoute "/upload" (choose [
|
||||||
route "/save" >=> Upload.save
|
route "/save" >=> Upload.save
|
||||||
routexp "/delete/(.*)" Upload.deleteFromDisk
|
routexp "/delete/(.*)" Upload.deleteFromDisk
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
{{ "admin/settings" | nav_link: "Settings" }}
|
{{ "admin/settings" | nav_link: "Settings" }}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if is_administrator %}
|
{%- if is_administrator %}
|
||||||
{{ "admin/themes" | nav_link: "Themes" }}
|
{{ "admin/dashboard/administration" | nav_link: "Admin" }}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
32
src/admin-theme/admin-dashboard.liquid
Normal file
32
src/admin-theme/admin-dashboard.liquid
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<h2 class="my-3">{{ page_title }}</h2>
|
||||||
|
<article>
|
||||||
|
<fieldset class="container pb-3">
|
||||||
|
<legend>Themes</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<a href="{{ "admin/theme/new" | 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>
|
||||||
|
<div class="row mwl-table-detail" id="theme_new"></div>
|
||||||
|
{{ theme_list }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="container">
|
||||||
|
<legend>Caches</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
TODO
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</article>
|
|
@ -1,6 +1,5 @@
|
||||||
<form method="post" id="themeList" class="container" hx-target="this" hx-swap="outerHTML show:window:top">
|
<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 }}">
|
<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" %}
|
{% include_template "_theme-list-columns" %}
|
||||||
{% for theme in themes -%}
|
{% for theme in themes -%}
|
||||||
<div class="row mwl-table-detail" id="theme_{{ theme.id }}">
|
<div class="row mwl-table-detail" id="theme_{{ theme.id }}">
|
||||||
|
@ -16,7 +15,7 @@
|
||||||
<span class="text-muted">v{{ theme.version }}</span>
|
<span class="text-muted">v{{ theme.version }}</span>
|
||||||
{% unless theme.is_in_use or theme.id == "default" %}
|
{% unless theme.is_in_use or theme.id == "default" %}
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- assign theme_del_link = "admin/" | append: theme.id | append: "/delete" | relative_link -%}
|
{%- assign theme_del_link = "admin/theme/" | append: theme.id | append: "/delete" | relative_link -%}
|
||||||
<a href="{{ theme_del_link }}" hx-post="{{ theme_del_link }}" class="text-danger"
|
<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.">
|
hx-confirm="Are you sure you want to delete the theme “{{ theme.name }}”? This action cannot be undone.">
|
||||||
Delete
|
Delete
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,26 +1,30 @@
|
||||||
<h2>{{ page_title }}</h2>
|
<div class="col">
|
||||||
<article>
|
<h5>{{ page_title }}</h5>
|
||||||
<form action="{{ "admin/theme/new" | relative_link }}"
|
<form action="{{ "admin/theme/new" | relative_link }}" method="post" class="container" enctype="multipart/form-data"
|
||||||
method="post" class="container" enctype="multipart/form-data" hx-boost="false">
|
hx-boost="false">
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-sm-6 offset-sm-3 pb-3">
|
<div class="col-12 col-sm-6 pb-3">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="file" id="file" name="file" class="form-control" accept=".zip" placeholder="Theme File" required>
|
<input type="file" id="file" name="file" class="form-control" accept=".zip" placeholder="Theme File" required>
|
||||||
<label for="file">Theme File</label>
|
<label for="file">Theme File</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-sm-6 pb-3">
|
<div class="col-12 col-sm-6 pb-3 d-flex justify-content-center align-items-center">
|
||||||
<div class="form-check form-switch pb-2">
|
<div class="form-check form-switch pb-2">
|
||||||
<input type="checkbox" name="DoOverwrite" id="doOverwrite" class="form-check-input" value="true">
|
<input type="checkbox" name="DoOverwrite" id="doOverwrite" class="form-check-input" value="true">
|
||||||
<label for="doOverwrite" class="form-check-label">Overwrite an Existing Theme If Required</label>
|
<label for="doOverwrite" class="form-check-label">Overwrite</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row pb-3">
|
<div class="row pb-3">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
<button type="submit" class="btn btn-primary">Upload Theme</button>
|
<button type="submit" class="btn btn-sm btn-primary">Upload Theme</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary ms-3"
|
||||||
|
onclick="document.getElementById('theme_new').innerHTML = ''">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user