mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: Multiple SCSS file support for themes (#7351)
Theme developers can include any number of scss files within the /scss/ directory of a theme. These can then be imported from the main common/desktop/mobile scss.
This commit is contained in:
@@ -59,4 +59,52 @@ describe Stylesheet::Importer do
|
||||
|
||||
end
|
||||
|
||||
context "extra_scss" do
|
||||
let(:scss) { "body { background: red}" }
|
||||
let(:theme) { Fabricate(:theme).tap { |t|
|
||||
t.set_field(target: :extra_scss, name: "my_files/magic", value: scss)
|
||||
t.save!
|
||||
}}
|
||||
|
||||
let(:importer) { described_class.new(theme: theme) }
|
||||
|
||||
it "should be able to import correctly" do
|
||||
# Import from regular theme file
|
||||
expect(
|
||||
importer.imports(
|
||||
"my_files/magic",
|
||||
"theme_#{theme.id}/desktop-scss-mytheme.scss"
|
||||
).source).to eq(scss)
|
||||
|
||||
# Import from some deep file
|
||||
expect(
|
||||
importer.imports(
|
||||
"my_files/magic",
|
||||
"theme_#{theme.id}/some/deep/folder/structure/myfile.scss"
|
||||
).source).to eq(scss)
|
||||
|
||||
# Import from parent dir
|
||||
expect(
|
||||
importer.imports(
|
||||
"../../my_files/magic",
|
||||
"theme_#{theme.id}/my_files/folder1/myfile.scss"
|
||||
).source).to eq(scss)
|
||||
|
||||
# Import from same dir without ./
|
||||
expect(
|
||||
importer.imports(
|
||||
"magic",
|
||||
"theme_#{theme.id}/my_files/myfile.scss"
|
||||
).source).to eq(scss)
|
||||
|
||||
# Import from same dir with ./
|
||||
expect(
|
||||
importer.imports(
|
||||
"./magic",
|
||||
"theme_#{theme.id}/my_files/myfile.scss"
|
||||
).source).to eq(scss)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -73,19 +73,23 @@ describe SvgSprite do
|
||||
|
||||
# Works when applying override
|
||||
theme.update_setting(:custom_icon, "gas-pump")
|
||||
theme.save!
|
||||
expect(SvgSprite.all_icons([theme.id])).to include("gas-pump")
|
||||
|
||||
# Works when changing override
|
||||
theme.update_setting(:custom_icon, "gamepad")
|
||||
theme.save!
|
||||
expect(SvgSprite.all_icons([theme.id])).to include("gamepad")
|
||||
expect(SvgSprite.all_icons([theme.id])).not_to include("gas-pump")
|
||||
|
||||
# FA5 syntax
|
||||
theme.update_setting(:custom_icon, "fab fa-bandcamp")
|
||||
theme.save!
|
||||
expect(SvgSprite.all_icons([theme.id])).to include("fab-bandcamp")
|
||||
|
||||
# Internal Discourse syntax + multiple icons
|
||||
theme.update_setting(:custom_icon, "fab-android|dragon")
|
||||
theme.save!
|
||||
expect(SvgSprite.all_icons([theme.id])).to include("fab-android")
|
||||
expect(SvgSprite.all_icons([theme.id])).to include("dragon")
|
||||
|
||||
@@ -94,6 +98,7 @@ describe SvgSprite do
|
||||
|
||||
# Check components are included
|
||||
theme.update(component: true)
|
||||
theme.save!
|
||||
parent_theme = Fabricate(:theme)
|
||||
parent_theme.add_child_theme!(theme)
|
||||
expect(SvgSprite.all_icons([parent_theme.id])).to include("dragon")
|
||||
|
||||
@@ -108,7 +108,7 @@ describe ThemeStore::TgzExporter do
|
||||
# but protection is in place 'just in case'
|
||||
expect do
|
||||
theme.set_field(target: :translations, name: "en", value: "hacked")
|
||||
theme.theme_fields[0].stubs(:file_path).returns("../../malicious")
|
||||
ThemeField.any_instance.stubs(:file_path).returns("../../malicious")
|
||||
theme.save!
|
||||
package
|
||||
end.to raise_error(RuntimeError)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Jobs::RebakeAllHtmlThemeFields do
|
||||
let(:theme) { Fabricate(:theme) }
|
||||
let(:theme_field) { ThemeField.create!(theme: theme, target_id: 0, name: "header", value: "<script>console.log(123)</script>") }
|
||||
|
||||
it 'extracts inline javascripts' do
|
||||
theme_field.update_attributes(value_baked: 'need to be rebaked')
|
||||
|
||||
described_class.new.execute_onceoff({})
|
||||
|
||||
theme_field.reload
|
||||
expect(theme_field.value_baked).to include('theme-javascripts')
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ describe RemoteTheme do
|
||||
`cd #{repo_dir} && git init . `
|
||||
`cd #{repo_dir} && git config user.email 'someone@cool.com'`
|
||||
`cd #{repo_dir} && git config user.name 'The Cool One'`
|
||||
`cd #{repo_dir} && mkdir desktop mobile common assets locales`
|
||||
`cd #{repo_dir} && mkdir desktop mobile common assets locales scss`
|
||||
files.each do |name, data|
|
||||
File.write("#{repo_dir}/#{name}", data)
|
||||
`cd #{repo_dir} && git add #{name}`
|
||||
@@ -46,6 +46,7 @@ describe RemoteTheme do
|
||||
setup_git_repo(
|
||||
"about.json" => about_json,
|
||||
"desktop/desktop.scss" => scss_data,
|
||||
"scss/file.scss" => ".class1{color:red}",
|
||||
"common/header.html" => "I AM HEADER",
|
||||
"common/random.html" => "I AM SILLY",
|
||||
"common/embedded.scss" => "EMBED",
|
||||
@@ -77,7 +78,7 @@ describe RemoteTheme do
|
||||
expect(remote.theme_version).to eq("1.0")
|
||||
expect(remote.minimum_discourse_version).to eq("1.0.0")
|
||||
|
||||
expect(@theme.theme_fields.length).to eq(6)
|
||||
expect(@theme.theme_fields.length).to eq(7)
|
||||
|
||||
mapped = Hash[*@theme.theme_fields.map { |f| ["#{f.target_id}-#{f.name}", f.value] }.flatten]
|
||||
|
||||
@@ -91,7 +92,7 @@ describe RemoteTheme do
|
||||
|
||||
expect(mapped["4-en"]).to eq("sometranslations")
|
||||
|
||||
expect(mapped.length).to eq(6)
|
||||
expect(mapped.length).to eq(7)
|
||||
|
||||
expect(@theme.settings.length).to eq(1)
|
||||
expect(@theme.settings.first.value).to eq(true)
|
||||
@@ -112,6 +113,8 @@ describe RemoteTheme do
|
||||
`cd #{initial_repo} && git add settings.yml`
|
||||
|
||||
File.delete("#{initial_repo}/settings.yaml")
|
||||
File.delete("#{initial_repo}/scss/file.scss")
|
||||
|
||||
`cd #{initial_repo} && git commit -am "update"`
|
||||
|
||||
time = Time.new('2001')
|
||||
@@ -122,7 +125,7 @@ describe RemoteTheme do
|
||||
expect(remote.remote_version).to eq(`cd #{initial_repo} && git rev-parse HEAD`.strip)
|
||||
|
||||
remote.update_from_remote
|
||||
@theme.save
|
||||
@theme.save!
|
||||
@theme.reload
|
||||
|
||||
scheme = ColorScheme.find_by(theme_id: @theme.id)
|
||||
@@ -132,6 +135,9 @@ describe RemoteTheme do
|
||||
|
||||
mapped = Hash[*@theme.theme_fields.map { |f| ["#{f.target_id}-#{f.name}", f.value] }.flatten]
|
||||
|
||||
# Scss file was deleted
|
||||
expect(mapped["5-file"]).to eq(nil)
|
||||
|
||||
expect(mapped["0-header"]).to eq("I AM UPDATED")
|
||||
expect(mapped["1-scss"]).to eq(scss_data)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ describe ThemeField do
|
||||
|
||||
it 'does not insert a script tag when there are no inline script' do
|
||||
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "body_tag", value: '<div>new div</div>')
|
||||
theme_field.ensure_baked!
|
||||
expect(theme_field.value_baked).to_not include('<script')
|
||||
end
|
||||
|
||||
@@ -53,7 +54,7 @@ describe ThemeField do
|
||||
HTML
|
||||
|
||||
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html)
|
||||
|
||||
theme_field.ensure_baked!
|
||||
expect(theme_field.value_baked).to include("<script src=\"#{theme_field.javascript_cache.url}\"></script>")
|
||||
expect(theme_field.value_baked).to include("external-script.js")
|
||||
expect(theme_field.value_baked).to include('<script type="text/template"')
|
||||
@@ -75,7 +76,7 @@ describe ThemeField do
|
||||
JavaScript
|
||||
|
||||
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html)
|
||||
|
||||
theme_field.ensure_baked!
|
||||
expect(theme_field.javascript_cache.content).to include(extracted)
|
||||
end
|
||||
|
||||
@@ -87,11 +88,13 @@ describe ThemeField do
|
||||
HTML
|
||||
|
||||
field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html)
|
||||
field.ensure_baked!
|
||||
expect(field.error).not_to eq(nil)
|
||||
expect(field.value_baked).to include("<script src=\"#{field.javascript_cache.url}\"></script>")
|
||||
expect(field.javascript_cache.content).to include("Theme Transpilation Error:")
|
||||
|
||||
field.update!(value: '')
|
||||
field.ensure_baked!
|
||||
expect(field.error).to eq(nil)
|
||||
end
|
||||
|
||||
@@ -102,8 +105,9 @@ HTML
|
||||
</script>
|
||||
HTML
|
||||
|
||||
ThemeField.create!(theme_id: 1, target_id: 3, name: "yaml", value: "string_setting: \"test text \\\" 123!\"")
|
||||
ThemeField.create!(theme_id: 1, target_id: 3, name: "yaml", value: "string_setting: \"test text \\\" 123!\"").ensure_baked!
|
||||
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "head_tag", value: html)
|
||||
theme_field.ensure_baked!
|
||||
javascript_cache = theme_field.javascript_cache
|
||||
|
||||
expect(theme_field.value_baked).to include("<script src=\"#{javascript_cache.url}\"></script>")
|
||||
@@ -115,15 +119,33 @@ HTML
|
||||
it "correctly generates errors for transpiled css" do
|
||||
css = "body {"
|
||||
field = ThemeField.create!(theme_id: 1, target_id: 0, name: "scss", value: css)
|
||||
field.reload
|
||||
field.ensure_baked!
|
||||
expect(field.error).not_to eq(nil)
|
||||
field.value = "body {color: blue};"
|
||||
field.save!
|
||||
field.reload
|
||||
field.ensure_baked!
|
||||
|
||||
expect(field.error).to eq(nil)
|
||||
end
|
||||
|
||||
it "allows importing scss files" do
|
||||
theme = Fabricate(:theme)
|
||||
main_field = theme.set_field(target: :common, name: :scss, value: ".class1{color: red}\n@import 'rootfile1';")
|
||||
theme.set_field(target: :extra_scss, name: "rootfile1", value: ".class2{color:green}\n@import 'foldername/subfile1';")
|
||||
theme.set_field(target: :extra_scss, name: "rootfile2", value: ".class3{color:green} ")
|
||||
theme.set_field(target: :extra_scss, name: "foldername/subfile1", value: ".class4{color:yellow}\n@import 'subfile2';")
|
||||
theme.set_field(target: :extra_scss, name: "foldername/subfile2", value: ".class5{color:yellow}\n@import '../rootfile2';")
|
||||
|
||||
theme.save!
|
||||
result = main_field.compile_scss[0]
|
||||
|
||||
expect(result).to include(".class1")
|
||||
expect(result).to include(".class2")
|
||||
expect(result).to include(".class3")
|
||||
expect(result).to include(".class4")
|
||||
expect(result).to include(".class5")
|
||||
end
|
||||
|
||||
def create_upload_theme_field!(name)
|
||||
ThemeField.create!(
|
||||
theme_id: 1,
|
||||
@@ -131,7 +153,7 @@ HTML
|
||||
value: "",
|
||||
type_id: ThemeField.types[:theme_upload_var],
|
||||
name: name,
|
||||
)
|
||||
).tap { |tf| tf.ensure_baked! }
|
||||
end
|
||||
|
||||
it "ensures we don't use invalid SCSS variable names" do
|
||||
@@ -145,7 +167,7 @@ HTML
|
||||
|
||||
def create_yaml_field(value)
|
||||
field = ThemeField.create!(theme_id: 1, target_id: Theme.targets[:settings], name: "yaml", value: value)
|
||||
field.reload
|
||||
field.ensure_baked!
|
||||
field
|
||||
end
|
||||
|
||||
@@ -179,7 +201,7 @@ HTML
|
||||
|
||||
field.value = "valid_setting: true"
|
||||
field.save!
|
||||
field.reload
|
||||
field.ensure_baked!
|
||||
expect(field.error).to eq(nil)
|
||||
end
|
||||
|
||||
@@ -319,6 +341,7 @@ HTML
|
||||
HTML
|
||||
|
||||
theme_field = ThemeField.create!(theme_id: theme.id, target_id: 0, name: "head_tag", value: html)
|
||||
theme_field.ensure_baked!
|
||||
javascript_cache = theme_field.javascript_cache
|
||||
expect(javascript_cache.content).to include("inline discourse plugin")
|
||||
expect(javascript_cache.content).to include("theme_translations.#{theme.id}.")
|
||||
|
||||
@@ -238,6 +238,7 @@ HTML
|
||||
context "plugin api" do
|
||||
def transpile(html)
|
||||
f = ThemeField.create!(target_id: Theme.targets[:mobile], theme_id: 1, name: "after_header", value: html)
|
||||
f.ensure_baked!
|
||||
return f.value_baked, f.javascript_cache
|
||||
end
|
||||
|
||||
@@ -307,6 +308,7 @@ HTML
|
||||
|
||||
setting = theme.settings.find { |s| s.name == :font_size }
|
||||
setting.value = '30px'
|
||||
theme.save!
|
||||
|
||||
scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id)
|
||||
expect(scss).to include("font-size:30px")
|
||||
@@ -356,6 +358,7 @@ HTML
|
||||
|
||||
setting = theme.settings.find { |s| s.name == :name }
|
||||
setting.value = 'bill'
|
||||
theme.save!
|
||||
|
||||
transpiled = <<~HTML
|
||||
(function() {
|
||||
@@ -428,6 +431,7 @@ HTML
|
||||
expect(user_themes).to eq([])
|
||||
|
||||
theme = Fabricate(:theme, name: "bob", user_selectable: true)
|
||||
theme.save!
|
||||
|
||||
json = Site.json_for(guardian)
|
||||
user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] }
|
||||
@@ -485,6 +489,7 @@ HTML
|
||||
expect(cached_settings(theme.id)).to match(/\"boolean_setting\":true/)
|
||||
|
||||
theme.settings.first.value = "false"
|
||||
theme.save!
|
||||
expect(cached_settings(theme.id)).to match(/\"boolean_setting\":false/)
|
||||
|
||||
child.set_field(target: :settings, name: "yaml", value: "integer_setting: 54")
|
||||
|
||||
Reference in New Issue
Block a user