package presets
import (
"fmt"
"log"
"net/http"
"os"
"regexp"
"strings"
"github.com/iancoleman/strcase"
"github.com/jinzhu/inflection"
"github.com/qor5/admin/presets/actions"
. "github.com/qor5/ui/vuetify"
"github.com/qor5/ui/vuetifyx"
"github.com/qor5/web"
"github.com/qor5/x/i18n"
"github.com/qor5/x/perm"
h "github.com/theplant/htmlgo"
"go.uber.org/zap"
goji "goji.io"
"goji.io/middleware"
"goji.io/pat"
"golang.org/x/text/language"
"golang.org/x/text/language/display"
)
type Builder struct {
prefix string
models []*ModelBuilder
mux *goji.Mux
builder *web.Builder
i18nBuilder *i18n.Builder
logger *zap.Logger
permissionBuilder *perm.Builder
verifier *perm.Verifier
layoutFunc func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc)
detailLayoutFunc func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc)
dataOperator DataOperator
messagesFunc MessagesFunc
homePageFunc web.PageFunc
notFoundFunc web.PageFunc
homePageLayoutConfig *LayoutConfig
notFoundPageLayoutConfig *LayoutConfig
brandFunc ComponentFunc
profileFunc ComponentFunc
switchLanguageFunc ComponentFunc
brandProfileSwitchLanguageDisplayFunc func(brand, profile, switchLanguage h.HTMLComponent) h.HTMLComponent
menuTopItems map[string]ComponentFunc
notificationCountFunc func(ctx *web.EventContext) int
notificationContentFunc ComponentFunc
brandTitle string
vuetifyOptions string
progressBarColor string
rightDrawerWidth string
writeFieldDefaults *FieldDefaults
listFieldDefaults *FieldDefaults
detailFieldDefaults *FieldDefaults
extraAssets []*extraAsset
assetFunc AssetFunc
menuGroups MenuGroups
menuOrder []interface{}
wrapHandlers map[string]func(in http.Handler) (out http.Handler)
}
type AssetFunc func(ctx *web.EventContext)
type extraAsset struct {
path string
contentType string
body web.ComponentsPack
refTag string
}
const (
CoreI18nModuleKey i18n.ModuleKey = "CoreI18nModuleKey"
ModelsI18nModuleKey i18n.ModuleKey = "ModelsI18nModuleKey"
)
const (
OpenConfirmDialog = "presets_ConfirmDialog"
)
func New() *Builder {
l, _ := zap.NewDevelopment()
r := &Builder{
logger: l,
builder: web.New(),
i18nBuilder: i18n.New().
RegisterForModule(language.English, CoreI18nModuleKey, Messages_en_US).
RegisterForModule(language.SimplifiedChinese, CoreI18nModuleKey, Messages_zh_CN).
RegisterForModule(language.Japanese, CoreI18nModuleKey, Messages_ja_JP),
writeFieldDefaults: NewFieldDefaults(WRITE),
listFieldDefaults: NewFieldDefaults(LIST),
detailFieldDefaults: NewFieldDefaults(DETAIL),
progressBarColor: "amber",
menuTopItems: make(map[string]ComponentFunc),
brandTitle: "Admin",
rightDrawerWidth: "600",
verifier: perm.NewVerifier(PermModule, nil),
homePageLayoutConfig: &LayoutConfig{SearchBoxInvisible: true},
notFoundPageLayoutConfig: &LayoutConfig{
SearchBoxInvisible: true,
NotificationCenterInvisible: true,
},
wrapHandlers: make(map[string]func(in http.Handler) (out http.Handler)),
}
r.GetWebBuilder().RegisterEventFunc(OpenConfirmDialog, r.openConfirmDialog)
r.layoutFunc = r.defaultLayout
r.detailLayoutFunc = r.defaultLayout
return r
}
func (b *Builder) I18n() (r *i18n.Builder) {
return b.i18nBuilder
}
func (b *Builder) SetI18n(v *i18n.Builder) (r *Builder) {
b.i18nBuilder = v
return b
}
func (b *Builder) Permission(v *perm.Builder) (r *Builder) {
b.permissionBuilder = v
b.verifier = perm.NewVerifier(PermModule, v)
return b
}
func (b *Builder) GetPermission() (r *perm.Builder) {
return b.permissionBuilder
}
func (b *Builder) URIPrefix(v string) (r *Builder) {
b.prefix = strings.TrimRight(v, "/")
return b
}
func (b *Builder) GetURIPrefix() string {
return b.prefix
}
func (b *Builder) LayoutFunc(v func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc)) (r *Builder) {
b.layoutFunc = v
return b
}
func (b *Builder) GetLayoutFunc() func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc) {
return b.layoutFunc
}
func (b *Builder) DetailLayoutFunc(v func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc)) (r *Builder) {
b.detailLayoutFunc = v
return b
}
func (b *Builder) GetDetailLayoutFunc() func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc) {
return b.detailLayoutFunc
}
func (b *Builder) HomePageLayoutConfig(v *LayoutConfig) (r *Builder) {
b.homePageLayoutConfig = v
return b
}
func (b *Builder) NotFoundPageLayoutConfig(v *LayoutConfig) (r *Builder) {
b.notFoundPageLayoutConfig = v
return b
}
func (b *Builder) Builder(v *web.Builder) (r *Builder) {
b.builder = v
return b
}
func (b *Builder) GetWebBuilder() (r *web.Builder) {
return b.builder
}
func (b *Builder) Logger(v *zap.Logger) (r *Builder) {
b.logger = v
return b
}
func (b *Builder) MessagesFunc(v MessagesFunc) (r *Builder) {
b.messagesFunc = v
return b
}
func (b *Builder) HomePageFunc(v web.PageFunc) (r *Builder) {
b.homePageFunc = v
return b
}
func (b *Builder) NotFoundFunc(v web.PageFunc) (r *Builder) {
b.notFoundFunc = v
return b
}
func (b *Builder) BrandFunc(v ComponentFunc) (r *Builder) {
b.brandFunc = v
return b
}
func (b *Builder) ProfileFunc(v ComponentFunc) (r *Builder) {
b.profileFunc = v
return b
}
func (b *Builder) GetProfileFunc() ComponentFunc {
return b.profileFunc
}
func (b *Builder) SwitchLanguageFunc(v ComponentFunc) (r *Builder) {
b.switchLanguageFunc = v
return b
}
func (b *Builder) BrandProfileSwitchLanguageDisplayFuncFunc(f func(brand, profile, switchLanguage h.HTMLComponent) h.HTMLComponent) (r *Builder) {
b.brandProfileSwitchLanguageDisplayFunc = f
return b
}
func (b *Builder) NotificationFunc(contentFunc ComponentFunc, countFunc func(ctx *web.EventContext) int) (r *Builder) {
b.notificationCountFunc = countFunc
b.notificationContentFunc = contentFunc
b.GetWebBuilder().RegisterEventFunc(actions.NotificationCenter, b.notificationCenter)
return b
}
func (b *Builder) BrandTitle(v string) (r *Builder) {
b.brandTitle = v
return b
}
func (b *Builder) GetBrandTitle() string {
return b.brandTitle
}
func (b *Builder) VuetifyOptions(v string) (r *Builder) {
b.vuetifyOptions = v
return b
}
func (b *Builder) RightDrawerWidth(v string) (r *Builder) {
b.rightDrawerWidth = v
return b
}
func (b *Builder) ProgressBarColor(v string) (r *Builder) {
b.progressBarColor = v
return b
}
func (b *Builder) GetProgressBarColor() string {
return b.progressBarColor
}
func (b *Builder) AssetFunc(v AssetFunc) (r *Builder) {
b.assetFunc = v
return b
}
func (b *Builder) ExtraAsset(path string, contentType string, body web.ComponentsPack, refTag ...string) (r *Builder) {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
var theOne *extraAsset
for _, ea := range b.extraAssets {
if ea.path == path {
theOne = ea
break
}
}
if theOne == nil {
theOne = &extraAsset{path: path, contentType: contentType, body: body}
b.extraAssets = append(b.extraAssets, theOne)
} else {
theOne.contentType = contentType
theOne.body = body
}
if len(refTag) > 0 {
theOne.refTag = refTag[0]
}
return b
}
func (b *Builder) FieldDefaults(v FieldMode) (r *FieldDefaults) {
if v == WRITE {
return b.writeFieldDefaults
}
if v == LIST {
return b.listFieldDefaults
}
if v == DETAIL {
return b.detailFieldDefaults
}
return r
}
func (b *Builder) NewFieldsBuilder(v FieldMode) (r *FieldsBuilder) {
r = NewFieldsBuilder().Defaults(b.FieldDefaults(v))
return
}
func (b *Builder) Model(v interface{}) (r *ModelBuilder) {
r = NewModelBuilder(b, v)
b.models = append(b.models, r)
return r
}
func (b *Builder) DataOperator(v DataOperator) (r *Builder) {
b.dataOperator = v
return b
}
func modelNames(ms []*ModelBuilder) (r []string) {
for _, m := range ms {
r = append(r, m.uriName)
}
return
}
func (b *Builder) defaultBrandFunc(ctx *web.EventContext) (r h.HTMLComponent) {
return
}
func (b *Builder) MenuGroup(name string) *MenuGroupBuilder {
mgb := b.menuGroups.MenuGroup(name)
if !b.isMenuGroupInOrder(mgb) {
b.menuOrder = append(b.menuOrder, mgb)
}
return mgb
}
func (b *Builder) isMenuGroupInOrder(mgb *MenuGroupBuilder) bool {
for _, v := range b.menuOrder {
if v == mgb {
return true
}
}
return false
}
func (b *Builder) removeMenuGroupInOrder(mgb *MenuGroupBuilder) {
for i, om := range b.menuOrder {
if om == mgb {
b.menuOrder = append(b.menuOrder[:i], b.menuOrder[i+1:]...)
break
}
}
}
// item can be Slug name, model name, *MenuGroupBuilder
// the underlying logic is using Slug name,
// so if the Slug name is customized, item must be the Slug name
// example:
// b.MenuOrder(
//
// b.MenuGroup("Product Management").SubItems(
// "products",
// "Variant",
// ),
// "customized-uri",
//
// )
func (b *Builder) MenuOrder(items ...interface{}) {
for _, item := range items {
switch v := item.(type) {
case string:
b.menuOrder = append(b.menuOrder, v)
case *MenuGroupBuilder:
if b.isMenuGroupInOrder(v) {
b.removeMenuGroupInOrder(v)
}
b.menuOrder = append(b.menuOrder, v)
default:
panic(fmt.Sprintf("unknown menu order item type: %T\n", item))
}
}
}
type defaultMenuIconRE struct {
re *regexp.Regexp
icon string
}
var defaultMenuIconREs = []defaultMenuIconRE{
// user
{re: regexp.MustCompile(`\busers?|members?\b`), icon: "person"},
// store
{re: regexp.MustCompile(`\bstores?\b`), icon: "store"},
// order
{re: regexp.MustCompile(`\borders?\b`), icon: "shopping_cart"},
// product
{re: regexp.MustCompile(`\bproducts?\b`), icon: "format_list_bulleted"},
// post
{re: regexp.MustCompile(`\bposts?|articles?\b`), icon: "article"},
// web
{re: regexp.MustCompile(`\bweb|site\b`), icon: "web"},
// seo
{re: regexp.MustCompile(`\bseo\b`), icon: "travel_explore"},
// i18n
{re: regexp.MustCompile(`\bi18n|translations?\b`), icon: "language"},
// chart
{re: regexp.MustCompile(`\banalytics?|charts?|statistics?\b`), icon: "analytics"},
// dashboard
{re: regexp.MustCompile(`\bdashboard\b`), icon: "dashboard"},
// setting
{re: regexp.MustCompile(`\bsettings?\b`), icon: "settings"},
}
func defaultMenuIcon(mLabel string) string {
ws := strings.Join(strings.Split(strcase.ToSnake(mLabel), "_"), " ")
for _, v := range defaultMenuIconREs {
if v.re.MatchString(ws) {
return v.icon
}
}
return "widgets"
}
const menuFontWeight = "500"
const subMenuFontWeight = "400"
func (b *Builder) menuItem(ctx *web.EventContext, m *ModelBuilder, isSub bool) (r h.HTMLComponent) {
menuIcon := m.menuIcon
fontWeight := subMenuFontWeight
if isSub {
// menuIcon = ""
} else {
fontWeight = menuFontWeight
if menuIcon == "" {
menuIcon = defaultMenuIcon(m.label)
}
}
href := m.Info().ListingHref()
if m.link != "" {
href = m.link
}
if m.defaultURLQueryFunc != nil {
href = fmt.Sprintf("%s?%s", href, m.defaultURLQueryFunc(ctx.R).Encode())
}
item := VListItem(
VListItemAction(
VIcon(menuIcon),
).Attr("style", "margin-right: 16px"),
VListItemContent(
VListItemTitle(
h.Text(i18n.T(ctx.R, ModelsI18nModuleKey, m.label)),
).Attr("style", fmt.Sprintf("white-space: normal; font-weight: %s;font-size: 14px;", fontWeight)),
),
)
if strings.HasPrefix(href, "/") {
item.Attr("@click", web.Plaid().PushStateURL(href).Go())
} else {
item.Href(href)
}
if b.isMenuItemActive(ctx, m) {
item = item.Class("v-list-item--active")
}
return item
}
func (b *Builder) isMenuItemActive(ctx *web.EventContext, m *ModelBuilder) bool {
href := m.Info().ListingHref()
if m.link != "" {
href = m.link
}
path := strings.TrimSuffix(ctx.R.URL.Path, "/")
if path == "" && href == "/" {
return true
}
if path == href {
return true
}
if href == b.prefix {
return false
}
if href != "/" && strings.HasPrefix(path, href) {
return true
}
return false
}
func (b *Builder) CreateMenus(ctx *web.EventContext) (r h.HTMLComponent) {
mMap := make(map[string]*ModelBuilder)
for _, m := range b.models {
mMap[m.uriName] = m
}
inOrderMap := make(map[string]struct{})
var menus []h.HTMLComponent
for _, om := range b.menuOrder {
switch v := om.(type) {
case *MenuGroupBuilder:
disabled := false
if b.verifier.Do(PermList).SnakeOn("mg_"+v.name).WithReq(ctx.R).IsAllowed() != nil {
disabled = true
}
groupIcon := v.icon
if groupIcon == "" {
groupIcon = defaultMenuIcon(v.name)
}
var subMenus = []h.HTMLComponent{
VListItem(
VListItemAction(
VIcon(groupIcon),
).Attr("style", "margin-right: 16px;"),
VListItemContent(
VListItemTitle(h.Text(i18n.T(ctx.R, ModelsI18nModuleKey, v.name))).
Attr("style", fmt.Sprintf("white-space: normal; font-weight: %s;font-size: 14px;", menuFontWeight)),
),
).Slot("activator").Class("pa-0"),
}
subCount := 0
hasActiveMenuItem := false
for _, subOm := range v.subMenuItems {
m, ok := mMap[subOm]
if !ok {
m = mMap[inflection.Plural(strcase.ToKebab(subOm))]
}
if m == nil {
continue
}
m.menuGroupName = v.name
if m.notInMenu {
continue
}
if m.Info().Verifier().Do(PermList).WithReq(ctx.R).IsAllowed() != nil {
continue
}
subMenus = append(subMenus, b.menuItem(ctx, m, true))
subCount++
inOrderMap[m.uriName] = struct{}{}
if b.isMenuItemActive(ctx, m) {
hasActiveMenuItem = true
}
}
if subCount == 0 {
continue
}
if disabled {
continue
}
menus = append(menus, VListGroup(
subMenus...).
Value(hasActiveMenuItem),
)
case string:
m, ok := mMap[v]
if !ok {
m = mMap[inflection.Plural(strcase.ToKebab(v))]
}
if m == nil {
continue
}
if m.Info().Verifier().Do(PermList).WithReq(ctx.R).IsAllowed() != nil {
continue
}
if m.notInMenu {
continue
}
menus = append(menus, b.menuItem(ctx, m, false))
inOrderMap[m.uriName] = struct{}{}
}
}
for _, m := range b.models {
_, ok := inOrderMap[m.uriName]
if ok {
continue
}
if m.Info().Verifier().Do(PermList).WithReq(ctx.R).IsAllowed() != nil {
continue
}
if m.notInMenu {
continue
}
menus = append(menus, b.menuItem(ctx, m, false))
}
r = h.Div(
VList(menus...).Class("primary--text").Dense(true),
)
return
}
func (b *Builder) RunBrandFunc(ctx *web.EventContext) (r h.HTMLComponent) {
if b.brandFunc != nil {
return b.brandFunc(ctx)
}
return VRow(h.H1(i18n.T(ctx.R, ModelsI18nModuleKey, b.brandTitle))).Class("text-button")
}
func (b *Builder) RunSwitchLanguageFunc(ctx *web.EventContext) (r h.HTMLComponent) {
if b.switchLanguageFunc != nil {
return b.switchLanguageFunc(ctx)
}
var supportLanguages = b.I18n().GetSupportLanguagesFromRequest(ctx.R)
if len(b.I18n().GetSupportLanguages()) <= 1 || len(supportLanguages) == 0 {
return nil
}
queryName := b.I18n().GetQueryName()
msgr := MustGetMessages(ctx.R)
if len(supportLanguages) == 1 {
return h.Template().Children(
h.Div(
VList(
VListItem(
VListItemIcon(
VIcon("translate").Small(true).Class("ml-1"),
).Attr("style", "margin-right: 16px"),
VListItemContent(
VListItemTitle(
h.Div(h.Text(fmt.Sprintf("%s%s %s", msgr.Language, msgr.Colon, display.Self.Name(supportLanguages[0])))).Role("button"),
),
),
).Class("pa-0").Dense(true),
).Class("pa-0 ma-n4 mt-n6"),
).Attr("@click", web.Plaid().Query(queryName, supportLanguages[0].String()).Go()),
)
}
var matcher = language.NewMatcher(supportLanguages)
lang := ctx.R.FormValue(queryName)
if lang == "" {
lang = b.i18nBuilder.GetCurrentLangFromCookie(ctx.R)
}
accept := ctx.R.Header.Get("Accept-Language")
var displayLanguage language.Tag
_, i := language.MatchStrings(matcher, lang, accept)
displayLanguage = supportLanguages[i]
var languages []h.HTMLComponent
for _, tag := range supportLanguages {
languages = append(languages,
h.Div(
VListItem(
VListItemContent(
VListItemTitle(
h.Div(h.Text(display.Self.Name(tag))),
),
),
).Attr("@click", web.Plaid().Query(queryName, tag.String()).Go()),
),
)
}
return VMenu().OffsetY(true).Children(
h.Template().Attr("v-slot:activator", "{on, attrs}").Children(
h.Div(
VList(
VListItem(
VListItemIcon(
VIcon("translate").Small(true).Class("ml-1"),
).Attr("style", "margin-right: 16px"),
VListItemContent(
VListItemTitle(
h.Text(fmt.Sprintf("%s%s %s", msgr.Language, msgr.Colon, display.Self.Name(displayLanguage))),
),
),
VListItemIcon(
VIcon("arrow_drop_down").Small(false).Class("mr-1"),
),
).Class("pa-0").Dense(true),
).Class("pa-0 ma-n4 mt-n6"),
).Attr("v-bind", "attrs").Attr("v-on", "on"),
),
VList(
languages...,
).Dense(true),
)
}
func (b *Builder) AddMenuTopItemFunc(key string, v ComponentFunc) (r *Builder) {
b.menuTopItems[key] = v
return b
}
func (b *Builder) RunBrandProfileSwitchLanguageDisplayFunc(brand, profile, switchLanguage h.HTMLComponent, ctx *web.EventContext) (r h.HTMLComponent) {
if b.brandProfileSwitchLanguageDisplayFunc != nil {
return b.brandProfileSwitchLanguageDisplayFunc(brand, profile, switchLanguage)
}
var items []h.HTMLComponent
items = append(items,
h.If(brand != nil,
VListItem(
VCardText(brand),
),
),
h.If(profile != nil,
VListItem(
VCardText(profile),
),
),
h.If(switchLanguage != nil,
VListItem(
VCardText(switchLanguage),
).Dense(true),
),
)
for _, v := range b.menuTopItems {
items = append(items,
h.If(v(ctx) != nil,
VListItem(
VCardText(v(ctx)),
),
))
}
return h.Div(
items...,
)
}
func MustGetMessages(r *http.Request) *Messages {
return i18n.MustGetModuleMessages(r, CoreI18nModuleKey, Messages_en_US).(*Messages)
}
const RightDrawerPortalName = "presets_RightDrawerPortalName"
const rightDrawerContentPortalName = "presets_RightDrawerContentPortalName"
const DialogPortalName = "presets_DialogPortalName"
const dialogContentPortalName = "presets_DialogContentPortalName"
const NotificationCenterPortalName = "notification-center"
const DefaultConfirmDialogPortalName = "presets_confirmDialogPortalName"
const ListingDialogPortalName = "presets_listingDialogPortalName"
const singletonEditingPortalName = "presets_SingletonEditingPortalName"
const closeRightDrawerVarScript = "vars.presetsRightDrawer = false"
const closeDialogVarScript = "vars.presetsDialog = false"
const CloseListingDialogVarScript = "vars.presetsListingDialog = false"
func (b *Builder) overlay(overlayType string, r *web.EventResponse, comp h.HTMLComponent, width string) {
if overlayType == actions.Dialog {
b.dialog(r, comp, width)
return
}
b.rightDrawer(r, comp, width)
}
func (b *Builder) rightDrawer(r *web.EventResponse, comp h.HTMLComponent, width string) {
if width == "" {
width = b.rightDrawerWidth
}
r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
Name: RightDrawerPortalName,
Body: VNavigationDrawer(
web.GlobalEvents().Attr("@keyup.esc", "vars.presetsRightDrawer = false"),
web.Portal(comp).Name(rightDrawerContentPortalName),
).
// Attr("@input", "plaidForm.dirty && vars.presetsRightDrawer == false && !confirm('You have unsaved changes on this form. If you close it, you will lose all unsaved changes. Are you sure you want to close it?') ? vars.presetsRightDrawer = true: vars.presetsRightDrawer = $event"). // remove because drawer plaidForm has to be reset when UpdateOverlayContent
Class("v-navigation-drawer--temporary").
Attr("v-model", "vars.presetsRightDrawer").
Right(true).
Fixed(true).
Attr("width", width).
Bottom(false).
Attr(":height", `"100%"`),
// Temporary(true),
// HideOverlay(true).
// Floating(true).
})
r.VarsScript = "setTimeout(function(){ vars.presetsRightDrawer = true }, 100)"
}
// Attr("@input", "alert(plaidForm.dirty) && !confirm('You have unsaved changes on this form. If you close it, you will lose all unsaved changes. Are you sure you want to close it?') ? vars.presetsDialog = true : vars.presetsDialog = $event").
func (b *Builder) dialog(r *web.EventResponse, comp h.HTMLComponent, width string) {
if width == "" {
width = b.rightDrawerWidth
}
r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
Name: DialogPortalName,
Body: web.Scope(
VDialog(
web.Portal(comp).Name(dialogContentPortalName),
).
Attr("v-model", "vars.presetsDialog").
Width(width),
).VSlot("{ plaidForm }"),
})
r.VarsScript = "setTimeout(function(){ vars.presetsDialog = true }, 100)"
}
type LayoutConfig struct {
SearchBoxInvisible bool
NotificationCenterInvisible bool
}
func (b *Builder) notificationCenter(ctx *web.EventContext) (er web.EventResponse, err error) {
total := b.notificationCountFunc(ctx)
content := b.notificationContentFunc(ctx)
icon := VIcon("notifications").Color("white")
er.Body = VMenu().OffsetY(true).Children(
h.Template().Attr("v-slot:activator", "{on, attrs}").Children(
VBtn("").Icon(true).Children(
h.If(total > 0,
VBadge(
icon,
).Content(total).Overlap(true).Color("red"),
).Else(icon),
).Attr("v-bind", "attrs").Attr("v-on", "on").Class("ml-1"),
),
VCard(content))
return
}
const (
ConfirmDialogConfirmEvent = "presets_ConfirmDialog_ConfirmEvent"
ConfirmDialogPromptText = "presets_ConfirmDialog_PromptText"
ConfirmDialogDialogPortalName = "presets_ConfirmDialog_DialogPortalName"
)
func (b *Builder) openConfirmDialog(ctx *web.EventContext) (er web.EventResponse, err error) {
confirmEvent := ctx.R.FormValue(ConfirmDialogConfirmEvent)
if confirmEvent == "" {
ShowMessage(&er, "confirm event is empty", "error")
return
}
msgr := MustGetMessages(ctx.R)
promptText := msgr.ConfirmDialogPromptText
if v := ctx.R.FormValue(ConfirmDialogPromptText); v != "" {
promptText = v
}
portal := DefaultConfirmDialogPortalName
if v := ctx.R.FormValue(ConfirmDialogDialogPortalName); v != "" {
portal = v
}
showVar := fmt.Sprintf("show_%s", portal)
er.UpdatePortals = append(er.UpdatePortals, &web.PortalUpdate{
Name: portal,
Body: VDialog(
VCard(
VCardTitle(VIcon("warning").Class("red--text mr-4"), h.Text(promptText)),
VCardActions(
VSpacer(),
VBtn(msgr.Cancel).
Depressed(true).
Class("ml-2").
On("click", fmt.Sprintf("vars.%s = false", showVar)),
VBtn(msgr.OK).
Color("primary").
Depressed(true).
Dark(true).
Attr("@click", fmt.Sprintf("%s;vars.%s = false", confirmEvent, showVar)),
),
),
).MaxWidth("600px").
Attr("v-model", fmt.Sprintf("vars.%s", showVar)).
Attr(web.InitContextVars, fmt.Sprintf(`{%s: false}`, showVar)),
})
er.VarsScript = fmt.Sprintf("setTimeout(function(){ vars.%s = true }, 100)", showVar)
return
}
func (b *Builder) defaultLayout(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc) {
return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
b.InjectAssets(ctx)
// call CreateMenus before in(ctx) to fill the menuGroupName for modelBuilders first
menu := b.CreateMenus(ctx)
var innerPr web.PageResponse
innerPr, err = in(ctx)
if err == perm.PermissionDenied {
pr.Body = h.Text(perm.PermissionDenied.Error())
return pr, nil
}
if err != nil {
panic(err)
}
var profile h.HTMLComponent
if b.profileFunc != nil {
profile = b.profileFunc(ctx)
}
showNotificationCenter := cfg == nil || !cfg.NotificationCenterInvisible
var notifier h.HTMLComponent
if b.notificationCountFunc != nil && b.notificationContentFunc != nil {
notifier = web.Portal().Name(NotificationCenterPortalName).Loader(web.GET().EventFunc(actions.NotificationCenter))
}
showSearchBox := cfg == nil || !cfg.SearchBoxInvisible
msgr := i18n.MustGetModuleMessages(ctx.R, CoreI18nModuleKey, Messages_en_US).(*Messages)
pr.PageTitle = fmt.Sprintf("%s - %s", innerPr.PageTitle, i18n.T(ctx.R, ModelsI18nModuleKey, b.brandTitle))
pr.Body = VApp(
VNavigationDrawer(
b.RunBrandProfileSwitchLanguageDisplayFunc(b.RunBrandFunc(ctx), profile, b.RunSwitchLanguageFunc(ctx), ctx),
VDivider(),
menu,
).App(true).
// Clipped(true).
Fixed(true).
Value(true).
Attr("v-model", "vars.navDrawer").
Attr(web.InitContextVars, `{navDrawer: null}`),
VAppBar(
VAppBarNavIcon().On("click.stop", "vars.navDrawer = !vars.navDrawer"),
h.Span(innerPr.PageTitle).Class("text-h6 font-weight-regular"),
VSpacer(),
h.If(showSearchBox,
VLayout(
// h.Form(
VTextField().
SoloInverted(true).
PrependIcon("search").
Label(msgr.Search).
Flat(true).
Clearable(true).
HideDetails(true).
Value(ctx.R.URL.Query().Get("keyword")).
Attr("@keyup.enter", web.Plaid().
ClearMergeQuery("page").
Query("keyword", web.Var("[$event.target.value]")).
MergeQuery(true).
PushState(true).
Go()).
Attr("@click:clear", web.Plaid().
Query("keyword", "").
PushState(true).
Go()),
// ).Method("GET"),
).AlignCenter(true).Attr("style", "max-width: 650px"),
),
h.If(showNotificationCenter,
notifier,
),
).Dark(true).
Color(ColorPrimary).
App(true).
Fixed(true),
// ClippedLeft(true),
web.Portal().Name(RightDrawerPortalName),
web.Portal().Name(DialogPortalName),
web.Portal().Name(DeleteConfirmPortalName),
web.Portal().Name(DefaultConfirmDialogPortalName),
web.Portal().Name(ListingDialogPortalName),
VProgressLinear().
Attr(":active", "isFetching").
Attr("style", "position: fixed; z-index: 99").
Indeterminate(true).
Height(2).
Color(b.progressBarColor),
h.Template(
VSnackbar(h.Text("{{vars.presetsMessage.message}}")).
Attr("v-model", "vars.presetsMessage.show").
Attr(":color", "vars.presetsMessage.color").
Timeout(2000).
Top(true),
).Attr("v-if", "vars.presetsMessage"),
VMain(
innerPr.Body.(h.HTMLComponent),
),
).Id("vt-app").
Attr(web.InitContextVars, `{presetsRightDrawer: false, presetsDialog: false, presetsListingDialog: false, presetsMessage: {show: false, color: "success", message: ""}}`)
return
}
}
// for pages outside the default presets layout
func (b *Builder) PlainLayout(in web.PageFunc) (out web.PageFunc) {
return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
b.InjectAssets(ctx)
var innerPr web.PageResponse
innerPr, err = in(ctx)
if err == perm.PermissionDenied {
pr.Body = h.Text(perm.PermissionDenied.Error())
return pr, nil
}
if err != nil {
panic(err)
}
pr.PageTitle = fmt.Sprintf("%s - %s", innerPr.PageTitle, i18n.T(ctx.R, ModelsI18nModuleKey, b.brandTitle))
pr.Body = VApp(
web.Portal().Name(DialogPortalName),
web.Portal().Name(DeleteConfirmPortalName),
web.Portal().Name(DefaultConfirmDialogPortalName),
VProgressLinear().
Attr(":active", "isFetching").
Attr("style", "position: fixed; z-index: 99").
Indeterminate(true).
Height(2).
Color(b.progressBarColor),
h.Template(
VSnackbar(h.Text("{{vars.presetsMessage.message}}")).
Attr("v-model", "vars.presetsMessage.show").
Attr(":color", "vars.presetsMessage.color").
Timeout(2000).
Top(true),
).Attr("v-if", "vars.presetsMessage"),
VMain(
innerPr.Body.(h.HTMLComponent),
),
).Id("vt-app").
Attr(web.InitContextVars, `{presetsDialog: false, presetsMessage: {show: false, color: "success", message: ""}}`)
return
}
}
func (b *Builder) InjectAssets(ctx *web.EventContext) {
ctx.Injector.HeadHTML(strings.Replace(`
`, "{{prefix}}", b.prefix, -1))
b.InjectExtraAssets(ctx)
if len(os.Getenv("DEV_PRESETS")) > 0 {
ctx.Injector.TailHTML(`
`)
} else {
ctx.Injector.TailHTML(strings.Replace(`
`, "{{prefix}}", b.prefix, -1))
}
if b.assetFunc != nil {
b.assetFunc(ctx)
}
}
func (b *Builder) InjectExtraAssets(ctx *web.EventContext) {
for _, ea := range b.extraAssets {
if len(ea.refTag) > 0 {
ctx.Injector.HeadHTML(ea.refTag)
continue
}
if strings.HasSuffix(ea.path, "css") {
ctx.Injector.HeadHTML(fmt.Sprintf("", b.extraFullPath(ea)))
continue
}
ctx.Injector.HeadHTML(fmt.Sprintf("", b.extraFullPath(ea)))
}
}
func (b *Builder) defaultHomePageFunc(ctx *web.EventContext) (r web.PageResponse, err error) {
r.Body = h.Div().Text("home")
return
}
func (b *Builder) getHomePageFunc() web.PageFunc {
if b.homePageFunc != nil {
return b.homePageFunc
}
return b.defaultHomePageFunc
}
func (b *Builder) DefaultNotFoundPageFunc(ctx *web.EventContext) (r web.PageResponse, err error) {
msgr := MustGetMessages(ctx.R)
r.Body = h.Div(
h.H1("404").Class("mb-2"),
h.Text(msgr.NotFoundPageNotice),
).Class("text-center mt-8")
return
}
func (b *Builder) getNotFoundPageFunc() web.PageFunc {
pf := b.DefaultNotFoundPageFunc
if b.notFoundFunc != nil {
pf = b.notFoundFunc
}
return func(ctx *web.EventContext) (r web.PageResponse, err error) {
ctx.W.WriteHeader(http.StatusNotFound)
return pf(ctx)
}
}
func (b *Builder) extraFullPath(ea *extraAsset) string {
return b.prefix + "/extra" + ea.path
}
func (b *Builder) initMux() {
b.logger.Info("initializing mux for", zap.Reflect("models", modelNames(b.models)), zap.String("prefix", b.prefix))
mux := goji.NewMux()
ub := b.builder
mainJSPath := b.prefix + "/assets/main.js"
mux.Handle(pat.Get(mainJSPath),
ub.PacksHandler("text/javascript",
Vuetify(b.vuetifyOptions),
JSComponentsPack(),
vuetifyx.JSComponentsPack(),
web.JSComponentsPack(),
),
)
log.Println("mounted url", mainJSPath)
vueJSPath := b.prefix + "/assets/vue.js"
mux.Handle(pat.Get(vueJSPath),
ub.PacksHandler("text/javascript",
web.JSVueComponentsPack(),
),
)
log.Println("mounted url", vueJSPath)
mainCSSPath := b.prefix + "/assets/main.css"
mux.Handle(pat.Get(mainCSSPath),
ub.PacksHandler("text/css",
CSSComponentsPack(),
),
)
log.Println("mounted url", mainCSSPath)
for _, ea := range b.extraAssets {
fullPath := b.extraFullPath(ea)
mux.Handle(pat.Get(fullPath), ub.PacksHandler(
ea.contentType,
ea.body,
))
log.Println("mounted url", fullPath)
}
homeURL := b.prefix
if homeURL == "" {
homeURL = "/"
}
mux.Handle(
pat.New(homeURL),
b.wrap(nil, b.layoutFunc(b.getHomePageFunc(), b.homePageLayoutConfig)),
)
for _, m := range b.models {
pluralUri := inflection.Plural(m.uriName)
info := m.Info()
routePath := info.ListingHref()
inPageFunc := m.listing.GetPageFunc()
if m.singleton {
inPageFunc = m.editing.singletonPageFunc
if m.layoutConfig == nil {
m.layoutConfig = &LayoutConfig{}
}
m.layoutConfig.SearchBoxInvisible = true
}
mux.Handle(
pat.New(routePath),
b.wrap(m, b.layoutFunc(inPageFunc, m.layoutConfig)),
)
log.Println("mounted url", routePath)
if m.hasDetailing {
routePath = fmt.Sprintf("%s/%s/:id", b.prefix, pluralUri)
mux.Handle(
pat.New(routePath),
b.wrap(m, b.detailLayoutFunc(m.detailing.GetPageFunc(), m.layoutConfig)),
)
log.Println("mounted url", routePath)
}
}
// Handle 404
mux.Use(func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RequestURI, b.prefix) || middleware.Handler(r.Context()) != nil {
handler.ServeHTTP(w, r)
return
}
b.wrap(
nil,
b.layoutFunc(b.getNotFoundPageFunc(), b.notFoundPageLayoutConfig),
).ServeHTTP(w, r)
return
})
})
b.mux = mux
}
func (b *Builder) AddWrapHandler(key string, f func(in http.Handler) (out http.Handler)) {
b.wrapHandlers[key] = f
}
func (b *Builder) wrap(m *ModelBuilder, pf web.PageFunc) http.Handler {
p := b.builder.Page(pf)
if m != nil {
m.registerDefaultEventFuncs()
p.MergeHub(&m.EventsHub)
}
handlers := b.I18n().EnsureLanguage(
p,
)
for _, wrapHandler := range b.wrapHandlers {
handlers = wrapHandler(handlers)
}
return handlers
}
func (b *Builder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if b.mux == nil {
b.initMux()
}
RedirectSlashes(b.mux).ServeHTTP(w, r)
}
func RedirectSlashes(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if len(path) > 1 && path[len(path)-1] == '/' {
if r.URL.RawQuery != "" {
path = fmt.Sprintf("%s?%s", path[:len(path)-1], r.URL.RawQuery)
} else {
path = path[:len(path)-1]
}
redirectURL := fmt.Sprintf("//%s%s", r.Host, path)
http.Redirect(w, r, redirectURL, 301)
return
}
next.ServeHTTP(w, r)
})
}