1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288 |
- 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(`
- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Mono">
- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
- <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
- <link rel="stylesheet" href="{{prefix}}/assets/main.css">
- <script src='{{prefix}}/assets/vue.js'></script>
- <style>
- [v-cloak] {
- display: none;
- }
- .vx-list-item--active {
- position: relative;
- }
- .vx-list-item--active:after {
- opacity: .12;
- background-color: currentColor;
- bottom: 0;
- content: "";
- left: 0;
- pointer-events: none;
- position: absolute;
- right: 0;
- top: 0;
- transition: .3s cubic-bezier(.25,.8,.5,1);
- line-height: 0;
- }
- .vx-list-item--active:hover {
- background-color: inherit!important;
- }
- </style>
- `, "{{prefix}}", b.prefix, -1))
- b.InjectExtraAssets(ctx)
- if len(os.Getenv("DEV_PRESETS")) > 0 {
- ctx.Injector.TailHTML(`
- <script src='http://localhost:3080/js/chunk-vendors.js'></script>
- <script src='http://localhost:3080/js/app.js'></script>
- <script src='http://localhost:3100/js/chunk-vendors.js'></script>
- <script src='http://localhost:3100/js/app.js'></script>
- `)
- } else {
- ctx.Injector.TailHTML(strings.Replace(`
- <script src='{{prefix}}/assets/main.js'></script>
- `, "{{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("<link rel=\"stylesheet\" href=\"%s\">", b.extraFullPath(ea)))
- continue
- }
- ctx.Injector.HeadHTML(fmt.Sprintf("<script src=\"%s\"></script>", 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)
- })
- }
|