mirror of https://github.com/artf/grapesjs.git
Browse Source
* Setup initial schema types * Setup initial schema methods * Add DataSource tests * Up test * Add test getValue * Cleanup * Up test * Add setValue method * Up docs * Check nested arrays * Add a test for nested setValue * Improve setValue for nested values * Improve setValue for nested values * Setup relation tests * Add getResolvedRecords * Resolve one to many relations in DataSource records * Add DataSourceSchema * Up type * Start data source providers * Start test provider * Add tests for loadProvider * Cleanup * Skip records if DataSource provider is set * Load providers on project load * Type keymap events * Move modal events * Move layer events * Move RTE events * Move selector events * Move StyleManager events * Move editor events * Start DataSource callbacks * Add data source callbacks * Update docs * Up device_manager jsdoc * Formatrelease-v0.22.14-rc.0
committed by
GitHub
41 changed files with 1586 additions and 403 deletions
@ -0,0 +1,13 @@ |
|||
export interface DataSourcesConfig { |
|||
/** |
|||
* If true, data source providers will be autoloaded on project load. |
|||
* @default false |
|||
*/ |
|||
autoloadProviders?: boolean; |
|||
} |
|||
|
|||
const config: () => DataSourcesConfig = () => ({ |
|||
autoloadProviders: false, |
|||
}); |
|||
|
|||
export default config; |
|||
@ -0,0 +1,28 @@ |
|||
/**{START_EVENTS}*/ |
|||
export enum KeymapsEvents { |
|||
/** |
|||
* @event `keymap:add` New keymap added. The new keymap object is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('keymap:add', (keymap) => { ... }); |
|||
*/ |
|||
add = 'keymap:add', |
|||
|
|||
/** |
|||
* @event `keymap:remove` Keymap removed. The removed keymap object is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('keymap:remove', (keymap) => { ... }); |
|||
*/ |
|||
remove = 'keymap:remove', |
|||
|
|||
/** |
|||
* @event `keymap:emit` Some keymap emitted. The keymapId, shortcutUsed, and Event are passed as arguments to the callback. |
|||
* @example |
|||
* editor.on('keymap:emit', (keymapId, shortcutUsed, event) => { ... }); |
|||
*/ |
|||
emit = 'keymap:emit', |
|||
emitId = 'keymap:emit:', |
|||
} |
|||
/**{END_EVENTS}*/ |
|||
|
|||
// need this to avoid the TS documentation generator to break
|
|||
export default KeymapsEvents; |
|||
@ -0,0 +1,27 @@ |
|||
/**{START_EVENTS}*/ |
|||
export enum ModalEvents { |
|||
/** |
|||
* @event `modal:open` Modal is opened |
|||
* @example |
|||
* editor.on('modal:open', () => { ... }); |
|||
*/ |
|||
open = 'modal:open', |
|||
|
|||
/** |
|||
* @event `modal:close` Modal is closed |
|||
* @example |
|||
* editor.on('modal:close', () => { ... }); |
|||
*/ |
|||
close = 'modal:close', |
|||
|
|||
/** |
|||
* @event `modal` Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('modal', ({ open, title, content, ... }) => { ... }); |
|||
*/ |
|||
all = 'modal', |
|||
} |
|||
/**{END_EVENTS}*/ |
|||
|
|||
// need this to avoid the TS documentation generator to break
|
|||
export default ModalEvents; |
|||
@ -0,0 +1,39 @@ |
|||
import Component from '../dom_components/model/Component'; |
|||
|
|||
export interface LayerData { |
|||
name: string; |
|||
open: boolean; |
|||
selected: boolean; |
|||
hovered: boolean; |
|||
visible: boolean; |
|||
locked: boolean; |
|||
components: Component[]; |
|||
} |
|||
|
|||
/**{START_EVENTS}*/ |
|||
export enum LayerEvents { |
|||
/** |
|||
* @event `layer:root` Root layer changed. The new root component is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('layer:root', (component) => { ... }); |
|||
*/ |
|||
root = 'layer:root', |
|||
|
|||
/** |
|||
* @event `layer:component` Component layer is updated. The updated component is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('layer:component', (component, opts) => { ... }); |
|||
*/ |
|||
component = 'layer:component', |
|||
|
|||
/** |
|||
* @event `layer:custom` Custom layer event. Object with container and root is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('layer:custom', ({ container, root }) => { ... }); |
|||
*/ |
|||
custom = 'layer:custom', |
|||
} |
|||
/**{END_EVENTS}*/ |
|||
|
|||
// need this to avoid the TS documentation generator to break
|
|||
export default LayerEvents; |
|||
@ -0,0 +1,39 @@ |
|||
import ComponentTextView from '../dom_components/view/ComponentTextView'; |
|||
|
|||
export interface ModelRTE { |
|||
currentView?: ComponentTextView; |
|||
} |
|||
|
|||
export type RichTextEditorEvent = `${RichTextEditorEvents}`; |
|||
|
|||
export interface RteDisableResult { |
|||
forceSync?: boolean; |
|||
} |
|||
|
|||
/**{START_EVENTS}*/ |
|||
export enum RichTextEditorEvents { |
|||
/** |
|||
* @event `rte:enable` RTE enabled. The view, on which RTE is enabled, and the RTE instance are passed as arguments. |
|||
* @example |
|||
* editor.on('rte:enable', (view, rte) => { ... }); |
|||
*/ |
|||
enable = 'rte:enable', |
|||
|
|||
/** |
|||
* @event `rte:disable` RTE disabled. The view, on which RTE is disabled, and the RTE instance are passed as arguments. |
|||
* @example |
|||
* editor.on('rte:disable', (view, rte) => { ... }); |
|||
*/ |
|||
disable = 'rte:disable', |
|||
|
|||
/** |
|||
* @event `rte:custom` Custom RTE event. Object with enabled status, container, and actions is passed as an argument. |
|||
* @example |
|||
* editor.on('rte:custom', ({ enabled, container, actions }) => { ... }); |
|||
*/ |
|||
custom = 'rte:custom', |
|||
} |
|||
/**{END_EVENTS}*/ |
|||
|
|||
// need this to avoid the TS documentation generator to break
|
|||
export default RichTextEditorEvents; |
|||
@ -0,0 +1,57 @@ |
|||
/**{START_EVENTS}*/ |
|||
export enum SelectorEvents { |
|||
/** |
|||
* @event `selector:add` Selector added. The Selector is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('selector:add', (selector) => { ... }); |
|||
*/ |
|||
add = 'selector:add', |
|||
|
|||
/** |
|||
* @event `selector:remove` Selector removed. The Selector is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('selector:remove', (selector) => { ... }); |
|||
*/ |
|||
remove = 'selector:remove', |
|||
|
|||
/** |
|||
* @event `selector:remove:before` Before selector remove. The Selector is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('selector:remove:before', (selector) => { ... }); |
|||
*/ |
|||
removeBefore = 'selector:remove:before', |
|||
|
|||
/** |
|||
* @event `selector:update` Selector updated. The Selector and the object containing changes are passed as arguments to the callback. |
|||
* @example |
|||
* editor.on('selector:update', (selector, changes) => { ... }); |
|||
*/ |
|||
update = 'selector:update', |
|||
|
|||
/** |
|||
* @event `selector:state` States changed. An object containing all the available data about the triggered event is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('selector:state', (state) => { ... }); |
|||
*/ |
|||
state = 'selector:state', |
|||
|
|||
/** |
|||
* @event `selector:custom` Custom selector event. An object containing states, selected selectors, and container is passed as an argument. |
|||
* @example |
|||
* editor.on('selector:custom', ({ states, selected, container }) => { ... }); |
|||
*/ |
|||
custom = 'selector:custom', |
|||
|
|||
/** |
|||
* @event `selector` Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('selector', ({ event, selector, changes, ... }) => { ... }); |
|||
*/ |
|||
all = 'selector', |
|||
} |
|||
/**{END_EVENTS}*/ |
|||
|
|||
export type SelectorStringObject = string | { name?: string; label?: string; type?: number }; |
|||
|
|||
// need this to avoid the TS documentation generator to break
|
|||
export default SelectorEvents; |
|||
@ -0,0 +1,88 @@ |
|||
import StyleManager from '.'; |
|||
import StyleableModel from '../domain_abstract/model/StyleableModel'; |
|||
import { PropertyNumberProps } from './model/PropertyNumber'; |
|||
import { PropertySelectProps } from './model/PropertySelect'; |
|||
import { PropertyStackProps } from './model/PropertyStack'; |
|||
|
|||
export type PropertyTypes = PropertyStackProps | PropertySelectProps | PropertyNumberProps; |
|||
|
|||
export type StyleTarget = StyleableModel; |
|||
|
|||
export type StyleModuleParam<T extends keyof StyleManager, N extends number> = Parameters<StyleManager[T]>[N]; |
|||
|
|||
/**{START_EVENTS}*/ |
|||
export enum StyleManagerEvents { |
|||
/** |
|||
* @event `style:sector:add` Sector added. The Sector is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('style:sector:add', (sector) => { ... }); |
|||
*/ |
|||
sectorAdd = 'style:sector:add', |
|||
|
|||
/** |
|||
* @event `style:sector:remove` Sector removed. The Sector is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('style:sector:remove', (sector) => { ... }); |
|||
*/ |
|||
sectorRemove = 'style:sector:remove', |
|||
|
|||
/** |
|||
* @event `style:sector:update` Sector updated. The Sector and the object containing changes are passed as arguments to the callback. |
|||
* @example |
|||
* editor.on('style:sector:update', (sector, changes) => { ... }); |
|||
*/ |
|||
sectorUpdate = 'style:sector:update', |
|||
|
|||
/** |
|||
* @event `style:property:add` Property added. The Property is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('style:property:add', (property) => { ... }); |
|||
*/ |
|||
propertyAdd = 'style:property:add', |
|||
|
|||
/** |
|||
* @event `style:property:remove` Property removed. The Property is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('style:property:remove', (property) => { ... }); |
|||
*/ |
|||
propertyRemove = 'style:property:remove', |
|||
|
|||
/** |
|||
* @event `style:property:update` Property updated. The Property and the object containing changes are passed as arguments to the callback. |
|||
* @example |
|||
* editor.on('style:property:update', (property, changes) => { ... }); |
|||
*/ |
|||
propertyUpdate = 'style:property:update', |
|||
|
|||
/** |
|||
* @event `style:target` Target selection changed. The target (or null in case the target is deselected) is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('style:target', (target) => { ... }); |
|||
*/ |
|||
target = 'style:target', |
|||
|
|||
/** |
|||
* @event `style:layer:select` Layer selected. Object containing layer data is passed as an argument. |
|||
* @example |
|||
* editor.on('style:layer:select', (data) => { ... }); |
|||
*/ |
|||
layerSelect = 'style:layer:select', |
|||
|
|||
/** |
|||
* @event `style:custom` Custom style event. Object containing all custom data is passed as an argument. |
|||
* @example |
|||
* editor.on('style:custom', ({ container }) => { ... }); |
|||
*/ |
|||
custom = 'style:custom', |
|||
|
|||
/** |
|||
* @event `style` Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback. |
|||
* @example |
|||
* editor.on('style', ({ event, sector, property, ... }) => { ... }); |
|||
*/ |
|||
all = 'style', |
|||
} |
|||
/**{END_EVENTS}*/ |
|||
|
|||
// need this to avoid the TS documentation generator to break
|
|||
export default StyleManagerEvents; |
|||
@ -0,0 +1,295 @@ |
|||
import { DataSourceManager } from '../../../../src'; |
|||
import DataSource from '../../../../src/data_sources/model/DataSource'; |
|||
import { |
|||
DataFieldPrimitiveType, |
|||
DataFieldSchemaNumber, |
|||
DataFieldSchemaString, |
|||
DataRecordProps, |
|||
DataSourceProviderResult, |
|||
} from '../../../../src/data_sources/types'; |
|||
import Editor from '../../../../src/editor/model/Editor'; |
|||
import { setupTestEditor } from '../../../common'; |
|||
|
|||
interface TestRecord extends DataRecordProps { |
|||
name?: string; |
|||
age?: number; |
|||
} |
|||
|
|||
const serializeRecords = (records: any[]) => JSON.parse(JSON.stringify(records)); |
|||
|
|||
describe('DataSource', () => { |
|||
let em: Editor; |
|||
let editor: Editor['Editor']; |
|||
let dsm: DataSourceManager; |
|||
let ds: DataSource<TestRecord>; |
|||
const categoryRecords = [ |
|||
{ id: 'cat1', uid: 'cat1-uid', name: 'Category 1' }, |
|||
{ id: 'cat2', uid: 'cat2-uid', name: 'Category 2' }, |
|||
{ id: 'cat3', uid: 'cat3-uid', name: 'Category 3' }, |
|||
]; |
|||
const userRecords = [ |
|||
{ id: 'user1', username: 'user_one' }, |
|||
{ id: 'user2', username: 'user_two' }, |
|||
{ id: 'user3', username: 'user_three' }, |
|||
]; |
|||
const blogRecords = [ |
|||
{ id: 'blog1', title: 'First Blog', author: 'user1', categories: ['cat1-uid'] }, |
|||
{ id: 'blog2', title: 'Second Blog', author: 'user2' }, |
|||
{ id: 'blog3', title: 'Third Blog', categories: ['cat1-uid', 'cat3-uid'] }, |
|||
]; |
|||
|
|||
const addTestDataSource = (records?: TestRecord[]) => { |
|||
return dsm.add<TestRecord>({ id: 'test', records: records || [{ id: 'user1', age: 30 }] }); |
|||
}; |
|||
|
|||
beforeEach(() => { |
|||
({ em, dsm, editor } = setupTestEditor()); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
em.destroy(); |
|||
}); |
|||
|
|||
describe('Schema', () => { |
|||
const schemaName: DataFieldSchemaString = { |
|||
type: DataFieldPrimitiveType.string, |
|||
label: 'Name', |
|||
}; |
|||
const schemaAge: DataFieldSchemaNumber = { |
|||
type: DataFieldPrimitiveType.number, |
|||
label: 'Age', |
|||
default: 18, |
|||
}; |
|||
|
|||
beforeEach(() => { |
|||
ds = addTestDataSource(); |
|||
}); |
|||
|
|||
test('Initialize with empty schema', () => { |
|||
expect(ds.schema).toEqual({}); |
|||
}); |
|||
|
|||
test('Add and update schema', () => { |
|||
const schemaNameDef: typeof ds.schema = { name: schemaName }; |
|||
const schemaAgeDef: typeof ds.schema = { age: schemaAge }; |
|||
ds.upSchema(schemaNameDef); |
|||
ds.upSchema(schemaAgeDef); |
|||
expect(ds.schema).toEqual({ ...schemaNameDef, ...schemaAgeDef }); |
|||
}); |
|||
|
|||
test('Should update existing field schema', () => { |
|||
ds.upSchema({ name: schemaName }); |
|||
|
|||
const updatedSchema: typeof ds.schema = { |
|||
name: { |
|||
...schemaName, |
|||
description: 'User name field', |
|||
}, |
|||
}; |
|||
ds.upSchema(updatedSchema); |
|||
expect(ds.schema).toEqual(updatedSchema); |
|||
}); |
|||
|
|||
test('Should get field schema', () => { |
|||
ds.upSchema({ |
|||
name: schemaName, |
|||
age: schemaAge, |
|||
}); |
|||
expect(ds.getSchemaField('name')).toEqual(schemaName); |
|||
expect(ds.getSchemaField('age')).toEqual(schemaAge); |
|||
expect(ds.getSchemaField('nonExistentField')).toBeUndefined(); |
|||
}); |
|||
|
|||
describe('Relations', () => { |
|||
beforeEach(() => { |
|||
dsm.add({ |
|||
id: 'categories', |
|||
records: categoryRecords, |
|||
}); |
|||
dsm.add({ |
|||
id: 'users', |
|||
records: userRecords, |
|||
}); |
|||
dsm.add({ |
|||
id: 'blogs', |
|||
records: blogRecords, |
|||
schema: { |
|||
title: { |
|||
type: DataFieldPrimitiveType.string, |
|||
}, |
|||
author: { |
|||
type: DataFieldPrimitiveType.relation, |
|||
target: 'users', |
|||
targetField: 'id', |
|||
}, |
|||
}, |
|||
}); |
|||
}); |
|||
|
|||
test('return default values', () => { |
|||
const blogsDS = dsm.get('blogs'); |
|||
expect(serializeRecords(blogsDS.getRecords())).toEqual(blogRecords); |
|||
}); |
|||
|
|||
test('return 1:1 resolved values', () => { |
|||
const blogsDS = dsm.get('blogs'); |
|||
const records = blogsDS.getResolvedRecords(); |
|||
expect(records).toEqual([ |
|||
{ ...blogRecords[0], author: userRecords[0] }, |
|||
{ ...blogRecords[1], author: userRecords[1] }, |
|||
blogRecords[2], |
|||
]); |
|||
}); |
|||
|
|||
test('return 1:many resolved values', () => { |
|||
const blogsDS = dsm.get('blogs'); |
|||
blogsDS.upSchema({ |
|||
categories: { |
|||
type: DataFieldPrimitiveType.relation, |
|||
target: 'categories', |
|||
targetField: 'uid', |
|||
isMany: true, |
|||
}, |
|||
}); |
|||
const records = blogsDS.getResolvedRecords(); |
|||
expect(records).toEqual([ |
|||
{ ...blogRecords[0], author: userRecords[0], categories: [categoryRecords[0]] }, |
|||
{ ...blogRecords[1], author: userRecords[1] }, |
|||
{ ...blogRecords[2], categories: [categoryRecords[0], categoryRecords[2]] }, |
|||
]); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('Providers', () => { |
|||
const testApiUrl = 'https://api.example.com/data'; |
|||
const testHeaders = { 'Content-Type': 'application/json' }; |
|||
const getMockSchema = () => ({ |
|||
author: { |
|||
type: DataFieldPrimitiveType.relation, |
|||
target: 'users', |
|||
targetField: 'id', |
|||
}, |
|||
}); |
|||
const getMockProviderResponse: () => DataSourceProviderResult = () => ({ |
|||
records: blogRecords, |
|||
schema: getMockSchema(), |
|||
}); |
|||
const getProviderBlogsGet = () => ({ url: testApiUrl, headers: testHeaders }); |
|||
const addBlogsWithProvider = () => { |
|||
return dsm.add({ |
|||
id: 'blogs', |
|||
name: 'My blogs', |
|||
provider: { |
|||
get: getProviderBlogsGet(), |
|||
}, |
|||
}); |
|||
}; |
|||
|
|||
beforeEach(() => { |
|||
jest.spyOn(global, 'fetch').mockImplementation(() => |
|||
Promise.resolve({ |
|||
ok: true, |
|||
json: () => Promise.resolve(getMockProviderResponse()), |
|||
} as Response), |
|||
); |
|||
|
|||
dsm.add({ |
|||
id: 'categories', |
|||
records: categoryRecords, |
|||
}); |
|||
dsm.add({ |
|||
id: 'users', |
|||
records: userRecords, |
|||
}); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
jest.restoreAllMocks(); |
|||
}); |
|||
|
|||
test('loadProvider', async () => { |
|||
const ds = addBlogsWithProvider(); |
|||
await ds.loadProvider(); |
|||
|
|||
expect(fetch).toHaveBeenCalledWith(testApiUrl, { headers: testHeaders }); |
|||
expect(ds.schema).toEqual(getMockSchema()); |
|||
expect(ds.getResolvedRecords()).toEqual([ |
|||
{ ...blogRecords[0], author: userRecords[0] }, |
|||
{ ...blogRecords[1], author: userRecords[1] }, |
|||
blogRecords[2], |
|||
]); |
|||
}); |
|||
|
|||
test('loadProvider with failed fetch', async () => { |
|||
jest.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error')); |
|||
|
|||
em.config.log = false; |
|||
const ds = addBlogsWithProvider(); |
|||
await ds.loadProvider(); |
|||
|
|||
expect(fetch).toHaveBeenCalledWith(testApiUrl, { headers: testHeaders }); |
|||
expect(ds.schema).toEqual({}); |
|||
expect(ds.getRecords().length).toBe(0); |
|||
}); |
|||
|
|||
test('records loaded from the provider are not persisted', async () => { |
|||
const ds = addBlogsWithProvider(); |
|||
const eventLoad = jest.fn(); |
|||
em.on(dsm.events.providerLoad, eventLoad); |
|||
|
|||
await ds.loadProvider(); |
|||
|
|||
expect(editor.getProjectData().dataSources).toEqual([ |
|||
{ id: 'categories', records: categoryRecords }, |
|||
{ id: 'users', records: userRecords }, |
|||
{ |
|||
id: 'blogs', |
|||
name: 'My blogs', |
|||
schema: getMockSchema(), |
|||
provider: { get: getProviderBlogsGet() }, |
|||
}, |
|||
]); |
|||
expect(eventLoad).toHaveBeenCalledTimes(1); |
|||
expect(eventLoad).toHaveBeenCalledWith({ |
|||
dataSource: ds, |
|||
result: getMockProviderResponse(), |
|||
}); |
|||
}); |
|||
|
|||
test('load providers on project load', (done) => { |
|||
dsm.getConfig().autoloadProviders = true; |
|||
|
|||
editor.on(dsm.events.providerLoadAll, () => { |
|||
expect(dsm.get('blogs').getResolvedRecords()).toEqual([ |
|||
{ ...blogRecords[0], author: userRecords[0] }, |
|||
{ ...blogRecords[1], author: userRecords[1] }, |
|||
blogRecords[2], |
|||
]); |
|||
|
|||
expect(editor.getProjectData().dataSources).toEqual([ |
|||
{ id: 'categories', records: categoryRecords }, |
|||
{ id: 'users', records: userRecords }, |
|||
{ |
|||
id: 'blogs', |
|||
schema: getMockSchema(), |
|||
provider: { get: testApiUrl }, |
|||
}, |
|||
]); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
editor.loadProjectData({ |
|||
dataSources: [ |
|||
{ id: 'categories', records: categoryRecords }, |
|||
{ id: 'users', records: userRecords }, |
|||
{ |
|||
id: 'blogs', |
|||
provider: { get: testApiUrl }, |
|||
}, |
|||
], |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue