diff --git a/Gemfile b/Gemfile index 004d730..27b5685 100644 --- a/Gemfile +++ b/Gemfile @@ -120,3 +120,6 @@ gem 'omniauth-github', '~> 1.3' gem 'omniauth-google-oauth2', '~> 0.8' gem 'config' + +gem 'kaminari', '~> 1.1' + diff --git a/Gemfile.lock b/Gemfile.lock index e95aca8..5d09d4d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,6 +146,18 @@ GEM jbuilder (2.9.1) activesupport (>= 4.2.0) jwt (2.2.1) + kaminari (1.1.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.1.1) + kaminari-activerecord (= 1.1.1) + kaminari-core (= 1.1.1) + kaminari-actionview (1.1.1) + actionview + kaminari-core (= 1.1.1) + kaminari-activerecord (1.1.1) + activerecord + kaminari-core (= 1.1.1) + kaminari-core (1.1.1) kramdown (1.17.0) launchy (2.4.3) addressable (~> 2.3) @@ -368,6 +380,7 @@ DEPENDENCIES identicon initial_avatar jbuilder (~> 2.7) + kaminari (~> 1.1) letter_opener_web listen (>= 3.0.5, < 3.2) omniauth (~> 1.9) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index eb7233d..11b84b1 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -11,7 +11,12 @@ class ProjectsController < ApplicationController :project_tags ] + PROJECT_PER_PAGE = 25 + def index + @projects = @projects.search_by_keyword(params[:keywords]).page(params[:page]).per(PROJECT_PER_PAGE) + # 自分が属している自身のプロジェクトがない場合には作成を促すメッセージを表示するためのフラグ + @is_not_exits_own_project = @projects.all? { |project| project.is_public && !project.users.include?(current_user) } end def show diff --git a/app/javascript/components/commons/TicketForm.vue b/app/javascript/components/commons/TicketForm.vue index 374de19..0477f74 100644 --- a/app/javascript/components/commons/TicketForm.vue +++ b/app/javascript/components/commons/TicketForm.vue @@ -68,6 +68,11 @@ export default { isNew: Boolean, afterSubmit: Function }, + data() { + return { + initialFocused: false + }; + }, mounted() { Vue.nextTick(() => { // Focus Input @@ -76,10 +81,17 @@ export default { }, watch: { isLoading: function(newLoading, oldLoading) { - Vue.nextTick(() => { - // Focus Input - this.$refs.titleInput.focus() - }) + // XXX: + // 一度だけ Title に Focus する + // この条件がないと、新規作成時に isLoading が変わる度に + // titleInput に focus があたってしまう + if (this.initialFocused === false) { + Vue.nextTick(() => { + // Focus Input + this.$refs.titleInput.focus() + this.initialFocused = true + }) + } } }, methods: { diff --git a/app/javascript/i18n/globals.json b/app/javascript/i18n/globals.json index d80f33a..af24d80 100644 --- a/app/javascript/i18n/globals.json +++ b/app/javascript/i18n/globals.json @@ -38,7 +38,8 @@ "task": "Task", "tag": "Tags", "unassigned": "Unassigned", - "search": "Search by keyword" + "search": "Search by keyword", + "is_public": "Public" }, "ticket": { "title": "Title", @@ -94,7 +95,8 @@ "storyDoesNotExists": "Story doesn't exists. Let's create a story with the \"Add Story\" button.", "storyDoesNotExistsInSprint": "Story doesn't exists. Let's add the added story to Sprint by D&D from Backlogs.", "historyIsEmpty": "History is empty.", - "tagInput": "Please input tags" + "tagInput": "Please input tags", + "publicProject": "This is a public project. Non-member users are only allowed to comment on tickets." }, "tab": { "comment": "Comment", @@ -140,7 +142,8 @@ "task": "タスク", "tag": "タグ", "unassigned": "未アサイン", - "search": "キーワード検索" + "search": "キーワード検索", + "is_public": "公開" }, "ticket": { "title": "タイトル", @@ -196,7 +199,8 @@ "storyDoesNotExists": "Story がありません。「Story を追加」ボタンで Story を作成してみましょう。", "storyDoesNotExistsInSprint": "Story がありません。Backlogs に追加した Story を D&D で Sprint に追加してみましょう", "historyIsEmpty": "更新履歴はありません", - "tagInput": "タグを入力してください" + "tagInput": "タグを入力してください", + "publicProject": "これは公開プロジェクトです。メンバーではないユーザはチケットへのコメントのみ許可されています。" }, "tab": { "comment": "コメント", diff --git a/app/javascript/packs/backlogs.js b/app/javascript/packs/backlogs.js index 1808c25..2ad5f97 100644 --- a/app/javascript/packs/backlogs.js +++ b/app/javascript/packs/backlogs.js @@ -8,6 +8,8 @@ import http from '../commons/custom-axios' document.addEventListener('DOMContentLoaded', () => { const rootElement = document.getElementById('content') const projectId = rootElement.dataset.projectId + const projectTitle = rootElement.dataset.projectTitle + const isPublic = rootElement.dataset.isPublic Vue.use(VueRouter) Vue.use(VueI18n) Vue.use(http, { store }) @@ -19,7 +21,9 @@ document.addEventListener('DOMContentLoaded', () => { component: BacklogsPage, meta: { newStory: true, - projectId: projectId + projectId: projectId, + projectTitle: projectTitle, + isPublic: isPublic } }, { @@ -27,7 +31,9 @@ document.addEventListener('DOMContentLoaded', () => { component: BacklogsPage, meta: { newStory: false, - projectId: projectId + projectId: projectId, + projectTitle: projectTitle, + isPublic: isPublic } }, { @@ -35,7 +41,9 @@ document.addEventListener('DOMContentLoaded', () => { component: BacklogsPage, meta: { newStory: false, - projectId: projectId + projectId: projectId, + projectTitle: projectTitle, + isPublic: isPublic } }, ] diff --git a/app/javascript/packs/kanban.js b/app/javascript/packs/kanban.js index 11a65ec..fdc892b 100644 --- a/app/javascript/packs/kanban.js +++ b/app/javascript/packs/kanban.js @@ -8,6 +8,8 @@ import http from '../commons/custom-axios' document.addEventListener('DOMContentLoaded', () => { const rootElement = document.getElementById('content') const projectId = rootElement.dataset.projectId + const projectTitle = rootElement.dataset.projectTitle + const isPublic = rootElement.dataset.isPublic const sprintId = rootElement.dataset.sprintId const sprintTitle = rootElement.dataset.sprintTitle Vue.use(VueRouter) @@ -23,6 +25,8 @@ document.addEventListener('DOMContentLoaded', () => { meta: { newTask: true, projectId: projectId, + projectTitle: projectTitle, + isPublic: isPublic, sprintId: sprintId, sprintTitle: sprintTitle } @@ -34,6 +38,8 @@ document.addEventListener('DOMContentLoaded', () => { meta: { newTask: false, projectId: projectId, + projectTitle: projectTitle, + isPublic: isPublic, sprintId: sprintId, sprintTitle: sprintTitle } @@ -45,6 +51,8 @@ document.addEventListener('DOMContentLoaded', () => { meta: { newTask: false, projectId: projectId, + projectTitle: projectTitle, + isPublic: isPublic, sprintId: sprintId, sprintTitle: sprintTitle } @@ -55,6 +63,8 @@ document.addEventListener('DOMContentLoaded', () => { meta: { newTask: false, projectId: projectId, + projectTitle: projectTitle, + isPublic: isPublic, sprintId: sprintId, sprintTitle: sprintTitle } diff --git a/app/javascript/pages/BacklogsPage.vue b/app/javascript/pages/BacklogsPage.vue index bfbe79f..e05d1b3 100644 --- a/app/javascript/pages/BacklogsPage.vue +++ b/app/javascript/pages/BacklogsPage.vue @@ -9,7 +9,21 @@ loader="dots">
-

{{ $t('title.masterBacklogs')}}

+
+
+

+ {{ projectTitle }} +

+
+
+ {{ $t('title.masterBacklogs')}} + + + {{ $t('title.is_public') }} + + +
+
<% end %> - <%= f.input :title %> - <%= f.input :body, as: :text, input_html: { rows: 5 } %> - <%= f.input :ticket_prefix, hint: t('message.ticket_prefix_description', default: 'Ticket Prefix is an arbitrary string that is added to the beginning of the ticket number.') %> + <%= f.input :title, placeholder: t('.placeholder.title', default: 'My awesome project') %> + <%= f.input :body, as: :text, input_html: { rows: 5 }, placeholder: t('.placeholder.description', default: 'Description format') %> + <%= f.input :ticket_prefix, + placeholder: t('.placeholder.ticket_prefix', default: 'e.g.) AWE'), + hint: t('message.ticket_prefix_description', default: 'Ticket Prefix is an arbitrary string that is added to the beginning of the ticket number.') %> <%= f.input :is_public %>
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 96d92d3..d0de8ee 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -1,13 +1,52 @@
-
- <%= link_to t('actions.create_project', default: 'Create Project'), new_project_path, class: 'btn btn-outline-secondary'%> +
+
+

<%= t('.title', default: 'Projects') %>

+
+ <%= link_to t('actions.create_project', default: 'Create Project'), new_project_path, class: 'btn btn-sm btn-primary shadow-sm'%>
- <% if @projects.empty? %> + <%= form_tag projects_path, method: :get, class: 'd-flex w-100 align-items-center justify-content-end' do %> +
+ <%= + text_field_tag( + :keywords, + params[:keywords], + placeholder: t('.placeholder_for_search', default: 'Search Projects'), + class: 'form-control form-control-sm' + ) + %> +
+ <%= button_tag type: :submit, class: 'btn btn-sm btn-outline-primary' do %> + <%= t('.search', default: 'Search') %> + <% end %> + <% end %> +
+ + <% if @is_not_exits_own_project %> +
+
+ <%= t( + '.is_not_exists_own_project', + default: "Can't find your own project. Create your project with the \"Create Project\" button" + ) + %> +
+
+ <% end %> + +
+ <% if @projects.empty? && params[:keywords].blank? %>
<%= t('.project_does_not_exists', default: "Project doesn't exists. Let's create a project with the \"Create Project\" button.") %>
<% end %> + <% if @projects.empty? && params[:keywords].present? %> +
+ <%= t('.project_does_not_exists_when_keywords', default: "Project doesn't exists.") %> + <%= link_to t('.back_to_projects_index', default: 'Back to project index'), projects_path %> +
+ <% end %>
    <% @projects.each do |project| %>
  • @@ -17,19 +56,27 @@
    <% if project.is_public %> - <%= t('.is_public', default: 'Public') %> + + <%= t('.is_public', default: 'Public') %> + <% end %> <%= link_to project.title, project_path(project) %>
    <%= project.body %>
    - <%= link_to edit_project_path(project) do %> - + <%= link_to edit_project_path(project), class: 'btn btn-sm btn-link' do %> + <%= t('.edit', default: 'Edit') %> <% end %>
  • <% end %>
+
+
+ <%= paginate @projects %> +
+
\ No newline at end of file diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 418ebae..820d4ac 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -5,14 +5,17 @@
<%= render 'commons/sidebar', { project: @project } %>
+ +
+ +
<% if @project.is_public %> -
+
<%= t('.public_project_message', default: 'This is a public project. Non-member users are only allowed to comment on tickets.') %>
<% end %> - -
- -
\ No newline at end of file diff --git a/app/views/sprints/kanban.html.erb b/app/views/sprints/kanban.html.erb index 81a8942..01aa0cb 100644 --- a/app/views/sprints/kanban.html.erb +++ b/app/views/sprints/kanban.html.erb @@ -5,16 +5,13 @@
<%= render 'commons/sidebar', { project: @sprint.project } %>
- <% if @sprint.project.is_public %> -
- <%= t('.public_project_message', default: 'This is a public project. Non-member users are only allowed to comment on tickets.') %> -
- <% end %>
+ data-sprint-title="<%= @sprint.title %>" + data-is-public="<%= @sprint.project.is_public %>">
diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 1e932cd..4bc29ec 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -6,7 +6,7 @@ ja: activerecord: attributes: project: - title: タイトル + title: プロジェクト名 body: 説明文 ticket_prefix: チケットプレフィックス is_public: 公開プロジェクト @@ -37,10 +37,23 @@ ja: closed_sprints: クローズした Sprint projects: index: + title: プロジェクト一覧 project_does_not_exists: プロジェクトが作成されていません。「プロジェクトを作成」ボタンからプロジェクトを作成してみましょう。 + project_does_not_exists_when_keywords: プロジェクトが見つかりませんでした。 is_public: 公開 + placeholder_for_search: プロジェクトを検索する + search: 検索する + back_to_projects_index: プロジェクト一覧に戻る + edit: 編集する + is_not_exists_own_project: あなた自身のプロジェクトが見つかりません。「プロジェクトを作成」ボタンであなたのプロジェクトを作ってみましょう。 + public_project_message: これは公開プロジェクトです。メンバーではないユーザはチケットへのコメントのみ許可されています。 show: public_project_message: これは公開プロジェクトです。メンバーではないユーザはチケットへのコメントのみ許可されています。 + form: + placeholder: + title: プロジェクト名を入力してください + description: ここにプロジェクトの説明文を書きます + ticket_prefix: 例) "RE" や "FAB" sprints: kanban: public_project_message: これは公開プロジェクトです。メンバーではないユーザはチケットへのコメントのみ許可されています。