- Add full chapter support (#6) - Add built-in redirect functionality (#39) - Support building Docker containers for release (#38) - Support canonical domain configuration (#37) - Add unit tests for domain/models and integration tests for all three data stores - Convert SQLite storage to use JSON documents, similar to PostgreSQL - Convert admin templates to Giraffe View Engine (from Liquid) - Add .NET 8 support
259 lines
12 KiB
Forth
259 lines
12 KiB
Forth
module MyWebLog.Views.User
|
|
|
|
open Giraffe.Htmx.Common
|
|
open Giraffe.ViewEngine
|
|
open Giraffe.ViewEngine.Htmx
|
|
open MyWebLog
|
|
open MyWebLog.ViewModels
|
|
|
|
/// User edit form
|
|
let edit (model: EditUserModel) app =
|
|
let levelOption value name =
|
|
option [ _value value; if model.AccessLevel = value then _selected ] [ txt name ]
|
|
div [ _class "col-12" ] [
|
|
h5 [ _class "my-3" ] [ txt app.PageTitle ]
|
|
form [ _hxPost (relUrl app "admin/settings/user/save"); _method "post"; _class "container"
|
|
_hxTarget "#user_panel"; _hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [
|
|
antiCsrf app
|
|
input [ _type "hidden"; _name "Id"; _value model.Id ]
|
|
div [ _class "row" ] [
|
|
div [ _class "col-12 col-md-5 col-lg-3 col-xxl-2 offset-xxl-1 mb-3" ] [
|
|
div [ _class "form-floating" ] [
|
|
select [ _name "AccessLevel"; _id "accessLevel"; _class "form-control"; _required
|
|
_autofocus ] [
|
|
levelOption (string Author) "Author"
|
|
levelOption (string Editor) "Editor"
|
|
levelOption (string WebLogAdmin) "Web Log Admin"
|
|
if app.IsAdministrator then levelOption (string Administrator) "Administrator"
|
|
]
|
|
label [ _for "accessLevel" ] [ raw "Access Level" ]
|
|
]
|
|
]
|
|
div [ _class "col-12 col-md-7 col-lg-4 col-xxl-3 mb-3" ] [
|
|
emailField [ _required ] (nameof model.Email) "E-mail Address" model.Email []
|
|
]
|
|
div [ _class "col-12 col-lg-5 mb-3" ] [
|
|
textField [] (nameof model.Url) "User’s Personal URL" model.Url []
|
|
]
|
|
]
|
|
div [ _class "row mb-3" ] [
|
|
div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 offset-xl-1 pb-3" ] [
|
|
textField [ _required ] (nameof model.FirstName) "First Name" model.FirstName []
|
|
]
|
|
div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 pb-3" ] [
|
|
textField [ _required ] (nameof model.LastName) "Last Name" model.LastName []
|
|
]
|
|
div [ _class "col-12 col-md-6 offset-md-3 col-lg-4 offset-lg-0 col-xl-3 offset-xl-1 pb-3" ] [
|
|
textField [ _required ] (nameof model.PreferredName) "Preferred Name" model.PreferredName []
|
|
]
|
|
]
|
|
div [ _class "row mb-3" ] [
|
|
div [ _class "col-12 col-xl-10 offset-xl-1" ] [
|
|
fieldset [ _class "p-2" ] [
|
|
legend [ _class "ps-1" ] [
|
|
if not model.IsNew then raw "Change "
|
|
raw "Password"
|
|
]
|
|
if not model.IsNew then
|
|
div [ _class "row" ] [
|
|
div [ _class "col" ] [
|
|
p [ _class "form-text" ] [
|
|
raw "Optional; leave blank not change the user’s password"
|
|
]
|
|
]
|
|
]
|
|
div [ _class "row" ] [
|
|
let attrs, newLbl = if model.IsNew then [ _required ], "" else [], "New "
|
|
div [ _class "col-12 col-md-6 pb-3" ] [
|
|
passwordField attrs (nameof model.Password) $"{newLbl}Password" "" []
|
|
]
|
|
div [ _class "col-12 col-md-6 pb-3" ] [
|
|
passwordField attrs (nameof model.PasswordConfirm) $"Confirm {newLbl}Password" "" []
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
div [ _class "row mb-3" ] [
|
|
div [ _class "col text-center" ] [
|
|
saveButton; raw " "
|
|
if model.IsNew then
|
|
button [ _type "button"; _class "btn btn-sm btn-secondary ms-3"
|
|
_onclick "document.getElementById('user_new').innerHTML = ''" ] [
|
|
raw "Cancel"
|
|
]
|
|
else
|
|
a [ _href (relUrl app "admin/settings/users"); _class "btn btn-sm btn-secondary ms-3" ] [
|
|
raw "Cancel"
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
|> List.singleton
|
|
|
|
|
|
/// User log on form
|
|
let logOn (model: LogOnModel) (app: AppViewContext) = [
|
|
h2 [ _class "my-3" ] [ rawText "Log On to "; encodedText app.WebLog.Name ]
|
|
article [ _class "py-3" ] [
|
|
form [ _action (relUrl app "user/log-on"); _method "post"; _class "container"; _hxPushUrl "true" ] [
|
|
antiCsrf app
|
|
if Option.isSome model.ReturnTo then input [ _type "hidden"; _name "ReturnTo"; _value model.ReturnTo.Value ]
|
|
div [ _class "row" ] [
|
|
div [ _class "col-12 col-md-6 col-lg-4 offset-lg-2 pb-3" ] [
|
|
emailField [ _required; _autofocus ] (nameof model.EmailAddress) "E-mail Address" "" []
|
|
]
|
|
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
|
|
passwordField [ _required ] (nameof model.Password) "Password" "" []
|
|
]
|
|
]
|
|
div [ _class "row pb-3" ] [
|
|
div [ _class "col text-center" ] [
|
|
button [ _type "submit"; _class "btn btn-primary" ] [ rawText "Log On" ]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
|
|
|
|
/// The list of users for a web log (part of web log settings page)
|
|
let userList (model: WebLogUser list) app =
|
|
let userCol = "col-12 col-md-4 col-xl-3"
|
|
let emailCol = "col-12 col-md-4 col-xl-4"
|
|
let cre8Col = "d-none d-xl-block col-xl-2"
|
|
let lastCol = "col-12 col-md-4 col-xl-3"
|
|
let badge = "ms-2 badge bg"
|
|
let userDetail (user: WebLogUser) =
|
|
div [ _class "row mwl-table-detail"; _id $"user_{user.Id}" ] [
|
|
div [ _class $"{userCol} no-wrap" ] [
|
|
txt user.PreferredName; raw " "
|
|
match user.AccessLevel with
|
|
| Administrator -> span [ _class $"{badge}-success" ] [ raw "ADMINISTRATOR" ]
|
|
| WebLogAdmin -> span [ _class $"{badge}-primary" ] [ raw "WEB LOG ADMIN" ]
|
|
| Editor -> span [ _class $"{badge}-secondary" ] [ raw "EDITOR" ]
|
|
| Author -> span [ _class $"{badge}-dark" ] [ raw "AUTHOR" ]
|
|
br []
|
|
if app.IsAdministrator || (app.IsWebLogAdmin && not (user.AccessLevel = Administrator)) then
|
|
let userUrl = relUrl app $"admin/settings/user/{user.Id}"
|
|
small [] [
|
|
a [ _href $"{userUrl}/edit"; _hxTarget $"#user_{user.Id}"
|
|
_hxSwap $"{HxSwap.InnerHtml} show:#user_{user.Id}:top" ] [
|
|
raw "Edit"
|
|
]
|
|
if app.UserId.Value <> user.Id then
|
|
span [ _class "text-muted" ] [ raw " • " ]
|
|
a [ _href userUrl; _hxDelete userUrl; _class "text-danger"
|
|
_hxConfirm $"Are you sure you want to delete the user “{user.PreferredName}”? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)" ] [
|
|
raw "Delete"
|
|
]
|
|
]
|
|
]
|
|
div [ _class emailCol ] [
|
|
txt $"{user.FirstName} {user.LastName}"; br []
|
|
small [ _class "text-muted" ] [
|
|
txt user.Email
|
|
if Option.isSome user.Url then
|
|
br []; txt user.Url.Value
|
|
]
|
|
]
|
|
div [ _class "d-none d-xl-block col-xl-2" ] [
|
|
if user.CreatedOn = Noda.epoch then raw "N/A" else longDate app user.CreatedOn
|
|
]
|
|
div [ _class "col-12 col-md-4 col-xl-3" ] [
|
|
match user.LastSeenOn with
|
|
| Some it -> longDate app it; raw " at "; shortTime app it
|
|
| None -> raw "--"
|
|
]
|
|
]
|
|
div [ _id "user_panel" ] [
|
|
a [ _href (relUrl app "admin/settings/user/new/edit"); _class "btn btn-primary btn-sm mb-3"
|
|
_hxTarget "#user_new" ] [
|
|
raw "Add a New User"
|
|
]
|
|
div [ _class "container g-0" ] [
|
|
div [ _class "row mwl-table-heading" ] [
|
|
div [ _class userCol ] [
|
|
raw "User"; span [ _class "d-md-none" ] [ raw "; Full Name / E-mail; Last Log On" ]
|
|
]
|
|
div [ _class $"{emailCol} d-none d-md-inline-block" ] [ raw "Full Name / E-mail" ]
|
|
div [ _class cre8Col ] [ raw "Created" ]
|
|
div [ _class $"{lastCol} d-none d-md-block" ] [ raw "Last Log On" ]
|
|
]
|
|
]
|
|
div [ _id "userList" ] [
|
|
div [ _class "container g-0" ] [
|
|
div [ _class "row mwl-table-detail"; _id "user_new" ] []
|
|
]
|
|
form [ _method "post"; _class "container g-0"; _hxTarget "#user_panel"
|
|
_hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [
|
|
antiCsrf app
|
|
yield! List.map userDetail model
|
|
]
|
|
]
|
|
]
|
|
|> List.singleton
|
|
|
|
|
|
/// Edit My Info form
|
|
let myInfo (model: EditMyInfoModel) (user: WebLogUser) app = [
|
|
h2 [ _class "my-3" ] [ txt app.PageTitle ]
|
|
article [] [
|
|
form [ _action (relUrl app "admin/my-info"); _method "post" ] [
|
|
antiCsrf app
|
|
div [ _class "d-flex flex-row flex-wrap justify-content-around" ] [
|
|
div [ _class "text-center mb-3 lh-sm" ] [
|
|
strong [ _class "text-decoration-underline" ] [ raw "Access Level" ]; br []
|
|
raw (string user.AccessLevel)
|
|
]
|
|
div [ _class "text-center mb-3 lh-sm" ] [
|
|
strong [ _class "text-decoration-underline" ] [ raw "Created" ]; br []
|
|
if user.CreatedOn = Noda.epoch then raw "N/A" else longDate app user.CreatedOn
|
|
]
|
|
div [ _class "text-center mb-3 lh-sm" ] [
|
|
strong [ _class "text-decoration-underline" ] [ raw "Last Log On" ]; br []
|
|
longDate app user.LastSeenOn.Value; raw " at "; shortTime app user.LastSeenOn.Value
|
|
]
|
|
]
|
|
div [ _class "container" ] [
|
|
div [ _class "row" ] [ div [ _class "col" ] [ hr [ _class "mt-0" ] ] ]
|
|
div [ _class "row mb-3" ] [
|
|
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
|
|
textField [ _required; _autofocus ] (nameof model.FirstName) "First Name" model.FirstName []
|
|
]
|
|
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
|
|
textField [ _required ] (nameof model.LastName) "Last Name" model.LastName []
|
|
]
|
|
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
|
|
textField [ _required ] (nameof model.PreferredName) "Preferred Name" model.PreferredName []
|
|
]
|
|
]
|
|
div [ _class "row mb-3" ] [
|
|
div [ _class "col" ] [
|
|
fieldset [ _class "p-2" ] [
|
|
legend [ _class "ps-1" ] [ raw "Change Password" ]
|
|
div [ _class "row" ] [
|
|
div [ _class "col" ] [
|
|
p [ _class "form-text" ] [
|
|
raw "Optional; leave blank to keep your current password"
|
|
]
|
|
]
|
|
]
|
|
div [ _class "row" ] [
|
|
div [ _class "col-12 col-md-6 pb-3" ] [
|
|
passwordField [] (nameof model.NewPassword) "New Password" "" []
|
|
]
|
|
div [ _class "col-12 col-md-6 pb-3" ] [
|
|
passwordField [] (nameof model.NewPasswordConfirm) "Confirm New Password" "" []
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
div [ _class "row" ] [ div [ _class "col text-center mb-3" ] [ saveButton ] ]
|
|
]
|
|
]
|
|
]
|
|
]
|