update the block editor data format
the legacy block editor code assumed the json format for the block_editors.blocks column was blocks: [data: {editor data}, id, timestamp] that was an artifact of the data structure emitted by editor.js. Now we just want to put the craft.js editor's json output directly into the blocks column. The data migration also changes the editor_version value from '1' to '0.1' because '1' doesn't seem right this commit also introduces data transformations to update the data to the latest version on the fly closes RCX-2369 flag=block_editor test plan: - before checking this branch out, have a block editor page - after checking this branch out - run bundle exec rake db:migrate RAILS_ENV=development - run psql canvas_development - select id, editor_version from block_editors; > expect your pages to have version 0.1 - edit the old v0.1 page > expect it to work - save > expect it to have v0.2 in the block_editors table - from the pages index page, choose Duplicate from a page's kabob menu > expect the correct type of page (rce or block) icon to be displyed > expect to be able to view the copy of the page > expect to be able to edit the copy of the page - run: bundle exec rake db:migrate:down VERSION=20220228162704 - select id, editor_version from block_editors; > expect the old v0.1 pages to revert to '1' > expect the new v0.2 pages to remain '0.2' Change-Id: I592bffc11018ab3c4f7d7cc4752202088d5700c5 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/357583 Migration-Review: Cody Cutrer <cody@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Eric Saupe <eric.saupe@instructure.com> QA-Review: Eric Saupe <eric.saupe@instructure.com> Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
parent
9dc23fe15e
commit
388d995568
|
@ -643,7 +643,7 @@ class WikiPagesApiController < ApplicationController
|
|||
def get_update_params(allowed_fields = Set[])
|
||||
# normalize parameters
|
||||
wiki_page_params = %w[title body notify_of_update published front_page editing_roles publish_at]
|
||||
wiki_page_params += [block_editor_attributes: [:time, :version, { blocks: [data: strong_anything] }]] if @context.account.feature_enabled?(:block_editor)
|
||||
wiki_page_params += [block_editor_attributes: [:time, :version, { blocks: strong_anything }]] if @context.account.feature_enabled?(:block_editor)
|
||||
page_params = params[:wiki_page] ? params[:wiki_page].permit(*wiki_page_params) : {}
|
||||
|
||||
if page_params.key?(:published)
|
||||
|
|
|
@ -589,6 +589,7 @@ class WikiPage < ActiveRecord::Base
|
|||
copy_title: nil
|
||||
}
|
||||
opts_with_default = default_opts.merge(opts)
|
||||
|
||||
result = WikiPage.new({
|
||||
title: opts_with_default[:copy_title] || get_copy_title(self, t("Copy"), self.title),
|
||||
wiki_id: self.wiki_id,
|
||||
|
@ -599,8 +600,14 @@ class WikiPage < ActiveRecord::Base
|
|||
user_id:,
|
||||
protected_editing:,
|
||||
editing_roles:,
|
||||
todo_date:
|
||||
todo_date:,
|
||||
})
|
||||
|
||||
if block_editor
|
||||
block_editor_attributes = { version: block_editor.version, blocks: block_editor.blocks }
|
||||
result.block_editor_attributes = block_editor_attributes
|
||||
end
|
||||
|
||||
if assignment && opts_with_default[:duplicate_assignment]
|
||||
result.assignment = assignment.duplicate({
|
||||
duplicate_wiki_page: false,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2024 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class FixBlockEditorVersion1 < ActiveRecord::Migration[7.1]
|
||||
tag :postdeploy
|
||||
|
||||
def up
|
||||
change_column_default :block_editors, :blocks, from: [], to: nil
|
||||
|
||||
DataFixup::FixBlockEditorVersion1.delay_if_production(
|
||||
priority: Delayed::LOW_PRIORITY,
|
||||
n_strand: "long_datafixups"
|
||||
).run
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2024 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module DataFixup::FixBlockEditorVersion1
|
||||
def self.run
|
||||
BlockEditor.where(editor_version: "1").in_batches(strategy: :pluck_ids).update_all(editor_version: "0.1")
|
||||
end
|
||||
end
|
|
@ -31,11 +31,7 @@ describe "Pages API", type: :request do
|
|||
{
|
||||
time: Time.now.to_i,
|
||||
version: "1",
|
||||
blocks: [
|
||||
{
|
||||
data: '{"ROOT":{"type": ...}'
|
||||
}
|
||||
]
|
||||
blocks: '{"ROOT":{"type": ...}'
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -46,11 +42,7 @@ describe "Pages API", type: :request do
|
|||
@block_page = @course.wiki_pages.create!(title: "Block editor page", block_editor_attributes: {
|
||||
time: Time.now.to_i,
|
||||
version: "1",
|
||||
blocks: [
|
||||
{
|
||||
data: '{"ROOT":{"type": ...}'
|
||||
}
|
||||
]
|
||||
blocks: '{"ROOT":{"type": ...}'
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -77,12 +69,11 @@ describe "Pages API", type: :request do
|
|||
format: "json",
|
||||
course_id: @course.to_param,
|
||||
include: ["body"])
|
||||
|
||||
expect(json[0].keys).not_to include("body")
|
||||
expect(json[0].keys).to include("block_editor_attributes")
|
||||
returned_attributes = json[0]["block_editor_attributes"]
|
||||
expect(returned_attributes["version"]).to eq(block_page_data[:version])
|
||||
expect(returned_attributes["blocks"][0]["data"]).to eq(block_page_data[:blocks][0][:data])
|
||||
expect(returned_attributes["blocks"]).to eq(block_page_data[:blocks])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -99,7 +90,7 @@ describe "Pages API", type: :request do
|
|||
expect(json["editor"]).to eq("block_editor")
|
||||
returned_attributes = json["block_editor_attributes"]
|
||||
expect(returned_attributes["version"]).to eq(block_page_data[:version])
|
||||
expect(returned_attributes["blocks"][0]["data"]).to eq(block_page_data[:blocks][0][:data])
|
||||
expect(returned_attributes["blocks"]).to eq(block_page_data[:blocks])
|
||||
end
|
||||
|
||||
it "retrieves rce editor page content and attributes" do
|
||||
|
@ -127,7 +118,7 @@ describe "Pages API", type: :request do
|
|||
expect(page.title).to eq "New Block Page!"
|
||||
expect(page.url).to eq "new-block-page"
|
||||
expect(page.body).to be_nil
|
||||
expect(page.block_editor["blocks"][0]["data"]).to eq block_page_data[:blocks][0][:data]
|
||||
expect(page.block_editor["blocks"]).to eq block_page_data[:blocks]
|
||||
expect(page.block_editor["editor_version"]).to eq block_page_data[:version]
|
||||
end
|
||||
|
||||
|
@ -143,7 +134,7 @@ describe "Pages API", type: :request do
|
|||
expect(page.is_front_page?).to be_truthy
|
||||
expect(page.title).to eq "New Block Front Page!"
|
||||
expect(page.body).to be_nil
|
||||
expect(page.block_editor["blocks"][0]["data"]).to eq block_page_data[:blocks][0][:data]
|
||||
expect(page.block_editor["blocks"]).to eq block_page_data[:blocks]
|
||||
expect(page.block_editor["editor_version"]).to eq block_page_data[:version]
|
||||
end
|
||||
end
|
||||
|
@ -153,11 +144,7 @@ describe "Pages API", type: :request do
|
|||
new_block_data = {
|
||||
time: Time.now.to_i,
|
||||
version: "1",
|
||||
blocks: [
|
||||
{
|
||||
data: '{"ROOT":{"a_different_type": ...}'
|
||||
}
|
||||
]
|
||||
blocks: '{"ROOT":{"a_different_type": ...}'
|
||||
}
|
||||
api_call(:put,
|
||||
"/api/v1/courses/#{@course.id}/pages/#{@block_page.url}",
|
||||
|
@ -168,7 +155,7 @@ describe "Pages API", type: :request do
|
|||
url_or_id: @block_page.url },
|
||||
{ wiki_page: { block_editor_attributes: new_block_data } })
|
||||
@block_page.reload
|
||||
expect(@block_page.block_editor["blocks"][0]["data"]).to eq new_block_data[:blocks][0][:data]
|
||||
expect(@block_page.block_editor["blocks"]).to eq new_block_data[:blocks]
|
||||
expect(@block_page.body).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -140,13 +140,13 @@ describe WikiPagesApiController, type: :request do
|
|||
it "succeeds" do
|
||||
block_editor_attributes = {
|
||||
time: Time.now.to_i,
|
||||
blocks: [{ "data" => { "text" => "test" }, "id" => "R0iGYLKhw2", "type" => "paragraph" }],
|
||||
version: "1.0"
|
||||
blocks: { "text" => "test", "id" => "R0iGYLKhw2", "type" => "paragraph" },
|
||||
version: "0.2"
|
||||
}
|
||||
create_wiki_page(@teacher, { title: "New Page", block_editor_attributes: })
|
||||
expect(WikiPage.last.title).to eq "New Page"
|
||||
expect(WikiPage.last.block_editor).to be_present
|
||||
expect(WikiPage.last.block_editor.blocks).to eq([{ "data" => { "text" => "test" }, "id" => "R0iGYLKhw2", "type" => "paragraph" }])
|
||||
expect(WikiPage.last.block_editor.blocks).to eq({ "text" => "test", "id" => "R0iGYLKhw2", "type" => "paragraph" })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -158,7 +158,7 @@ describe WikiPagesApiController, type: :request do
|
|||
it "ignores the block_editor_attributes" do
|
||||
block_editor_attributes = {
|
||||
time: Time.now.to_i,
|
||||
blocks: [{ "data" => { "text" => "test" }, "id" => "R0iGYLKhw2", "type" => "paragraph" }],
|
||||
blocks: { "text" => "test", "id" => "R0iGYLKhw2", "type" => "paragraph" },
|
||||
version: "1.0"
|
||||
}
|
||||
create_wiki_page(@teacher, { title: "New Page", block_editor_attributes: })
|
||||
|
|
|
@ -241,11 +241,7 @@ describe "Block Editor", :ignore_js_errors do
|
|||
block_editor_attributes: {
|
||||
time: Time.now.to_i,
|
||||
version: "1",
|
||||
blocks: [
|
||||
{
|
||||
data: "{\"ROOT\":{\"type\":{\"resolvedName\":\"PageBlock\"},\"isCanvas\":true,\"props\":{},\"displayName\":\"Page\",\"custom\":{},\"hidden\":false,\"nodes\":[\"AcfL3KeXTT\"],\"linkedNodes\":{}},\"AcfL3KeXTT\":{\"type\":{\"resolvedName\":\"BlankSection\"},\"isCanvas\":false,\"props\":{},\"displayName\":\"Blank Section\",\"custom\":{\"isSection\":true},\"parent\":\"ROOT\",\"hidden\":false,\"nodes\":[],\"linkedNodes\":{\"blank-section_nosection1\":\"0ZWqBwA2Ou\"}},\"0ZWqBwA2Ou\":{\"type\":{\"resolvedName\":\"NoSections\"},\"isCanvas\":true,\"props\":{\"className\":\"blank-section__inner\",\"placeholderText\":\"Drop a block to add it here\"},\"displayName\":\"NoSections\",\"custom\":{\"noToolbar\":true},\"parent\":\"AcfL3KeXTT\",\"hidden\":false,\"nodes\":[\"lLVSJCBPWm\"],\"linkedNodes\":{}},\"lLVSJCBPWm\":{\"type\":{\"resolvedName\":\"ImageBlock\"},\"isCanvas\":false,\"props\":{\"src\":\"/courses/#{@course.id}/files/#{@image.id}/preview\",\"variant\":\"default\",\"constraint\":\"cover\",\"maintainAspectRatio\":true,\"sizeVariant\":\"pixel\",\"width\":200,\"height\":100},\"displayName\":\"Image\",\"custom\":{\"isResizable\":true},\"parent\":\"0ZWqBwA2Ou\",\"hidden\":false,\"nodes\":[],\"linkedNodes\":{}}}"
|
||||
}
|
||||
]
|
||||
blocks: "{\"ROOT\":{\"type\":{\"resolvedName\":\"PageBlock\"},\"isCanvas\":true,\"props\":{},\"displayName\":\"Page\",\"custom\":{},\"hidden\":false,\"nodes\":[\"AcfL3KeXTT\"],\"linkedNodes\":{}},\"AcfL3KeXTT\":{\"type\":{\"resolvedName\":\"BlankSection\"},\"isCanvas\":false,\"props\":{},\"displayName\":\"Blank Section\",\"custom\":{\"isSection\":true},\"parent\":\"ROOT\",\"hidden\":false,\"nodes\":[],\"linkedNodes\":{\"blank-section_nosection1\":\"0ZWqBwA2Ou\"}},\"0ZWqBwA2Ou\":{\"type\":{\"resolvedName\":\"NoSections\"},\"isCanvas\":true,\"props\":{\"className\":\"blank-section__inner\",\"placeholderText\":\"Drop a block to add it here\"},\"displayName\":\"NoSections\",\"custom\":{\"noToolbar\":true},\"parent\":\"AcfL3KeXTT\",\"hidden\":false,\"nodes\":[\"lLVSJCBPWm\"],\"linkedNodes\":{}},\"lLVSJCBPWm\":{\"type\":{\"resolvedName\":\"ImageBlock\"},\"isCanvas\":false,\"props\":{\"src\":\"/courses/#{@course.id}/files/#{@image.id}/preview\",\"variant\":\"default\",\"constraint\":\"cover\",\"maintainAspectRatio\":true,\"sizeVariant\":\"pixel\",\"width\":200,\"height\":100},\"displayName\":\"Image\",\"custom\":{\"isResizable\":true},\"parent\":\"0ZWqBwA2Ou\",\"hidden\":false,\"nodes\":[],\"linkedNodes\":{}}}"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -47,11 +47,7 @@ describe "Block Editor", :ignore_js_errors do
|
|||
block_editor_attributes: {
|
||||
time: Time.now.to_i,
|
||||
version: "1",
|
||||
blocks: [
|
||||
{
|
||||
data: block_page_content
|
||||
}
|
||||
]
|
||||
blocks: block_page_content
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -34,11 +34,7 @@ module BlockEditorPage
|
|||
block_editor_attributes: {
|
||||
time: Time.now.to_i,
|
||||
version: "1",
|
||||
blocks: [
|
||||
{
|
||||
data: block_page_content
|
||||
}
|
||||
]
|
||||
blocks: block_page_content
|
||||
}
|
||||
)
|
||||
block_page
|
||||
|
|
|
@ -253,7 +253,7 @@ export default class WikiPageIndexView extends PaginatedCollectionView {
|
|||
block_editor_attributes: this.createNewPageWithBlockEditor
|
||||
? {
|
||||
version: '1',
|
||||
blocks: [{data: undefined}],
|
||||
blocks: undefined,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
|
|
|
@ -195,13 +195,13 @@ export default class WikiPageView extends Backbone.View {
|
|||
maybeRenderBlockEditorContent() {
|
||||
if (
|
||||
this.model.get('editor') === 'block_editor' &&
|
||||
this.model.get('block_editor_attributes')?.blocks?.[0]?.data
|
||||
this.model.get('block_editor_attributes')?.blocks
|
||||
) {
|
||||
import('@canvas/block-editor')
|
||||
.then(({renderBlockEditorView}) => {
|
||||
const container = document.getElementById('block-editor-content')
|
||||
container.classList.add('block-editor-view')
|
||||
const content = JSON.parse(this.model.get('block_editor_attributes').blocks[0].data)
|
||||
const content = this.model.get('block_editor_attributes')
|
||||
renderBlockEditorView(content, container)
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
|
@ -28,6 +28,12 @@ import {NewPageStepper} from './components/editor/NewPageStepper'
|
|||
import {RenderNode} from './components/editor/RenderNode'
|
||||
import {ErrorBoundary} from './components/editor/ErrorBoundary'
|
||||
import {closeExpandedBlocks} from './utils/cleanupBlocks'
|
||||
import {
|
||||
transform,
|
||||
LATEST_BLOCK_DATA_VERSION,
|
||||
type BlockEditorDataTypes,
|
||||
type BlockEditorData,
|
||||
} from './utils/transformations'
|
||||
|
||||
import './style.css'
|
||||
|
||||
|
@ -76,12 +82,11 @@ const DEFAULT_CONTENT = JSON.stringify({
|
|||
},
|
||||
})
|
||||
|
||||
type BlockEditorProps = {
|
||||
export type BlockEditorProps = {
|
||||
enabled?: boolean
|
||||
enableResizer?: boolean
|
||||
container: HTMLElement // the element that will shrink when drawers open
|
||||
version: string
|
||||
content: string
|
||||
content: BlockEditorDataTypes
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
|
@ -89,28 +94,35 @@ export default function BlockEditor({
|
|||
enabled = true,
|
||||
enableResizer = true,
|
||||
container,
|
||||
version,
|
||||
content,
|
||||
onCancel,
|
||||
}: BlockEditorProps) {
|
||||
const [json] = useState(content || DEFAULT_CONTENT)
|
||||
const [data] = useState<BlockEditorData>(() => {
|
||||
if (content?.blocks) {
|
||||
return transform(content)
|
||||
}
|
||||
return {version: '0.2', blocks: DEFAULT_CONTENT} as BlockEditorData
|
||||
})
|
||||
const [toolboxOpen, setToolboxOpen] = useState(false)
|
||||
const [stepperOpen, setStepperOpen] = useState(!content)
|
||||
const [stepperOpen, setStepperOpen] = useState(!content?.blocks)
|
||||
|
||||
RenderNode.globals.enableResizer = !!enableResizer
|
||||
|
||||
useEffect(() => {
|
||||
if (version !== '1') {
|
||||
if (data.version !== LATEST_BLOCK_DATA_VERSION) {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(I18n.t('wrong version, mayhem may ensue'))
|
||||
alert(I18n.t('Unknown block data version "%{v}", mayhem may ensue', {v: data.version}))
|
||||
}
|
||||
}, [json, version])
|
||||
}, [data.version])
|
||||
|
||||
const handleNodesChange = useCallback((query: any) => {
|
||||
// @ts-expect-error
|
||||
window.block_editor = () => ({
|
||||
query,
|
||||
getBlocks: (): string => closeExpandedBlocks(query),
|
||||
getBlocks: (): BlockEditorData => ({
|
||||
version: '0.2',
|
||||
blocks: closeExpandedBlocks(query),
|
||||
}),
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
@ -174,7 +186,7 @@ export default function BlockEditor({
|
|||
<Topbar onToolboxChange={handleOpenToolbox} toolboxOpen={toolboxOpen} />
|
||||
</div>
|
||||
<Flex.Item id="editor-area" shouldGrow={true} role="tree">
|
||||
<Frame data={json} />
|
||||
<Frame data={data.blocks} />
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
|
||||
|
|
|
@ -16,20 +16,43 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {Editor, Frame} from '@craftjs/core'
|
||||
import {blocks} from './components/blocks'
|
||||
import {
|
||||
transform,
|
||||
LATEST_BLOCK_DATA_VERSION,
|
||||
type BlockEditorDataTypes,
|
||||
type BlockEditorData,
|
||||
} from './utils/transformations'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
|
||||
import './style.css'
|
||||
|
||||
const I18n = useI18nScope('block-editor')
|
||||
|
||||
type BlockEditorViewProps = {
|
||||
content: string
|
||||
content: BlockEditorDataTypes
|
||||
}
|
||||
|
||||
const BlockEditorView = ({content}: BlockEditorViewProps) => {
|
||||
const [data] = useState<BlockEditorData>(() => {
|
||||
if (content?.blocks) {
|
||||
return transform(content)
|
||||
}
|
||||
return {version: '0.2', blocks: undefined} as BlockEditorData
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (data.version !== LATEST_BLOCK_DATA_VERSION) {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(I18n.t('Unknown block data version "%{v}", mayhem may ensue', {v: data.version}))
|
||||
}
|
||||
}, [data.version])
|
||||
|
||||
return (
|
||||
<Editor enabled={false} resolver={blocks}>
|
||||
<Frame data={content} />
|
||||
<Frame data={data.blocks} />
|
||||
</Editor>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,12 +24,12 @@ import {
|
|||
getByLabelText as domGetByLabelText,
|
||||
} from '@testing-library/dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import BlockEditor from '../BlockEditor'
|
||||
import BlockEditor, {type BlockEditorProps} from '../BlockEditor'
|
||||
import {blank_page, blank_section_with_text} from './test-content'
|
||||
|
||||
const user = userEvent.setup()
|
||||
|
||||
function renderEditor(props = {}) {
|
||||
function renderEditor(props: Partial<BlockEditorProps> = {}) {
|
||||
const container = document.createElement('div')
|
||||
container.id = 'drawer-layout-content'
|
||||
container.scrollTo = () => {}
|
||||
|
@ -38,9 +38,8 @@ function renderEditor(props = {}) {
|
|||
return render(
|
||||
<BlockEditor
|
||||
container={container}
|
||||
version="1"
|
||||
enableResizer={false} // jsdom doesn't render enough for BlockResizer to work
|
||||
content={blank_page}
|
||||
content={{version: '0.2', blocks: blank_page}}
|
||||
onCancel={() => {}}
|
||||
{...props}
|
||||
/>,
|
||||
|
@ -62,8 +61,8 @@ describe('BlockEditor', () => {
|
|||
})
|
||||
|
||||
it('warns on content version mismatch', () => {
|
||||
renderEditor({version: '2'})
|
||||
expect(window.alert).toHaveBeenCalledWith('wrong version, mayhem may ensue')
|
||||
renderEditor({content: {version: '2', blocks: blank_page}})
|
||||
expect(window.alert).toHaveBeenCalledWith('Unknown block data version "2", mayhem may ensue')
|
||||
})
|
||||
|
||||
describe('New page stepper', () => {
|
||||
|
@ -111,11 +110,23 @@ describe('BlockEditor', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('data transformations', () => {
|
||||
it('can edit version 0.1 data', () => {
|
||||
renderEditor({
|
||||
content: {
|
||||
version: '0.1',
|
||||
blocks: [{data: blank_section_with_text}],
|
||||
},
|
||||
})
|
||||
expect(screen.getByText('this is text.')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Preview', () => {
|
||||
it('toggles the preview', async () => {
|
||||
// rebnder a page with a blank section containing a text block
|
||||
const {getByText} = renderEditor({
|
||||
content: blank_section_with_text,
|
||||
content: {version: '0.2', blocks: blank_section_with_text},
|
||||
})
|
||||
await user.click(getByText('Preview').closest('button') as HTMLButtonElement)
|
||||
|
||||
|
@ -138,7 +149,7 @@ describe('BlockEditor', () => {
|
|||
it('adjusts the view size', async () => {
|
||||
// rebnder a page with a blank section containing a text block
|
||||
const {getByText} = renderEditor({
|
||||
content: blank_section_with_text,
|
||||
content: {version: '0.2', blocks: blank_section_with_text},
|
||||
})
|
||||
await user.click(getByText('Preview').closest('button') as HTMLButtonElement)
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import {Modal} from '@instructure/ui-modal'
|
|||
import {RadioInputGroup, RadioInput} from '@instructure/ui-radio-input'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import BlockEditorView from '../../BlockEditorView'
|
||||
import {LATEST_BLOCK_DATA_VERSION} from '../../utils/transformations'
|
||||
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
|
||||
|
@ -100,7 +101,9 @@ const PreviewModal = ({open, onDismiss}: PreviewModalProps) => {
|
|||
padding="0"
|
||||
margin="0 auto"
|
||||
>
|
||||
<BlockEditorView content={query.serialize()} />
|
||||
<BlockEditorView
|
||||
content={{version: LATEST_BLOCK_DATA_VERSION, blocks: query.serialize()}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Modal.Body>
|
||||
|
|
|
@ -22,6 +22,7 @@ import {getByText as domGetByText} from '@testing-library/dom'
|
|||
import userEvent from '@testing-library/user-event'
|
||||
import BlockEditor from '../../../BlockEditor'
|
||||
import {blank_section_with_button_and_heading} from '../../../__tests__/test-content'
|
||||
import {LATEST_BLOCK_DATA_VERSION} from '../../../utils/transformations'
|
||||
|
||||
const user = userEvent.setup()
|
||||
|
||||
|
@ -35,8 +36,7 @@ function renderEditor(props = {}) {
|
|||
<BlockEditor
|
||||
enableResizer={false} // jsdom doesn't render enough for BlockResizer to work
|
||||
container={container}
|
||||
version="1"
|
||||
content={blank_section_with_button_and_heading}
|
||||
content={{version: LATEST_BLOCK_DATA_VERSION, blocks: blank_section_with_button_and_heading}}
|
||||
onCancel={() => {}}
|
||||
{...props}
|
||||
/>,
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2024 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
transform,
|
||||
transform_0_1_to_0_2,
|
||||
LATEST_BLOCK_DATA_VERSION,
|
||||
type BlockEditorData,
|
||||
type BlockEditorDataTypes,
|
||||
type BlockEditorData_0_1,
|
||||
} from '../transformations'
|
||||
|
||||
describe('transformations', () => {
|
||||
it('returns the same data if the version is the latest', () => {
|
||||
const data: BlockEditorData = {
|
||||
version: LATEST_BLOCK_DATA_VERSION,
|
||||
blocks: 'blocks',
|
||||
}
|
||||
expect(transform(data)).toEqual(data)
|
||||
})
|
||||
|
||||
it('transforms version 0.1 to the latest version', () => {
|
||||
const data: BlockEditorDataTypes = {
|
||||
version: '0.1',
|
||||
blocks: [{data: 'blocks'}],
|
||||
}
|
||||
expect(transform(data)).toEqual({
|
||||
version: LATEST_BLOCK_DATA_VERSION,
|
||||
blocks: 'blocks',
|
||||
})
|
||||
})
|
||||
|
||||
describe('transform 0.1 to 0.2', () => {
|
||||
it('transforms data', () => {
|
||||
const data: BlockEditorData_0_1 = {
|
||||
version: '0.1',
|
||||
blocks: [{data: 'blocks'}],
|
||||
}
|
||||
expect(transform_0_1_to_0_2(data)).toEqual({
|
||||
version: '0.2',
|
||||
blocks: 'blocks',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (C) 2024 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const LATEST_BLOCK_DATA_VERSION = '0.2' as const
|
||||
|
||||
type jsonString = string
|
||||
|
||||
type BlockEditorData_0_2 = {
|
||||
version: '0.2'
|
||||
blocks?: jsonString
|
||||
}
|
||||
|
||||
type BlockType_0_1 = {
|
||||
data?: jsonString
|
||||
}
|
||||
type BlockEditorData_0_1 = {
|
||||
version: '0.1'
|
||||
blocks: BlockType_0_1[]
|
||||
}
|
||||
|
||||
// BlockEditorData is the latest version of the data structure
|
||||
type BlockEditorData = BlockEditorData_0_2
|
||||
|
||||
// any possible version of the data structure
|
||||
type BlockEditorDataTypes = BlockEditorData | BlockEditorData_0_1
|
||||
|
||||
const transform = (data: BlockEditorDataTypes): BlockEditorData => {
|
||||
let transformedData = data
|
||||
|
||||
if (data.version === '0.1') {
|
||||
transformedData = transform_0_1_to_0_2(data)
|
||||
}
|
||||
|
||||
return transformedData as BlockEditorData
|
||||
}
|
||||
|
||||
function transform_0_1_to_0_2(data: BlockEditorData_0_1): BlockEditorData_0_2 {
|
||||
const blocks = data.blocks[0].data
|
||||
|
||||
return {
|
||||
version: '0.2',
|
||||
blocks,
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
transform,
|
||||
transform_0_1_to_0_2,
|
||||
LATEST_BLOCK_DATA_VERSION,
|
||||
type BlockEditorData,
|
||||
type BlockEditorDataTypes,
|
||||
type BlockEditorData_0_1,
|
||||
}
|
|
@ -258,8 +258,7 @@ export default class WikiPageEditView extends ValidatedFormView {
|
|||
<Suspense fallback={<div>{I18n.t('Loading...')}</div>}>
|
||||
<BlockEditor
|
||||
container={container}
|
||||
version={blockEditorData.version}
|
||||
content={blockEditorData.blocks[0].data}
|
||||
content={blockEditorData}
|
||||
onCancel={this.cancel.bind(this)}
|
||||
/>
|
||||
</Suspense>,
|
||||
|
@ -421,11 +420,7 @@ export default class WikiPageEditView extends ValidatedFormView {
|
|||
}
|
||||
}
|
||||
if (window.block_editor) {
|
||||
this.blockEditorData = {
|
||||
time: Date.now(),
|
||||
version: '1',
|
||||
blocks: [{data: window.block_editor().getBlocks()}],
|
||||
}
|
||||
this.blockEditorData = window.block_editor().getBlocks()
|
||||
}
|
||||
|
||||
if (this.reloadView != null) {
|
||||
|
|
Loading…
Reference in New Issue