Remove theme editor refactor feature flag

closes CORE-690
closes CORE-691

Test Plan:
  - Theme Editor works completely
  - Moving between accordion pieces is accessible
  - There is focus indication between the Edit and Upload tabs
    when custom css/js is enabled

Change-Id: Ica93743b2828c1847d385c477c2f2b4255831fe2
Reviewed-on: https://gerrit.instructure.com/141784
Reviewed-by: Brent Burgoyne <bburgoyne@instructure.com>
QA-Review: Jeremy Putnam <jeremyp@instructure.com>
Tested-by: Jenkins
QA-Review: Tucker McKnight <tmcknight@instructure.com>
Product-Review: Clay Diffrient <cdiffrient@instructure.com>
This commit is contained in:
Clay Diffrient 2018-02-23 14:57:21 -07:00
parent 10e6dd617c
commit 20c4158dfd
9 changed files with 35 additions and 499 deletions

View File

@ -55,8 +55,7 @@ class BrandConfigsController < ApplicationController
hasUnsavedChanges: session.key?(:brand_config_md5),
variableSchema: default_schema,
allowGlobalIncludes: @account.allow_global_includes?,
account_id: @account.id,
REFACTOR_ENABLED: @account.feature_enabled?(:theme_editor_refactor)
account_id: @account.id
render html: '', layout: 'layouts/bare'
end

View File

@ -34,8 +34,7 @@ ReactDOM.render(
sharedBrandConfigs: window.ENV.sharedBrandConfigs,
allowGlobalIncludes: window.ENV.allowGlobalIncludes,
accountID: window.ENV.account_id,
useHighContrast: window.ENV.use_high_contrast,
refactorEnabled: window.ENV.REFACTOR_ENABLED
useHighContrast: window.ENV.use_high_contrast
}}
/>,
document.body

View File

