details.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. const _ = require('lodash')
  2. const app = require('../../app')
  3. const { Set } = require('immutable')
  4. const { dollarIcon, dropdownIcon } = require('../../assets')
  5. /**
  6. * @param {CrudPagesOptions} opts
  7. */
  8. const details = (opts) => {
  9. const hideCriteria = column => column.routeParam ? `ctrl.$routeParams.${column.routeParam} !== '${opts.paramAll}'` : 'false'
  10. const autocompleteInput = column => {
  11. if (!column.apiPrefix) throw new Error('apiPrefix is required for autocomplete fields')
  12. return html`
  13. <md-autocomplete flex
  14. md-selected-item="ctrl.autocomplete.${raw(column.camelName)}.selectedItem"
  15. md-search-text="ctrl.autocomplete.${raw(column.camelName)}.searchText"
  16. md-items="item in ctrl.autocomplete.${raw(column.camelName)}.getItems(ctrl.autocomplete.${raw(column.camelName)}.searchText)"
  17. md-item-text="item.name || item.key || item.id"
  18. md-require-match="true"
  19. md-selected-item-change="ctrl.autocomplete.${raw(column.camelName)}.onChange()"
  20. md-min-length="0"
  21. placeholder="${column.titleName}"
  22. ng-hide="${hideCriteria(column)}">
  23. <md-item-template>
  24. <span md-highlight-text="ctrl.searchText.${raw(column.camelName)}" md-highlight-flags="^i">{{item.name || item.key || item.id}}</span>
  25. </md-item-template>
  26. </md-autocomplete>
  27. `
  28. }
  29. const multiSelectInput = column => {
  30. if (!column.apiPrefix) throw new Error('apiPrefix is required for multi-select fields')
  31. return html`
  32. <md-input-container>
  33. <label>${column.titleName}</label>
  34. <md-select ng-model="model.${raw(column.camelName)}" multiple>
  35. <md-option ng-repeat="item in ::ctrl.multiSelect.${raw(column.camelName)}.items" value="{{item.key}}">
  36. {{item.name || item.key}}
  37. </md-option>
  38. </md-select>
  39. </md-input-container>
  40. `
  41. }
  42. const attrs = obj => obj ? raw(_.toPairs(obj).map(([key, value]) => `${key}="${value}"`).join(' ')) : ''
  43. const standardInput = column => html`
  44. <md-input-container flex ng-hide="${hideCriteria(column)}">
  45. <label>${column.titleName}</label>
  46. <input type="${column.type || 'text'}" ${attrs(column.attrs)} ng-model="model.${raw(column.camelName)}" />
  47. </md-input-container>
  48. `
  49. const currencyInput = column => html`
  50. <md-input-container flex ng-hide="${hideCriteria(column)}">
  51. <label>${column.titleName}</label>
  52. <input type="number" step="0.01" ng-model="model.${raw(column.camelName)}" />
  53. <md-icon md-svg-src="${dollarIcon}"></md-icon>
  54. </md-input-container>
  55. `
  56. const defaultField = column =>
  57. column.type === 'autocomplete'
  58. ? autocompleteInput(column)
  59. : column.type === 'currency'
  60. ? currencyInput(column)
  61. : column.type === 'multi-select'
  62. ? multiSelectInput(column)
  63. : standardInput(column)
  64. const layout = () => {
  65. if (opts.layout) {
  66. const cols = _.fromPairs(opts.columns.map(col => [col.camelName, col]))
  67. return opts.layout.map(section => html`
  68. <md-card>
  69. ${section.section ? html`<h2 style="padding-left: 15px">${section.section}</h2>`:''}
  70. <md-card-content layout="column" layout-margin>
  71. ${raw(section.rows ? section.rows.map(
  72. row => html`
  73. <div flex layout="column" layout-gt-sm="row" layout-margin>
  74. ${raw(row
  75. .map(field => cols[field])
  76. .map(c => c.field || defaultField(c))
  77. .join('\n')
  78. )}
  79. </div>
  80. `).join('\n')
  81. : '')}
  82. </md-card-content>
  83. </md-card>`).join('\n')
  84. } else {
  85. return html`
  86. <md-card>
  87. <md-card-content layout="column" layout-gt-sm="column" layout-margin>
  88. ${opts.columns.map(c => html`
  89. ${c.field || defaultField(c)}
  90. `)}
  91. </md-card-content>
  92. </md-card>
  93. `
  94. }
  95. }
  96. const template = html`
  97. <app-user-area title-text="${opts.titles && opts.titles.details ? `{{ctrl.titleFn(ctrl)}}` : `${opts.titleName} Details`}">
  98. <form name="form" ng-submit="ctrl.submit()" layout="column" layout-margin
  99. flex-xs="100"
  100. flex-sm="100"
  101. flex-md="70"
  102. flex-lg="70"
  103. flex-xl="70"
  104. >
  105. ${raw(layout())}
  106. <div>
  107. <md-button type="submit" class="md-raised md-primary">Submit</md-button>
  108. </div>
  109. </form>
  110. </app-user-area>
  111. `
  112. console.log(`crud: app${opts.pascalName}DetailsPage`)
  113. app.component(`app${opts.pascalName}DetailsPage`, {
  114. template,
  115. controllerAs: 'ctrl',
  116. controller: function(api, $scope, $routeParams, $mdToast, $location, $q, util) {
  117. this.titleFn = opts.titles && opts.titles.details
  118. this.$routeParams = $routeParams
  119. this.template = template // For inspection purposes
  120. this.apiPrefix = util.fillPath(opts.apiPrefix, $routeParams)
  121. this.appPrefix = util.fillPath(opts.appPrefix, $routeParams)
  122. this.isNew = $routeParams[opts.routeParam] === opts.paramNew
  123. const crud = api.crud(this.apiPrefix)
  124. let original
  125. if (this.isNew) {
  126. original = {}
  127. $scope.model = Object.create(original)
  128. this.loadingPromise = $q.resolve($scope.model)
  129. } else {
  130. this.loadingPromise = crud.read($routeParams[opts.routeParam]).then(model => {
  131. original = model
  132. $scope.model = Object.create(original)
  133. return $scope.model
  134. })
  135. }
  136. this.submit = async () => {
  137. try {
  138. if (this.isNew) {
  139. await crud.create($scope.model)
  140. } else {
  141. const obj = {}
  142. for (var key in $scope.model) {
  143. if ($scope.model.hasOwnProperty(key)) {
  144. obj[key] = $scope.model[key]
  145. }
  146. }
  147. await crud.update(original.id, obj)
  148. }
  149. $mdToast.showSimple(`${opts.titleName} saved.`)
  150. $location.url(this.appPrefix)
  151. } catch (err) {
  152. console.error(err)
  153. $mdToast.showSimple(`Could not save ${opts.titleName}: ${err.message || err.statusText || err}`)
  154. }
  155. }
  156. /* Autocomplete fields */
  157. this.searchText = {}
  158. this.autocomplete = {}
  159. opts.columns.filter(c => c.type === 'autocomplete').forEach(c => {
  160. const crud = api.crud(util.fillPath(c.apiPrefix, $routeParams))
  161. const ac = this.autocomplete[c.camelName] = {
  162. onChange() {
  163. $scope.model[c.camelName] = ac.selectedItem
  164. ? ac.selectedItem.id
  165. : null
  166. },
  167. getItems(searchText) {
  168. return crud.autocomplete(searchText)
  169. }
  170. }
  171. this.loadingPromise.then(model => {
  172. if (model[c.camelName]) {
  173. crud.read(model[c.camelName]).then(record => {
  174. ac.selectedItem = record
  175. })
  176. }
  177. })
  178. })
  179. /* Multi-Select fields */
  180. this.multiSelect = {}
  181. opts.columns.filter(c => c.type === 'multi-select').forEach(c => {
  182. const crud = api.crud(util.fillPath(c.apiPrefix, $routeParams))
  183. const ms = {}
  184. this.multiSelect[c.camelName] = ms
  185. const updateLabel = () => {
  186. ms.label = ms.set.toArray()
  187. .map(key => ms.lookup[key].name || key)
  188. .join(', ')
  189. }
  190. this.loadingPromise.then(() => {
  191. crud.list().then(items => {
  192. ms.lookup = _.fromPairs(items.map(x => [x.key, x]))
  193. ms.items = items
  194. ms.set = new Set($scope.model[c.camelName])
  195. updateLabel()
  196. })
  197. })
  198. ms.add = (key) => {
  199. ms.set = ms.set.add(key)
  200. $scope.model[c.camelName] = ms.set.toArray()
  201. updateLabel()
  202. }
  203. ms.remove = (key) => {
  204. ms.set = ms.set.remove(key)
  205. $scope.model[c.camelName] = ms.set.toArray()
  206. updateLabel()
  207. }
  208. ms.toggle = (key) =>
  209. ms.set.has(key) ? ms.remove(key) : ms.add(key)
  210. })
  211. }
  212. })
  213. }
  214. module.exports = details