From 0e893c9b003d33b3f907d29b840bd548900f8805 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 26 Jan 2026 16:37:09 +0900 Subject: [PATCH 01/29] =?UTF-8?q?MentorMemo=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E3=82=92=E4=BD=9C=E6=88=90=E3=81=97User=E3=83=A2=E3=83=87?= =?UTF-8?q?=E3=83=AB=E3=81=A8=E9=96=A2=E9=80=A3=E4=BB=98=E3=81=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/mentor_memo.rb | 8 ++++++++ app/models/user.rb | 12 ++++++++++++ db/migrate/20260126070916_create_mentor_memos.rb | 11 +++++++++++ db/schema.rb | 12 ++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 app/models/mentor_memo.rb create mode 100644 db/migrate/20260126070916_create_mentor_memos.rb diff --git a/app/models/mentor_memo.rb b/app/models/mentor_memo.rb new file mode 100644 index 00000000000..501df03d91d --- /dev/null +++ b/app/models/mentor_memo.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class MentorMemo < ApplicationRecord + belongs_to :recipient, class_name: 'User' + belongs_to :writer, class_name: 'User' + + validates :body, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb index 353b06f6320..512096f2389 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -214,6 +214,18 @@ class User < ApplicationRecord # rubocop:todo Metrics/ClassLength dependent: :delete_all, inverse_of: 'user' + has_many :received_memos, + class_name: 'MentorMemo', + foreign_key: 'recipient_id', + inverse_of: :recipient, + dependent: :destroy + + has_many :authored_memos, + class_name: 'MentorMemo', + foreign_key: 'writer_id', + inverse_of: :writer, + dependent: :destroy + has_one_attached :avatar has_one_attached :profile_image has_one_attached :diploma_file diff --git a/db/migrate/20260126070916_create_mentor_memos.rb b/db/migrate/20260126070916_create_mentor_memos.rb new file mode 100644 index 00000000000..f9970173e07 --- /dev/null +++ b/db/migrate/20260126070916_create_mentor_memos.rb @@ -0,0 +1,11 @@ +class CreateMentorMemos < ActiveRecord::Migration[8.1] + def change + create_table :mentor_memos do |t| + t.references :recipient, null: false, foreign_key: { to_table: :users } + t.references :writer, null: false, foreign_key: { to_table: :users } + t.text :body, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2653c7d8e5a..54772193bd6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -456,6 +456,16 @@ t.index ["survey_question_id"], name: "index_linear_scales_on_survey_question_id" end + create_table "mentor_memos", force: :cascade do |t| + t.text "body", null: false + t.datetime "created_at", null: false + t.bigint "recipient_id", null: false + t.datetime "updated_at", null: false + t.bigint "writer_id", null: false + t.index ["recipient_id"], name: "index_mentor_memos_on_recipient_id" + t.index ["writer_id"], name: "index_mentor_memos_on_writer_id" + end + create_table "micro_reports", force: :cascade do |t| t.bigint "comment_user_id" t.text "content", null: false @@ -1149,6 +1159,8 @@ add_foreign_key "learning_time_frames_users", "users" add_foreign_key "learning_times", "reports" add_foreign_key "linear_scales", "survey_questions" + add_foreign_key "mentor_memos", "users", column: "recipient_id" + add_foreign_key "mentor_memos", "users", column: "writer_id" add_foreign_key "micro_reports", "users" add_foreign_key "micro_reports", "users", column: "comment_user_id" add_foreign_key "movies", "users" From f32a3ea1493963991897d7d76aa71a5d38d3703a Mon Sep 17 00:00:00 2001 From: matsuyama Date: Tue, 24 Feb 2026 18:13:09 +0900 Subject: [PATCH 02/29] =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=81=AE=E5=88=9D=E6=9C=9F=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=82=92JS=E3=81=8B=E3=82=89Rails=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/users_controller.rb | 2 + app/javascript/user_mentor_memo.js | 38 ++++--------------- app/views/users/_mentor_memo.html.slim | 12 ++++++ ...html.slim => _user_mentor_memos.html.slim} | 22 +++++------ app/views/users/show.html.slim | 2 +- 5 files changed, 31 insertions(+), 45 deletions(-) create mode 100644 app/views/users/_mentor_memo.html.slim rename app/views/users/{_user_mentor_memo.html.slim => _user_mentor_memos.html.slim} (80%) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 82f06aaa98b..8307c47332f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -48,6 +48,8 @@ def show reports = @user.reports_with_learning_times @study_streak = StudyStreak.new(reports, include_wip: false) + @mentor_memos = @user.received_memos.includes(:writer).order(created_at: :desc) + if logged_in? render :show else diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/user_mentor_memo.js index c124627f747..cf8fc71b14c 100644 --- a/app/javascript/user_mentor_memo.js +++ b/app/javascript/user_mentor_memo.js @@ -12,8 +12,6 @@ document.addEventListener('DOMContentLoaded', () => { const memoDisplay = mentorMemo.querySelector('.memo-display') const memoEditor = mentorMemo.querySelector('.memo-editor') - const placeholder = memoDisplay.querySelector('.a-placeholder') - const emptyMessage = memoDisplay.querySelector('.o-empty-message') const memoDisplayContent = memoDisplay.querySelector( '.user-mentor-memo-content' ) @@ -24,36 +22,14 @@ document.addEventListener('DOMContentLoaded', () => { '.a-markdown-input__textarea' ) - fetch(`/api/users/${userId}.json`, { - method: 'GET', - headers: { - 'X-Requested-With': 'XMLHttpRequest' - }, - credentials: 'same-origin', - redirect: 'manual' + const memos = document.querySelectorAll('.mentor-memo') + memos.forEach((memo) => { + const memoDisplayContent = memo.querySelector('.memo-text') + const memoBody = memo.dataset.memo_body + if (!memoBody) return + + memoDisplayContent.innerHTML = markdownInitializer.render(memoBody) }) - .then((response) => { - return response.json() - }) - .then((json) => { - if (json.mentor_memo) { - savedMemo = json.mentor_memo - } - placeholder.classList.add('is-hidden') - if (savedMemo.length === 0) { - emptyMessage.classList.remove('is-hidden') - } else { - memoDisplayContent.classList.remove('is-hidden') - editorTextarea.value = savedMemo - switchMemoDisplay(memoDisplay, savedMemo) - TextareaInitializer.initialize('#js-user-mentor-memo') - memoDisplayContent.innerHTML = markdownInitializer.render(savedMemo) - memoEditorPreview.innerHTML = markdownInitializer.render(savedMemo) - } - }) - .catch((error) => { - console.warn(error) - }) const editButton = memoDisplay.querySelector('.card-footer-actions__action') const modalElements = [memoDisplay, memoEditor] diff --git a/app/views/users/_mentor_memo.html.slim b/app/views/users/_mentor_memo.html.slim new file mode 100644 index 00000000000..92b6ba49035 --- /dev/null +++ b/app/views/users/_mentor_memo.html.slim @@ -0,0 +1,12 @@ +.mentor-memo data-memo_body="#{memo.body}" + a href="#{memo.writer.url}" + = memo.writer.login_name + div + = l memo.created_at + div.a-long-text.is-sm.memo-text + = memo.body + - if memo.writer == current_user || current_user.admin? + button.js-edit-memo + | 編集 + button.js-delete-memo + | 削除 diff --git a/app/views/users/_user_mentor_memo.html.slim b/app/views/users/_user_mentor_memos.html.slim similarity index 80% rename from app/views/users/_user_mentor_memo.html.slim rename to app/views/users/_user_mentor_memos.html.slim index 1b6958ad946..c3e220f626d 100644 --- a/app/views/users/_user_mentor_memo.html.slim +++ b/app/views/users/_user_mentor_memos.html.slim @@ -7,19 +7,15 @@ section.a-card.is-memo.is-only-mentor.user-mentor-memo data-user_id=user_id hr.a-border-tint .card-body .card-body__description - .a-long-text.is-sm.a-placeholder - p - p - p - p - p - p - .o-empty-message.is-hidden - .o-empty-message__icon - i.fa-regular.fa-sad-tear - .o-empty-message__text - | ユーザーメモはまだありません。 - .a-long-text.is-sm.user-mentor-memo-content.is-hidden + .a-long-text.is-sm.mentor-memo-list + - memos.each do |memo| + = render partial: 'users/mentor_memo', locals: { memo: memo } + - if memos.empty? + .o-empty-message + .o-empty-message__icon + i.fa-regular.fa-sad-tear + .o-empty-message__text + | ユーザーメモはまだありません。 hr.a-border-tint footer.card-footer .card-main-actions diff --git a/app/views/users/show.html.slim b/app/views/users/show.html.slim index 99a04dbf9ac..197f4f395e5 100644 --- a/app/views/users/show.html.slim +++ b/app/views/users/show.html.slim @@ -122,7 +122,7 @@ .o-empty-message__text | 進路メモはまだありません。 - if admin_or_mentor_login? - = render 'users/user_mentor_memo', user_id: @user.id + = render 'users/user_mentor_memos', user_id: @user.id, memos: @mentor_memos - if visible_learning_time_frames?(@user) .col-xs-12.col-lg-4.col-xxl-3 From a67f0aa3733d9c33ed542d83539f96d36189b31a Mon Sep 17 00:00:00 2001 From: matsuyama Date: Tue, 3 Mar 2026 21:15:12 +0900 Subject: [PATCH 03/29] =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=81=AB=E3=83=A1=E3=83=A2=E3=81=AE=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E6=A9=9F=E8=83=BD=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/mentor_memos_controller.rb | 20 ++- app/javascript/user_mentor_memo.js | 115 +++++++++++------- app/views/users/_user_mentor_memos.html.slim | 20 +-- config/routes/api.rb | 1 + 4 files changed, 95 insertions(+), 61 deletions(-) diff --git a/app/controllers/api/mentor_memos_controller.rb b/app/controllers/api/mentor_memos_controller.rb index 8d94f15fcd4..3e0f92f6684 100644 --- a/app/controllers/api/mentor_memos_controller.rb +++ b/app/controllers/api/mentor_memos_controller.rb @@ -4,7 +4,19 @@ class API::MentorMemosController < API::BaseController before_action :require_mentor_login_for_api before_action -> { doorkeeper_authorize! :write }, only: %i[update], if: -> { doorkeeper_token.present? } before_action -> { doorkeeper_authorize! :mentor }, only: %i[update], if: -> { doorkeeper_token.present? } - before_action :set_user, only: %i[update] + before_action :set_user, only: %i[create update] + + def create + memo = MentorMemo.new(mentor_memo_params) + memo.writer = current_user + memo.recipient = @user + + if memo.save + render partial: 'users/mentor_memo', locals: { memo: }, status: :created + else + head :bad_request + end + end def update if @user.update_mentor_memo(user_params[:mentor_memo]) @@ -17,10 +29,10 @@ def update private def set_user - @user = User.find(params[:id]) + @user = User.find(params[:user_id]) end - def user_params - params.require(:user).permit(:mentor_memo) + def mentor_memo_params + params.require(:mentor_memo).permit(:body) end end diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/user_mentor_memo.js index cf8fc71b14c..bb3ef36f61a 100644 --- a/app/javascript/user_mentor_memo.js +++ b/app/javascript/user_mentor_memo.js @@ -3,18 +3,16 @@ import TextareaInitializer from 'textarea-initializer' import MarkdownInitializer from 'markdown-initializer' document.addEventListener('DOMContentLoaded', () => { - const mentorMemo = document.querySelector('.user-mentor-memo') + const mentorMemo = document.querySelector('.mentor-memos') if (mentorMemo) { + TextareaInitializer.initialize('#js-new-memo') const markdownInitializer = new MarkdownInitializer() const userId = mentorMemo.dataset.user_id - let savedMemo = '' + const savedMemo = '' - const memoDisplay = mentorMemo.querySelector('.memo-display') - const memoEditor = mentorMemo.querySelector('.memo-editor') + const memoDisplay = mentorMemo.querySelector('.memos-display') + const memoEditor = mentorMemo.querySelector('.new-memo-editor') - const memoDisplayContent = memoDisplay.querySelector( - '.user-mentor-memo-content' - ) const memoEditorPreview = memoEditor.querySelector( '.a-markdown-input__preview' ) @@ -31,35 +29,51 @@ document.addEventListener('DOMContentLoaded', () => { memoDisplayContent.innerHTML = markdownInitializer.render(memoBody) }) - const editButton = memoDisplay.querySelector('.card-footer-actions__action') - const modalElements = [memoDisplay, memoEditor] - editButton.addEventListener('click', () => + const addButton = memoDisplay.querySelector('.js-add-memo') + const addContainer = memoDisplay.querySelector('.js-add-action') + const modalElements = [addContainer, memoEditor] + addButton.addEventListener('click', () => { toggleClass(modalElements, 'is-hidden') - ) + editorTextarea.focus() + }) - const saveButton = memoEditor.querySelector('.is-primary') - saveButton.addEventListener('click', () => { + const saveButton = memoEditor.querySelector('.js-save-memo') + saveButton.disabled = true + editorTextarea.addEventListener('input', () => { + saveButton.disabled = editorTextarea.value.trim().length === 0 + }) + + saveButton.addEventListener('click', async () => { + const memoContent = editorTextarea.value + const html = await createMemo(memoContent, userId) + if (!html) return + + const newMemo = initializeNewMemo(html, markdownInitializer) + newMemo.scrollIntoView() toggleClass(modalElements, 'is-hidden') - savedMemo = editorTextarea.value - updateMemo(savedMemo, userId) - memoDisplayContent.innerHTML = markdownInitializer.render(savedMemo) - TextareaInitializer.initialize('#js-user-mentor-memo') - switchMemoDisplay(memoDisplay, savedMemo) + editorTextarea.value = '' + memoEditorPreview.innerHTML = '' + TextareaInitializer.initialize('#js-new-memo') + + const emptyMessage = mentorMemo.querySelector('.o-empty-message') + if (emptyMessage) { + emptyMessage.classList.add('is-hidden') + } }) - const cancelButton = memoEditor.querySelector('.is-secondary') + const cancelButton = memoEditor.querySelector('.js-cancel-memo') cancelButton.addEventListener('click', () => { toggleClass(modalElements, 'is-hidden') editorTextarea.value = savedMemo memoEditorPreview.innerHTML = markdownInitializer.render(savedMemo) - TextareaInitializer.initialize('#js-user-mentor-memo') + TextareaInitializer.initialize('#js-new-memo') }) editorTextarea.addEventListener('change', () => { memoEditorPreview.innerHTML = markdownInitializer.render( editorTextarea.value ) - TextareaInitializer.initialize('#js-user-mentor-memo') + TextareaInitializer.initialize('#js-new-memo') }) const editorTab = memoEditor.querySelector('.editor-tab') @@ -82,38 +96,45 @@ document.addEventListener('DOMContentLoaded', () => { } }) -function updateMemo(memo, userId) { +async function createMemo(memo, userId) { const params = { - user: { - mentor_memo: memo + mentor_memo: { + body: memo } } - fetch(`/api/mentor_memos/${userId}`, { - method: 'PUT', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/json; charset=utf-8', - 'X-CSRF-Token': CSRF.getToken() - }, - credentials: 'same-origin', - redirect: 'manual', - body: JSON.stringify(params) - }) - .then((response) => { - return response - }) - .catch((error) => { - console.warn(error) + try { + const response = await fetch(`/api/users/${userId}/mentor_memos/`, { + method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json; charset=utf-8', + 'X-CSRF-Token': CSRF.getToken() + }, + credentials: 'same-origin', + redirect: 'manual', + body: JSON.stringify(params) }) + + if (!response.ok) throw new Error('Failed to fetch') + + return response.text() + } catch (error) { + console.warn(error) + return false + } } -function switchMemoDisplay(memoDisplay, memo) { - const memoDisplayContent = memoDisplay.querySelector( - '.user-mentor-memo-content' - ) - const emptyMessage = memoDisplay.querySelector('.o-empty-message') - memoDisplayContent.classList.toggle('is-hidden', memo.length === 0) - emptyMessage.classList.toggle('is-hidden', memo.length !== 0) +function initializeNewMemo(html, markdownInitializer) { + const memoList = document.querySelector('.mentor-memo-list') + const memoDiv = document.createElement('div') + memoDiv.innerHTML = html + const newMemoElement = memoDiv.firstElementChild + memoList.prepend(newMemoElement) + const memoBody = newMemoElement.dataset.memo_body + const memoDisplayContent = newMemoElement.querySelector('.memo-text') + memoDisplayContent.innerHTML = markdownInitializer.render(memoBody) + + return newMemoElement } function toggleClass(elements, className) { diff --git a/app/views/users/_user_mentor_memos.html.slim b/app/views/users/_user_mentor_memos.html.slim index c3e220f626d..f4ab4c96aa7 100644 --- a/app/views/users/_user_mentor_memos.html.slim +++ b/app/views/users/_user_mentor_memos.html.slim @@ -1,5 +1,5 @@ -section.a-card.is-memo.is-only-mentor.user-mentor-memo data-user_id=user_id - .memo-display +section.a-card.is-memo.is-only-mentor.mentor-memos data-user_id=user_id + .memos-display .description header.card-header.is-sm h2.card-header__title @@ -7,7 +7,7 @@ section.a-card.is-memo.is-only-mentor.user-mentor-memo data-user_id=user_id hr.a-border-tint .card-body .card-body__description - .a-long-text.is-sm.mentor-memo-list + .mentor-memo-list - memos.each do |memo| = render partial: 'users/mentor_memo', locals: { memo: memo } - if memos.empty? @@ -17,14 +17,14 @@ section.a-card.is-memo.is-only-mentor.user-mentor-memo data-user_id=user_id .o-empty-message__text | ユーザーメモはまだありません。 hr.a-border-tint - footer.card-footer + footer.card-footer.js-add-action .card-main-actions .card-main-actions__items .card-main-actions__item - button.card-footer-actions__action.a-button.is-sm.is-secondary.is-block + button.card-footer-actions__action.a-button.is-sm.is-block.js-add-memo i.fa-solid.fa-pen - | 編集 - .memo-editor.is-hidden + | 追加 + .new-memo-editor.is-hidden .form-tabs.js-tabs .form-tabs__tab.js-tabs__tab.editor-tab.is-active | メモ @@ -34,7 +34,7 @@ section.a-card.is-memo.is-only-mentor.user-mentor-memo data-user_id=user_id .card-body__description .a-markdown-input.js-markdown-parent .a-markdown-input__inner.is-editor.js-tabs__content.is-active - textarea.a-text-input.a-markdown-input__textarea id='js-user-mentor-memo' data-preview='#user-mentor-memo-preview' name='user[memo]' + textarea.a-text-input.a-markdown-input__textarea id='js-new-memo' data-preview='#user-memo-preview' name='mentor_memo[body]' .a-markdown-input__inner.is-preview.js-tabs__content .a-long-text.is-md.a-markdown-input__preview hr.a-border-tint @@ -42,8 +42,8 @@ section.a-card.is-memo.is-only-mentor.user-mentor-memo data-user_id=user_id .card-main-actions .card-main-actions__items .card-main-actions__item - button.a-button.is-sm.is-primary.is-block + button.a-button.is-sm.is-primary.is-block.js-save-memo | 保存する .card-main-actions__item - button.a-button.is-sm.is-secondary.is-block + button.a-button.is-sm.is-secondary.is-block.js-cancel-memo | キャンセル diff --git a/config/routes/api.rb b/config/routes/api.rb index c3e5b0f57ec..4398d388348 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -40,6 +40,7 @@ resources :mention_users, only: %i(index) resources :users, only: %i(index show update) do resource :support_context, only: %i(show), controller: 'users/support_contexts' + resources :mentor_memos, only: %i(create) end get "users/tags/:tag", to: "users#index", as: :users_tag, tag: /.+/ resources :practices, only: %i(index show update) do From a876e8d4476d2c44eb5f940fa7ddbfa9c5eaff31 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Wed, 3 Jun 2026 02:09:02 +0900 Subject: [PATCH 04/29] =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=81=AE=E5=88=9D=E6=9C=9F=E5=8C=96=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=82=92initializeMentorMemo.js=E3=81=AB=E5=88=86?= =?UTF-8?q?=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/initializeMentorMemo.js | 16 ++++++++++++++++ app/javascript/user_mentor_memo.js | 20 +++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 app/javascript/initializeMentorMemo.js diff --git a/app/javascript/initializeMentorMemo.js b/app/javascript/initializeMentorMemo.js new file mode 100644 index 00000000000..37d191bd223 --- /dev/null +++ b/app/javascript/initializeMentorMemo.js @@ -0,0 +1,16 @@ +import TextareaInitializer from 'textarea-initializer' +import MarkdownInitializer from 'markdown-initializer' + +export default function initializeMemo(memo) { + const markdownInitializer = new MarkdownInitializer() + + const memoId = memo.dataset.memo_id + const memoBody = memo.dataset.memo_body + TextareaInitializer.initialize(`#js-mentor-memo-${memoId}`) + + const memoDisplayContent = memo.querySelector('.memo-text') + + if (memoBody) { + memoDisplayContent.innerHTML = markdownInitializer.render(memoBody) + } +} diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/user_mentor_memo.js index bb3ef36f61a..67201de8a69 100644 --- a/app/javascript/user_mentor_memo.js +++ b/app/javascript/user_mentor_memo.js @@ -1,6 +1,7 @@ import CSRF from 'csrf' import TextareaInitializer from 'textarea-initializer' import MarkdownInitializer from 'markdown-initializer' +import initializeMemo from './initializeMentorMemo' document.addEventListener('DOMContentLoaded', () => { const mentorMemo = document.querySelector('.mentor-memos') @@ -8,7 +9,6 @@ document.addEventListener('DOMContentLoaded', () => { TextareaInitializer.initialize('#js-new-memo') const markdownInitializer = new MarkdownInitializer() const userId = mentorMemo.dataset.user_id - const savedMemo = '' const memoDisplay = mentorMemo.querySelector('.memos-display') const memoEditor = mentorMemo.querySelector('.new-memo-editor') @@ -22,11 +22,7 @@ document.addEventListener('DOMContentLoaded', () => { const memos = document.querySelectorAll('.mentor-memo') memos.forEach((memo) => { - const memoDisplayContent = memo.querySelector('.memo-text') - const memoBody = memo.dataset.memo_body - if (!memoBody) return - - memoDisplayContent.innerHTML = markdownInitializer.render(memoBody) + initializeMemo(memo) }) const addButton = memoDisplay.querySelector('.js-add-memo') @@ -48,7 +44,7 @@ document.addEventListener('DOMContentLoaded', () => { const html = await createMemo(memoContent, userId) if (!html) return - const newMemo = initializeNewMemo(html, markdownInitializer) + const newMemo = initializeNewMemo(html) newMemo.scrollIntoView() toggleClass(modalElements, 'is-hidden') editorTextarea.value = '' @@ -64,8 +60,8 @@ document.addEventListener('DOMContentLoaded', () => { const cancelButton = memoEditor.querySelector('.js-cancel-memo') cancelButton.addEventListener('click', () => { toggleClass(modalElements, 'is-hidden') - editorTextarea.value = savedMemo - memoEditorPreview.innerHTML = markdownInitializer.render(savedMemo) + editorTextarea.value = '' + memoEditorPreview.innerHTML = '' TextareaInitializer.initialize('#js-new-memo') }) @@ -124,15 +120,13 @@ async function createMemo(memo, userId) { } } -function initializeNewMemo(html, markdownInitializer) { +function initializeNewMemo(html) { const memoList = document.querySelector('.mentor-memo-list') const memoDiv = document.createElement('div') memoDiv.innerHTML = html const newMemoElement = memoDiv.firstElementChild memoList.prepend(newMemoElement) - const memoBody = newMemoElement.dataset.memo_body - const memoDisplayContent = newMemoElement.querySelector('.memo-text') - memoDisplayContent.innerHTML = markdownInitializer.render(memoBody) + initializeMemo(newMemoElement) return newMemoElement } From 48c394395364950b7ca96728abf517cc5c471a90 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Wed, 3 Jun 2026 05:05:12 +0900 Subject: [PATCH 05/29] =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=81=AB=E3=83=A1=E3=83=A2=E3=81=AE=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E6=A9=9F=E8=83=BD=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/mentor_memos_controller.rb | 7 +- app/javascript/initializeMentorMemo.js | 98 ++++++++++++++++++- app/javascript/user_mentor_memo.js | 8 +- app/views/users/_mentor_memo.html.slim | 49 +++++++--- config/routes/api.rb | 3 +- 5 files changed, 143 insertions(+), 22 deletions(-) diff --git a/app/controllers/api/mentor_memos_controller.rb b/app/controllers/api/mentor_memos_controller.rb index 3e0f92f6684..2d4c7ef8db5 100644 --- a/app/controllers/api/mentor_memos_controller.rb +++ b/app/controllers/api/mentor_memos_controller.rb @@ -5,6 +5,7 @@ class API::MentorMemosController < API::BaseController before_action -> { doorkeeper_authorize! :write }, only: %i[update], if: -> { doorkeeper_token.present? } before_action -> { doorkeeper_authorize! :mentor }, only: %i[update], if: -> { doorkeeper_token.present? } before_action :set_user, only: %i[create update] + before_action :set_memo, only: %i[update] def create memo = MentorMemo.new(mentor_memo_params) @@ -19,7 +20,7 @@ def create end def update - if @user.update_mentor_memo(user_params[:mentor_memo]) + if @mentor_memo.update(mentor_memo_params) head :ok else head :bad_request @@ -32,6 +33,10 @@ def set_user @user = User.find(params[:user_id]) end + def set_memo + @mentor_memo = @user.received_memos.find(params[:id]) + end + def mentor_memo_params params.require(:mentor_memo).permit(:body) end diff --git a/app/javascript/initializeMentorMemo.js b/app/javascript/initializeMentorMemo.js index 37d191bd223..2c338f82dd0 100644 --- a/app/javascript/initializeMentorMemo.js +++ b/app/javascript/initializeMentorMemo.js @@ -1,16 +1,108 @@ +import CSRF from 'csrf' import TextareaInitializer from 'textarea-initializer' import MarkdownInitializer from 'markdown-initializer' -export default function initializeMemo(memo) { +export default function initializeMemo(memo, userId) { const markdownInitializer = new MarkdownInitializer() const memoId = memo.dataset.memo_id const memoBody = memo.dataset.memo_body - TextareaInitializer.initialize(`#js-mentor-memo-${memoId}`) + let savedMemo = memoBody || '' + TextareaInitializer.initialize(`#js-memo-${memoId}`) + const memoDisplay = memo.querySelector('.memo-display') const memoDisplayContent = memo.querySelector('.memo-text') + const memoEditor = memo.querySelector('.memo-editor') + const editorTextarea = memo.querySelector('.a-markdown-input__textarea') + const editorPreview = memo.querySelector('.a-markdown-input__preview') + if (memoBody) { - memoDisplayContent.innerHTML = markdownInitializer.render(memoBody) + memoDisplayContent.innerHTML = markdownInitializer.render(savedMemo) + editorPreview.innerHTML = markdownInitializer.render(savedMemo) + } + + const editButton = memo.querySelector('.js-edit-memo') + if (editButton) { + editButton.addEventListener('click', () => { + toggleEditor() + }) + } + + const saveButton = memo.querySelector('.js-save-memo') + editorTextarea.addEventListener('input', () => { + saveButton.disabled = editorTextarea.value.length === 0 + }) + + saveButton.addEventListener('click', async () => { + try { + const editedMemo = editorTextarea.value + await updateMemo(memoId, editedMemo, userId) + savedMemo = editedMemo + toggleEditor() + memoDisplayContent.innerHTML = markdownInitializer.render(savedMemo) + } catch (error) { + console.warn(error) + } + }) + + const cancelButton = memo.querySelector('.js-cancel-memo') + cancelButton.addEventListener('click', () => { + toggleEditor() + editorTextarea.value = savedMemo + editorPreview.innerHTML = markdownInitializer.render(savedMemo) + }) + + const editorTab = memoEditor.querySelector('.editor-tab') + const editorTabContent = memoEditor.querySelector('.is-editor') + const previewTab = memoEditor.querySelector('.preview-tab') + const previewTabContent = memoEditor.querySelector('.is-preview') + + const tabElements = [ + editorTab, + editorTabContent, + previewTab, + previewTabContent + ] + editorTab.addEventListener('click', () => + toggleClass(tabElements, 'is-active') + ) + previewTab.addEventListener('click', () => { + editorPreview.innerHTML = markdownInitializer.render(editorTextarea.value) + toggleClass(tabElements, 'is-active') + }) + + async function updateMemo(memoId, memoContent, userId) { + const params = { + mentor_memo: { + body: memoContent + } + } + const response = await fetch( + `/api/users/${userId}/mentor_memos/${memoId}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-Token': CSRF.getToken() + }, + credentials: 'same-origin', + redirect: 'manual', + body: JSON.stringify(params) + } + ) + + if (!response.ok) throw new Error('Failed to update') + } + + function toggleEditor() { + toggleClass([memoDisplay, memoEditor], 'is-hidden') + } + + function toggleClass(elements, className) { + elements.forEach((element) => { + element.classList.toggle(className) + }) } } diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/user_mentor_memo.js index 67201de8a69..c9121f9507c 100644 --- a/app/javascript/user_mentor_memo.js +++ b/app/javascript/user_mentor_memo.js @@ -22,7 +22,7 @@ document.addEventListener('DOMContentLoaded', () => { const memos = document.querySelectorAll('.mentor-memo') memos.forEach((memo) => { - initializeMemo(memo) + initializeMemo(memo, userId) }) const addButton = memoDisplay.querySelector('.js-add-memo') @@ -44,7 +44,7 @@ document.addEventListener('DOMContentLoaded', () => { const html = await createMemo(memoContent, userId) if (!html) return - const newMemo = initializeNewMemo(html) + const newMemo = initializeNewMemo(html, userId) newMemo.scrollIntoView() toggleClass(modalElements, 'is-hidden') editorTextarea.value = '' @@ -120,13 +120,13 @@ async function createMemo(memo, userId) { } } -function initializeNewMemo(html) { +function initializeNewMemo(html, userId) { const memoList = document.querySelector('.mentor-memo-list') const memoDiv = document.createElement('div') memoDiv.innerHTML = html const newMemoElement = memoDiv.firstElementChild memoList.prepend(newMemoElement) - initializeMemo(newMemoElement) + initializeMemo(newMemoElement, userId) return newMemoElement } diff --git a/app/views/users/_mentor_memo.html.slim b/app/views/users/_mentor_memo.html.slim index 92b6ba49035..697d84bf2b6 100644 --- a/app/views/users/_mentor_memo.html.slim +++ b/app/views/users/_mentor_memo.html.slim @@ -1,12 +1,37 @@ -.mentor-memo data-memo_body="#{memo.body}" - a href="#{memo.writer.url}" - = memo.writer.login_name - div - = l memo.created_at - div.a-long-text.is-sm.memo-text - = memo.body - - if memo.writer == current_user || current_user.admin? - button.js-edit-memo - | 編集 - button.js-delete-memo - | 削除 +.mentor-memo data-memo_body="#{memo.body}" data-memo_id="#{memo.id}" + .memo-display + a href="#{memo.writer.url}" + = memo.writer.login_name + div + = l memo.created_at + div.a-long-text.is-sm.memo-text + = memo.body + - if memo.writer == current_user || current_user.admin? + button.js-edit-memo + | 編集 + button.js-delete-memo + | 削除 + .memo-editor.is-hidden + .form-tabs.js-tabs + .form-tabs__tab.js-tabs__tab.editor-tab.is-active + | メモ + .form-tabs__tab.js-tabs__tab.preview-tab + | プレビュー + .card-body + .card-body__description + .a-markdown-input.js-markdown-parent + .a-markdown-input__inner.is-editor.js-tabs__content.is-active + textarea.a-text-input.a-markdown-input__textarea id="js-memo-#{memo.id}" data-preview="#mentor-memo-preview-#{memo.id}" name='mentor_memo[body]' + = memo.body + .a-markdown-input__inner.is-preview.js-tabs__content + .a-long-text.is-md.a-markdown-input__preview + hr.a-border-tint + .card-footer + .card-main-actions + .card-main-actions__items + .card-main-actions__item + button.a-button.is-sm.is-primary.is-block.js-save-memo + | 保存する + .card-main-actions__item + button.a-button.is-sm.is-secondary.is-block.js-cancel-memo + | キャンセル diff --git a/config/routes/api.rb b/config/routes/api.rb index 4398d388348..67c99396739 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -40,7 +40,7 @@ resources :mention_users, only: %i(index) resources :users, only: %i(index show update) do resource :support_context, only: %i(show), controller: 'users/support_contexts' - resources :mentor_memos, only: %i(create) + resources :mentor_memos, only: %i(create update) end get "users/tags/:tag", to: "users#index", as: :users_tag, tag: /.+/ resources :practices, only: %i(index show update) do @@ -67,7 +67,6 @@ namespace 'watches' do resources :toggle, only: %i(index) end - resources :mentor_memos, only: %i(update) resources :tags, only: %i(index update) resources :pages, only: %i(update) resources :questions, only: %i(index show update) From 712fdd146c99d2221512d901ee145974516008a3 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Wed, 3 Jun 2026 21:26:08 +0900 Subject: [PATCH 06/29] =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=81=AB=E3=83=A1=E3=83=A2=E3=81=AE=E5=89=8A?= =?UTF-8?q?=E9=99=A4=E6=A9=9F=E8=83=BD=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/mentor_memos_controller.rb | 14 +++++-- app/javascript/initializeMentorMemo.js | 41 ++++++++++++++++++- config/routes/api.rb | 2 +- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/mentor_memos_controller.rb b/app/controllers/api/mentor_memos_controller.rb index 2d4c7ef8db5..62d3e523d1c 100644 --- a/app/controllers/api/mentor_memos_controller.rb +++ b/app/controllers/api/mentor_memos_controller.rb @@ -2,10 +2,10 @@ class API::MentorMemosController < API::BaseController before_action :require_mentor_login_for_api - before_action -> { doorkeeper_authorize! :write }, only: %i[update], if: -> { doorkeeper_token.present? } - before_action -> { doorkeeper_authorize! :mentor }, only: %i[update], if: -> { doorkeeper_token.present? } - before_action :set_user, only: %i[create update] - before_action :set_memo, only: %i[update] + before_action -> { doorkeeper_authorize! :write }, only: %i[update destroy], if: -> { doorkeeper_token.present? } + before_action -> { doorkeeper_authorize! :mentor }, only: %i[update destroy], if: -> { doorkeeper_token.present? } + before_action :set_user, only: %i[create update destroy] + before_action :set_memo, only: %i[update destroy] def create memo = MentorMemo.new(mentor_memo_params) @@ -27,6 +27,12 @@ def update end end + def destroy + @mentor_memo.destroy + + head :no_content + end + private def set_user diff --git a/app/javascript/initializeMentorMemo.js b/app/javascript/initializeMentorMemo.js index 2c338f82dd0..2a3dd21e5d4 100644 --- a/app/javascript/initializeMentorMemo.js +++ b/app/javascript/initializeMentorMemo.js @@ -3,12 +3,12 @@ import TextareaInitializer from 'textarea-initializer' import MarkdownInitializer from 'markdown-initializer' export default function initializeMemo(memo, userId) { - const markdownInitializer = new MarkdownInitializer() - const memoId = memo.dataset.memo_id const memoBody = memo.dataset.memo_body let savedMemo = memoBody || '' + TextareaInitializer.initialize(`#js-memo-${memoId}`) + const markdownInitializer = new MarkdownInitializer() const memoDisplay = memo.querySelector('.memo-display') const memoDisplayContent = memo.querySelector('.memo-text') @@ -53,6 +53,26 @@ export default function initializeMemo(memo, userId) { editorPreview.innerHTML = markdownInitializer.render(savedMemo) }) + const deleteButton = memo.querySelector('.js-delete-memo') + if (deleteButton) { + deleteButton.addEventListener('click', async () => { + if (window.confirm('本当によろしいですか?')) { + try { + await deleteMemo(memoId, userId) + memo.remove() + + const mentorMemos = document.querySelector('.mentor-memos') + const emptyMessage = mentorMemos.querySelector('.o-empty-message') + const memoList = mentorMemos.querySelectorAll('.mentor-memo') + + if (!memoList.length) emptyMessage.classList.remove('is-hidden') + } catch (error) { + console.warn(error) + } + } + }) + } + const editorTab = memoEditor.querySelector('.editor-tab') const editorTabContent = memoEditor.querySelector('.is-editor') const previewTab = memoEditor.querySelector('.preview-tab') @@ -96,6 +116,23 @@ export default function initializeMemo(memo, userId) { if (!response.ok) throw new Error('Failed to update') } + async function deleteMemo(memoId, userId) { + const response = await fetch( + `/api/users/${userId}/mentor_memos/${memoId}`, + { + method: 'DELETE', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-Token': CSRF.getToken() + }, + credentials: 'same-origin', + redirect: 'manual' + } + ) + + if (!response.ok) throw new Error('Failed to delete') + } + function toggleEditor() { toggleClass([memoDisplay, memoEditor], 'is-hidden') } diff --git a/config/routes/api.rb b/config/routes/api.rb index 67c99396739..ed4569e2324 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -40,7 +40,7 @@ resources :mention_users, only: %i(index) resources :users, only: %i(index show update) do resource :support_context, only: %i(show), controller: 'users/support_contexts' - resources :mentor_memos, only: %i(create update) + resources :mentor_memos, only: %i(create update destroy) end get "users/tags/:tag", to: "users#index", as: :users_tag, tag: /.+/ resources :practices, only: %i(index show update) do From 9a38e03070ffcf4a1571c171c093429a300ed16d Mon Sep 17 00:00:00 2001 From: matsuyama Date: Thu, 4 Jun 2026 21:00:25 +0900 Subject: [PATCH 07/29] =?UTF-8?q?=E9=87=8D=E8=A4=87=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=82=8BTextareaInitializer=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/user_mentor_memo.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/user_mentor_memo.js index c9121f9507c..caa7a5e3b83 100644 --- a/app/javascript/user_mentor_memo.js +++ b/app/javascript/user_mentor_memo.js @@ -49,7 +49,6 @@ document.addEventListener('DOMContentLoaded', () => { toggleClass(modalElements, 'is-hidden') editorTextarea.value = '' memoEditorPreview.innerHTML = '' - TextareaInitializer.initialize('#js-new-memo') const emptyMessage = mentorMemo.querySelector('.o-empty-message') if (emptyMessage) { @@ -62,14 +61,12 @@ document.addEventListener('DOMContentLoaded', () => { toggleClass(modalElements, 'is-hidden') editorTextarea.value = '' memoEditorPreview.innerHTML = '' - TextareaInitializer.initialize('#js-new-memo') }) editorTextarea.addEventListener('change', () => { memoEditorPreview.innerHTML = markdownInitializer.render( editorTextarea.value ) - TextareaInitializer.initialize('#js-new-memo') }) const editorTab = memoEditor.querySelector('.editor-tab') From 0d94badaa63fa6af082e65c4c2c653d2717e0b09 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Fri, 5 Jun 2026 03:12:19 +0900 Subject: [PATCH 08/29] =?UTF-8?q?=E3=83=97=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E8=A1=A8=E7=A4=BA=E3=82=92=E3=82=BF=E3=83=96?= =?UTF-8?q?=E3=82=AF=E3=83=AA=E3=83=83=E3=82=AF=E6=99=82=E3=81=AB=E5=AE=9F?= =?UTF-8?q?=E8=A1=8C=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/user_mentor_memo.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/user_mentor_memo.js index caa7a5e3b83..16abb7220bf 100644 --- a/app/javascript/user_mentor_memo.js +++ b/app/javascript/user_mentor_memo.js @@ -63,12 +63,6 @@ document.addEventListener('DOMContentLoaded', () => { memoEditorPreview.innerHTML = '' }) - editorTextarea.addEventListener('change', () => { - memoEditorPreview.innerHTML = markdownInitializer.render( - editorTextarea.value - ) - }) - const editorTab = memoEditor.querySelector('.editor-tab') const editorTabContent = memoEditor.querySelector('.is-editor') const previewTab = memoEditor.querySelector('.preview-tab') @@ -83,9 +77,12 @@ document.addEventListener('DOMContentLoaded', () => { editorTab.addEventListener('click', () => toggleClass(tabElements, 'is-active') ) - previewTab.addEventListener('click', () => + previewTab.addEventListener('click', () => { + memoEditorPreview.innerHTML = markdownInitializer.render( + editorTextarea.value + ) toggleClass(tabElements, 'is-active') - ) + }) } }) From 54d3a08c6ebb60f7b9ba721369b7f710885b399a Mon Sep 17 00:00:00 2001 From: matsuyama Date: Fri, 5 Jun 2026 03:22:13 +0900 Subject: [PATCH 09/29] =?UTF-8?q?mentorMemo=E3=81=AE=E3=83=81=E3=82=A7?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=82=92=E6=97=A9=E6=9C=9F=E3=83=AA=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=83=B3=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/user_mentor_memo.js | 137 ++++++++++++++--------------- 1 file changed, 67 insertions(+), 70 deletions(-) diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/user_mentor_memo.js index 16abb7220bf..d35ff4cc1ef 100644 --- a/app/javascript/user_mentor_memo.js +++ b/app/javascript/user_mentor_memo.js @@ -5,85 +5,82 @@ import initializeMemo from './initializeMentorMemo' document.addEventListener('DOMContentLoaded', () => { const mentorMemo = document.querySelector('.mentor-memos') - if (mentorMemo) { - TextareaInitializer.initialize('#js-new-memo') - const markdownInitializer = new MarkdownInitializer() - const userId = mentorMemo.dataset.user_id + if (!mentorMemo) return - const memoDisplay = mentorMemo.querySelector('.memos-display') - const memoEditor = mentorMemo.querySelector('.new-memo-editor') + TextareaInitializer.initialize('#js-new-memo') + const markdownInitializer = new MarkdownInitializer() + const userId = mentorMemo.dataset.user_id - const memoEditorPreview = memoEditor.querySelector( - '.a-markdown-input__preview' - ) - const editorTextarea = memoEditor.querySelector( - '.a-markdown-input__textarea' - ) + const memoDisplay = mentorMemo.querySelector('.memos-display') + const memoEditor = mentorMemo.querySelector('.new-memo-editor') - const memos = document.querySelectorAll('.mentor-memo') - memos.forEach((memo) => { - initializeMemo(memo, userId) - }) + const memoEditorPreview = memoEditor.querySelector( + '.a-markdown-input__preview' + ) + const editorTextarea = memoEditor.querySelector('.a-markdown-input__textarea') - const addButton = memoDisplay.querySelector('.js-add-memo') - const addContainer = memoDisplay.querySelector('.js-add-action') - const modalElements = [addContainer, memoEditor] - addButton.addEventListener('click', () => { - toggleClass(modalElements, 'is-hidden') - editorTextarea.focus() - }) + const memos = document.querySelectorAll('.mentor-memo') + memos.forEach((memo) => { + initializeMemo(memo, userId) + }) - const saveButton = memoEditor.querySelector('.js-save-memo') - saveButton.disabled = true - editorTextarea.addEventListener('input', () => { - saveButton.disabled = editorTextarea.value.trim().length === 0 - }) + const addButton = memoDisplay.querySelector('.js-add-memo') + const addContainer = memoDisplay.querySelector('.js-add-action') + const modalElements = [addContainer, memoEditor] + addButton.addEventListener('click', () => { + toggleClass(modalElements, 'is-hidden') + editorTextarea.focus() + }) - saveButton.addEventListener('click', async () => { - const memoContent = editorTextarea.value - const html = await createMemo(memoContent, userId) - if (!html) return - - const newMemo = initializeNewMemo(html, userId) - newMemo.scrollIntoView() - toggleClass(modalElements, 'is-hidden') - editorTextarea.value = '' - memoEditorPreview.innerHTML = '' - - const emptyMessage = mentorMemo.querySelector('.o-empty-message') - if (emptyMessage) { - emptyMessage.classList.add('is-hidden') - } - }) + const saveButton = memoEditor.querySelector('.js-save-memo') + saveButton.disabled = true + editorTextarea.addEventListener('input', () => { + saveButton.disabled = editorTextarea.value.trim().length === 0 + }) - const cancelButton = memoEditor.querySelector('.js-cancel-memo') - cancelButton.addEventListener('click', () => { - toggleClass(modalElements, 'is-hidden') - editorTextarea.value = '' - memoEditorPreview.innerHTML = '' - }) + saveButton.addEventListener('click', async () => { + const memoContent = editorTextarea.value + const html = await createMemo(memoContent, userId) + if (!html) return - const editorTab = memoEditor.querySelector('.editor-tab') - const editorTabContent = memoEditor.querySelector('.is-editor') - const previewTab = memoEditor.querySelector('.preview-tab') - const previewTabContent = memoEditor.querySelector('.is-preview') - - const tabElements = [ - editorTab, - editorTabContent, - previewTab, - previewTabContent - ] - editorTab.addEventListener('click', () => - toggleClass(tabElements, 'is-active') + const newMemo = initializeNewMemo(html, userId) + newMemo.scrollIntoView() + toggleClass(modalElements, 'is-hidden') + editorTextarea.value = '' + memoEditorPreview.innerHTML = '' + + const emptyMessage = mentorMemo.querySelector('.o-empty-message') + if (emptyMessage) { + emptyMessage.classList.add('is-hidden') + } + }) + + const cancelButton = memoEditor.querySelector('.js-cancel-memo') + cancelButton.addEventListener('click', () => { + toggleClass(modalElements, 'is-hidden') + editorTextarea.value = '' + memoEditorPreview.innerHTML = '' + }) + + const editorTab = memoEditor.querySelector('.editor-tab') + const editorTabContent = memoEditor.querySelector('.is-editor') + const previewTab = memoEditor.querySelector('.preview-tab') + const previewTabContent = memoEditor.querySelector('.is-preview') + + const tabElements = [ + editorTab, + editorTabContent, + previewTab, + previewTabContent + ] + editorTab.addEventListener('click', () => + toggleClass(tabElements, 'is-active') + ) + previewTab.addEventListener('click', () => { + memoEditorPreview.innerHTML = markdownInitializer.render( + editorTextarea.value ) - previewTab.addEventListener('click', () => { - memoEditorPreview.innerHTML = markdownInitializer.render( - editorTextarea.value - ) - toggleClass(tabElements, 'is-active') - }) - } + }) }) async function createMemo(memo, userId) { From 7c165df7f8e314434e9038572ee9f6ed7b48e2c7 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Fri, 5 Jun 2026 05:17:01 +0900 Subject: [PATCH 10/29] =?UTF-8?q?=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92@rails/request.js=E3=81=A7=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/initializeMentorMemo.js | 32 +++++--------------------- app/javascript/user_mentor_memo.js | 17 ++++---------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/app/javascript/initializeMentorMemo.js b/app/javascript/initializeMentorMemo.js index 2a3dd21e5d4..6d6c5ee4134 100644 --- a/app/javascript/initializeMentorMemo.js +++ b/app/javascript/initializeMentorMemo.js @@ -1,6 +1,6 @@ -import CSRF from 'csrf' import TextareaInitializer from 'textarea-initializer' import MarkdownInitializer from 'markdown-initializer' +import { put, destroy } from '@rails/request.js' export default function initializeMemo(memo, userId) { const memoId = memo.dataset.memo_id @@ -98,36 +98,16 @@ export default function initializeMemo(memo, userId) { body: memoContent } } - const response = await fetch( - `/api/users/${userId}/mentor_memos/${memoId}`, - { - method: 'PUT', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - 'X-Requested-With': 'XMLHttpRequest', - 'X-CSRF-Token': CSRF.getToken() - }, - credentials: 'same-origin', - redirect: 'manual', - body: JSON.stringify(params) - } - ) + const response = await put(`/api/users/${userId}/mentor_memos/${memoId}`, { + body: params + }) if (!response.ok) throw new Error('Failed to update') } async function deleteMemo(memoId, userId) { - const response = await fetch( - `/api/users/${userId}/mentor_memos/${memoId}`, - { - method: 'DELETE', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'X-CSRF-Token': CSRF.getToken() - }, - credentials: 'same-origin', - redirect: 'manual' - } + const response = await destroy( + `/api/users/${userId}/mentor_memos/${memoId}` ) if (!response.ok) throw new Error('Failed to delete') diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/user_mentor_memo.js index d35ff4cc1ef..2851713ea93 100644 --- a/app/javascript/user_mentor_memo.js +++ b/app/javascript/user_mentor_memo.js @@ -1,7 +1,7 @@ -import CSRF from 'csrf' import TextareaInitializer from 'textarea-initializer' import MarkdownInitializer from 'markdown-initializer' import initializeMemo from './initializeMentorMemo' +import { post } from '@rails/request.js' document.addEventListener('DOMContentLoaded', () => { const mentorMemo = document.querySelector('.mentor-memos') @@ -90,23 +90,16 @@ async function createMemo(memo, userId) { } } try { - const response = await fetch(`/api/users/${userId}/mentor_memos/`, { - method: 'POST', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/json; charset=utf-8', - 'X-CSRF-Token': CSRF.getToken() - }, - credentials: 'same-origin', - redirect: 'manual', - body: JSON.stringify(params) + const response = await post(`/api/users/${userId}/mentor_memos/`, { + body: params }) if (!response.ok) throw new Error('Failed to fetch') - return response.text() + return await response.text } catch (error) { console.warn(error) + return false } } From 3dead7a89e472f4fe700e9622e98d5c200c582f2 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Fri, 5 Jun 2026 07:24:19 +0900 Subject: [PATCH 11/29] =?UTF-8?q?=E3=82=A8=E3=83=87=E3=82=A3=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E5=86=8D=E8=A1=A8=E7=A4=BA=E6=99=82=E3=81=AE=E3=82=BF?= =?UTF-8?q?=E3=83=96=E3=81=AE=E7=8A=B6=E6=85=8B=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/initializeMentorMemo.js | 8 +++++++ app/javascript/user_mentor_memo.js | 33 +++++++++++++++++--------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/javascript/initializeMentorMemo.js b/app/javascript/initializeMentorMemo.js index 6d6c5ee4134..e1a6bb222e7 100644 --- a/app/javascript/initializeMentorMemo.js +++ b/app/javascript/initializeMentorMemo.js @@ -26,6 +26,7 @@ export default function initializeMemo(memo, userId) { if (editButton) { editButton.addEventListener('click', () => { toggleEditor() + resetEditorTabs() }) } @@ -122,4 +123,11 @@ export default function initializeMemo(memo, userId) { element.classList.toggle(className) }) } + + function resetEditorTabs() { + editorTab.classList.add('is-active') + editorTabContent.classList.add('is-active') + previewTab.classList.remove('is-active') + previewTabContent.classList.remove('is-active') + } } diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/user_mentor_memo.js index 2851713ea93..16402a797e2 100644 --- a/app/javascript/user_mentor_memo.js +++ b/app/javascript/user_mentor_memo.js @@ -19,6 +19,18 @@ document.addEventListener('DOMContentLoaded', () => { ) const editorTextarea = memoEditor.querySelector('.a-markdown-input__textarea') + const editorTab = memoEditor.querySelector('.editor-tab') + const editorTabContent = memoEditor.querySelector('.is-editor') + const previewTab = memoEditor.querySelector('.preview-tab') + const previewTabContent = memoEditor.querySelector('.is-preview') + + const tabElements = [ + editorTab, + editorTabContent, + previewTab, + previewTabContent + ] + const memos = document.querySelectorAll('.mentor-memo') memos.forEach((memo) => { initializeMemo(memo, userId) @@ -29,6 +41,7 @@ document.addEventListener('DOMContentLoaded', () => { const modalElements = [addContainer, memoEditor] addButton.addEventListener('click', () => { toggleClass(modalElements, 'is-hidden') + resetEditorTabs(tabElements) editorTextarea.focus() }) @@ -62,17 +75,6 @@ document.addEventListener('DOMContentLoaded', () => { memoEditorPreview.innerHTML = '' }) - const editorTab = memoEditor.querySelector('.editor-tab') - const editorTabContent = memoEditor.querySelector('.is-editor') - const previewTab = memoEditor.querySelector('.preview-tab') - const previewTabContent = memoEditor.querySelector('.is-preview') - - const tabElements = [ - editorTab, - editorTabContent, - previewTab, - previewTabContent - ] editorTab.addEventListener('click', () => toggleClass(tabElements, 'is-active') ) @@ -120,3 +122,12 @@ function toggleClass(elements, className) { element.classList.toggle(className) }) } + +function resetEditorTabs(tabElements) { + const [editorTab, editorTabContent, previewTab, previewTabContent] = + tabElements + editorTab.classList.add('is-active') + editorTabContent.classList.add('is-active') + previewTab.classList.remove('is-active') + previewTabContent.classList.remove('is-active') +} From b3d14fa7a479faabff5316d77c8eb190f41a5d35 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Fri, 5 Jun 2026 10:31:42 +0900 Subject: [PATCH 12/29] =?UTF-8?q?doorkeepr=20authorization=E3=81=ABcreate?= =?UTF-8?q?=E3=82=A2=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/mentor_memos_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/mentor_memos_controller.rb b/app/controllers/api/mentor_memos_controller.rb index 62d3e523d1c..cf2b9802ff9 100644 --- a/app/controllers/api/mentor_memos_controller.rb +++ b/app/controllers/api/mentor_memos_controller.rb @@ -2,8 +2,8 @@ class API::MentorMemosController < API::BaseController before_action :require_mentor_login_for_api - before_action -> { doorkeeper_authorize! :write }, only: %i[update destroy], if: -> { doorkeeper_token.present? } - before_action -> { doorkeeper_authorize! :mentor }, only: %i[update destroy], if: -> { doorkeeper_token.present? } + before_action -> { doorkeeper_authorize! :write }, only: %i[create update destroy], if: -> { doorkeeper_token.present? } + before_action -> { doorkeeper_authorize! :mentor }, only: %i[create update destroy], if: -> { doorkeeper_token.present? } before_action :set_user, only: %i[create update destroy] before_action :set_memo, only: %i[update destroy] From fefc37da53eb7241657664f6b5bf96cab5239b06 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Fri, 5 Jun 2026 13:10:42 +0900 Subject: [PATCH 13/29] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=90=8D=E3=82=92user=5Fmentor=5Fmemo.js=E3=81=8B=E3=82=89ment?= =?UTF-8?q?or-memos.js=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/importmap_application.js | 2 +- app/javascript/{user_mentor_memo.js => mentor-memos.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/javascript/{user_mentor_memo.js => mentor-memos.js} (100%) diff --git a/app/javascript/importmap_application.js b/app/javascript/importmap_application.js index 692368516d5..1c106e25421 100644 --- a/app/javascript/importmap_application.js +++ b/app/javascript/importmap_application.js @@ -49,7 +49,7 @@ import 'upload-image-to-article' import 'header-dropdown' import 'postal-code-address' import 'editor-selection-form' -import 'user_mentor_memo' +import 'mentor-memos' import 'invitation-url-updater' import 'movie-fileinput' import 'payment-methods-check-boxes' diff --git a/app/javascript/user_mentor_memo.js b/app/javascript/mentor-memos.js similarity index 100% rename from app/javascript/user_mentor_memo.js rename to app/javascript/mentor-memos.js From ccb612b2018c9b040a8a0577c6c6d1d94bd0948e Mon Sep 17 00:00:00 2001 From: matsuyama Date: Fri, 5 Jun 2026 20:42:06 +0900 Subject: [PATCH 14/29] =?UTF-8?q?=E3=83=91=E3=83=BC=E3=82=B7=E3=83=A3?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=97=E3=81=A8?= =?UTF-8?q?=E6=B8=A1=E3=81=99=E3=83=87=E3=83=BC=E3=82=BF=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/products_controller.rb | 1 + app/controllers/reports_controller.rb | 1 + app/controllers/talks_controller.rb | 1 + app/views/products/show.html.slim | 2 +- app/views/reports/show.html.slim | 2 +- app/views/talks/show.html.slim | 2 +- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb index 0b4df8b93e9..ec8253d3805 100644 --- a/app/controllers/products_controller.rb +++ b/app/controllers/products_controller.rb @@ -28,6 +28,7 @@ def show Footprint.find_or_create_for(@product, current_user) @footprints = Footprint.fetch_for_resource(@product) @comments = @product.comments.order(:created_at) + @mentor_memos = @product.user.received_memos.includes(:writer).order(created_at: :desc) respond_to do |format| format.html format.md diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index f0fb7ed671c..e3b67bd084f 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -25,6 +25,7 @@ def show @recent_reports = Report.list.where(user_id: @report.user.id).limit(10) Footprint.find_or_create_for(@report, current_user) @footprints = Footprint.fetch_for_resource(@report) + @mentor_memos = @report.user.received_memos.includes(:writer).order(created_at: :desc) respond_to do |format| format.html format.md diff --git a/app/controllers/talks_controller.rb b/app/controllers/talks_controller.rb index 31534a88ec9..481ad8d05e2 100644 --- a/app/controllers/talks_controller.rb +++ b/app/controllers/talks_controller.rb @@ -32,6 +32,7 @@ def index def show @comments = @talk.comments.order(:created_at) @reports = @user.reports.list.limit(20) + @mentor_memos = @user.received_memos.includes(:writer).order(created_at: :desc) end private diff --git a/app/views/products/show.html.slim b/app/views/products/show.html.slim index 6bbeb7caa13..ee52d3ab136 100644 --- a/app/views/products/show.html.slim +++ b/app/views/products/show.html.slim @@ -116,7 +116,7 @@ header.page-header = render partial: 'reports/recent_reports', locals: { header_display: true } .side-tabs-contents__item#side-tabs-content-2 - = render 'users/user_mentor_memo', user_id: @product.user.id + = render 'users/user_mentor_memos', user_id: @product.user.id, memos: @mentor_memos .side-tabs-contents__item#side-tabs-content-3 .a-card .user-data diff --git a/app/views/reports/show.html.slim b/app/views/reports/show.html.slim index f9be55fdbbb..91d2190b7e2 100644 --- a/app/views/reports/show.html.slim +++ b/app/views/reports/show.html.slim @@ -70,7 +70,7 @@ .side-tabs-contents__item#side-tabs-content-1 = render partial: 'reports/recent_reports', locals: { header_display: false } .side-tabs-contents__item#side-tabs-content-2.is-only-mentor - = render 'users/user_mentor_memo', user_id: @report.user_id + = render 'users/user_mentor_memos', user_id: @report.user_id, memos: @mentor_memos .side-tabs-contents__item#side-tabs-content-3.is-only-mentor .a-card .user-data diff --git a/app/views/talks/show.html.slim b/app/views/talks/show.html.slim index 0b0c12c2b80..fde136f3ce0 100644 --- a/app/views/talks/show.html.slim +++ b/app/views/talks/show.html.slim @@ -81,7 +81,7 @@ .a-card .user-data .user-data__row - = render 'users/user_mentor_memo', user_id: @user.id + = render 'users/user_mentor_memos', user_id: @user.id, memos: @mentor_memos .user-data__row = render 'users/user_secret_attributes', user: @user .user-data__row From c8d9d162f3c1417c7c87cd993e5d97cde6a0f961 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Fri, 5 Jun 2026 23:22:11 +0900 Subject: [PATCH 15/29] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AAdiv=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/users/_mentor_memo.html.slim | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/users/_mentor_memo.html.slim b/app/views/users/_mentor_memo.html.slim index 697d84bf2b6..69dfdc95f2a 100644 --- a/app/views/users/_mentor_memo.html.slim +++ b/app/views/users/_mentor_memo.html.slim @@ -2,9 +2,8 @@ .memo-display a href="#{memo.writer.url}" = memo.writer.login_name - div = l memo.created_at - div.a-long-text.is-sm.memo-text + .a-long-text.is-sm.memo-text = memo.body - if memo.writer == current_user || current_user.admin? button.js-edit-memo From d3c9b46dba07cfeb7c292b5ff8000a1e45a878f6 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Sat, 6 Jun 2026 05:33:30 +0900 Subject: [PATCH 16/29] =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=81=AE=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E3=83=BB=E5=89=8A=E9=99=A4=E3=81=AB=E6=89=80=E6=9C=89?= =?UTF-8?q?=E8=80=85=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/mentor_memos_controller.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/controllers/api/mentor_memos_controller.rb b/app/controllers/api/mentor_memos_controller.rb index cf2b9802ff9..b8ff2647ed1 100644 --- a/app/controllers/api/mentor_memos_controller.rb +++ b/app/controllers/api/mentor_memos_controller.rb @@ -6,6 +6,7 @@ class API::MentorMemosController < API::BaseController before_action -> { doorkeeper_authorize! :mentor }, only: %i[create update destroy], if: -> { doorkeeper_token.present? } before_action :set_user, only: %i[create update destroy] before_action :set_memo, only: %i[update destroy] + before_action :authorize_memo, only: %i[update destroy] def create memo = MentorMemo.new(mentor_memo_params) @@ -46,4 +47,10 @@ def set_memo def mentor_memo_params params.require(:mentor_memo).permit(:body) end + + def authorize_memo + return if @mentor_memo.writer == current_user || current_user.admin? + + head :forbidden + end end From 2cb8f1df68e398fce2519316d090a613299c5a38 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Sat, 6 Jun 2026 06:52:23 +0900 Subject: [PATCH 17/29] =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=81=8C=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E3=81=97=E3=81=AA=E3=81=84=E6=99=82=E3=81=AE=E3=83=A1?= =?UTF-8?q?=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E5=B8=B8=E6=99=82?= =?UTF-8?q?DOM=E3=81=AB=E7=BD=AE=E3=81=8D=E3=80=81=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=81=AE=E5=88=B6=E5=BE=A1=E3=82=92JS=E3=81=A7=E8=A1=8C?= =?UTF-8?q?=E3=81=86=E3=82=88=E3=81=86=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mentor-memos.js | 12 ++++++++---- app/views/users/_user_mentor_memos.html.slim | 11 +++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/javascript/mentor-memos.js b/app/javascript/mentor-memos.js index 16402a797e2..316d97d035d 100644 --- a/app/javascript/mentor-memos.js +++ b/app/javascript/mentor-memos.js @@ -32,9 +32,14 @@ document.addEventListener('DOMContentLoaded', () => { ] const memos = document.querySelectorAll('.mentor-memo') - memos.forEach((memo) => { - initializeMemo(memo, userId) - }) + const emptyMessage = mentorMemo.querySelector('.o-empty-message') + if (memos.length) { + memos.forEach((memo) => { + initializeMemo(memo, userId) + }) + } else { + emptyMessage.classList.remove('is-hidden') + } const addButton = memoDisplay.querySelector('.js-add-memo') const addContainer = memoDisplay.querySelector('.js-add-action') @@ -62,7 +67,6 @@ document.addEventListener('DOMContentLoaded', () => { editorTextarea.value = '' memoEditorPreview.innerHTML = '' - const emptyMessage = mentorMemo.querySelector('.o-empty-message') if (emptyMessage) { emptyMessage.classList.add('is-hidden') } diff --git a/app/views/users/_user_mentor_memos.html.slim b/app/views/users/_user_mentor_memos.html.slim index f4ab4c96aa7..767d0842551 100644 --- a/app/views/users/_user_mentor_memos.html.slim +++ b/app/views/users/_user_mentor_memos.html.slim @@ -10,12 +10,11 @@ section.a-card.is-memo.is-only-mentor.mentor-memos data-user_id=user_id .mentor-memo-list - memos.each do |memo| = render partial: 'users/mentor_memo', locals: { memo: memo } - - if memos.empty? - .o-empty-message - .o-empty-message__icon - i.fa-regular.fa-sad-tear - .o-empty-message__text - | ユーザーメモはまだありません。 + .o-empty-message.is-hidden + .o-empty-message__icon + i.fa-regular.fa-sad-tear + .o-empty-message__text + | ユーザーメモはまだありません。 hr.a-border-tint footer.card-footer.js-add-action .card-main-actions From 1e8c81ba58a27a0402a16491b29614c98b9f0d91 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Sun, 7 Jun 2026 03:26:15 +0900 Subject: [PATCH 18/29] =?UTF-8?q?resetEditorTabs=E3=82=92DOMContentLoaded?= =?UTF-8?q?=E5=86=85=E3=81=AB=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mentor-memos.js | 42 ++++++++++++++++------------------ 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/app/javascript/mentor-memos.js b/app/javascript/mentor-memos.js index 316d97d035d..05affde3872 100644 --- a/app/javascript/mentor-memos.js +++ b/app/javascript/mentor-memos.js @@ -19,18 +19,6 @@ document.addEventListener('DOMContentLoaded', () => { ) const editorTextarea = memoEditor.querySelector('.a-markdown-input__textarea') - const editorTab = memoEditor.querySelector('.editor-tab') - const editorTabContent = memoEditor.querySelector('.is-editor') - const previewTab = memoEditor.querySelector('.preview-tab') - const previewTabContent = memoEditor.querySelector('.is-preview') - - const tabElements = [ - editorTab, - editorTabContent, - previewTab, - previewTabContent - ] - const memos = document.querySelectorAll('.mentor-memo') const emptyMessage = mentorMemo.querySelector('.o-empty-message') if (memos.length) { @@ -46,7 +34,7 @@ document.addEventListener('DOMContentLoaded', () => { const modalElements = [addContainer, memoEditor] addButton.addEventListener('click', () => { toggleClass(modalElements, 'is-hidden') - resetEditorTabs(tabElements) + resetEditorTabs() editorTextarea.focus() }) @@ -79,6 +67,18 @@ document.addEventListener('DOMContentLoaded', () => { memoEditorPreview.innerHTML = '' }) + const editorTab = memoEditor.querySelector('.editor-tab') + const editorTabContent = memoEditor.querySelector('.is-editor') + const previewTab = memoEditor.querySelector('.preview-tab') + const previewTabContent = memoEditor.querySelector('.is-preview') + + const tabElements = [ + editorTab, + editorTabContent, + previewTab, + previewTabContent + ] + editorTab.addEventListener('click', () => toggleClass(tabElements, 'is-active') ) @@ -87,6 +87,13 @@ document.addEventListener('DOMContentLoaded', () => { editorTextarea.value ) }) + + function resetEditorTabs() { + editorTab.classList.add('is-active') + editorTabContent.classList.add('is-active') + previewTab.classList.remove('is-active') + previewTabContent.classList.remove('is-active') + } }) async function createMemo(memo, userId) { @@ -126,12 +133,3 @@ function toggleClass(elements, className) { element.classList.toggle(className) }) } - -function resetEditorTabs(tabElements) { - const [editorTab, editorTabContent, previewTab, previewTabContent] = - tabElements - editorTab.classList.add('is-active') - editorTabContent.classList.add('is-active') - previewTab.classList.remove('is-active') - previewTabContent.classList.remove('is-active') -} From 11e194ebfcaf1e4b7e33ebb41dd84c9db5cb5af8 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Sun, 7 Jun 2026 04:51:08 +0900 Subject: [PATCH 19/29] =?UTF-8?q?save=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=AEd?= =?UTF-8?q?isabled=E7=8A=B6=E6=85=8B=E3=81=8C=E6=AD=A3=E3=81=97=E3=81=8F?= =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/initializeMentorMemo.js | 1 + app/javascript/mentor-memos.js | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/javascript/initializeMentorMemo.js b/app/javascript/initializeMentorMemo.js index e1a6bb222e7..de123ebc355 100644 --- a/app/javascript/initializeMentorMemo.js +++ b/app/javascript/initializeMentorMemo.js @@ -52,6 +52,7 @@ export default function initializeMemo(memo, userId) { toggleEditor() editorTextarea.value = savedMemo editorPreview.innerHTML = markdownInitializer.render(savedMemo) + saveButton.disabled = editorTextarea.value.trim().length === 0 }) const deleteButton = memo.querySelector('.js-delete-memo') diff --git a/app/javascript/mentor-memos.js b/app/javascript/mentor-memos.js index 05affde3872..adbb7988453 100644 --- a/app/javascript/mentor-memos.js +++ b/app/javascript/mentor-memos.js @@ -51,9 +51,7 @@ document.addEventListener('DOMContentLoaded', () => { const newMemo = initializeNewMemo(html, userId) newMemo.scrollIntoView() - toggleClass(modalElements, 'is-hidden') - editorTextarea.value = '' - memoEditorPreview.innerHTML = '' + closeEditor() if (emptyMessage) { emptyMessage.classList.add('is-hidden') @@ -62,9 +60,7 @@ document.addEventListener('DOMContentLoaded', () => { const cancelButton = memoEditor.querySelector('.js-cancel-memo') cancelButton.addEventListener('click', () => { - toggleClass(modalElements, 'is-hidden') - editorTextarea.value = '' - memoEditorPreview.innerHTML = '' + closeEditor() }) const editorTab = memoEditor.querySelector('.editor-tab') @@ -94,6 +90,13 @@ document.addEventListener('DOMContentLoaded', () => { previewTab.classList.remove('is-active') previewTabContent.classList.remove('is-active') } + + function closeEditor() { + toggleClass(modalElements, 'is-hidden') + editorTextarea.value = '' + memoEditorPreview.innerHTML = '' + saveButton.disabled = true + } }) async function createMemo(memo, userId) { From a1c26d4f2316b0539684e53fe7af8a07a5fae5ff Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 8 Jun 2026 08:04:22 +0900 Subject: [PATCH 20/29] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=83=95=E3=82=A3=E3=82=AF=E3=82=B9=E3=83=81=E3=83=A3=E3=81=8B?= =?UTF-8?q?=E3=82=89mentor=5Fmemo=E5=B1=9E=E6=80=A7=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/fixtures/users.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 91889968d96..98e6c1e3d3b 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -224,7 +224,6 @@ kimura: updated_at: "2014-01-01 00:00:07" created_at: "2014-01-01 00:00:07" last_activity_at: "2014-01-01 00:00:07" - mentor_memo: "kimuraさんのメモ" country_code: JP subdivision_code: '13' show_study_streak: true From 4ec5c32416b171a209cbda5d87e4f53962763eb6 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 8 Jun 2026 08:07:09 +0900 Subject: [PATCH 21/29] =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?User::MemoTest=E3=81=AB=E9=9B=86=E7=B4=84=E3=81=97=E3=80=81?= =?UTF-8?q?=E9=87=8D=E8=A4=87=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/models/user_test.rb | 11 ---- test/system/products/mentor_test.rb | 36 ------------ test/system/talks/content_test.rb | 11 ---- test/system/user/memos_test.rb | 90 ++++++++++++++++++++++++----- 4 files changed, 76 insertions(+), 72 deletions(-) diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 407f448daaa..58758ab7495 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -367,17 +367,6 @@ class UserTest < ActiveSupport::TestCase assert Following.find_by(follower_id: kimura.id, followed_id: hatsuno.id) end - test '#update_user_mentor_memo' do - user = users(:kimura) - assert_equal 'kimuraさんのメモ', user.mentor_memo - user.updated_at = Time.zone.local(2020, 1, 1, 0, 0, 0) - user.update_mentor_memo('新規メモ') - travel_to Time.zone.local(2020, 1, 1, 0, 0, 0) do - assert user.updated_at - end - assert_equal '新規メモ', user.mentor_memo - end - test '.delayed when there are users within 2 weeks from completion of last practice' do user = users(:nippounashi) practice1 = practices(:practice1) diff --git a/test/system/products/mentor_test.rb b/test/system/products/mentor_test.rb index 5ca1d1ba03b..a89854a4564 100644 --- a/test/system/products/mentor_test.rb +++ b/test/system/products/mentor_test.rb @@ -38,42 +38,6 @@ class MentorTest < ApplicationSystemTestCase end end - test 'display the user memos after click on user-memos tab' do - visit_with_auth "/products/#{products(:product2).id}", 'komagata' - find('#side-tabs-nav-2').click - assert_text 'kimuraさんのメモ' - end - - test 'can cancel editing of user-memos' do - visit_with_auth "/products/#{products(:product2).id}", 'komagata' - find('#side-tabs-nav-2').click - click_button '編集' - fill_in 'js-user-mentor-memo', with: '編集はできないはずです。' - click_button 'キャンセル' - assert_no_text '編集はできないはずです。' - assert_text 'kimuraさんのメモ' - end - - test 'can preview editing of user-memos' do - visit_with_auth "/products/#{products(:product2).id}", 'komagata' - find('#side-tabs-nav-2').click - assert_text 'kimuraさんのメモ' - click_button '編集' - fill_in 'js-user-mentor-memo', with: 'プレビューができます。' - find('.form-tabs__tab', text: 'プレビュー').click - assert_text 'プレビューができます。' - end - - test 'can update user-memos' do - visit_with_auth "/products/#{products(:product2).id}", 'komagata' - find('#side-tabs-nav-2').click - assert_text 'kimuraさんのメモ' - click_button '編集' - fill_in 'js-user-mentor-memo', with: '編集後のユーザーメモです。' - click_button '保存する' - assert_text '編集後のユーザーメモです。', wait: 10 - end - test 'display a list of products in side-column' do user = users(:kimura) visit_with_auth "/products/#{products(:product2).id}", 'mentormentaro' diff --git a/test/system/talks/content_test.rb b/test/system/talks/content_test.rb index 31160218e08..f0fa31e5f83 100644 --- a/test/system/talks/content_test.rb +++ b/test/system/talks/content_test.rb @@ -16,17 +16,6 @@ class ContentTest < ApplicationSystemTestCase assert_text 'ユーザー公開情報' end - test 'update memo' do - user = users(:kimura) - visit_with_auth "/talks/#{user.talk.id}", 'komagata' - assert_text 'kimuraさんのメモ' - click_button '編集' - fill_in 'js-user-mentor-memo', with: '相談部屋テストメモ' - click_button '保存する' - assert_text '相談部屋テストメモ' - assert_no_text 'kimuraさんのメモ' - end - test 'Displays a list of the 10 most recent reports' do user = users(:hajime) visit_with_auth "/talks/#{user.talk.id}", 'komagata' diff --git a/test/system/user/memos_test.rb b/test/system/user/memos_test.rb index 0dcdea6b15e..50db23419df 100644 --- a/test/system/user/memos_test.rb +++ b/test/system/user/memos_test.rb @@ -3,26 +3,88 @@ require 'application_system_test_case' class User::MemoTest < ApplicationSystemTestCase - test 'update memo' do - visit_with_auth user_path(users(:hatsuno)), 'komagata' - assert_text 'ユーザーメモはまだありません。' - click_button '編集' - fill_in 'js-user-mentor-memo', with: 'ユーザーメンターメモ' + setup do + @memo = MentorMemo.create!( + recipient: users(:kimura), + writer: users(:mentormentaro), + body: 'kimuraさんのメモ' + ) + end + + test 'add memo' do + visit_with_auth user_path(users(:kimura)), 'mentormentaro' + click_button '追加' + fill_in 'js-new-memo', with: '新しいメモ' click_button '保存する' - assert_text 'ユーザーメンターメモ' - assert_no_text 'ユーザーメモはまだありません。' + assert_text '新しいメモ' + end + + test 'update memo' do + visit_with_auth user_path(users(:kimura)), 'mentormentaro' + assert_text 'kimuraさんのメモ' + + within(".mentor-memo[data-memo_id='#{@memo.id}']") do + click_button '編集' + fill_in "js-memo-#{@memo.id}", with: '編集されたメモ' + click_button '保存する' + end + assert_text '編集されたメモ' + assert_no_text 'kimuraさんのメモ' + end + + test 'delete memo' do + visit_with_auth user_path(users(:kimura)), 'mentormentaro' + assert_text 'kimuraさんのメモ' + + within(".mentor-memo[data-memo_id='#{@memo.id}']") do + accept_confirm do + click_button '削除' + end + end + assert_no_text 'kimuraさんのメモ' end - test 'do not update memo when cancel' do + test 'admin can edit and delete others memo' do visit_with_auth user_path(users(:kimura)), 'komagata' + assert_button '編集' + assert_button '削除' + end + + test 'non writer cannot edit or delete memo' do + visit_with_auth user_path(users(:kimura)), 'long-id-mentor' + assert_no_button '編集' + assert_no_button '削除' + end + + test 'cancel editing does not update memo' do + visit_with_auth user_path(users(:kimura)), 'mentormentaro' assert_text 'kimuraさんのメモ' - assert_no_text 'ユーザーメモはまだありません。' - click_button '編集' - fill_in 'js-user-mentor-memo', with: 'ユーザーメンターメモ' - click_button 'キャンセル' - assert_no_text 'ユーザーメンターメモ' + + within(".mentor-memo[data-memo_id='#{@memo.id}']") do + click_button '編集' + fill_in "js-memo-#{@memo.id}", with: '編集されたメモ' + click_button 'キャンセル' + end assert_text 'kimuraさんのメモ' - assert_no_text 'ユーザーメモはまだありません。' + assert_no_text '編集されたメモ' + end + + test 'can preview while editing memo' do + visit_with_auth user_path(users(:kimura)), 'mentormentaro' + assert_text 'kimuraさんのメモ' + + within(".mentor-memo[data-memo_id='#{@memo.id}']") do + click_button '編集' + fill_in "js-memo-#{@memo.id}", with: 'プレビューができます。' + find('.form-tabs__tab', text: 'プレビュー').click + end + assert_text 'プレビューができます。' + end + + test 'show empty message when no memos' do + visit_with_auth user_path(users(:hajime)), 'mentormentaro' + assert_text 'ユーザーメモはまだありません。' + assert_no_selector '.mentor-memo' end test 'admin can see memo' do From 08dbe0402f0039802201003d5022b5e49d3f35d6 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Wed, 10 Jun 2026 06:27:26 +0900 Subject: [PATCH 22/29] =?UTF-8?q?=E3=82=AF=E3=82=A8=E3=83=AA=E3=81=AE?= =?UTF-8?q?=E3=82=B9=E3=82=B3=E3=83=BC=E3=83=97=E3=82=92mentor-memo?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mentor-memos.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mentor-memos.js b/app/javascript/mentor-memos.js index adbb7988453..a73571817db 100644 --- a/app/javascript/mentor-memos.js +++ b/app/javascript/mentor-memos.js @@ -19,7 +19,7 @@ document.addEventListener('DOMContentLoaded', () => { ) const editorTextarea = memoEditor.querySelector('.a-markdown-input__textarea') - const memos = document.querySelectorAll('.mentor-memo') + const memos = mentorMemo.querySelectorAll('.mentor-memo') const emptyMessage = mentorMemo.querySelector('.o-empty-message') if (memos.length) { memos.forEach((memo) => { From 235b0f49890fad2522e883ebd47132ca9d2dd0fb Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 17 Jun 2026 07:35:40 +0900 Subject: [PATCH 23/29] =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E5=90=91=E3=81=91=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=83=A1?= =?UTF-8?q?=E3=83=A2=E3=81=AE=E3=83=87=E3=82=B6=E3=82=A4=E3=83=B3=E3=82=92?= =?UTF-8?q?=E6=95=B4=E3=81=88=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/application.css | 1 + .../application/blocks/user/_mentor-memos.css | 79 +++++++++++++++++++ app/views/users/_mentor_memo.html.slim | 22 ++++-- app/views/users/_user_mentor_memos.html.slim | 19 +++-- 4 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 app/assets/stylesheets/application/blocks/user/_mentor-memos.css diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 7a041441082..4fef301af23 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -102,6 +102,7 @@ @import "./application/blocks/user/_completed-practices-progress.css"; @import "./application/blocks/user/_following.css"; @import "./application/blocks/user/_group-company-name.css"; +@import "./application/blocks/user/_mentor-memos.css"; @import "./application/blocks/user/_niconico-calendar.css"; @import "./application/blocks/user/_sns-links.css"; @import "./application/blocks/user/_user-data.css"; diff --git a/app/assets/stylesheets/application/blocks/user/_mentor-memos.css b/app/assets/stylesheets/application/blocks/user/_mentor-memos.css new file mode 100644 index 00000000000..1b69bb10a4f --- /dev/null +++ b/app/assets/stylesheets/application/blocks/user/_mentor-memos.css @@ -0,0 +1,79 @@ +.mentor-memo-list { + display: flex; + flex-direction: column; +} + +.mentor-memo { + font-size: 0.875rem; + line-height: 1.6; + padding: 1rem; +} + +@media (width <= 47.9375em) { + .mentor-memo { + padding: 0.75rem; + } +} + +.mentor-memo:not(:last-child) { + border-bottom: solid 1px var(--border-tint); +} + +.mentor-memo__meta { + display: flex; + align-items: center; + gap: 0.375rem; + margin-bottom: 0.5rem; +} + +.mentor-memo__writer-link { + display: inline-flex; + flex: 0 0 auto; + line-height: 1; +} + +.mentor-memo__writer { + font-size: 0.875rem; + line-height: 1.4; + font-weight: 600; +} + +.mentor-memo__created-at { + margin-left: auto; + font-size: 0.75rem; + line-height: 1.4; + white-space: nowrap; + color: var(--muted-text); +} + +.mentor-memo__body { + margin-bottom: 0.5rem; +} + +.mentor-memo__actions { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.mentor-memo__action { + font-size: 0.75rem; + line-height: 1; + display: inline-flex; + align-items: center; + gap: 0.25rem; + cursor: pointer; + background-color: transparent; + border: none; + padding: 0; + color: var(--muted-text); + transition: color 0.2s ease-in; +} + +.mentor-memo__action:hover { + color: var(--link-text); +} + +.mentor-memo__action.is-danger:hover { + color: var(--danger); +} diff --git a/app/views/users/_mentor_memo.html.slim b/app/views/users/_mentor_memo.html.slim index 69dfdc95f2a..c874f77e87b 100644 --- a/app/views/users/_mentor_memo.html.slim +++ b/app/views/users/_mentor_memo.html.slim @@ -1,15 +1,21 @@ .mentor-memo data-memo_body="#{memo.body}" data-memo_id="#{memo.id}" .memo-display - a href="#{memo.writer.url}" - = memo.writer.login_name - = l memo.created_at - .a-long-text.is-sm.memo-text + .mentor-memo__meta + = render 'users/icon', user: memo.writer, image_class: 'mentor-memo__writer-icon is-xs', link_class: 'mentor-memo__writer-link' + a.mentor-memo__writer.a-text-link href="#{memo.writer.url}" + = memo.writer.login_name + time.mentor-memo__created-at + = l memo.created_at + .a-long-text.is-sm.memo-text.mentor-memo__body = memo.body - if memo.writer == current_user || current_user.admin? - button.js-edit-memo - | 編集 - button.js-delete-memo - | 削除 + .mentor-memo__actions + button.mentor-memo__action.js-edit-memo + i.fa-solid.fa-pen + | 編集 + button.mentor-memo__action.is-danger.js-delete-memo + i.fa-solid.fa-trash-can + | 削除 .memo-editor.is-hidden .form-tabs.js-tabs .form-tabs__tab.js-tabs__tab.editor-tab.is-active diff --git a/app/views/users/_user_mentor_memos.html.slim b/app/views/users/_user_mentor_memos.html.slim index 767d0842551..5ca9a80915c 100644 --- a/app/views/users/_user_mentor_memos.html.slim +++ b/app/views/users/_user_mentor_memos.html.slim @@ -6,21 +6,20 @@ section.a-card.is-memo.is-only-mentor.mentor-memos data-user_id=user_id | メンター向けユーザーメモ hr.a-border-tint .card-body - .card-body__description - .mentor-memo-list - - memos.each do |memo| - = render partial: 'users/mentor_memo', locals: { memo: memo } - .o-empty-message.is-hidden - .o-empty-message__icon - i.fa-regular.fa-sad-tear - .o-empty-message__text - | ユーザーメモはまだありません。 + .mentor-memo-list + - memos.each do |memo| + = render partial: 'users/mentor_memo', locals: { memo: memo } + .o-empty-message.is-hidden + .o-empty-message__icon + i.fa-regular.fa-sad-tear + .o-empty-message__text + | ユーザーメモはまだありません。 hr.a-border-tint footer.card-footer.js-add-action .card-main-actions .card-main-actions__items .card-main-actions__item - button.card-footer-actions__action.a-button.is-sm.is-block.js-add-memo + button.a-button.is-sm.is-secondary.is-block.js-add-memo i.fa-solid.fa-pen | 追加 .new-memo-editor.is-hidden From 6c3ea4573749d06461893a9d6c9d6e82bde0ad37 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 17 Jun 2026 07:39:45 +0900 Subject: [PATCH 24/29] =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E5=90=91=E3=81=91=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=83=A1?= =?UTF-8?q?=E3=83=A2=E3=81=AEhtml=E3=81=AE=E6=A7=8B=E9=80=A0=E3=82=92?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/blocks/user/_mentor-memos.css | 5 ++- app/views/users/_mentor_memo.html.slim | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/assets/stylesheets/application/blocks/user/_mentor-memos.css b/app/assets/stylesheets/application/blocks/user/_mentor-memos.css index 1b69bb10a4f..f13aa717d7d 100644 --- a/app/assets/stylesheets/application/blocks/user/_mentor-memos.css +++ b/app/assets/stylesheets/application/blocks/user/_mentor-memos.css @@ -6,11 +6,14 @@ .mentor-memo { font-size: 0.875rem; line-height: 1.6; +} + +.mentor-memo__inner { padding: 1rem; } @media (width <= 47.9375em) { - .mentor-memo { + .mentor-memo__inner { padding: 0.75rem; } } diff --git a/app/views/users/_mentor_memo.html.slim b/app/views/users/_mentor_memo.html.slim index c874f77e87b..414c331f976 100644 --- a/app/views/users/_mentor_memo.html.slim +++ b/app/views/users/_mentor_memo.html.slim @@ -1,21 +1,22 @@ .mentor-memo data-memo_body="#{memo.body}" data-memo_id="#{memo.id}" .memo-display - .mentor-memo__meta - = render 'users/icon', user: memo.writer, image_class: 'mentor-memo__writer-icon is-xs', link_class: 'mentor-memo__writer-link' - a.mentor-memo__writer.a-text-link href="#{memo.writer.url}" - = memo.writer.login_name - time.mentor-memo__created-at - = l memo.created_at - .a-long-text.is-sm.memo-text.mentor-memo__body - = memo.body - - if memo.writer == current_user || current_user.admin? - .mentor-memo__actions - button.mentor-memo__action.js-edit-memo - i.fa-solid.fa-pen - | 編集 - button.mentor-memo__action.is-danger.js-delete-memo - i.fa-solid.fa-trash-can - | 削除 + .mentor-memo__inner + .mentor-memo__meta + = render 'users/icon', user: memo.writer, image_class: 'mentor-memo__writer-icon is-xs', link_class: 'mentor-memo__writer-link' + a.mentor-memo__writer.a-text-link href="#{memo.writer.url}" + = memo.writer.login_name + time.mentor-memo__created-at + = l memo.created_at + .a-long-text.is-sm.memo-text.mentor-memo__body + = memo.body + - if memo.writer == current_user || current_user.admin? + .mentor-memo__actions + button.mentor-memo__action.js-edit-memo + i.fa-solid.fa-pen + | 編集 + button.mentor-memo__action.is-danger.js-delete-memo + i.fa-solid.fa-trash-can + | 削除 .memo-editor.is-hidden .form-tabs.js-tabs .form-tabs__tab.js-tabs__tab.editor-tab.is-active From e36784034450ad1eb3f5f0118f943a5d04c237a5 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Thu, 18 Jun 2026 01:12:19 +0900 Subject: [PATCH 25/29] =?UTF-8?q?=E3=83=97=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=83=96=E3=81=8C=E6=AD=A3=E3=81=97=E3=81=8F?= =?UTF-8?q?=E5=88=87=E3=82=8A=E6=9B=BF=E3=82=8F=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mentor-memos.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/mentor-memos.js b/app/javascript/mentor-memos.js index a73571817db..c04b5a0d347 100644 --- a/app/javascript/mentor-memos.js +++ b/app/javascript/mentor-memos.js @@ -82,6 +82,7 @@ document.addEventListener('DOMContentLoaded', () => { memoEditorPreview.innerHTML = markdownInitializer.render( editorTextarea.value ) + toggleClass(tabElements, 'is-active') }) function resetEditorTabs() { From 04f57ee9752af8af14bca02db40ffb7ed8f5416f Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 22 Jun 2026 11:45:27 +0900 Subject: [PATCH 26/29] =?UTF-8?q?=E6=97=A2=E5=AD=98=E3=81=AE=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=82=BF=E3=83=BC=E3=83=A1=E3=83=A2=E3=82=92MentorMem?= =?UTF-8?q?o=E3=81=AB=E3=82=B3=E3=83=94=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...copy_user_mentor_memo_column_to_mentor_memos.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 db/data/20260622000124_copy_user_mentor_memo_column_to_mentor_memos.rb diff --git a/db/data/20260622000124_copy_user_mentor_memo_column_to_mentor_memos.rb b/db/data/20260622000124_copy_user_mentor_memo_column_to_mentor_memos.rb new file mode 100644 index 00000000000..7ac32fff9d1 --- /dev/null +++ b/db/data/20260622000124_copy_user_mentor_memo_column_to_mentor_memos.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CopyUserMentorMemoColumnToMentorMemos < ActiveRecord::Migration[8.1] + def up + pjord = User.find_by!(login_name: "pjord") + User.where.not(mentor_memo: [nil, ""] ).find_each do |user| + MentorMemo.create!(recipient: user, writer: pjord, body: "【2026年6月以前のメモ】\n#{user.mentor_memo}") + end + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end From e1ce7c13a31347b7dccdc796c31799c4848fa573 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Wed, 24 Jun 2026 11:04:40 +0900 Subject: [PATCH 27/29] =?UTF-8?q?=E9=96=A2=E6=95=B0=E5=90=8D=E3=82=92initi?= =?UTF-8?q?alizeNewMemo=E3=81=8B=E3=82=89addNewMemo=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mentor-memos.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mentor-memos.js b/app/javascript/mentor-memos.js index c04b5a0d347..89af23676d1 100644 --- a/app/javascript/mentor-memos.js +++ b/app/javascript/mentor-memos.js @@ -49,7 +49,7 @@ document.addEventListener('DOMContentLoaded', () => { const html = await createMemo(memoContent, userId) if (!html) return - const newMemo = initializeNewMemo(html, userId) + const newMemo = addNewMemo(html, userId) newMemo.scrollIntoView() closeEditor() @@ -121,7 +121,7 @@ async function createMemo(memo, userId) { } } -function initializeNewMemo(html, userId) { +function addNewMemo(html, userId) { const memoList = document.querySelector('.mentor-memo-list') const memoDiv = document.createElement('div') memoDiv.innerHTML = html From 6a424591a4ecd8a311a54a258b179be5cffb765a Mon Sep 17 00:00:00 2001 From: matsuyama Date: Wed, 24 Jun 2026 12:42:41 +0900 Subject: [PATCH 28/29] =?UTF-8?q?=E3=83=A1=E3=83=A2=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92InitializeNewMentorMemo=E3=81=AB?= =?UTF-8?q?=E5=88=86=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/initializeNewMentorMemo.js | 128 ++++++++++++++++++++++ app/javascript/mentor-memos.js | 122 +-------------------- 2 files changed, 130 insertions(+), 120 deletions(-) create mode 100644 app/javascript/initializeNewMentorMemo.js diff --git a/app/javascript/initializeNewMentorMemo.js b/app/javascript/initializeNewMentorMemo.js new file mode 100644 index 00000000000..658d60e4cb1 --- /dev/null +++ b/app/javascript/initializeNewMentorMemo.js @@ -0,0 +1,128 @@ +import TextareaInitializer from 'textarea-initializer' +import MarkdownInitializer from 'markdown-initializer' +import initializeMemo from './initializeMentorMemo' +import { post } from '@rails/request.js' + +export default function initializeNewMemo(userId) { + TextareaInitializer.initialize('#js-new-memo') + const markdownInitializer = new MarkdownInitializer() + + const mentorMemo = document.querySelector('.mentor-memos') + + const memoDisplay = mentorMemo.querySelector('.memos-display') + const memoEditor = mentorMemo.querySelector('.new-memo-editor') + + const memoEditorPreview = memoEditor.querySelector( + '.a-markdown-input__preview' + ) + const editorTextarea = memoEditor.querySelector('.a-markdown-input__textarea') + + const addButton = memoDisplay.querySelector('.js-add-memo') + const addContainer = memoDisplay.querySelector('.js-add-action') + const modalElements = [addContainer, memoEditor] + addButton.addEventListener('click', () => { + toggleClass(modalElements, 'is-hidden') + resetEditorTabs() + editorTextarea.focus() + }) + + const saveButton = memoEditor.querySelector('.js-save-memo') + saveButton.disabled = true + editorTextarea.addEventListener('input', () => { + saveButton.disabled = editorTextarea.value.trim().length === 0 + }) + + const emptyMessage = memoDisplay.querySelector('.o-empty-message') + saveButton.addEventListener('click', async () => { + const memoContent = editorTextarea.value + const html = await createMemo(memoContent, userId) + if (!html) return + + const newMemo = addNewMemo(html, userId) + newMemo.scrollIntoView() + closeEditor() + + if (emptyMessage) { + emptyMessage.classList.add('is-hidden') + } + }) + + const cancelButton = memoEditor.querySelector('.js-cancel-memo') + cancelButton.addEventListener('click', () => { + closeEditor() + }) + + const editorTab = memoEditor.querySelector('.editor-tab') + const editorTabContent = memoEditor.querySelector('.is-editor') + const previewTab = memoEditor.querySelector('.preview-tab') + const previewTabContent = memoEditor.querySelector('.is-preview') + + const tabElements = [ + editorTab, + editorTabContent, + previewTab, + previewTabContent + ] + + editorTab.addEventListener('click', () => + toggleClass(tabElements, 'is-active') + ) + previewTab.addEventListener('click', () => { + memoEditorPreview.innerHTML = markdownInitializer.render( + editorTextarea.value + ) + toggleClass(tabElements, 'is-active') + }) + + function resetEditorTabs() { + editorTab.classList.add('is-active') + editorTabContent.classList.add('is-active') + previewTab.classList.remove('is-active') + previewTabContent.classList.remove('is-active') + } + + function closeEditor() { + toggleClass(modalElements, 'is-hidden') + editorTextarea.value = '' + memoEditorPreview.innerHTML = '' + saveButton.disabled = true + } +} + +async function createMemo(memo, userId) { + const params = { + mentor_memo: { + body: memo + } + } + try { + const response = await post(`/api/users/${userId}/mentor_memos/`, { + body: params + }) + + if (!response.ok) throw new Error('Failed to fetch') + + return await response.text + } catch (error) { + console.warn(error) + + return false + } +} + +function addNewMemo(html, userId) { + const memoList = document.querySelector('.mentor-memo-list') + const memoDiv = document.createElement('div') + memoDiv.innerHTML = html + const newMemoElement = memoDiv.firstElementChild + memoList.prepend(newMemoElement) + initializeMemo(newMemoElement, userId) + + return newMemoElement +} + +function toggleClass(elements, className) { + elements.forEach((element) => { + element.classList.toggle(className) + }) +} diff --git a/app/javascript/mentor-memos.js b/app/javascript/mentor-memos.js index 89af23676d1..a5e68130196 100644 --- a/app/javascript/mentor-memos.js +++ b/app/javascript/mentor-memos.js @@ -1,24 +1,12 @@ -import TextareaInitializer from 'textarea-initializer' -import MarkdownInitializer from 'markdown-initializer' import initializeMemo from './initializeMentorMemo' -import { post } from '@rails/request.js' +import initializeNewMemo from './initializeNewMentorMemo' document.addEventListener('DOMContentLoaded', () => { const mentorMemo = document.querySelector('.mentor-memos') if (!mentorMemo) return - TextareaInitializer.initialize('#js-new-memo') - const markdownInitializer = new MarkdownInitializer() const userId = mentorMemo.dataset.user_id - const memoDisplay = mentorMemo.querySelector('.memos-display') - const memoEditor = mentorMemo.querySelector('.new-memo-editor') - - const memoEditorPreview = memoEditor.querySelector( - '.a-markdown-input__preview' - ) - const editorTextarea = memoEditor.querySelector('.a-markdown-input__textarea') - const memos = mentorMemo.querySelectorAll('.mentor-memo') const emptyMessage = mentorMemo.querySelector('.o-empty-message') if (memos.length) { @@ -29,111 +17,5 @@ document.addEventListener('DOMContentLoaded', () => { emptyMessage.classList.remove('is-hidden') } - const addButton = memoDisplay.querySelector('.js-add-memo') - const addContainer = memoDisplay.querySelector('.js-add-action') - const modalElements = [addContainer, memoEditor] - addButton.addEventListener('click', () => { - toggleClass(modalElements, 'is-hidden') - resetEditorTabs() - editorTextarea.focus() - }) - - const saveButton = memoEditor.querySelector('.js-save-memo') - saveButton.disabled = true - editorTextarea.addEventListener('input', () => { - saveButton.disabled = editorTextarea.value.trim().length === 0 - }) - - saveButton.addEventListener('click', async () => { - const memoContent = editorTextarea.value - const html = await createMemo(memoContent, userId) - if (!html) return - - const newMemo = addNewMemo(html, userId) - newMemo.scrollIntoView() - closeEditor() - - if (emptyMessage) { - emptyMessage.classList.add('is-hidden') - } - }) - - const cancelButton = memoEditor.querySelector('.js-cancel-memo') - cancelButton.addEventListener('click', () => { - closeEditor() - }) - - const editorTab = memoEditor.querySelector('.editor-tab') - const editorTabContent = memoEditor.querySelector('.is-editor') - const previewTab = memoEditor.querySelector('.preview-tab') - const previewTabContent = memoEditor.querySelector('.is-preview') - - const tabElements = [ - editorTab, - editorTabContent, - previewTab, - previewTabContent - ] - - editorTab.addEventListener('click', () => - toggleClass(tabElements, 'is-active') - ) - previewTab.addEventListener('click', () => { - memoEditorPreview.innerHTML = markdownInitializer.render( - editorTextarea.value - ) - toggleClass(tabElements, 'is-active') - }) - - function resetEditorTabs() { - editorTab.classList.add('is-active') - editorTabContent.classList.add('is-active') - previewTab.classList.remove('is-active') - previewTabContent.classList.remove('is-active') - } - - function closeEditor() { - toggleClass(modalElements, 'is-hidden') - editorTextarea.value = '' - memoEditorPreview.innerHTML = '' - saveButton.disabled = true - } + initializeNewMemo(userId) }) - -async function createMemo(memo, userId) { - const params = { - mentor_memo: { - body: memo - } - } - try { - const response = await post(`/api/users/${userId}/mentor_memos/`, { - body: params - }) - - if (!response.ok) throw new Error('Failed to fetch') - - return await response.text - } catch (error) { - console.warn(error) - - return false - } -} - -function addNewMemo(html, userId) { - const memoList = document.querySelector('.mentor-memo-list') - const memoDiv = document.createElement('div') - memoDiv.innerHTML = html - const newMemoElement = memoDiv.firstElementChild - memoList.prepend(newMemoElement) - initializeMemo(newMemoElement, userId) - - return newMemoElement -} - -function toggleClass(elements, className) { - elements.forEach((element) => { - element.classList.toggle(className) - }) -} From fd93424c845b92001f0fe136af653134ebf544ae Mon Sep 17 00:00:00 2001 From: matsuyama Date: Thu, 25 Jun 2026 02:54:56 +0900 Subject: [PATCH 29/29] =?UTF-8?q?=E7=A9=BA=E7=99=BD=E3=81=AE=E3=81=BF?= =?UTF-8?q?=E3=81=AE=E5=85=A5=E5=8A=9B=E6=99=82=E3=81=ABsave=E3=83=9C?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=81=8C=E7=84=A1=E5=8A=B9=E3=81=AB=E3=81=AA?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/initializeMentorMemo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/initializeMentorMemo.js b/app/javascript/initializeMentorMemo.js index de123ebc355..9075f244267 100644 --- a/app/javascript/initializeMentorMemo.js +++ b/app/javascript/initializeMentorMemo.js @@ -32,7 +32,7 @@ export default function initializeMemo(memo, userId) { const saveButton = memo.querySelector('.js-save-memo') editorTextarea.addEventListener('input', () => { - saveButton.disabled = editorTextarea.value.length === 0 + saveButton.disabled = editorTextarea.value.trim().length === 0 }) saveButton.addEventListener('click', async () => {