Commit 2ae28c5d by Andy Armstrong

Improve unit testing of xblock editing

parent 9de6f101
...@@ -30,13 +30,11 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers ...@@ -30,13 +30,11 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
beforeEach(function () { beforeEach(function () {
window.MockXBlock = function(runtime, element) { edit_helpers.installMockXBlock();
return { };
};
}); });
afterEach(function() { afterEach(function() {
window.MockXBlock = null; edit_helpers.uninstallMockXBlock();
}); });
it('can show itself', function() { it('can show itself', function() {
......
define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
"js/views/pages/container", "js/models/xblock_info"], "js/views/pages/container", "js/models/xblock_info"],
function ($, create_sinon, edit_helpers, XBlockContainerPage, XBlockInfo) { function ($, create_sinon, edit_helpers, ContainerPage, XBlockInfo) {
describe("XBlockContainerView", function() { describe("ContainerPage", function() {
var model, containerView, respondWithMockXBlockEditorFragment, mockContainerPage; var model, containerPage, respondWithMockXBlockEditorFragment, mockContainerPage;
mockContainerPage = readFixtures('mock/mock-container-page.underscore'); mockContainerPage = readFixtures('mock/mock-container-page.underscore');
...@@ -16,7 +16,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers" ...@@ -16,7 +16,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
display_name: 'Test Unit', display_name: 'Test Unit',
category: 'vertical' category: 'vertical'
}); });
containerView = new XBlockContainerPage({ containerPage = new ContainerPage({
model: model, model: model,
el: $('#content') el: $('#content')
}); });
...@@ -32,26 +32,26 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers" ...@@ -32,26 +32,26 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
it('can render itself', function() { it('can render itself', function() {
var requests = create_sinon.requests(this); var requests = create_sinon.requests(this);
containerView.render(); containerPage.render();
respondWithMockXBlockEditorFragment(requests, { respondWithMockXBlockEditorFragment(requests, {
html: mockContainerXBlockHtml, html: mockContainerXBlockHtml,
"resources": [] "resources": []
}); });
expect(containerView.$el.select('.xblock-header')).toBeTruthy(); expect(containerPage.$el.select('.xblock-header')).toBeTruthy();
expect(containerView.$('.wrapper-xblock')).not.toHaveClass('is-hidden'); expect(containerPage.$('.wrapper-xblock')).not.toHaveClass('is-hidden');
expect(containerView.$('.no-container-content')).toHaveClass('is-hidden'); expect(containerPage.$('.no-container-content')).toHaveClass('is-hidden');
}); });
it('shows a loading indicator', function() { it('shows a loading indicator', function() {
var requests = create_sinon.requests(this); var requests = create_sinon.requests(this);
containerView.render(); containerPage.render();
expect(containerView.$('.ui-loading')).not.toHaveClass('is-hidden'); expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden');
respondWithMockXBlockEditorFragment(requests, { respondWithMockXBlockEditorFragment(requests, {
html: mockContainerXBlockHtml, html: mockContainerXBlockHtml,
"resources": [] "resources": []
}); });
expect(containerView.$('.ui-loading')).toHaveClass('is-hidden'); expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
}); });
}); });
...@@ -59,28 +59,19 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers" ...@@ -59,28 +59,19 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
describe("Editing an xblock", function() { describe("Editing an xblock", function() {
var mockContainerXBlockHtml, var mockContainerXBlockHtml,
mockXBlockEditorHtml, mockXBlockEditorHtml,
saved,
newDisplayName = 'New Display Name'; newDisplayName = 'New Display Name';
beforeEach(function () { beforeEach(function () {
saved = false; edit_helpers.installMockXBlock({
window.MockXBlock = function(runtime, element) { data: "<p>Some HTML</p>",
return { metadata: {
save: function() { display_name: newDisplayName
saved = true; }
return { });
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
};
}
};
};
}); });
afterEach(function() { afterEach(function() {
window.MockXBlock = null; edit_helpers.uninstallMockXBlock();
edit_helpers.cancelModalIfShowing(); edit_helpers.cancelModalIfShowing();
}); });
...@@ -90,12 +81,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers" ...@@ -90,12 +81,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
it('can show an edit modal for a child xblock', function() { it('can show an edit modal for a child xblock', function() {
var requests = create_sinon.requests(this), var requests = create_sinon.requests(this),
editButtons; editButtons;
containerView.render(); containerPage.render();
respondWithMockXBlockEditorFragment(requests, { respondWithMockXBlockEditorFragment(requests, {
html: mockContainerXBlockHtml, html: mockContainerXBlockHtml,
"resources": [] "resources": []
}); });
editButtons = containerView.$('.edit-button'); editButtons = containerPage.$('.edit-button');
// The container renders four mock xblocks, so there should be four edit buttons // The container renders four mock xblocks, so there should be four edit buttons
expect(editButtons.length).toBe(4); expect(editButtons.length).toBe(4);
editButtons.first().click(); editButtons.first().click();
...@@ -111,14 +102,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers" ...@@ -111,14 +102,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
}); });
it('can save changes to settings', function() { it('can save changes to settings', function() {
var requests, editButtons, modal; var requests, editButtons, modal, mockUpdatedXBlockHtml;
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
requests = create_sinon.requests(this); requests = create_sinon.requests(this);
containerView.render(); containerPage.render();
respondWithMockXBlockEditorFragment(requests, { respondWithMockXBlockEditorFragment(requests, {
html: mockContainerXBlockHtml, html: mockContainerXBlockHtml,
"resources": [] "resources": []
}); });
editButtons = containerView.$('.edit-button'); editButtons = containerPage.$('.edit-button');
// The container renders four mock xblocks, so there should be four edit buttons // The container renders four mock xblocks, so there should be four edit buttons
expect(editButtons.length).toBe(4); expect(editButtons.length).toBe(4);
editButtons.first().click(); editButtons.first().click();
...@@ -131,10 +123,21 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers" ...@@ -131,10 +123,21 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
// Click on the settings tab // Click on the settings tab
modal.find('.settings-button').click(); modal.find('.settings-button').click();
// Change the display name's text // Change the display name's text
modal.find('.setting-input').text("New display name"); modal.find('.setting-input').text("Mock Update");
// Press the save button // Press the save button
modal.find('.action-save').click(); modal.find('.action-save').click();
expect(saved).toBe(true); // Respond to the save
create_sinon.respondWithJson(requests, {
id: model.id
});
// Respond to the request to refresh
respondWithMockXBlockEditorFragment(requests, {
html: mockUpdatedXBlockHtml,
"resources": []
});
// Verify that the xblock was updated
expect(containerPage.$('.mock-updated-content').text()).toBe('Mock Update');
expect(edit_helpers.hasSavedMockXBlock()).toBe(true);
}); });
}); });
...@@ -143,14 +146,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers" ...@@ -143,14 +146,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
it('shows the "no children" message', function() { it('shows the "no children" message', function() {
var requests = create_sinon.requests(this); var requests = create_sinon.requests(this);
containerView.render(); containerPage.render();
respondWithMockXBlockEditorFragment(requests, { respondWithMockXBlockEditorFragment(requests, {
html: mockContainerXBlockHtml, html: mockContainerXBlockHtml,
"resources": [] "resources": []
}); });
expect(containerView.$('.no-container-content')).not.toHaveClass('is-hidden'); expect(containerPage.$('.no-container-content')).not.toHaveClass('is-hidden');
expect(containerView.$('.wrapper-xblock')).toHaveClass('is-hidden'); expect(containerPage.$('.wrapper-xblock')).toHaveClass('is-hidden');
}); });
}); });
}); });
......
define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
"js/views/xblock_editor", "js/models/xblock_info"], "js/views/xblock_editor", "js/models/xblock_info"],
function ($, create_sinon, edit_helpers, XBlockEditorView, XBlockInfo) { function ($, _, create_sinon, edit_helpers, XBlockEditorView, XBlockInfo) {
describe("XBlockEditorView", function() { describe("XBlockEditorView", function() {
var model, editor; var model, editor, testDisplayName, mockSaveResponse;
testDisplayName = 'Test Display Name';
mockSaveResponse = {
data: "<p>Some HTML</p>",
metadata: {
display_name: testDisplayName
}
};
beforeEach(function () { beforeEach(function () {
edit_helpers.installEditTemplates(); edit_helpers.installEditTemplates();
...@@ -21,13 +29,11 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers ...@@ -21,13 +29,11 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers
var mockXBlockEditorHtml; var mockXBlockEditorHtml;
beforeEach(function () { beforeEach(function () {
window.MockXBlock = function(runtime, element) { edit_helpers.installMockXBlock(mockSaveResponse);
return { };
};
}); });
afterEach(function() { afterEach(function() {
window.MockXBlock = null; edit_helpers.uninstallMockXBlock();
}); });
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
...@@ -41,7 +47,22 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers ...@@ -41,7 +47,22 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers
}); });
expect(editor.$el.select('.xblock-header')).toBeTruthy(); expect(editor.$el.select('.xblock-header')).toBeTruthy();
expect(editor.getMode()).toEqual('editor'); expect(editor.getMode()).toEqual('settings');
});
it('saves any custom metadata', function() {
var requests = create_sinon.requests(this), request, response;
editor.render();
create_sinon.respondWithJson(requests, {
html: mockXBlockEditorHtml,
"resources": []
});
editor.save();
request = requests[requests.length - 1];
response = JSON.parse(request.requestBody);
expect(edit_helpers.hasSavedMockXBlock()).toBeTruthy();
expect(response.metadata.display_name).toBe(testDisplayName);
expect(response.metadata.custom_field).toBe('Custom Value');
}); });
}); });
...@@ -51,12 +72,11 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers ...@@ -51,12 +72,11 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers
mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-editor.underscore'); mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-editor.underscore');
beforeEach(function() { beforeEach(function() {
// Mock the VerticalDescriptor so that the module can be rendered edit_helpers.installMockXModule(mockSaveResponse);
window.VerticalDescriptor = XModule.Descriptor;
}); });
afterEach(function () { afterEach(function () {
window.VerticalDescriptor = null; edit_helpers.uninstallMockXModule();
}); });
it('can render itself', function() { it('can render itself', function() {
...@@ -70,26 +90,28 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers ...@@ -70,26 +90,28 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers
expect(editor.$el.select('.xblock-header')).toBeTruthy(); expect(editor.$el.select('.xblock-header')).toBeTruthy();
expect(editor.getMode()).toEqual('editor'); expect(editor.getMode()).toEqual('editor');
}); });
});
describe("Editing an xmodule (settings only)", function() {
var mockXModuleEditorHtml;
mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-editor.underscore'); it('saves any custom metadata', function() {
var requests = create_sinon.requests(this), request, response;
beforeEach(function() { editor.render();
edit_helpers.installEditTemplates(); create_sinon.respondWithJson(requests, {
html: mockXModuleEditorHtml,
// Mock the VerticalDescriptor so that the module can be rendered "resources": []
window.VerticalDescriptor = XModule.Descriptor; });
// Give the mock xblock a save method...
editor.xblock.save = window.MockDescriptor.save;
editor.save();
request = requests[requests.length - 1];
response = JSON.parse(request.requestBody);
expect(edit_helpers.hasSavedMockXModule()).toBeTruthy();
expect(response.metadata.display_name).toBe(testDisplayName);
expect(response.metadata.custom_field).toBe('Custom Value');
}); });
afterEach(function () { it('can render a module with only settings', function() {
window.VerticalDescriptor = null; var requests = create_sinon.requests(this), mockXModuleEditorHtml;
}); mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-editor.underscore');
it('can render itself', function() {
var requests = create_sinon.requests(this);
editor.render(); editor.render();
create_sinon.respondWithJson(requests, { create_sinon.respondWithJson(requests, {
html: mockXModuleEditorHtml, html: mockXModuleEditorHtml,
......
...@@ -10,9 +10,55 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers ...@@ -10,9 +10,55 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers
stringEntryTemplate = readFixtures('metadata-string-entry.underscore'), stringEntryTemplate = readFixtures('metadata-string-entry.underscore'),
editXBlockModalTemplate = readFixtures('edit-xblock-modal.underscore'), editXBlockModalTemplate = readFixtures('edit-xblock-modal.underscore'),
editorModeButtonTemplate = readFixtures('editor-mode-button.underscore'), editorModeButtonTemplate = readFixtures('editor-mode-button.underscore'),
xblockSaved,
installMockXBlock,
uninstallMockXBlock,
hasSavedMockXBlock,
xmoduleSaved,
installMockXModule,
uninstallMockXModule,
hasSavedMockXModule,
installEditTemplates, installEditTemplates,
showEditModal; showEditModal;
installMockXBlock = function(mockResult) {
xblockSaved = false;
window.MockXBlock = function(runtime, element) {
return {
save: function() {
xblockSaved = true;
return mockResult;
}
};
};
};
uninstallMockXBlock = function() {
window.MockXBlock = null;
};
hasSavedMockXBlock = function() {
return xblockSaved;
};
installMockXModule = function(mockResult) {
xmoduleSaved = false;
window.MockDescriptor = _.extend(XModule.Descriptor, {
save: function() {
xmoduleSaved = true;
return mockResult;
}
});
};
uninstallMockXModule = function() {
window.MockDescriptor = null;
};
hasSavedMockXModule = function() {
return xmoduleSaved;
};
installEditTemplates = function(append) { installEditTemplates = function(append) {
modal_helpers.installModalTemplates(append); modal_helpers.installModalTemplates(append);
...@@ -37,6 +83,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers ...@@ -37,6 +83,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers
}; };
return $.extend(modal_helpers, { return $.extend(modal_helpers, {
'installMockXBlock': installMockXBlock,
'hasSavedMockXBlock': hasSavedMockXBlock,
'uninstallMockXBlock': uninstallMockXBlock,
'installMockXModule': installMockXModule,
'hasSavedMockXModule': hasSavedMockXModule,
'uninstallMockXModule': uninstallMockXModule,
'installEditTemplates': installEditTemplates, 'installEditTemplates': installEditTemplates,
'showEditModal': showEditModal 'showEditModal': showEditModal
}); });
......
...@@ -85,8 +85,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js ...@@ -85,8 +85,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
}, },
save: function(options) { save: function(options) {
var xblock = this.xblock, var xblockInfo = this.model,
xblockInfo = this.model,
data, data,
saving; saving;
data = this.getXBlockData(); data = this.getXBlockData();
...@@ -131,14 +130,14 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js ...@@ -131,14 +130,14 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
// Walk through the set of elements which have the 'data-metadata_name' attribute and // Walk through the set of elements which have the 'data-metadata_name' attribute and
// build up an object to pass back to the server on the subsequent POST. // build up an object to pass back to the server on the subsequent POST.
// Note that these values will always be sent back on POST, even if they did not actually change. // Note that these values will always be sent back on POST, even if they did not actually change.
var metadata = {}, var metadata, metadataNameElements, i, element, metadataName;
metadataNameElements; metadata = {};
metadataNameElements = this.$('[data-metadata-name]'); metadataNameElements = this.$('[data-metadata-name]');
metadataNameElements.each(function (element) { for (i = 0; i < metadataNameElements.length; i++) {
var key = $(element).data("metadata-name"), element = metadataNameElements[i];
value = element.value; metadataName = $(element).data("metadata-name");
metadata[key] = value; metadata[metadataName] = element.value;
}); }
return metadata; return metadata;
}, },
......
...@@ -32,12 +32,12 @@ main_xblock_info = { ...@@ -32,12 +32,12 @@ main_xblock_info = {
<script type='text/javascript'> <script type='text/javascript'>
require(["domReady!", "jquery", "js/models/xblock_info", "js/views/pages/container", require(["domReady!", "jquery", "js/models/xblock_info", "js/views/pages/container",
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
function(doc, $, XBlockInfo, XBlockContainerPage) { function(doc, $, XBlockInfo, ContainerPage) {
var view, mainXBlockInfo; var view, mainXBlockInfo;
mainXBlockInfo = new XBlockInfo(${json.dumps(main_xblock_info) | n}); mainXBlockInfo = new XBlockInfo(${json.dumps(main_xblock_info) | n});
view = new XBlockContainerPage({ view = new ContainerPage({
el: $('#content'), el: $('#content'),
model: mainXBlockInfo model: mainXBlockInfo
}); });
......
<header class="xblock-header">
<div class="header-details">
<span>Mock XBlock</span>
</div>
<div class="header-actions">
<ul class="actions-list">
<li class="sr action-item">No Actions</li>
</ul>
</div>
</header>
<article class="xblock-render">
<div class="xblock xblock-student_view xmodule_display xmodule_VerticalModule"
data-runtime-class="PreviewRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1"
data-type="None">
<div class="mock-updated-content">Mock Update</div>
</div>
</article>
<div class="xblock xblock-studio_view" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock" <div class="xblock xblock-studio_view" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock"
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-block-type="mock" tabindex="0"> data-init="MockXBlock" data-runtime-class="StudioRuntime" data-block-type="mock" tabindex="0">
<div class="mock-xblock"> <div class="mock-xblock">
<h3>Mock XBlock Editor</h3> <h3>Mock XBlock Editor</h3>
</div> <div class="metadata_edit"></div>
<textarea data-metadata-name="custom_field">Custom Value</textarea>
</div>
</div> </div>
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="VerticalDescriptor" data-block-type="wrapper" tabindex="0"> <div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="MockDescriptor" data-block-type="wrapper" tabindex="0">
<div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/"> <div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
</div> </div>
<section class="sequence-edit"> <section class="sequence-edit">
...@@ -25,5 +25,6 @@ ...@@ -25,5 +25,6 @@
</script> </script>
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='{&#34;display_name&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: true, &#34;display_name&#34;: &#34;Display Name&#34;, &#34;help&#34;: &#34;This name appears in the horizontal navigation at the top of the page.&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: &#34;Poll Question&#34;, &#34;field_name&#34;: &#34;display_name&#34;, &#34;options&#34;: []}, &#34;due&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: false, &#34;display_name&#34;: &#34;due&#34;, &#34;help&#34;: &#34;Date that this problem is due by&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: null, &#34;field_name&#34;: &#34;due&#34;, &#34;options&#34;: []}}'/> <div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='{&#34;display_name&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: true, &#34;display_name&#34;: &#34;Display Name&#34;, &#34;help&#34;: &#34;This name appears in the horizontal navigation at the top of the page.&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: &#34;Poll Question&#34;, &#34;field_name&#34;: &#34;display_name&#34;, &#34;options&#34;: []}, &#34;due&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: false, &#34;display_name&#34;: &#34;due&#34;, &#34;help&#34;: &#34;Date that this problem is due by&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: null, &#34;field_name&#34;: &#34;due&#34;, &#34;options&#34;: []}}'/>
<textarea data-metadata-name="custom_field">Custom Value</textarea>
</section> </section>
</div> </div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment