課題管理ツール

ここではInputManJSのリッチテキストエディタを利用して文章の編集や、設定済みのレポートテンプレートを挿入することで簡単にレポート作成することや、コメントコンポーネントを使用して課題に対するコメントを投稿するサンプルを確認することができます。

InputManJSのコントロールを利用して、リッチテキスト形式で課題を入力したり、課題に対するコメントを投稿する機能を持つ課題管理ツールの実用例を紹介します。 このサンプルでは、次のシナリオを確認することができます。 課題の編集(GcRichTextEditor):「課題を編集」ボタンを押下すると、リッチテキストエディタで既存の内容が表示され、課題の内容を直接編集することができます。編集後は保存ボタンを押すことで、変更内容が反映されます。 新規課題を作成(GcRichTextEditor):「新規課題を作成」ボタンを押下すると、リッチテキストエディタが空の状態で表示され、内容を入力することができます。編集後は保存ボタンを押すことで、変更内容が反映されます。 レポート種類(GcComboBox):「新規課題を作成」ボタンを押下し、ドロップダウンリストより項目を選択すると、設定済みのテンプレートが表示されます。このテンプレートを編集し、課題を投稿します。 課題の保存(GcValidator):未入力の状態で保存ボタンを押下すると、検証コントロールで必須入力チェックが行われ、検証失敗をトーストで通知します。 課題に対するコメント(GcComment):コメントコンポーネントを使用して、コメントの追加、削除、返信、リアクションを利用できます。新規課題を作成する際に、既存のコメントがクリアされます。
import "@mescius/inputman.richtexteditor/CSS/gc.inputman.richtexteditor.css"; import "@mescius/inputman.comment/CSS/gc.inputman.comment.css"; import '@mescius/inputman/CSS/gc.inputman-js.css'; import "./styles.css"; import { InputMan } from "@mescius/inputman.richtexteditor"; import * as GC from "@mescius/inputman.comment"; import { users, postedComments } from './data'; import * as GcCommon from '@mescius/inputman'; import "@mescius/inputman.richtexteditor/JS/plugins/all"; const articleContentElement = document.getElementById("article_content"); const editArticleButton = document.getElementById("edit_article"); const newArticleButton = document.getElementById("new_article"); const articleContainer = document.getElementById("article_container"); const commentContainer = document.getElementById("comment_display"); const gcCommentElement = document.getElementById("comment"); const syncCheckedState = (container) => { const radios = container.querySelectorAll('input[type="radio"]'); radios.forEach((r) => { if (r.checked) { r.setAttribute('checked', 'checked'); } else { r.removeAttribute('checked'); } }); const selects = container.querySelectorAll('select'); selects.forEach((select) => { const options = select.querySelectorAll('option'); options.forEach((option) => { if (option.selected) { option.setAttribute('selected', 'selected'); } else { option.removeAttribute('selected'); } }); }); }; const createArticleEditor = (actionType) => { const contentElement = articleContentElement.cloneNode(true); syncCheckedState(contentElement); contentElement.id = ""; const breakline1 = document.createElement("br"); const breakline2 = document.createElement("br"); const okButton = document.createElement("div"); okButton.textContent = "保存"; okButton.classList.add("btn", "ok-btn"); const cancelButton = document.createElement("div"); cancelButton.textContent = "キャンセル"; cancelButton.classList.add("btn", "cancel-btn"); const dropdownLabel = document.createElement("label"); dropdownLabel.textContent = "レポート種類 "; dropdownLabel.htmlFor = "report-category"; const selectDropdown = document.createElement("select"); selectDropdown.id = "report-category"; let reportCategory; if (actionType == "edit") { articleContainer.appendChild(contentElement); articleContainer.appendChild(okButton); articleContainer.appendChild(cancelButton); } else if (actionType == "new") { contentElement.innerHTML = ""; commentContainer.style.display = "none"; articleContainer.appendChild(dropdownLabel); articleContainer.appendChild(selectDropdown); articleContainer.appendChild(breakline1); articleContainer.appendChild(breakline2); articleContainer.appendChild(contentElement); articleContainer.appendChild(okButton); articleContainer.appendChild(cancelButton); reportCategory = new GcCommon.InputMan.GcComboBox(selectDropdown, { items: ['バグ報告', '機能要望'], isEditable: false, }); reportCategory.addEventListener(GcCommon.InputMan.GcComboBoxEvent.SelectedChanged, (e) => { if (e.selectedIndex === 0) { rich.execCommand(InputMan.GcRichTextEditorCommand.Template) } else { rich.execCommand( InputMan.GcRichTextEditorCommand.InsertTemplate, "<section><h4>概要</h4><p>ここに要望の概要を記入してください。</p></section><section><h4>対象コントロール</h4><p>該当するコントロール名を記入してください。</p></section><section><h4>背景・課題</h4><p>この機能が必要性、現状の課題や制限などを記入してください。</p></section><section><h4>期待動作</h4><ul><li>XXXを実行される。</li><li>XXXが正しく動作する。</li></ul></section>" ); } }); } const restoreElements = () => { articleContainer.removeAttribute("mode"); contentElement.remove(); okButton.remove(); cancelButton.remove(); if (actionType == "new") { breakline1.remove(); breakline2.remove(); dropdownLabel.remove(); } }; cancelButton.addEventListener("click", () => { restoreElements(); rich.destroy(); if (actionType == "new") { reportCategory.destroy(); selectDropdown.remove(); commentContainer.style.display = "block"; } }); okButton.addEventListener("click", () => { syncCheckedState(rich.getContentBody()); if (gcRichTextEditorValidator.validate()) { const newContent = rich.getContent(); articleContentElement.innerHTML = newContent; restoreElements(); rich.destroy(); if (actionType == "new") { reportCategory.destroy(); selectDropdown.remove(); gcComment.clear(); commentContainer.style.display = "block"; } } else { return; } }); const rich = new InputMan.GcRichTextEditor(contentElement, { plugins: [InputMan.GcRichTextEditorPluginItem.All], contextmenu: [InputMan.GcRichTextEditorMenuItem.Template], templates: 'src/template.json', watermarkText: 'テンプレート挿入より報告のフォーマットを設定してから編集を行ってください...', }); const gcRichTextEditorValidator = new GcCommon.InputMan.GcValidator({ items: [ { control: rich, ruleSet: [ { rule: GcCommon.InputMan.ValidateType.Required, failMessage: (control) => `詳細を入力するか、テンプレートより挿入してください。`, } ], validateWhen: GcCommon.InputMan.ValidateWhen.Manual, } ], defaultNotify: { fail: { toast: { title: '課題内容が必須です。', position: GcCommon.InputMan.ToastPosition.TopCenter, duration: 3, showProgress: false, showClose: false, pauseOnHover: false, }, }, } }); }; editArticleButton.addEventListener("click", () => { articleContainer.setAttribute("mode", "edit"); createArticleEditor("edit"); }); newArticleButton.addEventListener("click", () => { articleContainer.setAttribute("mode", "edit"); createArticleEditor("new"); }); const gcComment = new GC.InputMan.GcComment(gcCommentElement, { openLinkMode: GC.InputMan.OpenLinkMode.NewTab, height: 200, userInfo: { id: "1", name: "森上 偉久馬", avatar: "$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar1.png", }, loadComments: (args) => { return { comments: postedComments, }; }, openLinkMode: GC.InputMan.OpenLinkMode.GoToComment, editorConfig: { editorType: GC.InputMan.GcCommentEditorType.GcRichTextEditor, plugins: [InputMan.GcRichTextEditorPluginItem.All], height: 150, menubar: [], toolbar: [ InputMan.GcRichTextEditorToolbarItem.Copy, InputMan.GcRichTextEditorToolbarItem.Cut, InputMan.GcRichTextEditorToolbarItem.Paste, InputMan.GcRichTextEditorToolbarItem.SeparateLine, InputMan.GcRichTextEditorToolbarItem.Styles, InputMan.GcRichTextEditorToolbarItem.AlignCenter, InputMan.GcRichTextEditorToolbarItem.H3, InputMan.GcRichTextEditorToolbarItem.H4, ] }, addCommentEditorPosition: GC.InputMan.GcCommentEditorPosition.Top, addNewCommentTo: GC.InputMan.AddNewCommentPosition.Top, sortInfo: { isDesc: true, sortBy: GC.InputMan.GcCommentSortBy.PostTime }, commentActionButtonType: GC.InputMan.GcCommentActionButtonType.IconText, loadUsersInfoHandler: (context) => { if (context.loadType === GC.InputMan.LoadUserType.FilterText) { return users.filter((u) => u.name.includes(context.value)); } else if (context.loadType === GC.InputMan.LoadUserType.ById) { return users.filter((u) => u.id === context.value); } }, updateTimeFormatter: (date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const formattedDate = `${year}/${month}/${day} ${hours}:${minutes}`; return `更新日時: ${formattedDate}`; }, });
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>実用例 - 課題管理ツール</title> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> window.onload = function () { System.import("./src/app"); }; </script> </head> <body> <div id="container"> <div id="article_container"> <div class="button-container"> <div class="edit-article" id="edit_article"> <img src="$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/EditIcon.svg" /> <div>課題を編集</div> </div> <div class="new-article" id="new_article"> <img src="$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/EditIcon.svg" /> <div>新規課題を作成</div> </div> </div> <div class="content" id="article_content"> <h3>バグ報告: 任意入力フィールドにもバリデーションエラーが表示される</h3> <hr style="border: 1px solid #ccc; margin: 16px 0;"> <section class="bug-description"> <h4>現象</h4> <p> お問い合わせフォームで、任意項目である「会社名」フィールドを空欄にした状態で「送信」ボタンを押下すると、エラーが表示され、送信できません。 </p> </section> <section class="repro-steps"> <h4>再現手順</h4> <ol> <li>ナビゲーションメニューから「お問い合わせ」を選択し、フォーム画面を開く</li> <li>「会社名」フィールドを空欄のままにする(任意項目)</li> <li>その他の必須項目(氏名、メールアドレス等)を入力する</li> <li>ページ下部の「送信」ボタンをクリックする</li> </ol> </section> <section class="repro-expected"> <h4>期待動作</h4> <ul> <li>任意項目が未入力であっても、エラーが発生することなくフォーム送信できる</li> </ul> </section> <section class="attachments"> <h4>添付ファイル</h4> <ul> <li> <p>スクリーンショット</p> <img loading="lazy" width="500" height="236" src="$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/validationerror.png" alt="スクリーンショット" class="noborder" /> </li> </ul> </section> <section class="additional-info"> <h4>補足</h4> <ul> <li> <p>コンソールログ(任意項目で送信失敗)</p> <pre class="validation-error">[ValidationError] フィールド '会社名' は必須です。<br>at validateForm (form.js:45)<br>at HTMLFormElement.onsubmit (contact.html:88)</pre> </li> </ul> </section> <div></div> </div> </div> <br> <div class="comment_display" id="comment_display"> <div id="comment"></div> </div> </div> </body> </html>
#container { height: 600px; overflow-y: scroll; } .button-container { display: flex; gap: 16px; } .edit-article, .new-article { margin-bottom: 10px; justify-content: center; width: fit-content; background-color: #ececec; padding: 5px 8px; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 8px; border: 1px solid #ddd; } .edit-article:hover, .new-article:hover { background-color: #dddddd; } #article_content { transition: background-color 0.2s; overflow-y: auto; border: 1px solid #ccc; padding: 16px; } #article_content:hover { background-color: #fafafa; } #article_container[mode="edit"] .edit-article, #article_container[mode="edit"] .new-article, #article_container[mode="edit"] #article_content { display: none; } #article_container[mode="edit"] .new-article ~ .comment_display { display: none; } .comment_display { transition: background-color 0.2s; margin: 10px; } .validation-error { background-color: #fafafa; padding: 12px 16px; border: 1px solid #ccc; font-family: monospace; white-space: pre-wrap; text-align: left; margin-top: 0; line-height: 1.2; } .btn { border: none; border-radius: 4px; color: #fff; line-height: 40px; outline: none; text-align: center; -webkit-user-select: none; user-select: none; display: inline; padding: 5px 16px; } .ok-btn { background-color: #1977ff; } .ok-btn:hover { background-color: #479eff; } .cancel-btn { background-color: #fff; color: #1977ff; border: 1px solid #1977ff; margin-left: 1em; } .cancel-btn:hover { background-color: #e6f0ff; color: #479eff; border: 1px solid #479eff; } #article_content input[type='radio'], #article_content select { pointer-events: none; }
export const postedComments = [ { id: '1', userInfo: { id: '1', name: '森上 偉久馬', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar1.png', }, content: '不具合と確認しました。', postTime: new Date(2025, 4, 20, 7, 15, 5), updateTime: new Date(2025, 4, 20, 7, 15, 5), }, { id: '2', userInfo: { id: '2', name: '葛城 孝史', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar2.png', }, content: 'この件は、次のバージョンで修正予定です。', postTime: new Date(2025, 4, 20, 8, 15, 5), updateTime: new Date(2025, 4, 20, 8, 15, 5), }, { id: '3', userInfo: { id: '5', name: '松沢 誠一', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar5.png', }, content: '了解しました。お客様に返信いたします。', parentCommentId: '2', postTime: new Date(2025, 4, 22, 7, 17, 47), updateTime: new Date(2025, 4, 22, 7, 17, 47), reactions: [ { reactionChar: '👍', count: 1, }, ], }, { id: '4', userInfo: { id: '6', name: '成宮 真紀', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar6.png', }, content: 'テストが完了しました。', postTime: new Date(2025, 4, 23, 7, 25, 42), updateTime: new Date(2025, 4, 23, 7, 25, 48), reactions: [ { reactionChar: '👍', count: 1, }, ], }, { id: '5', userInfo: { id: '1', name: '森上 偉久馬', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar1.png', }, content: '本件をクローズしました。', postTime: new Date(2025, 4, 28, 17, 10, 5), updateTime: new Date(2025, 4, 28, 17, 10, 5), reactions: [ { reactionChar: '👍', count: 1, }, ], }, ]; export const users = [ { id: '1', name: '森上 偉久馬', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar1.png', }, { id: '2', name: '葛城 孝史', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar2.png', }, { id: '3', name: '加藤 泰江', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar3.png', }, { id: '4', name: '川村 匡', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar4.png', }, { id: '5', name: '松沢 誠一', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar5.png', }, { id: '6', name: '成宮 真紀', avatar: '$IMDEMOROOT$/ja/samples/examples/comment/projectManagement/img/avatar6.png', }, ];
System.config({ transpiler: 'plugin-babel', babelOptions: { es2015: true }, meta: { '*.css': { loader: 'css' } }, paths: { // paths serve as alias 'npm:': 'node_modules/' }, // map tells the System loader where to look for things map: { '@mescius/inputman': 'npm:@mescius/inputman/index.js', '@mescius/inputman/CSS': 'npm:@mescius/inputman/CSS', '@mescius/inputman.richtexteditor': 'npm:@mescius/inputman.richtexteditor/index.js', "@mescius/inputman.richtexteditor/CSS": "npm:@mescius/inputman.richtexteditor/CSS", '@mescius/inputman.richtexteditor/JS/plugins/advlist': 'npm:@mescius/inputman.richtexteditor/JS/plugins/advlist/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/all': 'npm:@mescius/inputman.richtexteditor/JS/plugins/all/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/autosave': 'npm:@mescius/inputman.richtexteditor/JS/plugins/autosave/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/charmap': 'npm:@mescius/inputman.richtexteditor/JS/plugins/charmap/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/directionality': 'npm:@mescius/inputman.richtexteditor/JS/plugins/directionality/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/emoticons': 'npm:@mescius/inputman.richtexteditor/JS/plugins/emoticons/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/fullscreen': 'npm:@mescius/inputman.richtexteditor/JS/plugins/fullscreen/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/htmlcode': 'npm:@mescius/inputman.richtexteditor/JS/plugins/htmlcode/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/image': 'npm:@mescius/inputman.richtexteditor/JS/plugins/image/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/link': 'npm:@mescius/inputman.richtexteditor/JS/plugins/link/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/lists': 'npm:@mescius/inputman.richtexteditor/JS/plugins/lists/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/media': 'npm:@mescius/inputman.richtexteditor/JS/plugins/media/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/pagebreak': 'npm:@mescius/inputman.richtexteditor/JS/plugins/pagebreak/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/preview': 'npm:@mescius/inputman.richtexteditor/JS/plugins/preview/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/save': 'npm:@mescius/inputman.richtexteditor/JS/plugins/save/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/searchreplace': 'npm:@mescius/inputman.richtexteditor/JS/plugins/searchreplace/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/table': 'npm:@mescius/inputman.richtexteditor/JS/plugins/table/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/template': 'npm:@mescius/inputman.richtexteditor/JS/plugins/template/plugin.js', '@mescius/inputman.richtexteditor/JS/plugins/wordcount': 'npm:@mescius/inputman.richtexteditor/JS/plugins/wordcount/plugin.js', '@mescius/inputman.comment': 'npm:@mescius/inputman.comment/index.js', '@mescius/inputman.comment/CSS': 'npm:@mescius/inputman.comment/CSS', '@mescius/wijmo': 'npm:@mescius/wijmo/index.js', '@mescius/wijmo.styles': 'npm:@mescius/wijmo.styles', '@mescius/wijmo.cultures': 'npm:@mescius/wijmo.cultures', '@mescius/wijmo.input': 'npm:@mescius/wijmo.input/index.js', '@mescius/wijmo.grid': 'npm:@mescius/wijmo.grid/index.js', '@mescius/wijmo.nav': 'npm:@mescius/wijmo.nav/index.js', '@mescius/spread-sheets': 'npm:@mescius/spread-sheets/index.js', '@mescius/spread-sheets-resources-ja': 'npm:@mescius/spread-sheets-resources-ja/index.js', '@mescius/spread-sheets/styles': 'npm:@mescius/spread-sheets/styles', 'css': 'npm:systemjs-plugin-css/css.js', 'plugin-babel': 'npm:systemjs-plugin-babel/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel/systemjs-babel-browser.js' }, // packages tells the System loader how to load when no filename and/or no extension packages: { src: { defaultExtension: 'js' }, "node_modules": { defaultExtension: 'js' }, } });