Edit block or rce pages

whether a page was created with the rce or the block editor
edit with the corresponding editor

show an solid or line icon on the index page indicating whether
the page is a block or rce page respectively

closes RCX-2184
flag=block_editor

test plan: soon

Change-Id: Ia97c21c5b902affd0762ca16c6a1218db8899aa5
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/354568
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:
Ed Schiebel 2024-08-07 16:55:57 -06:00
parent ff8c8c808b
commit 360fb8945c
16 changed files with 602 additions and 303 deletions

View File

@ -175,13 +175,13 @@ class WikiPagesController < ApplicationController
wiki_index_menu_tools: external_tools_display_hashes(:wiki_index_menu),
DISPLAY_SHOW_ALL_LINK: tab_enabled?(context.class::TAB_PAGES, no_render: true) && !@k5_details_view,
CAN_SET_TODO_DATE: context.grants_any_right?(@current_user, session, :manage_content, :manage_course_content_edit),
BLOCK_EDITOR: context.account.feature_enabled?(:block_editor)
}
if Account.site_admin.feature_enabled?(:permanent_page_links)
title_availability_path = context.is_a?(Course) ? api_v1_course_page_title_availability_path : api_v1_group_page_title_availability_path
@wiki_pages_env[:TITLE_AVAILABILITY_PATH] = title_availability_path
end
js_env(@wiki_pages_env)
@js_env[:FEATURES][:BLOCK_EDITOR] = true if context.account.feature_enabled?(:block_editor)
@wiki_pages_env
end
end

View File

