diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b501ea9a58..d08f0d19f3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,5 @@ +redmine.orgのチケットURL: https://www.redmine.org/issues/xxxx -____________________________________________________________________ - -Your contributions to Redmine are welcome! - -Please **open an issue on the [official website]** instead of sending pull requests. - -Since the development of Redmine is not conducted on GitHub but on the [official website] and core developers are not monitoring the GitHub repo, pull requests might not get reviewed. - -For more detail about how to contribute, please see the wiki page [Contribute] on the [official website]. - -[official website]: https://www.redmine.org/ -[Contribute]: https://www.redmine.org/projects/redmine/wiki/Contribute +TODO: +- [ ] 単体テストかく +- [ ] ... diff --git a/.github/actions/comment_patch_url.rb b/.github/actions/comment_patch_url.rb new file mode 100755 index 0000000000..6483c20e59 --- /dev/null +++ b/.github/actions/comment_patch_url.rb @@ -0,0 +1,76 @@ +#!/usr/bin/env ruby + +require 'json' + +require 'faraday' + +REPO = 'redmine-patch-meetup/redmine-dev-mirror' + +WORKFLOW_RUN = JSON.parse ENV['WORKFLOW_RUN_JSON'] + +CONNECTION = Faraday.new('https://api.github.com/') do |conn| + conn.response :raise_error + conn.adapter Faraday.default_adapter +end + +def repo_resource(resource) + "repos/#{REPO}/#{resource}" +end + +def get_repo_resource(resource) + response = CONNECTION.get repo_resource(resource) + JSON.parse response.body +end + +def post_to_repo_resource(resource, body) + response = CONNECTION.post repo_resource(resource), + body.to_json, + "Content-Type" => "application/json", + "Authorization" => "token #{ENV['GITHUB_TOKEN']}" + JSON.parse response.body +end + +def patch_artifact_id + response = JSON.parse CONNECTION.get(WORKFLOW_RUN['artifacts_url']).body + patch_artifact = response['artifacts'].find { |artifact| artifact['name'] == 'patch' } + patch_artifact['id'] +end + +def get_suite_id + suite_url = WORKFLOW_RUN['check_suite_url'] + id_start_index = suite_url.rindex('/') + 1 + suite_url[id_start_index..-1] +end + +def patch_artifact_download_url + "https://github.com/#{REPO}/suites/#{get_suite_id}/artifacts/#{patch_artifact_id}" +end + +def pull_request_number + WORKFLOW_RUN.dig('pull_requests', 0, 'number') +end + +def post_pr_comment(pr_number, comment) + post_to_repo_resource "issues/#{pr_number}/comments", { body: comment } +end + +def find_previous_comment_id(pr_number) + comments = get_repo_resource "issues/#{pr_number}/comments" + previous_comment = comments.find { |comment| + comment['body'].include?('Patch can be downloaded [here]') && comment['user']['login'].include?('github-actions') + } + previous_comment['id'] if previous_comment +end + +def delete_comment(comment_id) + CONNECTION.delete repo_resource("issues/comments/#{comment_id}"), nil, "Authorization" => "token #{ENV['GITHUB_TOKEN']}" +end + +def main + existing_comment_id = find_previous_comment_id(pull_request_number) + delete_comment(existing_comment_id) if existing_comment_id + + post_pr_comment pull_request_number, "Patch can be downloaded [here](#{patch_artifact_download_url})" if pull_request_number +end + +main if __FILE__ == $0 diff --git a/.github/actions/test-with-db.sh b/.github/actions/test-with-db.sh new file mode 100755 index 0000000000..2aac7b33a7 --- /dev/null +++ b/.github/actions/test-with-db.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -x + +database=$1 + +cp ./config/database.$database.yml ./config/database.yml +bundle install --path vendor/bundle --without minimagick +bundle update +bundle exec rake db:create db:migrate +bundle exec rake test diff --git a/.github/workflows/comment-patch.yml b/.github/workflows/comment-patch.yml new file mode 100644 index 0000000000..0bdcb1d602 --- /dev/null +++ b/.github/workflows/comment-patch.yml @@ -0,0 +1,24 @@ +name: Comment Patch + +on: + workflow_run: + workflows: + - Create Patch + types: + - completed + branches-ignore: + - master + - develop + +jobs: + create-patch: + runs-on: ubuntu-latest + steps: + - name: Install gems + run: sudo gem install faraday + - uses: actions/checkout@v2 + - name: Comment Patch URL + run: ./.github/actions/comment_patch_url.rb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WORKFLOW_RUN_JSON: ${{ toJSON(github.event.workflow_run) }} diff --git a/.github/workflows/create-patch.yml b/.github/workflows/create-patch.yml new file mode 100644 index 0000000000..87acdd8e8c --- /dev/null +++ b/.github/workflows/create-patch.yml @@ -0,0 +1,35 @@ +name: Create Patch + +on: + push: + branches-ignore: + - master + - develop + +jobs: + create-patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Create Patch + run: | + git config user.email "$AUTHOR_EMAIL" + git config user.name "$AUTHOR_NAME" + git rev-parse --abbrev-ref HEAD > .branch-name + git checkout develop + git checkout -b patch + git merge --squash `cat .branch-name` + git commit -m "Patch for `cat .branch-name`" + PATCH_FILE=`cat .branch-name | sed 's/\//_/g'`.patch # Replace forward slash in branch name with _ + git format-patch develop..HEAD --stdout -k > $PATCH_FILE + echo "::set-output name=PATCH_FILE::$PATCH_FILE" + id: patch-creator + env: + AUTHOR_EMAIL: ${{ github.event.head_commit.author.email }} + AUTHOR_NAME: ${{ github.event.head_commit.author.name }} + - uses: actions/upload-artifact@v2 + with: + name: patch + path: ${{ steps.patch-creator.outputs.PATCH_FILE }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..58e4270fc6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,93 @@ +name: Test + +on: + push: + branches-ignore: + - master + - develop + +jobs: + test-with-mysql: + strategy: + fail-fast: false + matrix: + ruby: [2.5, 2.6, 2.7] + db_version: [5.7] + runs-on: ubuntu-latest + container: + image: ruby:${{ matrix.ruby }} + services: + db: + image: mysql:${{ matrix.db_version }} + env: + MYSQL_ROOT_PASSWORD: password + ports: + - 3306:3306 + steps: + - uses: actions/checkout@v2 + - name: Cache gems + uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ matrix.ruby }}-mysql-${{ hashFiles('**/Gemfile') }} + restore-keys: | + ${{ matrix.ruby }}-mysql- + ${{ matrix.ruby }}- + - name: Install & run tests + run: ./.github/actions/test-with-db.sh mysql + env: + DB_HOST: db + test-with-postgres: + strategy: + fail-fast: false + matrix: + ruby: [2.5, 2.6, 2.7] + db_version: [9.5] + runs-on: ubuntu-latest + container: + image: ruby:${{ matrix.ruby }} + services: + db: + image: postgres:${{ matrix.db_version }} + env: + LANG: C.UTF-8 + POSTGRES_INITDB_ARGS: --locale=C.UTF-8 + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - uses: actions/checkout@v2 + - name: Cache gems + uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ matrix.ruby }}-postgres-${{ hashFiles('**/Gemfile') }} + restore-keys: | + ${{ matrix.ruby }}-postgres- + ${{ matrix.ruby }}- + - name: Install & run tests + run: ./.github/actions/test-with-db.sh postgres + env: + DB_HOST: db + test-with-sqlite: + strategy: + fail-fast: false + matrix: + ruby: [2.5, 2.6, 2.7] + runs-on: ubuntu-latest + container: + image: ruby:${{ matrix.ruby }} + steps: + - uses: actions/checkout@v2 + - name: Cache gems + uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ matrix.ruby }}-sqlite-${{ hashFiles('**/Gemfile') }} + restore-keys: | + ${{ matrix.ruby }}-sqlite- + ${{ matrix.ruby }}- + - name: Install & run tests + run: ./.github/actions/test-with-db.sh sqlite diff --git a/.gitignore b/.gitignore index ebc25cbd86..d90a44f348 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ /node_modules yarn-error.log + +docker-compose.yml diff --git a/Gemfile b/Gemfile index 949fe86d2a..5c756894e0 100644 --- a/Gemfile +++ b/Gemfile @@ -88,6 +88,10 @@ end group :development do gem "yard" + gem "guard" + gem "guard-minitest" + gem "pry", "<= 0.12.2" if RUBY_VERSION < '2.4' + gem "pry-byebug" end group :test do diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000000..2ee21bd185 --- /dev/null +++ b/Guardfile @@ -0,0 +1,3 @@ +guard :minitest, all_on_start: false do + watch(%r{^test/(.*)test(.*)\.rb$}) +end diff --git a/README.md b/README.md new file mode 100644 index 0000000000..2e0ca56282 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Redmineパッチ会 + +Redmine本体の改善をできる人を増やし、Redmineパッチ会を継続的に行っていけることを目的として、2020年7月から基本毎月オンライン開催しています。 + +基本は複数人のチームに分かれて、相談しながらパッチを書いています。 +(一人でもOK、今後参加者を中心にやり方が変わっていくかも) + +## このリポジトリ + +オープンソースのプロジェクト管理システム、[Redmine](https://redmine.org/projects/redmine)のフォークリポジトリです。 +このリポジトリでRedmineのバグ修正や機能改善を行い、Redmine本体に取り込んでもらうことでRedmineをより良くしていけるよう活動しています。 + +## Redmineパッチ会に参加したい + +Redmineの改善に興味ある方であればどなたでも。 +プログラミングせずに画面の文言変更でもパッチは送れます。 +一緒に仕様を考えて、本家にチケットを作成するだけでもやれることはあります。 Ruby・Railsのプログラミング経験があると更に幅は広がります。 + +初参加の場合、見学からでもお気軽にどうぞ(^^ + +### 1. Connpassでイベントを公開しているので、参加申し込みをしてみましょう! + +https://redmine-patch.connpass.com/ + +### 2. 参加登録をしたら、オンラインのやりとり・当日の会場として利用しているDiscordに参加しよう! + +イベントに参加登録をした方にのみ参加用URLが確認可能です。 +参加の上で不安な点、わからない点があったらテキストチャンネルで気軽に相談してください👍 + +### 3. チーム開発に参加できる環境を整えよう!(プログラミング以外での参加の場合は不要) + +主に通話にDiscord、複数人でのコーディングにVisual Studio CodeのLive Share拡張を利用しています。 +**VSCodeのLive Shareでモブプロのように参加できるため、Redmineが動く開発環境がなくても参加できます。** + +* [Visual Studio Code](https://code.visualstudio.com/)をインストール +* Visual Studio Codeを開いて、拡張機能 [Live Share](https://marketplace.visualstudio.com/items?itemName=MS-vsliveshare.vsliveshare)をインストール +* (ない人は)Githubのアカウントを作成 + +### 4. イベントの時間になったらDiscordを開いて、ボイスチャンネルに参加しよう! + +(時間になったら他の参加者も参加しているはず) \ No newline at end of file diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index 4132fb86e7..0000000000 --- a/README.rdoc +++ /dev/null @@ -1,5 +0,0 @@ -= Redmine - -Redmine is a flexible project management web application written using Ruby on Rails framework. - -More details can be found in the doc directory or on the official website http://www.redmine.org diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 15aa665a0a..d6694c815a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -28,9 +28,11 @@ class ApplicationController < ActionController::Base include Redmine::Hook::Helper include RoutesHelper include AvatarsHelper + include IconsHelper helper :routes helper :avatars + helper :icons class_attribute :accept_api_auth_actions class_attribute :accept_rss_auth_actions diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 03fb26d4ca..b0210c2169 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -126,9 +126,14 @@ def link_to_issue(issue, options={}) # * :download - Force download (default: false) def link_to_attachment(attachment, options={}) text = options.delete(:text) || attachment.filename + icon = 'paperclip' + icon_only = false + if options.delete(:download) route_method = :download_named_attachment_url options[:filename] = attachment.filename + icon = 'download' + icon_only = true else route_method = :attachment_url # make sure we don't have an extraneous :filename in the options @@ -137,7 +142,7 @@ def link_to_attachment(attachment, options={}) html_options = options.slice!(:only_path, :filename) options[:only_path] = true unless options.key?(:only_path) url = send(route_method, attachment, options) - link_to text, url, html_options + link_to labeled_icon(text, icon, icon_only: icon_only, size: 12), url, html_options end # Generates a link to a SCM revision @@ -797,7 +802,7 @@ def actions_dropdown(&block) content = capture(&block) if content.present? trigger = - content_tag('span', l(:button_actions), :class => 'icon-only icon-actions', + content_tag('span', labeled_icon(l(:button_actions), 'ellipsis-h', icon_only: true), :class => 'icon icon-actions icon-svg', :title => l(:button_actions)) trigger = content_tag('span', trigger, :class => 'drdn-trigger') content = content_tag('div', content, :class => 'drdn-items') @@ -1832,8 +1837,8 @@ def heads_for_auto_complete(project) def copy_object_url_link(url) link_to_function( - l(:button_copy_link), 'copyTextToClipboard(this);', - class: 'icon icon-copy-link', + labeled_icon(l(:button_copy_link), 'link'), 'copyTextToClipboard(this);', + class: 'icon icon-copy-link icon-svg', data: {'clipboard-text' => url} ) end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb new file mode 100644 index 0000000000..5a9bce5d6b --- /dev/null +++ b/app/helpers/icons_helper.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module IconsHelper + DEFAULT_ICON_SIZE = 14 + DEFAULT_ICON_PATH = 'public/svgs' + + def labeled_icon(label, icon_name = nil, size: DEFAULT_ICON_SIZE, css_class: nil, icon_only: false, path: DEFAULT_ICON_PATH) + css_classes = [] + css_classes << "s#{size}" if size + css_classes << "#{css_class}" unless css_class.blank? + + svg = svg_file(icon_name, css_classes, path) + + if icon_only + svg + else + svg + label + end + end + + private + + def svg_file(icon, css_class, path) + file_path = "#{Rails.root}/#{path}/#{icon}.svg" + + if File.exists?(file_path) + doc = content_tag(:svg,:class => css_class, :xmlns => "http://www.w3.org/2000/svg") do + content_tag(:use, '', :href => "/svgs/#{icon}.svg#icon") + end + else + doc = "" + end + + raw doc + end +end diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb index d95d2b77b1..067e5a106d 100644 --- a/app/helpers/journals_helper.rb +++ b/app/helpers/journals_helper.rb @@ -34,27 +34,27 @@ def render_journal_actions(issue, journal, options={}) dropbown_links << copy_object_url_link(issue_url(issue, anchor: "note-#{indice}", only_path: false)) if journal.notes.present? if options[:reply_links] - links << link_to(l(:button_quote), + links << link_to(labeled_icon(l(:button_quote), 'quote-left', icon_only: true), quoted_issue_path(issue, :journal_id => journal, :journal_indice => indice), :remote => true, :method => 'post', :title => l(:button_quote), - :class => 'icon-only icon-comment' + :class => 'icon-only icon-comment icon-svg' ) end if journal.editable_by?(User.current) - links << link_to(l(:button_edit), + links << link_to(labeled_icon(l(:button_edit), 'pen-alt', icon_only: true), edit_journal_path(journal), :remote => true, :method => 'get', :title => l(:button_edit), - :class => 'icon-only icon-edit' + :class => 'icon-only icon-edit icon-svg' ) - dropbown_links << link_to(l(:button_delete), + dropbown_links << link_to(labeled_icon(l(:button_delete), 'trash-alt'), journal_path(journal, :journal => {:notes => ""}), :remote => true, :method => 'put', :data => {:confirm => l(:text_are_you_sure)}, - :class => 'icon icon-del' + :class => 'icon icon-del icon-svg' ) end end diff --git a/app/helpers/watchers_helper.rb b/app/helpers/watchers_helper.rb index d00c5b776c..f4a891a05d 100644 --- a/app/helpers/watchers_helper.rb +++ b/app/helpers/watchers_helper.rb @@ -26,15 +26,16 @@ def watcher_link(objects, user) return '' unless objects.any? watched = Watcher.any_watched?(objects, user) - css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ') + css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off icon-svg'].join(' ') text = watched ? l(:button_unwatch) : l(:button_watch) url = watch_path( :object_type => objects.first.class.to_s.underscore, :object_id => (objects.size == 1 ? objects.first.id : objects.map(&:id).sort) ) method = watched ? 'delete' : 'post' + icon = watched ? 'star' : 'star-off' - link_to text, url, :remote => true, :method => method, :class => css + link_to labeled_icon(text, icon), url, :remote => true, :method => method, :class => css end # Returns the css class used to identify watch links for a given +object+ diff --git a/app/views/attachments/_links.html.erb b/app/views/attachments/_links.html.erb index 25d022029f..dbb777b449 100644 --- a/app/views/attachments/_links.html.erb +++ b/app/views/attachments/_links.html.erb @@ -1,23 +1,23 @@
- <%= link_to(l(:label_edit_attachments), + <%= link_to(labeled_icon(l(:label_edit_attachments), 'pen-alt', icon_only: true), container_attachments_edit_path(container), :title => l(:label_edit_attachments), - :class => 'icon-only icon-edit' + :class => 'icon-only icon-edit icon-svg' ) if options[:editable] %> - <%= link_to(l(:label_download_all_attachments), + <%= link_to(labeled_icon(l(:label_download_all_attachments), 'download', icon_only: true), container_attachments_download_path(container), :title => l(:label_download_all_attachments), - :class => 'icon-only icon-download' + :class => 'icon-only icon-download icon-svg' ) if attachments.size > 1 %>
<% for attachment in attachments %> diff --git a/app/views/issues/_action_menu.html.erb b/app/views/issues/_action_menu.html.erb index 1c0dd05450..bc83525247 100644 --- a/app/views/issues/_action_menu.html.erb +++ b/app/views/issues/_action_menu.html.erb @@ -1,16 +1,16 @@
-<%= link_to l(:button_edit), edit_issue_path(@issue), +<%= link_to labeled_icon(l(:button_edit), 'pen-alt'), edit_issue_path(@issue), :onclick => 'showAndScrollTo("update", "issue_notes"); return false;', - :class => 'icon icon-edit', :accesskey => accesskey(:edit) if @issue.editable? %> -<%= link_to l(:button_log_time), new_issue_time_entry_path(@issue), - :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project) %> + :class => 'icon icon-edit icon-svg', :accesskey => accesskey(:edit) if @issue.editable? %> +<%= link_to labeled_icon(l(:button_log_time), 'time-add'), new_issue_time_entry_path(@issue), + :class => 'icon icon-time-add icon-svg' if User.current.allowed_to?(:log_time, @project) %> <%= watcher_link(@issue, User.current) %> -<%= link_to l(:button_copy), project_copy_issue_path(@project, @issue), - :class => 'icon icon-copy' if User.current.allowed_to?(:copy_issues, @project) && Issue.allowed_target_projects.any? %> +<%= link_to labeled_icon(l(:button_copy), 'copy'), project_copy_issue_path(@project, @issue), + :class => 'icon icon-copy icon-svg' if User.current.allowed_to?(:copy_issues, @project) && Issue.allowed_target_projects.any? %> <%= actions_dropdown do %> <%= copy_object_url_link(issue_url(@issue, only_path: false)) %> - <%= link_to l(:button_delete), issue_path(@issue), + <%= link_to labeled_icon(l(:button_delete), 'trash-alt'), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, - :method => :delete, :class => 'icon icon-del' if @issue.deletable? %> + :method => :delete, :class => 'icon icon-del icon-svg' if @issue.deletable? %> <% end %>
diff --git a/app/views/issues/index.html.erb b/app/views/issues/index.html.erb index 880c953eaa..bc7c4780d1 100644 --- a/app/views/issues/index.html.erb +++ b/app/views/issues/index.html.erb @@ -1,6 +1,6 @@
<% if User.current.allowed_to?(:add_issues, @project, :global => true) && (@project.nil? || Issue.allowed_target_trackers(@project).any?) %> - <%= link_to l(:label_issue_new), _new_project_issue_path(@project), :class => 'icon icon-add new-issue' %> + <%= link_to labeled_icon(l(:label_issue_new), 'plus-circle'), _new_project_issue_path(@project), :class => 'icon icon-svg icon-add new-issue' %> <% end %> <%= actions_dropdown do %> <% if @project %> diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb index 390728adad..3125e7777a 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -83,7 +83,7 @@ end %>
- <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if @issue.notes_addable? %> + <%= link_to labeled_icon(l(:button_quote), 'quote-left'), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment icon-svg' if @issue.notes_addable? %>

<%=l(:field_description)%>

diff --git a/config/database.mysql.yml b/config/database.mysql.yml new file mode 100644 index 0000000000..3a9ec22916 --- /dev/null +++ b/config/database.mysql.yml @@ -0,0 +1,18 @@ +default: &default + adapter: mysql2 + host: <%= ENV.fetch('DB_HOST', '127.0.0.1') %> + port: <%= ENV.fetch('DB_PORT', '3306') %> + username: root + password: password + +production: + <<: *default + database: redmine_production + +development: + <<: *default + database: redmine_development + +test: + <<: *default + database: redmine_test diff --git a/config/database.postgres.yml b/config/database.postgres.yml new file mode 100644 index 0000000000..20d7dd38e0 --- /dev/null +++ b/config/database.postgres.yml @@ -0,0 +1,19 @@ +default: &default + adapter: postgresql + encoding: utf8 + host: <%= ENV.fetch('DB_HOST', 'localhost') %> + port: <%= ENV.fetch('DB_PORT', '5432') %> + username: postgres + password: postgres + +production: + <<: *default + database: redmine_production + +development: + <<: *default + database: redmine_development + +test: + <<: *default + database: redmine_test diff --git a/config/database.sqlite.yml b/config/database.sqlite.yml new file mode 100644 index 0000000000..0d72b884a5 --- /dev/null +++ b/config/database.sqlite.yml @@ -0,0 +1,18 @@ +# Default setup is given for MySQL 5.7.7 or later. +# Examples for PostgreSQL, SQLite3 and SQL Server can be found at the end. +# Line indentation must be 2 spaces (no tabs). + +default: &default + adapter: sqlite3 + +production: + <<: *default + database: db/redmine_production.sqlite3 + +development: + <<: *default + database: db/redmine_development.sqlite3 + +test: + <<: *default + database: db/redmine_test.sqlite3 diff --git a/docker-compose.mysql.yml b/docker-compose.mysql.yml new file mode 100644 index 0000000000..d6033fe553 --- /dev/null +++ b/docker-compose.mysql.yml @@ -0,0 +1,14 @@ +version: '3.4' +services: + db: + image: mysql:${DB_VERSION:-latest} + environment: + MYSQL_ROOT_PASSWORD: password + volumes: + - mysql_data:/var/lib/mysql + ports: + - "3306:3306" + + +volumes: + mysql_data: diff --git a/docker-compose.postgres.yml b/docker-compose.postgres.yml new file mode 100644 index 0000000000..ad21b712e7 --- /dev/null +++ b/docker-compose.postgres.yml @@ -0,0 +1,15 @@ +version: '3.4' +services: + db: + image: postgres:${DB_VERSION:-latest} + volumes: + - postgres_data:/var/lib/postgresql + ports: + - "5432:5432" + environment: + LANG: C.UTF-8 + POSTGRES_INITDB_ARGS: --locale=C.UTF-8 + POSTGRES_PASSWORD: postgres + +volumes: + postgres_data: diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 78be5865e0..be316009e1 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -223,8 +223,8 @@ div + .drdn-items {border-top:1px solid #ccc;} .contextual .drdn-content {top:18px;} .contextual .drdn-items {padding:2px; min-width: 160px;} -.contextual .drdn-items>a {padding: 5px 8px;} -.contextual .drdn-items>a.icon {padding-left: 24px; background-position-x: 4px;} +.contextual .drdn-items>a {display: flex; padding: 5px 8px;} +.contextual .drdn-items>a.icon:not(.icon-svg) {padding-left: 24px; background-position-x: 4px;} .contextual .drdn-items>a:hover {color:#2A5685; border:1px solid #628db6; background-color:#eef5fd; border-radius:3px;} #project-jump.drdn {width:200px;display:inline-block;} @@ -464,7 +464,7 @@ div.square { width: .6em; height: .6em; } .contextual {float:right; white-space: nowrap; line-height:1.4em;margin:5px 0px; padding-left: 10px; font-size:0.9em;} -.contextual .icon {padding-top: 2px; padding-bottom: 3px;} +.contextual .icon:not(.icon-svg) {padding-top: 2px; padding-bottom: 3px;} .contextual input, .contextual select {font-size:0.9em;} .message .contextual { margin-top: 0; } @@ -1497,37 +1497,48 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container { white-space: pre-wrap; } +/***** SVGs ******/ +.s16 { + width: 16px; + height: 16px; +} + +.s14 { + width: 14px; + height: 14px; +} + +.s12 { + width: 12px; + height: 12px; +} + /***** Icons *****/ -.icon { +.icon:not(.icon-svg) { background-position: 0% 50%; background-repeat: no-repeat; padding-left: 20px; } -.icon-only { - background-position: 0% 50%; - background-repeat: no-repeat; - padding-left: 16px; - display: inline-block; - width: 0; - height: 16px; - overflow: hidden; - padding-top: 0; - padding-bottom: 0; - font-size: 8px; - vertical-align: middle; +.icon.icon-svg { + display: inline-flex; } -.icon-only::after { - content: "\a0"; + +a svg, .icon svg { + fill: #169; + margin-right: 4px; } -.icon-add { background-image: url(../images/add.png); } -.icon-edit { background-image: url(../images/edit.png); } -.icon-copy { background-image: url(../images/copy.png); } + +/*.icon-fav svg { fill: #fc8c12; }*/ + +.icon-add:not(.icon-svg) { background-image: url(../images/add.png); } +.icon-edit:not(.icon-svg) { background-image: url(../images/edit.png); } +.icon-copy:not(.icon-svg) { background-image: url(../images/copy.png); } .icon-duplicate { background-image: url(../images/duplicate.png); } -.icon-del { background-image: url(../images/delete.png); } +.icon-del:not(.icon-svg) { background-image: url(../images/delete.png); } .icon-move { background-image: url(../images/move.png); } .icon-save { background-image: url(../images/save.png); } -.icon-download { background-image: url(../images/download.png); } +.icon-download:not(.icon-svg) { background-image: url(../images/download.png); } .icon-cancel { background-image: url(../images/cancel.png); } .icon-multiple { background-image: url(../images/table_multiple.png); } .icon-folder { background-image: url(../images/folder.png); } @@ -1536,21 +1547,21 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container { .icon-user { background-image: url(../images/user.png); } .icon-project, .icon-projects { background-image: url(../images/projects.png); } .icon-help { background-image: url(../images/help.png); } -.icon-attachment { background-image: url(../images/attachment.png); } +.icon-attachment:not(.icon-svg) { background-image: url(../images/attachment.png); } .icon-history { background-image: url(../images/history.png); } .icon-time-entry, .icon-time { background-image: url(../images/time.png); } -.icon-time-add { background-image: url(../images/time_add.png); } +.icon-time-add:not(.icon-svg) { background-image: url(../images/time_add.png); } .icon-stats { background-image: url(../images/stats.png); } .icon-warning { background-image: url(../images/warning.png); } .icon-error { background-image: url(../images/exclamation.png); } -.icon-fav { background-image: url(../images/fav.png); } -.icon-fav-off { background-image: url(../images/fav_off.png); } +.icon-fav:not(.icon-svg) { background-image: url(../images/fav.png); } +.icon-fav-off:not(.icon-svg) { background-image: url(../images/fav_off.png); } .icon-reload { background-image: url(../images/reload.png); } .icon-lock, .icon-locked { background-image: url(../images/locked.png); } .icon-unlock { background-image: url(../images/unlock.png); } .icon-checked { background-image: url(../images/toggle_check.png); } .icon-report { background-image: url(../images/report.png); } -.icon-comment, .icon-comments { background-image: url(../images/comment.png); } +.icon-comment:not(.icon-svg), .icon-comments { background-image: url(../images/comment.png); } .icon-summary { background-image: url(../images/lightning.png); } .icon-server-authentication { background-image: url(../images/server_key.png); } .icon-issue { background-image: url(../images/ticket.png); } @@ -1586,7 +1597,7 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container { .icon-project { background-image: url(../images/projects.png); } .icon-add-bullet { background-image: url(../images/bullet_add.png); } .icon-shared { background-image: url(../images/link.png); } -.icon-actions { background-image: url(../images/3_bullets.png); } +.icon-actions:not(.icon-svg) { background-image: url(../images/3_bullets.png); } .icon-sort-handle { background-image: url(../images/reorder.png); } .icon-expanded { background-image: url(../images/arrow_down.png); } .icon-collapsed { background-image: url(../images/arrow_right.png); } @@ -1618,7 +1629,7 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container { .icon-file.application-pdf { background-image: url(../images/files/pdf.png); } .icon-file.application-zip { background-image: url(../images/files/zip.png); } .icon-file.application-gzip { background-image: url(../images/files/zip.png); } -.icon-copy-link { background-image: url(../images/copy_link.png); } +.icon-copy-link:not(.icon-svg) { background-image: url(../images/copy_link.png); } .sort-handle.ajax-loading { background-image: url(../images/loading.gif); } tr.ui-sortable-helper { border:1px solid #e4e4e4; } diff --git a/public/svgs/copy.svg b/public/svgs/copy.svg new file mode 100644 index 0000000000..c8d5a140d2 --- /dev/null +++ b/public/svgs/copy.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/download.svg b/public/svgs/download.svg new file mode 100644 index 0000000000..a8929a8cf4 --- /dev/null +++ b/public/svgs/download.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/ellipsis-h.svg b/public/svgs/ellipsis-h.svg new file mode 100644 index 0000000000..c369500897 --- /dev/null +++ b/public/svgs/ellipsis-h.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/link.svg b/public/svgs/link.svg new file mode 100644 index 0000000000..6226d765a9 --- /dev/null +++ b/public/svgs/link.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/paperclip.svg b/public/svgs/paperclip.svg new file mode 100644 index 0000000000..7d5e635f80 --- /dev/null +++ b/public/svgs/paperclip.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/pen-alt.svg b/public/svgs/pen-alt.svg new file mode 100644 index 0000000000..0bc5612d1b --- /dev/null +++ b/public/svgs/pen-alt.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/plus-circle.svg b/public/svgs/plus-circle.svg new file mode 100644 index 0000000000..4f21c94219 --- /dev/null +++ b/public/svgs/plus-circle.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/quote-left.svg b/public/svgs/quote-left.svg new file mode 100644 index 0000000000..60c515d9bc --- /dev/null +++ b/public/svgs/quote-left.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/star-off.svg b/public/svgs/star-off.svg new file mode 100644 index 0000000000..03595caf34 --- /dev/null +++ b/public/svgs/star-off.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/star.svg b/public/svgs/star.svg new file mode 100644 index 0000000000..3ed18fa596 --- /dev/null +++ b/public/svgs/star.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/time-add.svg b/public/svgs/time-add.svg new file mode 100644 index 0000000000..9d18296319 --- /dev/null +++ b/public/svgs/time-add.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/trash-alt.svg b/public/svgs/trash-alt.svg new file mode 100644 index 0000000000..98ec0ba6e2 --- /dev/null +++ b/public/svgs/trash-alt.svg @@ -0,0 +1 @@ +
- <%= link_to_attachment attachment, class: 'icon icon-attachment' -%> + <%= link_to_attachment attachment, class: 'icon icon-attachment icon-svg' -%> (<%= number_to_human_size attachment.filesize %>) - <%= link_to_attachment attachment, class: 'icon-only icon-download', title: l(:button_download), download: true -%> + <%= link_to_attachment attachment, class: 'icon-only icon-download icon-svg', title: l(:button_download), download: true -%> <%= attachment.description unless attachment.description.blank? %> @@ -27,10 +27,10 @@ <% if options[:deletable] %> - <%= link_to l(:button_delete), attachment_path(attachment), + <%= link_to labeled_icon(l(:button_delete), 'trash-alt', icon_only: true, size: 12), attachment_path(attachment), :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, - :class => 'delete icon-only icon-del', + :class => 'delete icon-only icon-del icon-svg', :title => l(:button_delete) %> <% end %>