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') }}
+
+
+
+
- {{ sprintTitle }}
+
+
+
+
{{ projectTitle }}
+
+
+
+ {{ sprintTitle }}
+
+
+ {{ $t('title.is_public') }}
+
+
+
+
+
+
@@ -141,6 +157,8 @@ export default {
data () {
return {
projectId: null,
+ projectTitle: '',
+ isPublic: '',
sprintId: null,
sprintTitle: null,
newTask: false,
@@ -149,8 +167,10 @@ export default {
},
mounted() {
this.projectId = this.$route.meta.projectId
+ this.projectTitle = this.$route.meta.projectTitle
this.sprintId = this.$route.meta.sprintId
this.sprintTitle = this.$route.meta.sprintTitle
+ this.isPublic = this.$route.meta.isPublic
this.newTask = this.$route.meta.newTask
this.setProjectId(this.projectId)
diff --git a/app/models/project.rb b/app/models/project.rb
index b1ad3d7..fe13b18 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -23,6 +23,15 @@ class Project < ApplicationRecord
validates :title, presence: true
validates :ticket_prefix, presence: true, uniqueness: true, format: { with: /[a-zA-Z]/}
+ scope :search_by_keyword, -> (keywords) {
+ if keywords.present?
+ columns = %i[title body]
+ where(keywords.split(/[[:space:]]/).reject(&:empty?).map { |keyword|
+ columns.map { |a| arel_table[a].matches("%#{keyword}%") }.inject(:or)
+ }.inject(:and))
+ end
+ }
+
def project_image_url
if image.present?
base64 = Base64.strict_encode64(image.download)
diff --git a/app/views/kaminari/_first_page.html.erb b/app/views/kaminari/_first_page.html.erb
new file mode 100644
index 0000000..32381a8
--- /dev/null
+++ b/app/views/kaminari/_first_page.html.erb
@@ -0,0 +1,3 @@
+
+ <%= link_to_unless current_page.first?, '', url, remote: remote, class: 'page-link rounded-0 fa fa-angle-double-left' %>
+
diff --git a/app/views/kaminari/_gap.html.erb b/app/views/kaminari/_gap.html.erb
new file mode 100644
index 0000000..2169c8e
--- /dev/null
+++ b/app/views/kaminari/_gap.html.erb
@@ -0,0 +1,3 @@
+
+ <%= link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link rounded-0' %>
+
diff --git a/app/views/kaminari/_last_page.html.erb b/app/views/kaminari/_last_page.html.erb
new file mode 100644
index 0000000..40f4b99
--- /dev/null
+++ b/app/views/kaminari/_last_page.html.erb
@@ -0,0 +1,3 @@
+
+ <%= link_to_unless current_page.last?, '', url, remote: remote, class: 'page-link rounded-0 fa fa-angle-double-right' %>
+
diff --git a/app/views/kaminari/_next_page.html.erb b/app/views/kaminari/_next_page.html.erb
new file mode 100644
index 0000000..971850a
--- /dev/null
+++ b/app/views/kaminari/_next_page.html.erb
@@ -0,0 +1,3 @@
+
+ <%= link_to_unless current_page.last?, '', url, rel: 'next', remote: remote, class: 'page-link rounded-0 fa fa-angle-right' %>
+
diff --git a/app/views/kaminari/_page.html.erb b/app/views/kaminari/_page.html.erb
new file mode 100644
index 0000000..5bfed45
--- /dev/null
+++ b/app/views/kaminari/_page.html.erb
@@ -0,0 +1,9 @@
+<% if page.current? %>
+
+ <%= content_tag :a, page, data: { remote: remote }, rel: page.rel, class: 'page-link rounded-0' %>
+
+<% else %>
+
+ <%= link_to page, url, remote: remote, rel: page.rel, class: 'page-link rounded-0' %>
+
+<% end %>
diff --git a/app/views/kaminari/_paginator.html.erb b/app/views/kaminari/_paginator.html.erb
new file mode 100644
index 0000000..bb5ed0f
--- /dev/null
+++ b/app/views/kaminari/_paginator.html.erb
@@ -0,0 +1,17 @@
+<%= paginator.render do %>
+
+
+
+<% end %>
diff --git a/app/views/kaminari/_prev_page.html.erb b/app/views/kaminari/_prev_page.html.erb
new file mode 100644
index 0000000..2eb281b
--- /dev/null
+++ b/app/views/kaminari/_prev_page.html.erb
@@ -0,0 +1,3 @@
+
+ <%= link_to_unless current_page.first?, '', url, rel: 'prev', remote: remote, class: 'page-link rounded-0 fa fa-angle-left' %>
+
diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb
index 4208f19..5706b74 100644
--- a/app/views/projects/_form.html.erb
+++ b/app/views/projects/_form.html.erb
@@ -11,9 +11,11 @@
<% 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 %>
+
+
+ <%= 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: これは公開プロジェクトです。メンバーではないユーザはチケットへのコメントのみ許可されています。