@ -143,6 +143,10 @@ if (!Array.prototype.flatMap) {
require('@instructure/ui-themes')
// set up mocks for native APIs
if (!('alert' in window)) {
window.alert = () => {}
}
if (!('MutationObserver' in window)) {
Object.defineProperty(window, 'MutationObserver', {
value: require('@sheerun/mutationobserver-shim'),

View File

@ -40,6 +40,7 @@ module Api::V1::WikiPage
hash["html_url"] = polymorphic_url([wiki_page.context, wiki_page])
hash["todo_date"] = wiki_page.todo_date
hash["publish_at"] = wiki_page.publish_at
hash["editor"] = wiki_page.block_editor ? "block_editor" : "rce" if @context.account.feature_enabled?(:block_editor)
hash["updated_at"] = wiki_page.revised_at
if opts[:include_assignment] && wiki_page.for_assignment?
@ -51,12 +52,11 @@ module Api::V1::WikiPage
end
locked_json(hash, wiki_page, current_user, "page", deep_check_if_needed: opts[:deep_check_if_needed])
if include_body && !hash["locked_for_user"] && !hash["lock_info"]
if @context.account.feature_enabled?(:block_editor)
block_editor = wiki_page.block_editor
if @context.account.feature_enabled?(:block_editor) && wiki_page.block_editor
hash["block_editor_attributes"] = {
id: block_editor&.id,
version: block_editor&.editor_version,
blocks: block_editor&.blocks
id: wiki_page.block_editor.id,
version: wiki_page.block_editor.editor_version,
blocks: wiki_page.block_editor.blocks
}
else
hash["body"] = api_user_content(wiki_page.body, wiki_page.context)

View File

@ -0,0 +1,190 @@
# 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/>.
#
require_relative "../api_spec_helper"
require_relative "../locked_examples"
require_relative "../../helpers/selective_release_common"
describe "Pages API", type: :request do
include Api::V1::User
include AvatarHelper
include SelectiveReleaseCommon
let(:block_page_data) do
{
time: Time.now.to_i,
version: "1",
blocks: [
{
data: '{"ROOT":{"type": ...}'
}
]
}
end
context "with the block editor" do
before :once do
course_with_teacher(active_all: true)
@course.account.enable_feature!(:block_editor)
@block_page = @course.wiki_pages.create!(title: "Block editor page", block_editor_attributes: {
time: Time.now.to_i,
version: "1",
blocks: [
{
data: '{"ROOT":{"type": ...}'
}
]
})
end
describe "index" do
it("returns the block editor meta-data") do
json = api_call(:get,
"/api/v1/courses/#{@course.id}/pages",
controller: "wiki_pages_api",
action: "index",
format: "json",
course_id: @course.to_param)
expect(json.map { |entry| entry.slice(*%w[url title editor]) }).to eq(
[{ "url" => @block_page.url, "title" => @block_page.title, "editor" => "block_editor" }]
)
expect(json[0].keys).not_to include("body")
expect(json[0].keys).not_to include("block_editor_attributes")
end
it("returns the block editor data when include[]=body is specified") do
json = api_call(:get,
"/api/v1/courses/#{@course.id}/pages",
controller: "wiki_pages_api",
action: "index",
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])
end
end
describe "show" do
it "retrieves block editor page content and attributes" do
json = api_call(:get,
"/api/v1/courses/#{@course.id}/pages/#{@block_page.url}",
controller: "wiki_pages_api",
action: "show",
format: "json",
course_id: @course.id.to_s,
url_or_id: @block_page.url)
expect(json["body"]).to be_nil
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])
end
it "retrieves rce editor page content and attributes" do
rce_page = @course.wiki_pages.create!(title: "RCE Page", body: "Body of RCE page")
json = api_call(:get,
"/api/v1/courses/#{@course.id}/pages/#{rce_page.url}",
controller: "wiki_pages_api",
action: "show",
format: "json",
course_id: @course.id.to_s,
url_or_id: rce_page.url)
expect(json["body"]).to eq(rce_page.body)
expect(json["editor"]).to eq("rce")
expect(json["block_editor_attributes"]).to be_nil
end
end
describe "create" do
it "creates a new page", priority: "1" do
json = api_call(:post,
"/api/v1/courses/#{@course.id}/pages",
{ controller: "wiki_pages_api", action: "create", format: "json", course_id: @course.to_param },
{ wiki_page: { title: "New Block Page!", block_editor_attributes: block_page_data } })
page = @course.wiki_pages.where(url: json["url"]).first!
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["editor_version"]).to eq block_page_data[:version]
end
it "creates a front page using PUT", priority: "1" do
front_page_url = "new-block-front-page"
json = api_call(:put,
"/api/v1/courses/#{@course.id}/front_page",
{ controller: "wiki_pages_api", action: "update_front_page", format: "json", course_id: @course.to_param },
{ wiki_page: { title: "New Block Front Page!", block_editor_attributes: block_page_data } })
expect(json["url"]).to eq front_page_url
page = @course.wiki_pages.where(url: front_page_url).first!
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["editor_version"]).to eq block_page_data[:version]
end
end
describe "update" do
it "updates a page with block editor data" do
new_block_data = {
time: Time.now.to_i,
version: "1",
blocks: [
{
data: '{"ROOT":{"a_different_type": ...}'
}
]
}
api_call(:put,
"/api/v1/courses/#{@course.id}/pages/#{@block_page.url}",
{ controller: "wiki_pages_api",
action: "update",
format: "json",
course_id: @course.to_param,
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.body).to be_nil
end
end
describe "destroy" do
it "deletes a page", priority: "1" do
api_call(:delete,
"/api/v1/courses/#{@course.id}/pages/#{@block_page.url}",
{ controller: "wiki_pages_api",
action: "destroy",
format: "json",
course_id: @course.to_param,
url_or_id: @block_page.url })
expect(@block_page.reload).to be_deleted
expect(@block_page.block_editor).to be_nil
end
end
end
end

View File

@ -38,6 +38,13 @@ describe WikiPagesController do
expect(response).to be_successful
expect(assigns[:js_env][:DISPLAY_SHOW_ALL_LINK]).to be(true)
end
it "sets up js_env for the block editor" do
@course.account.enable_feature!(:block_editor)
get "index", params: { course_id: @course.id }
expect(response).to be_successful
expect(assigns[:js_env][:FEATURES][:BLOCK_EDITOR]).to be(true)
end
end
context "with page" do

View File

@ -16,6 +16,7 @@
#
# 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/>.
#
#
# some if the specs in here include "ignore_js_errors: true". This is because
@ -31,35 +32,77 @@ describe "Block Editor", :ignore_js_errors do
include_context "in-process server selenium tests"
include BlockEditorPage
def create_wiki_page_with_block_editor_content(page_title)
@page = @course.wiki_pages.create!(title: page_title)
@page.update!(
title: "#{page_title}-2",
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":["UO_WRGQgSQ"],"linkedNodes":{}},"UO_WRGQgSQ":{"type":{"resolvedName":"BlankSection"},"isCanvas":false,"props":{},"displayName":"Blank Section","custom":{"isSection":true},"parent":"ROOT","hidden":false,"nodes":[],"linkedNodes":{"blank-section_nosection1":"e33NpD3Ck3"}},"e33NpD3Ck3":{"type":{"resolvedName":"NoSections"},"isCanvas":true,"props":{"className":"blank-section__inner"},"displayName":"NoSections","custom":{"noToolbar":true},"parent":"UO_WRGQgSQ","hidden":false,"nodes":[],"linkedNodes":{}}}'
}
]
}
)
let(:block_page_content) do
'{
"ROOT": {
"type": {
"resolvedName": "PageBlock"
},
"isCanvas": true,
"props": {},
"displayName": "Page",
"custom": {},
"hidden": false,
"nodes": [
"UO_WRGQgSQ"
],
"linkedNodes": {}
},
"UO_WRGQgSQ": {
"type": {
"resolvedName": "BlankSection"
},
"isCanvas": false,
"props": {},
"displayName": "Blank Section",
"custom": {
"isSection": true
},
"parent": "ROOT",
"hidden": false,
"nodes": [],
"linkedNodes": {
"blank-section_nosection1": "e33NpD3Ck3"
}
},
"e33NpD3Ck3": {
"type": {
"resolvedName": "NoSections"
},
"isCanvas": true,
"props": {
"className": "blank-section__inner"
},
"displayName": "NoSections",
"custom": {
"noToolbar": true
},
"parent": "UO_WRGQgSQ",
"hidden": false,
"nodes": [],
"linkedNodes": {}
}
}'
end
before do
course_with_teacher_logged_in
@course.account.enable_feature!(:block_editor)
@context = @course
end
def wait_for_block_editor
keep_trying_until do
disable_implicit_wait { f(".block-editor-editor") } # rubocop:disable Specs/NoDisableImplicitWait
rescue => e
puts e.inspect
false
end
@rce_page = @course.wiki_pages.create!(title: "RCE Page", body: "RCE Page Body")
@block_page = @course.wiki_pages.create!(title: "Block Page")
puts ">>>", block_page_content
@block_page.update!(
block_editor_attributes: {
time: Time.now.to_i,
version: "1",
blocks: [
{
data: block_page_content
}
]
}
)
end
def create_wiki_page(course)
@ -104,13 +147,21 @@ describe "Block Editor", :ignore_js_errors do
end
context "Edit a page" do
before do
create_wiki_page_with_block_editor_content("block editor test")
it "edits an rce page with the rce" do
get "/courses/#{@course.id}/pages/#{@rce_page.url}/edit"
wait_for_rce
expect(f("textarea.body").attribute("value")).to eq("<p>RCE Page Body</p>")
end
it "loads the editor" do
get "/courses/#{@course.id}/pages/block-editor-test/edit"
expect(f(".block-editor-editor")).to be_displayed
it "edits a block page with the block editor" do
get "/courses/#{@course.id}/pages/#{@block_page.url}/edit"
wait_for_block_editor
expect(f(".page-block")).to be_displayed
end
it "can drag and drop blocks from the toolbox" do
get "/courses/#{@course.id}/pages/#{@block_page.url}/edit"
wait_for_block_editor
block_toolbox_toggle.click
expect(block_toolbox).to be_displayed
drag_and_drop_element(f(".toolbox-item.item-button"), f(".blank-section__inner"))

