diff --git a/app/assets/javascripts/wizard/components/scheme-preview.js.es6 b/app/assets/javascripts/wizard/components/scheme-preview.js.es6
new file mode 100644
index 00000000000..484dafab5b1
--- /dev/null
+++ b/app/assets/javascripts/wizard/components/scheme-preview.js.es6
@@ -0,0 +1,199 @@
+/*eslint no-bitwise:0 */
+
+import { observes } from 'ember-addons/ember-computed-decorators';
+
+const WIDTH = 400;
+const HEIGHT = 220;
+const LINE_HEIGHT = 12.0;
+
+const LOREM = `
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+Nullam eget sem non elit tincidunt rhoncus. Fusce velit nisl,
+porttitor sed nisl ac, consectetur interdum metus. Fusce in
+consequat augue, vel facilisis felis. Nunc tellus elit, and
+semper vitae orci nec, blandit pharetra enim. Aenean a ebus
+posuere nunc. Maecenas ultrices viverra enim ac commodo
+Vestibulum nec quam sit amet libero ultricies sollicitudin.
+Nulla quis scelerisque sem, eget volutpat velit. Fusce eget
+accumsan sapien, nec feugiat quam. Quisque non risus.
+placerat lacus vitae, lacinia nisi. Sed metus arcu, iaculis
+sit amet cursus nec, sodales at eros.`;
+
+function loadImage(src) {
+ const img = new Image();
+ img.src = src;
+
+ return new Ember.RSVP.Promise(resolve => img.onload = () => resolve(img));
+};
+
+function parseColor(color) {
+ const m = color.match(/^#([0-9a-f]{6})$/i);
+ if (m) {
+ const c = m[1];
+ return [ parseInt(c.substr(0,2),16), parseInt(c.substr(2,2),16), parseInt(c.substr(4,2),16) ];
+ }
+
+ return [0, 0, 0];
+}
+
+function brightness(color) {
+ return (color[0] * 0.299) + (color[1] * 0.587) + (color[2] * 0.114);
+}
+
+function lighten(color, percent) {
+ return '#' +
+ ((0|(1<<8) + color[0] + (256 - color[0]) * percent / 100).toString(16)).substr(1) +
+ ((0|(1<<8) + color[1] + (256 - color[1]) * percent / 100).toString(16)).substr(1) +
+ ((0|(1<<8) + color[2] + (256 - color[2]) * percent / 100).toString(16)).substr(1);
+}
+
+function chooseBrighter(primary, secondary) {
+ const primaryCol = parseColor(primary);
+ const secondaryCol = parseColor(secondary);
+
+ return brightness(primaryCol) < brightness(secondaryCol) ? secondary : primary;
+}
+
+function darkLightDiff(adjusted, comparison, lightness, darkness) {
+ const adjustedCol = parseColor(adjusted);
+ const comparisonCol = parseColor(comparison);
+ return lighten(adjustedCol, (brightness(adjustedCol) < brightness(comparisonCol)) ?
+ lightness : darkness);
+}
+
+export default Ember.Component.extend({
+ ctx: null,
+ width: WIDTH,
+ height: HEIGHT,
+ loaded: false,
+ logo: null,
+
+ colorScheme: Ember.computed.alias('step.fieldsById.color_scheme.value'),
+
+ didInsertElement() {
+ this._super();
+ const c = this.$('canvas')[0];
+ this.ctx = c.getContext("2d");
+
+ Ember.RSVP.Promise.all([loadImage('/images/wizard/discourse-small.png'),
+ loadImage('/images/wizard/trout.png')]).then(result => {
+ this.logo = result[0];
+ this.avatar = result[1];
+ this.loaded = true;
+ this.triggerRepaint();
+ });
+ },
+
+ @observes('colorScheme')
+ triggerRepaint() {
+ Ember.run.scheduleOnce('afterRender', this, 'repaint');
+ },
+
+ repaint() {
+ if (!this.loaded) { return; }
+
+ const { ctx } = this;
+ const headerHeight = HEIGHT * 0.15;
+
+ const colorScheme = this.get('colorScheme');
+ const options = this.get('step.fieldsById.color_scheme.options');
+ const option = options.findProperty('id', colorScheme);
+ if (!option) { return; }
+
+ const colors = option.data.colors;
+ if (!colors) { return; }
+
+ ctx.fillStyle = colors.secondary;
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
+
+ // Header area
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(0, 0, WIDTH, headerHeight);
+ ctx.fillStyle = colors.header_background;
+ ctx.shadowColor = "rgba(0, 0, 0, 0.25)";
+ ctx.shadowBlur = 2;
+ ctx.shadowOffsetX = 0;
+ ctx.shadowOffsetY = 2;
+ ctx.fill();
+ ctx.restore();
+
+ const margin = WIDTH * 0.02;
+ const avatarSize = HEIGHT * 0.1;
+
+ // Logo
+ const headerMargin = headerHeight * 0.2;
+ const logoHeight = headerHeight - (headerMargin * 2);
+ const logoWidth = (logoHeight / this.logo.height) * this.logo.width;
+ ctx.drawImage(this.logo, headerMargin, headerMargin, logoWidth, logoHeight);
+
+ // Top right menu
+ ctx.drawImage(this.avatar, WIDTH - avatarSize - headerMargin, headerMargin, avatarSize, avatarSize);
+ ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 45, 55);
+ ctx.font = "0.75em FontAwesome";
+ ctx.fillText("\uf0c9", WIDTH - (avatarSize * 2) - (headerMargin * 0.5), avatarSize);
+ ctx.fillText("\uf002", WIDTH - (avatarSize * 3) - (headerMargin * 0.5), avatarSize);
+
+ // Draw a fake topic
+ ctx.drawImage(this.avatar, margin, headerHeight + (HEIGHT * 0.17), avatarSize, avatarSize);
+
+ ctx.beginPath();
+ ctx.fillStyle = colors.primary;
+ ctx.font = "bold 0.75em 'Arial'";
+ ctx.fillText("Welcome to Discourse", margin, (HEIGHT * 0.25));
+
+ ctx.font = "0.5em 'Arial'";
+
+ let line = 0;
+
+ const lines = LOREM.split("\n");
+ for (let i=0; i<10; i++) {
+ line = (HEIGHT * 0.3) + (i * LINE_HEIGHT);
+ ctx.fillText(lines[i], margin + avatarSize + margin, line);
+ }
+
+ // Reply Button
+ ctx.beginPath();
+ ctx.rect(WIDTH * 0.57, line + LINE_HEIGHT, WIDTH * 0.1, HEIGHT * 0.07);
+ ctx.fillStyle = colors.tertiary;
+ ctx.fill();
+ ctx.fillStyle = chooseBrighter(colors.primary, colors.secondary);
+ ctx.font = "8px 'Arial'";
+ ctx.fillText("Reply", WIDTH * 0.595, line + (LINE_HEIGHT * 1.8));
+
+ // Icons
+ ctx.font = "0.5em FontAwesome";
+ ctx.fillStyle = colors.love;
+ ctx.fillText("\uf004", WIDTH * 0.48, line + (LINE_HEIGHT * 1.8));
+ ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 65, 55);
+ ctx.fillText("\uf040", WIDTH * 0.525, line + (LINE_HEIGHT * 1.8));
+
+ // Draw Timeline
+ const timelineX = WIDTH * 0.8;
+ ctx.beginPath();
+ ctx.strokeStyle = colors.tertiary;
+ ctx.lineWidth = 0.5;
+ ctx.moveTo(timelineX, HEIGHT * 0.3);
+ ctx.lineTo(timelineX, HEIGHT * 0.6);
+ ctx.stroke();
+
+ // Timeline
+ ctx.beginPath();
+ ctx.strokeStyle = colors.tertiary;
+ ctx.lineWidth = 2;
+ ctx.moveTo(timelineX, HEIGHT * 0.3);
+ ctx.lineTo(timelineX, HEIGHT * 0.4);
+ ctx.stroke();
+
+ ctx.font = "Bold 0.5em Arial";
+ ctx.fillStyle = colors.primary;
+ ctx.fillText("1 / 20", timelineX + margin, (HEIGHT * 0.3) + (margin * 1.5));
+
+ // draw border
+ ctx.beginPath();
+ ctx.strokeStyle='rgba(0, 0, 0, 0.2)';
+ ctx.rect(0, 0, WIDTH, HEIGHT);
+ ctx.stroke();
+ }
+
+});
diff --git a/app/assets/javascripts/wizard/components/wizard-field.js.es6 b/app/assets/javascripts/wizard/components/wizard-field.js.es6
index cb4c67de9d0..67b65ee47b7 100644
--- a/app/assets/javascripts/wizard/components/wizard-field.js.es6
+++ b/app/assets/javascripts/wizard/components/wizard-field.js.es6
@@ -6,6 +6,9 @@ export default Ember.Component.extend({
@computed('field.id')
inputClassName: id => `field-${Ember.String.dasherize(id)}`,
- @computed('field.type')
- inputComponentName: type => `wizard-field-${type}`
+ @computed('field.type', 'field.id')
+ inputComponentName(type, id) {
+ return (type === 'component') ? Ember.String.dasherize(id) : `wizard-field-${type}`;
+ }
+
});
diff --git a/app/assets/javascripts/wizard/models/step.js.es6 b/app/assets/javascripts/wizard/models/step.js.es6
index 5fa3b61becb..a2ba11408e5 100644
--- a/app/assets/javascripts/wizard/models/step.js.es6
+++ b/app/assets/javascripts/wizard/models/step.js.es6
@@ -8,6 +8,13 @@ export default Ember.Object.extend(ValidState, {
@computed('index')
displayIndex: index => index + 1,
+ @computed('fields.[]')
+ fieldsById(fields) {
+ const lookup = {};
+ fields.forEach(field => lookup[field.get('id')] = field);
+ return lookup;
+ },
+
checkFields() {
let allValid = true;
this.get('fields').forEach(field => {
diff --git a/app/assets/javascripts/wizard/templates/components/scheme-preview.hbs b/app/assets/javascripts/wizard/templates/components/scheme-preview.hbs
new file mode 100644
index 00000000000..a4950856af0
--- /dev/null
+++ b/app/assets/javascripts/wizard/templates/components/scheme-preview.hbs
@@ -0,0 +1,4 @@
+
+
+
diff --git a/app/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs b/app/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs
index 8339c52697f..4f843aa3edd 100644
--- a/app/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs
+++ b/app/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs
@@ -1 +1 @@
-{{combo-box value=field.value content=field.options nameProperty="label" width="400px"}}
+{{combo-box class=inputClassName value=field.value content=field.options nameProperty="label" width="400px"}}
diff --git a/app/assets/javascripts/wizard/templates/components/wizard-field.hbs b/app/assets/javascripts/wizard/templates/components/wizard-field.hbs
index d757791f83d..973630851f0 100644
--- a/app/assets/javascripts/wizard/templates/components/wizard-field.hbs
+++ b/app/assets/javascripts/wizard/templates/components/wizard-field.hbs
@@ -2,7 +2,7 @@
{{field.label}}
- {{component inputComponentName field=field inputClassName=inputClassName}}
+ {{component inputComponentName field=field step=step inputClassName=inputClassName}}
{{#if field.errorDescription}}
diff --git a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs
index ff6a901208b..52d174d0627 100644
--- a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs
+++ b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs
@@ -8,7 +8,7 @@
{{#wizard-step-form step=step}}
{{#each step.fields as |field|}}
- {{wizard-field field=field}}
+ {{wizard-field field=field step=step}}
{{/each}}
{{/wizard-step-form}}
diff --git a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6
index 345f613563c..af59f65dd79 100644
--- a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6
+++ b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6
@@ -8,7 +8,7 @@ test("Wizard starts", assert => {
});
});
-test("Forum Name Step", assert => {
+test("Going back and forth in steps", assert => {
visit("/step/hello-world");
andThen(() => {
assert.ok(exists('.wizard-step'));
@@ -44,7 +44,10 @@ test("Forum Name Step", assert => {
assert.ok(!exists('.wizard-field .field-error-description'));
assert.ok(!exists('.wizard-step-title'));
assert.ok(!exists('.wizard-step-description'));
- assert.ok(exists('input.field-email'), "went to the next step");
+
+ assert.ok(exists('select.field-snack'), "went to the next step");
+ assert.ok(exists('.preview-area'), "renders the component field");
+
assert.ok(!exists('.wizard-btn.next'));
assert.ok(exists('.wizard-btn.done'), 'last step shows a done button');
assert.ok(exists('.wizard-btn.back'), 'shows the back button');
diff --git a/app/assets/javascripts/wizard/test/test_helper.js b/app/assets/javascripts/wizard/test/test_helper.js
index 17f51e38b00..caf53122b51 100644
--- a/app/assets/javascripts/wizard/test/test_helper.js
+++ b/app/assets/javascripts/wizard/test/test_helper.js
@@ -10,6 +10,7 @@
//= require ember-qunit
//= require ember-shim
//= require wizard-application
+//= require wizard-vendor
//= require helpers/assertions
//= require_tree ./acceptance
//= require_tree ./models
diff --git a/app/assets/javascripts/wizard/test/wizard-pretender.js.es6 b/app/assets/javascripts/wizard/test/wizard-pretender.js.es6
index 6e0808fc674..546f61fd7d9 100644
--- a/app/assets/javascripts/wizard/test/wizard-pretender.js.es6
+++ b/app/assets/javascripts/wizard/test/wizard-pretender.js.es6
@@ -49,7 +49,10 @@ export default function() {
{
id: 'second-step',
index: 1,
- fields: [{ id: 'email', type: 'text', required: true }],
+ fields: [
+ { id: 'snack', type: 'dropdown', required: true },
+ { id: 'scheme-preview', type: 'component' }
+ ],
previous: 'hello-world'
}]
}
diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss
index dfffa573b76..cfb048e5b6c 100644
--- a/app/assets/stylesheets/wizard.scss
+++ b/app/assets/stylesheets/wizard.scss
@@ -16,6 +16,9 @@ body.wizard {
.select {
width: 400px;
}
+.select2-results .select2-highlighted {
+ background: #ff9;
+}
.wizard-column {
background-color: white;
diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb
index 0c1867f45e7..11c022583b7 100644
--- a/app/models/color_scheme.rb
+++ b/app/models/color_scheme.rb
@@ -3,6 +3,32 @@ require_dependency 'distributed_cache'
class ColorScheme < ActiveRecord::Base
+ def self.themes
+ base_with_hash = {}
+ base_colors.each do |name, color|
+ base_with_hash[name] = "##{color}"
+ end
+
+ [
+ { id: 'default', colors: base_with_hash },
+ {
+ id: 'dark',
+ colors: {
+ "primary" => '#dddddd',
+ "secondary" => '#222222',
+ "tertiary" => '#0f82af',
+ "quaternary" => '#c14924',
+ "header_background" => '#111111',
+ "header_primary" => '#333333',
+ "highlight" => '#a87137',
+ "danger" => '#e45735',
+ "success" => '#1ca551',
+ "love" => '#fa6c8d'
+ }
+ }
+ ]
+ end
+
def self.hex_cache
@hex_cache ||= DistributedCache.new("scheme_hex_for_name")
end
@@ -30,7 +56,7 @@ class ColorScheme < ActiveRecord::Base
@mutex.synchronize do
return @base_colors if @base_colors
@base_colors = {}
- read_colors_file.each do |line|
+ File.readlines(BASE_COLORS_FILE).each do |line|
matches = /\$([\w]+):\s*#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})(?:[;]|\s)/.match(line.strip)
@base_colors[matches[1]] = matches[2] if matches
end
@@ -38,10 +64,6 @@ class ColorScheme < ActiveRecord::Base
@base_colors
end
- def self.read_colors_file
- File.readlines(BASE_COLORS_FILE)
- end
-
def self.enabled
current_version.find_by(enabled: true)
end
@@ -114,7 +136,6 @@ class ColorScheme < ActiveRecord::Base
DiscourseStylesheets.cache.clear
end
-
def dump_hex_cache
self.class.hex_cache.clear
end
diff --git a/app/serializers/wizard_field_serializer.rb b/app/serializers/wizard_field_serializer.rb
index 07babfed04d..7f69605eead 100644
--- a/app/serializers/wizard_field_serializer.rb
+++ b/app/serializers/wizard_field_serializer.rb
@@ -52,7 +52,17 @@ class WizardFieldSerializer < ApplicationSerializer
def options
object.options.map do |o|
- {id: o, label: I18n.t("#{i18n_key}.options.#{o}")}
+
+ result = {id: o, label: I18n.t("#{i18n_key}.options.#{o}")}
+
+ data = object.option_data[o]
+ if data.present?
+ as_json = data.dup
+ as_json.delete(:id)
+ result[:data] = as_json
+ end
+
+ result
end
end
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 3548ce27816..c3be14d8a97 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -3247,8 +3247,8 @@ en:
color_scheme:
label: "Color Scheme"
options:
- default: "Default Scheme"
- dark: "Dark Scheme"
+ default: "Simple"
+ dark: "Dark"
finished:
title: "Your Discourse Forum is Ready!"
diff --git a/db/migrate/20160906200439_add_via_wizard_to_color_schemes.rb b/db/migrate/20160906200439_add_via_wizard_to_color_schemes.rb
new file mode 100644
index 00000000000..40c69d41672
--- /dev/null
+++ b/db/migrate/20160906200439_add_via_wizard_to_color_schemes.rb
@@ -0,0 +1,5 @@
+class AddViaWizardToColorSchemes < ActiveRecord::Migration
+ def change
+ add_column :color_schemes, :via_wizard, :boolean, default: false, null: false
+ end
+end
diff --git a/lib/wizard.rb b/lib/wizard.rb
index a700f2457ad..7e8d46a00c4 100644
--- a/lib/wizard.rb
+++ b/lib/wizard.rb
@@ -44,8 +44,9 @@ class Wizard
theme = wizard.create_step('colors')
scheme = theme.add_field(id: 'color_scheme', type: 'dropdown', required: true)
- scheme.add_option('default')
- scheme.add_option('dark')
+ ColorScheme.themes.each {|t| scheme.add_option(t[:id], t) }
+
+ theme.add_field(id: 'scheme_preview', type: 'component')
wizard.append_step(theme)
finished = wizard.create_step('finished')
diff --git a/lib/wizard/field.rb b/lib/wizard/field.rb
index 83b6763bd4b..70afc2df88b 100644
--- a/lib/wizard/field.rb
+++ b/lib/wizard/field.rb
@@ -1,7 +1,7 @@
class Wizard
class Field
- attr_reader :id, :type, :required, :value, :options
+ attr_reader :id, :type, :required, :value, :options, :option_data
attr_accessor :step
def initialize(attrs)
@@ -12,10 +12,12 @@ class Wizard
@required = !!attrs[:required]
@value = attrs[:value]
@options = []
+ @option_data = {}
end
- def add_option(id)
+ def add_option(id, data=nil)
@options << id
+ @option_data[id] = data
end
end
diff --git a/lib/wizard/step_updater.rb b/lib/wizard/step_updater.rb
index aaf6208b4b2..543f55eff44 100644
--- a/lib/wizard/step_updater.rb
+++ b/lib/wizard/step_updater.rb
@@ -23,6 +23,34 @@ class Wizard
update_setting(:site_contact_username, fields, :site_contact_username)
end
+ def update_colors(fields)
+ scheme_name = fields[:color_scheme]
+
+ theme = ColorScheme.themes.find {|s| s[:id] == scheme_name }
+
+ colors = []
+ theme[:colors].each do |name, hex|
+ colors << {name: name, hex: hex[1..-1] }
+ end
+
+ attrs = {
+ enabled: true,
+ name: I18n.t("wizard.step.colors.fields.color_scheme.options.#{scheme_name}"),
+ colors: colors
+ }
+
+ scheme = ColorScheme.where(via_wizard: true).first
+ if scheme.present?
+ attrs[:colors] = colors
+ revisor = ColorSchemeRevisor.new(scheme, attrs)
+ revisor.revise
+ else
+ attrs[:via_wizard] = true
+ scheme = ColorScheme.new(attrs)
+ scheme.save!
+ end
+ end
+
def success?
@errors.blank?
end
diff --git a/public/images/wizard/discourse-small.png b/public/images/wizard/discourse-small.png
new file mode 100644
index 00000000000..9fc9748d9ff
Binary files /dev/null and b/public/images/wizard/discourse-small.png differ
diff --git a/public/images/wizard/trout.png b/public/images/wizard/trout.png
new file mode 100644
index 00000000000..5af72ee4258
Binary files /dev/null and b/public/images/wizard/trout.png differ
diff --git a/spec/components/step_updater_spec.rb b/spec/components/step_updater_spec.rb
index df982600466..336c9fe3336 100644
--- a/spec/components/step_updater_spec.rb
+++ b/spec/components/step_updater_spec.rb
@@ -21,7 +21,7 @@ describe Wizard::StepUpdater do
contact_url: 'http://example.com/custom-contact-url',
site_contact_username: user.username)
- expect(updater.success?).to eq(true)
+ expect(updater).to be_success
expect(SiteSetting.contact_email).to eq("eviltrout@example.com")
expect(SiteSetting.contact_url).to eq("http://example.com/custom-contact-url")
expect(SiteSetting.site_contact_username).to eq(user.username)
@@ -30,9 +30,39 @@ describe Wizard::StepUpdater do
it "doesn't update when there are errors" do
updater.update(contact_email: 'not-an-email',
site_contact_username: 'not-a-username')
- expect(updater.success?).to eq(false)
+ expect(updater).to be_success
expect(updater.errors).to be_present
end
end
+ context "colors step" do
+ let(:updater) { Wizard::StepUpdater.new(user, 'colors') }
+
+ context "with an existing color scheme" do
+ let!(:color_scheme) { Fabricate(:color_scheme, name: 'existing', via_wizard: true) }
+
+ it "updates the scheme" do
+ updater.update(color_scheme: 'dark')
+ expect(updater.success?).to eq(true)
+
+ color_scheme.reload
+ expect(color_scheme).to be_enabled
+
+ end
+ end
+
+ context "without an existing scheme" do
+
+ it "creates the scheme" do
+ updater.update(color_scheme: 'dark')
+ expect(updater.success?).to eq(true)
+
+ color_scheme = ColorScheme.where(via_wizard: true).first
+ expect(color_scheme).to be_present
+ expect(color_scheme).to be_enabled
+ expect(color_scheme.colors).to be_present
+ end
+ end
+ end
+
end