CollectionView.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /*
  2. * Copyright (C) 2016 - present Instructure, Inc.
  3. *
  4. * This file is part of Canvas.
  5. *
  6. * Canvas is free software: you can redistribute it and/or modify it under
  7. * the terms of the GNU Affero General Public License as published by the Free
  8. * Software Foundation, version 3 of the License.
  9. *
  10. * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
  11. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  13. * details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License along
  16. * with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. import I18n from 'i18n!theme_collection_view'
  19. import React from 'react'
  20. import PropTypes from 'prop-types'
  21. import _ from 'underscore'
  22. import customTypes from './PropTypes'
  23. import submitHtmlForm from './submitHtmlForm'
  24. import ThemeCard from './ThemeCard'
  25. const blankConfig = {
  26. name: I18n.t('Default Template'),
  27. brand_config: {
  28. md5: '',
  29. variables: {}
  30. }
  31. }
  32. const isSystemTheme = (sharedBrandConfig) => !sharedBrandConfig.account_id
  33. export default React.createClass({
  34. displayName: 'CollectionView',
  35. propTypes: {
  36. sharedBrandConfigs: PropTypes.arrayOf(customTypes.sharedBrandConfig).isRequired,
  37. activeBrandConfig: customTypes.brandConfig.isRequired,
  38. accountID: PropTypes.string.isRequired,
  39. brandableVariableDefaults: customTypes.brandableVariableDefaults,
  40. baseBrandableVariables: customTypes.variables
  41. },
  42. getInitialState () {
  43. return {
  44. newThemeModalIsOpen: false,
  45. brandConfigBeingDeleted: null
  46. }
  47. },
  48. brandVariableValue (brandConfig, variableName) {
  49. const explicitValue = brandConfig && brandConfig.variables[variableName]
  50. if (explicitValue) return explicitValue
  51. const variableInfo = this.props.brandableVariableDefaults[variableName]
  52. let _default = variableInfo.default
  53. if (_default && _default[0] === '$') {
  54. return this.brandVariableValue(brandConfig, _default.substring(1))
  55. }
  56. return this.props.baseBrandableVariables[variableName]
  57. },
  58. startFromBlankSlate() {
  59. const md5 = ''
  60. this.saveToSession(md5)
  61. },
  62. startEditing({md5ToActivate, sharedBrandConfigToStartEditing}) {
  63. if (md5ToActivate === (this.props.activeBrandConfig && this.props.activeBrandConfig.md5)) {
  64. md5ToActivate = undefined
  65. }
  66. if (sharedBrandConfigToStartEditing) {
  67. sessionStorage.setItem('sharedBrandConfigBeingEdited', JSON.stringify(sharedBrandConfigToStartEditing))
  68. } else {
  69. sessionStorage.removeItem('sharedBrandConfigBeingEdited')
  70. }
  71. submitHtmlForm(`/accounts/${this.props.accountID}/brand_configs/save_to_user_session`, 'POST', md5ToActivate)
  72. },
  73. deleteSharedBrandConfig (sharedBrandConfigId) {
  74. $.ajaxJSON(`/api/v1/shared_brand_configs/${sharedBrandConfigId}`, 'DELETE', {}, () => {
  75. window.location.reload()
  76. })
  77. },
  78. isActiveBrandConfig (brandConfig) {
  79. if (this.props.activeBrandConfig) {
  80. return brandConfig.md5 === this.props.activeBrandConfig.md5
  81. } else {
  82. return brandConfig === blankConfig.brand_config
  83. }
  84. },
  85. isDeletable (sharedBrandConfig) {
  86. // Globally-shared themes and the active theme are not deletable
  87. return !isSystemTheme(sharedBrandConfig) &&
  88. !this.isActiveBrandConfig(sharedBrandConfig.brand_config)
  89. },
  90. thingsToShow () {
  91. const thingsToShow = [blankConfig].concat(this.props.sharedBrandConfigs)
  92. const isActive = (sharedBrandConfig) => this.isActiveBrandConfig(sharedBrandConfig.brand_config)
  93. // Add in a tile for the active theme if it is otherwise not present in the shared ones
  94. if (this.props.activeBrandConfig && !_.find(this.props.sharedBrandConfigs, isActive)) {
  95. const cardForActiveBrandConfig = {
  96. brand_config: this.props.activeBrandConfig,
  97. account_id: this.props.accountID
  98. }
  99. thingsToShow.unshift(cardForActiveBrandConfig)
  100. }
  101. // Make sure the active theme shows up first
  102. const sorted = _.sortBy(thingsToShow, thing => !isActive(thing))
  103. // split the globally shared themes and the ones that people in this account have shared apart
  104. return _.groupBy(sorted, (sbc) => isSystemTheme(sbc) ? 'globalThemes' : 'accountSpecificThemes')
  105. },
  106. renderCard (sharedConfig) {
  107. const onClick = () => {
  108. const isReadOnly = isSystemTheme(sharedConfig)
  109. this.startEditing({
  110. md5ToActivate: sharedConfig.brand_config.md5,
  111. sharedBrandConfigToStartEditing: !isReadOnly && sharedConfig
  112. })
  113. }
  114. const isActiveEditableTheme = (sbc) =>
  115. !isSystemTheme(sbc) && (
  116. this.props.activeBrandConfig &&
  117. this.props.activeBrandConfig.md5 === sbc.brand_config.md5
  118. )
  119. // even if this theme's md5 is active, don't mark it as active if it is a system theme
  120. // and there is an account-shared theme that also matches the active md5
  121. const isActiveBrandConfig = this.isActiveBrandConfig(sharedConfig.brand_config) && (
  122. !isSystemTheme(sharedConfig) ||
  123. !this.props.sharedBrandConfigs.some(isActiveEditableTheme)
  124. )
  125. return (
  126. <ThemeCard
  127. key={sharedConfig.id + sharedConfig.brand_config.md5}
  128. name={sharedConfig.name}
  129. isActiveBrandConfig={isActiveBrandConfig}
  130. getVariable ={this.brandVariableValue.bind(this, sharedConfig.brand_config)}
  131. open ={onClick}
  132. isDeletable ={this.isDeletable(sharedConfig)}
  133. isBeingDeleted={this.state.brandConfigBeingDeleted === sharedConfig}
  134. startDeleting ={() => this.setState({brandConfigBeingDeleted: sharedConfig})}
  135. cancelDeleting={() => this.setState({brandConfigBeingDeleted: null})}
  136. onDelete ={() => this.deleteSharedBrandConfig(sharedConfig.id)}
  137. />
  138. )
  139. },
  140. render () {
  141. const thingsToShow = this.thingsToShow()
  142. return (
  143. <div>
  144. <div className="ic-Action-header">
  145. <div className="ic-Action-header__Primary">
  146. <h1 className="ic-Action-header__Heading">{I18n.t('Themes')}</h1>
  147. </div>
  148. <div className="ic-Action-header__Secondary">
  149. <div className="al-dropdown__container">
  150. <button
  151. className="al-trigger Button Button--primary"
  152. type="button"
  153. title={I18n.t('Add Theme')}
  154. aria-label={I18n.t('Add Theme')}
  155. >
  156. <i className="icon-plus" />
  157. &nbsp;
  158. {I18n.t('Theme')}
  159. &nbsp;&nbsp;
  160. <i className="icon-mini-arrow-down" />
  161. </button>
  162. <ul className="al-options ic-ThemeCard-add-template-menu">
  163. <li className="ui-menu-item ui-menu-item--helper-text">
  164. {I18n.t('Create theme based on')} &hellip;
  165. </li>
  166. {
  167. ['globalThemes', 'accountSpecificThemes'].map(collection =>
  168. _.map(thingsToShow[collection], sharedConfig =>
  169. <li>
  170. <a
  171. aria-role="button"
  172. href="javascript:;"
  173. onClick={() => this.startEditing({md5ToActivate: sharedConfig.brand_config.md5})}
  174. >
  175. {sharedConfig.name}
  176. </a>
  177. </li>
  178. )
  179. )
  180. }
  181. </ul>
  182. </div>
  183. </div>
  184. </div>
  185. {thingsToShow.globalThemes &&
  186. <div className="ic-ThemeCard-container">
  187. <h3 className="ic-ThemeCard-container__Heading">
  188. <span className="ic-ThemeCard-container__Heading-text">
  189. {I18n.t('Templates')}
  190. <button
  191. type="button"
  192. className="Button Button--icon-action"
  193. data-tooltip='{"tooltipClass":"popover popover-padded", "position":"left"}'
  194. title={I18n.t('Default templates are used as starting points for new themes and cannot be deleted.')}
  195. >
  196. <i className="icon-question" aria-hidden="true" />
  197. </button>
  198. </span>
  199. </h3>
  200. <div className="ic-ThemeCard-container__Main">
  201. {thingsToShow.globalThemes.map(this.renderCard)}
  202. </div>
  203. </div>
  204. }
  205. {thingsToShow.accountSpecificThemes &&
  206. <div className="ic-ThemeCard-container">
  207. <h3 className="ic-ThemeCard-container__Heading">
  208. <span className="ic-ThemeCard-container__Heading-text">
  209. {I18n.t('My Themes')}
  210. </span>
  211. </h3>
  212. <div className="ic-ThemeCard-container__Main">
  213. {thingsToShow.accountSpecificThemes.map(this.renderCard)}
  214. </div>
  215. </div>
  216. }
  217. </div>
  218. )
  219. }
  220. })