listing.go 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476
  1. package presets
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "net/url"
  8. "path"
  9. "strconv"
  10. "strings"
  11. "github.com/qor5/admin/presets/actions"
  12. . "github.com/qor5/ui/vuetify"
  13. vx "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. )
  19. type ListingBuilder struct {
  20. mb *ModelBuilder
  21. bulkActions []*BulkActionBuilder
  22. actions []*ActionBuilder
  23. actionsAsMenu bool
  24. rowMenu *RowMenuBuilder
  25. filterDataFunc FilterDataFunc
  26. filterTabsFunc FilterTabsFunc
  27. newBtnFunc ComponentFunc
  28. pageFunc web.PageFunc
  29. cellWrapperFunc vx.CellWrapperFunc
  30. Searcher SearchFunc
  31. searchColumns []string
  32. perPage int64
  33. totalVisible int64
  34. orderBy string
  35. orderableFields []*OrderableField
  36. selectableColumns bool
  37. conditions []*SQLCondition
  38. dialogWidth string
  39. dialogHeight string
  40. FieldsBuilder
  41. }
  42. func (mb *ModelBuilder) Listing(vs ...string) (r *ListingBuilder) {
  43. r = mb.listing
  44. if len(vs) == 0 {
  45. return
  46. }
  47. r.Only(vs...)
  48. return r
  49. }
  50. func (b *ListingBuilder) Only(vs ...string) (r *ListingBuilder) {
  51. r = b
  52. ivs := make([]interface{}, 0, len(vs))
  53. for _, v := range vs {
  54. ivs = append(ivs, v)
  55. }
  56. r.FieldsBuilder = *r.FieldsBuilder.Only(ivs...)
  57. return
  58. }
  59. func (b *ListingBuilder) PageFunc(pf web.PageFunc) (r *ListingBuilder) {
  60. b.pageFunc = pf
  61. return b
  62. }
  63. func (b *ListingBuilder) CellWrapperFunc(cwf vx.CellWrapperFunc) (r *ListingBuilder) {
  64. b.cellWrapperFunc = cwf
  65. return b
  66. }
  67. func (b *ListingBuilder) SearchFunc(v SearchFunc) (r *ListingBuilder) {
  68. b.Searcher = v
  69. return b
  70. }
  71. func (b *ListingBuilder) SearchColumns(vs ...string) (r *ListingBuilder) {
  72. b.searchColumns = vs
  73. return b
  74. }
  75. func (b *ListingBuilder) PerPage(v int64) (r *ListingBuilder) {
  76. b.perPage = v
  77. return b
  78. }
  79. func (b *ListingBuilder) TotalVisible(v int64) (r *ListingBuilder) {
  80. b.totalVisible = v
  81. return b
  82. }
  83. func (b *ListingBuilder) OrderBy(v string) (r *ListingBuilder) {
  84. b.orderBy = v
  85. return b
  86. }
  87. func (b *ListingBuilder) NewButtonFunc(v ComponentFunc) (r *ListingBuilder) {
  88. b.newBtnFunc = v
  89. return b
  90. }
  91. func (b *ListingBuilder) ActionsAsMenu(v bool) (r *ListingBuilder) {
  92. b.actionsAsMenu = v
  93. return b
  94. }
  95. type OrderableField struct {
  96. FieldName string
  97. DBColumn string
  98. }
  99. func (b *ListingBuilder) OrderableFields(v []*OrderableField) (r *ListingBuilder) {
  100. b.orderableFields = v
  101. return b
  102. }
  103. func (b *ListingBuilder) SelectableColumns(v bool) (r *ListingBuilder) {
  104. b.selectableColumns = v
  105. return b
  106. }
  107. func (b *ListingBuilder) Conditions(v []*SQLCondition) (r *ListingBuilder) {
  108. b.conditions = v
  109. return b
  110. }
  111. func (b *ListingBuilder) DialogWidth(v string) (r *ListingBuilder) {
  112. b.dialogWidth = v
  113. return b
  114. }
  115. func (b *ListingBuilder) DialogHeight(v string) (r *ListingBuilder) {
  116. b.dialogHeight = v
  117. return b
  118. }
  119. func (b *ListingBuilder) GetPageFunc() web.PageFunc {
  120. if b.pageFunc != nil {
  121. return b.pageFunc
  122. }
  123. return b.defaultPageFunc
  124. }
  125. const bulkPanelOpenParamName = "bulkOpen"
  126. const actionPanelOpenParamName = "actionOpen"
  127. const DeleteConfirmPortalName = "deleteConfirm"
  128. const dataTablePortalName = "dataTable"
  129. const dataTableAdditionsPortalName = "dataTableAdditions"
  130. const listingDialogContentPortalName = "listingDialogContentPortal"
  131. func (b *ListingBuilder) defaultPageFunc(ctx *web.EventContext) (r web.PageResponse, err error) {
  132. if b.mb.Info().Verifier().Do(PermList).WithReq(ctx.R).IsAllowed() != nil {
  133. err = perm.PermissionDenied
  134. return
  135. }
  136. msgr := MustGetMessages(ctx.R)
  137. title := msgr.ListingObjectTitle(i18n.T(ctx.R, ModelsI18nModuleKey, b.mb.label))
  138. r.PageTitle = title
  139. r.Body = b.listingComponent(ctx, false)
  140. return
  141. }
  142. func (b *ListingBuilder) listingComponent(
  143. ctx *web.EventContext,
  144. inDialog bool,
  145. ) h.HTMLComponent {
  146. ctx.R = ctx.R.WithContext(context.WithValue(ctx.R.Context(), ctxInDialog, inDialog))
  147. msgr := MustGetMessages(ctx.R)
  148. var tabsAndActionsBar h.HTMLComponent
  149. {
  150. filterTabs := b.filterTabs(ctx, inDialog)
  151. var actionsComponent h.HTMLComponents
  152. if v := b.actionsComponent(msgr, ctx, inDialog); v != nil {
  153. actionsComponent = append(actionsComponent, v)
  154. }
  155. if b.newBtnFunc != nil {
  156. if btn := b.newBtnFunc(ctx); btn != nil {
  157. actionsComponent = append(actionsComponent, b.newBtnFunc(ctx))
  158. }
  159. } else {
  160. disableNewBtn := b.mb.Info().Verifier().Do(PermCreate).WithReq(ctx.R).IsAllowed() != nil
  161. if !disableNewBtn {
  162. onclick := web.Plaid().EventFunc(actions.New)
  163. if inDialog {
  164. onclick.URL(ctx.R.RequestURI).
  165. Query(ParamOverlay, actions.Dialog).
  166. Query(ParamInDialog, true).
  167. Query(ParamListingQueries, ctx.Queries().Encode())
  168. }
  169. actionsComponent = append(actionsComponent, VBtn(msgr.New).
  170. Color("primary").
  171. Depressed(true).
  172. Dark(true).Class("ml-2").
  173. Disabled(disableNewBtn).
  174. Attr("@click", onclick.Go()))
  175. }
  176. }
  177. if filterTabs != nil || len(actionsComponent) > 0 {
  178. tabsAndActionsBar = VToolbar(
  179. filterTabs,
  180. VSpacer(),
  181. actionsComponent,
  182. ).Flat(true)
  183. }
  184. }
  185. var filterBar h.HTMLComponent
  186. if b.filterDataFunc != nil {
  187. fd := b.filterDataFunc(ctx)
  188. fd.SetByQueryString(ctx.R.URL.RawQuery)
  189. filterBar = b.filterBar(ctx, msgr, fd, inDialog)
  190. }
  191. dataTable, dataTableAdditions := b.getTableComponents(ctx, inDialog)
  192. var dialogHeaderBar h.HTMLComponent
  193. if inDialog {
  194. title := msgr.ListingObjectTitle(i18n.T(ctx.R, ModelsI18nModuleKey, b.mb.label))
  195. var searchBox h.HTMLComponent
  196. if b.mb.layoutConfig == nil || !b.mb.layoutConfig.SearchBoxInvisible {
  197. searchBox = VTextField().
  198. PrependInnerIcon("search").
  199. Placeholder(msgr.Search).
  200. HideDetails(true).
  201. Value(ctx.R.URL.Query().Get("keyword")).
  202. Attr("@keyup.enter", web.Plaid().
  203. URL(ctx.R.RequestURI).
  204. Query("keyword", web.Var("[$event.target.value]")).
  205. MergeQuery(true).
  206. EventFunc(actions.UpdateListingDialog).
  207. Go()).
  208. Attr("@click:clear", web.Plaid().
  209. URL(ctx.R.RequestURI).
  210. Query("keyword", "").
  211. MergeQuery(true).
  212. EventFunc(actions.UpdateListingDialog).
  213. Go()).
  214. Class("ma-0 pa-0 mr-6")
  215. }
  216. dialogHeaderBar = VAppBar(
  217. VToolbarTitle("").
  218. Children(h.Text(title)),
  219. VSpacer(),
  220. searchBox,
  221. VBtn("").Icon(true).
  222. Children(VIcon("close")).
  223. Large(true).
  224. Attr("@click.stop", CloseListingDialogVarScript),
  225. ).Color("white").Elevation(0).Dense(true)
  226. }
  227. return VContainer(
  228. dialogHeaderBar,
  229. tabsAndActionsBar,
  230. h.Div(
  231. VCard(
  232. filterBar,
  233. VDivider(),
  234. VCardText(
  235. web.Portal(dataTable).Name(dataTablePortalName),
  236. ).Class("pa-0"),
  237. ),
  238. web.Portal(dataTableAdditions).Name(dataTableAdditionsPortalName),
  239. ).Class("mt-2"),
  240. ).Fluid(true).
  241. Class("white").
  242. Attr(web.InitContextVars, `{currEditingListItemID: ''}`)
  243. }
  244. func (b *ListingBuilder) cellComponentFunc(f *FieldBuilder) vx.CellComponentFunc {
  245. return func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
  246. return f.compFunc(obj, b.mb.getComponentFuncField(f), ctx)
  247. }
  248. }
  249. func getSelectedIds(ctx *web.EventContext) (selected []string) {
  250. selectedValue := ctx.R.URL.Query().Get(ParamSelectedIds)
  251. if len(selectedValue) > 0 {
  252. selected = strings.Split(selectedValue, ",")
  253. }
  254. return selected
  255. }
  256. func (b *ListingBuilder) bulkPanel(
  257. bulk *BulkActionBuilder,
  258. selectedIds []string,
  259. processedSelectedIds []string,
  260. ctx *web.EventContext,
  261. ) (r h.HTMLComponent) {
  262. msgr := MustGetMessages(ctx.R)
  263. var errComp h.HTMLComponent
  264. if vErr, ok := ctx.Flash.(*web.ValidationErrors); ok {
  265. if gErr := vErr.GetGlobalError(); gErr != "" {
  266. errComp = VAlert(h.Text(gErr)).
  267. Border("left").
  268. Type("error").
  269. Elevation(2).
  270. ColoredBorder(true)
  271. }
  272. }
  273. var processSelectedIdsNotice h.HTMLComponent
  274. if len(processedSelectedIds) < len(selectedIds) {
  275. unactionables := make([]string, 0, len(selectedIds))
  276. {
  277. processedSelectedIdsM := make(map[string]struct{})
  278. for _, v := range processedSelectedIds {
  279. processedSelectedIdsM[v] = struct{}{}
  280. }
  281. for _, v := range selectedIds {
  282. if _, ok := processedSelectedIdsM[v]; !ok {
  283. unactionables = append(unactionables, v)
  284. }
  285. }
  286. }
  287. if len(unactionables) > 0 {
  288. var noticeText string
  289. if bulk.selectedIdsProcessorNoticeFunc != nil {
  290. noticeText = bulk.selectedIdsProcessorNoticeFunc(selectedIds, processedSelectedIds, unactionables)
  291. } else {
  292. var idsText string
  293. if len(unactionables) <= 10 {
  294. idsText = strings.Join(unactionables, ", ")
  295. } else {
  296. idsText = fmt.Sprintf("%s...(+%d)", strings.Join(unactionables[:10], ", "), len(unactionables)-10)
  297. }
  298. noticeText = msgr.BulkActionSelectedIdsProcessNotice(idsText)
  299. }
  300. processSelectedIdsNotice = VAlert(h.Text(noticeText)).
  301. Type("warning")
  302. }
  303. }
  304. onOK := web.Plaid().EventFunc(actions.DoBulkAction).
  305. Query(ParamBulkActionName, bulk.name).
  306. MergeQuery(true)
  307. if isInDialogFromQuery(ctx) {
  308. onOK.URL(ctx.R.RequestURI)
  309. }
  310. return VCard(
  311. VCardTitle(
  312. h.Text(bulk.NameLabel.label),
  313. ),
  314. VCardText(
  315. errComp,
  316. processSelectedIdsNotice,
  317. bulk.compFunc(selectedIds, ctx),
  318. ),
  319. VCardActions(
  320. VSpacer(),
  321. VBtn(msgr.Cancel).
  322. Depressed(true).
  323. Class("ml-2").
  324. Attr("@click", closeDialogVarScript),
  325. VBtn(msgr.OK).
  326. Color("primary").
  327. Depressed(true).
  328. Dark(true).
  329. Attr("@click", onOK.Go()),
  330. ),
  331. )
  332. }
  333. func (b *ListingBuilder) actionPanel(action *ActionBuilder, ctx *web.EventContext) (r h.HTMLComponent) {
  334. msgr := MustGetMessages(ctx.R)
  335. var errComp h.HTMLComponent
  336. if vErr, ok := ctx.Flash.(*web.ValidationErrors); ok {
  337. if gErr := vErr.GetGlobalError(); gErr != "" {
  338. errComp = VAlert(h.Text(gErr)).
  339. Border("left").
  340. Type("error").
  341. Elevation(2).
  342. ColoredBorder(true)
  343. }
  344. }
  345. onOK := web.Plaid().EventFunc(actions.DoListingAction).
  346. Query(ParamListingActionName, action.name).
  347. MergeQuery(true)
  348. if isInDialogFromQuery(ctx) {
  349. onOK.URL(ctx.R.RequestURI)
  350. }
  351. var comp h.HTMLComponent
  352. if action.compFunc != nil {
  353. comp = action.compFunc("", ctx)
  354. }
  355. return VCard(
  356. VCardTitle(
  357. h.Text(action.NameLabel.label),
  358. ),
  359. VCardText(
  360. errComp,
  361. comp,
  362. ),
  363. VCardActions(
  364. VSpacer(),
  365. VBtn(msgr.Cancel).
  366. Depressed(true).
  367. Class("ml-2").
  368. Attr("@click", closeDialogVarScript),
  369. VBtn(msgr.OK).
  370. Color("primary").
  371. Depressed(true).
  372. Dark(true).
  373. Attr("@click", onOK.Go()),
  374. ),
  375. )
  376. }
  377. func (b *ListingBuilder) deleteConfirmation(ctx *web.EventContext) (r web.EventResponse, err error) {
  378. msgr := MustGetMessages(ctx.R)
  379. id := ctx.R.FormValue(ParamID)
  380. promptID := id
  381. if v := ctx.R.FormValue("prompt_id"); v != "" {
  382. promptID = v
  383. }
  384. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  385. Name: DeleteConfirmPortalName,
  386. Body: VDialog(
  387. VCard(
  388. VCardTitle(h.Text(msgr.DeleteConfirmationText(promptID))),
  389. VCardActions(
  390. VSpacer(),
  391. VBtn(msgr.Cancel).
  392. Depressed(true).
  393. Class("ml-2").
  394. On("click", "vars.deleteConfirmation = false"),
  395. VBtn(msgr.Delete).
  396. Color("primary").
  397. Depressed(true).
  398. Dark(true).
  399. Attr("@click", web.Plaid().
  400. EventFunc(actions.DoDelete).
  401. Queries(ctx.Queries()).
  402. URL(ctx.R.URL.Path).
  403. Go()),
  404. ),
  405. ),
  406. ).MaxWidth("600px").
  407. Attr("v-model", "vars.deleteConfirmation").
  408. Attr(web.InitContextVars, `{deleteConfirmation: false}`),
  409. })
  410. r.VarsScript = "setTimeout(function(){ vars.deleteConfirmation = true }, 100)"
  411. return
  412. }
  413. func (b *ListingBuilder) openActionDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
  414. actionName := ctx.R.URL.Query().Get(actionPanelOpenParamName)
  415. action := getAction(b.actions, actionName)
  416. if action == nil {
  417. err = errors.New("cannot find requested action")
  418. return
  419. }
  420. b.mb.p.dialog(
  421. &r,
  422. b.actionPanel(action, ctx),
  423. action.dialogWidth,
  424. )
  425. return
  426. }
  427. func (b *ListingBuilder) openBulkActionDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
  428. msgr := MustGetMessages(ctx.R)
  429. selected := getSelectedIds(ctx)
  430. bulkName := ctx.R.URL.Query().Get(bulkPanelOpenParamName)
  431. bulk := getBulkAction(b.bulkActions, bulkName)
  432. if bulk == nil {
  433. err = errors.New("cannot find requested action")
  434. return
  435. }
  436. if len(selected) == 0 {
  437. ShowMessage(&r, "Please select record", "warning")
  438. return
  439. }
  440. // If selectedIdsProcessorFunc is not nil, process the request in it and skip the confirmation dialog
  441. var processedSelectedIds []string
  442. if bulk.selectedIdsProcessorFunc != nil {
  443. processedSelectedIds, err = bulk.selectedIdsProcessorFunc(selected, ctx)
  444. if err != nil {
  445. return
  446. }
  447. if len(processedSelectedIds) == 0 {
  448. if bulk.selectedIdsProcessorNoticeFunc != nil {
  449. ShowMessage(&r, bulk.selectedIdsProcessorNoticeFunc(selected, processedSelectedIds, selected), "warning")
  450. } else {
  451. ShowMessage(&r, msgr.BulkActionNoAvailableRecords, "warning")
  452. }
  453. return
  454. }
  455. } else {
  456. processedSelectedIds = selected
  457. }
  458. b.mb.p.dialog(
  459. &r,
  460. b.bulkPanel(bulk, selected, processedSelectedIds, ctx),
  461. bulk.dialogWidth,
  462. )
  463. return
  464. }
  465. func (b *ListingBuilder) doBulkAction(ctx *web.EventContext) (r web.EventResponse, err error) {
  466. bulk := getBulkAction(b.bulkActions, ctx.R.FormValue(ParamBulkActionName))
  467. if bulk == nil {
  468. panic("bulk required")
  469. }
  470. if b.mb.Info().Verifier().SnakeDo(PermBulkActions, bulk.name).WithReq(ctx.R).IsAllowed() != nil {
  471. ShowMessage(&r, perm.PermissionDenied.Error(), "warning")
  472. return
  473. }
  474. selectedIds := getSelectedIds(ctx)
  475. var err1 error
  476. var processedSelectedIds []string
  477. if bulk.selectedIdsProcessorFunc != nil {
  478. processedSelectedIds, err1 = bulk.selectedIdsProcessorFunc(selectedIds, ctx)
  479. } else {
  480. processedSelectedIds = selectedIds
  481. }
  482. if err1 == nil {
  483. err1 = bulk.updateFunc(processedSelectedIds, ctx)
  484. }
  485. if err1 != nil {
  486. if _, ok := err1.(*web.ValidationErrors); !ok {
  487. vErr := &web.ValidationErrors{}
  488. vErr.GlobalError(err1.Error())
  489. ctx.Flash = vErr
  490. }
  491. }
  492. if ctx.Flash != nil {
  493. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  494. Name: dialogContentPortalName,
  495. Body: b.bulkPanel(bulk, selectedIds, processedSelectedIds, ctx),
  496. })
  497. return
  498. }
  499. msgr := MustGetMessages(ctx.R)
  500. ShowMessage(&r, msgr.SuccessfullyUpdated, "")
  501. if isInDialogFromQuery(ctx) {
  502. qs := ctx.Queries()
  503. qs.Del(bulkPanelOpenParamName)
  504. qs.Del(ParamBulkActionName)
  505. web.AppendVarsScripts(&r,
  506. closeDialogVarScript,
  507. web.Plaid().
  508. URL(ctx.R.RequestURI).
  509. EventFunc(actions.UpdateListingDialog).
  510. Queries(qs).
  511. Go(),
  512. )
  513. } else {
  514. r.PushState = web.Location(url.Values{bulkPanelOpenParamName: []string{}}).MergeQuery(true)
  515. }
  516. return
  517. }
  518. func (b *ListingBuilder) doListingAction(ctx *web.EventContext) (r web.EventResponse, err error) {
  519. action := getAction(b.actions, ctx.R.FormValue(ParamListingActionName))
  520. if action == nil {
  521. panic("action required")
  522. }
  523. if b.mb.Info().Verifier().SnakeDo(PermDoListingAction, action.name).WithReq(ctx.R).IsAllowed() != nil {
  524. ShowMessage(&r, perm.PermissionDenied.Error(), "warning")
  525. return
  526. }
  527. if err := action.updateFunc("", ctx); err != nil {
  528. if _, ok := err.(*web.ValidationErrors); !ok {
  529. vErr := &web.ValidationErrors{}
  530. vErr.GlobalError(err.Error())
  531. ctx.Flash = vErr
  532. }
  533. }
  534. if ctx.Flash != nil {
  535. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  536. Name: dialogContentPortalName,
  537. Body: b.actionPanel(action, ctx),
  538. })
  539. return
  540. }
  541. msgr := MustGetMessages(ctx.R)
  542. ShowMessage(&r, msgr.SuccessfullyUpdated, "")
  543. if isInDialogFromQuery(ctx) {
  544. qs := ctx.Queries()
  545. qs.Del(actionPanelOpenParamName)
  546. qs.Del(ParamListingActionName)
  547. web.AppendVarsScripts(&r,
  548. closeDialogVarScript,
  549. web.Plaid().
  550. URL(ctx.R.RequestURI).
  551. EventFunc(actions.UpdateListingDialog).
  552. Queries(qs).
  553. Go(),
  554. )
  555. } else {
  556. r.PushState = web.Location(url.Values{actionPanelOpenParamName: []string{}}).MergeQuery(true)
  557. }
  558. return
  559. }
  560. const ActiveFilterTabQueryKey = "active_filter_tab"
  561. func (b *ListingBuilder) filterTabs(
  562. ctx *web.EventContext,
  563. inDialog bool,
  564. ) (r h.HTMLComponent) {
  565. if b.filterTabsFunc == nil {
  566. return
  567. }
  568. qs := ctx.R.URL.Query()
  569. tabs := VTabs().ShowArrows(true)
  570. tabsData := b.filterTabsFunc(ctx)
  571. for i, tab := range tabsData {
  572. if tab.ID == "" {
  573. tab.ID = fmt.Sprintf("tab%d", i)
  574. }
  575. }
  576. value := -1
  577. activeTabValue := qs.Get(ActiveFilterTabQueryKey)
  578. for i, td := range tabsData {
  579. // Find selected tab by active_filter_tab=xx in the url query
  580. if activeTabValue == td.ID {
  581. value = i
  582. }
  583. tabContent := h.Text(td.Label)
  584. if td.AdvancedLabel != nil {
  585. tabContent = td.AdvancedLabel
  586. }
  587. totalQuery := url.Values{}
  588. totalQuery.Set(ActiveFilterTabQueryKey, td.ID)
  589. for k, v := range td.Query {
  590. totalQuery[k] = v
  591. }
  592. onclick := web.Plaid().Queries(totalQuery)
  593. if inDialog {
  594. onclick.URL(ctx.R.RequestURI).
  595. EventFunc(actions.UpdateListingDialog)
  596. } else {
  597. onclick.PushState(true)
  598. }
  599. tabs.AppendChildren(
  600. VTab(tabContent).
  601. Attr("@click", onclick.Go()),
  602. )
  603. }
  604. return tabs.Value(value)
  605. }
  606. type selectColumns struct {
  607. DisplayColumns []string `json:"displayColumns,omitempty"`
  608. SortedColumns []sortedColumn `json:"sortedColumns,omitempty"`
  609. }
  610. type sortedColumn struct {
  611. Name string `json:"name"`
  612. Label string `json:"label"`
  613. }
  614. func (b *ListingBuilder) selectColumnsBtn(
  615. pageURL *url.URL,
  616. ctx *web.EventContext,
  617. inDialog bool,
  618. ) (btn h.HTMLComponent, displaySortedFields []*FieldBuilder) {
  619. var (
  620. _, respath = path.Split(pageURL.Path)
  621. displayColumnsName = fmt.Sprintf("%s_display_columns", respath)
  622. sortedColumnsName = fmt.Sprintf("%s_sorted_columns", respath)
  623. originalColumns []string
  624. displayColumns []string
  625. sortedColumns []string
  626. )
  627. for _, f := range b.fields {
  628. if b.mb.Info().Verifier().Do(PermList).SnakeOn("f_"+f.name).WithReq(ctx.R).IsAllowed() != nil {
  629. continue
  630. }
  631. originalColumns = append(originalColumns, f.name)
  632. }
  633. // get the columns setting from url params or cookie data
  634. if urldata := pageURL.Query().Get(displayColumnsName); urldata != "" {
  635. if urlColumns := strings.Split(urldata, ","); len(urlColumns) > 0 {
  636. displayColumns = urlColumns
  637. }
  638. }
  639. if urldata := pageURL.Query().Get(sortedColumnsName); urldata != "" {
  640. if urlColumns := strings.Split(urldata, ","); len(urlColumns) > 0 {
  641. sortedColumns = urlColumns
  642. }
  643. }
  644. // get the columns setting from cookie data
  645. if len(displayColumns) == 0 {
  646. cookiedata, err := ctx.R.Cookie(displayColumnsName)
  647. if err == nil {
  648. if cookieColumns := strings.Split(cookiedata.Value, ","); len(cookieColumns) > 0 {
  649. displayColumns = cookieColumns
  650. }
  651. }
  652. }
  653. if len(sortedColumns) == 0 {
  654. cookiedata, err := ctx.R.Cookie(sortedColumnsName)
  655. if err == nil {
  656. if cookieColumns := strings.Split(cookiedata.Value, ","); len(cookieColumns) > 0 {
  657. sortedColumns = cookieColumns
  658. }
  659. }
  660. }
  661. // check if listing fileds is changed. if yes, use the original columns
  662. var originalFiledsChanged bool
  663. if len(sortedColumns) > 0 && len(originalColumns) != len(sortedColumns) {
  664. originalFiledsChanged = true
  665. }
  666. if len(sortedColumns) > 0 && !originalFiledsChanged {
  667. for _, sortedColumn := range sortedColumns {
  668. var find bool
  669. for _, originalColumn := range originalColumns {
  670. if sortedColumn == originalColumn {
  671. find = true
  672. break
  673. }
  674. }
  675. if !find {
  676. originalFiledsChanged = true
  677. break
  678. }
  679. }
  680. }
  681. if len(displayColumns) > 0 && !originalFiledsChanged {
  682. for _, displayColumn := range displayColumns {
  683. var find bool
  684. for _, originalColumn := range originalColumns {
  685. if displayColumn == originalColumn {
  686. find = true
  687. break
  688. }
  689. }
  690. if !find {
  691. originalFiledsChanged = true
  692. break
  693. }
  694. }
  695. }
  696. // save display columns setting on cookie
  697. if !originalFiledsChanged && len(displayColumns) > 0 {
  698. http.SetCookie(ctx.W, &http.Cookie{
  699. Name: displayColumnsName,
  700. Value: strings.Join(displayColumns, ","),
  701. })
  702. }
  703. // save sorted columns setting on cookie
  704. if !originalFiledsChanged && len(sortedColumns) > 0 {
  705. http.SetCookie(ctx.W, &http.Cookie{
  706. Name: sortedColumnsName,
  707. Value: strings.Join(sortedColumns, ","),
  708. })
  709. }
  710. // set the data for displaySortedFields on data table
  711. if originalFiledsChanged || (len(sortedColumns) == 0 && len(displayColumns) == 0) {
  712. displaySortedFields = b.fields
  713. }
  714. if originalFiledsChanged || len(displayColumns) == 0 {
  715. displayColumns = originalColumns
  716. }
  717. if originalFiledsChanged || len(sortedColumns) == 0 {
  718. sortedColumns = originalColumns
  719. }
  720. if len(displaySortedFields) == 0 {
  721. for _, sortedColumn := range sortedColumns {
  722. for _, displayColumn := range displayColumns {
  723. if sortedColumn == displayColumn {
  724. displaySortedFields = append(displaySortedFields, b.Field(sortedColumn))
  725. break
  726. }
  727. }
  728. }
  729. }
  730. // set the data for selected columns on toolbar
  731. selectColumns := selectColumns{
  732. DisplayColumns: displayColumns,
  733. }
  734. for _, sc := range sortedColumns {
  735. selectColumns.SortedColumns = append(selectColumns.SortedColumns, sortedColumn{
  736. Name: sc,
  737. Label: i18n.PT(ctx.R, ModelsI18nModuleKey, b.mb.label, b.mb.getLabel(b.Field(sc).NameLabel)),
  738. })
  739. }
  740. msgr := MustGetMessages(ctx.R)
  741. onOK := web.Plaid().
  742. Query(displayColumnsName, web.Var("locals.displayColumns")).
  743. Query(sortedColumnsName, web.Var("locals.sortedColumns.map(column => column.name )")).
  744. MergeQuery(true)
  745. if inDialog {
  746. onOK.URL(ctx.R.RequestURI).
  747. EventFunc(actions.UpdateListingDialog)
  748. }
  749. // add the HTML component of columns setting into toolbar
  750. btn = VMenu(
  751. web.Slot(
  752. VBtn("").Children(VIcon("settings")).Attr("v-on", "on").Text(true).Fab(true).Small(true),
  753. ).Name("activator").Scope("{ on }"),
  754. web.Scope(VList(
  755. h.Tag("vx-draggable").Attr("v-model", "locals.sortedColumns", "draggable", ".vx_column_item", "animation", "300").Children(
  756. h.Div(
  757. VListItem(
  758. VListItemContent(
  759. VListItemTitle(
  760. VSwitch().Dense(true).Attr("v-model", "locals.displayColumns", ":value", "column.name", ":label", "column.label", "@click", "event.preventDefault()"),
  761. ),
  762. ),
  763. VListItemIcon(
  764. VIcon("reorder"),
  765. ).Attr("style", "margin-top: 28px"),
  766. ),
  767. VDivider(),
  768. ).Attr("v-for", "(column, index) in locals.sortedColumns", ":key", "column.name", "class", "vx_column_item"),
  769. ),
  770. VListItem(
  771. VListItemAction(VBtn(msgr.Cancel).Elevation(0).Attr("@click", `vars.selectColumnsMenu = false`)),
  772. VListItemAction(VBtn(msgr.OK).Elevation(0).Color("primary").Attr("@click", `vars.selectColumnsMenu = false;`+onOK.Go()))),
  773. ).Dense(true)).
  774. Init(h.JSONString(selectColumns)).
  775. VSlot("{ locals }"),
  776. ).OffsetY(true).CloseOnClick(false).CloseOnContentClick(false).
  777. Attr(web.InitContextVars, `{selectColumnsMenu: false}`).
  778. Attr("v-model", "vars.selectColumnsMenu")
  779. return
  780. }
  781. func (b *ListingBuilder) filterBar(
  782. ctx *web.EventContext,
  783. msgr *Messages,
  784. fd vx.FilterData,
  785. inDialog bool,
  786. ) (filterBar h.HTMLComponent) {
  787. if fd == nil {
  788. return nil
  789. }
  790. noVisiableItem := true
  791. for _, d := range fd {
  792. if !d.Invisible {
  793. noVisiableItem = false
  794. break
  795. }
  796. }
  797. if noVisiableItem {
  798. return nil
  799. }
  800. ft := vx.FilterTranslations{}
  801. ft.Clear = msgr.FiltersClear
  802. ft.Add = msgr.FiltersAdd
  803. ft.Apply = msgr.FilterApply
  804. for _, d := range fd {
  805. d.Translations = vx.FilterIndependentTranslations{
  806. FilterBy: msgr.FilterBy(d.Label),
  807. }
  808. }
  809. ft.Date.To = msgr.FiltersDateTo
  810. ft.Number.And = msgr.FiltersNumberAnd
  811. ft.Number.Equals = msgr.FiltersNumberEquals
  812. ft.Number.Between = msgr.FiltersNumberBetween
  813. ft.Number.GreaterThan = msgr.FiltersNumberGreaterThan
  814. ft.Number.LessThan = msgr.FiltersNumberLessThan
  815. ft.String.Equals = msgr.FiltersStringEquals
  816. ft.String.Contains = msgr.FiltersStringContains
  817. ft.MultipleSelect.In = msgr.FiltersMultipleSelectIn
  818. ft.MultipleSelect.NotIn = msgr.FiltersMultipleSelectNotIn
  819. filter := vx.VXFilter(fd).Translations(ft)
  820. if inDialog {
  821. filter.OnChange(web.Plaid().
  822. URL(ctx.R.RequestURI).
  823. StringQuery(web.Var("$event.encodedFilterData")).
  824. ClearMergeQuery(web.Var("$event.filterKeys")).
  825. EventFunc(actions.UpdateListingDialog).
  826. Go())
  827. }
  828. return VToolbar(
  829. filter,
  830. ).Flat(true).AutoHeight(true).Class("py-2")
  831. }
  832. func getLocalPerPage(
  833. ctx *web.EventContext,
  834. mb *ModelBuilder,
  835. ) int64 {
  836. c, err := ctx.R.Cookie("_perPage")
  837. if err != nil {
  838. return 0
  839. }
  840. vals := strings.Split(c.Value, "$")
  841. for _, v := range vals {
  842. vvs := strings.Split(v, "#")
  843. if len(vvs) != 2 {
  844. continue
  845. }
  846. if vvs[0] == mb.uriName {
  847. r, _ := strconv.ParseInt(vvs[1], 10, 64)
  848. return r
  849. }
  850. }
  851. return 0
  852. }
  853. func setLocalPerPage(
  854. ctx *web.EventContext,
  855. mb *ModelBuilder,
  856. v int64,
  857. ) {
  858. var oldVals []string
  859. {
  860. c, err := ctx.R.Cookie("_perPage")
  861. if err == nil {
  862. oldVals = strings.Split(c.Value, "$")
  863. }
  864. }
  865. newVals := []string{fmt.Sprintf("%s#%d", mb.uriName, v)}
  866. for _, v := range oldVals {
  867. vvs := strings.Split(v, "#")
  868. if len(vvs) != 2 {
  869. continue
  870. }
  871. if vvs[0] == mb.uriName {
  872. continue
  873. }
  874. newVals = append(newVals, v)
  875. }
  876. http.SetCookie(ctx.W, &http.Cookie{
  877. Name: "_perPage",
  878. Value: strings.Join(newVals, "$"),
  879. })
  880. }
  881. type ColOrderBy struct {
  882. FieldName string
  883. // ASC, DESC
  884. OrderBy string
  885. }
  886. func GetOrderBysFromQuery(query url.Values) []*ColOrderBy {
  887. r := make([]*ColOrderBy, 0)
  888. qs := strings.Split(query.Get("order_by"), ",")
  889. for _, q := range qs {
  890. ss := strings.Split(q, "_")
  891. ssl := len(ss)
  892. if ssl == 1 {
  893. continue
  894. }
  895. if ss[ssl-1] != "ASC" && ss[ssl-1] != "DESC" {
  896. continue
  897. }
  898. r = append(r, &ColOrderBy{
  899. FieldName: strings.Join(ss[:ssl-1], "_"),
  900. OrderBy: ss[ssl-1],
  901. })
  902. }
  903. return r
  904. }
  905. func newQueryWithFieldToggleOrderBy(query url.Values, fieldName string) url.Values {
  906. oldOrderBys := GetOrderBysFromQuery(query)
  907. newOrderBysQueryValue := []string{}
  908. existed := false
  909. for _, oob := range oldOrderBys {
  910. if oob.FieldName == fieldName {
  911. existed = true
  912. if oob.OrderBy == "ASC" {
  913. newOrderBysQueryValue = append(newOrderBysQueryValue, oob.FieldName+"_DESC")
  914. }
  915. continue
  916. }
  917. newOrderBysQueryValue = append(newOrderBysQueryValue, oob.FieldName+"_"+oob.OrderBy)
  918. }
  919. if !existed {
  920. newOrderBysQueryValue = append(newOrderBysQueryValue, fieldName+"_ASC")
  921. }
  922. newQuery := make(url.Values)
  923. for k, v := range query {
  924. newQuery[k] = v
  925. }
  926. newQuery.Set("order_by", strings.Join(newOrderBysQueryValue, ","))
  927. return newQuery
  928. }
  929. func (b *ListingBuilder) getTableComponents(
  930. ctx *web.EventContext,
  931. inDialog bool,
  932. ) (
  933. dataTable h.HTMLComponent,
  934. // pagination, no-record message
  935. datatableAdditions h.HTMLComponent,
  936. ) {
  937. msgr := MustGetMessages(ctx.R)
  938. qs := ctx.R.URL.Query()
  939. var requestPerPage int64
  940. qPerPageStr := qs.Get("per_page")
  941. qPerPage, _ := strconv.ParseInt(qPerPageStr, 10, 64)
  942. if qPerPage != 0 {
  943. setLocalPerPage(ctx, b.mb, qPerPage)
  944. requestPerPage = qPerPage
  945. } else if cPerPage := getLocalPerPage(ctx, b.mb); cPerPage != 0 {
  946. requestPerPage = cPerPage
  947. }
  948. perPage := b.perPage
  949. if requestPerPage != 0 {
  950. perPage = requestPerPage
  951. }
  952. if perPage == 0 {
  953. perPage = 50
  954. }
  955. if perPage > 1000 {
  956. perPage = 1000
  957. }
  958. totalVisible := b.totalVisible
  959. if totalVisible == 0 {
  960. totalVisible = 10
  961. }
  962. var orderBySQL string
  963. orderBys := GetOrderBysFromQuery(qs)
  964. // map[FieldName]DBColumn
  965. orderableFieldMap := make(map[string]string)
  966. for _, v := range b.orderableFields {
  967. orderableFieldMap[v.FieldName] = v.DBColumn
  968. }
  969. for _, ob := range orderBys {
  970. dbCol, ok := orderableFieldMap[ob.FieldName]
  971. if !ok {
  972. continue
  973. }
  974. orderBySQL += fmt.Sprintf("%s %s,", dbCol, ob.OrderBy)
  975. }
  976. if orderBySQL != "" {
  977. orderBySQL = orderBySQL[:len(orderBySQL)-1]
  978. }
  979. if orderBySQL == "" {
  980. if b.orderBy != "" {
  981. orderBySQL = b.orderBy
  982. } else {
  983. orderBySQL = fmt.Sprintf("%s DESC", b.mb.primaryField)
  984. }
  985. }
  986. searchParams := &SearchParams{
  987. KeywordColumns: b.searchColumns,
  988. Keyword: qs.Get("keyword"),
  989. PerPage: perPage,
  990. OrderBy: orderBySQL,
  991. PageURL: ctx.R.URL,
  992. SQLConditions: b.conditions,
  993. }
  994. searchParams.Page, _ = strconv.ParseInt(qs.Get("page"), 10, 64)
  995. if searchParams.Page == 0 {
  996. searchParams.Page = 1
  997. }
  998. var fd vx.FilterData
  999. if b.filterDataFunc != nil {
  1000. fd = b.filterDataFunc(ctx)
  1001. cond, args := fd.SetByQueryString(ctx.R.URL.RawQuery)
  1002. searchParams.SQLConditions = append(searchParams.SQLConditions, &SQLCondition{
  1003. Query: cond,
  1004. Args: args,
  1005. })
  1006. }
  1007. if b.Searcher == nil || b.mb.p.dataOperator == nil {
  1008. panic("presets.New().DataOperator(...) required")
  1009. }
  1010. var objs interface{}
  1011. var totalCount int
  1012. var err error
  1013. objs, totalCount, err = b.Searcher(b.mb.NewModelSlice(), searchParams, ctx)
  1014. if err != nil {
  1015. panic(err)
  1016. }
  1017. haveCheckboxes := len(b.bulkActions) > 0
  1018. pagesCount := int(int64(totalCount)/searchParams.PerPage + 1)
  1019. if int64(totalCount)%searchParams.PerPage == 0 {
  1020. pagesCount--
  1021. }
  1022. var cellWraperFunc = func(cell h.MutableAttrHTMLComponent, id string, obj interface{}, dataTableID string) h.HTMLComponent {
  1023. tdbind := cell
  1024. if b.mb.hasDetailing && !b.mb.detailing.drawer {
  1025. tdbind.SetAttr("@click.self", web.Plaid().
  1026. PushStateURL(
  1027. b.mb.Info().
  1028. DetailingHref(id)).
  1029. Go())
  1030. } else {
  1031. event := actions.Edit
  1032. if b.mb.hasDetailing {
  1033. event = actions.DetailingDrawer
  1034. }
  1035. onclick := web.Plaid().
  1036. EventFunc(event).
  1037. Query(ParamID, id)
  1038. if inDialog {
  1039. onclick.URL(ctx.R.RequestURI).
  1040. Query(ParamOverlay, actions.Dialog).
  1041. Query(ParamInDialog, true).
  1042. Query(ParamListingQueries, ctx.Queries().Encode())
  1043. }
  1044. tdbind.SetAttr("@click.self",
  1045. onclick.Go()+fmt.Sprintf(`; vars.currEditingListItemID="%s-%s"`, dataTableID, id))
  1046. }
  1047. return tdbind
  1048. }
  1049. if b.cellWrapperFunc != nil {
  1050. cellWraperFunc = b.cellWrapperFunc
  1051. }
  1052. var displayFields = b.fields
  1053. var selectColumnsBtn h.HTMLComponent
  1054. if b.selectableColumns {
  1055. selectColumnsBtn, displayFields = b.selectColumnsBtn(ctx.R.URL, ctx, inDialog)
  1056. }
  1057. sDataTable := vx.DataTable(objs).
  1058. CellWrapperFunc(cellWraperFunc).
  1059. HeadCellWrapperFunc(func(cell h.MutableAttrHTMLComponent, field string, title string) h.HTMLComponent {
  1060. if _, ok := orderableFieldMap[field]; ok {
  1061. var orderBy string
  1062. var orderByIdx int
  1063. for i, ob := range orderBys {
  1064. if ob.FieldName == field {
  1065. orderBy = ob.OrderBy
  1066. orderByIdx = i + 1
  1067. break
  1068. }
  1069. }
  1070. th := h.Th("").Style("cursor: pointer; white-space: nowrap;").
  1071. Children(
  1072. h.Span(title).
  1073. Style("text-decoration: underline;"),
  1074. h.If(orderBy == "ASC",
  1075. VIcon("arrow_drop_up").Small(true),
  1076. h.Span(fmt.Sprint(orderByIdx)),
  1077. ).ElseIf(orderBy == "DESC",
  1078. VIcon("arrow_drop_down").Small(true),
  1079. h.Span(fmt.Sprint(orderByIdx)),
  1080. ).Else(
  1081. // take up place
  1082. h.Span("").Style("visibility: hidden;").Children(
  1083. VIcon("arrow_drop_down").Small(true),
  1084. h.Span(fmt.Sprint(orderByIdx)),
  1085. ),
  1086. ),
  1087. )
  1088. qs.Del("__execute_event__")
  1089. newQuery := newQueryWithFieldToggleOrderBy(qs, field)
  1090. onclick := web.Plaid().
  1091. Queries(newQuery)
  1092. if inDialog {
  1093. onclick.URL(ctx.R.RequestURI).
  1094. EventFunc(actions.UpdateListingDialog)
  1095. } else {
  1096. onclick.PushState(true)
  1097. }
  1098. th.Attr("@click", onclick.Go())
  1099. cell = th
  1100. }
  1101. return cell
  1102. }).
  1103. RowWrapperFunc(func(row h.MutableAttrHTMLComponent, id string, obj interface{}, dataTableID string) h.HTMLComponent {
  1104. row.SetAttr(":class", fmt.Sprintf(`{"vx-list-item--active primary--text": vars.presetsRightDrawer && vars.currEditingListItemID==="%s-%s"}`, dataTableID, id))
  1105. return row
  1106. }).
  1107. RowMenuItemFuncs(b.RowMenu().listingItemFuncs(ctx)...).
  1108. Selectable(haveCheckboxes).
  1109. SelectionParamName(ParamSelectedIds).
  1110. SelectedCountLabel(msgr.ListingSelectedCountNotice).
  1111. SelectableColumnsBtn(selectColumnsBtn).
  1112. ClearSelectionLabel(msgr.ListingClearSelection)
  1113. if inDialog {
  1114. sDataTable.OnSelectAllFunc(func(idsOfPage []string, ctx *web.EventContext) string {
  1115. return web.Plaid().
  1116. URL(ctx.R.RequestURI).
  1117. EventFunc(actions.UpdateListingDialog).
  1118. Query(ParamSelectedIds,
  1119. web.Var(fmt.Sprintf(`{value: %s, add: $event, remove: !$event}`, h.JSONString(idsOfPage))),
  1120. ).
  1121. MergeQuery(true).
  1122. Go()
  1123. })
  1124. sDataTable.OnSelectFunc(func(id string, ctx *web.EventContext) string {
  1125. return web.Plaid().
  1126. URL(ctx.R.RequestURI).
  1127. EventFunc(actions.UpdateListingDialog).
  1128. Query(ParamSelectedIds,
  1129. web.Var(fmt.Sprintf(`{value: %s, add: $event, remove: !$event}`, h.JSONString(id))),
  1130. ).
  1131. MergeQuery(true).
  1132. Go()
  1133. })
  1134. sDataTable.OnClearSelectionFunc(func(ctx *web.EventContext) string {
  1135. return web.Plaid().
  1136. URL(ctx.R.RequestURI).
  1137. EventFunc(actions.UpdateListingDialog).
  1138. Query(ParamSelectedIds, "").
  1139. MergeQuery(true).
  1140. Go()
  1141. })
  1142. }
  1143. dataTable = sDataTable
  1144. for _, f := range displayFields {
  1145. if b.mb.Info().Verifier().Do(PermList).SnakeOn("f_"+f.name).WithReq(ctx.R).IsAllowed() != nil {
  1146. continue
  1147. }
  1148. f = b.getFieldOrDefault(f.name) // fill in empty compFunc and setter func with default
  1149. dataTable.(*vx.DataTableBuilder).Column(f.name).
  1150. Title(i18n.PT(ctx.R, ModelsI18nModuleKey, b.mb.label, b.mb.getLabel(f.NameLabel))).
  1151. CellComponentFunc(b.cellComponentFunc(f))
  1152. }
  1153. if totalCount > 0 {
  1154. tpb := vx.VXTablePagination().
  1155. Total(int64(totalCount)).
  1156. CurrPage(searchParams.Page).
  1157. PerPage(searchParams.PerPage).
  1158. CustomPerPages([]int64{b.perPage}).
  1159. PerPageText(msgr.PaginationRowsPerPage)
  1160. if inDialog {
  1161. tpb.OnSelectPerPage(web.Plaid().
  1162. URL(ctx.R.RequestURI).
  1163. Query("per_page", web.Var("[$event]")).
  1164. MergeQuery(true).
  1165. EventFunc(actions.UpdateListingDialog).
  1166. Go())
  1167. tpb.OnPrevPage(web.Plaid().
  1168. URL(ctx.R.RequestURI).
  1169. Query("page", searchParams.Page-1).
  1170. MergeQuery(true).
  1171. EventFunc(actions.UpdateListingDialog).
  1172. Go())
  1173. tpb.OnNextPage(web.Plaid().
  1174. URL(ctx.R.RequestURI).
  1175. Query("page", searchParams.Page+1).
  1176. MergeQuery(true).
  1177. EventFunc(actions.UpdateListingDialog).
  1178. Go())
  1179. }
  1180. datatableAdditions = tpb
  1181. } else {
  1182. datatableAdditions = h.Div(h.Text(msgr.ListingNoRecordToShow)).Class("mt-10 text-center grey--text text--darken-2")
  1183. }
  1184. return
  1185. }
  1186. func (b *ListingBuilder) reloadList(ctx *web.EventContext) (r web.EventResponse, err error) {
  1187. dataTable, dataTableAdditions := b.getTableComponents(ctx, false)
  1188. r.UpdatePortals = append(r.UpdatePortals,
  1189. &web.PortalUpdate{
  1190. Name: dataTablePortalName,
  1191. Body: dataTable,
  1192. },
  1193. &web.PortalUpdate{
  1194. Name: dataTableAdditionsPortalName,
  1195. Body: dataTableAdditions,
  1196. },
  1197. )
  1198. return
  1199. }
  1200. func (b *ListingBuilder) actionsComponent(
  1201. msgr *Messages,
  1202. ctx *web.EventContext,
  1203. inDialog bool,
  1204. ) h.HTMLComponent {
  1205. var actionBtns []h.HTMLComponent
  1206. // Render bulk actions
  1207. for _, ba := range b.bulkActions {
  1208. if b.mb.Info().Verifier().SnakeDo(PermBulkActions, ba.name).WithReq(ctx.R).IsAllowed() != nil {
  1209. continue
  1210. }
  1211. var btn h.HTMLComponent
  1212. if ba.buttonCompFunc != nil {
  1213. btn = ba.buttonCompFunc(ctx)
  1214. } else {
  1215. buttonColor := ba.buttonColor
  1216. if buttonColor == "" {
  1217. buttonColor = ColorSecondary
  1218. }
  1219. onclick := web.Plaid().EventFunc(actions.OpenBulkActionDialog).
  1220. Queries(url.Values{bulkPanelOpenParamName: []string{ba.name}}).
  1221. MergeQuery(true)
  1222. if inDialog {
  1223. onclick.URL(ctx.R.RequestURI).
  1224. Query(ParamInDialog, inDialog)
  1225. }
  1226. btn = VBtn(b.mb.getLabel(ba.NameLabel)).
  1227. Color(buttonColor).
  1228. Depressed(true).
  1229. Dark(true).
  1230. Class("ml-2").
  1231. Attr("@click", onclick.Go())
  1232. }
  1233. actionBtns = append(actionBtns, btn)
  1234. }
  1235. // Render actions
  1236. for _, ba := range b.actions {
  1237. if b.mb.Info().Verifier().SnakeDo(PermActions, ba.name).WithReq(ctx.R).IsAllowed() != nil {
  1238. continue
  1239. }
  1240. var btn h.HTMLComponent
  1241. if ba.buttonCompFunc != nil {
  1242. btn = ba.buttonCompFunc(ctx)
  1243. } else {
  1244. buttonColor := ba.buttonColor
  1245. if buttonColor == "" {
  1246. buttonColor = ColorPrimary
  1247. }
  1248. onclick := web.Plaid().EventFunc(actions.OpenActionDialog).
  1249. Queries(url.Values{actionPanelOpenParamName: []string{ba.name}}).
  1250. MergeQuery(true)
  1251. if inDialog {
  1252. onclick.URL(ctx.R.RequestURI).
  1253. Query(ParamInDialog, inDialog)
  1254. }
  1255. btn = VBtn(b.mb.getLabel(ba.NameLabel)).
  1256. Color(buttonColor).
  1257. Depressed(true).
  1258. Dark(true).
  1259. Class("ml-2").
  1260. Attr("@click", onclick.Go())
  1261. }
  1262. actionBtns = append(actionBtns, btn)
  1263. }
  1264. if len(actionBtns) == 0 {
  1265. return nil
  1266. }
  1267. if b.actionsAsMenu {
  1268. var listItems []h.HTMLComponent
  1269. for _, btn := range actionBtns {
  1270. listItems = append(listItems, VListItem(btn))
  1271. }
  1272. return h.Components(VMenu(
  1273. web.Slot(
  1274. VBtn("Actions").
  1275. Attr("v-bind", "attrs").
  1276. Attr("v-on", "on"),
  1277. ).Name("activator").Scope("{ on, attrs }"),
  1278. VList(listItems...),
  1279. ).OpenOnHover(true).
  1280. OffsetY(true).
  1281. AllowOverflow(true))
  1282. }
  1283. return h.Components(actionBtns...)
  1284. }
  1285. func (b *ListingBuilder) openListingDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
  1286. content := VCard(
  1287. web.Portal(b.listingComponent(ctx, true)).
  1288. Name(listingDialogContentPortalName),
  1289. ).Attr("id", "listingDialog")
  1290. dialog := VDialog(content).
  1291. Attr("v-model", "vars.presetsListingDialog")
  1292. if b.dialogWidth != "" {
  1293. dialog.Width(b.dialogWidth)
  1294. }
  1295. if b.dialogHeight != "" {
  1296. content.Attr("height", b.dialogHeight)
  1297. }
  1298. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  1299. Name: ListingDialogPortalName,
  1300. Body: web.Scope(dialog).VSlot("{ plaidForm }"),
  1301. })
  1302. r.VarsScript = "setTimeout(function(){ vars.presetsListingDialog = true }, 100)"
  1303. return
  1304. }
  1305. func (b *ListingBuilder) updateListingDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
  1306. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  1307. Name: listingDialogContentPortalName,
  1308. Body: b.listingComponent(ctx, true),
  1309. })
  1310. web.AppendVarsScripts(&r, `
  1311. var listingDialogElem = document.getElementById('listingDialog');
  1312. if (listingDialogElem.offsetHeight > parseInt(listingDialogElem.style.minHeight || '0', 10)) {
  1313. listingDialogElem.style.minHeight = listingDialogElem.offsetHeight+'px';
  1314. };`)
  1315. return
  1316. }