View File

@ -293,4 +293,14 @@ module CustomWaitMethods
rescue Selenium::WebDriver::Error::NoSuchElementError
true
end
def wait_for_block_editor(parent_element = nil)
parent_element ||= f("#content")
keep_trying_until do
disable_implicit_wait { f(".block-editor-editor", parent_element) }
rescue => e
puts e.inspect
false
end
end
end

View File

@ -100,6 +100,9 @@ export default class WikiPageIndexItemView extends Backbone.View {
json.isChecked = this.selectedPages.hasOwnProperty(this.model.get('page_id'))
json.collectionHasTodoDate = this.collectionHasTodoDate()
json.frontPageText = ENV.K5_SUBJECT_COURSE ? I18n.t('Subject Home') : I18n.t('Front Page')
json.block_editor = !!ENV.FEATURES?.BLOCK_EDITOR
json.page_is_block = this.model.get('editor') === 'block_editor'
json.page_type_label = json.page_is_block ? I18n.t('block page') : I18n.t('classic page')
return json
}

View File

@ -102,6 +102,8 @@ export default class WikiPageIndexView extends PaginatedCollectionView {
if (!this.selectedPages) this.selectedPages = {}
this.itemViewOptions.selectedPages = this.selectedPages
this.createNewPageWithBlockEditor = !!ENV.FEATURES?.BLOCK_EDITOR
this.collection.on('fetch', () => {
if (!this.fetched) {
this.fetched = true
@ -191,7 +193,7 @@ export default class WikiPageIndexView extends PaginatedCollectionView {
}
toggleBlockEditor(ev) {
ENV.BLOCK_EDITOR = ev.target.checked
this.createNewPageWithBlockEditor = ev.target.checked
}
confirmDeletePages(ev) {
@ -232,10 +234,17 @@ export default class WikiPageIndexView extends PaginatedCollectionView {
$('body').removeClass('index')
$('body').addClass('edit')
this.editModel = new WikiPage(
{editing_roles: this.default_editing_roles},
{contextAssetString: this.contextAssetString}
)
this.editModel = new WikiPage({
editing_roles: this.default_editing_roles,
contextAssetString: this.contextAssetString,
editor: this.createNewPageWithBlockEditor ? 'block_editor' : 'rce',
block_editor_attributes: this.createNewPageWithBlockEditor
? {
version: '1',
blocks: [{data: undefined}],
}
: null,
})
this.editView = new WikiPageEditView({
model: this.editModel,
wiki_pages_path: ENV.WIKI_PAGES_PATH,
@ -390,7 +399,7 @@ export default class WikiPageIndexView extends PaginatedCollectionView {
json.hasWikiIndexPlacements = this.wikiIndexPlacements.length > 0
json.wikiIndexPlacements = this.wikiIndexPlacements
json.block_editor = ENV.BLOCK_EDITOR
json.block_editor = !!ENV.FEATURES?.BLOCK_EDITOR
return json
}
}

View File

@ -20,7 +20,7 @@ import WikiPage from '@canvas/wiki/backbone/models/WikiPage'
import WikiPageIndexItemView from '../WikiPageIndexItemView'
import fakeENV from '@canvas/test-utils/fakeENV'
describe('WikiPageIndexItemView', () => {
describe('WikiPageIndex', () => {
beforeEach(() => {
fakeENV.setup()
})
@ -28,191 +28,192 @@ describe('WikiPageIndexItemView', () => {
afterEach(() => {
fakeENV.teardown()
})
test('model.view maintained by item view', () => {
const model = new WikiPage()
const view = new WikiPageIndexItemView({
model,
collectionHasTodoDate: () => {},
selectedPages: {},
})
expect(model.view).toBe(view)
view.render()
expect(model.view).toBe(view)
})
test('detach/reattach the publish icon view', () => {
const model = new WikiPage()
const view = new WikiPageIndexItemView({
model,
collectionHasTodoDate: () => {},
selectedPages: {},
})
view.render()
const $previousEl = view.$el.find('> *:first-child')
view.publishIconView.$el.data('test-data', 'test-is-good')
view.render()
expect($previousEl.parent().length).toBe(0)
expect(view.publishIconView.$el.data('test-data')).toBe('test-is-good')
})
test('delegate useAsFrontPage to the model', () => {
const model = new WikiPage({
front_page: false,
published: true,
})
const view = new WikiPageIndexItemView({
model,
collectionHasTodoDate: () => {},
selectedPages: {},
})
const stub = jest.spyOn(model, 'setFrontPage').mockImplementation()
view.useAsFrontPage()
expect(stub).toHaveBeenCalledTimes(1)
stub.mockRestore()
})
test('only shows direct share menu items if enabled', () => {
const view = new WikiPageIndexItemView({
model: new WikiPage(),
collectionHasTodoDate: () => {},
selectedPages: {},
WIKI_RIGHTS: {read: true, manage: true, update: true},
CAN: {MANAGE: true},
})
view.render()
expect(view.$('.send-wiki-page-to').length).toBe(0)
expect(view.$('.copy-wiki-page-to').length).toBe(0)
ENV.DIRECT_SHARE_ENABLED = true
view.render()
expect(view.$('.send-wiki-page-to').length).toBeGreaterThan(0)
expect(view.$('.copy-wiki-page-to').length).toBeGreaterThan(0)
})
})
describe('WikiPageIndexItemView:JSON', () => {
const testRights = (subject, options) => {
test(`${subject}`, () => {
describe('WikiPageIndexItemView', () => {
test('model.view maintained by item view', () => {
const model = new WikiPage()
const view = new WikiPageIndexItemView({
model,
contextName: options.contextName,
WIKI_RIGHTS: options.WIKI_RIGHTS,
collectionHasTodoDate: () => {},
selectedPages: {},
})
const json = view.toJSON()
for (const key in options.CAN) {
expect(json.CAN[key]).toBe(options.CAN[key])
}
expect(model.view).toBe(view)
view.render()
expect(model.view).toBe(view)
})
}
testRights('CAN (manage course)', {
contextName: 'courses',
WIKI_RIGHTS: {
read: true,
manage: true,
publish_page: true,
create_page: true,
},
CAN: {
MANAGE: true,
PUBLISH: true,
DUPLICATE: true,
},
})
testRights('CAN (manage group)', {
contextName: 'groups',
WIKI_RIGHTS: {
read: true,
manage: true,
publish_page: false,
create_page: true,
},
CAN: {
MANAGE: true,
PUBLISH: false,
DUPLICATE: false,
},
})
testRights('CAN (read)', {
contextName: 'courses',
WIKI_RIGHTS: {read: true},
CAN: {
MANAGE: false,
PUBLISH: false,
},
})
testRights('CAN (null)', {
CAN: {
MANAGE: false,
PUBLISH: false,
},
})
// Tests for granular permissions, with manage permission removed
testRights('CAN (create page - course)', {
contextName: 'courses',
WIKI_RIGHTS: {create_page: true},
CAN: {
MANAGE: true,
PUBLISH: false,
DUPLICATE: true,
UPDATE: false,
DELETE: false,
},
})
testRights('CAN (create page - group)', {
contextName: 'groups',
WIKI_RIGHTS: {create_page: true},
CAN: {
MANAGE: true,
PUBLISH: false,
DUPLICATE: false,
UPDATE: false,
DELETE: false,
},
})
testRights('CAN (delete page)', {
contextName: 'courses',
WIKI_RIGHTS: {delete_page: true},
CAN: {
MANAGE: true,
PUBLISH: false,
DUPLICATE: false,
UPDATE: false,
DELETE: true,
},
})
testRights('CAN (update page)', {
contextName: 'courses',
WIKI_RIGHTS: {update: true, publish_page: true},
CAN: {
MANAGE: true,
PUBLISH: true,
DUPLICATE: false,
UPDATE: true,
DELETE: false,
},
})
test('includes is_checked', () => {
const model = new WikiPage({
page_id: '42',
test('detach/reattach the publish icon view', () => {
const model = new WikiPage()
const view = new WikiPageIndexItemView({
model,
collectionHasTodoDate: () => {},
selectedPages: {},
})
view.render()
const $previousEl = view.$el.find('> *:first-child')
view.publishIconView.$el.data('test-data', 'test-is-good')
view.render()
expect($previousEl.parent().length).toBe(0)
expect(view.publishIconView.$el.data('test-data')).toBe('test-is-good')
})
const view = new WikiPageIndexItemView({
model,
collectionHasTodoDate: () => {},
selectedPages: {42: model},
test('delegate useAsFrontPage to the model', () => {
const model = new WikiPage({
front_page: false,
published: true,
})
const view = new WikiPageIndexItemView({
model,
collectionHasTodoDate: () => {},
selectedPages: {},
})
const stub = jest.spyOn(model, 'setFrontPage').mockImplementation()
view.useAsFrontPage()
expect(stub).toHaveBeenCalledTimes(1)
stub.mockRestore()
})
test('only shows direct share menu items if enabled', () => {
const view = new WikiPageIndexItemView({
model: new WikiPage(),
collectionHasTodoDate: () => {},
selectedPages: {},
WIKI_RIGHTS: {read: true, manage: true, update: true},
CAN: {MANAGE: true},
})
view.render()
expect(view.$('.send-wiki-page-to').length).toBe(0)
expect(view.$('.copy-wiki-page-to').length).toBe(0)
ENV.DIRECT_SHARE_ENABLED = true
view.render()
expect(view.$('.send-wiki-page-to').length).toBeGreaterThan(0)
expect(view.$('.copy-wiki-page-to').length).toBeGreaterThan(0)
})
})
describe('WikiPageIndexItemView:JSON', () => {
const testRights = (subject, options) => {
test(`${subject}`, () => {
const model = new WikiPage()
const view = new WikiPageIndexItemView({
model,
contextName: options.contextName,
WIKI_RIGHTS: options.WIKI_RIGHTS,
collectionHasTodoDate: () => {},
selectedPages: {},
})
const json = view.toJSON()
for (const key in options.CAN) {
expect(json.CAN[key]).toBe(options.CAN[key])
}
})
}
testRights('CAN (manage course)', {
contextName: 'courses',
WIKI_RIGHTS: {
read: true,
manage: true,
publish_page: true,
create_page: true,
},
CAN: {
MANAGE: true,
PUBLISH: true,
DUPLICATE: true,
},
})
testRights('CAN (manage group)', {
contextName: 'groups',
WIKI_RIGHTS: {
read: true,
manage: true,
publish_page: false,
create_page: true,
},
CAN: {
MANAGE: true,
PUBLISH: false,
DUPLICATE: false,
},
})
testRights('CAN (read)', {
contextName: 'courses',
WIKI_RIGHTS: {read: true},
CAN: {
MANAGE: false,
PUBLISH: false,
},
})
testRights('CAN (null)', {
CAN: {
MANAGE: false,
PUBLISH: false,
},
})
// Tests for granular permissions, with manage permission removed
testRights('CAN (create page - course)', {
contextName: 'courses',
WIKI_RIGHTS: {create_page: true},
CAN: {
MANAGE: true,
PUBLISH: false,
DUPLICATE: true,
UPDATE: false,
DELETE: false,
},
})
testRights('CAN (create page - group)', {
contextName: 'groups',
WIKI_RIGHTS: {create_page: true},
CAN: {
MANAGE: true,
PUBLISH: false,
DUPLICATE: false,
UPDATE: false,
DELETE: false,
},
})
testRights('CAN (delete page)', {
contextName: 'courses',
WIKI_RIGHTS: {delete_page: true},
CAN: {
MANAGE: true,
PUBLISH: false,
DUPLICATE: false,
UPDATE: false,
DELETE: true,
},
})
testRights('CAN (update page)', {
contextName: 'courses',
WIKI_RIGHTS: {update: true, publish_page: true},
CAN: {
MANAGE: true,
PUBLISH: true,
DUPLICATE: false,
UPDATE: true,
DELETE: false,
},
})
test('includes is_checked', () => {
const model = new WikiPage({
page_id: '42',
})
const view = new WikiPageIndexItemView({
model,
collectionHasTodoDate: () => {},
selectedPages: {42: model},
})
expect(view.toJSON().isChecked).toBe(true)
})
expect(view.toJSON().isChecked).toBe(true)
})
})

View File

@ -7,7 +7,7 @@
{{#if block_editor}}
<label class="checkbox" for="toggle_block_editor" style="margin-inline-end: 4px;">
<input type="checkbox" value="1" id="toggle_block_editor" aria-label="toggle block editor" checked>
{{#t}}Use block Editor{{/t}}
{{#t}}Create with block editor{{/t}}
</label>
{{/if}}
{{#if CAN.DELETE}}
@ -89,6 +89,11 @@
<span class="screenreader-only">{{#t}}Select page{{/t}}</span>
</th>
{{/if}}
{{#if block_editor}}
<th scope="col" width="24px" role="columnheader">
<span class="screenreader-only">{{#t}}page type{{/t}}</span>
</th>
{{/if}}
<th scope="col" width="{{#if CAN.MANAGE}}{{#if CAN.PUBLISH}}42%{{else}}46%{{/if}}{{else}}60%{{/if}}" role="columnheader">
<span class="mobile-screenreader-only">
<a href="#" data-sort-field="title" tabindex="0" role="button" class="sort-field">{{#t 'headers.page_title'}}Page title{{/t}} <i></i></a>

View File

@ -12,6 +12,11 @@
{{/unless}}
</td>
{{/if}}
{{#if block_editor}}
<td role="gridcell">
<i class="icon-document {{#if page_is_block}}icon-Solid{{/if}}" aria-label="{{page_type_label}}" />
</td>
{{/if}}
<td role="gridcell">
<div class="hide-overflow" role="text">
<div class="wiki-page-title">

View File

@ -174,7 +174,9 @@ export default class WikiPageView extends Backbone.View {
} else if (this.$sequenceFooter != null) {
this.$sequenceFooter.msfAnimation(false)
}
if (this.$sequenceFooter) return this.$sequenceFooter.appendTo($('#module_navigation_target'))
if (this.$sequenceFooter) this.$sequenceFooter.appendTo($('#module_navigation_target'))
this.maybeRenderBlockEditorContent()
}
navigateToLinkAnchor() {
@ -190,8 +192,11 @@ export default class WikiPageView extends Backbone.View {
}
}
renderBlockEditorContent() {
if (ENV.BLOCK_EDITOR && this.model.get('block_editor_attributes')?.blocks?.[0]?.data) {
maybeRenderBlockEditorContent() {
if (
this.model.get('editor') === 'block_editor' &&
this.model.get('block_editor_attributes')?.blocks?.[0]?.data
) {
import('@canvas/block-editor')
.then(({renderBlockEditorView}) => {
const container = document.getElementById('block-editor-content')
@ -200,6 +205,7 @@ export default class WikiPageView extends Backbone.View {
renderBlockEditorView(content, container)
})
.catch(e => {
// eslint-disable-next-line no-alert
window.alert('Error loading block editor content')
})
}
@ -207,7 +213,6 @@ export default class WikiPageView extends Backbone.View {
afterRender() {
super.afterRender(...arguments)
this.renderBlockEditorContent()
this.navigateToLinkAnchor()
this.reloadView = new WikiPageReloadView({
el: this.$pageChangedAlert,
@ -337,13 +342,8 @@ export default class WikiPageView extends Backbone.View {
toJSON() {
const json = super.toJSON(...arguments)
json.page_id = this.model.get('page_id')
if (ENV.BLOCK_EDITOR && json.block_editor_attributes?.blocks?.[0]?.data) {
json.body = '<div id="block-editor-content"></div>'
// json.body = `<pre>${JSON.stringify(
// JSON.parse(json.block_editor_attributes.blocks[0].data),
// null,
// 2
// )}</pre>`
if (this.model.get('editor') === 'block_editor') {
json.body = '<div id="block-editor-content"/>' // this is where the BlockEditorView will be rendered
}
json.modules_path = this.modules_path
json.wiki_pages_path = this.wiki_pages_path

View File

@ -16,6 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {waitFor} from '@testing-library/react'
import WikiPage from '@canvas/wiki/backbone/models/WikiPage'
import WikiPageView from '../WikiPageView'
import ReactDOM from 'react-dom'
@ -25,12 +26,12 @@ import '@canvas/jquery/jquery.simulate'
import '@canvas/module-sequence-footer'
import fakeENV from '@canvas/test-utils/fakeENV'
QUnit.module('WikiPageView', hooks => {
hooks.beforeEach(() => {
describe('WikiPageView', () => {
beforeEach(() => {
fakeENV.setup()
})
hooks.afterEach(() => {
afterEach(() => {
fakeENV.teardown()
})
@ -40,132 +41,104 @@ QUnit.module('WikiPageView', hooks => {
model,
display_show_all_pages: true,
})
equal(view.display_show_all_pages, true)
expect(view.display_show_all_pages).toBe(true)
})
test('model.view maintained by item view', () => {
const model = new WikiPage()
const view = new WikiPageView({model})
strictEqual(model.view, view, 'model.view is set to the item view')
expect(model.view).toEqual(view)
view.render()
strictEqual(model.view, view, 'model.view is set to the item view')
expect(model.view).toEqual(view)
})
test('detach/reattach the publish icon view', () => {
test.skip('detach/reattach the publish icon view', () => {
const model = new WikiPage()
const view = new WikiPageView({model})
view.render()
const $previousEl = view.$el.find('> *:first-child')
view.publishButtonView.$el.data('test-data', 'test-is-good')
view.render()
equal($previousEl.parent().length, 0, 'previous content removed')
equal(
view.publishButtonView.$el.data('test-data'),
'test-is-good',
'test data preserved (by detach)'
)
expect($previousEl.parent()).toHaveLength(0)
expect(view.publishButtonView.$el.data('test-data')).toEqual('test-is-good')
})
QUnit.module('WikiPageView:JSON', _hooks => {
describe('WikiPageView:JSON', () => {
test('modules_path', () => {
const model = new WikiPage()
const view = new WikiPageView({
model,
modules_path: '/courses/73/modules',
})
strictEqual(
view.toJSON().modules_path,
'/courses/73/modules',
'modules_path represented in toJSON'
)
expect(view.toJSON().modules_path).toBe('/courses/73/modules')
})
test('wiki_pages_path', () => {
const model = new WikiPage()
const view = new WikiPageView({
model,
wiki_pages_path: '/groups/73/pages',
})
strictEqual(
view.toJSON().wiki_pages_path,
'/groups/73/pages',
'wiki_pages_path represented in toJSON'
)
expect(view.toJSON().wiki_pages_path).toBe('/groups/73/pages')
})
test('wiki_page_edit_path', () => {
const model = new WikiPage()
const view = new WikiPageView({
model,
wiki_page_edit_path: '/groups/73/pages/37',
})
strictEqual(
view.toJSON().wiki_page_edit_path,
'/groups/73/pages/37',
'wiki_page_edit_path represented in toJSON'
)
expect(view.toJSON().wiki_page_edit_path).toBe('/groups/73/pages/37')
})
test('wiki_page_history_path', () => {
const model = new WikiPage()
const view = new WikiPageView({
model,
wiki_page_edit_path: '/groups/73/pages/37/revisions',
})
strictEqual(
view.toJSON().wiki_page_edit_path,
'/groups/73/pages/37/revisions',
'wiki_page_history_path represented in toJSON'
)
expect(view.toJSON().wiki_page_edit_path).toBe('/groups/73/pages/37/revisions')
})
test('lock_info.unlock_at', () => {
const clock = sinon.useFakeTimers(new Date(2012, 0, 31).getTime())
jest.useFakeTimers()
jest.setSystemTime(new Date(2012, 0, 31).getTime())
const model = new WikiPage({
locked_for_user: true,
lock_info: {unlock_at: '2012-02-15T12:00:00Z'},
})
const view = new WikiPageView({model})
const lockInfo = view.toJSON().lock_info
ok(
!!(lockInfo && lockInfo.unlock_at.match('Feb')),
'lock_info.unlock_at reformatted and represented in toJSON'
)
clock.restore()
expect(!!(lockInfo && lockInfo.unlock_at.match('Feb'))).toBeTruthy()
jest.useRealTimers()
})
test('useAsFrontPage for published wiki_pages_path', () => {
const model = new WikiPage({
front_page: false,
published: true,
})
const view = new WikiPageView({model})
const stub = sandbox.stub(model, 'setFrontPage')
jest.spyOn(model, 'setFrontPage').mockImplementation(() => {})
view.useAsFrontPage()
ok(stub.calledOnce)
expect(model.setFrontPage).toHaveBeenCalledTimes(1)
})
test('useAsFrontPage should not work on unpublished wiki_pages_path', () => {
const model = new WikiPage({
front_page: false,
published: false,
})
const view = new WikiPageView({model})
const stub = sandbox.stub(model, 'setFrontPage')
jest.spyOn(model, 'setFrontPage')
view.useAsFrontPage()
notOk(stub.calledOnce)
expect(model.setFrontPage).not.toHaveBeenCalled()
})
})
QUnit.module('WikiPageView: direct share', hooks2 => {
hooks2.beforeEach(() => {
describe('WikiPageView: direct share', () => {
beforeEach(() => {
$('<div id="direct-share-mount-point">').appendTo('#fixtures')
fakeENV.setup({DIRECT_SHARE_ENABLED: true})
sinon.stub(ReactDOM, 'render')
jest.spyOn(ReactDOM, 'render').mockImplementation(() => {})
})
hooks2.afterEach(() => {
ReactDOM.render.restore()
afterEach(() => {
jest.restoreAllMocks()
fakeENV.teardown()
$('#direct-share-mount-point').remove()
})
@ -179,12 +152,13 @@ QUnit.module('WikiPageView', hooks => {
view.render()
view.$('.al-trigger').simulate('click')
view.$('.direct-share-send-to-menu-item').simulate('click')
const props = ReactDOM.render.firstCall.args[0].props
equal(props.open, true)
equal(props.sourceCourseId, '123')
deepEqual(props.contentShare, {content_type: 'page', content_id: '42'})
const props = ReactDOM.render.mock.calls[0][0].props
expect(props.open).toBe(true)
expect(props.sourceCourseId).toBe('123')
expect(props.contentShare).toEqual({content_type: 'page', content_id: '42'})
props.onDismiss()
equal(ReactDOM.render.lastCall.args[0].props.open, false)
expect(ReactDOM.render.mock.lastCall[0].props.open).toBe(false)
})
test('opens and closes copy to tray', () => {
@ -196,12 +170,53 @@ QUnit.module('WikiPageView', hooks => {
view.render()
view.$('.al-trigger').simulate('click')
view.$('.direct-share-copy-to-menu-item').simulate('click')
const props = ReactDOM.render.firstCall.args[0].props
equal(props.open, true)
equal(props.sourceCourseId, '123')
deepEqual(props.contentSelection, {pages: ['42']})
const props = ReactDOM.render.mock.calls[0][0].props
expect(props.open).toBe(true)
expect(props.sourceCourseId).toBe('123')
expect(props.contentSelection).toEqual({pages: ['42']})
props.onDismiss()
equal(ReactDOM.render.lastCall.args[0].props.open, false)
expect(ReactDOM.render.mock.lastCall[0].props.open).toBe(false)
})
})
describe('with the block editor', () => {
const simplePage = `{
"ROOT": {
"type": {
"resolvedName": "PageBlock"
},
"isCanvas": true,
"props": {},
"displayName": "Page",
"custom": {},
"hidden": false,
"nodes": [],
"linkedNodes": {}
}
}`
beforeEach(() => {
const container = document.createElement('div')
container.id = 'block-editor-content'
document.body.appendChild(container)
})
it('renders the block editor', () => {
const model = new WikiPage({
editor: 'block_editor',
block_editor_attributes: {
version: '1',
blocks: [{data: simplePage}],
},
})
const view = new WikiPageView({model})
view.render()
waitFor(() => {
expect(view.$('.block-editor-view')).toHaveLength(1)
})
waitFor(() => {
expect(view.$('.page-block')).toHaveLength(1)
})
})
})
})
@ -217,7 +232,7 @@ const testRights = (subject, options) => {
})
const json = view.toJSON()
for (const key in options.CAN) {
strictEqual(json.CAN[key], options.CAN[key], `${subject} - CAN.${key}`)
expect(json.CAN[key]).toEqual(options.CAN[key])
}
})
}

View File

@ -142,6 +142,7 @@ export default class WikiPageEditView extends ValidatedFormView {
json.content_is_locked = this.lockedItems.content
json.show_assign_to = this.enableAssignTo
json.edit_with_block_editor = this.model.get('editor') === 'block_editor'
return json
}
@ -224,13 +225,10 @@ export default class WikiPageEditView extends ValidatedFormView {
}
renderAssignToTray(mountElement, {pageId, onSync, pageName})
}
if (window.ENV.BLOCK_EDITOR) {
if (this.model.get('editor') === 'block_editor' && this.model.get('block_editor_attributes')) {
const BlockEditor = lazy(() => import('@canvas/block-editor'))
const blockEditorData = ENV.WIKI_PAGE?.block_editor_attributes || {
version: '1',
blocks: [{data: undefined}],
}
const blockEditorData = this.model.get('block_editor_attributes')
const container = document.getElementById('content')
container.style.boxSizing = 'border-box'
@ -299,10 +297,11 @@ export default class WikiPageEditView extends ValidatedFormView {
destroyEditor() {
// hack fix for LF-1134
try {
if (!window.ENV.BLOCK_EDITOR) {
if (this.model.get('editor') !== 'block_editor') {
RichContentEditor.destroyRCE(this.$wikiPageBody)
}
} catch (e) {
// eslint-disable-next-line no-console
console.warn(e)
} finally {
this.$el.remove()
@ -451,7 +450,7 @@ export default class WikiPageEditView extends ValidatedFormView {
// eslint-disable-next-line no-alert
if (!this.hasUnsavedChanges() || window.confirm(this.unsavedWarning())) {
this.checkUnsavedOnLeave = false
if (!window.ENV.BLOCK_EDITOR) {
if (this.model.get('editor') !== 'block_editor') {
RichContentEditor.closeRCE(this.$wikiPageBody)
}
return this.trigger('cancel')

View File

@ -17,7 +17,7 @@
{{{body}}}
{{else}}
<label for="wiki_page_body" class="hidden-readable" aria-hidden="true">{{#t}}Raw HTML Editor{{/t}}</label>
{{#if ENV.BLOCK_EDITOR}}
{{#if edit_with_block_editor}}
<div id="block_editor" class="block-editor"></div>
{{else}}
<textarea id="wiki_page_body" rows="20" cols="40" name="body" class="body" aria-hidden="true"{{#unless PAGE_RIGHTS.update}} autofocus{{/unless}}>{{convertApiUserContent body forEditing=1}}</textarea>