presets.go 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288
  1. package presets
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. "os"
  7. "regexp"
  8. "strings"
  9. "github.com/iancoleman/strcase"
  10. "github.com/jinzhu/inflection"
  11. "github.com/qor5/admin/presets/actions"
  12. . "github.com/qor5/ui/vuetify"
  13. "github.com/qor5/ui/vuetifyx"
  14. "github.com/qor5/web"
  15. "github.com/qor5/x/i18n"
  16. "github.com/qor5/x/perm"
  17. h "github.com/theplant/htmlgo"
  18. "go.uber.org/zap"
  19. goji "goji.io"
  20. "goji.io/middleware"
  21. "goji.io/pat"
  22. "golang.org/x/text/language"
  23. "golang.org/x/text/language/display"
  24. )
  25. type Builder struct {
  26. prefix string
  27. models []*ModelBuilder
  28. mux *goji.Mux
  29. builder *web.Builder
  30. i18nBuilder *i18n.Builder
  31. logger *zap.Logger
  32. permissionBuilder *perm.Builder
  33. verifier *perm.Verifier
  34. layoutFunc func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc)
  35. detailLayoutFunc func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc)
  36. dataOperator DataOperator
  37. messagesFunc MessagesFunc
  38. homePageFunc web.PageFunc
  39. notFoundFunc web.PageFunc
  40. homePageLayoutConfig *LayoutConfig
  41. notFoundPageLayoutConfig *LayoutConfig
  42. brandFunc ComponentFunc
  43. profileFunc ComponentFunc
  44. switchLanguageFunc ComponentFunc
  45. brandProfileSwitchLanguageDisplayFunc func(brand, profile, switchLanguage h.HTMLComponent) h.HTMLComponent
  46. menuTopItems map[string]ComponentFunc
  47. notificationCountFunc func(ctx *web.EventContext) int
  48. notificationContentFunc ComponentFunc
  49. brandTitle string
  50. vuetifyOptions string
  51. progressBarColor string
  52. rightDrawerWidth string
  53. writeFieldDefaults *FieldDefaults
  54. listFieldDefaults *FieldDefaults
  55. detailFieldDefaults *FieldDefaults
  56. extraAssets []*extraAsset
  57. assetFunc AssetFunc
  58. menuGroups MenuGroups
  59. menuOrder []interface{}
  60. wrapHandlers map[string]func(in http.Handler) (out http.Handler)
  61. }
  62. type AssetFunc func(ctx *web.EventContext)
  63. type extraAsset struct {
  64. path string
  65. contentType string
  66. body web.ComponentsPack
  67. refTag string
  68. }
  69. const (
  70. CoreI18nModuleKey i18n.ModuleKey = "CoreI18nModuleKey"
  71. ModelsI18nModuleKey i18n.ModuleKey = "ModelsI18nModuleKey"
  72. )
  73. const (
  74. OpenConfirmDialog = "presets_ConfirmDialog"
  75. )
  76. func New() *Builder {
  77. l, _ := zap.NewDevelopment()
  78. r := &Builder{
  79. logger: l,
  80. builder: web.New(),
  81. i18nBuilder: i18n.New().
  82. RegisterForModule(language.English, CoreI18nModuleKey, Messages_en_US).
  83. RegisterForModule(language.SimplifiedChinese, CoreI18nModuleKey, Messages_zh_CN).
  84. RegisterForModule(language.Japanese, CoreI18nModuleKey, Messages_ja_JP),
  85. writeFieldDefaults: NewFieldDefaults(WRITE),
  86. listFieldDefaults: NewFieldDefaults(LIST),
  87. detailFieldDefaults: NewFieldDefaults(DETAIL),
  88. progressBarColor: "amber",
  89. menuTopItems: make(map[string]ComponentFunc),
  90. brandTitle: "Admin",
  91. rightDrawerWidth: "600",
  92. verifier: perm.NewVerifier(PermModule, nil),
  93. homePageLayoutConfig: &LayoutConfig{SearchBoxInvisible: true},
  94. notFoundPageLayoutConfig: &LayoutConfig{
  95. SearchBoxInvisible: true,
  96. NotificationCenterInvisible: true,
  97. },
  98. wrapHandlers: make(map[string]func(in http.Handler) (out http.Handler)),
  99. }
  100. r.GetWebBuilder().RegisterEventFunc(OpenConfirmDialog, r.openConfirmDialog)
  101. r.layoutFunc = r.defaultLayout
  102. r.detailLayoutFunc = r.defaultLayout
  103. return r
  104. }
  105. func (b *Builder) I18n() (r *i18n.Builder) {
  106. return b.i18nBuilder
  107. }
  108. func (b *Builder) SetI18n(v *i18n.Builder) (r *Builder) {
  109. b.i18nBuilder = v
  110. return b
  111. }
  112. func (b *Builder) Permission(v *perm.Builder) (r *Builder) {
  113. b.permissionBuilder = v
  114. b.verifier = perm.NewVerifier(PermModule, v)
  115. return b
  116. }
  117. func (b *Builder) GetPermission() (r *perm.Builder) {
  118. return b.permissionBuilder
  119. }
  120. func (b *Builder) URIPrefix(v string) (r *Builder) {
  121. b.prefix = strings.TrimRight(v, "/")
  122. return b
  123. }
  124. func (b *Builder) GetURIPrefix() string {
  125. return b.prefix
  126. }
  127. func (b *Builder) LayoutFunc(v func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc)) (r *Builder) {
  128. b.layoutFunc = v
  129. return b
  130. }
  131. func (b *Builder) GetLayoutFunc() func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc) {
  132. return b.layoutFunc
  133. }
  134. func (b *Builder) DetailLayoutFunc(v func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc)) (r *Builder) {
  135. b.detailLayoutFunc = v
  136. return b
  137. }
  138. func (b *Builder) GetDetailLayoutFunc() func(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc) {
  139. return b.detailLayoutFunc
  140. }
  141. func (b *Builder) HomePageLayoutConfig(v *LayoutConfig) (r *Builder) {
  142. b.homePageLayoutConfig = v
  143. return b
  144. }
  145. func (b *Builder) NotFoundPageLayoutConfig(v *LayoutConfig) (r *Builder) {
  146. b.notFoundPageLayoutConfig = v
  147. return b
  148. }
  149. func (b *Builder) Builder(v *web.Builder) (r *Builder) {
  150. b.builder = v
  151. return b
  152. }
  153. func (b *Builder) GetWebBuilder() (r *web.Builder) {
  154. return b.builder
  155. }
  156. func (b *Builder) Logger(v *zap.Logger) (r *Builder) {
  157. b.logger = v
  158. return b
  159. }
  160. func (b *Builder) MessagesFunc(v MessagesFunc) (r *Builder) {
  161. b.messagesFunc = v
  162. return b
  163. }
  164. func (b *Builder) HomePageFunc(v web.PageFunc) (r *Builder) {
  165. b.homePageFunc = v
  166. return b
  167. }
  168. func (b *Builder) NotFoundFunc(v web.PageFunc) (r *Builder) {
  169. b.notFoundFunc = v
  170. return b
  171. }
  172. func (b *Builder) BrandFunc(v ComponentFunc) (r *Builder) {
  173. b.brandFunc = v
  174. return b
  175. }
  176. func (b *Builder) ProfileFunc(v ComponentFunc) (r *Builder) {
  177. b.profileFunc = v
  178. return b
  179. }
  180. func (b *Builder) GetProfileFunc() ComponentFunc {
  181. return b.profileFunc
  182. }
  183. func (b *Builder) SwitchLanguageFunc(v ComponentFunc) (r *Builder) {
  184. b.switchLanguageFunc = v
  185. return b
  186. }
  187. func (b *Builder) BrandProfileSwitchLanguageDisplayFuncFunc(f func(brand, profile, switchLanguage h.HTMLComponent) h.HTMLComponent) (r *Builder) {
  188. b.brandProfileSwitchLanguageDisplayFunc = f
  189. return b
  190. }
  191. func (b *Builder) NotificationFunc(contentFunc ComponentFunc, countFunc func(ctx *web.EventContext) int) (r *Builder) {
  192. b.notificationCountFunc = countFunc
  193. b.notificationContentFunc = contentFunc
  194. b.GetWebBuilder().RegisterEventFunc(actions.NotificationCenter, b.notificationCenter)
  195. return b
  196. }
  197. func (b *Builder) BrandTitle(v string) (r *Builder) {
  198. b.brandTitle = v
  199. return b
  200. }
  201. func (b *Builder) GetBrandTitle() string {
  202. return b.brandTitle
  203. }
  204. func (b *Builder) VuetifyOptions(v string) (r *Builder) {
  205. b.vuetifyOptions = v
  206. return b
  207. }
  208. func (b *Builder) RightDrawerWidth(v string) (r *Builder) {
  209. b.rightDrawerWidth = v
  210. return b
  211. }
  212. func (b *Builder) ProgressBarColor(v string) (r *Builder) {
  213. b.progressBarColor = v
  214. return b
  215. }
  216. func (b *Builder) GetProgressBarColor() string {
  217. return b.progressBarColor
  218. }
  219. func (b *Builder) AssetFunc(v AssetFunc) (r *Builder) {
  220. b.assetFunc = v
  221. return b
  222. }
  223. func (b *Builder) ExtraAsset(path string, contentType string, body web.ComponentsPack, refTag ...string) (r *Builder) {
  224. if !strings.HasPrefix(path, "/") {
  225. path = "/" + path
  226. }
  227. var theOne *extraAsset
  228. for _, ea := range b.extraAssets {
  229. if ea.path == path {
  230. theOne = ea
  231. break
  232. }
  233. }
  234. if theOne == nil {
  235. theOne = &extraAsset{path: path, contentType: contentType, body: body}
  236. b.extraAssets = append(b.extraAssets, theOne)
  237. } else {
  238. theOne.contentType = contentType
  239. theOne.body = body
  240. }
  241. if len(refTag) > 0 {
  242. theOne.refTag = refTag[0]
  243. }
  244. return b
  245. }
  246. func (b *Builder) FieldDefaults(v FieldMode) (r *FieldDefaults) {
  247. if v == WRITE {
  248. return b.writeFieldDefaults
  249. }
  250. if v == LIST {
  251. return b.listFieldDefaults
  252. }
  253. if v == DETAIL {
  254. return b.detailFieldDefaults
  255. }
  256. return r
  257. }
  258. func (b *Builder) NewFieldsBuilder(v FieldMode) (r *FieldsBuilder) {
  259. r = NewFieldsBuilder().Defaults(b.FieldDefaults(v))
  260. return
  261. }
  262. func (b *Builder) Model(v interface{}) (r *ModelBuilder) {
  263. r = NewModelBuilder(b, v)
  264. b.models = append(b.models, r)
  265. return r
  266. }
  267. func (b *Builder) DataOperator(v DataOperator) (r *Builder) {
  268. b.dataOperator = v
  269. return b
  270. }
  271. func modelNames(ms []*ModelBuilder) (r []string) {
  272. for _, m := range ms {
  273. r = append(r, m.uriName)
  274. }
  275. return
  276. }
  277. func (b *Builder) defaultBrandFunc(ctx *web.EventContext) (r h.HTMLComponent) {
  278. return
  279. }
  280. func (b *Builder) MenuGroup(name string) *MenuGroupBuilder {
  281. mgb := b.menuGroups.MenuGroup(name)
  282. if !b.isMenuGroupInOrder(mgb) {
  283. b.menuOrder = append(b.menuOrder, mgb)
  284. }
  285. return mgb
  286. }
  287. func (b *Builder) isMenuGroupInOrder(mgb *MenuGroupBuilder) bool {
  288. for _, v := range b.menuOrder {
  289. if v == mgb {
  290. return true
  291. }
  292. }
  293. return false
  294. }
  295. func (b *Builder) removeMenuGroupInOrder(mgb *MenuGroupBuilder) {
  296. for i, om := range b.menuOrder {
  297. if om == mgb {
  298. b.menuOrder = append(b.menuOrder[:i], b.menuOrder[i+1:]...)
  299. break
  300. }
  301. }
  302. }
  303. // item can be Slug name, model name, *MenuGroupBuilder
  304. // the underlying logic is using Slug name,
  305. // so if the Slug name is customized, item must be the Slug name
  306. // example:
  307. // b.MenuOrder(
  308. //
  309. // b.MenuGroup("Product Management").SubItems(
  310. // "products",
  311. // "Variant",
  312. // ),
  313. // "customized-uri",
  314. //
  315. // )
  316. func (b *Builder) MenuOrder(items ...interface{}) {
  317. for _, item := range items {
  318. switch v := item.(type) {
  319. case string:
  320. b.menuOrder = append(b.menuOrder, v)
  321. case *MenuGroupBuilder:
  322. if b.isMenuGroupInOrder(v) {
  323. b.removeMenuGroupInOrder(v)
  324. }
  325. b.menuOrder = append(b.menuOrder, v)
  326. default:
  327. panic(fmt.Sprintf("unknown menu order item type: %T\n", item))
  328. }
  329. }
  330. }
  331. type defaultMenuIconRE struct {
  332. re *regexp.Regexp
  333. icon string
  334. }
  335. var defaultMenuIconREs = []defaultMenuIconRE{
  336. // user
  337. {re: regexp.MustCompile(`\busers?|members?\b`), icon: "person"},
  338. // store
  339. {re: regexp.MustCompile(`\bstores?\b`), icon: "store"},
  340. // order
  341. {re: regexp.MustCompile(`\borders?\b`), icon: "shopping_cart"},
  342. // product
  343. {re: regexp.MustCompile(`\bproducts?\b`), icon: "format_list_bulleted"},
  344. // post
  345. {re: regexp.MustCompile(`\bposts?|articles?\b`), icon: "article"},
  346. // web
  347. {re: regexp.MustCompile(`\bweb|site\b`), icon: "web"},
  348. // seo
  349. {re: regexp.MustCompile(`\bseo\b`), icon: "travel_explore"},
  350. // i18n
  351. {re: regexp.MustCompile(`\bi18n|translations?\b`), icon: "language"},
  352. // chart
  353. {re: regexp.MustCompile(`\banalytics?|charts?|statistics?\b`), icon: "analytics"},
  354. // dashboard
  355. {re: regexp.MustCompile(`\bdashboard\b`), icon: "dashboard"},
  356. // setting
  357. {re: regexp.MustCompile(`\bsettings?\b`), icon: "settings"},
  358. }
  359. func defaultMenuIcon(mLabel string) string {
  360. ws := strings.Join(strings.Split(strcase.ToSnake(mLabel), "_"), " ")
  361. for _, v := range defaultMenuIconREs {
  362. if v.re.MatchString(ws) {
  363. return v.icon
  364. }
  365. }
  366. return "widgets"
  367. }
  368. const menuFontWeight = "500"
  369. const subMenuFontWeight = "400"
  370. func (b *Builder) menuItem(ctx *web.EventContext, m *ModelBuilder, isSub bool) (r h.HTMLComponent) {
  371. menuIcon := m.menuIcon
  372. fontWeight := subMenuFontWeight
  373. if isSub {
  374. // menuIcon = ""
  375. } else {
  376. fontWeight = menuFontWeight
  377. if menuIcon == "" {
  378. menuIcon = defaultMenuIcon(m.label)
  379. }
  380. }
  381. href := m.Info().ListingHref()
  382. if m.link != "" {
  383. href = m.link
  384. }
  385. if m.defaultURLQueryFunc != nil {
  386. href = fmt.Sprintf("%s?%s", href, m.defaultURLQueryFunc(ctx.R).Encode())
  387. }
  388. item := VListItem(
  389. VListItemAction(
  390. VIcon(menuIcon),
  391. ).Attr("style", "margin-right: 16px"),
  392. VListItemContent(
  393. VListItemTitle(
  394. h.Text(i18n.T(ctx.R, ModelsI18nModuleKey, m.label)),
  395. ).Attr("style", fmt.Sprintf("white-space: normal; font-weight: %s;font-size: 14px;", fontWeight)),
  396. ),
  397. )
  398. if strings.HasPrefix(href, "/") {
  399. item.Attr("@click", web.Plaid().PushStateURL(href).Go())
  400. } else {
  401. item.Href(href)
  402. }
  403. if b.isMenuItemActive(ctx, m) {
  404. item = item.Class("v-list-item--active")
  405. }
  406. return item
  407. }
  408. func (b *Builder) isMenuItemActive(ctx *web.EventContext, m *ModelBuilder) bool {
  409. href := m.Info().ListingHref()
  410. if m.link != "" {
  411. href = m.link
  412. }
  413. path := strings.TrimSuffix(ctx.R.URL.Path, "/")
  414. if path == "" && href == "/" {
  415. return true
  416. }
  417. if path == href {
  418. return true
  419. }
  420. if href == b.prefix {
  421. return false
  422. }
  423. if href != "/" && strings.HasPrefix(path, href) {
  424. return true
  425. }
  426. return false
  427. }
  428. func (b *Builder) CreateMenus(ctx *web.EventContext) (r h.HTMLComponent) {
  429. mMap := make(map[string]*ModelBuilder)
  430. for _, m := range b.models {
  431. mMap[m.uriName] = m
  432. }
  433. inOrderMap := make(map[string]struct{})
  434. var menus []h.HTMLComponent
  435. for _, om := range b.menuOrder {
  436. switch v := om.(type) {
  437. case *MenuGroupBuilder:
  438. disabled := false
  439. if b.verifier.Do(PermList).SnakeOn("mg_"+v.name).WithReq(ctx.R).IsAllowed() != nil {
  440. disabled = true
  441. }
  442. groupIcon := v.icon
  443. if groupIcon == "" {
  444. groupIcon = defaultMenuIcon(v.name)
  445. }
  446. var subMenus = []h.HTMLComponent{
  447. VListItem(
  448. VListItemAction(
  449. VIcon(groupIcon),
  450. ).Attr("style", "margin-right: 16px;"),
  451. VListItemContent(
  452. VListItemTitle(h.Text(i18n.T(ctx.R, ModelsI18nModuleKey, v.name))).
  453. Attr("style", fmt.Sprintf("white-space: normal; font-weight: %s;font-size: 14px;", menuFontWeight)),
  454. ),
  455. ).Slot("activator").Class("pa-0"),
  456. }
  457. subCount := 0
  458. hasActiveMenuItem := false
  459. for _, subOm := range v.subMenuItems {
  460. m, ok := mMap[subOm]
  461. if !ok {
  462. m = mMap[inflection.Plural(strcase.ToKebab(subOm))]
  463. }
  464. if m == nil {
  465. continue
  466. }
  467. m.menuGroupName = v.name
  468. if m.notInMenu {
  469. continue
  470. }
  471. if m.Info().Verifier().Do(PermList).WithReq(ctx.R).IsAllowed() != nil {
  472. continue
  473. }
  474. subMenus = append(subMenus, b.menuItem(ctx, m, true))
  475. subCount++
  476. inOrderMap[m.uriName] = struct{}{}
  477. if b.isMenuItemActive(ctx, m) {
  478. hasActiveMenuItem = true
  479. }
  480. }
  481. if subCount == 0 {
  482. continue
  483. }
  484. if disabled {
  485. continue
  486. }
  487. menus = append(menus, VListGroup(
  488. subMenus...).
  489. Value(hasActiveMenuItem),
  490. )
  491. case string:
  492. m, ok := mMap[v]
  493. if !ok {
  494. m = mMap[inflection.Plural(strcase.ToKebab(v))]
  495. }
  496. if m == nil {
  497. continue
  498. }
  499. if m.Info().Verifier().Do(PermList).WithReq(ctx.R).IsAllowed() != nil {
  500. continue
  501. }
  502. if m.notInMenu {
  503. continue
  504. }
  505. menus = append(menus, b.menuItem(ctx, m, false))
  506. inOrderMap[m.uriName] = struct{}{}
  507. }
  508. }
  509. for _, m := range b.models {
  510. _, ok := inOrderMap[m.uriName]
  511. if ok {
  512. continue
  513. }
  514. if m.Info().Verifier().Do(PermList).WithReq(ctx.R).IsAllowed() != nil {
  515. continue
  516. }
  517. if m.notInMenu {
  518. continue
  519. }
  520. menus = append(menus, b.menuItem(ctx, m, false))
  521. }
  522. r = h.Div(
  523. VList(menus...).Class("primary--text").Dense(true),
  524. )
  525. return
  526. }
  527. func (b *Builder) RunBrandFunc(ctx *web.EventContext) (r h.HTMLComponent) {
  528. if b.brandFunc != nil {
  529. return b.brandFunc(ctx)
  530. }
  531. return VRow(h.H1(i18n.T(ctx.R, ModelsI18nModuleKey, b.brandTitle))).Class("text-button")
  532. }
  533. func (b *Builder) RunSwitchLanguageFunc(ctx *web.EventContext) (r h.HTMLComponent) {
  534. if b.switchLanguageFunc != nil {
  535. return b.switchLanguageFunc(ctx)
  536. }
  537. var supportLanguages = b.I18n().GetSupportLanguagesFromRequest(ctx.R)
  538. if len(b.I18n().GetSupportLanguages()) <= 1 || len(supportLanguages) == 0 {
  539. return nil
  540. }
  541. queryName := b.I18n().GetQueryName()
  542. msgr := MustGetMessages(ctx.R)
  543. if len(supportLanguages) == 1 {
  544. return h.Template().Children(
  545. h.Div(
  546. VList(
  547. VListItem(
  548. VListItemIcon(
  549. VIcon("translate").Small(true).Class("ml-1"),
  550. ).Attr("style", "margin-right: 16px"),
  551. VListItemContent(
  552. VListItemTitle(
  553. h.Div(h.Text(fmt.Sprintf("%s%s %s", msgr.Language, msgr.Colon, display.Self.Name(supportLanguages[0])))).Role("button"),
  554. ),
  555. ),
  556. ).Class("pa-0").Dense(true),
  557. ).Class("pa-0 ma-n4 mt-n6"),
  558. ).Attr("@click", web.Plaid().Query(queryName, supportLanguages[0].String()).Go()),
  559. )
  560. }
  561. var matcher = language.NewMatcher(supportLanguages)
  562. lang := ctx.R.FormValue(queryName)
  563. if lang == "" {
  564. lang = b.i18nBuilder.GetCurrentLangFromCookie(ctx.R)
  565. }
  566. accept := ctx.R.Header.Get("Accept-Language")
  567. var displayLanguage language.Tag
  568. _, i := language.MatchStrings(matcher, lang, accept)
  569. displayLanguage = supportLanguages[i]
  570. var languages []h.HTMLComponent
  571. for _, tag := range supportLanguages {
  572. languages = append(languages,
  573. h.Div(
  574. VListItem(
  575. VListItemContent(
  576. VListItemTitle(
  577. h.Div(h.Text(display.Self.Name(tag))),
  578. ),
  579. ),
  580. ).Attr("@click", web.Plaid().Query(queryName, tag.String()).Go()),
  581. ),
  582. )
  583. }
  584. return VMenu().OffsetY(true).Children(
  585. h.Template().Attr("v-slot:activator", "{on, attrs}").Children(
  586. h.Div(
  587. VList(
  588. VListItem(
  589. VListItemIcon(
  590. VIcon("translate").Small(true).Class("ml-1"),
  591. ).Attr("style", "margin-right: 16px"),
  592. VListItemContent(
  593. VListItemTitle(
  594. h.Text(fmt.Sprintf("%s%s %s", msgr.Language, msgr.Colon, display.Self.Name(displayLanguage))),
  595. ),
  596. ),
  597. VListItemIcon(
  598. VIcon("arrow_drop_down").Small(false).Class("mr-1"),
  599. ),
  600. ).Class("pa-0").Dense(true),
  601. ).Class("pa-0 ma-n4 mt-n6"),
  602. ).Attr("v-bind", "attrs").Attr("v-on", "on"),
  603. ),
  604. VList(
  605. languages...,
  606. ).Dense(true),
  607. )
  608. }
  609. func (b *Builder) AddMenuTopItemFunc(key string, v ComponentFunc) (r *Builder) {
  610. b.menuTopItems[key] = v
  611. return b
  612. }
  613. func (b *Builder) RunBrandProfileSwitchLanguageDisplayFunc(brand, profile, switchLanguage h.HTMLComponent, ctx *web.EventContext) (r h.HTMLComponent) {
  614. if b.brandProfileSwitchLanguageDisplayFunc != nil {
  615. return b.brandProfileSwitchLanguageDisplayFunc(brand, profile, switchLanguage)
  616. }
  617. var items []h.HTMLComponent
  618. items = append(items,
  619. h.If(brand != nil,
  620. VListItem(
  621. VCardText(brand),
  622. ),
  623. ),
  624. h.If(profile != nil,
  625. VListItem(
  626. VCardText(profile),
  627. ),
  628. ),
  629. h.If(switchLanguage != nil,
  630. VListItem(
  631. VCardText(switchLanguage),
  632. ).Dense(true),
  633. ),
  634. )
  635. for _, v := range b.menuTopItems {
  636. items = append(items,
  637. h.If(v(ctx) != nil,
  638. VListItem(
  639. VCardText(v(ctx)),
  640. ),
  641. ))
  642. }
  643. return h.Div(
  644. items...,
  645. )
  646. }
  647. func MustGetMessages(r *http.Request) *Messages {
  648. return i18n.MustGetModuleMessages(r, CoreI18nModuleKey, Messages_en_US).(*Messages)
  649. }
  650. const RightDrawerPortalName = "presets_RightDrawerPortalName"
  651. const rightDrawerContentPortalName = "presets_RightDrawerContentPortalName"
  652. const DialogPortalName = "presets_DialogPortalName"
  653. const dialogContentPortalName = "presets_DialogContentPortalName"
  654. const NotificationCenterPortalName = "notification-center"
  655. const DefaultConfirmDialogPortalName = "presets_confirmDialogPortalName"
  656. const ListingDialogPortalName = "presets_listingDialogPortalName"
  657. const singletonEditingPortalName = "presets_SingletonEditingPortalName"
  658. const closeRightDrawerVarScript = "vars.presetsRightDrawer = false"
  659. const closeDialogVarScript = "vars.presetsDialog = false"
  660. const CloseListingDialogVarScript = "vars.presetsListingDialog = false"
  661. func (b *Builder) overlay(overlayType string, r *web.EventResponse, comp h.HTMLComponent, width string) {
  662. if overlayType == actions.Dialog {
  663. b.dialog(r, comp, width)
  664. return
  665. }
  666. b.rightDrawer(r, comp, width)
  667. }
  668. func (b *Builder) rightDrawer(r *web.EventResponse, comp h.HTMLComponent, width string) {
  669. if width == "" {
  670. width = b.rightDrawerWidth
  671. }
  672. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  673. Name: RightDrawerPortalName,
  674. Body: VNavigationDrawer(
  675. web.GlobalEvents().Attr("@keyup.esc", "vars.presetsRightDrawer = false"),
  676. web.Portal(comp).Name(rightDrawerContentPortalName),
  677. ).
  678. // 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
  679. Class("v-navigation-drawer--temporary").
  680. Attr("v-model", "vars.presetsRightDrawer").
  681. Right(true).
  682. Fixed(true).
  683. Attr("width", width).
  684. Bottom(false).
  685. Attr(":height", `"100%"`),
  686. // Temporary(true),
  687. // HideOverlay(true).
  688. // Floating(true).
  689. })
  690. r.VarsScript = "setTimeout(function(){ vars.presetsRightDrawer = true }, 100)"
  691. }
  692. // 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").
  693. func (b *Builder) dialog(r *web.EventResponse, comp h.HTMLComponent, width string) {
  694. if width == "" {
  695. width = b.rightDrawerWidth
  696. }
  697. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  698. Name: DialogPortalName,
  699. Body: web.Scope(
  700. VDialog(
  701. web.Portal(comp).Name(dialogContentPortalName),
  702. ).
  703. Attr("v-model", "vars.presetsDialog").
  704. Width(width),
  705. ).VSlot("{ plaidForm }"),
  706. })
  707. r.VarsScript = "setTimeout(function(){ vars.presetsDialog = true }, 100)"
  708. }
  709. type LayoutConfig struct {
  710. SearchBoxInvisible bool
  711. NotificationCenterInvisible bool
  712. }
  713. func (b *Builder) notificationCenter(ctx *web.EventContext) (er web.EventResponse, err error) {
  714. total := b.notificationCountFunc(ctx)
  715. content := b.notificationContentFunc(ctx)
  716. icon := VIcon("notifications").Color("white")
  717. er.Body = VMenu().OffsetY(true).Children(
  718. h.Template().Attr("v-slot:activator", "{on, attrs}").Children(
  719. VBtn("").Icon(true).Children(
  720. h.If(total > 0,
  721. VBadge(
  722. icon,
  723. ).Content(total).Overlap(true).Color("red"),
  724. ).Else(icon),
  725. ).Attr("v-bind", "attrs").Attr("v-on", "on").Class("ml-1"),
  726. ),
  727. VCard(content))
  728. return
  729. }
  730. const (
  731. ConfirmDialogConfirmEvent = "presets_ConfirmDialog_ConfirmEvent"
  732. ConfirmDialogPromptText = "presets_ConfirmDialog_PromptText"
  733. ConfirmDialogDialogPortalName = "presets_ConfirmDialog_DialogPortalName"
  734. )
  735. func (b *Builder) openConfirmDialog(ctx *web.EventContext) (er web.EventResponse, err error) {
  736. confirmEvent := ctx.R.FormValue(ConfirmDialogConfirmEvent)
  737. if confirmEvent == "" {
  738. ShowMessage(&er, "confirm event is empty", "error")
  739. return
  740. }
  741. msgr := MustGetMessages(ctx.R)
  742. promptText := msgr.ConfirmDialogPromptText
  743. if v := ctx.R.FormValue(ConfirmDialogPromptText); v != "" {
  744. promptText = v
  745. }
  746. portal := DefaultConfirmDialogPortalName
  747. if v := ctx.R.FormValue(ConfirmDialogDialogPortalName); v != "" {
  748. portal = v
  749. }
  750. showVar := fmt.Sprintf("show_%s", portal)
  751. er.UpdatePortals = append(er.UpdatePortals, &web.PortalUpdate{
  752. Name: portal,
  753. Body: VDialog(
  754. VCard(
  755. VCardTitle(VIcon("warning").Class("red--text mr-4"), h.Text(promptText)),
  756. VCardActions(
  757. VSpacer(),
  758. VBtn(msgr.Cancel).
  759. Depressed(true).
  760. Class("ml-2").
  761. On("click", fmt.Sprintf("vars.%s = false", showVar)),
  762. VBtn(msgr.OK).
  763. Color("primary").
  764. Depressed(true).
  765. Dark(true).
  766. Attr("@click", fmt.Sprintf("%s;vars.%s = false", confirmEvent, showVar)),
  767. ),
  768. ),
  769. ).MaxWidth("600px").
  770. Attr("v-model", fmt.Sprintf("vars.%s", showVar)).
  771. Attr(web.InitContextVars, fmt.Sprintf(`{%s: false}`, showVar)),
  772. })
  773. er.VarsScript = fmt.Sprintf("setTimeout(function(){ vars.%s = true }, 100)", showVar)
  774. return
  775. }
  776. func (b *Builder) defaultLayout(in web.PageFunc, cfg *LayoutConfig) (out web.PageFunc) {
  777. return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
  778. b.InjectAssets(ctx)
  779. // call CreateMenus before in(ctx) to fill the menuGroupName for modelBuilders first
  780. menu := b.CreateMenus(ctx)
  781. var innerPr web.PageResponse
  782. innerPr, err = in(ctx)
  783. if err == perm.PermissionDenied {
  784. pr.Body = h.Text(perm.PermissionDenied.Error())
  785. return pr, nil
  786. }
  787. if err != nil {
  788. panic(err)
  789. }
  790. var profile h.HTMLComponent
  791. if b.profileFunc != nil {
  792. profile = b.profileFunc(ctx)
  793. }
  794. showNotificationCenter := cfg == nil || !cfg.NotificationCenterInvisible
  795. var notifier h.HTMLComponent
  796. if b.notificationCountFunc != nil && b.notificationContentFunc != nil {
  797. notifier = web.Portal().Name(NotificationCenterPortalName).Loader(web.GET().EventFunc(actions.NotificationCenter))
  798. }
  799. showSearchBox := cfg == nil || !cfg.SearchBoxInvisible
  800. msgr := i18n.MustGetModuleMessages(ctx.R, CoreI18nModuleKey, Messages_en_US).(*Messages)
  801. pr.PageTitle = fmt.Sprintf("%s - %s", innerPr.PageTitle, i18n.T(ctx.R, ModelsI18nModuleKey, b.brandTitle))
  802. pr.Body = VApp(
  803. VNavigationDrawer(
  804. b.RunBrandProfileSwitchLanguageDisplayFunc(b.RunBrandFunc(ctx), profile, b.RunSwitchLanguageFunc(ctx), ctx),
  805. VDivider(),
  806. menu,
  807. ).App(true).
  808. // Clipped(true).
  809. Fixed(true).
  810. Value(true).
  811. Attr("v-model", "vars.navDrawer").
  812. Attr(web.InitContextVars, `{navDrawer: null}`),
  813. VAppBar(
  814. VAppBarNavIcon().On("click.stop", "vars.navDrawer = !vars.navDrawer"),
  815. h.Span(innerPr.PageTitle).Class("text-h6 font-weight-regular"),
  816. VSpacer(),
  817. h.If(showSearchBox,
  818. VLayout(
  819. // h.Form(
  820. VTextField().
  821. SoloInverted(true).
  822. PrependIcon("search").
  823. Label(msgr.Search).
  824. Flat(true).
  825. Clearable(true).
  826. HideDetails(true).
  827. Value(ctx.R.URL.Query().Get("keyword")).
  828. Attr("@keyup.enter", web.Plaid().
  829. ClearMergeQuery("page").
  830. Query("keyword", web.Var("[$event.target.value]")).
  831. MergeQuery(true).
  832. PushState(true).
  833. Go()).
  834. Attr("@click:clear", web.Plaid().
  835. Query("keyword", "").
  836. PushState(true).
  837. Go()),
  838. // ).Method("GET"),
  839. ).AlignCenter(true).Attr("style", "max-width: 650px"),
  840. ),
  841. h.If(showNotificationCenter,
  842. notifier,
  843. ),
  844. ).Dark(true).
  845. Color(ColorPrimary).
  846. App(true).
  847. Fixed(true),
  848. // ClippedLeft(true),
  849. web.Portal().Name(RightDrawerPortalName),
  850. web.Portal().Name(DialogPortalName),
  851. web.Portal().Name(DeleteConfirmPortalName),
  852. web.Portal().Name(DefaultConfirmDialogPortalName),
  853. web.Portal().Name(ListingDialogPortalName),
  854. VProgressLinear().
  855. Attr(":active", "isFetching").
  856. Attr("style", "position: fixed; z-index: 99").
  857. Indeterminate(true).
  858. Height(2).
  859. Color(b.progressBarColor),
  860. h.Template(
  861. VSnackbar(h.Text("{{vars.presetsMessage.message}}")).
  862. Attr("v-model", "vars.presetsMessage.show").
  863. Attr(":color", "vars.presetsMessage.color").
  864. Timeout(2000).
  865. Top(true),
  866. ).Attr("v-if", "vars.presetsMessage"),
  867. VMain(
  868. innerPr.Body.(h.HTMLComponent),
  869. ),
  870. ).Id("vt-app").
  871. Attr(web.InitContextVars, `{presetsRightDrawer: false, presetsDialog: false, presetsListingDialog: false, presetsMessage: {show: false, color: "success", message: ""}}`)
  872. return
  873. }
  874. }
  875. // for pages outside the default presets layout
  876. func (b *Builder) PlainLayout(in web.PageFunc) (out web.PageFunc) {
  877. return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
  878. b.InjectAssets(ctx)
  879. var innerPr web.PageResponse
  880. innerPr, err = in(ctx)
  881. if err == perm.PermissionDenied {
  882. pr.Body = h.Text(perm.PermissionDenied.Error())
  883. return pr, nil
  884. }
  885. if err != nil {
  886. panic(err)
  887. }
  888. pr.PageTitle = fmt.Sprintf("%s - %s", innerPr.PageTitle, i18n.T(ctx.R, ModelsI18nModuleKey, b.brandTitle))
  889. pr.Body = VApp(
  890. web.Portal().Name(DialogPortalName),
  891. web.Portal().Name(DeleteConfirmPortalName),
  892. web.Portal().Name(DefaultConfirmDialogPortalName),
  893. VProgressLinear().
  894. Attr(":active", "isFetching").
  895. Attr("style", "position: fixed; z-index: 99").
  896. Indeterminate(true).
  897. Height(2).
  898. Color(b.progressBarColor),
  899. h.Template(
  900. VSnackbar(h.Text("{{vars.presetsMessage.message}}")).
  901. Attr("v-model", "vars.presetsMessage.show").
  902. Attr(":color", "vars.presetsMessage.color").
  903. Timeout(2000).
  904. Top(true),
  905. ).Attr("v-if", "vars.presetsMessage"),
  906. VMain(
  907. innerPr.Body.(h.HTMLComponent),
  908. ),
  909. ).Id("vt-app").
  910. Attr(web.InitContextVars, `{presetsDialog: false, presetsMessage: {show: false, color: "success", message: ""}}`)
  911. return
  912. }
  913. }
  914. func (b *Builder) InjectAssets(ctx *web.EventContext) {
  915. ctx.Injector.HeadHTML(strings.Replace(`
  916. <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Mono">
  917. <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
  918. <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
  919. <link rel="stylesheet" href="{{prefix}}/assets/main.css">
  920. <script src='{{prefix}}/assets/vue.js'></script>
  921. <style>
  922. [v-cloak] {
  923. display: none;
  924. }
  925. .vx-list-item--active {
  926. position: relative;
  927. }
  928. .vx-list-item--active:after {
  929. opacity: .12;
  930. background-color: currentColor;
  931. bottom: 0;
  932. content: "";
  933. left: 0;
  934. pointer-events: none;
  935. position: absolute;
  936. right: 0;
  937. top: 0;
  938. transition: .3s cubic-bezier(.25,.8,.5,1);
  939. line-height: 0;
  940. }
  941. .vx-list-item--active:hover {
  942. background-color: inherit!important;
  943. }
  944. </style>
  945. `, "{{prefix}}", b.prefix, -1))
  946. b.InjectExtraAssets(ctx)
  947. if len(os.Getenv("DEV_PRESETS")) > 0 {
  948. ctx.Injector.TailHTML(`
  949. <script src='http://localhost:3080/js/chunk-vendors.js'></script>
  950. <script src='http://localhost:3080/js/app.js'></script>
  951. <script src='http://localhost:3100/js/chunk-vendors.js'></script>
  952. <script src='http://localhost:3100/js/app.js'></script>
  953. `)
  954. } else {
  955. ctx.Injector.TailHTML(strings.Replace(`
  956. <script src='{{prefix}}/assets/main.js'></script>
  957. `, "{{prefix}}", b.prefix, -1))
  958. }
  959. if b.assetFunc != nil {
  960. b.assetFunc(ctx)
  961. }
  962. }
  963. func (b *Builder) InjectExtraAssets(ctx *web.EventContext) {
  964. for _, ea := range b.extraAssets {
  965. if len(ea.refTag) > 0 {
  966. ctx.Injector.HeadHTML(ea.refTag)
  967. continue
  968. }
  969. if strings.HasSuffix(ea.path, "css") {
  970. ctx.Injector.HeadHTML(fmt.Sprintf("<link rel=\"stylesheet\" href=\"%s\">", b.extraFullPath(ea)))
  971. continue
  972. }
  973. ctx.Injector.HeadHTML(fmt.Sprintf("<script src=\"%s\"></script>", b.extraFullPath(ea)))
  974. }
  975. }
  976. func (b *Builder) defaultHomePageFunc(ctx *web.EventContext) (r web.PageResponse, err error) {
  977. r.Body = h.Div().Text("home")
  978. return
  979. }
  980. func (b *Builder) getHomePageFunc() web.PageFunc {
  981. if b.homePageFunc != nil {
  982. return b.homePageFunc
  983. }
  984. return b.defaultHomePageFunc
  985. }
  986. func (b *Builder) DefaultNotFoundPageFunc(ctx *web.EventContext) (r web.PageResponse, err error) {
  987. msgr := MustGetMessages(ctx.R)
  988. r.Body = h.Div(
  989. h.H1("404").Class("mb-2"),
  990. h.Text(msgr.NotFoundPageNotice),
  991. ).Class("text-center mt-8")
  992. return
  993. }
  994. func (b *Builder) getNotFoundPageFunc() web.PageFunc {
  995. pf := b.DefaultNotFoundPageFunc
  996. if b.notFoundFunc != nil {
  997. pf = b.notFoundFunc
  998. }
  999. return func(ctx *web.EventContext) (r web.PageResponse, err error) {
  1000. ctx.W.WriteHeader(http.StatusNotFound)
  1001. return pf(ctx)
  1002. }
  1003. }
  1004. func (b *Builder) extraFullPath(ea *extraAsset) string {
  1005. return b.prefix + "/extra" + ea.path
  1006. }
  1007. func (b *Builder) initMux() {
  1008. b.logger.Info("initializing mux for", zap.Reflect("models", modelNames(b.models)), zap.String("prefix", b.prefix))
  1009. mux := goji.NewMux()
  1010. ub := b.builder
  1011. mainJSPath := b.prefix + "/assets/main.js"
  1012. mux.Handle(pat.Get(mainJSPath),
  1013. ub.PacksHandler("text/javascript",
  1014. Vuetify(b.vuetifyOptions),
  1015. JSComponentsPack(),
  1016. vuetifyx.JSComponentsPack(),
  1017. web.JSComponentsPack(),
  1018. ),
  1019. )
  1020. log.Println("mounted url", mainJSPath)
  1021. vueJSPath := b.prefix + "/assets/vue.js"
  1022. mux.Handle(pat.Get(vueJSPath),
  1023. ub.PacksHandler("text/javascript",
  1024. web.JSVueComponentsPack(),
  1025. ),
  1026. )
  1027. log.Println("mounted url", vueJSPath)
  1028. mainCSSPath := b.prefix + "/assets/main.css"
  1029. mux.Handle(pat.Get(mainCSSPath),
  1030. ub.PacksHandler("text/css",
  1031. CSSComponentsPack(),
  1032. ),
  1033. )
  1034. log.Println("mounted url", mainCSSPath)
  1035. for _, ea := range b.extraAssets {
  1036. fullPath := b.extraFullPath(ea)
  1037. mux.Handle(pat.Get(fullPath), ub.PacksHandler(
  1038. ea.contentType,
  1039. ea.body,
  1040. ))
  1041. log.Println("mounted url", fullPath)
  1042. }
  1043. homeURL := b.prefix
  1044. if homeURL == "" {
  1045. homeURL = "/"
  1046. }
  1047. mux.Handle(
  1048. pat.New(homeURL),
  1049. b.wrap(nil, b.layoutFunc(b.getHomePageFunc(), b.homePageLayoutConfig)),
  1050. )
  1051. for _, m := range b.models {
  1052. pluralUri := inflection.Plural(m.uriName)
  1053. info := m.Info()
  1054. routePath := info.ListingHref()
  1055. inPageFunc := m.listing.GetPageFunc()
  1056. if m.singleton {
  1057. inPageFunc = m.editing.singletonPageFunc
  1058. if m.layoutConfig == nil {
  1059. m.layoutConfig = &LayoutConfig{}
  1060. }
  1061. m.layoutConfig.SearchBoxInvisible = true
  1062. }
  1063. mux.Handle(
  1064. pat.New(routePath),
  1065. b.wrap(m, b.layoutFunc(inPageFunc, m.layoutConfig)),
  1066. )
  1067. log.Println("mounted url", routePath)
  1068. if m.hasDetailing {
  1069. routePath = fmt.Sprintf("%s/%s/:id", b.prefix, pluralUri)
  1070. mux.Handle(
  1071. pat.New(routePath),
  1072. b.wrap(m, b.detailLayoutFunc(m.detailing.GetPageFunc(), m.layoutConfig)),
  1073. )
  1074. log.Println("mounted url", routePath)
  1075. }
  1076. }
  1077. // Handle 404
  1078. mux.Use(func(handler http.Handler) http.Handler {
  1079. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1080. if !strings.HasPrefix(r.RequestURI, b.prefix) || middleware.Handler(r.Context()) != nil {
  1081. handler.ServeHTTP(w, r)
  1082. return
  1083. }
  1084. b.wrap(
  1085. nil,
  1086. b.layoutFunc(b.getNotFoundPageFunc(), b.notFoundPageLayoutConfig),
  1087. ).ServeHTTP(w, r)
  1088. return
  1089. })
  1090. })
  1091. b.mux = mux
  1092. }
  1093. func (b *Builder) AddWrapHandler(key string, f func(in http.Handler) (out http.Handler)) {
  1094. b.wrapHandlers[key] = f
  1095. }
  1096. func (b *Builder) wrap(m *ModelBuilder, pf web.PageFunc) http.Handler {
  1097. p := b.builder.Page(pf)
  1098. if m != nil {
  1099. m.registerDefaultEventFuncs()
  1100. p.MergeHub(&m.EventsHub)
  1101. }
  1102. handlers := b.I18n().EnsureLanguage(
  1103. p,
  1104. )
  1105. for _, wrapHandler := range b.wrapHandlers {
  1106. handlers = wrapHandler(handlers)
  1107. }
  1108. return handlers
  1109. }
  1110. func (b *Builder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  1111. if b.mux == nil {
  1112. b.initMux()
  1113. }
  1114. RedirectSlashes(b.mux).ServeHTTP(w, r)
  1115. }
  1116. func RedirectSlashes(next http.Handler) http.Handler {
  1117. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1118. path := r.URL.Path
  1119. if len(path) > 1 && path[len(path)-1] == '/' {
  1120. if r.URL.RawQuery != "" {
  1121. path = fmt.Sprintf("%s?%s", path[:len(path)-1], r.URL.RawQuery)
  1122. } else {
  1123. path = path[:len(path)-1]
  1124. }
  1125. redirectURL := fmt.Sprintf("//%s%s", r.Host, path)
  1126. http.Redirect(w, r, redirectURL, 301)
  1127. return
  1128. }
  1129. next.ServeHTTP(w, r)
  1130. })
  1131. }