@ -76,12 +76,7 @@ export default class ThemeEditor extends React.Component {
variableSchema: customTypes.variableSchema,
allowGlobalIncludes: PropTypes.bool,
accountID: PropTypes.string,
useHighContrast: PropTypes.bool,
refactorEnabled: PropTypes.bool
}
static defaultProps = {
refactorEnabled: false
useHighContrast: PropTypes.bool
}
constructor(props) {
@ -274,9 +269,7 @@ export default class ThemeEditor extends React.Component {
$.ajax({
url: `/accounts/${this.props.accountID}/brand_configs`,
type: 'POST',
data: this.props.refactorEnabled
? this.processThemeStoreForSubmit()
: new FormData(this.ThemeEditorForm),
data: this.processThemeStoreForSubmit(),
processData: false,
contentType: false,
dataType: 'json'
@ -479,120 +472,16 @@ export default class ThemeEditor extends React.Component {
'Theme__layout--is-active-theme'}`}
>
<div className="Theme__editor">
{this.props.refactorEnabled ? (
<ThemeEditorSidebar
themeStore={this.state.themeStore}
handleThemeStateChange={this.handleThemeStateChange}
refactorEnabled={this.props.refactorEnabled}
allowGlobalIncludes={this.props.allowGlobalIncludes}
brandConfig={this.props.brandConfig}
variableSchema={this.props.variableSchema}
getDisplayValue={this.getDisplayValue}
changeSomething={this.changeSomething}
changedValues={this.state.changedValues}
/>
) : (
<div className="Theme__editor-tabs">
{this.renderTabInputs()}
<div className="Theme__editor-tab-label-layout">{this.renderTabLabels()}</div>
<div id="te-editor-panel" className="Theme__editor-tabs_panel">
<ThemeEditorAccordion
refactorEnabled={this.props.refactorEnabled}
variableSchema={this.props.variableSchema}
brandConfigVariables={this.props.brandConfig.variables}
getDisplayValue={this.getDisplayValue}
changedValues={this.state.changedValues}
changeSomething={this.changeSomething}
/>
</div>
{this.props.allowGlobalIncludes ? (
<div id="te-upload-panel" className="Theme__editor-tabs_panel">
<div className="Theme__editor-upload-overrides">
<div className="Theme__editor-upload-warning">
<div className="Theme__editor-upload-warning_icon">
<i className="icon-warning" />
</div>
<div>
<p className="Theme__editor-upload-warning_text-emphasis">
{I18n.t(
'Custom CSS and Javascript may cause accessibility issues or conflicts with future Canvas updates!'
)}
</p>
<p
dangerouslySetInnerHTML={{
__html: I18n.t(
'Before implementing custom CSS or Javascript, please refer to *our documentation*.',
{
wrappers: [
'<a href="https://community.canvaslms.com/docs/DOC-3010" target="_blank">$1</a>'
]
}
)
}}
/>
</div>
</div>
<div className="Theme__editor-upload-overrides_header">
{I18n.t(
'File(s) will be included on all pages in the Canvas desktop application.'
)}
</div>
<div className="Theme__editor-upload-overrides_form">
<ThemeEditorFileUpload
label={I18n.t('CSS file')}
accept=".css"
name="css_overrides"
currentValue={this.props.brandConfig.css_overrides}
userInput={this.state.changedValues.css_overrides}
onChange={this.changeSomething.bind(null, 'css_overrides')}
/>
<ThemeEditorFileUpload
label={I18n.t('JavaScript file')}
accept=".js"
name="js_overrides"
currentValue={this.props.brandConfig.js_overrides}
userInput={this.state.changedValues.js_overrides}
onChange={this.changeSomething.bind(null, 'js_overrides')}
/>
</div>
</div>
<div className="Theme__editor-upload-overrides">
<div className="Theme__editor-upload-overrides_header">
{I18n.t(
'File(s) will be included when user content is displayed within the Canvas iOS or Android apps, and in third-party apps built on our API.'
)}
</div>
<div className="Theme__editor-upload-overrides_form">
<ThemeEditorFileUpload
label={I18n.t('Mobile app CSS file')}
accept=".css"
name="mobile_css_overrides"
currentValue={this.props.brandConfig.mobile_css_overrides}
userInput={this.state.changedValues.mobile_css_overrides}
onChange={this.changeSomething.bind(null, 'mobile_css_overrides')}
/>
<ThemeEditorFileUpload
label={I18n.t('Mobile app JavaScript file')}
accept=".js"
name="mobile_js_overrides"
currentValue={this.props.brandConfig.mobile_js_overrides}
userInput={this.state.changedValues.mobile_js_overrides}
onChange={this.changeSomething.bind(null, 'mobile_js_overrides')}
/>
</div>
</div>
</div>
) : null}
</div>
)}
<ThemeEditorSidebar
themeStore={this.state.themeStore}
handleThemeStateChange={this.handleThemeStateChange}
allowGlobalIncludes={this.props.allowGlobalIncludes}
brandConfig={this.props.brandConfig}
variableSchema={this.props.variableSchema}
getDisplayValue={this.getDisplayValue}
changeSomething={this.changeSomething}
changedValues={this.state.changedValues}
/>
</div>
<div className="Theme__preview">

View File

@ -37,74 +37,18 @@ export default class ThemeEditorAccordion extends React.Component {
changedValues: PropTypes.object.isRequired,
changeSomething: PropTypes.func.isRequired,
getDisplayValue: PropTypes.func.isRequired,
refactorEnabled: PropTypes.bool,
accordionContextOverride: PropTypes.object, // Temporary prop that should be removed after removing the refactorEnabled stuff
themeState: PropTypes.object,
handleThemeStateChange: PropTypes.func
}
static defaultProps = {
refactorEnabled: false
}
state = {
expandedIndex: Number(window.sessionStorage.getItem(activeIndexKey))
}
componentDidMount() {
if (!this.props.refactorEnabled) {
this.initAccordion()
}
}
setStoredAccordionIndex(index) {
window.sessionStorage.setItem(activeIndexKey, index)
}
getStoredAccordionIndex = () => {
if (!this.props.refactorEnabled) {
// Note that "Number(null)" returns 0
return Number(window.sessionStorage.getItem(activeIndexKey))
}
}
// Returns the index of the current accordion pane open
getCurrentIndex = () => {
if (!this.props.refactorEnabled) {
return $(this.rootNode).accordion('option', 'active')
}
}
// Remembers which accordion pane is open
rememberActiveIndex = () => {
if (!this.props.refactorEnabled) {
const index = this.getCurrentIndex()
this.setStoredAccordionIndex(index)
}
}
initAccordion = () => {
if (!this.props.refactorEnabled) {
const self = this.props.accordionContextOverride || this
const index = self.getStoredAccordionIndex()
$(self.rootNode).accordion({
active: index,
header: 'h3',
heightStyle: 'content',
beforeActivate(event, ui) {
const previewIframe = $('#previewIframe')
if ($.trim(ui.newHeader[0].innerText) === 'Login Screen') {
const loginPreview = previewIframe.contents().find('#login-preview')
if (loginPreview) previewIframe.scrollTo(loginPreview)
} else {
previewIframe.scrollTo(0)
}
},
activate: self.rememberActiveIndex
})
}
}
handleToggle = (event, expanded, index) => {
this.setState(
{
@ -125,7 +69,6 @@ export default class ThemeEditorAccordion extends React.Component {
placeholder: this.props.getDisplayValue(varDef.variable_name),
themeState: this.props.themeState,
handleThemeStateChange: this.props.handleThemeStateChange,
refactorEnabled: this.props.refactorEnabled,
varDef
}
@ -158,45 +101,24 @@ export default class ThemeEditorAccordion extends React.Component {
}
render() {
if (!this.props.refactorEnabled) {
return (
<div
ref={c => (this.rootNode = c)}
className="accordion ui-accordion--mini Theme__editor-accordion"
>
{this.props.variableSchema.map(variableGroup => [
<h3>
<a href="#">
<div className="te-Flex">
<span className="te-Flex__block">{variableGroup.group_name}</span>
<i className="Theme__editor-accordion-icon icon-mini-arrow-right" />
</div>
</a>
</h3>,
<div>{variableGroup.variables.map(this.renderRow)}</div>
])}
</div>
)
} else {
return (
<div>
{this.props.variableSchema.map((variableGroup, index) => (
<ThemeEditorVariableGroup
key={variableGroup.group_name}
summary={
<Text as="h3" weight="bold">
{variableGroup.group_name}
</Text>
}
index={index}
expanded={index === this.state.expandedIndex}
onToggle={this.handleToggle}
>
{variableGroup.variables.map(this.renderRow)}
</ThemeEditorVariableGroup>
))}
</div>
)
}
return (
<div>
{this.props.variableSchema.map((variableGroup, index) => (
<ThemeEditorVariableGroup
key={variableGroup.group_name}
summary={
<Text as="h3" weight="bold">
{variableGroup.group_name}
</Text>
}
index={index}
expanded={index === this.state.expandedIndex}
onToggle={this.handleToggle}
>
{variableGroup.variables.map(this.renderRow)}
</ThemeEditorVariableGroup>
))}
</div>
)
}
}

View File

@ -30,15 +30,13 @@ export default class ThemeEditorColorRow extends Component {
userInput: customTypes.userVariableInput,
placeholder: PropTypes.string.isRequired,
themeState: PropTypes.object,
handleThemeStateChange: PropTypes.func,
refactorEnabled: PropTypes.bool
handleThemeStateChange: PropTypes.func
}
static defaultProps = {
userInput: {},
themeState: {},
handleThemeStateChange() {},
refactorEnabled: false
handleThemeStateChange() {}
}
state = {}
@ -122,13 +120,6 @@ export default class ThemeEditorColorRow extends Component {
const hexValue = this.hexVal(colorVal)
return (
<span>
{!this.props.refactorEnabled && (
<input
type="hidden"
name={`brand_config[variables][${this.props.varDef.variable_name}]`}
value={hexValue}
/>
)}
<input
ref={c => (this.textInput = c)}
type="text"

View File

@ -24,7 +24,6 @@ import Container from '@instructure/ui-core/lib/components/Container'
import types from './PropTypes'
import ThemeEditorAccordion from './ThemeEditorAccordion'
import ThemeEditorFileUpload from './ThemeEditorFileUpload'
import ThemeEditor from './ThemeEditor'
export default function ThemeEditorSidebar(props) {
if (props.allowGlobalIncludes) {
@ -32,7 +31,6 @@ export default function ThemeEditorSidebar(props) {
<TabList variant="minimal" size="medium" padding="0">
<TabPanel title={I18n.t('Edit')}>
<ThemeEditorAccordion
refactorEnabled={props.refactorEnabled}
variableSchema={props.variableSchema}
brandConfigVariables={props.brandConfig.variables}
getDisplayValue={props.getDisplayValue}

View File

@ -50,7 +50,6 @@ function generateProps(type, overrides = {}) {
},
changeSomething() {},
getDisplayValue: () => 'display_name',
refactorEnabled: true,
...overrides
}
}

View File

@ -115,14 +115,6 @@ class Feature
applies_to: 'RootAccount',
state: 'hidden'
},
'theme_editor_refactor' =>
{
display_name: -> { I18n.t('Theme Editor Refactor')},
description: -> { I18n.t('Move to using InstUI for several components and implementing a store system') },
applies_to: 'Account',
state: 'hidden',
development: true
},
'section_specific_announcements' =>
{
display_name: -> { I18n.t('Section Specific Announcements') },

View File

@ -1,253 +0,0 @@
/*
* Copyright (C) 2016 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import ReactDOM from 'react-dom'
import TestUtils from 'react-addons-test-utils'
import jQuery from 'jquery'
import ThemeEditorAccordion from 'jsx/theme_editor/ThemeEditorAccordion'
import RangeInput from 'jsx/theme_editor/RangeInput'
import ColorRow from 'jsx/theme_editor/ThemeEditorColorRow'
import ImageRow from 'jsx/theme_editor/ThemeEditorImageRow'
let elem, props
QUnit.module('ThemeEditorAccordion Component', {
setup() {
elem = document.createElement('div')
props = {
variableSchema: [],
brandConfigVariables: {},
changedValues: sinon.stub(),
changeSomething: sinon.stub(),
getDisplayValue: sinon.stub()
}
}
})
test('Initializes jQuery accordion', () => {
sinon.spy(jQuery.fn, 'accordion')
const component = ReactDOM.render(<ThemeEditorAccordion {...props} />, elem)
ok(
jQuery(jQuery.fn.accordion.calledOn(component.rootNode)),
'called jquery accordion plugin on dom node'
)
ok(
jQuery.fn.accordion.calledWithMatch({
header: 'h3',
heightStyle: 'content'
}),
'passes configuration options to jquery plugin'
)
jQuery.fn.accordion.restore()
})
test('Opens last used accordion tab', () => {
let options // Stores the last object passed to $dom.accorion({...})
sinon.stub(jQuery.fn, 'accordion').callsFake(opts => {
// Allows us to save the last accordion call if it was an object
// Ex: it won't save if $dom.accordion('options','active') is called
if (typeof opts === 'object') {
options = opts
}
})
let mem // the saved index
let cur // the current index
const dom = document.createElement('div')
const context = {
rootNode: dom,
getStoredAccordionIndex: () => mem || 0,
rememberActiveIndex: () => {
mem = cur
}
}
const component = ReactDOM.render(
<ThemeEditorAccordion {...props} accordionContextOverride={context} />,
elem
)
function initAccordion() {
component.initAccordion()
}
// Simulate opening a pane
function selectPane(index) {
cur = index
options.activate()
}
initAccordion()
// The active pane index is expected to be 0 by default
equal(options.active, 0, 'Opens first pane if none previously recorded')
selectPane(1)
initAccordion() // Simulate page refresh
equal(options.active, 1, 'Remembers and opens second pane after refresh')
selectPane(2)
initAccordion()
equal(options.active, 2, 'Remembers and opens third pane after refresh')
jQuery.fn.accordion.restore()
})
function testRenderRow(type, Component) {
return () => {
props.variableSchema = [
{
group_name: 'test',
variables: [
{
default: 'default',
human_name: 'Friendly Foo',
variable_name: 'foo',
type
}
]
}
]
props.brandConfigVariables = {
foo: 'bar'
}
props.changedValues = {
foo: {val: 'baz'}
}
const component = ReactDOM.render(<ThemeEditorAccordion {...props} />, elem)
const varDef = props.variableSchema[0].variables[0]
const expectedDisplayValue = 'display value'
props.getDisplayValue.returns(expectedDisplayValue)
const row = component.renderRow(varDef)
equal(row.type, Component, 'renders a ThemeEditorColorRow')
equal(row.props.key, varDef.variableName, 'uses variable name as key')
equal(
row.props.currentValue,
props.brandConfigVariables.foo,
'passes current value from brandConfigVariables'
)
equal(row.props.userInput, props.changedValues.foo, 'passes changed value as user input')
row.props.onChange()
ok(
props.changeSomething.calledWith(varDef.variable_name),
'passes bound onChange with variable name'
)
ok(
props.getDisplayValue.calledWith(varDef.variable_name),
'calls props.getDisplayName with variable name'
)
equal(row.props.placeholder, expectedDisplayValue, 'uses display value as placeholder')
equal(row.props.varDef, varDef, 'passes varDef as prop')
}
}
test('renderRow color', testRenderRow('color', ColorRow))
test('renderRow image', testRenderRow('image', ImageRow))
test('renderRow percentage', () => {
props.variableSchema = [
{
group_name: 'test',
variables: [
{
default: '0.1',
human_name: 'Friendly Foo',
variable_name: 'foo',
type: 'percentage'
}
]
}
]
props.brandConfigVariables = {
foo: 0.2
}
props.changedValues = {
foo: {val: 0.3}
}
const component = ReactDOM.render(<ThemeEditorAccordion {...props} />, elem)
const varDef = props.variableSchema[0].variables[0]
const expectedDisplayValue = 'display value'
props.getDisplayValue.returns(expectedDisplayValue)
const row = component.renderRow(varDef)
equal(row.type, RangeInput, 'renders a ThemeEditorColorRow')
equal(row.props.key, varDef.variableName, 'uses variable name as key')
equal(row.props.labelText, varDef.human_name, 'passes human name as label text')
equal(row.props.defaultValue, 0.2, 'passes currentValue to defaultValue as float')
row.props.onChange()
ok(
props.changeSomething.calledWith(varDef.variable_name),
'passes bound onChange with variable name'
)
ok(
props.getDisplayValue.calledWith(varDef.variable_name),
'calls props.getDisplayName with variable name'
)
equal(row.props.formatValue(0.472), '47%', 'formateValue returns a whole number percent string')
})
test('renders each group', () => {
props.variableSchema = [
{
group_name: 'Foo',
variables: []
},
{
group_name: 'Bar',
variables: []
}
]
const component = ReactDOM.render(<ThemeEditorAccordion {...props} />, elem)
const node = component.rootNode
const headings = node.querySelectorAll('.Theme__editor-accordion > h3')
props.variableSchema.forEach((group, index) => {
equal(
headings[index].textContent,
group.group_name,
`has heading for "${group.group_name}" group`
)
})
})
test('renders a row for each variable in the group', () => {
props.variableSchema = [
{
group_name: 'Test Group',
variables: [
{
default: '#047',
human_name: 'Color',
variable_name: 'color',
type: 'color'
},
{
default: 'image.png',
human_name: 'Image',
variable_name: 'image',
type: 'image',
accept: 'image/*'
}
]
}
]
const shallowRenderer = TestUtils.createRenderer()
shallowRenderer.render(<ThemeEditorAccordion {...props} />)
const vdom = shallowRenderer.getRenderOutput()
const rows = vdom.props.children[0][1].props.children
equal(rows[0].type, ColorRow, 'renders color row')
equal(rows[1].type, ImageRow, 'renders image row')
})